@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
|
@@ -43,8 +43,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
43
43
|
// read for. `--sweep` prints, without changing done, every actor field (creator, user, *_by, ...) no `equals` pins and every
|
|
44
44
|
// list no expect closes: advice for fixing a ruling as a class (its hits include lists a pinned text already fixes).
|
|
45
45
|
//
|
|
46
|
-
// Usage: twin-standard examples <vendor> [--sweep] (from the pack repository's root
|
|
47
|
-
// bun scripts/vendor-examples.ts <vendor> runs the same)
|
|
46
|
+
// Usage: twin-standard examples <vendor> [--sweep] (from the pack repository's root)
|
|
48
47
|
import { parseExactJson, exactInteger, isExactIntegerValue } from '@volter/world-core/exact-json';
|
|
49
48
|
import { hasOwnSurface } from "../lanes.js";
|
|
50
49
|
import { archiveSource } from "../archive-source.js";
|
|
@@ -56,7 +55,7 @@ import { basename, join } from 'node:path';
|
|
|
56
55
|
import { gunzipSync } from 'node:zlib';
|
|
57
56
|
import { inMemoryWorld, walk, usePacks } from "../walk/behavior-journey.js";
|
|
58
57
|
import { vendorFetch, vendorStream } from "../walk/vendor-fetch.js";
|
|
59
|
-
import { at as selectorAt, captureAt, expandFills, isResp, isGit,
|
|
58
|
+
import { at as selectorAt, captureAt, expandFills, isResp, isGit, isSocket, isWait } from "../walk/journey-kit.js";
|
|
60
59
|
import { lanesOf, shapeChecker } from "../walk/shape.js";
|
|
61
60
|
import { applySpecPatch, discoveryToOpenAPI, mergeDocuments, readSpecDocuments, slug } from "../spec.js";
|
|
62
61
|
import { packsRoot } from "./packs-root.js";
|
|
@@ -70,6 +69,7 @@ let specSchemas = {};
|
|
|
70
69
|
const schemasByOperation = {};
|
|
71
70
|
/** Each operation's request-body schema and its JSON answers' schemas by status, as the served spec gives them. */
|
|
72
71
|
const operationSchemas = {};
|
|
72
|
+
const operationKey = (operation, lane) => lane ? `${lane}:${operation}` : operation;
|
|
73
73
|
/** A page's placeholder id in an answer-map key is the id sent at the corresponding published request position.
|
|
74
74
|
* Only declared placeholders bind, and an ambiguous repeated placeholder cannot stand for a guessed map key. */
|
|
75
75
|
function answerKeys(output, published, sent, placeholder, rawBindings = false) {
|
|
@@ -94,7 +94,7 @@ function answerKeys(output, published, sent, placeholder, rawBindings = false) {
|
|
|
94
94
|
: value;
|
|
95
95
|
return mapped(output);
|
|
96
96
|
}
|
|
97
|
-
/** The schemas the twin serves: the vendor's, with the pack's spec patches (spec/patches.json) applied as
|
|
97
|
+
/** The schemas the twin serves: the vendor's, with the pack's spec patches (spec/patches.json) applied as the derivation
|
|
98
98
|
* applies them. A sample is checked against what the twin is derived from; the published examples themselves stay the
|
|
99
99
|
* vendor's bytes. */
|
|
100
100
|
function served(pkg, doc) {
|
|
@@ -106,7 +106,7 @@ function served(pkg, doc) {
|
|
|
106
106
|
applySpecPatch(patched, p);
|
|
107
107
|
return patched;
|
|
108
108
|
}
|
|
109
|
-
|
|
109
|
+
const restXmlService = (model) => Object.values(model.shapes).some((shape) => shape.type === 'service' && shape.traits?.['aws.protocols#restXml'] !== undefined);
|
|
110
110
|
/** A translated placeholder-key map is a complete map even when its surrounding documented request is partial. */
|
|
111
111
|
function requestMapKeysHold(published, translated, sent, placeholder) {
|
|
112
112
|
if (!placeholder || !published || typeof published !== 'object')
|
|
@@ -124,11 +124,11 @@ function requestMapKeysHold(published, translated, sent, placeholder) {
|
|
|
124
124
|
});
|
|
125
125
|
}
|
|
126
126
|
const PRELUDE = { Boolean: 'boolean', PrimitiveBoolean: 'boolean', Integer: 'integer', PrimitiveInteger: 'integer', Long: 'long', PrimitiveLong: 'long', Float: 'float', Double: 'double' };
|
|
127
|
-
const scalarType = (target) =>
|
|
128
|
-
const scalarOf = (target, v) => {
|
|
127
|
+
const scalarType = (model, target) => model.shapes[target]?.type ?? PRELUDE[target.split('#')[1] ?? ''] ?? 'string';
|
|
128
|
+
const scalarOf = (model, target, v) => {
|
|
129
129
|
if (typeof v !== 'string')
|
|
130
130
|
return v;
|
|
131
|
-
const t = scalarType(target);
|
|
131
|
+
const t = scalarType(model, target);
|
|
132
132
|
if (t === 'boolean')
|
|
133
133
|
return v === 'true';
|
|
134
134
|
if (['integer', 'long', 'short', 'byte'].includes(t))
|
|
@@ -138,10 +138,10 @@ const scalarOf = (target, v) => {
|
|
|
138
138
|
const arrayOf = (v) => (v === undefined || v === '' ? [] : Array.isArray(v) ? v : [v]);
|
|
139
139
|
/** An answer's XML members (xml-view) read by the model's shape: each member under its xmlName, a flattened list the
|
|
140
140
|
* repeated element, a wrapped list its container's member elements, scalars as their shape's type. */
|
|
141
|
-
function typedXml(target, v) {
|
|
142
|
-
const shape =
|
|
141
|
+
function typedXml(model, target, v) {
|
|
142
|
+
const shape = model.shapes[target];
|
|
143
143
|
if (!shape || shape.type !== 'structure')
|
|
144
|
-
return scalarOf(target, v);
|
|
144
|
+
return scalarOf(model, target, v);
|
|
145
145
|
if (!v || typeof v !== 'object')
|
|
146
146
|
return v === '' ? {} : v;
|
|
147
147
|
const obj = v;
|
|
@@ -150,38 +150,38 @@ function typedXml(target, v) {
|
|
|
150
150
|
if (Object.keys(m.traits ?? {}).some((t) => /^smithy\.api#http(Header|PrefixHeaders|ResponseCode)$/.test(t)))
|
|
151
151
|
continue;
|
|
152
152
|
const el = String(m.traits?.['smithy.api#xmlName'] ?? name);
|
|
153
|
-
const ts =
|
|
153
|
+
const ts = model.shapes[m.target];
|
|
154
154
|
const raw = obj[el];
|
|
155
155
|
if (ts?.type === 'list') {
|
|
156
156
|
if (m.traits?.['smithy.api#xmlFlattened'] !== undefined) {
|
|
157
157
|
if (raw !== undefined)
|
|
158
|
-
out[name] = arrayOf(raw).map((i) => typedXml(ts.member.target, i));
|
|
158
|
+
out[name] = arrayOf(raw).map((i) => typedXml(model, ts.member.target, i));
|
|
159
159
|
continue;
|
|
160
160
|
}
|
|
161
161
|
if (raw === undefined)
|
|
162
162
|
continue;
|
|
163
163
|
const memberEl = String(ts.member.traits?.['smithy.api#xmlName'] ?? 'member');
|
|
164
|
-
out[name] = arrayOf(raw === '' ? undefined : raw[memberEl]).map((i) => typedXml(ts.member.target, i));
|
|
164
|
+
out[name] = arrayOf(raw === '' ? undefined : raw[memberEl]).map((i) => typedXml(model, ts.member.target, i));
|
|
165
165
|
}
|
|
166
166
|
else if (raw !== undefined)
|
|
167
|
-
out[name] = typedXml(m.target, raw);
|
|
167
|
+
out[name] = typedXml(model, m.target, raw);
|
|
168
168
|
}
|
|
169
169
|
return out;
|
|
170
170
|
}
|
|
171
171
|
/** A restXml operation's success answer as its output structure: the body's members by shape (or its payload
|
|
172
172
|
* member's), and the members its headers carry. */
|
|
173
|
-
function restXmlAnswer(opId, body, headers = {}) {
|
|
174
|
-
const out =
|
|
173
|
+
function restXmlAnswer({ id: opId, model }, body, headers = {}) {
|
|
174
|
+
const out = model.shapes[model.shapes[opId]?.output?.target ?? ''];
|
|
175
175
|
if (!out)
|
|
176
176
|
return body;
|
|
177
177
|
const members = Object.entries((out.members ?? {}));
|
|
178
178
|
const payload = members.find(([, m]) => m.traits?.['smithy.api#httpPayload'] !== undefined);
|
|
179
|
-
const answer = (payload ? { [payload[0]]: typedXml(payload[1].target, body) } : typedXml(
|
|
179
|
+
const answer = (payload ? { [payload[0]]: typedXml(model, payload[1].target, body) } : typedXml(model, model.shapes[opId].output.target, body));
|
|
180
180
|
const lower = Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]));
|
|
181
181
|
for (const [name, m] of members) {
|
|
182
182
|
const h = m.traits?.['smithy.api#httpHeader'];
|
|
183
183
|
if (h !== undefined && lower[String(h).toLowerCase()] !== undefined)
|
|
184
|
-
answer[name] = scalarOf(m.target, lower[String(h).toLowerCase()]);
|
|
184
|
+
answer[name] = scalarOf(model, m.target, lower[String(h).toLowerCase()]);
|
|
185
185
|
// a map bound to a header prefix (S3's Metadata, `x-amz-meta-*`): every header under it, by the rest of its name
|
|
186
186
|
const prefix = m.traits?.['smithy.api#httpPrefixHeaders'];
|
|
187
187
|
if (prefix !== undefined)
|
|
@@ -191,8 +191,8 @@ function restXmlAnswer(opId, body, headers = {}) {
|
|
|
191
191
|
}
|
|
192
192
|
/** Where a restXml example's input is not what the step sends: each member the example gives, at its binding (a path
|
|
193
193
|
* label, a query key, a header; a payload or body member is the step's body and is not compared). */
|
|
194
|
-
function restXmlInputProblems(opId, input, path, headers, placeholder, rawPath) {
|
|
195
|
-
const shape =
|
|
194
|
+
function restXmlInputProblems({ id: opId, model }, input, path, headers, placeholder, rawPath) {
|
|
195
|
+
const shape = model.shapes[model.shapes[opId]?.input?.target ?? ''];
|
|
196
196
|
const url = new URL(path, 'http://twin.local');
|
|
197
197
|
// the step as written, its captures unfilled: a value it takes from a capture is the World's own id (an S3 upload's
|
|
198
198
|
// UploadId), which stands for the example's literal as a spec's ids stand for the World's (architecture, "Published examples")
|
|
@@ -236,11 +236,13 @@ function lenientJson(text) {
|
|
|
236
236
|
* response's `example`/`examples` (a `$ref` to `components.examples` followed), a Swagger 2 response's `examples`, Stripe's
|
|
237
237
|
* fixtures (one sample per resource, beside its spec), and OpenAI's `x-oaiMeta.examples` (its `response` is JSON text; one that is not JSON, a stream or an abridged answer, is
|
|
238
238
|
* kept as text and compared with the published framing and payload). */
|
|
239
|
-
function published(pkg) {
|
|
239
|
+
function published(pkg, lane) {
|
|
240
240
|
// Both the vendor API and its lanes publish examples; a root spec must not hide a lane. A lane's example names its
|
|
241
241
|
// lane, the operation being the lane's own (its owner is `<lane>:<id>` in the vendor's owners)
|
|
242
|
-
const
|
|
243
|
-
|
|
242
|
+
const smithy = ['smithy.json.gz', 'smithy.json'].map((name) => join(pkg, 'spec', name)).find(existsSync);
|
|
243
|
+
const model = smithy ? parseExactJson((smithy.endsWith('.gz') ? gunzipSync(readFileSync(smithy)) : readFileSync(smithy)).toString('utf8')) : undefined;
|
|
244
|
+
const own = [...(hasOwnSurface(pkg) ? fromSpec(pkg, lane, model) : []), ...fromDocs(pkg, lane, model)].map((e) => ({ ...e, lane }));
|
|
245
|
+
return [...own, ...lanesOf(pkg).flatMap((dir) => published(dir, lane ? `${lane}:${basename(dir)}` : basename(dir)))];
|
|
244
246
|
}
|
|
245
247
|
/** The examples a vendor's documentation pages publish beside its spec (a schedule page's expressions, each with what
|
|
246
248
|
* it means), vendored in `spec/doc-examples.json` as `[{title, source, operation, request?, input?, output?, means, status?}]`:
|
|
@@ -251,7 +253,7 @@ function published(pkg) {
|
|
|
251
253
|
* reference writes every property, and every scalar as a string, `"verified": "true"` beside a schema saying boolean)
|
|
252
254
|
* shows what the answer can hold, not one request's answer: its example is a sample against the served schema, and a
|
|
253
255
|
* string where the schema says number, integer or boolean is read as that type, in the answer and the request body. */
|
|
254
|
-
function fromDocs(pkg) {
|
|
256
|
+
function fromDocs(pkg, lane, model) {
|
|
255
257
|
const file = join(pkg, 'spec', 'doc-examples.json');
|
|
256
258
|
if (!existsSync(file))
|
|
257
259
|
return [];
|
|
@@ -294,12 +296,11 @@ function fromDocs(pkg) {
|
|
|
294
296
|
for (const d of docs)
|
|
295
297
|
if (d.placeholders && (!d.placeholders.names?.length || !d.placeholders.quote?.trim()))
|
|
296
298
|
throw new Error(`${file}: "${d.title}" names placeholders without the page's words for them (placeholders.quote)`);
|
|
297
|
-
const smithy = ['smithy.json.gz', 'smithy.json'].some((f) => existsSync(join(pkg, 'spec', f)));
|
|
298
299
|
// a page's example is a fragment (an expression, not the request that carries it): the body holds it
|
|
299
300
|
return docs.map((d) => {
|
|
300
301
|
if (!d.rendered)
|
|
301
302
|
return fromPage(d, d.output, d.input);
|
|
302
|
-
const schemas = operationSchemas[d.operation];
|
|
303
|
+
const schemas = operationSchemas[operationKey(d.operation, lane)];
|
|
303
304
|
const schema = schemas?.responses[String(d.status ?? 200)];
|
|
304
305
|
if (!schema)
|
|
305
306
|
throw new Error(`${file}: "${d.title}" is rendered from a schema the served spec does not give (${d.operation}, ${d.status ?? 200})`);
|
|
@@ -311,7 +312,14 @@ function fromDocs(pkg) {
|
|
|
311
312
|
function fromPage(d, output, input) {
|
|
312
313
|
const named = d.placeholders?.names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
|
313
314
|
const placeholder = named ? new RegExp(`${placeholderIds.source}|^(?:${named})$`, placeholderIds.flags) : placeholderIds;
|
|
314
|
-
|
|
315
|
+
let restXml;
|
|
316
|
+
if (model && restXmlService(model)) {
|
|
317
|
+
const id = Object.keys(model.shapes).find((key) => key.endsWith(`#${d.operation}`));
|
|
318
|
+
if (!id)
|
|
319
|
+
throw new Error(`${file}: "${d.title}" names no operation in its XML service (${d.operation})`);
|
|
320
|
+
restXml = { id, model };
|
|
321
|
+
}
|
|
322
|
+
return { names: d.placeholders?.names, ...(d.wire ? { wire: d.wire } : {}), id: `${d.operation}#doc ${d.title}`, operation: d.operation, output, ...(d.status ? { status: d.status } : {}), ...(input !== undefined ? { input, partial: true, placeholder } : {}), ...(model ? (restXml ? { restXml } : { target: d.operation }) : {}), ...(d.request ? { request: d.request } : {}), ...(authorizationAbsent.has(d) ? { authorizationAbsent: true } : {}) };
|
|
315
323
|
}
|
|
316
324
|
}
|
|
317
325
|
/** A rendered object the page does not fill: any object stands where it is. */
|
|
@@ -400,12 +408,25 @@ function decodedExample(media, value) {
|
|
|
400
408
|
return value;
|
|
401
409
|
}
|
|
402
410
|
}
|
|
403
|
-
function fromSpec(pkg) {
|
|
411
|
+
function fromSpec(pkg, lane, model) {
|
|
404
412
|
const rfc = existsSync(join(pkg, 'spec')) ? readdirSync(join(pkg, 'spec')).find((f) => /^rfc\d+\.txt$/.test(f)) : undefined;
|
|
405
413
|
if (rfc)
|
|
406
414
|
return scenarios(readFileSync(join(pkg, 'spec', rfc), 'utf8'));
|
|
407
|
-
|
|
408
|
-
|
|
415
|
+
const out = [];
|
|
416
|
+
if (model) {
|
|
417
|
+
const restXml = restXmlService(model);
|
|
418
|
+
for (const [id, shape] of Object.entries(model.shapes)) {
|
|
419
|
+
const examples = shape.traits?.['smithy.api#examples'];
|
|
420
|
+
const operation = id.split('#')[1];
|
|
421
|
+
// an awsJson operation is named by its x-amz-target header, `<service>.<operation>`; its body is the input. A
|
|
422
|
+
// restXml operation (S3's) is named by its method and path, and its input is where each member's binding puts it
|
|
423
|
+
for (const e of examples ?? [])
|
|
424
|
+
out.push({ id: `${operation}#${e.title}`, operation, output: e.error ? e.error.content : e.output, input: e.input ?? {}, ...(restXml ? { restXml: { id, model } } : { target: operation }) });
|
|
425
|
+
}
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
428
|
+
// a spec vendored as published or gzipped, as the derivation reads it
|
|
429
|
+
const spec = ['openapi.json.gz', 'openapi.json', 'openapi.yaml.gz', 'openapi.yaml', 'discovery.json.gz', 'discovery.json'].map((f) => join(pkg, 'spec', f)).find((p) => existsSync(p));
|
|
409
430
|
// a Protocol Buffers service publishes no examples in its .proto files: its examples are its documentation's alone
|
|
410
431
|
if (!spec && existsSync(join(pkg, 'spec', 'proto')))
|
|
411
432
|
return [];
|
|
@@ -420,23 +441,7 @@ function fromSpec(pkg) {
|
|
|
420
441
|
if (!spec && !documents)
|
|
421
442
|
throw new Error(`${pkg}: no published-example reader for this spec yet`);
|
|
422
443
|
const text = spec ? (spec.endsWith('.gz') ? gunzipSync(readFileSync(spec)) : readFileSync(spec)).toString('utf8') : '';
|
|
423
|
-
|
|
424
|
-
if (spec && /smithy\.json(\.gz)?$/.test(spec)) {
|
|
425
|
-
const model = parseExactJson(text);
|
|
426
|
-
const restXml = Object.values(model.shapes).some((sh) => sh.type === 'service' && sh.traits?.['aws.protocols#restXml'] !== undefined);
|
|
427
|
-
if (restXml)
|
|
428
|
-
xmlModel = model;
|
|
429
|
-
for (const [id, shape] of Object.entries(model.shapes)) {
|
|
430
|
-
const examples = shape.traits?.['smithy.api#examples'];
|
|
431
|
-
const operation = id.split('#')[1];
|
|
432
|
-
// an awsJson operation is named by its x-amz-target header, `<service>.<operation>`; its body is the input. A
|
|
433
|
-
// restXml operation (S3's) is named by its method and path, and its input is where each member's binding puts it
|
|
434
|
-
for (const e of examples ?? [])
|
|
435
|
-
out.push({ id: `${operation}#${e.title}`, operation, output: e.error ? e.error.content : e.output, input: e.input ?? {}, ...(restXml ? { restXml: id } : { target: operation }) });
|
|
436
|
-
}
|
|
437
|
-
return out;
|
|
438
|
-
}
|
|
439
|
-
// a Google API Discovery document publishes no examples; it is read as the OpenAPI document derive-pack reads it as,
|
|
444
|
+
// a Google API Discovery document publishes no examples; it is read as the OpenAPI document the derivation reads it as,
|
|
440
445
|
// its patches applied to the Discovery document first (spec-ir-discovery.ts)
|
|
441
446
|
const discovery = spec !== undefined && /discovery\.json(\.gz)?$/.test(spec);
|
|
442
447
|
const docs = (documents ? Object.values(documents.documents) : [discovery ? discoveryToOpenAPI(parseExactJson(text)) : /\.yaml(\.gz)?$/.test(spec) ? parseExactJson(JSON.stringify(parseYaml(text, { intAsBigInt: true }), (_key, value) => typeof value === 'bigint' ? exactInteger(String(value)) : value)) : parseExactJson(text)]);
|
|
@@ -454,8 +459,9 @@ function fromSpec(pkg) {
|
|
|
454
459
|
continue;
|
|
455
460
|
// Swagger 2: the request is the `in: body` parameter's schema, an answer its response's `schema`
|
|
456
461
|
const bodyParam = (op.parameters ?? []).find((x) => x.in === 'body');
|
|
457
|
-
|
|
458
|
-
|
|
462
|
+
const key = operationKey(idOf(op, method, path), lane);
|
|
463
|
+
schemasByOperation[key] = specSchemas;
|
|
464
|
+
operationSchemas[key] = {
|
|
459
465
|
request: json(op.requestBody?.content) ?? bodyParam?.schema,
|
|
460
466
|
responses: Object.fromEntries(Object.entries(op.responses ?? {}).flatMap(([code, r]) => { const sch = json(r.content) ?? r.schema; return sch ? [[code, sch]] : []; })),
|
|
461
467
|
};
|
|
@@ -978,17 +984,17 @@ function outputSchemaMismatches(schema, value, prefix, depth = 0) {
|
|
|
978
984
|
* recorded with the per-example classification. Request echoes and generated output are absent. */
|
|
979
985
|
const storedCollectionFields = {
|
|
980
986
|
'ai-gateway': { listModels: ['data'] },
|
|
981
|
-
aws: { ListBuckets: ['Buckets'], ListMultipartUploads: ['Uploads'], ListObjects: ['Contents', 'CommonPrefixes'], ListObjectsV2: ['Contents', 'CommonPrefixes'] },
|
|
982
|
-
cloudflare: { ListBuckets: ['Buckets'], ListMultipartUploads: ['Uploads'], ListObjects: ['Contents', 'CommonPrefixes'], ListObjectsV2: ['Contents', 'CommonPrefixes'] },
|
|
987
|
+
aws: { 's3:ListBuckets': ['Buckets'], 's3:ListMultipartUploads': ['Uploads'], 's3:ListObjects': ['Contents', 'CommonPrefixes'], 's3:ListObjectsV2': ['Contents', 'CommonPrefixes'] },
|
|
988
|
+
cloudflare: { 'r2:ListBuckets': ['Buckets'], 'r2:ListMultipartUploads': ['Uploads'], 'r2:ListObjects': ['Contents', 'CommonPrefixes'], 'r2:ListObjectsV2': ['Contents', 'CommonPrefixes'] },
|
|
983
989
|
discord: { oauth2_token: ['guild.roles'], get_my_oauth2_authorization: ['scopes'] },
|
|
984
990
|
github: { 'repos/get-branch': ['commit.parents'], 'repos/get-commit': ['parents', 'files'], 'repos/get-content': ['', 'entries'], 'repos/compare-commits': ['commits', 'files'], 'git/get-tree': ['tree'], 'git/list-matching-refs': [''], 'repos/get-repo-rulesets': [''] },
|
|
985
|
-
hackernews: { getItem: ['kids'], getUser: ['submitted'], getUpdates: ['items', 'profiles'], getTopStories: [''], getNewStories: [''], getBestStories: [''], getAskStories: [''], getShowStories: [''], getJobStories: [''] },
|
|
991
|
+
hackernews: { 'api:getItem': ['kids'], 'api:getUser': ['submitted'], 'api:getUpdates': ['items', 'profiles'], 'api:getTopStories': [''], 'api:getNewStories': [''], 'api:getBestStories': [''], 'api:getAskStories': [''], 'api:getShowStories': [''], 'api:getJobStories': [''] },
|
|
986
992
|
linkedin: { findOrganizationAclsByRoleAssignee: ['elements'] },
|
|
987
993
|
'npm-registry': { 'libnpmsearch.searchStream': ['objects', 'objects.[].package.maintainers'] },
|
|
988
994
|
openrouter: { getCurrentKey: ['data.allowed_data_regions'], getModels: ['data'] },
|
|
989
995
|
redis: { SCAN: ['[].1'] },
|
|
990
996
|
resend: { 'domains/get': ['records'], 'emails/list-receiving': ['data', 'data.[].attachments'] },
|
|
991
|
-
sentry: { listOrganizations: ['', '[].features'] },
|
|
997
|
+
sentry: { 'api:listOrganizations': ['', '[].features'] },
|
|
992
998
|
slack: { conversations_rename: ['channel.previous_names'], conversations_invite: ['channel.previous_names'], conversations_members: ['members'], conversations_list: ['channels', 'channels.[].previous_names'], users_conversations: ['channels', 'channels.[].previous_names'], conversations_info: ['channel.previous_names'], conversations_replies: ['messages'], conversations_history: ['messages'], usergroups_list: ['usergroups'] },
|
|
993
999
|
tinybird: { datasources: ['datasources'], jobs: ['jobs'], pipes: ['pipes'], tokens: ['tokens'], token_get: ['scopes'], token_refresh: ['scopes'], pipe_set_endpoint: ['nodes'] },
|
|
994
1000
|
};
|
|
@@ -1104,7 +1110,7 @@ if (!vendor || vendor.startsWith('-')) {
|
|
|
1104
1110
|
const PKG = join(PACKS, vendor);
|
|
1105
1111
|
const file = join(PKG, 'journeys', 'vendor-examples.json');
|
|
1106
1112
|
await loadPlaceholders(PKG);
|
|
1107
|
-
const examples = published(PKG);
|
|
1113
|
+
const examples = published(PKG, undefined);
|
|
1108
1114
|
const byId = new Map(examples.map((e) => [e.id, e]));
|
|
1109
1115
|
if (byId.size !== examples.length)
|
|
1110
1116
|
throw new Error(`${vendor}: ${examples.length - byId.size} published examples share an id`);
|
|
@@ -1121,11 +1127,13 @@ const shape = await shapeChecker(PKG);
|
|
|
1121
1127
|
for (const e of examples) {
|
|
1122
1128
|
if (e.patchRemoved || !e.request || !shape)
|
|
1123
1129
|
continue;
|
|
1124
|
-
const
|
|
1130
|
+
const hit = shape.operationOf(e.request.method, new URL(e.request.url, `http://${journey.host ?? 'twin.local'}`).href, e.request.headers);
|
|
1131
|
+
const routed = hit ? operationKey(hit.op.id, hit.lane) : undefined;
|
|
1125
1132
|
if (e.operation === 'gap' && routed === undefined)
|
|
1126
1133
|
continue;
|
|
1127
|
-
|
|
1128
|
-
|
|
1134
|
+
const expected = operationKey(e.operation, e.lane);
|
|
1135
|
+
if (routed !== expected)
|
|
1136
|
+
throw new Error(`spec/doc-examples.json: "${e.id}" sends ${e.request.method} ${e.request.url}, which the spec routes to ${routed ?? 'no operation'}, not ${expected}`);
|
|
1129
1137
|
}
|
|
1130
1138
|
const sweeping = process.argv.includes('--sweep');
|
|
1131
1139
|
const sweepHits = [];
|
|
@@ -1258,18 +1266,20 @@ await walk(journey, world.root(), failures, {
|
|
|
1258
1266
|
after: (step, _path, answer, stepFailures) => {
|
|
1259
1267
|
if (isGit(step) || isWait(step) || isSocket(step))
|
|
1260
1268
|
return;
|
|
1261
|
-
|
|
1269
|
+
const request = answer.request;
|
|
1270
|
+
const hit = request ? shape?.operationOf(request.method, request.url, request.headers) : undefined;
|
|
1271
|
+
const operation = hit?.op;
|
|
1272
|
+
if (request) {
|
|
1262
1273
|
const st = step;
|
|
1263
|
-
const method =
|
|
1274
|
+
const method = request.method;
|
|
1264
1275
|
// a read by what the operation does (an RPC wire POSTs its reads), else by its verb
|
|
1265
|
-
const
|
|
1266
|
-
const opClass = op?.class;
|
|
1276
|
+
const opClass = operation?.class;
|
|
1267
1277
|
const isRead = opClass ? ['retrieve', 'list', 'computed'].includes(opClass) : method === 'GET';
|
|
1268
1278
|
if (isRead)
|
|
1269
|
-
reads.push({ path: bare(
|
|
1279
|
+
reads.push({ path: bare(request.url), body: JSON.stringify(answer.body ?? '') });
|
|
1270
1280
|
else if (st.example && answer.status < 300 && answer.body && typeof answer.body === 'object' && !Array.isArray(answer.body) && Object.keys(answer.body).length <= 3) {
|
|
1271
1281
|
const body = answer.body;
|
|
1272
|
-
const segs = bare(
|
|
1282
|
+
const segs = bare(request.url).split('/');
|
|
1273
1283
|
// an action on a subject (`/emails/{id}/cancel`) touches the subject; a create touches the new id under the collection;
|
|
1274
1284
|
// a write whose subject is neither minted nor in its path (an RPC call naming it in its body) is left to the judge
|
|
1275
1285
|
const minted = typeof body.id === 'string' ? body.id : '';
|
|
@@ -1277,8 +1287,7 @@ await walk(journey, world.root(), failures, {
|
|
|
1277
1287
|
if (minted || idInPath) {
|
|
1278
1288
|
const base = minted && !segs.includes(minted) ? `${segs.join('/')}/${minted}` : /^[a-z_-]+$/.test(segs.at(-1) ?? '') && segs.length > 2 ? segs.slice(0, -1).join('/') : segs.join('/');
|
|
1279
1289
|
// only an effect the vendor gives a door to read: a GET of the subject or of its collection (a moderation has neither)
|
|
1280
|
-
const
|
|
1281
|
-
const gets = (at) => shape?.operationOf('GET', at, hdrs)?.op.method.toUpperCase() === 'GET';
|
|
1290
|
+
const gets = (at) => shape?.operationOf('GET', new URL(at, request.url).href, request.headers)?.op.method.toUpperCase() === 'GET';
|
|
1282
1291
|
const door = !shape || gets(base) || gets(base.split('/').slice(0, -1).join('/'));
|
|
1283
1292
|
if (door)
|
|
1284
1293
|
thinWrites.push({ step: st.name, base, id: base.split('/').at(-1) ?? '', from: reads.length, deletes: method === 'DELETE' || opClass === 'delete' });
|
|
@@ -1308,20 +1317,20 @@ await walk(journey, world.root(), failures, {
|
|
|
1308
1317
|
unknown.push(s.example);
|
|
1309
1318
|
return;
|
|
1310
1319
|
}
|
|
1311
|
-
|
|
1320
|
+
const key = operationKey(ex.operation, ex.lane);
|
|
1321
|
+
specSchemas = schemasByOperation[key] ?? {};
|
|
1312
1322
|
replayed.add(s.example);
|
|
1313
|
-
if (ex.operation === 'gap' && ex.request) {
|
|
1314
|
-
|
|
1315
|
-
if (s.method !== ex.request.method || sent.href !== ex.request.url)
|
|
1323
|
+
if (ex.operation === 'gap' && ex.request && request) {
|
|
1324
|
+
if (request.method !== ex.request.method || request.url !== ex.request.url)
|
|
1316
1325
|
failures.push({ step: s.name, beat: 'EXAMPLE', detail: 'the unknown request differs from its recorded method or URL' });
|
|
1317
1326
|
}
|
|
1318
|
-
if (ex.authorizationAbsent &&
|
|
1327
|
+
if (ex.authorizationAbsent && request && new Headers(request.headers).has('authorization'))
|
|
1319
1328
|
failures.push({ step: s.name, beat: 'EXAMPLE', detail: 'the recording has no Authorization header, but the replay sends one' });
|
|
1320
1329
|
// the step calls the example's operation (the spec's, as the walk's SHAPE check reads the request)
|
|
1321
|
-
if (
|
|
1322
|
-
const called =
|
|
1323
|
-
if (called && called !==
|
|
1324
|
-
failures.push({ step: s.name, beat: 'EXAMPLE', detail: `calls ${called}, not the example's operation ${
|
|
1330
|
+
if (request && shape) {
|
|
1331
|
+
const called = hit ? operationKey(hit.op.id, hit.lane) : undefined;
|
|
1332
|
+
if (called && called !== key)
|
|
1333
|
+
failures.push({ step: s.name, beat: 'EXAMPLE', detail: `calls ${called}, not the example's operation ${key}` });
|
|
1325
1334
|
}
|
|
1326
1335
|
// the example is sent as published: its input as the body, to its operation
|
|
1327
1336
|
// a published field the step leaves out (`unsent`: a rendered value naming an object the World does not hold) must
|
|
@@ -1329,26 +1338,28 @@ await walk(journey, world.root(), failures, {
|
|
|
1329
1338
|
for (const [path, why] of Object.entries(s.unsent ?? {})) {
|
|
1330
1339
|
if (!why)
|
|
1331
1340
|
failures.push({ step: s.name, beat: 'EXAMPLE', detail: `unsent ${path} gives no reason` });
|
|
1332
|
-
if (requiredAt(operationSchemas[
|
|
1341
|
+
if (requiredAt(operationSchemas[key]?.request, path))
|
|
1333
1342
|
failures.push({ step: s.name, beat: 'EXAMPLE', detail: `unsent ${path} is required by the operation's request schema` });
|
|
1334
1343
|
}
|
|
1335
1344
|
const publishedBody = exampleBody({ ...s, body: ex.input, form: undefined, bodyBase64: undefined }, ex.input);
|
|
1336
1345
|
const inputType = Object.entries(ex.request?.headers ?? {}).find(([k]) => k.toLowerCase() === 'content-type')?.[1]
|
|
1337
|
-
??
|
|
1346
|
+
?? (request ? new Headers(request.headers).get('content-type') ?? undefined : undefined);
|
|
1338
1347
|
const publishedInput = s.unsent && publishedBody !== undefined ? withoutPaths(publishedBody, Object.keys(s.unsent)) : publishedBody;
|
|
1339
1348
|
// the body the step sends, every check below reading it alike: no body (a GET) is the empty input a page publishes for it
|
|
1340
1349
|
const body = isResp(s) ? s.resp : exampleBody(s, publishedInput) ?? {};
|
|
1341
1350
|
// A placeholder used as a request-map key is bound by its scalar request position, just as an answer key is.
|
|
1342
1351
|
const input = answerKeys(publishedInput, publishedInput, body, ex.placeholder, true);
|
|
1343
|
-
if (ex.restXml)
|
|
1344
|
-
|
|
1345
|
-
|
|
1352
|
+
if (ex.restXml) {
|
|
1353
|
+
if (request)
|
|
1354
|
+
for (const p of restXmlInputProblems(ex.restXml, (input ?? {}), shape?.pathOf(request.method, request.url, request.headers) ?? request.url, request.headers, placeholderIds, s.path))
|
|
1355
|
+
failures.push({ step: s.name, beat: 'EXAMPLE', detail: `the request is not the example's published input: ${p}` });
|
|
1356
|
+
}
|
|
1346
1357
|
else if (input !== undefined && (!requestMapKeysHold(publishedInput, input, body, ex.placeholder) || (ex.partial ? !holds(body, input, ex.placeholder, undefined, ex.names, false, inputType) : !same(body, input, ex.placeholder ?? /(?!)/, undefined, ex.names, false, inputType))))
|
|
1347
1358
|
failures.push({ step: s.name, beat: 'EXAMPLE', detail: typeof input === 'string' ? comparedText(body, input, ex.placeholder ?? /(?!)/, undefined, ex.names, inputType)?.problem ?? "the body is not the example's published input" : "the body is not the example's published input" });
|
|
1348
1359
|
// a scenario's turn is sent as the RFC writes it: its client lines (none for the greeting, a connect)
|
|
1349
1360
|
if (ex.lines && JSON.stringify([s.send ?? []].flat()) !== JSON.stringify(ex.lines))
|
|
1350
1361
|
failures.push({ step: s.name, beat: 'EXAMPLE', detail: `sends ${JSON.stringify(s.send ?? null)}, not the scenario's ${JSON.stringify(ex.lines)}` });
|
|
1351
|
-
const target =
|
|
1362
|
+
const target = request ? new Headers(request.headers).get('x-amz-target') : undefined;
|
|
1352
1363
|
if (ex.target && !target?.endsWith(`.${ex.target}`))
|
|
1353
1364
|
failures.push({ step: s.name, beat: 'EXAMPLE', detail: `x-amz-target is ${target}, not the example's operation ${ex.target}` });
|
|
1354
1365
|
if (ex.status && answer.status !== ex.status && !s.exampleDiffers?.status)
|
|
@@ -1391,9 +1402,8 @@ await walk(journey, world.root(), failures, {
|
|
|
1391
1402
|
const published = isResp(s) && publishedValue !== undefined ? (Array.isArray(s.resp?.[0]) ? publishedValue : [publishedValue]) : publishedValue;
|
|
1392
1403
|
const responseType = answer.headers?.['content-type'];
|
|
1393
1404
|
const literal = isResp(s) || (published !== null && typeof published !== 'object');
|
|
1394
|
-
const valueSchema = ex.schema ?? operationSchemas[
|
|
1395
|
-
comparisonCollections = [...(storedCollectionFields[vendor]?.[
|
|
1396
|
-
const operation = shape?.operationOf((s.method ?? 'GET').toUpperCase(), _path, { ...(journey.headers ?? {}), ...(s.headers ?? {}) })?.op;
|
|
1405
|
+
const valueSchema = ex.schema ?? operationSchemas[key]?.responses[String(answer.status)];
|
|
1406
|
+
comparisonCollections = [...(storedCollectionFields[vendor]?.[key] ?? [])];
|
|
1397
1407
|
// A derived resource list names its holder; never treat every array under a read as a list.
|
|
1398
1408
|
if (operation?.class === 'list' && operation.answers?.list) {
|
|
1399
1409
|
const holder = operation.answers.key ? published?.[operation.answers.key] : published;
|
|
@@ -1439,7 +1449,7 @@ await walk(journey, world.root(), failures, {
|
|
|
1439
1449
|
},
|
|
1440
1450
|
});
|
|
1441
1451
|
const owners = (await vendorFetch(PACKS, vendor))?.(world.root())?.owners?.() ?? (await vendorStream(PACKS, vendor))?.(world.root())?.owners?.() ?? {};
|
|
1442
|
-
const ownerOf = (e) => e.wire === 'client' ? 'gap' : owners[e.
|
|
1452
|
+
const ownerOf = (e) => e.wire === 'client' ? 'gap' : owners[operationKey(e.operation, e.lane)];
|
|
1443
1453
|
const patchRemoved = examples.filter((e) => e.patchRemoved);
|
|
1444
1454
|
const unserved = examples.filter((e) => !e.patchRemoved && ownerOf(e) === 'gap');
|
|
1445
1455
|
const recorded = Object.keys(journey.notReplayed ?? {});
|
package/dist/src/verdict.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// A JUDGE'S INDEPENDENCE (the grade's outline-judged and life-judged, the published examples' verdict): a verdict rules
|
|
2
2
|
// only when it names its judge (`judge.agent`) and the authors of what it judges (`authors`), and the judge is none of
|
|
3
3
|
// them. The dispatcher stamps author names consistently; a judge started independently
|
|
4
|
-
// (
|
|
4
|
+
// (`twin-standard judge-brief` prints its brief) names its reviewer role (`judge.name`). One judging per pack: the
|
|
5
5
|
// verdict stands, and later changes to what it judged keep it (architecture, "Writing a life", Judge).
|
|
6
6
|
/** Committed attribution names people or author roles, never execution identities. */
|
|
7
7
|
export function attributionName(name) {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { type TwinStream } from '@volter/world-core/runtime';
|
|
2
2
|
import { type Captured, type GitStep, type Journey, type JourneyStep, type LineStep, type RespStep } from './journey-kit.js';
|
|
3
|
+
import { type ReplayStep } from './replay.js';
|
|
4
|
+
import type { ManagedDatabaseWalk } from './walk-database.js';
|
|
5
|
+
export { leaked } from './shape.js';
|
|
3
6
|
export declare function usePacks(dir: string, options?: {
|
|
4
7
|
dependenciesFrom?: string;
|
|
5
8
|
}): void;
|
|
@@ -13,26 +16,31 @@ export declare function inMemoryWorld(label: string): {
|
|
|
13
16
|
root: () => string;
|
|
14
17
|
restore: () => void;
|
|
15
18
|
};
|
|
19
|
+
type SentRequest = {
|
|
20
|
+
method: string;
|
|
21
|
+
url: string;
|
|
22
|
+
headers: Record<string, string>;
|
|
23
|
+
};
|
|
24
|
+
type StepAnswer = {
|
|
25
|
+
status: number;
|
|
26
|
+
body: unknown;
|
|
27
|
+
headers?: Record<string, string>;
|
|
28
|
+
request?: SentRequest;
|
|
29
|
+
wireBody?: unknown;
|
|
30
|
+
sql?: ReplayStep['sql'];
|
|
31
|
+
connection?: ReplayStep['connection'];
|
|
32
|
+
};
|
|
16
33
|
export type Failure = {
|
|
17
34
|
step: string;
|
|
18
35
|
beat: string;
|
|
19
36
|
detail: string;
|
|
20
37
|
};
|
|
21
|
-
/** What a stored row keeps for the twin that no schema of the vendor's spec names, by path, in anything the World shows a
|
|
22
|
-
* client: the pack's `_`-prefixed bookkeeping (GitHub's `_links` is the vendor's), and, in what no answer's SHAPE
|
|
23
|
-
* reads (an event's delivery, a socket frame), the kernel's own `updatedAt`, which a raw row filtered on `_` alone
|
|
24
|
-
* carries (Slack's message events once did). The kernel's `deleted` is not read there: vendors send their own
|
|
25
|
-
* (Stripe's deleted customer's `deleted: true`). */
|
|
26
|
-
export declare function leaked(v: unknown, vendorFields: Set<string>, at?: string, unshaped?: boolean): string[];
|
|
27
38
|
/** What a caller that scores a walk observes around each step: the step, its resolved path and body,
|
|
28
|
-
* and (after) the answer and the failures the step produced.
|
|
39
|
+
* and (after) the answer and the failures the step produced. HTTP answers include the constructed request and
|
|
40
|
+
* decoded wire body separately from the capture view. */
|
|
29
41
|
export type StepHooks = {
|
|
30
42
|
before?: (step: JourneyStep | LineStep | GitStep | RespStep, path: string, body: unknown) => void;
|
|
31
|
-
after?: (step: JourneyStep | LineStep | GitStep | RespStep, path: string, answer:
|
|
32
|
-
status: number;
|
|
33
|
-
body: unknown;
|
|
34
|
-
headers?: Record<string, string>;
|
|
35
|
-
}, failures: Failure[]) => void;
|
|
43
|
+
after?: (step: JourneyStep | LineStep | GitStep | RespStep, path: string, answer: StepAnswer, failures: Failure[]) => void;
|
|
36
44
|
};
|
|
37
45
|
/** A pack served for a walk the way a World runs it (its server.ts on a port, the World's root and scenario its options):
|
|
38
46
|
* the walk's requests go to it over HTTP, the vendor's host in their Host header, instead of to the fetch in process;
|
|
@@ -42,9 +50,9 @@ export type ServedTwin = (root: string, options: {
|
|
|
42
50
|
application?: import('@volter/world-core').PackFetchOptions['application'];
|
|
43
51
|
}) => Promise<{
|
|
44
52
|
scheme?: 'http' | 'https';
|
|
45
|
-
fetch: (request: Request, options?: {
|
|
53
|
+
fetch: ((request: Request, options?: {
|
|
46
54
|
credentials?: RequestCredentials;
|
|
47
|
-
}) => Promise<Response
|
|
55
|
+
}) => Promise<Response>) & ManagedDatabaseWalk;
|
|
48
56
|
stop: () => void | Promise<void>;
|
|
49
57
|
socket?: (url: string, headers: Record<string, string>) => WebSocket;
|
|
50
58
|
stream?: TwinStream;
|