@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
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Canonical geometry for the Spacefast Open Graph image exported from Figma.
3
+ *
4
+ * Renderers may use different engines, but they must consume these dimensions
5
+ * and positions so every first-party surface produces the same template.
6
+ */
7
+ export const SPACEFAST_OG_TEMPLATE = {
8
+ width: 2400,
9
+ height: 800,
10
+ background: "#fcfcfa",
11
+ grid: {
12
+ size: 80,
13
+ stroke: "#e8e8e5",
14
+ strokeWidth: 2,
15
+ },
16
+ title: {
17
+ left: 120,
18
+ top: 96,
19
+ width: 820,
20
+ fontFamily: "Recoleta",
21
+ fontWeight: 400,
22
+ fontSize: 112,
23
+ lineHeight: 0.9,
24
+ letterSpacing: -4,
25
+ },
26
+ wordmark: {
27
+ left: 136,
28
+ bottom: 96,
29
+ width: 360,
30
+ height: 68,
31
+ },
32
+ globe: {
33
+ left: 1178,
34
+ top: 96,
35
+ width: 160,
36
+ height: 160,
37
+ },
38
+ pin: {
39
+ left: 1602,
40
+ top: -82,
41
+ width: 426,
42
+ height: 402,
43
+ },
44
+ people: {
45
+ left: 1290,
46
+ top: 386,
47
+ width: 490,
48
+ height: 421,
49
+ },
50
+ branch: {
51
+ left: 2036,
52
+ top: 336,
53
+ width: 256,
54
+ height: 264,
55
+ },
56
+ };
@@ -1,4 +1,3 @@
1
- import { MANAGEMENT_LABEL_PREFIX } from "../config/domains.js";
2
1
  import { INFRA_SLUG_BLOCKLIST, PRODUCT_PLATFORM_SLUG_BLOCKLIST, SUBSTRING_SLUG_BLOCKLIST, TRADEMARK_EXACT_SLUG_BLOCKLIST, } from "./blocklist.js";
3
2
  export { INFRA_SLUG_BLOCKLIST, PRODUCT_PLATFORM_SLUG_BLOCKLIST, SUBSTRING_SLUG_BLOCKLIST, TRADEMARK_EXACT_SLUG_BLOCKLIST, } from "./blocklist.js";
4
3
  export const SLUG_MIN_LENGTH = 3;
@@ -9,8 +8,8 @@ const productPlatformReservedSlugs = new Set(PRODUCT_PLATFORM_SLUG_BLOCKLIST);
9
8
  const trademarkExactReservedSlugs = new Set(TRADEMARK_EXACT_SLUG_BLOCKLIST);
10
9
  export function evaluateSlugCandidate(input, options = {}) {
11
10
  const maxLength = options.maxLength ?? TEAM_SLUG_MAX_LENGTH;
12
- // Explicit user choices are rejected rather than silently reshaped. Case,
13
- // surrounding whitespace, and diacritics are the only accepted canonicalization.
11
+ // Case, surrounding whitespace, and diacritics are the only canonicalization.
12
+ // Anything else is rejected rather than silently reshaped.
14
13
  const casefolded = input
15
14
  .trim()
16
15
  .normalize("NFD")
@@ -47,20 +46,15 @@ function evaluateStructuralRules(normalized, structuralCandidate, maxLength) {
47
46
  if (/^[0-9]+$/.test(normalized)) {
48
47
  return invalid("Slug cannot be all numeric.");
49
48
  }
50
- if (normalized.startsWith(MANAGEMENT_LABEL_PREFIX)) {
51
- return reserved(`Slug cannot start with reserved prefix ${MANAGEMENT_LABEL_PREFIX}.`);
52
- }
53
49
  return { ok: true, normalized };
54
50
  }
55
- // A substring reservation survives numeric suffixing (`paypal-2` still
56
- // contains `paypal`). Suggestion generators use this structured signal to
57
- // abandon the base instead of exhausting every suffix.
51
+ // A substring reservation survives numeric suffixing (`paypal-2` still contains
52
+ // `paypal`), so suggestion generators abandon the base instead of trying suffixes.
58
53
  function matchingReservedSubstring(normalized) {
59
54
  return SUBSTRING_SLUG_BLOCKLIST.find((reservedSubstring) => normalized.includes(reservedSubstring));
60
55
  }
61
56
  export function slugPolicyFailureSurvivesSuffix(normalized) {
62
- return (matchingReservedSubstring(normalized) !== undefined ||
63
- normalized.startsWith(MANAGEMENT_LABEL_PREFIX));
57
+ return matchingReservedSubstring(normalized) !== undefined;
64
58
  }
65
59
  function evaluateReservedRules(normalized) {
66
60
  if (infraReservedSlugs.has(normalized)) {
@@ -2,6 +2,7 @@ import { errorDocsUrl, errorTitle, PROBLEM_CONTENT_TYPE } from "../contracts/err
2
2
  /** The original fetch implementation captured before a test installs a stub. */
3
3
  export const realFetch = globalThis.fetch;
4
4
  export function stubFetch(handler) {
5
+ // SAFETY: The wrapper accepts fetch's parameter types and always resolves to the handler's Response.
5
6
  globalThis.fetch = (async (input, init) => handler(input instanceof Request && init === undefined ? input : new Request(input, init)));
6
7
  }
7
8
  export function restoreFetch() {
@@ -1,10 +1,17 @@
1
1
  /**
2
- * Builds a browser handoff URL around a short-lived, one-use exchange token.
3
- *
4
- * The durable credential never appears in this URL. The access origin consumes
5
- * the final path segment once and immediately redirects browsers to a clean
6
- * path-scoped session. `publicId` is used only by agent handoffs, whose clean
7
- * static document remains `/agent/<id>`.
2
+ * The opaque one-use token every browser-handoff URL ends with. Exported
3
+ * because the agent-handoff grammar is built on it and several entrypoints
4
+ * (the CLI's argv sniffer among them) need the shape without paying for the
5
+ * agent registry that `docs/agent-setup.ts` pulls in.
6
+ */
7
+ export declare const BROWSER_HANDOFF_TOKEN_PATTERN: RegExp;
8
+ /** Path shape of an agent-handoff link, for callers that have no expected origin. */
9
+ export declare const AGENT_HANDOFF_PATH_PATTERN: RegExp;
10
+ /**
11
+ * A browser handoff URL around a short-lived, one-use exchange token. The
12
+ * durable credential never appears in it: the access origin consumes the final
13
+ * path segment once, then redirects to a clean path-scoped session. A caller can
14
+ * add a non-secret `publicId` only when its protocol genuinely needs one.
8
15
  */
9
16
  export declare function createBrowserCredentialUrl(input: {
10
17
  origin: string;
@@ -1,4 +1,13 @@
1
- const browserHandoffSegmentPattern = /^[A-Za-z0-9_-]{16,512}$/;
1
+ /**
2
+ * The opaque one-use token every browser-handoff URL ends with. Exported
3
+ * because the agent-handoff grammar is built on it and several entrypoints
4
+ * (the CLI's argv sniffer among them) need the shape without paying for the
5
+ * agent registry that `docs/agent-setup.ts` pulls in.
6
+ */
7
+ export const BROWSER_HANDOFF_TOKEN_PATTERN = /^[A-Za-z0-9_-]{16,512}$/;
8
+ /** Path shape of an agent-handoff link, for callers that have no expected origin. */
9
+ export const AGENT_HANDOFF_PATH_PATTERN = /^\/handoffs\/[A-Za-z0-9_-]{16,512}\/?$/;
10
+ const browserHandoffSegmentPattern = BROWSER_HANDOFF_TOKEN_PATTERN;
2
11
  const browserHandoffPublicIdPattern = /^[A-Za-z0-9_-]{1,512}$/;
3
12
  function requireBrowserHandoffSegment(value) {
4
13
  if (!browserHandoffSegmentPattern.test(value)) {
@@ -7,12 +16,10 @@ function requireBrowserHandoffSegment(value) {
7
16
  return value;
8
17
  }
9
18
  /**
10
- * Builds a browser handoff URL around a short-lived, one-use exchange token.
11
- *
12
- * The durable credential never appears in this URL. The access origin consumes
13
- * the final path segment once and immediately redirects browsers to a clean
14
- * path-scoped session. `publicId` is used only by agent handoffs, whose clean
15
- * static document remains `/agent/<id>`.
19
+ * A browser handoff URL around a short-lived, one-use exchange token. The
20
+ * durable credential never appears in it: the access origin consumes the final
21
+ * path segment once, then redirects to a clean path-scoped session. A caller can
22
+ * add a non-secret `publicId` only when its protocol genuinely needs one.
16
23
  */
17
24
  export function createBrowserCredentialUrl(input) {
18
25
  const origin = new URL(input.origin);
@@ -1,3 +1,4 @@
1
+ import { type SpaceCronConfig } from "../contracts/space-config.js";
1
2
  export type BuildSettingsInput = {
2
3
  fileNames: Iterable<string>;
3
4
  packageJson?: string | null | undefined;
@@ -24,7 +25,7 @@ export type BuildAnalyzerCandidateRankInput = {
24
25
  rootDirectory: string | null;
25
26
  fileNames: Iterable<string>;
26
27
  };
27
- export declare function hasWorkspaceSignal(fileNames: Iterable<string>, packageJsonText?: string | null): unknown;
28
+ export declare function hasWorkspaceSignal(fileNames: Iterable<string>, packageJsonText?: string | null): boolean;
28
29
  export declare function rankBuildAnalyzerCandidate(input: BuildAnalyzerCandidateRankInput): {
29
30
  confidence: number;
30
31
  reasons: string[];
@@ -43,15 +44,34 @@ export type PlatformConfig = {
43
44
  redirects: Array<Record<string, unknown>>;
44
45
  rewrites: Array<Record<string, unknown>>;
45
46
  headers: Array<Record<string, unknown>>;
47
+ crons: Array<Record<string, unknown>>;
48
+ unsupportedFeatures: string[];
49
+ };
50
+ export type NetlifyBuildPlugin = {
51
+ package: string;
52
+ inputs: NetlifyBuildPluginInputs;
53
+ };
54
+ export type NetlifyBuildPluginInputValue = null | boolean | number | string | NetlifyBuildPluginInputValue[] | {
55
+ [name: string]: NetlifyBuildPluginInputValue;
56
+ };
57
+ export type NetlifyBuildPluginInputs = {
58
+ [name: string]: NetlifyBuildPluginInputValue;
59
+ };
60
+ export type NetlifyBuildConfiguration = {
61
+ environment: Record<string, string>;
62
+ plugins: NetlifyBuildPlugin[];
46
63
  unsupportedFeatures: string[];
47
64
  };
48
65
  export declare function stringField(value: Record<string, unknown> | null, key: string): string | null;
49
66
  export declare function numberField(value: Record<string, unknown> | null, key: string): number | null;
67
+ export declare function parseNetlifyBuildConfiguration(value: string | null | undefined): NetlifyBuildConfiguration | null;
50
68
  export declare function detectPlatformConfigs(input: Omit<BuildSettingsInput, "fileNames" | "packageJson">): PlatformConfig[];
51
69
  export type PlatformRoutingConversion = {
52
70
  redirectLines: string[];
53
71
  headerBlocks: string[];
72
+ crons: SpaceCronConfig[];
54
73
  notes: string[];
55
74
  };
56
75
  export declare function convertPlatformRouting(configs: PlatformConfig[]): PlatformRoutingConversion;
76
+ export declare const BUILD_MARKER_FILE_NAMES: Set<string>;
57
77
  export declare function detectBuildSettings(input: BuildSettingsInput): DetectedBuildSettings;
@@ -1,5 +1,7 @@
1
1
  import { load as parseYaml } from "js-yaml";
2
2
  import { parse as parseToml } from "smol-toml";
3
+ import { z } from "zod";
4
+ import { SPACE_CONFIG_CAPS, spaceCronKey, spaceCronSchema, } from "../contracts/space-config.js";
3
5
  export const BUILD_ANALYZER_CANDIDATE_ROOTS = ["apps", "packages", "sites", "docs"];
4
6
  const PACKAGE_MANAGER_ONLY_FILES = new Set([
5
7
  "package.json",
@@ -12,12 +14,15 @@ const PACKAGE_MANAGER_ONLY_FILES = new Set([
12
14
  "Gemfile",
13
15
  "Gemfile.lock",
14
16
  ]);
17
+ const WORKSPACE_ROOT_FILES = new Set([
18
+ "pnpm-workspace.yaml",
19
+ "turbo.json",
20
+ "nx.json",
21
+ "lerna.json",
22
+ ]);
15
23
  export function hasWorkspaceSignal(fileNames, packageJsonText) {
16
24
  const names = new Set(fileNames);
17
- if (names.has("pnpm-workspace.yaml") ||
18
- names.has("turbo.json") ||
19
- names.has("nx.json") ||
20
- names.has("lerna.json")) {
25
+ if ([...WORKSPACE_ROOT_FILES].some((file) => names.has(file))) {
21
26
  return true;
22
27
  }
23
28
  if (!packageJsonText) {
@@ -25,10 +30,14 @@ export function hasWorkspaceSignal(fileNames, packageJsonText) {
25
30
  }
26
31
  try {
27
32
  const parsed = JSON.parse(packageJsonText);
28
- return (Array.isArray(parsed.workspaces) ||
29
- (parsed.workspaces &&
30
- typeof parsed.workspaces === "object" &&
31
- Array.isArray(parsed.workspaces.packages)));
33
+ if (Array.isArray(parsed.workspaces)) {
34
+ return true;
35
+ }
36
+ if (!parsed.workspaces || typeof parsed.workspaces !== "object") {
37
+ return false;
38
+ }
39
+ // SAFETY: The preceding non-null object check proves workspaces can carry an optional packages property.
40
+ return Array.isArray(parsed.workspaces.packages);
32
41
  }
33
42
  catch {
34
43
  return false;
@@ -72,12 +81,15 @@ export function hasBuildAnalyzerCandidateSignal(input) {
72
81
  input.detected.buildCommand !== null ||
73
82
  names.has("index.html"));
74
83
  }
84
+ const BUILD_ENVIRONMENT_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
85
+ const netlifyBuildEnvironmentValueSchema = z.union([z.string(), z.number(), z.boolean()]);
75
86
  function parseJsonObject(value) {
76
87
  if (!value) {
77
88
  return null;
78
89
  }
79
90
  try {
80
91
  const parsed = JSON.parse(value);
92
+ // SAFETY: The conditional establishes parsed is a non-null, non-array object before returning it as a dictionary.
81
93
  return parsed && typeof parsed === "object" && !Array.isArray(parsed)
82
94
  ? parsed
83
95
  : null;
@@ -98,6 +110,7 @@ function parseTomlObject(value) {
98
110
  }
99
111
  try {
100
112
  const parsed = parseToml(value);
113
+ // SAFETY: The conditional establishes parsed is a non-null, non-array object before returning it as a dictionary.
101
114
  return parsed && typeof parsed === "object" && !Array.isArray(parsed)
102
115
  ? parsed
103
116
  : null;
@@ -112,6 +125,7 @@ function parseYamlObject(value) {
112
125
  }
113
126
  try {
114
127
  const parsed = parseYaml(value);
128
+ // SAFETY: The conditional establishes parsed is a non-null, non-array object before returning it as a dictionary.
115
129
  return parsed && typeof parsed === "object" && !Array.isArray(parsed)
116
130
  ? parsed
117
131
  : null;
@@ -130,6 +144,7 @@ export function numberField(value, key) {
130
144
  }
131
145
  function objectField(value, key) {
132
146
  const field = value?.[key];
147
+ // SAFETY: The conditional establishes field is a non-null, non-array object before returning it as a dictionary.
133
148
  return field && typeof field === "object" && !Array.isArray(field)
134
149
  ? field
135
150
  : null;
@@ -140,6 +155,54 @@ function arrayField(value, key) {
140
155
  ? field.filter((entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry))
141
156
  : [];
142
157
  }
158
+ function netlifyBuildEnvironment(build) {
159
+ const environment = objectField(build, "environment");
160
+ const values = {};
161
+ const unsupportedFeatures = [];
162
+ for (const [name, value] of Object.entries(environment ?? {})) {
163
+ if (!BUILD_ENVIRONMENT_NAME.test(name)) {
164
+ unsupportedFeatures.push(`Netlify build environment variable "${name}" has an invalid name.`);
165
+ continue;
166
+ }
167
+ const parsed = netlifyBuildEnvironmentValueSchema.safeParse(value);
168
+ if (parsed.success) {
169
+ values[name] = String(parsed.data);
170
+ continue;
171
+ }
172
+ unsupportedFeatures.push(`Netlify build environment variable "${name}" must have a scalar value.`);
173
+ }
174
+ return { values, unsupportedFeatures };
175
+ }
176
+ function netlifyBuildPlugins(config) {
177
+ const plugins = [];
178
+ const unsupportedFeatures = [];
179
+ for (const entry of arrayField(config, "plugins")) {
180
+ const packageName = stringField(entry, "package");
181
+ if (!packageName) {
182
+ unsupportedFeatures.push("A Netlify build plugin is missing its package name.");
183
+ continue;
184
+ }
185
+ const inputsResult = z
186
+ .record(z.string(), z.json())
187
+ .safeParse(objectField(entry, "inputs") ?? {});
188
+ const inputs = inputsResult.success ? inputsResult.data : {};
189
+ plugins.push({ package: packageName, inputs });
190
+ }
191
+ return { plugins, unsupportedFeatures };
192
+ }
193
+ export function parseNetlifyBuildConfiguration(value) {
194
+ const config = parseTomlObject(value);
195
+ if (!config) {
196
+ return null;
197
+ }
198
+ const environment = netlifyBuildEnvironment(objectField(config, "build"));
199
+ const plugins = netlifyBuildPlugins(config);
200
+ return {
201
+ environment: environment.values,
202
+ plugins: plugins.plugins,
203
+ unsupportedFeatures: [...environment.unsupportedFeatures, ...plugins.unsupportedFeatures],
204
+ };
205
+ }
143
206
  function vercelConfig(value) {
144
207
  const config = parseJsonObjectWithComments(value);
145
208
  if (!config) {
@@ -155,11 +218,11 @@ function vercelConfig(value) {
155
218
  redirects: arrayField(config, "redirects"),
156
219
  rewrites: arrayField(config, "rewrites"),
157
220
  headers: arrayField(config, "headers"),
221
+ crons: arrayField(config, "crons"),
158
222
  unsupportedFeatures: [
159
223
  ...("functions" in config ? ["Vercel functions are not converted."] : []),
160
224
  ...("middleware" in config ? ["Vercel middleware is not converted."] : []),
161
225
  ...("images" in config ? ["Vercel image optimization config is not converted."] : []),
162
- ...("crons" in config ? ["Vercel cron jobs are not converted."] : []),
163
226
  ],
164
227
  };
165
228
  }
@@ -169,6 +232,7 @@ function netlifyConfig(value) {
169
232
  return null;
170
233
  }
171
234
  const build = objectField(config, "build");
235
+ const nativeBuild = parseNetlifyBuildConfiguration(value);
172
236
  return {
173
237
  platform: "netlify",
174
238
  rootDirectory: stringField(build, "base") ?? stringField(config, "base"),
@@ -179,10 +243,13 @@ function netlifyConfig(value) {
179
243
  redirects: arrayField(config, "redirects"),
180
244
  rewrites: [],
181
245
  headers: arrayField(config, "headers"),
246
+ // Netlify has no scheduled-request config: its scheduled functions are
247
+ // declared in function code, which this importer does not read.
248
+ crons: [],
182
249
  unsupportedFeatures: [
183
250
  ...("functions" in config ? ["Netlify functions are not converted."] : []),
184
251
  ...("edge_functions" in config ? ["Netlify edge functions are not converted."] : []),
185
- ...("plugins" in config ? ["Netlify build plugins are not converted."] : []),
252
+ ...(nativeBuild?.unsupportedFeatures ?? []),
186
253
  ...("processing" in config ? ["Netlify post-processing config is not converted."] : []),
187
254
  ],
188
255
  };
@@ -224,6 +291,7 @@ function cloudflarePagesConfig(input) {
224
291
  redirects: [],
225
292
  rewrites: [],
226
293
  headers: [],
294
+ crons: [],
227
295
  unsupportedFeatures,
228
296
  };
229
297
  }
@@ -276,7 +344,7 @@ function vercelDestination(destination, splat) {
276
344
  converted = converted.replaceAll("$1", ":splat");
277
345
  }
278
346
  else if (splat) {
279
- // `:name`, `:name*`, or `:name+` but not a longer name sharing the prefix.
347
+ // `:name`, `:name*`, or `:name+`, but not a longer name sharing the prefix.
280
348
  const reference = new RegExp(`\\${splat}[*+]?(?![A-Za-z0-9_])`, "g");
281
349
  converted = converted.replace(reference, ":splat");
282
350
  }
@@ -387,6 +455,7 @@ function convertVercelHeaderRule(rule, out) {
387
455
  const entries = rule.headers;
388
456
  const headers = (Array.isArray(entries) ? entries : [])
389
457
  .map((entry) => {
458
+ // SAFETY: The conditional establishes entry is a non-null object before reading dictionary properties.
390
459
  const header = entry && typeof entry === "object" ? entry : null;
391
460
  const key = stringField(header, "key");
392
461
  const value = header && typeof header.value === "string" ? header.value : null;
@@ -425,8 +494,43 @@ function convertNetlifyHeaderRule(rule, out) {
425
494
  }
426
495
  out.headerBlocks.push(headerBlock(matcher, headers));
427
496
  }
497
+ /**
498
+ * One `vercel.json` cron entry into an `sf.jsonc` `crons` entry. Both sides are
499
+ * `{ path, schedule }`, so a valid entry converts verbatim. What this guards is
500
+ * the tail: Vercel accepts paths and schedules our grammar does not, and has no
501
+ * per-project cap. Every rejection names its entry, because a cron that
502
+ * silently stops existing is the migration failure nobody notices.
503
+ */
504
+ function convertVercelCron(rule, out) {
505
+ const path = stringField(rule, "path");
506
+ const schedule = stringField(rule, "schedule");
507
+ const label = `vercel.json cron ${path ?? "(missing path)"}`;
508
+ if (!path || !schedule) {
509
+ out.notes.push(`${label}: skipped — missing path or schedule.`);
510
+ return;
511
+ }
512
+ const parsed = spaceCronSchema.safeParse({ path, schedule });
513
+ if (!parsed.success) {
514
+ out.notes.push(`${label}: skipped — ${parsed.error.issues.map((issue) => issue.message).join("; ")}`);
515
+ return;
516
+ }
517
+ if (out.crons.some((entry) => spaceCronKey(entry.path) === spaceCronKey(path))) {
518
+ out.notes.push(`${label}: skipped — another cron already schedules this path.`);
519
+ return;
520
+ }
521
+ if (out.crons.length >= SPACE_CONFIG_CAPS.crons) {
522
+ out.notes.push(`${label}: skipped — at most ${SPACE_CONFIG_CAPS.crons} crons are supported.`);
523
+ return;
524
+ }
525
+ out.crons.push(parsed.data);
526
+ }
428
527
  export function convertPlatformRouting(configs) {
429
- const out = { redirectLines: [], headerBlocks: [], notes: [] };
528
+ const out = {
529
+ redirectLines: [],
530
+ headerBlocks: [],
531
+ crons: [],
532
+ notes: [],
533
+ };
430
534
  for (const config of configs) {
431
535
  if (config.platform === "vercel") {
432
536
  for (const rule of config.redirects) {
@@ -438,6 +542,9 @@ export function convertPlatformRouting(configs) {
438
542
  for (const rule of config.headers) {
439
543
  convertVercelHeaderRule(rule, out);
440
544
  }
545
+ for (const rule of config.crons) {
546
+ convertVercelCron(rule, out);
547
+ }
441
548
  }
442
549
  if (config.platform === "netlify") {
443
550
  // Netlify expresses rewrites as 200-status redirects; both arrive in
@@ -542,9 +649,8 @@ const FRAMEWORKS = [
542
649
  defaultBuildCommand: "vinxi build",
543
650
  },
544
651
  {
545
- // Qwik City starters build with the repo's `build` script (`qwik build`
546
- // orchestrates the package.json build.* scripts, so there is no
547
- // meaningful bare-binary default); static output lands in dist.
652
+ // Qwik City builds through the repo's `build` script, so there is no
653
+ // meaningful bare-binary default; static output lands in dist.
548
654
  preset: "qwik",
549
655
  dependency: "@builder.io/qwik-city",
550
656
  outputDirectory: "dist",
@@ -671,6 +777,18 @@ const FRAMEWORKS = [
671
777
  defaultBuildCommand: "bundle exec middleman build",
672
778
  },
673
779
  ];
780
+ // Every file name the build analyzer keys on. An archive carrying any of them
781
+ // is a project to build, not a website.
782
+ export const BUILD_MARKER_FILE_NAMES = new Set([
783
+ ...PACKAGE_MANAGER_ONLY_FILES,
784
+ ...WORKSPACE_ROOT_FILES,
785
+ "vercel.json",
786
+ "netlify.toml",
787
+ "wrangler.toml",
788
+ "wrangler.json",
789
+ "wrangler.jsonc",
790
+ ...FRAMEWORKS.flatMap((framework) => framework.configFiles ?? []),
791
+ ]);
674
792
  function frameworkForPreset(frameworkPreset) {
675
793
  return FRAMEWORKS.find((framework) => framework.preset === frameworkPreset) ?? null;
676
794
  }
@@ -1,10 +1,8 @@
1
1
  /**
2
2
  * Recursively key-sorted JSON, so a value round-tripped through Postgres jsonb
3
- * (which canonicalizes key order) digests identically to one freshly built in
4
- * JS. Arrays keep their original order since order is meaningful there.
3
+ * digests identically to one freshly built in JS. Arrays keep their order.
5
4
  *
6
- * Every digest that has to agree across the control plane, the CLI and the
7
- * capsule compiler is built on this exact spelling change it and stored
8
- * hashes stop matching freshly computed ones.
5
+ * The control plane, the CLI and the capsule compiler all digest this exact
6
+ * spelling. Change it and stored hashes stop matching new ones.
9
7
  */
10
8
  export declare function canonicalJson(value: unknown): string;
@@ -1,17 +1,16 @@
1
1
  /**
2
2
  * Recursively key-sorted JSON, so a value round-tripped through Postgres jsonb
3
- * (which canonicalizes key order) digests identically to one freshly built in
4
- * JS. Arrays keep their original order since order is meaningful there.
3
+ * digests identically to one freshly built in JS. Arrays keep their order.
5
4
  *
6
- * Every digest that has to agree across the control plane, the CLI and the
7
- * capsule compiler is built on this exact spelling change it and stored
8
- * hashes stop matching freshly computed ones.
5
+ * The control plane, the CLI and the capsule compiler all digest this exact
6
+ * spelling. Change it and stored hashes stop matching new ones.
9
7
  */
10
8
  export function canonicalJson(value) {
11
9
  if (Array.isArray(value)) {
12
10
  return `[${value.map(canonicalJson).join(",")}]`;
13
11
  }
14
12
  if (value && typeof value === "object") {
13
+ // SAFETY: This branch establishes value is a non-null object before enumerating its entries.
15
14
  return `{${Object.entries(value)
16
15
  .toSorted(([left], [right]) => left.localeCompare(right))
17
16
  .map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
@@ -1,23 +1,11 @@
1
- /**
2
- * How a client addresses Cast over Phoenix. Both Spacefast clients — the
3
- * dashboard's notice subscription and the injected Collab SDK — build their
4
- * socket and their topics here, and nowhere else.
5
- */
1
+ /** Every Spacefast client builds its Cast socket and topics here, and nowhere else. */
6
2
  /**
7
3
  * The Phoenix topic a Cast room's messages travel on.
8
4
  *
9
- * Phoenix routes a broadcast by the *literal* topic string. Cast broadcasts
10
- * anything computed outside a channel process Space notices, REST-published
11
- * room events to the topic `Cast.RoomTopic.for/2` builds, so a client that
12
- * encodes the room key any other way joins a real and permanently silent room.
13
- * Both sides therefore encode exactly once, here and in that module: the `~n`
14
- * table first (it survives round-tripping a key that itself contains `~` or
15
- * `%`), then `encodeURIComponent`.
16
- *
17
- * Proven by delivery, not by inspection: `./cast-transport.test.ts` pins the
18
- * strings, and on the other side `room_channel_notice_test.exs` and
19
- * `spacefast_inbox_fanout_test.exs` join those literal topics and assert a real
20
- * Cast broadcast arrives on them.
5
+ * Phoenix routes a broadcast by the *literal* topic string, so a client that
6
+ * encodes the room key differently from `Cast.RoomTopic.for/2` joins a real and
7
+ * permanently silent room. Both sides encode once: the `~n` table first, then
8
+ * `encodeURIComponent`.
21
9
  */
22
10
  export declare function castRoomTopic(resourceKey: string, roomKey: string): string;
23
11
  /**
@@ -1,12 +1,7 @@
1
- /**
2
- * How a client addresses Cast over Phoenix. Both Spacefast clients — the
3
- * dashboard's notice subscription and the injected Collab SDK — build their
4
- * socket and their topics here, and nowhere else.
5
- */
1
+ /** Every Spacefast client builds its Cast socket and topics here, and nowhere else. */
6
2
  /**
7
3
  * `~n` escapes, the inverse of `CastWeb.ApiContext.decode_path_segment/1`.
8
- * `~` goes first in intent: the map is applied in one pass, so an already
9
- * escaped marker can never be escaped twice.
4
+ * Applied in one pass with `~` first, so an escaped marker is never escaped twice.
10
5
  */
11
6
  const ESCAPED = new Map([
12
7
  ["~", "~0"],
@@ -19,18 +14,10 @@ const ESCAPED = new Map([
19
14
  /**
20
15
  * The Phoenix topic a Cast room's messages travel on.
21
16
  *
22
- * Phoenix routes a broadcast by the *literal* topic string. Cast broadcasts
23
- * anything computed outside a channel process Space notices, REST-published
24
- * room events to the topic `Cast.RoomTopic.for/2` builds, so a client that
25
- * encodes the room key any other way joins a real and permanently silent room.
26
- * Both sides therefore encode exactly once, here and in that module: the `~n`
27
- * table first (it survives round-tripping a key that itself contains `~` or
28
- * `%`), then `encodeURIComponent`.
29
- *
30
- * Proven by delivery, not by inspection: `./cast-transport.test.ts` pins the
31
- * strings, and on the other side `room_channel_notice_test.exs` and
32
- * `spacefast_inbox_fanout_test.exs` join those literal topics and assert a real
33
- * Cast broadcast arrives on them.
17
+ * Phoenix routes a broadcast by the *literal* topic string, so a client that
18
+ * encodes the room key differently from `Cast.RoomTopic.for/2` joins a real and
19
+ * permanently silent room. Both sides encode once: the `~n` table first, then
20
+ * `encodeURIComponent`.
34
21
  */
35
22
  export function castRoomTopic(resourceKey, roomKey) {
36
23
  let escaped = "";
@@ -1,6 +1,5 @@
1
1
  // The space key (`sfc_…`, or the legacy five-word form) is the one claim
2
- // credential; it authenticates as a `Bearer` header and rides claim-link
3
- // fragments verbatim.
2
+ // credential: it rides `Bearer` headers and claim-link fragments verbatim.
4
3
  export function normalizeClaimToken(token) {
5
4
  const trimmed = token?.trim();
6
5
  if (!trimmed) {
@@ -21,12 +21,10 @@ export function normalizeCommentAvatarUrl(value) {
21
21
  if (source.origin === CANONICAL_GRAVATAR_ORIGIN || source.origin === AVATAR_PROXY_ORIGIN) {
22
22
  return source.href.length <= MAX_AVATAR_URL_LENGTH ? source.href : null;
23
23
  }
24
- // Assign the proxy path instead of parsing `<host><path>` as a URL. A host
25
- // with a non-default port contains a colon and would otherwise become a
26
- // made-up scheme (`javascript:8443/...`, for example). Photon addresses an
27
- // HTTPS source with `ssl=1`; without it, an HTTPS and HTTP source collapse
28
- // to the same proxy URL. `source.host` deliberately retains an explicit
29
- // non-default port.
24
+ // Assign the proxy path instead of parsing `<host><path>` as a URL: a host
25
+ // with a non-default port carries a colon and would become a made-up scheme
26
+ // (`javascript:8443/...`). `ssl=1` keeps an HTTPS and an HTTP source from
27
+ // collapsing to the same proxy URL.
30
28
  const proxied = new URL(AVATAR_PROXY_ORIGIN);
31
29
  proxied.pathname = `/${source.host}${source.pathname}`;
32
30
  proxied.search = source.search;
@@ -10,15 +10,15 @@ export async function mapConcurrent(items, limit, mapper) {
10
10
  if (index >= items.length) {
11
11
  return;
12
12
  }
13
+ // SAFETY: the bounds check above proves this index addresses an item in the input array.
13
14
  results[index] = await mapper(items[index], index);
14
15
  await worker();
15
16
  }
16
17
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
17
18
  return results;
18
19
  }
19
- // Runs predicates in bounded batches while preserving Array.find semantics:
20
- // the earliest matching item wins, and only a rejection before that match is
21
- // observable. Later batches are never started after a match is found.
20
+ // Bounded batches with Array.find semantics: the earliest matching item wins,
21
+ // and only a rejection before that match is observable.
22
22
  export async function findConcurrent(items, limit, predicate) {
23
23
  if (!Number.isInteger(limit) || limit < 1) {
24
24
  throw new Error("Concurrency limit must be a positive integer.");