@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.
@@ -1,3 +1,5 @@
1
+ import { storageTenant } from './request-scope.ts';
2
+ import { isWorldAccountName } from './world-account-name.ts';
1
3
  import { parseExactJson, compareExactNumbers, exactInteger, isExactIntegerValue } from './exact-json.ts';
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
@@ -20,7 +22,7 @@ import { openSessions, socketWrite, type SocketDecl, type SocketSession } from '
20
22
  import { liveAccess } from './live/access.ts';
21
23
  import type { LiveAccess, LiveDecl } from './live/types.ts';
22
24
  import { awaitWrite, awaitCondition, heldReadsWrite } from './held-reads.ts';
23
- import { MACHINE_POOL, poolFor, guardedMachinePool, type MachinePool, type MachineHttpReply, type LightPoolOptions } from './machines.ts';
25
+ import { MACHINE_POOL, poolFor, guardedMachinePool, type MachinePool, type MachineHttpReply } from './machines.ts';
24
26
  import { askApplication, deliverEvents, type ApplicationAnswer, type ApplicationStandIn, type EventRender, type EventScheme, type EventsDecl, type EventValues } from './events.ts';
25
27
  import type { RateBudgetDeclaration } from './rateBudget.ts';
26
28
  import { handlerCrypto, hmac, ksuidFrom, lettersFrom, sha256, uuidFrom, type HandlerCrypto } from './signing.ts';
@@ -524,7 +526,7 @@ export type DerivedManifest = {
524
526
  * an organization, a team, an account — and a caller reaches only its own tenant's, as a parent's children are
525
527
  * reached under it. `field` holds it on each subject; `semantics/tenant.ts` names the caller's. The core scopes every
526
528
  * read, list, update and delete by it and stamps it on every create, for each resource not `shared`. */
527
- tenant?: { field: string };
529
+ tenant?: { field: string; storage?: 'namespaced' };
528
530
  /** The vendor's own lanes behind this manifest's API ("Other wires: lanes"), dispatched by the kernel before the
529
531
  * vendor's own doors, clock and API (LanesDecl). */
530
532
  lanes?: LanesDecl;
@@ -744,13 +746,19 @@ function notFound(manifest: DerivedManifest, object: string, id: string, param?:
744
746
 
745
747
  // ── the tree ─────────────────────────────────────────────────────────────────────────────────
746
748
 
747
- export const storedType = (m: DerivedManifest, resource: string): string => m.resources[resource]?.storedAs ?? resource;
749
+ const TENANT_TYPE = '::account:';
750
+ const accountScope = (m: DerivedManifest): string | undefined => m.tenant?.storage === 'namespaced' ? storageTenant(m.service) : undefined;
751
+ const scopeLabel = (m: DerivedManifest, label: string): string => { const account = accountScope(m); return account && account !== 'default' ? `account:${account}:${label}` : label; };
752
+ export const storedType = (m: DerivedManifest, resource: string): string => {
753
+ const base = m.resources[resource]?.storedAs ?? resource, account = accountScope(m);
754
+ return account && account !== 'default' && !m.resources[resource]?.shared ? `${base}${TENANT_TYPE}${encodeURIComponent(account)}` : base;
755
+ };
748
756
 
749
757
  // A service's resources by type, folded once per World state (the tree's stamp; any write moves it), not on every read:
750
758
  // a read of one resource type projected the service's whole tree, so a pack whose tree also holds many subjects of
751
759
  // other types (PlanetScale's SQL rows beside its management records) paid for all of them on each read. A read gets its
752
760
  // own copy of each resource, as twinResources hands out, with the vendor's own id/type/updatedAt a spread would drop.
753
- const typeIndexes = new WeakMap<object, Map<string, { stamp: string; byType: Map<string, Map<string, TwinResource>> }>>();
761
+ const typeIndexes = new WeakMap<object, Map<string, { stamp: string; byType: Map<string, Map<string, TwinResource>>; typeByKey: Map<string, string> }>>();
754
762
  /** A service's resources of one type, each the reader's own copy: `twinResources` filtered by type, from an index
755
763
  * that follows each write rather than projecting the whole tree per call. */
756
764
  export function resourcesOfType(service: string, type: string, root?: string): TwinResource[] {
@@ -764,8 +772,13 @@ export function resourcesOfType(service: string, type: string, root?: string): T
764
772
  if (held && delta) {
765
773
  // the index keeps the tree's order: a subject updated where it stands stays there, one new to the tree (or made
766
774
  // again after a delete) goes to the end in the order the tree holds it
767
- for (const k of [...delta.removed, ...delta.appended]) held.byType.get(k.slice(0, k.indexOf(':')))?.delete(k);
775
+ for (const k of [...delta.removed, ...delta.appended]) {
776
+ const type = held.typeByKey.get(k);
777
+ if (type !== undefined) held.byType.get(type)?.delete(k);
778
+ held.typeByKey.delete(k);
779
+ }
768
780
  const byKey = new Map(delta.changed.map((r) => [`${r.type}:${r.id}`, r]));
781
+ for (const [k, r] of byKey) held.typeByKey.set(k, r.type);
769
782
  for (const [k, r] of byKey) if (!delta.appended.includes(k)) (held.byType.get(r.type) ?? held.byType.set(r.type, new Map()).get(r.type)!).set(k, r);
770
783
  for (const k of delta.appended) { const r = byKey.get(k)!; (held.byType.get(r.type) ?? held.byType.set(r.type, new Map()).get(r.type)!).set(k, r); }
771
784
  held.stamp = delta.stamp;
@@ -775,7 +788,7 @@ export function resourcesOfType(service: string, type: string, root?: string): T
775
788
  if (!held || held.stamp !== stamp) {
776
789
  const byType = new Map<string, Map<string, TwinResource>>();
777
790
  for (const r of twinResources(service, root)) (byType.get(r.type) ?? byType.set(r.type, new Map()).get(r.type)!).set(`${r.type}:${r.id}`, r);
778
- held = { stamp, byType };
791
+ held = { stamp, byType, typeByKey: new Map([...byType].flatMap(([type, rows]) => [...rows.keys()].map(key => [key, type] as const))) };
779
792
  indexes.set(key, held);
780
793
  }
781
794
  }
@@ -785,7 +798,9 @@ export function resourcesOfType(service: string, type: string, root?: string): T
785
798
  function stored(m: DerivedManifest, resource: string, root?: string, opts: { withDeleted?: boolean } = {}): TwinResource[] {
786
799
  const type = storedType(m, resource);
787
800
  const when = Object.entries(m.resources[resource]?.readableWhen ?? {});
788
- return resourcesOfType(m.service, type, root).filter((r) => (opts.withDeleted || r.deleted !== true) && when.every(([k, v]) => (r as Record<string, unknown>)[k] === v));
801
+ const rows = m.tenant?.storage === 'namespaced' && accountScope(m) === undefined && !m.resources[resource]?.shared
802
+ ? twinResources(m.service, root).filter(row => row.type === type || row.type.startsWith(`${type}${TENANT_TYPE}`)) : resourcesOfType(m.service, type, root);
803
+ return rows.filter((r) => (opts.withDeleted || r.deleted !== true) && when.every(([k, v]) => (r as Record<string, unknown>)[k] === v));
789
804
  }
790
805
 
791
806
  /** The subject an item operation names: the manifest's key over the path parameters, or the last one. */
@@ -821,7 +836,7 @@ function missingParent(m: DerivedManifest, resource: string, call: DerivedCall,
821
836
  const id = parentValue(parent, call.params);
822
837
  const where = Object.entries(parent.where ?? {}).map(([field, template]) => [field, String(fill(template, call.params))] as const);
823
838
  const found = (r: TwinResource): boolean => (where.length ? where.every(([field, v]) => String(parentField(r, field)) === v) : r.id === id)
824
- && (!m.tenant || m.resources[parent.resource]?.shared || (r as Record<string, unknown>)[m.tenant.field] === tenant);
839
+ && (!m.tenant || m.tenant.storage === 'namespaced' || m.resources[parent.resource]?.shared || (r as Record<string, unknown>)[m.tenant.field] === tenant);
825
840
  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));
826
841
  }
827
842
 
@@ -953,15 +968,34 @@ function worldSeed(service: string, root: string | undefined, at: string): Promi
953
968
  const pending = seeding.get(key);
954
969
  if (pending) return pending;
955
970
  const made = (async () => {
956
- const value = Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(32))).toString('hex');
957
- // the World's own bookkeeping, made on first use whatever the request: a read-only one lands it too (a vendor's move)
958
- await runAsVendorMove(() => applyTwinWrite(service, { operation: 'world_seed.record', subjectType: SEED, subjectId: 'seed', fields: { value }, occurredAt: at, actor: { kind: 'system' } }, root));
971
+ // The action lock also joins first asks from several renderers/processes to the same seed.
972
+ const { value } = await runAsVendorMove(() => applyTwinWriteAtomic(service, snapshot => {
973
+ const existing = snapshot.find(row => row.type === SEED && row.id === 'seed');
974
+ if (typeof existing?.value === 'string') return { kind: 'skip', value: existing.value };
975
+ const value = Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(32))).toString('hex');
976
+ return { kind: 'write', value, write: { operation: 'world_seed.record', subjectType: SEED, subjectId: 'seed', fields: { value }, occurredAt: at, actor: { kind: 'system' } } };
977
+ }, root));
959
978
  return value;
960
979
  })().finally(() => seeding.delete(key));
961
980
  seeding.set(key, made);
962
981
  return made;
963
982
  }
964
983
 
984
+ /** World-owned generated values use the same private seed and log as pack secrets. */
985
+ export async function worldSecret(service: string, root: string, label: string, at = new Date().toISOString()): Promise<string> {
986
+ return hmac(await worldSeed(service, root, at), label, 'base64url');
987
+ }
988
+
989
+ /** The same retained account primitive for pack doors and private native twin fronts. */
990
+ export async function worldAccount(service: string, root: string | undefined, id: unknown = 'default', at = worldNow(), provisioning = false): Promise<string> {
991
+ if (!isWorldAccountName(id)) throw new Error('Credential account must be a name of at most 128 letters, digits, dots, underscores or hyphens');
992
+ await applyTwinWriteAtomic(service, (snapshot) => {
993
+ const held = snapshot.find(row => row.type === '_world_account' && row.id === id);
994
+ 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' } } };
995
+ }, root);
996
+ return id;
997
+ }
998
+
965
999
  /** A World's signing key for a label (ctx.signingKey): made once from the platform's random source, then read; the
966
1000
  * making is one per service store and label in the process, as the seed's is. */
967
1001
  const SIGNING_KEY = '_world_signing_key';
@@ -995,7 +1029,13 @@ function worldSigningKey(service: string, root: string | undefined, at: string,
995
1029
  return made;
996
1030
  }
997
1031
 
998
- function mintId(m: DerivedManifest, resource: string, root?: string, at?: string, snapshot?: readonly TwinResource[]): string {
1032
+ /** Vendor ids remain unique across its accounts, including public links that carry an id instead of a key. */
1033
+ function mintRows(m: DerivedManifest, resource: string, root?: string, snapshot?: readonly TwinResource[]): readonly TwinResource[] {
1034
+ const base = m.resources[resource]?.storedAs ?? resource;
1035
+ if (m.tenant?.storage === 'namespaced') return (snapshot ?? twinResources(m.service, root)).filter(row => row.type === base || row.type.startsWith(`${base}${TENANT_TYPE}`));
1036
+ return snapshot ? snapshot.filter(row => row.type === base) : resourcesOfType(m.service, base, root);
1037
+ }
1038
+ function mintId(m: DerivedManifest, resource: string, root: string | undefined, at: string, snapshot?: readonly TwinResource[]): string {
999
1039
  if (!m.resources[resource]) throw new Error(`mint: ${resource} is not a resource ${m.vendor}'s manifest declares`);
1000
1040
  const prefix = m.resources[resource]!.idPrefix;
1001
1041
  const template = m.resources[resource]!.ids ?? m.ids.template;
@@ -1012,7 +1052,7 @@ function mintId(m: DerivedManifest, resource: string, root?: string, at?: string
1012
1052
  if (digits) {
1013
1053
  const width = Number(digits[1]);
1014
1054
  if (!Number.isInteger(width) || width < 1 || width > 77) throw new Error('mint: decimal width must be between 1 and 77');
1015
- const held = new Set((snapshot ? snapshot.filter((r) => r.type === storedType(m, resource)) : resourcesOfType(m.service, storedType(m, resource), root)).map((r) => r.id));
1055
+ const held = new Set(mintRows(m, resource, root, snapshot).map((r) => r.id));
1016
1056
  const floor = 10n ** BigInt(width - 1);
1017
1057
  for (let n = held.size + 1; ; n++) {
1018
1058
  const id = String(floor + BigInt('0x' + sha256(`${m.service}:${resource}:${n}`)) % (9n * floor));
@@ -1031,7 +1071,7 @@ function mintId(m: DerivedManifest, resource: string, root?: string, at?: string
1031
1071
  const pattern = new RegExp(`^${escapeRe(String(fill(head!, { prefix })))}(\\d+)${escapeRe(String(fill(tail, { prefix })))}$`);
1032
1072
  let max = 0n;
1033
1073
  // the one type, from the index that follows writes (a scan of the whole World per create grew with it)
1034
- for (const r of (snapshot ? snapshot.filter((r) => r.type === storedType(m, resource)) : resourcesOfType(m.service, storedType(m, resource), root))) {
1074
+ for (const r of mintRows(m, resource, root, snapshot)) {
1035
1075
  const hit = pattern.exec(r.id);
1036
1076
  if (hit && BigInt(hit[1]!) > max) max = BigInt(hit[1]!);
1037
1077
  }
@@ -1045,13 +1085,13 @@ function mintId(m: DerivedManifest, resource: string, root?: string, at?: string
1045
1085
  /** The last snowflake minted per vendor and World in this process: two mints before the first is written (a message
1046
1086
  * and its attachment) must not meet. */
1047
1087
  const lastSnowflake = new Map<string, bigint>();
1048
- function mintSnowflake(m: DerivedManifest, epoch: bigint, root?: string, at?: string, snapshot?: readonly TwinResource[]): string {
1049
- const ms = BigInt(Date.parse(at ?? '') || 0);
1088
+ function mintSnowflake(m: DerivedManifest, epoch: bigint, root: string | undefined, at: string, snapshot?: readonly TwinResource[]): string {
1089
+ const ms = BigInt(Date.parse(at) || 0);
1050
1090
  const base = ms > epoch ? (ms - epoch) << 22n : 0n;
1051
1091
  let max = -1n;
1052
1092
  for (const [name, decl] of Object.entries(m.resources)) {
1053
1093
  if (!/^\{snowflake:\d+\}$/.test(decl.ids ?? m.ids.template)) continue;
1054
- for (const r of (snapshot ? snapshot.filter((r) => r.type === storedType(m, name)) : resourcesOfType(m.service, storedType(m, name), root))) if (/^\d+$/.test(r.id)) { const v = BigInt(r.id); if (v > max) max = v; }
1094
+ for (const r of mintRows(m, name, root, snapshot)) if (/^\d+$/.test(r.id)) { const v = BigInt(r.id); if (v > max) max = v; }
1055
1095
  }
1056
1096
  const key = `${m.service}\0${root ?? ''}`;
1057
1097
  const last = lastSnowflake.get(key) ?? -1n;
@@ -1063,9 +1103,9 @@ function mintSnowflake(m: DerivedManifest, epoch: bigint, root?: string, at?: st
1063
1103
 
1064
1104
  /** The next KSUID for a resource (Clerk's and Svix's ids, `ksuidFrom`): the World's instant, then the type's mint
1065
1105
  * count, so a frozen clock still lists them as made; past any the World already holds. */
1066
- function mintKsuid(m: DerivedManifest, resource: string, at: string | undefined, root?: string, prefix = '', snapshot?: readonly TwinResource[], width = 27): string {
1067
- const held = new Set((snapshot ? snapshot.filter((r) => r.type === storedType(m, resource)) : resourcesOfType(m.service, storedType(m, resource), root)).map((r) => r.id));
1068
- const seconds = Math.floor((Date.parse(at ?? '') || 0) / 1000);
1106
+ function mintKsuid(m: DerivedManifest, resource: string, at: string, root: string | undefined, prefix: string, snapshot: readonly TwinResource[] | undefined, width: number): string {
1107
+ const held = new Set(mintRows(m, resource, root, snapshot).map((r) => r.id));
1108
+ const seconds = Math.floor((Date.parse(at) || 0) / 1000);
1069
1109
  for (let n = held.size + 1; ; n++) {
1070
1110
  const id = `${prefix}${ksuidFrom(seconds, n, `${m.service}:${resource}:${n}`, width)}`;
1071
1111
  if (!held.has(id)) return id;
@@ -1074,8 +1114,8 @@ function mintKsuid(m: DerivedManifest, resource: string, at: string | undefined,
1074
1114
 
1075
1115
  /** The next id of `count` lowercase letters for a resource, derived from the resource and the count before it, past
1076
1116
  * any the World already holds. */
1077
- function mintLetters(m: DerivedManifest, resource: string, count: number, root?: string, prefix = '', snapshot?: readonly TwinResource[]): string {
1078
- const held = new Set((snapshot ? snapshot.filter((r) => r.type === storedType(m, resource)) : resourcesOfType(m.service, storedType(m, resource), root)).map((r) => r.id));
1117
+ function mintLetters(m: DerivedManifest, resource: string, count: number, root: string | undefined, prefix: string, snapshot?: readonly TwinResource[]): string {
1118
+ const held = new Set(mintRows(m, resource, root, snapshot).map((r) => r.id));
1079
1119
  for (let n = held.size + 1; ; n++) {
1080
1120
  const id = `${prefix}${lettersFrom(`${m.service}:${resource}:${n}`, count)}`;
1081
1121
  if (!held.has(id)) return id;
@@ -1084,8 +1124,8 @@ function mintLetters(m: DerivedManifest, resource: string, count: number, root?:
1084
1124
 
1085
1125
  /** The next hex id for a resource: N hex digits derived from the resource and the count before it, past any the World
1086
1126
  * already holds (a Cloudflare id, 32). */
1087
- function mintHex(m: DerivedManifest, resource: string, count: number, root?: string, prefix = '', snapshot?: readonly TwinResource[]): string {
1088
- const held = new Set((snapshot ? snapshot.filter((r) => r.type === storedType(m, resource)) : resourcesOfType(m.service, storedType(m, resource), root)).map((r) => r.id));
1127
+ function mintHex(m: DerivedManifest, resource: string, count: number, root: string | undefined, prefix: string, snapshot?: readonly TwinResource[]): string {
1128
+ const held = new Set(mintRows(m, resource, root, snapshot).map((r) => r.id));
1089
1129
  for (let n = held.size + 1; ; n++) {
1090
1130
  const id = `${prefix}${sha256(`${m.service}:${resource}:${n}`).slice(0, count)}`;
1091
1131
  if (!held.has(id)) return id;
@@ -1095,8 +1135,7 @@ function mintHex(m: DerivedManifest, resource: string, count: number, root?: str
1095
1135
  /** The next UUID for a resource: a version-4-shaped UUID derived from the resource and the count before it, past any
1096
1136
  * the World already holds. */
1097
1137
  function mintUuid(m: DerivedManifest, resource: string, root?: string, snapshot?: readonly TwinResource[]): string {
1098
- const type = storedType(m, resource);
1099
- const held = new Set((snapshot ? snapshot.filter((r) => r.type === type) : resourcesOfType(m.service, type, root)).map((r) => r.id));
1138
+ const held = new Set(mintRows(m, resource, root, snapshot).map((r) => r.id));
1100
1139
  for (let n = held.size + 1; ; n++) {
1101
1140
  const id = uuidFrom(`${m.service}:${resource}:${n}`);
1102
1141
  if (!held.has(id)) return id;
@@ -1352,13 +1391,14 @@ async function finishWrite(m: DerivedManifest, call: DerivedCall, resource: stri
1352
1391
  // the managed database), at the write's moment
1353
1392
  const request = requestOfCall.get(call) ?? call.request;
1354
1393
  const context = (): Promise<SemanticsContext> => contextFor(m, { ...call, request: request.clone() }, { ...(scopeOfCall.get(call) ?? {}), ...(root !== undefined ? { root } : {}), clock: () => occurredAt });
1355
- if (m.onWrite) await m.onWrite({ operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone(), context });
1394
+ if (m.onWrite) await m.onWrite({ operation, storedType: m.resources[resource]?.storedAs ?? resource, body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone(), context });
1356
1395
  if (m.events) {
1357
- const events = m.events;
1358
- const write = { operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone() };
1396
+ const endpoint = (kind: import('./events.ts').EndpointsDecl): import('./events.ts').EndpointsDecl => ({ ...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) }])) } : {}) });
1397
+ 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) };
1398
+ const write = { operation, storedType: m.resources[resource]?.storedAs ?? resource, body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone() };
1359
1399
  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);
1360
1400
  }
1361
- if (m.sockets?.length) socketWrite(m.service, root, { operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone() });
1401
+ if (m.sockets?.length) socketWrite(m.service, root, { operation, storedType: m.resources[resource]?.storedAs ?? resource, body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone() });
1362
1402
  // a read held until the World writes (held-reads.ts) hears it
1363
1403
  heldReadsWrite(m.service, root, storedType(m, resource));
1364
1404
  // the subject's id as it stands after the write: the vendor's, when a live head minted one
@@ -1517,21 +1557,21 @@ function adoptedPath(m: DerivedManifest, call: DerivedCall, root?: string): Deri
1517
1557
  /** A call's fields with each declared reference that names an adopted subject by its local id moved to the adopted
1518
1558
  * id. A call (a handler's operation) need not say which subject type it writes, so every reference the pack declares
1519
1559
  * is tried: a field is rewritten only when its value is a local id the alias map holds for the reference's target. */
1520
- function adoptedInCall(service: string, fields: Record<string, unknown>, root?: string): Record<string, unknown> {
1521
- const refs = packReferences(service);
1560
+ function adoptedInCall(m: DerivedManifest, fields: Record<string, unknown>, root?: string): Record<string, unknown> {
1561
+ const service = m.service, refs = packReferences(service);
1522
1562
  if (refs.length === 0) return fields;
1523
1563
  const aliases = aliasesNow(service, root);
1524
1564
  if (aliases.size === 0) return fields;
1525
1565
  let out = fields;
1526
1566
  for (const ref of refs) {
1527
1567
  const local = ref.key(out as SubjectFields);
1528
- const adopted = local === undefined ? undefined : aliases.get(`${ref.to}:${local}`);
1568
+ const adopted = local === undefined ? undefined : aliases.get(`${storedType(m, ref.to)}:${local}`);
1529
1569
  if (adopted !== undefined && adopted !== local) out = { ...out, ...ref.adopt(out as SubjectFields, adopted) };
1530
1570
  }
1531
1571
  return out;
1532
1572
  }
1533
1573
  async function boundaryParams(m: DerivedManifest, call: DerivedCall, root?: string): Promise<Record<string, unknown>> {
1534
- return adoptedInCall(m.service, await readParams(m, call.request, call.operation), root);
1574
+ return adoptedInCall(m, await readParams(m, call.request, call.operation), root);
1535
1575
  }
1536
1576
 
1537
1577
  /** The refusal a subject's fields would draw from its resource's `unique` rules: another live subject (not `except`)
@@ -1726,7 +1766,7 @@ export async function serveCore(m: DerivedManifest, call: DerivedCall, scope: Co
1726
1766
  const parent = parentOfCall(decl, call);
1727
1767
  const parentId = parent ? parentValue(parent, call.params) : undefined;
1728
1768
  // the caller's tenant, when the vendor has tenancy and the resource is not shared by every tenant
1729
- const tenantField = m.tenant && !decl.shared ? m.tenant.field : undefined;
1769
+ const tenantField = m.tenant?.storage !== 'namespaced' && m.tenant && !decl.shared ? m.tenant.field : undefined;
1730
1770
  const tenant = tenantField && scope.tenant ? await scope.tenant(await contextFor(m, { ...call, request: call.request.clone() }, scope)) : undefined;
1731
1771
  if (tenantField && tenant === undefined) return { unmodeled: `the caller names no tenant (semantics/tenant.ts) for ${resource}` };
1732
1772
  const orphan = missingParent(m, resource, call, root, tenant);
@@ -1917,6 +1957,8 @@ export type SemanticsContext = {
1917
1957
  * random, and keeps as bookkeeping no door answers. The same label gives the same secret for the World's whole life,
1918
1958
  * and no one can compute it from ids the World shows, as they can a value derived from them alone. */
1919
1959
  secret(label: string): Promise<string>;
1960
+ /** Select an account at a pack's credential door. Packs retain their own vendor account/key semantics. */
1961
+ account(field?: string): Promise<string>;
1920
1962
  /** The World's machine pool (machines.ts), as the pack's `machine-pool` door enrolled it; `none` when none was. */
1921
1963
  machines: MachinePool;
1922
1964
  /** A signing key pair for `label` (an issuer's ID tokens, a provider's JWTs, a CA's or a log's key), PEM: RSA 2048
@@ -1930,7 +1972,7 @@ export type SemanticsContext = {
1930
1972
  /** Allocate a stored occurrence and append its fields under one action lock. The optional field factory is pure. */
1931
1973
  create(resource: string, fields: Record<string, unknown> | ((id: string) => Record<string, unknown>), operation: string): Promise<Record<string, unknown>>;
1932
1974
  /** Create under declared unique keys and the action lock; a collision returns the existing own-field view without a write. */
1933
- createUnique(resource: string, fields: Record<string, unknown> | ((id: string) => Record<string, unknown>), operation: string, options?: { input: 'omit' }): Promise<{ created: boolean; row: Record<string, unknown> }>;
1975
+ createUnique(resource: string, fields: Record<string, unknown> | ((id: string) => Record<string, unknown>), operation: string, options?: { input?: 'omit'; id?: string }): Promise<{ created: boolean; row: Record<string, unknown> }>;
1934
1976
  /** Decide an update from the current live stored row under the action lock; undefined leaves it unchanged. */
1935
1977
  change(resource: string, id: string, decide: (row: Record<string, unknown>) => Record<string, unknown> | undefined, operation: string, options?: { input: 'omit' }): Promise<Record<string, unknown> | undefined>;
1936
1978
  write(resource: string, id: string, fields: Record<string, unknown>, operation: string): Promise<Record<string, unknown>>;
@@ -2098,7 +2140,7 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2098
2140
  const raw = !parts && typeof body === 'string' && body !== '' ? await rawBody(call.request) : undefined;
2099
2141
  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 } : {}) });
2100
2142
  // a handler reading the body itself sees the adopted id too (the boundary's one resolution, below)
2101
- if (body && typeof body === 'object' && !Array.isArray(body)) body = adoptedInCall(m.service, body as Record<string, unknown>, root);
2143
+ if (body && typeof body === 'object' && !Array.isArray(body)) body = adoptedInCall(m, body as Record<string, unknown>, root);
2102
2144
  // every read of the body is of a copy, so the handler's own `call.request` and the core's answer (`core()`) each
2103
2145
  // still find it unread, whichever of them reads it first
2104
2146
  const pristine = call.request.clone();
@@ -2114,6 +2156,13 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2114
2156
  const rawResources = new WeakMap<Record<string, unknown>, string>();
2115
2157
  const rawRow = (resource: string, row: TwinResource): Record<string, unknown> => {
2116
2158
  const name = m.resources[resource] ? resource : Object.keys(m.resources).find((name) => storedType(m, name) === row.type);
2159
+ if (m.tenant?.storage === 'namespaced') {
2160
+ const [base, encoded] = row.type.split(TENANT_TYPE);
2161
+ const owner = m.resources[name ?? resource]?.shared && row[m.tenant.field] !== undefined ? row[m.tenant.field] : encoded ? decodeURIComponent(encoded) : 'default';
2162
+ row = copyResource(row);
2163
+ row.type = base!;
2164
+ row[m.tenant.field] = owner;
2165
+ }
2117
2166
  rawResources.set(row as Record<string, unknown>, name ?? resource);
2118
2167
  return row as Record<string, unknown>;
2119
2168
  };
@@ -2131,7 +2180,7 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2131
2180
  asVendor: (fn) => runAsVendorMove(fn),
2132
2181
  git: (name) => {
2133
2182
  if (!/^[A-Za-z0-9_.-]+(\/[A-Za-z0-9_.-]+)*$/.test(name) || name.split('/').some((seg) => seg === '.' || seg === '..')) throw new Error(`semantics: ${name} is not a git repository name`);
2134
- const plane = `${worldPaths(m.service, root).dir}/git`;
2183
+ const plane = `${worldPaths(m.service, root).dir}/git${accountScope(m) && accountScope(m) !== 'default' ? `/accounts/${encodeURIComponent(accountScope(m)!)}` : ''}`;
2135
2184
  return { store: new GitObjectStore(getActiveBlobStore(), `${plane}/repos/${name}`), refs: new GitRefs(getActiveWorldStore(), `${plane}/repos/${name}`) };
2136
2185
  },
2137
2186
  mail: (route, mail, headers) => sendMail(route, mail, headers),
@@ -2168,16 +2217,20 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2168
2217
  return value;
2169
2218
  },
2170
2219
  occurredAt: at,
2171
- secret: async (label) => hmac(await worldSeed(m.service, root, at), label, 'base64url'),
2220
+ secret: async (label) => hmac(await worldSeed(m.service, root, at), scopeLabel(m, label), 'base64url'),
2221
+ account: async (field = 'account') => {
2222
+ const fields = body && typeof body === 'object' && !Array.isArray(body) ? body as Record<string, unknown> : {};
2223
+ return worldAccount(m.service, root, fields[field] ?? 'default', at, call.request.headers.get('x-volter-world-activity') === 'provisioning');
2224
+ },
2172
2225
  record: async (type, fields, id, operation) => {
2173
2226
  if (!type.startsWith('_')) throw new Error(`semantics: ${type} is not a bookkeeping type (bookkeeping types start with _)`);
2174
2227
  // a bookkeeping subject a resource counts (`counts` naming it: sentry's issue users) keeps that count as any
2175
2228
  // child does, the parent it left recounted too
2176
- const keeps = counted(m, type);
2229
+ const keeps = counted(m, type), physicalType = storedType(m, type);
2177
2230
  if (id !== undefined) {
2178
2231
  const before = operation ? stateBeforeWrite(m, type, id, root) : undefined;
2179
- const prior = keeps ? resourcesOfType(m.service, type, root).find((r) => r.id === id) : undefined;
2180
- const { resource: row, result } = await applyTwinWrite(m.service, { operation: operation ?? `${type.slice(1)}.record`, subjectType: type, subjectId: id, fields, ...provisioningInput(call), occurredAt: at, actor: { kind: 'system' } }, root);
2232
+ const prior = keeps ? resourcesOfType(m.service, physicalType, root).find((r) => r.id === id) : undefined;
2233
+ 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);
2181
2234
  if (operation) observeStateWrite(m, result, before, root, stateChecks);
2182
2235
  if (keeps) await keepCounts(m, call, type, row as unknown as Record<string, unknown>, params, root, at, prior && { ...prior } as Record<string, unknown>);
2183
2236
  return id;
@@ -2188,25 +2241,25 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2188
2241
  const prefix = `${type.slice(1)}_`;
2189
2242
  let max = 0n;
2190
2243
  for (const r of resources) {
2191
- if (r.type !== type) continue;
2244
+ if (r.type !== physicalType) continue;
2192
2245
  const digits = r.id.startsWith(prefix) ? r.id.slice(prefix.length) : '';
2193
2246
  if (/^\d+$/.test(digits) && BigInt(digits) > max) max = BigInt(digits);
2194
2247
  }
2195
2248
  return `${prefix}${max + 1n}`;
2196
2249
  })();
2197
- return { kind: 'write', value: subject, write: { operation: operation ?? `${type.slice(1)}.record`, subjectType: type, subjectId: subject, fields, ...provisioningInput(call), occurredAt: at, actor: { kind: 'system' } } };
2198
- }, root, () => resourcesOfType(m.service, type, root));
2250
+ return { kind: 'write', value: subject, write: { operation: operation ?? `${type.slice(1)}.record`, subjectType: physicalType, subjectId: subject, fields, ...provisioningInput(call), occurredAt: at, actor: { kind: 'system' } } };
2251
+ }, root, () => resourcesOfType(m.service, physicalType, root));
2199
2252
  if (operation && result) observeStateWrite(m, result, undefined, root, stateChecks);
2200
- const made = keeps && value !== undefined ? resourcesOfType(m.service, type, root).find((r) => r.id === value) : undefined;
2253
+ const made = keeps && value !== undefined ? resourcesOfType(m.service, physicalType, root).find((r) => r.id === value) : undefined;
2201
2254
  if (made) await keepCounts(m, call, type, made as unknown as Record<string, unknown>, params, root, at);
2202
2255
  return value;
2203
2256
  },
2204
2257
  get machines() {
2205
- const enrolled = resourcesOfType(m.service, MACHINE_POOL, root).find((r) => r.id === 'pool') as unknown as (LightPoolOptions & { kind?: MachinePool['kind']; httpReplies?: MachineHttpReply[] }) | undefined;
2206
- return guardedMachinePool(poolFor(m.service, root, enrolled?.kind ?? 'none', enrolled?.httpReplies, { images: enrolled?.images }), m.service, ['retrieve', 'list'].includes(call.operation.class) || (m.reads?.includes(call.operation.id) ?? false));
2258
+ const enrolled = resourcesOfType(m.service, MACHINE_POOL, root).find((r) => r.id === 'pool') as unknown as { kind?: MachinePool['kind']; httpReplies?: MachineHttpReply[] } | undefined;
2259
+ 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));
2207
2260
  },
2208
- signingKey: (label, alg) => worldSigningKey(m.service, root, at, label, alg ?? 'RS256'),
2209
- heldSigningKey: (label, alg) => heldWorldSigningKey(m.service, root, label, alg ?? 'RS256'),
2261
+ signingKey: (label, alg) => worldSigningKey(m.service, root, at, scopeLabel(m, label), alg ?? 'RS256'),
2262
+ heldSigningKey: (label, alg) => heldWorldSigningKey(m.service, root, scopeLabel(m, label), alg ?? 'RS256'),
2210
2263
  create: (resource, fields, operation) => atomicWrite(m, call, resource, (snapshot) => {
2211
2264
  const id = mintId(m, resource, root, at, snapshot);
2212
2265
  return { id, fields: typeof fields === 'function' ? fields(id) : fields };
@@ -2217,7 +2270,7 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2217
2270
  if (!rules?.length) throw new Error('createUnique requires declared unique keys');
2218
2271
  let existing: Record<string, unknown> | undefined;
2219
2272
  const row = await atomicWrite(m, call, resource, (snapshot) => {
2220
- const id = mintId(m, resource, root, at, snapshot);
2273
+ const id = options?.id ?? mintId(m, resource, root, at, snapshot);
2221
2274
  const selected = typeof fields === 'function' ? fields(id) : fields;
2222
2275
  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])));
2223
2276
  if (hit) { existing = view(m, resource, hit); return undefined; }
@@ -2255,7 +2308,16 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2255
2308
  ok: (fields) => Response.json({ ...(m.success ?? {}), ...fields }),
2256
2309
  row: (resource, id, opts) => { const row = stored(m, resource, root, opts).find((r) => r.id === id); return row ? rawRow(resource, row) : undefined; },
2257
2310
  rowsRaw: (resource, opts) => stored(m, resource, root, opts).map((row) => rawRow(resource, row)),
2258
- tree: () => twinResources(m.service, root) as Array<Record<string, unknown>>,
2311
+ tree: () => {
2312
+ const rows = twinResources(m.service, root);
2313
+ if (m.tenant?.storage !== 'namespaced') return rows as Array<Record<string, unknown>>;
2314
+ const account = accountScope(m);
2315
+ return rows.filter(row => {
2316
+ const [base, encoded] = row.type.split(TENANT_TYPE);
2317
+ const name = Object.keys(m.resources).find(name => (m.resources[name]?.storedAs ?? name) === base) ?? base!;
2318
+ return account === undefined || m.resources[name]?.shared || (encoded ? decodeURIComponent(encoded) : 'default') === account;
2319
+ }).map(row => rawRow(row.type.split(TENANT_TYPE)[0]!, row));
2320
+ },
2259
2321
  history: (resource, id) => subjectHistory(m.service, { type: storedType(m, resource), id }, root).map((e) => ({ ...(e.operation ? { operation: e.operation } : {}), ...(e.fields ? { fields: e.fields as Record<string, unknown> } : {}), ...(e.occurredAt ? { occurredAt: e.occurredAt } : {}) })),
2260
2322
  raw: (body, init = {}) => new Response(body, { status: init.status ?? 200, headers: init.headers ?? {} }),
2261
2323
  atomically: async (decide) => {
@@ -2309,17 +2371,17 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2309
2371
  engine: scope.database === undefined ? unboundDatabase(m.service) : managedDatabase(scope.database, { readOnly: scope.readOnly === true || isReadOnlyRequest(call.request) }),
2310
2372
  redis: (commands, options) => execRedisRun(commands, {
2311
2373
  root, occurredAt: at, service: m.service, readOnly: scope.readOnly === true || isReadOnlyRequest(call.request), dialect: options.dialect,
2312
- ...(options.database === undefined ? {} : { database: options.database }),
2374
+ ...(accountScope(m) && accountScope(m) !== 'default' ? { database: scopeLabel(m, options.database ?? 'default') } : options.database === undefined ? {} : { database: options.database }),
2313
2375
  ...(options.transaction === undefined ? {} : { transaction: options.transaction }),
2314
2376
  }),
2315
- live: liveAccess({ rows: (type) => resourcesOfType(m.service, type, root) as unknown as Array<Record<string, unknown>>, blob: (key) => readResourceBlob(m.service, key, root) }, call.request),
2377
+ live: liveAccess({ rows: (type) => stored(m, type, root).map(row => rawRow(type, row)), blob: (key) => readResourceBlob(m.service, scopeLabel(m, key), root) }, call.request),
2316
2378
  blobs: {
2317
- put: (key, bytes) => writeResourceBlob(m.service, key, bytes, root),
2318
- putStream: (key, body, accept) => writeResourceBlobStream(m.service, key, body, accept, root),
2319
- get: (key) => readResourceBlob(m.service, key, root),
2320
- remove: (key) => removeResourceBlob(m.service, key, root),
2321
- size: (key) => resourceBlobSize(m.service, key, root),
2322
- range: (key, start, endInclusive) => readResourceBlobRange(m.service, key, start, endInclusive, root),
2379
+ put: (key, bytes) => writeResourceBlob(m.service, scopeLabel(m, key), bytes, root),
2380
+ putStream: (key, body, accept) => writeResourceBlobStream(m.service, scopeLabel(m, key), body, accept, root),
2381
+ get: (key) => readResourceBlob(m.service, scopeLabel(m, key), root),
2382
+ remove: (key) => removeResourceBlob(m.service, scopeLabel(m, key), root),
2383
+ size: (key) => resourceBlobSize(m.service, scopeLabel(m, key), root),
2384
+ range: (key, start, endInclusive) => readResourceBlobRange(m.service, scopeLabel(m, key), start, endInclusive, root),
2323
2385
  },
2324
2386
  merge: (resource, prior, next) => mergeField(m.resources[resource]?.update, prior, next),
2325
2387
  conflict: (resource, fields, except) => {
@@ -2614,7 +2676,7 @@ export function authRefusal(m: DerivedManifest, request: Request, root?: string)
2614
2676
  const held = m.auth.held;
2615
2677
  if (held && !held.standing?.includes(key)) {
2616
2678
  const hash = sha256(key);
2617
- const holds = resourcesOfType(m.service, held.storedAs, root).some((r) => r.deleted !== true && (r as Record<string, unknown>)[held.hashField] === hash && (held.unless === undefined || (r as Record<string, unknown>)[held.unless] !== true));
2679
+ const holds = stored(m, held.storedAs, root).some((r) => r.deleted !== true && (r as Record<string, unknown>)[held.hashField] === hash && (held.unless === undefined || (r as Record<string, unknown>)[held.unless] !== true));
2618
2680
  if (!holds) return vendorError(errorManifest, invalid);
2619
2681
  }
2620
2682
  return undefined;
@@ -2691,7 +2753,7 @@ export function crossCutting(m: DerivedManifest, opts: CoreScope & { readOnly?:
2691
2753
  const url = new URL(request.url);
2692
2754
  const signature = hashFieldValue({ path: url.pathname + url.search, body: await bodyTextOf(request.clone()) });
2693
2755
  const id = `idem_${hex(key)}`;
2694
- const prior = twinResources(m.service, opts.root).find((r) => r.type === m.idempotency!.storedAs && r.id === id) as Record<string, unknown> | undefined;
2756
+ const prior = twinResources(m.service, opts.root).find((r) => r.type === storedType(m, m.idempotency!.storedAs) && r.id === id) as Record<string, unknown> | undefined;
2695
2757
  if (prior) {
2696
2758
  if (m.idempotency.conflict && prior.requestHash !== undefined && prior.requestHash !== signature) return vendorError(m, m.idempotency.conflict);
2697
2759
  const answer = prior.response as { status: number; body?: unknown; text?: string; contentType?: string };
@@ -2704,7 +2766,7 @@ export function crossCutting(m: DerivedManifest, opts: CoreScope & { readOnly?:
2704
2766
  if (m.idempotency.onlySuccess && (response.status < 200 || response.status >= 300)) return response;
2705
2767
  const contentType = response.headers.get('content-type') ?? 'application/json';
2706
2768
  const text = await response.clone().text();
2707
- 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);
2769
+ 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);
2708
2770
  return response;
2709
2771
  }
2710
2772
  }
package/src/index.ts CHANGED
@@ -63,8 +63,8 @@ export { assetContentType, packAsset } from './pack-assets.ts';
63
63
  export { bytesResponse, contentTypeOf, fileResponse } from './file-response.ts';
64
64
 
65
65
  // ── the kernel's deterministic helpers ─────────────────────────────────────────────────────────
66
- 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.ts';
67
- export type { HandlerCrypto, Jwk, Jwks, JwtVerdict, SigningKey } from './signing.ts';
66
+ 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.ts';
67
+ export type { HandlerCrypto, Jwk, Jwks, JwtDecodeOptions, JwtSignatureOptions, JwtVerdict, JwtVerificationKey, SigningKey } from './signing.ts';
68
68
  export { blobDigest } from './blob-store.ts';
69
69
  // compressed bytes a wire carries (a SARIF upload), over web streams
70
70
  export { gunzip, gzip } from './bytes.ts';
@@ -109,3 +109,6 @@ export { assertBrowserName, browserOrigin, validateBrowserState, validateBrowser
109
109
 
110
110
  export * as managedOriginalInputs from './managed-original-inputs.ts';
111
111
  export * as managedGoPrograms from './managed-go-programs.ts';
112
+
113
+ export { worldSecret, worldAccount } from './derived-core.ts';
114
+ export { isWorldAccountName } from './world-account-name.ts';
package/src/machines.ts CHANGED
@@ -3,14 +3,12 @@
3
3
  // `POST /_twin/machine-pool`, recorded in the pack's own tree; a handler reaches it as `ctx.machines`. `docker` runs
4
4
  // each machine as a local container with its internal ports published on loopback; `none` runs nothing; `fake`
5
5
  // records what it was asked (capability verification). Nothing here runs at module scope, and `node:child_process`
6
- // is reached only when a compute pool is asked to act. The light pool delegates to the host's Substrate adapter.
6
+ // is reached only when a compute pool is asked to act.
7
7
  import { refuseReadOnlyWrite } from './request-scope.ts';
8
8
  import { nodeBuiltin } from './serve-http.ts';
9
9
  import { refuseRequiredMachineTransport } from '../required-credential-transport.cjs';
10
10
 
11
11
  export type MachineSpec = { name: string; image: string; env?: Record<string, string>; ports?: number[]; command?: string[]; checks?: Array<{ port: number; type: 'http' | 'tcp'; path?: string }>; resetOnStop?: boolean; mounts?: Array<{ name: string; path: string }>; signal?: AbortSignal; startTimeoutMs?: number };
12
- export type GuestImage = { context: string; dockerfile?: string; runtime?: 'node' | 'bun' | 'python' | 'deno'; command?: string[]; cwd?: string; env?: Record<string, string>; prepared?: boolean };
13
- export type LightPoolOptions = { images?: Record<string, GuestImage> };
14
12
  export type MachineRun = { ports: Array<{ internal: number; host: number; url?: string }> };
15
13
  export type MachineStatus = { state: 'started' | 'paused' | 'stopped' | 'exited' | 'failed'; exitCode?: number; reason?: string };
16
14
  export type MachineFileStat = { type: 'file' | 'directory'; size: number; mode: number; mtime: string };
@@ -27,7 +25,7 @@ export type MachineProcess = { pid: number; events: AsyncIterable<{ stdout?: str
27
25
  export type ExecSpec = { args: string[]; env?: Record<string, string>; cwd?: string };
28
26
  export type MachineHttpReply = { port: number; method: string; path: string; requestText?: string; status: number; headers?: Record<string, string>; body?: string };
29
27
  export type MachinePool = {
30
- readonly kind: 'docker' | 'none' | 'fake' | 'light';
28
+ readonly kind: 'docker' | 'none' | 'fake';
31
29
  /** Optional capability admission, before vendor resource creation; starts no process. */
32
30
  admitRun?(spec: MachineSpec): Promise<void>;
33
31
  /** runs the image; a failure (no container runtime, an image that will not start) rejects with the runtime's message */
@@ -51,33 +49,7 @@ export function isMachineCapabilityGap(error: unknown): boolean {
51
49
  return typeof error === 'object' && error !== null && 'code' in error && error.code === 'WORLD_MACHINE_CAPABILITY_GAP';
52
50
  }
53
51
  export const MACHINE_POOL = '_machine_pool';
54
- export const POOL_KINDS = ['docker', 'none', 'fake', 'light'] as const;
55
-
56
- /** The host owns provider bootstrapping. Browser/kernel imports stay inert, and a missing
57
- * host provider refuses rather than falling back to metadata-only success. */
58
- function lightPool(world: string, root: string | undefined, options: LightPoolOptions): MachinePool {
59
- let ready: Promise<MachinePool> | undefined;
60
- const load = (): Promise<MachinePool> => ready ??= (async () => {
61
- const entry = typeof process === 'undefined' ? undefined : process.env.VOLTER_WORLD_GUEST_RUNNER;
62
- if (!entry) throw new Error('light guest runner is unavailable: run this twin inside a World');
63
- const provider = await import(entry);
64
- return provider.createLightGuestPool({ world, root, ...options }) as MachinePool;
65
- })();
66
- return {
67
- kind: 'light',
68
- admitRun: async (spec) => { await (await load()).admitRun?.(spec); },
69
- run: async (spec) => (await load()).run(spec),
70
- exec: async (name, spec) => (await load()).exec(name, spec),
71
- request: async (name, port, request) => (await load()).request(name, port, request),
72
- status: async (name) => (await load()).status(name),
73
- ports: async (name) => (await load()).ports(name),
74
- files: async (name) => (await load()).files(name),
75
- pause: async (name) => (await load()).pause(name),
76
- resume: async (name) => (await load()).resume(name),
77
- stop: async (name) => (await load()).stop(name),
78
- remove: async (name) => (await load()).remove(name),
79
- };
80
- }
52
+ export const POOL_KINDS = ['docker', 'none', 'fake'] as const;
81
53
 
82
54
  /** A container's name for a machine: the World's prefix, the World's own mark (a machine id repeats across Worlds and
83
55
  * branches, which mint alike), then the vendor's machine name made safe for docker. */
@@ -100,8 +72,8 @@ function dockerPool(world: string): MachinePool {
100
72
  const state = JSON.parse(await docker(['inspect', '--format', '{{json .State}}', containerName(world, name)]));
101
73
  return state.Running ? { state: 'started' } : { state: 'exited', exitCode: state.ExitCode, ...(state.Error ? { reason: state.Error } : {}) };
102
74
  },
103
- async ports() { throw new Error('port discovery requires the light guest backing'); },
104
- async files() { throw new Error('file access requires the light guest backing'); },
75
+ async ports() { throw new Error('port discovery is unavailable'); },
76
+ async files() { throw new Error('file access is unavailable'); },
105
77
  async run(spec) {
106
78
  refuseRequiredMachineTransport(process.env, 'Docker machine application transport');
107
79
  const name = containerName(world, spec.name);
@@ -187,8 +159,8 @@ function dockerPool(world: string): MachinePool {
187
159
  }
188
160
 
189
161
  /** The pool of a kind, for a World (`world`: the mark its containers carry). */
190
- export function machinePool(kind: MachinePool['kind'], world = 'world', httpReplies: readonly MachineHttpReply[] = [], light: LightPoolOptions = {}, root?: string): MachinePool {
191
- if (kind === 'light') return lightPool(world, root, light);
162
+ export function machinePool(kind: MachinePool['kind'], world = 'world', httpReplies: readonly MachineHttpReply[] = []): MachinePool {
163
+ if (!POOL_KINDS.includes(kind)) throw new Error(`kind must be one of ${POOL_KINDS.join(', ')}`);
192
164
  if (kind === 'docker') return dockerPool(world);
193
165
  if (kind === 'fake') {
194
166
  const asked: NonNullable<MachinePool['asked']> = [];
@@ -237,14 +209,14 @@ export function machinePool(kind: MachinePool['kind'], world = 'world', httpRepl
237
209
 
238
210
  /** 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). */
239
211
  const pools = new Map<string, MachinePool>();
240
- export function poolFor(service: string, root: string | undefined, kind: MachinePool['kind'], httpReplies: readonly MachineHttpReply[] = [], light: LightPoolOptions = {}): MachinePool {
241
- const key = `${service}\0${root ?? ''}\0${kind}${kind === 'light' ? JSON.stringify(light) : ''}`;
212
+ export function poolFor(service: string, root: string | undefined, kind: MachinePool['kind'], httpReplies: readonly MachineHttpReply[] = []): MachinePool {
213
+ const key = `${service}\0${root ?? ''}\0${kind}`;
242
214
  let pool = pools.get(key);
243
215
  if (!pool) {
244
216
  // the World's mark: a short digest of the vendor and the World's tree, so two Worlds' machines never share a name
245
217
  let h = 0;
246
218
  for (const ch of `${service}:${root ?? ''}`) h = (Math.imul(h, 31) + ch.charCodeAt(0)) >>> 0;
247
- pool = machinePool(kind, h.toString(16).padStart(8, '0'), httpReplies, light, root);
219
+ pool = machinePool(kind, h.toString(16).padStart(8, '0'), httpReplies);
248
220
  pools.set(key, pool);
249
221
  }
250
222
  return pool;