@volter/world-core 3.0.156 → 3.0.158

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 +53 -7
  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 +50 -9
  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
@@ -68,6 +68,8 @@ export type Transition = {
68
68
  export type StateField = {
69
69
  initial: string | boolean;
70
70
  transitions: Transition[];
71
+ /** Plain updates may repeat the current representation without another state move. */
72
+ unchanged?: 'allow';
71
73
  derive?: Array<{
72
74
  state: string;
73
75
  when: {
@@ -139,6 +141,8 @@ export type ResourceDecl = {
139
141
  resource: string;
140
142
  allowDeleted?: boolean;
141
143
  where?: Record<string, string>;
144
+ /** Global subject paths omit the collection's parent labels; tenancy still applies. */
145
+ whenNamed?: true;
142
146
  /** private fields a child observed under its parent keeps from the parent's parameters (`_jurisdiction:
143
147
  * '{jurisdiction}'`), where the child's own read names none and a later read under it needs one ("Refresh") */
144
148
  keep?: Record<string, string>;
@@ -215,7 +219,9 @@ export type ResourceDecl = {
215
219
  * holds this one's id (or vendor `parentKey`), only those with `present` set or `absent` unset when named and without
216
220
  * any matching `unless` field, plus `plus` (a customer's
217
221
  * `next_invoice_sequence` is one past its numbered invoices). `by` may be a dot path; exact field names take
218
- * precedence. The kernel keeps each whenever a child is written. */
222
+ * precedence. The kernel keeps each whenever a child is written, the parent it left too when the write moves it; `of`
223
+ * may name a bookkeeping type no resource declares, written with `ctx.record`. A count is kept by the lane that
224
+ * declares it: a child another lane writes is not counted there. */
219
225
  counts?: Record<string, {
220
226
  of: string;
221
227
  by: string;
@@ -261,6 +267,10 @@ export type ResourceDecl = {
261
267
  } | {
262
268
  get: string;
263
269
  known?: true;
270
+ gone?: {
271
+ status: number;
272
+ says: string;
273
+ };
264
274
  }) & {
265
275
  params?: Record<string, string>;
266
276
  headers?: Record<string, string>;
@@ -376,6 +386,17 @@ export type ErrorSpec = {
376
386
  param?: string;
377
387
  kind?: string;
378
388
  };
389
+ /** How a form value outside its spec's list is refused (DerivedOperation.enums): the error, its message filled with
390
+ * `{param}` (the bracket name) and `{values}` (the values it takes); `documented`, values the vendor documents outside
391
+ * its spec, by operation and path; `version`, the header naming the API version a request is made at and the version
392
+ * the spec is, so a request pinned to another version, whose lists the pack does not hold, is not checked. */
393
+ export type EnumRefusal = ErrorSpec & {
394
+ documented?: Record<string, Record<string, string[]>>;
395
+ version?: {
396
+ header: string;
397
+ served: string;
398
+ };
399
+ };
379
400
  /** A vendor screen a World serves (docs/contributing/architecture.md, "Screens"). A `flow` is a hosted
380
401
  * page an application sends its user through, held to its documented round trip; a `workspace` is a
381
402
  * screen people work in. `demand` says who reaches it; `controls` names what a person acts on, the
@@ -401,6 +422,14 @@ export type ScreenDecl = {
401
422
  * takes, with its demand (an upload bucket a page posts a lease's form to: Reddit's S3 hosts) (architecture, "A page
402
423
  * at a World's place") */
403
424
  takes?: ReadonlyArray<'POST'>;
425
+ /** a workspace page that shows one record: the stored type and the path parameter its id fills (a pull request's
426
+ * page, `{number}`); `?show=<type>:<id>` asked of the pack's workspaces opens it (architecture, "Workspace") */
427
+ shows?: {
428
+ type: string;
429
+ param: string;
430
+ };
431
+ /** the stored types a workspace page lists, where `?show=` lands for a record no page shows by itself */
432
+ lists?: ReadonlyArray<string>;
404
433
  /** the vendor's documentation of the round trip or the screen */
405
434
  source: string;
406
435
  };
@@ -443,6 +472,7 @@ export type DerivedManifest = {
443
472
  body: {
444
473
  form?: {
445
474
  coerce: boolean;
475
+ enumRefusal?: EnumRefusal;
446
476
  };
447
477
  json?: 'always';
448
478
  validation?: {
@@ -458,6 +488,7 @@ export type DerivedManifest = {
458
488
  unknown: string;
459
489
  type: string;
460
490
  range: string;
491
+ arrays?: Record<string, 'string'>;
461
492
  };
462
493
  };
463
494
  /** `acceptProvided`: a create that names its own `id` keeps it (a vendor that seeds by id). The template counts
@@ -469,6 +500,13 @@ export type DerivedManifest = {
469
500
  };
470
501
  /** operations the derived core must not claim though their resource is declared: a gap until someone models them */
471
502
  unmodeled?: string[];
503
+ /** documented variants of a served operation that no demand asks for, by operation: each a parameter present
504
+ * (`pause_collection`), present on any item of a list (`items[].price_data`), or with a value
505
+ * (`payment_method_data.type=sepa_debit`, `payment_method_types[]=sepa_debit`), or holding one entry of a comma-separated
506
+ * value (`tweet.fields~=geo`; a parameter whose name holds a dot is read by that name). A request asking one is answered the
507
+ * manifest's gap before its handler runs, as an operation no handler serves is (architecture, "Creating a pack",
508
+ * step 7), so no pack line answers it */
509
+ variants?: Record<string, string[]>;
472
510
  /** operations that only read though nothing in the spec says so (a POST that returns data):
473
511
  * a read-only twin answers them */
474
512
  reads?: string[];
@@ -506,6 +544,9 @@ export type DerivedManifest = {
506
544
  readOnly: ErrorSpec;
507
545
  /** what a body labelled JSON that does not parse answers; absent, a 400 in the vendor's error body */
508
546
  malformedBody?: ErrorSpec;
547
+ /** the vendor's front merges repeated slashes in a path before it routes (nginx's default `merge_slashes`): a request
548
+ * to `//r/x//about` is `/r/x/about`, merged on arrival before a door, a screen or an operation is matched */
549
+ mergeSlashes?: boolean;
509
550
  /** a request header that selects the vendor's API version, and the answer to a malformed one
510
551
  * (`{value}` in its message is the header's value) */
511
552
  version?: {
@@ -702,7 +743,13 @@ export type DerivedManifest = {
702
743
  method: string;
703
744
  path: string;
704
745
  note?: string;
746
+ redactBody?: string[];
705
747
  }>;
748
+ /** REST refresh binds observed tenant metadata through the root account identity endpoint. */
749
+ refreshIdentity?: {
750
+ operation: string;
751
+ field: string;
752
+ };
706
753
  /** The vendor's GraphQL API beside its REST one (`semantics/graphql.ts` over `generated/graphql-sdl.gen.json`): the
707
754
  * paths it is POSTed to, and the answer to a query selecting a field nothing models (`{field}`, `{type}`). */
708
755
  graphql?: {
@@ -939,8 +986,6 @@ export type SemanticsContext = {
939
986
  /** A mail the vendor sends a person, over SMTP to the route its settings name (smtp.ts), under the World's egress rule;
940
987
  * throws when it cannot be delivered. */
941
988
  mail(route: SmtpRoute, mail: Mail, headers?: Record<string, string>): Promise<void>;
942
- /** who is calling, when the manifest says how to tell */
943
- actor: string | undefined;
944
989
  now(): unknown;
945
990
  /** Render a stored row or vendor view using its resource declaration, retaining the stored subject id fallback. */
946
991
  render(row: Record<string, unknown>, resource?: string): Record<string, unknown>;
@@ -985,12 +1030,16 @@ export type SemanticsContext = {
985
1030
  /** Allocate a stored occurrence and append its fields under one action lock. The optional field factory is pure. */
986
1031
  create(resource: string, fields: Record<string, unknown> | ((id: string) => Record<string, unknown>), operation: string): Promise<Record<string, unknown>>;
987
1032
  /** Create under declared unique keys and the action lock; a collision returns the existing own-field view without a write. */
988
- createUnique(resource: string, fields: Record<string, unknown> | ((id: string) => Record<string, unknown>), operation: string): Promise<{
1033
+ createUnique(resource: string, fields: Record<string, unknown> | ((id: string) => Record<string, unknown>), operation: string, options?: {
1034
+ input: 'omit';
1035
+ }): Promise<{
989
1036
  created: boolean;
990
1037
  row: Record<string, unknown>;
991
1038
  }>;
992
1039
  /** Decide an update from the current live stored row under the action lock; undefined leaves it unchanged. */
993
- change(resource: string, id: string, decide: (row: Record<string, unknown>) => Record<string, unknown> | undefined, operation: string): Promise<Record<string, unknown> | undefined>;
1040
+ change(resource: string, id: string, decide: (row: Record<string, unknown>) => Record<string, unknown> | undefined, operation: string, options?: {
1041
+ input: 'omit';
1042
+ }): Promise<Record<string, unknown> | undefined>;
994
1043
  write(resource: string, id: string, fields: Record<string, unknown>, operation: string): Promise<Record<string, unknown>>;
995
1044
  /**
996
1045
  * One command's writes as one group (S2 R17; docs/contributing/architecture.md, "A command's writes as one group").
@@ -1126,6 +1175,16 @@ export type SemanticsContext = {
1126
1175
  put(key: string, bytes: Uint8Array): Promise<void>;
1127
1176
  get(key: string): Promise<Uint8Array | null>;
1128
1177
  remove(key: string): Promise<void>;
1178
+ /** a body written as it arrives (a request's own stream), kept only when `accept` holds for its size and SHA-256:
1179
+ * an upload of a large object never held whole. True when kept */
1180
+ putStream(key: string, body: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>, accept: (written: {
1181
+ size: number;
1182
+ sha256: string;
1183
+ }) => boolean): Promise<boolean>;
1184
+ /** a blob's length, or null when there is none: a large object is answered without loading it */
1185
+ size(key: string): Promise<number | null>;
1186
+ /** the bytes `start`..`endInclusive` of a blob (a ranged read where the store has one), or null when there is none */
1187
+ range(key: string, start: number, endInclusive: number): Promise<Uint8Array | null>;
1129
1188
  };
1130
1189
  /** A body's fields checked against the operation's declared ones under the manifest's `body.strict`: the fields, or
1131
1190
  * the vendor's refusal of the first that is unknown, of another type or out of its bounds. */
@@ -1155,10 +1214,17 @@ export type HandlerContext = Omit<SemanticsContext, 'now' | 'core' | 'atomically
1155
1214
  export type WriteHookContext = Omit<HandlerContext, 'write' | 'batch' | 'create' | 'createUnique' | 'change' | 'issue' | 'record' | 'accumulate' | 'at' | 'over'>;
1156
1215
  /** A derived pack's handler: one operation, by its operationId, over the contract's context. */
1157
1216
  export type Handler = (ctx: HandlerContext) => Promise<Response>;
1158
- /** The context the kernel opens for a call (a handler's, a door's, a screen's). Kernel-internal: packs are given it. */
1159
1217
  export declare function contextFor(m: DerivedManifest, call: DerivedCall, scope: CoreScope): Promise<SemanticsContext>;
1160
1218
  /** A pack's semantics handlers as the dispatch's handlers. */
1161
1219
  export declare function bindSemantics(m: DerivedManifest, handlers: Record<string, Semantics>, scope?: CoreScope): Record<string, DerivedHandler>;
1220
+ /** The first of a request's form parameters whose value is not one its spec lists (DerivedOperation.enums), with the
1221
+ * bracket name a form gives it (`spending_controls[spending_limits][0][interval]`) and the values it takes. */
1222
+ export declare function valueOutside(params: Record<string, unknown>, enums: Readonly<Record<string, ReadonlyArray<string>>>): {
1223
+ param: string;
1224
+ values: ReadonlyArray<string>;
1225
+ } | undefined;
1226
+ /** Whether a request's parameters ask one of the operation's undemanded variants (DerivedManifest.variants). */
1227
+ export declare function variantAsked(params: Record<string, unknown>, variants: readonly string[], headers?: Headers): boolean;
1162
1228
  /** The derived core as the dispatch's core: it owns every operation on a resource the manifest declares. */
1163
1229
  export declare function coreFor(m: DerivedManifest, scope?: CoreScope): {
1164
1230
  owns: (o: DerivedOperation) => boolean;
@@ -22,7 +22,7 @@ import { MACHINE_POOL, poolFor, guardedMachinePool } from "./machines.js";
22
22
  import { askApplication, deliverEvents } from "./events.js";
23
23
  import { handlerCrypto, hmac, ksuidFrom, lettersFrom, sha256, uuidFrom } from "./signing.js";
24
24
  import { hashFieldValue } from "./hash.js";
25
- import { readResourceBlob, removeResourceBlob, writeResourceBlob } from "./resource-blob.js";
25
+ import { readResourceBlob, readResourceBlobRange, removeResourceBlob, resourceBlobSize, writeResourceBlob, writeResourceBlobStream } from "./resource-blob.js";
26
26
  import { managedDatabase, unboundDatabase } from "./managed-database.js";
27
27
  import { redisServiceTimes } from "./redis/service-times.js";
28
28
  import { execRedisRun } from "./redis/engine.js";
@@ -73,12 +73,13 @@ export function strictFields(m, operation, body) {
73
73
  if (!field)
74
74
  return refuse(strict.unknown, { name });
75
75
  const { type } = field;
76
- const fits = value === null
76
+ const fits = (value === null && !(type === 'array' && strict.arrays?.[name]))
77
77
  || (type === 'boolean' && typeof value === 'boolean')
78
78
  || (type === 'integer' && isExactIntegerValue(value))
79
79
  || (type === 'number' && (typeof value === 'number' || isExactIntegerValue(value)))
80
80
  || (type === 'string' && typeof value === 'string')
81
- || !['boolean', 'integer', 'number', 'string'].includes(type);
81
+ || (type === 'array' && Array.isArray(value) && (!strict.arrays?.[name] || value.every(item => typeof item === strict.arrays[name])))
82
+ || !['boolean', 'integer', 'number', 'string', 'array'].includes(type);
82
83
  if (!fits)
83
84
  return refuse(strict.type, { name, type: type === 'integer' ? 'an integer' : `a ${type}` });
84
85
  if ((typeof value === 'number' || isExactIntegerValue(value)) && ((field.minimum !== undefined && compareExactNumbers(value, field.minimum) < 0) || (field.maximum !== undefined && compareExactNumbers(value, field.maximum) > 0))) {
@@ -303,13 +304,20 @@ function parentField(row, path) {
303
304
  value = value && typeof value === 'object' ? value[key] : undefined;
304
305
  return value;
305
306
  }
306
- function missingParent(m, resource, call, root) {
307
- const parent = m.resources[resource]?.parent;
307
+ function parentOfCall(decl, call) {
308
+ const parent = decl?.parent;
309
+ const names = parent?.params ?? (parent?.param === undefined ? [] : [parent.param]);
310
+ return parent?.whenNamed && names.length && !names.some((name) => Object.hasOwn(call.params, name))
311
+ ? undefined : parent;
312
+ }
313
+ function missingParent(m, resource, call, root, tenant) {
314
+ const parent = parentOfCall(m.resources[resource], call);
308
315
  if (!parent)
309
316
  return undefined;
310
317
  const id = parentValue(parent, call.params);
311
318
  const where = Object.entries(parent.where ?? {}).map(([field, template]) => [field, String(fill(template, call.params))]);
312
- const found = (r) => (where.length ? where.every(([field, v]) => String(parentField(r, field)) === v) : r.id === id);
319
+ const found = (r) => (where.length ? where.every(([field, v]) => String(parentField(r, field)) === v) : r.id === id)
320
+ && (!m.tenant || m.resources[parent.resource]?.shared || r[m.tenant.field] === tenant);
313
321
  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));
314
322
  }
315
323
  /** The vendor's view of a stored subject: its own fields, bookkeeping (`_`) left out. */
@@ -692,7 +700,11 @@ function recordedInput(m, call, merged) {
692
700
  const id = call.operation.id;
693
701
  const sent = sentOf.get(call);
694
702
  const activity = call.request.headers.get('x-volter-world-activity') === 'provisioning' ? { activity: 'provisioning' } : {};
695
- 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 };
703
+ const door = m.doors?.find(d => d.id === id);
704
+ const body = sent?.body ?? merged;
705
+ const safeBody = door?.redactBody?.length && body && typeof body === 'object' && !Array.isArray(body)
706
+ ? Object.fromEntries(Object.entries(body).filter(([key]) => !door.redactBody.includes(key))) : body;
707
+ 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 };
696
708
  if (id === 'graphql') {
697
709
  const b = (sent?.body ?? {});
698
710
  return { ...activity, graphql: { query: b.query, variables: b.variables ?? {}, ...(b.operationName ? { operationName: b.operationName } : {}) }, ...(sent?.lane ? { lane: sent.lane } : {}), headers: sent?.headers ?? {} };
@@ -711,28 +723,27 @@ const scopeOfCall = new WeakMap();
711
723
  const requestOfCall = new WeakMap();
712
724
  async function writeDetailed(m, call, resource, id, fields, operation, params, root, occurredAt, checks) {
713
725
  const before = stateBeforeWrite(m, resource, id, root);
726
+ const prior = counted(m, resource) ? stored(m, resource, root).find((r) => r.id === id) : undefined;
714
727
  const withFiles = async (input) => { const files = input.operationId ? await keptFiles(m, call, root) : undefined; return files ? { ...input, files } : input; };
715
728
  const { resource: row, result } = await applyTwinWrite(m.service,
716
- // who made it: the login the pack's identity reads off the request, when it declares one, so a subject's
717
- // history can say who did each thing
718
729
  // 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
719
730
  inVendorMove()
720
731
  ? { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: { clock: true }, occurredAt, actor: { kind: 'system' } }
721
- : { 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) } : {}) } }, root);
732
+ : { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: await withFiles(recordedInput(m, call, params)), occurredAt, actor: { kind: 'agent' } }, root);
722
733
  observeStateWrite(m, result, before, root, checks);
723
- return finishWrite(m, call, resource, row, result, operation, params, root, occurredAt);
734
+ return finishWrite(m, call, resource, row, result, operation, params, root, occurredAt, prior && { ...prior });
724
735
  }
725
- async function atomicWrite(m, call, resource, decide, operation, params, root, occurredAt, unchangedId, checks) {
726
- const input = recordedInput(m, call, params);
727
- const files = input.operationId ? await keptFiles(m, call, root) : undefined;
736
+ async function atomicWrite(m, call, resource, decide, operation, params, root, occurredAt, unchangedId, checks, omitInput = false) {
737
+ const input = omitInput ? undefined : recordedInput(m, call, params);
738
+ const files = input?.operationId ? await keptFiles(m, call, root) : undefined;
728
739
  let before;
729
740
  const committed = await applyTwinWriteAtomic(m.service, (snapshot) => {
730
741
  const selected = decide(snapshot);
731
- if (selected && transitionObserver)
742
+ if (selected && (transitionObserver || counted(m, resource)))
732
743
  before = snapshot.find((r) => r.type === storedType(m, resource) && r.id === selected.id);
733
744
  return selected ? { kind: 'write', value: selected.id, write: {
734
745
  operation, subjectType: storedType(m, resource), subjectId: selected.id, fields: selected.fields,
735
- input: files ? { ...input, files } : input, occurredAt, actor: { kind: 'agent', ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) },
746
+ ...(input ? { input: files ? { ...input, files } : input } : {}), occurredAt, actor: { kind: 'agent', ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) },
736
747
  } } : { kind: 'skip', value: undefined };
737
748
  }, root);
738
749
  if (!committed.result || !committed.value) {
@@ -741,10 +752,10 @@ async function atomicWrite(m, call, resource, decide, operation, params, root, o
741
752
  }
742
753
  observeStateWrite(m, committed.result, before, root, checks);
743
754
  const row = resourcesOfType(m.service, storedType(m, resource), root).find((r) => r.id === resolveSubjectId(m.service, storedType(m, resource), committed.value, root));
744
- return (await finishWrite(m, call, resource, row, committed.result, operation, params, root, occurredAt)).body;
755
+ return (await finishWrite(m, call, resource, row, committed.result, operation, params, root, occurredAt, before)).body;
745
756
  }
746
- async function finishWrite(m, call, resource, row, result, operation, params, root, occurredAt) {
747
- await keepCounts(m, call, resource, row, params, root, occurredAt);
757
+ async function finishWrite(m, call, resource, row, result, operation, params, root, occurredAt, prior) {
758
+ await keepCounts(m, call, resource, row, params, root, occurredAt, prior);
748
759
  const body = view(m, resource, row);
749
760
  // the write's own context for its hooks, in the scope the call was served in (a walk's application answers, a tenant,
750
761
  // the managed database), at the write's moment
@@ -766,27 +777,30 @@ async function finishWrite(m, call, resource, row, result, operation, params, ro
766
777
  }
767
778
  /** The declared counts a written subject is a child in (a resource's `counts`), each kept on its parent: written when
768
779
  * the count moved, through the write path, so the parent's history says when. */
769
- async function keepCounts(m, call, resource, row, params, root, occurredAt) {
780
+ async function keepCounts(m, call, resource, row, params, root, occurredAt, prior) {
770
781
  for (const [parent, decl] of Object.entries(m.resources)) {
771
782
  for (const [field, c] of Object.entries(decl.counts ?? {})) {
772
783
  if (storedType(m, c.of) !== storedType(m, resource))
773
784
  continue;
774
- const parentId = parentField(row, c.by);
775
- if (parentId === undefined || parentId === null)
776
- continue;
777
- const pid = String(parentId);
778
- const held = stored(m, parent, root).find((r) => String(c.parentKey ? ownFields(r)[c.parentKey] : r.id) === pid);
779
- if (!held)
780
- continue;
781
- const n = stored(m, c.of, root).filter((r) => {
782
- const x = r;
783
- 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);
784
- }).length + (c.plus ?? 0);
785
- if (held[field] !== n)
786
- await writeDetailed(m, call, parent, String(held.id), { [field]: n }, `${storedType(m, parent)}.counted`, params, root, occurredAt);
785
+ // the parent the subject is under now, and the one it left when the write moved it: both counts changed
786
+ const pids = new Set([parentField(row, c.by), prior === undefined ? undefined : parentField(prior, c.by)].filter((v) => v !== undefined && v !== null).map(String));
787
+ for (const pid of pids) {
788
+ const held = stored(m, parent, root).find((r) => String(c.parentKey ? ownFields(r)[c.parentKey] : r.id) === pid);
789
+ if (!held)
790
+ continue;
791
+ const n = stored(m, c.of, root).filter((r) => {
792
+ const x = r;
793
+ 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);
794
+ }).length + (c.plus ?? 0);
795
+ if (held[field] !== n)
796
+ await writeDetailed(m, call, parent, String(held.id), { [field]: n }, `${storedType(m, parent)}.counted`, params, root, occurredAt);
797
+ }
787
798
  }
788
799
  }
789
800
  }
801
+ /** Whether any resource counts this one (`counts`): only then does a write read the subject as it stood, for the parent
802
+ * it may leave. */
803
+ const counted = (m, resource) => Object.values(m.resources).some((d) => Object.values(d.counts ?? {}).some((c) => storedType(m, c.of) === storedType(m, resource)));
790
804
  /** An answer in the vendor's success envelope, under the operation's key, when the vendor has one. */
791
805
  function envelope(m, op, body) {
792
806
  return m.success && op.answers?.key ? { ...m.success, [op.answers.key]: body } : body;
@@ -1133,16 +1147,16 @@ export async function serveCore(m, call, scope = {}) {
1133
1147
  // the body is read from a copy, so the call's own request is still unread for the write hook's context
1134
1148
  const params = await boundaryParams(m, { ...call, request: call.request.clone() }, root);
1135
1149
  const idParam = subjectOf(m, resource, call);
1136
- const orphan = missingParent(m, resource, call, root);
1137
- if (orphan)
1138
- return { served: orphan };
1139
- const parent = decl.parent;
1150
+ const parent = parentOfCall(decl, call);
1140
1151
  const parentId = parent ? parentValue(parent, call.params) : undefined;
1141
1152
  // the caller's tenant, when the vendor has tenancy and the resource is not shared by every tenant
1142
1153
  const tenantField = m.tenant && !decl.shared ? m.tenant.field : undefined;
1143
1154
  const tenant = tenantField && scope.tenant ? await scope.tenant(await contextFor(m, { ...call, request: call.request.clone() }, scope)) : undefined;
1144
1155
  if (tenantField && tenant === undefined)
1145
1156
  return { unmodeled: `the caller names no tenant (semantics/tenant.ts) for ${resource}` };
1157
+ const orphan = missingParent(m, resource, call, root, tenant);
1158
+ if (orphan)
1159
+ return { served: orphan };
1146
1160
  const underParent = (r) => (!parent || r[parent.field] === parentId)
1147
1161
  && (!tenantField || r[tenantField] === tenant);
1148
1162
  const paths = expandPaths(m, params);
@@ -1226,6 +1240,10 @@ export async function serveCore(m, call, scope = {}) {
1226
1240
  const requested = op.class === 'update' ? data[field] : undefined;
1227
1241
  if (op.class === 'update' && requested === undefined)
1228
1242
  continue;
1243
+ if (op.class === 'update' && sdecl.unchanged === 'allow' && stateOf(sdecl, current[field]) === stateOf(sdecl, requested)) {
1244
+ changes[field] = requested;
1245
+ continue;
1246
+ }
1229
1247
  const { move, refusal } = transitionFor(field, sdecl, op.id, current[field], requested, hit.id);
1230
1248
  if (refusal)
1231
1249
  return { served: vendorError(m, refusal) };
@@ -1286,6 +1304,12 @@ export function sse(m, events, operationId) {
1286
1304
  return new Response(body, { status: 200, headers: { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-cache' } });
1287
1305
  }
1288
1306
  /** The context the kernel opens for a call (a handler's, a door's, a screen's). Kernel-internal: packs are given it. */
1307
+ /** A media type whose body the core reads for a context (and records with a write): JSON, a form, multipart, text,
1308
+ * or none named. */
1309
+ const coreReadsBody = (type) => {
1310
+ const media = (type ?? '').split(';')[0].trim().toLowerCase();
1311
+ return media === '' || media === 'application/json' || media.endsWith('+json') || media === 'application/x-www-form-urlencoded' || media.startsWith('multipart/') || media.startsWith('text/');
1312
+ };
1289
1313
  export async function contextFor(m, call, scope) {
1290
1314
  const root = scope.root;
1291
1315
  const asAsked = call;
@@ -1295,8 +1319,10 @@ export async function contextFor(m, call, scope) {
1295
1319
  // the guard's parse when it made one (taken once), else this context's own
1296
1320
  // a body that is its payload's bytes (S3's PutObject) is never read as JSON, whatever its label
1297
1321
  const payload = bytesPayload(call.operation);
1298
- const parsed = payload ? undefined : takeParsedBody(call.request);
1299
- const text = await bodyTextOf(call.request.clone()).catch(() => '');
1322
+ // a screen's body the core does not read (an upload's bytes) is left to its handler, unread (DerivedCall.streamed)
1323
+ const opaque = call.streamed === true && !coreReadsBody(call.request.headers.get('content-type'));
1324
+ const parsed = payload || opaque ? undefined : takeParsedBody(call.request);
1325
+ const text = opaque ? '' : await bodyTextOf(call.request.clone()).catch(() => '');
1300
1326
  let body = parsed?.value;
1301
1327
  if (!parsed) {
1302
1328
  if (payload)
@@ -1359,7 +1385,6 @@ export async function contextFor(m, call, scope) {
1359
1385
  return { store: new GitObjectStore(getActiveBlobStore(), `${plane}/repos/${name}`), refs: new GitRefs(getActiveWorldStore(), `${plane}/repos/${name}`) };
1360
1386
  },
1361
1387
  mail: (route, mail, headers) => sendMail(route, mail, headers),
1362
- actor: m.identity ? m.identity(call.request.headers.get('authorization'), root) : undefined,
1363
1388
  now: () => now(m, at),
1364
1389
  render: (row, resource) => {
1365
1390
  const raw = rawResources.has(row);
@@ -1400,11 +1425,17 @@ export async function contextFor(m, call, scope) {
1400
1425
  record: async (type, fields, id, operation) => {
1401
1426
  if (!type.startsWith('_'))
1402
1427
  throw new Error(`semantics: ${type} is not a bookkeeping type (bookkeeping types start with _)`);
1428
+ // a bookkeeping subject a resource counts (`counts` naming it: sentry's issue users) keeps that count as any
1429
+ // child does, the parent it left recounted too
1430
+ const keeps = counted(m, type);
1403
1431
  if (id !== undefined) {
1404
1432
  const before = operation ? stateBeforeWrite(m, type, id, root) : undefined;
1405
- 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);
1433
+ const prior = keeps ? resourcesOfType(m.service, type, root).find((r) => r.id === id) : undefined;
1434
+ 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);
1406
1435
  if (operation)
1407
1436
  observeStateWrite(m, result, before, root, stateChecks);
1437
+ if (keeps)
1438
+ await keepCounts(m, call, type, row, params, root, at, prior && { ...prior });
1408
1439
  return id;
1409
1440
  }
1410
1441
  const { value, result } = await applyTwinWriteAtomic(m.service, (resources) => {
@@ -1425,6 +1456,9 @@ export async function contextFor(m, call, scope) {
1425
1456
  }, root, () => resourcesOfType(m.service, type, root));
1426
1457
  if (operation && result)
1427
1458
  observeStateWrite(m, result, undefined, root, stateChecks);
1459
+ const made = keeps && value !== undefined ? resourcesOfType(m.service, type, root).find((r) => r.id === value) : undefined;
1460
+ if (made)
1461
+ await keepCounts(m, call, type, made, params, root, at);
1428
1462
  return value;
1429
1463
  },
1430
1464
  get machines() {
@@ -1437,7 +1471,9 @@ export async function contextFor(m, call, scope) {
1437
1471
  const id = mintId(m, resource, root, at, snapshot);
1438
1472
  return { id, fields: typeof fields === 'function' ? fields(id) : fields };
1439
1473
  }, operation, params, root, at, undefined, stateChecks).then((row) => row),
1440
- createUnique: async (resource, fields, operation) => {
1474
+ createUnique: async (resource, fields, operation, options) => {
1475
+ if (options?.input === 'omit' && !resource.startsWith('_'))
1476
+ throw new Error('only bookkeeping writes may omit request input');
1441
1477
  const rules = m.resources[resource]?.unique;
1442
1478
  if (!rules?.length)
1443
1479
  throw new Error('createUnique requires declared unique keys');
@@ -1451,16 +1487,20 @@ export async function contextFor(m, call, scope) {
1451
1487
  return undefined;
1452
1488
  }
1453
1489
  return { id, fields: selected };
1454
- }, operation, params, root, at, undefined, stateChecks);
1490
+ }, operation, params, root, at, undefined, stateChecks, options?.input === 'omit');
1455
1491
  return existing ? { created: false, row: existing } : { created: true, row: row };
1456
1492
  },
1457
- change: (resource, id, decide, operation) => atomicWrite(m, call, resource, (snapshot) => {
1458
- const row = snapshot.find((r) => r.type === storedType(m, resource) && r.id === id && r.deleted !== true);
1459
- if (!row)
1460
- return undefined;
1461
- const fields = decide(row);
1462
- return fields === undefined ? undefined : { id, fields };
1463
- }, operation, params, root, at, id, stateChecks),
1493
+ change: (resource, id, decide, operation, options) => {
1494
+ if (options?.input === 'omit' && !resource.startsWith('_'))
1495
+ throw new Error('only bookkeeping writes may omit request input');
1496
+ return atomicWrite(m, call, resource, (snapshot) => {
1497
+ const row = snapshot.find((r) => r.type === storedType(m, resource) && r.id === id && r.deleted !== true);
1498
+ if (!row)
1499
+ return undefined;
1500
+ const fields = decide(row);
1501
+ return fields === undefined ? undefined : { id, fields };
1502
+ }, operation, params, root, at, id, stateChecks, options?.input === 'omit');
1503
+ },
1464
1504
  write: (resource, id, fields, operation) => write(m, call, resource, id, fields, operation, params, root, at, stateChecks),
1465
1505
  accumulate: async (resource, id, deltas, operation) => {
1466
1506
  if (!resource.startsWith('_') || !m.resources[resource])
@@ -1546,8 +1586,11 @@ export async function contextFor(m, call, scope) {
1546
1586
  live: liveAccess({ rows: (type) => resourcesOfType(m.service, type, root), blob: (key) => readResourceBlob(m.service, key, root) }, call.request),
1547
1587
  blobs: {
1548
1588
  put: (key, bytes) => writeResourceBlob(m.service, key, bytes, root),
1589
+ putStream: (key, body, accept) => writeResourceBlobStream(m.service, key, body, accept, root),
1549
1590
  get: (key) => readResourceBlob(m.service, key, root),
1550
1591
  remove: (key) => removeResourceBlob(m.service, key, root),
1592
+ size: (key) => resourceBlobSize(m.service, key, root),
1593
+ range: (key, start, endInclusive) => readResourceBlobRange(m.service, key, start, endInclusive, root),
1551
1594
  },
1552
1595
  merge: (resource, prior, next) => mergeField(m.resources[resource]?.update, prior, next),
1553
1596
  conflict: (resource, fields, except) => {
@@ -1702,7 +1745,105 @@ async function writeGroup(m, call, writes, params, root, occurredAt) {
1702
1745
  /** A pack's semantics handlers as the dispatch's handlers. */
1703
1746
  export function bindSemantics(m, handlers, scope = {}) {
1704
1747
  prepareViewOmissions(m);
1705
- return Object.fromEntries(Object.entries(handlers).map(([id, h]) => [id, async (call) => { const ctx = await contextFor(m, call, scope); redisServiceTimes.lap('6c the handler\'s own context opened'); return h(ctx); }]));
1748
+ return Object.fromEntries(Object.entries(handlers).map(([id, h]) => [id, async (call) => {
1749
+ const ctx = await contextFor(m, call, scope);
1750
+ redisServiceTimes.lap('6c the handler\'s own context opened');
1751
+ // an operation's own variants, else the ones every operation shares (`'*'`)
1752
+ const variants = m.variants?.[id] ?? m.variants?.['*'];
1753
+ if (variants && variantAsked(ctx.params, variants, call.request.headers))
1754
+ return variantGap(m, call.request);
1755
+ const outside = enumRefused(m, call, ctx.params);
1756
+ return outside ?? h(ctx);
1757
+ }]));
1758
+ }
1759
+ /** The first of a request's form parameters whose value is not one its spec lists (DerivedOperation.enums), with the
1760
+ * bracket name a form gives it (`spending_controls[spending_limits][0][interval]`) and the values it takes. */
1761
+ export function valueOutside(params, enums) {
1762
+ const at = (value, path, name, values) => {
1763
+ if (value === undefined || value === null)
1764
+ return undefined;
1765
+ if (!path.length)
1766
+ return typeof value === 'object' || values.includes(String(value)) ? undefined : { param: name, values };
1767
+ const [head, ...rest] = path;
1768
+ if (head === '[]') {
1769
+ const items = Array.isArray(value) ? value.map((v, i) => [String(i), v]) : value && typeof value === 'object' ? Object.entries(value) : [];
1770
+ for (const [i, v] of items) {
1771
+ const found = at(v, rest, `${name}[${i}]`, values);
1772
+ if (found)
1773
+ return found;
1774
+ }
1775
+ return undefined;
1776
+ }
1777
+ return value && typeof value === 'object' && !Array.isArray(value) ? at(value[head], rest, `${name}[${head}]`, values) : undefined;
1778
+ };
1779
+ for (const [path, values] of Object.entries(enums)) {
1780
+ const [first, ...rest] = path.split('.');
1781
+ const found = at(params[first], rest, first, values);
1782
+ if (found)
1783
+ return found;
1784
+ }
1785
+ return undefined;
1786
+ }
1787
+ /** The manifest's refusal of a form value outside the operation's spec lists (and the values documented beside
1788
+ * them), unless the request names an API version other than the one the spec is. */
1789
+ function enumRefused(m, call, params) {
1790
+ const refusal = m.body.form?.enumRefusal;
1791
+ if (!refusal || !call.operation.enums)
1792
+ return undefined;
1793
+ const pinned = refusal.version ? call.request.headers.get(refusal.version.header) : null;
1794
+ if (pinned && pinned !== refusal.version.served)
1795
+ return undefined;
1796
+ const documented = refusal.documented?.[call.operation.id] ?? {};
1797
+ const enums = Object.fromEntries(Object.entries(call.operation.enums).map(([path, values]) => [path, [...values, ...(documented[path] ?? [])]]));
1798
+ const outside = valueOutside(params, enums);
1799
+ if (!outside)
1800
+ return undefined;
1801
+ const { documented: _d, version: _v, ...error } = refusal;
1802
+ return vendorError(m, { ...error, param: outside.param, message: error.message.replace(/\{param\}/g, outside.param).replace(/\{values\}/g, oneOfList(outside.values)) });
1803
+ }
1804
+ /** A list of values as a sentence names them: `a, b, or c`. */
1805
+ const oneOfList = (values) => (values.length < 2 ? values.join('') : `${values.slice(0, -1).join(', ')}, or ${values[values.length - 1]}`);
1806
+ /** Whether a request's parameters ask one of the operation's undemanded variants (DerivedManifest.variants). */
1807
+ export function variantAsked(params, variants, headers) {
1808
+ const valuesAt = (value, path) => {
1809
+ if (!path.length)
1810
+ return value === undefined ? [] : [value];
1811
+ const [head, ...rest] = path;
1812
+ // `!name` is the field left out: the value itself where it has none (`items[].!id`, an item naming no id)
1813
+ if (head.startsWith('!'))
1814
+ return value && typeof value === 'object' && value[head.slice(1)] === undefined ? [value] : [];
1815
+ const list = head.endsWith('[]');
1816
+ const key = list ? head.slice(0, -2) : head;
1817
+ const next = value && typeof value === 'object' ? value[key] : undefined;
1818
+ if (!list)
1819
+ return valuesAt(next, rest);
1820
+ const items = Array.isArray(next) ? next : next && typeof next === 'object' ? Object.values(next) : [];
1821
+ return items.flatMap((item) => valuesAt(item, rest));
1822
+ };
1823
+ // a variant joined with `&` is its parts asked together (`trial_end=now&items`): the combination, not either alone;
1824
+ // a part led by `!` is a parameter left out (`items[].price&!items[].id`, an item added rather than changed)
1825
+ return variants.some((variant) => variant.split('&').every((part) => {
1826
+ const absent = part.startsWith('!');
1827
+ // `name~=value` is one entry of a comma-separated value (`tweet.fields~=geo`, X's fields lists); `name=value` the whole
1828
+ const body = absent ? part.slice(1) : part;
1829
+ const listed = body.includes('~=');
1830
+ const [path, wanted] = listed ? body.split('~=') : body.split('=');
1831
+ // a part led by `@` names a request header (`@stripe-account&application_fee_amount`, a charge made on a connected
1832
+ // account with the platform's fee)
1833
+ const header = path.startsWith('@') ? headers?.get(path.slice(1)) : undefined;
1834
+ // a parameter whose own name holds a dot (X's `tweet.fields`) is read by that name before the path is walked
1835
+ const found = path.startsWith('@') ? (header === null || header === undefined ? [] : [header])
1836
+ : Object.hasOwn(params, path) ? (params[path] === undefined ? [] : [params[path]]) : valuesAt(params, path.split('.'));
1837
+ const asked = wanted === undefined ? found.length > 0
1838
+ : found.some((value) => (listed ? String(value).split(',').map((entry) => entry.trim()).includes(wanted) : String(value) === wanted));
1839
+ return absent ? !asked : asked;
1840
+ }));
1841
+ }
1842
+ /** The manifest's gap, as a variant is answered: the unknown-request refusal (else its not-found). */
1843
+ function variantGap(m, request) {
1844
+ const nf = m.notFound;
1845
+ const gap = m.gap ?? { status: nf.status, message: nf.message, ...(nf.code !== undefined ? { code: nf.code } : {}), ...(nf.kind !== undefined ? { kind: nf.kind } : {}) };
1846
+ return vendorError(m, { ...gap, message: gap.message.replace(/\{method\}/g, request.method).replace(/\{path\}/g, new URL(request.url).pathname) });
1706
1847
  }
1707
1848
  /** The derived core as the dispatch's core: it owns every operation on a resource the manifest declares. */
1708
1849
  export function coreFor(m, scope = {}) {