@volter/world-core 3.0.161 → 3.0.162
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 +9 -1
- package/dist/src/derived-core.js +120 -57
- 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 +7 -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 +125 -63
- package/src/index.ts +5 -2
- package/src/machines.ts +10 -38
- package/src/pack-fetch.ts +27 -5
- package/src/packRegistry.ts +2 -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
|
|
@@ -235,7 +237,13 @@ function notFound(manifest, object, id, param) {
|
|
|
235
237
|
return vendorError(manifest, { status: nf.status, message: String(fill(message, { object, id })), ...(code ? { code } : {}), ...(nf.kind ? { kind: nf.kind } : {}), ...(param ? { param } : {}) });
|
|
236
238
|
}
|
|
237
239
|
// ── the tree ─────────────────────────────────────────────────────────────────────────────────
|
|
238
|
-
|
|
240
|
+
const TENANT_TYPE = '::account:';
|
|
241
|
+
const accountScope = (m) => m.tenant?.storage === 'namespaced' ? storageTenant(m.service) : undefined;
|
|
242
|
+
const scopeLabel = (m, label) => { const account = accountScope(m); return account && account !== 'default' ? `account:${account}:${label}` : label; };
|
|
243
|
+
export const storedType = (m, resource) => {
|
|
244
|
+
const base = m.resources[resource]?.storedAs ?? resource, account = accountScope(m);
|
|
245
|
+
return account && account !== 'default' && !m.resources[resource]?.shared ? `${base}${TENANT_TYPE}${encodeURIComponent(account)}` : base;
|
|
246
|
+
};
|
|
239
247
|
// A service's resources by type, folded once per World state (the tree's stamp; any write moves it), not on every read:
|
|
240
248
|
// a read of one resource type projected the service's whole tree, so a pack whose tree also holds many subjects of
|
|
241
249
|
// other types (PlanetScale's SQL rows beside its management records) paid for all of them on each read. A read gets its
|
|
@@ -254,9 +262,15 @@ export function resourcesOfType(service, type, root) {
|
|
|
254
262
|
if (held && delta) {
|
|
255
263
|
// the index keeps the tree's order: a subject updated where it stands stays there, one new to the tree (or made
|
|
256
264
|
// 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.
|
|
265
|
+
for (const k of [...delta.removed, ...delta.appended]) {
|
|
266
|
+
const type = held.typeByKey.get(k);
|
|
267
|
+
if (type !== undefined)
|
|
268
|
+
held.byType.get(type)?.delete(k);
|
|
269
|
+
held.typeByKey.delete(k);
|
|
270
|
+
}
|
|
259
271
|
const byKey = new Map(delta.changed.map((r) => [`${r.type}:${r.id}`, r]));
|
|
272
|
+
for (const [k, r] of byKey)
|
|
273
|
+
held.typeByKey.set(k, r.type);
|
|
260
274
|
for (const [k, r] of byKey)
|
|
261
275
|
if (!delta.appended.includes(k))
|
|
262
276
|
(held.byType.get(r.type) ?? held.byType.set(r.type, new Map()).get(r.type)).set(k, r);
|
|
@@ -273,7 +287,7 @@ export function resourcesOfType(service, type, root) {
|
|
|
273
287
|
const byType = new Map();
|
|
274
288
|
for (const r of twinResources(service, root))
|
|
275
289
|
(byType.get(r.type) ?? byType.set(r.type, new Map()).get(r.type)).set(`${r.type}:${r.id}`, r);
|
|
276
|
-
held = { stamp, byType };
|
|
290
|
+
held = { stamp, byType, typeByKey: new Map([...byType].flatMap(([type, rows]) => [...rows.keys()].map(key => [key, type]))) };
|
|
277
291
|
indexes.set(key, held);
|
|
278
292
|
}
|
|
279
293
|
}
|
|
@@ -282,7 +296,9 @@ export function resourcesOfType(service, type, root) {
|
|
|
282
296
|
function stored(m, resource, root, opts = {}) {
|
|
283
297
|
const type = storedType(m, resource);
|
|
284
298
|
const when = Object.entries(m.resources[resource]?.readableWhen ?? {});
|
|
285
|
-
|
|
299
|
+
const rows = m.tenant?.storage === 'namespaced' && accountScope(m) === undefined && !m.resources[resource]?.shared
|
|
300
|
+
? twinResources(m.service, root).filter(row => row.type === type || row.type.startsWith(`${type}${TENANT_TYPE}`)) : resourcesOfType(m.service, type, root);
|
|
301
|
+
return rows.filter((r) => (opts.withDeleted || r.deleted !== true) && when.every(([k, v]) => r[k] === v));
|
|
286
302
|
}
|
|
287
303
|
/** The subject an item operation names: the manifest's key over the path parameters, or the last one. */
|
|
288
304
|
function subjectOf(m, resource, call) {
|
|
@@ -317,7 +333,7 @@ function missingParent(m, resource, call, root, tenant) {
|
|
|
317
333
|
const id = parentValue(parent, call.params);
|
|
318
334
|
const where = Object.entries(parent.where ?? {}).map(([field, template]) => [field, String(fill(template, call.params))]);
|
|
319
335
|
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);
|
|
336
|
+
&& (!m.tenant || m.tenant.storage === 'namespaced' || m.resources[parent.resource]?.shared || r[m.tenant.field] === tenant);
|
|
321
337
|
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
338
|
}
|
|
323
339
|
/** The vendor's view of a stored subject: its own fields, bookkeeping (`_`) left out. */
|
|
@@ -430,14 +446,33 @@ function worldSeed(service, root, at) {
|
|
|
430
446
|
if (pending)
|
|
431
447
|
return pending;
|
|
432
448
|
const made = (async () => {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
449
|
+
// The action lock also joins first asks from several renderers/processes to the same seed.
|
|
450
|
+
const { value } = await runAsVendorMove(() => applyTwinWriteAtomic(service, snapshot => {
|
|
451
|
+
const existing = snapshot.find(row => row.type === SEED && row.id === 'seed');
|
|
452
|
+
if (typeof existing?.value === 'string')
|
|
453
|
+
return { kind: 'skip', value: existing.value };
|
|
454
|
+
const value = Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(32))).toString('hex');
|
|
455
|
+
return { kind: 'write', value, write: { operation: 'world_seed.record', subjectType: SEED, subjectId: 'seed', fields: { value }, occurredAt: at, actor: { kind: 'system' } } };
|
|
456
|
+
}, root));
|
|
436
457
|
return value;
|
|
437
458
|
})().finally(() => seeding.delete(key));
|
|
438
459
|
seeding.set(key, made);
|
|
439
460
|
return made;
|
|
440
461
|
}
|
|
462
|
+
/** World-owned generated values use the same private seed and log as pack secrets. */
|
|
463
|
+
export async function worldSecret(service, root, label, at = new Date().toISOString()) {
|
|
464
|
+
return hmac(await worldSeed(service, root, at), label, 'base64url');
|
|
465
|
+
}
|
|
466
|
+
/** The same retained account primitive for pack doors and private native twin fronts. */
|
|
467
|
+
export async function worldAccount(service, root, id = 'default', at = worldNow(), provisioning = false) {
|
|
468
|
+
if (!isWorldAccountName(id))
|
|
469
|
+
throw new Error('Credential account must be a name of at most 128 letters, digits, dots, underscores or hyphens');
|
|
470
|
+
await applyTwinWriteAtomic(service, (snapshot) => {
|
|
471
|
+
const held = snapshot.find(row => row.type === '_world_account' && row.id === id);
|
|
472
|
+
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' } } };
|
|
473
|
+
}, root);
|
|
474
|
+
return id;
|
|
475
|
+
}
|
|
441
476
|
/** A World's signing key for a label (ctx.signingKey): made once from the platform's random source, then read; the
|
|
442
477
|
* making is one per service store and label in the process, as the seed's is. */
|
|
443
478
|
const SIGNING_KEY = '_world_signing_key';
|
|
@@ -472,6 +507,13 @@ function worldSigningKey(service, root, at, label, alg) {
|
|
|
472
507
|
keying.set(key, made);
|
|
473
508
|
return made;
|
|
474
509
|
}
|
|
510
|
+
/** Vendor ids remain unique across its accounts, including public links that carry an id instead of a key. */
|
|
511
|
+
function mintRows(m, resource, root, snapshot) {
|
|
512
|
+
const base = m.resources[resource]?.storedAs ?? resource;
|
|
513
|
+
if (m.tenant?.storage === 'namespaced')
|
|
514
|
+
return (snapshot ?? twinResources(m.service, root)).filter(row => row.type === base || row.type.startsWith(`${base}${TENANT_TYPE}`));
|
|
515
|
+
return snapshot ? snapshot.filter(row => row.type === base) : resourcesOfType(m.service, base, root);
|
|
516
|
+
}
|
|
475
517
|
function mintId(m, resource, root, at, snapshot) {
|
|
476
518
|
if (!m.resources[resource])
|
|
477
519
|
throw new Error(`mint: ${resource} is not a resource ${m.vendor}'s manifest declares`);
|
|
@@ -495,7 +537,7 @@ function mintId(m, resource, root, at, snapshot) {
|
|
|
495
537
|
const width = Number(digits[1]);
|
|
496
538
|
if (!Number.isInteger(width) || width < 1 || width > 77)
|
|
497
539
|
throw new Error('mint: decimal width must be between 1 and 77');
|
|
498
|
-
const held = new Set((
|
|
540
|
+
const held = new Set(mintRows(m, resource, root, snapshot).map((r) => r.id));
|
|
499
541
|
const floor = 10n ** BigInt(width - 1);
|
|
500
542
|
for (let n = held.size + 1;; n++) {
|
|
501
543
|
const id = String(floor + BigInt('0x' + sha256(`${m.service}:${resource}:${n}`)) % (9n * floor));
|
|
@@ -518,7 +560,7 @@ function mintId(m, resource, root, at, snapshot) {
|
|
|
518
560
|
const pattern = new RegExp(`^${escapeRe(String(fill(head, { prefix })))}(\\d+)${escapeRe(String(fill(tail, { prefix })))}$`);
|
|
519
561
|
let max = 0n;
|
|
520
562
|
// 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 (
|
|
563
|
+
for (const r of mintRows(m, resource, root, snapshot)) {
|
|
522
564
|
const hit = pattern.exec(r.id);
|
|
523
565
|
if (hit && BigInt(hit[1]) > max)
|
|
524
566
|
max = BigInt(hit[1]);
|
|
@@ -533,13 +575,13 @@ function mintId(m, resource, root, at, snapshot) {
|
|
|
533
575
|
* and its attachment) must not meet. */
|
|
534
576
|
const lastSnowflake = new Map();
|
|
535
577
|
function mintSnowflake(m, epoch, root, at, snapshot) {
|
|
536
|
-
const ms = BigInt(Date.parse(at
|
|
578
|
+
const ms = BigInt(Date.parse(at) || 0);
|
|
537
579
|
const base = ms > epoch ? (ms - epoch) << 22n : 0n;
|
|
538
580
|
let max = -1n;
|
|
539
581
|
for (const [name, decl] of Object.entries(m.resources)) {
|
|
540
582
|
if (!/^\{snowflake:\d+\}$/.test(decl.ids ?? m.ids.template))
|
|
541
583
|
continue;
|
|
542
|
-
for (const r of (
|
|
584
|
+
for (const r of mintRows(m, name, root, snapshot))
|
|
543
585
|
if (/^\d+$/.test(r.id)) {
|
|
544
586
|
const v = BigInt(r.id);
|
|
545
587
|
if (v > max)
|
|
@@ -555,9 +597,9 @@ function mintSnowflake(m, epoch, root, at, snapshot) {
|
|
|
555
597
|
}
|
|
556
598
|
/** The next KSUID for a resource (Clerk's and Svix's ids, `ksuidFrom`): the World's instant, then the type's mint
|
|
557
599
|
* 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
|
|
600
|
+
function mintKsuid(m, resource, at, root, prefix, snapshot, width) {
|
|
601
|
+
const held = new Set(mintRows(m, resource, root, snapshot).map((r) => r.id));
|
|
602
|
+
const seconds = Math.floor((Date.parse(at) || 0) / 1000);
|
|
561
603
|
for (let n = held.size + 1;; n++) {
|
|
562
604
|
const id = `${prefix}${ksuidFrom(seconds, n, `${m.service}:${resource}:${n}`, width)}`;
|
|
563
605
|
if (!held.has(id))
|
|
@@ -566,8 +608,8 @@ function mintKsuid(m, resource, at, root, prefix = '', snapshot, width = 27) {
|
|
|
566
608
|
}
|
|
567
609
|
/** The next id of `count` lowercase letters for a resource, derived from the resource and the count before it, past
|
|
568
610
|
* any the World already holds. */
|
|
569
|
-
function mintLetters(m, resource, count, root, prefix
|
|
570
|
-
const held = new Set((
|
|
611
|
+
function mintLetters(m, resource, count, root, prefix, snapshot) {
|
|
612
|
+
const held = new Set(mintRows(m, resource, root, snapshot).map((r) => r.id));
|
|
571
613
|
for (let n = held.size + 1;; n++) {
|
|
572
614
|
const id = `${prefix}${lettersFrom(`${m.service}:${resource}:${n}`, count)}`;
|
|
573
615
|
if (!held.has(id))
|
|
@@ -576,8 +618,8 @@ function mintLetters(m, resource, count, root, prefix = '', snapshot) {
|
|
|
576
618
|
}
|
|
577
619
|
/** The next hex id for a resource: N hex digits derived from the resource and the count before it, past any the World
|
|
578
620
|
* already holds (a Cloudflare id, 32). */
|
|
579
|
-
function mintHex(m, resource, count, root, prefix
|
|
580
|
-
const held = new Set((
|
|
621
|
+
function mintHex(m, resource, count, root, prefix, snapshot) {
|
|
622
|
+
const held = new Set(mintRows(m, resource, root, snapshot).map((r) => r.id));
|
|
581
623
|
for (let n = held.size + 1;; n++) {
|
|
582
624
|
const id = `${prefix}${sha256(`${m.service}:${resource}:${n}`).slice(0, count)}`;
|
|
583
625
|
if (!held.has(id))
|
|
@@ -587,8 +629,7 @@ function mintHex(m, resource, count, root, prefix = '', snapshot) {
|
|
|
587
629
|
/** The next UUID for a resource: a version-4-shaped UUID derived from the resource and the count before it, past any
|
|
588
630
|
* the World already holds. */
|
|
589
631
|
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));
|
|
632
|
+
const held = new Set(mintRows(m, resource, root, snapshot).map((r) => r.id));
|
|
592
633
|
for (let n = held.size + 1;; n++) {
|
|
593
634
|
const id = uuidFrom(`${m.service}:${resource}:${n}`);
|
|
594
635
|
if (!held.has(id))
|
|
@@ -849,14 +890,15 @@ async function finishWrite(m, call, resource, row, result, operation, params, ro
|
|
|
849
890
|
const request = requestOfCall.get(call) ?? call.request;
|
|
850
891
|
const context = () => contextFor(m, { ...call, request: request.clone() }, { ...(scopeOfCall.get(call) ?? {}), ...(root !== undefined ? { root } : {}), clock: () => occurredAt });
|
|
851
892
|
if (m.onWrite)
|
|
852
|
-
await m.onWrite({ operation, storedType:
|
|
893
|
+
await m.onWrite({ operation, storedType: m.resources[resource]?.storedAs ?? resource, body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone(), context });
|
|
853
894
|
if (m.events) {
|
|
854
|
-
const
|
|
855
|
-
const
|
|
895
|
+
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) }])) } : {}) });
|
|
896
|
+
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) };
|
|
897
|
+
const write = { operation, storedType: m.resources[resource]?.storedAs ?? resource, body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone() };
|
|
856
898
|
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
899
|
}
|
|
858
900
|
if (m.sockets?.length)
|
|
859
|
-
socketWrite(m.service, root, { operation, storedType:
|
|
901
|
+
socketWrite(m.service, root, { operation, storedType: m.resources[resource]?.storedAs ?? resource, body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone() });
|
|
860
902
|
// a read held until the World writes (held-reads.ts) hears it
|
|
861
903
|
heldReadsWrite(m.service, root, storedType(m, resource));
|
|
862
904
|
// the subject's id as it stands after the write: the vendor's, when a live head minted one
|
|
@@ -1018,8 +1060,8 @@ function adoptedPath(m, call, root) {
|
|
|
1018
1060
|
/** A call's fields with each declared reference that names an adopted subject by its local id moved to the adopted
|
|
1019
1061
|
* id. A call (a handler's operation) need not say which subject type it writes, so every reference the pack declares
|
|
1020
1062
|
* 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);
|
|
1063
|
+
function adoptedInCall(m, fields, root) {
|
|
1064
|
+
const service = m.service, refs = packReferences(service);
|
|
1023
1065
|
if (refs.length === 0)
|
|
1024
1066
|
return fields;
|
|
1025
1067
|
const aliases = aliasesNow(service, root);
|
|
@@ -1028,14 +1070,14 @@ function adoptedInCall(service, fields, root) {
|
|
|
1028
1070
|
let out = fields;
|
|
1029
1071
|
for (const ref of refs) {
|
|
1030
1072
|
const local = ref.key(out);
|
|
1031
|
-
const adopted = local === undefined ? undefined : aliases.get(`${ref.to}:${local}`);
|
|
1073
|
+
const adopted = local === undefined ? undefined : aliases.get(`${storedType(m, ref.to)}:${local}`);
|
|
1032
1074
|
if (adopted !== undefined && adopted !== local)
|
|
1033
1075
|
out = { ...out, ...ref.adopt(out, adopted) };
|
|
1034
1076
|
}
|
|
1035
1077
|
return out;
|
|
1036
1078
|
}
|
|
1037
1079
|
async function boundaryParams(m, call, root) {
|
|
1038
|
-
return adoptedInCall(m
|
|
1080
|
+
return adoptedInCall(m, await readParams(m, call.request, call.operation), root);
|
|
1039
1081
|
}
|
|
1040
1082
|
/** The refusal a subject's fields would draw from its resource's `unique` rules: another live subject (not `except`)
|
|
1041
1083
|
* already holds every field of a rule, each field given. */
|
|
@@ -1237,7 +1279,7 @@ export async function serveCore(m, call, scope = {}) {
|
|
|
1237
1279
|
const parent = parentOfCall(decl, call);
|
|
1238
1280
|
const parentId = parent ? parentValue(parent, call.params) : undefined;
|
|
1239
1281
|
// 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;
|
|
1282
|
+
const tenantField = m.tenant?.storage !== 'namespaced' && m.tenant && !decl.shared ? m.tenant.field : undefined;
|
|
1241
1283
|
const tenant = tenantField && scope.tenant ? await scope.tenant(await contextFor(m, { ...call, request: call.request.clone() }, scope)) : undefined;
|
|
1242
1284
|
if (tenantField && tenant === undefined)
|
|
1243
1285
|
return { unmodeled: `the caller names no tenant (semantics/tenant.ts) for ${resource}` };
|
|
@@ -1433,7 +1475,7 @@ export async function contextFor(m, call, scope) {
|
|
|
1433
1475
|
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
1476
|
// a handler reading the body itself sees the adopted id too (the boundary's one resolution, below)
|
|
1435
1477
|
if (body && typeof body === 'object' && !Array.isArray(body))
|
|
1436
|
-
body = adoptedInCall(m
|
|
1478
|
+
body = adoptedInCall(m, body, root);
|
|
1437
1479
|
// every read of the body is of a copy, so the handler's own `call.request` and the core's answer (`core()`) each
|
|
1438
1480
|
// still find it unread, whichever of them reads it first
|
|
1439
1481
|
const pristine = call.request.clone();
|
|
@@ -1450,6 +1492,13 @@ export async function contextFor(m, call, scope) {
|
|
|
1450
1492
|
const rawResources = new WeakMap();
|
|
1451
1493
|
const rawRow = (resource, row) => {
|
|
1452
1494
|
const name = m.resources[resource] ? resource : Object.keys(m.resources).find((name) => storedType(m, name) === row.type);
|
|
1495
|
+
if (m.tenant?.storage === 'namespaced') {
|
|
1496
|
+
const [base, encoded] = row.type.split(TENANT_TYPE);
|
|
1497
|
+
const owner = m.resources[name ?? resource]?.shared && row[m.tenant.field] !== undefined ? row[m.tenant.field] : encoded ? decodeURIComponent(encoded) : 'default';
|
|
1498
|
+
row = copyResource(row);
|
|
1499
|
+
row.type = base;
|
|
1500
|
+
row[m.tenant.field] = owner;
|
|
1501
|
+
}
|
|
1453
1502
|
rawResources.set(row, name ?? resource);
|
|
1454
1503
|
return row;
|
|
1455
1504
|
};
|
|
@@ -1468,7 +1517,7 @@ export async function contextFor(m, call, scope) {
|
|
|
1468
1517
|
git: (name) => {
|
|
1469
1518
|
if (!/^[A-Za-z0-9_.-]+(\/[A-Za-z0-9_.-]+)*$/.test(name) || name.split('/').some((seg) => seg === '.' || seg === '..'))
|
|
1470
1519
|
throw new Error(`semantics: ${name} is not a git repository name`);
|
|
1471
|
-
const plane = `${worldPaths(m.service, root).dir}/git`;
|
|
1520
|
+
const plane = `${worldPaths(m.service, root).dir}/git${accountScope(m) && accountScope(m) !== 'default' ? `/accounts/${encodeURIComponent(accountScope(m))}` : ''}`;
|
|
1472
1521
|
return { store: new GitObjectStore(getActiveBlobStore(), `${plane}/repos/${name}`), refs: new GitRefs(getActiveWorldStore(), `${plane}/repos/${name}`) };
|
|
1473
1522
|
},
|
|
1474
1523
|
mail: (route, mail, headers) => sendMail(route, mail, headers),
|
|
@@ -1508,17 +1557,21 @@ export async function contextFor(m, call, scope) {
|
|
|
1508
1557
|
return value;
|
|
1509
1558
|
},
|
|
1510
1559
|
occurredAt: at,
|
|
1511
|
-
secret: async (label) => hmac(await worldSeed(m.service, root, at), label, 'base64url'),
|
|
1560
|
+
secret: async (label) => hmac(await worldSeed(m.service, root, at), scopeLabel(m, label), 'base64url'),
|
|
1561
|
+
account: async (field = 'account') => {
|
|
1562
|
+
const fields = body && typeof body === 'object' && !Array.isArray(body) ? body : {};
|
|
1563
|
+
return worldAccount(m.service, root, fields[field] ?? 'default', at, call.request.headers.get('x-volter-world-activity') === 'provisioning');
|
|
1564
|
+
},
|
|
1512
1565
|
record: async (type, fields, id, operation) => {
|
|
1513
1566
|
if (!type.startsWith('_'))
|
|
1514
1567
|
throw new Error(`semantics: ${type} is not a bookkeeping type (bookkeeping types start with _)`);
|
|
1515
1568
|
// a bookkeeping subject a resource counts (`counts` naming it: sentry's issue users) keeps that count as any
|
|
1516
1569
|
// child does, the parent it left recounted too
|
|
1517
|
-
const keeps = counted(m, type);
|
|
1570
|
+
const keeps = counted(m, type), physicalType = storedType(m, type);
|
|
1518
1571
|
if (id !== undefined) {
|
|
1519
1572
|
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:
|
|
1573
|
+
const prior = keeps ? resourcesOfType(m.service, physicalType, root).find((r) => r.id === id) : undefined;
|
|
1574
|
+
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
1575
|
if (operation)
|
|
1523
1576
|
observeStateWrite(m, result, before, root, stateChecks);
|
|
1524
1577
|
if (keeps)
|
|
@@ -1531,7 +1584,7 @@ export async function contextFor(m, call, scope) {
|
|
|
1531
1584
|
const prefix = `${type.slice(1)}_`;
|
|
1532
1585
|
let max = 0n;
|
|
1533
1586
|
for (const r of resources) {
|
|
1534
|
-
if (r.type !==
|
|
1587
|
+
if (r.type !== physicalType)
|
|
1535
1588
|
continue;
|
|
1536
1589
|
const digits = r.id.startsWith(prefix) ? r.id.slice(prefix.length) : '';
|
|
1537
1590
|
if (/^\d+$/.test(digits) && BigInt(digits) > max)
|
|
@@ -1539,21 +1592,21 @@ export async function contextFor(m, call, scope) {
|
|
|
1539
1592
|
}
|
|
1540
1593
|
return `${prefix}${max + 1n}`;
|
|
1541
1594
|
})();
|
|
1542
|
-
return { kind: 'write', value: subject, write: { operation: operation ?? `${type.slice(1)}.record`, subjectType:
|
|
1543
|
-
}, root, () => resourcesOfType(m.service,
|
|
1595
|
+
return { kind: 'write', value: subject, write: { operation: operation ?? `${type.slice(1)}.record`, subjectType: physicalType, subjectId: subject, fields, ...provisioningInput(call), occurredAt: at, actor: { kind: 'system' } } };
|
|
1596
|
+
}, root, () => resourcesOfType(m.service, physicalType, root));
|
|
1544
1597
|
if (operation && result)
|
|
1545
1598
|
observeStateWrite(m, result, undefined, root, stateChecks);
|
|
1546
|
-
const made = keeps && value !== undefined ? resourcesOfType(m.service,
|
|
1599
|
+
const made = keeps && value !== undefined ? resourcesOfType(m.service, physicalType, root).find((r) => r.id === value) : undefined;
|
|
1547
1600
|
if (made)
|
|
1548
1601
|
await keepCounts(m, call, type, made, params, root, at);
|
|
1549
1602
|
return value;
|
|
1550
1603
|
},
|
|
1551
1604
|
get machines() {
|
|
1552
1605
|
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
|
|
1606
|
+
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
1607
|
},
|
|
1555
|
-
signingKey: (label, alg) => worldSigningKey(m.service, root, at, label, alg ?? 'RS256'),
|
|
1556
|
-
heldSigningKey: (label, alg) => heldWorldSigningKey(m.service, root, label, alg ?? 'RS256'),
|
|
1608
|
+
signingKey: (label, alg) => worldSigningKey(m.service, root, at, scopeLabel(m, label), alg ?? 'RS256'),
|
|
1609
|
+
heldSigningKey: (label, alg) => heldWorldSigningKey(m.service, root, scopeLabel(m, label), alg ?? 'RS256'),
|
|
1557
1610
|
create: (resource, fields, operation) => atomicWrite(m, call, resource, (snapshot) => {
|
|
1558
1611
|
const id = mintId(m, resource, root, at, snapshot);
|
|
1559
1612
|
return { id, fields: typeof fields === 'function' ? fields(id) : fields };
|
|
@@ -1566,7 +1619,7 @@ export async function contextFor(m, call, scope) {
|
|
|
1566
1619
|
throw new Error('createUnique requires declared unique keys');
|
|
1567
1620
|
let existing;
|
|
1568
1621
|
const row = await atomicWrite(m, call, resource, (snapshot) => {
|
|
1569
|
-
const id = mintId(m, resource, root, at, snapshot);
|
|
1622
|
+
const id = options?.id ?? mintId(m, resource, root, at, snapshot);
|
|
1570
1623
|
const selected = typeof fields === 'function' ? fields(id) : fields;
|
|
1571
1624
|
const hit = snapshot.find((r) => r.type === storedType(m, resource) && r.deleted !== true && rules.some((rule) => rule.fields.every((f) => selected[f] !== undefined && view(m, resource, r)[f] === selected[f])));
|
|
1572
1625
|
if (hit) {
|
|
@@ -1612,7 +1665,17 @@ export async function contextFor(m, call, scope) {
|
|
|
1612
1665
|
ok: (fields) => Response.json({ ...(m.success ?? {}), ...fields }),
|
|
1613
1666
|
row: (resource, id, opts) => { const row = stored(m, resource, root, opts).find((r) => r.id === id); return row ? rawRow(resource, row) : undefined; },
|
|
1614
1667
|
rowsRaw: (resource, opts) => stored(m, resource, root, opts).map((row) => rawRow(resource, row)),
|
|
1615
|
-
tree: () =>
|
|
1668
|
+
tree: () => {
|
|
1669
|
+
const rows = twinResources(m.service, root);
|
|
1670
|
+
if (m.tenant?.storage !== 'namespaced')
|
|
1671
|
+
return rows;
|
|
1672
|
+
const account = accountScope(m);
|
|
1673
|
+
return rows.filter(row => {
|
|
1674
|
+
const [base, encoded] = row.type.split(TENANT_TYPE);
|
|
1675
|
+
const name = Object.keys(m.resources).find(name => (m.resources[name]?.storedAs ?? name) === base) ?? base;
|
|
1676
|
+
return account === undefined || m.resources[name]?.shared || (encoded ? decodeURIComponent(encoded) : 'default') === account;
|
|
1677
|
+
}).map(row => rawRow(row.type.split(TENANT_TYPE)[0], row));
|
|
1678
|
+
},
|
|
1616
1679
|
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
1680
|
raw: (body, init = {}) => new Response(body, { status: init.status ?? 200, headers: init.headers ?? {} }),
|
|
1618
1681
|
atomically: async (decide) => {
|
|
@@ -1667,17 +1730,17 @@ export async function contextFor(m, call, scope) {
|
|
|
1667
1730
|
engine: scope.database === undefined ? unboundDatabase(m.service) : managedDatabase(scope.database, { readOnly: scope.readOnly === true || isReadOnlyRequest(call.request) }),
|
|
1668
1731
|
redis: (commands, options) => execRedisRun(commands, {
|
|
1669
1732
|
root, occurredAt: at, service: m.service, readOnly: scope.readOnly === true || isReadOnlyRequest(call.request), dialect: options.dialect,
|
|
1670
|
-
...(options.database === undefined ? {} : { database: options.database }),
|
|
1733
|
+
...(accountScope(m) && accountScope(m) !== 'default' ? { database: scopeLabel(m, options.database ?? 'default') } : options.database === undefined ? {} : { database: options.database }),
|
|
1671
1734
|
...(options.transaction === undefined ? {} : { transaction: options.transaction }),
|
|
1672
1735
|
}),
|
|
1673
|
-
live: liveAccess({ rows: (type) =>
|
|
1736
|
+
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
1737
|
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),
|
|
1738
|
+
put: (key, bytes) => writeResourceBlob(m.service, scopeLabel(m, key), bytes, root),
|
|
1739
|
+
putStream: (key, body, accept) => writeResourceBlobStream(m.service, scopeLabel(m, key), body, accept, root),
|
|
1740
|
+
get: (key) => readResourceBlob(m.service, scopeLabel(m, key), root),
|
|
1741
|
+
remove: (key) => removeResourceBlob(m.service, scopeLabel(m, key), root),
|
|
1742
|
+
size: (key) => resourceBlobSize(m.service, scopeLabel(m, key), root),
|
|
1743
|
+
range: (key, start, endInclusive) => readResourceBlobRange(m.service, scopeLabel(m, key), start, endInclusive, root),
|
|
1681
1744
|
},
|
|
1682
1745
|
merge: (resource, prior, next) => mergeField(m.resources[resource]?.update, prior, next),
|
|
1683
1746
|
conflict: (resource, fields, except) => {
|
|
@@ -2005,7 +2068,7 @@ export function authRefusal(m, request, root) {
|
|
|
2005
2068
|
const held = m.auth.held;
|
|
2006
2069
|
if (held && !held.standing?.includes(key)) {
|
|
2007
2070
|
const hash = sha256(key);
|
|
2008
|
-
const holds =
|
|
2071
|
+
const holds = stored(m, held.storedAs, root).some((r) => r.deleted !== true && r[held.hashField] === hash && (held.unless === undefined || r[held.unless] !== true));
|
|
2009
2072
|
if (!holds)
|
|
2010
2073
|
return vendorError(errorManifest, invalid);
|
|
2011
2074
|
}
|
|
@@ -2101,7 +2164,7 @@ export function crossCutting(m, opts = {}) {
|
|
|
2101
2164
|
const url = new URL(request.url);
|
|
2102
2165
|
const signature = hashFieldValue({ path: url.pathname + url.search, body: await bodyTextOf(request.clone()) });
|
|
2103
2166
|
const id = `idem_${hex(key)}`;
|
|
2104
|
-
const prior = twinResources(m.service, opts.root).find((r) => r.type === m.idempotency.storedAs && r.id === id);
|
|
2167
|
+
const prior = twinResources(m.service, opts.root).find((r) => r.type === storedType(m, m.idempotency.storedAs) && r.id === id);
|
|
2105
2168
|
if (prior) {
|
|
2106
2169
|
if (m.idempotency.conflict && prior.requestHash !== undefined && prior.requestHash !== signature)
|
|
2107
2170
|
return vendorError(m, m.idempotency.conflict);
|
|
@@ -2117,7 +2180,7 @@ export function crossCutting(m, opts = {}) {
|
|
|
2117
2180
|
return response;
|
|
2118
2181
|
const contentType = response.headers.get('content-type') ?? 'application/json';
|
|
2119
2182
|
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);
|
|
2183
|
+
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
2184
|
return response;
|
|
2122
2185
|
}
|
|
2123
2186
|
}
|
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";
|
package/dist/src/machines.d.ts
CHANGED
|
@@ -17,18 +17,6 @@ export type MachineSpec = {
|
|
|
17
17
|
signal?: AbortSignal;
|
|
18
18
|
startTimeoutMs?: number;
|
|
19
19
|
};
|
|
20
|
-
export type GuestImage = {
|
|
21
|
-
context: string;
|
|
22
|
-
dockerfile?: string;
|
|
23
|
-
runtime?: 'node' | 'bun' | 'python' | 'deno';
|
|
24
|
-
command?: string[];
|
|
25
|
-
cwd?: string;
|
|
26
|
-
env?: Record<string, string>;
|
|
27
|
-
prepared?: boolean;
|
|
28
|
-
};
|
|
29
|
-
export type LightPoolOptions = {
|
|
30
|
-
images?: Record<string, GuestImage>;
|
|
31
|
-
};
|
|
32
20
|
export type MachineRun = {
|
|
33
21
|
ports: Array<{
|
|
34
22
|
internal: number;
|
|
@@ -85,7 +73,7 @@ export type MachineHttpReply = {
|
|
|
85
73
|
body?: string;
|
|
86
74
|
};
|
|
87
75
|
export type MachinePool = {
|
|
88
|
-
readonly kind: 'docker' | 'none' | 'fake'
|
|
76
|
+
readonly kind: 'docker' | 'none' | 'fake';
|
|
89
77
|
/** Optional capability admission, before vendor resource creation; starts no process. */
|
|
90
78
|
admitRun?(spec: MachineSpec): Promise<void>;
|
|
91
79
|
/** runs the image; a failure (no container runtime, an image that will not start) rejects with the runtime's message */
|
|
@@ -115,10 +103,10 @@ export type MachinePool = {
|
|
|
115
103
|
/** A declared compute capability gap, distinct from a failed admitted guest. */
|
|
116
104
|
export declare function isMachineCapabilityGap(error: unknown): boolean;
|
|
117
105
|
export declare const MACHINE_POOL = "_machine_pool";
|
|
118
|
-
export declare const POOL_KINDS: readonly ["docker", "none", "fake"
|
|
106
|
+
export declare const POOL_KINDS: readonly ["docker", "none", "fake"];
|
|
119
107
|
/** The pool of a kind, for a World (`world`: the mark its containers carry). */
|
|
120
|
-
export declare function machinePool(kind: MachinePool['kind'], world?: string, httpReplies?: readonly MachineHttpReply[]
|
|
121
|
-
export declare function poolFor(service: string, root: string | undefined, kind: MachinePool['kind'], httpReplies?: readonly MachineHttpReply[]
|
|
108
|
+
export declare function machinePool(kind: MachinePool['kind'], world?: string, httpReplies?: readonly MachineHttpReply[]): MachinePool;
|
|
109
|
+
export declare function poolFor(service: string, root: string | undefined, kind: MachinePool['kind'], httpReplies?: readonly MachineHttpReply[]): MachinePool;
|
|
122
110
|
/** Re-enrolling a verification pool starts a fresh fake execution record. */
|
|
123
111
|
export declare function resetFakePool(service: string, root: string | undefined): void;
|
|
124
112
|
/** Every machine mutation shares the tree's refusal seam. HTTP/RPC reads must be explicitly
|
package/dist/src/machines.js
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
-
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
return path;
|
|
8
|
-
};
|
|
9
1
|
// The World's machine pool (architecture, "Other wires: … machines"): what runs a vendor's customer's image when the
|
|
10
2
|
// vendor is one that runs images (Fly's Machines). It is a platform provider enrolled through the kernel's door
|
|
11
3
|
// `POST /_twin/machine-pool`, recorded in the pack's own tree; a handler reaches it as `ctx.machines`. `docker` runs
|
|
12
4
|
// each machine as a local container with its internal ports published on loopback; `none` runs nothing; `fake`
|
|
13
5
|
// records what it was asked (capability verification). Nothing here runs at module scope, and `node:child_process`
|
|
14
|
-
// is reached only when a compute pool is asked to act.
|
|
6
|
+
// is reached only when a compute pool is asked to act.
|
|
15
7
|
import { refuseReadOnlyWrite } from "./request-scope.js";
|
|
16
8
|
import { nodeBuiltin } from "./serve-http.js";
|
|
17
9
|
import { refuseRequiredMachineTransport } from '../required-credential-transport.cjs';
|
|
@@ -20,33 +12,7 @@ export function isMachineCapabilityGap(error) {
|
|
|
20
12
|
return typeof error === 'object' && error !== null && 'code' in error && error.code === 'WORLD_MACHINE_CAPABILITY_GAP';
|
|
21
13
|
}
|
|
22
14
|
export const MACHINE_POOL = '_machine_pool';
|
|
23
|
-
export const POOL_KINDS = ['docker', 'none', 'fake'
|
|
24
|
-
/** The host owns provider bootstrapping. Browser/kernel imports stay inert, and a missing
|
|
25
|
-
* host provider refuses rather than falling back to metadata-only success. */
|
|
26
|
-
function lightPool(world, root, options) {
|
|
27
|
-
let ready;
|
|
28
|
-
const load = () => ready ??= (async () => {
|
|
29
|
-
const entry = typeof process === 'undefined' ? undefined : process.env.VOLTER_WORLD_GUEST_RUNNER;
|
|
30
|
-
if (!entry)
|
|
31
|
-
throw new Error('light guest runner is unavailable: run this twin inside a World');
|
|
32
|
-
const provider = await import(__rewriteRelativeImportExtension(entry));
|
|
33
|
-
return provider.createLightGuestPool({ world, root, ...options });
|
|
34
|
-
})();
|
|
35
|
-
return {
|
|
36
|
-
kind: 'light',
|
|
37
|
-
admitRun: async (spec) => { await (await load()).admitRun?.(spec); },
|
|
38
|
-
run: async (spec) => (await load()).run(spec),
|
|
39
|
-
exec: async (name, spec) => (await load()).exec(name, spec),
|
|
40
|
-
request: async (name, port, request) => (await load()).request(name, port, request),
|
|
41
|
-
status: async (name) => (await load()).status(name),
|
|
42
|
-
ports: async (name) => (await load()).ports(name),
|
|
43
|
-
files: async (name) => (await load()).files(name),
|
|
44
|
-
pause: async (name) => (await load()).pause(name),
|
|
45
|
-
resume: async (name) => (await load()).resume(name),
|
|
46
|
-
stop: async (name) => (await load()).stop(name),
|
|
47
|
-
remove: async (name) => (await load()).remove(name),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
15
|
+
export const POOL_KINDS = ['docker', 'none', 'fake'];
|
|
50
16
|
/** A container's name for a machine: the World's prefix, the World's own mark (a machine id repeats across Worlds and
|
|
51
17
|
* branches, which mint alike), then the vendor's machine name made safe for docker. */
|
|
52
18
|
const containerName = (world, name) => `volter-world-machine-${world}-${name.replace(/[^A-Za-z0-9_.-]/g, '-')}`;
|
|
@@ -66,8 +32,8 @@ function dockerPool(world) {
|
|
|
66
32
|
const state = JSON.parse(await docker(['inspect', '--format', '{{json .State}}', containerName(world, name)]));
|
|
67
33
|
return state.Running ? { state: 'started' } : { state: 'exited', exitCode: state.ExitCode, ...(state.Error ? { reason: state.Error } : {}) };
|
|
68
34
|
},
|
|
69
|
-
async ports() { throw new Error('port discovery
|
|
70
|
-
async files() { throw new Error('file access
|
|
35
|
+
async ports() { throw new Error('port discovery is unavailable'); },
|
|
36
|
+
async files() { throw new Error('file access is unavailable'); },
|
|
71
37
|
async run(spec) {
|
|
72
38
|
refuseRequiredMachineTransport(process.env, 'Docker machine application transport');
|
|
73
39
|
const name = containerName(world, spec.name);
|
|
@@ -175,9 +141,9 @@ function dockerPool(world) {
|
|
|
175
141
|
};
|
|
176
142
|
}
|
|
177
143
|
/** The pool of a kind, for a World (`world`: the mark its containers carry). */
|
|
178
|
-
export function machinePool(kind, world = 'world', httpReplies = []
|
|
179
|
-
if (kind
|
|
180
|
-
|
|
144
|
+
export function machinePool(kind, world = 'world', httpReplies = []) {
|
|
145
|
+
if (!POOL_KINDS.includes(kind))
|
|
146
|
+
throw new Error(`kind must be one of ${POOL_KINDS.join(', ')}`);
|
|
181
147
|
if (kind === 'docker')
|
|
182
148
|
return dockerPool(world);
|
|
183
149
|
if (kind === 'fake') {
|
|
@@ -231,15 +197,15 @@ export function machinePool(kind, world = 'world', httpReplies = [], light = {},
|
|
|
231
197
|
}
|
|
232
198
|
/** A vendor's pool in a World, one per World and kind for the life of the process (a fake pool keeps what it was asked). */
|
|
233
199
|
const pools = new Map();
|
|
234
|
-
export function poolFor(service, root, kind, httpReplies = []
|
|
235
|
-
const key = `${service}\0${root ?? ''}\0${kind}
|
|
200
|
+
export function poolFor(service, root, kind, httpReplies = []) {
|
|
201
|
+
const key = `${service}\0${root ?? ''}\0${kind}`;
|
|
236
202
|
let pool = pools.get(key);
|
|
237
203
|
if (!pool) {
|
|
238
204
|
// the World's mark: a short digest of the vendor and the World's tree, so two Worlds' machines never share a name
|
|
239
205
|
let h = 0;
|
|
240
206
|
for (const ch of `${service}:${root ?? ''}`)
|
|
241
207
|
h = (Math.imul(h, 31) + ch.charCodeAt(0)) >>> 0;
|
|
242
|
-
pool = machinePool(kind, h.toString(16).padStart(8, '0'), httpReplies
|
|
208
|
+
pool = machinePool(kind, h.toString(16).padStart(8, '0'), httpReplies);
|
|
243
209
|
pools.set(key, pool);
|
|
244
210
|
}
|
|
245
211
|
return pool;
|