@spacefast/common 0.0.8 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/contracts/activity.d.ts +1 -1
  2. package/dist/contracts/activity.js +11 -6
  3. package/dist/contracts/annotations.d.ts +2 -0
  4. package/dist/contracts/annotations.js +2 -1
  5. package/dist/contracts/api-keys.d.ts +4 -0
  6. package/dist/contracts/api-keys.js +4 -0
  7. package/dist/contracts/archives.d.ts +2 -8
  8. package/dist/contracts/archives.js +3 -8
  9. package/dist/contracts/builds.d.ts +1 -0
  10. package/dist/contracts/builds.js +5 -0
  11. package/dist/contracts/comments.d.ts +309 -0
  12. package/dist/contracts/comments.js +200 -0
  13. package/dist/contracts/device-auth.d.ts +221 -1
  14. package/dist/contracts/device-auth.js +106 -4
  15. package/dist/contracts/device-team-scope.d.ts +10 -0
  16. package/dist/contracts/device-team-scope.js +17 -0
  17. package/dist/contracts/error-code-meta.d.ts +52 -0
  18. package/dist/contracts/error-code-meta.js +13 -0
  19. package/dist/contracts/error-codes.d.ts +1 -1
  20. package/dist/contracts/error-codes.js +13 -0
  21. package/dist/contracts/features.d.ts +37 -38
  22. package/dist/contracts/features.js +51 -59
  23. package/dist/contracts/generated-feature-launch-entries.d.ts +2 -2
  24. package/dist/contracts/generated-feature-launch-entries.js +6 -6
  25. package/dist/contracts/ids.d.ts +1 -1
  26. package/dist/contracts/ids.js +1 -1
  27. package/dist/contracts/oauth-resources.d.ts +7 -0
  28. package/dist/contracts/oauth-resources.js +10 -2
  29. package/dist/contracts/operations.d.ts +1 -3
  30. package/dist/contracts/operations.js +5 -1
  31. package/dist/contracts/pages.d.ts +70 -0
  32. package/dist/contracts/pages.js +89 -0
  33. package/dist/contracts/publishes.d.ts +5 -0
  34. package/dist/contracts/publishes.js +25 -8
  35. package/dist/contracts/resources.d.ts +14 -62
  36. package/dist/contracts/runtime-api.d.ts +13 -15
  37. package/dist/contracts/runtime-api.js +12 -70
  38. package/dist/contracts/space-config.d.ts +32 -90
  39. package/dist/contracts/space-config.js +49 -54
  40. package/dist/contracts/spaces.d.ts +47 -187
  41. package/dist/contracts/spaces.js +19 -3
  42. package/dist/contracts/superadmin-spaces.d.ts +14 -62
  43. package/dist/contracts/superadmin-teams.d.ts +79 -0
  44. package/dist/contracts/superadmin-teams.js +36 -0
  45. package/dist/contracts/superadmin.d.ts +2 -0
  46. package/dist/contracts/superadmin.js +1 -0
  47. package/dist/contracts/teams.d.ts +1 -0
  48. package/dist/contracts/teams.js +8 -0
  49. package/dist/contracts/theme-json.js +4 -5
  50. package/dist/dashboard-paths/index.d.ts +31 -0
  51. package/dist/dashboard-paths/index.js +80 -0
  52. package/dist/docs/agent-handoff-document.d.ts +15 -0
  53. package/dist/docs/agent-handoff-document.js +179 -0
  54. package/dist/docs/agent-setup.d.ts +12 -1
  55. package/dist/docs/agent-setup.js +30 -16
  56. package/dist/docs/catalog.d.ts +2 -2
  57. package/dist/docs/catalog.js +2 -2
  58. package/dist/docs/error-docs.js +52 -0
  59. package/dist/utils/asset-fingerprint.d.ts +53 -0
  60. package/dist/utils/asset-fingerprint.js +574 -0
  61. package/dist/utils/credential-policy.d.ts +5 -0
  62. package/dist/utils/credential-policy.js +31 -8
  63. package/dist/utils/pages.d.ts +53 -0
  64. package/dist/utils/pages.js +274 -0
  65. package/dist/utils/query-keys.d.ts +4 -3
  66. package/dist/utils/query-keys.js +4 -3
  67. package/dist/utils/space-config.d.ts +2 -2
  68. package/dist/utils/space-config.js +1 -1
  69. package/dist/utils/wpcom-auth-redirects.d.ts +0 -4
  70. package/dist/utils/wpcom-auth-redirects.js +0 -6
  71. package/dist/vocabulary.d.ts +2 -2
  72. package/package.json +6 -1
  73. package/dist/contracts/space-theme.d.ts +0 -25
  74. package/dist/contracts/space-theme.js +0 -49
  75. package/dist/utils/gate-theme.d.ts +0 -61
  76. package/dist/utils/gate-theme.js +0 -217
  77. package/dist/utils/space-theme.d.ts +0 -18
  78. package/dist/utils/space-theme.js +0 -98
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Dashboard route first-segments that cannot be used as team slugs.
3
+ *
4
+ * Append-only: add a new top-level static dashboard route's first segment here
5
+ * before its route module lands. A repository structural test enforces this.
6
+ */
7
+ export declare const TEAM_SLUG_ROUTE_RESERVATIONS: readonly ["accept-beta-invite", "accept-invitation", "account", "agent", "claim", "device", "drop", "new-team", "oauth", "sign-in", "start", "terminal", "two-factor"];
8
+ /**
9
+ * Dashboard route first-segments that cannot be used as space slugs.
10
+ *
11
+ * Append-only: add a new static route directly below a team's first segment
12
+ * here before its route module lands. A repository structural test enforces
13
+ * this.
14
+ */
15
+ export declare const SPACE_SLUG_ROUTE_RESERVATIONS: readonly ["billing", "connect", "domains", "drop", "members", "new", "settings"];
16
+ export type TeamSlugRouteReservation = (typeof TEAM_SLUG_ROUTE_RESERVATIONS)[number];
17
+ export type SpaceSlugRouteReservation = (typeof SPACE_SLUG_ROUTE_RESERVATIONS)[number];
18
+ export declare const TEAM_SLUG_ROUTE_RESERVATION_SET: ReadonlySet<string>;
19
+ export declare const SPACE_SLUG_ROUTE_RESERVATION_SET: ReadonlySet<string>;
20
+ export type SpaceSettingsDestination = "access" | "advanced" | "appearance" | "builds" | "integrations" | "integrations/add" | "source-builds";
21
+ export type TeamSettingsDestination = "access" | "audit" | "developer" | "developer/agents" | "developer/environment-variables" | "developer/handoffs" | "developer/service-tokens" | "developer/webhooks" | "integrations";
22
+ export declare function teamPath(teamSlug: string): string;
23
+ export declare function spacePath(teamSlug: string, spaceSlug: string): string;
24
+ export declare function versionDetailPath(teamSlug: string, spaceSlug: string, versionRef: string): string;
25
+ export declare function buildDetailPath(teamSlug: string, spaceSlug: string, buildId: string): string;
26
+ export declare function spaceDomainsPath(teamSlug: string, spaceSlug: string): string;
27
+ export declare function spaceSettingsPath(teamSlug: string, spaceSlug: string, destination?: SpaceSettingsDestination): string;
28
+ export declare function teamDomainsPath(teamSlug: string): string;
29
+ export declare function teamBillingPath(teamSlug: string): string;
30
+ export declare function teamDomainDetailPath(teamSlug: string, domainRef: string): string;
31
+ export declare function teamSettingsPath(teamSlug: string, destination?: TeamSettingsDestination): string;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Dashboard route first-segments that cannot be used as team slugs.
3
+ *
4
+ * Append-only: add a new top-level static dashboard route's first segment here
5
+ * before its route module lands. A repository structural test enforces this.
6
+ */
7
+ export const TEAM_SLUG_ROUTE_RESERVATIONS = [
8
+ "accept-beta-invite",
9
+ "accept-invitation",
10
+ "account",
11
+ // Not a TanStack route module: /agent* is the edge-served handoff-document
12
+ // surface (apps/my/public/_redirects + vite middleware, #417). Those rewrite
13
+ // rules deliberately swallow every /agent-prefixed path, so a team named
14
+ // "agent" could never reach its canonical dashboard. Reserved per the spec's
15
+ // own rule that a static first segment is reserved before the surface owning
16
+ // it ships (#417 predates this set; this closes that drift).
17
+ "agent",
18
+ "claim",
19
+ "device",
20
+ "drop",
21
+ "new-team",
22
+ "oauth",
23
+ "sign-in",
24
+ "start",
25
+ "terminal",
26
+ "two-factor",
27
+ ];
28
+ /**
29
+ * Dashboard route first-segments that cannot be used as space slugs.
30
+ *
31
+ * Append-only: add a new static route directly below a team's first segment
32
+ * here before its route module lands. A repository structural test enforces
33
+ * this.
34
+ */
35
+ export const SPACE_SLUG_ROUTE_RESERVATIONS = [
36
+ "billing",
37
+ "connect",
38
+ "domains",
39
+ "drop",
40
+ "members",
41
+ "new",
42
+ "settings",
43
+ ];
44
+ export const TEAM_SLUG_ROUTE_RESERVATION_SET = new Set(TEAM_SLUG_ROUTE_RESERVATIONS);
45
+ export const SPACE_SLUG_ROUTE_RESERVATION_SET = new Set(SPACE_SLUG_ROUTE_RESERVATIONS);
46
+ function dynamicSegment(value) {
47
+ return encodeURIComponent(value);
48
+ }
49
+ export function teamPath(teamSlug) {
50
+ return `/${dynamicSegment(teamSlug)}`;
51
+ }
52
+ export function spacePath(teamSlug, spaceSlug) {
53
+ return `${teamPath(teamSlug)}/${dynamicSegment(spaceSlug)}`;
54
+ }
55
+ export function versionDetailPath(teamSlug, spaceSlug, versionRef) {
56
+ return `${spacePath(teamSlug, spaceSlug)}/versions/${dynamicSegment(versionRef)}`;
57
+ }
58
+ export function buildDetailPath(teamSlug, spaceSlug, buildId) {
59
+ return `${spacePath(teamSlug, spaceSlug)}/builds/${dynamicSegment(buildId)}`;
60
+ }
61
+ export function spaceDomainsPath(teamSlug, spaceSlug) {
62
+ return `${spacePath(teamSlug, spaceSlug)}/domains`;
63
+ }
64
+ export function spaceSettingsPath(teamSlug, spaceSlug, destination) {
65
+ const root = `${spacePath(teamSlug, spaceSlug)}/settings`;
66
+ return destination ? `${root}/${destination}` : root;
67
+ }
68
+ export function teamDomainsPath(teamSlug) {
69
+ return `${teamPath(teamSlug)}/domains`;
70
+ }
71
+ export function teamBillingPath(teamSlug) {
72
+ return `${teamPath(teamSlug)}/billing`;
73
+ }
74
+ export function teamDomainDetailPath(teamSlug, domainRef) {
75
+ return `${teamDomainsPath(teamSlug)}/${dynamicSegment(domainRef)}`;
76
+ }
77
+ export function teamSettingsPath(teamSlug, destination) {
78
+ const root = `${teamPath(teamSlug)}/settings`;
79
+ return destination ? `${root}/${destination}` : root;
80
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The exact markdown body served to agents at `/agent/<documentId>`.
3
+ * Deterministic and ID-free on purpose — see the module comment.
4
+ */
5
+ export declare function renderAgentHandoffDocumentMarkdown(input: {
6
+ mcpEndpoint: string;
7
+ }): string;
8
+ /**
9
+ * The browser variant served at `/agent/<documentId>`: the full instructions in
10
+ * the initial response body, zero scripts (nothing may ever read or echo the
11
+ * URL fragment), same bytes for every ID.
12
+ */
13
+ export declare function renderAgentHandoffDocumentHtml(input: {
14
+ mcpEndpoint: string;
15
+ }): string;
@@ -0,0 +1,179 @@
1
+ /*
2
+ * The public setup document behind agent handoff links.
3
+ *
4
+ * Handoff links are `https://<dashboard-origin>/agent/<documentId>#<secret>` and
5
+ * the minted prompt tells the agent to fetch that URL. Fragments never reach the
6
+ * server, so this fetch is UNAUTHENTICATED BY DESIGN — which means the response
7
+ * must never become an existence oracle. The contract here is
8
+ * indistinguishability: every well-formed document ID serves the exact same
9
+ * bytes, with the same status and the same timing, whether or not a handoff with
10
+ * that ID exists. That is why these renderers take no document ID and hit no
11
+ * store: the document is one static artifact, target-agnostic, published behind
12
+ * a `/agent/*` rewrite on the dashboard space. Per-target polish deliberately
13
+ * loses to indistinguishability — instead of looking up the handoff's client
14
+ * target (which would need a public by-ID lookup, an oracle), the document
15
+ * carries a named section for every connect target and the minted prompt's
16
+ * "follow the <client> setup instructions" resolves to the right section.
17
+ *
18
+ * Lifecycle safety stays where the secret authenticates: an expired, used, or
19
+ * revoked link fails at the app-private exchange, never at fetch time (see
20
+ * ./agent-setup.ts, `AGENT_HANDOFF_DECLINE_REASONS`).
21
+ */
22
+ import { buildConnectTargets } from "../agents/connect-targets.js";
23
+ import { BRAND } from "../brand.js";
24
+ import { AGENT_HANDOFF_BROWSER_APPROVAL_GUIDANCE, AGENT_HANDOFF_CLI_APPROVAL_GUIDANCE, AGENT_HANDOFF_CLI_TARGET_IDS, AGENT_HANDOFF_FETCH_SAFETY, AGENT_HANDOFF_FRAGMENT_HYGIENE, AGENT_HANDOFF_PREAMBLE, AGENT_HANDOFF_PUBLISH_PROSE, agentHandoffConnectSections, agentSetupTextUrl, } from "./agent-setup.js";
25
+ const DOCUMENT_TITLE = `Set up ${BRAND.productName} from an agent handoff`;
26
+ const DOCUMENT_SCOPE = `This is the public setup document for ${BRAND.productName} agent handoff links (\`/agent/<id>\` on the dashboard origin). Every link serves this same document — fetching it never uses a handoff and reveals nothing about one.`;
27
+ const CONNECT_INTRO = "Set up the client you are running in. Each section stands on its own.";
28
+ /**
29
+ * The exact markdown body served to agents at `/agent/<documentId>`.
30
+ * Deterministic and ID-free on purpose — see the module comment.
31
+ */
32
+ export function renderAgentHandoffDocumentMarkdown(input) {
33
+ const targets = buildConnectTargets({ mcpEndpoint: input.mcpEndpoint });
34
+ const cliTargetNames = targets
35
+ .filter((target) => AGENT_HANDOFF_CLI_TARGET_IDS.includes(target.id))
36
+ .map((target) => target.name);
37
+ const connectSections = targets.flatMap((target) => {
38
+ const sections = agentHandoffConnectSections(target);
39
+ sections.unshift(`### ${target.name}`);
40
+ return sections;
41
+ });
42
+ const sections = [
43
+ `# ${DOCUMENT_TITLE}`,
44
+ DOCUMENT_SCOPE,
45
+ AGENT_HANDOFF_PREAMBLE,
46
+ ["## Connect", CONNECT_INTRO, ...connectSections].join("\n\n"),
47
+ [
48
+ "## Continue the approved handoff",
49
+ AGENT_HANDOFF_FETCH_SAFETY,
50
+ `From ${cliTargetNames.join(" or ")} (or any client with a shell): ${AGENT_HANDOFF_CLI_APPROVAL_GUIDANCE}`,
51
+ `From every other client: ${AGENT_HANDOFF_BROWSER_APPROVAL_GUIDANCE}`,
52
+ AGENT_HANDOFF_FRAGMENT_HYGIENE,
53
+ ].join("\n\n"),
54
+ ["## Publish", AGENT_HANDOFF_PUBLISH_PROSE].join("\n\n"),
55
+ `General agent instructions: ${agentSetupTextUrl}`,
56
+ ];
57
+ return `${sections.join("\n\n")}\n`;
58
+ }
59
+ function escapeHtml(value) {
60
+ return value
61
+ .replaceAll("&", "&amp;")
62
+ .replaceAll("<", "&lt;")
63
+ .replaceAll(">", "&gt;")
64
+ .replaceAll('"', "&quot;");
65
+ }
66
+ function linkify(escaped) {
67
+ return escaped.replace(/https?:\/\/[^\s<)]+/g, (url) => `<a href="${url}" rel="noopener">${url}</a>`);
68
+ }
69
+ /** Inline markdown: backtick code spans stay literal; bare URLs become links. */
70
+ function renderInline(text) {
71
+ return text
72
+ .split("`")
73
+ .map((part, index) => index % 2 === 1 ? `<code>${escapeHtml(part)}</code>` : linkify(escapeHtml(part)))
74
+ .join("");
75
+ }
76
+ /**
77
+ * Renders the document's own markdown grammar (headings, paragraphs, fenced
78
+ * code, inline code, bare URLs) to HTML. Not a general markdown engine: the
79
+ * input is our generated document above, so anything else stays literal text.
80
+ */
81
+ function markdownToHtmlBody(markdown) {
82
+ const html = [];
83
+ let paragraph = [];
84
+ let codeBlock = null;
85
+ const flushParagraph = () => {
86
+ if (paragraph.length > 0) {
87
+ html.push(`<p>${paragraph.map(renderInline).join("\n")}</p>`);
88
+ paragraph = [];
89
+ }
90
+ };
91
+ for (const line of markdown.split("\n")) {
92
+ if (codeBlock !== null) {
93
+ if (line.startsWith("```")) {
94
+ html.push(`<pre><code>${escapeHtml(codeBlock.join("\n"))}</code></pre>`);
95
+ codeBlock = null;
96
+ }
97
+ else {
98
+ codeBlock.push(line);
99
+ }
100
+ continue;
101
+ }
102
+ if (line.startsWith("```")) {
103
+ flushParagraph();
104
+ codeBlock = [];
105
+ continue;
106
+ }
107
+ const heading = /^(#{1,3}) (.*)$/.exec(line);
108
+ if (heading?.[1] && heading[2] !== undefined) {
109
+ flushParagraph();
110
+ const level = heading[1].length;
111
+ html.push(`<h${level}>${renderInline(heading[2])}</h${level}>`);
112
+ continue;
113
+ }
114
+ if (line.trim() === "") {
115
+ flushParagraph();
116
+ continue;
117
+ }
118
+ paragraph.push(line);
119
+ }
120
+ flushParagraph();
121
+ return html.join("\n");
122
+ }
123
+ const DOCUMENT_STYLES = `
124
+ :root { color-scheme: light dark; }
125
+ body {
126
+ margin: 0 auto;
127
+ padding: 3rem 1.25rem 5rem;
128
+ max-width: 42rem;
129
+ font: 16px/1.6 ui-sans-serif, system-ui, sans-serif;
130
+ background: #ffffff;
131
+ color: #1a1a1e;
132
+ }
133
+ h1 { font-size: 1.6rem; line-height: 1.25; margin: 0 0 1rem; }
134
+ h2 { font-size: 1.2rem; margin: 2.5rem 0 0.75rem; }
135
+ h3 { font-size: 1rem; margin: 2rem 0 0.5rem; }
136
+ p { margin: 0.75rem 0; }
137
+ a { color: inherit; text-underline-offset: 3px; word-break: break-word; }
138
+ code {
139
+ font: 0.875em ui-monospace, monospace;
140
+ background: rgba(125, 125, 135, 0.14);
141
+ border-radius: 4px;
142
+ padding: 0.1em 0.35em;
143
+ }
144
+ pre {
145
+ background: rgba(125, 125, 135, 0.1);
146
+ border: 1px solid rgba(125, 125, 135, 0.25);
147
+ border-radius: 8px;
148
+ padding: 0.875rem 1rem;
149
+ overflow-x: auto;
150
+ }
151
+ pre code { background: none; padding: 0; font-size: 0.8125rem; }
152
+ @media (prefers-color-scheme: dark) {
153
+ body { background: #0e0e10; color: #ececf1; }
154
+ }
155
+ `.trim();
156
+ /**
157
+ * The browser variant served at `/agent/<documentId>`: the full instructions in
158
+ * the initial response body, zero scripts (nothing may ever read or echo the
159
+ * URL fragment), same bytes for every ID.
160
+ */
161
+ export function renderAgentHandoffDocumentHtml(input) {
162
+ const markdown = renderAgentHandoffDocumentMarkdown(input);
163
+ return [
164
+ "<!doctype html>",
165
+ '<html lang="en">',
166
+ "<head>",
167
+ '<meta charset="utf-8">',
168
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
169
+ '<meta name="robots" content="noindex">',
170
+ `<title>${escapeHtml(DOCUMENT_TITLE)}</title>`,
171
+ `<style>${DOCUMENT_STYLES}</style>`,
172
+ "</head>",
173
+ "<body>",
174
+ markdownToHtmlBody(markdown),
175
+ "</body>",
176
+ "</html>",
177
+ "",
178
+ ].join("\n");
179
+ }
@@ -1,4 +1,4 @@
1
- import { type ConnectTargetId } from "../agents/connect-targets.js";
1
+ import { type ConnectTarget, type ConnectTargetId } from "../agents/connect-targets.js";
2
2
  export declare const agentSetupUrl: string;
3
3
  export declare const agentSetupTextUrl: string;
4
4
  export declare const agentSetupHostLabel: string;
@@ -46,6 +46,17 @@ export declare function parseAgentHandoffUrl(value: string, expectedOrigin: stri
46
46
  documentUrl: string;
47
47
  secret: string;
48
48
  }>;
49
+ /** Connect targets whose handoff continues through the CLI stdin exchange. */
50
+ export declare const AGENT_HANDOFF_CLI_TARGET_IDS: readonly ConnectTargetId[];
51
+ export declare const AGENT_HANDOFF_PREAMBLE = "Talk to the user before installing tools or publishing. Confirm the file or folder they want to make public.";
52
+ /** The fetch-safety contract: reading the document never consumes or leaks a handoff. */
53
+ export declare const AGENT_HANDOFF_FETCH_SAFETY = "Keep the original handoff link private. Fetching this document did not use the link or send its fragment secret; the secret is spent only by one successful exchange. An expired, used, or revoked link fails safely, so ask the user for a fresh one from the dashboard.";
54
+ export declare const AGENT_HANDOFF_FRAGMENT_HYGIENE = "Keep its fragment out of commands, request URLs, logs, and replies.";
55
+ export declare const AGENT_HANDOFF_CLI_APPROVAL_GUIDANCE: string;
56
+ export declare const AGENT_HANDOFF_BROWSER_APPROVAL_GUIDANCE = "When the client opens browser approval, ask the user to approve there before publishing.";
57
+ export declare const AGENT_HANDOFF_PUBLISH_PROSE = "After approval, confirm the selected Space and permissions, then publish only the files the user approved. Share the live URL and immutable version URL from the receipt.";
58
+ /** One target's Connect prose: blurb, copyable setup blocks, deeplink/download lines. */
59
+ export declare function agentHandoffConnectSections(target: ConnectTarget): string[];
49
60
  /** Builds every copy-ready handoff artifact from one validated URL and target catalog row. */
50
61
  export declare function buildAgentHandoff(input: {
51
62
  expectedOrigin: string;
@@ -98,32 +98,46 @@ function validateMcpEndpoint(value) {
98
98
  }
99
99
  return url.href.replace(/\/$/, "");
100
100
  }
101
- function renderAgentHandoffInstructions(documentUrl, target) {
101
+ /*
102
+ * Shared handoff prose. The per-handoff "Copy Markdown" artifact (below) and the
103
+ * public /agent/<id> document (./agent-handoff-document.ts) must tell agents the
104
+ * same contract in the same words, so the sentences live here exactly once.
105
+ */
106
+ /** Connect targets whose handoff continues through the CLI stdin exchange. */
107
+ export const AGENT_HANDOFF_CLI_TARGET_IDS = ["claude-code", "codex"];
108
+ export const AGENT_HANDOFF_PREAMBLE = "Talk to the user before installing tools or publishing. Confirm the file or folder they want to make public.";
109
+ /** The fetch-safety contract: reading the document never consumes or leaks a handoff. */
110
+ export const AGENT_HANDOFF_FETCH_SAFETY = "Keep the original handoff link private. Fetching this document did not use the link or send its fragment secret; the secret is spent only by one successful exchange. An expired, used, or revoked link fails safely, so ask the user for a fresh one from the dashboard.";
111
+ export const AGENT_HANDOFF_FRAGMENT_HYGIENE = "Keep its fragment out of commands, request URLs, logs, and replies.";
112
+ export const AGENT_HANDOFF_CLI_APPROVAL_GUIDANCE = `Run \`sf setup agent\` to initiate the handoff exchange; if \`sf\` is missing, install it first (macOS/Linux: \`${spacefastInstallCurlCommand}\`; Windows PowerShell: \`${spacefastInstallPowerShellCommand}\`). When it asks for the handoff link on stdin, paste the original link into that prompt; never pass the link as an argv value. Ask the user to approve in the browser, then wait for the CLI's approval result before publishing.`;
113
+ export const AGENT_HANDOFF_BROWSER_APPROVAL_GUIDANCE = "When the client opens browser approval, ask the user to approve there before publishing.";
114
+ export const AGENT_HANDOFF_PUBLISH_PROSE = "After approval, confirm the selected Space and permissions, then publish only the files the user approved. Share the live URL and immutable version URL from the receipt.";
115
+ /** One target's Connect prose: blurb, copyable setup blocks, deeplink/download lines. */
116
+ export function agentHandoffConnectSections(target) {
102
117
  const setupSections = [target.primary, target.config].flatMap((setup) => setup ? [[setup.label, `\`\`\`${setup.language}`, setup.value, "```"].join("\n")] : []);
103
- const connectSections = [
118
+ return [
104
119
  target.blurb,
105
120
  ...setupSections,
106
- target.deeplink ? `${target.deeplink.label}: ${target.deeplink.href}` : null,
107
- target.download ? `${target.download.label}: ${target.download.href}` : null,
108
- ].filter((section) => section !== null);
109
- const approvalGuidance = target.id === "claude-code" || target.id === "codex"
110
- ? `Run \`sf setup agent\` to initiate the handoff exchange; if \`sf\` is missing, install it first (macOS/Linux: \`${spacefastInstallCurlCommand}\`; Windows PowerShell: \`${spacefastInstallPowerShellCommand}\`). When it asks for the handoff link on stdin, paste the original link into that prompt; never pass the link as an argv value. Ask the user to approve in the browser, then wait for the CLI's approval result before publishing.`
111
- : "When the client opens browser approval, ask the user to approve there before publishing.";
121
+ ...(target.deeplink ? [`${target.deeplink.label}: ${target.deeplink.href}`] : []),
122
+ ...(target.download ? [`${target.download.label}: ${target.download.href}`] : []),
123
+ ];
124
+ }
125
+ function renderAgentHandoffInstructions(documentUrl, target) {
126
+ const approvalGuidance = AGENT_HANDOFF_CLI_TARGET_IDS.includes(target.id)
127
+ ? AGENT_HANDOFF_CLI_APPROVAL_GUIDANCE
128
+ : AGENT_HANDOFF_BROWSER_APPROVAL_GUIDANCE;
112
129
  const sections = [
113
130
  `# Set up ${BRAND.productName} with ${target.name}`,
114
131
  `This is the public setup document for ${documentUrl}.`,
115
- "Talk to the user before installing tools or publishing. Confirm the file or folder they want to make public.",
116
- ["## Connect", ...connectSections].join("\n\n"),
132
+ AGENT_HANDOFF_PREAMBLE,
133
+ ["## Connect", ...agentHandoffConnectSections(target)].join("\n\n"),
117
134
  [
118
135
  "## Continue the approved handoff",
119
- "Keep the original handoff link private. Fetching this document did not use the link or send its fragment secret; the secret is spent only by one successful exchange. An expired, used, or revoked link fails safely, so ask the user for a fresh one from the dashboard.",
136
+ AGENT_HANDOFF_FETCH_SAFETY,
120
137
  approvalGuidance,
121
- "Keep its fragment out of commands, request URLs, logs, and replies.",
122
- ].join("\n\n"),
123
- [
124
- "## Publish",
125
- "After approval, confirm the selected Space and permissions, then publish only the files the user approved. Share the live URL and immutable version URL from the receipt.",
138
+ AGENT_HANDOFF_FRAGMENT_HYGIENE,
126
139
  ].join("\n\n"),
140
+ ["## Publish", AGENT_HANDOFF_PUBLISH_PROSE].join("\n\n"),
127
141
  `General agent instructions: ${agentSetupTextUrl}`,
128
142
  ];
129
143
  return `${sections.join("\n\n")}\n`;
@@ -472,10 +472,10 @@ export declare const DOCS_CATALOG: readonly [{
472
472
  readonly title: "sf api";
473
473
  readonly kind: "cli";
474
474
  readonly tier: "full";
475
- readonly summary: "Make an authenticated raw call to the platform API.";
475
+ readonly summary: "Call the core public API through a safe escape hatch.";
476
476
  readonly keywords: readonly ["api", "sf api", "http", "raw", "request", "curl", "endpoint"];
477
477
  readonly url: "https://spacefast.com/docs/cli";
478
- readonly body: "`sf api <METHOD> <PATH>` sends an authenticated request against the platform API and prints the `{ data }` / `{ error }` envelope handy for endpoints without a dedicated command.";
478
+ readonly body: "`sf api <METHOD> /v1/...` calls only the core public contract with resolved CLI credentials. JSON envelopes print verbatim; cursor lists support `--paginate`, and non-JSON bodies require `--output` or `--raw-stdout`.";
479
479
  }, {
480
480
  readonly slug: "cli/mcp";
481
481
  readonly title: "sf mcp";
@@ -537,10 +537,10 @@ const HAND_WRITTEN_DOCS_CATALOG = [
537
537
  title: "sf api",
538
538
  kind: "cli",
539
539
  tier: "full",
540
- summary: "Make an authenticated raw call to the platform API.",
540
+ summary: "Call the core public API through a safe escape hatch.",
541
541
  keywords: ["api", "sf api", "http", "raw", "request", "curl", "endpoint"],
542
542
  url: `${DOCS_BASE_URL}/cli`,
543
- body: "`sf api <METHOD> <PATH>` sends an authenticated request against the platform API and prints the `{ data }` / `{ error }` envelope handy for endpoints without a dedicated command.",
543
+ body: "`sf api <METHOD> /v1/...` calls only the core public contract with resolved CLI credentials. JSON envelopes print verbatim; cursor lists support `--paginate`, and non-JSON bodies require `--output` or `--raw-stdout`.",
544
544
  },
545
545
  {
546
546
  slug: "cli/mcp",
@@ -28,6 +28,14 @@ export const ERROR_DOCS = {
28
28
  summary: "The authorization request was denied by the user.",
29
29
  fix: "Restart the login flow and approve the request to continue.",
30
30
  },
31
+ agent_handoff_declined: {
32
+ summary: "The app-private agent handoff cannot be redeemed.",
33
+ fix: "Ask the user to create a fresh handoff for this Space and client, then redeem that new link once.",
34
+ },
35
+ agent_handoff_limit_reached: {
36
+ summary: "This user already has the maximum number of pending handoffs for the Space.",
37
+ fix: "Redeem or revoke an existing pending handoff for the Space before creating another.",
38
+ },
31
39
  ambiguous_space_slug: {
32
40
  summary: "The slug matches spaces in more than one of your teams, so it cannot be resolved.",
33
41
  fix: "Pass a teamRef alongside the slug, or reference the space by its spc_ id.",
@@ -160,6 +168,10 @@ export const ERROR_DOCS = {
160
168
  summary: "This CLI version is too old to talk to the API safely.",
161
169
  fix: "Upgrade the CLI to the latest release and retry.",
162
170
  },
171
+ comment_screenshot_rate_limited: {
172
+ summary: "Too many anonymous comment screenshot uploads were attempted.",
173
+ fix: "Wait for the one-minute window to reset, then retry the screenshot upload.",
174
+ },
163
175
  config_file_too_large: {
164
176
  summary: "The sf.jsonc configuration file exceeds the maximum size.",
165
177
  fix: "Keep the configuration file under the documented size limit (256 KB).",
@@ -536,6 +548,10 @@ export const ERROR_DOCS = {
536
548
  summary: "The authorization grant is invalid or was already used.",
537
549
  fix: "Restart the login flow to obtain a new grant.",
538
550
  },
551
+ invalid_idempotency_key: {
552
+ summary: "The Idempotency-Key header is missing, empty, or too long.",
553
+ fix: "Send a non-empty Idempotency-Key header of at most 512 characters.",
554
+ },
539
555
  invalid_publish_archive: {
540
556
  summary: "The uploaded archive could not be read as a valid zip.",
541
557
  fix: "Re-create the archive as a standard zip and upload again.",
@@ -664,6 +680,10 @@ export const ERROR_DOCS = {
664
680
  summary: "The upload manifest declares more files than the allowed ceiling.",
665
681
  fix: "Reduce the file count or split content across multiple spaces.",
666
682
  },
683
+ mcp_acted_for_team_deleted: {
684
+ summary: "This MCP approval or execution run acted only for a team that has since been deleted, so it can no longer be approved, denied, or canceled.",
685
+ fix: "Nothing to do — the record is read-only history. Restore the deleted team within its recovery window if you still need to act on it.",
686
+ },
667
687
  member_already_exists: {
668
688
  summary: "This user is already a member of the team.",
669
689
  fix: "Update the existing member's role instead of inviting again.",
@@ -780,6 +800,10 @@ export const ERROR_DOCS = {
780
800
  summary: "Someone published new space settings after the base you loaded, so saving would overwrite their change.",
781
801
  fix: "Reload the latest settings, review the difference, and save again; pass force only to overwrite deliberately.",
782
802
  },
803
+ publish_bytes_missing: {
804
+ summary: "The publish has no complete staged content to materialize.",
805
+ fix: "Finish uploading the staged content, or start a new publish.",
806
+ },
783
807
  publish_config_unsupported: {
784
808
  summary: "The publish carries configuration this endpoint does not support.",
785
809
  fix: "Move the configuration into sf.jsonc or the space settings.",
@@ -792,10 +816,34 @@ export const ERROR_DOCS = {
792
816
  summary: "A declared file was never uploaded, so the publish cannot finalize.",
793
817
  fix: "Upload every declared file, or resume the upload session to see what is missing.",
794
818
  },
819
+ publish_hash_mismatch: {
820
+ summary: "Publish content does not match its declared sha256.",
821
+ fix: "Re-send the file with matching bytes, or correct the declared hash.",
822
+ },
823
+ publish_inline_limit_exceeded: {
824
+ summary: "The inline publish exceeds the file-count or total-size limit.",
825
+ fix: "Trim the inline payload, or switch to an upload-based publish for larger content.",
826
+ },
827
+ publish_not_cancelable: {
828
+ summary: "The publish has progressed past the point where it can be canceled.",
829
+ fix: "Wait for the publish to finish, then roll back by promoting a previous version if needed.",
830
+ },
831
+ publish_path_collision: {
832
+ summary: "Two entries in the publish payload normalize to the same path.",
833
+ fix: "Remove the duplicate; each path may appear once across files and deletes.",
834
+ },
835
+ publish_path_invalid: {
836
+ summary: "A publish path is not a relative, normalized POSIX path.",
837
+ fix: "Use forward-slash relative paths without traversal segments or control characters.",
838
+ },
795
839
  publish_setup_failed: {
796
840
  summary: "Provisioning for this publish failed.",
797
841
  fix: "Retry the publish; if it persists, contact support with the requestId.",
798
842
  },
843
+ publish_snapshot_empty: {
844
+ summary: "An empty snapshot was sent without an explicit base version.",
845
+ fix: "Include at least one file, or declare the base version to intentionally publish an empty site.",
846
+ },
799
847
  publish_upload_failed: {
800
848
  summary: "Uploading the publish content failed.",
801
849
  fix: "Retry the publish; partial uploads are resumable through the upload session.",
@@ -972,6 +1020,10 @@ export const ERROR_DOCS = {
972
1020
  summary: "The space expired and is no longer served.",
973
1021
  fix: "If it is within the recovery window, claim or restore it; otherwise publish again.",
974
1022
  },
1023
+ space_has_active_publish: {
1024
+ summary: "The space has a publish in progress, which blocks this operation.",
1025
+ fix: "Wait for the publish to finish or cancel it, then retry.",
1026
+ },
975
1027
  space_has_active_version: {
976
1028
  summary: "The space still has an active version, which blocks this operation.",
977
1029
  fix: "Delete or supersede the active version first.",
@@ -0,0 +1,53 @@
1
+ export declare const FINGERPRINT_IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
2
+ export declare const FINGERPRINT_HASH_LENGTH = 8;
3
+ export type FingerprintCandidateFile = {
4
+ /** Canonical manifest path (no leading slash). */
5
+ path: string;
6
+ /** Full lowercase-hex sha256 of the file's bytes. */
7
+ sha256?: string | undefined;
8
+ /** UTF-8 text for html/css files that may need reference rewriting. */
9
+ text?: string | undefined;
10
+ };
11
+ export type FingerprintSkipReason = "css_cycle" | "alias_collision" | "user_cache_control" | "budget";
12
+ export type FingerprintPlan = {
13
+ /**
14
+ * Alias manifest entries to add, sorted by sourcePath; content is identical
15
+ * to sourcePath's. This is also the "what got fingerprinted" relation —
16
+ * one array, `path` = the hashed alias, `sourcePath` = the original.
17
+ */
18
+ aliases: Array<{
19
+ path: string;
20
+ sourcePath: string;
21
+ }>;
22
+ /** html/css files whose published content changes (both paths, one blob). */
23
+ rewrites: Array<{
24
+ path: string;
25
+ text: string;
26
+ sha256: string;
27
+ }>;
28
+ /** Eligible-looking files the pass left on the default lane, with reasons. */
29
+ skipped: Array<{
30
+ path: string;
31
+ reason: FingerprintSkipReason;
32
+ }>;
33
+ };
34
+ export declare function planAssetFingerprints(input: {
35
+ files: readonly FingerprintCandidateFile[];
36
+ /** sha256 as lowercase hex over UTF-8 bytes of `text`. */
37
+ hashHex: (text: string) => string;
38
+ /**
39
+ * True when a user `_headers` Cache-Control rule already covers this URL
40
+ * path ("/"-prefixed). Files whose original OR probe-alias path is covered
41
+ * are never fingerprinted — user rules always win.
42
+ */
43
+ pathCoveredByUserCacheControl?: ((urlPath: string) => boolean) | undefined;
44
+ /** Extra per-path exclusion (e.g. header-rule budget trimming). */
45
+ excludePath?: ((path: string) => boolean) | undefined;
46
+ }): FingerprintPlan;
47
+ /**
48
+ * Merges the generated immutable pins into user `_headers` content. The user
49
+ * content is preserved byte-for-byte; generated exact-path rules are appended
50
+ * (per-path Cache-Control conflicts are impossible — files covered by a user
51
+ * Cache-Control rule are never fingerprinted).
52
+ */
53
+ export declare function mergeFingerprintHeaders(userHeaders: string | undefined, aliasPaths: readonly string[]): string;