@volter/world-core 3.0.5 → 3.0.6

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.
@@ -124,6 +124,33 @@ function unitOf(units, input) {
124
124
  ?? units.find((u) => u.surface.operations.some((o) => o.id === id))
125
125
  ?? (input.graphql ? units[0] : undefined);
126
126
  }
127
+ /** The account a root is, before a perform sends a path naming the World's ("The account a root is"): the World's
128
+ * account its own doors made, unadopted, adopts the one account the root's credential lists. Read once per deploy (an
129
+ * adopted account is not asked again); a list the vendor refuses, or one naming several accounts, adopts none. */
130
+ async function adoptAccount(units, execute, ctx) {
131
+ if (ctx.service === undefined)
132
+ return;
133
+ for (const u of units) {
134
+ const m = u.manifest;
135
+ const decl = m.account ? m.resources[m.account] : undefined;
136
+ if (!m.account || !decl)
137
+ continue;
138
+ const type = storedType(m, m.account);
139
+ const local = readTree(ctx.service, ctx.root).filter((r) => r.type === type && r.deleted !== true && ctx.resolve(type, String(r.id)) === String(r.id));
140
+ const scope = decl.refresh;
141
+ const op = scope && 'list' in scope ? u.surface.operations.find((o) => o.id === scope.list) : undefined;
142
+ if (!local.length || !op)
143
+ continue;
144
+ const answer = await execute({ method: op.method.toUpperCase(), path: `${op.basePath ?? u.surface.basePath ?? ''}${op.path}`, ...(u.lane ? { lane: u.lane } : {}) });
145
+ if (answer.status >= 400)
146
+ continue;
147
+ const items = itemsOf(op, parse(answer));
148
+ const id = items.length === 1 ? storedIdOf(decl, items[0]) : undefined;
149
+ if (id === undefined || local.some((l) => String(l.id) === id))
150
+ continue;
151
+ observeResources(ctx.service, [{ type, id, fields: items[0], adopts: String(local[0].id) }], ctx.root !== undefined ? { root: ctx.root } : {});
152
+ }
153
+ }
127
154
  function performOf(units) {
128
155
  // the request a group's first entry recorded (or one a protocol hook changed), sent to the vendor
129
156
  const derived = async (execute, group, ctx, input) => {
@@ -190,7 +217,7 @@ function performOf(units) {
190
217
  // a retried deploy makes nothing twice, and two Worlds deploying into one account never collide
191
218
  if (m.idempotency)
192
219
  headers[m.idempotency.header] = hashFieldValue({ credential: ctx.credential ?? '', entry: first.id });
193
- const answer = await execute({ method, path, headers, ...(sent.body !== undefined ? { body: sent.body } : {}) });
220
+ const answer = await execute({ method, path, headers, ...(sent.body !== undefined ? { body: sent.body } : {}), ...(unit.lane ? { lane: unit.lane } : {}) });
194
221
  const parsed = parse(answer);
195
222
  const shape = refusalShape(m);
196
223
  const refused = answer.status >= 400 || (shape.length > 0 && shape.every(([p, v]) => at(parsed, p) === v));
@@ -231,6 +258,7 @@ function performOf(units) {
231
258
  return { externalId, actionId: primary.id, data: obj, ...(also.length ? { also } : {}) };
232
259
  };
233
260
  return async (execute, group, ctx) => {
261
+ await adoptAccount(units, execute, ctx);
234
262
  const input = (group[0].input ?? {});
235
263
  const unit = unitOf(units, input);
236
264
  // a protocol one request cannot express is the pack's hook, which composes the derived perform ("Protocols a
@@ -297,7 +325,8 @@ function refreshOf(units, service) {
297
325
  return async (execute, opts) => {
298
326
  const observed = [];
299
327
  const complete = [];
300
- for (const { manifest: m, surface } of units) {
328
+ for (const unit of units) {
329
+ const { manifest: m, surface } = unit;
301
330
  // parents before children, however deep
302
331
  const scoped = Object.entries(m.resources).filter(([, d]) => d.refresh && !('none' in d.refresh));
303
332
  const depth = (r, seen = new Set()) => { const p = m.resources[r]?.parent?.resource; return p && !seen.has(p) ? 1 + depth(p, seen.add(r)) : 0; };
@@ -319,7 +348,7 @@ function refreshOf(units, service) {
319
348
  // a parent the operation names in its query, not its path (Slack's conversations.history `channel`)
320
349
  const byQuery = decl.parent?.param && parent !== undefined && !op.path.includes(`{${decl.parent.param}}`) ? { [decl.parent.param]: String(parent) } : {};
321
350
  const q = new URLSearchParams({ ...page, ...byQuery });
322
- const answer = await execute({ method: op.method.toUpperCase(), path: [...q].length ? `${path}?${q}` : path });
351
+ const answer = await execute({ method: op.method.toUpperCase(), path: [...q].length ? `${path}?${q}` : path, ...(unit.lane ? { lane: unit.lane } : {}) });
323
352
  if (answer.status === 401 || answer.status === 403)
324
353
  throw new Error(`${answer.status}: the vendor refused the root's credential; nothing folded`);
325
354
  if (answer.status === 429)
@@ -12,8 +12,14 @@ import type { HostRule } from './packRegistry.js';
12
12
  * and no sealed credential migrates.
13
13
  */
14
14
  export type TwinAuthStrategy =
15
- /** the key rides in the query string (`?key=…`, `?appid=…`) — no header will do */
15
+ /** A vendor of lanes whose lanes authenticate apart (Cloudflare's API by bearer, its R2 by SigV4): a lane named takes
16
+ * its own strategy, every other call the sealed headers; the request names its lane (`RemoteExecuteRequest.lane`). */
16
17
  {
18
+ in: 'lanes';
19
+ lanes: Record<string, TwinAuthStrategy>;
20
+ }
21
+ /** the key rides in the query string (`?key=…`, `?appid=…`) — no header will do */
22
+ | {
17
23
  in: 'query';
18
24
  name: string;
19
25
  }
@@ -23,7 +29,10 @@ export type TwinAuthStrategy =
23
29
  | {
24
30
  in: 'signature';
25
31
  algorithm: 'aws-sigv4';
26
- scope: (req: {
32
+ scope: {
33
+ region: string;
34
+ service: string;
35
+ } | ((req: {
27
36
  method: string;
28
37
  path: string;
29
38
  host: string;
@@ -32,7 +41,7 @@ export type TwinAuthStrategy =
32
41
  }) => {
33
42
  region: string;
34
43
  service: string;
35
- };
44
+ });
36
45
  }
37
46
  /** a signature computed PER REQUEST over bytes only the pack can canonicalize */
38
47
  | {
@@ -130,6 +130,14 @@ async function awsSigV4Headers(input) {
130
130
  };
131
131
  }
132
132
  export function buildRemoteExecute(origin, credential, auth, vendorHosts, options = {}) {
133
+ if (auth?.in === 'lanes') {
134
+ // each lane's strategy over the same origin and credential, the budget charged once, around them all
135
+ const own = { ...options, vendor: undefined };
136
+ const byLane = new Map(Object.entries(auth.lanes).map(([lane, strategy]) => [lane, buildRemoteExecute(origin, credential, strategy, vendorHosts, own)]));
137
+ const headers = buildRemoteExecute(origin, credential, undefined, vendorHosts, own);
138
+ const execute = (request) => (byLane.get(request.lane ?? '') ?? headers)(request);
139
+ return options.vendor === undefined ? execute : budgeted(options.vendor, execute, options.custody?.key ?? JSON.stringify(credential.headers));
140
+ }
133
141
  const execute = auth?.in === 'exchange' ? exchangeExecute(origin, credential, auth, vendorHosts, options.custody) : direct(origin, credential, auth, vendorHosts);
134
142
  return options.vendor === undefined ? execute : budgeted(options.vendor, execute, options.custody?.key ?? JSON.stringify(credential.headers));
135
143
  }
@@ -234,7 +242,7 @@ function direct(origin, credential, auth, vendorHosts) {
234
242
  throw new Error('remote execute: the aws-sigv4 credential strategy needs `keyId` on the sealed credential, and it is absent');
235
243
  }
236
244
  // the HOST goes with it: an AWS endpoint names its own region, and only the pack knows how
237
- const { region, service } = auth.scope({ method, path, host: target.host, headers: Object.fromEntries(outbound.entries()), ...(body === undefined ? {} : { body }) });
245
+ const { region, service } = typeof auth.scope === 'function' ? auth.scope({ method, path, host: target.host, headers: Object.fromEntries(outbound.entries()), ...(body === undefined ? {} : { body }) }) : auth.scope;
238
246
  const signed = await awsSigV4Headers({ method, url: target, ...(body === undefined ? {} : { body }), keyId, secret, region, service, headers: outbound, at: new Date() });
239
247
  for (const [name, value] of Object.entries(signed))
240
248
  outbound.set(name, value);
@@ -16,6 +16,8 @@ export type RemoteExecuteRequest = {
16
16
  * that its upload calls carry instead of the API token), on the root's own origin: it is sent as the request's
17
17
  * Authorization and the sealed credential does not go with it. */
18
18
  issued?: string;
19
+ /** the lane of a vendor of lanes the request is (its strategy, when its lanes authenticate apart) */
20
+ lane?: string;
19
21
  };
20
22
  export type RemoteExecuteResponse = {
21
23
  status: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-core",
3
- "version": "3.0.5",
3
+ "version": "3.0.6",
4
4
  "description": "The kernel of Volter World: one log per twin, branches as pointers, checkpoints, the fold that keeps a twin current, the head that performs a write against the vendor, references, and the git plane. A twin package builds on it; the runtime serves it.",
5
5
  "keywords": [
6
6
  "twin",
@@ -111,6 +111,29 @@ function unitOf(units: Unit[], input: Row): Unit | undefined {
111
111
  ?? (input.graphql ? units[0] : undefined);
112
112
  }
113
113
 
114
+ /** The account a root is, before a perform sends a path naming the World's ("The account a root is"): the World's
115
+ * account its own doors made, unadopted, adopts the one account the root's credential lists. Read once per deploy (an
116
+ * adopted account is not asked again); a list the vendor refuses, or one naming several accounts, adopts none. */
117
+ async function adoptAccount(units: Unit[], execute: RemoteExecute, ctx: PerformContext): Promise<void> {
118
+ if (ctx.service === undefined) return;
119
+ for (const u of units) {
120
+ const m = u.manifest;
121
+ const decl = m.account ? m.resources[m.account] : undefined;
122
+ if (!m.account || !decl) continue;
123
+ const type = storedType(m, m.account);
124
+ const local = (readTree(ctx.service, ctx.root) as Row[]).filter((r) => r.type === type && r.deleted !== true && ctx.resolve(type, String(r.id)) === String(r.id));
125
+ const scope = decl.refresh;
126
+ const op = scope && 'list' in scope ? u.surface.operations.find((o) => o.id === scope.list) : undefined;
127
+ if (!local.length || !op) continue;
128
+ const answer = await execute({ method: op.method.toUpperCase(), path: `${op.basePath ?? u.surface.basePath ?? ''}${op.path}`, ...(u.lane ? { lane: u.lane } : {}) });
129
+ if (answer.status >= 400) continue;
130
+ const items = itemsOf(op, parse(answer));
131
+ const id = items.length === 1 ? storedIdOf(decl, items[0]!) : undefined;
132
+ if (id === undefined || local.some((l) => String(l.id) === id)) continue;
133
+ observeResources(ctx.service, [{ type, id, fields: items[0] as ObservedResource['fields'], adopts: String(local[0]!.id) }], ctx.root !== undefined ? { root: ctx.root } : {});
134
+ }
135
+ }
136
+
114
137
  function performOf(units: Unit[]): PerformAction {
115
138
  // the request a group's first entry recorded (or one a protocol hook changed), sent to the vendor
116
139
  const derived = async (execute: RemoteExecute, group: TwinAction[], ctx: PerformContext, input: Row): Promise<PushOutcome> => {
@@ -159,7 +182,7 @@ function performOf(units: Unit[]): PerformAction {
159
182
  const headers: Record<string, string> = { ...((object(input.headers) ?? {}) as Record<string, string>), ...(sent.type ? { 'content-type': sent.type } : {}) };
160
183
  // a retried deploy makes nothing twice, and two Worlds deploying into one account never collide
161
184
  if (m.idempotency) headers[m.idempotency.header] = hashFieldValue({ credential: ctx.credential ?? '', entry: first.id });
162
- const answer = await execute({ method, path, headers, ...(sent.body !== undefined ? { body: sent.body } : {}) });
185
+ const answer = await execute({ method, path, headers, ...(sent.body !== undefined ? { body: sent.body } : {}), ...(unit.lane ? { lane: unit.lane } : {}) });
163
186
  const parsed = parse(answer);
164
187
  const shape = refusalShape(m);
165
188
  const refused = answer.status >= 400 || (shape.length > 0 && shape.every(([p, v]) => at(parsed, p) === v));
@@ -196,6 +219,7 @@ function performOf(units: Unit[]): PerformAction {
196
219
  return { externalId, actionId: primary.id, data: obj, ...(also.length ? { also } : {}) };
197
220
  };
198
221
  return async (execute: RemoteExecute, group: TwinAction[], ctx: PerformContext): Promise<PushOutcome> => {
222
+ await adoptAccount(units, execute, ctx);
199
223
  const input = (group[0]!.input ?? {}) as Row;
200
224
  const unit = unitOf(units, input);
201
225
  // a protocol one request cannot express is the pack's hook, which composes the derived perform ("Protocols a
@@ -256,7 +280,8 @@ function refreshOf(units: Unit[], service: string): NonNullable<StateSystemAdapt
256
280
  return async (execute, opts) => {
257
281
  const observed: ObservedResource[] = [];
258
282
  const complete: Completed[] = [];
259
- for (const { manifest: m, surface } of units) {
283
+ for (const unit of units) {
284
+ const { manifest: m, surface } = unit;
260
285
  // parents before children, however deep
261
286
  const scoped = Object.entries(m.resources).filter(([, d]) => d.refresh && !('none' in d.refresh));
262
287
  const depth = (r: string, seen = new Set<string>()): number => { const p = m.resources[r]?.parent?.resource; return p && !seen.has(p) ? 1 + depth(p, seen.add(r)) : 0; };
@@ -277,7 +302,7 @@ function refreshOf(units: Unit[], service: string): NonNullable<StateSystemAdapt
277
302
  // a parent the operation names in its query, not its path (Slack's conversations.history `channel`)
278
303
  const byQuery = decl.parent?.param && parent !== undefined && !op.path.includes(`{${decl.parent.param}}`) ? { [decl.parent.param]: String(parent) } : {};
279
304
  const q = new URLSearchParams({ ...page, ...byQuery });
280
- const answer = await execute({ method: op.method.toUpperCase(), path: [...q].length ? `${path}?${q}` : path });
305
+ const answer = await execute({ method: op.method.toUpperCase(), path: [...q].length ? `${path}?${q}` : path, ...(unit.lane ? { lane: unit.lane } : {}) });
281
306
  if (answer.status === 401 || answer.status === 403) throw new Error(`${answer.status}: the vendor refused the root's credential; nothing folded`);
282
307
  if (answer.status === 429) throw new Error(`429: rate limited${answer.headers['retry-after'] ? `, retry after ${answer.headers['retry-after']}s` : ''}`);
283
308
  if (answer.status >= 400) throw new Error(`${answer.status}: ${refusalWords(m, parse(answer))}`);
package/src/executor.ts CHANGED
@@ -21,12 +21,15 @@ import { RateBudget } from './rateBudget.ts';
21
21
  * and no sealed credential migrates.
22
22
  */
23
23
  export type TwinAuthStrategy =
24
+ /** A vendor of lanes whose lanes authenticate apart (Cloudflare's API by bearer, its R2 by SigV4): a lane named takes
25
+ * its own strategy, every other call the sealed headers; the request names its lane (`RemoteExecuteRequest.lane`). */
26
+ | { in: 'lanes'; lanes: Record<string, TwinAuthStrategy> }
24
27
  /** the key rides in the query string (`?key=…`, `?appid=…`) — no header will do */
25
28
  | { in: 'query'; name: string }
26
29
  /** AWS Signature Version 4 — a signature computed per request over the whole canonical request.
27
30
  * `scope` answers which region and service THIS request is for, because a pack may route several
28
31
  * services over one executor (aws routes six); it is pure and is handed no secret. */
29
- | { in: 'signature'; algorithm: 'aws-sigv4'; scope: (req: { method: string; path: string; host: string; headers: Record<string, string>; body?: string | Uint8Array }) => { region: string; service: string } }
32
+ | { in: 'signature'; algorithm: 'aws-sigv4'; scope: { region: string; service: string } | ((req: { method: string; path: string; host: string; headers: Record<string, string>; body?: string | Uint8Array }) => { region: string; service: string }) }
30
33
  /** a signature computed PER REQUEST over bytes only the pack can canonicalize */
31
34
  | {
32
35
  in: 'signature';
@@ -223,6 +226,14 @@ export type CredentialCustody = { key: string; open: () => Promise<CredentialPay
223
226
  * made, refused when the budget is spent, and settled with the vendor's answer (its back-off honoured). */
224
227
  export type RemoteExecuteOptions = { custody?: CredentialCustody; vendor?: string };
225
228
  export function buildRemoteExecute(origin: URL, credential: CredentialPayload, auth?: TwinAuthStrategy, vendorHosts?: readonly HostRule[], options: RemoteExecuteOptions = {}): RemoteExecute {
229
+ if (auth?.in === 'lanes') {
230
+ // each lane's strategy over the same origin and credential, the budget charged once, around them all
231
+ const own = { ...options, vendor: undefined };
232
+ const byLane = new Map(Object.entries(auth.lanes).map(([lane, strategy]) => [lane, buildRemoteExecute(origin, credential, strategy, vendorHosts, own)]));
233
+ const headers = buildRemoteExecute(origin, credential, undefined, vendorHosts, own);
234
+ const execute: RemoteExecute = (request) => (byLane.get(request.lane ?? '') ?? headers)(request);
235
+ return options.vendor === undefined ? execute : budgeted(options.vendor, execute, options.custody?.key ?? JSON.stringify(credential.headers));
236
+ }
226
237
  const execute = auth?.in === 'exchange' ? exchangeExecute(origin, credential, auth, vendorHosts, options.custody) : direct(origin, credential, auth, vendorHosts);
227
238
  return options.vendor === undefined ? execute : budgeted(options.vendor, execute, options.custody?.key ?? JSON.stringify(credential.headers));
228
239
  }
@@ -242,7 +253,7 @@ function budgeted(vendor: string, execute: RemoteExecute, token: string): Remote
242
253
  }
243
254
 
244
255
  /** The call made as the root's credential is applied by the strategy (header replacement when none). */
245
- function direct(origin: URL, credential: CredentialPayload, auth?: Exclude<TwinAuthStrategy, { in: 'exchange' }>, vendorHosts?: readonly HostRule[]): RemoteExecute {
256
+ function direct(origin: URL, credential: CredentialPayload, auth?: Exclude<TwinAuthStrategy, { in: 'exchange' | 'lanes' }>, vendorHosts?: readonly HostRule[]): RemoteExecute {
246
257
  return async ({ method, path, headers, body, responseType, presigned, issued }) => {
247
258
  // A PRESIGNED UPLOAD URL the vendor returned: its own signature is its authorization, so the
248
259
  // sealed credential never goes with it, whatever host it names. A host other than the root's
@@ -314,7 +325,7 @@ function direct(origin: URL, credential: CredentialPayload, auth?: Exclude<TwinA
314
325
  throw new Error('remote execute: the aws-sigv4 credential strategy needs `keyId` on the sealed credential, and it is absent');
315
326
  }
316
327
  // the HOST goes with it: an AWS endpoint names its own region, and only the pack knows how
317
- const { region, service } = auth.scope({ method, path, host: target.host, headers: Object.fromEntries(outbound.entries()), ...(body === undefined ? {} : { body }) });
328
+ const { region, service } = typeof auth.scope === 'function' ? auth.scope({ method, path, host: target.host, headers: Object.fromEntries(outbound.entries()), ...(body === undefined ? {} : { body }) }) : auth.scope;
318
329
  const signed = await awsSigV4Headers({ method, url: target, ...(body === undefined ? {} : { body }), keyId, secret, region, service, headers: outbound, at: new Date() });
319
330
  for (const [name, value] of Object.entries(signed)) outbound.set(name, value);
320
331
  } else {
@@ -20,6 +20,8 @@ export type RemoteExecuteRequest = {
20
20
  * that its upload calls carry instead of the API token), on the root's own origin: it is sent as the request's
21
21
  * Authorization and the sealed credential does not go with it. */
22
22
  issued?: string;
23
+ /** the lane of a vendor of lanes the request is (its strategy, when its lanes authenticate apart) */
24
+ lane?: string;
23
25
  };
24
26
  export type RemoteExecuteResponse = {
25
27
  status: number;