@spacefast/common 0.0.5 → 0.0.6

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 (150) hide show
  1. package/dist/agents/connect-targets.d.ts +133 -0
  2. package/dist/agents/connect-targets.js +224 -0
  3. package/dist/brand.d.ts +1 -0
  4. package/dist/brand.js +5 -0
  5. package/dist/config/domains.d.ts +12 -6
  6. package/dist/config/domains.js +30 -38
  7. package/dist/contracts/access.d.ts +825 -125
  8. package/dist/contracts/access.js +771 -148
  9. package/dist/contracts/activity.d.ts +1 -1
  10. package/dist/contracts/activity.js +28 -2
  11. package/dist/contracts/annotations.d.ts +2 -4
  12. package/dist/contracts/api-keys.d.ts +3 -4
  13. package/dist/contracts/api-keys.js +9 -6
  14. package/dist/contracts/archives.d.ts +8 -2
  15. package/dist/contracts/archives.js +11 -4
  16. package/dist/contracts/billing.d.ts +0 -9
  17. package/dist/contracts/builds.d.ts +25 -21
  18. package/dist/contracts/builds.js +12 -0
  19. package/dist/contracts/common.d.ts +2 -0
  20. package/dist/contracts/common.js +4 -0
  21. package/dist/contracts/continuation.d.ts +0 -3
  22. package/dist/contracts/countries.d.ts +2 -0
  23. package/dist/contracts/countries.js +261 -0
  24. package/dist/contracts/deployments.d.ts +34 -34
  25. package/dist/contracts/device-auth.d.ts +4 -11
  26. package/dist/contracts/device-auth.js +4 -0
  27. package/dist/contracts/docs.d.ts +119 -0
  28. package/dist/contracts/docs.js +51 -0
  29. package/dist/contracts/domains.d.ts +96 -54
  30. package/dist/contracts/domains.js +26 -4
  31. package/dist/contracts/enums.d.ts +15 -16
  32. package/dist/contracts/enums.js +25 -14
  33. package/dist/contracts/error-code-meta.d.ts +72 -0
  34. package/dist/contracts/error-code-meta.js +96 -10
  35. package/dist/contracts/error-codes.d.ts +1 -1
  36. package/dist/contracts/error-codes.js +14 -0
  37. package/dist/contracts/events.d.ts +1 -1
  38. package/dist/contracts/features.d.ts +76 -6
  39. package/dist/contracts/features.js +178 -13
  40. package/dist/contracts/git.d.ts +2 -8
  41. package/dist/contracts/ids.d.ts +5 -0
  42. package/dist/contracts/ids.js +5 -0
  43. package/dist/contracts/mcp.d.ts +8 -10
  44. package/dist/contracts/me.js +1 -1
  45. package/dist/contracts/oauth-resources.d.ts +19 -8
  46. package/dist/contracts/oauth-resources.js +44 -13
  47. package/dist/contracts/platform.d.ts +0 -102
  48. package/dist/contracts/platform.js +0 -50
  49. package/dist/contracts/push-new.d.ts +4 -4
  50. package/dist/contracts/quotas.d.ts +2 -0
  51. package/dist/contracts/quotas.js +8 -0
  52. package/dist/contracts/repository-connections.d.ts +6 -8
  53. package/dist/contracts/resources.d.ts +97 -13
  54. package/dist/contracts/resources.js +26 -2
  55. package/dist/contracts/routes.d.ts +0 -2
  56. package/dist/contracts/runtime-api.d.ts +361 -39
  57. package/dist/contracts/runtime-api.js +275 -6
  58. package/dist/contracts/sites.d.ts +19 -2
  59. package/dist/contracts/sites.js +21 -0
  60. package/dist/contracts/space-config.d.ts +201 -2
  61. package/dist/contracts/space-config.js +135 -5
  62. package/dist/contracts/spaces.d.ts +364 -193
  63. package/dist/contracts/spaces.js +40 -8
  64. package/dist/contracts/superadmin-emails.d.ts +4 -2
  65. package/dist/contracts/superadmin-emails.js +8 -0
  66. package/dist/contracts/superadmin-queues.d.ts +26 -26
  67. package/dist/contracts/superadmin-spaces.d.ts +170 -42
  68. package/dist/contracts/superadmin.d.ts +6 -93
  69. package/dist/contracts/superadmin.js +6 -14
  70. package/dist/contracts/tags.d.ts +10 -9
  71. package/dist/contracts/tags.js +2 -1
  72. package/dist/contracts/teams.d.ts +21 -22
  73. package/dist/contracts/teams.js +0 -1
  74. package/dist/contracts/theme-json.d.ts +30 -0
  75. package/dist/contracts/theme-json.js +48 -0
  76. package/dist/contracts/variables.d.ts +0 -2
  77. package/dist/contracts/variables.js +0 -5
  78. package/dist/contracts/webhooks.d.ts +2 -0
  79. package/dist/contracts/webhooks.js +1 -1
  80. package/dist/contracts/zero.d.ts +5 -10
  81. package/dist/docs/agent-prose.d.ts +43 -3
  82. package/dist/docs/agent-prose.js +146 -7
  83. package/dist/docs/agent-setup.js +45 -13
  84. package/dist/docs/agent-solutions.d.ts +5 -5
  85. package/dist/docs/agent-solutions.js +2 -2
  86. package/dist/docs/catalog.d.ts +650 -0
  87. package/dist/docs/catalog.js +758 -0
  88. package/dist/docs/error-docs.d.ts +0 -2
  89. package/dist/docs/error-docs.js +77 -35
  90. package/dist/docs/index-build.d.ts +17 -0
  91. package/dist/docs/index-build.js +24 -0
  92. package/dist/docs/search.d.ts +54 -0
  93. package/dist/docs/search.js +96 -0
  94. package/dist/docs/skill-distribution.d.ts +5 -3
  95. package/dist/docs/skill-distribution.js +8 -8
  96. package/dist/utils/access-match.d.ts +39 -0
  97. package/dist/utils/access-match.js +296 -0
  98. package/dist/utils/auth-redirect.d.ts +3 -1
  99. package/dist/utils/auth-redirect.js +21 -1
  100. package/dist/utils/build-settings.d.ts +6 -0
  101. package/dist/utils/build-settings.js +429 -46
  102. package/dist/utils/claim-token.d.ts +8 -0
  103. package/dist/utils/claim-token.js +23 -2
  104. package/dist/utils/content-type.d.ts +1 -0
  105. package/dist/utils/content-type.js +1 -1
  106. package/dist/utils/credential-policy.d.ts +54 -0
  107. package/dist/utils/{access-policy.js → credential-policy.js} +28 -2
  108. package/dist/utils/gate-theme.d.ts +61 -0
  109. package/dist/utils/gate-theme.js +217 -0
  110. package/dist/utils/git-repository.d.ts +0 -1
  111. package/dist/utils/git-repository.js +0 -3
  112. package/dist/utils/id-hints.d.ts +8 -0
  113. package/dist/utils/id-hints.js +61 -0
  114. package/dist/utils/local-space-state.d.ts +91 -0
  115. package/dist/utils/local-space-state.js +251 -0
  116. package/dist/utils/oauth-signed-query.d.ts +8 -0
  117. package/dist/utils/oauth-signed-query.js +26 -0
  118. package/dist/utils/publish-detection.d.ts +0 -8
  119. package/dist/utils/publish-detection.js +0 -93
  120. package/dist/utils/publish-policy.d.ts +6 -5
  121. package/dist/utils/publish-policy.js +71 -7
  122. package/dist/utils/query-keys.d.ts +15 -4
  123. package/dist/utils/query-keys.js +27 -4
  124. package/dist/utils/runtime-paths.d.ts +0 -1
  125. package/dist/utils/runtime-paths.js +0 -1
  126. package/dist/utils/runtime-upload-batch.d.ts +18 -0
  127. package/dist/utils/runtime-upload-batch.js +90 -0
  128. package/dist/utils/runtime-upload.d.ts +2 -0
  129. package/dist/utils/runtime-upload.js +5 -0
  130. package/dist/utils/space-config.d.ts +4 -0
  131. package/dist/utils/space-config.js +10 -1
  132. package/dist/utils/space-theme.d.ts +2 -0
  133. package/dist/utils/space-theme.js +9 -0
  134. package/dist/utils/static-runtime-policy.d.ts +0 -21
  135. package/dist/utils/static-runtime-policy.js +19 -103
  136. package/dist/utils/upload-session.d.ts +1 -1
  137. package/dist/utils/wpcom-auth-redirects.d.ts +32 -2
  138. package/dist/utils/wpcom-auth-redirects.js +26 -10
  139. package/dist/vocabulary.d.ts +130 -30
  140. package/dist/vocabulary.js +77 -36
  141. package/package.json +16 -1
  142. package/dist/contracts/account.d.ts +0 -28
  143. package/dist/contracts/account.js +0 -54
  144. package/dist/contracts/intercom.d.ts +0 -6
  145. package/dist/contracts/intercom.js +0 -5
  146. package/dist/contracts/telemetry.d.ts +0 -2
  147. package/dist/contracts/telemetry.js +0 -1
  148. package/dist/utils/access-policy.d.ts +0 -39
  149. package/dist/utils/error-display.d.ts +0 -7
  150. package/dist/utils/error-display.js +0 -42
@@ -4,8 +4,11 @@ export const qk = {
4
4
  accountSessions: () => ["account", "sessions"],
5
5
  accountConnections: () => ["account", "connections"],
6
6
  accountInvitations: () => ["account", "invitations"],
7
- apiTokens: () => ["api-tokens"],
7
+ // Results are team-filtered, so the team lands in the key; calling with no
8
+ // teamId yields the bare prefix (personal scope, and invalidate-all sweeps).
9
+ apiTokens: (teamId) => teamId === undefined ? ["api-tokens"] : ["api-tokens", teamId],
8
10
  agentAccounts: () => ["agent-accounts"],
11
+ myShelf: () => ["my-shelf"],
9
12
  mcpDashboard: () => ["mcp", "dashboard"],
10
13
  mcpApproval: (id) => ["mcp", "approval", id],
11
14
  // The teams LIST gets its own leaf so invalidating it never cascades into
@@ -31,13 +34,24 @@ export const qk = {
31
34
  spaceByRef: (teamRef, spaceRef) => ["spaces", "by-ref", teamRef, spaceRef],
32
35
  spaceVersions: (id) => ["spaces", id, "versions"],
33
36
  spaceCollaboration: (id) => ["spaces", id, "collaboration"],
37
+ spaceCollaborationEmbedConfig: (id) => ["spaces", id, "collaboration", "embed-config"],
34
38
  spaceAnnotations: (id) => ["spaces", id, "annotations"],
35
39
  spaceActivity: (id, limit) => ["spaces", id, "activity", limit],
36
40
  spaceDeployments: (id) => ["spaces", id, "deployments"],
37
41
  spaceTags: (id) => ["spaces", id, "tags"],
38
- spaceTagVersion: (id, versionId) => ["spaces", id, "tags", "versions", versionId],
42
+ spaceTagVersion: (id, versionId) => ["spaces", id, "tags", versionId],
43
+ spaceTagDiff: (id, versionId) => ["spaces", id, "tags", versionId, "diff"],
44
+ spaceTagReleases: (id) => ["spaces", id, "tags", "releases"],
45
+ // The merged (system+team+space) read-only view; never a write base — see
46
+ // `fetchSpaceReleasedGraph` in apps/my/src/lib/space-queries.ts.
47
+ spaceTagsEffective: (id) => ["spaces", id, "tags", "effective"],
39
48
  build: (id) => ["builds", id],
40
- buildLogs: (id) => ["builds", id, "logs"],
49
+ // Observers with different limits must not share a cache entry; calling with
50
+ // no limit yields the bare prefix for invalidating every limit at once.
51
+ buildLogs: (id, limit) => limit === undefined
52
+ ? ["builds", id, "logs"]
53
+ : ["builds", id, "logs", limit],
54
+ spaceBuilds: (id) => ["spaces", id, "builds"],
41
55
  spaceRepositoryConnection: (id, connectionType = "connected") => ["spaces", id, "repository-connection", connectionType],
42
56
  repositoryInstallations: (spaceId) => ["repository-connections", "installations", spaceId],
43
57
  repositoryInstallationRepositories: (installationId) => ["repository-connections", "installations", installationId, "repositories"],
@@ -47,7 +61,11 @@ export const qk = {
47
61
  webhook: (webhookId) => ["webhooks", webhookId],
48
62
  webhookDeliveries: (webhookId) => ["webhooks", webhookId, "deliveries"],
49
63
  spaceVersion: (id, versionId) => ["spaces", id, "versions", versionId],
50
- spaceVersionAnnotations: (id, versionId, status = "open") => ["spaces", id, "versions", versionId, "annotations", status],
64
+ // Calling with no status yields the bare prefix, for invalidating every
65
+ // status-scoped annotations list at once.
66
+ spaceVersionAnnotations: (id, versionId, status) => status === undefined
67
+ ? ["spaces", id, "versions", versionId, "annotations"]
68
+ : ["spaces", id, "versions", versionId, "annotations", status],
51
69
  spaceVersionFiles: (id, versionId) => ["spaces", id, "versions", versionId, "files"],
52
70
  spaceVersionFile: (id, versionId, path) => ["spaces", id, "versions", versionId, "files", path],
53
71
  spaceVersionFileDirectUrl: (id, versionId, path) => ["spaces", id, "versions", versionId, "files", path, "direct-url"],
@@ -68,6 +86,11 @@ export const qk = {
68
86
  spaceCertificates: (id) => ["spaces", id, "runtime", "certificates"],
69
87
  spaceLogs: (id, kind) => ["spaces", id, "logs", kind],
70
88
  spaceAnalytics: (id, window) => ["spaces", id, "analytics", window],
89
+ spaceFirewall: (id) => ["spaces", id, "access", "firewall"],
90
+ spaceAccessViews: (id, since) => ["spaces", id, "access", "views", since ?? ""],
91
+ teamServiceTokens: (teamId) => ["teams", teamId, "access", "service-tokens"],
92
+ teamAccessDefaults: (teamId) => ["teams", teamId, "access", "defaults"],
93
+ teamAccessConnections: (teamId) => ["teams", teamId, "access", "connections"],
71
94
  domain: (teamId, ref) => ["teams", teamId, "domains", ref],
72
95
  domainDiagnostics: (teamId, domainId) => ["teams", teamId, "domains", domainId, "diagnostics"],
73
96
  domainDnsCapabilities: (domainId) => ["domains", domainId, "dns", "capabilities"],
@@ -2,6 +2,5 @@ export declare const SPACEFAST_RUNTIME_NAMESPACE: "/__spacefast";
2
2
  export declare const RUNTIME_MANAGEMENT_API_PATH: "/__spacefast/api.php";
3
3
  export declare const RUNTIME_UPLOAD_API_PATH: "/__spacefast/upload.php";
4
4
  export declare const RUNTIME_HEALTH_PATH: "/__spacefast/health.php";
5
- export declare const RUNTIME_ACCESS_CALLBACK_PATH: "/__spacefast/access-callback.php";
6
5
  export declare function runtimeManagementPath(suffix: string): string;
7
6
  export declare function runtimeUploadPath(params: URLSearchParams | Record<string, string>): string;
@@ -2,7 +2,6 @@ export const SPACEFAST_RUNTIME_NAMESPACE = "/__spacefast";
2
2
  export const RUNTIME_MANAGEMENT_API_PATH = `${SPACEFAST_RUNTIME_NAMESPACE}/api.php`;
3
3
  export const RUNTIME_UPLOAD_API_PATH = `${SPACEFAST_RUNTIME_NAMESPACE}/upload.php`;
4
4
  export const RUNTIME_HEALTH_PATH = `${SPACEFAST_RUNTIME_NAMESPACE}/health.php`;
5
- export const RUNTIME_ACCESS_CALLBACK_PATH = `${SPACEFAST_RUNTIME_NAMESPACE}/access-callback.php`;
6
5
  export function runtimeManagementPath(suffix) {
7
6
  const route = normalizeRuntimeRoute(suffix);
8
7
  if (route === "/") {
@@ -0,0 +1,18 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { RUNTIME_UPLOAD_BATCH_MAX_BYTES, RUNTIME_UPLOAD_BATCH_MAX_FILES } from "./runtime-upload.js";
3
+ import { type UploadTarget } from "./upload-session.js";
4
+ export { RUNTIME_UPLOAD_BATCH_MAX_BYTES, RUNTIME_UPLOAD_BATCH_MAX_FILES };
5
+ export type RuntimeBatchArchiveEntry = {
6
+ path: string;
7
+ getBytes(): Buffer | Promise<Buffer>;
8
+ };
9
+ export declare function runtimeBatchUploadUnsupported(error: unknown): boolean;
10
+ export declare function batchTargetForUploadTarget(uploadId: string, target: UploadTarget): UploadTarget | null;
11
+ export declare function tarPathParts(filePath: string): {
12
+ name: string;
13
+ prefix: string;
14
+ } | null;
15
+ export declare function tarEndSize(): number;
16
+ export declare function tarEntrySize(path: string, size: number): number | null;
17
+ export declare function tarPadding(size: number): number;
18
+ export declare function tarArchive(entries: readonly RuntimeBatchArchiveEntry[], pathTooLongError: (path: string) => Error): Promise<Buffer<ArrayBuffer>>;
@@ -0,0 +1,90 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { RUNTIME_UPLOAD_BATCH_MAX_BYTES, RUNTIME_UPLOAD_BATCH_MAX_FILES, } from "./runtime-upload.js";
3
+ import { UploadTargetError } from "./upload-session.js";
4
+ // Batch planning + tar-writing wire contract shared by the CLI and the control
5
+ // plane's inline publish path. This is the sender half of the runtime's tar
6
+ // batch parser (runtime/engine/admin/upload.php); any change here must stay in
7
+ // lockstep with that file.
8
+ export { RUNTIME_UPLOAD_BATCH_MAX_BYTES, RUNTIME_UPLOAD_BATCH_MAX_FILES };
9
+ export function runtimeBatchUploadUnsupported(error) {
10
+ return error instanceof UploadTargetError && (error.status === 404 || error.status === 405);
11
+ }
12
+ export function batchTargetForUploadTarget(uploadId, target) {
13
+ const url = new URL(target.url);
14
+ if (!url.pathname.endsWith("/upload.php")) {
15
+ return null;
16
+ }
17
+ url.search = new URLSearchParams({ op: "batch", upload_id: uploadId }).toString();
18
+ return {
19
+ ...target,
20
+ path: "__spacefast_batch.tar",
21
+ method: "POST",
22
+ url: url.toString(),
23
+ headers: {
24
+ ...target.headers,
25
+ "content-type": "application/x-tar",
26
+ },
27
+ };
28
+ }
29
+ export function tarPathParts(filePath) {
30
+ if (Buffer.byteLength(filePath) <= 100) {
31
+ return { name: filePath, prefix: "" };
32
+ }
33
+ const segments = filePath.split("/");
34
+ const name = segments.pop() ?? "";
35
+ const prefix = segments.join("/");
36
+ if (!name || Buffer.byteLength(name) > 100 || Buffer.byteLength(prefix) > 155) {
37
+ return null;
38
+ }
39
+ return { name, prefix };
40
+ }
41
+ export function tarEndSize() {
42
+ return 1024;
43
+ }
44
+ export function tarEntrySize(path, size) {
45
+ if (!tarPathParts(path)) {
46
+ return null;
47
+ }
48
+ return 512 + size + tarPadding(size);
49
+ }
50
+ export function tarPadding(size) {
51
+ return (512 - (size % 512)) % 512;
52
+ }
53
+ export async function tarArchive(entries, pathTooLongError) {
54
+ const chunks = [];
55
+ for (const entry of entries) {
56
+ const parts = tarPathParts(entry.path);
57
+ if (!parts) {
58
+ throw pathTooLongError(entry.path);
59
+ }
60
+ const body = Buffer.from(await entry.getBytes());
61
+ const header = Buffer.alloc(512);
62
+ header.write(parts.name, 0, Math.min(100, Buffer.byteLength(parts.name)), "utf8");
63
+ header.write("0000644\0", 100, 8, "ascii");
64
+ header.write("0000000\0", 108, 8, "ascii");
65
+ header.write("0000000\0", 116, 8, "ascii");
66
+ header.write(body.length.toString(8).padStart(11, "0") + "\0", 124, 12, "ascii");
67
+ header.write("00000000000\0", 136, 12, "ascii");
68
+ header.fill(0x20, 148, 156);
69
+ header[156] = "0".charCodeAt(0);
70
+ header.write("ustar", 257, 5, "ascii");
71
+ header.write("00", 263, 2, "ascii");
72
+ if (parts.prefix) {
73
+ header.write(parts.prefix, 345, Math.min(155, Buffer.byteLength(parts.prefix)), "utf8");
74
+ }
75
+ let checksum = 0;
76
+ for (const byte of header) {
77
+ checksum += byte;
78
+ }
79
+ header.write(checksum.toString(8).padStart(6, "0"), 148, 6, "ascii");
80
+ header[154] = 0;
81
+ header[155] = 0x20;
82
+ chunks.push(header, body);
83
+ const padding = tarPadding(body.length);
84
+ if (padding > 0) {
85
+ chunks.push(Buffer.alloc(padding));
86
+ }
87
+ }
88
+ chunks.push(Buffer.alloc(tarEndSize()));
89
+ return Buffer.concat(chunks);
90
+ }
@@ -1,4 +1,6 @@
1
1
  export declare const RUNTIME_UPLOAD_METHOD: "PUT";
2
+ export declare const RUNTIME_UPLOAD_BATCH_MAX_FILES = 500;
3
+ export declare const RUNTIME_UPLOAD_BATCH_MAX_BYTES: number;
2
4
  export declare function runtimeUploadCanonicalPath(path: string): string;
3
5
  export declare function runtimeUploadSessionBaseUrl(input: {
4
6
  baseUrl: string;
@@ -1,5 +1,10 @@
1
1
  import { runtimeUploadPath } from "./runtime-paths.js";
2
2
  export const RUNTIME_UPLOAD_METHOD = "PUT";
3
+ // Mirrors STATTIC_RUNTIME_UPLOAD_BATCH_MAX_FILES/_MAX_BYTES in
4
+ // runtime/engine/admin/upload.php (parity-guarded in
5
+ // apps/control-plane/src/runtime/php-policy-parity.test.ts).
6
+ export const RUNTIME_UPLOAD_BATCH_MAX_FILES = 500;
7
+ export const RUNTIME_UPLOAD_BATCH_MAX_BYTES = 8 * 1024 * 1024;
3
8
  export function runtimeUploadCanonicalPath(path) {
4
9
  // Canonical path form (spec "Upload Contract"): UTF-8, Unicode NFC,
5
10
  // percent-encoded once. The runtime rejects non-canonical encodings.
@@ -37,9 +37,13 @@ export type ResolvedServingConfig = {
37
37
  path: string;
38
38
  status: 200 | 404;
39
39
  } | null;
40
+ /** W7.1 clean URLs: serve flat `<name>.html` at its extensionless URL. */
41
+ cleanUrls: boolean;
40
42
  listing: boolean;
41
43
  viewer: boolean;
42
44
  meta: NonNullable<SpaceConfig["meta"]> | null;
45
+ /** Gate-page theme tokens (access-plan §5.7 tier 1); null when unset. */
46
+ pagesTheme: NonNullable<NonNullable<SpaceConfig["pages"]>["theme"]> | null;
43
47
  /** Hidden experimental build-pipeline flag; false keeps today's behavior. */
44
48
  experimentalGutenberg: boolean;
45
49
  /** Resolved inject snippets; only honored when experimentalGutenberg is true. */
@@ -303,7 +303,9 @@ const PRIVATE_SERVING_FILES = new Set([
303
303
  ...SPACE_CONFIG_PRIVATE_FILES,
304
304
  ]);
305
305
  function privateServingPath(path) {
306
- if (PRIVATE_SERVING_FILES.has(path) || path === ".well-known/stattic-runtime") {
306
+ if (PRIVATE_SERVING_FILES.has(path) ||
307
+ path === ".well-known/spacefast-runtime" ||
308
+ path === ".well-known/stattic-runtime") {
307
309
  return true;
308
310
  }
309
311
  const segments = path.toLowerCase().split("/");
@@ -381,10 +383,17 @@ export function resolveEffectiveSpaceConfig(input) {
381
383
  const serving = {
382
384
  index: merged.index ?? inferredRootHtmlIndex ?? false,
383
385
  fallback,
386
+ // Clean URLs (runtime W7.1): an explicit config value wins; the default is
387
+ // ON for plain static sites and OFF when a 200-status SPA fallback owns
388
+ // extensionless routes (the fallback answers them instead). The runtime
389
+ // resolver (management.php) applies the same default for artifacts
390
+ // finalized without the key.
391
+ cleanUrls: merged.cleanUrls ?? !(fallback && fallback.status === 200),
384
392
  listing,
385
393
  // The single-file viewer rides the listing bundle (artifact-style spaces).
386
394
  viewer: listing,
387
395
  meta: merged.meta ?? null,
396
+ pagesTheme: merged.pages?.theme && Object.keys(merged.pages.theme).length > 0 ? merged.pages.theme : null,
388
397
  experimentalGutenberg: merged.experimental_gutenberg === true,
389
398
  inject: merged.inject ?? null,
390
399
  };
@@ -13,4 +13,6 @@ export type CompiledSpaceTheme = {
13
13
  };
14
14
  /** Engine-enforced ceiling for the compiled declaration block. */
15
15
  export declare const SPACE_THEME_CSS_MAX_BYTES: number;
16
+ export declare function isSafeThemeColor(value: string): boolean;
17
+ export declare function isSafeThemeFontFamily(value: string): boolean;
16
18
  export declare function compileSpaceTheme(theme: SpaceTheme | null | undefined): CompiledSpaceTheme;
@@ -25,6 +25,15 @@ function safeValue(value, pattern) {
25
25
  const trimmed = value.trim();
26
26
  return trimmed.length > 0 && !FORBIDDEN_VALUE_PATTERN.test(trimmed) && pattern.test(trimmed);
27
27
  }
28
+ // Exported so other compilers that consume the same preset grammar (the gate-page
29
+ // theme cascade's `theme.json`/`sf.theme.jsonc` tiers, access-plan §8) reuse this
30
+ // ONE allowlist instead of re-deriving it — same posture, same forbidden set.
31
+ export function isSafeThemeColor(value) {
32
+ return safeValue(value, COLOR_PATTERN);
33
+ }
34
+ export function isSafeThemeFontFamily(value) {
35
+ return safeValue(value, FONT_FAMILY_PATTERN);
36
+ }
28
37
  export function compileSpaceTheme(theme) {
29
38
  if (!theme) {
30
39
  return { css: null, warnings: [] };
@@ -7,27 +7,6 @@ export declare function pathExtension(filePath: string): string;
7
7
  /** Basename without its extension (the "stem"), posix semantics. */
8
8
  export declare function pathStem(filePath: string): string;
9
9
  export declare function isPhpLikeStaticPath(filePath: string): boolean;
10
- /**
11
- * Classifies a candidate hash token — the final `.`/`-`-delimited tail of a
12
- * filename stem (see isImmutableAssetPath) — as a real bundler content hash.
13
- *
14
- * Immutable caching is a one-year pin, so the test is asymmetric on purpose: a
15
- * missed hash costs a cheap 304 revalidation, a false positive serves stale
16
- * content for a year. When a token is ambiguous we say no. The discriminator is
17
- * NOT character case — modern bundlers emit BOTH lowercase-hex and mixed-case
18
- * base64url hashes — it is entropy plus the absence of dictionary-word shape.
19
- *
20
- * IRREDUCIBLE LIMIT: an 8-char base64url token is the same size as a short
21
- * CamelCase-with-version-digit human name, and every default Vite/Rollup hash we
22
- * must accept carries only one digit (BzeDGMM3, 4A_iFExJ). So a token like
23
- * `D3GeoMap`, `McKayCV2`, or `v2-WebGL` is character-for-character indistinguishable
24
- * from a real hash and is accepted by clause B. No filename-only rule can separate
25
- * these without also rejecting the bundler hashes the spec mandates. The durable
26
- * fix is to gate immutable caching on path convention (Vite `assets/`, Next
27
- * `_next/static/`, SvelteKit `_app/immutable/`, …) rather than guessing from the
28
- * stem; this heuristic is the best-effort fallback when that context is absent.
29
- */
30
- export declare function looksLikeBundlerFingerprint(segment: string): boolean;
31
10
  export declare function isImmutableAssetPath(filePath: string): boolean;
32
11
  /** Forced inert serving policy for a committed static file. PHP-like files are never executable. */
33
12
  export declare function runtimeFileSafety(filePath: string): {
@@ -46,19 +46,17 @@ export const PLATFORM_MANAGED_RESPONSE_HEADERS = [
46
46
  export const NON_IMMUTABLE_EXTENSIONS = ["html", "htm", "php", "txt", "xml"];
47
47
  const PHP_LIKE_EXTENSION_SET = new Set(PHP_LIKE_EXTENSIONS);
48
48
  const NON_IMMUTABLE_EXTENSION_SET = new Set(NON_IMMUTABLE_EXTENSIONS);
49
- // Two real bundler content-hash shapes. (A) lowercase hex (git/webpack short
50
- // SHAs, many tools): >=7 chars of [0-9a-f]. (B) base64url (Vite/Rollup/esbuild/
51
- // Next): >=8 chars of [A-Za-z0-9_-]. Both forms are recognized; the entropy
52
- // guards in looksLikeBundlerFingerprint are what keep human names out.
53
- const HEX_FINGERPRINT = /^[0-9a-f]{7,}$/;
54
- const BASE64URL_FINGERPRINT = /^[A-Za-z0-9_-]{8,}$/;
55
- // Durable path-convention layer over the filename-entropy heuristic
56
- // (looksLikeBundlerFingerprint). These are reserved build-output directories that the
57
- // framework OWNS and fills exclusively with content-addressed ([contenthash]) output by
58
- // construction so every asset served from under them is safe to pin immutable for a
59
- // year regardless of how its individual filename reads. This recovers the real hashes the
60
- // filename heuristic conservatively MISSES (no-digit / single-case base64url draws like
61
- // `oton0Guf`, `DYyuCeEr`) without loosening the heuristic itself.
49
+ // Immutable caching is decided by DETERMINISTIC signals only reserved framework
50
+ // build directories below, plus user-declared `_headers` Cache-Control rules that
51
+ // override the baked default at serve time. There is deliberately NO filename-entropy
52
+ // heuristic: an 8-char base64url bundler hash is character-for-character
53
+ // indistinguishable from a short human name (D3GeoMap, McKayCV2), so any guess either
54
+ // pins a user's mutable file for a year (stale site nobody can fix — browsers have no
55
+ // bust channel) or silently downgrades real hashes to revalidate-every-load, which
56
+ // under the wp.cloud edge per-IP rate limiter turned dashboard page loads into 429
57
+ // storms (2026-07-06). Unrecognized files default to the safe direction:
58
+ // `max-age=0, must-revalidate` for browsers, long s-maxage + purge-on-publish at the
59
+ // edge.
62
60
  //
63
61
  // Each prefix is HIGH-CONFIDENCE guaranteed-hashed:
64
62
  // - `_next/static/` Next.js immutable build assets (chunks/css/media), all
@@ -71,9 +69,10 @@ const BASE64URL_FINGERPRINT = /^[A-Za-z0-9_-]{8,}$/;
71
69
  //
72
70
  // DELIBERATELY EXCLUDED: generic user-writable dirs (Vite's `assets/`, CRA/Gatsby's
73
71
  // `static/`). Users drop NON-hashed files there, so widening to them would create
74
- // year-long false positives. Those stay gated by the filename heuristic only. This layer
75
- // is purely ADDITIVE: it can only turn a heuristic-`false` into `true` for assets under a
76
- // guaranteed-hashed dir; it never flips a `true` to `false`.
72
+ // year-long false positives. Deployments of bundler output declare those dirs immutable
73
+ // themselves via a `_headers` rule (e.g. `/assets/*` `Cache-Control: public,
74
+ // max-age=31536000, immutable`), which the publisher a build we ran, an agent, a CI
75
+ // deploy — writes because it KNOWS the output is content-hashed.
77
76
  export const GUARANTEED_IMMUTABLE_PREFIXES = [
78
77
  "_next/static/",
79
78
  "_app/immutable/",
@@ -109,97 +108,14 @@ export function pathStem(filePath) {
109
108
  export function isPhpLikeStaticPath(filePath) {
110
109
  return PHP_LIKE_EXTENSION_SET.has(pathExtension(filePath));
111
110
  }
112
- /**
113
- * Classifies a candidate hash token — the final `.`/`-`-delimited tail of a
114
- * filename stem (see isImmutableAssetPath) — as a real bundler content hash.
115
- *
116
- * Immutable caching is a one-year pin, so the test is asymmetric on purpose: a
117
- * missed hash costs a cheap 304 revalidation, a false positive serves stale
118
- * content for a year. When a token is ambiguous we say no. The discriminator is
119
- * NOT character case — modern bundlers emit BOTH lowercase-hex and mixed-case
120
- * base64url hashes — it is entropy plus the absence of dictionary-word shape.
121
- *
122
- * IRREDUCIBLE LIMIT: an 8-char base64url token is the same size as a short
123
- * CamelCase-with-version-digit human name, and every default Vite/Rollup hash we
124
- * must accept carries only one digit (BzeDGMM3, 4A_iFExJ). So a token like
125
- * `D3GeoMap`, `McKayCV2`, or `v2-WebGL` is character-for-character indistinguishable
126
- * from a real hash and is accepted by clause B. No filename-only rule can separate
127
- * these without also rejecting the bundler hashes the spec mandates. The durable
128
- * fix is to gate immutable caching on path convention (Vite `assets/`, Next
129
- * `_next/static/`, SvelteKit `_app/immutable/`, …) rather than guessing from the
130
- * stem; this heuristic is the best-effort fallback when that context is absent.
131
- */
132
- export function looksLikeBundlerFingerprint(segment) {
133
- // (A) Lowercase-hex hash (git short-SHA, webpack/Vite/Gatsby `[contenthash]`):
134
- // a1b2c3d, 3f9a2b7e, d41d8cd98f00b204e980. Always require >=1 hex letter and
135
- // >=1 digit so all-letter words (deadbeef) and digit-only runs (1234567) are out.
136
- if (HEX_FINGERPRINT.test(segment) && /[a-f]/.test(segment) && /[0-9]/.test(segment)) {
137
- // A long all-hex token is unambiguous: no human authors a 16+ char hex name, so
138
- // length alone is entropy proof and the anti-word guards would only lose real
139
- // hashes (most 16/20-char contenthashes contain an incidental 4-run).
140
- if (segment.length >= 16) {
141
- return true;
142
- }
143
- // Short hex (7-15) overlaps with hex-spelled words/years/colors, so require >=2
144
- // digits and forbid a run of 4+ hex letters (deadbeef, cafe, face, decade) or
145
- // 4+ digits (2024, 1234) — low-entropy human false positives.
146
- if ((segment.match(/[0-9]/g) ?? []).length >= 2 &&
147
- !/[a-f]{4}/.test(segment) &&
148
- !/[0-9]{4}/.test(segment)) {
149
- return true;
150
- }
151
- }
152
- // (B) Base64url hash (Vite/Rollup/esbuild/Next): BzeDGMM3, 4A_iFExJ, P1Tx-Cyu,
153
- // Z-cBR0Xb, 9-ygPMte. Random base64url draws — but immutable is a one-year pin, so
154
- // against the human names that mimic this shape (acronym data/UI names, version
155
- // and fiscal labels, dates, resolutions) we demand the full BALANCED-entropy
156
- // signature of a real hash, and accept the safe cost of missing the minority of
157
- // genuine hashes that fall short (a cheap 304):
158
- // - >=2 lowercase AND >=2 uppercase letters. Real base64url hashes carry several
159
- // of each; acronym-dominant human names (USMap-AKHIORWA, Map-USA_CA_TX,
160
- // MyCSS2DUI) have at most one lowercase token and are rejected here. This also
161
- // conservatively drops single-case and no-/one-lowercase hashes (DYyuCeEr,
162
- // ChJ_j-JJ) — the safe miss direction.
163
- // - at least one digit. ~1/4 of base64url hashes lack one and become cheap 304s,
164
- // but requiring it removes the all-letter acronym names that are otherwise
165
- // indistinguishable from a hash (USMap-AKHIORWA has no digit).
166
- // - no run of 3+ lowercase letters: a dictionary-word body (MyComponent
167
- // "omponent", LoginOAuth2 "ogin", HTTPClient2 "lient"). A packed hash breaks
168
- // lowercase up with caps/digits.
169
- // - no run of 2+ digits: clustered digits are human years/resolutions/sequences/
170
- // dates (2024, 1080, Win10, 01); a hash scatters single digits between letters.
171
- // <8 chars (Widget2, API2, v2) never qualifies via the regex length floor.
172
- if (BASE64URL_FINGERPRINT.test(segment) &&
173
- (segment.match(/[a-z]/g) ?? []).length >= 2 &&
174
- (segment.match(/[A-Z]/g) ?? []).length >= 2 &&
175
- /[0-9]/.test(segment) &&
176
- !/[a-z]{3}/.test(segment) &&
177
- !/[0-9]{2}/.test(segment)) {
178
- return true;
179
- }
180
- return false;
181
- }
182
111
  export function isImmutableAssetPath(filePath) {
183
112
  if (NON_IMMUTABLE_EXTENSION_SET.has(pathExtension(filePath))) {
184
113
  return false;
185
114
  }
186
- // Path-convention layer (additive): anything under a guaranteed-content-hashed
187
- // framework build dir is immutable regardless of filename. The extension gate above
188
- // still applies, so an `index.html` accidentally under `_next/static/` is never pinned.
189
- if (isUnderGuaranteedImmutablePrefix(filePath)) {
190
- return true;
191
- }
192
- // Fallback for assets OUTSIDE those dirs: the filename-entropy heuristic.
193
- const stem = pathStem(filePath);
194
- for (let index = 0; index < stem.length; index += 1) {
195
- if (index > 0 && stem[index - 1] !== "." && stem[index - 1] !== "-") {
196
- continue;
197
- }
198
- if (looksLikeBundlerFingerprint(stem.slice(index))) {
199
- return true;
200
- }
201
- }
202
- return false;
115
+ // Path-convention layer: anything under a guaranteed-content-hashed framework build
116
+ // dir is immutable regardless of filename. The extension gate above still applies,
117
+ // so an `index.html` accidentally under `_next/static/` is never pinned.
118
+ return isUnderGuaranteedImmutablePrefix(filePath);
203
119
  }
204
120
  /** Forced inert serving policy for a committed static file. PHP-like files are never executable. */
205
121
  export function runtimeFileSafety(filePath) {
@@ -28,7 +28,7 @@ export declare function executeUploadSession(input: {
28
28
  session: UploadSession;
29
29
  targets?: readonly UploadTarget[] | undefined;
30
30
  concurrency?: number | undefined;
31
- fetch?: typeof fetch | undefined;
31
+ fetch?: ((...args: Parameters<typeof fetch>) => ReturnType<typeof fetch>) | undefined;
32
32
  prepareTarget?: ((target: UploadTarget) => Promise<UploadTargetBody> | UploadTargetBody) | undefined;
33
33
  onEvent?: ((event: UploadSessionEvent) => void) | undefined;
34
34
  }): Promise<void>;
@@ -1,11 +1,41 @@
1
1
  export type LoginIntentSearch = {
2
2
  direct?: boolean | undefined;
3
3
  returnTo?: string | undefined;
4
+ /** Route straight to WordPress.com's signup page instead of its login page. */
5
+ signup?: boolean | undefined;
4
6
  };
7
+ /**
8
+ * Optional query params WordPress.com's OAuth2 authorize endpoint understands
9
+ * (wpcom `wp-content/mu-plugins/oauth2/oauth2.php`). All are cosmetic/flow
10
+ * hints — dropping them never breaks the OAuth handshake.
11
+ */
12
+ export type WpcomAuthorizeExtras = {
13
+ /** Prefills the email field on the WP.com login screen (`user_email`). */
14
+ userEmail?: string | undefined;
15
+ /** Localizes the WP.com login/consent/signup screens (`locale`). */
16
+ locale?: string | undefined;
17
+ /**
18
+ * `implicit=false`: WP.com signs the user out of WordPress.com and shows a
19
+ * fresh login instead of silently reusing the current WP.com session. The
20
+ * only way to let a user switch WP.com accounts — without it, sign-out on
21
+ * our side loops them straight back into the same account.
22
+ */
23
+ switchAccount?: boolean | undefined;
24
+ /** `new-user=1`: routes straight to the WP.com signup page. */
25
+ signup?: boolean | undefined;
26
+ };
27
+ export declare function decorateWpcomAuthorizeUrl(authorizeUrl: string, extras?: WpcomAuthorizeExtras): string;
5
28
  export declare function buildDashboardLoginPath(input: LoginIntentSearch): string;
6
- export declare function buildWpcomAuthStartPath(input: LoginIntentSearch): string;
29
+ export declare function buildWpcomAuthStartPath(input: LoginIntentSearch & {
30
+ /**
31
+ * A signed OAuth authorize query (Better Auth's login-page redirect).
32
+ * Carried through the WordPress.com round trip so the auth server can
33
+ * resume the authorization flow the moment the session exists. Opaque
34
+ * here — the auth server verifies its signature.
35
+ */
36
+ oauthQuery?: string | undefined;
37
+ }): string;
7
38
  export declare function buildPostAuthPath(input: LoginIntentSearch): string;
8
- export declare function buildWpcomLoginUrl(authorizeUrl: string): string;
9
39
  export declare function buildWpcomSignInOptions(input: LoginIntentSearch, dashboardOrigin: string): {
10
40
  readonly provider: "wpcom";
11
41
  readonly callbackURL: string;
@@ -1,4 +1,24 @@
1
1
  import { sanitizeDashboardReturnTo } from "@spacefast/common/utils/auth-redirect";
2
+ export function decorateWpcomAuthorizeUrl(authorizeUrl, extras = {}) {
3
+ const url = new URL(authorizeUrl);
4
+ const email = extras.userEmail?.trim();
5
+ if (email && email.includes("@") && email.length <= 254) {
6
+ url.searchParams.set("user_email", email);
7
+ }
8
+ // WP.com validates locales against its own list and falls back to English,
9
+ // so a light shape check is enough here. English is the default — omit it.
10
+ const locale = extras.locale?.trim().toLowerCase().replace("_", "-");
11
+ if (locale && /^[a-z]{2}(-[a-z]{2})?$/.test(locale) && locale !== "en") {
12
+ url.searchParams.set("locale", locale);
13
+ }
14
+ if (extras.switchAccount) {
15
+ url.searchParams.set("implicit", "false");
16
+ }
17
+ if (extras.signup) {
18
+ url.searchParams.set("new-user", "1");
19
+ }
20
+ return url.toString();
21
+ }
2
22
  export function buildDashboardLoginPath(input) {
3
23
  const url = new URL("/sign-in", "http://dashboard.local");
4
24
  if (input.direct) {
@@ -16,6 +36,12 @@ export function buildWpcomAuthStartPath(input) {
16
36
  if (returnTo) {
17
37
  url.searchParams.set("returnTo", returnTo);
18
38
  }
39
+ if (input.signup) {
40
+ url.searchParams.set("signup", "1");
41
+ }
42
+ if (input.oauthQuery) {
43
+ url.searchParams.set("oauth_query", input.oauthQuery);
44
+ }
19
45
  return `${url.pathname}${url.search}`;
20
46
  }
21
47
  export function buildPostAuthPath(input) {
@@ -27,16 +53,6 @@ export function buildPostAuthPath(input) {
27
53
  }
28
54
  return `${url.pathname}${url.search}`;
29
55
  }
30
- export function buildWpcomLoginUrl(authorizeUrl) {
31
- const authUrl = new URL(authorizeUrl);
32
- const loginUrl = new URL("https://wordpress.com/log-in");
33
- const clientId = authUrl.searchParams.get("client_id");
34
- if (clientId) {
35
- loginUrl.searchParams.set("client_id", clientId);
36
- }
37
- loginUrl.searchParams.set("redirect_to", authUrl.toString());
38
- return loginUrl.toString();
39
- }
40
56
  export function buildWpcomSignInOptions(input, dashboardOrigin) {
41
57
  const errorCallbackUrl = new URL(buildDashboardLoginPath(input), dashboardOrigin);
42
58
  errorCallbackUrl.searchParams.set("claim_error", "login");