@spacefast/common 0.0.23 → 0.0.26

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 (224) hide show
  1. package/brand-assets/LICENSE-Haskoy.txt +93 -0
  2. package/brand-assets/haskoy-latin-variable.woff2 +0 -0
  3. package/brand-assets/spacefast-favicon.svg +4 -4
  4. package/brand-assets/spacefast-sf-full-bleed.svg +3 -3
  5. package/brand-assets/spacefast-wordmark.svg +2 -4
  6. package/dist/agents/connect-targets.js +1 -1
  7. package/dist/agents/private-key-oauth.d.ts +0 -1
  8. package/dist/agents/private-key-oauth.js +46 -63
  9. package/dist/brand-assets-build.js +4 -6
  10. package/dist/brand-assets.d.ts +5 -15
  11. package/dist/brand-assets.js +15 -14
  12. package/dist/config/domains.d.ts +47 -7
  13. package/dist/config/domains.js +66 -10
  14. package/dist/config/index.d.ts +0 -1
  15. package/dist/config/index.js +0 -1
  16. package/dist/contracts/abuse.d.ts +2 -0
  17. package/dist/contracts/abuse.js +12 -0
  18. package/dist/contracts/access.d.ts +124 -12
  19. package/dist/contracts/access.js +73 -9
  20. package/dist/contracts/activity.d.ts +1 -1
  21. package/dist/contracts/activity.js +5 -12
  22. package/dist/contracts/api-keys.d.ts +21 -21
  23. package/dist/contracts/api-keys.js +2 -2
  24. package/dist/contracts/beta.js +1 -1
  25. package/dist/contracts/billing.d.ts +3 -35
  26. package/dist/contracts/billing.js +33 -52
  27. package/dist/contracts/builds.d.ts +30 -18
  28. package/dist/contracts/builds.js +28 -9
  29. package/dist/contracts/channels.js +1 -1
  30. package/dist/contracts/cli.d.ts +10 -0
  31. package/dist/contracts/cli.js +10 -0
  32. package/dist/contracts/collab-public.d.ts +63 -0
  33. package/dist/contracts/collab-public.js +46 -0
  34. package/dist/contracts/comments.d.ts +282 -53
  35. package/dist/contracts/comments.js +173 -52
  36. package/dist/contracts/common.d.ts +3 -3
  37. package/dist/contracts/common.js +5 -5
  38. package/dist/contracts/crons.d.ts +35 -0
  39. package/dist/contracts/crons.js +42 -0
  40. package/dist/contracts/device-auth.d.ts +6 -6
  41. package/dist/contracts/device-auth.js +2 -2
  42. package/dist/contracts/docs.js +2 -2
  43. package/dist/contracts/domains.d.ts +38 -27
  44. package/dist/contracts/domains.js +29 -2
  45. package/dist/contracts/enums.d.ts +34 -30
  46. package/dist/contracts/enums.js +62 -48
  47. package/dist/contracts/error-code-meta.d.ts +50 -125
  48. package/dist/contracts/error-code-meta.js +29 -41
  49. package/dist/contracts/error-codes.d.ts +1 -1
  50. package/dist/contracts/error-codes.js +14 -33
  51. package/dist/contracts/execution.d.ts +47 -66
  52. package/dist/contracts/execution.js +53 -49
  53. package/dist/contracts/feature-lifecycle-core.d.ts +0 -6
  54. package/dist/contracts/feature-lifecycle-core.js +2 -31
  55. package/dist/contracts/feature-lifecycle.d.ts +12 -52
  56. package/dist/contracts/feature-lifecycle.js +16 -59
  57. package/dist/contracts/features.d.ts +48 -60
  58. package/dist/contracts/features.js +59 -51
  59. package/dist/contracts/functions.d.ts +266 -72
  60. package/dist/contracts/functions.js +260 -37
  61. package/dist/contracts/git.d.ts +28 -34
  62. package/dist/contracts/git.js +10 -11
  63. package/dist/contracts/grants.d.ts +9 -5
  64. package/dist/contracts/grants.js +18 -9
  65. package/dist/contracts/ids.js +2 -2
  66. package/dist/contracts/internal.d.ts +8 -6
  67. package/dist/contracts/internal.js +12 -12
  68. package/dist/contracts/mcp.d.ts +11 -0
  69. package/dist/contracts/mcp.js +5 -0
  70. package/dist/contracts/me.js +1 -1
  71. package/dist/contracts/oauth-resources.d.ts +4 -3
  72. package/dist/contracts/oauth-resources.js +12 -3
  73. package/dist/contracts/operations.d.ts +90 -5
  74. package/dist/contracts/operations.js +77 -3
  75. package/dist/contracts/pages.d.ts +12 -7
  76. package/dist/contracts/pages.js +24 -1
  77. package/dist/contracts/plan-policy.d.ts +0 -2
  78. package/dist/contracts/plan-policy.js +2 -3
  79. package/dist/contracts/platform.d.ts +61 -148
  80. package/dist/contracts/platform.js +70 -126
  81. package/dist/contracts/publish-archive.d.ts +508 -0
  82. package/dist/contracts/publish-archive.js +32 -0
  83. package/dist/contracts/push-new.d.ts +4 -4
  84. package/dist/contracts/quotas.js +2 -2
  85. package/dist/contracts/repository-connections.d.ts +43 -30
  86. package/dist/contracts/repository-connections.js +11 -3
  87. package/dist/contracts/resources.d.ts +12 -24
  88. package/dist/contracts/resources.js +36 -16
  89. package/dist/contracts/runtime-api.d.ts +433 -192
  90. package/dist/contracts/runtime-api.js +421 -356
  91. package/dist/contracts/runtime-app.d.ts +127 -120
  92. package/dist/contracts/runtime-app.js +19 -10
  93. package/dist/contracts/runtime-db.d.ts +5 -5
  94. package/dist/contracts/runtime-db.js +2 -2
  95. package/dist/contracts/runtime-purge.d.ts +8 -0
  96. package/dist/contracts/runtime-purge.js +7 -0
  97. package/dist/contracts/runtime-services.d.ts +219 -0
  98. package/dist/contracts/runtime-services.js +437 -0
  99. package/dist/contracts/runtime-storage.d.ts +46 -32
  100. package/dist/contracts/runtime-storage.js +31 -8
  101. package/dist/contracts/sf-config-v1.d.ts +48 -10
  102. package/dist/contracts/space-config.d.ts +69 -18
  103. package/dist/contracts/space-config.js +226 -27
  104. package/dist/contracts/spaces.d.ts +236 -450
  105. package/dist/contracts/spaces.js +131 -151
  106. package/dist/contracts/superadmin-activity.d.ts +57 -4
  107. package/dist/contracts/superadmin-activity.js +48 -2
  108. package/dist/contracts/superadmin-search.d.ts +126 -0
  109. package/dist/contracts/superadmin-search.js +61 -0
  110. package/dist/contracts/superadmin-spaces.d.ts +361 -92
  111. package/dist/contracts/superadmin-spaces.js +97 -24
  112. package/dist/contracts/superadmin-teams.d.ts +28 -10
  113. package/dist/contracts/superadmin-teams.js +5 -8
  114. package/dist/contracts/superadmin-tenants.d.ts +0 -6
  115. package/dist/contracts/superadmin-tenants.js +1 -7
  116. package/dist/contracts/superadmin.d.ts +135 -54
  117. package/dist/contracts/superadmin.js +46 -23
  118. package/dist/contracts/tags.d.ts +12 -12
  119. package/dist/contracts/tags.js +3 -3
  120. package/dist/contracts/teams.d.ts +49 -14
  121. package/dist/contracts/teams.js +40 -22
  122. package/dist/contracts/transfers.d.ts +0 -49
  123. package/dist/contracts/transfers.js +4 -8
  124. package/dist/contracts/usage.d.ts +61 -0
  125. package/dist/contracts/usage.js +64 -0
  126. package/dist/contracts/variables.d.ts +2 -7
  127. package/dist/contracts/variables.js +9 -8
  128. package/dist/contracts/webhooks.d.ts +2 -2
  129. package/dist/contracts/zero.d.ts +102 -73
  130. package/dist/contracts/zero.js +94 -32
  131. package/dist/dashboard-paths/index.d.ts +1 -1
  132. package/dist/dashboard-paths/index.js +1 -0
  133. package/dist/docs/agent-prose.d.ts +2 -2
  134. package/dist/docs/agent-prose.js +63 -70
  135. package/dist/docs/agent-setup.js +45 -15
  136. package/dist/docs/catalog.d.ts +24 -24
  137. package/dist/docs/catalog.js +35 -24
  138. package/dist/docs/error-docs.js +194 -270
  139. package/dist/docs/skills.d.ts +12 -12
  140. package/dist/docs/skills.js +4 -4
  141. package/dist/test-helpers/fetch-stub.d.ts +16 -0
  142. package/dist/test-helpers/fetch-stub.js +25 -0
  143. package/dist/utils/build-settings.d.ts +5 -1
  144. package/dist/utils/build-settings.js +91 -13
  145. package/dist/utils/canonical-json.d.ts +10 -0
  146. package/dist/utils/canonical-json.js +22 -0
  147. package/dist/utils/cast-transport.d.ts +28 -0
  148. package/dist/utils/cast-transport.js +52 -0
  149. package/dist/utils/claim-token.d.ts +0 -8
  150. package/dist/utils/claim-token.js +3 -15
  151. package/dist/utils/concurrency.js +1 -0
  152. package/dist/utils/credential-policy.d.ts +1 -1
  153. package/dist/utils/credential-policy.js +4 -3
  154. package/dist/utils/dns-instructions.js +9 -9
  155. package/dist/utils/env-file.d.ts +17 -0
  156. package/dist/utils/env-file.js +96 -0
  157. package/dist/utils/error-code.d.ts +2 -0
  158. package/dist/utils/error-code.js +8 -0
  159. package/dist/utils/generate-space-name.d.ts +8 -10
  160. package/dist/utils/generate-space-name.js +206 -21
  161. package/dist/utils/grant-decision.fixtures.json +2938 -0
  162. package/dist/utils/grants.d.ts +2 -0
  163. package/dist/utils/grants.js +26 -1
  164. package/dist/utils/gravatar.d.ts +9 -0
  165. package/dist/utils/gravatar.js +31 -0
  166. package/dist/utils/idempotency.d.ts +6 -0
  167. package/dist/utils/idempotency.js +28 -0
  168. package/dist/utils/local-space-state.d.ts +9 -16
  169. package/dist/utils/local-space-state.js +20 -50
  170. package/dist/utils/one-shot-replay.d.ts +4 -1
  171. package/dist/utils/page-colors.d.ts +14 -12
  172. package/dist/utils/page-colors.js +15 -13
  173. package/dist/utils/page-fonts.d.ts +2 -2
  174. package/dist/utils/page-fonts.js +15 -4
  175. package/dist/utils/pages.d.ts +21 -3
  176. package/dist/utils/pages.js +42 -13
  177. package/dist/utils/publish-form-data.d.ts +12 -0
  178. package/dist/utils/publish-form-data.js +10 -0
  179. package/dist/utils/publish-policy.d.ts +27 -0
  180. package/dist/utils/publish-policy.fixtures.json +403 -0
  181. package/dist/utils/publish-policy.js +45 -20
  182. package/dist/utils/push-new.d.ts +7 -0
  183. package/dist/utils/push-new.js +7 -0
  184. package/dist/utils/query-keys.d.ts +4 -5
  185. package/dist/utils/query-keys.js +2 -3
  186. package/dist/utils/runtime-paths.d.ts +5 -0
  187. package/dist/utils/runtime-paths.js +5 -0
  188. package/dist/utils/runtime-upload-batch.d.ts +0 -5
  189. package/dist/utils/runtime-upload-batch.js +1 -27
  190. package/dist/utils/runtime-upload.d.ts +6 -11
  191. package/dist/utils/runtime-upload.js +10 -13
  192. package/dist/utils/secure-local-file.js +5 -16
  193. package/dist/utils/space-config.d.ts +1 -34
  194. package/dist/utils/space-config.js +8 -326
  195. package/dist/utils/static-runtime-policy.d.ts +9 -3
  196. package/dist/utils/static-runtime-policy.fixtures.json +38 -41
  197. package/dist/utils/static-runtime-policy.js +22 -56
  198. package/dist/utils/storage-policy.d.ts +1 -1
  199. package/dist/utils/storage-policy.js +3 -1
  200. package/dist/utils/structured-error.d.ts +14 -0
  201. package/dist/utils/structured-error.js +22 -0
  202. package/dist/utils/upload-session.d.ts +4 -1
  203. package/dist/utils/upload-session.js +33 -5
  204. package/dist/utils/version-path.js +13 -4
  205. package/dist/vocabulary.d.ts +13 -11
  206. package/dist/vocabulary.js +42 -28
  207. package/package.json +7 -17
  208. package/dist/config/postgres-budget.d.ts +0 -129
  209. package/dist/config/postgres-budget.js +0 -140
  210. package/dist/config/postgres-budget.json +0 -51
  211. package/dist/contracts/archives.d.ts +0 -331
  212. package/dist/contracts/archives.js +0 -120
  213. package/dist/contracts/deployments.d.ts +0 -574
  214. package/dist/contracts/deployments.js +0 -97
  215. package/dist/contracts/sites.d.ts +0 -416
  216. package/dist/contracts/sites.js +0 -168
  217. package/dist/contracts/superadmin-queues.d.ts +0 -414
  218. package/dist/contracts/superadmin-queues.js +0 -202
  219. package/dist/contracts/superadmin-runtime.d.ts +0 -27
  220. package/dist/contracts/superadmin-runtime.js +0 -38
  221. package/dist/index.d.ts +0 -1
  222. package/dist/index.js +0 -1
  223. package/dist/utils/api.d.ts +0 -4
  224. package/dist/utils/api.js +0 -13
@@ -44,6 +44,8 @@ export type GrantEvaluation = {
44
44
  matches: GrantMatch[];
45
45
  };
46
46
  export declare function grantResourcePatternMatches(pattern: string, resource: string, resourceKind?: "route" | "artifact"): boolean;
47
+ /** Does a Grant's resource set reach this path? Include, minus exclude. */
48
+ export declare function grantResourceSetMatches(resources: Grant["resources"], resource: string, resourceKind?: "route" | "artifact"): boolean;
47
49
  export type GrantEvaluationExplanation = {
48
50
  grantId: string;
49
51
  failures: GrantCheckFailure[];
@@ -27,6 +27,8 @@ function targetMatches(selector, target) {
27
27
  return target.kind === "version" && target.versionId === selector.versionId;
28
28
  case "branch":
29
29
  return target.kind === "branch" && target.branch === selector.branch;
30
+ // Immutable Version hosts only. The live host is its own target: sharing
31
+ // "every Version" does not thereby publish the live site.
30
32
  case "all_versions":
31
33
  return target.kind === "version";
32
34
  }
@@ -70,6 +72,29 @@ export function grantResourcePatternMatches(pattern, resource, resourceKind = "r
70
72
  };
71
73
  return matches(0, 0);
72
74
  }
75
+ /**
76
+ * Exclude patterns read wider than include patterns, and that asymmetry is the
77
+ * rule, not an accident: the route lane collapses a terminal `index.html`
78
+ * (normalizeAccessScopePath) while the artifact lane keeps it, so an exclude
79
+ * naming an index artifact must also close the route that artifact serves.
80
+ * Include never gets the same courtesy — granting the stripped route would hand
81
+ * out access nobody wrote down. The serving engine compiles the identical pair
82
+ * (access-rules.php _stattic_grant_exclude_pattern_variants).
83
+ */
84
+ function grantExcludePatternMatches(pattern, resource, resourceKind) {
85
+ if (grantResourcePatternMatches(pattern, resource, resourceKind))
86
+ return true;
87
+ const canonical = normalizeGrantResourcePattern(pattern);
88
+ if (!canonical.endsWith("/index.html"))
89
+ return false;
90
+ const stripped = canonical.slice(0, -"/index.html".length);
91
+ return grantResourcePatternMatches(stripped === "" ? "/" : stripped, resource, resourceKind);
92
+ }
93
+ /** Does a Grant's resource set reach this path? Include, minus exclude. */
94
+ export function grantResourceSetMatches(resources, resource, resourceKind = "route") {
95
+ return (resources.include.some((pattern) => grantResourcePatternMatches(pattern, resource, resourceKind)) &&
96
+ !resources.exclude.some((pattern) => grantExcludePatternMatches(pattern, resource, resourceKind)));
97
+ }
73
98
  function grantCheckFailures(grant, input, principal, now) {
74
99
  const failures = [];
75
100
  if (grant.source.kind === "config" && !input.spaceClaimed) {
@@ -120,7 +145,7 @@ function grantCheckFailures(grant, input, principal, now) {
120
145
  if (!included) {
121
146
  failures.push("resource_not_included");
122
147
  }
123
- else if (grant.resources.exclude.some((pattern) => grantResourcePatternMatches(pattern, input.resource, input.resourceKind))) {
148
+ else if (grant.resources.exclude.some((pattern) => grantExcludePatternMatches(pattern, input.resource, input.resourceKind ?? "route"))) {
124
149
  failures.push("resource_excluded");
125
150
  }
126
151
  return failures;
@@ -0,0 +1,9 @@
1
+ /** Gravatar's canonical address form: trimmed and lowercased. */
2
+ export declare function normalizeGravatarEmail(email: string | null | undefined): string | null;
3
+ /** The canonical avatar URL for an already-computed SHA-256 hash. */
4
+ export declare function gravatarUrlFromHash(hashHex: string): string;
5
+ /**
6
+ * SHA-256 hex of a normalized email — Gravatar's identifier. Uses WebCrypto so
7
+ * a browser can derive it locally and never send the raw address anywhere.
8
+ */
9
+ export declare function gravatarEmailHash(email: string | null | undefined): Promise<string | null>;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The one Gravatar URL shape Spacefast emits. Every surface — control-plane
3
+ * profiles, the Comments SDK, Slack notifications — derives the same address
4
+ * for the same person, so an avatar is recognizable wherever it appears and
5
+ * Cast can validate a guest avatar against a single pattern.
6
+ *
7
+ * `d=404` is deliberate: a person without a Gravatar gets no image at all, and
8
+ * the consumer renders its own initials/palette fallback instead of a stock
9
+ * silhouette. `s=160` covers the largest avatar we render, retina included.
10
+ */
11
+ const GRAVATAR_AVATAR_QUERY = "d=404&r=g&s=160";
12
+ /** Gravatar's canonical address form: trimmed and lowercased. */
13
+ export function normalizeGravatarEmail(email) {
14
+ const normalized = email?.trim().toLowerCase();
15
+ return normalized ? normalized : null;
16
+ }
17
+ /** The canonical avatar URL for an already-computed SHA-256 hash. */
18
+ export function gravatarUrlFromHash(hashHex) {
19
+ return `https://gravatar.com/avatar/${hashHex}?${GRAVATAR_AVATAR_QUERY}`;
20
+ }
21
+ /**
22
+ * SHA-256 hex of a normalized email — Gravatar's identifier. Uses WebCrypto so
23
+ * a browser can derive it locally and never send the raw address anywhere.
24
+ */
25
+ export async function gravatarEmailHash(email) {
26
+ const normalized = normalizeGravatarEmail(email);
27
+ if (!normalized || !globalThis.crypto?.subtle)
28
+ return null;
29
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(normalized));
30
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
31
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Stable, secret-safe key for one exact logical mutation attempt. Shared by
3
+ * every client surface: the same publish issued through the CLI and through
4
+ * MCP must land on one key or the API stops deduplicating the retry.
5
+ */
6
+ export declare function stableIdempotencyKey(prefix: string, parts: unknown): string;
@@ -0,0 +1,28 @@
1
+ import { createHash } from "node:crypto";
2
+ /**
3
+ * Key-sorted JSON that treats an absent key and an `undefined` value as the
4
+ * same thing. Deliberately *not* `canonicalJson`: a key the CLI omits and one
5
+ * the MCP server sets to `undefined` describe the same mutation, so they have
6
+ * to hash alike. Merging the two spellings would change every minted key.
7
+ */
8
+ function keyJson(value) {
9
+ if (Array.isArray(value)) {
10
+ return `[${value.map(keyJson).join(",")}]`;
11
+ }
12
+ if (value && typeof value === "object") {
13
+ return `{${Object.entries(value)
14
+ .filter(([, entry]) => entry !== undefined)
15
+ .toSorted(([left], [right]) => left.localeCompare(right))
16
+ .map(([key, entry]) => `${JSON.stringify(key)}:${keyJson(entry)}`)
17
+ .join(",")}}`;
18
+ }
19
+ return JSON.stringify(value ?? null);
20
+ }
21
+ /**
22
+ * Stable, secret-safe key for one exact logical mutation attempt. Shared by
23
+ * every client surface: the same publish issued through the CLI and through
24
+ * MCP must land on one key or the API stops deduplicating the retry.
25
+ */
26
+ export function stableIdempotencyKey(prefix, parts) {
27
+ return `${prefix}-${createHash("sha256").update(keyJson(parts)).digest("hex")}`;
28
+ }
@@ -1,6 +1,5 @@
1
1
  import { z } from "zod";
2
2
  export declare const LOCAL_SPACE_STATE_DIR_NAME = ".spacefast";
3
- export declare const LEGACY_LOCAL_SPACE_STATE_DIR_NAME = ".stattic";
4
3
  export declare const LOCAL_SPACE_LINK_FILE_NAME = "space.json";
5
4
  export declare const LOCAL_SPACE_STATE_FILE_NAME = "state.json";
6
5
  /**
@@ -23,6 +22,12 @@ export declare const localSpaceLinkSchema: z.ZodObject<{
23
22
  apiBaseUrl: z.ZodOptional<z.ZodString>;
24
23
  }, z.core.$loose>;
25
24
  export declare const LOCAL_SPACE_STATE_SCHEMA_VERSION = 1;
25
+ export declare const creationAttemptSchema: z.ZodObject<{
26
+ key: z.ZodString;
27
+ principal: z.ZodString;
28
+ kind: z.ZodLiteral<"authenticated">;
29
+ createdAt: z.ZodString;
30
+ }, z.core.$strict>;
26
31
  export declare const localSpaceStateSchema: z.ZodObject<{
27
32
  schemaVersion: z.ZodLiteral<1>;
28
33
  spaceId: z.ZodOptional<z.ZodString>;
@@ -42,22 +47,14 @@ export declare const localSpaceStateSchema: z.ZodObject<{
42
47
  pendingPublish: z.ZodOptional<z.ZodObject<{
43
48
  key: z.ZodString;
44
49
  principal: z.ZodString;
45
- kind: z.ZodEnum<{
46
- anonymous: "anonymous";
47
- authenticated: "authenticated";
48
- }>;
49
- intent: z.ZodOptional<z.ZodString>;
50
+ kind: z.ZodLiteral<"authenticated">;
50
51
  createdAt: z.ZodString;
51
52
  }, z.core.$strict>>;
52
53
  /** Settled creation receipt identity retained for invocations that began before its commit. */
53
54
  creationReplay: z.ZodOptional<z.ZodObject<{
54
55
  key: z.ZodString;
55
56
  principal: z.ZodString;
56
- kind: z.ZodEnum<{
57
- anonymous: "anonymous";
58
- authenticated: "authenticated";
59
- }>;
60
- intent: z.ZodOptional<z.ZodString>;
57
+ kind: z.ZodLiteral<"authenticated">;
61
58
  createdAt: z.ZodString;
62
59
  }, z.core.$strict>>;
63
60
  /** Random replay capability reserved before consuming a claim credential. */
@@ -127,10 +124,7 @@ export declare function localSpaceStateDir(root: string): string;
127
124
  export declare function localSpaceStatePath(root: string): string;
128
125
  export declare function localSpaceLinkPath(root: string): string;
129
126
  export declare function readLocalSpaceLink(root: string): Promise<LocalSpaceLink | null>;
130
- /**
131
- * Like readLocalSpaceLink, but reports WHICH file matched — callers surface the
132
- * legacy `.stattic/space.json` path to users so migrations stay visible.
133
- */
127
+ /** Like readLocalSpaceLink, but reports the file path alongside the link. */
134
128
  export declare function readLocalSpaceLinkWithPath(root: string): Promise<{
135
129
  link: LocalSpaceLink;
136
130
  path: string;
@@ -151,7 +145,6 @@ export declare function mutateLocalSpaceState(root: string, mutate: (current: Lo
151
145
  /** Fault-injection observer used to synchronize concurrency tests. */
152
146
  onConflict?: (() => void | Promise<void>) | undefined;
153
147
  }): Promise<LocalSpaceStateFile>;
154
- export declare function writeLocalSpaceState(root: string, state: LocalSpaceStateWrite): Promise<string>;
155
148
  /** Reserve random replay custody before a claim token can be consumed. */
156
149
  export declare function reserveLocalCredentialReplay(root: string, input: {
157
150
  operation: LocalCredentialReplayOperation;
@@ -5,7 +5,6 @@ import { z } from "zod";
5
5
  import { createOneShotReplayIdentity } from "./one-shot-replay.js";
6
6
  import { SecureLocalFileConflictError, secureReadFile, secureWriteFile, } from "./secure-local-file.js";
7
7
  export const LOCAL_SPACE_STATE_DIR_NAME = ".spacefast";
8
- export const LEGACY_LOCAL_SPACE_STATE_DIR_NAME = ".stattic";
9
8
  export const LOCAL_SPACE_LINK_FILE_NAME = "space.json";
10
9
  export const LOCAL_SPACE_STATE_FILE_NAME = "state.json";
11
10
  /**
@@ -30,6 +29,17 @@ export const localSpaceLinkSchema = z
30
29
  })
31
30
  .loose();
32
31
  export const LOCAL_SPACE_STATE_SCHEMA_VERSION = 1;
32
+ // One reservation, two slots: it is written as `pendingPublish` and moved to
33
+ // `creationReplay` once its receipt commits. Space creation is the only
34
+ // mutation that reserves one — credential-free publishes are not idempotent.
35
+ export const creationAttemptSchema = z
36
+ .object({
37
+ key: z.string().min(1),
38
+ principal: z.string().regex(/^[0-9a-f]{64}$/),
39
+ kind: z.literal("authenticated"),
40
+ createdAt: z.string().datetime(),
41
+ })
42
+ .strict();
33
43
  export const localSpaceStateSchema = z.strictObject({
34
44
  schemaVersion: z.literal(LOCAL_SPACE_STATE_SCHEMA_VERSION),
35
45
  spaceId: z.string().min(1).optional(),
@@ -46,27 +56,9 @@ export const localSpaceStateSchema = z.strictObject({
46
56
  /** Secret per-checkout entropy used only to namespace replay-safe first publishes. */
47
57
  anonymousPublishNonce: z.string().min(32).optional(),
48
58
  /** Unsettled first-publish mutation. Retries must reuse this exact key. */
49
- pendingPublish: z
50
- .object({
51
- key: z.string().min(1),
52
- principal: z.string().regex(/^[0-9a-f]{64}$/),
53
- kind: z.enum(["anonymous", "authenticated"]),
54
- intent: z.string().min(1).optional(),
55
- createdAt: z.string().datetime(),
56
- })
57
- .strict()
58
- .optional(),
59
+ pendingPublish: creationAttemptSchema.optional(),
59
60
  /** Settled creation receipt identity retained for invocations that began before its commit. */
60
- creationReplay: z
61
- .object({
62
- key: z.string().min(1),
63
- principal: z.string().regex(/^[0-9a-f]{64}$/),
64
- kind: z.enum(["anonymous", "authenticated"]),
65
- intent: z.string().min(1).optional(),
66
- createdAt: z.string().datetime(),
67
- })
68
- .strict()
69
- .optional(),
61
+ creationReplay: creationAttemptSchema.optional(),
70
62
  /** Random replay capability reserved before consuming a claim credential. */
71
63
  pendingCredentialReplay: z
72
64
  .object({
@@ -99,9 +91,6 @@ export function localSpaceStatePath(root) {
99
91
  export function localSpaceLinkPath(root) {
100
92
  return path.join(localSpaceStateDir(root), LOCAL_SPACE_LINK_FILE_NAME);
101
93
  }
102
- function legacyLinkPath(root) {
103
- return path.join(root, LEGACY_LOCAL_SPACE_STATE_DIR_NAME, LOCAL_SPACE_LINK_FILE_NAME);
104
- }
105
94
  async function readJsonFileWithRaw(root, relativePath) {
106
95
  const raw = await secureReadFile({ root, relativePath, encoding: "utf8" }).catch((error) => {
107
96
  if (error.code === "ENOENT")
@@ -156,28 +145,17 @@ function linkSpaceId(link) {
156
145
  export async function readLocalSpaceLink(root) {
157
146
  return (await readLocalSpaceLinkWithPath(root))?.link ?? null;
158
147
  }
159
- /**
160
- * Like readLocalSpaceLink, but reports WHICH file matched — callers surface the
161
- * legacy `.stattic/space.json` path to users so migrations stay visible.
162
- */
148
+ /** Like readLocalSpaceLink, but reports the file path alongside the link. */
163
149
  export async function readLocalSpaceLinkWithPath(root) {
164
150
  const primary = parseLink(await readJsonFile(root, path.join(LOCAL_SPACE_STATE_DIR_NAME, LOCAL_SPACE_LINK_FILE_NAME)));
165
- if (primary) {
166
- return { link: primary, path: localSpaceLinkPath(root) };
167
- }
168
- const legacy = parseLink(await readJsonFile(root, path.join(LEGACY_LOCAL_SPACE_STATE_DIR_NAME, LOCAL_SPACE_LINK_FILE_NAME)));
169
- return legacy ? { link: legacy, path: legacyLinkPath(root) } : null;
151
+ return primary ? { link: primary, path: localSpaceLinkPath(root) } : null;
170
152
  }
171
153
  export async function readLocalSpaceStateAt(root) {
172
154
  const statePath = localSpaceStatePath(root);
173
155
  const primaryState = await readStateFile(root, path.join(LOCAL_SPACE_STATE_DIR_NAME, LOCAL_SPACE_STATE_FILE_NAME));
174
- const legacyState = primaryState
175
- ? undefined
176
- : await readStateFile(root, path.join(LEGACY_LOCAL_SPACE_STATE_DIR_NAME, LOCAL_SPACE_STATE_FILE_NAME));
177
- const state = primaryState?.state ?? legacyState?.state ?? null;
156
+ const state = primaryState?.state ?? null;
178
157
  const linkPath = localSpaceLinkPath(root);
179
- const link = parseLink(await readJsonFile(root, path.join(LOCAL_SPACE_STATE_DIR_NAME, LOCAL_SPACE_LINK_FILE_NAME))) ??
180
- parseLink(await readJsonFile(root, path.join(LEGACY_LOCAL_SPACE_STATE_DIR_NAME, LOCAL_SPACE_LINK_FILE_NAME)));
158
+ const link = parseLink(await readJsonFile(root, path.join(LOCAL_SPACE_STATE_DIR_NAME, LOCAL_SPACE_LINK_FILE_NAME)));
181
159
  if (!state && !link)
182
160
  return null;
183
161
  const committedSpaceId = linkSpaceId(link);
@@ -334,11 +312,7 @@ export async function mutateLocalSpaceState(root, mutate, options = {}) {
334
312
  /* eslint-disable no-await-in-loop -- compare-and-swap retries are intentionally serialized against the latest committed state. */
335
313
  for (let attempt = 1; attempt <= LOCAL_STATE_MUTATION_ATTEMPTS; attempt += 1) {
336
314
  try {
337
- const primaryState = await readJsonFileWithRaw(root, stateRelativePath);
338
- const legacyState = primaryState
339
- ? undefined
340
- : await readJsonFileWithRaw(root, path.join(LEGACY_LOCAL_SPACE_STATE_DIR_NAME, LOCAL_SPACE_STATE_FILE_NAME));
341
- const source = primaryState ?? legacyState;
315
+ const source = await readJsonFileWithRaw(root, stateRelativePath);
342
316
  const parsed = source ? localSpaceStateSchema.safeParse(source.value) : null;
343
317
  if (source && !parsed?.success) {
344
318
  throw new Error(`Local Spacefast state at ${localSpaceStatePath(root)} is invalid.`);
@@ -353,14 +327,14 @@ export async function mutateLocalSpaceState(root, mutate, options = {}) {
353
327
  schemaVersion: LOCAL_SPACE_STATE_SCHEMA_VERSION,
354
328
  });
355
329
  const serialized = `${JSON.stringify(payload, null, 2)}\n`;
356
- if (primaryState?.raw === serialized)
330
+ if (source?.raw === serialized)
357
331
  return payload;
358
332
  await writeJsonAtomic({
359
333
  root,
360
334
  relativePath: stateRelativePath,
361
335
  value: payload,
362
336
  mode: 0o600,
363
- expectedContent: primaryState?.raw ?? null,
337
+ expectedContent: source?.raw ?? null,
364
338
  ...(options.beforeCommit ? { beforeCommit: options.beforeCommit } : {}),
365
339
  ...(options.afterBackup ? { afterBackup: options.afterBackup } : {}),
366
340
  });
@@ -379,10 +353,6 @@ export async function mutateLocalSpaceState(root, mutate, options = {}) {
379
353
  /* eslint-enable no-await-in-loop */
380
354
  throw new LocalSpaceStateConflictError("Local Spacefast state stayed busy for too long.");
381
355
  }
382
- export async function writeLocalSpaceState(root, state) {
383
- await mutateLocalSpaceState(root, () => state);
384
- return localSpaceStatePath(root);
385
- }
386
356
  function credentialFingerprint(credential) {
387
357
  return createHash("sha256").update(credential).digest("hex");
388
358
  }
@@ -11,4 +11,7 @@ export type OneShotReplayIdentity = {
11
11
  */
12
12
  export declare function createOneShotReplayIdentity(prefix: string): OneShotReplayIdentity;
13
13
  /** Production headers for one one-shot attempt (or one device-poll sequence). */
14
- export declare function oneShotReplayHeaders(identity: OneShotReplayIdentity, sequence?: number): Record<"idempotency-key" | "x-spacefast-idempotency-principal", string>;
14
+ export declare function oneShotReplayHeaders(identity: OneShotReplayIdentity, sequence?: number): {
15
+ "idempotency-key": string;
16
+ "x-spacefast-idempotency-principal": string;
17
+ };
@@ -1,17 +1,19 @@
1
1
  import type { SpacePageTheme } from "../contracts/space-config.js";
2
- /** Default page palette (mirrored into the shared template CSS). */
2
+ /** Default page palette (mirrored into the shared template CSS) — the
3
+ * printed-matter scheme: warm canvas, ink type, marker accent (always a
4
+ * ground with ink on top; the link derivation handles its text-grade). */
3
5
  export declare const PAGE_COLOR_DEFAULTS: {
4
- readonly accent: "#ff4217";
5
- readonly bg: "#f9f8f4";
6
- readonly fg: "#1a1913";
7
- readonly muted: "#6f6b62";
8
- readonly card: "#f9f8f4";
9
- readonly border: "#ddd9cf";
10
- readonly darkBg: "#141313";
11
- readonly darkFg: "#e9e7e2";
12
- readonly darkMuted: "#8f8b84";
13
- readonly darkCard: "#141313";
14
- readonly darkBorder: "#2b2a27";
6
+ readonly accent: "#d8f24b";
7
+ readonly bg: "#f7f5ef";
8
+ readonly fg: "#0c0c0c";
9
+ readonly muted: "#6e6a5e";
10
+ readonly card: "#f7f5ef";
11
+ readonly border: "#e5e1d4";
12
+ readonly darkBg: "#0c0c0c";
13
+ readonly darkFg: "#f0eee6";
14
+ readonly darkMuted: "#a6a193";
15
+ readonly darkCard: "#0c0c0c";
16
+ readonly darkBorder: "#2b2823";
15
17
  };
16
18
  export type PageSchemeColors = {
17
19
  bg: string;
@@ -18,19 +18,21 @@
18
18
  // Everything is pure and deterministic: publish finalize, dashboard previews,
19
19
  // and tests all call the same functions and get the same pixels.
20
20
  import { formatHex, oklch, parse, wcagContrast } from "culori";
21
- /** Default page palette (mirrored into the shared template CSS). */
21
+ /** Default page palette (mirrored into the shared template CSS) — the
22
+ * printed-matter scheme: warm canvas, ink type, marker accent (always a
23
+ * ground with ink on top; the link derivation handles its text-grade). */
22
24
  export const PAGE_COLOR_DEFAULTS = {
23
- accent: "#ff4217",
24
- bg: "#f9f8f4",
25
- fg: "#1a1913",
26
- muted: "#6f6b62",
27
- card: "#f9f8f4",
28
- border: "#ddd9cf",
29
- darkBg: "#141313",
30
- darkFg: "#e9e7e2",
31
- darkMuted: "#8f8b84",
32
- darkCard: "#141313",
33
- darkBorder: "#2b2a27",
25
+ accent: "#d8f24b",
26
+ bg: "#f7f5ef",
27
+ fg: "#0c0c0c",
28
+ muted: "#6e6a5e",
29
+ card: "#f7f5ef",
30
+ border: "#e5e1d4",
31
+ darkBg: "#0c0c0c",
32
+ darkFg: "#f0eee6",
33
+ darkMuted: "#a6a193",
34
+ darkCard: "#0c0c0c",
35
+ darkBorder: "#2b2823",
34
36
  };
35
37
  /** WCAG AA for body-size text. */
36
38
  const TEXT_CONTRAST = 4.5;
@@ -139,7 +141,7 @@ function schemeFromBackground(background) {
139
141
  /** The one entry point: raw theme in, everything the templates paint with out. */
140
142
  export function derivePageColors(theme) {
141
143
  const accentInput = theme.accent?.trim();
142
- const accent = safeParse(accentInput) ? accentInput : PAGE_COLOR_DEFAULTS.accent;
144
+ const accent = accentInput && safeParse(accentInput) ? accentInput : PAGE_COLOR_DEFAULTS.accent;
143
145
  const onAccent = onAccentColor(accent);
144
146
  const background = theme.background?.trim();
145
147
  if (background && safeParse(background)) {