@volter/world-core 3.0.161 → 3.0.163
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.
- package/dist/generated/pack-facts.json +84 -5
- package/dist/inject.cjs +2 -2
- package/dist/src/derived-core.d.ts +44 -3
- package/dist/src/derived-core.js +231 -62
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +3 -1
- package/dist/src/machines.d.ts +4 -16
- package/dist/src/machines.js +10 -44
- package/dist/src/pack-fetch.js +29 -5
- package/dist/src/packRegistry.d.ts +19 -0
- package/dist/src/protobuf.d.ts +4 -0
- package/dist/src/protobuf.js +43 -2
- package/dist/src/request-scope.d.ts +2 -0
- package/dist/src/request-scope.js +9 -0
- package/dist/src/runtime.d.ts +6 -3
- package/dist/src/runtime.js +4 -1
- package/dist/src/serve-http.d.ts +11 -0
- package/dist/src/serve-http.js +132 -0
- package/dist/src/signing.d.ts +30 -7
- package/dist/src/signing.js +32 -16
- package/dist/src/sockets.d.ts +1 -1
- package/dist/src/sockets.js +11 -7
- package/dist/src/world-account-name.d.ts +2 -0
- package/dist/src/world-account-name.js +4 -0
- package/generated/pack-facts.json +84 -5
- package/inject.cjs +2 -2
- package/package.json +1 -1
- package/src/derived-core.ts +248 -70
- package/src/index.ts +5 -2
- package/src/machines.ts +10 -38
- package/src/pack-fetch.ts +27 -5
- package/src/packRegistry.ts +8 -2
- package/src/protobuf.ts +39 -2
- package/src/request-scope.ts +9 -0
- package/src/runtime.ts +8 -3
- package/src/serve-http.ts +111 -0
- package/src/signing.ts +38 -18
- package/src/sockets.ts +12 -9
- package/src/world-account-name.ts +4 -0
package/dist/src/derived-core.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { storageTenant } from "./request-scope.js";
|
|
2
|
+
import { isWorldAccountName } from "./world-account-name.js";
|
|
1
3
|
import { parseExactJson, compareExactNumbers, exactInteger, isExactIntegerValue } from "./exact-json.js";
|
|
2
4
|
// THE DERIVED CORE — what a derived pack does for an operation it declares no handler for
|
|
3
5
|
// (docs/contributing/architecture.md, "Protocol 3"). The generated surface says what the
|
|
@@ -35,6 +37,7 @@ import { vendorFetch } from "./vendor-call.js";
|
|
|
35
37
|
import { packReferences } from "./references.js";
|
|
36
38
|
import { twinPublicBase, withRequestScopes } from "./twin-fetch.js";
|
|
37
39
|
import { inVendorMove, isReadOnlyRequest, runAsVendorMove, writesRefused } from "./request-scope.js";
|
|
40
|
+
import { getPack } from "./packRegistry.js";
|
|
38
41
|
import { bodyTextOf, checkJsonBody, takeParsedBody } from "./derived.js";
|
|
39
42
|
/** What a move to `to` stores in the field: a boolean field's value is a boolean, and a derived field's
|
|
40
43
|
* value is written by the move's effects (a state name is not a timestamp), so it stores nothing itself. */
|
|
@@ -235,7 +238,13 @@ function notFound(manifest, object, id, param) {
|
|
|
235
238
|
return vendorError(manifest, { status: nf.status, message: String(fill(message, { object, id })), ...(code ? { code } : {}), ...(nf.kind ? { kind: nf.kind } : {}), ...(param ? { param } : {}) });
|
|
236
239
|
}
|
|
237
240
|
// ── the tree ─────────────────────────────────────────────────────────────────────────────────
|
|
238
|
-
|
|
241
|
+
const TENANT_TYPE = '::account:';
|
|
242
|
+
const accountScope = (m) => m.tenant?.storage === 'namespaced' ? storageTenant(m.service) : undefined;
|
|
243
|
+
const scopeLabel = (m, label) => { const account = accountScope(m); return account && account !== 'default' ? `account:${account}:${label}` : label; };
|
|
244
|
+
export const storedType = (m, resource) => {
|
|
245
|
+
const base = m.resources[resource]?.storedAs ?? resource, account = accountScope(m);
|
|
246
|
+
return account && account !== 'default' && !m.resources[resource]?.shared ? `${base}${TENANT_TYPE}${encodeURIComponent(account)}` : base;
|
|
247
|
+
};
|
|
239
248
|
// A service's resources by type, folded once per World state (the tree's stamp; any write moves it), not on every read:
|
|
240
249
|
// a read of one resource type projected the service's whole tree, so a pack whose tree also holds many subjects of
|
|
241
250
|
// other types (PlanetScale's SQL rows beside its management records) paid for all of them on each read. A read gets its
|
|
@@ -254,9 +263,15 @@ export function resourcesOfType(service, type, root) {
|
|
|
254
263
|
if (held && delta) {
|
|
255
264
|
// the index keeps the tree's order: a subject updated where it stands stays there, one new to the tree (or made
|
|
256
265
|
// again after a delete) goes to the end in the order the tree holds it
|
|
257
|
-
for (const k of [...delta.removed, ...delta.appended])
|
|
258
|
-
held.
|
|
266
|
+
for (const k of [...delta.removed, ...delta.appended]) {
|
|
267
|
+
const type = held.typeByKey.get(k);
|
|
268
|
+
if (type !== undefined)
|
|
269
|
+
held.byType.get(type)?.delete(k);
|
|
270
|
+
held.typeByKey.delete(k);
|
|
271
|
+
}
|
|
259
272
|
const byKey = new Map(delta.changed.map((r) => [`${r.type}:${r.id}`, r]));
|
|
273
|
+
for (const [k, r] of byKey)
|
|
274
|
+
held.typeByKey.set(k, r.type);
|
|
260
275
|
for (const [k, r] of byKey)
|
|
261
276
|
if (!delta.appended.includes(k))
|
|
262
277
|
(held.byType.get(r.type) ?? held.byType.set(r.type, new Map()).get(r.type)).set(k, r);
|
|
@@ -273,7 +288,7 @@ export function resourcesOfType(service, type, root) {
|
|
|
273
288
|
const byType = new Map();
|
|
274
289
|
for (const r of twinResources(service, root))
|
|
275
290
|
(byType.get(r.type) ?? byType.set(r.type, new Map()).get(r.type)).set(`${r.type}:${r.id}`, r);
|
|
276
|
-
held = { stamp, byType };
|
|
291
|
+
held = { stamp, byType, typeByKey: new Map([...byType].flatMap(([type, rows]) => [...rows.keys()].map(key => [key, type]))) };
|
|
277
292
|
indexes.set(key, held);
|
|
278
293
|
}
|
|
279
294
|
}
|
|
@@ -282,7 +297,9 @@ export function resourcesOfType(service, type, root) {
|
|
|
282
297
|
function stored(m, resource, root, opts = {}) {
|
|
283
298
|
const type = storedType(m, resource);
|
|
284
299
|
const when = Object.entries(m.resources[resource]?.readableWhen ?? {});
|
|
285
|
-
|
|
300
|
+
const rows = m.tenant?.storage === 'namespaced' && accountScope(m) === undefined && !m.resources[resource]?.shared
|
|
301
|
+
? twinResources(m.service, root).filter(row => row.type === type || row.type.startsWith(`${type}${TENANT_TYPE}`)) : resourcesOfType(m.service, type, root);
|
|
302
|
+
return rows.filter((r) => (opts.withDeleted || r.deleted !== true) && when.every(([k, v]) => r[k] === v));
|
|
286
303
|
}
|
|
287
304
|
/** The subject an item operation names: the manifest's key over the path parameters, or the last one. */
|
|
288
305
|
function subjectOf(m, resource, call) {
|
|
@@ -317,7 +334,7 @@ function missingParent(m, resource, call, root, tenant) {
|
|
|
317
334
|
const id = parentValue(parent, call.params);
|
|
318
335
|
const where = Object.entries(parent.where ?? {}).map(([field, template]) => [field, String(fill(template, call.params))]);
|
|
319
336
|
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);
|
|
337
|
+
&& (!m.tenant || m.tenant.storage === 'namespaced' || m.resources[parent.resource]?.shared || r[m.tenant.field] === tenant);
|
|
321
338
|
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));
|
|
322
339
|
}
|
|
323
340
|
/** The vendor's view of a stored subject: its own fields, bookkeeping (`_`) left out. */
|
|
@@ -430,14 +447,84 @@ function worldSeed(service, root, at) {
|
|
|
430
447
|
if (pending)
|
|
431
448
|
return pending;
|
|
432
449
|
const made = (async () => {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
450
|
+
// The action lock also joins first asks from several renderers/processes to the same seed.
|
|
451
|
+
const { value } = await runAsVendorMove(() => applyTwinWriteAtomic(service, snapshot => {
|
|
452
|
+
const existing = snapshot.find(row => row.type === SEED && row.id === 'seed');
|
|
453
|
+
if (typeof existing?.value === 'string')
|
|
454
|
+
return { kind: 'skip', value: existing.value };
|
|
455
|
+
const value = Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(32))).toString('hex');
|
|
456
|
+
return { kind: 'write', value, write: { operation: 'world_seed.record', subjectType: SEED, subjectId: 'seed', fields: { value }, occurredAt: at, actor: { kind: 'system' } } };
|
|
457
|
+
}, root));
|
|
436
458
|
return value;
|
|
437
459
|
})().finally(() => seeding.delete(key));
|
|
438
460
|
seeding.set(key, made);
|
|
439
461
|
return made;
|
|
440
462
|
}
|
|
463
|
+
/** World-owned generated values use the same private seed and log as pack secrets. */
|
|
464
|
+
export async function worldSecret(service, root, label, at = new Date().toISOString()) {
|
|
465
|
+
return hmac(await worldSeed(service, root, at), label, 'base64url');
|
|
466
|
+
}
|
|
467
|
+
/** The same retained account primitive for pack doors and private native twin fronts. */
|
|
468
|
+
export async function worldAccount(service, root, id = 'default', at = worldNow(), provisioning = false) {
|
|
469
|
+
if (!isWorldAccountName(id))
|
|
470
|
+
throw new Error('Credential account must be a name of at most 128 letters, digits, dots, underscores or hyphens');
|
|
471
|
+
await applyTwinWriteAtomic(service, (snapshot) => {
|
|
472
|
+
const held = snapshot.find(row => row.type === '_world_account' && row.id === id);
|
|
473
|
+
return held ? { kind: 'skip', value: id } : { kind: 'write', value: id, write: { operation: 'world_account.create', subjectType: '_world_account', subjectId: id, fields: { name: id }, ...(provisioning ? { input: { activity: 'provisioning' } } : {}), occurredAt: at, actor: { kind: 'system' } } };
|
|
474
|
+
}, root);
|
|
475
|
+
return id;
|
|
476
|
+
}
|
|
477
|
+
function nativeAccountLink(row, field) {
|
|
478
|
+
if (!row || row.deleted === true)
|
|
479
|
+
return undefined;
|
|
480
|
+
const fields = ownFields(row);
|
|
481
|
+
const value = field === undefined ? fields : fields[field];
|
|
482
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
483
|
+
return undefined;
|
|
484
|
+
const link = value;
|
|
485
|
+
return typeof link.resource === 'string' && typeof link.id === 'string' ? { resource: link.resource, id: link.id } : undefined;
|
|
486
|
+
}
|
|
487
|
+
const sameAccountLink = (a, b) => a?.resource === b?.resource && a?.id === b?.id;
|
|
488
|
+
/** Native creation and reconstruction run outside the action lock; only the retained link is compared and written. */
|
|
489
|
+
async function retainNativeAccountLink(service, root, at, call, type, id, field, resource, make, read) {
|
|
490
|
+
for (;;) {
|
|
491
|
+
const before = nativeAccountLink(resourcesOfType(service, type, root).find((row) => row.id === id), field);
|
|
492
|
+
const held = before && await read(before);
|
|
493
|
+
if (held !== undefined)
|
|
494
|
+
return held;
|
|
495
|
+
const made = { resource, id: await make() };
|
|
496
|
+
if (await read(made) === undefined)
|
|
497
|
+
throw new Error(`Credential ${type}/${id}: native ${resource}/${made.id} is unavailable after creation`);
|
|
498
|
+
const { value } = await applyTwinWriteAtomic(service, (snapshot) => {
|
|
499
|
+
const current = nativeAccountLink(snapshot.find((row) => row.type === type && row.id === id), field);
|
|
500
|
+
if (!sameAccountLink(current, before))
|
|
501
|
+
return { kind: 'skip', value: current };
|
|
502
|
+
return {
|
|
503
|
+
kind: 'write', value: made,
|
|
504
|
+
write: {
|
|
505
|
+
operation: `${type.slice(1)}.link`, subjectType: type, subjectId: id,
|
|
506
|
+
fields: field === undefined ? made : { name: id, [field]: made },
|
|
507
|
+
...provisioningInput(call), occurredAt: at, actor: { kind: 'system' },
|
|
508
|
+
},
|
|
509
|
+
};
|
|
510
|
+
}, root);
|
|
511
|
+
const linked = value && await read(value);
|
|
512
|
+
if (linked !== undefined)
|
|
513
|
+
return linked;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function vendorName(label, rule, { suffixed = false, identity = label } = {}) {
|
|
517
|
+
const allowed = new RegExp(rule.allowed.source, rule.allowed.flags.replace(/[gy]/g, ''));
|
|
518
|
+
const separator = rule.separator ?? '-';
|
|
519
|
+
if ([...separator].length !== 1 || !allowed.test(separator))
|
|
520
|
+
throw new Error('Credential name separator must be one allowed character');
|
|
521
|
+
const first = rule.first && new RegExp(rule.first.source, rule.first.flags.replace(/[gy]/g, ''));
|
|
522
|
+
let name = [...(rule.lower ? label.toLowerCase() : label)].map((letter) => allowed.test(letter) ? letter : separator).join('');
|
|
523
|
+
name = name.replace(new RegExp(`${escapeRe(separator)}+`, 'g'), separator);
|
|
524
|
+
if (first && !first.test(name[0] ?? ''))
|
|
525
|
+
name = `w${separator}${name}`;
|
|
526
|
+
return name !== label || name.length > rule.max || suffixed ? `${name.slice(0, Math.max(0, rule.max - 9))}${separator}${sha256(identity).slice(0, 8)}` : name;
|
|
527
|
+
}
|
|
441
528
|
/** A World's signing key for a label (ctx.signingKey): made once from the platform's random source, then read; the
|
|
442
529
|
* making is one per service store and label in the process, as the seed's is. */
|
|
443
530
|
const SIGNING_KEY = '_world_signing_key';
|
|
@@ -472,6 +559,13 @@ function worldSigningKey(service, root, at, label, alg) {
|
|
|
472
559
|
keying.set(key, made);
|
|
473
560
|
return made;
|
|
474
561
|
}
|
|
562
|
+
/** Vendor ids remain unique across its accounts, including public links that carry an id instead of a key. */
|
|
563
|
+
function mintRows(m, resource, root, snapshot) {
|
|
564
|
+
const base = m.resources[resource]?.storedAs ?? resource;
|
|
565
|
+
if (m.tenant?.storage === 'namespaced')
|
|
566
|
+
return (snapshot ?? twinResources(m.service, root)).filter(row => row.type === base || row.type.startsWith(`${base}${TENANT_TYPE}`));
|
|
567
|
+
return snapshot ? snapshot.filter(row => row.type === base) : resourcesOfType(m.service, base, root);
|
|
568
|
+
}
|
|
475
569
|
function mintId(m, resource, root, at, snapshot) {
|
|
476
570
|
if (!m.resources[resource])
|
|
477
571
|
throw new Error(`mint: ${resource} is not a resource ${m.vendor}'s manifest declares`);
|
|
@@ -495,7 +589,7 @@ function mintId(m, resource, root, at, snapshot) {
|
|
|
495
589
|
const width = Number(digits[1]);
|
|
496
590
|
if (!Number.isInteger(width) || width < 1 || width > 77)
|
|
497
591
|
throw new Error('mint: decimal width must be between 1 and 77');
|
|
498
|
-
const held = new Set((
|
|
592
|
+
const held = new Set(mintRows(m, resource, root, snapshot).map((r) => r.id));
|
|
499
593
|
const floor = 10n ** BigInt(width - 1);
|
|
500
594
|
for (let n = held.size + 1;; n++) {
|
|
501
595
|
const id = String(floor + BigInt('0x' + sha256(`${m.service}:${resource}:${n}`)) % (9n * floor));
|
|
@@ -518,7 +612,7 @@ function mintId(m, resource, root, at, snapshot) {
|
|
|
518
612
|
const pattern = new RegExp(`^${escapeRe(String(fill(head, { prefix })))}(\\d+)${escapeRe(String(fill(tail, { prefix })))}$`);
|
|
519
613
|
let max = 0n;
|
|
520
614
|
// the one type, from the index that follows writes (a scan of the whole World per create grew with it)
|
|
521
|
-
for (const r of (
|
|
615
|
+
for (const r of mintRows(m, resource, root, snapshot)) {
|
|
522
616
|
const hit = pattern.exec(r.id);
|
|
523
617
|
if (hit && BigInt(hit[1]) > max)
|
|
524
618
|
max = BigInt(hit[1]);
|
|
@@ -533,13 +627,13 @@ function mintId(m, resource, root, at, snapshot) {
|
|
|
533
627
|
* and its attachment) must not meet. */
|
|
534
628
|
const lastSnowflake = new Map();
|
|
535
629
|
function mintSnowflake(m, epoch, root, at, snapshot) {
|
|
536
|
-
const ms = BigInt(Date.parse(at
|
|
630
|
+
const ms = BigInt(Date.parse(at) || 0);
|
|
537
631
|
const base = ms > epoch ? (ms - epoch) << 22n : 0n;
|
|
538
632
|
let max = -1n;
|
|
539
633
|
for (const [name, decl] of Object.entries(m.resources)) {
|
|
540
634
|
if (!/^\{snowflake:\d+\}$/.test(decl.ids ?? m.ids.template))
|
|
541
635
|
continue;
|
|
542
|
-
for (const r of (
|
|
636
|
+
for (const r of mintRows(m, name, root, snapshot))
|
|
543
637
|
if (/^\d+$/.test(r.id)) {
|
|
544
638
|
const v = BigInt(r.id);
|
|
545
639
|
if (v > max)
|
|
@@ -555,9 +649,9 @@ function mintSnowflake(m, epoch, root, at, snapshot) {
|
|
|
555
649
|
}
|
|
556
650
|
/** The next KSUID for a resource (Clerk's and Svix's ids, `ksuidFrom`): the World's instant, then the type's mint
|
|
557
651
|
* count, so a frozen clock still lists them as made; past any the World already holds. */
|
|
558
|
-
function mintKsuid(m, resource, at, root, prefix
|
|
559
|
-
const held = new Set((
|
|
560
|
-
const seconds = Math.floor((Date.parse(at
|
|
652
|
+
function mintKsuid(m, resource, at, root, prefix, snapshot, width) {
|
|
653
|
+
const held = new Set(mintRows(m, resource, root, snapshot).map((r) => r.id));
|
|
654
|
+
const seconds = Math.floor((Date.parse(at) || 0) / 1000);
|
|
561
655
|
for (let n = held.size + 1;; n++) {
|
|
562
656
|
const id = `${prefix}${ksuidFrom(seconds, n, `${m.service}:${resource}:${n}`, width)}`;
|
|
563
657
|
if (!held.has(id))
|
|
@@ -566,8 +660,8 @@ function mintKsuid(m, resource, at, root, prefix = '', snapshot, width = 27) {
|
|
|
566
660
|
}
|
|
567
661
|
/** The next id of `count` lowercase letters for a resource, derived from the resource and the count before it, past
|
|
568
662
|
* any the World already holds. */
|
|
569
|
-
function mintLetters(m, resource, count, root, prefix
|
|
570
|
-
const held = new Set((
|
|
663
|
+
function mintLetters(m, resource, count, root, prefix, snapshot) {
|
|
664
|
+
const held = new Set(mintRows(m, resource, root, snapshot).map((r) => r.id));
|
|
571
665
|
for (let n = held.size + 1;; n++) {
|
|
572
666
|
const id = `${prefix}${lettersFrom(`${m.service}:${resource}:${n}`, count)}`;
|
|
573
667
|
if (!held.has(id))
|
|
@@ -576,8 +670,8 @@ function mintLetters(m, resource, count, root, prefix = '', snapshot) {
|
|
|
576
670
|
}
|
|
577
671
|
/** The next hex id for a resource: N hex digits derived from the resource and the count before it, past any the World
|
|
578
672
|
* already holds (a Cloudflare id, 32). */
|
|
579
|
-
function mintHex(m, resource, count, root, prefix
|
|
580
|
-
const held = new Set((
|
|
673
|
+
function mintHex(m, resource, count, root, prefix, snapshot) {
|
|
674
|
+
const held = new Set(mintRows(m, resource, root, snapshot).map((r) => r.id));
|
|
581
675
|
for (let n = held.size + 1;; n++) {
|
|
582
676
|
const id = `${prefix}${sha256(`${m.service}:${resource}:${n}`).slice(0, count)}`;
|
|
583
677
|
if (!held.has(id))
|
|
@@ -587,8 +681,7 @@ function mintHex(m, resource, count, root, prefix = '', snapshot) {
|
|
|
587
681
|
/** The next UUID for a resource: a version-4-shaped UUID derived from the resource and the count before it, past any
|
|
588
682
|
* the World already holds. */
|
|
589
683
|
function mintUuid(m, resource, root, snapshot) {
|
|
590
|
-
const
|
|
591
|
-
const held = new Set((snapshot ? snapshot.filter((r) => r.type === type) : resourcesOfType(m.service, type, root)).map((r) => r.id));
|
|
684
|
+
const held = new Set(mintRows(m, resource, root, snapshot).map((r) => r.id));
|
|
592
685
|
for (let n = held.size + 1;; n++) {
|
|
593
686
|
const id = uuidFrom(`${m.service}:${resource}:${n}`);
|
|
594
687
|
if (!held.has(id))
|
|
@@ -849,14 +942,15 @@ async function finishWrite(m, call, resource, row, result, operation, params, ro
|
|
|
849
942
|
const request = requestOfCall.get(call) ?? call.request;
|
|
850
943
|
const context = () => contextFor(m, { ...call, request: request.clone() }, { ...(scopeOfCall.get(call) ?? {}), ...(root !== undefined ? { root } : {}), clock: () => occurredAt });
|
|
851
944
|
if (m.onWrite)
|
|
852
|
-
await m.onWrite({ operation, storedType:
|
|
945
|
+
await m.onWrite({ operation, storedType: m.resources[resource]?.storedAs ?? resource, body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone(), context });
|
|
853
946
|
if (m.events) {
|
|
854
|
-
const
|
|
855
|
-
const
|
|
947
|
+
const endpoint = (kind) => ({ ...kind, storedAs: storedType(m, kind.storedAs), ...(kind.joins ? { joins: Object.fromEntries(Object.entries(kind.joins).map(([name, join]) => [name, { ...join, storedAs: storedType(m, join.storedAs) }])) } : {}) });
|
|
948
|
+
const events = { ...m.events, ...(m.events.store ? { store: storedType(m, m.events.store) } : {}), endpoints: 'storedAs' in m.events.endpoints ? endpoint(m.events.endpoints) : m.events.endpoints.map(endpoint) };
|
|
949
|
+
const write = { operation, storedType: m.resources[resource]?.storedAs ?? resource, body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone() };
|
|
856
950
|
await deliverEvents(m.service, events, write, async (type) => (events.render ? events.render(await context(), write, type) : body), undefined, events.values ? async (type) => events.values(await context(), write, type) : undefined);
|
|
857
951
|
}
|
|
858
952
|
if (m.sockets?.length)
|
|
859
|
-
socketWrite(m.service, root, { operation, storedType:
|
|
953
|
+
socketWrite(m.service, root, { operation, storedType: m.resources[resource]?.storedAs ?? resource, body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone() });
|
|
860
954
|
// a read held until the World writes (held-reads.ts) hears it
|
|
861
955
|
heldReadsWrite(m.service, root, storedType(m, resource));
|
|
862
956
|
// the subject's id as it stands after the write: the vendor's, when a live head minted one
|
|
@@ -1018,8 +1112,8 @@ function adoptedPath(m, call, root) {
|
|
|
1018
1112
|
/** A call's fields with each declared reference that names an adopted subject by its local id moved to the adopted
|
|
1019
1113
|
* id. A call (a handler's operation) need not say which subject type it writes, so every reference the pack declares
|
|
1020
1114
|
* is tried: a field is rewritten only when its value is a local id the alias map holds for the reference's target. */
|
|
1021
|
-
function adoptedInCall(
|
|
1022
|
-
const refs = packReferences(service);
|
|
1115
|
+
function adoptedInCall(m, fields, root) {
|
|
1116
|
+
const service = m.service, refs = packReferences(service);
|
|
1023
1117
|
if (refs.length === 0)
|
|
1024
1118
|
return fields;
|
|
1025
1119
|
const aliases = aliasesNow(service, root);
|
|
@@ -1028,14 +1122,14 @@ function adoptedInCall(service, fields, root) {
|
|
|
1028
1122
|
let out = fields;
|
|
1029
1123
|
for (const ref of refs) {
|
|
1030
1124
|
const local = ref.key(out);
|
|
1031
|
-
const adopted = local === undefined ? undefined : aliases.get(`${ref.to}:${local}`);
|
|
1125
|
+
const adopted = local === undefined ? undefined : aliases.get(`${storedType(m, ref.to)}:${local}`);
|
|
1032
1126
|
if (adopted !== undefined && adopted !== local)
|
|
1033
1127
|
out = { ...out, ...ref.adopt(out, adopted) };
|
|
1034
1128
|
}
|
|
1035
1129
|
return out;
|
|
1036
1130
|
}
|
|
1037
1131
|
async function boundaryParams(m, call, root) {
|
|
1038
|
-
return adoptedInCall(m
|
|
1132
|
+
return adoptedInCall(m, await readParams(m, call.request, call.operation), root);
|
|
1039
1133
|
}
|
|
1040
1134
|
/** The refusal a subject's fields would draw from its resource's `unique` rules: another live subject (not `except`)
|
|
1041
1135
|
* already holds every field of a rule, each field given. */
|
|
@@ -1237,7 +1331,7 @@ export async function serveCore(m, call, scope = {}) {
|
|
|
1237
1331
|
const parent = parentOfCall(decl, call);
|
|
1238
1332
|
const parentId = parent ? parentValue(parent, call.params) : undefined;
|
|
1239
1333
|
// the caller's tenant, when the vendor has tenancy and the resource is not shared by every tenant
|
|
1240
|
-
const tenantField = m.tenant && !decl.shared ? m.tenant.field : undefined;
|
|
1334
|
+
const tenantField = m.tenant?.storage !== 'namespaced' && m.tenant && !decl.shared ? m.tenant.field : undefined;
|
|
1241
1335
|
const tenant = tenantField && scope.tenant ? await scope.tenant(await contextFor(m, { ...call, request: call.request.clone() }, scope)) : undefined;
|
|
1242
1336
|
if (tenantField && tenant === undefined)
|
|
1243
1337
|
return { unmodeled: `the caller names no tenant (semantics/tenant.ts) for ${resource}` };
|
|
@@ -1433,7 +1527,7 @@ export async function contextFor(m, call, scope) {
|
|
|
1433
1527
|
sentOf.set(call, { ...sentRequest(m, asAsked, parts ? Object.fromEntries(parts.filter((p) => p.filename === null).map((p) => [p.name, new TextDecoder().decode(p.body)])) : raw ?? body), ...(parts ? { parts } : {}), ...(raw ? { type: call.request.headers.get('content-type') ?? undefined } : {}) });
|
|
1434
1528
|
// a handler reading the body itself sees the adopted id too (the boundary's one resolution, below)
|
|
1435
1529
|
if (body && typeof body === 'object' && !Array.isArray(body))
|
|
1436
|
-
body = adoptedInCall(m
|
|
1530
|
+
body = adoptedInCall(m, body, root);
|
|
1437
1531
|
// every read of the body is of a copy, so the handler's own `call.request` and the core's answer (`core()`) each
|
|
1438
1532
|
// still find it unread, whichever of them reads it first
|
|
1439
1533
|
const pristine = call.request.clone();
|
|
@@ -1447,12 +1541,25 @@ export async function contextFor(m, call, scope) {
|
|
|
1447
1541
|
});
|
|
1448
1542
|
const paths = expandPaths(m, params);
|
|
1449
1543
|
const stateChecks = new Map();
|
|
1544
|
+
const credentialDoor = m.descriptor?.credentialDoor ?? getPack(m.vendor)?.credentialDoor;
|
|
1545
|
+
const credentialFields = body && typeof body === 'object' && !Array.isArray(body) ? body : {};
|
|
1450
1546
|
const rawResources = new WeakMap();
|
|
1451
1547
|
const rawRow = (resource, row) => {
|
|
1452
1548
|
const name = m.resources[resource] ? resource : Object.keys(m.resources).find((name) => storedType(m, name) === row.type);
|
|
1549
|
+
if (m.tenant?.storage === 'namespaced') {
|
|
1550
|
+
const [base, encoded] = row.type.split(TENANT_TYPE);
|
|
1551
|
+
const owner = m.resources[name ?? resource]?.shared && row[m.tenant.field] !== undefined ? row[m.tenant.field] : encoded ? decodeURIComponent(encoded) : 'default';
|
|
1552
|
+
row = copyResource(row);
|
|
1553
|
+
row.type = base;
|
|
1554
|
+
row[m.tenant.field] = owner;
|
|
1555
|
+
}
|
|
1453
1556
|
rawResources.set(row, name ?? resource);
|
|
1454
1557
|
return row;
|
|
1455
1558
|
};
|
|
1559
|
+
const liveNativeRow = (link) => {
|
|
1560
|
+
const row = context.row(link.resource, link.id);
|
|
1561
|
+
return row && row.deleted !== true ? row : undefined;
|
|
1562
|
+
};
|
|
1456
1563
|
const context = {
|
|
1457
1564
|
call,
|
|
1458
1565
|
params,
|
|
@@ -1468,7 +1575,7 @@ export async function contextFor(m, call, scope) {
|
|
|
1468
1575
|
git: (name) => {
|
|
1469
1576
|
if (!/^[A-Za-z0-9_.-]+(\/[A-Za-z0-9_.-]+)*$/.test(name) || name.split('/').some((seg) => seg === '.' || seg === '..'))
|
|
1470
1577
|
throw new Error(`semantics: ${name} is not a git repository name`);
|
|
1471
|
-
const plane = `${worldPaths(m.service, root).dir}/git`;
|
|
1578
|
+
const plane = `${worldPaths(m.service, root).dir}/git${accountScope(m) && accountScope(m) !== 'default' ? `/accounts/${encodeURIComponent(accountScope(m))}` : ''}`;
|
|
1472
1579
|
return { store: new GitObjectStore(getActiveBlobStore(), `${plane}/repos/${name}`), refs: new GitRefs(getActiveWorldStore(), `${plane}/repos/${name}`) };
|
|
1473
1580
|
},
|
|
1474
1581
|
mail: (route, mail, headers) => sendMail(route, mail, headers),
|
|
@@ -1508,17 +1615,50 @@ export async function contextFor(m, call, scope) {
|
|
|
1508
1615
|
return value;
|
|
1509
1616
|
},
|
|
1510
1617
|
occurredAt: at,
|
|
1511
|
-
secret: async (label) => hmac(await worldSeed(m.service, root, at), label, 'base64url'),
|
|
1618
|
+
secret: async (label) => hmac(await worldSeed(m.service, root, at), scopeLabel(m, label), 'base64url'),
|
|
1619
|
+
account: async (field = credentialDoor?.account?.field ?? 'account') => {
|
|
1620
|
+
return worldAccount(m.service, root, credentialFields[field] ?? 'default', at, call.request.headers.get('x-volter-world-activity') === 'provisioning');
|
|
1621
|
+
},
|
|
1622
|
+
organization: async () => {
|
|
1623
|
+
const id = await context.account();
|
|
1624
|
+
const row = projectOwnerResources('_world', root, { type: '_world_organization', id }).find((row) => row.type === '_world_organization' && row.id === id && row.deleted !== true);
|
|
1625
|
+
return row ? { id, label: row.label, members: [...row.members] } : { id, label: id, members: ['owner@world.test'] };
|
|
1626
|
+
},
|
|
1627
|
+
deployment: () => {
|
|
1628
|
+
const id = credentialFields[credentialDoor?.deployment?.field ?? 'deployment'] ?? '';
|
|
1629
|
+
if (id !== '' && !isWorldAccountName(id))
|
|
1630
|
+
throw new Error('Credential deployment must be a name of at most 128 letters, digits, dots, underscores or hyphens');
|
|
1631
|
+
return id;
|
|
1632
|
+
},
|
|
1633
|
+
vendorName,
|
|
1634
|
+
tenant: async (resource, make) => {
|
|
1635
|
+
const id = await context.account();
|
|
1636
|
+
return retainNativeAccountLink(m.service, root, at, call, '_world_account', id, 'tenant', resource, async () => make(await context.organization()), async (link) => liveNativeRow(link));
|
|
1637
|
+
},
|
|
1638
|
+
deploymentObject: async (kind, resource, make) => {
|
|
1639
|
+
const organization = await context.account(), deployment = context.deployment();
|
|
1640
|
+
return retainNativeAccountLink(m.service, root, at, call, '_world_deployment_object', `${organization}/${deployment}/${kind}`, undefined, resource, () => make(deployment), async (link) => liveNativeRow(link));
|
|
1641
|
+
},
|
|
1642
|
+
accountKey: async (purpose, resource, key) => {
|
|
1643
|
+
const organization = await context.account(), deployment = context.deployment();
|
|
1644
|
+
return retainNativeAccountLink(m.service, root, at, call, '_world_account_key', `${organization}/${deployment}/${purpose}`, undefined, resource, () => key.make(), async (link) => {
|
|
1645
|
+
const row = liveNativeRow(link);
|
|
1646
|
+
if (!row)
|
|
1647
|
+
return undefined;
|
|
1648
|
+
const secret = await key.read(row);
|
|
1649
|
+
return secret === undefined ? undefined : { id: link.id, secret };
|
|
1650
|
+
});
|
|
1651
|
+
},
|
|
1512
1652
|
record: async (type, fields, id, operation) => {
|
|
1513
1653
|
if (!type.startsWith('_'))
|
|
1514
1654
|
throw new Error(`semantics: ${type} is not a bookkeeping type (bookkeeping types start with _)`);
|
|
1515
1655
|
// a bookkeeping subject a resource counts (`counts` naming it: sentry's issue users) keeps that count as any
|
|
1516
1656
|
// child does, the parent it left recounted too
|
|
1517
|
-
const keeps = counted(m, type);
|
|
1657
|
+
const keeps = counted(m, type), physicalType = storedType(m, type);
|
|
1518
1658
|
if (id !== undefined) {
|
|
1519
1659
|
const before = operation ? stateBeforeWrite(m, type, id, root) : undefined;
|
|
1520
|
-
const prior = keeps ? resourcesOfType(m.service,
|
|
1521
|
-
const { resource: row, result } = await applyTwinWrite(m.service, { operation: operation ?? `${type.slice(1)}.record`, subjectType:
|
|
1660
|
+
const prior = keeps ? resourcesOfType(m.service, physicalType, root).find((r) => r.id === id) : undefined;
|
|
1661
|
+
const { resource: row, result } = await applyTwinWrite(m.service, { operation: operation ?? `${type.slice(1)}.record`, subjectType: physicalType, subjectId: id, fields, ...provisioningInput(call), occurredAt: at, actor: { kind: 'system' } }, root);
|
|
1522
1662
|
if (operation)
|
|
1523
1663
|
observeStateWrite(m, result, before, root, stateChecks);
|
|
1524
1664
|
if (keeps)
|
|
@@ -1531,7 +1671,7 @@ export async function contextFor(m, call, scope) {
|
|
|
1531
1671
|
const prefix = `${type.slice(1)}_`;
|
|
1532
1672
|
let max = 0n;
|
|
1533
1673
|
for (const r of resources) {
|
|
1534
|
-
if (r.type !==
|
|
1674
|
+
if (r.type !== physicalType)
|
|
1535
1675
|
continue;
|
|
1536
1676
|
const digits = r.id.startsWith(prefix) ? r.id.slice(prefix.length) : '';
|
|
1537
1677
|
if (/^\d+$/.test(digits) && BigInt(digits) > max)
|
|
@@ -1539,21 +1679,21 @@ export async function contextFor(m, call, scope) {
|
|
|
1539
1679
|
}
|
|
1540
1680
|
return `${prefix}${max + 1n}`;
|
|
1541
1681
|
})();
|
|
1542
|
-
return { kind: 'write', value: subject, write: { operation: operation ?? `${type.slice(1)}.record`, subjectType:
|
|
1543
|
-
}, root, () => resourcesOfType(m.service,
|
|
1682
|
+
return { kind: 'write', value: subject, write: { operation: operation ?? `${type.slice(1)}.record`, subjectType: physicalType, subjectId: subject, fields, ...provisioningInput(call), occurredAt: at, actor: { kind: 'system' } } };
|
|
1683
|
+
}, root, () => resourcesOfType(m.service, physicalType, root));
|
|
1544
1684
|
if (operation && result)
|
|
1545
1685
|
observeStateWrite(m, result, undefined, root, stateChecks);
|
|
1546
|
-
const made = keeps && value !== undefined ? resourcesOfType(m.service,
|
|
1686
|
+
const made = keeps && value !== undefined ? resourcesOfType(m.service, physicalType, root).find((r) => r.id === value) : undefined;
|
|
1547
1687
|
if (made)
|
|
1548
1688
|
await keepCounts(m, call, type, made, params, root, at);
|
|
1549
1689
|
return value;
|
|
1550
1690
|
},
|
|
1551
1691
|
get machines() {
|
|
1552
1692
|
const enrolled = resourcesOfType(m.service, MACHINE_POOL, root).find((r) => r.id === 'pool');
|
|
1553
|
-
return guardedMachinePool(poolFor(m.service, root, enrolled?.kind ?? 'none', enrolled?.httpReplies
|
|
1693
|
+
return guardedMachinePool(poolFor(m.service, root, enrolled?.kind ?? 'none', enrolled?.httpReplies), m.service, ['retrieve', 'list'].includes(call.operation.class) || (m.reads?.includes(call.operation.id) ?? false));
|
|
1554
1694
|
},
|
|
1555
|
-
signingKey: (label, alg) => worldSigningKey(m.service, root, at, label, alg ?? 'RS256'),
|
|
1556
|
-
heldSigningKey: (label, alg) => heldWorldSigningKey(m.service, root, label, alg ?? 'RS256'),
|
|
1695
|
+
signingKey: (label, alg) => worldSigningKey(m.service, root, at, scopeLabel(m, label), alg ?? 'RS256'),
|
|
1696
|
+
heldSigningKey: (label, alg) => heldWorldSigningKey(m.service, root, scopeLabel(m, label), alg ?? 'RS256'),
|
|
1557
1697
|
create: (resource, fields, operation) => atomicWrite(m, call, resource, (snapshot) => {
|
|
1558
1698
|
const id = mintId(m, resource, root, at, snapshot);
|
|
1559
1699
|
return { id, fields: typeof fields === 'function' ? fields(id) : fields };
|
|
@@ -1566,9 +1706,14 @@ export async function contextFor(m, call, scope) {
|
|
|
1566
1706
|
throw new Error('createUnique requires declared unique keys');
|
|
1567
1707
|
let existing;
|
|
1568
1708
|
const row = await atomicWrite(m, call, resource, (snapshot) => {
|
|
1569
|
-
const id = mintId(m, resource, root, at, snapshot);
|
|
1709
|
+
const id = options?.id ?? mintId(m, resource, root, at, snapshot);
|
|
1570
1710
|
const selected = typeof fields === 'function' ? fields(id) : fields;
|
|
1571
|
-
const hit = snapshot.find((r) =>
|
|
1711
|
+
const hit = snapshot.find((r) => {
|
|
1712
|
+
if (r.type !== storedType(m, resource) || r.deleted === true)
|
|
1713
|
+
return false;
|
|
1714
|
+
const fields = { [m.resources[resource]?.idAs ?? 'id']: r.id, ...ownFields(r) };
|
|
1715
|
+
return rules.some((rule) => rule.fields.every((field) => selected[field] !== undefined && fields[field] === selected[field]));
|
|
1716
|
+
});
|
|
1572
1717
|
if (hit) {
|
|
1573
1718
|
existing = view(m, resource, hit);
|
|
1574
1719
|
return undefined;
|
|
@@ -1608,11 +1753,21 @@ export async function contextFor(m, call, scope) {
|
|
|
1608
1753
|
},
|
|
1609
1754
|
writeDetailed: (resource, id, fields, operation) => writeDetailed(m, call, resource, id, fields, operation, params, root, at, stateChecks),
|
|
1610
1755
|
resolve: (resource, id) => resolveSubjectId(m.service, storedType(m, resource), id, root),
|
|
1611
|
-
storedType: (resource) =>
|
|
1756
|
+
storedType: (resource) => m.resources[resource]?.storedAs ?? resource,
|
|
1612
1757
|
ok: (fields) => Response.json({ ...(m.success ?? {}), ...fields }),
|
|
1613
1758
|
row: (resource, id, opts) => { const row = stored(m, resource, root, opts).find((r) => r.id === id); return row ? rawRow(resource, row) : undefined; },
|
|
1614
1759
|
rowsRaw: (resource, opts) => stored(m, resource, root, opts).map((row) => rawRow(resource, row)),
|
|
1615
|
-
tree: () =>
|
|
1760
|
+
tree: () => {
|
|
1761
|
+
const rows = twinResources(m.service, root);
|
|
1762
|
+
if (m.tenant?.storage !== 'namespaced')
|
|
1763
|
+
return rows;
|
|
1764
|
+
const account = accountScope(m);
|
|
1765
|
+
return rows.filter(row => {
|
|
1766
|
+
const [base, encoded] = row.type.split(TENANT_TYPE);
|
|
1767
|
+
const name = Object.keys(m.resources).find(name => (m.resources[name]?.storedAs ?? name) === base) ?? base;
|
|
1768
|
+
return account === undefined || m.resources[name]?.shared || (encoded ? decodeURIComponent(encoded) : 'default') === account;
|
|
1769
|
+
}).map(row => rawRow(row.type.split(TENANT_TYPE)[0], row));
|
|
1770
|
+
},
|
|
1616
1771
|
history: (resource, id) => subjectHistory(m.service, { type: storedType(m, resource), id }, root).map((e) => ({ ...(e.operation ? { operation: e.operation } : {}), ...(e.fields ? { fields: e.fields } : {}), ...(e.occurredAt ? { occurredAt: e.occurredAt } : {}) })),
|
|
1617
1772
|
raw: (body, init = {}) => new Response(body, { status: init.status ?? 200, headers: init.headers ?? {} }),
|
|
1618
1773
|
atomically: async (decide) => {
|
|
@@ -1667,17 +1822,17 @@ export async function contextFor(m, call, scope) {
|
|
|
1667
1822
|
engine: scope.database === undefined ? unboundDatabase(m.service) : managedDatabase(scope.database, { readOnly: scope.readOnly === true || isReadOnlyRequest(call.request) }),
|
|
1668
1823
|
redis: (commands, options) => execRedisRun(commands, {
|
|
1669
1824
|
root, occurredAt: at, service: m.service, readOnly: scope.readOnly === true || isReadOnlyRequest(call.request), dialect: options.dialect,
|
|
1670
|
-
...(options.database === undefined ? {} : { database: options.database }),
|
|
1825
|
+
...(accountScope(m) && accountScope(m) !== 'default' ? { database: scopeLabel(m, options.database ?? 'default') } : options.database === undefined ? {} : { database: options.database }),
|
|
1671
1826
|
...(options.transaction === undefined ? {} : { transaction: options.transaction }),
|
|
1672
1827
|
}),
|
|
1673
|
-
live: liveAccess({ rows: (type) =>
|
|
1828
|
+
live: liveAccess({ rows: (type) => stored(m, type, root).map(row => rawRow(type, row)), blob: (key) => readResourceBlob(m.service, scopeLabel(m, key), root) }, call.request),
|
|
1674
1829
|
blobs: {
|
|
1675
|
-
put: (key, bytes) => writeResourceBlob(m.service, key, bytes, root),
|
|
1676
|
-
putStream: (key, body, accept) => writeResourceBlobStream(m.service, key, body, accept, root),
|
|
1677
|
-
get: (key) => readResourceBlob(m.service, key, root),
|
|
1678
|
-
remove: (key) => removeResourceBlob(m.service, key, root),
|
|
1679
|
-
size: (key) => resourceBlobSize(m.service, key, root),
|
|
1680
|
-
range: (key, start, endInclusive) => readResourceBlobRange(m.service, key, start, endInclusive, root),
|
|
1830
|
+
put: (key, bytes) => writeResourceBlob(m.service, scopeLabel(m, key), bytes, root),
|
|
1831
|
+
putStream: (key, body, accept) => writeResourceBlobStream(m.service, scopeLabel(m, key), body, accept, root),
|
|
1832
|
+
get: (key) => readResourceBlob(m.service, scopeLabel(m, key), root),
|
|
1833
|
+
remove: (key) => removeResourceBlob(m.service, scopeLabel(m, key), root),
|
|
1834
|
+
size: (key) => resourceBlobSize(m.service, scopeLabel(m, key), root),
|
|
1835
|
+
range: (key, start, endInclusive) => readResourceBlobRange(m.service, scopeLabel(m, key), start, endInclusive, root),
|
|
1681
1836
|
},
|
|
1682
1837
|
merge: (resource, prior, next) => mergeField(m.resources[resource]?.update, prior, next),
|
|
1683
1838
|
conflict: (resource, fields, except) => {
|
|
@@ -1719,10 +1874,12 @@ const BATCH_REFUSED_READS = ['get', 'rows', 'tree', 'history', 'find', 'list', '
|
|
|
1719
1874
|
/** The members that write, or wait on writes, or answer as another context: each lands the staged writes first. */
|
|
1720
1875
|
const BATCH_LANDING_MEMBERS = ['create', 'createUnique', 'change', 'accumulate', 'writeDetailed', 'record', 'atomically', 'remove', 'issue', 'at', 'over', 'core', 'awaitWrite', 'awaitCondition',
|
|
1721
1876
|
// a World's seed and signing keys are written when first asked (worldSeed, worldSigningKey, as a vendor's move)
|
|
1722
|
-
'secret', 'signingKey'];
|
|
1877
|
+
'secret', 'signingKey', 'account', 'organization', 'tenant', 'deploymentObject', 'accountKey'];
|
|
1723
1878
|
/** `ctx.batch` over a context whose writes may be grouped (see `batchable`). */
|
|
1724
1879
|
function bindBatch(m, call, context, params, root, at, rawRow) {
|
|
1725
1880
|
let staged;
|
|
1881
|
+
// A callback's native object must land before the helper writes its retained link.
|
|
1882
|
+
let nativeAccountWrites = 0;
|
|
1726
1883
|
// each staged subject as it will stand, by `type:id`, in the order first written
|
|
1727
1884
|
const subjects = new Map();
|
|
1728
1885
|
const land = async () => {
|
|
@@ -1738,7 +1895,19 @@ function bindBatch(m, call, context, params, root, at, rawRow) {
|
|
|
1738
1895
|
const plain = ctx[name];
|
|
1739
1896
|
if (typeof plain !== 'function')
|
|
1740
1897
|
continue;
|
|
1741
|
-
ctx[name] = async (...args) => {
|
|
1898
|
+
ctx[name] = async (...args) => {
|
|
1899
|
+
await land();
|
|
1900
|
+
const native = name === 'tenant' || name === 'deploymentObject' || name === 'accountKey';
|
|
1901
|
+
if (native)
|
|
1902
|
+
nativeAccountWrites++;
|
|
1903
|
+
try {
|
|
1904
|
+
return await plain(...args);
|
|
1905
|
+
}
|
|
1906
|
+
finally {
|
|
1907
|
+
if (native)
|
|
1908
|
+
nativeAccountWrites--;
|
|
1909
|
+
}
|
|
1910
|
+
};
|
|
1742
1911
|
}
|
|
1743
1912
|
for (const name of BATCH_REFUSED_READS) {
|
|
1744
1913
|
const plain = ctx[name];
|
|
@@ -1753,7 +1922,7 @@ function bindBatch(m, call, context, params, root, at, rawRow) {
|
|
|
1753
1922
|
const plainWrite = context.write, plainRow = context.row, plainRowsRaw = context.rowsRaw;
|
|
1754
1923
|
context.write = async (resource, id, fields, operation) => {
|
|
1755
1924
|
// a vendor's own move (ctx.asVendor) is the clock's write, recorded as the system's: it lands alone, in its place
|
|
1756
|
-
if (!staged || inVendorMove()) {
|
|
1925
|
+
if (!staged || nativeAccountWrites > 0 || inVendorMove()) {
|
|
1757
1926
|
await land();
|
|
1758
1927
|
return plainWrite(resource, id, fields, operation);
|
|
1759
1928
|
}
|
|
@@ -2005,7 +2174,7 @@ export function authRefusal(m, request, root) {
|
|
|
2005
2174
|
const held = m.auth.held;
|
|
2006
2175
|
if (held && !held.standing?.includes(key)) {
|
|
2007
2176
|
const hash = sha256(key);
|
|
2008
|
-
const holds =
|
|
2177
|
+
const holds = stored(m, held.storedAs, root).some((r) => r.deleted !== true && r[held.hashField] === hash && (held.unless === undefined || r[held.unless] !== true));
|
|
2009
2178
|
if (!holds)
|
|
2010
2179
|
return vendorError(errorManifest, invalid);
|
|
2011
2180
|
}
|
|
@@ -2101,7 +2270,7 @@ export function crossCutting(m, opts = {}) {
|
|
|
2101
2270
|
const url = new URL(request.url);
|
|
2102
2271
|
const signature = hashFieldValue({ path: url.pathname + url.search, body: await bodyTextOf(request.clone()) });
|
|
2103
2272
|
const id = `idem_${hex(key)}`;
|
|
2104
|
-
const prior = twinResources(m.service, opts.root).find((r) => r.type === m.idempotency.storedAs && r.id === id);
|
|
2273
|
+
const prior = twinResources(m.service, opts.root).find((r) => r.type === storedType(m, m.idempotency.storedAs) && r.id === id);
|
|
2105
2274
|
if (prior) {
|
|
2106
2275
|
if (m.idempotency.conflict && prior.requestHash !== undefined && prior.requestHash !== signature)
|
|
2107
2276
|
return vendorError(m, m.idempotency.conflict);
|
|
@@ -2117,7 +2286,7 @@ export function crossCutting(m, opts = {}) {
|
|
|
2117
2286
|
return response;
|
|
2118
2287
|
const contentType = response.headers.get('content-type') ?? 'application/json';
|
|
2119
2288
|
const text = await response.clone().text();
|
|
2120
|
-
await applyTwinWrite(m.service, { operation: 'idempotency.record', subjectType: m.idempotency.storedAs, subjectId: id, fields: { response: { status: response.status, text, contentType }, requestHash: signature }, occurredAt: (opts.clock ?? worldNow)(), actor: { kind: 'system' } }, opts.root);
|
|
2289
|
+
await applyTwinWrite(m.service, { operation: 'idempotency.record', subjectType: storedType(m, m.idempotency.storedAs), subjectId: id, fields: { response: { status: response.status, text, contentType }, requestHash: signature }, occurredAt: (opts.clock ?? worldNow)(), actor: { kind: 'system' } }, opts.root);
|
|
2121
2290
|
return response;
|
|
2122
2291
|
}
|
|
2123
2292
|
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -47,8 +47,8 @@ export { siteLabel, siteUrlOf, twinSiteUrl, twinVendorUrl } from './twin-fetch.j
|
|
|
47
47
|
export { statefulTwinManifest, twinManifest } from './scenario.js';
|
|
48
48
|
export { assetContentType, packAsset } from './pack-assets.js';
|
|
49
49
|
export { bytesResponse, contentTypeOf, fileResponse } from './file-response.js';
|
|
50
|
-
export { certificateOf, digest, digestBytes, equalSecrets, oauth1BaseString, oauth1Encode, oauth1Header, oauth1Signature, hmac, jwks, jwtDecode, jwtSign, jwtVerify, keyId, lettersFrom, base62From, md5, publicKeyOf, sha256, signWith, SIGNING_ALGORITHMS, uuidFrom, verifyWith } from './signing.js';
|
|
51
|
-
export type { HandlerCrypto, Jwk, Jwks, JwtVerdict, SigningKey } from './signing.js';
|
|
50
|
+
export { certificateOf, digest, digestBytes, equalSecrets, oauth1BaseString, oauth1Encode, oauth1Header, oauth1Signature, hmac, jwks, jwtDecode, jwtSign, jwtSignClaims, jwtVerify, jwtVerifySignature, keyId, lettersFrom, base62From, md5, publicKeyOf, sha256, signWith, SIGNING_ALGORITHMS, uuidFrom, verifyWith } from './signing.js';
|
|
51
|
+
export type { HandlerCrypto, Jwk, Jwks, JwtDecodeOptions, JwtSignatureOptions, JwtVerdict, JwtVerificationKey, SigningKey } from './signing.js';
|
|
52
52
|
export { blobDigest } from './blob-store.js';
|
|
53
53
|
export { gunzip, gzip } from './bytes.js';
|
|
54
54
|
export { ownFields } from './log.js';
|
|
@@ -81,3 +81,5 @@ export { readHostPort } from './host-port.js';
|
|
|
81
81
|
export { assertBrowserName, browserOrigin, validateBrowserState, validateBrowserStorageState, validateBrowserContext, browserAcceptLanguage, cookieMatchesHost, type BrowserCookie, type BrowserState, type BrowserStorageState, type BrowserContext, type BrowserIndexedDB } from './browser-state.js';
|
|
82
82
|
export * as managedOriginalInputs from './managed-original-inputs.js';
|
|
83
83
|
export * as managedGoPrograms from './managed-go-programs.js';
|
|
84
|
+
export { worldSecret, worldAccount } from './derived-core.js';
|
|
85
|
+
export { isWorldAccountName } from './world-account-name.js';
|
package/dist/src/index.js
CHANGED
|
@@ -37,7 +37,7 @@ export { statefulTwinManifest, twinManifest } from "./scenario.js";
|
|
|
37
37
|
export { assetContentType, packAsset } from "./pack-assets.js";
|
|
38
38
|
export { bytesResponse, contentTypeOf, fileResponse } from "./file-response.js";
|
|
39
39
|
// ── the kernel's deterministic helpers ─────────────────────────────────────────────────────────
|
|
40
|
-
export { certificateOf, digest, digestBytes, equalSecrets, oauth1BaseString, oauth1Encode, oauth1Header, oauth1Signature, hmac, jwks, jwtDecode, jwtSign, jwtVerify, keyId, lettersFrom, base62From, md5, publicKeyOf, sha256, signWith, SIGNING_ALGORITHMS, uuidFrom, verifyWith } from "./signing.js";
|
|
40
|
+
export { certificateOf, digest, digestBytes, equalSecrets, oauth1BaseString, oauth1Encode, oauth1Header, oauth1Signature, hmac, jwks, jwtDecode, jwtSign, jwtSignClaims, jwtVerify, jwtVerifySignature, keyId, lettersFrom, base62From, md5, publicKeyOf, sha256, signWith, SIGNING_ALGORITHMS, uuidFrom, verifyWith } from "./signing.js";
|
|
41
41
|
export { blobDigest } from "./blob-store.js";
|
|
42
42
|
// compressed bytes a wire carries (a SARIF upload), over web streams
|
|
43
43
|
export { gunzip, gzip } from "./bytes.js";
|
|
@@ -73,3 +73,5 @@ export { readHostPort } from "./host-port.js";
|
|
|
73
73
|
export { assertBrowserName, browserOrigin, validateBrowserState, validateBrowserStorageState, validateBrowserContext, browserAcceptLanguage, cookieMatchesHost } from "./browser-state.js";
|
|
74
74
|
export * as managedOriginalInputs from "./managed-original-inputs.js";
|
|
75
75
|
export * as managedGoPrograms from "./managed-go-programs.js";
|
|
76
|
+
export { worldSecret, worldAccount } from "./derived-core.js";
|
|
77
|
+
export { isWorldAccountName } from "./world-account-name.js";
|