@volter/world-core 3.0.155 → 3.0.157
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/attach.cjs +2 -0
- package/capability-decision.cjs +6 -4
- package/dist/attach.cjs +2 -0
- package/dist/capability-decision.cjs +6 -4
- package/dist/generated/pack-facts.json +204 -4
- package/dist/inject.cjs +150 -86
- package/dist/network-policy.cjs +11 -1
- package/dist/network-policy.d.cts +1 -0
- package/dist/pack-facts.d.cts +3 -0
- package/dist/required-credential-transport.cjs +29 -0
- package/dist/required-credential-transport.d.cts +3 -0
- package/dist/src/actions.d.ts +15 -0
- package/dist/src/actions.js +82 -21
- package/dist/src/ancestry.d.ts +5 -4
- package/dist/src/ancestry.js +53 -28
- package/dist/src/blob-store.d.ts +16 -0
- package/dist/src/blob-store.js +76 -1
- package/dist/src/changeset.js +1 -1
- package/dist/src/clickhouse/compression.d.ts +28 -0
- package/dist/src/clickhouse/compression.js +281 -0
- package/dist/src/clickhouse/index.d.ts +1 -0
- package/dist/src/clickhouse/index.js +1 -0
- package/dist/src/derived-core.d.ts +84 -7
- package/dist/src/derived-core.js +336 -61
- package/dist/src/derived-real.js +97 -15
- package/dist/src/derived.d.ts +5 -0
- package/dist/src/derived.js +3 -0
- package/dist/src/executor.d.ts +7 -0
- package/dist/src/executor.js +15 -1
- package/dist/src/fork.js +1 -1
- package/dist/src/git/lfs.d.ts +13 -3
- package/dist/src/git/lfs.js +41 -9
- package/dist/src/grpc-wire.js +2 -1
- package/dist/src/h2.js +63 -10
- package/dist/src/head.d.ts +5 -0
- package/dist/src/head.js +15 -0
- package/dist/src/history.d.ts +1 -1
- package/dist/src/history.js +7 -7
- package/dist/src/index.d.ts +5 -1
- package/dist/src/index.js +6 -1
- package/dist/src/live/ingest.js +9 -13
- package/dist/src/live/rtmp.js +7 -5
- package/dist/src/live/segmenter.js +7 -1
- package/dist/src/log.d.ts +23 -3
- package/dist/src/log.js +160 -37
- package/dist/src/machines.js +5 -1
- package/dist/src/managed-go-programs.d.ts +55 -0
- package/dist/src/managed-go-programs.js +133 -0
- package/dist/src/managed-original-inputs.d.ts +101 -0
- package/dist/src/managed-original-inputs.js +125 -0
- package/dist/src/mongodb/client.d.ts +8 -0
- package/dist/src/mongodb/client.js +27 -0
- package/dist/src/mongodb/index.d.ts +2 -0
- package/dist/src/mongodb/index.js +5 -0
- package/dist/src/mongodb/stream.d.ts +97 -0
- package/dist/src/mongodb/stream.js +206 -0
- package/dist/src/observe.js +1 -1
- package/dist/src/pack-fetch.d.ts +6 -1
- package/dist/src/pack-fetch.js +69 -14
- package/dist/src/pack-paths.d.ts +2 -1
- package/dist/src/pack-paths.js +6 -1
- package/dist/src/packRegistry.d.ts +10 -14
- package/dist/src/packRegistry.js +2 -13
- package/dist/src/protocol.d.ts +13 -0
- package/dist/src/protocol.js +13 -0
- package/dist/src/redis/engine.d.ts +10 -4
- package/dist/src/redis/engine.js +103 -32
- package/dist/src/redis/member-storage.js +20 -8
- package/dist/src/redis/service-times.d.ts +25 -0
- package/dist/src/redis/service-times.js +87 -0
- package/dist/src/redis/stream.d.ts +2 -1
- package/dist/src/redis/stream.js +109 -20
- package/dist/src/resource-blob.d.ts +4 -0
- package/dist/src/resource-blob.js +8 -1
- package/dist/src/runtime-state.d.ts +11 -0
- package/dist/src/runtime-state.js +13 -0
- package/dist/src/runtime.d.ts +7 -5
- package/dist/src/runtime.js +7 -4
- package/dist/src/serve-http.d.ts +2 -0
- package/dist/src/serve-http.js +73 -6
- package/dist/src/serve.d.ts +10 -0
- package/dist/src/serve.js +43 -3
- package/dist/src/sockets.d.ts +4 -1
- package/dist/src/sockets.js +59 -6
- package/dist/src/storage.d.ts +2 -1
- package/dist/src/storage.js +6 -3
- package/dist/src/vendor-call.js +58 -0
- package/dist/src/world-boot.d.ts +1 -0
- package/dist/src/world-boot.js +6 -0
- package/dist/src/world-clock.js +6 -2
- package/dist/src/world-store.d.ts +122 -7
- package/dist/src/world-store.js +959 -103
- package/dist/src/xml.d.ts +30 -0
- package/dist/src/xml.js +74 -0
- package/generated/pack-facts.json +204 -4
- package/inject.cjs +150 -86
- package/network-policy.cjs +11 -1
- package/network-policy.d.cts +1 -0
- package/pack-facts.d.cts +3 -0
- package/package.json +27 -1
- package/required-credential-transport.cjs +29 -0
- package/required-credential-transport.d.cts +3 -0
- package/src/actions.ts +78 -21
- package/src/ancestry.ts +46 -24
- package/src/blob-store.ts +59 -1
- package/src/changeset.ts +1 -1
- package/src/clickhouse/compression.ts +172 -0
- package/src/clickhouse/index.ts +2 -0
- package/src/derived-core.ts +356 -61
- package/src/derived-real.ts +80 -14
- package/src/derived.ts +9 -1
- package/src/executor.ts +17 -3
- package/src/fork.ts +1 -1
- package/src/git/lfs.ts +40 -9
- package/src/grpc-wire.ts +2 -1
- package/src/h2.ts +46 -5
- package/src/head.ts +15 -0
- package/src/history.ts +7 -7
- package/src/index.ts +7 -1
- package/src/live/ingest.ts +7 -3
- package/src/live/rtmp.ts +7 -5
- package/src/live/segmenter.ts +7 -1
- package/src/log.ts +147 -32
- package/src/machines.ts +5 -1
- package/src/managed-go-programs.ts +146 -0
- package/src/managed-original-inputs.ts +163 -0
- package/src/mongodb/client.ts +27 -0
- package/src/mongodb/index.ts +5 -0
- package/src/mongodb/stream.ts +223 -0
- package/src/observe.ts +1 -1
- package/src/pack-fetch.ts +66 -16
- package/src/pack-paths.ts +5 -1
- package/src/packRegistry.ts +3 -15
- package/src/protocol.ts +13 -0
- package/src/redis/engine.ts +100 -57
- package/src/redis/member-storage.ts +17 -9
- package/src/redis/service-times.ts +81 -0
- package/src/redis/stream.ts +66 -14
- package/src/resource-blob.ts +9 -1
- package/src/runtime-state.ts +14 -0
- package/src/runtime.ts +10 -3
- package/src/serve-http.ts +65 -6
- package/src/serve.ts +45 -3
- package/src/sockets.ts +39 -8
- package/src/storage.ts +5 -3
- package/src/vendor-call.ts +54 -0
- package/src/world-boot.ts +6 -0
- package/src/world-clock.ts +6 -2
- package/src/world-store.ts +768 -91
- package/src/xml.ts +71 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface NativeGoKitReference {
|
|
2
|
+
package: {
|
|
3
|
+
name: string;
|
|
4
|
+
version: string;
|
|
5
|
+
};
|
|
6
|
+
target: 'wasm32-wali-go-single-thread';
|
|
7
|
+
releaseSha256: string;
|
|
8
|
+
producerSha256: string;
|
|
9
|
+
sourceSha256: string;
|
|
10
|
+
selection: {
|
|
11
|
+
path: string;
|
|
12
|
+
sha256: string;
|
|
13
|
+
};
|
|
14
|
+
recipe: {
|
|
15
|
+
path: string;
|
|
16
|
+
sha256: string;
|
|
17
|
+
};
|
|
18
|
+
installedRoot: string;
|
|
19
|
+
}
|
|
20
|
+
export interface NativeGoOperation {
|
|
21
|
+
sourceService: string;
|
|
22
|
+
role: 'server' | 'prerequisite' | 'health';
|
|
23
|
+
kit: string;
|
|
24
|
+
program: {
|
|
25
|
+
name: string;
|
|
26
|
+
version: string;
|
|
27
|
+
manifestId: string;
|
|
28
|
+
manifestSha256: string;
|
|
29
|
+
path: string;
|
|
30
|
+
};
|
|
31
|
+
/** Resolved arguments require separate comparison with the full source. */
|
|
32
|
+
args: string[];
|
|
33
|
+
/** Names of existing World values, never caller secrets in this record. */
|
|
34
|
+
environmentBindings: Record<string, string>;
|
|
35
|
+
requires: {
|
|
36
|
+
service: string;
|
|
37
|
+
condition: 'service_started' | 'service_healthy' | 'service_completed_successfully';
|
|
38
|
+
}[];
|
|
39
|
+
}
|
|
40
|
+
export interface NativeGoReference {
|
|
41
|
+
version: 2;
|
|
42
|
+
implementation: string;
|
|
43
|
+
release: string;
|
|
44
|
+
directory: string;
|
|
45
|
+
snapshotSha256: string;
|
|
46
|
+
declarationSha256: string;
|
|
47
|
+
image: string;
|
|
48
|
+
kits: Record<string, NativeGoKitReference>;
|
|
49
|
+
server: NativeGoOperation;
|
|
50
|
+
prerequisites: NativeGoOperation[];
|
|
51
|
+
health: NativeGoOperation;
|
|
52
|
+
}
|
|
53
|
+
/** All fields are declared data. The later owned consumer must authenticate
|
|
54
|
+
* the complete source, kit, original image and admitted installed manifests. */
|
|
55
|
+
export declare function nativeGoReference(value: unknown): NativeGoReference;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/** Tagged managed Go operation metadata. This structural reader grants no
|
|
2
|
+
* source/image correspondence, filesystem custody, catalog admission or exec. */
|
|
3
|
+
import { posix } from 'node:path';
|
|
4
|
+
const fail = (field) => { throw new Error('Native Go operation metadata is invalid: ' + field); };
|
|
5
|
+
function object(value, fields, label) {
|
|
6
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
7
|
+
return fail(label);
|
|
8
|
+
const row = value;
|
|
9
|
+
if (Object.keys(row).length !== fields.length || fields.some(key => !Object.hasOwn(row, key)))
|
|
10
|
+
return fail(label);
|
|
11
|
+
return row;
|
|
12
|
+
}
|
|
13
|
+
function text(value, label) {
|
|
14
|
+
if (typeof value !== 'string' || !value || value.includes('\0'))
|
|
15
|
+
return fail(label);
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
function digest(value, label) {
|
|
19
|
+
const out = text(value, label);
|
|
20
|
+
if (!/^[a-f0-9]{64}$/u.test(out))
|
|
21
|
+
return fail(label);
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
function relativePath(value, label) {
|
|
25
|
+
const out = text(value, label);
|
|
26
|
+
if (out.startsWith('/') || out.includes('\\') || posix.normalize(out) !== out
|
|
27
|
+
|| out.split('/').some(part => !part || part === '.' || part === '..'))
|
|
28
|
+
return fail(label);
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
function installedPath(value, label) {
|
|
32
|
+
const out = text(value, label);
|
|
33
|
+
if (!out.startsWith('/opt/programs/') || out.includes('\\') || posix.normalize(out) !== out
|
|
34
|
+
|| out.split('/').slice(1).some(part => !part || part === '.' || part === '..'))
|
|
35
|
+
return fail(label);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
function pin(value, label) {
|
|
39
|
+
const row = object(value, ['path', 'sha256'], label);
|
|
40
|
+
return { path: relativePath(row.path, label + '.path'), sha256: digest(row.sha256, label + '.sha256') };
|
|
41
|
+
}
|
|
42
|
+
function kit(value, label) {
|
|
43
|
+
const row = object(value, ['package', 'target', 'releaseSha256', 'producerSha256', 'sourceSha256', 'selection', 'recipe', 'installedRoot'], label);
|
|
44
|
+
const pack = object(row.package, ['name', 'version'], label + '.package');
|
|
45
|
+
if (row.target !== 'wasm32-wali-go-single-thread')
|
|
46
|
+
return fail(label + '.target');
|
|
47
|
+
return { package: { name: text(pack.name, label + '.package.name'), version: text(pack.version, label + '.package.version') },
|
|
48
|
+
target: row.target, releaseSha256: digest(row.releaseSha256, label + '.releaseSha256'),
|
|
49
|
+
producerSha256: digest(row.producerSha256, label + '.producerSha256'), sourceSha256: digest(row.sourceSha256, label + '.sourceSha256'),
|
|
50
|
+
selection: pin(row.selection, label + '.selection'), recipe: pin(row.recipe, label + '.recipe'),
|
|
51
|
+
installedRoot: installedPath(row.installedRoot, label + '.installedRoot') };
|
|
52
|
+
}
|
|
53
|
+
function operation(value, role, kits, label) {
|
|
54
|
+
const row = object(value, ['sourceService', 'role', 'kit', 'program', 'args', 'environmentBindings', 'requires'], label);
|
|
55
|
+
const selected = text(row.kit, label + '.kit');
|
|
56
|
+
if (row.role !== role || !Object.hasOwn(kits, selected))
|
|
57
|
+
return fail(label + '.role/kit');
|
|
58
|
+
const provider = kits[selected];
|
|
59
|
+
const command = object(row.program, ['name', 'version', 'manifestId', 'manifestSha256', 'path'], label + '.program');
|
|
60
|
+
const path = installedPath(command.path, label + '.program.path'), version = text(command.version, label + '.program.version');
|
|
61
|
+
if (version !== provider.package.version || !path.startsWith(provider.installedRoot + '/'))
|
|
62
|
+
return fail(label + '.program placement/version');
|
|
63
|
+
if (!Array.isArray(row.args))
|
|
64
|
+
return fail(label + '.args');
|
|
65
|
+
const args = [];
|
|
66
|
+
for (const arg of row.args) {
|
|
67
|
+
if (typeof arg !== 'string' || arg.includes('\0'))
|
|
68
|
+
return fail(label + '.args.' + args.length);
|
|
69
|
+
args.push(arg);
|
|
70
|
+
}
|
|
71
|
+
if (!row.environmentBindings || typeof row.environmentBindings !== 'object' || Array.isArray(row.environmentBindings))
|
|
72
|
+
return fail(label + '.environmentBindings');
|
|
73
|
+
const bindings = Object.create(null);
|
|
74
|
+
for (const [name, value] of Object.entries(row.environmentBindings)) {
|
|
75
|
+
const source = text(value, label + '.environmentBindings.' + name);
|
|
76
|
+
if (![name, source].every(item => /^[A-Za-z_][A-Za-z0-9_]*$/u.test(item)))
|
|
77
|
+
return fail(label + '.environmentBindings name');
|
|
78
|
+
bindings[name] = source;
|
|
79
|
+
}
|
|
80
|
+
if (!Array.isArray(row.requires))
|
|
81
|
+
return fail(label + '.requires');
|
|
82
|
+
const dependencies = [];
|
|
83
|
+
for (const value of row.requires) {
|
|
84
|
+
const edge = object(value, ['service', 'condition'], label + '.requires');
|
|
85
|
+
if (edge.condition !== 'service_started' && edge.condition !== 'service_healthy' && edge.condition !== 'service_completed_successfully')
|
|
86
|
+
return fail(label + '.requires.condition');
|
|
87
|
+
const service = text(edge.service, label + '.requires.service');
|
|
88
|
+
if (dependencies.some(prior => prior.service === service))
|
|
89
|
+
return fail(label + '.requires duplicate service');
|
|
90
|
+
dependencies.push({ service, condition: edge.condition });
|
|
91
|
+
}
|
|
92
|
+
return { sourceService: text(row.sourceService, label + '.sourceService'), role, kit: selected,
|
|
93
|
+
program: { name: text(command.name, label + '.program.name'), version, manifestId: text(command.manifestId, label + '.program.manifestId'),
|
|
94
|
+
manifestSha256: digest(command.manifestSha256, label + '.program.manifestSha256'), path },
|
|
95
|
+
args, environmentBindings: bindings, requires: dependencies };
|
|
96
|
+
}
|
|
97
|
+
/** All fields are declared data. The later owned consumer must authenticate
|
|
98
|
+
* the complete source, kit, original image and admitted installed manifests. */
|
|
99
|
+
export function nativeGoReference(value) {
|
|
100
|
+
const row = object(value, ['version', 'implementation', 'release', 'directory', 'snapshotSha256', 'declarationSha256', 'image', 'kits', 'server', 'prerequisites', 'health'], 'reference');
|
|
101
|
+
if (row.version !== 2)
|
|
102
|
+
return fail('version');
|
|
103
|
+
const image = text(row.image, 'image');
|
|
104
|
+
if (!/^[^\s@]+@sha256:[a-f0-9]{64}$/u.test(image))
|
|
105
|
+
return fail('image');
|
|
106
|
+
if (!row.kits || typeof row.kits !== 'object' || Array.isArray(row.kits) || !Object.keys(row.kits).length)
|
|
107
|
+
return fail('kits');
|
|
108
|
+
const kits = Object.create(null);
|
|
109
|
+
for (const [name, value] of Object.entries(row.kits))
|
|
110
|
+
kits[text(name, 'kit name')] = kit(value, 'kits.' + name);
|
|
111
|
+
const server = operation(row.server, 'server', kits, 'server'), health = operation(row.health, 'health', kits, 'health');
|
|
112
|
+
const implementation = text(row.implementation, 'implementation'), release = text(row.release, 'release');
|
|
113
|
+
if (kits[server.kit].package.name !== implementation || kits[server.kit].package.version !== release
|
|
114
|
+
|| health.sourceService !== server.sourceService)
|
|
115
|
+
return fail('server/health implementation');
|
|
116
|
+
if (!Array.isArray(row.prerequisites))
|
|
117
|
+
return fail('prerequisites');
|
|
118
|
+
const prerequisites = [];
|
|
119
|
+
for (const value of row.prerequisites)
|
|
120
|
+
prerequisites.push(operation(value, 'prerequisite', kits, 'prerequisites.' + prerequisites.length));
|
|
121
|
+
if (new Set(prerequisites.map(item => item.sourceService)).size !== prerequisites.length
|
|
122
|
+
|| prerequisites.some(item => item.sourceService === server.sourceService))
|
|
123
|
+
return fail('prerequisite service');
|
|
124
|
+
const usedKits = new Set([server.kit, health.kit, ...prerequisites.map(item => item.kit)]);
|
|
125
|
+
if (Object.keys(kits).some(name => !usedKits.has(name)))
|
|
126
|
+
return fail('unconsumed kit');
|
|
127
|
+
for (const item of prerequisites)
|
|
128
|
+
if (!server.requires.some(edge => edge.service === item.sourceService && edge.condition === 'service_completed_successfully'))
|
|
129
|
+
return fail('server prerequisite completion edge');
|
|
130
|
+
return { version: 2, implementation, release, directory: relativePath(row.directory, 'directory'),
|
|
131
|
+
snapshotSha256: digest(row.snapshotSha256, 'snapshotSha256'), declarationSha256: digest(row.declarationSha256, 'declarationSha256'),
|
|
132
|
+
image, kits, server, prerequisites, health };
|
|
133
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export interface OriginalInputReference {
|
|
2
|
+
version: 1;
|
|
3
|
+
/** Relative to the needs file, or to the World configuration after init copies it. */
|
|
4
|
+
root: string;
|
|
5
|
+
manifest: "manifest.json";
|
|
6
|
+
sha256: string;
|
|
7
|
+
inventoryDigest: string;
|
|
8
|
+
}
|
|
9
|
+
export interface OriginalInputCapability {
|
|
10
|
+
version: 1;
|
|
11
|
+
manifestArg: "--original-inputs";
|
|
12
|
+
rootArg: "--original-root";
|
|
13
|
+
digestArg: "--original-inputs-sha256";
|
|
14
|
+
/** Image paths whose complete overlay contents the consumer requires. */
|
|
15
|
+
paths: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface OriginalInputEntry {
|
|
18
|
+
target: string;
|
|
19
|
+
type: "file" | "directory" | "link";
|
|
20
|
+
mode: number;
|
|
21
|
+
bytes?: number;
|
|
22
|
+
sha256?: string;
|
|
23
|
+
object?: string;
|
|
24
|
+
link?: string;
|
|
25
|
+
source: {
|
|
26
|
+
kind: "image" | "project";
|
|
27
|
+
path: string;
|
|
28
|
+
layer?: string;
|
|
29
|
+
blob?: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export interface ManagedOriginalInputs {
|
|
33
|
+
kind: "volter.managed-original-inputs";
|
|
34
|
+
version: 1;
|
|
35
|
+
service: string;
|
|
36
|
+
family: string;
|
|
37
|
+
/** Canonical selected service declaration; used to refuse a different stack on reuse. */
|
|
38
|
+
declaration: string;
|
|
39
|
+
source: {
|
|
40
|
+
image: string;
|
|
41
|
+
manifest: string;
|
|
42
|
+
config: string;
|
|
43
|
+
layers: string[];
|
|
44
|
+
commit: string;
|
|
45
|
+
compose: string;
|
|
46
|
+
composeSha256: string;
|
|
47
|
+
composeBytes: number;
|
|
48
|
+
composeObject: string;
|
|
49
|
+
};
|
|
50
|
+
/** Authenticated complete image metadata, before the declared mounts overlay it. */
|
|
51
|
+
imageInventory: Array<{
|
|
52
|
+
target: string;
|
|
53
|
+
type: string;
|
|
54
|
+
mode: number;
|
|
55
|
+
bytes: number;
|
|
56
|
+
layer: string;
|
|
57
|
+
link?: string;
|
|
58
|
+
}>;
|
|
59
|
+
imageInventoryDigest: string;
|
|
60
|
+
projectInventory: Array<{
|
|
61
|
+
path: string;
|
|
62
|
+
mode: number;
|
|
63
|
+
blob: string;
|
|
64
|
+
}>;
|
|
65
|
+
projectInventoryDigest: string;
|
|
66
|
+
roots: string[];
|
|
67
|
+
/** Complete contents beneath roots, plus every original project bind mount. */
|
|
68
|
+
inventory: OriginalInputEntry[];
|
|
69
|
+
inventoryDigest: string;
|
|
70
|
+
start: {
|
|
71
|
+
imageConfig: Record<string, unknown>;
|
|
72
|
+
service: Record<string, unknown>;
|
|
73
|
+
imageEntrypoint: string[] | null;
|
|
74
|
+
imageCommand: string[] | null;
|
|
75
|
+
command?: string[];
|
|
76
|
+
entrypoint?: string[];
|
|
77
|
+
environment: Record<string, string>;
|
|
78
|
+
envFiles: Array<{
|
|
79
|
+
path: string;
|
|
80
|
+
bytes: number;
|
|
81
|
+
sha256: string;
|
|
82
|
+
object: string;
|
|
83
|
+
}>;
|
|
84
|
+
unset: string[];
|
|
85
|
+
selfHosts: string[];
|
|
86
|
+
mounts: Array<{
|
|
87
|
+
kind: string;
|
|
88
|
+
source?: string;
|
|
89
|
+
target: string;
|
|
90
|
+
readOnly: boolean;
|
|
91
|
+
}>;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export declare const originalTarget: (path: string) => boolean;
|
|
95
|
+
export declare const originalRelative: (path: string) => boolean;
|
|
96
|
+
export declare function referenceOf(value: unknown): OriginalInputReference;
|
|
97
|
+
/** Hash and validate neutral carrier data. The caller supplies I/O; no pack/vendor behavior or import effects. */
|
|
98
|
+
export declare function readSnapshot(bytes: Uint8Array, expected: string, readObject: (path: string, bytes: number) => Uint8Array): {
|
|
99
|
+
manifest: ManagedOriginalInputs;
|
|
100
|
+
objects: ReadonlyMap<string, Uint8Array>;
|
|
101
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/** Source-bound files at a managed replacement. No vendor interpretation or host paths. */
|
|
2
|
+
import * as nodeCrypto from 'node:crypto';
|
|
3
|
+
const digest = (algorithm, value) => nodeCrypto.createHash(algorithm).update(value).digest('hex');
|
|
4
|
+
export const originalTarget = (path) => path === "/" || (path.startsWith("/") && !path.slice(1).split("/").some(part => !part || part === "." || part === ".." || /[\0\r\n\\]/u.test(part)));
|
|
5
|
+
export const originalRelative = (path) => !!path && !path.split("/").some(part => !part || part === "." || part === ".." || /[\0\r\n\\:]/u.test(part));
|
|
6
|
+
const hex = (value) => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
7
|
+
const oci = (value) => typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value);
|
|
8
|
+
const bytesOf = (value) => Number.isSafeInteger(value) && Number(value) >= 0;
|
|
9
|
+
const strings = (value) => Array.isArray(value) && value.every(entry => typeof entry === 'string' && !/[\0\r\n]/.test(entry));
|
|
10
|
+
const bound = 128 * 1024 * 1024;
|
|
11
|
+
export function referenceOf(value) {
|
|
12
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
13
|
+
throw new Error('Original inputs need a versioned reference');
|
|
14
|
+
const ref = value;
|
|
15
|
+
if (ref.version !== 1 || ref.manifest !== 'manifest.json' || typeof ref.root !== 'string' || !originalRelative(ref.root) || !hex(ref.sha256) || !hex(ref.inventoryDigest)
|
|
16
|
+
|| Object.keys(ref).some(key => !['version', 'root', 'manifest', 'sha256', 'inventoryDigest'].includes(key)))
|
|
17
|
+
throw new Error('Invalid original-input reference');
|
|
18
|
+
return { ...ref };
|
|
19
|
+
}
|
|
20
|
+
/** Hash and validate neutral carrier data. The caller supplies I/O; no pack/vendor behavior or import effects. */
|
|
21
|
+
export function readSnapshot(bytes, expected, readObject) {
|
|
22
|
+
if (!(bytes instanceof Uint8Array) || bytes.length > 64 * 1024 * 1024 || !hex(expected) || digest('sha256', bytes) !== expected)
|
|
23
|
+
throw new Error('Original-input manifest identity differs');
|
|
24
|
+
const manifest = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
25
|
+
const source = manifest.source, start = manifest.start;
|
|
26
|
+
if (manifest.kind !== 'volter.managed-original-inputs' || manifest.version !== 1 || typeof manifest.service !== 'string' || !manifest.service || typeof manifest.family !== 'string' || !/^[a-z][a-z0-9-]*$/.test(manifest.family)
|
|
27
|
+
|| typeof manifest.declaration !== 'string' || !source || !/^[a-f0-9]{40}$/.test(source.commit) || !/^[A-Za-z0-9./:_-]+@sha256:[a-f0-9]{64}$/.test(source.image)
|
|
28
|
+
|| !oci(source.manifest) || !oci(source.config) || !Array.isArray(source.layers) || !source.layers.every(oci) || !originalRelative(source.compose)
|
|
29
|
+
|| !Array.isArray(manifest.imageInventory) || !Array.isArray(manifest.projectInventory) || !Array.isArray(manifest.inventory) || !strings(manifest.roots) || !manifest.roots.length || !manifest.roots.every(originalTarget) || new Set(manifest.roots).size !== manifest.roots.length
|
|
30
|
+
|| !hex(manifest.imageInventoryDigest) || digest('sha256', JSON.stringify(manifest.imageInventory)) !== manifest.imageInventoryDigest
|
|
31
|
+
|| !hex(manifest.inventoryDigest) || digest('sha256', JSON.stringify(manifest.inventory)) !== manifest.inventoryDigest
|
|
32
|
+
|| !hex(manifest.projectInventoryDigest) || digest('sha256', JSON.stringify(manifest.projectInventory)) !== manifest.projectInventoryDigest
|
|
33
|
+
|| !start || !start.imageConfig || typeof start.imageConfig !== 'object' || Array.isArray(start.imageConfig) || !start.service || typeof start.service !== 'object' || Array.isArray(start.service)
|
|
34
|
+
|| !strings(start.selfHosts) || !start.selfHosts.length || !strings(start.unset) || !Array.isArray(start.envFiles) || !Array.isArray(start.mounts)
|
|
35
|
+
|| !start.environment || typeof start.environment !== 'object' || Array.isArray(start.environment) || Object.entries(start.environment).some(([key, value]) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || typeof value !== 'string')
|
|
36
|
+
|| [start.imageEntrypoint, start.imageCommand].some(value => value !== null && !strings(value)) || [start.command, start.entrypoint].some(value => value !== undefined && !strings(value)))
|
|
37
|
+
throw new Error('Invalid original-input manifest');
|
|
38
|
+
if (JSON.stringify(JSON.parse(manifest.declaration)) !== JSON.stringify(start.service) || start.service.name !== manifest.service
|
|
39
|
+
|| JSON.stringify(start.command) !== JSON.stringify(start.service.command) || JSON.stringify(start.entrypoint) !== JSON.stringify(start.service.entrypoint)
|
|
40
|
+
|| JSON.stringify(start.imageEntrypoint) !== JSON.stringify(start.imageConfig.Entrypoint ?? null) || JSON.stringify(start.imageCommand) !== JSON.stringify(start.imageConfig.Cmd ?? null)
|
|
41
|
+
|| !Array.isArray(start.service.volumes)
|
|
42
|
+
|| JSON.stringify(start.mounts.map(mount => [mount.kind, mount.source ?? null, mount.target, mount.readOnly])) !== JSON.stringify(start.service.volumes.map(mount => [mount.kind, mount.source ?? null, mount.target, mount.readOnly])))
|
|
43
|
+
throw new Error('Original-input start declaration differs');
|
|
44
|
+
const project = new Map();
|
|
45
|
+
for (const entry of manifest.projectInventory) {
|
|
46
|
+
if (!entry || !originalRelative(entry.path) || project.has(entry.path) || ![0o100644, 0o100755, 0o120000, 0o160000].includes(entry.mode) || !/^[a-f0-9]{40}$/.test(entry.blob))
|
|
47
|
+
throw new Error('Invalid original project inventory');
|
|
48
|
+
project.set(entry.path, entry);
|
|
49
|
+
}
|
|
50
|
+
const image = new Map();
|
|
51
|
+
for (const entry of manifest.imageInventory) {
|
|
52
|
+
if (!entry || !originalTarget(entry.target) || image.has(entry.target) || !['file', 'directory', 'link', 'other'].includes(entry.type) || !Number.isInteger(entry.mode) || entry.mode < 0 || entry.mode > 0o7777 || !bytesOf(entry.bytes) || !source.layers.includes(entry.layer)
|
|
53
|
+
|| (entry.type === 'link' && (typeof entry.link !== 'string' || /[\0\r\n\\]/.test(entry.link))))
|
|
54
|
+
throw new Error('Invalid original image inventory');
|
|
55
|
+
image.set(entry.target, entry);
|
|
56
|
+
}
|
|
57
|
+
const targets = new Set(), selected = new Map(), expectedObjects = new Map();
|
|
58
|
+
const hold = (entry) => {
|
|
59
|
+
if (!hex(entry.sha256) || !bytesOf(entry.bytes) || entry.object !== `objects/${entry.sha256}` || (expectedObjects.has(entry.sha256) && expectedObjects.get(entry.sha256) !== entry.bytes))
|
|
60
|
+
throw new Error('Invalid original-input payload identity');
|
|
61
|
+
expectedObjects.set(entry.sha256, entry.bytes);
|
|
62
|
+
};
|
|
63
|
+
for (const entry of manifest.inventory) {
|
|
64
|
+
if (!entry || !originalTarget(entry.target) || targets.has(entry.target) || !['file', 'directory', 'link'].includes(entry.type) || !Number.isInteger(entry.mode) || entry.mode < 0 || entry.mode > 0o7777 || !entry.source || !['image', 'project'].includes(entry.source.kind))
|
|
65
|
+
throw new Error('Invalid original-input inventory');
|
|
66
|
+
targets.add(entry.target);
|
|
67
|
+
selected.set(entry.target, entry);
|
|
68
|
+
if (entry.source.kind === 'image') {
|
|
69
|
+
const original = image.get(entry.source.path);
|
|
70
|
+
if (!original || original.layer !== entry.source.layer || original.type !== entry.type || original.mode !== entry.mode || (entry.type === 'file' && original.bytes !== entry.bytes) || (entry.type === 'link' && original.link !== entry.link))
|
|
71
|
+
throw new Error('Original-input image provenance differs');
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const original = project.get(entry.source.path);
|
|
75
|
+
if (!originalRelative(entry.source.path) || (entry.type === 'file' && (!original || original.blob !== entry.source.blob || (original.mode & 0o777) !== entry.mode)))
|
|
76
|
+
throw new Error('Original-input project provenance differs');
|
|
77
|
+
}
|
|
78
|
+
if (entry.type === 'file')
|
|
79
|
+
hold(entry);
|
|
80
|
+
else if (entry.object !== undefined || entry.bytes !== undefined || entry.sha256 !== undefined || (entry.type === 'link' && (typeof entry.link !== 'string' || /[\0\r\n\\]/.test(entry.link))))
|
|
81
|
+
throw new Error('Invalid original-input non-file');
|
|
82
|
+
}
|
|
83
|
+
for (const mount of start.mounts)
|
|
84
|
+
if (!mount || !['bind', 'named', 'anonymous'].includes(mount.kind) || !originalTarget(mount.target) || typeof mount.readOnly !== 'boolean' || (mount.source !== undefined && typeof mount.source !== 'string'))
|
|
85
|
+
throw new Error('Invalid original-input mount declaration');
|
|
86
|
+
const roots = new Set(manifest.roots), mounted = new Set(start.mounts.filter(mount => mount.kind === 'bind').map(mount => mount.target));
|
|
87
|
+
const under = (path, roots) => {
|
|
88
|
+
for (let at = path;; at = at.slice(0, at.lastIndexOf('/')) || '/') {
|
|
89
|
+
if (roots.has(at))
|
|
90
|
+
return true;
|
|
91
|
+
if (at === '/')
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
for (const entry of image.values())
|
|
96
|
+
if (under(entry.target, roots) && !under(entry.target, mounted)) {
|
|
97
|
+
const captured = selected.get(entry.target);
|
|
98
|
+
if (!captured || captured.source.kind !== 'image' || captured.source.path !== entry.target)
|
|
99
|
+
throw new Error('Original image input was omitted from its declared root');
|
|
100
|
+
}
|
|
101
|
+
hold({ sha256: source.composeSha256, bytes: source.composeBytes, object: source.composeObject });
|
|
102
|
+
for (const file of start.envFiles) {
|
|
103
|
+
if (!file || !originalRelative(file.path))
|
|
104
|
+
throw new Error('Invalid original-input env_file');
|
|
105
|
+
hold(file);
|
|
106
|
+
}
|
|
107
|
+
if ([...expectedObjects.values()].reduce((sum, bytes) => sum + bytes, 0) > bound)
|
|
108
|
+
throw new Error('Original-input snapshot exceeds its byte bound');
|
|
109
|
+
const objects = new Map();
|
|
110
|
+
for (const [sha256, size] of expectedObjects) {
|
|
111
|
+
const data = readObject(`objects/${sha256}`, size);
|
|
112
|
+
if (!(data instanceof Uint8Array) || data.length !== size || digest('sha256', data) !== sha256)
|
|
113
|
+
throw new Error('Original-input payload bytes differ');
|
|
114
|
+
objects.set(sha256, data.slice());
|
|
115
|
+
}
|
|
116
|
+
for (const entry of manifest.inventory)
|
|
117
|
+
if (entry.type === 'file' && entry.source.kind === 'project') {
|
|
118
|
+
const data = objects.get(entry.sha256), header = new TextEncoder().encode(`blob ${data.length}\0`), blob = new Uint8Array(header.length + data.length);
|
|
119
|
+
blob.set(header);
|
|
120
|
+
blob.set(data, header.length);
|
|
121
|
+
if (digest('sha1', blob) !== entry.source.blob)
|
|
122
|
+
throw new Error('Original-input project blob identity differs');
|
|
123
|
+
}
|
|
124
|
+
return { manifest, objects };
|
|
125
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type MongoCodec, type MongoDocument } from './stream.js';
|
|
2
|
+
/** Sends bytes and returns every byte the server answered to them. */
|
|
3
|
+
export type MongoExchange = (bytes: Uint8Array) => Promise<Uint8Array>;
|
|
4
|
+
/** A client on one connection: `command` sends a document to `database` and resolves with the reply document,
|
|
5
|
+
* whatever its `ok`. A reply that is not one whole OP_MSG answering this request is an error. */
|
|
6
|
+
export declare function mongoClient(exchange: MongoExchange, codec: MongoCodec): {
|
|
7
|
+
command(database: string, command: MongoDocument): Promise<MongoDocument>;
|
|
8
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// A MongoDB client over any byte exchange: the other side of ./stream.ts, so a journey speaks the wire as an
|
|
2
|
+
// application's driver does. It sends one command document as an OP_MSG and returns the document the server
|
|
3
|
+
// answered; it reads what the protocol answers and decides nothing.
|
|
4
|
+
// source: archive:https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz#sha256=a12bd599cb4f39cdf222b32380ea3efd1f60c6d88615e0700632a303a9006b02!/package/src/cmap/commands.ts "moreToCome is an OP_MSG only concept"
|
|
5
|
+
import { MongoOp, decodeMsg, encodeMsg } from "./stream.js";
|
|
6
|
+
/** A client on one connection: `command` sends a document to `database` and resolves with the reply document,
|
|
7
|
+
* whatever its `ok`. A reply that is not one whole OP_MSG answering this request is an error. */
|
|
8
|
+
export function mongoClient(exchange, codec) {
|
|
9
|
+
let nextRequest = 0;
|
|
10
|
+
return {
|
|
11
|
+
async command(database, command) {
|
|
12
|
+
const requestId = ++nextRequest;
|
|
13
|
+
const answered = await exchange(encodeMsg(codec, { ...command, $db: database }, requestId, 0));
|
|
14
|
+
if (answered.length < 16)
|
|
15
|
+
throw new Error('the server answered no message');
|
|
16
|
+
const view = new DataView(answered.buffer, answered.byteOffset, answered.byteLength);
|
|
17
|
+
const length = view.getInt32(0, true), responseTo = view.getInt32(8, true), opcode = view.getInt32(12, true);
|
|
18
|
+
if (length !== answered.length)
|
|
19
|
+
throw new Error(`the server answered ${answered.length} bytes, and its message says ${length}`);
|
|
20
|
+
if (opcode !== MongoOp.MSG)
|
|
21
|
+
throw new Error(`the server answered opcode ${opcode}, not OP_MSG`);
|
|
22
|
+
if (responseTo !== requestId)
|
|
23
|
+
throw new Error(`the server answered request ${responseTo}, not ${requestId}`);
|
|
24
|
+
return decodeMsg(codec, answered).document;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// THE KERNEL'S MONGODB WIRE (`@volter/world-core/mongodb`): MongoDB's message framing as a TwinStream, and its client.
|
|
2
|
+
// A pack that serves MongoDB passes what a command means (its engine) and the document codec; the kernel names no vendor.
|
|
3
|
+
// docs/contributing/architecture.md, "Other wires", the MongoDB wire.
|
|
4
|
+
export * from "./stream.js";
|
|
5
|
+
export * from "./client.js";
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { TwinStream } from '../twin-fetch.js';
|
|
2
|
+
/** A command or reply document as the pack's codec gives it: the pack decides how BSON's types appear in it. */
|
|
3
|
+
export type MongoDocument = Record<string, unknown>;
|
|
4
|
+
/** BSON, as the pack supplies it (the `bson` library, installed by the pack's package.json). The kernel frames
|
|
5
|
+
* documents and never reads inside one but for a command's `$db`. */
|
|
6
|
+
export type MongoCodec = {
|
|
7
|
+
serialize(document: MongoDocument): Uint8Array;
|
|
8
|
+
deserialize(bytes: Uint8Array): MongoDocument;
|
|
9
|
+
};
|
|
10
|
+
/** What a pack's stream reads of the `bson` library its generated fetch's `loadBson` resolves from the pack: the codec,
|
|
11
|
+
* extended JSON, and the number kinds a reply names. The kernel declares the shape and imports nothing of it. */
|
|
12
|
+
export type BsonLibrary = {
|
|
13
|
+
serialize(document: MongoDocument): Uint8Array;
|
|
14
|
+
deserialize(bytes: Uint8Array, options?: {
|
|
15
|
+
promoteValues?: boolean;
|
|
16
|
+
promoteLongs?: boolean;
|
|
17
|
+
bsonRegExp?: boolean;
|
|
18
|
+
}): MongoDocument;
|
|
19
|
+
EJSON: {
|
|
20
|
+
serialize(value: unknown, options?: {
|
|
21
|
+
relaxed?: boolean;
|
|
22
|
+
}): MongoDocument;
|
|
23
|
+
deserialize(value: unknown, options?: {
|
|
24
|
+
relaxed?: boolean;
|
|
25
|
+
}): MongoDocument;
|
|
26
|
+
};
|
|
27
|
+
Double: new (value: number) => unknown;
|
|
28
|
+
Int32: new (value: number) => unknown;
|
|
29
|
+
Long: {
|
|
30
|
+
fromNumber(value: number): unknown;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
/** What the sender's message said beside its command: whether it will take more than one reply to it (OP_MSG's exhaustAllowed). */
|
|
34
|
+
export type MongoAsked = {
|
|
35
|
+
exhaustAllowed: boolean;
|
|
36
|
+
};
|
|
37
|
+
declare const TIMED: unique symbol;
|
|
38
|
+
/**
|
|
39
|
+
* An answer the wire sends LATER, or goes on sending: the vendor's protocol does this in real time (an awaitable
|
|
40
|
+
* `hello` is held for the sender's maxAwaitTimeMS, and one sent with exhaustAllowed is answered again every such
|
|
41
|
+
* interval, each reply marked moreToCome). The pack says WHAT and WHEN as data and holds no clock: the same request
|
|
42
|
+
* gets the same answer every time. The wire does the holding and the repeating on the wall clock, and the delay is
|
|
43
|
+
* never read back into state (architecture, "The MongoDB wire").
|
|
44
|
+
*/
|
|
45
|
+
export type MongoTimedReply = {
|
|
46
|
+
readonly [TIMED]: true;
|
|
47
|
+
reply: MongoDocument;
|
|
48
|
+
afterMs: number;
|
|
49
|
+
everyMs?: number;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* `reply` after `afterMs` (0: at once); with `everyMs`, again each `everyMs` until the peer closes, where the sender
|
|
53
|
+
* allowed more than one reply. The two differ in the vendor's own answers: a hello naming the server's current
|
|
54
|
+
* topology is first answered after the wait, one naming another process at once, and both then repeat at the wait.
|
|
55
|
+
*/
|
|
56
|
+
export declare const timedReply: (reply: MongoDocument, afterMs: number, everyMs?: number) => MongoTimedReply;
|
|
57
|
+
export type MongoStreamOptions<Session> = {
|
|
58
|
+
codec: MongoCodec;
|
|
59
|
+
/** A connection opened: what the pack keeps for it (its sessions, an open transaction); never the tree. */
|
|
60
|
+
open(peer: string): Session;
|
|
61
|
+
/** Answer ONE command document sent to `database`. A document sequence (an insert's `documents`, an update's
|
|
62
|
+
* `updates`) arrives as that field holding the array. The answer is the reply document, `ok: 1` or the vendor's
|
|
63
|
+
* error document; a command the pack does not serve is the pack's to answer as the vendor answers an unknown one. */
|
|
64
|
+
command(session: Session, database: string, command: MongoDocument, asked: MongoAsked): Promise<MongoDocument | MongoTimedReply>;
|
|
65
|
+
/** The connection went away: abort what it left open. */
|
|
66
|
+
close?(session: Session): void;
|
|
67
|
+
/** Told why a connection is ended by this stream and not by its peer: the pack threw answering a command, or a
|
|
68
|
+
* message was one this wire does not serve. Ending the connection is all the wire can say to the client, so the
|
|
69
|
+
* reason is said here; unset, it is written to the process's error output. A stream that ends connections in
|
|
70
|
+
* silence leaves its operator with a client that hangs and nothing to read. */
|
|
71
|
+
ended?(reason: string, peer: string): void;
|
|
72
|
+
/** The largest message accepted; the vendor's server names 48,000,000 in its hello (`maxMessageSizeBytes`). */
|
|
73
|
+
maxMessageSizeBytes?: number;
|
|
74
|
+
};
|
|
75
|
+
export declare const MongoOp: {
|
|
76
|
+
readonly REPLY: 1;
|
|
77
|
+
readonly QUERY: 2004;
|
|
78
|
+
readonly MSG: 2013;
|
|
79
|
+
};
|
|
80
|
+
/** OP_MSG's flag bits. */
|
|
81
|
+
export declare const MongoMsgFlag: {
|
|
82
|
+
readonly CHECKSUM_PRESENT: number;
|
|
83
|
+
readonly MORE_TO_COME: number;
|
|
84
|
+
readonly EXHAUST_ALLOWED: number;
|
|
85
|
+
};
|
|
86
|
+
/** An OP_MSG message carrying one document as its body section. */
|
|
87
|
+
export declare function encodeMsg(codec: MongoCodec, document: MongoDocument, requestId: number, responseTo: number, flags?: number): Uint8Array;
|
|
88
|
+
/** The command of an OP_MSG message: its body section, with each document sequence as the field it names. */
|
|
89
|
+
export declare function decodeMsg(codec: MongoCodec, message: Uint8Array): {
|
|
90
|
+
flags: number;
|
|
91
|
+
document: MongoDocument;
|
|
92
|
+
};
|
|
93
|
+
/** MongoDB's wire, served: each command document answered by the pack, in arrival order, on one connection. */
|
|
94
|
+
export declare function createMongoStream<Session>(options: MongoStreamOptions<Session>): TwinStream;
|
|
95
|
+
/** The bytes of a C string, for a client's OP_QUERY. */
|
|
96
|
+
export declare const cstring: (text: string) => Uint8Array;
|
|
97
|
+
export {};
|