@spacefast/common 0.0.3 → 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 (159) 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 +829 -129
  8. package/dist/contracts/access.js +771 -148
  9. package/dist/contracts/activity.d.ts +5 -5
  10. package/dist/contracts/activity.js +28 -2
  11. package/dist/contracts/annotations.d.ts +3 -5
  12. package/dist/contracts/api-keys.d.ts +9 -10
  13. package/dist/contracts/api-keys.js +9 -6
  14. package/dist/contracts/archives.d.ts +22 -16
  15. package/dist/contracts/archives.js +11 -4
  16. package/dist/contracts/billing.d.ts +21 -30
  17. package/dist/contracts/builds.d.ts +21 -17
  18. package/dist/contracts/builds.js +12 -0
  19. package/dist/contracts/channels.d.ts +2 -2
  20. package/dist/contracts/common.d.ts +7 -5
  21. package/dist/contracts/common.js +4 -0
  22. package/dist/contracts/continuation.d.ts +63 -0
  23. package/dist/contracts/continuation.js +91 -0
  24. package/dist/contracts/countries.d.ts +2 -0
  25. package/dist/contracts/countries.js +261 -0
  26. package/dist/contracts/deployments.d.ts +34 -34
  27. package/dist/contracts/device-auth.d.ts +4 -11
  28. package/dist/contracts/device-auth.js +4 -0
  29. package/dist/contracts/docs.d.ts +119 -0
  30. package/dist/contracts/docs.js +51 -0
  31. package/dist/contracts/domains.d.ts +295 -253
  32. package/dist/contracts/domains.js +26 -4
  33. package/dist/contracts/enums.d.ts +17 -18
  34. package/dist/contracts/enums.js +25 -14
  35. package/dist/contracts/error-code-meta.d.ts +84 -0
  36. package/dist/contracts/error-code-meta.js +99 -10
  37. package/dist/contracts/error-codes.d.ts +1 -1
  38. package/dist/contracts/error-codes.js +17 -0
  39. package/dist/contracts/events.d.ts +13 -13
  40. package/dist/contracts/features.d.ts +93 -23
  41. package/dist/contracts/features.js +178 -13
  42. package/dist/contracts/git.d.ts +3 -9
  43. package/dist/contracts/ids.d.ts +5 -0
  44. package/dist/contracts/ids.js +5 -0
  45. package/dist/contracts/internal.d.ts +3 -3
  46. package/dist/contracts/mcp.d.ts +34 -36
  47. package/dist/contracts/me.d.ts +8 -8
  48. package/dist/contracts/me.js +1 -1
  49. package/dist/contracts/oauth-resources.d.ts +19 -8
  50. package/dist/contracts/oauth-resources.js +44 -13
  51. package/dist/contracts/operations.d.ts +2 -2
  52. package/dist/contracts/plan-policy.d.ts +4 -4
  53. package/dist/contracts/platform.d.ts +3 -105
  54. package/dist/contracts/platform.js +0 -50
  55. package/dist/contracts/push-new.d.ts +4 -4
  56. package/dist/contracts/quotas.d.ts +2 -0
  57. package/dist/contracts/quotas.js +8 -0
  58. package/dist/contracts/repository-connections.d.ts +1 -3
  59. package/dist/contracts/resources.d.ts +112 -28
  60. package/dist/contracts/resources.js +26 -2
  61. package/dist/contracts/routes.d.ts +0 -2
  62. package/dist/contracts/runtime-api.d.ts +365 -43
  63. package/dist/contracts/runtime-api.js +275 -6
  64. package/dist/contracts/sites.d.ts +45 -28
  65. package/dist/contracts/sites.js +21 -0
  66. package/dist/contracts/space-config.d.ts +201 -2
  67. package/dist/contracts/space-config.js +135 -5
  68. package/dist/contracts/spaces.d.ts +423 -162
  69. package/dist/contracts/spaces.js +94 -8
  70. package/dist/contracts/superadmin-emails.d.ts +7 -5
  71. package/dist/contracts/superadmin-emails.js +8 -0
  72. package/dist/contracts/superadmin-queues.d.ts +32 -32
  73. package/dist/contracts/superadmin-spaces.d.ts +418 -24
  74. package/dist/contracts/superadmin-spaces.js +24 -0
  75. package/dist/contracts/superadmin-tenants.d.ts +8 -8
  76. package/dist/contracts/superadmin.d.ts +17 -104
  77. package/dist/contracts/superadmin.js +6 -14
  78. package/dist/contracts/tags.d.ts +42 -41
  79. package/dist/contracts/tags.js +2 -1
  80. package/dist/contracts/teams.d.ts +40 -23
  81. package/dist/contracts/teams.js +20 -1
  82. package/dist/contracts/theme-json.d.ts +30 -0
  83. package/dist/contracts/theme-json.js +48 -0
  84. package/dist/contracts/transfers.d.ts +9 -9
  85. package/dist/contracts/variables.d.ts +14 -16
  86. package/dist/contracts/variables.js +0 -5
  87. package/dist/contracts/webhooks.d.ts +6 -4
  88. package/dist/contracts/webhooks.js +1 -1
  89. package/dist/contracts/zero.d.ts +13 -18
  90. package/dist/docs/agent-prose.d.ts +43 -3
  91. package/dist/docs/agent-prose.js +153 -10
  92. package/dist/docs/agent-setup.js +48 -15
  93. package/dist/docs/agent-solutions.d.ts +5 -5
  94. package/dist/docs/agent-solutions.js +2 -2
  95. package/dist/docs/catalog.d.ts +650 -0
  96. package/dist/docs/catalog.js +758 -0
  97. package/dist/docs/error-docs.d.ts +0 -2
  98. package/dist/docs/error-docs.js +89 -35
  99. package/dist/docs/index-build.d.ts +17 -0
  100. package/dist/docs/index-build.js +24 -0
  101. package/dist/docs/search.d.ts +54 -0
  102. package/dist/docs/search.js +96 -0
  103. package/dist/docs/skill-distribution.d.ts +5 -3
  104. package/dist/docs/skill-distribution.js +8 -8
  105. package/dist/utils/access-match.d.ts +39 -0
  106. package/dist/utils/access-match.js +296 -0
  107. package/dist/utils/auth-redirect.d.ts +3 -1
  108. package/dist/utils/auth-redirect.js +21 -1
  109. package/dist/utils/build-settings.d.ts +6 -0
  110. package/dist/utils/build-settings.js +429 -46
  111. package/dist/utils/claim-token.d.ts +8 -0
  112. package/dist/utils/claim-token.js +23 -2
  113. package/dist/utils/content-type.d.ts +1 -0
  114. package/dist/utils/content-type.js +1 -1
  115. package/dist/utils/credential-policy.d.ts +54 -0
  116. package/dist/utils/{access-policy.js → credential-policy.js} +28 -2
  117. package/dist/utils/gate-theme.d.ts +61 -0
  118. package/dist/utils/gate-theme.js +217 -0
  119. package/dist/utils/git-repository.d.ts +0 -1
  120. package/dist/utils/git-repository.js +0 -3
  121. package/dist/utils/id-hints.d.ts +8 -0
  122. package/dist/utils/id-hints.js +61 -0
  123. package/dist/utils/local-space-state.d.ts +91 -0
  124. package/dist/utils/local-space-state.js +251 -0
  125. package/dist/utils/oauth-signed-query.d.ts +8 -0
  126. package/dist/utils/oauth-signed-query.js +26 -0
  127. package/dist/utils/publish-detection.d.ts +0 -8
  128. package/dist/utils/publish-detection.js +0 -93
  129. package/dist/utils/publish-policy.d.ts +6 -5
  130. package/dist/utils/publish-policy.js +71 -7
  131. package/dist/utils/query-keys.d.ts +17 -6
  132. package/dist/utils/query-keys.js +27 -4
  133. package/dist/utils/runtime-paths.d.ts +0 -1
  134. package/dist/utils/runtime-paths.js +0 -1
  135. package/dist/utils/runtime-upload-batch.d.ts +18 -0
  136. package/dist/utils/runtime-upload-batch.js +90 -0
  137. package/dist/utils/runtime-upload.d.ts +2 -0
  138. package/dist/utils/runtime-upload.js +5 -0
  139. package/dist/utils/space-config.d.ts +4 -0
  140. package/dist/utils/space-config.js +10 -1
  141. package/dist/utils/space-theme.d.ts +2 -0
  142. package/dist/utils/space-theme.js +9 -0
  143. package/dist/utils/static-runtime-policy.d.ts +1 -1
  144. package/dist/utils/static-runtime-policy.js +47 -15
  145. package/dist/utils/upload-session.d.ts +1 -1
  146. package/dist/utils/wpcom-auth-redirects.d.ts +32 -2
  147. package/dist/utils/wpcom-auth-redirects.js +26 -10
  148. package/dist/vocabulary.d.ts +130 -30
  149. package/dist/vocabulary.js +77 -36
  150. package/package.json +16 -1
  151. package/dist/contracts/account.d.ts +0 -28
  152. package/dist/contracts/account.js +0 -54
  153. package/dist/contracts/intercom.d.ts +0 -6
  154. package/dist/contracts/intercom.js +0 -5
  155. package/dist/contracts/telemetry.d.ts +0 -2
  156. package/dist/contracts/telemetry.js +0 -1
  157. package/dist/utils/access-policy.d.ts +0 -39
  158. package/dist/utils/error-display.d.ts +0 -7
  159. 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: [] };
@@ -1,12 +1,12 @@
1
1
  export declare const PHP_LIKE_EXTENSIONS: string[];
2
2
  export declare const PLATFORM_MANAGED_RESPONSE_HEADERS: string[];
3
3
  export declare const NON_IMMUTABLE_EXTENSIONS: string[];
4
+ export declare const GUARANTEED_IMMUTABLE_PREFIXES: string[];
4
5
  /** Lowercased extension without the leading dot, posix semantics. Dotfiles have no extension. */
5
6
  export declare function pathExtension(filePath: string): string;
6
7
  /** Basename without its extension (the "stem"), posix semantics. */
7
8
  export declare function pathStem(filePath: string): string;
8
9
  export declare function isPhpLikeStaticPath(filePath: string): boolean;
9
- export declare function looksLikeBundlerFingerprint(segment: string): boolean;
10
10
  export declare function isImmutableAssetPath(filePath: string): boolean;
11
11
  /** Forced inert serving policy for a committed static file. PHP-like files are never executable. */
12
12
  export declare function runtimeFileSafety(filePath: string): {
@@ -46,7 +46,49 @@ 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
- const FINGERPRINTED_ASSET_SEGMENT = /^[A-Za-z0-9_-]{8,}$/;
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.
60
+ //
61
+ // Each prefix is HIGH-CONFIDENCE guaranteed-hashed:
62
+ // - `_next/static/` Next.js immutable build assets (chunks/css/media), all
63
+ // [contenthash]; Next itself serves this dir with `immutable`.
64
+ // - `_app/immutable/` SvelteKit (Vite) — the directory is literally named `immutable`
65
+ // and holds only fingerprinted chunks/assets.
66
+ // - `_nuxt/` Nuxt build dir — Vite-hashed client bundle, served from a
67
+ // reserved app-owned path.
68
+ // - `_astro/` Astro bundled assets — all content-hashed.
69
+ //
70
+ // DELIBERATELY EXCLUDED: generic user-writable dirs (Vite's `assets/`, CRA/Gatsby's
71
+ // `static/`). Users drop NON-hashed files there, so widening to them would create
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.
76
+ export const GUARANTEED_IMMUTABLE_PREFIXES = [
77
+ "_next/static/",
78
+ "_app/immutable/",
79
+ "_nuxt/",
80
+ "_astro/",
81
+ ];
82
+ /** True when the path lives under a framework dir where every emitted asset is content-hashed. */
83
+ function isUnderGuaranteedImmutablePrefix(filePath) {
84
+ const normalized = filePath.startsWith("/") ? filePath.slice(1) : filePath;
85
+ for (const prefix of GUARANTEED_IMMUTABLE_PREFIXES) {
86
+ if (normalized.startsWith(prefix)) {
87
+ return true;
88
+ }
89
+ }
90
+ return false;
91
+ }
50
92
  function lastSegment(filePath) {
51
93
  const slash = filePath.lastIndexOf("/");
52
94
  return slash >= 0 ? filePath.slice(slash + 1) : filePath;
@@ -66,24 +108,14 @@ export function pathStem(filePath) {
66
108
  export function isPhpLikeStaticPath(filePath) {
67
109
  return PHP_LIKE_EXTENSION_SET.has(pathExtension(filePath));
68
110
  }
69
- export function looksLikeBundlerFingerprint(segment) {
70
- return (FINGERPRINTED_ASSET_SEGMENT.test(segment) &&
71
- (/\d/.test(segment) || /_/.test(segment) || (/[a-z]/.test(segment) && /[A-Z]/.test(segment))));
72
- }
73
111
  export function isImmutableAssetPath(filePath) {
74
112
  if (NON_IMMUTABLE_EXTENSION_SET.has(pathExtension(filePath))) {
75
113
  return false;
76
114
  }
77
- const stem = pathStem(filePath);
78
- for (let index = 0; index < stem.length; index += 1) {
79
- if (index > 0 && stem[index - 1] !== "." && stem[index - 1] !== "-") {
80
- continue;
81
- }
82
- if (looksLikeBundlerFingerprint(stem.slice(index))) {
83
- return true;
84
- }
85
- }
86
- 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);
87
119
  }
88
120
  /** Forced inert serving policy for a committed static file. PHP-like files are never executable. */
89
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");