From feb62ae0f8170bcfc029c5a2690b83926efa140e Mon Sep 17 00:00:00 2001 From: yushen Date: Thu, 26 Mar 2026 14:51:41 +0800 Subject: [PATCH 1/7] feat(auth): add browser-based CLI login flow `multica auth login` now opens the browser for email verification, receives the JWT via localhost callback, and exchanges it for a PAT. The legacy PAT-paste flow is preserved via `--token` flag. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/web/app/(auth)/login/page.tsx | 9 ++ server/cmd/multica/cmd_auth.go | 146 ++++++++++++++++++++++++++++- server/internal/cli/client.go | 30 ++++++ 3 files changed, 184 insertions(+), 1 deletion(-) diff --git a/apps/web/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx index d7e45010..1638d299 100644 --- a/apps/web/app/(auth)/login/page.tsx +++ b/apps/web/app/(auth)/login/page.tsx @@ -70,6 +70,15 @@ function LoginPageContent() { setError(""); setSubmitting(true); try { + const cliCallback = searchParams.get("cli_callback"); + if (cliCallback) { + // CLI browser login: verify code, get JWT, redirect to CLI callback. + const { token } = await api.verifyCode(email, value); + const separator = cliCallback.includes("?") ? "&" : "?"; + window.location.href = `${cliCallback}${separator}token=${encodeURIComponent(token)}`; + return; + } + await verifyCode(email, value); const wsList = await api.listWorkspaces(); await hydrateWorkspace(wsList); diff --git a/server/cmd/multica/cmd_auth.go b/server/cmd/multica/cmd_auth.go index 09dd1883..09734c30 100644 --- a/server/cmd/multica/cmd_auth.go +++ b/server/cmd/multica/cmd_auth.go @@ -4,7 +4,11 @@ import ( "bufio" "context" "fmt" + "net" + "net/http" "os" + "os/exec" + "runtime" "strings" "time" @@ -20,7 +24,7 @@ var authCmd = &cobra.Command{ var authLoginCmd = &cobra.Command{ Use: "login", - Short: "Authenticate with a personal access token", + Short: "Authenticate with Multica", RunE: runAuthLogin, } @@ -37,6 +41,7 @@ var authLogoutCmd = &cobra.Command{ } func init() { + authLoginCmd.Flags().Bool("token", false, "Authenticate by pasting a personal access token") authCmd.AddCommand(authLoginCmd) authCmd.AddCommand(authStatusCmd) authCmd.AddCommand(authLogoutCmd) @@ -50,7 +55,146 @@ func resolveToken() string { return cfg.Token } +func resolveAppURL(cmd *cobra.Command) string { + val := cli.FlagOrEnv(cmd, "", "MULTICA_APP_URL", "") + if val != "" { + return strings.TrimRight(val, "/") + } + return "http://localhost:3000" +} + +func openBrowser(url string) error { + var cmd string + var args []string + switch runtime.GOOS { + case "darwin": + cmd = "open" + args = []string{url} + case "linux": + cmd = "xdg-open" + args = []string{url} + case "windows": + cmd = "cmd" + args = []string{"/c", "start", url} + default: + return fmt.Errorf("unsupported platform: %s", runtime.GOOS) + } + return exec.Command(cmd, args...).Start() +} + func runAuthLogin(cmd *cobra.Command, _ []string) error { + useToken, _ := cmd.Flags().GetBool("token") + if useToken { + return runAuthLoginToken(cmd) + } + return runAuthLoginBrowser(cmd) +} + +func runAuthLoginBrowser(cmd *cobra.Command) error { + serverURL := resolveServerURL(cmd) + appURL := resolveAppURL(cmd) + + // Start a local HTTP server on a random port to receive the callback. + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return fmt.Errorf("failed to start local server: %w", err) + } + defer listener.Close() + + port := listener.Addr().(*net.TCPAddr).Port + callbackURL := fmt.Sprintf("http://localhost:%d/callback", port) + loginURL := fmt.Sprintf("%s/login?cli_callback=%s", appURL, callbackURL) + + // Channel to receive the JWT from the browser callback. + jwtCh := make(chan string, 1) + errCh := make(chan error, 1) + + mux := http.NewServeMux() + mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) { + token := r.URL.Query().Get("token") + if token == "" { + http.Error(w, "missing token", http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(`

Authentication successful!

You can close this tab and return to the terminal.

`)) + jwtCh <- token + }) + + srv := &http.Server{Handler: mux} + go func() { + if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed { + errCh <- err + } + }() + defer srv.Close() + + // Open the browser. + fmt.Fprintln(os.Stderr, "Opening browser to authenticate...") + if err := openBrowser(loginURL); err != nil { + fmt.Fprintf(os.Stderr, "Could not open browser automatically.\n") + } + fmt.Fprintf(os.Stderr, "If the browser didn't open, visit:\n %s\n\nWaiting for authentication...\n", loginURL) + + // Wait for the JWT from the callback (timeout 5 minutes). + var jwtToken string + select { + case jwtToken = <-jwtCh: + case err := <-errCh: + return fmt.Errorf("local server error: %w", err) + case <-time.After(5 * time.Minute): + return fmt.Errorf("timed out waiting for authentication") + } + + // Use the JWT to create a PAT via the existing API. + client := cli.NewAPIClient(serverURL, "", jwtToken) + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + hostname, _ := os.Hostname() + if hostname == "" { + hostname = "unknown" + } + patName := fmt.Sprintf("CLI (%s)", hostname) + expiresInDays := 90 + + var patResp struct { + Token string `json:"token"` + } + err = client.PostJSON(ctx, "/api/tokens", map[string]any{ + "name": patName, + "expires_in_days": expiresInDays, + }, &patResp) + if err != nil { + return fmt.Errorf("failed to create access token: %w", err) + } + + // Verify the PAT works. + patClient := cli.NewAPIClient(serverURL, "", patResp.Token) + var me struct { + Name string `json:"name"` + Email string `json:"email"` + } + if err := patClient.GetJSON(ctx, "/api/me", &me); err != nil { + return fmt.Errorf("token verification failed: %w", err) + } + + // Save to config. + cfg, _ := cli.LoadCLIConfig() + cfg.Token = patResp.Token + if cfg.ServerURL == "" { + cfg.ServerURL = serverURL + } + if err := cli.SaveCLIConfig(cfg); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + fmt.Fprintf(os.Stderr, "Authenticated as %s (%s)\nToken saved to config.\n", me.Name, me.Email) + return nil +} + +func runAuthLoginToken(cmd *cobra.Command) error { fmt.Print("Enter your personal access token: ") scanner := bufio.NewScanner(os.Stdin) if !scanner.Scan() { diff --git a/server/internal/cli/client.go b/server/internal/cli/client.go index 548d078c..7bd88d6e 100644 --- a/server/internal/cli/client.go +++ b/server/internal/cli/client.go @@ -84,6 +84,36 @@ func (c *APIClient) DeleteJSON(ctx context.Context, path string) error { return nil } +// PostJSON performs a POST request with a JSON body. +func (c *APIClient) PostJSON(ctx context.Context, path string, body any, out any) error { + data, err := json.Marshal(body) + if err != nil { + return err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+path, bytes.NewReader(data)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + c.setHeaders(req) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + respData, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) + return fmt.Errorf("POST %s returned %d: %s", path, resp.StatusCode, strings.TrimSpace(string(respData))) + } + if out == nil { + return nil + } + return json.NewDecoder(resp.Body).Decode(out) +} + // PutJSON performs a PUT request with a JSON body. func (c *APIClient) PutJSON(ctx context.Context, path string, body any, out any) error { data, err := json.Marshal(body) From 788ba502f621f4d0e62a6363e046e34b5165b386 Mon Sep 17 00:00:00 2001 From: yushen Date: Thu, 26 Mar 2026 14:55:03 +0800 Subject: [PATCH 2/7] fix(auth): validate cli_callback to localhost and URL-encode callback param - Prevent open redirect / JWT theft by only allowing localhost/127.0.0.1 as cli_callback hostname - URL-encode the callback URL in the login query string - Simplify resolveAppURL to use os.Getenv directly (no phantom flag) Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/web/app/(auth)/login/page.tsx | 13 +++++++++++++ server/cmd/multica/cmd_auth.go | 10 +++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/apps/web/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx index 1638d299..ee64ee30 100644 --- a/apps/web/app/(auth)/login/page.tsx +++ b/apps/web/app/(auth)/login/page.tsx @@ -73,6 +73,19 @@ function LoginPageContent() { const cliCallback = searchParams.get("cli_callback"); if (cliCallback) { // CLI browser login: verify code, get JWT, redirect to CLI callback. + // Only allow localhost callbacks to prevent open redirect / JWT theft. + try { + const cbUrl = new URL(cliCallback); + if (cbUrl.hostname !== "localhost" && cbUrl.hostname !== "127.0.0.1") { + setError("Invalid callback URL"); + setSubmitting(false); + return; + } + } catch { + setError("Invalid callback URL"); + setSubmitting(false); + return; + } const { token } = await api.verifyCode(email, value); const separator = cliCallback.includes("?") ? "&" : "?"; window.location.href = `${cliCallback}${separator}token=${encodeURIComponent(token)}`; diff --git a/server/cmd/multica/cmd_auth.go b/server/cmd/multica/cmd_auth.go index 09734c30..3f8d98ef 100644 --- a/server/cmd/multica/cmd_auth.go +++ b/server/cmd/multica/cmd_auth.go @@ -6,6 +6,7 @@ import ( "fmt" "net" "net/http" + "net/url" "os" "os/exec" "runtime" @@ -55,9 +56,8 @@ func resolveToken() string { return cfg.Token } -func resolveAppURL(cmd *cobra.Command) string { - val := cli.FlagOrEnv(cmd, "", "MULTICA_APP_URL", "") - if val != "" { +func resolveAppURL() string { + if val := strings.TrimSpace(os.Getenv("MULTICA_APP_URL")); val != "" { return strings.TrimRight(val, "/") } return "http://localhost:3000" @@ -92,7 +92,7 @@ func runAuthLogin(cmd *cobra.Command, _ []string) error { func runAuthLoginBrowser(cmd *cobra.Command) error { serverURL := resolveServerURL(cmd) - appURL := resolveAppURL(cmd) + appURL := resolveAppURL() // Start a local HTTP server on a random port to receive the callback. listener, err := net.Listen("tcp", "127.0.0.1:0") @@ -103,7 +103,7 @@ func runAuthLoginBrowser(cmd *cobra.Command) error { port := listener.Addr().(*net.TCPAddr).Port callbackURL := fmt.Sprintf("http://localhost:%d/callback", port) - loginURL := fmt.Sprintf("%s/login?cli_callback=%s", appURL, callbackURL) + loginURL := fmt.Sprintf("%s/login?cli_callback=%s", appURL, url.QueryEscape(callbackURL)) // Channel to receive the JWT from the browser callback. jwtCh := make(chan string, 1) From b9a4fa1a6dc8c885f21b880fa6de68bae497cb73 Mon Sep 17 00:00:00 2001 From: yushen Date: Thu, 26 Mar 2026 15:04:46 +0800 Subject: [PATCH 3/7] fix(auth): add CSRF state param, scheme validation, and .envrc to gitignore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add state parameter to CLI browser login flow for CSRF protection — CLI generates a random state, frontend passes it through, CLI verifies on callback. Also restrict cli_callback to http: scheme only. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 1 + apps/web/app/(auth)/login/page.tsx | 10 ++++++++-- server/cmd/multica/cmd_auth.go | 17 ++++++++++++++++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index d9c40de0..7d7617b7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules dist *.log .DS_Store +.envrc # build outputs .next diff --git a/apps/web/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx index ee64ee30..4f8409d6 100644 --- a/apps/web/app/(auth)/login/page.tsx +++ b/apps/web/app/(auth)/login/page.tsx @@ -73,9 +73,14 @@ function LoginPageContent() { const cliCallback = searchParams.get("cli_callback"); if (cliCallback) { // CLI browser login: verify code, get JWT, redirect to CLI callback. - // Only allow localhost callbacks to prevent open redirect / JWT theft. + // Only allow http://localhost callbacks to prevent open redirect / JWT theft. try { const cbUrl = new URL(cliCallback); + if (cbUrl.protocol !== "http:") { + setError("Invalid callback URL"); + setSubmitting(false); + return; + } if (cbUrl.hostname !== "localhost" && cbUrl.hostname !== "127.0.0.1") { setError("Invalid callback URL"); setSubmitting(false); @@ -87,8 +92,9 @@ function LoginPageContent() { return; } const { token } = await api.verifyCode(email, value); + const cliState = searchParams.get("cli_state") || ""; const separator = cliCallback.includes("?") ? "&" : "?"; - window.location.href = `${cliCallback}${separator}token=${encodeURIComponent(token)}`; + window.location.href = `${cliCallback}${separator}token=${encodeURIComponent(token)}&state=${encodeURIComponent(cliState)}`; return; } diff --git a/server/cmd/multica/cmd_auth.go b/server/cmd/multica/cmd_auth.go index 3f8d98ef..d2d406f2 100644 --- a/server/cmd/multica/cmd_auth.go +++ b/server/cmd/multica/cmd_auth.go @@ -3,6 +3,8 @@ package main import ( "bufio" "context" + "crypto/rand" + "encoding/hex" "fmt" "net" "net/http" @@ -103,7 +105,15 @@ func runAuthLoginBrowser(cmd *cobra.Command) error { port := listener.Addr().(*net.TCPAddr).Port callbackURL := fmt.Sprintf("http://localhost:%d/callback", port) - loginURL := fmt.Sprintf("%s/login?cli_callback=%s", appURL, url.QueryEscape(callbackURL)) + + // Generate a random state parameter for CSRF protection. + stateBytes := make([]byte, 16) + if _, err := rand.Read(stateBytes); err != nil { + return fmt.Errorf("failed to generate state: %w", err) + } + state := hex.EncodeToString(stateBytes) + + loginURL := fmt.Sprintf("%s/login?cli_callback=%s&cli_state=%s", appURL, url.QueryEscape(callbackURL), url.QueryEscape(state)) // Channel to receive the JWT from the browser callback. jwtCh := make(chan string, 1) @@ -116,6 +126,11 @@ func runAuthLoginBrowser(cmd *cobra.Command) error { http.Error(w, "missing token", http.StatusBadRequest) return } + returnedState := r.URL.Query().Get("state") + if returnedState != state { + http.Error(w, "invalid state parameter", http.StatusBadRequest) + return + } w.Header().Set("Content-Type", "text/html") w.Write([]byte(`

Authentication successful!

You can close this tab and return to the terminal.

`)) jwtCh <- token From fb2e286cfe9fa07c5bcd45b39000c925c45e2b48 Mon Sep 17 00:00:00 2001 From: yushen Date: Thu, 26 Mar 2026 15:04:50 +0800 Subject: [PATCH 4/7] test(cli): add unit tests for PostJSON API client method Cover success response, error status codes, nil output, and workspace header propagation. Co-Authored-By: Claude Opus 4.6 (1M context) --- server/internal/cli/client_test.go | 104 +++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 server/internal/cli/client_test.go diff --git a/server/internal/cli/client_test.go b/server/internal/cli/client_test.go new file mode 100644 index 00000000..7fbe933c --- /dev/null +++ b/server/internal/cli/client_test.go @@ -0,0 +1,104 @@ +package cli + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" +) + +func TestPostJSON(t *testing.T) { + type reqBody struct { + Name string `json:"name"` + Age int `json:"age"` + } + type respBody struct { + ID string `json:"id"` + } + + t.Run("success", func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("expected POST, got %s", r.Method) + } + if ct := r.Header.Get("Content-Type"); ct != "application/json" { + t.Errorf("expected Content-Type application/json, got %s", ct) + } + if auth := r.Header.Get("Authorization"); auth != "Bearer test-token" { + t.Errorf("expected Authorization Bearer test-token, got %s", auth) + } + + var body reqBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatalf("failed to decode request body: %v", err) + } + if body.Name != "alice" || body.Age != 30 { + t.Errorf("unexpected body: %+v", body) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(respBody{ID: "123"}) + })) + defer srv.Close() + + client := NewAPIClient(srv.URL, "", "test-token") + var out respBody + err := client.PostJSON(context.Background(), "/test", reqBody{Name: "alice", Age: 30}, &out) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if out.ID != "123" { + t.Errorf("expected ID 123, got %s", out.ID) + } + }) + + t.Run("error status", func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + io.WriteString(w, "bad request") + })) + defer srv.Close() + + client := NewAPIClient(srv.URL, "", "test-token") + err := client.PostJSON(context.Background(), "/test", reqBody{Name: "bob"}, nil) + if err == nil { + t.Fatal("expected error, got nil") + } + if got := err.Error(); got != "POST /test returned 400: bad request" { + t.Errorf("unexpected error message: %s", got) + } + }) + + t.Run("nil output", func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusCreated) + })) + defer srv.Close() + + client := NewAPIClient(srv.URL, "", "test-token") + err := client.PostJSON(context.Background(), "/test", reqBody{Name: "charlie"}, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("workspace header", func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if ws := r.Header.Get("X-Workspace-ID"); ws != "ws-abc" { + t.Errorf("expected X-Workspace-ID ws-abc, got %s", ws) + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(respBody{ID: "456"}) + })) + defer srv.Close() + + client := NewAPIClient(srv.URL, "ws-abc", "test-token") + var out respBody + err := client.PostJSON(context.Background(), "/test", reqBody{}, &out) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) +} From de322f7a5168418d07d564a5948e741610144c39 Mon Sep 17 00:00:00 2001 From: yushen Date: Thu, 26 Mar 2026 15:44:01 +0800 Subject: [PATCH 5/7] fix(auth): improve CLI login callback page to match frontend design Replace bare HTML with a styled card layout featuring dark/light mode support, Multica brand icon, and auto-close behavior. Co-Authored-By: Claude Opus 4.6 (1M context) --- server/cmd/multica/cmd_auth.go | 41 +++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/server/cmd/multica/cmd_auth.go b/server/cmd/multica/cmd_auth.go index d2d406f2..194c41e4 100644 --- a/server/cmd/multica/cmd_auth.go +++ b/server/cmd/multica/cmd_auth.go @@ -132,7 +132,7 @@ func runAuthLoginBrowser(cmd *cobra.Command) error { return } w.Header().Set("Content-Type", "text/html") - w.Write([]byte(`

Authentication successful!

You can close this tab and return to the terminal.

`)) + w.Write([]byte(callbackSuccessHTML)) jwtCh <- token }) @@ -282,6 +282,45 @@ func runAuthStatus(cmd *cobra.Command, _ []string) error { return nil } +const callbackSuccessHTML = ` + + + + +Multica — Authenticated + + + +
+
+ +
+
+

Authentication successful

+

You can close this tab and return to the terminal.

+

Your CLI session is now authenticated.

+
+ + +` + func runAuthLogout(_ *cobra.Command, _ []string) error { cfg, _ := cli.LoadCLIConfig() if cfg.Token == "" { From de1b7e3377eb120bc519575c48ec4973bcb64d87 Mon Sep 17 00:00:00 2001 From: yushen Date: Thu, 26 Mar 2026 15:44:05 +0800 Subject: [PATCH 6/7] fix(auth): reduce verification code rate limit from 60s to 10s Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/web/app/(auth)/login/page.tsx | 4 ++-- server/internal/handler/auth.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/app/(auth)/login/page.tsx b/apps/web/app/(auth)/login/page.tsx index 4f8409d6..de94a4ee 100644 --- a/apps/web/app/(auth)/login/page.tsx +++ b/apps/web/app/(auth)/login/page.tsx @@ -54,7 +54,7 @@ function LoginPageContent() { await sendCode(email); setStep("code"); setCode(""); - setCooldown(60); + setCooldown(10); } catch (err) { setError( err instanceof Error ? err.message : "Failed to send code. Make sure the server is running." @@ -118,7 +118,7 @@ function LoginPageContent() { setError(""); try { await sendCode(email); - setCooldown(60); + setCooldown(10); } catch (err) { setError( err instanceof Error ? err.message : "Failed to resend code" diff --git a/server/internal/handler/auth.go b/server/internal/handler/auth.go index 4575fdea..d0e4fedb 100644 --- a/server/internal/handler/auth.go +++ b/server/internal/handler/auth.go @@ -213,9 +213,9 @@ func (h *Handler) SendCode(w http.ResponseWriter, r *http.Request) { return } - // Rate limit: max 1 code per 60 seconds per email + // Rate limit: max 1 code per 10 seconds per email latest, err := h.Queries.GetLatestCodeByEmail(r.Context(), email) - if err == nil && time.Since(latest.CreatedAt.Time) < 60*time.Second { + if err == nil && time.Since(latest.CreatedAt.Time) < 10*time.Second { writeError(w, http.StatusTooManyRequests, "please wait before requesting another code") return } From 072ee83ee8b39c40bac9b23888b19efaf9c57fd1 Mon Sep 17 00:00:00 2001 From: yushen Date: Thu, 26 Mar 2026 15:44:10 +0800 Subject: [PATCH 7/7] fix(db): renumber inbox_actor migration to resolve 009 conflict 009_inbox_actor conflicted with 009_verification_code, causing actor_type/actor_id columns to never be added and /api/inbox to 500. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../{009_inbox_actor.down.sql => 012_inbox_actor.down.sql} | 0 .../migrations/{009_inbox_actor.up.sql => 012_inbox_actor.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename server/migrations/{009_inbox_actor.down.sql => 012_inbox_actor.down.sql} (100%) rename server/migrations/{009_inbox_actor.up.sql => 012_inbox_actor.up.sql} (100%) diff --git a/server/migrations/009_inbox_actor.down.sql b/server/migrations/012_inbox_actor.down.sql similarity index 100% rename from server/migrations/009_inbox_actor.down.sql rename to server/migrations/012_inbox_actor.down.sql diff --git a/server/migrations/009_inbox_actor.up.sql b/server/migrations/012_inbox_actor.up.sql similarity index 100% rename from server/migrations/009_inbox_actor.up.sql rename to server/migrations/012_inbox_actor.up.sql