@volter/twin-standard 1.0.153 → 1.0.155
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/client-sdks/package.json +8 -0
- package/client-sdks/pnpm-lock.yaml +2420 -0
- package/client-sdks/pnpm-workspace.yaml +9 -0
- package/dist/client-sdks/package.json +8 -0
- package/dist/client-sdks/pnpm-lock.yaml +2420 -0
- package/dist/client-sdks/pnpm-workspace.yaml +9 -0
- package/dist/src/clients/sentry.js +2 -2
- package/dist/src/conformance.js +1 -1
- package/dist/src/consumer.js +8 -5
- package/dist/src/create-pack.js +1 -4
- package/dist/src/dependency-lock.d.ts +16 -0
- package/dist/src/dependency-lock.js +30 -0
- package/dist/src/pack-facts.d.ts +7 -6
- package/dist/src/pack-facts.js +8 -1
- package/dist/src/published.js +2 -2
- package/dist/src/walk/journey-kit.d.ts +2 -2
- package/dist/src/walk/serve-pack.d.ts +4 -1
- package/dist/src/walk/serve-pack.js +4 -5
- package/dist/src/walk/vendor-fetch.js +10 -11
- package/dist/src/walk/walk-database.d.ts +3 -4
- package/dist/src/walk/walk-database.js +35 -67
- package/package.json +12 -3
- package/src/clients/sentry.ts +2 -2
- package/src/conformance.ts +1 -1
- package/src/consumer.ts +8 -5
- package/src/create-pack.ts +1 -4
- package/src/dependency-lock.ts +27 -0
- package/src/pack-facts.ts +9 -6
- package/src/published.ts +2 -2
- package/src/walk/journey-kit.ts +2 -2
- package/src/walk/serve-pack.ts +7 -7
- package/src/walk/vendor-fetch.ts +12 -12
- package/src/walk/walk-database.ts +37 -70
- package/client-sdks/bun.lock +0 -459
- package/dist/client-sdks/bun.lock +0 -459
|
@@ -19,9 +19,9 @@ import { standardPackageFile } from "../package-file.js";
|
|
|
19
19
|
const SDKS = standardPackageFile('client-sdks/');
|
|
20
20
|
async function sdkOf(name) {
|
|
21
21
|
if (!existsSync(new URL('node_modules/', SDKS))) {
|
|
22
|
-
const install = spawnSync('
|
|
22
|
+
const install = spawnSync('pnpm', ['install', '--frozen-lockfile', '--ignore-scripts'], { cwd: SDKS, stdio: 'inherit' });
|
|
23
23
|
if (install.status !== 0)
|
|
24
|
-
throw new Error('client-sdks:
|
|
24
|
+
throw new Error('client-sdks: pnpm install --frozen-lockfile failed');
|
|
25
25
|
}
|
|
26
26
|
return await import(__rewriteRelativeImportExtension(pathToFileURL(createRequire(new URL('package.json', SDKS)).resolve(name)).href));
|
|
27
27
|
}
|
package/dist/src/conformance.js
CHANGED
|
@@ -219,7 +219,7 @@ async function registered(pack, runtime) {
|
|
|
219
219
|
* journeys, read from the file list `npm pack` makes. Escaped once: seven packs published their lanes' src without
|
|
220
220
|
* their spec, so the catalog's conformance of the tarball never read those lanes. */
|
|
221
221
|
function published(pack) {
|
|
222
|
-
const run = spawnSync('npm', ['pack', '--dry-run', '--json', '--ignore-scripts'], { cwd: pack, encoding: 'utf8' });
|
|
222
|
+
const run = spawnSync('npm', ['pack', '--force', '--dry-run', '--json', '--ignore-scripts'], { cwd: pack, encoding: 'utf8' });
|
|
223
223
|
let files;
|
|
224
224
|
try {
|
|
225
225
|
files = new Set((JSON.parse(run.stdout)[0]?.files ?? []).map((f) => f.path));
|
package/dist/src/consumer.js
CHANGED
|
@@ -6,6 +6,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:pat
|
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { standardPackageFile } from "./package-file.js";
|
|
9
|
+
import { dependencyLock } from "./dependency-lock.js";
|
|
9
10
|
function packageIdentity(entry, name) {
|
|
10
11
|
let directory = dirname(realpathSync(entry));
|
|
11
12
|
while (true) {
|
|
@@ -81,6 +82,8 @@ export function prepareConsumerApp(packDir, appDir, installation) {
|
|
|
81
82
|
if (existsSync(appDir))
|
|
82
83
|
throw new Error('Consumer app must be a new directory');
|
|
83
84
|
const app = JSON.parse(recipe.files['package.json']);
|
|
85
|
+
app.packageManager = 'pnpm@12.15.0';
|
|
86
|
+
app.devEngines = { packageManager: { name: 'pnpm', version: '12.15.0', onFail: 'error' } };
|
|
84
87
|
if (app.scripts || app.workspaces)
|
|
85
88
|
throw new Error('Consumer fixture supplies direct SDK commands, not install scripts or workspace links');
|
|
86
89
|
for (const [name, version] of Object.entries({ ...app.dependencies, ...app.devDependencies, ...app.optionalDependencies })) {
|
|
@@ -102,8 +105,9 @@ export function prepareConsumerApp(packDir, appDir, installation) {
|
|
|
102
105
|
if (Object.hasOwn(app.dependencies ?? {}, metadata.name))
|
|
103
106
|
throw new Error('Consumer SDK dependencies must not substitute the candidate package');
|
|
104
107
|
app.devDependencies = { ...app.devDependencies, '@volter/world-core': tools['@volter/world-core'], [metadata.name]: installation.artifact ? `file:${installation.artifact}` : metadata.version };
|
|
105
|
-
files.find(([path]) => path === 'package.json')[1] = JSON.stringify(app, null, 2) + '\n';
|
|
106
108
|
}
|
|
109
|
+
files.find(([path]) => path === 'package.json')[1] = JSON.stringify(app, null, 2) + '\n';
|
|
110
|
+
files.push(['pnpm-workspace.yaml', 'packages:\n - .\npackageImportMethod: hardlink\nnodeLinker: hoisted\nautoInstallPeers: false\nstrictPeerDependencies: false\n']);
|
|
107
111
|
mkdirSync(appDir, { recursive: true });
|
|
108
112
|
for (const [path, content] of files) {
|
|
109
113
|
mkdirSync(dirname(join(appDir, path)), { recursive: true });
|
|
@@ -127,11 +131,10 @@ export async function runConsumerWorkflow(packDir, options) {
|
|
|
127
131
|
if (location.startsWith('..') || isAbsolute(location))
|
|
128
132
|
throw new Error(`${name}: consumer resolves an evaluator or ancestor dependency; install it in the app`);
|
|
129
133
|
}
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
if (candidate.version !== metadata.version || appLock.packages?.[`node_modules/${metadata.name}`]?.integrity !== options.artifactIntegrity)
|
|
134
|
+
const appLock = dependencyLock(appRoot), appLockBytes = appLock.bytes;
|
|
135
|
+
if (candidate.version !== metadata.version || appLock.entry(metadata.name)?.integrity !== options.artifactIntegrity)
|
|
133
136
|
throw new Error('Consumer app candidate does not match the exact assessed artifact');
|
|
134
|
-
if (kernel.version !== tools['@volter/world-core'] || appLock.
|
|
137
|
+
if (kernel.version !== tools['@volter/world-core'] || appLock.entry('@volter/world-core')?.version !== kernel.version)
|
|
135
138
|
throw new Error('Consumer app kernel does not match the CLI kernel pin');
|
|
136
139
|
const application = { candidate: { package: metadata.name, version: candidate.version, integrity: options.artifactIntegrity }, kernel: { package: '@volter/world-core', version: kernel.version }, dependencyLockSha256: createHash('sha256').update(appLockBytes).digest('hex'), appLocal: true };
|
|
137
140
|
const facts = JSON.parse(readFileSync(join(packDir, 'generated/pack-facts.json'), 'utf8'));
|
package/dist/src/create-pack.js
CHANGED
|
@@ -371,7 +371,7 @@ export function cliFile(vendor, live = false) {
|
|
|
371
371
|
const Name = pascal(vendor);
|
|
372
372
|
const ingest = live ? `// the RTMP ingest's side port, which the World allocates (the manifest's sidePorts)\nconst ingestPort = Number(optionValue(rest, '--ingest-port', '0')) || undefined;\n` : '';
|
|
373
373
|
const served = live ? ` process.stdout.write(s.ingestPort ? \`${vendor} twin ingest at rtmp://127.0.0.1:\${s.ingestPort}\\n\` : '${vendor} twin: no RTMP ingest\\n');\n` : '';
|
|
374
|
-
return `#!/usr/bin/env node\n// world-${vendor}: serve the ${vendor} twin.\nimport { hasFlag, optionValue } from '@volter/world-core/args';\nimport { keepProcessAlive } from '@volter/world-core/lifecycle';\nimport { create${Name}TwinServer } from './server.ts';\n\nconst [cmd, ...rest] = process.argv.slice(2);\nconst port = Number(optionValue(rest, '--port', '0')) || undefined;\nconst root = optionValue(rest, '--root') || undefined;\nconst readOnly = hasFlag(rest, '--read-only');\n// the
|
|
374
|
+
return `#!/usr/bin/env node\n// world-${vendor}: serve the ${vendor} twin.\nimport { hasFlag, optionValue } from '@volter/world-core/args';\nimport { keepProcessAlive } from '@volter/world-core/lifecycle';\nimport { create${Name}TwinServer } from './server.ts';\n\nconst [cmd, ...rest] = process.argv.slice(2);\nconst port = Number(optionValue(rest, '--port', '0')) || undefined;\nconst root = optionValue(rest, '--root') || undefined;\nconst readOnly = hasFlag(rest, '--read-only');\n// the selected twin's private Postgres, which the runtime binds a pack declaring managedDatabase to\nconst database = optionValue(rest, '--database') || undefined;\n// the World's scenario file, for a model vendor's turns\nconst scenarioPath = optionValue(rest, '--scenario') || undefined;\n${ingest}\nif (cmd === 'serve' || cmd === undefined) {\n const s = await create${Name}TwinServer({ readOnly, ...(root ? { root } : {}), ...(port ? { port } : {}), ...(database ? { database } : {}), ...(scenarioPath ? { scenarioPath } : {})${live ? ', ...(ingestPort ? { ingestPort } : {})' : ''} });\n process.stdout.write(\`${vendor} twin\${readOnly ? ' [read-only]' : ''} at http://127.0.0.1:\${s.port}\\n\`);\n${served} await keepProcessAlive();\n} else {\n process.stdout.write('Usage: world-${vendor} serve [--port N] [--root DIR] [--read-only] [--database URL] [--scenario FILE]${live ? ' [--ingest-port N]' : ''}\\n');\n}\n`;
|
|
375
375
|
}
|
|
376
376
|
/** A vendor id as a name in code: `npm-registry` is `NpmRegistry`. */
|
|
377
377
|
const pascal = (vendor) => vendor.split(/[^A-Za-z0-9]+/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join('');
|
|
@@ -421,7 +421,6 @@ Contributor mechanics belong to the [pack procedure](https://world-docs.volter.a
|
|
|
421
421
|
}
|
|
422
422
|
export function initFiles(vendor) {
|
|
423
423
|
const Name = pascal(vendor);
|
|
424
|
-
const semantics = `${Name[0].toLowerCase()}${Name.slice(1)}Semantics`;
|
|
425
424
|
return {
|
|
426
425
|
'package.json': `${JSON.stringify({
|
|
427
426
|
name: `@volter/twin-${vendor}`, version: '1.0.0', description: `The ${vendor} twin (Protocol 3).`, author: 'Volter (https://github.com/volter-ai)', license: 'Apache-2.0',
|
|
@@ -454,8 +453,6 @@ export function initFiles(vendor) {
|
|
|
454
453
|
* surface of its own under `<lane>/src`, keeping the vendor's one store and sharing its `src/semantics/shared.ts`, and the
|
|
455
454
|
* pack's server routing each lane's host to it. */
|
|
456
455
|
export function laneInitFiles(vendor, lanes) {
|
|
457
|
-
const Name = pascal(vendor);
|
|
458
|
-
const camel = (s) => { const p = pascal(s); return `${p[0].toLowerCase()}${p.slice(1)}`; };
|
|
459
456
|
const single = initFiles(vendor);
|
|
460
457
|
const files = {
|
|
461
458
|
'package.json': single['package.json'].replace('"spec",', lanes.map((l) => `"${l}",`).join('\n ')),
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** The application's selected direct dependency, including the exact installer's integrity. */
|
|
2
|
+
export declare function dependencyLock(root: string): {
|
|
3
|
+
bytes: NonSharedBuffer;
|
|
4
|
+
file: string;
|
|
5
|
+
entry(name: string): {
|
|
6
|
+
version: any;
|
|
7
|
+
integrity: any;
|
|
8
|
+
} | undefined;
|
|
9
|
+
} | {
|
|
10
|
+
bytes: NonSharedBuffer;
|
|
11
|
+
file: string;
|
|
12
|
+
entry(name: string): {
|
|
13
|
+
version?: string;
|
|
14
|
+
integrity?: string;
|
|
15
|
+
} | undefined;
|
|
16
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { parseAllDocuments } from 'yaml';
|
|
4
|
+
/** The application's selected direct dependency, including the exact installer's integrity. */
|
|
5
|
+
export function dependencyLock(root) {
|
|
6
|
+
const pnpm = join(root, 'pnpm-lock.yaml');
|
|
7
|
+
if (existsSync(pnpm)) {
|
|
8
|
+
const bytes = readFileSync(pnpm);
|
|
9
|
+
const documents = parseAllDocuments(bytes.toString());
|
|
10
|
+
for (const doc of documents)
|
|
11
|
+
if (doc.errors.length)
|
|
12
|
+
throw doc.errors[0];
|
|
13
|
+
const lock = documents.map(doc => doc.toJS()).find(doc => doc?.importers?.['.'] && !doc.importers['.'].packageManagerDependencies);
|
|
14
|
+
if (!lock)
|
|
15
|
+
throw new Error('No pnpm application importer');
|
|
16
|
+
return { bytes, file: pnpm, entry(name) {
|
|
17
|
+
const importer = lock.importers['.'];
|
|
18
|
+
const selected = importer.dependencies?.[name] ?? importer.devDependencies?.[name] ?? importer.optionalDependencies?.[name];
|
|
19
|
+
if (!selected)
|
|
20
|
+
return undefined;
|
|
21
|
+
const selectedRef = String(selected.version);
|
|
22
|
+
const ref = lock.packages?.[`${name}@${selectedRef}`] ? selectedRef : selectedRef.replace(/\(.*$/, '');
|
|
23
|
+
const pkg = lock.packages?.[`${name}@${ref}`];
|
|
24
|
+
return { version: pkg?.version ?? ref, integrity: pkg?.resolution?.integrity };
|
|
25
|
+
} };
|
|
26
|
+
}
|
|
27
|
+
const file = join(root, 'package-lock.json'), bytes = readFileSync(file);
|
|
28
|
+
const lock = JSON.parse(bytes.toString());
|
|
29
|
+
return { bytes, file, entry(name) { return lock.packages?.[`node_modules/${name}`]; } };
|
|
30
|
+
}
|
package/dist/src/pack-facts.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TwinPack } from '@volter/world-core/runtime';
|
|
1
2
|
export type PackFacts = {
|
|
2
3
|
/** Explicit vendor-server backing; this entry declares no pack protocol or factory. */
|
|
3
4
|
serverBacking?: {
|
|
@@ -62,11 +63,7 @@ export type PackFacts = {
|
|
|
62
63
|
};
|
|
63
64
|
endpointEnvNone?: string;
|
|
64
65
|
/** The door that issues the application a credential the twin accepts (see TwinPack.credentialDoor). */
|
|
65
|
-
credentialDoor?:
|
|
66
|
-
path: string;
|
|
67
|
-
body: Record<string, unknown>;
|
|
68
|
-
fill: Record<string, string>;
|
|
69
|
-
};
|
|
66
|
+
credentialDoor?: TwinPack['credentialDoor'];
|
|
70
67
|
credentialIssuer?: string;
|
|
71
68
|
/** The Prisma driver adapter this database twin is reached through (see TwinPack.prismaAdapter). */
|
|
72
69
|
prismaAdapter?: {
|
|
@@ -88,10 +85,14 @@ export type PackFacts = {
|
|
|
88
85
|
paths: string[];
|
|
89
86
|
};
|
|
90
87
|
};
|
|
91
|
-
/** The
|
|
88
|
+
/** The selected twin's private SQL identity and credential field (see TwinPack.managedDatabase). */
|
|
92
89
|
managedDatabase?: {
|
|
93
90
|
kind: 'postgres';
|
|
94
91
|
arg: string;
|
|
92
|
+
user: string;
|
|
93
|
+
database: string;
|
|
94
|
+
clock: 'wall';
|
|
95
|
+
credentialField: string;
|
|
95
96
|
note: string;
|
|
96
97
|
};
|
|
97
98
|
/** Listeners the twin opens beside its main port (see TwinPack.sidePorts). */
|
package/dist/src/pack-facts.js
CHANGED
|
@@ -175,9 +175,16 @@ export async function compilePackFacts(dirs) {
|
|
|
175
175
|
if (pack.managedDatabase !== undefined) {
|
|
176
176
|
const md = pack.managedDatabase;
|
|
177
177
|
if (md.kind !== 'postgres')
|
|
178
|
-
throw new Error(`pack-facts: ${vendor} declares managedDatabase.kind "${String(md.kind)}";
|
|
178
|
+
throw new Error(`pack-facts: ${vendor} declares managedDatabase.kind "${String(md.kind)}"; private SQL supports postgres`);
|
|
179
179
|
if (typeof md.arg !== 'string' || !/^--[a-z][a-z-]*$/.test(md.arg))
|
|
180
180
|
throw new Error(`pack-facts: ${vendor} declares managedDatabase.arg "${String(md.arg)}"; it is the serve flag the URL follows (--name)`);
|
|
181
|
+
for (const key of ['user', 'database'])
|
|
182
|
+
if (typeof md[key] !== 'string' || !md[key] || /[\r\n\0$]/u.test(md[key]) || Buffer.byteLength(md[key], 'utf8') > 63)
|
|
183
|
+
throw new Error(`pack-facts: ${vendor} private database ${key} must be a literal PostgreSQL identifier of at most 63 bytes`);
|
|
184
|
+
if (md.clock !== 'wall')
|
|
185
|
+
throw new Error(`pack-facts: ${vendor} private postgres must declare its wall clock`);
|
|
186
|
+
if (typeof md.credentialField !== 'string' || !Object.values(pack.credentialDoor?.fill ?? {}).includes(md.credentialField) || /^(?:file|directory|side):/.test(md.credentialField))
|
|
187
|
+
throw new Error(`pack-facts: ${vendor} private database must name its credential door's scalar URL field`);
|
|
181
188
|
if (typeof md.note !== 'string' || md.note.trim() === '')
|
|
182
189
|
throw new Error(`pack-facts: ${vendor} declares managedDatabase with no note`);
|
|
183
190
|
facts.managedDatabase = pack.managedDatabase;
|
package/dist/src/published.js
CHANGED
|
@@ -15,7 +15,7 @@ export const vendorOfPackage = (name) => name.replace(/^@[^/]+\//, '').replace(/
|
|
|
15
15
|
async function withPublished(spec, opts, use) {
|
|
16
16
|
const tmp = mkdtempSync(join(tmpdir(), 'twin-standard-'));
|
|
17
17
|
try {
|
|
18
|
-
const pack = spawnSync('npm', ['pack', spec, '--pack-destination', tmp, '--silent', ...(opts.registry ? ['--registry', opts.registry] : [])], { encoding: 'utf8' });
|
|
18
|
+
const pack = spawnSync('npm', ['pack', '--force', spec, '--pack-destination', tmp, '--silent', ...(opts.registry ? ['--registry', opts.registry] : [])], { encoding: 'utf8' });
|
|
19
19
|
const tarball = readdirSync(tmp).find((f) => f.endsWith('.tgz'));
|
|
20
20
|
if (pack.status !== 0 || !tarball)
|
|
21
21
|
throw new Error(`${spec}: the registry gave no tarball (${String(pack.stderr).trim().split('\n').at(-1) ?? ''})`);
|
|
@@ -72,7 +72,7 @@ export async function conformPublished(spec, opts = {}) {
|
|
|
72
72
|
const platform = Object.keys(deps).filter((d) => d.startsWith('@volter/'));
|
|
73
73
|
const others = Object.entries(deps).filter(([d]) => !d.startsWith('@volter/')).map(([d, v]) => `${d}@${v}`);
|
|
74
74
|
if (others.length) {
|
|
75
|
-
const install = spawnSync('
|
|
75
|
+
const install = spawnSync('pnpm', ['add', '--prod', '--config.autoInstallPeers=false', '--package-import-method=hardlink', '--config.nodeLinker=hoisted', '--ignore-scripts', ...(opts.registry ? ['--registry', opts.registry] : []), ...others], { cwd: dirname(dir), encoding: 'utf8' });
|
|
76
76
|
if (install.status !== 0)
|
|
77
77
|
throw new Error(`${spec}: its dependencies do not install (${String(install.stderr).trim().split('\n').at(-1) ?? ''})`);
|
|
78
78
|
}
|
|
@@ -367,8 +367,8 @@ export type WaitStep = {
|
|
|
367
367
|
wait: string;
|
|
368
368
|
};
|
|
369
369
|
export declare const isWait: (step: Step) => step is WaitStep;
|
|
370
|
-
/** What an application sends the
|
|
371
|
-
* pack whose
|
|
370
|
+
/** What an application sends the selected twin's database over its SQL wire, not through the vendor's HTTP API (a
|
|
371
|
+
* pack whose descriptor declares a private SQL product (`managedDatabase`)): its migrations as its deploy applies
|
|
372
372
|
* them and the queries it sends, each on the application's one connection to the walked World's database (world-core
|
|
373
373
|
* `databaseSession`), so a transaction it begins stays open into its next steps, as on its own connection. `sql` alone is
|
|
374
374
|
* one simple Query (a migration file, a `BEGIN`); with `values` it is one extended-protocol statement, the values bind
|
|
@@ -4,4 +4,7 @@ export type ServedPack = {
|
|
|
4
4
|
stop: () => void | Promise<void>;
|
|
5
5
|
managedDatabaseUrl?: () => Promise<string>;
|
|
6
6
|
};
|
|
7
|
-
export declare function servePack(pack: string, options:
|
|
7
|
+
export declare function servePack(pack: string, options: {
|
|
8
|
+
root?: string;
|
|
9
|
+
scenarioPath?: string;
|
|
10
|
+
}): Promise<ServedPack>;
|
|
@@ -8,8 +8,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
};
|
|
9
9
|
// The standard serves a pack with its declared backing, as the runtime binds it in a World.
|
|
10
10
|
import { join } from 'node:path';
|
|
11
|
-
import {
|
|
12
|
-
import { startPgliteHost } from "./walk-database.js";
|
|
11
|
+
import { startTwinDatabase } from "./walk-database.js";
|
|
13
12
|
export async function servePack(pack, options) {
|
|
14
13
|
const mod = await import(__rewriteRelativeImportExtension(join(pack, 'src', 'server.ts')));
|
|
15
14
|
const create = Object.entries(mod).find(([name, value]) => /^create\w+TwinServer$/.test(name) && typeof value === 'function')?.[1];
|
|
@@ -17,8 +16,8 @@ export async function servePack(pack, options) {
|
|
|
17
16
|
throw new Error('src/server.ts exports no create<Vendor>TwinServer');
|
|
18
17
|
const descriptor = (await import(__rewriteRelativeImportExtension(join(pack, 'src', 'index.ts')))).pack;
|
|
19
18
|
let database;
|
|
20
|
-
if (descriptor?.managedDatabase?.kind === 'postgres'
|
|
21
|
-
database = await
|
|
19
|
+
if (descriptor?.managedDatabase?.kind === 'postgres')
|
|
20
|
+
database = await startTwinDatabase(pack, descriptor);
|
|
22
21
|
let server;
|
|
23
22
|
try {
|
|
24
23
|
server = await create({ port: 0, ...options, ...(database ? { database: database.url } : {}) });
|
|
@@ -33,7 +32,7 @@ export async function servePack(pack, options) {
|
|
|
33
32
|
let stopping;
|
|
34
33
|
return {
|
|
35
34
|
...server,
|
|
36
|
-
managedDatabaseUrl: async () =>
|
|
35
|
+
managedDatabaseUrl: async () => backing.url,
|
|
37
36
|
stop: () => stopping ??= (async () => { await server.stop(); await backing.stop(); })(),
|
|
38
37
|
};
|
|
39
38
|
}
|
|
@@ -10,8 +10,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
10
10
|
// index. What the catalog's walkers drive (behavior journeys, the spec census probe, the invariants)
|
|
11
11
|
// since the worker render left with the v1 host.
|
|
12
12
|
import { join } from 'node:path';
|
|
13
|
-
import {
|
|
14
|
-
import { worldNow } from '@volter/world-core/runtime';
|
|
13
|
+
import { startTwinDatabase } from "./walk-database.js";
|
|
15
14
|
import { walkPackDirectory } from "./pack-directory.js";
|
|
16
15
|
export async function vendorFetch(twinDir, vendor, dependenciesFrom = twinDir) {
|
|
17
16
|
const directory = walkPackDirectory(twinDir, vendor, dependenciesFrom);
|
|
@@ -29,20 +28,20 @@ export async function vendorFetch(twinDir, vendor, dependenciesFrom = twinDir) {
|
|
|
29
28
|
if (!name)
|
|
30
29
|
return undefined;
|
|
31
30
|
const factory = mod[name];
|
|
32
|
-
|
|
31
|
+
const descriptor = mod.pack;
|
|
32
|
+
if (!descriptor?.managedDatabase)
|
|
33
33
|
return (root, options = {}) => factory({ root, ...options });
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// walked World's (set before each request: the walk moves the World clock between steps).
|
|
34
|
+
// Each walked twin owns a fresh SQL product through the ordinary selected-twin lifecycle.
|
|
35
|
+
// Native SQL uses the descriptor's explicit wall clock; kernel World time is not claimed for it.
|
|
37
36
|
return (root, options = {}) => {
|
|
38
37
|
const bare = factory({ root, ...options });
|
|
39
38
|
let bound;
|
|
40
39
|
let host;
|
|
41
|
-
const bind = () => (bound ??=
|
|
42
|
-
const served = (request) => bind().then((fetch) => {
|
|
43
|
-
//
|
|
44
|
-
const managedDatabaseUrl = () => bind().then(() => {
|
|
45
|
-
//
|
|
40
|
+
const bind = () => (bound ??= startTwinDatabase(directory, descriptor).then((started) => { host = started; return factory({ root, database: started.url, ...options }); }));
|
|
41
|
+
const served = (request) => bind().then((fetch) => { return fetch(request); });
|
|
42
|
+
// The twin's SQL product, for what an application sends over its own wire (journey-kit.ts).
|
|
43
|
+
const managedDatabaseUrl = () => bind().then(() => { return host.url; });
|
|
44
|
+
// The walk's end retires the selected twin's private engine through its World.
|
|
46
45
|
const stopDatabase = async () => { if (bound === undefined)
|
|
47
46
|
return; await bound.catch(() => undefined); await host?.stop(); };
|
|
48
47
|
return Object.assign(served, bare, { managedDatabaseUrl, stopDatabase });
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import type { TwinPack } from '@volter/world-core/runtime';
|
|
2
|
+
export type TwinDatabase = {
|
|
2
3
|
url: string;
|
|
3
|
-
port: number;
|
|
4
|
-
clock: (at: string) => void;
|
|
5
4
|
stop: () => Promise<void>;
|
|
6
5
|
};
|
|
7
|
-
export declare function
|
|
6
|
+
export declare function startTwinDatabase(pack: string, descriptor: TwinPack): Promise<TwinDatabase>;
|
|
@@ -1,108 +1,76 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// A managed SQL walk selects the pack in a task-owned World. The same private twin
|
|
2
|
+
// engine lifecycle serves both the World and the walker; no infrastructure is assembled here.
|
|
3
3
|
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { createServer } from 'node:net';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
6
4
|
import { createRequire } from 'node:module';
|
|
7
|
-
import { dirname,
|
|
8
|
-
import { worldNow } from '@volter/world-core/runtime';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
9
6
|
let loaded;
|
|
10
7
|
const worldRuntime = () => loaded ??= (async () => {
|
|
11
|
-
let entry;
|
|
12
8
|
try {
|
|
13
|
-
|
|
9
|
+
createRequire(import.meta.url).resolve('@volter/world-runtime');
|
|
14
10
|
}
|
|
15
11
|
catch {
|
|
16
|
-
throw new Error('A managed
|
|
12
|
+
throw new Error('A managed SQL walk needs @volter/world-runtime installed beside @volter/twin-standard');
|
|
17
13
|
}
|
|
18
|
-
return
|
|
14
|
+
return import('@volter/world-runtime');
|
|
19
15
|
})();
|
|
20
|
-
async function
|
|
21
|
-
return new Promise((done, fail) => {
|
|
22
|
-
const server = createServer();
|
|
23
|
-
server.once('error', fail);
|
|
24
|
-
server.listen(0, '127.0.0.1', () => {
|
|
25
|
-
const port = server.address().port;
|
|
26
|
-
server.close(() => done(port));
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
// Sequential replays declare the same database endpoint. A concurrent walk declares its own port. Only a successful
|
|
31
|
-
// runtime teardown releases this slot; unknown cleanup never licenses reusing its port or deleting its state.
|
|
32
|
-
let declaredPort;
|
|
33
|
-
let declaredHeld = false;
|
|
34
|
-
export async function startPgliteHost() {
|
|
16
|
+
export async function startTwinDatabase(pack, descriptor) {
|
|
35
17
|
if (!process.env.VOLTER_WORLD)
|
|
36
|
-
throw new Error('A managed
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
declaredHeld = true;
|
|
45
|
-
const fake = { user: 'world', database: 'world', password: 'walk-only-fake-password' };
|
|
46
|
-
const url = `postgresql://${fake.user}:${fake.password}@127.0.0.1:${port}/${fake.database}`;
|
|
18
|
+
throw new Error('A managed SQL walk requires its enclosing World');
|
|
19
|
+
const definition = descriptor.managedDatabase, door = descriptor.credentialDoor;
|
|
20
|
+
if (!definition || !door || !definition.credentialField)
|
|
21
|
+
throw new Error('The selected twin must declare its SQL credential door field');
|
|
22
|
+
const { runWorld, worldConfigDocument } = await worldRuntime();
|
|
23
|
+
// Keep the disposable World on the pack's own volume and package-resolution ancestry.
|
|
24
|
+
const root = mkdtempSync(join(dirname(pack), '.walk-database-'));
|
|
25
|
+
const manifest = JSON.parse(readFileSync(join(pack, 'package.json'), 'utf8'));
|
|
47
26
|
const config = join(root, 'world.json');
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
image: 'postgres:18-alpine', ports: [`127.0.0.1:${port}:5432`],
|
|
51
|
-
environment: { POSTGRES_USER: fake.user, POSTGRES_DB: fake.database, POSTGRES_PASSWORD: fake.password },
|
|
52
|
-
} } }, null, 2) + '\n');
|
|
53
|
-
writeFileSync(config, JSON.stringify(worldConfigDocument({ id: name,
|
|
54
|
-
env: { DATABASE_URL: url, VOLTER_WORLD_INFRA_BACKING: 'pglite' }, network: { egress: [] },
|
|
55
|
-
services: [{ id: 'infrastructure', type: 'external', controlPlane: true, external: {
|
|
56
|
-
up: [...command, 'up'], status: [...command, 'status'], down: [...command, 'down'],
|
|
57
|
-
discover: [{ as: 'DATABASE_URL', jsonPath: 'connections.DATABASE_URL', protocol: 'postgres' }],
|
|
58
|
-
readyWhen: { command: command[0], args: [...command.slice(1), 'status'], timeoutMs: 120_000, intervalMs: 500 },
|
|
59
|
-
} }],
|
|
27
|
+
writeFileSync(config, JSON.stringify(worldConfigDocument({ id: 'pack-walk-sql', stripEnv: ['*'], network: { egress: [] },
|
|
28
|
+
services: [{ id: descriptor.vendor, type: 'twin', package: manifest.name }],
|
|
60
29
|
}), null, 2) + '\n');
|
|
61
|
-
setWorldClock(name, root, worldNow());
|
|
62
30
|
const readyFile = join(root, 'ready.json'), releaseFile = join(root, 'release');
|
|
63
|
-
//
|
|
64
|
-
// connection env, and exits when the caller's sequence is done. Caller loss belongs to the runtime's run guardian.
|
|
31
|
+
// Ask the selected twin's actual product door. The runtime alone allocates and owns its engine.
|
|
65
32
|
const hold = `
|
|
66
|
-
import { existsSync, renameSync, watch, writeFileSync } from 'node:fs';
|
|
33
|
+
import { existsSync, readFileSync, renameSync, watch, writeFileSync } from 'node:fs';
|
|
67
34
|
import { dirname } from 'node:path';
|
|
68
|
-
const [ready, release] = process.argv.slice(1);
|
|
69
|
-
if (!process.env.
|
|
35
|
+
const [ready, release, vendor, path, body, field] = process.argv.slice(1);
|
|
36
|
+
if (!process.env.VOLTER_WORLD_INSTANCE) throw new Error('Missing held World instance');
|
|
37
|
+
const instance = JSON.parse(readFileSync(process.env.VOLTER_WORLD_INSTANCE, 'utf8'));
|
|
38
|
+
const service = instance.services[vendor];
|
|
39
|
+
if (!service?.url) throw new Error('Selected twin has no product endpoint');
|
|
40
|
+
const response = await fetch(new URL(path, service.url), {method:'POST',headers:{'content-type':'application/json'},body});
|
|
41
|
+
if (!response.ok) throw new Error('Selected twin SQL credential door refused: ' + response.status);
|
|
42
|
+
const result = await response.json();
|
|
43
|
+
if (typeof result[field] !== 'string' || !result[field]) throw new Error('SQL credential door omitted its declared field');
|
|
70
44
|
const done = () => { if (existsSync(release)) process.exit(0); };
|
|
71
45
|
watch(dirname(release), done);
|
|
72
|
-
writeFileSync(ready + '.tmp', JSON.stringify({ url:
|
|
46
|
+
writeFileSync(ready + '.tmp', JSON.stringify({ url: result[field] }), {mode:0o600});
|
|
73
47
|
renameSync(ready + '.tmp', ready);
|
|
74
48
|
done();`;
|
|
75
49
|
let finished;
|
|
76
|
-
const task = runWorld(config, [process.execPath, '--input-type=module', '-e', hold, readyFile, releaseFile
|
|
77
|
-
|
|
50
|
+
const task = runWorld(config, [process.execPath, '--input-type=module', '-e', hold, readyFile, releaseFile,
|
|
51
|
+
descriptor.vendor, door.path, JSON.stringify(door.body), definition.credentialField], {
|
|
52
|
+
root, cwd: root, owner: 'pack-walk-twin-private-sql',
|
|
78
53
|
}).then(result => { finished = { result, failed: false }; return result; }, error => {
|
|
79
54
|
finished = { error, failed: true };
|
|
80
55
|
throw error;
|
|
81
56
|
});
|
|
82
|
-
// Startup and normal stop await this promise; keep an early rejection handled while waiting for readiness.
|
|
83
57
|
void task.catch(() => undefined);
|
|
84
58
|
while (!existsSync(readyFile)) {
|
|
85
59
|
if (finished) {
|
|
86
|
-
if (!finished.failed && holdsDeclared)
|
|
87
|
-
declaredHeld = false;
|
|
88
60
|
if (finished.failed)
|
|
89
61
|
throw finished.error;
|
|
90
|
-
throw new Error(`
|
|
62
|
+
throw new Error(`Twin SQL World ended before its consumer was ready (exit ${finished.result.exitCode}); evidence: ${root}`);
|
|
91
63
|
}
|
|
92
64
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
93
65
|
}
|
|
94
66
|
const ready = JSON.parse(readFileSync(readyFile, 'utf8'));
|
|
95
67
|
let stopping;
|
|
96
|
-
return {
|
|
97
|
-
url: ready.url, port: Number(new URL(ready.url).port),
|
|
98
|
-
clock: at => setWorldClock(name, root, at),
|
|
68
|
+
return { url: ready.url,
|
|
99
69
|
stop: () => stopping ??= (async () => {
|
|
100
70
|
writeFileSync(releaseFile, 'released\n');
|
|
101
71
|
const result = await task;
|
|
102
72
|
if (result.exitCode !== 0)
|
|
103
|
-
throw new Error(`
|
|
104
|
-
if (holdsDeclared)
|
|
105
|
-
declaredHeld = false;
|
|
73
|
+
throw new Error(`Twin SQL World consumer exited ${result.exitCode}; evidence: ${root}`);
|
|
106
74
|
})(),
|
|
107
75
|
};
|
|
108
76
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/twin-standard",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.155",
|
|
4
4
|
"description": "The Protocol 3 standard for Volter twin packs: the criteria a pack is graded by, the grader, the gate a pack repository and the twin catalog run on every change and submission, and the spec derivation the grade checks against. Pin a version: every place a pack is judged judges by that standard.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
|
@@ -20,8 +20,9 @@
|
|
|
20
20
|
"README.md",
|
|
21
21
|
"LICENSE",
|
|
22
22
|
"client-sdks/package.json",
|
|
23
|
-
"client-sdks/
|
|
23
|
+
"client-sdks/pnpm-lock.yaml",
|
|
24
24
|
"!**/*.test.ts",
|
|
25
|
+
"client-sdks/pnpm-workspace.yaml",
|
|
25
26
|
"dist"
|
|
26
27
|
],
|
|
27
28
|
"repository": {
|
|
@@ -75,7 +76,7 @@
|
|
|
75
76
|
"yaml": "2.9.0"
|
|
76
77
|
},
|
|
77
78
|
"peerDependencies": {
|
|
78
|
-
"@volter/world-core": "3.0.
|
|
79
|
+
"@volter/world-core": "3.0.163",
|
|
79
80
|
"@volter/world-runtime": "*",
|
|
80
81
|
"playwright": "^1.63.0"
|
|
81
82
|
},
|
|
@@ -112,5 +113,13 @@
|
|
|
112
113
|
},
|
|
113
114
|
"engines": {
|
|
114
115
|
"node": ">=22.6"
|
|
116
|
+
},
|
|
117
|
+
"packageManager": "pnpm@12.15.0",
|
|
118
|
+
"devEngines": {
|
|
119
|
+
"packageManager": {
|
|
120
|
+
"name": "pnpm",
|
|
121
|
+
"version": "12.15.0",
|
|
122
|
+
"onFail": "error"
|
|
123
|
+
}
|
|
115
124
|
}
|
|
116
125
|
}
|
package/src/clients/sentry.ts
CHANGED
|
@@ -13,8 +13,8 @@ import { standardPackageFile } from '../package-file.ts';
|
|
|
13
13
|
const SDKS = standardPackageFile('client-sdks/');
|
|
14
14
|
async function sdkOf(name: string): Promise<any> {
|
|
15
15
|
if (!existsSync(new URL('node_modules/', SDKS))) {
|
|
16
|
-
const install = spawnSync('
|
|
17
|
-
if (install.status !== 0) throw new Error('client-sdks:
|
|
16
|
+
const install = spawnSync('pnpm', ['install', '--frozen-lockfile', '--ignore-scripts'], { cwd: SDKS, stdio: 'inherit' });
|
|
17
|
+
if (install.status !== 0) throw new Error('client-sdks: pnpm install --frozen-lockfile failed');
|
|
18
18
|
}
|
|
19
19
|
return await import(pathToFileURL(createRequire(new URL('package.json', SDKS)).resolve(name)).href);
|
|
20
20
|
}
|
package/src/conformance.ts
CHANGED
|
@@ -211,7 +211,7 @@ async function registered(pack: string, runtime: Runtime): Promise<string[]> {
|
|
|
211
211
|
* journeys, read from the file list `npm pack` makes. Escaped once: seven packs published their lanes' src without
|
|
212
212
|
* their spec, so the catalog's conformance of the tarball never read those lanes. */
|
|
213
213
|
function published(pack: string): string[] {
|
|
214
|
-
const run = spawnSync('npm', ['pack', '--dry-run', '--json', '--ignore-scripts'], { cwd: pack, encoding: 'utf8' });
|
|
214
|
+
const run = spawnSync('npm', ['pack', '--force', '--dry-run', '--json', '--ignore-scripts'], { cwd: pack, encoding: 'utf8' });
|
|
215
215
|
let files: Set<string>;
|
|
216
216
|
try { files = new Set(((JSON.parse(run.stdout) as Array<{ files: Array<{ path: string }> }>)[0]?.files ?? []).map((f) => f.path)); } catch { return [`npm pack lists no files (${String(run.stderr).trim().split('\n').at(-1) ?? ''})`]; }
|
|
217
217
|
const out: string[] = [];
|
package/src/consumer.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:pat
|
|
|
6
6
|
import { spawn } from 'node:child_process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { standardPackageFile } from './package-file.ts';
|
|
9
|
+
import { dependencyLock } from './dependency-lock.ts';
|
|
9
10
|
|
|
10
11
|
export type ConsumerRecipe = {
|
|
11
12
|
schemaVersion: 1;
|
|
@@ -79,6 +80,8 @@ export function prepareConsumerApp(packDir: string, appDir: string, installation
|
|
|
79
80
|
const recipe = consumerRecipe(vendor, packDir);
|
|
80
81
|
if (existsSync(appDir)) throw new Error('Consumer app must be a new directory');
|
|
81
82
|
const app = JSON.parse(recipe.files['package.json']!);
|
|
83
|
+
app.packageManager = 'pnpm@12.15.0';
|
|
84
|
+
app.devEngines = { packageManager: { name: 'pnpm', version: '12.15.0', onFail: 'error' } };
|
|
82
85
|
if (app.scripts || app.workspaces) throw new Error('Consumer fixture supplies direct SDK commands, not install scripts or workspace links');
|
|
83
86
|
for (const [name, version] of Object.entries({ ...app.dependencies, ...app.devDependencies, ...app.optionalDependencies })) {
|
|
84
87
|
if (typeof version !== 'string' || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) throw new Error(`${name}: customer SDK dependencies require exact registry versions`);
|
|
@@ -94,8 +97,9 @@ export function prepareConsumerApp(packDir: string, appDir: string, installation
|
|
|
94
97
|
if (installation.artifact && (!isAbsolute(installation.artifact) || !statSync(installation.artifact).isFile())) throw new Error('Consumer candidate must be an absolute prepared archive path');
|
|
95
98
|
if (Object.hasOwn(app.dependencies ?? {}, metadata.name)) throw new Error('Consumer SDK dependencies must not substitute the candidate package');
|
|
96
99
|
app.devDependencies = { ...app.devDependencies, '@volter/world-core': tools['@volter/world-core'], [metadata.name]: installation.artifact ? `file:${installation.artifact}` : metadata.version };
|
|
97
|
-
files.find(([path]) => path === 'package.json')![1] = JSON.stringify(app, null, 2) + '\n';
|
|
98
100
|
}
|
|
101
|
+
files.find(([path]) => path === 'package.json')![1] = JSON.stringify(app, null, 2) + '\n';
|
|
102
|
+
files.push(['pnpm-workspace.yaml', 'packages:\n - .\npackageImportMethod: hardlink\nnodeLinker: hoisted\nautoInstallPeers: false\nstrictPeerDependencies: false\n']);
|
|
99
103
|
mkdirSync(appDir, { recursive: true });
|
|
100
104
|
for (const [path, content] of files) {
|
|
101
105
|
mkdirSync(dirname(join(appDir, path)), { recursive: true });
|
|
@@ -117,10 +121,9 @@ export async function runConsumerWorkflow(packDir: string, options: ConsumerOpti
|
|
|
117
121
|
const location = relative(join(appRoot, 'node_modules'), identity.directory);
|
|
118
122
|
if (location.startsWith('..') || isAbsolute(location)) throw new Error(`${name}: consumer resolves an evaluator or ancestor dependency; install it in the app`);
|
|
119
123
|
}
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
if (kernel.version !== tools['@volter/world-core'] || appLock.packages?.['node_modules/@volter/world-core']?.version !== kernel.version) throw new Error('Consumer app kernel does not match the CLI kernel pin');
|
|
124
|
+
const appLock = dependencyLock(appRoot), appLockBytes = appLock.bytes;
|
|
125
|
+
if (candidate.version !== metadata.version || appLock.entry(metadata.name)?.integrity !== options.artifactIntegrity) throw new Error('Consumer app candidate does not match the exact assessed artifact');
|
|
126
|
+
if (kernel.version !== tools['@volter/world-core'] || appLock.entry('@volter/world-core')?.version !== kernel.version) throw new Error('Consumer app kernel does not match the CLI kernel pin');
|
|
124
127
|
const application = { candidate: { package: metadata.name, version: candidate.version, integrity: options.artifactIntegrity }, kernel: { package: '@volter/world-core', version: kernel.version }, dependencyLockSha256: createHash('sha256').update(appLockBytes).digest('hex'), appLocal: true };
|
|
125
128
|
const facts = JSON.parse(readFileSync(join(packDir, 'generated/pack-facts.json'), 'utf8'));
|
|
126
129
|
const vendor = Object.keys(facts.packs ?? {})[0];
|