@spacefast/common 0.0.24 → 0.2.1

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 (320) 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.d.ts +6 -146
  7. package/dist/agents/connect-targets.js +63 -295
  8. package/dist/agents/private-key-oauth.d.ts +31 -2
  9. package/dist/agents/private-key-oauth.js +166 -92
  10. package/dist/agents/registry/core.d.ts +292 -0
  11. package/dist/agents/registry/core.js +1388 -0
  12. package/dist/agents/registry/deeplink-probes.d.ts +23 -0
  13. package/dist/agents/registry/deeplink-probes.js +18 -0
  14. package/dist/agents/registry/node.d.ts +56 -0
  15. package/dist/agents/registry/node.js +339 -0
  16. package/dist/brand-assets-build.js +3 -6
  17. package/dist/brand-assets.d.ts +5 -15
  18. package/dist/brand-assets.js +12 -14
  19. package/dist/brand.d.ts +17 -0
  20. package/dist/brand.js +24 -4
  21. package/dist/config/domains.d.ts +79 -12
  22. package/dist/config/domains.js +128 -29
  23. package/dist/config/index.js +1 -2
  24. package/dist/contracts/abuse.js +9 -24
  25. package/dist/contracts/access-profiles.d.ts +86 -0
  26. package/dist/contracts/access-profiles.js +89 -0
  27. package/dist/contracts/access.d.ts +24 -30
  28. package/dist/contracts/access.js +33 -64
  29. package/dist/contracts/activity.d.ts +18 -1
  30. package/dist/contracts/activity.js +72 -26
  31. package/dist/contracts/analytics.js +8 -10
  32. package/dist/contracts/api-keys.d.ts +77 -52
  33. package/dist/contracts/api-keys.js +44 -24
  34. package/dist/contracts/application-journal.d.ts +216 -0
  35. package/dist/contracts/application-journal.js +167 -0
  36. package/dist/contracts/auth.d.ts +90 -17
  37. package/dist/contracts/auth.js +86 -17
  38. package/dist/contracts/beta.d.ts +9 -4
  39. package/dist/contracts/beta.js +20 -16
  40. package/dist/contracts/billing.d.ts +2 -1
  41. package/dist/contracts/billing.js +37 -11
  42. package/dist/contracts/bootstrap.d.ts +791 -0
  43. package/dist/contracts/bootstrap.js +51 -0
  44. package/dist/contracts/builds.d.ts +209 -18
  45. package/dist/contracts/builds.js +93 -27
  46. package/dist/contracts/channels.d.ts +43 -0
  47. package/dist/contracts/channels.js +25 -9
  48. package/dist/contracts/cli.d.ts +7 -0
  49. package/dist/contracts/cli.js +6 -0
  50. package/dist/contracts/collab-public.d.ts +1 -1
  51. package/dist/contracts/collab-public.js +5 -8
  52. package/dist/contracts/comments.d.ts +44 -34
  53. package/dist/contracts/comments.js +107 -77
  54. package/dist/contracts/commerce.d.ts +165 -0
  55. package/dist/contracts/commerce.js +146 -0
  56. package/dist/contracts/common.d.ts +3 -3
  57. package/dist/contracts/common.js +20 -31
  58. package/dist/contracts/content-contract-verification.d.ts +521 -0
  59. package/dist/contracts/content-contract-verification.js +320 -0
  60. package/dist/contracts/content-platform-fixture.d.ts +1278 -0
  61. package/dist/contracts/content-platform-fixture.js +44 -0
  62. package/dist/contracts/content-program.d.ts +994 -0
  63. package/dist/contracts/content-program.js +658 -0
  64. package/dist/contracts/content-sync.d.ts +290 -0
  65. package/dist/contracts/content-sync.js +167 -0
  66. package/dist/contracts/content.d.ts +432 -0
  67. package/dist/contracts/content.js +303 -0
  68. package/dist/contracts/continuation.d.ts +17 -1
  69. package/dist/contracts/continuation.js +23 -23
  70. package/dist/contracts/countries.js +3 -4
  71. package/dist/contracts/crons.d.ts +52 -0
  72. package/dist/contracts/crons.js +60 -0
  73. package/dist/contracts/dashboard-prefs.d.ts +56 -0
  74. package/dist/contracts/dashboard-prefs.js +51 -0
  75. package/dist/contracts/device-auth.d.ts +234 -37
  76. package/dist/contracts/device-auth.js +267 -53
  77. package/dist/contracts/docs.d.ts +1 -5
  78. package/dist/contracts/docs.js +7 -15
  79. package/dist/contracts/domains.d.ts +501 -93
  80. package/dist/contracts/domains.js +193 -55
  81. package/dist/contracts/email-preferences.d.ts +3 -3
  82. package/dist/contracts/email-preferences.js +6 -7
  83. package/dist/contracts/enums.d.ts +27 -26
  84. package/dist/contracts/enums.js +68 -61
  85. package/dist/contracts/error-code-meta.d.ts +283 -7
  86. package/dist/contracts/error-code-meta.js +97 -5
  87. package/dist/contracts/error-codes.d.ts +5 -5
  88. package/dist/contracts/error-codes.js +82 -17
  89. package/dist/contracts/events.d.ts +100 -20
  90. package/dist/contracts/events.js +107 -20
  91. package/dist/contracts/execution.d.ts +48 -64
  92. package/dist/contracts/execution.js +52 -62
  93. package/dist/contracts/feature-lifecycle.d.ts +73 -27
  94. package/dist/contracts/feature-lifecycle.js +86 -26
  95. package/dist/contracts/features.d.ts +68 -19
  96. package/dist/contracts/features.js +42 -41
  97. package/dist/contracts/fixtures/content-platform-v1.json +1108 -0
  98. package/dist/contracts/frame-session.d.ts +192 -0
  99. package/dist/contracts/frame-session.js +164 -0
  100. package/dist/contracts/functions.d.ts +338 -164
  101. package/dist/contracts/functions.js +367 -198
  102. package/dist/contracts/git.d.ts +27 -33
  103. package/dist/contracts/git.js +10 -11
  104. package/dist/contracts/grant-copy.d.ts +43 -0
  105. package/dist/contracts/grant-copy.js +44 -0
  106. package/dist/contracts/grants.d.ts +62 -54
  107. package/dist/contracts/grants.js +124 -23
  108. package/dist/contracts/ids.d.ts +4 -1
  109. package/dist/contracts/ids.js +6 -3
  110. package/dist/contracts/internal.d.ts +8 -18
  111. package/dist/contracts/internal.js +13 -14
  112. package/dist/contracts/mcp.d.ts +28 -0
  113. package/dist/contracts/mcp.js +28 -0
  114. package/dist/contracts/me.d.ts +6 -1
  115. package/dist/contracts/me.js +12 -6
  116. package/dist/contracts/notifications.d.ts +27 -29
  117. package/dist/contracts/notifications.js +9 -4
  118. package/dist/contracts/oauth-resources.d.ts +112 -10
  119. package/dist/contracts/oauth-resources.js +116 -11
  120. package/dist/contracts/oauth-scope-actions.d.ts +90 -0
  121. package/dist/contracts/oauth-scope-actions.js +126 -0
  122. package/dist/contracts/operations.d.ts +27 -4
  123. package/dist/contracts/operations.js +12 -6
  124. package/dist/contracts/pages.d.ts +31 -7
  125. package/dist/contracts/pages.js +22 -5
  126. package/dist/contracts/partner-notifications.d.ts +3 -0
  127. package/dist/contracts/partner-notifications.js +65 -0
  128. package/dist/contracts/{platform.d.ts → partner.d.ts} +29 -42
  129. package/dist/contracts/{platform.js → partner.js} +27 -40
  130. package/dist/contracts/plan-policy.js +1 -1
  131. package/dist/contracts/principal-assertion.d.ts +43 -0
  132. package/dist/contracts/principal-assertion.js +67 -0
  133. package/dist/contracts/principals.d.ts +73 -0
  134. package/dist/contracts/principals.js +59 -0
  135. package/dist/contracts/privacy.d.ts +11 -0
  136. package/dist/contracts/privacy.js +17 -0
  137. package/dist/contracts/problem-document.js +3 -4
  138. package/dist/contracts/publish-archive.d.ts +269 -161
  139. package/dist/contracts/publish-archive.js +11 -1
  140. package/dist/contracts/push-new.d.ts +8 -8
  141. package/dist/contracts/quotas.js +5 -5
  142. package/dist/contracts/realtime.d.ts +52 -0
  143. package/dist/contracts/realtime.js +54 -0
  144. package/dist/contracts/repository-connections.d.ts +44 -31
  145. package/dist/contracts/repository-connections.js +17 -5
  146. package/dist/contracts/resources.d.ts +105 -14
  147. package/dist/contracts/resources.js +150 -26
  148. package/dist/contracts/route-inventory.d.ts +358 -0
  149. package/dist/contracts/route-inventory.js +141 -0
  150. package/dist/contracts/runtime-api.d.ts +206 -82
  151. package/dist/contracts/runtime-api.js +131 -70
  152. package/dist/contracts/runtime-app.d.ts +13 -23
  153. package/dist/contracts/runtime-app.js +39 -24
  154. package/dist/contracts/runtime-components.d.ts +143 -0
  155. package/dist/contracts/runtime-components.js +199 -0
  156. package/dist/contracts/runtime-db.d.ts +49 -23
  157. package/dist/contracts/runtime-db.js +87 -22
  158. package/dist/contracts/runtime-purge.d.ts +8 -0
  159. package/dist/contracts/runtime-purge.js +5 -0
  160. package/dist/contracts/runtime-services.d.ts +43 -75
  161. package/dist/contracts/runtime-services.js +29 -49
  162. package/dist/contracts/runtime-storage.d.ts +15 -14
  163. package/dist/contracts/runtime-storage.js +30 -14
  164. package/dist/contracts/sf-config-v1.d.ts +48 -14
  165. package/dist/contracts/slugs.js +13 -4
  166. package/dist/contracts/space-config.d.ts +283 -19
  167. package/dist/contracts/space-config.js +430 -80
  168. package/dist/contracts/spaces.d.ts +376 -290
  169. package/dist/contracts/spaces.js +200 -135
  170. package/dist/contracts/superadmin-activity.d.ts +3 -43
  171. package/dist/contracts/superadmin-activity.js +6 -30
  172. package/dist/contracts/superadmin-emails.d.ts +1 -0
  173. package/dist/contracts/superadmin-emails.js +17 -1
  174. package/dist/contracts/superadmin-runtime.d.ts +199 -0
  175. package/dist/contracts/superadmin-runtime.js +141 -0
  176. package/dist/contracts/superadmin-search.d.ts +22 -2
  177. package/dist/contracts/superadmin-search.js +14 -6
  178. package/dist/contracts/superadmin-spaces.d.ts +438 -189
  179. package/dist/contracts/superadmin-spaces.js +131 -25
  180. package/dist/contracts/superadmin-teams.d.ts +5 -2
  181. package/dist/contracts/superadmin-teams.js +1 -2
  182. package/dist/contracts/superadmin-tenants.d.ts +627 -12
  183. package/dist/contracts/superadmin-tenants.js +32 -13
  184. package/dist/contracts/superadmin.d.ts +232 -37
  185. package/dist/contracts/superadmin.js +106 -22
  186. package/dist/contracts/tags.d.ts +85 -58
  187. package/dist/contracts/tags.js +47 -37
  188. package/dist/contracts/teams.d.ts +86 -26
  189. package/dist/contracts/teams.js +90 -31
  190. package/dist/contracts/tenants.d.ts +135 -0
  191. package/dist/contracts/tenants.js +120 -0
  192. package/dist/contracts/test-triggers.d.ts +70 -0
  193. package/dist/contracts/test-triggers.js +116 -0
  194. package/dist/contracts/theme-json.js +3 -8
  195. package/dist/contracts/transfers.d.ts +1 -0
  196. package/dist/contracts/transfers.js +3 -2
  197. package/dist/contracts/usage.d.ts +209 -0
  198. package/dist/contracts/usage.js +124 -12
  199. package/dist/contracts/variables.d.ts +2 -2
  200. package/dist/contracts/variables.js +9 -13
  201. package/dist/contracts/webhooks.d.ts +13 -5
  202. package/dist/contracts/webhooks.js +3 -1
  203. package/dist/contracts/zero.d.ts +72 -81
  204. package/dist/contracts/zero.js +80 -78
  205. package/dist/dashboard-paths/index.d.ts +24 -21
  206. package/dist/dashboard-paths/index.js +35 -47
  207. package/dist/dashboard-paths/route-reservations.generated.d.ts +2 -0
  208. package/dist/dashboard-paths/route-reservations.generated.js +36 -0
  209. package/dist/docs/agent-arrival.d.ts +92 -0
  210. package/dist/docs/agent-arrival.js +227 -0
  211. package/dist/docs/agent-output-policy.d.ts +6 -0
  212. package/dist/docs/agent-output-policy.js +22 -0
  213. package/dist/docs/agent-prose.d.ts +12 -18
  214. package/dist/docs/agent-prose.js +73 -51
  215. package/dist/docs/agent-setup.d.ts +72 -41
  216. package/dist/docs/agent-setup.js +339 -436
  217. package/dist/docs/catalog.d.ts +153 -95
  218. package/dist/docs/catalog.js +163 -48
  219. package/dist/docs/error-docs.d.ts +2098 -1
  220. package/dist/docs/error-docs.js +421 -144
  221. package/dist/docs/index-build.d.ts +0 -1
  222. package/dist/docs/index-build.js +2 -3
  223. package/dist/docs/page-context-prompt.d.ts +77 -0
  224. package/dist/docs/page-context-prompt.js +131 -0
  225. package/dist/docs/reference-urls.d.ts +46 -0
  226. package/dist/docs/reference-urls.js +79 -0
  227. package/dist/docs/search.d.ts +6 -8
  228. package/dist/docs/search.js +10 -13
  229. package/dist/docs/skill-distribution.d.ts +4 -1
  230. package/dist/docs/skill-distribution.js +10 -5
  231. package/dist/docs/skills.d.ts +120 -120
  232. package/dist/docs/skills.js +121 -73
  233. package/dist/docs/start-prompts.d.ts +45 -0
  234. package/dist/docs/start-prompts.js +62 -0
  235. package/dist/domain-exploration.js +9 -13
  236. package/dist/og-template.d.ts +56 -0
  237. package/dist/og-template.js +56 -0
  238. package/dist/slug-policy/index.js +5 -11
  239. package/dist/test-helpers/fetch-stub.js +1 -0
  240. package/dist/utils/browser-credential.d.ts +13 -6
  241. package/dist/utils/browser-credential.js +14 -7
  242. package/dist/utils/build-settings.d.ts +21 -1
  243. package/dist/utils/build-settings.js +133 -15
  244. package/dist/utils/canonical-json.d.ts +3 -5
  245. package/dist/utils/canonical-json.js +4 -5
  246. package/dist/utils/cast-transport.d.ts +5 -17
  247. package/dist/utils/cast-transport.js +6 -19
  248. package/dist/utils/claim-token.js +1 -2
  249. package/dist/utils/comment-avatars.js +4 -6
  250. package/dist/utils/concurrency.js +3 -3
  251. package/dist/utils/content-type.d.ts +7 -6
  252. package/dist/utils/content-type.js +73 -51
  253. package/dist/utils/credential-policy.d.ts +50 -6
  254. package/dist/utils/credential-policy.js +126 -59
  255. package/dist/utils/dns-instructions.js +9 -9
  256. package/dist/utils/egress-policy.fixtures.json +1 -1
  257. package/dist/utils/error-code.js +1 -0
  258. package/dist/utils/generate-space-name.d.ts +3 -8
  259. package/dist/utils/generate-space-name.js +3 -8
  260. package/dist/utils/grant-decision.fixtures.json +2 -2
  261. package/dist/utils/grants.d.ts +0 -1
  262. package/dist/utils/grants.js +7 -8
  263. package/dist/utils/gravatar.d.ts +2 -4
  264. package/dist/utils/gravatar.js +6 -12
  265. package/dist/utils/id-hints.js +14 -23
  266. package/dist/utils/idempotency.d.ts +3 -3
  267. package/dist/utils/idempotency.js +6 -7
  268. package/dist/utils/local-space-state.d.ts +23 -26
  269. package/dist/utils/local-space-state.js +46 -46
  270. package/dist/utils/oauth-signed-query.d.ts +3 -5
  271. package/dist/utils/oauth-signed-query.js +6 -9
  272. package/dist/utils/one-shot-replay.d.ts +7 -6
  273. package/dist/utils/one-shot-replay.js +3 -5
  274. package/dist/utils/page-colors.d.ts +15 -19
  275. package/dist/utils/page-colors.js +23 -45
  276. package/dist/utils/page-fonts.d.ts +1 -1
  277. package/dist/utils/page-fonts.js +8 -20
  278. package/dist/utils/page-preview.d.ts +0 -1
  279. package/dist/utils/page-preview.js +0 -1
  280. package/dist/utils/pages.d.ts +33 -3
  281. package/dist/utils/pages.js +83 -30
  282. package/dist/utils/privacy-regions.d.ts +19 -0
  283. package/dist/utils/privacy-regions.js +91 -0
  284. package/dist/utils/publish-form-data.d.ts +4 -5
  285. package/dist/utils/publish-form-data.js +2 -3
  286. package/dist/utils/publish-policy.d.ts +11 -17
  287. package/dist/utils/publish-policy.fixtures.json +11 -5
  288. package/dist/utils/publish-policy.js +65 -110
  289. package/dist/utils/query-client.d.ts +2 -0
  290. package/dist/utils/query-client.js +10 -1
  291. package/dist/utils/runtime-paths.d.ts +4 -4
  292. package/dist/utils/runtime-paths.js +6 -8
  293. package/dist/utils/runtime-upload.d.ts +9 -0
  294. package/dist/utils/runtime-upload.js +12 -0
  295. package/dist/utils/secure-local-file.d.ts +3 -3
  296. package/dist/utils/secure-local-file.js +23 -40
  297. package/dist/utils/space-config.d.ts +12 -15
  298. package/dist/utils/space-config.js +13 -16
  299. package/dist/utils/static-runtime-policy.d.ts +10 -7
  300. package/dist/utils/static-runtime-policy.fixtures.json +38 -41
  301. package/dist/utils/static-runtime-policy.generated.d.ts +16 -0
  302. package/dist/utils/static-runtime-policy.generated.js +70 -0
  303. package/dist/utils/static-runtime-policy.js +31 -132
  304. package/dist/utils/storage-policy.js +1 -2
  305. package/dist/utils/upload-session.d.ts +4 -1
  306. package/dist/utils/upload-session.js +41 -19
  307. package/dist/utils/version-path.js +11 -19
  308. package/dist/utils/wpcom-auth-redirects.d.ts +5 -14
  309. package/dist/utils/wpcom-auth-redirects.js +1 -4
  310. package/dist/vocabulary.d.ts +13 -13
  311. package/dist/vocabulary.js +52 -27
  312. package/package.json +15 -18
  313. package/dist/agents/client-registry.d.ts +0 -22
  314. package/dist/agents/client-registry.js +0 -41
  315. package/dist/docs/agent-handoff-document.d.ts +0 -15
  316. package/dist/docs/agent-handoff-document.js +0 -180
  317. package/dist/docs/agent-solutions.d.ts +0 -110
  318. package/dist/docs/agent-solutions.js +0 -183
  319. package/dist/utils/query-keys.d.ts +0 -84
  320. package/dist/utils/query-keys.js +0 -108
@@ -1,44 +1,27 @@
1
- // Readability engine for Spacefast-authored pages.
2
- //
3
- // Theme colors are intent, not CSS: users and agents push any accent or
4
- // background (dashboard, spacefast.jsonc, API) and the platform guarantees
5
- // every rendered page stays readable. This module derives the full set of
6
- // colors the templates actually paint with:
7
- //
8
- // - `onAccent` — button-label color on accent fills (black or white).
9
- // - `link` — the accent, lightness-adjusted until it meets WCAG AA
10
- // against the surface it sits on. Hue and chroma survive,
11
- // so it still reads as the brand color.
12
- // - `pinned` — when a background is set, a complete coherent scheme
13
- // (fg/muted/surface/border) derived from it and pinned for
14
- // every visitor. A chosen background IS the page; the
15
- // visitor's OS dark-mode setting must not repaint half the
16
- // variables into an unreadable hybrid.
17
- //
18
- // Everything is pure and deterministic: publish finalize, dashboard previews,
19
- // and tests all call the same functions and get the same pixels.
1
+ // Readability engine for Spacefast-authored pages. Users and agents push any
2
+ // accent or background; this module derives the colors the templates paint
3
+ // with so every page meets WCAG AA. A pushed background pins a whole scheme,
4
+ // because the visitor's OS dark-mode setting must not repaint half the
5
+ // variables into an unreadable hybrid.
20
6
  import { formatHex, oklch, parse, wcagContrast } from "culori";
21
- /** Default page palette (mirrored into the shared template CSS). */
7
+ /** Default page palette, mirrored into the shared template CSS. */
22
8
  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",
9
+ accent: "#d8f24b",
10
+ bg: "#f7f5ef",
11
+ fg: "#0c0c0c",
12
+ muted: "#6e6a5e",
13
+ card: "#f7f5ef",
14
+ border: "#e5e1d4",
15
+ darkBg: "#0c0c0c",
16
+ darkFg: "#f0eee6",
17
+ darkMuted: "#a6a193",
18
+ darkCard: "#0c0c0c",
19
+ darkBorder: "#2b2823",
34
20
  };
35
- /** WCAG AA for body-size text. */
21
+ // WCAG AA: 4.5 for text, 3 for non-text control boundaries.
36
22
  const TEXT_CONTRAST = 4.5;
37
- /** Body-size button labels on accent fills. */
38
23
  const UI_CONTRAST = 4.5;
39
- /** Essential control boundaries against their painted surface. */
40
24
  const CONTROL_CONTRAST = 3;
41
- /** Primary foreground meets AA even on mid-tone custom backgrounds. */
42
25
  const FG_CONTRAST = 4.5;
43
26
  function safeParse(value) {
44
27
  if (!value)
@@ -57,10 +40,9 @@ function isLightSurface(surface) {
57
40
  return contrast("#000000", surface) >= contrast("#ffffff", surface);
58
41
  }
59
42
  /**
60
- * Return `color` unchanged when it already meets `target` on `surface`;
61
- * otherwise move its OKLCH lightness (hue/chroma preserved) just far enough
62
- * to meet it. Falls back to plain black/white when even full lightness range
63
- * cannot reach the target (e.g. a mid-gray surface).
43
+ * Moves `color`'s OKLCH lightness (hue and chroma preserved) just far enough
44
+ * to meet `target` on `surface`. Falls back to black or white when the full
45
+ * lightness range cannot reach it, as on a mid-gray surface.
64
46
  */
65
47
  export function readableOn(color, surface, target) {
66
48
  if (contrast(color, surface) >= target)
@@ -76,8 +58,7 @@ export function readableOn(color, surface, target) {
76
58
  if (contrast(atLimit, surface) < target) {
77
59
  return isLightSurface(surface) ? "#000000" : "#ffffff";
78
60
  }
79
- // Binary search for the lightness closest to the original that still meets
80
- // the target; contrast is monotonic in lightness on a fixed surface side.
61
+ // Contrast is monotonic in lightness on a fixed surface side.
81
62
  let searchLimit = limit;
82
63
  let searchStart = base.l;
83
64
  for (let i = 0; i < 24; i += 1) {
@@ -91,14 +72,12 @@ export function readableOn(color, surface, target) {
91
72
  }
92
73
  return formatHex({ ...base, l: searchLimit });
93
74
  }
94
- /** Button-label color for an accent fill: white when it can carry, else ink. */
95
75
  export function onAccentColor(accent) {
96
76
  return contrast("#ffffff", accent) >= UI_CONTRAST ? "#ffffff" : "#000000";
97
77
  }
98
78
  function clamp(value, min, max) {
99
79
  return Math.min(max, Math.max(min, value));
100
80
  }
101
- /** Derive a complete coherent scheme from a pushed background color. */
102
81
  function schemeFromBackground(background) {
103
82
  const parsed = safeParse(background);
104
83
  const bg = parsed ? formatHex(parsed) : PAGE_COLOR_DEFAULTS.bg;
@@ -136,10 +115,9 @@ function schemeFromBackground(background) {
136
115
  },
137
116
  };
138
117
  }
139
- /** The one entry point: raw theme in, everything the templates paint with out. */
140
118
  export function derivePageColors(theme) {
141
119
  const accentInput = theme.accent?.trim();
142
- const accent = safeParse(accentInput) ? accentInput : PAGE_COLOR_DEFAULTS.accent;
120
+ const accent = accentInput && safeParse(accentInput) ? accentInput : PAGE_COLOR_DEFAULTS.accent;
143
121
  const onAccent = onAccentColor(accent);
144
122
  const background = theme.background?.trim();
145
123
  if (background && safeParse(background)) {
@@ -1,2 +1,2 @@
1
1
  export declare const PAGE_FONT_FACES: string;
2
- export declare const PAGE_FONT_PRELOAD_HTML: string;
2
+ export declare const PAGE_FONT_PRELOAD_HTML = "<link rel=\"preload\" href=\"https://wordpress.com/i/fonts/recoleta/400.woff2\" as=\"font\" type=\"font/woff2\" crossorigin>";
@@ -1,22 +1,10 @@
1
- // The platform page fonts, loaded from shared origins so every space hostname
2
- // reuses one warm browser-cache entry instead of a cold per-space fetch:
3
- // Recoleta (commercial, never committed to this repo) from wordpress.com's
4
- // font CDN, Haskoy from spacefast.com (apps/www/public/assets/fonts, served
5
- // immutable with CORS — see wwwHeaders in apps/www/src/lib/routing.mjs).
6
- // `font-display:optional` renders the fallback and never swaps mid-view when a
7
- // font arrives late — cached loads (everything after the first) get the brand
8
- // fonts with no flicker either way.
1
+ // Recoleta is the only downloaded face (DESIGN.md §4); body and mono are system
2
+ // stacks. It is commercial, never committed to this repo, and loads from
3
+ // wordpress.com's font CDN so every space hostname shares one cache entry.
4
+ // `font-display:optional` never swaps mid-view when a font arrives late.
9
5
  const RECOLETA_BASE = "https://wordpress.com/i/fonts/recoleta/";
10
- const HASKOY_URL = "https://spacefast.com/assets/fonts/haskoy-latin-variable.woff2";
11
6
  const RECOLETA_WEIGHTS = [300, 400, 500, 600, 700];
12
- const faces = [
13
- ...RECOLETA_WEIGHTS.map((weight) => `@font-face{font-family:"Recoleta";src:url("${RECOLETA_BASE}${weight}.woff2") format("woff2");font-style:normal;font-weight:${weight};font-display:optional}`),
14
- `@font-face{font-family:"Haskoy";src:url("${HASKOY_URL}") format("woff2");font-style:normal;font-weight:100 900;font-display:optional}`,
15
- ];
16
- export const PAGE_FONT_FACES = faces.join("");
17
- // Only the faces the default pages actually render: Recoleta 400 for headings,
18
- // Haskoy for everything else. Fonts are a cross-origin fetch; `crossorigin` is
19
- // required — font requests are CORS mode, and a mismatched preload is discarded.
20
- export const PAGE_FONT_PRELOAD_HTML = [`${RECOLETA_BASE}400.woff2`, HASKOY_URL]
21
- .map((url) => `<link rel="preload" href="${url}" as="font" type="font/woff2" crossorigin>`)
22
- .join("");
7
+ export const PAGE_FONT_FACES = RECOLETA_WEIGHTS.map((weight) => `@font-face{font-family:"Recoleta";src:url("${RECOLETA_BASE}${weight}.woff2") format("woff2");font-style:normal;font-weight:${weight};font-display:optional}`).join("");
8
+ // Only the weight the default pages render. `crossorigin` is required: font
9
+ // requests are CORS mode, and a mismatched preload is discarded.
10
+ export const PAGE_FONT_PRELOAD_HTML = `<link rel="preload" href="${RECOLETA_BASE}400.woff2" as="font" type="font/woff2" crossorigin>`;
@@ -3,5 +3,4 @@
3
3
  * the preview page is reachable only under this query flag.
4
4
  */
5
5
  export declare const PAGE_PREVIEW_QUERY = "spacefast_view";
6
- /** The preview page for a file whose bytes live at `fileHref`. */
7
6
  export declare function previewPageHref(fileHref: string): string;
@@ -3,7 +3,6 @@
3
3
  * the preview page is reachable only under this query flag.
4
4
  */
5
5
  export const PAGE_PREVIEW_QUERY = "spacefast_view";
6
- /** The preview page for a file whose bytes live at `fileHref`. */
7
6
  export function previewPageHref(fileHref) {
8
7
  return `${fileHref}${fileHref.includes("?") ? "&" : "?"}${PAGE_PREVIEW_QUERY}=1`;
9
8
  }
@@ -1,4 +1,5 @@
1
- import { type CustomPageId, type SitePageId } from "../contracts/pages.js";
1
+ import { type CustomPageId, type PlatformPageId, type SitePageId } from "../contracts/pages.js";
2
+ import { type DerivedPageColors } from "./page-colors.js";
2
3
  export { PAGE_PREVIEW_QUERY, previewPageHref } from "./page-preview.js";
3
4
  export type PageTheme = {
4
5
  accent?: string | undefined;
@@ -21,7 +22,7 @@ export type PagePreviewFile = {
21
22
  mime?: string;
22
23
  };
23
24
  export type PageExpansionContext = {
24
- page: CustomPageId | "layout";
25
+ page: CustomPageId | PlatformPageId | "layout";
25
26
  theme?: PageTheme;
26
27
  content?: string;
27
28
  displayPath?: string;
@@ -29,6 +30,9 @@ export type PageExpansionContext = {
29
30
  itemCount?: number;
30
31
  visibleItemCount?: number;
31
32
  hideSpacefastBranding?: boolean;
33
+ supportUrl?: string;
34
+ partnerBranding?: boolean;
35
+ useSystemDisplayFont?: boolean;
32
36
  files?: readonly PageFileEntry[];
33
37
  file?: PagePreviewFile;
34
38
  };
@@ -40,6 +44,13 @@ export type PageDiagnostic = {
40
44
  };
41
45
  export declare function isPagePublicPath(path: string): boolean;
42
46
  export declare function escapePageHtml(value: string): string;
47
+ /**
48
+ * The custom properties a theme paints with. A custom background pins a
49
+ * complete scheme for both OS modes; `--sf-bg` alone would leave dark-mode
50
+ * visitors with the dark foreground on a light background. Exported so the
51
+ * dashboard preview paints the same tokens from an unsaved draft.
52
+ */
53
+ export declare function pageThemeCss(theme: PageTheme, derived?: DerivedPageColors): string;
43
54
  export declare function expandPageHtml(source: string, context: PageExpansionContext): string;
44
55
  export declare function validatePageSources(input: {
45
56
  pages: Readonly<Record<string, string>>;
@@ -49,7 +60,26 @@ export declare function isCompleteHtmlDocument(source: string): boolean;
49
60
  export declare function applyPageLayout(layout: string | null, content: string, context: Omit<PageExpansionContext, "page" | "content">): string;
50
61
  export declare function inferPageThemeFromThemeJson(raw: string): Partial<PageTheme>;
51
62
  export declare function defaultPageBody(id: SitePageId): string;
52
- export declare const DEFAULT_PAGE_TEMPLATES: Readonly<Record<SitePageId, string>>;
63
+ export declare const DEFAULT_PAGE_TEMPLATES: {
64
+ "404": string;
65
+ denied: string;
66
+ access: string;
67
+ index: string;
68
+ preview: string;
69
+ };
70
+ export declare const DEFAULT_PLATFORM_PAGE_TEMPLATES: {
71
+ login: string;
72
+ undeployed: string;
73
+ suspended: string;
74
+ legal: string;
75
+ gone: string;
76
+ "runtime-error": string;
77
+ "rate-limited": string;
78
+ "tier-unavailable": string;
79
+ "proxy-error": string;
80
+ "method-not-allowed": string;
81
+ "content-too-large": string;
82
+ };
53
83
  export declare const DEFAULT_LAYOUT_TEMPLATE: string;
54
84
  export declare function defaultPageTemplate(id: SitePageId): string;
55
85
  export declare function pageIdFromFilename(filename: string): CustomPageId | null;
@@ -33,17 +33,17 @@ export function escapePageHtml(value) {
33
33
  .replaceAll('"', "&quot;")
34
34
  .replaceAll("'", "&#039;");
35
35
  }
36
- // Pushed colors are intent; the readability engine (page-colors.ts) derives
37
- // what the templates actually paint with. A custom background pins a complete
38
- // scheme for both OS modes — emitting `--sf-bg` alone used to leave dark-mode
39
- // visitors with the dark foreground on the user's light background.
40
- function themeStyle(theme) {
36
+ /**
37
+ * The custom properties a theme paints with. A custom background pins a
38
+ * complete scheme for both OS modes; `--sf-bg` alone would leave dark-mode
39
+ * visitors with the dark foreground on a light background. Exported so the
40
+ * dashboard preview paints the same tokens from an unsaved draft.
41
+ */
42
+ export function pageThemeCss(theme, derived = derivePageColors(theme)) {
41
43
  const root = [];
42
44
  const dark = [];
43
45
  if (theme.accent || theme.background) {
44
- const derived = derivePageColors(theme);
45
- // `derived.accent` is the parse-validated input (or the default), never
46
- // an arbitrary string straight from the theme.
46
+ // `derived.accent` is parse-validated, never a raw string from the theme.
47
47
  if (theme.accent)
48
48
  root.push(`--sf-accent:${derived.accent}`);
49
49
  root.push(`--sf-on-accent:${derived.onAccent}`, `--sf-link:${derived.link}`);
@@ -59,7 +59,11 @@ function themeStyle(theme) {
59
59
  if (root.length === 0)
60
60
  return "";
61
61
  const darkBlock = dark.length > 0 ? `@media(prefers-color-scheme:dark){:root{${dark.join(";")}}}` : "";
62
- return `<style>:root{${root.join(";")}}${darkBlock}</style>`;
62
+ return `:root{${root.join(";")}}${darkBlock}`;
63
+ }
64
+ function themeStyle(theme) {
65
+ const css = pageThemeCss(theme);
66
+ return css ? `<style>${css}</style>` : "";
63
67
  }
64
68
  const runtimeSlot = (name, fallback) => `<!--sf-runtime:${name}:start-->${fallback}<!--sf-runtime:${name}:end-->`;
65
69
  function renderFiles(files) {
@@ -72,8 +76,8 @@ function renderFiles(files) {
72
76
  function renderFile(file) {
73
77
  if (!file)
74
78
  return '<p class="sf-empty">File unavailable.</p>';
75
- // This page is itself the `?spacefast_view=1` representation, so the embed,
76
- // the download and the raw link all point straight at the canonical path.
79
+ // This page is itself the `?spacefast_view=1` representation, so every link
80
+ // points straight at the canonical path.
77
81
  const href = escapePageHtml(file.href);
78
82
  const name = escapePageHtml(file.name);
79
83
  const media = file.mime?.startsWith("image/")
@@ -81,11 +85,13 @@ function renderFile(file) {
81
85
  : `<iframe class="sf-preview" src="${href}" title="Preview of ${name}" loading="lazy" sandbox></iframe>`;
82
86
  return `<div class="sf-preview-wrap">${media}</div><dl class="sf-file-meta"><div><dt>Name</dt><dd>${name}</dd></div>${file.size ? `<div><dt>Size</dt><dd>${escapePageHtml(file.size)}</dd></div>` : ""}${file.mime ? `<div><dt>Type</dt><dd>${escapePageHtml(file.mime)}</dd></div>` : ""}</dl><div class="sf-actions"><a class="sf-button" href="${href}" download>Download</a><a class="sf-ghost" href="${href}">View raw</a></div>`;
83
87
  }
84
- function renderSpacefastBranding(hidden) {
88
+ function renderSpacefastBranding(hidden, supportUrl, partnerBranding) {
85
89
  const wordmark = hidden
86
90
  ? ""
87
- : `<span class="sf-powered" tabindex="0"><svg class="sf-wordmark" viewBox="0 0 326 62" role="img" aria-label="Spacefast"><rect class="sf-wordmark-shadow" x="14" y="12" width="306" height="48"></rect><rect class="sf-wordmark-plate" x="10" y="8" width="306" height="48"></rect><path class="sf-wordmark-paper" d="${SPACEFAST_WORDMARK_PATH}"></path></svg><span class="sf-powered-line"><a href="https://spacefast.com">Best way to share what your agent made</a></span></span>`;
88
- return `<footer class="sf-colophon"><div class="sf-colophon-row">${wordmark}<a class="sf-help" href="https://spacefast.com/help">Need help?</a></div></footer>`;
91
+ : `<span class="sf-powered" tabindex="0"><svg class="sf-wordmark" viewBox="0 0 152 28.9062" role="img" aria-label="Spacefast"><path fill="currentColor" d="${SPACEFAST_WORDMARK_PATH}"></path></svg><span class="sf-powered-line"><a href="https://spacefast.com">Best way to share what your agent made</a></span></span>`;
92
+ const helpUrl = supportUrl ?? (partnerBranding ? null : "https://spacefast.com/help");
93
+ const help = helpUrl ? `<a class="sf-help" href="${escapePageHtml(helpUrl)}">Need help?</a>` : "";
94
+ return `<footer class="sf-colophon"><div class="sf-colophon-row">${wordmark}${help}</div></footer>`;
89
95
  }
90
96
  function renderElement(element, context) {
91
97
  switch (element) {
@@ -110,7 +116,7 @@ function renderElement(element, context) {
110
116
  return escapePageHtml(visible < count ? `${label} · showing first ${visible}` : label);
111
117
  }
112
118
  case "sf-spacefast-branding":
113
- return renderSpacefastBranding(context.hideSpacefastBranding);
119
+ return renderSpacefastBranding(context.hideSpacefastBranding, context.supportUrl, context.partnerBranding);
114
120
  case "sf-files":
115
121
  return renderFiles(context.files ?? []);
116
122
  case "sf-file":
@@ -128,15 +134,21 @@ const ELEMENT_PATTERN = /<\s*(sf-[a-z0-9-]+)\b[^>]*>(?:\s*<\s*\/\s*\1\s*>)?|<\s*
128
134
  export function expandPageHtml(source, context) {
129
135
  let output = source.replace(ELEMENT_PATTERN, (_match, paired, unary) => {
130
136
  const name = (paired ?? unary)?.toLowerCase();
137
+ // SAFETY: The set membership check admits only names from the PageElement literal set.
131
138
  return name && knownElements.has(name) ? renderElement(name, context) : _match;
132
139
  });
133
140
  if (output.includes("/*sf-default-fonts*/")) {
134
- output = output.replaceAll("/*sf-default-fonts*/", PAGE_FONT_FACES);
135
- // The faces point at shared font origins; preloading ahead of the inline
136
- // stylesheet starts the cross-origin fetches with the parse.
137
- output = /<\s*style\b/i.test(output)
138
- ? output.replace(/<\s*style\b/i, (tag) => `${PAGE_FONT_PRELOAD_HTML}${tag}`)
139
- : `${PAGE_FONT_PRELOAD_HTML}${output}`;
141
+ output = output.replaceAll("/*sf-default-fonts*/", context.useSystemDisplayFont ? "" : PAGE_FONT_FACES);
142
+ if (context.useSystemDisplayFont) {
143
+ output = output.replaceAll('--sf-display-font:"Recoleta",Georgia,serif', "--sf-display-font:var(--sf-font)");
144
+ }
145
+ // Preloading ahead of the inline stylesheet starts the cross-origin
146
+ // fetches with the parse.
147
+ output = context.useSystemDisplayFont
148
+ ? output
149
+ : /<\s*style\b/i.test(output)
150
+ ? output.replace(/<\s*style\b/i, (tag) => `${PAGE_FONT_PRELOAD_HTML}${tag}`)
151
+ : `${PAGE_FONT_PRELOAD_HTML}${output}`;
140
152
  }
141
153
  const tokens = themeStyle(context.theme ?? {});
142
154
  if (tokens) {
@@ -187,8 +199,7 @@ export function validatePageSources(input) {
187
199
  const path = sourcePath.includes("_pages/") ? sourcePath : `_pages/${sourcePath}`;
188
200
  const filename = path.slice(path.lastIndexOf("_pages/") + 7);
189
201
  const id = filename.replace(/\.html$/i, "");
190
- // A collab layout is Space-level — one review room, one URL so unlike the
191
- // gate and listing pages it has no per-directory form.
202
+ // One review room per Space, so collab has no per-directory form.
192
203
  if (!customPageIds.has(id) ||
193
204
  filename !== `${id}.html` ||
194
205
  (id === "collab" && path !== COLLAB_PAGE_SOURCE)) {
@@ -216,8 +227,7 @@ export function validatePageSources(input) {
216
227
  path,
217
228
  });
218
229
  for (const element of foundElements(source)) {
219
- // Collab elements are the browser SDK's, not the compiler's: legal in the
220
- // review room, meaningless anywhere the SDK is not the point.
230
+ // Collab elements belong to the browser SDK, legal only in the review room.
221
231
  if (id === "collab" && collabElements.has(element))
222
232
  continue;
223
233
  if (!knownElements.has(element))
@@ -255,8 +265,8 @@ export function validatePageSources(input) {
255
265
  message: "The index page does not render <sf-files>.",
256
266
  path,
257
267
  });
258
- // Without the lane slot the runtime has nowhere to inject the working
259
- // sign-in/password/request blocks and the template ships a dead gate.
268
+ // Without the lane slot the runtime cannot inject the sign-in, password
269
+ // and request blocks, and the gate ships dead.
260
270
  if (id === "access" && !/<\s*sf-access-lanes\b/i.test(source))
261
271
  diagnostics.push({
262
272
  severity: "error",
@@ -374,10 +384,9 @@ export function defaultPageBody(id) {
374
384
  const source = DEFAULT_PAGE_TEMPLATES[id];
375
385
  return source.match(/<body[^>]*>([\s\S]*?)<\/body>/i)?.[1] ?? source;
376
386
  }
377
- // The built-in palette runs through the same readability engine as pushed
378
- // themes, so buttons and links remain readable for every accepted accent.
387
+ // The built-in palette runs through the same readability engine as pushed themes.
379
388
  const defaultPageColors = derivePageColors({});
380
- const sharedCss = `/*sf-default-fonts*/:root{color-scheme:light dark;--sf-accent:${PAGE_COLOR_DEFAULTS.accent};--sf-bg:${PAGE_COLOR_DEFAULTS.bg};--sf-fg:${PAGE_COLOR_DEFAULTS.fg};--sf-muted:${PAGE_COLOR_DEFAULTS.muted};--sf-card:${PAGE_COLOR_DEFAULTS.card};--sf-border:${PAGE_COLOR_DEFAULTS.border};--sf-link:${defaultPageColors.link};--sf-on-accent:${defaultPageColors.onAccent};--sf-error:#a82d1a;--sf-font:"Haskoy",-apple-system,BlinkMacSystemFont,ui-sans-serif,"Segoe UI",sans-serif;--sf-display-font:"Recoleta",Georgia,serif;--sf-mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace}*{box-sizing:border-box}html{min-width:320px;background:var(--sf-bg);-webkit-font-smoothing:antialiased}body{margin:0;min-height:100vh;background:var(--sf-bg);color:var(--sf-fg);font:14px/1.65 var(--sf-font)}a{color:inherit}.sf-page{min-height:100vh;display:flex;flex-direction:column;padding:clamp(22px,4vw,44px)}.sf-header{display:flex;align-items:center;min-height:32px}.sf-page-center .sf-header{justify-content:center}.sf-brand{display:inline-flex;min-width:0;align-items:center;gap:10px;color:var(--sf-fg);text-decoration:none}.sf-brand-mark{width:7px;height:7px;flex:none;background:var(--sf-accent)}.sf-logo{width:28px;height:28px;flex:none;object-fit:contain}.sf-brand strong{min-width:0;overflow-wrap:anywhere;font-size:13px;font-weight:600}.sf-main{flex:1;width:100%;padding:clamp(34px,9vh,84px) 0}.sf-page-plain .sf-main{max-width:680px}.sf-page-center .sf-main{max-width:620px;margin:auto;display:flex;flex-direction:column;align-items:center;text-align:center}.sf-eyebrow{margin:0 0 14px;color:var(--sf-muted);font:500 11px/1.5 var(--sf-mono);letter-spacing:.12em;text-transform:uppercase;overflow-wrap:anywhere}.sf-page-center .sf-eyebrow{margin-bottom:16px}h1{max-width:20ch;margin:0 0 12px;color:var(--sf-fg);font:400 clamp(1.9rem,5vw,2.8rem)/1.22 var(--sf-display-font);letter-spacing:-.012em;text-wrap:balance}.sf-copy,.sf-sub{max-width:52ch;margin:0;color:var(--sf-muted);text-wrap:pretty}.sf-inline-path{font-family:var(--sf-mono);font-size:.9em;color:var(--sf-fg);overflow-wrap:anywhere}.sf-actions{display:flex;flex-wrap:wrap;align-items:center;gap:14px;margin-top:22px}.sf-page-center .sf-actions{justify-content:center}.sf-button{display:inline-flex;min-height:44px;align-items:center;justify-content:center;border:0;background:var(--sf-accent);color:var(--sf-on-accent);padding:10px 16px;font:600 13px/1.4 var(--sf-font);text-decoration:none;cursor:pointer}.sf-ghost{color:var(--sf-muted);text-underline-offset:3px}.sf-button:hover{filter:brightness(1.06)}a:focus-visible,button:focus-visible{outline:2px solid var(--sf-link);outline-offset:2px}.sf-copy+.sf-copy{margin-top:12px}.sf-files{width:100%;margin:24px 0 0;padding:0;list-style:none}.sf-files li{display:flex;min-width:0;align-items:baseline;gap:18px;border-bottom:1px solid var(--sf-border)}.sf-files a{min-width:0;flex:1;padding:10px 2px;color:var(--sf-fg);font-weight:500;text-decoration:none}.sf-files a:hover{color:var(--sf-link)}.sf-file-name{overflow-wrap:anywhere}.sf-file-size{flex:none;padding:10px 2px;color:var(--sf-muted);font:12px/1.5 var(--sf-mono);white-space:nowrap}.sf-empty{max-width:420px;margin:24px 0 0;border:1px dashed var(--sf-border);padding:18px 20px;color:var(--sf-muted)}.sf-preview-wrap{width:min(100%,560px);aspect-ratio:16/9;margin:10px 0 18px;overflow:hidden;background:#fff;outline:1px solid color-mix(in srgb,var(--sf-fg) 10%,transparent);outline-offset:-1px}.sf-preview{display:block;width:100%;height:100%;border:0;object-fit:contain}.sf-file-meta{width:min(100%,560px);margin:0}.sf-file-meta div{display:flex;min-width:0;justify-content:space-between;gap:20px;border-bottom:1px solid var(--sf-border);padding:9px 2px}.sf-file-meta dt{color:var(--sf-fg);font-weight:600}.sf-file-meta dd{min-width:0;margin:0;color:var(--sf-muted);font-family:var(--sf-mono);overflow-wrap:anywhere;text-align:right}.sf-colophon{display:flex;flex-direction:column;align-items:center;gap:10px;padding:44px 0 26px;margin-top:auto;color:var(--sf-muted)}.sf-page-plain .sf-colophon{align-items:flex-start}.sf-colophon-row{display:flex;align-items:center;gap:20px;flex-wrap:wrap}.sf-powered{display:inline-flex;align-items:center;gap:10px;cursor:pointer;outline:none}.sf-powered-line{display:none;font-size:12px;color:var(--sf-muted)}.sf-powered:hover .sf-powered-line,.sf-powered:focus .sf-powered-line,.sf-powered:focus-within .sf-powered-line{display:inline}.sf-powered-line a{color:var(--sf-link);text-decoration:underline;text-underline-offset:3px}.sf-help{font-size:12px;color:var(--sf-muted);text-decoration:underline;text-underline-offset:3px}.sf-help:hover{color:var(--sf-fg)}.sf-wordmark{display:block;flex:none;width:auto;height:11px}.sf-wordmark-shadow{fill:var(--sf-accent)}.sf-wordmark-plate{fill:#141419}.sf-wordmark-paper{fill:#fbf8f1}@media(max-width:520px){.sf-page{padding:20px}.sf-button{min-height:48px}.sf-file-meta div{flex-direction:column;gap:2px}.sf-file-meta dd{text-align:left}.sf-colophon{padding-top:32px}}@media(prefers-color-scheme:dark){:root{--sf-bg:${PAGE_COLOR_DEFAULTS.darkBg};--sf-fg:${PAGE_COLOR_DEFAULTS.darkFg};--sf-muted:${PAGE_COLOR_DEFAULTS.darkMuted};--sf-card:${PAGE_COLOR_DEFAULTS.darkCard};--sf-border:${PAGE_COLOR_DEFAULTS.darkBorder};--sf-link:${defaultPageColors.linkDark};--sf-error:#ff8a7a}.sf-preview-wrap{outline-color:#ffffff1a}}@media(forced-colors:active){.sf-brand-mark{background:CanvasText}.sf-button{border:1px solid ButtonText}.sf-powered:focus{outline:1px solid CanvasText;outline-offset:3px}}`;
389
+ const sharedCss = `/*sf-default-fonts*/:root{color-scheme:light dark;--sf-accent:${PAGE_COLOR_DEFAULTS.accent};--sf-bg:${PAGE_COLOR_DEFAULTS.bg};--sf-fg:${PAGE_COLOR_DEFAULTS.fg};--sf-muted:${PAGE_COLOR_DEFAULTS.muted};--sf-card:${PAGE_COLOR_DEFAULTS.card};--sf-border:${PAGE_COLOR_DEFAULTS.border};--sf-link:${defaultPageColors.link};--sf-on-accent:${defaultPageColors.onAccent};--sf-error:#a82d1a;--sf-font:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",system-ui,Roboto,"Helvetica Neue",Arial,sans-serif;--sf-display-font:"Recoleta",Georgia,serif;--sf-mono:ui-monospace,"SF Mono",Menlo,Consolas,monospace}*{box-sizing:border-box}html{min-width:320px;background:var(--sf-bg);-webkit-font-smoothing:antialiased}body{margin:0;min-height:100vh;background:var(--sf-bg);color:var(--sf-fg);font:14px/1.65 var(--sf-font)}a{color:inherit}.sf-page{min-height:100vh;display:flex;flex-direction:column;padding:clamp(22px,4vw,44px)}.sf-header{display:flex;align-items:center;min-height:32px}.sf-page-center .sf-header{justify-content:center}.sf-brand{display:inline-flex;min-width:0;align-items:center;gap:10px;color:var(--sf-fg);text-decoration:none}.sf-brand-mark{width:7px;height:7px;flex:none;background:var(--sf-accent)}.sf-logo{width:28px;height:28px;flex:none;object-fit:contain}.sf-brand strong{min-width:0;overflow-wrap:anywhere;font-size:13px;font-weight:600}.sf-main{flex:1;width:100%;padding:clamp(34px,9vh,84px) 0}.sf-page-plain .sf-main{max-width:680px}.sf-page-center .sf-main{max-width:620px;margin:auto;display:flex;flex-direction:column;align-items:center;text-align:center}.sf-eyebrow{margin:0 0 14px;color:var(--sf-muted);font:500 11px/1.5 var(--sf-mono);letter-spacing:.12em;text-transform:uppercase;overflow-wrap:anywhere}.sf-page-center .sf-eyebrow{margin-bottom:16px}h1{max-width:20ch;margin:0 0 12px;color:var(--sf-fg);font:400 clamp(1.9rem,5vw,2.8rem)/1.22 var(--sf-display-font);letter-spacing:-.012em;text-wrap:balance}.sf-copy,.sf-sub{max-width:52ch;margin:0;color:var(--sf-muted);text-wrap:pretty}.sf-inline-path{font-family:var(--sf-mono);font-size:.9em;color:var(--sf-fg);overflow-wrap:anywhere}.sf-actions{display:flex;flex-wrap:wrap;align-items:center;gap:14px;margin-top:22px}.sf-page-center .sf-actions{justify-content:center}.sf-button{display:inline-flex;min-height:44px;align-items:center;justify-content:center;border:0;background:var(--sf-accent);color:var(--sf-on-accent);padding:10px 16px;font:600 13px/1.4 var(--sf-font);text-decoration:none;cursor:pointer}.sf-ghost{color:var(--sf-muted);text-underline-offset:3px}.sf-button:hover{filter:brightness(1.06)}a:focus-visible,button:focus-visible{outline:2px solid var(--sf-link);outline-offset:2px}.sf-copy+.sf-copy{margin-top:12px}.sf-files{width:100%;margin:24px 0 0;padding:0;list-style:none}.sf-files li{display:flex;min-width:0;align-items:baseline;gap:18px;border-bottom:1px solid var(--sf-border)}.sf-files a{min-width:0;flex:1;padding:10px 2px;color:var(--sf-fg);font-weight:500;text-decoration:none}.sf-files a:hover{color:var(--sf-link)}.sf-file-name{overflow-wrap:anywhere}.sf-file-size{flex:none;padding:10px 2px;color:var(--sf-muted);font:12px/1.5 var(--sf-mono);white-space:nowrap}.sf-empty{max-width:420px;margin:24px 0 0;border:1px dashed var(--sf-border);padding:18px 20px;color:var(--sf-muted)}.sf-preview-wrap{width:min(100%,560px);aspect-ratio:16/9;margin:10px 0 18px;overflow:hidden;background:#fff;outline:1px solid color-mix(in srgb,var(--sf-fg) 10%,transparent);outline-offset:-1px}.sf-preview{display:block;width:100%;height:100%;border:0;object-fit:contain}.sf-file-meta{width:min(100%,560px);margin:0}.sf-file-meta div{display:flex;min-width:0;justify-content:space-between;gap:20px;border-bottom:1px solid var(--sf-border);padding:9px 2px}.sf-file-meta dt{color:var(--sf-fg);font-weight:600}.sf-file-meta dd{min-width:0;margin:0;color:var(--sf-muted);font-family:var(--sf-mono);overflow-wrap:anywhere;text-align:right}.sf-colophon{display:flex;flex-direction:column;align-items:center;gap:10px;padding:44px 0 26px;margin-top:auto;color:var(--sf-muted)}.sf-page-plain .sf-colophon{align-items:flex-start}.sf-colophon-row{display:flex;align-items:center;gap:20px;flex-wrap:wrap}.sf-powered{display:inline-flex;align-items:center;gap:10px;cursor:pointer;outline:none}.sf-powered-line{display:none;font-size:12px;color:var(--sf-muted)}.sf-powered:hover .sf-powered-line,.sf-powered:focus .sf-powered-line,.sf-powered:focus-within .sf-powered-line{display:inline}.sf-powered-line a{color:var(--sf-link);text-decoration:underline;text-underline-offset:3px}.sf-help{font-size:12px;color:var(--sf-muted);text-decoration:underline;text-underline-offset:3px}.sf-help:hover{color:var(--sf-fg)}.sf-wordmark{display:block;flex:none;width:auto;height:14px;color:var(--sf-fg)}@media(max-width:520px){.sf-page{padding:20px}.sf-button{min-height:48px}.sf-file-meta div{flex-direction:column;gap:2px}.sf-file-meta dd{text-align:left}.sf-colophon{padding-top:32px}}@media(prefers-color-scheme:dark){:root{--sf-bg:${PAGE_COLOR_DEFAULTS.darkBg};--sf-fg:${PAGE_COLOR_DEFAULTS.darkFg};--sf-muted:${PAGE_COLOR_DEFAULTS.darkMuted};--sf-card:${PAGE_COLOR_DEFAULTS.darkCard};--sf-border:${PAGE_COLOR_DEFAULTS.darkBorder};--sf-link:${defaultPageColors.linkDark};--sf-error:#ff8a7a}.sf-preview-wrap{outline-color:#ffffff1a}}@media(forced-colors:active){.sf-brand-mark{background:CanvasText}.sf-button{border:1px solid ButtonText}.sf-powered:focus{outline:1px solid CanvasText;outline-offset:3px}}`;
381
390
  const accessCss = `.sf-page.access .sf-main{max-width:26rem;padding:clamp(42px,8vh,72px) 0}.sf-page.access h1{max-width:18ch;margin-bottom:14px;font:600 clamp(2rem,7vw,2.65rem)/1.12 var(--sf-font);letter-spacing:-.035em}.sf-page.access .sf-copy{font-size:15px;line-height:1.6}.sf-access-default{display:flex;width:100%;flex-direction:column;align-items:center}.sf-access-status{padding:10px 12px;border:1px solid color-mix(in srgb,var(--sf-fg) 10%,transparent);border-radius:8px;background:color-mix(in srgb,var(--sf-fg) 4%,transparent);color:var(--sf-fg)}.sf-access-lanes{display:flex;flex-direction:column;gap:16px;width:min(23rem,100%);margin-top:30px;text-align:left}.sf-access-lanes form{display:flex;flex-direction:column;gap:14px;margin:0}.sf-input{box-sizing:border-box;width:100%;min-height:46px;padding:11px 12px;border:1px solid color-mix(in srgb,var(--sf-fg) 18%,transparent);border-radius:8px;outline:2px solid transparent;outline-offset:-1px;background:color-mix(in srgb,var(--sf-bg) 92%,var(--sf-fg));color:var(--sf-fg);font:500 15px/1.45 var(--sf-font);appearance:none}.sf-input::placeholder{color:color-mix(in srgb,var(--sf-muted) 82%,transparent);font:inherit}.sf-input:hover{border-color:color-mix(in srgb,var(--sf-fg) 34%,transparent)}.sf-input:focus-visible{border-color:var(--sf-accent);outline-color:var(--sf-accent)}textarea.sf-input{min-height:92px;resize:vertical}.sf-label{color:var(--sf-fg);font:600 13px/1.4 var(--sf-font)}.sf-access-lanes .sf-button{width:100%;min-height:46px;border-radius:8px}.sf-access-or{display:flex;align-items:center;gap:12px;color:var(--sf-muted);font-size:13px}.sf-access-or:before,.sf-access-or:after{content:"";flex:1;border-top:1px solid color-mix(in srgb,var(--sf-fg) 12%,transparent)}.sf-error{margin:0;color:var(--sf-error);font-size:14px}.sf-access-password-row{display:flex;gap:8px}.sf-access-password-row .sf-input{min-width:0;flex:1}.sf-access-password-row .sf-button{width:auto;flex:none}.sf-access-request{padding-top:2px}.sf-access-request summary{padding:4px 0;cursor:pointer;color:var(--sf-muted);font:550 14px/1.5 var(--sf-font)}.sf-access-request summary:hover{color:var(--sf-fg)}.sf-access-request[open] summary{color:var(--sf-fg)}.sf-access-request form{margin-top:16px}@media(max-width:520px){.sf-page.access .sf-main{padding:38px 0}.sf-page.access h1{font-size:2rem}.sf-page.access .sf-copy{font-size:16px}.sf-input{min-height:48px;font-size:16px}.sf-access-lanes .sf-button{min-height:48px}.sf-access-password-row{flex-direction:column}.sf-access-password-row .sf-button{width:100%}}@media(prefers-color-scheme:dark){.sf-access-status{background:color-mix(in srgb,var(--sf-fg) 5%,transparent)}}`;
382
391
  const pageHeader = '<header class="sf-header"><a class="sf-brand" href="/" aria-label="Homepage"><span class="sf-brand-mark" aria-hidden="true"></span><sf-logo></sf-logo><strong><sf-name></sf-name></strong></a></header>';
383
392
  const pageFooter = "<sf-spacefast-branding></sf-spacefast-branding>";
@@ -394,14 +403,58 @@ export const DEFAULT_PAGE_TEMPLATES = {
394
403
  index: document("index", "<sf-page-title></sf-page-title>", '<p class="sf-eyebrow"><sf-page-path></sf-page-path></p><h1><sf-page-title></sf-page-title></h1><p class="sf-sub"><sf-page-summary></sf-page-summary></p><sf-files></sf-files>', "plain"),
395
404
  preview: document("preview", "<sf-page-title></sf-page-title>", '<p class="sf-eyebrow"><sf-page-path></sf-page-path></p><h1><sf-page-title></sf-page-title></h1><sf-file></sf-file>', "plain"),
396
405
  };
406
+ const PLATFORM_PAGE_COPY = {
407
+ login: [401, "This space is for members", "Sign in to continue — you’ll come right back here."],
408
+ undeployed: [503, "Waiting for launch", "This space hasn’t been published yet. Check back soon."],
409
+ suspended: [402, "This space is paused", "Serving is on hold until billing is sorted out."],
410
+ legal: [
411
+ 451,
412
+ "Unavailable for legal reasons",
413
+ "This space is blocked in response to a legal demand.",
414
+ ],
415
+ gone: [404, "Nothing here", "This space is no longer available."],
416
+ "runtime-error": [
417
+ 500,
418
+ "Something broke on our side",
419
+ "The runtime hit an error serving this page. Try again in a moment.",
420
+ ],
421
+ "rate-limited": [
422
+ 429,
423
+ "Slow down a second",
424
+ "Too many requests hit this space at once. Give it a moment and try again.",
425
+ ],
426
+ "tier-unavailable": [503, "Back in a bit", "This space is temporarily unavailable."],
427
+ "proxy-error": [502, "The upstream missed the connection", "Try again in a moment."],
428
+ "method-not-allowed": [405, "That method won’t work here", "Try the request another way."],
429
+ "content-too-large": [413, "That request is too large", "Send a smaller request and try again."],
430
+ };
431
+ function platformDocument(id) {
432
+ const [status, title, description] = PLATFORM_PAGE_COPY[id];
433
+ return `<!doctype html>\n<html lang="en">\n<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex"><title>${title}</title><style>${sharedCss}</style></head>\n<body><div class="sf-page sf-page-center">${pageHeader}<main class="sf-main"><p class="sf-eyebrow">${status}</p><h1>${title}</h1><p class="sf-copy">${description}</p></main>${pageFooter}</div></body>\n</html>\n`;
434
+ }
435
+ export const DEFAULT_PLATFORM_PAGE_TEMPLATES = {
436
+ login: platformDocument("login"),
437
+ undeployed: platformDocument("undeployed"),
438
+ suspended: platformDocument("suspended"),
439
+ legal: platformDocument("legal"),
440
+ gone: platformDocument("gone"),
441
+ "runtime-error": platformDocument("runtime-error"),
442
+ "rate-limited": platformDocument("rate-limited"),
443
+ "tier-unavailable": platformDocument("tier-unavailable"),
444
+ "proxy-error": platformDocument("proxy-error"),
445
+ "method-not-allowed": platformDocument("method-not-allowed"),
446
+ "content-too-large": platformDocument("content-too-large"),
447
+ };
397
448
  export const DEFAULT_LAYOUT_TEMPLATE = `<!doctype html>\n<html lang="en">\n<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title><sf-page-title></sf-page-title></title><style>${sharedCss}</style></head>\n<body><sf-content></sf-content></body>\n</html>\n`;
398
449
  export function defaultPageTemplate(id) {
399
450
  return DEFAULT_PAGE_TEMPLATES[id];
400
451
  }
401
452
  export function pageIdFromFilename(filename) {
402
453
  const id = filename.replace(/\.html$/, "");
454
+ // SAFETY: The set membership check admits only ids from the CustomPageId literal set.
403
455
  return customPageIds.has(id) ? id : null;
404
456
  }
405
457
  export function allDefaultPageTemplates() {
458
+ // SAFETY: SITE_PAGE_IDS maps every SitePageId to its corresponding default template entry.
406
459
  return Object.fromEntries(SITE_PAGE_IDS.map((id) => [id, DEFAULT_PAGE_TEMPLATES[id]]));
407
460
  }
@@ -0,0 +1,19 @@
1
+ /** EU 27 + CH, IS, LI, NO, GB. */
2
+ export declare const GDPR_COUNTRY_CODES: Set<string>;
3
+ export declare const GEO_COUNTRY_UNKNOWN = "unknown";
4
+ /**
5
+ * GDPR applicability FAILS CLOSED: an unknown or missing country is treated as
6
+ * inside the GDPR zone (wpcom `isGdprCountry`). Country codes are ISO 3166-1
7
+ * alpha-2, case-insensitive.
8
+ */
9
+ export declare function isGdprCountry(countryCode: string | null | undefined): boolean;
10
+ /** US states with comprehensive privacy laws, lowercase full names (wpcom `CCPARegions`). */
11
+ export declare const CCPA_REGIONS: Set<string>;
12
+ /**
13
+ * CCPA applicability FAILS OPEN: an unknown or missing region is NOT assumed
14
+ * to be a CCPA region (wpcom `pertainsToCCPA`: "if we aren't sure what the
15
+ * region is we shouldn't assume it pertains to CCPA"). Matches on substring,
16
+ * like wpcom, so a region string such as "california" or "California, US"
17
+ * both qualify.
18
+ */
19
+ export declare function isCcpaRegion(countryCode: string | null | undefined, region: string | null | undefined): boolean;
@@ -0,0 +1,91 @@
1
+ // Privacy-regulation region lists, ported from wpcom's a8c-analytics
2
+ // (`a8c-analytics-src/kit/constants.ts`, verified 2026-08-23) so Spacefast's
3
+ // consent behavior matches WordPress.com's. One home for both the server-side
4
+ // Tag SDK and the dashboard consent engine.
5
+ /** EU 27 + CH, IS, LI, NO, GB. */
6
+ export const GDPR_COUNTRY_CODES = new Set([
7
+ "AT",
8
+ "BE",
9
+ "BG",
10
+ "CY",
11
+ "CZ",
12
+ "DE",
13
+ "DK",
14
+ "EE",
15
+ "ES",
16
+ "FI",
17
+ "FR",
18
+ "GR",
19
+ "HR",
20
+ "HU",
21
+ "IE",
22
+ "IT",
23
+ "LT",
24
+ "LU",
25
+ "LV",
26
+ "MT",
27
+ "NL",
28
+ "PL",
29
+ "PT",
30
+ "RO",
31
+ "SE",
32
+ "SI",
33
+ "SK",
34
+ "CH",
35
+ "IS",
36
+ "LI",
37
+ "NO",
38
+ "GB",
39
+ ]);
40
+ export const GEO_COUNTRY_UNKNOWN = "unknown";
41
+ /**
42
+ * GDPR applicability FAILS CLOSED: an unknown or missing country is treated as
43
+ * inside the GDPR zone (wpcom `isGdprCountry`). Country codes are ISO 3166-1
44
+ * alpha-2, case-insensitive.
45
+ */
46
+ export function isGdprCountry(countryCode) {
47
+ if (!countryCode || countryCode.toLowerCase() === GEO_COUNTRY_UNKNOWN) {
48
+ return true;
49
+ }
50
+ return GDPR_COUNTRY_CODES.has(countryCode.toUpperCase());
51
+ }
52
+ /** US states with comprehensive privacy laws, lowercase full names (wpcom `CCPARegions`). */
53
+ export const CCPA_REGIONS = new Set([
54
+ "california",
55
+ "colorado",
56
+ "connecticut",
57
+ "delaware",
58
+ "florida",
59
+ "indiana",
60
+ "iowa",
61
+ "kentucky",
62
+ "maryland",
63
+ "minnesota",
64
+ "montana",
65
+ "nebraska",
66
+ "new hampshire",
67
+ "new jersey",
68
+ "oregon",
69
+ "rhode island",
70
+ "tennessee",
71
+ "texas",
72
+ "utah",
73
+ "virginia",
74
+ ]);
75
+ /**
76
+ * CCPA applicability FAILS OPEN: an unknown or missing region is NOT assumed
77
+ * to be a CCPA region (wpcom `pertainsToCCPA`: "if we aren't sure what the
78
+ * region is we shouldn't assume it pertains to CCPA"). Matches on substring,
79
+ * like wpcom, so a region string such as "california" or "California, US"
80
+ * both qualify.
81
+ */
82
+ export function isCcpaRegion(countryCode, region) {
83
+ if (countryCode && countryCode.toUpperCase() !== "US") {
84
+ return false;
85
+ }
86
+ if (!region || region.toLowerCase() === GEO_COUNTRY_UNKNOWN) {
87
+ return false;
88
+ }
89
+ const normalized = region.toLowerCase();
90
+ return [...CCPA_REGIONS].some((candidate) => normalized.includes(candidate));
91
+ }
@@ -16,17 +16,16 @@ export declare function createPublishFormData(input: {
16
16
  upload: DirectPublishUpload;
17
17
  clientAttribution: string;
18
18
  spaceId?: string | undefined;
19
+ teamId?: string | undefined;
19
20
  slug?: string | undefined;
20
21
  title?: string | undefined;
21
22
  description?: string | undefined;
22
23
  /** Live promotion intent for archive builds; null produces an unpromoted ready version. */
23
24
  channel?: "live" | null | undefined;
24
25
  /**
25
- * Control-plane-only provenance for the version's build/deploy read models
26
- * a compiled runtime (Zero capsule, Functions worker) declares its artifact
27
- * here under its own key (e.g. `statticZero`). Same shape the CLI sends on the
28
- * declared-session path; the multipart intake reads it from the `metadata`
29
- * JSON field and folds it into `publishRequestSchema.sourceMetadata`.
26
+ * Build provenance for the version's build/deploy read models. A compiled
27
+ * runtime declares its artifact under its own key (e.g. `statticZero`); the
28
+ * multipart intake folds it into `publishRequestSchema.sourceMetadata`.
30
29
  */
31
30
  sourceMetadata?: Record<string, unknown> | undefined;
32
31
  /** Bounded build transcript recorded on the version when a build ran. */
@@ -3,11 +3,10 @@ export function createPublishFormData(input) {
3
3
  const form = new FormData();
4
4
  if (input.spaceId)
5
5
  form.append("spaceId", input.spaceId);
6
+ if (input.teamId)
7
+ form.append("teamId", input.teamId);
6
8
  if (input.description)
7
9
  form.append("description", input.description);
8
- // The multipart intake merges this JSON field into the parsed publish body
9
- // (it reads `payload`/`publish`/`metadata`), so build provenance rides the
10
- // one-shot form exactly as it rides the CLI's declared-session JSON body.
11
10
  if (input.sourceMetadata || input.buildLog || input.channel !== undefined) {
12
11
  form.append("metadata", JSON.stringify({
13
12
  ...(input.sourceMetadata ? { sourceMetadata: input.sourceMetadata } : {}),