@spacefast/common 0.0.21 → 0.0.24

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 (182) hide show
  1. package/dist/agents/connect-targets.d.ts +9 -6
  2. package/dist/agents/connect-targets.js +9 -6
  3. package/dist/config/domains.d.ts +1 -1
  4. package/dist/config/domains.js +1 -0
  5. package/dist/config/index.d.ts +0 -1
  6. package/dist/config/index.js +0 -1
  7. package/dist/contracts/abuse.d.ts +2 -0
  8. package/dist/contracts/abuse.js +12 -0
  9. package/dist/contracts/access.d.ts +121 -9
  10. package/dist/contracts/access.js +73 -9
  11. package/dist/contracts/activity.d.ts +1 -1
  12. package/dist/contracts/activity.js +0 -9
  13. package/dist/contracts/api-keys.d.ts +21 -21
  14. package/dist/contracts/beta.js +1 -1
  15. package/dist/contracts/billing.d.ts +1 -34
  16. package/dist/contracts/billing.js +4 -53
  17. package/dist/contracts/builds.d.ts +8 -8
  18. package/dist/contracts/builds.js +10 -3
  19. package/dist/contracts/cli.d.ts +10 -0
  20. package/dist/contracts/cli.js +10 -0
  21. package/dist/contracts/collab-public.d.ts +63 -0
  22. package/dist/contracts/collab-public.js +46 -0
  23. package/dist/contracts/comments.d.ts +283 -54
  24. package/dist/contracts/comments.js +173 -52
  25. package/dist/contracts/common.d.ts +3 -3
  26. package/dist/contracts/device-auth.d.ts +6 -6
  27. package/dist/contracts/device-auth.js +1 -1
  28. package/dist/contracts/docs.d.ts +5 -5
  29. package/dist/contracts/domains.d.ts +36 -36
  30. package/dist/contracts/enums.d.ts +33 -30
  31. package/dist/contracts/enums.js +52 -38
  32. package/dist/contracts/error-code-meta.d.ts +37 -126
  33. package/dist/contracts/error-code-meta.js +12 -41
  34. package/dist/contracts/error-codes.d.ts +1 -1
  35. package/dist/contracts/error-codes.js +10 -32
  36. package/dist/contracts/execution.d.ts +35 -57
  37. package/dist/contracts/execution.js +41 -40
  38. package/dist/contracts/feature-lifecycle-core.d.ts +0 -6
  39. package/dist/contracts/feature-lifecycle-core.js +2 -31
  40. package/dist/contracts/feature-lifecycle.d.ts +8 -38
  41. package/dist/contracts/feature-lifecycle.js +16 -48
  42. package/dist/contracts/features.d.ts +15 -13
  43. package/dist/contracts/features.js +39 -38
  44. package/dist/contracts/functions.d.ts +97 -70
  45. package/dist/contracts/functions.js +101 -34
  46. package/dist/contracts/git.d.ts +1 -1
  47. package/dist/contracts/grants.d.ts +7 -3
  48. package/dist/contracts/grants.js +18 -9
  49. package/dist/contracts/internal.d.ts +14 -1
  50. package/dist/contracts/internal.js +17 -7
  51. package/dist/contracts/oauth-resources.d.ts +4 -3
  52. package/dist/contracts/oauth-resources.js +12 -3
  53. package/dist/contracts/operations.d.ts +90 -5
  54. package/dist/contracts/operations.js +75 -1
  55. package/dist/contracts/pages.d.ts +6 -1
  56. package/dist/contracts/pages.js +24 -1
  57. package/dist/contracts/plan-policy.d.ts +1 -3
  58. package/dist/contracts/plan-policy.js +1 -2
  59. package/dist/contracts/platform.d.ts +61 -148
  60. package/dist/contracts/platform.js +70 -126
  61. package/dist/contracts/publish-archive.d.ts +498 -0
  62. package/dist/contracts/publish-archive.js +25 -0
  63. package/dist/contracts/resources.d.ts +12 -24
  64. package/dist/contracts/resources.js +32 -12
  65. package/dist/contracts/runtime-api.d.ts +369 -175
  66. package/dist/contracts/runtime-api.js +372 -348
  67. package/dist/contracts/runtime-app.d.ts +127 -120
  68. package/dist/contracts/runtime-app.js +19 -10
  69. package/dist/contracts/runtime-db.d.ts +1 -1
  70. package/dist/contracts/runtime-services.d.ts +219 -0
  71. package/dist/contracts/runtime-services.js +437 -0
  72. package/dist/contracts/runtime-storage.d.ts +45 -32
  73. package/dist/contracts/runtime-storage.js +30 -8
  74. package/dist/contracts/space-config.d.ts +10 -16
  75. package/dist/contracts/space-config.js +14 -10
  76. package/dist/contracts/spaces.d.ts +241 -436
  77. package/dist/contracts/spaces.js +128 -145
  78. package/dist/contracts/superadmin-activity.d.ts +57 -4
  79. package/dist/contracts/superadmin-activity.js +48 -2
  80. package/dist/contracts/superadmin-search.d.ts +126 -0
  81. package/dist/contracts/superadmin-search.js +61 -0
  82. package/dist/contracts/superadmin-spaces.d.ts +358 -89
  83. package/dist/contracts/superadmin-spaces.js +97 -24
  84. package/dist/contracts/superadmin-teams.d.ts +28 -10
  85. package/dist/contracts/superadmin-teams.js +5 -8
  86. package/dist/contracts/superadmin.d.ts +0 -32
  87. package/dist/contracts/superadmin.js +3 -11
  88. package/dist/contracts/tags.d.ts +9 -9
  89. package/dist/contracts/teams.d.ts +44 -2
  90. package/dist/contracts/teams.js +43 -23
  91. package/dist/contracts/transfers.d.ts +0 -49
  92. package/dist/contracts/transfers.js +2 -6
  93. package/dist/contracts/usage.d.ts +61 -0
  94. package/dist/contracts/usage.js +64 -0
  95. package/dist/contracts/variables.d.ts +2 -7
  96. package/dist/contracts/variables.js +6 -5
  97. package/dist/contracts/webhooks.d.ts +2 -2
  98. package/dist/contracts/zero.d.ts +87 -56
  99. package/dist/contracts/zero.js +82 -20
  100. package/dist/dashboard-paths/index.d.ts +1 -1
  101. package/dist/dashboard-paths/index.js +1 -0
  102. package/dist/docs/agent-prose.d.ts +1 -1
  103. package/dist/docs/agent-prose.js +62 -66
  104. package/dist/docs/agent-setup.js +49 -23
  105. package/dist/docs/agent-solutions.d.ts +4 -4
  106. package/dist/docs/agent-solutions.js +8 -8
  107. package/dist/docs/catalog.d.ts +19 -19
  108. package/dist/docs/catalog.js +30 -19
  109. package/dist/docs/error-docs.js +63 -151
  110. package/dist/docs/skills.d.ts +3 -3
  111. package/dist/docs/skills.js +1 -1
  112. package/dist/test-helpers/fetch-stub.d.ts +16 -0
  113. package/dist/test-helpers/fetch-stub.js +24 -0
  114. package/dist/utils/build-settings.js +9 -3
  115. package/dist/utils/canonical-json.d.ts +10 -0
  116. package/dist/utils/canonical-json.js +21 -0
  117. package/dist/utils/cast-transport.d.ts +28 -0
  118. package/dist/utils/cast-transport.js +52 -0
  119. package/dist/utils/claim-token.d.ts +0 -8
  120. package/dist/utils/claim-token.js +3 -15
  121. package/dist/utils/credential-policy.d.ts +1 -1
  122. package/dist/utils/env-file.d.ts +17 -0
  123. package/dist/utils/env-file.js +96 -0
  124. package/dist/utils/error-code.d.ts +2 -0
  125. package/dist/utils/error-code.js +7 -0
  126. package/dist/utils/generate-space-name.d.ts +8 -10
  127. package/dist/utils/generate-space-name.js +206 -21
  128. package/dist/utils/grant-decision.fixtures.json +2938 -0
  129. package/dist/utils/grants.d.ts +2 -0
  130. package/dist/utils/grants.js +25 -2
  131. package/dist/utils/gravatar.d.ts +9 -0
  132. package/dist/utils/gravatar.js +31 -0
  133. package/dist/utils/idempotency.d.ts +6 -0
  134. package/dist/utils/idempotency.js +28 -0
  135. package/dist/utils/local-space-state.d.ts +1 -5
  136. package/dist/utils/local-space-state.js +7 -26
  137. package/dist/utils/page-fonts.d.ts +2 -2
  138. package/dist/utils/page-fonts.js +22 -4
  139. package/dist/utils/pages.d.ts +1 -2
  140. package/dist/utils/pages.js +21 -5
  141. package/dist/utils/publish-form-data.d.ts +12 -0
  142. package/dist/utils/publish-form-data.js +10 -0
  143. package/dist/utils/publish-policy.d.ts +20 -0
  144. package/dist/utils/publish-policy.fixtures.json +397 -0
  145. package/dist/utils/publish-policy.js +36 -19
  146. package/dist/utils/push-new.d.ts +7 -0
  147. package/dist/utils/push-new.js +7 -0
  148. package/dist/utils/query-keys.d.ts +0 -1
  149. package/dist/utils/query-keys.js +0 -1
  150. package/dist/utils/runtime-paths.d.ts +5 -0
  151. package/dist/utils/runtime-paths.js +5 -0
  152. package/dist/utils/runtime-upload-batch.d.ts +0 -5
  153. package/dist/utils/runtime-upload-batch.js +1 -27
  154. package/dist/utils/runtime-upload.d.ts +3 -13
  155. package/dist/utils/runtime-upload.js +4 -14
  156. package/dist/utils/space-config.d.ts +0 -33
  157. package/dist/utils/space-config.js +5 -326
  158. package/dist/utils/storage-policy.d.ts +1 -1
  159. package/dist/utils/storage-policy.js +3 -1
  160. package/dist/utils/structured-error.d.ts +14 -0
  161. package/dist/utils/structured-error.js +22 -0
  162. package/dist/utils/version-path.js +12 -3
  163. package/dist/vocabulary.d.ts +5 -3
  164. package/dist/vocabulary.js +13 -10
  165. package/package.json +10 -5
  166. package/dist/config/postgres-budget.d.ts +0 -129
  167. package/dist/config/postgres-budget.js +0 -140
  168. package/dist/config/postgres-budget.json +0 -51
  169. package/dist/contracts/archives.d.ts +0 -331
  170. package/dist/contracts/archives.js +0 -120
  171. package/dist/contracts/deployments.d.ts +0 -574
  172. package/dist/contracts/deployments.js +0 -97
  173. package/dist/contracts/sites.d.ts +0 -416
  174. package/dist/contracts/sites.js +0 -168
  175. package/dist/contracts/superadmin-queues.d.ts +0 -414
  176. package/dist/contracts/superadmin-queues.js +0 -202
  177. package/dist/contracts/superadmin-runtime.d.ts +0 -27
  178. package/dist/contracts/superadmin-runtime.js +0 -38
  179. package/dist/index.d.ts +0 -1
  180. package/dist/index.js +0 -1
  181. package/dist/utils/api.d.ts +0 -4
  182. package/dist/utils/api.js +0 -13
@@ -0,0 +1,437 @@
1
+ /**
2
+ * Platform services that tenant code reaches through the runtime rather than
3
+ * through a credential of its own: Gravatar, spam checking, and mail.
4
+ *
5
+ * Three properties hold for all three, and they are the reason this file
6
+ * exists instead of three unrelated ones:
7
+ *
8
+ * - **The credential is never in tenant reach.** An Akismet key or a Gravatar
9
+ * token in a bundle is a published secret, and a wp.cloud `SMTP_PASS` in a
10
+ * capsule is worse. Every call below is brokered: the runtime attaches
11
+ * authority outside the code that asked for the call.
12
+ * - **The interface is the contract, not the accessor.** Zero hands these to a
13
+ * handler as `ctx.gravatar` / `ctx.spam` / `ctx.email`; Functions binds them
14
+ * as `env.GRAVATAR` / `env.SPAM` / `env.EMAIL`. Method names and semantic
15
+ * fields stay shared. Zero's `check` narrows the input because its HTTP host
16
+ * can add request metadata; a detached Functions call supplies that metadata
17
+ * explicitly.
18
+ * - **The client is the same source on both tiers.** See
19
+ * `RUNTIME_SERVICE_CLIENT_SOURCE`: normalization, validation and the pure
20
+ * avatar-URL construction are one implementation injected into both runtimes,
21
+ * so the two tiers cannot drift into behaving differently.
22
+ *
23
+ * What differs between tiers is the transport under `call()`, and only that.
24
+ */
25
+ /* -------------------------------------------------------------------------- */
26
+ /* Gravatar */
27
+ /* -------------------------------------------------------------------------- */
28
+ /**
29
+ * What Gravatar serves when an address has no image. `404` is here so callers
30
+ * can detect absence by status instead of receiving a generated face they then
31
+ * have to recognise as a placeholder.
32
+ */
33
+ export const GRAVATAR_DEFAULTS = [
34
+ "404",
35
+ "mp",
36
+ "identicon",
37
+ "monsterid",
38
+ "wavatar",
39
+ "retro",
40
+ "robohash",
41
+ "blank",
42
+ ];
43
+ export const GRAVATAR_RATINGS = ["g", "pg", "r", "x"];
44
+ /** Gravatar's own ceiling. Larger values are refused rather than clamped. */
45
+ export const GRAVATAR_AVATAR_MAX_SIZE = 2048;
46
+ /* -------------------------------------------------------------------------- */
47
+ /* Spam */
48
+ /* -------------------------------------------------------------------------- */
49
+ /**
50
+ * Akismet's recognised content types. The vendor is not in the name of
51
+ * anything an author touches — `ctx.spam` outlives whichever service answers
52
+ * it — but the vocabulary is Akismet's because the classifier is trained on
53
+ * it, and inventing synonyms would only lose signal.
54
+ */
55
+ export const SPAM_CONTENT_TYPES = [
56
+ "comment",
57
+ "reply",
58
+ "forum-post",
59
+ "contact-form",
60
+ "signup",
61
+ "message",
62
+ ];
63
+ /* -------------------------------------------------------------------------- */
64
+ /* Upstreams */
65
+ /* -------------------------------------------------------------------------- */
66
+ /**
67
+ * The hosts a broker may reach on a tenant's behalf are deliberately NOT listed
68
+ * here. That table lives in `stattic-runtime-core`'s egress policy, where it is
69
+ * generated into both the runtime's PHP constants and the Rust broker, so one
70
+ * declaration is enforced at both places a connection is actually made. A copy
71
+ * in this file would be a third statement of the same fact that nothing checks.
72
+ *
73
+ * The `gravatar.com` in the avatar URL below is not that table: it is a public
74
+ * URL handed to a browser, not an egress target.
75
+ */
76
+ /* -------------------------------------------------------------------------- */
77
+ /* The shared client */
78
+ /* -------------------------------------------------------------------------- */
79
+ /**
80
+ * The client both runtimes inject, as source.
81
+ *
82
+ * It is a string rather than a module because neither runtime can import from
83
+ * npm at the point it needs this: Zero compiles a capsule to QuickJS bytecode
84
+ * with a generated prelude, and the Functions host injects modules into a
85
+ * tenant isolate it does not build. Shipping one source to both is what makes
86
+ * "the same API" a fact about the code rather than a promise in a doc.
87
+ *
88
+ * It closes over nothing of ours. The host defines `call(service, operation,
89
+ * payload)` and everything else — normalization, the pure avatar URL, the
90
+ * shapes returned to tenant code — is here, identical on both tiers.
91
+ *
92
+ * The SHA-256 is hand-rolled for the same reason: QuickJS has no WebCrypto,
93
+ * and Cloudflare's `crypto.subtle` is async, which would force `avatarUrl` to
94
+ * return a promise on one tier and a string on the other. A synchronous
95
+ * implementation is the only way the signature can be identical.
96
+ */
97
+ export const RUNTIME_SERVICE_CLIENT_SOURCE = String.raw `
98
+ const SERVICE_DEFAULTS = ${JSON.stringify(GRAVATAR_DEFAULTS)};
99
+ const SERVICE_RATINGS = ${JSON.stringify(GRAVATAR_RATINGS)};
100
+ const SPAM_TYPES = ${JSON.stringify(SPAM_CONTENT_TYPES)};
101
+ const GRAVATAR_MAX_SIZE = ${GRAVATAR_AVATAR_MAX_SIZE};
102
+
103
+ class ServiceError extends Error {
104
+ constructor(code, message) {
105
+ super(message);
106
+ this.name = "SpacefastServiceError";
107
+ this.code = code;
108
+ }
109
+ }
110
+
111
+ function invalid(message) {
112
+ return new ServiceError("service_payload_invalid", message);
113
+ }
114
+
115
+ // UTF-8 by hand: TextEncoder is not guaranteed in QuickJS, and the digest below
116
+ // must agree byte for byte with the one the other tier computes.
117
+ function utf8Bytes(value) {
118
+ const bytes = [];
119
+ for (let index = 0; index < value.length; index++) {
120
+ let point = value.charCodeAt(index);
121
+ if (point >= 0xd800 && point <= 0xdbff && index + 1 < value.length) {
122
+ const low = value.charCodeAt(index + 1);
123
+ if (low >= 0xdc00 && low <= 0xdfff) {
124
+ point = ((point - 0xd800) << 10) + (low - 0xdc00) + 0x10000;
125
+ index++;
126
+ }
127
+ }
128
+ if (point < 0x80) {
129
+ bytes.push(point);
130
+ } else if (point < 0x800) {
131
+ bytes.push(0xc0 | (point >> 6), 0x80 | (point & 0x3f));
132
+ } else if (point < 0x10000) {
133
+ bytes.push(0xe0 | (point >> 12), 0x80 | ((point >> 6) & 0x3f), 0x80 | (point & 0x3f));
134
+ } else {
135
+ bytes.push(
136
+ 0xf0 | (point >> 18),
137
+ 0x80 | ((point >> 12) & 0x3f),
138
+ 0x80 | ((point >> 6) & 0x3f),
139
+ 0x80 | (point & 0x3f),
140
+ );
141
+ }
142
+ }
143
+ return bytes;
144
+ }
145
+
146
+ const SHA256_K = [
147
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
148
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
149
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
150
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
151
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
152
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
153
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
154
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
155
+ ];
156
+
157
+ function sha256Hex(value) {
158
+ const bytes = utf8Bytes(value);
159
+ const bitLength = bytes.length * 8;
160
+ bytes.push(0x80);
161
+ while (bytes.length % 64 !== 56) bytes.push(0);
162
+ // Length is 64-bit big-endian; inputs here are addresses, so the high word is
163
+ // always zero and is written as such rather than computed.
164
+ bytes.push(0, 0, 0, 0);
165
+ bytes.push((bitLength >>> 24) & 0xff, (bitLength >>> 16) & 0xff, (bitLength >>> 8) & 0xff, bitLength & 0xff);
166
+
167
+ const hash = [
168
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
169
+ ];
170
+ const words = new Array(64);
171
+
172
+ for (let offset = 0; offset < bytes.length; offset += 64) {
173
+ for (let index = 0; index < 16; index++) {
174
+ const at = offset + index * 4;
175
+ words[index] =
176
+ ((bytes[at] << 24) | (bytes[at + 1] << 16) | (bytes[at + 2] << 8) | bytes[at + 3]) >>> 0;
177
+ }
178
+ for (let index = 16; index < 64; index++) {
179
+ const a = words[index - 15];
180
+ const b = words[index - 2];
181
+ const s0 = ((a >>> 7) | (a << 25)) ^ ((a >>> 18) | (a << 14)) ^ (a >>> 3);
182
+ const s1 = ((b >>> 17) | (b << 15)) ^ ((b >>> 19) | (b << 13)) ^ (b >>> 10);
183
+ words[index] = (words[index - 16] + s0 + words[index - 7] + s1) >>> 0;
184
+ }
185
+
186
+ let [a, b, c, d, e, f, g, h] = hash;
187
+ for (let index = 0; index < 64; index++) {
188
+ const s1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
189
+ const ch = (e & f) ^ (~e & g);
190
+ const temp1 = (h + s1 + ch + SHA256_K[index] + words[index]) >>> 0;
191
+ const s0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
192
+ const maj = (a & b) ^ (a & c) ^ (b & c);
193
+ const temp2 = (s0 + maj) >>> 0;
194
+ h = g;
195
+ g = f;
196
+ f = e;
197
+ e = (d + temp1) >>> 0;
198
+ d = c;
199
+ c = b;
200
+ b = a;
201
+ a = (temp1 + temp2) >>> 0;
202
+ }
203
+ hash[0] = (hash[0] + a) >>> 0;
204
+ hash[1] = (hash[1] + b) >>> 0;
205
+ hash[2] = (hash[2] + c) >>> 0;
206
+ hash[3] = (hash[3] + d) >>> 0;
207
+ hash[4] = (hash[4] + e) >>> 0;
208
+ hash[5] = (hash[5] + f) >>> 0;
209
+ hash[6] = (hash[6] + g) >>> 0;
210
+ hash[7] = (hash[7] + h) >>> 0;
211
+ }
212
+
213
+ let hex = "";
214
+ for (const word of hash) {
215
+ hex += ("00000000" + (word >>> 0).toString(16)).slice(-8);
216
+ }
217
+ return hex;
218
+ }
219
+
220
+ // Gravatar's own identifier rule. Trim and lower-case only: it deliberately
221
+ // does not strip dots or plus-tags, because two addresses that differ there are
222
+ // two accounts as far as Gravatar is concerned.
223
+ function gravatarHash(email) {
224
+ if (typeof email !== "string" || email.trim() === "") {
225
+ throw invalid("An email address is required.");
226
+ }
227
+ return sha256Hex(email.trim().toLowerCase());
228
+ }
229
+
230
+ function makeGravatar(call) {
231
+ return Object.freeze({
232
+ avatarUrl(email, options) {
233
+ const hash = gravatarHash(email);
234
+ const settings = options && typeof options === "object" ? options : {};
235
+ const query = [];
236
+ if (settings.size !== undefined) {
237
+ const size = Number(settings.size);
238
+ if (!Number.isInteger(size) || size < 1 || size > GRAVATAR_MAX_SIZE) {
239
+ throw invalid("Avatar size must be an integer between 1 and " + GRAVATAR_MAX_SIZE + ".");
240
+ }
241
+ query.push("s=" + size);
242
+ }
243
+ if (settings.default !== undefined) {
244
+ if (!SERVICE_DEFAULTS.includes(settings.default)) {
245
+ throw invalid("Avatar default must be one of: " + SERVICE_DEFAULTS.join(", ") + ".");
246
+ }
247
+ query.push("d=" + encodeURIComponent(settings.default));
248
+ }
249
+ if (settings.rating !== undefined) {
250
+ if (!SERVICE_RATINGS.includes(settings.rating)) {
251
+ throw invalid("Avatar rating must be one of: " + SERVICE_RATINGS.join(", ") + ".");
252
+ }
253
+ query.push("r=" + settings.rating);
254
+ }
255
+ if (settings.forceDefault === true) {
256
+ query.push("f=y");
257
+ }
258
+ return (
259
+ "https://gravatar.com/avatar/" + hash + (query.length > 0 ? "?" + query.join("&") : "")
260
+ );
261
+ },
262
+ async profile(email) {
263
+ return call("gravatar", "profile", { hash: gravatarHash(email) });
264
+ },
265
+ });
266
+ }
267
+
268
+ function spamPayload(submission) {
269
+ if (!submission || typeof submission !== "object") {
270
+ throw invalid("A submission object is required.");
271
+ }
272
+ if (typeof submission.content !== "string" || submission.content === "") {
273
+ throw invalid("Submission content is required.");
274
+ }
275
+ if (typeof submission.userIp !== "string" || submission.userIp.trim() === "") {
276
+ throw invalid("Submission userIp is required: a verdict without it is unreliable.");
277
+ }
278
+ if (submission.type !== undefined && !SPAM_TYPES.includes(submission.type)) {
279
+ throw invalid("Submission type must be one of: " + SPAM_TYPES.join(", ") + ".");
280
+ }
281
+ if (submission.authorRole !== undefined && submission.authorRole !== "administrator") {
282
+ throw invalid('Submission authorRole may only be "administrator".');
283
+ }
284
+ const payload = {
285
+ content: submission.content,
286
+ userIp: submission.userIp.trim(),
287
+ type: submission.type || "comment",
288
+ };
289
+ for (const field of [
290
+ "userAgent",
291
+ "referrer",
292
+ "permalink",
293
+ "authorName",
294
+ "authorEmail",
295
+ "authorUrl",
296
+ "authorRole",
297
+ "createdAt",
298
+ ]) {
299
+ if (typeof submission[field] === "string" && submission[field] !== "") {
300
+ payload[field] = submission[field];
301
+ }
302
+ }
303
+ if (Array.isArray(submission.languages) && submission.languages.length > 0) {
304
+ payload.languages = submission.languages.filter((entry) => typeof entry === "string");
305
+ }
306
+ return payload;
307
+ }
308
+
309
+ function requestSpamPayload(submission, requestContext) {
310
+ const userIp =
311
+ requestContext && typeof requestContext.userIp === "string"
312
+ ? requestContext.userIp.trim()
313
+ : "";
314
+ if (userIp === "") {
315
+ throw new ServiceError(
316
+ "service_context_unavailable",
317
+ "The runtime did not receive a trusted visitor IP for this spam check.",
318
+ );
319
+ }
320
+ // Rebuild the network fields from host-owned context even when untyped
321
+ // JavaScript smuggles same-named properties into the author submission.
322
+ const payload = spamPayload({
323
+ ...submission,
324
+ userIp,
325
+ userAgent: undefined,
326
+ referrer: undefined,
327
+ permalink: undefined,
328
+ });
329
+ for (const field of ["userAgent", "referrer", "permalink"]) {
330
+ if (typeof requestContext[field] === "string" && requestContext[field] !== "") {
331
+ payload[field] = requestContext[field];
332
+ }
333
+ }
334
+ return payload;
335
+ }
336
+
337
+ function makeSpam(call, requestContext) {
338
+ return Object.freeze({
339
+ async check(submission) {
340
+ const payload =
341
+ requestContext === undefined
342
+ ? spamPayload(submission)
343
+ : requestSpamPayload(submission, requestContext);
344
+ const result = await call("spam", "check", payload);
345
+ return Object.freeze({ spam: result.spam === true, discard: result.discard === true });
346
+ },
347
+ async reportSpam(submission) {
348
+ await call("spam", "report_spam", spamPayload(submission));
349
+ },
350
+ async reportHam(submission) {
351
+ await call("spam", "report_ham", spamPayload(submission));
352
+ },
353
+ });
354
+ }
355
+
356
+ function emailAddress(value, field) {
357
+ if (typeof value === "string") {
358
+ if (value.trim() === "") throw invalid(field + " must not be empty.");
359
+ return { email: value.trim() };
360
+ }
361
+ if (value && typeof value === "object" && typeof value.email === "string" && value.email.trim() !== "") {
362
+ const address = { email: value.email.trim() };
363
+ if (typeof value.name === "string" && value.name !== "") address.name = value.name;
364
+ return address;
365
+ }
366
+ throw invalid(field + " must be an address or an { email, name } object.");
367
+ }
368
+
369
+ function emailAddressList(value, field) {
370
+ const entries = Array.isArray(value) ? value : [value];
371
+ if (entries.length === 0) throw invalid(field + " must name at least one recipient.");
372
+ return entries.map((entry) => emailAddress(entry, field));
373
+ }
374
+
375
+ function emailPayload(message) {
376
+ if (!message || typeof message !== "object") {
377
+ throw invalid("A message object is required.");
378
+ }
379
+ if (typeof message.subject !== "string" || message.subject === "") {
380
+ throw invalid("Message subject is required.");
381
+ }
382
+ const hasText = typeof message.text === "string" && message.text !== "";
383
+ const hasHtml = typeof message.html === "string" && message.html !== "";
384
+ if (!hasText && !hasHtml) {
385
+ throw invalid("Message must carry text, html, or both.");
386
+ }
387
+ const payload = {
388
+ from: emailAddress(message.from, "from"),
389
+ to: emailAddressList(message.to, "to"),
390
+ subject: message.subject,
391
+ };
392
+ if (hasText) payload.text = message.text;
393
+ if (hasHtml) payload.html = message.html;
394
+ if (message.cc !== undefined) payload.cc = emailAddressList(message.cc, "cc");
395
+ if (message.bcc !== undefined) payload.bcc = emailAddressList(message.bcc, "bcc");
396
+ if (message.replyTo !== undefined) payload.replyTo = emailAddress(message.replyTo, "replyTo");
397
+ if (message.headers !== undefined) {
398
+ if (!message.headers || typeof message.headers !== "object") {
399
+ throw invalid("Message headers must be an object.");
400
+ }
401
+ const headers = {};
402
+ for (const name of Object.keys(message.headers)) {
403
+ const value = message.headers[name];
404
+ if (typeof value !== "string") throw invalid("Header " + name + " must be a string.");
405
+ // Refused here rather than at the broker so the author sees the field
406
+ // name they wrote. The broker refuses again — this is convenience, not
407
+ // the boundary.
408
+ if (/[^\x21-\x39\x3b-\x7e]/.test(name)) {
409
+ throw invalid("Header " + name + " is not a valid header name.");
410
+ }
411
+ if (/[\r\n\x00]/.test(value)) {
412
+ throw invalid("Header " + name + " must not contain line breaks or NUL.");
413
+ }
414
+ headers[name] = value;
415
+ }
416
+ payload.headers = headers;
417
+ }
418
+ return payload;
419
+ }
420
+
421
+ function makeEmail(call) {
422
+ return Object.freeze({
423
+ async send(message) {
424
+ const result = await call("email", "send", emailPayload(message));
425
+ return Object.freeze({ messageId: result.messageId });
426
+ },
427
+ });
428
+ }
429
+
430
+ function makeRuntimeServices(call, requestContext) {
431
+ return {
432
+ gravatar: makeGravatar(call),
433
+ spam: makeSpam(call, requestContext),
434
+ email: makeEmail(call),
435
+ };
436
+ }
437
+ `;
@@ -1,79 +1,70 @@
1
1
  import { z } from "zod";
2
- export declare const runtimeStorageVisibilitySchema: z.ZodEnum<{
3
- public: "public";
4
- private: "private";
5
- }>;
6
- export type RuntimeStorageVisibility = z.infer<typeof runtimeStorageVisibilitySchema>;
2
+ /** Objects are addressed by a bare 128-bit random id — there is no key and no visibility. */
7
3
  export declare const runtimeStorageObjectIdSchema: z.ZodString;
8
- export declare const runtimeStorageObjectKeySchema: z.ZodString;
4
+ /** The runtime-scoped, revocable read key that makes a `/__stattic/u/<id>` URL readable. */
5
+ export declare const runtimeStorageReadKeySchema: z.ZodString;
6
+ export type UploadedObject = {
7
+ id: string;
8
+ contentType: string;
9
+ size: number;
10
+ url: string;
11
+ };
12
+ export type StorageClient = {
13
+ upload(file: Blob): Promise<UploadedObject>;
14
+ get(id: string): Promise<Response | null>;
15
+ delete(id: string): Promise<void>;
16
+ };
9
17
  export declare const runtimeStorageListQuerySchema: z.ZodObject<{
10
18
  cursor: z.ZodOptional<z.ZodString>;
11
19
  limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
12
- visibility: z.ZodOptional<z.ZodEnum<{
13
- public: "public";
14
- private: "private";
15
- }>>;
16
20
  }, z.core.$strict>;
17
21
  export declare const runtimeStorageObjectSchema: z.ZodObject<{
18
- key: z.ZodString;
19
- visibility: z.ZodEnum<{
20
- public: "public";
21
- private: "private";
22
- }>;
22
+ id: z.ZodString;
23
23
  contentType: z.ZodString;
24
24
  createdAt: z.ZodString;
25
25
  size: z.ZodNumber;
26
26
  uploaderId: z.ZodString;
27
27
  sha256: z.ZodString;
28
+ url: z.ZodString;
28
29
  }, z.core.$strict>;
29
30
  export type RuntimeStorageObject = z.infer<typeof runtimeStorageObjectSchema>;
30
31
  export declare const runtimeStorageListResponseSchema: z.ZodObject<{
31
32
  objects: z.ZodArray<z.ZodObject<{
32
- key: z.ZodString;
33
- visibility: z.ZodEnum<{
34
- public: "public";
35
- private: "private";
36
- }>;
33
+ id: z.ZodString;
37
34
  contentType: z.ZodString;
38
35
  createdAt: z.ZodString;
39
36
  size: z.ZodNumber;
40
37
  uploaderId: z.ZodString;
41
38
  sha256: z.ZodString;
39
+ url: z.ZodString;
42
40
  }, z.core.$strict>>;
43
41
  cursor: z.ZodNullable<z.ZodString>;
44
42
  hasMore: z.ZodBoolean;
45
- usedBytes: z.ZodNumber;
46
- quotaBytes: z.ZodNumber;
47
43
  }, z.core.$strict>;
48
44
  export type RuntimeStorageListResponse = z.infer<typeof runtimeStorageListResponseSchema>;
49
45
  export declare const runtimeStorageListPublicResponseSchema: z.ZodObject<{
50
46
  data: z.ZodObject<{
51
47
  objects: z.ZodArray<z.ZodObject<{
52
- key: z.ZodString;
53
- visibility: z.ZodEnum<{
54
- public: "public";
55
- private: "private";
56
- }>;
48
+ id: z.ZodString;
57
49
  contentType: z.ZodString;
58
50
  createdAt: z.ZodString;
59
51
  size: z.ZodNumber;
60
52
  uploaderId: z.ZodString;
61
53
  sha256: z.ZodString;
54
+ url: z.ZodString;
62
55
  }, z.core.$strict>>;
63
56
  cursor: z.ZodNullable<z.ZodString>;
64
57
  hasMore: z.ZodBoolean;
65
- usedBytes: z.ZodNumber;
66
- quotaBytes: z.ZodNumber;
67
58
  }, z.core.$strict>;
68
59
  }, z.core.$strip>;
69
60
  export declare const runtimeStorageDeleteResponseSchema: z.ZodObject<{
70
- key: z.ZodString;
61
+ id: z.ZodString;
71
62
  deleted: z.ZodBoolean;
72
63
  }, z.core.$strict>;
73
64
  export type RuntimeStorageDeleteResponse = z.infer<typeof runtimeStorageDeleteResponseSchema>;
74
65
  export declare const runtimeStorageDeletePublicResponseSchema: z.ZodObject<{
75
66
  data: z.ZodObject<{
76
- key: z.ZodString;
67
+ id: z.ZodString;
77
68
  deleted: z.ZodBoolean;
78
69
  }, z.core.$strict>;
79
70
  operation: z.ZodNullable<z.ZodObject<{
@@ -99,8 +90,8 @@ export declare const runtimeStorageDeletePublicResponseSchema: z.ZodObject<{
99
90
  code: z.ZodString;
100
91
  severity: z.ZodEnum<{
101
92
  info: "info";
102
- error: "error";
103
93
  warning: "warning";
94
+ error: "error";
104
95
  }>;
105
96
  message: z.ZodString;
106
97
  path: z.ZodOptional<z.ZodNullable<z.ZodString>>;
@@ -118,3 +109,25 @@ export declare const runtimeStorageDeletePublicResponseSchema: z.ZodObject<{
118
109
  finishedAt: z.ZodOptional<z.ZodString>;
119
110
  }, z.core.$strip>>;
120
111
  }, z.core.$strip>;
112
+ export declare const runtimeStorageReadKeyResponseSchema: z.ZodObject<{
113
+ key: z.ZodString;
114
+ }, z.core.$strict>;
115
+ export type RuntimeStorageReadKeyResponse = z.infer<typeof runtimeStorageReadKeyResponseSchema>;
116
+ export declare const runtimeStorageReadKeyRotateResponseSchema: z.ZodObject<{
117
+ key: z.ZodString;
118
+ rotatedAt: z.ZodString;
119
+ purge: z.ZodObject<{
120
+ status: z.ZodEnum<{
121
+ failed: "failed";
122
+ pending: "pending";
123
+ ok: "ok";
124
+ }>;
125
+ mode: z.ZodEnum<{
126
+ domain: "domain";
127
+ none: "none";
128
+ urls: "urls";
129
+ }>;
130
+ urls: z.ZodOptional<z.ZodNumber>;
131
+ }, z.core.$strict>;
132
+ }, z.core.$strict>;
133
+ export type RuntimeStorageReadKeyRotateResponse = z.infer<typeof runtimeStorageReadKeyRotateResponseSchema>;
@@ -1,24 +1,27 @@
1
1
  import { z } from "zod";
2
2
  import { dataEnvelope, mutationEnvelope } from "./common.js";
3
- export const runtimeStorageVisibilitySchema = z.enum(["public", "private"]);
3
+ /** Objects are addressed by a bare 128-bit random id — there is no key and no visibility. */
4
4
  export const runtimeStorageObjectIdSchema = z.string().regex(/^[a-f0-9]{32}$/);
5
- export const runtimeStorageObjectKeySchema = z.string().regex(/^(public|private)\/[a-f0-9]{32}$/);
5
+ /** The runtime-scoped, revocable read key that makes a `/__stattic/u/<id>` URL readable. */
6
+ export const runtimeStorageReadKeySchema = z.string().regex(/^[a-f0-9]{32}$/);
6
7
  export const runtimeStorageListQuerySchema = z
7
8
  .object({
8
9
  cursor: z.string().min(1).max(2_048).optional(),
9
10
  limit: z.coerce.number().int().min(1).max(100).default(50),
10
- visibility: runtimeStorageVisibilitySchema.optional(),
11
11
  })
12
12
  .strict();
13
13
  export const runtimeStorageObjectSchema = z
14
14
  .object({
15
- key: runtimeStorageObjectKeySchema,
16
- visibility: runtimeStorageVisibilitySchema,
15
+ id: runtimeStorageObjectIdSchema,
17
16
  contentType: z.string().min(1).max(255),
18
17
  createdAt: z.string().datetime(),
19
18
  size: z.number().int().nonnegative(),
20
19
  uploaderId: z.string().min(1).max(255),
21
20
  sha256: z.string().regex(/^[a-f0-9]{64}$/),
21
+ // Path-relative and composed from the read key that was current at response
22
+ // time: `/__stattic/u/<id>?k=<key>`. The caller absolutizes it against
23
+ // whichever of the space's hostnames it is presenting.
24
+ url: z.string().min(1),
22
25
  })
23
26
  .strict();
24
27
  export const runtimeStorageListResponseSchema = z
@@ -26,15 +29,34 @@ export const runtimeStorageListResponseSchema = z
26
29
  objects: z.array(runtimeStorageObjectSchema),
27
30
  cursor: z.string().nullable(),
28
31
  hasMore: z.boolean(),
29
- usedBytes: z.number().int().nonnegative(),
30
- quotaBytes: z.number().int().positive(),
31
32
  })
32
33
  .strict();
33
34
  export const runtimeStorageListPublicResponseSchema = dataEnvelope(runtimeStorageListResponseSchema);
34
35
  export const runtimeStorageDeleteResponseSchema = z
35
36
  .object({
36
- key: runtimeStorageObjectKeySchema,
37
+ id: runtimeStorageObjectIdSchema,
37
38
  deleted: z.boolean(),
38
39
  })
39
40
  .strict();
40
41
  export const runtimeStorageDeletePublicResponseSchema = mutationEnvelope(runtimeStorageDeleteResponseSchema);
42
+ export const runtimeStorageReadKeyResponseSchema = z
43
+ .object({ key: runtimeStorageReadKeySchema })
44
+ .strict();
45
+ // Rotation is the emergency lever: every URL minted under the old key answers
46
+ // 404 afterwards, so the runtime purges every hostname on the box and reports
47
+ // how that purge landed.
48
+ export const runtimeStorageReadKeyRotateResponseSchema = z
49
+ .object({
50
+ key: runtimeStorageReadKeySchema,
51
+ rotatedAt: z.string().datetime({ offset: true }),
52
+ // `none` is the receipt for a runtime that serves no hostname yet, so there
53
+ // was nothing to purge.
54
+ purge: z
55
+ .object({
56
+ status: z.enum(["ok", "failed", "pending"]),
57
+ mode: z.enum(["urls", "domain", "none"]),
58
+ urls: z.number().int().nonnegative().optional(),
59
+ })
60
+ .strict(),
61
+ })
62
+ .strict();