diff --git a/LOCAL_DEVELOPMENT.md b/LOCAL_DEVELOPMENT.md index 397d8f88..50b9f80e 100644 --- a/LOCAL_DEVELOPMENT.md +++ b/LOCAL_DEVELOPMENT.md @@ -314,18 +314,8 @@ Run the local daemon: make daemon ``` -Normal flow: - -1. start the daemon -2. open the pairing link it prints -3. choose the workspace in the browser -4. let the daemon register its local runtime - -Debug shortcut: - -- you can set `MULTICA_WORKSPACE_ID` in your env file -- this skips normal pairing -- treat it as a local shortcut, not the default workflow +The daemon authenticates using the CLI's stored token (`multica login`). +It registers runtimes for all watched workspaces from the CLI config. ## Troubleshooting diff --git a/apps/web/app/pair/local/page.test.tsx b/apps/web/app/pair/local/page.test.tsx deleted file mode 100644 index 38ec775b..00000000 --- a/apps/web/app/pair/local/page.test.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render, screen, waitFor } from "@testing-library/react"; - -const { - mockGetDaemonPairingSession, - mockApproveDaemonPairingSession, - mockWorkspace, - mockAuthValue, -} = vi.hoisted(() => ({ - mockGetDaemonPairingSession: vi.fn(), - mockApproveDaemonPairingSession: vi.fn(), - mockWorkspace: { - id: "05ce77f1-7c45-4735-b1f7-619347f7f76c", - name: "Jiayuan's Workspace", - slug: "jiayuan-05ce77f1", - description: null, - settings: {}, - created_at: "2026-03-24T00:00:00Z", - updated_at: "2026-03-24T00:00:00Z", - }, - mockAuthValue: { - user: { - id: "user-1", - name: "Jiayuan", - email: "jiayuan@example.com", - avatar_url: null, - created_at: "2026-03-24T00:00:00Z", - updated_at: "2026-03-24T00:00:00Z", - }, - workspaces: [] as Array<{ - id: string; - name: string; - slug: string; - description: null; - settings: Record; - created_at: string; - updated_at: string; - }>, - workspace: null as null | { - id: string; - name: string; - slug: string; - description: null; - settings: Record; - created_at: string; - updated_at: string; - }, - isLoading: false, - }, -})); - -mockAuthValue.workspaces = [mockWorkspace]; -mockAuthValue.workspace = mockWorkspace; - -vi.mock("next/navigation", () => ({ - useSearchParams: () => new URLSearchParams("token=test-token"), -})); - -vi.mock("@/shared/api", () => ({ - api: { - getDaemonPairingSession: mockGetDaemonPairingSession, - approveDaemonPairingSession: mockApproveDaemonPairingSession, - }, -})); - -vi.mock("@/features/auth", () => ({ - useAuthStore: (selector: (s: any) => any) => - selector(mockAuthValue), -})); - -vi.mock("@/features/workspace", () => ({ - useWorkspaceStore: (selector: (s: any) => any) => - selector(mockAuthValue), -})); - -import LocalDaemonPairPage from "./page"; - -describe("LocalDaemonPairPage", () => { - beforeEach(() => { - vi.clearAllMocks(); - mockGetDaemonPairingSession.mockResolvedValue({ - token: "test-token", - daemon_id: "local-daemon", - device_name: "Jiayuans-MacBook-Pro.local", - runtime_name: "Local Codex", - runtime_type: "codex", - runtime_version: "codex-cli 0.116.0", - workspace_id: mockWorkspace.id, - status: "pending", - approved_at: null, - claimed_at: null, - expires_at: "2026-03-24T07:20:00Z", - link_url: null, - }); - }); - - it("shows the selected workspace name instead of the raw id", async () => { - render(); - - await waitFor(() => { - expect(mockGetDaemonPairingSession).toHaveBeenCalledWith("test-token"); - }); - - expect(await screen.findByText("Jiayuan's Workspace")).toBeInTheDocument(); - expect(screen.queryByText(mockWorkspace.id)).not.toBeInTheDocument(); - }); -}); diff --git a/apps/web/app/pair/local/page.tsx b/apps/web/app/pair/local/page.tsx deleted file mode 100644 index 6f94653d..00000000 --- a/apps/web/app/pair/local/page.tsx +++ /dev/null @@ -1,181 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { Suspense, useEffect, useMemo, useState } from "react"; -import { useSearchParams } from "next/navigation"; -import type { DaemonPairingSession } from "@/shared/types"; -import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; -import { - Select, - SelectTrigger, - SelectContent, - SelectItem, -} from "@/components/ui/select"; -import { api } from "@/shared/api"; -import { useAuthStore } from "@/features/auth"; -import { useWorkspaceStore } from "@/features/workspace"; - -function formatExpiresAt(value: string) { - return new Date(value).toLocaleString("en-US", { - month: "short", - day: "numeric", - hour: "numeric", - minute: "2-digit", - }); -} - -function LocalDaemonPairPageContent() { - const searchParams = useSearchParams(); - const token = searchParams.get("token") ?? ""; - const user = useAuthStore((s) => s.user); - const isLoading = useAuthStore((s) => s.isLoading); - const workspace = useWorkspaceStore((s) => s.workspace); - const workspaces = useWorkspaceStore((s) => s.workspaces); - const [session, setSession] = useState(null); - const [selectedWorkspaceId, setSelectedWorkspaceId] = useState(""); - const [loading, setLoading] = useState(true); - const [submitting, setSubmitting] = useState(false); - const [error, setError] = useState(""); - - const nextLoginURL = useMemo(() => { - const next = `/pair/local?token=${encodeURIComponent(token)}`; - return `/login?next=${encodeURIComponent(next)}`; - }, [token]); - const selectedWorkspace = useMemo( - () => workspaces.find((item) => item.id === selectedWorkspaceId) ?? null, - [selectedWorkspaceId, workspaces], - ); - - useEffect(() => { - if (!token) { - setError("Missing pairing token."); - setLoading(false); - return; - } - - setLoading(true); - api.getDaemonPairingSession(token) - .then((value) => { - setSession(value); - setSelectedWorkspaceId(value.workspace_id || workspace?.id || workspaces[0]?.id || ""); - }) - .catch((err) => setError(err instanceof Error ? err.message : "Failed to load pairing session.")) - .finally(() => setLoading(false)); - }, [token, workspace?.id, workspaces]); - - const approve = async () => { - if (!token || !selectedWorkspaceId) return; - setSubmitting(true); - setError(""); - try { - const approved = await api.approveDaemonPairingSession(token, { - workspace_id: selectedWorkspaceId, - }); - setSession(approved); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to approve pairing session."); - } finally { - setSubmitting(false); - } - }; - - return ( -
-
-
-

Connect Local Codex Runtime

-

- Approve this pairing request to register your local Codex runtime with a workspace. -

-
- - {loading || isLoading ? ( -
Loading pairing session...
- ) : error ? ( -
- {error} -
- ) : session ? ( - <> -
-
{session.runtime_name}
-
- {session.device_name} - {session.runtime_version ? ` ยท ${session.runtime_version}` : ""} -
-
- {session.runtime_type} -
-
- Expires {formatExpiresAt(session.expires_at)} -
-
- - {!user ? ( -
-

- Sign in first, then choose which workspace should own this local runtime. -

- - Sign in to continue - -
- ) : session.status === "approved" || session.status === "claimed" ? ( -
- This runtime is linked to a workspace. Return to the daemon window to finish setup. -
- ) : session.status === "expired" ? ( -
- This pairing link expired. Restart the daemon to generate a new link. -
- ) : workspaces.length === 0 ? ( -
- You do not have a workspace yet. Create one first, then reopen this pairing link. -
- ) : ( -
-
- - -
- - -
- )} - - ) : null} -
-
- ); -} - -export default function LocalDaemonPairPage() { - return ( - - - - ); -} diff --git a/apps/web/shared/api/client.ts b/apps/web/shared/api/client.ts index f6a080b0..9b41a9bd 100644 --- a/apps/web/shared/api/client.ts +++ b/apps/web/shared/api/client.ts @@ -12,8 +12,6 @@ import type { UpdateAgentRequest, AgentTask, AgentRuntime, - DaemonPairingSession, - ApproveDaemonPairingSessionRequest, InboxItem, IssueSubscriber, Comment, @@ -360,20 +358,6 @@ export class ApiClient { return this.fetch(`/api/issues/${issueId}/task-runs`); } - async getDaemonPairingSession(token: string): Promise { - return this.fetch(`/api/daemon/pairing-sessions/${token}`); - } - - async approveDaemonPairingSession( - token: string, - data: ApproveDaemonPairingSessionRequest, - ): Promise { - return this.fetch(`/api/daemon/pairing-sessions/${token}/approve`, { - method: "POST", - body: JSON.stringify(data), - }); - } - // Inbox async listInbox(): Promise { return this.fetch("/api/inbox"); diff --git a/apps/web/shared/types/daemon.ts b/apps/web/shared/types/daemon.ts deleted file mode 100644 index 459a67a5..00000000 --- a/apps/web/shared/types/daemon.ts +++ /dev/null @@ -1,22 +0,0 @@ -export type DaemonPairingSessionStatus = "pending" | "approved" | "claimed" | "expired"; - -export interface DaemonPairingSession { - token: string; - daemon_id: string; - device_name: string; - runtime_name: string; - runtime_type: string; - runtime_version: string; - workspace_id: string | null; - status: DaemonPairingSessionStatus; - approved_at: string | null; - claimed_at: string | null; - expires_at: string; - created_at: string; - updated_at: string; - link_url?: string | null; -} - -export interface ApproveDaemonPairingSessionRequest { - workspace_id: string; -} diff --git a/apps/web/shared/types/index.ts b/apps/web/shared/types/index.ts index 4c105ff5..709c7f18 100644 --- a/apps/web/shared/types/index.ts +++ b/apps/web/shared/types/index.ts @@ -27,7 +27,6 @@ export type { InboxItem, InboxSeverity, InboxItemType } from "./inbox"; export type { Comment, CommentType, CommentAuthorType, Reaction } from "./comment"; export type { TimelineEntry } from "./activity"; export type { IssueSubscriber } from "./subscriber"; -export type { DaemonPairingSession, DaemonPairingSessionStatus, ApproveDaemonPairingSessionRequest } from "./daemon"; export type * from "./events"; export type * from "./api"; export type { Attachment } from "./attachment"; diff --git a/server/cmd/server/router.go b/server/cmd/server/router.go index 8cc84c1c..400d3c40 100644 --- a/server/cmd/server/router.go +++ b/server/cmd/server/router.go @@ -83,11 +83,9 @@ func NewRouter(pool *pgxpool.Pool, hub *realtime.Hub, bus *events.Bus) chi.Route r.Post("/auth/send-code", h.SendCode) r.Post("/auth/verify-code", h.VerifyCode) - // Daemon API routes (no user auth; daemon auth deferred to later) + // Daemon API routes (all require a valid token) r.Route("/api/daemon", func(r chi.Router) { - r.Post("/pairing-sessions", h.CreateDaemonPairingSession) - r.Get("/pairing-sessions/{token}", h.GetDaemonPairingSession) - r.Post("/pairing-sessions/{token}/claim", h.ClaimDaemonPairingSession) + r.Use(middleware.Auth(queries)) r.Post("/register", h.DaemonRegister) r.Post("/deregister", h.DaemonDeregister) @@ -150,8 +148,6 @@ func NewRouter(pool *pgxpool.Pool, hub *realtime.Hub, bus *events.Bus) chi.Route r.Delete("/{id}", h.RevokePersonalAccessToken) }) - r.Post("/api/daemon/pairing-sessions/{token}/approve", h.ApproveDaemonPairingSession) - // --- Workspace-scoped routes (all require workspace membership) --- r.Group(func(r chi.Router) { r.Use(middleware.RequireWorkspaceMember(queries)) diff --git a/server/internal/handler/daemon.go b/server/internal/handler/daemon.go index d91b9376..ae7803c4 100644 --- a/server/internal/handler/daemon.go +++ b/server/internal/handler/daemon.go @@ -53,6 +53,12 @@ func (h *Handler) DaemonRegister(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusBadRequest, "at least one runtime is required") return } + + // Verify the caller is a member of the target workspace. + if _, ok := h.requireWorkspaceMember(w, r, req.WorkspaceID, "workspace not found"); !ok { + return + } + ws, err := h.Queries.GetWorkspace(r.Context(), parseUUID(req.WorkspaceID)) if err != nil { writeError(w, http.StatusNotFound, "workspace not found") diff --git a/server/internal/handler/daemon_pairing.go b/server/internal/handler/daemon_pairing.go deleted file mode 100644 index 9cf7747f..00000000 --- a/server/internal/handler/daemon_pairing.go +++ /dev/null @@ -1,386 +0,0 @@ -package handler - -import ( - "context" - "crypto/rand" - "encoding/hex" - "encoding/json" - "fmt" - "net/http" - "net/url" - "os" - "strings" - "time" - - "github.com/go-chi/chi/v5" - "github.com/jackc/pgx/v5/pgtype" -) - -const daemonPairingTTL = 10 * time.Minute - -type daemonPairingSessionRecord struct { - Token string - DaemonID string - DeviceName string - RuntimeName string - RuntimeType string - RuntimeVersion string - WorkspaceID pgtype.UUID - ApprovedBy pgtype.UUID - Status string - ApprovedAt pgtype.Timestamptz - ClaimedAt pgtype.Timestamptz - ExpiresAt pgtype.Timestamptz - CreatedAt pgtype.Timestamptz - UpdatedAt pgtype.Timestamptz -} - -type DaemonPairingSessionResponse struct { - Token string `json:"token"` - DaemonID string `json:"daemon_id"` - DeviceName string `json:"device_name"` - RuntimeName string `json:"runtime_name"` - RuntimeType string `json:"runtime_type"` - RuntimeVersion string `json:"runtime_version"` - WorkspaceID *string `json:"workspace_id"` - Status string `json:"status"` - ApprovedAt *string `json:"approved_at"` - ClaimedAt *string `json:"claimed_at"` - ExpiresAt string `json:"expires_at"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` - LinkURL *string `json:"link_url,omitempty"` -} - -type CreateDaemonPairingSessionRequest struct { - DaemonID string `json:"daemon_id"` - DeviceName string `json:"device_name"` - RuntimeName string `json:"runtime_name"` - RuntimeType string `json:"runtime_type"` - RuntimeVersion string `json:"runtime_version"` -} - -type ApproveDaemonPairingSessionRequest struct { - WorkspaceID string `json:"workspace_id"` -} - -func daemonAppBaseURL() string { - for _, key := range []string{"MULTICA_APP_URL", "FRONTEND_ORIGIN"} { - if value := strings.TrimSpace(os.Getenv(key)); value != "" { - return strings.TrimRight(value, "/") - } - } - return "http://localhost:3000" -} - -func daemonPairingLinkURL(token string) string { - base := daemonAppBaseURL() - return base + "/pair/local?token=" + url.QueryEscape(token) -} - -func daemonPairingSessionToResponse(rec daemonPairingSessionRecord, includeLink bool) DaemonPairingSessionResponse { - resp := DaemonPairingSessionResponse{ - Token: rec.Token, - DaemonID: rec.DaemonID, - DeviceName: rec.DeviceName, - RuntimeName: rec.RuntimeName, - RuntimeType: rec.RuntimeType, - RuntimeVersion: rec.RuntimeVersion, - WorkspaceID: uuidToPtr(rec.WorkspaceID), - Status: rec.Status, - ApprovedAt: timestampToPtr(rec.ApprovedAt), - ClaimedAt: timestampToPtr(rec.ClaimedAt), - ExpiresAt: timestampToString(rec.ExpiresAt), - CreatedAt: timestampToString(rec.CreatedAt), - UpdatedAt: timestampToString(rec.UpdatedAt), - } - if includeLink { - link := daemonPairingLinkURL(rec.Token) - resp.LinkURL = &link - } - return resp -} - -func randomDaemonPairingToken() (string, error) { - bytes := make([]byte, 16) - if _, err := rand.Read(bytes); err != nil { - return "", err - } - return hex.EncodeToString(bytes), nil -} - -func (h *Handler) getDaemonPairingSession(ctx context.Context, token string) (daemonPairingSessionRecord, error) { - if h.DB == nil { - return daemonPairingSessionRecord{}, fmt.Errorf("database executor is not configured") - } - - var rec daemonPairingSessionRecord - err := h.DB.QueryRow(ctx, ` - SELECT - token, - daemon_id, - device_name, - runtime_name, - runtime_type, - runtime_version, - workspace_id, - approved_by, - status, - approved_at, - claimed_at, - expires_at, - created_at, - updated_at - FROM daemon_pairing_session - WHERE token = $1 - `, token).Scan( - &rec.Token, - &rec.DaemonID, - &rec.DeviceName, - &rec.RuntimeName, - &rec.RuntimeType, - &rec.RuntimeVersion, - &rec.WorkspaceID, - &rec.ApprovedBy, - &rec.Status, - &rec.ApprovedAt, - &rec.ClaimedAt, - &rec.ExpiresAt, - &rec.CreatedAt, - &rec.UpdatedAt, - ) - if err != nil { - return daemonPairingSessionRecord{}, err - } - - if rec.Status == "pending" && rec.ExpiresAt.Valid && rec.ExpiresAt.Time.Before(time.Now()) { - if _, err := h.DB.Exec(ctx, ` - UPDATE daemon_pairing_session - SET status = 'expired', updated_at = now() - WHERE token = $1 AND status = 'pending' - `, token); err == nil { - rec.Status = "expired" - rec.UpdatedAt = pgtype.Timestamptz{Time: time.Now(), Valid: true} - } - } - - return rec, nil -} - -func (h *Handler) CreateDaemonPairingSession(w http.ResponseWriter, r *http.Request) { - if h.DB == nil { - writeError(w, http.StatusInternalServerError, "database executor is not configured") - return - } - - var req CreateDaemonPairingSessionRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeError(w, http.StatusBadRequest, "invalid request body") - return - } - - req.DaemonID = strings.TrimSpace(req.DaemonID) - req.DeviceName = strings.TrimSpace(req.DeviceName) - req.RuntimeName = strings.TrimSpace(req.RuntimeName) - req.RuntimeType = strings.TrimSpace(req.RuntimeType) - req.RuntimeVersion = strings.TrimSpace(req.RuntimeVersion) - - if req.DaemonID == "" { - writeError(w, http.StatusBadRequest, "daemon_id is required") - return - } - if req.DeviceName == "" { - writeError(w, http.StatusBadRequest, "device_name is required") - return - } - if req.RuntimeName == "" { - writeError(w, http.StatusBadRequest, "runtime_name is required") - return - } - if req.RuntimeType == "" { - writeError(w, http.StatusBadRequest, "runtime_type is required") - return - } - - token, err := randomDaemonPairingToken() - if err != nil { - writeError(w, http.StatusInternalServerError, "failed to create pairing token") - return - } - - expiresAt := time.Now().Add(daemonPairingTTL) - var rec daemonPairingSessionRecord - err = h.DB.QueryRow(r.Context(), ` - INSERT INTO daemon_pairing_session ( - token, - daemon_id, - device_name, - runtime_name, - runtime_type, - runtime_version, - expires_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7) - RETURNING - token, - daemon_id, - device_name, - runtime_name, - runtime_type, - runtime_version, - workspace_id, - approved_by, - status, - approved_at, - claimed_at, - expires_at, - created_at, - updated_at - `, - token, - req.DaemonID, - req.DeviceName, - req.RuntimeName, - req.RuntimeType, - req.RuntimeVersion, - expiresAt, - ).Scan( - &rec.Token, - &rec.DaemonID, - &rec.DeviceName, - &rec.RuntimeName, - &rec.RuntimeType, - &rec.RuntimeVersion, - &rec.WorkspaceID, - &rec.ApprovedBy, - &rec.Status, - &rec.ApprovedAt, - &rec.ClaimedAt, - &rec.ExpiresAt, - &rec.CreatedAt, - &rec.UpdatedAt, - ) - if err != nil { - writeError(w, http.StatusInternalServerError, "failed to create pairing session") - return - } - - writeJSON(w, http.StatusCreated, daemonPairingSessionToResponse(rec, true)) -} - -func (h *Handler) GetDaemonPairingSession(w http.ResponseWriter, r *http.Request) { - token := chi.URLParam(r, "token") - rec, err := h.getDaemonPairingSession(r.Context(), token) - if err != nil { - writeError(w, http.StatusNotFound, "pairing session not found") - return - } - writeJSON(w, http.StatusOK, daemonPairingSessionToResponse(rec, true)) -} - -func (h *Handler) ApproveDaemonPairingSession(w http.ResponseWriter, r *http.Request) { - token := chi.URLParam(r, "token") - rec, err := h.getDaemonPairingSession(r.Context(), token) - if err != nil { - writeError(w, http.StatusNotFound, "pairing session not found") - return - } - if rec.Status == "expired" { - writeError(w, http.StatusBadRequest, "pairing session expired") - return - } - if rec.Status == "claimed" { - writeError(w, http.StatusBadRequest, "pairing session already claimed") - return - } - if rec.Status == "approved" { - writeJSON(w, http.StatusOK, daemonPairingSessionToResponse(rec, true)) - return - } - - var req ApproveDaemonPairingSessionRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeError(w, http.StatusBadRequest, "invalid request body") - return - } - if req.WorkspaceID == "" { - writeError(w, http.StatusBadRequest, "workspace_id is required") - return - } - - userID, ok := requireUserID(w, r) - if !ok { - return - } - if _, ok := h.requireWorkspaceMember(w, r, req.WorkspaceID, "workspace not found"); !ok { - return - } - - if h.DB == nil { - writeError(w, http.StatusInternalServerError, "database executor is not configured") - return - } - - if _, err := h.DB.Exec(r.Context(), ` - UPDATE daemon_pairing_session - SET - workspace_id = $2, - approved_by = $3, - status = 'approved', - approved_at = now(), - updated_at = now() - WHERE token = $1 AND status = 'pending' - `, token, parseUUID(req.WorkspaceID), parseUUID(userID)); err != nil { - writeError(w, http.StatusInternalServerError, "failed to approve pairing session") - return - } - - rec, err = h.getDaemonPairingSession(r.Context(), token) - if err != nil { - writeError(w, http.StatusInternalServerError, "failed to reload pairing session") - return - } - - writeJSON(w, http.StatusOK, daemonPairingSessionToResponse(rec, true)) -} - -func (h *Handler) ClaimDaemonPairingSession(w http.ResponseWriter, r *http.Request) { - token := chi.URLParam(r, "token") - rec, err := h.getDaemonPairingSession(r.Context(), token) - if err != nil { - writeError(w, http.StatusNotFound, "pairing session not found") - return - } - if rec.Status == "claimed" { - writeJSON(w, http.StatusOK, daemonPairingSessionToResponse(rec, true)) - return - } - if rec.Status != "approved" { - writeError(w, http.StatusBadRequest, "pairing session is not approved") - return - } - - if h.DB == nil { - writeError(w, http.StatusInternalServerError, "database executor is not configured") - return - } - - if _, err := h.DB.Exec(r.Context(), ` - UPDATE daemon_pairing_session - SET - status = 'claimed', - claimed_at = now(), - updated_at = now() - WHERE token = $1 AND status = 'approved' - `, token); err != nil { - writeError(w, http.StatusInternalServerError, "failed to claim pairing session") - return - } - - rec, err = h.getDaemonPairingSession(r.Context(), token) - if err != nil { - writeError(w, http.StatusInternalServerError, "failed to reload pairing session") - return - } - - writeJSON(w, http.StatusOK, daemonPairingSessionToResponse(rec, true)) -} diff --git a/server/internal/handler/handler_test.go b/server/internal/handler/handler_test.go index 15e1769b..9a5ec3c8 100644 --- a/server/internal/handler/handler_test.go +++ b/server/internal/handler/handler_test.go @@ -729,6 +729,7 @@ func TestDaemonRegisterMissingWorkspaceReturns404(t *testing.T) { "runtimes":[{"name":"Local Codex","type":"codex","version":"1.0.0","status":"online"}] }`)) req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-User-ID", testUserID) testHandler.DaemonRegister(w, req) if w.Code != http.StatusNotFound { diff --git a/server/migrations/029_drop_daemon_pairing.down.sql b/server/migrations/029_drop_daemon_pairing.down.sql new file mode 100644 index 00000000..c39431e7 --- /dev/null +++ b/server/migrations/029_drop_daemon_pairing.down.sql @@ -0,0 +1,21 @@ +-- Re-create the daemon_pairing_session table (from migration 005). +CREATE TABLE IF NOT EXISTS daemon_pairing_session ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + token TEXT NOT NULL UNIQUE, + daemon_id TEXT NOT NULL, + device_name TEXT NOT NULL DEFAULT '', + runtime_name TEXT NOT NULL DEFAULT '', + runtime_type TEXT NOT NULL DEFAULT '', + runtime_version TEXT NOT NULL DEFAULT '', + workspace_id UUID REFERENCES workspace(id), + approved_by UUID REFERENCES "user"(id), + status TEXT NOT NULL DEFAULT 'pending', + approved_at TIMESTAMPTZ, + claimed_at TIMESTAMPTZ, + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_daemon_pairing_session_token ON daemon_pairing_session(token); +CREATE INDEX IF NOT EXISTS idx_daemon_pairing_session_status ON daemon_pairing_session(status, expires_at); diff --git a/server/migrations/029_drop_daemon_pairing.up.sql b/server/migrations/029_drop_daemon_pairing.up.sql new file mode 100644 index 00000000..25e28eb7 --- /dev/null +++ b/server/migrations/029_drop_daemon_pairing.up.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS daemon_pairing_session;