@volter/world-core 3.0.156 → 3.0.157

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 (74) hide show
  1. package/attach.cjs +2 -0
  2. package/capability-decision.cjs +6 -4
  3. package/dist/attach.cjs +2 -0
  4. package/dist/capability-decision.cjs +6 -4
  5. package/dist/generated/pack-facts.json +77 -8
  6. package/dist/network-policy.cjs +11 -1
  7. package/dist/network-policy.d.cts +1 -0
  8. package/dist/pack-facts.d.cts +3 -0
  9. package/dist/required-credential-transport.cjs +29 -0
  10. package/dist/required-credential-transport.d.cts +3 -0
  11. package/dist/src/blob-store.d.ts +16 -0
  12. package/dist/src/blob-store.js +76 -1
  13. package/dist/src/derived-core.d.ts +72 -6
  14. package/dist/src/derived-core.js +192 -51
  15. package/dist/src/derived-real.js +97 -15
  16. package/dist/src/derived.d.ts +5 -0
  17. package/dist/src/executor.d.ts +7 -0
  18. package/dist/src/executor.js +15 -1
  19. package/dist/src/git/lfs.d.ts +13 -3
  20. package/dist/src/git/lfs.js +41 -9
  21. package/dist/src/head.d.ts +2 -0
  22. package/dist/src/head.js +6 -0
  23. package/dist/src/index.d.ts +1 -0
  24. package/dist/src/index.js +1 -0
  25. package/dist/src/live/ingest.js +9 -13
  26. package/dist/src/live/rtmp.js +7 -5
  27. package/dist/src/live/segmenter.js +7 -1
  28. package/dist/src/machines.js +5 -1
  29. package/dist/src/managed-go-programs.d.ts +55 -0
  30. package/dist/src/managed-go-programs.js +133 -0
  31. package/dist/src/mongodb/stream.d.ts +23 -0
  32. package/dist/src/pack-fetch.d.ts +6 -1
  33. package/dist/src/pack-fetch.js +49 -6
  34. package/dist/src/pack-paths.d.ts +2 -1
  35. package/dist/src/pack-paths.js +6 -1
  36. package/dist/src/resource-blob.d.ts +4 -0
  37. package/dist/src/resource-blob.js +8 -1
  38. package/dist/src/runtime.d.ts +3 -3
  39. package/dist/src/runtime.js +2 -2
  40. package/dist/src/serve-http.js +53 -3
  41. package/dist/src/sockets.d.ts +4 -1
  42. package/dist/src/sockets.js +59 -6
  43. package/dist/src/vendor-call.js +58 -0
  44. package/dist/src/world-store.d.ts +15 -1
  45. package/dist/src/world-store.js +78 -2
  46. package/generated/pack-facts.json +77 -8
  47. package/network-policy.cjs +11 -1
  48. package/network-policy.d.cts +1 -0
  49. package/pack-facts.d.cts +3 -0
  50. package/package.json +8 -1
  51. package/required-credential-transport.cjs +29 -0
  52. package/required-credential-transport.d.cts +3 -0
  53. package/src/blob-store.ts +59 -1
  54. package/src/derived-core.ts +215 -51
  55. package/src/derived-real.ts +80 -14
  56. package/src/derived.ts +6 -1
  57. package/src/executor.ts +17 -3
  58. package/src/git/lfs.ts +40 -9
  59. package/src/head.ts +6 -0
  60. package/src/index.ts +1 -0
  61. package/src/live/ingest.ts +7 -3
  62. package/src/live/rtmp.ts +7 -5
  63. package/src/live/segmenter.ts +7 -1
  64. package/src/machines.ts +5 -1
  65. package/src/managed-go-programs.ts +146 -0
  66. package/src/mongodb/stream.ts +10 -0
  67. package/src/pack-fetch.ts +46 -8
  68. package/src/pack-paths.ts +5 -1
  69. package/src/resource-blob.ts +9 -1
  70. package/src/runtime.ts +4 -2
  71. package/src/serve-http.ts +43 -3
  72. package/src/sockets.ts +39 -8
  73. package/src/vendor-call.ts +54 -0
  74. package/src/world-store.ts +66 -2
@@ -25,7 +25,7 @@ import { askApplication, deliverEvents, type ApplicationAnswer, type Application
25
25
  import type { RateBudgetDeclaration } from './rateBudget.ts';
26
26
  import { handlerCrypto, hmac, ksuidFrom, lettersFrom, sha256, uuidFrom, type HandlerCrypto } from './signing.ts';
27
27
  import { hashFieldValue } from './hash.ts';
28
- import { readResourceBlob, removeResourceBlob, writeResourceBlob } from './resource-blob.ts';
28
+ import { readResourceBlob, readResourceBlobRange, removeResourceBlob, resourceBlobSize, writeResourceBlob, writeResourceBlobStream } from './resource-blob.ts';
29
29
  import { managedDatabase, unboundDatabase, type ManagedDatabase } from './managed-database.ts';
30
30
  import { redisServiceTimes } from './redis/service-times.ts';
31
31
  import { execRedisRun, type RedisDialect, type RunItem } from './redis/engine.ts';
@@ -84,6 +84,8 @@ export type Transition = {
84
84
  export type StateField = {
85
85
  initial: string | boolean;
86
86
  transitions: Transition[];
87
+ /** Plain updates may repeat the current representation without another state move. */
88
+ unchanged?: 'allow';
87
89
  derive?: Array<{ state: string; when: { equals?: unknown; gt?: number; truthy?: boolean; absent?: boolean } }>;
88
90
  vendorInitial?: string;
89
91
  };
@@ -149,6 +151,8 @@ export type ResourceDecl = {
149
151
  * fields when its stored id is not what the path names (a repository stored as `repo:7` with `owner`
150
152
  * and `name`, addressed as `{owner}/{repo}`): each stored field against a template over the path. */
151
153
  parent?: { param?: string; params?: string[]; value?: string; field: string; resource: string; allowDeleted?: boolean; where?: Record<string, string>;
154
+ /** Global subject paths omit the collection's parent labels; tenancy still applies. */
155
+ whenNamed?: true;
152
156
  /** private fields a child observed under its parent keeps from the parent's parameters (`_jurisdiction:
153
157
  * '{jurisdiction}'`), where the child's own read names none and a later read under it needs one ("Refresh") */
154
158
  keep?: Record<string, string> };
@@ -201,7 +205,9 @@ export type ResourceDecl = {
201
205
  * holds this one's id (or vendor `parentKey`), only those with `present` set or `absent` unset when named and without
202
206
  * any matching `unless` field, plus `plus` (a customer's
203
207
  * `next_invoice_sequence` is one past its numbered invoices). `by` may be a dot path; exact field names take
204
- * precedence. The kernel keeps each whenever a child is written. */
208
+ * precedence. The kernel keeps each whenever a child is written, the parent it left too when the write moves it; `of`
209
+ * may name a bookkeeping type no resource declares, written with `ctx.record`. A count is kept by the lane that
210
+ * declares it: a child another lane writes is not counted there. */
205
211
  counts?: Record<string, { of: string; by: string; parentKey?: string; present?: string; absent?: string; unless?: Record<string, unknown>; plus?: number }>;
206
212
  /** how the vendor's state of this type is read back into a root (the real-system adapters, "Refresh"): the list that
207
213
  * enumerates it (once per parent, under `parent`; `complete: false` when it is not the whole type; `items`, the answer's
@@ -217,7 +223,7 @@ export type ResourceDecl = {
217
223
  * jurisdictions). A blob read by an `operation` (a GET of the unit's surface, filled from the subject's fields and
218
224
  * its parent's parameters) keeps the bytes in the World's blobs, their key in `field`, and in `headersField` the
219
225
  * answer's headers `keepHeaders` names (a name ending `*` a prefix: `x-amz-meta-*`), both fields private. */
220
- refresh?: ({ list: string; unpaginated?: true; keyed?: true; detail?: string; detailMerge?: string; detailWith?: Record<string, string>; complete?: false; items?: string; shape?: RefreshShape; next?: string | Record<string, string>; nextLink?: { header: string; rel: string; query: string; when?: Record<string, string> }; cursor?: string; more?: string } | { get: string; known?: true }) & { params?: Record<string, string>; headers?: Record<string, string>; variants?: ReadonlyArray<{ headers: Record<string, string> }>; blobs?: ReadonlyArray<{ path: string; key: string; resource?: string; fields?: Record<string, string> } | { operation: string; field: string; headers?: Record<string, string>; headersField?: string; keepHeaders?: string[] }>; query?: Record<string, string>; idAs?: string; graphql?: { query: string; variables?: Record<string, unknown>; items: string; pageInfo?: string; cursor?: string } } | { none: string };
226
+ refresh?: ({ list: string; unpaginated?: true; keyed?: true; detail?: string; detailMerge?: string; detailWith?: Record<string, string>; complete?: false; items?: string; shape?: RefreshShape; next?: string | Record<string, string>; nextLink?: { header: string; rel: string; query: string; when?: Record<string, string> }; cursor?: string; more?: string } | { get: string; known?: true; gone?: { status: number; says: string } }) & { params?: Record<string, string>; headers?: Record<string, string>; variants?: ReadonlyArray<{ headers: Record<string, string> }>; blobs?: ReadonlyArray<{ path: string; key: string; resource?: string; fields?: Record<string, string> } | { operation: string; field: string; headers?: Record<string, string>; headersField?: string; keepHeaders?: string[] }>; query?: Record<string, string>; idAs?: string; graphql?: { query: string; variables?: Record<string, unknown>; items: string; pageInfo?: string; cursor?: string } } | { none: string };
221
227
  /** the other resources one create of this one makes, and where its answer names each's id (a JSON path) */
222
228
  companions?: Record<string, string>;
223
229
  /** the field telling a newer copy of a subject from an older (an `updated_at`): an ingested event older than the
@@ -274,6 +280,11 @@ export type VendorManifest = { vendor: string; discovery?: DerivedManifest['disc
274
280
 
275
281
  /** `code` as the vendor writes it: a string, or a number (Discord's JSON error codes are integers) */
276
282
  export type ErrorSpec = { status: number; message: string; code?: string | number; param?: string; kind?: string };
283
+ /** How a form value outside its spec's list is refused (DerivedOperation.enums): the error, its message filled with
284
+ * `{param}` (the bracket name) and `{values}` (the values it takes); `documented`, values the vendor documents outside
285
+ * its spec, by operation and path; `version`, the header naming the API version a request is made at and the version
286
+ * the spec is, so a request pinned to another version, whose lists the pack does not hold, is not checked. */
287
+ export type EnumRefusal = ErrorSpec & { documented?: Record<string, Record<string, string[]>>; version?: { header: string; served: string } };
277
288
 
278
289
  /** A vendor screen a World serves (docs/contributing/architecture.md, "Screens"). A `flow` is a hosted
279
290
  * page an application sends its user through, held to its documented round trip; a `workspace` is a
@@ -300,6 +311,11 @@ export type ScreenDecl = {
300
311
  * takes, with its demand (an upload bucket a page posts a lease's form to: Reddit's S3 hosts) (architecture, "A page
301
312
  * at a World's place") */
302
313
  takes?: ReadonlyArray<'POST'>;
314
+ /** a workspace page that shows one record: the stored type and the path parameter its id fills (a pull request's
315
+ * page, `{number}`); `?show=<type>:<id>` asked of the pack's workspaces opens it (architecture, "Workspace") */
316
+ shows?: { type: string; param: string };
317
+ /** the stored types a workspace page lists, where `?show=` lands for a record no page shows by itself */
318
+ lists?: ReadonlyArray<string>;
303
319
  /** the vendor's documentation of the round trip or the screen */
304
320
  source: string;
305
321
  };
@@ -335,13 +351,20 @@ export type DerivedManifest = {
335
351
  /** `strict`: a vendor that refuses a body field its operation does not declare, a value of another type, or a number
336
352
  * outside the spec's bounds (a NestJS validation pipe: Supabase's "property x should not exist"), with its messages;
337
353
  * `{name}`, `{type}` (`a string`, `an integer`), `{min}` and `{max}` are filled. `ctx.fields` checks a body so. */
338
- body: { form?: { coerce: boolean }; json?: 'always'; validation?: { operations?: ReadonlyArray<string>; status: number; missing: unknown; type: unknown; range: unknown }; strict?: { status: number; code?: string; unknown: string; type: string; range: string } };
354
+ body: { form?: { coerce: boolean; enumRefusal?: EnumRefusal }; json?: 'always'; validation?: { operations?: ReadonlyArray<string>; status: number; missing: unknown; type: unknown; range: unknown }; strict?: { status: number; code?: string; unknown: string; type: string; range: string; arrays?: Record<string, 'string'> } };
339
355
  /** `acceptProvided`: a create that names its own `id` keeps it (a vendor that seeds by id). The template counts
340
356
  * (`{prefix}_{n}`), or is `{uuid}` for a vendor whose ids are UUIDs: the next is derived from the resource and its
341
357
  * count, so a World mints the same ids every run */
342
358
  ids: { template: string; acceptProvided?: boolean };
343
359
  /** operations the derived core must not claim though their resource is declared: a gap until someone models them */
344
360
  unmodeled?: string[];
361
+ /** documented variants of a served operation that no demand asks for, by operation: each a parameter present
362
+ * (`pause_collection`), present on any item of a list (`items[].price_data`), or with a value
363
+ * (`payment_method_data.type=sepa_debit`, `payment_method_types[]=sepa_debit`), or holding one entry of a comma-separated
364
+ * value (`tweet.fields~=geo`; a parameter whose name holds a dot is read by that name). A request asking one is answered the
365
+ * manifest's gap before its handler runs, as an operation no handler serves is (architecture, "Creating a pack",
366
+ * step 7), so no pack line answers it */
367
+ variants?: Record<string, string[]>;
345
368
  /** operations that only read though nothing in the spec says so (a POST that returns data):
346
369
  * a read-only twin answers them */
347
370
  reads?: string[];
@@ -370,6 +393,9 @@ export type DerivedManifest = {
370
393
  readOnly: ErrorSpec;
371
394
  /** what a body labelled JSON that does not parse answers; absent, a 400 in the vendor's error body */
372
395
  malformedBody?: ErrorSpec;
396
+ /** the vendor's front merges repeated slashes in a path before it routes (nginx's default `merge_slashes`): a request
397
+ * to `//r/x//about` is `/r/x/about`, merged on arrival before a door, a screen or an operation is matched */
398
+ mergeSlashes?: boolean;
373
399
  /** a request header that selects the vendor's API version, and the answer to a malformed one
374
400
  * (`{value}` in its message is the header's value) */
375
401
  version?: { header: string; pattern: string; error: ErrorSpec };
@@ -475,7 +501,9 @@ export type DerivedManifest = {
475
501
  };
476
502
  /** The World's doors (architecture, "Doors, screens and the gap"): each a method and a path under `/_twin/` (with
477
503
  * `{param}` segments), answered by the export of `semantics/doors.ts` its id names, over the contract's context. */
478
- doors?: Array<{ id: string; method: string; path: string; note?: string }>;
504
+ doors?: Array<{ id: string; method: string; path: string; note?: string; redactBody?: string[] }>;
505
+ /** REST refresh binds observed tenant metadata through the root account identity endpoint. */
506
+ refreshIdentity?: { operation: string; field: string };
479
507
  /** The vendor's GraphQL API beside its REST one (`semantics/graphql.ts` over `generated/graphql-sdl.gen.json`): the
480
508
  * paths it is POSTed to, and the answer to a query selecting a field nothing models (`{field}`, `{type}`). */
481
509
  graphql?: { paths: ReadonlyArray<string>; unmodeled: { message: string; type?: string; status?: number } };
@@ -568,12 +596,13 @@ export function strictFields(m: DerivedManifest, operation: DerivedOperation, bo
568
596
  const field = declared.get(name);
569
597
  if (!field) return refuse(strict.unknown, { name });
570
598
  const { type } = field;
571
- const fits = value === null
599
+ const fits = (value === null && !(type === 'array' && strict.arrays?.[name]))
572
600
  || (type === 'boolean' && typeof value === 'boolean')
573
601
  || (type === 'integer' && isExactIntegerValue(value))
574
602
  || (type === 'number' && (typeof value === 'number' || isExactIntegerValue(value)))
575
603
  || (type === 'string' && typeof value === 'string')
576
- || !['boolean', 'integer', 'number', 'string'].includes(type);
604
+ || (type === 'array' && Array.isArray(value) && (!strict.arrays?.[name] || value.every(item => typeof item === strict.arrays![name])))
605
+ || !['boolean', 'integer', 'number', 'string', 'array'].includes(type);
577
606
  if (!fits) return refuse(strict.type, { name, type: type === 'integer' ? 'an integer' : `a ${type}` });
578
607
  if ((typeof value === 'number' || isExactIntegerValue(value)) && ((field.minimum !== undefined && compareExactNumbers(value, field.minimum) < 0) || (field.maximum !== undefined && compareExactNumbers(value, field.maximum) > 0))) {
579
608
  return refuse(strict.range, { name, min: field.minimum ?? null, max: field.maximum ?? null });
@@ -779,12 +808,20 @@ function parentField(row: unknown, path: string): unknown {
779
808
  return value;
780
809
  }
781
810
 
782
- function missingParent(m: DerivedManifest, resource: string, call: DerivedCall, root?: string): Response | undefined {
783
- const parent = m.resources[resource]?.parent;
811
+ function parentOfCall(decl: ResourceDecl | undefined, call: DerivedCall): ResourceDecl['parent'] {
812
+ const parent = decl?.parent;
813
+ const names = parent?.params ?? (parent?.param === undefined ? [] : [parent.param]);
814
+ return parent?.whenNamed && names.length && !names.some((name) => Object.hasOwn(call.params, name))
815
+ ? undefined : parent;
816
+ }
817
+
818
+ function missingParent(m: DerivedManifest, resource: string, call: DerivedCall, root?: string, tenant?: string): Response | undefined {
819
+ const parent = parentOfCall(m.resources[resource], call);
784
820
  if (!parent) return undefined;
785
821
  const id = parentValue(parent, call.params);
786
822
  const where = Object.entries(parent.where ?? {}).map(([field, template]) => [field, String(fill(template, call.params))] as const);
787
- const found = (r: TwinResource): boolean => (where.length ? where.every(([field, v]) => String(parentField(r, field)) === v) : r.id === id);
823
+ const found = (r: TwinResource): boolean => (where.length ? where.every(([field, v]) => String(parentField(r, field)) === v) : r.id === id)
824
+ && (!m.tenant || m.resources[parent.resource]?.shared || (r as Record<string, unknown>)[m.tenant.field] === tenant);
788
825
  return id !== undefined && stored(m, parent.resource, root, { withDeleted: parent.allowDeleted === true }).some(found) ? undefined : notFound(m, parent.resource, String(id), parent.param ?? parent.params?.at(-1));
789
826
  }
790
827
 
@@ -1168,7 +1205,11 @@ function recordedInput(m: DerivedManifest, call: DerivedCall, merged: Record<str
1168
1205
  const id = call.operation.id;
1169
1206
  const sent = sentOf.get(call);
1170
1207
  const activity = call.request.headers.get('x-volter-world-activity') === 'provisioning' ? { activity: 'provisioning' } : {};
1171
- const asSent = sent ? { ...activity, path: sent.path, query: sent.query, body: sent.body, headers: sent.headers, ...(sent.lane ? { lane: sent.lane } : {}), ...(sent.requestId ? { requestId: sent.requestId } : {}), ...(sent.type ? { contentType: sent.type } : {}) } : { ...activity, path: call.params, body: merged };
1208
+ const door = m.doors?.find(d => d.id === id);
1209
+ const body = sent?.body ?? merged;
1210
+ const safeBody = door?.redactBody?.length && body && typeof body === 'object' && !Array.isArray(body)
1211
+ ? Object.fromEntries(Object.entries(body).filter(([key]) => !door.redactBody!.includes(key))) : body;
1212
+ const asSent = sent ? { ...activity, path: sent.path, query: sent.query, body: safeBody, headers: sent.headers, ...(sent.lane ? { lane: sent.lane } : {}), ...(sent.requestId ? { requestId: sent.requestId } : {}), ...(sent.type ? { contentType: sent.type } : {}) } : { ...activity, path: call.params, body: safeBody };
1172
1213
  if (id === 'graphql') {
1173
1214
  const b = (sent?.body ?? {}) as { query?: unknown; variables?: unknown; operationName?: unknown };
1174
1215
  return { ...activity, graphql: { query: b.query, variables: b.variables ?? {}, ...(b.operationName ? { operationName: b.operationName } : {}) }, ...(sent?.lane ? { lane: sent.lane } : {}), headers: sent?.headers ?? {} };
@@ -1186,34 +1227,33 @@ const requestOfCall = new WeakMap<DerivedCall, Request>();
1186
1227
 
1187
1228
  async function writeDetailed(m: DerivedManifest, call: DerivedCall, resource: string, id: string, fields: Record<string, unknown>, operation: string, params: Record<string, unknown>, root: string | undefined, occurredAt: string, checks?: StateChecks): Promise<{ body: Record<string, unknown>; id: string; vendorData?: unknown }> {
1188
1229
  const before = stateBeforeWrite(m, resource, id, root);
1230
+ const prior = counted(m, resource) ? stored(m, resource, root).find((r) => r.id === id) as Record<string, unknown> | undefined : undefined;
1189
1231
  const withFiles = async (input: Record<string, unknown>): Promise<Record<string, unknown>> => { const files = input.operationId ? await keptFiles(m, call, root) : undefined; return files ? { ...input, files } : input; };
1190
1232
  const { resource: row, result } = await applyTwinWrite(
1191
1233
  m.service,
1192
- // who made it: the login the pack's identity reads off the request, when it declares one, so a subject's
1193
- // history can say who did each thing
1194
1234
  // a vendor's own move (ctx.asVendor, the clock) is the World's, as the clock's writes are: no API call a head performs
1195
1235
  inVendorMove()
1196
1236
  ? { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: { clock: true }, occurredAt, actor: { kind: 'system' } }
1197
- : { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: await withFiles(recordedInput(m, call, params)), occurredAt, actor: { kind: 'agent', ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) } },
1237
+ : { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: await withFiles(recordedInput(m, call, params)), occurredAt, actor: { kind: 'agent' } },
1198
1238
  root,
1199
1239
  );
1200
1240
  observeStateWrite(m, result, before, root, checks);
1201
- return finishWrite(m, call, resource, row, result, operation, params, root, occurredAt);
1241
+ return finishWrite(m, call, resource, row, result, operation, params, root, occurredAt, prior && { ...prior });
1202
1242
  }
1203
1243
 
1204
1244
  async function atomicWrite(m: DerivedManifest, call: DerivedCall, resource: string,
1205
1245
  decide: (snapshot: readonly TwinResource[]) => { id: string; fields: Record<string, unknown> } | undefined,
1206
- operation: string, params: Record<string, unknown>, root: string | undefined, occurredAt: string, unchangedId?: string, checks?: StateChecks,
1246
+ operation: string, params: Record<string, unknown>, root: string | undefined, occurredAt: string, unchangedId?: string, checks?: StateChecks, omitInput = false,
1207
1247
  ): Promise<Record<string, unknown> | undefined> {
1208
- const input = recordedInput(m, call, params);
1209
- const files = input.operationId ? await keptFiles(m, call, root) : undefined;
1248
+ const input = omitInput ? undefined : recordedInput(m, call, params);
1249
+ const files = input?.operationId ? await keptFiles(m, call, root) : undefined;
1210
1250
  let before: TwinResource | undefined;
1211
1251
  const committed = await applyTwinWriteAtomic(m.service, (snapshot) => {
1212
1252
  const selected = decide(snapshot);
1213
- if (selected && transitionObserver) before = snapshot.find((r) => r.type === storedType(m, resource) && r.id === selected.id);
1253
+ if (selected && (transitionObserver || counted(m, resource))) before = snapshot.find((r) => r.type === storedType(m, resource) && r.id === selected.id);
1214
1254
  return selected ? { kind: 'write', value: selected.id, write: {
1215
1255
  operation, subjectType: storedType(m, resource), subjectId: selected.id, fields: selected.fields,
1216
- input: files ? { ...input, files } : input, occurredAt, actor: { kind: 'agent' as const, ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) },
1256
+ ...(input ? { input: files ? { ...input, files } : input } : {}), occurredAt, actor: { kind: 'agent' as const, ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) },
1217
1257
  } } : { kind: 'skip', value: undefined };
1218
1258
  }, root);
1219
1259
  if (!committed.result || !committed.value) {
@@ -1222,14 +1262,14 @@ async function atomicWrite(m: DerivedManifest, call: DerivedCall, resource: stri
1222
1262
  }
1223
1263
  observeStateWrite(m, committed.result, before, root, checks);
1224
1264
  const row = resourcesOfType(m.service, storedType(m, resource), root).find((r) => r.id === resolveSubjectId(m.service, storedType(m, resource), committed.value!, root))!;
1225
- return (await finishWrite(m, call, resource, row, committed.result, operation, params, root, occurredAt)).body;
1265
+ return (await finishWrite(m, call, resource, row, committed.result, operation, params, root, occurredAt, before as unknown as Record<string, unknown> | undefined)).body;
1226
1266
  }
1227
1267
 
1228
1268
  async function finishWrite(m: DerivedManifest, call: DerivedCall, resource: string, row: TwinResource,
1229
1269
  result: { externalId?: string; vendorData?: unknown }, operation: string, params: Record<string, unknown>,
1230
- root: string | undefined, occurredAt: string,
1270
+ root: string | undefined, occurredAt: string, prior?: Record<string, unknown>,
1231
1271
  ): Promise<{ body: Record<string, unknown>; id: string; vendorData?: unknown }> {
1232
- await keepCounts(m, call, resource, row as unknown as Record<string, unknown>, params, root, occurredAt);
1272
+ await keepCounts(m, call, resource, row as unknown as Record<string, unknown>, params, root, occurredAt, prior);
1233
1273
  const body = view(m, resource, row);
1234
1274
  // the write's own context for its hooks, in the scope the call was served in (a walk's application answers, a tenant,
1235
1275
  // the managed database), at the write's moment
@@ -1250,24 +1290,30 @@ async function finishWrite(m: DerivedManifest, call: DerivedCall, resource: stri
1250
1290
 
1251
1291
  /** The declared counts a written subject is a child in (a resource's `counts`), each kept on its parent: written when
1252
1292
  * the count moved, through the write path, so the parent's history says when. */
1253
- async function keepCounts(m: DerivedManifest, call: DerivedCall, resource: string, row: Record<string, unknown>, params: Record<string, unknown>, root: string | undefined, occurredAt: string): Promise<void> {
1293
+ async function keepCounts(m: DerivedManifest, call: DerivedCall, resource: string, row: Record<string, unknown>, params: Record<string, unknown>, root: string | undefined, occurredAt: string, prior?: Record<string, unknown>): Promise<void> {
1254
1294
  for (const [parent, decl] of Object.entries(m.resources)) {
1255
1295
  for (const [field, c] of Object.entries(decl.counts ?? {})) {
1256
1296
  if (storedType(m, c.of) !== storedType(m, resource)) continue;
1257
- const parentId = parentField(row, c.by);
1258
- if (parentId === undefined || parentId === null) continue;
1259
- const pid = String(parentId);
1260
- const held = stored(m, parent, root).find((r) => String(c.parentKey ? ownFields(r)[c.parentKey] : r.id) === pid) as Record<string, unknown> | undefined;
1261
- if (!held) continue;
1262
- const n = stored(m, c.of, root).filter((r) => {
1263
- const x = r as Record<string, unknown>;
1264
- return String(parentField(x, c.by)) === pid && (c.present === undefined || (x[c.present] !== undefined && x[c.present] !== null)) && (c.absent === undefined || parentField(ownFields(r), c.absent) === undefined || parentField(ownFields(r), c.absent) === null) && Object.entries(c.unless ?? {}).every(([key, value]) => ownFields(r)[key] !== value);
1265
- }).length + (c.plus ?? 0);
1266
- if (held[field] !== n) await writeDetailed(m, call, parent, String(held.id), { [field]: n }, `${storedType(m, parent)}.counted`, params, root, occurredAt);
1297
+ // the parent the subject is under now, and the one it left when the write moved it: both counts changed
1298
+ const pids = new Set([parentField(row, c.by), prior === undefined ? undefined : parentField(prior, c.by)].filter((v) => v !== undefined && v !== null).map(String));
1299
+ for (const pid of pids) {
1300
+ const held = stored(m, parent, root).find((r) => String(c.parentKey ? ownFields(r)[c.parentKey] : r.id) === pid) as Record<string, unknown> | undefined;
1301
+ if (!held) continue;
1302
+ const n = stored(m, c.of, root).filter((r) => {
1303
+ const x = r as Record<string, unknown>;
1304
+ return String(parentField(x, c.by)) === pid && (c.present === undefined || (x[c.present] !== undefined && x[c.present] !== null)) && (c.absent === undefined || parentField(ownFields(r), c.absent) === undefined || parentField(ownFields(r), c.absent) === null) && Object.entries(c.unless ?? {}).every(([key, value]) => ownFields(r)[key] !== value);
1305
+ }).length + (c.plus ?? 0);
1306
+ if (held[field] !== n) await writeDetailed(m, call, parent, String(held.id), { [field]: n }, `${storedType(m, parent)}.counted`, params, root, occurredAt);
1307
+ }
1267
1308
  }
1268
1309
  }
1269
1310
  }
1270
1311
 
1312
+ /** Whether any resource counts this one (`counts`): only then does a write read the subject as it stood, for the parent
1313
+ * it may leave. */
1314
+ const counted = (m: DerivedManifest, resource: string): boolean =>
1315
+ Object.values(m.resources).some((d) => Object.values(d.counts ?? {}).some((c) => storedType(m, c.of) === storedType(m, resource)));
1316
+
1271
1317
  /** An answer in the vendor's success envelope, under the operation's key, when the vendor has one. */
1272
1318
  function envelope(m: DerivedManifest, op: DerivedOperation, body: Record<string, unknown>): Record<string, unknown> {
1273
1319
  return m.success && op.answers?.key ? { ...m.success, [op.answers.key]: body } : body;
@@ -1600,14 +1646,14 @@ export async function serveCore(m: DerivedManifest, call: DerivedCall, scope: Co
1600
1646
  // the body is read from a copy, so the call's own request is still unread for the write hook's context
1601
1647
  const params = await boundaryParams(m, { ...call, request: call.request.clone() }, root);
1602
1648
  const idParam = subjectOf(m, resource, call);
1603
- const orphan = missingParent(m, resource, call, root);
1604
- if (orphan) return { served: orphan };
1605
- const parent = decl.parent;
1649
+ const parent = parentOfCall(decl, call);
1606
1650
  const parentId = parent ? parentValue(parent, call.params) : undefined;
1607
1651
  // the caller's tenant, when the vendor has tenancy and the resource is not shared by every tenant
1608
1652
  const tenantField = m.tenant && !decl.shared ? m.tenant.field : undefined;
1609
1653
  const tenant = tenantField && scope.tenant ? await scope.tenant(await contextFor(m, { ...call, request: call.request.clone() }, scope)) : undefined;
1610
1654
  if (tenantField && tenant === undefined) return { unmodeled: `the caller names no tenant (semantics/tenant.ts) for ${resource}` };
1655
+ const orphan = missingParent(m, resource, call, root, tenant);
1656
+ if (orphan) return { served: orphan };
1611
1657
  const underParent = (r: TwinResource): boolean => (!parent || (r as Record<string, unknown>)[parent.field] === parentId)
1612
1658
  && (!tenantField || (r as Record<string, unknown>)[tenantField] === tenant);
1613
1659
  const paths = expandPaths(m, params);
@@ -1681,6 +1727,7 @@ export async function serveCore(m: DerivedManifest, call: DerivedCall, scope: Co
1681
1727
  for (const [field, sdecl] of Object.entries(decl.state ?? {})) {
1682
1728
  const requested = op.class === 'update' ? data[field] : undefined;
1683
1729
  if (op.class === 'update' && requested === undefined) continue;
1730
+ if (op.class === 'update' && sdecl.unchanged === 'allow' && stateOf(sdecl, current[field]) === stateOf(sdecl, requested)) { changes[field] = requested; continue; }
1684
1731
  const { move, refusal } = transitionFor(field, sdecl, op.id, current[field], requested, hit.id);
1685
1732
  if (refusal) return { served: vendorError(m, refusal) };
1686
1733
  if (!move) {
@@ -1768,8 +1815,6 @@ export type SemanticsContext = {
1768
1815
  /** A mail the vendor sends a person, over SMTP to the route its settings name (smtp.ts), under the World's egress rule;
1769
1816
  * throws when it cannot be delivered. */
1770
1817
  mail(route: SmtpRoute, mail: Mail, headers?: Record<string, string>): Promise<void>;
1771
- /** who is calling, when the manifest says how to tell */
1772
- actor: string | undefined;
1773
1818
  now(): unknown;
1774
1819
  /** Render a stored row or vendor view using its resource declaration, retaining the stored subject id fallback. */
1775
1820
  render(row: Record<string, unknown>, resource?: string): Record<string, unknown>;
@@ -1808,9 +1853,9 @@ export type SemanticsContext = {
1808
1853
  /** Allocate a stored occurrence and append its fields under one action lock. The optional field factory is pure. */
1809
1854
  create(resource: string, fields: Record<string, unknown> | ((id: string) => Record<string, unknown>), operation: string): Promise<Record<string, unknown>>;
1810
1855
  /** Create under declared unique keys and the action lock; a collision returns the existing own-field view without a write. */
1811
- createUnique(resource: string, fields: Record<string, unknown> | ((id: string) => Record<string, unknown>), operation: string): Promise<{ created: boolean; row: Record<string, unknown> }>;
1856
+ createUnique(resource: string, fields: Record<string, unknown> | ((id: string) => Record<string, unknown>), operation: string, options?: { input: 'omit' }): Promise<{ created: boolean; row: Record<string, unknown> }>;
1812
1857
  /** Decide an update from the current live stored row under the action lock; undefined leaves it unchanged. */
1813
- change(resource: string, id: string, decide: (row: Record<string, unknown>) => Record<string, unknown> | undefined, operation: string): Promise<Record<string, unknown> | undefined>;
1858
+ change(resource: string, id: string, decide: (row: Record<string, unknown>) => Record<string, unknown> | undefined, operation: string, options?: { input: 'omit' }): Promise<Record<string, unknown> | undefined>;
1814
1859
  write(resource: string, id: string, fields: Record<string, unknown>, operation: string): Promise<Record<string, unknown>>;
1815
1860
  /**
1816
1861
  * One command's writes as one group (S2 R17; docs/contributing/architecture.md, "A command's writes as one group").
@@ -1909,7 +1954,16 @@ export type SemanticsContext = {
1909
1954
  parts(): Promise<MultipartPart[]>;
1910
1955
  /** The pack's resource blobs (an object's bytes), by key, on the World's branch: they branch with the World, and a
1911
1956
  * read falls back to an ancestor's. The row that names a blob is the pack's; the bytes are here. */
1912
- blobs: { put(key: string, bytes: Uint8Array): Promise<void>; get(key: string): Promise<Uint8Array | null>; remove(key: string): Promise<void> };
1957
+ blobs: {
1958
+ put(key: string, bytes: Uint8Array): Promise<void>; get(key: string): Promise<Uint8Array | null>; remove(key: string): Promise<void>;
1959
+ /** a body written as it arrives (a request's own stream), kept only when `accept` holds for its size and SHA-256:
1960
+ * an upload of a large object never held whole. True when kept */
1961
+ putStream(key: string, body: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>, accept: (written: { size: number; sha256: string }) => boolean): Promise<boolean>;
1962
+ /** a blob's length, or null when there is none: a large object is answered without loading it */
1963
+ size(key: string): Promise<number | null>;
1964
+ /** the bytes `start`..`endInclusive` of a blob (a ranged read where the store has one), or null when there is none */
1965
+ range(key: string, start: number, endInclusive: number): Promise<Uint8Array | null>;
1966
+ };
1913
1967
  /** A body's fields checked against the operation's declared ones under the manifest's `body.strict`: the fields, or
1914
1968
  * the vendor's refusal of the first that is unknown, of another type or out of its bounds. */
1915
1969
  fields(body: Record<string, unknown>): { fields: Record<string, unknown> } | { refused: Response };
@@ -1937,6 +1991,12 @@ export type WriteHookContext = Omit<HandlerContext, 'write' | 'batch' | 'create'
1937
1991
  export type Handler = (ctx: HandlerContext) => Promise<Response>;
1938
1992
 
1939
1993
  /** The context the kernel opens for a call (a handler's, a door's, a screen's). Kernel-internal: packs are given it. */
1994
+ /** A media type whose body the core reads for a context (and records with a write): JSON, a form, multipart, text,
1995
+ * or none named. */
1996
+ const coreReadsBody = (type: string | null): boolean => {
1997
+ const media = (type ?? '').split(';')[0]!.trim().toLowerCase();
1998
+ return media === '' || media === 'application/json' || media.endsWith('+json') || media === 'application/x-www-form-urlencoded' || media.startsWith('multipart/') || media.startsWith('text/');
1999
+ };
1940
2000
  export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: CoreScope): Promise<SemanticsContext> {
1941
2001
  const root = scope.root;
1942
2002
  const asAsked = call;
@@ -1946,8 +2006,10 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
1946
2006
  // the guard's parse when it made one (taken once), else this context's own
1947
2007
  // a body that is its payload's bytes (S3's PutObject) is never read as JSON, whatever its label
1948
2008
  const payload = bytesPayload(call.operation);
1949
- const parsed = payload ? undefined : takeParsedBody(call.request);
1950
- const text = await bodyTextOf(call.request.clone()).catch(() => '');
2009
+ // a screen's body the core does not read (an upload's bytes) is left to its handler, unread (DerivedCall.streamed)
2010
+ const opaque = call.streamed === true && !coreReadsBody(call.request.headers.get('content-type'));
2011
+ const parsed = payload || opaque ? undefined : takeParsedBody(call.request);
2012
+ const text = opaque ? '' : await bodyTextOf(call.request.clone()).catch(() => '');
1951
2013
  let body: unknown = parsed?.value;
1952
2014
  if (!parsed) { if (payload) body = text || undefined; else { try { body = text ? parseExactJson(text) : undefined; } catch { body = text; } } }
1953
2015
  // the request as it came, before the core's parameter merge and alias adoption: what a write records, so a perform
@@ -1996,7 +2058,6 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
1996
2058
  return { store: new GitObjectStore(getActiveBlobStore(), `${plane}/repos/${name}`), refs: new GitRefs(getActiveWorldStore(), `${plane}/repos/${name}`) };
1997
2059
  },
1998
2060
  mail: (route, mail, headers) => sendMail(route, mail, headers),
1999
- actor: m.identity ? m.identity(call.request.headers.get('authorization'), root) : undefined,
2000
2061
  now: () => now(m, at),
2001
2062
  render: (row, resource) => {
2002
2063
  const raw = rawResources.has(row);
@@ -2033,10 +2094,15 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2033
2094
  secret: async (label) => hmac(await worldSeed(m.service, root, at), label, 'base64url'),
2034
2095
  record: async (type, fields, id, operation) => {
2035
2096
  if (!type.startsWith('_')) throw new Error(`semantics: ${type} is not a bookkeeping type (bookkeeping types start with _)`);
2097
+ // a bookkeeping subject a resource counts (`counts` naming it: sentry's issue users) keeps that count as any
2098
+ // child does, the parent it left recounted too
2099
+ const keeps = counted(m, type);
2036
2100
  if (id !== undefined) {
2037
2101
  const before = operation ? stateBeforeWrite(m, type, id, root) : undefined;
2038
- const { result } = await applyTwinWrite(m.service, { operation: operation ?? `${type.slice(1)}.record`, subjectType: type, subjectId: id, fields, ...provisioningInput(call), occurredAt: at, actor: { kind: 'system' } }, root);
2102
+ const prior = keeps ? resourcesOfType(m.service, type, root).find((r) => r.id === id) : undefined;
2103
+ const { resource: row, result } = await applyTwinWrite(m.service, { operation: operation ?? `${type.slice(1)}.record`, subjectType: type, subjectId: id, fields, ...provisioningInput(call), occurredAt: at, actor: { kind: 'system' } }, root);
2039
2104
  if (operation) observeStateWrite(m, result, before, root, stateChecks);
2105
+ if (keeps) await keepCounts(m, call, type, row as unknown as Record<string, unknown>, params, root, at, prior && { ...prior } as Record<string, unknown>);
2040
2106
  return id;
2041
2107
  }
2042
2108
  const { value, result } = await applyTwinWriteAtomic(m.service, (resources) => {
@@ -2054,6 +2120,8 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2054
2120
  return { kind: 'write', value: subject, write: { operation: operation ?? `${type.slice(1)}.record`, subjectType: type, subjectId: subject, fields, ...provisioningInput(call), occurredAt: at, actor: { kind: 'system' } } };
2055
2121
  }, root, () => resourcesOfType(m.service, type, root));
2056
2122
  if (operation && result) observeStateWrite(m, result, undefined, root, stateChecks);
2123
+ const made = keeps && value !== undefined ? resourcesOfType(m.service, type, root).find((r) => r.id === value) : undefined;
2124
+ if (made) await keepCounts(m, call, type, made as unknown as Record<string, unknown>, params, root, at);
2057
2125
  return value;
2058
2126
  },
2059
2127
  get machines() {
@@ -2066,7 +2134,8 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2066
2134
  const id = mintId(m, resource, root, at, snapshot);
2067
2135
  return { id, fields: typeof fields === 'function' ? fields(id) : fields };
2068
2136
  }, operation, params, root, at, undefined, stateChecks).then((row) => row!),
2069
- createUnique: async (resource, fields, operation) => {
2137
+ createUnique: async (resource, fields, operation, options) => {
2138
+ if (options?.input === 'omit' && !resource.startsWith('_')) throw new Error('only bookkeeping writes may omit request input');
2070
2139
  const rules = m.resources[resource]?.unique;
2071
2140
  if (!rules?.length) throw new Error('createUnique requires declared unique keys');
2072
2141
  let existing: Record<string, unknown> | undefined;
@@ -2076,15 +2145,18 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2076
2145
  const hit = snapshot.find((r) => r.type === storedType(m, resource) && r.deleted !== true && rules.some((rule) => rule.fields.every((f) => selected[f] !== undefined && view(m, resource, r)[f] === selected[f])));
2077
2146
  if (hit) { existing = view(m, resource, hit); return undefined; }
2078
2147
  return { id, fields: selected };
2079
- }, operation, params, root, at, undefined, stateChecks);
2148
+ }, operation, params, root, at, undefined, stateChecks, options?.input === 'omit');
2080
2149
  return existing ? { created: false, row: existing } : { created: true, row: row! };
2081
2150
  },
2082
- change: (resource, id, decide, operation) => atomicWrite(m, call, resource, (snapshot) => {
2151
+ change: (resource, id, decide, operation, options) => {
2152
+ if (options?.input === 'omit' && !resource.startsWith('_')) throw new Error('only bookkeeping writes may omit request input');
2153
+ return atomicWrite(m, call, resource, (snapshot) => {
2083
2154
  const row = snapshot.find((r) => r.type === storedType(m, resource) && r.id === id && r.deleted !== true);
2084
2155
  if (!row) return undefined;
2085
2156
  const fields = decide(row as Record<string, unknown>);
2086
2157
  return fields === undefined ? undefined : { id, fields };
2087
- }, operation, params, root, at, id, stateChecks),
2158
+ }, operation, params, root, at, id, stateChecks, options?.input === 'omit');
2159
+ },
2088
2160
  write: (resource, id, fields, operation) => write(m, call, resource, id, fields, operation, params, root, at, stateChecks),
2089
2161
  accumulate: async (resource, id, deltas, operation) => {
2090
2162
  if (!resource.startsWith('_') || !m.resources[resource]) throw new Error('accumulate requires a declared bookkeeping resource');
@@ -2166,8 +2238,11 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2166
2238
  live: liveAccess({ rows: (type) => resourcesOfType(m.service, type, root) as unknown as Array<Record<string, unknown>>, blob: (key) => readResourceBlob(m.service, key, root) }, call.request),
2167
2239
  blobs: {
2168
2240
  put: (key, bytes) => writeResourceBlob(m.service, key, bytes, root),
2241
+ putStream: (key, body, accept) => writeResourceBlobStream(m.service, key, body, accept, root),
2169
2242
  get: (key) => readResourceBlob(m.service, key, root),
2170
2243
  remove: (key) => removeResourceBlob(m.service, key, root),
2244
+ size: (key) => resourceBlobSize(m.service, key, root),
2245
+ range: (key, start, endInclusive) => readResourceBlobRange(m.service, key, start, endInclusive, root),
2171
2246
  },
2172
2247
  merge: (resource, prior, next) => mergeField(m.resources[resource]?.update, prior, next),
2173
2248
  conflict: (resource, fields, except) => {
@@ -2309,7 +2384,96 @@ async function writeGroup(m: DerivedManifest, call: DerivedCall, writes: readonl
2309
2384
  /** A pack's semantics handlers as the dispatch's handlers. */
2310
2385
  export function bindSemantics(m: DerivedManifest, handlers: Record<string, Semantics>, scope: CoreScope = {}): Record<string, DerivedHandler> {
2311
2386
  prepareViewOmissions(m);
2312
- return Object.fromEntries(Object.entries(handlers).map(([id, h]) => [id, async (call: DerivedCall) => { const ctx = await contextFor(m, call, scope); redisServiceTimes.lap('6c the handler\'s own context opened'); return h(ctx); }]));
2387
+ return Object.fromEntries(Object.entries(handlers).map(([id, h]) => [id, async (call: DerivedCall) => {
2388
+ const ctx = await contextFor(m, call, scope);
2389
+ redisServiceTimes.lap('6c the handler\'s own context opened');
2390
+ // an operation's own variants, else the ones every operation shares (`'*'`)
2391
+ const variants = m.variants?.[id] ?? m.variants?.['*'];
2392
+ if (variants && variantAsked(ctx.params as Record<string, unknown>, variants, call.request.headers)) return variantGap(m, call.request);
2393
+ const outside = enumRefused(m, call, ctx.params as Record<string, unknown>);
2394
+ return outside ?? h(ctx);
2395
+ }]));
2396
+ }
2397
+
2398
+ /** The first of a request's form parameters whose value is not one its spec lists (DerivedOperation.enums), with the
2399
+ * bracket name a form gives it (`spending_controls[spending_limits][0][interval]`) and the values it takes. */
2400
+ export function valueOutside(params: Record<string, unknown>, enums: Readonly<Record<string, ReadonlyArray<string>>>): { param: string; values: ReadonlyArray<string> } | undefined {
2401
+ const at = (value: unknown, path: string[], name: string, values: ReadonlyArray<string>): { param: string; values: ReadonlyArray<string> } | undefined => {
2402
+ if (value === undefined || value === null) return undefined;
2403
+ if (!path.length) return typeof value === 'object' || values.includes(String(value)) ? undefined : { param: name, values };
2404
+ const [head, ...rest] = path;
2405
+ if (head === '[]') {
2406
+ const items = Array.isArray(value) ? value.map((v, i) => [String(i), v] as const) : value && typeof value === 'object' ? Object.entries(value) : [];
2407
+ for (const [i, v] of items) { const found = at(v, rest, `${name}[${i}]`, values); if (found) return found; }
2408
+ return undefined;
2409
+ }
2410
+ return value && typeof value === 'object' && !Array.isArray(value) ? at((value as Record<string, unknown>)[head!], rest, `${name}[${head}]`, values) : undefined;
2411
+ };
2412
+ for (const [path, values] of Object.entries(enums)) {
2413
+ const [first, ...rest] = path.split('.');
2414
+ const found = at(params[first!], rest, first!, values);
2415
+ if (found) return found;
2416
+ }
2417
+ return undefined;
2418
+ }
2419
+
2420
+ /** The manifest's refusal of a form value outside the operation's spec lists (and the values documented beside
2421
+ * them), unless the request names an API version other than the one the spec is. */
2422
+ function enumRefused(m: DerivedManifest, call: DerivedCall, params: Record<string, unknown>): Response | undefined {
2423
+ const refusal = m.body.form?.enumRefusal;
2424
+ if (!refusal || !call.operation.enums) return undefined;
2425
+ const pinned = refusal.version ? call.request.headers.get(refusal.version.header) : null;
2426
+ if (pinned && pinned !== refusal.version!.served) return undefined;
2427
+ const documented = refusal.documented?.[call.operation.id] ?? {};
2428
+ const enums = Object.fromEntries(Object.entries(call.operation.enums).map(([path, values]) => [path, [...values, ...(documented[path] ?? [])]]));
2429
+ const outside = valueOutside(params, enums);
2430
+ if (!outside) return undefined;
2431
+ const { documented: _d, version: _v, ...error } = refusal;
2432
+ return vendorError(m, { ...error, param: outside.param, message: error.message.replace(/\{param\}/g, outside.param).replace(/\{values\}/g, oneOfList(outside.values)) });
2433
+ }
2434
+
2435
+ /** A list of values as a sentence names them: `a, b, or c`. */
2436
+ const oneOfList = (values: ReadonlyArray<string>): string => (values.length < 2 ? values.join('') : `${values.slice(0, -1).join(', ')}, or ${values[values.length - 1]}`);
2437
+
2438
+ /** Whether a request's parameters ask one of the operation's undemanded variants (DerivedManifest.variants). */
2439
+ export function variantAsked(params: Record<string, unknown>, variants: readonly string[], headers?: Headers): boolean {
2440
+ const valuesAt = (value: unknown, path: string[]): unknown[] => {
2441
+ if (!path.length) return value === undefined ? [] : [value];
2442
+ const [head, ...rest] = path;
2443
+ // `!name` is the field left out: the value itself where it has none (`items[].!id`, an item naming no id)
2444
+ if (head!.startsWith('!')) return value && typeof value === 'object' && (value as Record<string, unknown>)[head!.slice(1)] === undefined ? [value] : [];
2445
+ const list = head!.endsWith('[]');
2446
+ const key = list ? head!.slice(0, -2) : head!;
2447
+ const next = value && typeof value === 'object' ? (value as Record<string, unknown>)[key] : undefined;
2448
+ if (!list) return valuesAt(next, rest);
2449
+ const items = Array.isArray(next) ? next : next && typeof next === 'object' ? Object.values(next) : [];
2450
+ return items.flatMap((item) => valuesAt(item, rest));
2451
+ };
2452
+ // a variant joined with `&` is its parts asked together (`trial_end=now&items`): the combination, not either alone;
2453
+ // a part led by `!` is a parameter left out (`items[].price&!items[].id`, an item added rather than changed)
2454
+ return variants.some((variant) => variant.split('&').every((part) => {
2455
+ const absent = part.startsWith('!');
2456
+ // `name~=value` is one entry of a comma-separated value (`tweet.fields~=geo`, X's fields lists); `name=value` the whole
2457
+ const body = absent ? part.slice(1) : part;
2458
+ const listed = body.includes('~=');
2459
+ const [path, wanted] = listed ? body.split('~=') : body.split('=');
2460
+ // a part led by `@` names a request header (`@stripe-account&application_fee_amount`, a charge made on a connected
2461
+ // account with the platform's fee)
2462
+ const header = path!.startsWith('@') ? headers?.get(path!.slice(1)) : undefined;
2463
+ // a parameter whose own name holds a dot (X's `tweet.fields`) is read by that name before the path is walked
2464
+ const found = path!.startsWith('@') ? (header === null || header === undefined ? [] : [header])
2465
+ : Object.hasOwn(params, path!) ? (params[path!] === undefined ? [] : [params[path!]]) : valuesAt(params, path!.split('.'));
2466
+ const asked = wanted === undefined ? found.length > 0
2467
+ : found.some((value) => (listed ? String(value).split(',').map((entry) => entry.trim()).includes(wanted) : String(value) === wanted));
2468
+ return absent ? !asked : asked;
2469
+ }));
2470
+ }
2471
+
2472
+ /** The manifest's gap, as a variant is answered: the unknown-request refusal (else its not-found). */
2473
+ function variantGap(m: DerivedManifest, request: Request): Response {
2474
+ const nf = m.notFound;
2475
+ const gap: ErrorSpec = m.gap ?? { status: nf.status, message: nf.message, ...(nf.code !== undefined ? { code: nf.code } : {}), ...(nf.kind !== undefined ? { kind: nf.kind } : {}) };
2476
+ return vendorError(m, { ...gap, message: gap.message.replace(/\{method\}/g, request.method).replace(/\{path\}/g, new URL(request.url).pathname) });
2313
2477
  }
2314
2478
 
2315
2479
  /** The derived core as the dispatch's core: it owns every operation on a resource the manifest declares. */