@volter/world-core 3.0.162 → 3.0.163

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,7 +15,7 @@ import { type MultipartPart } from './multipart.js';
15
15
  import type { CorsDecl } from './cors.js';
16
16
  import { type TwinAction } from './actions.js';
17
17
  import type { DerivedCall, DerivedCoreOutcome, DerivedHandler, DerivedOperation } from './derived.js';
18
- import type { PackDescriptor } from './packRegistry.js';
18
+ import { type PackDescriptor } from './packRegistry.js';
19
19
  import type { ScenarioDecision, ScenarioHandler } from './scenario.js';
20
20
  /** How a stored field gets its value when the server assigns it. `now` is the world clock in the
21
21
  * manifest's time format; `id` the subject's id; a `value` is stored as given. */
@@ -921,6 +921,18 @@ export type CoreScope = {
921
921
  export declare function worldSecret(service: string, root: string, label: string, at?: string): Promise<string>;
922
922
  /** The same retained account primitive for pack doors and private native twin fronts. */
923
923
  export declare function worldAccount(service: string, root: string | undefined, id?: unknown, at?: string, provisioning?: boolean): Promise<string>;
924
+ type WorldOrganization = {
925
+ id: string;
926
+ label: string;
927
+ members: string[];
928
+ };
929
+ type VendorNameRule = {
930
+ max: number;
931
+ lower?: boolean;
932
+ allowed: RegExp;
933
+ separator?: string;
934
+ first?: RegExp;
935
+ };
924
936
  /** The lane a vendor's router sent a request to (pack-fetch's laneRouter sets it). */
925
937
  export declare const LANE_HEADER = "x-volter-lane";
926
938
  /** What a coverage measurement sees of a pack's declared state logic: each move a request made and each refusal a
@@ -1018,6 +1030,27 @@ export type SemanticsContext = {
1018
1030
  secret(label: string): Promise<string>;
1019
1031
  /** Select an account at a pack's credential door. Packs retain their own vendor account/key semantics. */
1020
1032
  account(field?: string): Promise<string>;
1033
+ /** The selected organization's label and people from the World's organization registry. */
1034
+ organization(): Promise<WorldOrganization>;
1035
+ /** The selected deployment, or an empty string for the organization's own level. */
1036
+ deployment(): string;
1037
+ /** A vendor-safe label; retained links keep identity independent of names. */
1038
+ vendorName(label: string, rule: VendorNameRule, options?: {
1039
+ suffixed?: boolean;
1040
+ identity?: string;
1041
+ }): string;
1042
+ /** Retain the native customer tenant for the selected organization. */
1043
+ tenant(resource: string, make: (organization: WorldOrganization) => Promise<string>): Promise<Record<string, unknown>>;
1044
+ /** Retain one native resource of this kind for the selected organization and deployment. */
1045
+ deploymentObject(kind: string, resource: string, make: (deployment: string) => Promise<string>): Promise<Record<string, unknown>>;
1046
+ /** Reconstruct a live native key, replacing its link when the vendor's reader refuses it. */
1047
+ accountKey(purpose: string, resource: string, key: {
1048
+ make: () => Promise<string>;
1049
+ read: (row: Record<string, unknown>) => Promise<string | undefined>;
1050
+ }): Promise<{
1051
+ id: string;
1052
+ secret: string;
1053
+ }>;
1021
1054
  /** The World's machine pool (machines.ts), as the pack's `machine-pool` door enrolled it; `none` when none was. */
1022
1055
  machines: MachinePool;
1023
1056
  /** 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
@@ -1219,7 +1252,7 @@ export type HandlerContext = Omit<SemanticsContext, 'now' | 'core' | 'atomically
1219
1252
  };
1220
1253
  /** What a write hook reads: the writing call's context without its ways to write, so a hook that renders an event
1221
1254
  * cannot write again (and run itself again). */
1222
- export type WriteHookContext = Omit<HandlerContext, 'write' | 'batch' | 'create' | 'createUnique' | 'change' | 'issue' | 'record' | 'accumulate' | 'at' | 'over'>;
1255
+ export type WriteHookContext = Omit<HandlerContext, 'write' | 'batch' | 'create' | 'createUnique' | 'change' | 'issue' | 'record' | 'accumulate' | 'tenant' | 'deploymentObject' | 'accountKey' | 'at' | 'over'>;
1223
1256
  /** A derived pack's handler: one operation, by its operationId, over the contract's context. */
1224
1257
  export type Handler = (ctx: HandlerContext) => Promise<Response>;
1225
1258
  export declare function contextFor(m: DerivedManifest, call: DerivedCall, scope: CoreScope): Promise<SemanticsContext>;
@@ -37,6 +37,7 @@ import { vendorFetch } from "./vendor-call.js";
37
37
  import { packReferences } from "./references.js";
38
38
  import { twinPublicBase, withRequestScopes } from "./twin-fetch.js";
39
39
  import { inVendorMove, isReadOnlyRequest, runAsVendorMove, writesRefused } from "./request-scope.js";
40
+ import { getPack } from "./packRegistry.js";
40
41
  import { bodyTextOf, checkJsonBody, takeParsedBody } from "./derived.js";
41
42
  /** What a move to `to` stores in the field: a boolean field's value is a boolean, and a derived field's
42
43
  * value is written by the move's effects (a state name is not a timestamp), so it stores nothing itself. */
@@ -473,6 +474,57 @@ export async function worldAccount(service, root, id = 'default', at = worldNow(
473
474
  }, root);
474
475
  return id;
475
476
  }
477
+ function nativeAccountLink(row, field) {
478
+ if (!row || row.deleted === true)
479
+ return undefined;
480
+ const fields = ownFields(row);
481
+ const value = field === undefined ? fields : fields[field];
482
+ if (!value || typeof value !== 'object' || Array.isArray(value))
483
+ return undefined;
484
+ const link = value;
485
+ return typeof link.resource === 'string' && typeof link.id === 'string' ? { resource: link.resource, id: link.id } : undefined;
486
+ }
487
+ const sameAccountLink = (a, b) => a?.resource === b?.resource && a?.id === b?.id;
488
+ /** Native creation and reconstruction run outside the action lock; only the retained link is compared and written. */
489
+ async function retainNativeAccountLink(service, root, at, call, type, id, field, resource, make, read) {
490
+ for (;;) {
491
+ const before = nativeAccountLink(resourcesOfType(service, type, root).find((row) => row.id === id), field);
492
+ const held = before && await read(before);
493
+ if (held !== undefined)
494
+ return held;
495
+ const made = { resource, id: await make() };
496
+ if (await read(made) === undefined)
497
+ throw new Error(`Credential ${type}/${id}: native ${resource}/${made.id} is unavailable after creation`);
498
+ const { value } = await applyTwinWriteAtomic(service, (snapshot) => {
499
+ const current = nativeAccountLink(snapshot.find((row) => row.type === type && row.id === id), field);
500
+ if (!sameAccountLink(current, before))
501
+ return { kind: 'skip', value: current };
502
+ return {
503
+ kind: 'write', value: made,
504
+ write: {
505
+ operation: `${type.slice(1)}.link`, subjectType: type, subjectId: id,
506
+ fields: field === undefined ? made : { name: id, [field]: made },
507
+ ...provisioningInput(call), occurredAt: at, actor: { kind: 'system' },
508
+ },
509
+ };
510
+ }, root);
511
+ const linked = value && await read(value);
512
+ if (linked !== undefined)
513
+ return linked;
514
+ }
515
+ }
516
+ function vendorName(label, rule, { suffixed = false, identity = label } = {}) {
517
+ const allowed = new RegExp(rule.allowed.source, rule.allowed.flags.replace(/[gy]/g, ''));
518
+ const separator = rule.separator ?? '-';
519
+ if ([...separator].length !== 1 || !allowed.test(separator))
520
+ throw new Error('Credential name separator must be one allowed character');
521
+ const first = rule.first && new RegExp(rule.first.source, rule.first.flags.replace(/[gy]/g, ''));
522
+ let name = [...(rule.lower ? label.toLowerCase() : label)].map((letter) => allowed.test(letter) ? letter : separator).join('');
523
+ name = name.replace(new RegExp(`${escapeRe(separator)}+`, 'g'), separator);
524
+ if (first && !first.test(name[0] ?? ''))
525
+ name = `w${separator}${name}`;
526
+ return name !== label || name.length > rule.max || suffixed ? `${name.slice(0, Math.max(0, rule.max - 9))}${separator}${sha256(identity).slice(0, 8)}` : name;
527
+ }
476
528
  /** A World's signing key for a label (ctx.signingKey): made once from the platform's random source, then read; the
477
529
  * making is one per service store and label in the process, as the seed's is. */
478
530
  const SIGNING_KEY = '_world_signing_key';
@@ -1489,6 +1541,8 @@ export async function contextFor(m, call, scope) {
1489
1541
  });
1490
1542
  const paths = expandPaths(m, params);
1491
1543
  const stateChecks = new Map();
1544
+ const credentialDoor = m.descriptor?.credentialDoor ?? getPack(m.vendor)?.credentialDoor;
1545
+ const credentialFields = body && typeof body === 'object' && !Array.isArray(body) ? body : {};
1492
1546
  const rawResources = new WeakMap();
1493
1547
  const rawRow = (resource, row) => {
1494
1548
  const name = m.resources[resource] ? resource : Object.keys(m.resources).find((name) => storedType(m, name) === row.type);
@@ -1502,6 +1556,10 @@ export async function contextFor(m, call, scope) {
1502
1556
  rawResources.set(row, name ?? resource);
1503
1557
  return row;
1504
1558
  };
1559
+ const liveNativeRow = (link) => {
1560
+ const row = context.row(link.resource, link.id);
1561
+ return row && row.deleted !== true ? row : undefined;
1562
+ };
1505
1563
  const context = {
1506
1564
  call,
1507
1565
  params,
@@ -1558,9 +1616,38 @@ export async function contextFor(m, call, scope) {
1558
1616
  },
1559
1617
  occurredAt: at,
1560
1618
  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');
1619
+ account: async (field = credentialDoor?.account?.field ?? 'account') => {
1620
+ return worldAccount(m.service, root, credentialFields[field] ?? 'default', at, call.request.headers.get('x-volter-world-activity') === 'provisioning');
1621
+ },
1622
+ organization: async () => {
1623
+ const id = await context.account();
1624
+ const row = projectOwnerResources('_world', root, { type: '_world_organization', id }).find((row) => row.type === '_world_organization' && row.id === id && row.deleted !== true);
1625
+ return row ? { id, label: row.label, members: [...row.members] } : { id, label: id, members: ['owner@world.test'] };
1626
+ },
1627
+ deployment: () => {
1628
+ const id = credentialFields[credentialDoor?.deployment?.field ?? 'deployment'] ?? '';
1629
+ if (id !== '' && !isWorldAccountName(id))
1630
+ throw new Error('Credential deployment must be a name of at most 128 letters, digits, dots, underscores or hyphens');
1631
+ return id;
1632
+ },
1633
+ vendorName,
1634
+ tenant: async (resource, make) => {
1635
+ const id = await context.account();
1636
+ return retainNativeAccountLink(m.service, root, at, call, '_world_account', id, 'tenant', resource, async () => make(await context.organization()), async (link) => liveNativeRow(link));
1637
+ },
1638
+ deploymentObject: async (kind, resource, make) => {
1639
+ const organization = await context.account(), deployment = context.deployment();
1640
+ return retainNativeAccountLink(m.service, root, at, call, '_world_deployment_object', `${organization}/${deployment}/${kind}`, undefined, resource, () => make(deployment), async (link) => liveNativeRow(link));
1641
+ },
1642
+ accountKey: async (purpose, resource, key) => {
1643
+ const organization = await context.account(), deployment = context.deployment();
1644
+ return retainNativeAccountLink(m.service, root, at, call, '_world_account_key', `${organization}/${deployment}/${purpose}`, undefined, resource, () => key.make(), async (link) => {
1645
+ const row = liveNativeRow(link);
1646
+ if (!row)
1647
+ return undefined;
1648
+ const secret = await key.read(row);
1649
+ return secret === undefined ? undefined : { id: link.id, secret };
1650
+ });
1564
1651
  },
1565
1652
  record: async (type, fields, id, operation) => {
1566
1653
  if (!type.startsWith('_'))
@@ -1621,7 +1708,12 @@ export async function contextFor(m, call, scope) {
1621
1708
  const row = await atomicWrite(m, call, resource, (snapshot) => {
1622
1709
  const id = options?.id ?? mintId(m, resource, root, at, snapshot);
1623
1710
  const selected = typeof fields === 'function' ? fields(id) : fields;
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])));
1711
+ const hit = snapshot.find((r) => {
1712
+ if (r.type !== storedType(m, resource) || r.deleted === true)
1713
+ return false;
1714
+ const fields = { [m.resources[resource]?.idAs ?? 'id']: r.id, ...ownFields(r) };
1715
+ return rules.some((rule) => rule.fields.every((field) => selected[field] !== undefined && fields[field] === selected[field]));
1716
+ });
1625
1717
  if (hit) {
1626
1718
  existing = view(m, resource, hit);
1627
1719
  return undefined;
@@ -1661,7 +1753,7 @@ export async function contextFor(m, call, scope) {
1661
1753
  },
1662
1754
  writeDetailed: (resource, id, fields, operation) => writeDetailed(m, call, resource, id, fields, operation, params, root, at, stateChecks),
1663
1755
  resolve: (resource, id) => resolveSubjectId(m.service, storedType(m, resource), id, root),
1664
- storedType: (resource) => storedType(m, resource),
1756
+ storedType: (resource) => m.resources[resource]?.storedAs ?? resource,
1665
1757
  ok: (fields) => Response.json({ ...(m.success ?? {}), ...fields }),
1666
1758
  row: (resource, id, opts) => { const row = stored(m, resource, root, opts).find((r) => r.id === id); return row ? rawRow(resource, row) : undefined; },
1667
1759
  rowsRaw: (resource, opts) => stored(m, resource, root, opts).map((row) => rawRow(resource, row)),
@@ -1782,10 +1874,12 @@ const BATCH_REFUSED_READS = ['get', 'rows', 'tree', 'history', 'find', 'list', '
1782
1874
  /** The members that write, or wait on writes, or answer as another context: each lands the staged writes first. */
1783
1875
  const BATCH_LANDING_MEMBERS = ['create', 'createUnique', 'change', 'accumulate', 'writeDetailed', 'record', 'atomically', 'remove', 'issue', 'at', 'over', 'core', 'awaitWrite', 'awaitCondition',
1784
1876
  // a World's seed and signing keys are written when first asked (worldSeed, worldSigningKey, as a vendor's move)
1785
- 'secret', 'signingKey'];
1877
+ 'secret', 'signingKey', 'account', 'organization', 'tenant', 'deploymentObject', 'accountKey'];
1786
1878
  /** `ctx.batch` over a context whose writes may be grouped (see `batchable`). */
1787
1879
  function bindBatch(m, call, context, params, root, at, rawRow) {
1788
1880
  let staged;
1881
+ // A callback's native object must land before the helper writes its retained link.
1882
+ let nativeAccountWrites = 0;
1789
1883
  // each staged subject as it will stand, by `type:id`, in the order first written
1790
1884
  const subjects = new Map();
1791
1885
  const land = async () => {
@@ -1801,7 +1895,19 @@ function bindBatch(m, call, context, params, root, at, rawRow) {
1801
1895
  const plain = ctx[name];
1802
1896
  if (typeof plain !== 'function')
1803
1897
  continue;
1804
- ctx[name] = async (...args) => { await land(); return plain(...args); };
1898
+ ctx[name] = async (...args) => {
1899
+ await land();
1900
+ const native = name === 'tenant' || name === 'deploymentObject' || name === 'accountKey';
1901
+ if (native)
1902
+ nativeAccountWrites++;
1903
+ try {
1904
+ return await plain(...args);
1905
+ }
1906
+ finally {
1907
+ if (native)
1908
+ nativeAccountWrites--;
1909
+ }
1910
+ };
1805
1911
  }
1806
1912
  for (const name of BATCH_REFUSED_READS) {
1807
1913
  const plain = ctx[name];
@@ -1816,7 +1922,7 @@ function bindBatch(m, call, context, params, root, at, rawRow) {
1816
1922
  const plainWrite = context.write, plainRow = context.row, plainRowsRaw = context.rowsRaw;
1817
1923
  context.write = async (resource, id, fields, operation) => {
1818
1924
  // a vendor's own move (ctx.asVendor) is the clock's write, recorded as the system's: it lands alone, in its place
1819
- if (!staged || inVendorMove()) {
1925
+ if (!staged || nativeAccountWrites > 0 || inVendorMove()) {
1820
1926
  await land();
1821
1927
  return plainWrite(resource, id, fields, operation);
1822
1928
  }
@@ -204,6 +204,18 @@ export type TwinPack = {
204
204
  fill: Record<string, string>;
205
205
  account?: {
206
206
  field: string;
207
+ } & ({
208
+ tenancy?: never;
209
+ source?: never;
210
+ } | {
211
+ tenancy: string;
212
+ source: string;
213
+ });
214
+ deployment?: {
215
+ field: string;
216
+ object: string;
217
+ source: string;
218
+ organizationOutputs?: string[];
207
219
  };
208
220
  };
209
221
  /** Another pack of this vendor issues the API's credentials; architecture, descriptor World wiring. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/world-core",
3
- "version": "3.0.162",
3
+ "version": "3.0.163",
4
4
  "description": "The kernel of Volter World: one log per twin, branches as pointers, checkpoints, the fold that keeps a twin current, the head that performs a write against the vendor, references, and the git plane. A twin package builds on it; the runtime serves it.",
5
5
  "keywords": [
6
6
  "twin",
@@ -43,7 +43,7 @@ import type { SubjectFields } from './hash.ts';
43
43
  import { twinPublicBase, withRequestScopes } from './twin-fetch.ts';
44
44
  import { inVendorMove, isReadOnlyRequest, runAsVendorMove, writesRefused } from './request-scope.ts';
45
45
  import type { DerivedCall, DerivedCoreOutcome, DerivedHandler, DerivedOperation } from './derived.ts';
46
- import type { PackDescriptor } from './packRegistry.ts';
46
+ import { getPack, type PackDescriptor } from './packRegistry.ts';
47
47
  import type { ScenarioDecision, ScenarioHandler } from './scenario.ts';
48
48
  import { bodyTextOf, checkJsonBody, takeParsedBody } from './derived.ts';
49
49
 
@@ -996,6 +996,62 @@ export async function worldAccount(service: string, root: string | undefined, id
996
996
  return id;
997
997
  }
998
998
 
999
+ type WorldOrganization = { id: string; label: string; members: string[] };
1000
+ type VendorNameRule = { max: number; lower?: boolean; allowed: RegExp; separator?: string; first?: RegExp };
1001
+ type NativeAccountLink = { resource: string; id: string };
1002
+
1003
+ function nativeAccountLink(row: TwinResource | undefined, field?: string): NativeAccountLink | undefined {
1004
+ if (!row || row.deleted === true) return undefined;
1005
+ const fields = ownFields(row);
1006
+ const value = field === undefined ? fields : fields[field];
1007
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
1008
+ const link = value as Record<string, unknown>;
1009
+ return typeof link.resource === 'string' && typeof link.id === 'string' ? { resource: link.resource, id: link.id } : undefined;
1010
+ }
1011
+
1012
+ const sameAccountLink = (a: NativeAccountLink | undefined, b: NativeAccountLink | undefined): boolean =>
1013
+ a?.resource === b?.resource && a?.id === b?.id;
1014
+
1015
+ /** Native creation and reconstruction run outside the action lock; only the retained link is compared and written. */
1016
+ async function retainNativeAccountLink<T>(
1017
+ service: string, root: string | undefined, at: string, call: DerivedCall,
1018
+ type: string, id: string, field: string | undefined, resource: string,
1019
+ make: () => Promise<string>, read: (link: NativeAccountLink) => Promise<T | undefined>,
1020
+ ): Promise<T> {
1021
+ for (;;) {
1022
+ const before = nativeAccountLink(resourcesOfType(service, type, root).find((row) => row.id === id), field);
1023
+ const held = before && await read(before);
1024
+ if (held !== undefined) return held;
1025
+ const made = { resource, id: await make() };
1026
+ if (await read(made) === undefined) throw new Error(`Credential ${type}/${id}: native ${resource}/${made.id} is unavailable after creation`);
1027
+ const { value } = await applyTwinWriteAtomic(service, (snapshot) => {
1028
+ const current = nativeAccountLink(snapshot.find((row) => row.type === type && row.id === id), field);
1029
+ if (!sameAccountLink(current, before)) return { kind: 'skip', value: current };
1030
+ return {
1031
+ kind: 'write', value: made,
1032
+ write: {
1033
+ operation: `${type.slice(1)}.link`, subjectType: type, subjectId: id,
1034
+ fields: field === undefined ? made : { name: id, [field]: made },
1035
+ ...provisioningInput(call), occurredAt: at, actor: { kind: 'system' },
1036
+ },
1037
+ };
1038
+ }, root);
1039
+ const linked = value && await read(value);
1040
+ if (linked !== undefined) return linked;
1041
+ }
1042
+ }
1043
+
1044
+ function vendorName(label: string, rule: VendorNameRule, { suffixed = false, identity = label }: { suffixed?: boolean; identity?: string } = {}): string {
1045
+ const allowed = new RegExp(rule.allowed.source, rule.allowed.flags.replace(/[gy]/g, ''));
1046
+ const separator = rule.separator ?? '-';
1047
+ if ([...separator].length !== 1 || !allowed.test(separator)) throw new Error('Credential name separator must be one allowed character');
1048
+ const first = rule.first && new RegExp(rule.first.source, rule.first.flags.replace(/[gy]/g, ''));
1049
+ let name = [...(rule.lower ? label.toLowerCase() : label)].map((letter) => allowed.test(letter) ? letter : separator).join('');
1050
+ name = name.replace(new RegExp(`${escapeRe(separator)}+`, 'g'), separator);
1051
+ if (first && !first.test(name[0] ?? '')) name = `w${separator}${name}`;
1052
+ return name !== label || name.length > rule.max || suffixed ? `${name.slice(0, Math.max(0, rule.max - 9))}${separator}${sha256(identity).slice(0, 8)}` : name;
1053
+ }
1054
+
999
1055
  /** A World's signing key for a label (ctx.signingKey): made once from the platform's random source, then read; the
1000
1056
  * making is one per service store and label in the process, as the seed's is. */
1001
1057
  const SIGNING_KEY = '_world_signing_key';
@@ -1959,6 +2015,18 @@ export type SemanticsContext = {
1959
2015
  secret(label: string): Promise<string>;
1960
2016
  /** Select an account at a pack's credential door. Packs retain their own vendor account/key semantics. */
1961
2017
  account(field?: string): Promise<string>;
2018
+ /** The selected organization's label and people from the World's organization registry. */
2019
+ organization(): Promise<WorldOrganization>;
2020
+ /** The selected deployment, or an empty string for the organization's own level. */
2021
+ deployment(): string;
2022
+ /** A vendor-safe label; retained links keep identity independent of names. */
2023
+ vendorName(label: string, rule: VendorNameRule, options?: { suffixed?: boolean; identity?: string }): string;
2024
+ /** Retain the native customer tenant for the selected organization. */
2025
+ tenant(resource: string, make: (organization: WorldOrganization) => Promise<string>): Promise<Record<string, unknown>>;
2026
+ /** Retain one native resource of this kind for the selected organization and deployment. */
2027
+ deploymentObject(kind: string, resource: string, make: (deployment: string) => Promise<string>): Promise<Record<string, unknown>>;
2028
+ /** Reconstruct a live native key, replacing its link when the vendor's reader refuses it. */
2029
+ accountKey(purpose: string, resource: string, key: { make: () => Promise<string>; read: (row: Record<string, unknown>) => Promise<string | undefined> }): Promise<{ id: string; secret: string }>;
1962
2030
  /** The World's machine pool (machines.ts), as the pack's `machine-pool` door enrolled it; `none` when none was. */
1963
2031
  machines: MachinePool;
1964
2032
  /** 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
@@ -2105,7 +2173,7 @@ export type HandlerContext = Omit<SemanticsContext, 'now' | 'core' | 'atomically
2105
2173
  };
2106
2174
  /** What a write hook reads: the writing call's context without its ways to write, so a hook that renders an event
2107
2175
  * cannot write again (and run itself again). */
2108
- export type WriteHookContext = Omit<HandlerContext, 'write' | 'batch' | 'create' | 'createUnique' | 'change' | 'issue' | 'record' | 'accumulate' | 'at' | 'over'>;
2176
+ export type WriteHookContext = Omit<HandlerContext, 'write' | 'batch' | 'create' | 'createUnique' | 'change' | 'issue' | 'record' | 'accumulate' | 'tenant' | 'deploymentObject' | 'accountKey' | 'at' | 'over'>;
2109
2177
  /** A derived pack's handler: one operation, by its operationId, over the contract's context. */
2110
2178
  export type Handler = (ctx: HandlerContext) => Promise<Response>;
2111
2179
 
@@ -2153,6 +2221,8 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2153
2221
  });
2154
2222
  const paths = expandPaths(m, params);
2155
2223
  const stateChecks: StateChecks = new Map();
2224
+ const credentialDoor = m.descriptor?.credentialDoor ?? getPack(m.vendor)?.credentialDoor;
2225
+ const credentialFields = body && typeof body === 'object' && !Array.isArray(body) ? body as Record<string, unknown> : {};
2156
2226
  const rawResources = new WeakMap<Record<string, unknown>, string>();
2157
2227
  const rawRow = (resource: string, row: TwinResource): Record<string, unknown> => {
2158
2228
  const name = m.resources[resource] ? resource : Object.keys(m.resources).find((name) => storedType(m, name) === row.type);
@@ -2166,6 +2236,10 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2166
2236
  rawResources.set(row as Record<string, unknown>, name ?? resource);
2167
2237
  return row as Record<string, unknown>;
2168
2238
  };
2239
+ const liveNativeRow = (link: NativeAccountLink): Record<string, unknown> | undefined => {
2240
+ const row = context.row(link.resource, link.id);
2241
+ return row && row.deleted !== true ? row : undefined;
2242
+ };
2169
2243
  const context: SemanticsContext = {
2170
2244
  call,
2171
2245
  params,
@@ -2218,9 +2292,39 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2218
2292
  },
2219
2293
  occurredAt: at,
2220
2294
  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');
2295
+ account: async (field = credentialDoor?.account?.field ?? 'account') => {
2296
+ return worldAccount(m.service, root, credentialFields[field] ?? 'default', at, call.request.headers.get('x-volter-world-activity') === 'provisioning');
2297
+ },
2298
+ organization: async () => {
2299
+ const id = await context.account();
2300
+ const row = projectOwnerResources('_world', root, { type: '_world_organization', id }).find((row) => row.type === '_world_organization' && row.id === id && row.deleted !== true);
2301
+ return row ? { id, label: row.label as string, members: [...(row.members as string[])] } : { id, label: id, members: ['owner@world.test'] };
2302
+ },
2303
+ deployment: () => {
2304
+ const id = credentialFields[credentialDoor?.deployment?.field ?? 'deployment'] ?? '';
2305
+ if (id !== '' && !isWorldAccountName(id)) throw new Error('Credential deployment must be a name of at most 128 letters, digits, dots, underscores or hyphens');
2306
+ return id;
2307
+ },
2308
+ vendorName,
2309
+ tenant: async (resource, make) => {
2310
+ const id = await context.account();
2311
+ return retainNativeAccountLink(m.service, root, at, call, '_world_account', id, 'tenant', resource,
2312
+ async () => make(await context.organization()), async (link) => liveNativeRow(link));
2313
+ },
2314
+ deploymentObject: async (kind, resource, make) => {
2315
+ const organization = await context.account(), deployment = context.deployment();
2316
+ return retainNativeAccountLink(m.service, root, at, call, '_world_deployment_object', `${organization}/${deployment}/${kind}`, undefined, resource,
2317
+ () => make(deployment), async (link) => liveNativeRow(link));
2318
+ },
2319
+ accountKey: async (purpose, resource, key) => {
2320
+ const organization = await context.account(), deployment = context.deployment();
2321
+ return retainNativeAccountLink(m.service, root, at, call, '_world_account_key', `${organization}/${deployment}/${purpose}`, undefined, resource,
2322
+ () => key.make(), async (link) => {
2323
+ const row = liveNativeRow(link);
2324
+ if (!row) return undefined;
2325
+ const secret = await key.read(row);
2326
+ return secret === undefined ? undefined : { id: link.id, secret };
2327
+ });
2224
2328
  },
2225
2329
  record: async (type, fields, id, operation) => {
2226
2330
  if (!type.startsWith('_')) throw new Error(`semantics: ${type} is not a bookkeeping type (bookkeeping types start with _)`);
@@ -2272,7 +2376,11 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2272
2376
  const row = await atomicWrite(m, call, resource, (snapshot) => {
2273
2377
  const id = options?.id ?? mintId(m, resource, root, at, snapshot);
2274
2378
  const selected = typeof fields === 'function' ? fields(id) : fields;
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])));
2379
+ const hit = snapshot.find((r) => {
2380
+ if (r.type !== storedType(m, resource) || r.deleted === true) return false;
2381
+ const fields = { [m.resources[resource]?.idAs ?? 'id']: r.id, ...ownFields(r) };
2382
+ return rules.some((rule) => rule.fields.every((field) => selected[field] !== undefined && fields[field] === selected[field]));
2383
+ });
2276
2384
  if (hit) { existing = view(m, resource, hit); return undefined; }
2277
2385
  return { id, fields: selected };
2278
2386
  }, operation, params, root, at, undefined, stateChecks, options?.input === 'omit');
@@ -2304,7 +2412,7 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
2304
2412
  },
2305
2413
  writeDetailed: (resource, id, fields, operation) => writeDetailed(m, call, resource, id, fields, operation, params, root, at, stateChecks),
2306
2414
  resolve: (resource, id) => resolveSubjectId(m.service, storedType(m, resource), id, root),
2307
- storedType: (resource) => storedType(m, resource),
2415
+ storedType: (resource) => m.resources[resource]?.storedAs ?? resource,
2308
2416
  ok: (fields) => Response.json({ ...(m.success ?? {}), ...fields }),
2309
2417
  row: (resource, id, opts) => { const row = stored(m, resource, root, opts).find((r) => r.id === id); return row ? rawRow(resource, row) : undefined; },
2310
2418
  rowsRaw: (resource, opts) => stored(m, resource, root, opts).map((row) => rawRow(resource, row)),
@@ -2427,12 +2535,14 @@ const BATCH_REFUSED_READS = ['get', 'rows', 'tree', 'history', 'find', 'list', '
2427
2535
  /** The members that write, or wait on writes, or answer as another context: each lands the staged writes first. */
2428
2536
  const BATCH_LANDING_MEMBERS = ['create', 'createUnique', 'change', 'accumulate', 'writeDetailed', 'record', 'atomically', 'remove', 'issue', 'at', 'over', 'core', 'awaitWrite', 'awaitCondition',
2429
2537
  // a World's seed and signing keys are written when first asked (worldSeed, worldSigningKey, as a vendor's move)
2430
- 'secret', 'signingKey'] as const;
2538
+ 'secret', 'signingKey', 'account', 'organization', 'tenant', 'deploymentObject', 'accountKey'] as const;
2431
2539
 
2432
2540
  /** `ctx.batch` over a context whose writes may be grouped (see `batchable`). */
2433
2541
  function bindBatch(m: DerivedManifest, call: DerivedCall, context: SemanticsContext, params: Record<string, unknown>, root: string | undefined, at: string,
2434
2542
  rawRow: (resource: string, row: TwinResource) => Record<string, unknown>): void {
2435
2543
  let staged: StagedWrite[] | undefined;
2544
+ // A callback's native object must land before the helper writes its retained link.
2545
+ let nativeAccountWrites = 0;
2436
2546
  // each staged subject as it will stand, by `type:id`, in the order first written
2437
2547
  const subjects = new Map<string, Subject>();
2438
2548
  const land = async (): Promise<void> => {
@@ -2446,7 +2556,13 @@ function bindBatch(m: DerivedManifest, call: DerivedCall, context: SemanticsCont
2446
2556
  for (const name of BATCH_LANDING_MEMBERS) {
2447
2557
  const plain = ctx[name];
2448
2558
  if (typeof plain !== 'function') continue;
2449
- ctx[name] = async (...args: unknown[]) => { await land(); return (plain as (...a: unknown[]) => unknown)(...args); };
2559
+ ctx[name] = async (...args: unknown[]) => {
2560
+ await land();
2561
+ const native = name === 'tenant' || name === 'deploymentObject' || name === 'accountKey';
2562
+ if (native) nativeAccountWrites++;
2563
+ try { return await (plain as (...a: unknown[]) => unknown)(...args); }
2564
+ finally { if (native) nativeAccountWrites--; }
2565
+ };
2450
2566
  }
2451
2567
  for (const name of BATCH_REFUSED_READS) {
2452
2568
  const plain = ctx[name];
@@ -2459,7 +2575,7 @@ function bindBatch(m: DerivedManifest, call: DerivedCall, context: SemanticsCont
2459
2575
  const plainWrite = context.write, plainRow = context.row, plainRowsRaw = context.rowsRaw;
2460
2576
  context.write = async (resource, id, fields, operation) => {
2461
2577
  // a vendor's own move (ctx.asVendor) is the clock's write, recorded as the system's: it lands alone, in its place
2462
- if (!staged || inVendorMove()) { await land(); return plainWrite(resource, id, fields, operation); }
2578
+ if (!staged || nativeAccountWrites > 0 || inVendorMove()) { await land(); return plainWrite(resource, id, fields, operation); }
2463
2579
  const type = storedType(m, resource), key = `${type}:${id}`;
2464
2580
  // the one subject, as the memoized tree holds it (readTreeMap would copy the whole tree on every staged write)
2465
2581
  const subject = foldLocalWrite(subjects.get(key) ?? heldSubject(m.service, type, id, root), type, id, fields as SubjectFields, at);
@@ -221,7 +221,13 @@ export type TwinPack = {
221
221
  * answer: `init` writes `$issue:<service>` for each, and `up`, once this twin is running, POSTs the body to the door
222
222
  * and sets each name to its field (`file:<field>` materializes one file; `directory:<field>` a relative-file map). A twin that holds its keys (refusing any it
223
223
  * did not issue) declares one, so a World's applications hold keys it issued rather than fixtures it refuses. */
224
- credentialDoor?: { path: string; body: Record<string, unknown>; fill: Record<string, string>; account?: { field: string } };
224
+ credentialDoor?: {
225
+ path: string;
226
+ body: Record<string, unknown>;
227
+ fill: Record<string, string>;
228
+ account?: { field: string } & ({ tenancy?: never; source?: never } | { tenancy: string; source: string });
229
+ deployment?: { field: string; object: string; source: string; organizationOutputs?: string[] };
230
+ };
225
231
  /** Another pack of this vendor issues the API's credentials; architecture, descriptor World wiring. */
226
232
  credentialIssuer?: string;
227
233
  prismaAdapter?: { adapter: string; export: string; urlEnv: string };