@volter/world-core 3.0.160 → 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 +190 -6
- 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/live/rtmp.js +3 -0
- 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 +190 -6
- package/inject.cjs +2 -2
- package/package.json +9 -1
- package/src/derived-core.ts +125 -63
- package/src/index.ts +5 -2
- package/src/live/rtmp.ts +3 -0
- 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/src/derived-core.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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])
|
|
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
|
-
|
|
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
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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
|
-
|
|
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((
|
|
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 (
|
|
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
|
|
1049
|
-
const ms = BigInt(Date.parse(at
|
|
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 (
|
|
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
|
|
1067
|
-
const held = new Set((
|
|
1068
|
-
const seconds = Math.floor((Date.parse(at
|
|
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
|
|
1078
|
-
const held = new Set((
|
|
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
|
|
1088
|
-
const held = new Set((
|
|
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
|
|
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:
|
|
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
|
|
1358
|
-
const
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
2180
|
-
const { resource: row, result } = await applyTwinWrite(m.service, { operation: operation ?? `${type.slice(1)}.record`, subjectType:
|
|
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 !==
|
|
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:
|
|
2198
|
-
}, root, () => resourcesOfType(m.service,
|
|
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,
|
|
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
|
|
2206
|
-
return guardedMachinePool(poolFor(m.service, root, enrolled?.kind ?? 'none', enrolled?.httpReplies
|
|
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: () =>
|
|
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) =>
|
|
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 =
|
|
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/live/rtmp.ts
CHANGED
|
@@ -204,6 +204,9 @@ export function rtmpSession(sink: TwinStreamSink, publisher: RtmpPublisher, log:
|
|
|
204
204
|
const media = async (hand: () => void | Promise<void>): Promise<void> => {
|
|
205
205
|
try { await hand(); } catch (error) {
|
|
206
206
|
if (!(error instanceof RtmpRefusal)) throw error;
|
|
207
|
+
// said in the World's log too, not only to the publisher: an ended broadcast is otherwise unreadable afterwards
|
|
208
|
+
// (the stream key is the publish name, so it is never logged)
|
|
209
|
+
log(`a publish to ${app || 'its app'} was refused and ended: ${error.message}`);
|
|
207
210
|
status(publishStream, 'error', error.code, error.message);
|
|
208
211
|
await stop();
|
|
209
212
|
close();
|
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.
|
|
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'
|
|
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'
|
|
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
|
|
104
|
-
async files() { throw new Error('file access
|
|
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[] = []
|
|
191
|
-
if (kind
|
|
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[] = []
|
|
241
|
-
const key = `${service}\0${root ?? ''}\0${kind}
|
|
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
|
|
219
|
+
pool = machinePool(kind, h.toString(16).padStart(8, '0'), httpReplies);
|
|
248
220
|
pools.set(key, pool);
|
|
249
221
|
}
|
|
250
222
|
return pool;
|