@spacefast/common 0.0.12 → 0.0.17

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 (189) hide show
  1. package/dist/agents/client-registry.d.ts +22 -0
  2. package/dist/agents/client-registry.js +41 -0
  3. package/dist/agents/connect-targets.d.ts +51 -6
  4. package/dist/agents/connect-targets.js +93 -28
  5. package/dist/brand.d.ts +0 -11
  6. package/dist/brand.js +0 -20
  7. package/dist/config/domains.d.ts +15 -6
  8. package/dist/config/domains.js +24 -10
  9. package/dist/config/postgres-budget.d.ts +3 -0
  10. package/dist/config/postgres-budget.js +6 -1
  11. package/dist/config/postgres-budget.json +2 -1
  12. package/dist/contracts/abuse.js +1 -1
  13. package/dist/contracts/access.d.ts +486 -1196
  14. package/dist/contracts/access.js +323 -1054
  15. package/dist/contracts/activity.d.ts +1 -1
  16. package/dist/contracts/activity.js +11 -15
  17. package/dist/contracts/api-keys.d.ts +16 -0
  18. package/dist/contracts/api-keys.js +11 -9
  19. package/dist/contracts/archives.d.ts +1 -2
  20. package/dist/contracts/archives.js +15 -17
  21. package/dist/contracts/beta.js +1 -1
  22. package/dist/contracts/builds.js +14 -14
  23. package/dist/contracts/channels.js +2 -2
  24. package/dist/contracts/comments.d.ts +143 -99
  25. package/dist/contracts/comments.js +194 -79
  26. package/dist/contracts/common.d.ts +12 -12
  27. package/dist/contracts/common.js +33 -30
  28. package/dist/contracts/continuation.js +5 -5
  29. package/dist/contracts/countries.js +2 -7
  30. package/dist/contracts/data-source-routes.d.ts +20 -0
  31. package/dist/contracts/data-source-routes.js +15 -0
  32. package/dist/contracts/device-auth.d.ts +13 -9
  33. package/dist/contracts/device-auth.js +12 -12
  34. package/dist/contracts/docs.d.ts +10 -10
  35. package/dist/contracts/domains.js +39 -33
  36. package/dist/contracts/enums.d.ts +4 -14
  37. package/dist/contracts/enums.js +3 -10
  38. package/dist/contracts/error-code-meta.d.ts +122 -56
  39. package/dist/contracts/error-code-meta.js +43 -19
  40. package/dist/contracts/error-codes.d.ts +7 -4
  41. package/dist/contracts/error-codes.js +48 -24
  42. package/dist/contracts/events.d.ts +22 -15
  43. package/dist/contracts/events.js +33 -5
  44. package/dist/contracts/execution.d.ts +238 -0
  45. package/dist/contracts/execution.js +309 -0
  46. package/dist/contracts/feature-lifecycle.d.ts +39 -188
  47. package/dist/contracts/feature-lifecycle.js +52 -196
  48. package/dist/contracts/features.d.ts +13 -115
  49. package/dist/contracts/features.js +5 -35
  50. package/dist/contracts/functions.d.ts +597 -0
  51. package/dist/contracts/functions.js +475 -0
  52. package/dist/contracts/grants.d.ts +1175 -0
  53. package/dist/contracts/grants.js +783 -0
  54. package/dist/contracts/ids.d.ts +7 -5
  55. package/dist/contracts/ids.js +7 -7
  56. package/dist/contracts/internal.js +2 -2
  57. package/dist/contracts/mcp.d.ts +17 -556
  58. package/dist/contracts/mcp.js +9 -103
  59. package/dist/contracts/me.js +3 -4
  60. package/dist/contracts/notifications.d.ts +108 -0
  61. package/dist/contracts/notifications.js +54 -0
  62. package/dist/contracts/oauth-resources.d.ts +22 -5
  63. package/dist/contracts/oauth-resources.js +23 -2
  64. package/dist/contracts/operations.d.ts +0 -48
  65. package/dist/contracts/operations.js +0 -19
  66. package/dist/contracts/pages.d.ts +10 -14
  67. package/dist/contracts/pages.js +17 -21
  68. package/dist/contracts/plan-policy.d.ts +1 -8
  69. package/dist/contracts/plan-policy.js +1 -4
  70. package/dist/contracts/platform.d.ts +78 -30
  71. package/dist/contracts/platform.js +69 -20
  72. package/dist/contracts/problem-document.d.ts +14 -0
  73. package/dist/contracts/problem-document.js +35 -0
  74. package/dist/contracts/push-new.d.ts +41 -2
  75. package/dist/contracts/push-new.js +4 -0
  76. package/dist/contracts/quotas.d.ts +0 -1
  77. package/dist/contracts/quotas.js +0 -5
  78. package/dist/contracts/repository-connections.d.ts +1 -1
  79. package/dist/contracts/resources.d.ts +16 -71
  80. package/dist/contracts/resources.js +13 -16
  81. package/dist/contracts/runtime-api.d.ts +666 -303
  82. package/dist/contracts/runtime-api.js +272 -113
  83. package/dist/contracts/runtime-app.d.ts +168 -0
  84. package/dist/contracts/runtime-app.js +42 -0
  85. package/dist/contracts/runtime-db.d.ts +424 -0
  86. package/dist/contracts/runtime-db.js +108 -0
  87. package/dist/contracts/runtime-storage.d.ts +120 -0
  88. package/dist/contracts/runtime-storage.js +40 -0
  89. package/dist/contracts/sf-config-v1.d.ts +54 -1
  90. package/dist/contracts/sf-config-v1.js +2 -2
  91. package/dist/contracts/sites.d.ts +6 -0
  92. package/dist/contracts/sites.js +30 -11
  93. package/dist/contracts/space-config.d.ts +91 -121
  94. package/dist/contracts/space-config.js +464 -42
  95. package/dist/contracts/spaces.d.ts +300 -171
  96. package/dist/contracts/spaces.js +128 -98
  97. package/dist/contracts/superadmin-emails.d.ts +2 -2
  98. package/dist/contracts/superadmin-spaces.d.ts +26 -130
  99. package/dist/contracts/superadmin-spaces.js +22 -0
  100. package/dist/contracts/superadmin-tenants.js +4 -4
  101. package/dist/contracts/superadmin.d.ts +4 -0
  102. package/dist/contracts/superadmin.js +14 -1
  103. package/dist/contracts/tags.d.ts +6 -6
  104. package/dist/contracts/tags.js +1 -1
  105. package/dist/contracts/teams.d.ts +9 -9
  106. package/dist/contracts/teams.js +25 -16
  107. package/dist/contracts/theme-json.js +1 -1
  108. package/dist/contracts/transfers.d.ts +0 -1
  109. package/dist/contracts/transfers.js +4 -4
  110. package/dist/contracts/variables.js +3 -3
  111. package/dist/contracts/zero.d.ts +384 -596
  112. package/dist/contracts/zero.js +167 -299
  113. package/dist/dashboard-paths/index.d.ts +18 -3
  114. package/dist/dashboard-paths/index.js +19 -0
  115. package/dist/docs/agent-handoff-document.d.ts +2 -2
  116. package/dist/docs/agent-handoff-document.js +8 -7
  117. package/dist/docs/agent-prose.d.ts +22 -3
  118. package/dist/docs/agent-prose.js +227 -55
  119. package/dist/docs/agent-setup.d.ts +14 -11
  120. package/dist/docs/agent-setup.js +289 -61
  121. package/dist/docs/agent-solutions.d.ts +6 -6
  122. package/dist/docs/agent-solutions.js +24 -22
  123. package/dist/docs/catalog.d.ts +53 -98
  124. package/dist/docs/catalog.js +90 -117
  125. package/dist/docs/error-docs.js +304 -236
  126. package/dist/docs/skill-distribution.d.ts +2 -0
  127. package/dist/docs/skill-distribution.js +7 -4
  128. package/dist/docs/skills.d.ts +546 -0
  129. package/dist/docs/skills.js +340 -0
  130. package/dist/utils/auth-redirect.d.ts +1 -3
  131. package/dist/utils/auth-redirect.js +1 -21
  132. package/dist/utils/browser-credential.d.ts +19 -0
  133. package/dist/utils/browser-credential.js +62 -0
  134. package/dist/utils/build-settings.d.ts +2 -0
  135. package/dist/utils/build-settings.js +61 -9
  136. package/dist/utils/comment-avatars.d.ts +7 -0
  137. package/dist/utils/comment-avatars.js +42 -0
  138. package/dist/utils/content-type.d.ts +3 -3
  139. package/dist/utils/content-type.js +38 -61
  140. package/dist/utils/credential-policy.d.ts +1 -1
  141. package/dist/utils/credential-policy.js +4 -0
  142. package/dist/utils/domain-diagnostics.d.ts +5 -5
  143. package/dist/utils/domain-diagnostics.js +21 -10
  144. package/dist/utils/egress-policy.fixtures.json +1 -1
  145. package/dist/utils/grants.d.ts +53 -0
  146. package/dist/utils/grants.js +160 -0
  147. package/dist/utils/local-space-state.d.ts +78 -4
  148. package/dist/utils/local-space-state.js +384 -82
  149. package/dist/utils/one-shot-replay.d.ts +14 -0
  150. package/dist/utils/one-shot-replay.js +21 -0
  151. package/dist/utils/page-preview.d.ts +7 -0
  152. package/dist/utils/page-preview.js +9 -0
  153. package/dist/utils/pages.d.ts +2 -5
  154. package/dist/utils/pages.js +64 -33
  155. package/dist/utils/publish-form-data.d.ts +22 -0
  156. package/dist/utils/publish-form-data.js +26 -0
  157. package/dist/utils/publish-policy.d.ts +34 -4
  158. package/dist/utils/publish-policy.js +149 -62
  159. package/dist/utils/query-keys.d.ts +8 -10
  160. package/dist/utils/query-keys.js +14 -10
  161. package/dist/utils/runtime-paths.d.ts +3 -0
  162. package/dist/utils/runtime-paths.js +7 -0
  163. package/dist/utils/secure-local-file.d.ts +73 -0
  164. package/dist/utils/secure-local-file.js +1373 -0
  165. package/dist/utils/space-config.d.ts +17 -4
  166. package/dist/utils/space-config.js +151 -8
  167. package/dist/utils/static-runtime-policy.d.ts +5 -0
  168. package/dist/utils/static-runtime-policy.js +54 -10
  169. package/dist/utils/storage-policy.d.ts +20 -0
  170. package/dist/utils/storage-policy.fixtures.json +174 -0
  171. package/dist/utils/storage-policy.js +193 -0
  172. package/dist/utils/upload-session.d.ts +15 -1
  173. package/dist/utils/upload-session.js +68 -2
  174. package/dist/utils/version-file-browser.d.ts +13 -0
  175. package/dist/utils/version-file-browser.js +59 -0
  176. package/package.json +20 -1
  177. package/dist/contracts/mounts.d.ts +0 -142
  178. package/dist/contracts/mounts.js +0 -56
  179. package/dist/utils/access-grant.d.ts +0 -14
  180. package/dist/utils/access-grant.js +0 -88
  181. package/dist/utils/access-match.d.ts +0 -46
  182. package/dist/utils/access-match.fixtures.json +0 -513
  183. package/dist/utils/access-match.js +0 -312
  184. package/dist/utils/access-verdict.fixtures.json +0 -421
  185. package/dist/utils/asset-fingerprint.d.ts +0 -53
  186. package/dist/utils/asset-fingerprint.js +0 -574
  187. package/dist/utils/sf-config-v1.d.ts +0 -2
  188. package/dist/utils/sf-config-v1.fixtures.json +0 -9
  189. package/dist/utils/sf-config-v1.js +0 -551
@@ -2,9 +2,9 @@
2
2
  * Map a file extension to a Content-Type header value. Returns
3
3
  * `application/octet-stream` when the extension is not recognized.
4
4
  *
5
- * This is the superset of the extensions handled by the control-plane
6
- * runtime service and the spacefast-cli uploader, so both sides agree on the
7
- * same content types when uploading and serving version files.
5
+ * MIME extension coverage comes from the maintained `mime` database. The
6
+ * small overrides above preserve intentional existing policy; this module
7
+ * does not carry a second partial extension registry.
8
8
  */
9
9
  export declare function contentTypeForPath(filePath: string): string;
10
10
  export declare function textTypeForPath(filePath: string): string | null;
@@ -1,71 +1,41 @@
1
+ import mime from "mime";
2
+ const CONTENT_TYPE_OVERRIDES = {
3
+ // Preserve the public uploader/runtime policy where the database's preferred
4
+ // server-oriented type differs from the established browser-facing type.
5
+ ".cjs": "text/javascript",
6
+ ".cts": "text/plain",
7
+ ".d.ts": "text/plain",
8
+ ".ico": "image/x-icon",
9
+ ".mts": "text/plain",
10
+ ".rs": "text/plain",
11
+ ".sh": "text/plain",
12
+ ".tgz": "application/gzip",
13
+ ".ts": "text/plain",
14
+ };
15
+ const UTF8_APPLICATION_TYPES = new Set([
16
+ "application/json",
17
+ "application/manifest+json",
18
+ "application/xml",
19
+ ]);
1
20
  /**
2
21
  * Map a file extension to a Content-Type header value. Returns
3
22
  * `application/octet-stream` when the extension is not recognized.
4
23
  *
5
- * This is the superset of the extensions handled by the control-plane
6
- * runtime service and the spacefast-cli uploader, so both sides agree on the
7
- * same content types when uploading and serving version files.
24
+ * MIME extension coverage comes from the maintained `mime` database. The
25
+ * small overrides above preserve intentional existing policy; this module
26
+ * does not carry a second partial extension registry.
8
27
  */
9
28
  export function contentTypeForPath(filePath) {
10
- const extensionIndex = filePath.lastIndexOf(".");
11
- const extension = extensionIndex >= 0 ? filePath.slice(extensionIndex).toLowerCase() : "";
12
- switch (extension) {
13
- case ".html":
14
- case ".htm":
15
- return "text/html; charset=utf-8";
16
- case ".css":
17
- return "text/css; charset=utf-8";
18
- case ".js":
19
- case ".mjs":
20
- case ".cjs":
21
- return "text/javascript; charset=utf-8";
22
- case ".json":
23
- case ".map":
24
- return "application/json; charset=utf-8";
25
- case ".webmanifest":
26
- return "application/manifest+json; charset=utf-8";
27
- case ".gz":
28
- case ".tgz":
29
- return "application/gzip";
30
- case ".txt":
31
- case ".sh":
32
- return "text/plain; charset=utf-8";
33
- case ".md":
34
- return "text/markdown; charset=utf-8";
35
- case ".csv":
36
- return "text/csv; charset=utf-8";
37
- case ".xml":
38
- return "application/xml; charset=utf-8";
39
- case ".svg":
40
- return "image/svg+xml";
41
- case ".png":
42
- return "image/png";
43
- case ".jpg":
44
- case ".jpeg":
45
- return "image/jpeg";
46
- case ".gif":
47
- return "image/gif";
48
- case ".webp":
49
- return "image/webp";
50
- case ".avif":
51
- return "image/avif";
52
- case ".ico":
53
- return "image/x-icon";
54
- case ".pdf":
55
- return "application/pdf";
56
- case ".woff":
57
- return "font/woff";
58
- case ".woff2":
59
- return "font/woff2";
60
- case ".ttf":
61
- return "font/ttf";
62
- case ".otf":
63
- return "font/otf";
64
- case ".wasm":
65
- return "application/wasm";
66
- default:
67
- return "application/octet-stream";
29
+ const normalizedPath = filePath.toLowerCase();
30
+ const override = Object.entries(CONTENT_TYPE_OVERRIDES).find(([suffix]) => normalizedPath.endsWith(suffix))?.[1];
31
+ const type = override ?? mime.getType(filePath);
32
+ if (!type) {
33
+ return "application/octet-stream";
68
34
  }
35
+ if (type.startsWith("text/") || UTF8_APPLICATION_TYPES.has(type)) {
36
+ return `${type}; charset=utf-8`;
37
+ }
38
+ return type;
69
39
  }
70
40
  function hasPrefix(bytes, prefix) {
71
41
  return prefix.every((value, index) => bytes[index] === value);
@@ -141,5 +111,12 @@ export function contentTypeForBytes(filePath, bytes) {
141
111
  if (textType && bytesLookTextual(bytes)) {
142
112
  return textType;
143
113
  }
114
+ // Container signatures such as ISO-BMFF, WebM, and Ogg are shared by audio
115
+ // and video variants. The extension is the reliable discriminator for the
116
+ // manifest declaration, while unknown binary extensions remain generic.
117
+ const pathType = contentTypeForPath(filePath);
118
+ if (pathType.startsWith("audio/") || pathType.startsWith("video/")) {
119
+ return pathType;
120
+ }
144
121
  return "application/octet-stream";
145
122
  }
@@ -16,7 +16,7 @@ export type PolicyAuthorizeInput = {
16
16
  resource: string | readonly string[];
17
17
  conditions?: RequestCondition;
18
18
  };
19
- export declare const publishActions: ("spaces:read" | "spaces:write" | "spaces:create" | "spaces:publish" | "versions:read" | "versions:promote" | "versions:download" | "builds:read" | "builds:create")[];
19
+ export declare const publishActions: ("spaces:read" | "spaces:write" | "spaces:create" | "spaces:publish" | "versions:read" | "versions:promote" | "versions:download" | "builds:read" | "builds:trigger" | "builds:create")[];
20
20
  export declare function compileApiKeyPreset(preset: ApiKeyPreset, input: PolicyCompileInput): ApiCredentialPolicy;
21
21
  /** Expand a named API-key preset into the finite permissions sent on the public wire contract. */
22
22
  export declare function apiKeyPresetPermissions(preset: ApiKeyPreset): StatticAction[];
@@ -8,10 +8,12 @@ export const publishActions = [
8
8
  "versions:promote",
9
9
  "versions:download",
10
10
  "builds:read",
11
+ "builds:trigger",
11
12
  "builds:create",
12
13
  ];
13
14
  const spaceAdminActions = [
14
15
  ...publishActions,
16
+ "spaces:rename",
15
17
  "spaces:delete",
16
18
  "spaces:transfer",
17
19
  "spaces:export",
@@ -81,12 +83,14 @@ export function compileTeamRolePolicy(input) {
81
83
  "spaces:read",
82
84
  "spaces:create",
83
85
  "spaces:write",
86
+ "spaces:rename",
84
87
  "spaces:publish",
85
88
  "versions:read",
86
89
  "versions:promote",
87
90
  "versions:download",
88
91
  "domains:read",
89
92
  "builds:read",
93
+ "builds:trigger",
90
94
  "builds:create",
91
95
  "teams:read",
92
96
  "billing:read",
@@ -1,15 +1,15 @@
1
1
  export type HostnameDiagnosticsForActions = {
2
2
  hostname: string;
3
- providerIps: string[];
4
- dnsInstructions: unknown[];
3
+ providerIps?: string[] | undefined;
4
+ dnsInstructions?: unknown[] | undefined;
5
5
  dns: {
6
6
  pointsToProvider: boolean;
7
7
  txtVerified: boolean | null;
8
8
  expectedTxtValue: string | null;
9
9
  records?: {
10
- a: string[];
11
- aaaa: string[];
12
- };
10
+ a?: string[] | undefined;
11
+ aaaa?: string[] | undefined;
12
+ } | undefined;
13
13
  };
14
14
  ssl: {
15
15
  status: string;
@@ -3,8 +3,20 @@ import { addressDnsInstructionIsObserved, addressDnsInstructionsForHostname, uns
3
3
  export function domainRequiredActions(hostnameDiagnostics) {
4
4
  const actions = [];
5
5
  for (const entry of hostnameDiagnostics) {
6
- const unsupportedRecords = entry.dns.records
7
- ? unsupportedAddressDnsRecordsForHostname({ ...entry, records: entry.dns.records })
6
+ const records = entry.dns.records
7
+ ? {
8
+ a: entry.dns.records.a ?? [],
9
+ aaaa: entry.dns.records.aaaa ?? [],
10
+ }
11
+ : undefined;
12
+ const dnsInstructions = entry.dnsInstructions ?? [];
13
+ const providerIps = entry.providerIps ?? [];
14
+ const unsupportedRecords = records
15
+ ? unsupportedAddressDnsRecordsForHostname({
16
+ hostname: entry.hostname,
17
+ dnsInstructions,
18
+ records,
19
+ })
8
20
  : [];
9
21
  actions.push(...unsupportedRecords.map((record) => ({
10
22
  code: "dns_unaccepted_address",
@@ -12,13 +24,12 @@ export function domainRequiredActions(hostnameDiagnostics) {
12
24
  message: `Remove the ${record.type} record on ${record.name} with value ${record.value}; it does not match a Spacefast target for this hostname.`,
13
25
  })));
14
26
  if (!entry.dns.pointsToProvider) {
15
- const routingInstructions = addressDnsInstructionsForHostname(entry);
16
- const observedAddressRecords = entry.dns.records;
17
- const missingRoutingInstructions = observedAddressRecords
18
- ? routingInstructions.filter((instruction) => !addressDnsInstructionIsObserved({
19
- instruction,
20
- records: observedAddressRecords,
21
- }))
27
+ const routingInstructions = addressDnsInstructionsForHostname({
28
+ hostname: entry.hostname,
29
+ dnsInstructions,
30
+ });
31
+ const missingRoutingInstructions = records
32
+ ? routingInstructions.filter((instruction) => !addressDnsInstructionIsObserved({ instruction, records }))
22
33
  : routingInstructions;
23
34
  if (missingRoutingInstructions.length > 0) {
24
35
  actions.push(...missingRoutingInstructions.map((instruction) => ({
@@ -28,7 +39,7 @@ export function domainRequiredActions(hostnameDiagnostics) {
28
39
  })));
29
40
  }
30
41
  else if (routingInstructions.length === 0) {
31
- const targets = entry.providerIps.length > 0 ? entry.providerIps.join(", ") : "Spacefast";
42
+ const targets = providerIps.length > 0 ? providerIps.join(", ") : "Spacefast";
32
43
  actions.push({
33
44
  code: "dns_not_pointing",
34
45
  hostname: entry.hostname,
@@ -1,5 +1,5 @@
1
1
  {
2
- "comment": "Shared egress-policy corpus (spec: one proxy-egress-safety invariant). Exercised by BOTH enforcers: apps/control-plane/src/http/egress-guard.ts (control-plane SSRF guard) and runtime/engine/shared/egress.php (runtime proxy egress). Every case must produce the same verdict in TypeScript and PHP; parity is asserted by apps/control-plane/src/runtime/php-policy-parity.test.ts and src/http/egress-guard.test.ts.",
2
+ "comment": "Shared egress-policy corpus (spec: one proxy-egress-safety invariant). Exercised by THREE enforcers: apps/control-plane/src/http/egress-guard.ts (control-plane SSRF guard), runtime/engine/shared/egress.php (runtime proxy egress), and crates/stattic-runtime-core (Rust finalizer policy). Every case must produce the same verdict in TypeScript, PHP, and Rust; parity is asserted by apps/control-plane/src/runtime/php-policy-parity.test.ts, src/http/egress-guard.test.ts, and crates/stattic-runtime-core/src/policy.rs::proxy_destinations_match_the_shared_egress_corpus.",
3
3
  "ips": [
4
4
  { "ip": "127.0.0.1", "public": false, "reason": "loopback" },
5
5
  { "ip": "127.8.9.10", "public": false, "reason": "loopback /8" },
@@ -0,0 +1,53 @@
1
+ import { type Grant, type GrantCapability, type GrantCheckFailure, type GrantSource } from "../contracts/grants.js";
2
+ export type GrantPrincipal = {
3
+ teamIds?: readonly string[];
4
+ personIds?: readonly string[];
5
+ linkIds?: readonly string[];
6
+ passwordCredentialIds?: readonly string[];
7
+ machineIds?: readonly string[];
8
+ externalIdentities?: readonly {
9
+ issuer: string;
10
+ subject: string;
11
+ }[];
12
+ verifiedEmail?: boolean;
13
+ ipAddress?: string;
14
+ country?: string;
15
+ userAgent?: string;
16
+ };
17
+ export type GrantEvaluationTarget = {
18
+ kind: "live";
19
+ } | {
20
+ kind: "version";
21
+ versionId: string;
22
+ } | {
23
+ kind: "branch";
24
+ branch: string;
25
+ };
26
+ export type GrantEvaluationInput = {
27
+ grants: readonly Grant[];
28
+ resource: string;
29
+ resourceKind?: "route" | "artifact";
30
+ target: GrantEvaluationTarget;
31
+ principal?: GrantPrincipal;
32
+ spaceClaimed: boolean;
33
+ now?: Date;
34
+ useCounts?: Readonly<Record<string, number>>;
35
+ acquiredGrantIds?: readonly string[];
36
+ };
37
+ export type GrantMatch = {
38
+ grantId: string;
39
+ capabilities: GrantCapability[];
40
+ source: GrantSource;
41
+ };
42
+ export type GrantEvaluation = {
43
+ capabilities: GrantCapability[];
44
+ matches: GrantMatch[];
45
+ };
46
+ export declare function grantResourcePatternMatches(pattern: string, resource: string, resourceKind?: "route" | "artifact"): boolean;
47
+ export type GrantEvaluationExplanation = {
48
+ grantId: string;
49
+ failures: GrantCheckFailure[];
50
+ };
51
+ export declare function explainGrants(input: GrantEvaluationInput): GrantEvaluationExplanation[];
52
+ export declare function evaluateGrants(input: GrantEvaluationInput): GrantEvaluation;
53
+ export declare function grantAllows(input: GrantEvaluationInput, capability: GrantCapability): boolean;
@@ -0,0 +1,160 @@
1
+ import { normalizeAccessScopePath } from "../contracts/access.js";
2
+ import { GRANT_CAPABILITIES, grantNetworkCidrMatches, normalizeGrantResourcePattern, } from "../contracts/grants.js";
3
+ function audienceMatches(audience, principal) {
4
+ switch (audience.kind) {
5
+ case "public":
6
+ return true;
7
+ case "team":
8
+ return principal.teamIds?.includes(audience.teamId) ?? false;
9
+ case "person":
10
+ return principal.personIds?.includes(audience.personId) ?? false;
11
+ case "link":
12
+ return principal.linkIds?.includes(audience.linkId) ?? false;
13
+ case "password":
14
+ return principal.passwordCredentialIds?.includes(audience.credentialId) ?? false;
15
+ case "machine":
16
+ return principal.machineIds?.includes(audience.machineId) ?? false;
17
+ case "external":
18
+ return (principal.externalIdentities?.some((identity) => identity.issuer === audience.issuer && identity.subject === audience.subject) ?? false);
19
+ }
20
+ throw new Error("Unsupported Grant audience.");
21
+ }
22
+ function targetMatches(selector, target) {
23
+ switch (selector.kind) {
24
+ case "live":
25
+ return target.kind === "live";
26
+ case "version":
27
+ return target.kind === "version" && target.versionId === selector.versionId;
28
+ case "branch":
29
+ return target.kind === "branch" && target.branch === selector.branch;
30
+ case "all_versions":
31
+ return target.kind === "version";
32
+ }
33
+ throw new Error("Unsupported Grant target.");
34
+ }
35
+ function normalizeGrantEvaluationResource(resource, resourceKind) {
36
+ if (resourceKind === "route")
37
+ return normalizeAccessScopePath(resource);
38
+ if (!resource.startsWith("/")) {
39
+ throw new TypeError("artifact resource must be an absolute path");
40
+ }
41
+ const canonical = normalizeGrantResourcePattern(resource);
42
+ if (canonical.includes("*")) {
43
+ throw new TypeError("artifact resource cannot contain wildcards");
44
+ }
45
+ return canonical;
46
+ }
47
+ export function grantResourcePatternMatches(pattern, resource, resourceKind = "route") {
48
+ const canonicalPattern = normalizeGrantResourcePattern(pattern);
49
+ const canonicalResource = normalizeGrantEvaluationResource(resource, resourceKind);
50
+ const patternSegments = canonicalPattern === "/" ? [] : canonicalPattern.slice(1).split("/");
51
+ const resourceSegments = canonicalResource === "/" ? [] : canonicalResource.slice(1).split("/");
52
+ const matches = (patternIndex, resourceIndex) => {
53
+ const patternSegment = patternSegments[patternIndex];
54
+ if (patternSegment === undefined)
55
+ return resourceIndex === resourceSegments.length;
56
+ if (patternSegment === "**") {
57
+ if (patternIndex === patternSegments.length - 1)
58
+ return true;
59
+ for (let next = resourceIndex; next <= resourceSegments.length; next++) {
60
+ if (matches(patternIndex + 1, next))
61
+ return true;
62
+ }
63
+ return false;
64
+ }
65
+ if (resourceIndex >= resourceSegments.length)
66
+ return false;
67
+ if (patternSegment !== "*" && patternSegment !== resourceSegments[resourceIndex])
68
+ return false;
69
+ return matches(patternIndex + 1, resourceIndex + 1);
70
+ };
71
+ return matches(0, 0);
72
+ }
73
+ function grantCheckFailures(grant, input, principal, now) {
74
+ const failures = [];
75
+ if (grant.source.kind === "config" && !input.spaceClaimed) {
76
+ failures.push("space_unclaimed_config");
77
+ }
78
+ if (!audienceMatches(grant.audience, principal))
79
+ failures.push("audience");
80
+ if (!targetMatches(grant.target, input.target))
81
+ failures.push("target");
82
+ const timestamp = now.getTime();
83
+ if (!Number.isFinite(timestamp)) {
84
+ failures.push("invalid_time");
85
+ }
86
+ else {
87
+ const notBefore = grant.constraints.notBefore;
88
+ if (notBefore !== undefined && timestamp < Date.parse(notBefore))
89
+ failures.push("not_before");
90
+ const expiresAt = grant.constraints.expiresAt;
91
+ if (expiresAt !== undefined && timestamp >= Date.parse(expiresAt))
92
+ failures.push("expired");
93
+ }
94
+ const maxUses = grant.constraints.maxUses;
95
+ if (maxUses !== undefined && !input.acquiredGrantIds?.includes(grant.id)) {
96
+ const uses = input.useCounts?.[grant.id];
97
+ if (!Number.isInteger(uses) || (uses ?? 0) < 0 || (uses ?? 0) >= maxUses) {
98
+ failures.push("max_uses");
99
+ }
100
+ }
101
+ if (grant.constraints.requireVerifiedEmail && principal.verifiedEmail !== true) {
102
+ failures.push("verified_email");
103
+ }
104
+ if (grant.constraints.network &&
105
+ ((grant.constraints.network.ipCidrs !== undefined &&
106
+ (!principal.ipAddress ||
107
+ !grant.constraints.network.ipCidrs.some((cidr) => grantNetworkCidrMatches(cidr, principal.ipAddress ?? "")))) ||
108
+ (grant.constraints.network.countries !== undefined &&
109
+ (!principal.country ||
110
+ !grant.constraints.network.countries.includes(principal.country.toUpperCase()))) ||
111
+ (grant.constraints.network.excludedCountries !== undefined &&
112
+ (!principal.country ||
113
+ grant.constraints.network.excludedCountries.includes(principal.country.toUpperCase()))) ||
114
+ (grant.constraints.network.excludedUserAgentSubstrings !== undefined &&
115
+ (!principal.userAgent ||
116
+ grant.constraints.network.excludedUserAgentSubstrings.some((needle) => principal.userAgent?.toLowerCase().includes(needle.toLowerCase())))))) {
117
+ failures.push("network");
118
+ }
119
+ const included = grant.resources.include.some((pattern) => grantResourcePatternMatches(pattern, input.resource, input.resourceKind));
120
+ if (!included) {
121
+ failures.push("resource_not_included");
122
+ }
123
+ else if (grant.resources.exclude.some((pattern) => grantResourcePatternMatches(pattern, input.resource, input.resourceKind))) {
124
+ failures.push("resource_excluded");
125
+ }
126
+ return failures;
127
+ }
128
+ export function explainGrants(input) {
129
+ const principal = input.principal ?? {};
130
+ const now = input.now ?? new Date();
131
+ return input.grants.map((grant) => ({
132
+ grantId: grant.id,
133
+ failures: grantCheckFailures(grant, input, principal, now),
134
+ }));
135
+ }
136
+ export function evaluateGrants(input) {
137
+ const principal = input.principal ?? {};
138
+ const now = input.now ?? new Date();
139
+ const matches = [];
140
+ const capabilities = new Set();
141
+ for (const grant of input.grants) {
142
+ if (grantCheckFailures(grant, input, principal, now).length > 0)
143
+ continue;
144
+ const matchedCapabilities = [...grant.capabilities];
145
+ matches.push({
146
+ grantId: grant.id,
147
+ capabilities: matchedCapabilities,
148
+ source: grant.source,
149
+ });
150
+ for (const capability of matchedCapabilities)
151
+ capabilities.add(capability);
152
+ }
153
+ return {
154
+ capabilities: GRANT_CAPABILITIES.filter((capability) => capabilities.has(capability)),
155
+ matches,
156
+ };
157
+ }
158
+ export function grantAllows(input, capability) {
159
+ return evaluateGrants(input).capabilities.includes(capability);
160
+ }
@@ -13,15 +13,18 @@ export declare const LOCAL_SPACE_STATE_FILE_NAME = "state.json";
13
13
  * `.spacefast/state.json` is the local credential/cache axis: it records the
14
14
  * resolved `spaceId`, provider `apiUrl`, anonymous `claimToken` before claim,
15
15
  * durable `accessToken` after continuation, and small publish caches such as
16
- * `lastVersionId`. It is secret, machine-written, gitignored, and merge-written
17
- * so newer tools preserve unknown fields from older/newer tools.
16
+ * `lastVersionId`. It is secret, machine-written, gitignored, and merge-written.
17
+ * The strict versioned schema fails closed instead of letting an older tool
18
+ * reinterpret fields written under a contract it does not understand.
18
19
  */
19
20
  export declare const localSpaceLinkSchema: z.ZodObject<{
20
21
  team: z.ZodOptional<z.ZodString>;
21
22
  space: z.ZodString;
22
23
  apiBaseUrl: z.ZodOptional<z.ZodString>;
23
24
  }, z.core.$loose>;
25
+ export declare const LOCAL_SPACE_STATE_SCHEMA_VERSION = 1;
24
26
  export declare const localSpaceStateSchema: z.ZodObject<{
27
+ schemaVersion: z.ZodLiteral<1>;
25
28
  spaceId: z.ZodOptional<z.ZodString>;
26
29
  apiUrl: z.ZodOptional<z.ZodString>;
27
30
  teamSlug: z.ZodOptional<z.ZodString>;
@@ -30,11 +33,52 @@ export declare const localSpaceStateSchema: z.ZodObject<{
30
33
  expiresAt: z.ZodOptional<z.ZodString>;
31
34
  accessToken: z.ZodOptional<z.ZodString>;
32
35
  lastVersionId: z.ZodOptional<z.ZodString>;
36
+ pendingOperationId: z.ZodOptional<z.ZodString>;
33
37
  lastVersionUrl: z.ZodOptional<z.ZodString>;
34
38
  lastSpaceUrl: z.ZodOptional<z.ZodString>;
35
- }, z.core.$loose>;
39
+ /** Secret per-checkout entropy used only to namespace replay-safe first publishes. */
40
+ anonymousPublishNonce: z.ZodOptional<z.ZodString>;
41
+ /** Unsettled first-publish mutation. Retries must reuse this exact key. */
42
+ pendingPublish: z.ZodOptional<z.ZodObject<{
43
+ key: z.ZodString;
44
+ principal: z.ZodString;
45
+ kind: z.ZodEnum<{
46
+ anonymous: "anonymous";
47
+ authenticated: "authenticated";
48
+ }>;
49
+ intent: z.ZodOptional<z.ZodString>;
50
+ createdAt: z.ZodString;
51
+ }, z.core.$strict>>;
52
+ /** Settled creation receipt identity retained for invocations that began before its commit. */
53
+ creationReplay: z.ZodOptional<z.ZodObject<{
54
+ key: z.ZodString;
55
+ principal: z.ZodString;
56
+ kind: z.ZodEnum<{
57
+ anonymous: "anonymous";
58
+ authenticated: "authenticated";
59
+ }>;
60
+ intent: z.ZodOptional<z.ZodString>;
61
+ createdAt: z.ZodString;
62
+ }, z.core.$strict>>;
63
+ /** Random replay capability reserved before consuming a claim credential. */
64
+ pendingCredentialReplay: z.ZodOptional<z.ZodObject<{
65
+ operation: z.ZodEnum<{
66
+ "claim-rotate": "claim-rotate";
67
+ "claim-exchange": "claim-exchange";
68
+ }>;
69
+ credentialFingerprint: z.ZodString;
70
+ key: z.ZodString;
71
+ principal: z.ZodString;
72
+ createdAt: z.ZodString;
73
+ }, z.core.$strict>>;
74
+ }, z.core.$strict>;
36
75
  export type LocalSpaceLink = z.infer<typeof localSpaceLinkSchema>;
37
76
  export type LocalSpaceStateFile = z.infer<typeof localSpaceStateSchema>;
77
+ export type LocalSpaceStateWrite = Omit<LocalSpaceStateFile, "schemaVersion"> & {
78
+ schemaVersion?: typeof LOCAL_SPACE_STATE_SCHEMA_VERSION;
79
+ };
80
+ export type LocalCredentialReplay = NonNullable<LocalSpaceStateFile["pendingCredentialReplay"]>;
81
+ export type LocalCredentialReplayOperation = LocalCredentialReplay["operation"];
38
82
  export type DiscoveredLocalSpaceState = {
39
83
  root: string;
40
84
  statePath: string | null;
@@ -48,6 +92,7 @@ export type DiscoveredLocalSpaceState = {
48
92
  claimUrl: string | undefined;
49
93
  expiresAt: string | undefined;
50
94
  lastVersionId: string | undefined;
95
+ pendingOperationId: string | undefined;
51
96
  };
52
97
  export type SaveLocalSpaceStateInput = {
53
98
  spaceId: string;
@@ -58,9 +103,26 @@ export type SaveLocalSpaceStateInput = {
58
103
  expiresAt?: string | undefined;
59
104
  accessToken?: string | undefined;
60
105
  lastVersionId?: string | undefined;
106
+ pendingOperationId?: string | undefined;
61
107
  lastVersionUrl?: string | undefined;
62
108
  lastSpaceUrl?: string | undefined;
109
+ /** Required when replacing one shown-once credential with another. */
110
+ expectedCredential?: {
111
+ claimToken?: string | undefined;
112
+ accessToken?: string | undefined;
113
+ };
114
+ /** Replay reservation completed by this shown-once credential replacement. */
115
+ credentialReplay?: LocalCredentialReplay | undefined;
116
+ /** Explicit relinks may replace another Space; ordinary cache writes may not. */
117
+ replaceSpace?: boolean | undefined;
63
118
  };
119
+ export declare class LocalSpaceStateConflictError extends Error {
120
+ name: string;
121
+ }
122
+ export declare class LocalSpaceStateError extends Error {
123
+ readonly path: string;
124
+ constructor(filePath: string, cause?: unknown);
125
+ }
64
126
  export declare function localSpaceStateDir(root: string): string;
65
127
  export declare function localSpaceStatePath(root: string): string;
66
128
  export declare function localSpaceLinkPath(root: string): string;
@@ -83,7 +145,19 @@ export declare function readLocalSpaceStateAt(root: string): Promise<DiscoveredL
83
145
  export declare function findUpward<T>(probe: (dir: string) => Promise<T | null>, startDir?: string, boundary?: string): Promise<T | null>;
84
146
  export declare function discoverLocalSpaceState(startDir?: string, boundary?: string): Promise<DiscoveredLocalSpaceState | null>;
85
147
  export declare function writeLocalSpaceLink(root: string, link: LocalSpaceLink): Promise<string>;
86
- export declare function writeLocalSpaceState(root: string, state: LocalSpaceStateFile): Promise<string>;
148
+ export declare function mutateLocalSpaceState(root: string, mutate: (current: LocalSpaceStateFile) => LocalSpaceStateWrite, options?: {
149
+ beforeCommit?: (() => void | Promise<void>) | undefined;
150
+ afterBackup?: (() => void | Promise<void>) | undefined;
151
+ /** Fault-injection observer used to synchronize concurrency tests. */
152
+ onConflict?: (() => void | Promise<void>) | undefined;
153
+ }): Promise<LocalSpaceStateFile>;
154
+ export declare function writeLocalSpaceState(root: string, state: LocalSpaceStateWrite): Promise<string>;
155
+ /** Reserve random replay custody before a claim token can be consumed. */
156
+ export declare function reserveLocalCredentialReplay(root: string, input: {
157
+ operation: LocalCredentialReplayOperation;
158
+ claimToken: string;
159
+ allowUnstoredClaimToken?: boolean | undefined;
160
+ }): Promise<LocalCredentialReplay>;
87
161
  export declare function saveLocalSpaceState(root: string, input: SaveLocalSpaceStateInput): Promise<{
88
162
  statePath: string;
89
163
  linkPath: string;