@volter/twin-standard 1.0.158 → 1.0.160
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/README.md +3 -2
- package/client-sdks/package.json +1 -0
- package/client-sdks/pnpm-lock.yaml +210 -0
- package/dist/client-sdks/package.json +1 -0
- package/dist/client-sdks/pnpm-lock.yaml +210 -0
- package/dist/src/assessment.d.ts +13 -3
- package/dist/src/assessment.js +10 -6
- package/dist/src/browser-assessment.js +16 -13
- package/dist/src/check-sources.js +2 -2
- package/dist/src/clients/harness.d.ts +25 -4
- package/dist/src/clients/harness.js +184 -22
- package/dist/src/clients/jira.js +11 -2
- package/dist/src/clients/resend.first-use.json +2 -2
- package/dist/src/clients/run.js +19 -14
- package/dist/src/conformance.d.ts +3 -4
- package/dist/src/conformance.js +396 -203
- package/dist/src/consumer.d.ts +22 -1
- package/dist/src/consumer.js +91 -26
- package/dist/src/create-pack.d.ts +1 -1
- package/dist/src/create-pack.js +13 -12
- package/dist/src/credential-issuer-import.d.ts +3 -0
- package/dist/src/credential-issuer-import.js +42 -0
- package/dist/src/derive.js +6 -3
- package/dist/src/gate.js +4 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/lanes.d.ts +2 -0
- package/dist/src/lanes.js +7 -0
- package/dist/src/pack-facts.d.ts +3 -0
- package/dist/src/pack-facts.js +36 -1
- package/dist/src/protocol-3.js +13 -8
- package/dist/src/spec-documents.d.ts +4 -0
- package/dist/src/spec-documents.js +110 -3
- package/dist/src/spec-ir-client.js +1 -1
- package/dist/src/spec-ir-commands.js +1 -1
- package/dist/src/spec-ir-lines.js +1 -1
- package/dist/src/spec-ir-proto.d.ts +1 -1
- package/dist/src/spec-patches.js +2 -2
- package/dist/src/tools/judge-brief.js +1 -2
- package/dist/src/tools/life-coverage-walk.js +38 -26
- package/dist/src/tools/life-coverage.js +2 -2
- package/dist/src/tools/packs-root.js +3 -3
- package/dist/src/tools/score-pack.js +7 -7
- package/dist/src/tools/vendor-examples.js +95 -85
- package/dist/src/verdict.js +1 -1
- package/dist/src/walk/behavior-journey.d.ts +22 -14
- package/dist/src/walk/behavior-journey.js +199 -133
- package/dist/src/walk/journey-kit.d.ts +5 -4
- package/dist/src/walk/journey-kit.js +6 -2
- package/dist/src/walk/replay.d.ts +23 -0
- package/dist/src/walk/replay.js +118 -0
- package/dist/src/walk/serve-pack.d.ts +2 -2
- package/dist/src/walk/serve-pack.js +44 -2
- package/dist/src/walk/shape.d.ts +26 -5
- package/dist/src/walk/shape.js +167 -57
- package/dist/src/walk/vendor-fetch.d.ts +7 -2
- package/dist/src/walk/vendor-fetch.js +13 -2
- package/dist/src/walk/walk-database.d.ts +9 -1
- package/dist/src/walk/walk-database.js +35 -13
- package/dist/src/walk/xml-view.d.ts +3 -2
- package/dist/src/walk/xml-view.js +5 -4
- package/package.json +6 -4
- package/src/assessment.ts +12 -8
- package/src/browser-assessment.ts +12 -5
- package/src/check-sources.ts +2 -2
- package/src/clients/harness.ts +142 -19
- package/src/clients/jira.ts +4 -2
- package/src/clients/resend.first-use.json +2 -2
- package/src/clients/run.ts +17 -15
- package/src/conformance.ts +317 -147
- package/src/consumer.ts +83 -21
- package/src/create-pack.ts +13 -12
- package/src/credential-issuer-import.ts +38 -0
- package/src/derive.ts +6 -4
- package/src/gate.ts +3 -0
- package/src/index.ts +1 -1
- package/src/lanes.ts +7 -0
- package/src/pack-facts.ts +25 -2
- package/src/protocol-3.ts +12 -8
- package/src/spec-documents.ts +92 -3
- package/src/spec-ir-client.ts +1 -1
- package/src/spec-ir-commands.ts +1 -1
- package/src/spec-ir-lines.ts +1 -1
- package/src/spec-ir-proto.ts +1 -1
- package/src/spec-patches.ts +2 -2
- package/src/tools/judge-brief.ts +1 -2
- package/src/tools/life-coverage-walk.ts +32 -22
- package/src/tools/life-coverage.ts +2 -2
- package/src/tools/packs-root.ts +3 -3
- package/src/tools/score-pack.ts +8 -8
- package/src/tools/vendor-examples.ts +92 -80
- package/src/verdict.ts +1 -1
- package/src/walk/behavior-journey.ts +158 -109
- package/src/walk/journey-kit.ts +9 -7
- package/src/walk/replay.ts +103 -0
- package/src/walk/serve-pack.ts +30 -5
- package/src/walk/shape.ts +157 -55
- package/src/walk/vendor-fetch.ts +16 -4
- package/src/walk/walk-database.ts +41 -15
- package/src/walk/xml-view.ts +5 -4
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** PostgreSQL's ISO timestamp text, retaining its six fractional digits and timezone offset. */
|
|
2
|
+
function timestamp(value, oid) {
|
|
3
|
+
if (value === 'infinity')
|
|
4
|
+
return 10n ** 30n;
|
|
5
|
+
if (value === '-infinity')
|
|
6
|
+
return -(10n ** 30n);
|
|
7
|
+
if (typeof value !== 'string')
|
|
8
|
+
return undefined;
|
|
9
|
+
const m = /^(\d{4,6})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?(Z|([+-])(\d{2})(?::?(\d{2}))?(?::?(\d{2}))?)?( BC)?$/.exec(value);
|
|
10
|
+
if (!m || (oid === 1184) !== (m[8] !== undefined))
|
|
11
|
+
return undefined;
|
|
12
|
+
const [year, month, day, hour, minute, second] = m.slice(1, 7).map(Number);
|
|
13
|
+
if (!year || !month || month > 12 || !day || hour > 23 || minute > 59 || second > 59)
|
|
14
|
+
return undefined;
|
|
15
|
+
const astronomicalYear = m[13] ? 1 - year : year;
|
|
16
|
+
const leap = astronomicalYear % 4 === 0 && (astronomicalYear % 100 !== 0 || astronomicalYear % 400 === 0);
|
|
17
|
+
if (day > [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1])
|
|
18
|
+
return undefined;
|
|
19
|
+
const zoneHour = Number(m[10] ?? 0), zoneMinute = Number(m[11] ?? 0), zoneSecond = Number(m[12] ?? 0);
|
|
20
|
+
if (zoneHour > 15 || zoneMinute > 59 || zoneSecond > 59)
|
|
21
|
+
return undefined;
|
|
22
|
+
const adjustedYear = astronomicalYear - (month <= 2 ? 1 : 0);
|
|
23
|
+
const era = Math.floor(adjustedYear / 400), yearOfEra = adjustedYear - era * 400;
|
|
24
|
+
const dayOfYear = Math.floor((153 * (month + (month > 2 ? -3 : 9)) + 2) / 5) + day - 1;
|
|
25
|
+
const days = era * 146097 + yearOfEra * 365 + Math.floor(yearOfEra / 4) - Math.floor(yearOfEra / 100) + dayOfYear - 719468;
|
|
26
|
+
const offset = (zoneHour * 3600 + zoneMinute * 60 + zoneSecond) * (m[9] === '-' ? -1 : 1);
|
|
27
|
+
return BigInt(days) * 86400000000n + BigInt(hour * 3600 + minute * 60 + second - offset) * 1000000n + BigInt((m[7] ?? '').padEnd(6, '0'));
|
|
28
|
+
}
|
|
29
|
+
/** A journey's declared native timestamp columns are present, valid and nondecreasing down the returned rows. */
|
|
30
|
+
export function timestampOrderFailures(body, timeFields, columns) {
|
|
31
|
+
if (!columns.length)
|
|
32
|
+
return [];
|
|
33
|
+
const rows = body && typeof body === 'object' ? body.rows : undefined;
|
|
34
|
+
if (!Array.isArray(rows))
|
|
35
|
+
return ['timestamp ordering requires SQL result rows'];
|
|
36
|
+
const failures = [];
|
|
37
|
+
for (const column of columns) {
|
|
38
|
+
const oid = timeFields[column];
|
|
39
|
+
if (oid !== 1114 && oid !== 1184) {
|
|
40
|
+
failures.push(`${column} is not a native timestamp column`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
let previous;
|
|
44
|
+
for (const [i, row] of rows.entries()) {
|
|
45
|
+
const at = timestamp(row && typeof row === 'object' ? row[column] : undefined, oid);
|
|
46
|
+
if (at === undefined)
|
|
47
|
+
failures.push(`rows.${i}.${column} is not a non-null native timestamp`);
|
|
48
|
+
else if (previous !== undefined && at < previous)
|
|
49
|
+
failures.push(`rows.${i}.${column} precedes the previous row's timestamp`);
|
|
50
|
+
previous = at;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return failures;
|
|
54
|
+
}
|
|
55
|
+
/** Compare received traces by their selected engine declarations, retaining every undeclared byte. */
|
|
56
|
+
export function compareReplays(first, second) {
|
|
57
|
+
const diagnostics = [];
|
|
58
|
+
if (!first.length)
|
|
59
|
+
diagnostics.push('the journey answered no steps');
|
|
60
|
+
if (first.length !== second.length)
|
|
61
|
+
diagnostics.push(`the replays answered ${first.length} and ${second.length} steps`);
|
|
62
|
+
const times = new Map();
|
|
63
|
+
for (let i = 0; i < Math.min(first.length, second.length); i++) {
|
|
64
|
+
const a = first[i], b = second[i];
|
|
65
|
+
const where = `step ${i + 1} (${a.name})`;
|
|
66
|
+
if (a.name !== b.name || a.path !== b.path || a.status !== b.status || JSON.stringify(a.sql) !== JSON.stringify(b.sql) || JSON.stringify(a.connection) !== JSON.stringify(b.connection)) {
|
|
67
|
+
diagnostics.push(`${where}: request, status or native engine provenance differs`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const fields = a.sql?.clock === 'wall' ? a.sql.timeFields : {};
|
|
71
|
+
const same = (left, right, path) => {
|
|
72
|
+
if (path.length === 1 && a.connection?.kind === 'postgres' && a.connection.authenticated === true && path[0] === a.connection.field)
|
|
73
|
+
return typeof left === 'string' && typeof right === 'string';
|
|
74
|
+
const oid = path.length === 3 && path[0] === 'rows' ? fields[path[2]] : undefined;
|
|
75
|
+
if ((oid === 1114 || oid === 1184) && left !== null && right !== null) {
|
|
76
|
+
const x = timestamp(left, oid), y = timestamp(right, oid);
|
|
77
|
+
if (x === undefined || y === undefined) {
|
|
78
|
+
diagnostics.push(`${where}: ${path.join('.')} is not a valid native timestamp`);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const values = times.get(oid) ?? [];
|
|
82
|
+
values.push({ first: x, second: y, where: `${where}: ${path.join('.')}` });
|
|
83
|
+
times.set(oid, values);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(left) || Array.isArray(right))
|
|
87
|
+
return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, n) => same(value, right[n], [...path, String(n)]));
|
|
88
|
+
if (left && right && typeof left === 'object' && typeof right === 'object') {
|
|
89
|
+
const keys = Object.keys(left);
|
|
90
|
+
return JSON.stringify(keys) === JSON.stringify(Object.keys(right)) && keys.every(key => same(left[key], right[key], [...path, key]));
|
|
91
|
+
}
|
|
92
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
93
|
+
};
|
|
94
|
+
if (!same(a.body, b.body, []))
|
|
95
|
+
diagnostics.push(`${where}: response bytes differ`);
|
|
96
|
+
}
|
|
97
|
+
// Equal timestamps form one group: ties may split or coincide, while a strict order cannot reverse.
|
|
98
|
+
for (const values of times.values()) {
|
|
99
|
+
values.sort((a, b) => a.first < b.first ? -1 : a.first > b.first ? 1 : 0);
|
|
100
|
+
let greatest;
|
|
101
|
+
for (let i = 0; i < values.length;) {
|
|
102
|
+
let end = i + 1;
|
|
103
|
+
while (end < values.length && values[end].first === values[i].first)
|
|
104
|
+
end++;
|
|
105
|
+
let next = greatest;
|
|
106
|
+
for (let n = i; n < end; n++) {
|
|
107
|
+
const at = values[n];
|
|
108
|
+
if (greatest && at.second < greatest.value)
|
|
109
|
+
diagnostics.push(`${at.where}: timestamp order reverses relative to ${greatest.where}`);
|
|
110
|
+
if (!next || at.second > next.value)
|
|
111
|
+
next = { value: at.second, where: at.where };
|
|
112
|
+
}
|
|
113
|
+
greatest = next;
|
|
114
|
+
i = end;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { equal: diagnostics.length === 0, diagnostics };
|
|
118
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { type ManagedDatabaseWalk } from './walk-database.js';
|
|
1
2
|
export type ServedPack = {
|
|
2
3
|
port: number;
|
|
3
4
|
inspectPort?: number;
|
|
4
5
|
wirePort?: number;
|
|
5
6
|
stop: () => void | Promise<void>;
|
|
6
|
-
|
|
7
|
-
};
|
|
7
|
+
} & Omit<ManagedDatabaseWalk, 'stopDatabase'>;
|
|
8
8
|
export declare function servePack(pack: string, options: {
|
|
9
9
|
root?: string;
|
|
10
10
|
scenarioPath?: string;
|
|
@@ -10,6 +10,19 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { startTwinDatabase } from "./walk-database.js";
|
|
12
12
|
export async function servePack(pack, options) {
|
|
13
|
+
const previousBoot = process.env.VOLTER_WORLD_BOOT_ID;
|
|
14
|
+
process.env.VOLTER_WORLD_BOOT_ID = 'conformance';
|
|
15
|
+
try {
|
|
16
|
+
return await serve(pack, options);
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
if (previousBoot === undefined)
|
|
20
|
+
delete process.env.VOLTER_WORLD_BOOT_ID;
|
|
21
|
+
else
|
|
22
|
+
process.env.VOLTER_WORLD_BOOT_ID = previousBoot;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function serve(pack, options) {
|
|
13
26
|
const mod = await import(__rewriteRelativeImportExtension(join(pack, 'src', 'server.ts')));
|
|
14
27
|
const create = Object.entries(mod).find(([name, value]) => /^create\w+TwinServer$/.test(name) && typeof value === 'function')?.[1];
|
|
15
28
|
if (!create)
|
|
@@ -23,9 +36,20 @@ export async function servePack(pack, options) {
|
|
|
23
36
|
server = await create({ port: 0, ...options, ...(database ? { database: database.url } : {}) });
|
|
24
37
|
}
|
|
25
38
|
catch (error) {
|
|
26
|
-
|
|
39
|
+
try {
|
|
40
|
+
await database?.stop();
|
|
41
|
+
}
|
|
42
|
+
catch (cleanup) {
|
|
43
|
+
throw new AggregateError([error, cleanup], 'Pack startup and backing retirement failed');
|
|
44
|
+
}
|
|
27
45
|
throw error;
|
|
28
46
|
}
|
|
47
|
+
// a raw-TCP pack's HTTP control seam is the side port its descriptor declares for HTTP (a mail relay's policy door),
|
|
48
|
+
// where the pack's factory names it by that side port's option
|
|
49
|
+
const declared = descriptor?.transport === 'raw-tcp' ? descriptor.sidePorts?.find((side) => (side.scheme ?? 'http') === 'http') : undefined;
|
|
50
|
+
const control = declared ? server[declared.option] : undefined;
|
|
51
|
+
if (server.inspectPort === undefined && typeof control === 'number')
|
|
52
|
+
server = { ...server, inspectPort: control };
|
|
29
53
|
if (!database)
|
|
30
54
|
return server;
|
|
31
55
|
const backing = database;
|
|
@@ -33,6 +57,24 @@ export async function servePack(pack, options) {
|
|
|
33
57
|
return {
|
|
34
58
|
...server,
|
|
35
59
|
managedDatabaseUrl: async () => backing.url,
|
|
36
|
-
|
|
60
|
+
managedDatabaseClock: descriptor.managedDatabase.clock,
|
|
61
|
+
managedDatabaseConnection: backing.connection,
|
|
62
|
+
stop: () => stopping ??= (async () => {
|
|
63
|
+
const failures = [];
|
|
64
|
+
try {
|
|
65
|
+
await server.stop();
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
failures.push(error);
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
await backing.stop();
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
failures.push(error);
|
|
75
|
+
}
|
|
76
|
+
if (failures.length)
|
|
77
|
+
throw new AggregateError(failures, 'Pack retirement failed');
|
|
78
|
+
})(),
|
|
37
79
|
};
|
|
38
80
|
}
|
package/dist/src/walk/shape.d.ts
CHANGED
|
@@ -15,14 +15,21 @@ export type ShapeOperation = {
|
|
|
15
15
|
alternatives?: string[];
|
|
16
16
|
};
|
|
17
17
|
};
|
|
18
|
+
/** Every selected unit's vendor fields, including nested spec schemas, GraphQL and native error wires. */
|
|
19
|
+
export declare function vendorFieldsOf(pkg: string): Promise<Set<string>>;
|
|
20
|
+
/** Decode a vendor answer without rounding wire integers; XML reads as its root's members. */
|
|
21
|
+
export declare function decodeResponse(text: string, mediaType: string | undefined): unknown;
|
|
22
|
+
/** Private stored fields, except names the vendor owns, in any answer, event or frame. */
|
|
23
|
+
export declare function leaked(v: unknown, vendorFields: Set<string>, at?: string, unshaped?: boolean): string[];
|
|
18
24
|
export type ShapeChecker = {
|
|
19
|
-
/** The spec operation
|
|
25
|
+
/** The spec operation the routed request matches, if any. */
|
|
20
26
|
operationOf: (method: string, path: string, headers?: Record<string, string>) => {
|
|
21
27
|
op: ShapeOperation;
|
|
22
28
|
params: Record<string, string>;
|
|
29
|
+
lane?: string;
|
|
23
30
|
} | undefined;
|
|
24
31
|
/** A request's URL as the unit routes it (a virtual-hosted S3 request's bucket moved from its host into its path). */
|
|
25
|
-
pathOf
|
|
32
|
+
pathOf: (method: string, path: string, headers?: Record<string, string>) => string;
|
|
26
33
|
/** What in an answer the spec does not give the operation's resource (or its view). */
|
|
27
34
|
failures: (op: ShapeOperation, body: unknown, mediaType?: string, accept?: string, status?: number, exchange?: {
|
|
28
35
|
method: string;
|
|
@@ -30,14 +37,28 @@ export type ShapeChecker = {
|
|
|
30
37
|
}) => string[];
|
|
31
38
|
/** Whether a request pins an API version before the spec's, whose answers the spec does not describe (a Stripe
|
|
32
39
|
* mobile SDK's `Stripe-Version: 2024-06-20`), by the version header the manifest declares. */
|
|
33
|
-
pinsOlder: (headers: Record<string, string> | undefined) => boolean;
|
|
40
|
+
pinsOlder: (headers: Record<string, string> | undefined, op: ShapeOperation) => boolean;
|
|
41
|
+
};
|
|
42
|
+
/** The same response rules apply to a life's requests and the unscripted surface sweep. */
|
|
43
|
+
export declare function responseProblems(checker: ShapeChecker | undefined, schemaFields: Set<string>, exchange: {
|
|
44
|
+
method: string;
|
|
45
|
+
path: string;
|
|
46
|
+
requestHeaders: Record<string, string>;
|
|
47
|
+
graphqlBody: unknown;
|
|
48
|
+
status: number;
|
|
49
|
+
mediaType: string | undefined;
|
|
50
|
+
body: unknown;
|
|
51
|
+
text: string;
|
|
52
|
+
}): {
|
|
53
|
+
shape: string[];
|
|
54
|
+
leaks: string[];
|
|
34
55
|
};
|
|
35
56
|
/** The directories under a pack that are lanes: a derived unit with its own generated surface
|
|
36
57
|
* (`../twin-packs-p3/aws/secretsmanager/`, `../twin-packs-p3/planetscale/api/`). */
|
|
37
58
|
export declare function lanesOf(pkg: string): string[];
|
|
38
59
|
/** A derived pack's shape checker, or undefined for a pack with no generated surface. A pack's walk reaches its lanes'
|
|
39
60
|
* operations through the pack's router (a psdb life creates its database's passwords through the management API), so
|
|
40
|
-
*
|
|
61
|
+
* the core gateway selects the owning lane before its surface is matched and its answer judged. */
|
|
41
62
|
export declare function shapeChecker(pkg: string): Promise<ShapeChecker | undefined>;
|
|
42
63
|
/** Where one connection stands for the check: the command whose intermediate reply the next lines answer. */
|
|
43
64
|
export type LineConnection = {
|
|
@@ -48,7 +69,7 @@ export type LineShape = {
|
|
|
48
69
|
* command-reply table reads them; moves `conn` past an intermediate reply. */
|
|
49
70
|
check: (conn: LineConnection, line: string | null, codes: number[]) => string[];
|
|
50
71
|
};
|
|
51
|
-
/** The SHAPE of a line protocol (
|
|
72
|
+
/** The SHAPE of a line protocol (the derivation, from an RFC's command-reply table): each reply a
|
|
52
73
|
* code the spec allows the command it answers, or one any command may answer. A line after an
|
|
53
74
|
* intermediate reply (DATA's payload, a SASL response) is answered as that command's `then`. */
|
|
54
75
|
export declare function lineShape(pkg: string): LineShape | undefined;
|
package/dist/src/walk/shape.js
CHANGED
|
@@ -10,45 +10,151 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
10
10
|
// candidate only the values the spec lists (docs/contributing/architecture.md, "Protocol 3",
|
|
11
11
|
// State and The report), and every field the spec requires. The walk (behavior-journey.ts) runs it on every successful step of a derived
|
|
12
12
|
// pack, so a walk an author iterates with fails where the score would; score-pack reads it from there.
|
|
13
|
-
import { existsSync,
|
|
14
|
-
import { join } from 'node:path';
|
|
15
|
-
import { compileSurface, matchOperation } from '@volter/world-core/runtime';
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { basename, join } from 'node:path';
|
|
15
|
+
import { compileSurface, hostOf, laneSelector, matchOperation, slashesMerged, unitRequestRouter, worldDoorMatcher } from '@volter/world-core/runtime';
|
|
16
|
+
import { parseExactJson } from '@volter/world-core/exact-json';
|
|
17
|
+
import { hostRules } from '@volter/world-core/host-routing';
|
|
16
18
|
import { binaryMedia } from "../spec-ir.js";
|
|
19
|
+
import { schemaFieldNames } from "../spec-documents.js";
|
|
20
|
+
import { unitsIn } from "../lanes.js";
|
|
21
|
+
import { xmlMembers } from "./xml-view.js";
|
|
22
|
+
import { parse, visit } from 'graphql';
|
|
17
23
|
/** Whether a resource is one of the objects `said` names (a union's `object` lists every alternative's). */
|
|
18
24
|
const isObject = (r, said) => typeof said === 'string' && (r.fields.find((f) => f.name === 'object')?.enum ?? []).includes(said);
|
|
25
|
+
/** Field names owned by the vendor's resource schemas and native error templates. */
|
|
26
|
+
function vendorFieldNames(surface, ...errors) {
|
|
27
|
+
const fields = new Set((surface?.resources ?? []).flatMap((resource) => resource.fields.map((field) => typeof field === 'string' ? field : field.name)));
|
|
28
|
+
const visit = (value) => {
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
for (const item of value)
|
|
31
|
+
visit(item);
|
|
32
|
+
}
|
|
33
|
+
else if (value && typeof value === 'object')
|
|
34
|
+
for (const [name, item] of Object.entries(value)) {
|
|
35
|
+
fields.add(name);
|
|
36
|
+
visit(item);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
for (const error of errors)
|
|
40
|
+
visit(error);
|
|
41
|
+
return fields;
|
|
42
|
+
}
|
|
43
|
+
/** Every selected unit's vendor fields, including nested spec schemas, GraphQL and native error wires. */
|
|
44
|
+
export async function vendorFieldsOf(pkg) {
|
|
45
|
+
const fields = new Set();
|
|
46
|
+
for (const unit of unitsIn(pkg)) {
|
|
47
|
+
const manifestPath = join(unit, 'src', 'manifest.ts');
|
|
48
|
+
const { manifest } = existsSync(manifestPath) ? await import(__rewriteRelativeImportExtension(manifestPath)) : {};
|
|
49
|
+
for (const name of ['surface.gen.json', 'graphql.gen.json']) {
|
|
50
|
+
const path = join(unit, 'src', 'generated', name);
|
|
51
|
+
if (existsSync(path))
|
|
52
|
+
for (const field of vendorFieldNames(JSON.parse(readFileSync(path, 'utf8'))))
|
|
53
|
+
fields.add(field);
|
|
54
|
+
}
|
|
55
|
+
for (const field of schemaFieldNames(join(unit, 'spec')))
|
|
56
|
+
fields.add(field);
|
|
57
|
+
for (const field of vendorFieldNames(undefined, manifest?.error, ...(manifest?.auth?.wires ?? []).map((wire) => wire.error)))
|
|
58
|
+
fields.add(field);
|
|
59
|
+
}
|
|
60
|
+
return fields;
|
|
61
|
+
}
|
|
62
|
+
/** Decode a vendor answer without rounding wire integers; XML reads as its root's members. */
|
|
63
|
+
export function decodeResponse(text, mediaType) {
|
|
64
|
+
let body;
|
|
65
|
+
try {
|
|
66
|
+
body = parseExactJson(text);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
body = text;
|
|
70
|
+
}
|
|
71
|
+
return typeof body === 'string' && /xml/.test(mediaType ?? '') ? xmlMembers(text) ?? text : body;
|
|
72
|
+
}
|
|
73
|
+
/** Private stored fields, except names the vendor owns, in any answer, event or frame. */
|
|
74
|
+
export function leaked(v, vendorFields, at = '', unshaped = false) {
|
|
75
|
+
if (Array.isArray(v))
|
|
76
|
+
return v.flatMap((x, i) => leaked(x, vendorFields, `${at}[${i}]`, unshaped)).slice(0, 5);
|
|
77
|
+
if (!v || typeof v !== 'object')
|
|
78
|
+
return [];
|
|
79
|
+
return Object.entries(v).flatMap(([k, x]) => ((k.startsWith('_') || (unshaped && k === 'updatedAt')) && !vendorFields.has(k) ? [`${at}${at ? '.' : ''}${k}`] : leaked(x, vendorFields, `${at}${at ? '.' : ''}${k}`, unshaped))).slice(0, 5);
|
|
80
|
+
}
|
|
81
|
+
/** Vendor schema fields plus the response keys this GraphQL request selects, including aliases. */
|
|
82
|
+
function responseFieldNames(schemaFields, graphqlBody) {
|
|
83
|
+
const fields = new Set(schemaFields);
|
|
84
|
+
if (graphqlBody && typeof graphqlBody === 'object' && typeof graphqlBody.query === 'string') {
|
|
85
|
+
try {
|
|
86
|
+
visit(parse(graphqlBody.query), { Field: (field) => { fields.add(field.alias?.value ?? field.name.value); } });
|
|
87
|
+
}
|
|
88
|
+
catch { /* malformed documents have no selected response keys */ }
|
|
89
|
+
}
|
|
90
|
+
return fields;
|
|
91
|
+
}
|
|
92
|
+
/** The same response rules apply to a life's requests and the unscripted surface sweep. */
|
|
93
|
+
export function responseProblems(checker, schemaFields, exchange) {
|
|
94
|
+
const hit = checker?.operationOf(exchange.method, exchange.path, exchange.requestHeaders);
|
|
95
|
+
const shaped = exchange.status >= 200 && exchange.status < 300 && hit !== undefined && !checker.pinsOlder(exchange.requestHeaders, hit.op);
|
|
96
|
+
return {
|
|
97
|
+
shape: shaped ? checker.failures(hit.op, exchange.body, exchange.mediaType, new Headers(exchange.requestHeaders).get('accept') ?? undefined, exchange.status, { method: exchange.method, responseText: exchange.text }) : [],
|
|
98
|
+
leaks: leaked(exchange.body, responseFieldNames(schemaFields, exchange.graphqlBody), '', !shaped || !hit?.op.answers),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
19
101
|
/** The directories under a pack that are lanes: a derived unit with its own generated surface
|
|
20
102
|
* (`../twin-packs-p3/aws/secretsmanager/`, `../twin-packs-p3/planetscale/api/`). */
|
|
21
103
|
export function lanesOf(pkg) {
|
|
22
|
-
|
|
23
|
-
return [];
|
|
24
|
-
return readdirSync(pkg, { withFileTypes: true })
|
|
25
|
-
.filter((e) => e.isDirectory() && !['src', 'spec', 'journeys', 'node_modules', 'scripts', 'test-fixtures'].includes(e.name) && existsSync(join(pkg, e.name, 'src', 'generated', 'surface.gen.json')))
|
|
26
|
-
.map((e) => join(pkg, e.name));
|
|
104
|
+
return unitsIn(pkg).filter((unit) => unit !== pkg && existsSync(join(unit, 'src', 'generated', 'surface.gen.json')));
|
|
27
105
|
}
|
|
28
106
|
/** A derived pack's shape checker, or undefined for a pack with no generated surface. A pack's walk reaches its lanes'
|
|
29
107
|
* operations through the pack's router (a psdb life creates its database's passwords through the management API), so
|
|
30
|
-
*
|
|
108
|
+
* the core gateway selects the owning lane before its surface is matched and its answer judged. */
|
|
31
109
|
export async function shapeChecker(pkg) {
|
|
32
|
-
const units =
|
|
33
|
-
|
|
110
|
+
const units = await Promise.all(unitsIn(pkg).map(async (dir) => {
|
|
111
|
+
const path = join(dir, 'src', 'manifest.ts');
|
|
112
|
+
const { manifest } = existsSync(path) ? await import(__rewriteRelativeImportExtension(path)) : {};
|
|
113
|
+
return { dir, manifest, response: unitResponseOwner(dir, manifest), door: worldDoorMatcher(manifest?.doors ?? []) };
|
|
114
|
+
}));
|
|
115
|
+
if (!units.some((unit) => unit.response?.checker))
|
|
34
116
|
return undefined;
|
|
35
|
-
|
|
36
|
-
|
|
117
|
+
const root = units[0];
|
|
118
|
+
const arrival = (method, path, headers) => {
|
|
119
|
+
const request = new Request(new URL(path, 'http://twin.local'), { method, headers });
|
|
120
|
+
return root.manifest?.mergeSlashes ? slashesMerged(request) : request;
|
|
121
|
+
};
|
|
122
|
+
const selected = root.manifest?.lanes ? laneSelector(root.manifest.lanes, Object.fromEntries(units.slice(1).map((unit) => [basename(unit.dir), {
|
|
123
|
+
manifest: unit.manifest ?? {}, fetch: { serves: unit.response?.checker?.serves,
|
|
124
|
+
contentHosts: (unit.manifest?.screens ?? []).filter((screen) => screen.kind === 'content' && screen.status === 'done').flatMap((screen) => [screen.host, ...(screen.hosts ?? [])]).filter((host) => !/[{<]/.test(host)).map((host) => host.toLowerCase()) },
|
|
125
|
+
}])), root.response?.checker?.serves) : undefined;
|
|
37
126
|
const owner = new WeakMap();
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
127
|
+
const selectedRequest = (method, path, headers) => {
|
|
128
|
+
const request = arrival(method, path, headers);
|
|
129
|
+
const source = root.manifest?.mergeSlashes ? request.url : path;
|
|
130
|
+
const rootDoor = root.door(request.method, new URL(request.url).pathname);
|
|
131
|
+
const name = rootDoor ? undefined : selected?.(request);
|
|
132
|
+
const candidates = rootDoor ? [root] : selected ? [name === undefined ? root : units.find((unit) => basename(unit.dir) === name)] : units;
|
|
133
|
+
for (const unit of candidates) {
|
|
134
|
+
if (!unit)
|
|
135
|
+
continue;
|
|
136
|
+
const routed = unit.response?.pathOf(method, source, headers) ?? request.url;
|
|
137
|
+
if (rootDoor || unit.door(method, new URL(routed).pathname))
|
|
138
|
+
return { path: routed };
|
|
139
|
+
const checker = unit.response?.checker;
|
|
140
|
+
const hit = checker?.operationOf(method, source, headers);
|
|
141
|
+
if (hit)
|
|
142
|
+
return { path: routed, checker, hit: unit === root ? hit : { ...hit, lane: basename(unit.dir) } };
|
|
143
|
+
}
|
|
144
|
+
return { path: candidates[0]?.response?.pathOf(method, source, headers) ?? request.url };
|
|
145
|
+
};
|
|
146
|
+
const operationOf = (method, path, headers) => {
|
|
147
|
+
const selected = selectedRequest(method, path, headers);
|
|
148
|
+
if (!selected.hit)
|
|
49
149
|
return undefined;
|
|
50
|
-
|
|
51
|
-
|
|
150
|
+
owner.set(selected.hit.op, selected.checker);
|
|
151
|
+
return selected.hit;
|
|
152
|
+
};
|
|
153
|
+
return {
|
|
154
|
+
pinsOlder: (headers, op) => owner.get(op).pinsOlder(headers, op),
|
|
155
|
+
pathOf: (method, path, headers) => selectedRequest(method, path, headers).path,
|
|
156
|
+
operationOf,
|
|
157
|
+
failures: (op, body, mediaType, accept, status, exchange) => owner.get(op).failures(op, body, mediaType, accept, status, exchange),
|
|
52
158
|
};
|
|
53
159
|
}
|
|
54
160
|
/** Accept's most specific media range decides its quality; an explicit q=0 overrides a wildcard. */
|
|
@@ -67,53 +173,54 @@ function acceptsMedia(accept, media, defaultMedia) {
|
|
|
67
173
|
}).sort((a, b) => b.specificity - a.specificity || b.q - a.q);
|
|
68
174
|
return (matches[0]?.q ?? 0) > 0;
|
|
69
175
|
}
|
|
70
|
-
|
|
176
|
+
function unitResponseOwner(pkg, manifest) {
|
|
71
177
|
const surfacePath = join(pkg, 'src', 'generated', 'surface.gen.json');
|
|
72
178
|
if (!existsSync(surfacePath))
|
|
73
179
|
return undefined;
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
// (Upstash's REST envelope), which a check by method and path cannot see: its replies are not shape-checked yet, and
|
|
77
|
-
// its examples assert what each answers
|
|
78
|
-
if (['command-reply', 'command-table'].includes(String(surface.format)))
|
|
79
|
-
return undefined;
|
|
80
|
-
const manifestModule = existsSync(join(pkg, 'src', 'manifest.ts')) ? (await import(__rewriteRelativeImportExtension(join(pkg, 'src', 'manifest.ts')))) : {};
|
|
81
|
-
// a unit whose host can name part of what its path names (S3's virtual-hosted bucket) is routed as the kernel routes it: a host whose label is part of the path (the manifest's pathFromHost, S3's virtual-hosted
|
|
82
|
-
// addressing) puts it in front of the path
|
|
83
|
-
const hostPath = manifestModule.manifest?.pathFromHost ? new RegExp(manifestModule.manifest.pathFromHost, 'i') : undefined;
|
|
84
|
-
const pathStyle = (u) => {
|
|
85
|
-
const url = new URL(u);
|
|
86
|
-
const label = hostPath?.exec(url.hostname)?.[1];
|
|
87
|
-
return label ? `${url.protocol}//${url.host}/${label}${url.pathname === '/' ? '' : url.pathname}${url.search}` : u;
|
|
88
|
-
};
|
|
180
|
+
const source = JSON.parse(readFileSync(surfacePath, 'utf8'));
|
|
181
|
+
const surface = { ...source, operations: source.operations ?? [] };
|
|
89
182
|
// a lane is routed by host ("Other wires: lanes"): one whose manifest names the hosts it serves is judged on those
|
|
90
183
|
// alone, so a page or door of its pack on another host (a dashboard, a World door) is never read as one of its operations
|
|
91
|
-
const hosts =
|
|
92
|
-
const
|
|
93
|
-
const versionHeader =
|
|
184
|
+
const hosts = manifest?.hosts;
|
|
185
|
+
const claims = hostRules(hosts);
|
|
186
|
+
const versionHeader = manifest?.version?.header.toLowerCase();
|
|
94
187
|
const specDate = /^\d{4}-\d{2}-\d{2}/.exec(String(surface.version))?.[0];
|
|
95
188
|
// the path parameters whose values hold slashes (a git ref, a file path): the surface's own `spanning`, and the
|
|
96
189
|
// manifest's where the spec cannot say
|
|
97
|
-
const spans = [...(surface.spanning ?? []), ...(
|
|
98
|
-
const
|
|
190
|
+
const spans = [...(surface.spanning ?? []), ...(manifest?.spanning ?? [])];
|
|
191
|
+
const effectiveSurface = { ...surface, ...(spans.length ? { spanning: spans } : {}) };
|
|
192
|
+
const routes = compileSurface(effectiveSurface);
|
|
193
|
+
const routed = unitRequestRouter(manifest ?? {}, effectiveSurface, routes);
|
|
194
|
+
const door = worldDoorMatcher(manifest?.doors ?? []);
|
|
195
|
+
const requestAt = (method, path, headers) => {
|
|
196
|
+
const request = new Request(new URL(path, 'http://twin.local'), { method, headers });
|
|
197
|
+
return routed(manifest?.mergeSlashes ? slashesMerged(request) : request);
|
|
198
|
+
};
|
|
199
|
+
const pathOf = (method, path, headers) => requestAt(method, path, headers).url;
|
|
200
|
+
// Command surfaces retain routing ownership; their replies are checked through their native protocol.
|
|
201
|
+
if (['command-reply', 'command-table'].includes(String(surface.format)))
|
|
202
|
+
return { pathOf };
|
|
99
203
|
const resources = new Map(surface.resources.map((r) => [r.name, r]));
|
|
100
|
-
|
|
204
|
+
const checker = {
|
|
205
|
+
serves: (method, path) => Boolean(matchOperation(routes, method, path)),
|
|
101
206
|
pinsOlder: (headers) => {
|
|
102
207
|
if (!versionHeader || !specDate || !headers)
|
|
103
208
|
return false;
|
|
104
209
|
const pinned = Object.entries(headers).find(([k]) => k.toLowerCase() === versionHeader)?.[1];
|
|
105
210
|
return typeof pinned === 'string' && pinned.slice(0, 10) < specDate;
|
|
106
211
|
},
|
|
107
|
-
pathOf: (path) => (/^https?:\/\//.test(path) ? pathStyle(path) : path),
|
|
108
212
|
operationOf: (method, path, headers) => {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
213
|
+
const request = requestAt(method, path, headers);
|
|
214
|
+
const url = new URL(request.url);
|
|
215
|
+
if (/^https?:\/\//.test(path)) {
|
|
216
|
+
const original = new URL(path);
|
|
217
|
+
if (hosts && !claims.takes(hostOf(request), original.pathname))
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
112
220
|
// a World door the manifest declares is that door, never the vendor's operation at a path it would also match
|
|
113
|
-
if (
|
|
221
|
+
if (door(request.method, url.pathname))
|
|
114
222
|
return undefined;
|
|
115
|
-
const
|
|
116
|
-
const hit = matchOperation(routes, method, url.pathname, url.searchParams, h) ?? [...routes.keys()].map((m) => matchOperation(routes, m, url.pathname, url.searchParams, h)).find(Boolean);
|
|
223
|
+
const hit = matchOperation(routes, request.method, url.pathname, url.searchParams, request.headers);
|
|
117
224
|
return hit ? { op: hit.operation, params: hit.params } : undefined;
|
|
118
225
|
},
|
|
119
226
|
failures: (op, body, mediaType, accept, status = op.successStatus ?? 200, exchange) => {
|
|
@@ -131,6 +238,8 @@ async function unitChecker(pkg) {
|
|
|
131
238
|
const primary = op.answers ? resources.get(op.answers.resource) : undefined;
|
|
132
239
|
if (!primary)
|
|
133
240
|
return [];
|
|
241
|
+
if (exchange && /xml/.test(media ?? ''))
|
|
242
|
+
body = xmlMembers(exchange.responseText, primary.fields.map((field) => field.name)) ?? body;
|
|
134
243
|
if (!body || typeof body !== 'object')
|
|
135
244
|
return binary ? [`the answer is not the operation's ${primary.name} resource`] : [];
|
|
136
245
|
// an answer that may be one of several resources (a card or a bank account) is judged as the one it says it is
|
|
@@ -149,7 +258,7 @@ async function unitChecker(pkg) {
|
|
|
149
258
|
const known = new Map([...r.fields.map((f) => [f.name, f]), ...(op.answers.extends ?? []).map((name) => [name, {}])]);
|
|
150
259
|
for (const [k, v] of Object.entries(item)) {
|
|
151
260
|
const f = known.get(k);
|
|
152
|
-
const local =
|
|
261
|
+
const local = manifest?.worldFields?.[r.name]?.[k];
|
|
153
262
|
if (local) {
|
|
154
263
|
if (!k.startsWith('twin_') || !local.why?.trim() || f)
|
|
155
264
|
found.push(`${r.name}.${k}: a World field must have a twin_ name and reason and must not replace a vendor field`);
|
|
@@ -181,8 +290,9 @@ async function unitChecker(pkg) {
|
|
|
181
290
|
return [...new Set(out)];
|
|
182
291
|
},
|
|
183
292
|
};
|
|
293
|
+
return { pathOf, checker };
|
|
184
294
|
}
|
|
185
|
-
/** The SHAPE of a line protocol (
|
|
295
|
+
/** The SHAPE of a line protocol (the derivation, from an RFC's command-reply table): each reply a
|
|
186
296
|
* code the spec allows the command it answers, or one any command may answer. A line after an
|
|
187
297
|
* intermediate reply (DATA's payload, a SASL response) is answered as that command's `then`. */
|
|
188
298
|
export function lineShape(pkg) {
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
import { type ManagedDatabaseWalk } from './walk-database.js';
|
|
1
2
|
import { type TwinStream } from '@volter/world-core/runtime';
|
|
2
3
|
import type { PackFetchOptions } from '@volter/world-core';
|
|
3
4
|
type ApplicationStandIn = NonNullable<PackFetchOptions['application']>;
|
|
4
5
|
/** A pack's fetch over a World's root; a walk whose life names a scenario loads it as the World's (`scenarioPath`), and a
|
|
5
6
|
* life's application answers stand in for the application the vendor asks (`application`, this fetch's alone). */
|
|
6
|
-
export type VendorFetchFactory = (root: string, options?: {
|
|
7
|
+
export type VendorFetchFactory = ((root: string, options?: {
|
|
7
8
|
scenarioPath?: string;
|
|
8
9
|
application?: ApplicationStandIn;
|
|
9
|
-
}) => (request: Request) => Response | Promise<Response
|
|
10
|
+
}) => ((request: Request) => Response | Promise<Response>) & ManagedDatabaseWalk) & {
|
|
11
|
+
managedDatabaseDoor?: string;
|
|
12
|
+
};
|
|
10
13
|
export declare function vendorFetch(twinDir: string, vendor: string, dependenciesFrom?: string): Promise<VendorFetchFactory | undefined>;
|
|
14
|
+
/** The selected pack's same fetch and native backing for in-process and browser transports. */
|
|
15
|
+
export declare function packFetch(directory: string): Promise<VendorFetchFactory | undefined>;
|
|
11
16
|
/** Named views of an answer: what a journey step's `view` asserts against. The kit's own: `jwt:<field>`, the payload of the
|
|
12
17
|
* JSON Web Token at that field of the answer, decoded as the application verifying it reads it (a session token's
|
|
13
18
|
* claims). A pack's own (`answerViews` in its index) beside them. */
|
|
@@ -16,6 +16,10 @@ export async function vendorFetch(twinDir, vendor, dependenciesFrom = twinDir) {
|
|
|
16
16
|
const directory = walkPackDirectory(twinDir, vendor, dependenciesFrom);
|
|
17
17
|
if (!directory)
|
|
18
18
|
return undefined;
|
|
19
|
+
return packFetch(directory);
|
|
20
|
+
}
|
|
21
|
+
/** The selected pack's same fetch and native backing for in-process and browser transports. */
|
|
22
|
+
export async function packFetch(directory) {
|
|
19
23
|
let mod;
|
|
20
24
|
try {
|
|
21
25
|
mod = (await import(__rewriteRelativeImportExtension(join(directory, 'src', 'index.ts'))));
|
|
@@ -31,9 +35,10 @@ export async function vendorFetch(twinDir, vendor, dependenciesFrom = twinDir) {
|
|
|
31
35
|
const descriptor = mod.pack;
|
|
32
36
|
if (!descriptor?.managedDatabase)
|
|
33
37
|
return (root, options = {}) => factory({ root, ...options });
|
|
38
|
+
const managedDatabaseClock = descriptor.managedDatabase.clock;
|
|
34
39
|
// Each walked twin owns a fresh SQL product through the ordinary selected-twin lifecycle.
|
|
35
40
|
// Native SQL uses the descriptor's explicit wall clock; kernel World time is not claimed for it.
|
|
36
|
-
|
|
41
|
+
const backed = (root, options = {}) => {
|
|
37
42
|
const bare = factory({ root, ...options });
|
|
38
43
|
let bound;
|
|
39
44
|
let host;
|
|
@@ -41,11 +46,17 @@ export async function vendorFetch(twinDir, vendor, dependenciesFrom = twinDir) {
|
|
|
41
46
|
const served = (request) => bind().then((fetch) => { return fetch(request); });
|
|
42
47
|
// The twin's SQL product, for what an application sends over its own wire (journey-kit.ts).
|
|
43
48
|
const managedDatabaseUrl = () => bind().then(() => { return host.url; });
|
|
49
|
+
const managedDatabaseConnection = async (...args) => {
|
|
50
|
+
await bind();
|
|
51
|
+
return host.connection(...args);
|
|
52
|
+
};
|
|
44
53
|
// The walk's end retires the selected twin's private engine through its World.
|
|
45
54
|
const stopDatabase = async () => { if (bound === undefined)
|
|
46
55
|
return; await bound.catch(() => undefined); await host?.stop(); };
|
|
47
|
-
return Object.assign(served, bare, { managedDatabaseUrl, stopDatabase });
|
|
56
|
+
return Object.assign(served, bare, { managedDatabaseUrl, managedDatabaseClock, managedDatabaseConnection, stopDatabase });
|
|
48
57
|
};
|
|
58
|
+
backed.managedDatabaseDoor = descriptor.credentialDoor?.path;
|
|
59
|
+
return backed;
|
|
49
60
|
}
|
|
50
61
|
const jwtView = (field) => (body) => {
|
|
51
62
|
const token = body?.[field];
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type TwinPack } from '@volter/world-core/runtime';
|
|
2
|
+
import type { ReplayStep } from './replay.js';
|
|
2
3
|
export type TwinDatabase = {
|
|
3
4
|
url: string;
|
|
5
|
+
connection: (method: string, path: string, status: number, body: unknown) => Promise<ReplayStep['connection']>;
|
|
4
6
|
stop: () => Promise<void>;
|
|
5
7
|
};
|
|
8
|
+
export type ManagedDatabaseWalk = {
|
|
9
|
+
managedDatabaseUrl?: () => Promise<string>;
|
|
10
|
+
managedDatabaseClock?: 'wall';
|
|
11
|
+
managedDatabaseConnection?: TwinDatabase['connection'];
|
|
12
|
+
stopDatabase?: () => Promise<void>;
|
|
13
|
+
};
|
|
6
14
|
export declare function startTwinDatabase(pack: string, descriptor: TwinPack): Promise<TwinDatabase>;
|