@spacefast/common 0.0.13 → 0.0.18

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 (154) 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 +149 -44
  14. package/dist/contracts/access.js +94 -8
  15. package/dist/contracts/activity.js +5 -1
  16. package/dist/contracts/api-keys.js +9 -9
  17. package/dist/contracts/archives.js +11 -11
  18. package/dist/contracts/beta.js +1 -1
  19. package/dist/contracts/builds.js +14 -14
  20. package/dist/contracts/channels.js +2 -2
  21. package/dist/contracts/comments.d.ts +86 -15
  22. package/dist/contracts/comments.js +116 -20
  23. package/dist/contracts/common.d.ts +12 -12
  24. package/dist/contracts/common.js +33 -30
  25. package/dist/contracts/continuation.js +5 -5
  26. package/dist/contracts/data-source-routes.js +1 -1
  27. package/dist/contracts/device-auth.d.ts +6 -6
  28. package/dist/contracts/device-auth.js +12 -12
  29. package/dist/contracts/docs.d.ts +10 -10
  30. package/dist/contracts/domains.js +39 -33
  31. package/dist/contracts/enums.d.ts +2 -10
  32. package/dist/contracts/enums.js +0 -2
  33. package/dist/contracts/error-code-meta.d.ts +116 -47
  34. package/dist/contracts/error-code-meta.js +37 -16
  35. package/dist/contracts/error-codes.d.ts +7 -4
  36. package/dist/contracts/error-codes.js +46 -21
  37. package/dist/contracts/events.d.ts +10 -3
  38. package/dist/contracts/events.js +33 -5
  39. package/dist/contracts/execution.d.ts +26 -0
  40. package/dist/contracts/execution.js +21 -0
  41. package/dist/contracts/feature-lifecycle.d.ts +21 -24
  42. package/dist/contracts/feature-lifecycle.js +28 -26
  43. package/dist/contracts/features.js +4 -4
  44. package/dist/contracts/functions.d.ts +8 -140
  45. package/dist/contracts/functions.js +9 -19
  46. package/dist/contracts/git.d.ts +1 -1
  47. package/dist/contracts/grants.d.ts +96 -79
  48. package/dist/contracts/grants.js +130 -43
  49. package/dist/contracts/ids.d.ts +2 -2
  50. package/dist/contracts/ids.js +2 -4
  51. package/dist/contracts/internal.js +2 -2
  52. package/dist/contracts/mcp.d.ts +17 -556
  53. package/dist/contracts/mcp.js +9 -103
  54. package/dist/contracts/me.js +3 -4
  55. package/dist/contracts/notifications.d.ts +108 -0
  56. package/dist/contracts/notifications.js +54 -0
  57. package/dist/contracts/oauth-resources.d.ts +18 -1
  58. package/dist/contracts/oauth-resources.js +22 -2
  59. package/dist/contracts/operations.d.ts +2 -50
  60. package/dist/contracts/operations.js +0 -19
  61. package/dist/contracts/pages.d.ts +8 -4
  62. package/dist/contracts/pages.js +16 -3
  63. package/dist/contracts/platform.js +10 -8
  64. package/dist/contracts/problem-document.d.ts +14 -0
  65. package/dist/contracts/problem-document.js +35 -0
  66. package/dist/contracts/push-new.d.ts +21 -6
  67. package/dist/contracts/push-new.js +3 -7
  68. package/dist/contracts/resources.d.ts +8 -7
  69. package/dist/contracts/resources.js +9 -6
  70. package/dist/contracts/runtime-api.d.ts +162 -92
  71. package/dist/contracts/runtime-api.js +99 -27
  72. package/dist/contracts/runtime-app.d.ts +41 -11
  73. package/dist/contracts/runtime-app.js +22 -1
  74. package/dist/contracts/runtime-db.d.ts +424 -0
  75. package/dist/contracts/runtime-db.js +108 -0
  76. package/dist/contracts/runtime-storage.d.ts +120 -0
  77. package/dist/contracts/runtime-storage.js +40 -0
  78. package/dist/contracts/sf-config-v1.d.ts +49 -1
  79. package/dist/contracts/sf-config-v1.js +2 -2
  80. package/dist/contracts/sites.d.ts +1 -0
  81. package/dist/contracts/sites.js +20 -12
  82. package/dist/contracts/space-config.d.ts +62 -22
  83. package/dist/contracts/space-config.js +173 -47
  84. package/dist/contracts/spaces.d.ts +79 -45
  85. package/dist/contracts/spaces.js +66 -50
  86. package/dist/contracts/superadmin-spaces.d.ts +13 -2
  87. package/dist/contracts/superadmin-spaces.js +15 -0
  88. package/dist/contracts/superadmin-tenants.js +4 -4
  89. package/dist/contracts/superadmin.d.ts +4 -0
  90. package/dist/contracts/superadmin.js +14 -1
  91. package/dist/contracts/tags.d.ts +17 -17
  92. package/dist/contracts/tags.js +1 -1
  93. package/dist/contracts/teams.js +25 -16
  94. package/dist/contracts/theme-json.js +1 -1
  95. package/dist/contracts/transfers.js +4 -4
  96. package/dist/contracts/variables.js +3 -3
  97. package/dist/contracts/zero.d.ts +204 -338
  98. package/dist/contracts/zero.js +94 -98
  99. package/dist/dashboard-paths/index.d.ts +16 -1
  100. package/dist/dashboard-paths/index.js +18 -0
  101. package/dist/docs/agent-handoff-document.d.ts +2 -2
  102. package/dist/docs/agent-handoff-document.js +8 -7
  103. package/dist/docs/agent-prose.d.ts +22 -3
  104. package/dist/docs/agent-prose.js +221 -51
  105. package/dist/docs/agent-setup.d.ts +13 -10
  106. package/dist/docs/agent-setup.js +279 -53
  107. package/dist/docs/agent-solutions.d.ts +6 -6
  108. package/dist/docs/agent-solutions.js +23 -21
  109. package/dist/docs/catalog.d.ts +29 -74
  110. package/dist/docs/catalog.js +65 -92
  111. package/dist/docs/error-docs.js +293 -221
  112. package/dist/docs/skill-distribution.d.ts +2 -0
  113. package/dist/docs/skill-distribution.js +7 -4
  114. package/dist/docs/skills.d.ts +546 -0
  115. package/dist/docs/skills.js +340 -0
  116. package/dist/utils/browser-credential.d.ts +19 -0
  117. package/dist/utils/browser-credential.js +62 -0
  118. package/dist/utils/comment-avatars.d.ts +7 -0
  119. package/dist/utils/comment-avatars.js +42 -0
  120. package/dist/utils/content-type.d.ts +3 -3
  121. package/dist/utils/content-type.js +38 -61
  122. package/dist/utils/domain-diagnostics.d.ts +5 -5
  123. package/dist/utils/domain-diagnostics.js +21 -10
  124. package/dist/utils/local-space-state.d.ts +78 -4
  125. package/dist/utils/local-space-state.js +383 -82
  126. package/dist/utils/one-shot-replay.d.ts +14 -0
  127. package/dist/utils/one-shot-replay.js +21 -0
  128. package/dist/utils/page-preview.d.ts +7 -0
  129. package/dist/utils/page-preview.js +9 -0
  130. package/dist/utils/pages.d.ts +2 -11
  131. package/dist/utils/pages.js +58 -14
  132. package/dist/utils/publish-form-data.d.ts +22 -0
  133. package/dist/utils/publish-form-data.js +26 -0
  134. package/dist/utils/publish-policy.js +20 -4
  135. package/dist/utils/query-keys.d.ts +7 -4
  136. package/dist/utils/query-keys.js +13 -4
  137. package/dist/utils/runtime-paths.d.ts +3 -0
  138. package/dist/utils/runtime-paths.js +7 -0
  139. package/dist/utils/secure-local-file.d.ts +73 -0
  140. package/dist/utils/secure-local-file.js +1373 -0
  141. package/dist/utils/space-config.d.ts +13 -4
  142. package/dist/utils/space-config.js +123 -15
  143. package/dist/utils/storage-policy.d.ts +20 -0
  144. package/dist/utils/storage-policy.fixtures.json +174 -0
  145. package/dist/utils/storage-policy.js +193 -0
  146. package/dist/utils/upload-session.d.ts +12 -1
  147. package/dist/utils/upload-session.js +37 -2
  148. package/dist/utils/version-file-browser.d.ts +13 -0
  149. package/dist/utils/version-file-browser.js +59 -0
  150. package/package.json +19 -1
  151. package/dist/contracts/mounts.d.ts +0 -142
  152. package/dist/contracts/mounts.js +0 -56
  153. package/dist/utils/asset-fingerprint.d.ts +0 -64
  154. package/dist/utils/asset-fingerprint.js +0 -620
@@ -1,6 +1,6 @@
1
1
  import { type Grant } from "../contracts/grants.js";
2
2
  import { type SpaceConfig, type SpaceConfigFile } from "../contracts/space-config.js";
3
- export type SpaceConfigBlockingIssueCode = "config_file_too_large" | "config_templates_over_limit" | "config_meta_too_long" | "config_invalid" | "inject_snippet_too_large" | "inject_invalid";
3
+ export type SpaceConfigBlockingIssueCode = "config_file_too_large" | "config_templates_over_limit" | "config_meta_too_long" | "config_name_too_long" | "config_functions_key_removed" | "config_invalid" | "config_runtime_invalid_kind" | "config_runtime_entry_missing" | "inject_snippet_too_large" | "inject_invalid";
4
4
  export type SpaceConfigIssue = {
5
5
  severity: "info" | "warning" | "error";
6
6
  code: SpaceConfigBlockingIssueCode | "single_html_index" | "files_mode_inferred";
@@ -15,12 +15,21 @@ export declare function compileSpaceConfigPublicGrant(fileConfig: Pick<SpaceConf
15
15
  type SpaceConfigBlockingIssue = SpaceConfigIssue & {
16
16
  code: SpaceConfigBlockingIssueCode;
17
17
  };
18
+ /**
19
+ * Keys a committed `sf.jsonc` owns outright: they are facts about the content
20
+ * of a version (who may read it, what it is called, what runs it), so they roll
21
+ * forward and back with the version instead of living in the space's mutable
22
+ * overlay. API intake refuses them rather than storing or dropping them.
23
+ */
24
+ export declare const SPACE_CONFIG_FILE_ONLY_KEYS: readonly ["access", "name", "runtime"];
18
25
  /**
19
26
  * Validates a space `config` overlay at API intake (PATCH /spaces, publish
20
- * `config`): the 16 KB compiled-overlay cap plus the shared shape caps.
21
- * Returns the first blocking issue, or null when the overlay is acceptable.
27
+ * `config`): file-only keys, the 16 KB compiled-overlay cap, plus the shared
28
+ * shape caps. Returns the first blocking issue, or null when the overlay is
29
+ * acceptable. Takes the raw request value, because the overlay schema strips
30
+ * file-only keys and this is what has to see them.
22
31
  */
23
- export declare function spaceConfigOverlayIssue(config: SpaceConfig | Record<string, unknown>): SpaceConfigBlockingIssue | null;
32
+ export declare function spaceConfigOverlayIssue(config: unknown): SpaceConfigBlockingIssue | null;
24
33
  export declare function stripJsonComments(raw: string): string;
25
34
  export type ParsedSpaceConfigFile = {
26
35
  /** Parsed config (cap- and shape-valid keys only); null when unusable. */
@@ -123,19 +123,103 @@ function capIssues(candidate) {
123
123
  }
124
124
  return issues;
125
125
  }
126
+ // File-only keys (`name`, `runtime`) never reach the API overlay, so their
127
+ // checks live beside the shared caps rather than inside them. Both run BEFORE
128
+ // zod so a bad declaration reports its stable code instead of a shape error.
129
+ const RUNTIME_KINDS = new Set(["zero", "functions"]);
130
+ function fileOnlyIssues(candidate) {
131
+ const issues = [];
132
+ const name = candidate.name;
133
+ if (typeof name === "string" && name.trim().length > SPACE_CONFIG_CAPS.nameChars) {
134
+ issues.push({
135
+ severity: "error",
136
+ code: "config_name_too_long",
137
+ message: `name supports up to ${SPACE_CONFIG_CAPS.nameChars} characters.`,
138
+ path: "name",
139
+ details: { length: name.trim().length, limit: SPACE_CONFIG_CAPS.nameChars },
140
+ });
141
+ }
142
+ const runtime = candidate.runtime;
143
+ if (runtime === undefined) {
144
+ return issues;
145
+ }
146
+ if (typeof runtime !== "object" || runtime === null || Array.isArray(runtime)) {
147
+ issues.push({
148
+ severity: "error",
149
+ code: "config_runtime_invalid_kind",
150
+ message: 'runtime must be an object declaring a kind of "zero" or "functions".',
151
+ path: "runtime",
152
+ });
153
+ return issues;
154
+ }
155
+ const declaration = runtime;
156
+ const kind = declaration.kind;
157
+ if (typeof kind !== "string" || !RUNTIME_KINDS.has(kind)) {
158
+ issues.push({
159
+ severity: "error",
160
+ code: "config_runtime_invalid_kind",
161
+ message: 'runtime.kind must be "zero" or "functions".',
162
+ path: "runtime.kind",
163
+ ...(typeof kind === "string" ? { details: { kind } } : {}),
164
+ });
165
+ return issues;
166
+ }
167
+ if (kind === "zero") {
168
+ // Zero is never inferred, so a declaration that names neither entry has
169
+ // nothing to compile — failing here beats publishing static files under a
170
+ // config that promised a database app.
171
+ for (const key of ["server", "client"]) {
172
+ const value = declaration[key];
173
+ if (typeof value !== "string" || value.trim().length === 0) {
174
+ issues.push({
175
+ severity: "error",
176
+ code: "config_runtime_entry_missing",
177
+ message: `runtime.${key} is required and must name a module when runtime.kind is "zero".`,
178
+ path: `runtime.${key}`,
179
+ });
180
+ }
181
+ }
182
+ return issues;
183
+ }
184
+ const entry = declaration.entry;
185
+ if (entry !== undefined && (typeof entry !== "string" || entry.trim().length === 0)) {
186
+ issues.push({
187
+ severity: "error",
188
+ code: "config_runtime_entry_missing",
189
+ message: "runtime.entry must name a module when present; omit it to detect the worker instead.",
190
+ path: "runtime.entry",
191
+ });
192
+ }
193
+ return issues;
194
+ }
195
+ /**
196
+ * Keys a committed `sf.jsonc` owns outright: they are facts about the content
197
+ * of a version (who may read it, what it is called, what runs it), so they roll
198
+ * forward and back with the version instead of living in the space's mutable
199
+ * overlay. API intake refuses them rather than storing or dropping them.
200
+ */
201
+ export const SPACE_CONFIG_FILE_ONLY_KEYS = ["access", "name", "runtime"];
126
202
  /**
127
203
  * Validates a space `config` overlay at API intake (PATCH /spaces, publish
128
- * `config`): the 16 KB compiled-overlay cap plus the shared shape caps.
129
- * Returns the first blocking issue, or null when the overlay is acceptable.
204
+ * `config`): file-only keys, the 16 KB compiled-overlay cap, plus the shared
205
+ * shape caps. Returns the first blocking issue, or null when the overlay is
206
+ * acceptable. Takes the raw request value, because the overlay schema strips
207
+ * file-only keys and this is what has to see them.
130
208
  */
131
209
  export function spaceConfigOverlayIssue(config) {
132
- if (Object.prototype.hasOwnProperty.call(config, "access")) {
133
- return {
134
- severity: "error",
135
- code: "config_invalid",
136
- message: "access is only valid in the committed sf.jsonc file.",
137
- path: "access",
138
- };
210
+ if (config === null || config === undefined || typeof config !== "object") {
211
+ return null;
212
+ }
213
+ for (const key of SPACE_CONFIG_FILE_ONLY_KEYS) {
214
+ if (Object.prototype.hasOwnProperty.call(config, key)) {
215
+ return {
216
+ severity: "error",
217
+ code: "config_invalid",
218
+ message: `${key} is only valid in the committed ${SPACE_CONFIG_CANONICAL_FILE} file.`,
219
+ path: key,
220
+ details: { file: SPACE_CONFIG_CANONICAL_FILE },
221
+ };
222
+ }
139
223
  }
140
224
  const encoded = JSON.stringify(config);
141
225
  if (byteLength(encoded) > SPACE_CONFIG_CAPS.overlayBytes) {
@@ -149,9 +233,18 @@ export function spaceConfigOverlayIssue(config) {
149
233
  }
150
234
  return capIssues(config)[0] ?? null;
151
235
  }
152
- // Routing concerns never live in sf.jsonc (spec: redirects, rewrites,
153
- // proxies, and headers stay exclusively in `_redirects`/`_headers`).
154
- const ROUTING_KEYS = new Set(["redirects", "rewrites", "proxies", "proxy", "headers", "routes"]);
236
+ // Other platforms' routing vocabulary. We have one matcher grammar; naming it
237
+ // four more ways would be four more things to keep in sync.
238
+ const ROUTING_KEYS = new Set(["proxies", "proxy", "routes"]);
239
+ // OUR routing vocabulary. These keys are real sf.jsonc keys, owned end to end
240
+ // by the routing compiler (crates/stattic-runtime-core): the strict v1 compiler
241
+ // validates them and the merge folds them in behind `_redirects`/`_headers`.
242
+ // This lane resolves the serving config, so it neither validates nor carries
243
+ // them — it just leaves them alone.
244
+ const ROUTING_RULE_KEYS = new Set(["redirects", "rewrites", "headers"]);
245
+ // Replaced by the `runtime` declaration; never silently ignored like other
246
+ // unknown keys, because it is the one key whose loss changes what gets served.
247
+ const REMOVED_FUNCTIONS_KEY = "functions";
155
248
  const KNOWN_FILE_KEYS = new Set(Object.keys(spaceConfigFileSchema.shape));
156
249
  // Strips JSONC comments and trailing commas (string-aware single pass).
157
250
  export function stripJsonComments(raw) {
@@ -290,7 +383,23 @@ export function parseSpaceConfigFile(raw, options = {}) {
290
383
  if (KNOWN_FILE_KEYS.has(key)) {
291
384
  continue;
292
385
  }
293
- if (ROUTING_KEYS.has(key)) {
386
+ if (ROUTING_RULE_KEYS.has(key)) {
387
+ delete record[key];
388
+ continue;
389
+ }
390
+ if (key === REMOVED_FUNCTIONS_KEY) {
391
+ // The runtime declaration replaced this key outright (spec "The old
392
+ // top-level functions.* key is removed in the same cutover"): warning-and-
393
+ // strip would publish static files under a config that asked for a
394
+ // worker, so it blocks with its own code instead.
395
+ issues.push({
396
+ severity: "error",
397
+ code: "config_functions_key_removed",
398
+ message: `The "${REMOVED_FUNCTIONS_KEY}" key was removed from ${configPath}; declare runtime.kind "functions" instead.`,
399
+ path: key,
400
+ });
401
+ }
402
+ else if (ROUTING_KEYS.has(key)) {
294
403
  issues.push({
295
404
  severity: "error",
296
405
  code: "config_invalid",
@@ -308,8 +417,7 @@ export function parseSpaceConfigFile(raw, options = {}) {
308
417
  }
309
418
  delete record[key];
310
419
  }
311
- const caps = capIssues(record);
312
- issues.push(...caps);
420
+ issues.push(...capIssues(record), ...fileOnlyIssues(record));
313
421
  if (issues.some((issue) => issue.severity === "error")) {
314
422
  return { config: null, issues };
315
423
  }
@@ -0,0 +1,20 @@
1
+ export declare const STORAGE_OBJECT_MAX_FILE_BYTES: number;
2
+ export declare const STORAGE_OBJECT_MAX_OWNER_BYTES: number;
3
+ export declare const STORAGE_CONTENT_SNIFF_BYTES = 512;
4
+ export declare const STORAGE_BLOCKED_CONTENT_TYPES: string[];
5
+ export declare const STORAGE_BLOCKED_CONTENT_TYPE_FRAGMENTS: string[];
6
+ export declare const STORAGE_BLOCKED_MAGIC_HEX: string[];
7
+ export declare const STORAGE_BLOCKED_TEXT_PREFIXES: string[];
8
+ export declare function storageDeclaredTypeBlocked(contentType: string): boolean;
9
+ export declare function storageContentBlocked(contentType: string, input: Uint8Array): boolean;
10
+ export type StorageByteRange = {
11
+ unsatisfiable: true;
12
+ } | {
13
+ start: number;
14
+ end: number;
15
+ header: string;
16
+ };
17
+ export declare function storageParseSingleRange(rangeHeader: string | null | undefined, size: number): StorageByteRange | null;
18
+ export declare function storageIfNoneMatchMatches(header: string | null | undefined, etag: string): boolean;
19
+ export declare function storageParseHttpDate(value: string): number | null;
20
+ export declare function storageIfRangeMatches(ifRange: string, etag: string, lastModified: string): boolean;
@@ -0,0 +1,174 @@
1
+ {
2
+ "content": [
3
+ {
4
+ "name": "declared html",
5
+ "contentType": "text/html; charset=utf-8",
6
+ "hex": "706c61696e",
7
+ "expected": true
8
+ },
9
+ {
10
+ "name": "declared javascript",
11
+ "contentType": "application/javascript",
12
+ "hex": "706c61696e",
13
+ "expected": true
14
+ },
15
+ {
16
+ "name": "windows executable magic",
17
+ "contentType": "application/octet-stream",
18
+ "hex": "4d5a0000",
19
+ "expected": true
20
+ },
21
+ {
22
+ "name": "mach executable magic",
23
+ "contentType": "application/octet-stream",
24
+ "hex": "feedfacf0000",
25
+ "expected": true
26
+ },
27
+ {
28
+ "name": "trimmed active html",
29
+ "contentType": "text/plain",
30
+ "hex": "20090a3c7363726970743e",
31
+ "expected": true
32
+ },
33
+ {
34
+ "name": "svg remains allowed",
35
+ "contentType": "image/svg+xml",
36
+ "hex": "3c73766720786d6c6e733d22687474703a2f2f7777772e77332e6f72672f323030302f737667223e",
37
+ "expected": false
38
+ },
39
+ {
40
+ "name": "plain bytes remain allowed",
41
+ "contentType": "application/octet-stream",
42
+ "hex": "68656c6c6f2073746f72616765",
43
+ "expected": false
44
+ }
45
+ ],
46
+ "ranges": [
47
+ { "name": "absent", "header": null, "size": 14, "expected": null },
48
+ {
49
+ "name": "full tail",
50
+ "header": "bytes=7-",
51
+ "size": 14,
52
+ "expected": { "start": 7, "end": 13, "header": "bytes=7-13" }
53
+ },
54
+ {
55
+ "name": "bounded",
56
+ "header": "bytes=0-6",
57
+ "size": 14,
58
+ "expected": { "start": 0, "end": 6, "header": "bytes=0-6" }
59
+ },
60
+ {
61
+ "name": "suffix",
62
+ "header": "bytes=-4",
63
+ "size": 14,
64
+ "expected": { "start": 10, "end": 13, "header": "bytes=10-13" }
65
+ },
66
+ {
67
+ "name": "clamped",
68
+ "header": "bytes=10-99",
69
+ "size": 14,
70
+ "expected": { "start": 10, "end": 13, "header": "bytes=10-13" }
71
+ },
72
+ {
73
+ "name": "unsatisfiable",
74
+ "header": "bytes=20-30",
75
+ "size": 14,
76
+ "expected": { "unsatisfiable": true }
77
+ },
78
+ { "name": "multiple ignored", "header": "bytes=0-1,4-5", "size": 14, "expected": null },
79
+ { "name": "malformed ignored", "header": "items=0-1", "size": 14, "expected": null }
80
+ ],
81
+ "ifNoneMatch": [
82
+ { "name": "exact", "header": "\"abc\"", "etag": "\"abc\"", "expected": true },
83
+ { "name": "weak", "header": "W/\"abc\"", "etag": "\"abc\"", "expected": true },
84
+ { "name": "list", "header": "\"no\", W/\"abc\"", "etag": "\"abc\"", "expected": true },
85
+ { "name": "wildcard", "header": "*", "etag": "\"abc\"", "expected": true },
86
+ { "name": "miss", "header": "\"no\"", "etag": "\"abc\"", "expected": false }
87
+ ],
88
+ "ifRange": [
89
+ {
90
+ "name": "exact etag",
91
+ "header": "\"abc\"",
92
+ "etag": "\"abc\"",
93
+ "lastModified": "Wed, 21 Oct 2015 07:28:00 GMT",
94
+ "expected": true
95
+ },
96
+ {
97
+ "name": "weak etag rejected",
98
+ "header": "W/\"abc\"",
99
+ "etag": "\"abc\"",
100
+ "lastModified": "Wed, 21 Oct 2015 07:28:00 GMT",
101
+ "expected": false
102
+ },
103
+ {
104
+ "name": "newer date",
105
+ "header": "Wed, 21 Oct 2015 07:29:00 GMT",
106
+ "etag": "\"abc\"",
107
+ "lastModified": "Wed, 21 Oct 2015 07:28:00 GMT",
108
+ "expected": true
109
+ },
110
+ {
111
+ "name": "older date",
112
+ "header": "Wed, 21 Oct 2015 07:27:00 GMT",
113
+ "etag": "\"abc\"",
114
+ "lastModified": "Wed, 21 Oct 2015 07:28:00 GMT",
115
+ "expected": false
116
+ },
117
+ {
118
+ "name": "leap second",
119
+ "header": "Wed, 21 Oct 2015 07:28:60 GMT",
120
+ "etag": "\"abc\"",
121
+ "lastModified": "Wed, 21 Oct 2015 07:28:59 GMT",
122
+ "expected": true
123
+ },
124
+ {
125
+ "name": "obsolete RFC 850 date",
126
+ "header": "Sunday, 06-Nov-94 08:49:37 GMT",
127
+ "etag": "\"abc\"",
128
+ "lastModified": "Sun, 06 Nov 1994 08:49:37 GMT",
129
+ "expected": true
130
+ },
131
+ {
132
+ "name": "obsolete asctime date",
133
+ "header": "Sun Nov 6 08:49:37 1994",
134
+ "etag": "\"abc\"",
135
+ "lastModified": "Sun, 06 Nov 1994 08:49:37 GMT",
136
+ "expected": true
137
+ },
138
+ {
139
+ "name": "relative date rejected",
140
+ "header": "tomorrow",
141
+ "etag": "\"abc\"",
142
+ "lastModified": "Wed, 21 Oct 2015 07:28:00 GMT",
143
+ "expected": false
144
+ },
145
+ {
146
+ "name": "wrong weekday rejected",
147
+ "header": "Thu, 21 Oct 2015 07:29:00 GMT",
148
+ "etag": "\"abc\"",
149
+ "lastModified": "Wed, 21 Oct 2015 07:28:00 GMT",
150
+ "expected": false
151
+ },
152
+ {
153
+ "name": "invalid calendar date rejected",
154
+ "header": "Fri, 31 Feb 2023 07:29:00 GMT",
155
+ "etag": "\"abc\"",
156
+ "lastModified": "Wed, 21 Oct 2015 07:28:00 GMT",
157
+ "expected": false
158
+ },
159
+ {
160
+ "name": "out-of-range time rejected",
161
+ "header": "Wed, 21 Oct 2015 07:28:61 GMT",
162
+ "etag": "\"abc\"",
163
+ "lastModified": "Wed, 21 Oct 2015 07:28:00 GMT",
164
+ "expected": false
165
+ },
166
+ {
167
+ "name": "trailing garbage rejected",
168
+ "header": "Wed, 21 Oct 2015 07:29:00 GMT later",
169
+ "etag": "\"abc\"",
170
+ "lastModified": "Wed, 21 Oct 2015 07:28:00 GMT",
171
+ "expected": false
172
+ }
173
+ ]
174
+ }
@@ -0,0 +1,193 @@
1
+ // Single source of truth for storage HTTP and object-intake policy.
2
+ //
3
+ // TypeScript adapters import this module directly. The PHP runtime binding is
4
+ // generated into runtime/engine/shared/storage-policy.generated.php by
5
+ // `bun --filter @spacefast/control-plane runtime:codegen-policy`. Both execute
6
+ // the same storage-policy.fixtures.json corpus.
7
+ export const STORAGE_OBJECT_MAX_FILE_BYTES = 5 * 1024 * 1024;
8
+ export const STORAGE_OBJECT_MAX_OWNER_BYTES = 100 * 1024 * 1024;
9
+ export const STORAGE_CONTENT_SNIFF_BYTES = 512;
10
+ export const STORAGE_BLOCKED_CONTENT_TYPES = [
11
+ "application/java-archive",
12
+ "application/java-vm",
13
+ "application/vnd.microsoft.portable-executable",
14
+ "application/x-dosexec",
15
+ "application/x-httpd-php",
16
+ "application/x-java-archive",
17
+ "application/x-mach-binary",
18
+ "application/x-msdownload",
19
+ "application/xhtml+xml",
20
+ "text/html",
21
+ ];
22
+ export const STORAGE_BLOCKED_CONTENT_TYPE_FRAGMENTS = ["javascript", "ecmascript"];
23
+ export const STORAGE_BLOCKED_MAGIC_HEX = [
24
+ "4d5a",
25
+ "7f454c46",
26
+ "feedface",
27
+ "cefaedfe",
28
+ "feedfacf",
29
+ "cffaedfe",
30
+ "cafebabe",
31
+ ];
32
+ export const STORAGE_BLOCKED_TEXT_PREFIXES = ["#!", "<!doctype html", "<html", "<script", "<?php"];
33
+ const BLOCKED_CONTENT_TYPES = new Set(STORAGE_BLOCKED_CONTENT_TYPES);
34
+ export function storageDeclaredTypeBlocked(contentType) {
35
+ const declared = contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
36
+ return (BLOCKED_CONTENT_TYPES.has(declared) ||
37
+ STORAGE_BLOCKED_CONTENT_TYPE_FRAGMENTS.some((fragment) => declared.includes(fragment)));
38
+ }
39
+ function bytesStartWithHex(bytes, hex) {
40
+ if (bytes.byteLength < hex.length / 2)
41
+ return false;
42
+ for (let offset = 0; offset < hex.length; offset += 2) {
43
+ if (bytes[offset / 2] !== Number.parseInt(hex.slice(offset, offset + 2), 16)) {
44
+ return false;
45
+ }
46
+ }
47
+ return true;
48
+ }
49
+ export function storageContentBlocked(contentType, input) {
50
+ if (storageDeclaredTypeBlocked(contentType))
51
+ return true;
52
+ const prefix = input.subarray(0, STORAGE_CONTENT_SNIFF_BYTES);
53
+ if (STORAGE_BLOCKED_MAGIC_HEX.some((magic) => bytesStartWithHex(prefix, magic))) {
54
+ return true;
55
+ }
56
+ const decoded = new TextDecoder().decode(prefix);
57
+ let contentStart = 0;
58
+ while (contentStart < decoded.length) {
59
+ const code = decoded.charCodeAt(contentStart);
60
+ if (code !== 0 && code !== 9 && code !== 10 && code !== 11 && code !== 13 && code !== 32) {
61
+ break;
62
+ }
63
+ contentStart += 1;
64
+ }
65
+ const text = decoded.slice(contentStart).toLowerCase();
66
+ return STORAGE_BLOCKED_TEXT_PREFIXES.some((blocked) => text.startsWith(blocked));
67
+ }
68
+ export function storageParseSingleRange(rangeHeader, size) {
69
+ if (!rangeHeader || rangeHeader.trim() === "" || rangeHeader.includes(","))
70
+ return null;
71
+ const match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader.trim());
72
+ if (!match || (!match[1] && !match[2]))
73
+ return null;
74
+ if (!match[1]) {
75
+ const suffix = Number(match[2]);
76
+ if (!Number.isSafeInteger(suffix) || suffix <= 0 || size <= 0) {
77
+ return { unsatisfiable: true };
78
+ }
79
+ const start = Math.max(0, size - suffix);
80
+ const end = size - 1;
81
+ return { start, end, header: `bytes=${start}-${end}` };
82
+ }
83
+ const start = Number(match[1]);
84
+ const requestedEnd = match[2] ? Number(match[2]) : size - 1;
85
+ const end = Math.min(requestedEnd, size - 1);
86
+ if (!Number.isSafeInteger(start) ||
87
+ !Number.isSafeInteger(requestedEnd) ||
88
+ start >= size ||
89
+ start > end ||
90
+ size < 0) {
91
+ return { unsatisfiable: true };
92
+ }
93
+ return { start, end, header: `bytes=${start}-${end}` };
94
+ }
95
+ function storageEtagOpaqueTag(etag) {
96
+ const trimmed = etag.trim();
97
+ return /^W\//i.test(trimmed) ? trimmed.slice(2).trim() : trimmed;
98
+ }
99
+ export function storageIfNoneMatchMatches(header, etag) {
100
+ const value = header?.trim() ?? "";
101
+ if (value === "")
102
+ return false;
103
+ if (value === "*")
104
+ return true;
105
+ const target = storageEtagOpaqueTag(etag);
106
+ if (target === "")
107
+ return false;
108
+ return value
109
+ .split(",")
110
+ .map((candidate) => candidate.trim())
111
+ .filter(Boolean)
112
+ .some((candidate) => storageEtagOpaqueTag(candidate) === target);
113
+ }
114
+ const HTTP_MONTHS = [
115
+ "Jan",
116
+ "Feb",
117
+ "Mar",
118
+ "Apr",
119
+ "May",
120
+ "Jun",
121
+ "Jul",
122
+ "Aug",
123
+ "Sep",
124
+ "Oct",
125
+ "Nov",
126
+ "Dec",
127
+ ];
128
+ const HTTP_WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
129
+ const HTTP_WEEKDAYS_LONG = [
130
+ "Sunday",
131
+ "Monday",
132
+ "Tuesday",
133
+ "Wednesday",
134
+ "Thursday",
135
+ "Friday",
136
+ "Saturday",
137
+ ];
138
+ function storageHttpDateTimestamp(weekday, day, month, year, hour, minute, second) {
139
+ if (hour > 23 || minute > 59 || second > 60)
140
+ return null;
141
+ const normalizedSecond = Math.min(second, 59);
142
+ const date = new Date(0);
143
+ date.setUTCFullYear(year, month, day);
144
+ date.setUTCHours(hour, minute, normalizedSecond, 0);
145
+ if (date.getUTCFullYear() !== year ||
146
+ date.getUTCMonth() !== month ||
147
+ date.getUTCDate() !== day ||
148
+ date.getUTCHours() !== hour ||
149
+ date.getUTCMinutes() !== minute ||
150
+ date.getUTCSeconds() !== normalizedSecond ||
151
+ (weekday !== null && date.getUTCDay() !== weekday)) {
152
+ return null;
153
+ }
154
+ return date.getTime() + (second === 60 ? 1_000 : 0);
155
+ }
156
+ // HTTP-date has exactly three accepted wire forms (RFC 9110 §5.6.7). General
157
+ // date parsers also accept relative/free-form input such as `tomorrow`, so
158
+ // validators must parse the grammar and validate its calendar components.
159
+ export function storageParseHttpDate(value) {
160
+ const input = value.trim();
161
+ let match = /^(Sun|Mon|Tue|Wed|Thu|Fri|Sat), (\d{2}) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}) (\d{2}):(\d{2}):(\d{2}) GMT$/.exec(input);
162
+ if (match) {
163
+ return storageHttpDateTimestamp(HTTP_WEEKDAYS.indexOf(match[1] ?? ""), Number(match[2]), HTTP_MONTHS.indexOf(match[3] ?? ""), Number(match[4]), Number(match[5]), Number(match[6]), Number(match[7]));
164
+ }
165
+ match =
166
+ /^(Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday), (\d{2})-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d{2}) (\d{2}):(\d{2}):(\d{2}) GMT$/.exec(input);
167
+ if (match) {
168
+ const now = new Date();
169
+ let year = Math.floor(now.getUTCFullYear() / 100) * 100 + Number(match[4]);
170
+ const candidate = storageHttpDateTimestamp(null, Number(match[2]), HTTP_MONTHS.indexOf(match[3] ?? ""), year, Number(match[5]), Number(match[6]), Number(match[7]));
171
+ const fiftyYearsFromNow = new Date(now);
172
+ fiftyYearsFromNow.setUTCFullYear(now.getUTCFullYear() + 50);
173
+ if (candidate !== null && candidate > fiftyYearsFromNow.getTime())
174
+ year -= 100;
175
+ return storageHttpDateTimestamp(HTTP_WEEKDAYS_LONG.indexOf(match[1] ?? ""), Number(match[2]), HTTP_MONTHS.indexOf(match[3] ?? ""), year, Number(match[5]), Number(match[6]), Number(match[7]));
176
+ }
177
+ match =
178
+ /^(Sun|Mon|Tue|Wed|Thu|Fri|Sat) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (?:([0-9]{2})| ([0-9])) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/.exec(input);
179
+ if (!match)
180
+ return null;
181
+ return storageHttpDateTimestamp(HTTP_WEEKDAYS.indexOf(match[1] ?? ""), Number(match[3] ?? match[4]), HTTP_MONTHS.indexOf(match[2] ?? ""), Number(match[8]), Number(match[5]), Number(match[6]), Number(match[7]));
182
+ }
183
+ export function storageIfRangeMatches(ifRange, etag, lastModified) {
184
+ const value = ifRange.trim();
185
+ if (value === "" || /^W\//i.test(value))
186
+ return false;
187
+ if (value.startsWith('"')) {
188
+ return !/^W\//i.test(etag.trim()) && etag.trim() === value;
189
+ }
190
+ const requestTime = storageParseHttpDate(value);
191
+ const modifiedTime = storageParseHttpDate(lastModified);
192
+ return requestTime !== null && modifiedTime !== null && requestTime >= modifiedTime;
193
+ }
@@ -19,10 +19,21 @@ export type UploadSessionEvent = {
19
19
  target: UploadTarget;
20
20
  error: unknown;
21
21
  };
22
+ /** The RFC 9457 problem document a runtime upload target returns on failure. */
23
+ export type UploadTargetServerError = {
24
+ code: string;
25
+ message: string;
26
+ pointer?: string;
27
+ type?: string;
28
+ details?: unknown;
29
+ suggestions?: readonly string[];
30
+ requestId?: string;
31
+ };
22
32
  export declare class UploadTargetError extends Error {
23
33
  readonly target: UploadTarget;
24
34
  readonly status: number;
25
- constructor(target: UploadTarget, status: number, message: string);
35
+ readonly serverError?: UploadTargetServerError | undefined;
36
+ constructor(target: UploadTarget, status: number, message: string, serverError?: UploadTargetServerError | undefined);
26
37
  }
27
38
  export declare function uploadPageHasRemainder(page: UploadSession): boolean;
28
39
  export declare const UPLOAD_STALLED_RESUME_MAX_ATTEMPTS = 3;
@@ -1,13 +1,47 @@
1
1
  export class UploadTargetError extends Error {
2
2
  target;
3
3
  status;
4
- constructor(target, status, message) {
4
+ serverError;
5
+ constructor(target, status, message, serverError) {
5
6
  super(message);
6
7
  this.target = target;
7
8
  this.status = status;
9
+ this.serverError = serverError;
8
10
  this.name = "UploadTargetError";
9
11
  }
10
12
  }
13
+ function parseUploadTargetServerError(raw) {
14
+ let parsed;
15
+ try {
16
+ parsed = JSON.parse(raw);
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ if (!parsed || typeof parsed !== "object") {
22
+ return undefined;
23
+ }
24
+ const record = parsed;
25
+ if (typeof record.code !== "string" || typeof record.detail !== "string") {
26
+ return undefined;
27
+ }
28
+ const stringField = (name) => typeof record[name] === "string" ? record[name] : undefined;
29
+ const suggestions = Array.isArray(record.suggestions)
30
+ ? record.suggestions.filter((entry) => typeof entry === "string")
31
+ : [];
32
+ const pointer = stringField("pointer");
33
+ const type = stringField("type");
34
+ const requestId = stringField("requestId");
35
+ return {
36
+ code: record.code,
37
+ message: record.detail,
38
+ ...(pointer ? { pointer } : {}),
39
+ ...(type ? { type } : {}),
40
+ ...(record.details === undefined ? {} : { details: record.details }),
41
+ ...(suggestions.length > 0 ? { suggestions } : {}),
42
+ ...(requestId ? { requestId } : {}),
43
+ };
44
+ }
11
45
  // Manifest paging policy — ONE definition, shared by every uploader (the CLI
12
46
  // and the control plane's inline uploader).
13
47
  //
@@ -78,8 +112,9 @@ export async function executeUploadSession(input) {
78
112
  });
79
113
  if (!response.ok) {
80
114
  const details = await response.text().catch(() => "");
115
+ const serverError = parseUploadTargetServerError(details);
81
116
  const suffix = details.trim() ? `: ${details.trim().slice(0, 1_000)}` : "";
82
- throw new UploadTargetError(target, response.status, `Upload failed for ${target.path} with ${response.status} ${response.statusText}${suffix}.`);
117
+ throw new UploadTargetError(target, response.status, `Upload failed for ${target.path} with ${response.status} ${response.statusText}${suffix}.`, serverError);
83
118
  }
84
119
  input.onEvent?.({ type: "target-done", target });
85
120
  }