@volter/world-core 3.0.155 → 3.0.157
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/attach.cjs +2 -0
- package/capability-decision.cjs +6 -4
- package/dist/attach.cjs +2 -0
- package/dist/capability-decision.cjs +6 -4
- package/dist/generated/pack-facts.json +204 -4
- package/dist/inject.cjs +150 -86
- package/dist/network-policy.cjs +11 -1
- package/dist/network-policy.d.cts +1 -0
- package/dist/pack-facts.d.cts +3 -0
- package/dist/required-credential-transport.cjs +29 -0
- package/dist/required-credential-transport.d.cts +3 -0
- package/dist/src/actions.d.ts +15 -0
- package/dist/src/actions.js +82 -21
- package/dist/src/ancestry.d.ts +5 -4
- package/dist/src/ancestry.js +53 -28
- package/dist/src/blob-store.d.ts +16 -0
- package/dist/src/blob-store.js +76 -1
- package/dist/src/changeset.js +1 -1
- package/dist/src/clickhouse/compression.d.ts +28 -0
- package/dist/src/clickhouse/compression.js +281 -0
- package/dist/src/clickhouse/index.d.ts +1 -0
- package/dist/src/clickhouse/index.js +1 -0
- package/dist/src/derived-core.d.ts +84 -7
- package/dist/src/derived-core.js +336 -61
- package/dist/src/derived-real.js +97 -15
- package/dist/src/derived.d.ts +5 -0
- package/dist/src/derived.js +3 -0
- package/dist/src/executor.d.ts +7 -0
- package/dist/src/executor.js +15 -1
- package/dist/src/fork.js +1 -1
- package/dist/src/git/lfs.d.ts +13 -3
- package/dist/src/git/lfs.js +41 -9
- package/dist/src/grpc-wire.js +2 -1
- package/dist/src/h2.js +63 -10
- package/dist/src/head.d.ts +5 -0
- package/dist/src/head.js +15 -0
- package/dist/src/history.d.ts +1 -1
- package/dist/src/history.js +7 -7
- package/dist/src/index.d.ts +5 -1
- package/dist/src/index.js +6 -1
- package/dist/src/live/ingest.js +9 -13
- package/dist/src/live/rtmp.js +7 -5
- package/dist/src/live/segmenter.js +7 -1
- package/dist/src/log.d.ts +23 -3
- package/dist/src/log.js +160 -37
- package/dist/src/machines.js +5 -1
- package/dist/src/managed-go-programs.d.ts +55 -0
- package/dist/src/managed-go-programs.js +133 -0
- package/dist/src/managed-original-inputs.d.ts +101 -0
- package/dist/src/managed-original-inputs.js +125 -0
- package/dist/src/mongodb/client.d.ts +8 -0
- package/dist/src/mongodb/client.js +27 -0
- package/dist/src/mongodb/index.d.ts +2 -0
- package/dist/src/mongodb/index.js +5 -0
- package/dist/src/mongodb/stream.d.ts +97 -0
- package/dist/src/mongodb/stream.js +206 -0
- package/dist/src/observe.js +1 -1
- package/dist/src/pack-fetch.d.ts +6 -1
- package/dist/src/pack-fetch.js +69 -14
- package/dist/src/pack-paths.d.ts +2 -1
- package/dist/src/pack-paths.js +6 -1
- package/dist/src/packRegistry.d.ts +10 -14
- package/dist/src/packRegistry.js +2 -13
- package/dist/src/protocol.d.ts +13 -0
- package/dist/src/protocol.js +13 -0
- package/dist/src/redis/engine.d.ts +10 -4
- package/dist/src/redis/engine.js +103 -32
- package/dist/src/redis/member-storage.js +20 -8
- package/dist/src/redis/service-times.d.ts +25 -0
- package/dist/src/redis/service-times.js +87 -0
- package/dist/src/redis/stream.d.ts +2 -1
- package/dist/src/redis/stream.js +109 -20
- package/dist/src/resource-blob.d.ts +4 -0
- package/dist/src/resource-blob.js +8 -1
- package/dist/src/runtime-state.d.ts +11 -0
- package/dist/src/runtime-state.js +13 -0
- package/dist/src/runtime.d.ts +7 -5
- package/dist/src/runtime.js +7 -4
- package/dist/src/serve-http.d.ts +2 -0
- package/dist/src/serve-http.js +73 -6
- package/dist/src/serve.d.ts +10 -0
- package/dist/src/serve.js +43 -3
- package/dist/src/sockets.d.ts +4 -1
- package/dist/src/sockets.js +59 -6
- package/dist/src/storage.d.ts +2 -1
- package/dist/src/storage.js +6 -3
- package/dist/src/vendor-call.js +58 -0
- package/dist/src/world-boot.d.ts +1 -0
- package/dist/src/world-boot.js +6 -0
- package/dist/src/world-clock.js +6 -2
- package/dist/src/world-store.d.ts +122 -7
- package/dist/src/world-store.js +959 -103
- package/dist/src/xml.d.ts +30 -0
- package/dist/src/xml.js +74 -0
- package/generated/pack-facts.json +204 -4
- package/inject.cjs +150 -86
- package/network-policy.cjs +11 -1
- package/network-policy.d.cts +1 -0
- package/pack-facts.d.cts +3 -0
- package/package.json +27 -1
- package/required-credential-transport.cjs +29 -0
- package/required-credential-transport.d.cts +3 -0
- package/src/actions.ts +78 -21
- package/src/ancestry.ts +46 -24
- package/src/blob-store.ts +59 -1
- package/src/changeset.ts +1 -1
- package/src/clickhouse/compression.ts +172 -0
- package/src/clickhouse/index.ts +2 -0
- package/src/derived-core.ts +356 -61
- package/src/derived-real.ts +80 -14
- package/src/derived.ts +9 -1
- package/src/executor.ts +17 -3
- package/src/fork.ts +1 -1
- package/src/git/lfs.ts +40 -9
- package/src/grpc-wire.ts +2 -1
- package/src/h2.ts +46 -5
- package/src/head.ts +15 -0
- package/src/history.ts +7 -7
- package/src/index.ts +7 -1
- package/src/live/ingest.ts +7 -3
- package/src/live/rtmp.ts +7 -5
- package/src/live/segmenter.ts +7 -1
- package/src/log.ts +147 -32
- package/src/machines.ts +5 -1
- package/src/managed-go-programs.ts +146 -0
- package/src/managed-original-inputs.ts +163 -0
- package/src/mongodb/client.ts +27 -0
- package/src/mongodb/index.ts +5 -0
- package/src/mongodb/stream.ts +223 -0
- package/src/observe.ts +1 -1
- package/src/pack-fetch.ts +66 -16
- package/src/pack-paths.ts +5 -1
- package/src/packRegistry.ts +3 -15
- package/src/protocol.ts +13 -0
- package/src/redis/engine.ts +100 -57
- package/src/redis/member-storage.ts +17 -9
- package/src/redis/service-times.ts +81 -0
- package/src/redis/stream.ts +66 -14
- package/src/resource-blob.ts +9 -1
- package/src/runtime-state.ts +14 -0
- package/src/runtime.ts +10 -3
- package/src/serve-http.ts +65 -6
- package/src/serve.ts +45 -3
- package/src/sockets.ts +39 -8
- package/src/storage.ts +5 -3
- package/src/vendor-call.ts +54 -0
- package/src/world-boot.ts +6 -0
- package/src/world-clock.ts +6 -2
- package/src/world-store.ts +768 -91
- package/src/xml.ts +71 -0
package/dist/src/derived-real.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseExactJson, exactInteger, compareExactNumbers } from "./exact-json.js";
|
|
2
|
-
import { aliasesFrom, parentEntries, readParentTreeMap, readTree } from "./log.js";
|
|
2
|
+
import { aliasesFrom, parentEntries, readParentTreeMap, readTree, wholeLog } from "./log.js";
|
|
3
3
|
import { storedType } from "./derived-core.js";
|
|
4
4
|
import { verifyEvent } from "./events.js";
|
|
5
5
|
import { hashFieldValue } from "./hash.js";
|
|
@@ -210,13 +210,30 @@ function sentTo(m, path, params) {
|
|
|
210
210
|
const fixedHeaders = (op) => Object.fromEntries(Object.entries(op.headers ?? {}).filter((e) => typeof e[1] === 'string'));
|
|
211
211
|
/** The body as the operation's encoding sends it. */
|
|
212
212
|
function encode(op, body) {
|
|
213
|
-
|
|
213
|
+
// a body the request carried where the spec declares none (Tinybird's Events API, NDJSON its client sends as text):
|
|
214
|
+
// the body as recorded, as the vendor read it
|
|
215
|
+
if (body === undefined)
|
|
214
216
|
return {};
|
|
215
217
|
if (op.bodyEncoding === 'form') {
|
|
218
|
+
// a form's nested values in the bracket form a form-encoded API reads (`scope[type]=account`, `items[0][price]=…`),
|
|
219
|
+
// the form the kernel's own parseBracketForm reads back; never a value's JSON text
|
|
216
220
|
const form = new URLSearchParams();
|
|
221
|
+
const put = (key, v) => {
|
|
222
|
+
if (v === undefined || v === null)
|
|
223
|
+
return;
|
|
224
|
+
if (Array.isArray(v)) {
|
|
225
|
+
v.forEach((item, i) => put(`${key}[${i}]`, item));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (typeof v === 'object') {
|
|
229
|
+
for (const [k, x] of Object.entries(v))
|
|
230
|
+
put(`${key}[${k}]`, x);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
form.append(key, String(v));
|
|
234
|
+
};
|
|
217
235
|
for (const [k, v] of Object.entries(object(body) ?? {}))
|
|
218
|
-
|
|
219
|
-
form.set(k, typeof v === 'object' ? JSON.stringify(v) : String(v));
|
|
236
|
+
put(k, v);
|
|
220
237
|
return { body: form.toString(), type: 'application/x-www-form-urlencoded' };
|
|
221
238
|
}
|
|
222
239
|
return { body: JSON.stringify(body), type: 'application/json' };
|
|
@@ -293,9 +310,14 @@ async function graphqlAnswers(m, surface, input, parsed) {
|
|
|
293
310
|
/** The account a root is, before a perform sends a path naming the World's ("The account a root is"): the World's
|
|
294
311
|
* account its own doors made, unadopted, adopts the one account the root's credential lists. Read once per deploy (an
|
|
295
312
|
* adopted account is not asked again); a list the vendor refuses, or one naming several accounts, adopts none. */
|
|
313
|
+
/** The accounts a root's list already named as the World's own (the World's id is the vendor's), by service and root:
|
|
314
|
+
* nothing to adopt, so not asked again. */
|
|
315
|
+
const accountsMatched = new Map();
|
|
296
316
|
async function adoptAccount(units, execute, ctx) {
|
|
297
317
|
if (ctx.service === undefined)
|
|
298
318
|
return;
|
|
319
|
+
const matched = accountsMatched.get(`${ctx.service}\u0000${ctx.root ?? ''}`) ?? new Set();
|
|
320
|
+
accountsMatched.set(`${ctx.service}\u0000${ctx.root ?? ''}`, matched);
|
|
299
321
|
for (const u of units) {
|
|
300
322
|
const m = u.manifest;
|
|
301
323
|
const decl = m.account ? m.resources[m.account] : undefined;
|
|
@@ -305,13 +327,15 @@ async function adoptAccount(units, execute, ctx) {
|
|
|
305
327
|
const local = readTree(ctx.service, ctx.root).filter((r) => r.type === type && r.deleted !== true && ctx.resolve(type, String(r.id)) === String(r.id));
|
|
306
328
|
const scope = decl.refresh;
|
|
307
329
|
const op = scope && 'list' in scope ? u.surface.operations.find((o) => o.id === scope.list) : undefined;
|
|
308
|
-
if (!local.length || !op)
|
|
330
|
+
if (!local.length || !op || local.every((l) => matched.has(`${type}:${String(l.id)}`)))
|
|
309
331
|
continue;
|
|
310
332
|
const answer = await execute({ method: op.method.toUpperCase(), path: sentTo(m, marked(op, `${op.basePath ?? u.surface.basePath ?? ''}${op.path}`), {}), ...(u.lane ? { lane: u.lane } : {}), headers: fixedHeaders(op) });
|
|
311
333
|
if (answer.status >= 400)
|
|
312
334
|
continue;
|
|
313
335
|
const items = itemsOf(op, parse(answer));
|
|
314
336
|
const id = items.length === 1 ? storedIdOf(decl, items[0]) : undefined;
|
|
337
|
+
if (id !== undefined && local.some((l) => String(l.id) === id))
|
|
338
|
+
matched.add(`${type}:${id}`);
|
|
315
339
|
if (id === undefined || local.some((l) => String(l.id) === id))
|
|
316
340
|
continue;
|
|
317
341
|
observeResources(ctx.service, [{ type, id, fields: items[0], adopts: String(local[0].id) }], ctx.root !== undefined ? { root: ctx.root } : {});
|
|
@@ -548,8 +572,28 @@ function scopeIdOf(scope, fields) {
|
|
|
548
572
|
const value = field ? at(fields, field) : undefined;
|
|
549
573
|
return value === undefined || value === null || value === '' ? undefined : String(value);
|
|
550
574
|
}
|
|
575
|
+
/** A refresh larger than one window of the vendor's rate budget paces itself: a call the budget refuses before making
|
|
576
|
+
* it (RateBudgetError) waits the time the refusal names, at most two minutes a wait, and is made then. The budget is
|
|
577
|
+
* never exceeded and no vendor answer is retried; without it a refresh needing more than one window never finishes
|
|
578
|
+
* (Slack's, after a deploy spent the window: every rerun refused again from the start). */
|
|
579
|
+
function paced(execute) {
|
|
580
|
+
return async (request) => {
|
|
581
|
+
for (let waits = 0;; waits++) {
|
|
582
|
+
try {
|
|
583
|
+
return await execute(request);
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
const wait = error.name === 'RateBudgetError' ? error.retryAfterMs : undefined;
|
|
587
|
+
if (wait === undefined || wait > 120_000 || waits >= 60)
|
|
588
|
+
throw error;
|
|
589
|
+
await new Promise((done) => setTimeout(done, wait + 250));
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
}
|
|
551
594
|
function refreshOf(units, service) {
|
|
552
|
-
return async (
|
|
595
|
+
return async (unpaced, opts) => {
|
|
596
|
+
const execute = paced(unpaced);
|
|
553
597
|
const observed = [];
|
|
554
598
|
const complete = [];
|
|
555
599
|
// what the root's credential may not read (a token scoped to some of the account): left as the World holds it, said
|
|
@@ -557,6 +601,19 @@ function refreshOf(units, service) {
|
|
|
557
601
|
let answered = 0;
|
|
558
602
|
for (const { manifest: m, surface, lane } of units) {
|
|
559
603
|
const onLane = lane ? { lane } : {};
|
|
604
|
+
const tenantBinding = {};
|
|
605
|
+
if (m.refreshIdentity) {
|
|
606
|
+
const identity = surface.operations.find(operation => operation.id === m.refreshIdentity.operation);
|
|
607
|
+
if (!m.tenant || !identity || identity.method !== 'get' || identity.path.includes('{'))
|
|
608
|
+
throw new Error(`${m.vendor}: refresh identity needs a parameterless GET and declared tenant field`);
|
|
609
|
+
const answer = await execute({ method: 'GET', path: `${identity.basePath ?? surface.basePath ?? ''}${identity.path}`, ...onLane, headers: fixedHeaders(identity) });
|
|
610
|
+
if (answer.status >= 400 || refusedIn(m, parse(answer)))
|
|
611
|
+
throw new Error(`${m.vendor}: root account identity refused; nothing folded`);
|
|
612
|
+
const tenant = at(parse(answer), m.refreshIdentity.field);
|
|
613
|
+
if (typeof tenant !== 'string' || !tenant)
|
|
614
|
+
throw new Error(`${m.vendor}: root account identity is absent; nothing folded`);
|
|
615
|
+
tenantBinding[m.tenant.field] = tenant;
|
|
616
|
+
}
|
|
560
617
|
// parents before children, however deep
|
|
561
618
|
const scoped = Object.entries(m.resources).filter(([, d]) => d.refresh && !('none' in d.refresh));
|
|
562
619
|
const depth = (r, seen = new Set()) => { const p = m.resources[r]?.parent?.resource; return p && !seen.has(p) ? 1 + depth(p, seen.add(r)) : 0; };
|
|
@@ -565,6 +622,8 @@ function refreshOf(units, service) {
|
|
|
565
622
|
const scope = decl.refresh;
|
|
566
623
|
if ('none' in scope)
|
|
567
624
|
continue;
|
|
625
|
+
if (m.refreshIdentity && (!('list' in scope) || 'graphql' in scope))
|
|
626
|
+
throw new Error(`${m.vendor}: account-bound refresh requires REST list scopes`);
|
|
568
627
|
if ('graphql' in scope && scope.graphql) {
|
|
569
628
|
await refreshGraphql(m, resource, decl, scope, execute, opts.scope, observed, complete);
|
|
570
629
|
answered += 1;
|
|
@@ -582,6 +641,17 @@ function refreshOf(units, service) {
|
|
|
582
641
|
if ('get' in scope && scope.known) {
|
|
583
642
|
const aliases = aliasesFrom(parentEntries(service, opts.root));
|
|
584
643
|
const held = [...readParentTreeMap(service, opts.root)].filter(([, r]) => r.type === type && r.fields.deleted !== true);
|
|
644
|
+
// and each a door made (a person's act on the vendor's site: a group they add the bot to), which the vendor holds
|
|
645
|
+
// as the World does though no deploy landed it
|
|
646
|
+
const rooted = new Set(held.map(([k]) => k));
|
|
647
|
+
const doorMade = new Set(wholeLog(service, opts.root).filter((e) => e.subject.type === type && e.input?.door !== undefined).map((e) => String(e.subject.id)));
|
|
648
|
+
for (const r of readTree(service, opts.root)) {
|
|
649
|
+
const key = `${type}:${String(r.id)}`;
|
|
650
|
+
if (r.type !== type || r.deleted === true || !doorMade.has(String(r.id)) || held.some(([k]) => k === key))
|
|
651
|
+
continue;
|
|
652
|
+
const { type: _t, id, ...fields } = r;
|
|
653
|
+
held.push([key, { type, id: String(id), fields }]);
|
|
654
|
+
}
|
|
585
655
|
const read = [];
|
|
586
656
|
// a 403 leaves the type as the World holds it, never complete
|
|
587
657
|
let refused = false;
|
|
@@ -606,6 +676,9 @@ function refreshOf(units, service) {
|
|
|
606
676
|
const target = marked(op, path);
|
|
607
677
|
const sent = asked.length ? `${target}${target.includes('?') ? '&' : '?'}${new URLSearchParams(asked)}` : target;
|
|
608
678
|
const answer = await execute({ method: op.method.toUpperCase(), path: sentTo(m, sent, subject), ...onLane, headers: fixedHeaders(op) });
|
|
679
|
+
// a door's subject the vendor does not show the root (another bot's private chat) stays as the World holds it
|
|
680
|
+
if (!rooted.has(key) && answer.status >= 400 && answer.status !== 429)
|
|
681
|
+
continue;
|
|
609
682
|
if (answer.status === 401)
|
|
610
683
|
throw new Error(`401: the vendor refused the root's credential (${op.method.toUpperCase()} ${path}); nothing folded`);
|
|
611
684
|
if (answer.status === 403) {
|
|
@@ -617,7 +690,8 @@ function refreshOf(units, service) {
|
|
|
617
690
|
throw new Error(`429: rate limited${answer.headers['retry-after'] ? `, retry after ${answer.headers['retry-after']}s` : ''}`);
|
|
618
691
|
read.push(String(subject.id));
|
|
619
692
|
answered += 1;
|
|
620
|
-
|
|
693
|
+
// the vendor's answer for a subject it no longer has: a 404, or the scope's `gone` (Telegram's 400 `chat not found`)
|
|
694
|
+
if (answer.status === 404 || (scope.gone && answer.status === scope.gone.status && refusalWords(m, parse(answer)).includes(scope.gone.says)))
|
|
621
695
|
continue;
|
|
622
696
|
if (answer.status >= 400 || refusedIn(m, parse(answer)))
|
|
623
697
|
throw new Error(`${m.vendor}: ${resource} refresh ${op.id} (${op.method.toUpperCase()} ${op.path}): ${answer.status}: ${refusalWords(m, parse(answer))}`);
|
|
@@ -674,9 +748,12 @@ function refreshOf(units, service) {
|
|
|
674
748
|
: decl.parent?.params?.length && under ? Object.fromEntries(Object.entries(under.params).filter(([n]) => !op.path.includes(`{${n}}`) && (op.query ?? []).some((x) => x.name === n))) : {};
|
|
675
749
|
// a list asked by POST carries its parent in its body too, unless the operation reads it from its query
|
|
676
750
|
const parentInQuery = !rpc || (op.query ?? []).some((x) => x.name === decl.parent?.param);
|
|
677
|
-
|
|
751
|
+
// a parameter the operation declares in its query, and not its body, goes in the query of a list asked by POST
|
|
752
|
+
// too (TikTok's video list: `fields` in the query, its cursor in the body)
|
|
753
|
+
const inQuery = (n) => (op.query ?? []).some((x) => x.name === n) && !(op.body ?? []).some((b) => b.name === n);
|
|
754
|
+
const q = new URLSearchParams(rpc ? { ...Object.fromEntries(Object.entries(page).filter(([k]) => inQuery(k))), ...(parentInQuery ? byQuery : {}) } : { ...page, ...byQuery });
|
|
678
755
|
// Fixed parameters and cursors take the operation's body types, not their manifest string encoding.
|
|
679
|
-
const typedPage = Object.fromEntries(Object.entries(page).map(([k, v]) => {
|
|
756
|
+
const typedPage = Object.fromEntries(Object.entries(page).filter(([k]) => !rpc || !inQuery(k)).map(([k, v]) => {
|
|
680
757
|
const type = (op.body ?? []).find((b) => b.name === k)?.type;
|
|
681
758
|
const value = type === 'integer' ? exactInteger(v) : type === 'number' ? Number(v)
|
|
682
759
|
: type === 'boolean' && (v === 'true' || v === 'false') ? v === 'true' : v;
|
|
@@ -739,9 +816,12 @@ function refreshOf(units, service) {
|
|
|
739
816
|
const detail = surface.operations.find((o) => o.id === scope.detail);
|
|
740
817
|
if (!detail)
|
|
741
818
|
throw new Error(`${m.vendor}: ${resource}'s refresh detail names ${scope.detail}, no operation of the surface`);
|
|
742
|
-
const id = mapId ?? storedIdOf(decl, item);
|
|
819
|
+
const id = mapId ?? storedIdOf(decl, { ...under?.params, ...item });
|
|
820
|
+
// the item's own id names it in the detail's path where its stored id is a composite key (a Cloudflare
|
|
821
|
+
// script stored `<account>/<name>` is read at `/scripts/{script_name}` by its name)
|
|
822
|
+
const own = item[decl.idAs ?? 'id'];
|
|
743
823
|
const path = `${detail.basePath ?? surface.basePath ?? ''}${detail.path}`.replace(/\{([^}]+)\}/g, (_, n) => {
|
|
744
|
-
const value = under?.params[n] ?? item[n] ?? id;
|
|
824
|
+
const value = under?.params[n] ?? item[n] ?? (decl.key && (typeof own === 'string' || typeof own === 'number') ? own : id);
|
|
745
825
|
if (value === undefined)
|
|
746
826
|
throw new Error(`${m.vendor}: ${resource}'s refresh detail has no value for ${n}`);
|
|
747
827
|
return encodeURIComponent(String(value));
|
|
@@ -766,8 +846,10 @@ function refreshOf(units, service) {
|
|
|
766
846
|
// twin_local_ports) is the answering World's, never the vendor's, and is not folded in
|
|
767
847
|
const local = m.worldFields?.[resource] ?? {};
|
|
768
848
|
const vendorShape = Object.fromEntries(Object.entries(full).filter(([k]) => !(k in local)));
|
|
769
|
-
const fields = { ...vendorShape, ...kept, ...(decl.parent && parent !== undefined ? { [decl.parent.field]: parent } : {}) };
|
|
770
|
-
|
|
849
|
+
const fields = { ...vendorShape, ...(!decl.shared ? tenantBinding : {}), ...kept, ...(decl.parent && parent !== undefined ? { [decl.parent.field]: parent } : {}) };
|
|
850
|
+
// a key part the item does not carry is the path parameter its parent was listed under (Cloudflare's secret,
|
|
851
|
+
// `{account_id}/{script_name}::{name}`, listed as its name and type alone)
|
|
852
|
+
const id = mapId ?? scopeIdOf(scope, fields) ?? storedIdOf(decl, { ...under?.params, ...fields });
|
|
771
853
|
if (id === undefined)
|
|
772
854
|
continue;
|
|
773
855
|
observed.push({ type, id, fields: fields });
|
|
@@ -856,7 +938,7 @@ function refreshOf(units, service) {
|
|
|
856
938
|
// scoped to one resource (`RootConfig.scope`, GitHub's `repos/o/r`) lists only what is under it, so a type with no
|
|
857
939
|
// parent is never the account's whole list there
|
|
858
940
|
if (whole && !decl.parent && !opts.scope)
|
|
859
|
-
complete.push(type);
|
|
941
|
+
complete.push(m.refreshIdentity && m.tenant && !decl.shared ? { type, field: m.tenant.field, within: [String(tenantBinding[m.tenant.field])] } : type);
|
|
860
942
|
else if (whole && parents.length && decl.parent)
|
|
861
943
|
complete.push({ type, field: decl.parent.field, within: parents.map((p) => p.value) });
|
|
862
944
|
}
|
|
@@ -971,7 +1053,7 @@ async function refreshGraphql(m, resource, decl, scope, execute, rootScope, obse
|
|
|
971
1053
|
const items = listed(value).map(object).filter((x) => x !== undefined);
|
|
972
1054
|
for (const item of items) {
|
|
973
1055
|
const fields = { ...item, ...(decl.parent && under ? { [decl.parent.field]: under.value } : {}) };
|
|
974
|
-
const id = storedIdOf(decl, fields);
|
|
1056
|
+
const id = storedIdOf(decl, { ...under?.params, ...fields });
|
|
975
1057
|
if (id !== undefined)
|
|
976
1058
|
observed.push({ type, id, fields: fields });
|
|
977
1059
|
}
|
package/dist/src/derived.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export type DerivedOperation = {
|
|
|
19
19
|
emptySuccess?: boolean;
|
|
20
20
|
/** a form-encoded operation's number and boolean fields, by bracket path (`items.[].quantity`, `metadata.*`) */
|
|
21
21
|
scalars?: Readonly<Record<string, string | undefined>>;
|
|
22
|
+
/** a form-encoded operation's string fields that take only listed values, by the same bracket path */
|
|
23
|
+
enums?: Readonly<Record<string, ReadonlyArray<string>>>;
|
|
22
24
|
/** what the success answer is: a resource, a list of them, and the envelope key holding it */
|
|
23
25
|
answers?: {
|
|
24
26
|
resource: string;
|
|
@@ -66,6 +68,9 @@ export type DerivedCall = {
|
|
|
66
68
|
request: Request;
|
|
67
69
|
operation: DerivedOperation;
|
|
68
70
|
params: Record<string, string>;
|
|
71
|
+
/** a World screen's call, which no perform sends again: a body in a media type the core does not read (anything
|
|
72
|
+
* but JSON, a form, multipart or text) is the handler's alone, left unread and unrecorded, so an upload streams */
|
|
73
|
+
streamed?: true;
|
|
69
74
|
};
|
|
70
75
|
export declare function bodyTextOf(request: Request): Promise<string>;
|
|
71
76
|
/** The body's text, as JSON, or undefined when it is not JSON; the parse is kept for the first taker. */
|
package/dist/src/derived.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseExactJson } from "./exact-json.js";
|
|
2
2
|
import { runWithRequestTrace } from "./trace-context.js";
|
|
3
|
+
import { redisServiceTimes } from "./redis/service-times.js";
|
|
3
4
|
/**
|
|
4
5
|
* A request's body as text, read once per request: the dispatch reads it and tags the request with it, and every clone
|
|
5
6
|
* made from a tagged request carries the tag, so the malformed-JSON guard, the idempotency signature, the handler's
|
|
@@ -169,8 +170,10 @@ export function createDerivedFetch(options) {
|
|
|
169
170
|
for (const observer of dispatchObservers ?? [])
|
|
170
171
|
observer({ surface: options.surface, operation: matched.operation, owner: handlers[matched.operation.id] ? 'handler' : options.core?.owns(matched.operation) ? 'core' : 'gap' });
|
|
171
172
|
// the body read once, as text, for everything the dispatch does with it (bodyTextOf)
|
|
173
|
+
redisServiceTimes.lap('6a around entered, guards and scenario passed, operation matched again');
|
|
172
174
|
if (request.method !== 'GET' && request.method !== 'HEAD' && request.body && !(request.headers.get('content-type') ?? '').includes('multipart'))
|
|
173
175
|
tagBody(request, await request.clone().text());
|
|
176
|
+
redisServiceTimes.lap('6b the dispatch read the body as text');
|
|
174
177
|
// the path prefix's named groups (pack-fetch.ts, `pathPrefix`) are the call's parameters too
|
|
175
178
|
const prefixed = request.headers.get(PREFIX_PARAMS_HEADER);
|
|
176
179
|
const call = { request, operation: matched.operation, params: prefixed ? { ...parseExactJson(prefixed), ...matched.params } : matched.params };
|
package/dist/src/executor.d.ts
CHANGED
|
@@ -120,6 +120,10 @@ export type TwinAuthStrategy =
|
|
|
120
120
|
answer: string;
|
|
121
121
|
credential: string;
|
|
122
122
|
};
|
|
123
|
+
/** The answer's field giving the token's lifetime in seconds (a dotted path; OAuth's `expires_in`): a token held past
|
|
124
|
+
* it is exchanged again before the next call. For a vendor that answers an expired token with no `retryOn` status
|
|
125
|
+
* (TikTok's API for Business answers HTTP 200 with a code), and so a long-lived process never sends a dead token. */
|
|
126
|
+
expiresIn?: string;
|
|
123
127
|
};
|
|
124
128
|
/** Throws with the precise refusal; returns the parsed URL when the origin is usable. */
|
|
125
129
|
export declare function validateRemoteOrigin(origin: string, opts?: {
|
|
@@ -132,10 +136,13 @@ export declare function validateRemoteOrigin(origin: string, opts?: {
|
|
|
132
136
|
* refused, as before. */
|
|
133
137
|
/** Where a credential is kept (the head's and refresh's root: head.ts rootCustody): `key` names the credential stably
|
|
134
138
|
* across rotations and anew when another is sealed in its place (the token cache is keyed by it, not by the secret's value), `open` reads it as sealed now, `seal` keeps a rotated one. */
|
|
139
|
+
/** `unsealed`: where a rotation the seal failed to keep is recorded durably (the field and the cause, never a value), since
|
|
140
|
+
* the root cannot renew once the process holding the new secret ends; a later successful seal clears it. */
|
|
135
141
|
export type CredentialCustody = {
|
|
136
142
|
key: string;
|
|
137
143
|
open: () => Promise<CredentialPayload>;
|
|
138
144
|
seal: (credential: CredentialPayload) => Promise<void> | void;
|
|
145
|
+
unsealed?: (field: string, cause: string) => void;
|
|
139
146
|
};
|
|
140
147
|
/** `vendor`: the vendor whose rate budget every call is charged to (D8): the call is priced and reserved before it is
|
|
141
148
|
* made, refused when the budget is spent, and settled with the vendor's answer (its back-off honoured). */
|
package/dist/src/executor.js
CHANGED
|
@@ -365,6 +365,7 @@ function exchangeExecute(origin, sealed, auth, vendorHosts, custody) {
|
|
|
365
365
|
return held;
|
|
366
366
|
};
|
|
367
367
|
const key = custody ? `${origin.href}\n${custody.key}` : `${origin.href}\n${JSON.stringify(sealed)}`;
|
|
368
|
+
let expiresAt;
|
|
368
369
|
const exchange = async () => {
|
|
369
370
|
// a rotating secret is read as kept NOW (another executor or process may have rotated it since this one was
|
|
370
371
|
// built), or as this process holds it when its seal failed and the file still holds the spent value. Two
|
|
@@ -409,6 +410,11 @@ function exchangeExecute(origin, sealed, auth, vendorHosts, custody) {
|
|
|
409
410
|
const code = parsed && typeof parsed === 'object' ? parsed.error : undefined;
|
|
410
411
|
throw new Error(`remote execute: the token exchange answered no \`${auth.token}\`${typeof code === 'string' && code !== '' ? ` (${code.slice(0, 120)})` : ''}`);
|
|
411
412
|
}
|
|
413
|
+
if (auth.expiresIn) {
|
|
414
|
+
const seconds = Number(auth.expiresIn.split('.').reduce((at, k) => (at && typeof at === 'object' ? at[k] : undefined), parsed));
|
|
415
|
+
// a minute short of the stated lifetime, so a call in flight never carries a token that dies on the way
|
|
416
|
+
expiresAt = Number.isFinite(seconds) && seconds > 0 ? Date.now() + Math.max(0, seconds - 60) * 1000 : undefined;
|
|
417
|
+
}
|
|
412
418
|
if (auth.rotate) {
|
|
413
419
|
const next = auth.rotate.answer.split('.').reduce((at, k) => (at && typeof at === 'object' ? at[k] : undefined), parsed);
|
|
414
420
|
if (typeof next === 'string' && next !== '' && next !== credential[auth.rotate.credential]) {
|
|
@@ -423,6 +429,10 @@ function exchangeExecute(origin, sealed, auth, vendorHosts, custody) {
|
|
|
423
429
|
}
|
|
424
430
|
catch (error) {
|
|
425
431
|
unsealedRotations.set(key, { replaced, credential });
|
|
432
|
+
try {
|
|
433
|
+
custody.unsealed?.(auth.rotate.credential, error instanceof Error ? error.message : String(error));
|
|
434
|
+
}
|
|
435
|
+
catch { /* the stderr line below still says it */ }
|
|
426
436
|
console.error(`remote execute: the vendor rotated \`${auth.rotate.credential}\` and sealing the new one failed (${error instanceof Error ? error.message : String(error)}); this process holds it until a seal succeeds`);
|
|
427
437
|
}
|
|
428
438
|
}
|
|
@@ -432,12 +442,16 @@ function exchangeExecute(origin, sealed, auth, vendorHosts, custody) {
|
|
|
432
442
|
};
|
|
433
443
|
const current = () => {
|
|
434
444
|
let held = exchangedTokens.get(key);
|
|
445
|
+
if (held?.expiresAt !== undefined && Date.now() >= held.expiresAt) {
|
|
446
|
+
exchangedTokens.delete(key);
|
|
447
|
+
held = undefined;
|
|
448
|
+
}
|
|
435
449
|
if (!held) {
|
|
436
450
|
const token = exchange();
|
|
437
451
|
held = { token };
|
|
438
452
|
exchangedTokens.set(key, held);
|
|
439
453
|
const entry = held;
|
|
440
|
-
token.
|
|
454
|
+
token.then(() => { entry.expiresAt = expiresAt; }, () => { if (exchangedTokens.get(key) === entry)
|
|
441
455
|
exchangedTokens.delete(key); });
|
|
442
456
|
}
|
|
443
457
|
return held;
|
package/dist/src/fork.js
CHANGED
|
@@ -56,7 +56,7 @@ export function forkTwin(opts) {
|
|
|
56
56
|
};
|
|
57
57
|
getActiveWorldStore().write(forkMetaPath(service, toRoot), `${JSON.stringify(meta, null, 2)}\n`);
|
|
58
58
|
return meta;
|
|
59
|
-
});
|
|
59
|
+
}, { operation: 'pointer publication' });
|
|
60
60
|
}
|
|
61
61
|
export function readForkMeta(service, root) {
|
|
62
62
|
const path = forkMetaPath(service, root);
|
package/dist/src/git/lfs.d.ts
CHANGED
|
@@ -4,9 +4,17 @@ export type LfsObject = {
|
|
|
4
4
|
size: number | string;
|
|
5
5
|
};
|
|
6
6
|
/** Where a pack keeps its objects: by oid. */
|
|
7
|
+
/** Where a server keeps its LFS objects: an object's size when it holds one (the batch API answers from it, never
|
|
8
|
+
* loading the object), and the store an upload's verified bytes go to. */
|
|
7
9
|
export type LfsStore = {
|
|
8
|
-
|
|
10
|
+
size(oid: string): Promise<number | null>;
|
|
9
11
|
put(oid: string, bytes: Uint8Array): Promise<void>;
|
|
12
|
+
/** an upload's body written as it arrives and kept only when `accept` holds for its size and SHA-256 (`ctx.blobs.putStream`):
|
|
13
|
+
* a store with it never holds an object whole */
|
|
14
|
+
putStream?(oid: string, chunks: AsyncIterable<Uint8Array>, accept: (written: {
|
|
15
|
+
size: number;
|
|
16
|
+
sha256: string;
|
|
17
|
+
}) => boolean): Promise<boolean>;
|
|
10
18
|
};
|
|
11
19
|
/** A pointer file's object (`version https://git-lfs.github.com/spec/v1`, `oid sha256:<hex>`, `size <n>`), or null
|
|
12
20
|
* for any other file. */
|
|
@@ -24,5 +32,7 @@ export declare function lfsBatch(body: unknown, opts: {
|
|
|
24
32
|
header?: Record<string, string>;
|
|
25
33
|
}): Promise<Response>;
|
|
26
34
|
/** A basic transfer's upload (`PUT` of the object's bytes): kept when they hash to the oid and have the declared size
|
|
27
|
-
* (the size is the request's Content-Length when the caller has no other), else refused and nothing kept.
|
|
28
|
-
|
|
35
|
+
* (the size is the request's Content-Length when the caller has no other), else refused and nothing kept. A body
|
|
36
|
+
* stream goes to the store's `putStream` as it arrives, read no further than the declared size; bytes, or a store
|
|
37
|
+
* without one, are checked whole. */
|
|
38
|
+
export declare function lfsUpload(body: Uint8Array | ReadableStream<Uint8Array> | null, o: LfsObject, store: LfsStore): Promise<Response>;
|
package/dist/src/git/lfs.js
CHANGED
|
@@ -46,22 +46,54 @@ export async function lfsBatch(body, opts) {
|
|
|
46
46
|
objects.push({ ...o, error: { code: 422, message: 'Invalid object' } });
|
|
47
47
|
continue;
|
|
48
48
|
}
|
|
49
|
-
const held = await opts.store.
|
|
49
|
+
const held = await opts.store.size(o.oid);
|
|
50
50
|
if (operation === 'upload') {
|
|
51
|
-
objects.push(held ? { ...o, authenticated: true } : { ...o, authenticated: true, actions: { upload: { href: opts.href('upload', o), ...(opts.header ? { header: opts.header } : {}), expires_in: 3600 } } });
|
|
51
|
+
objects.push(held !== null ? { ...o, authenticated: true } : { ...o, authenticated: true, actions: { upload: { href: opts.href('upload', o), ...(opts.header ? { header: opts.header } : {}), expires_in: 3600 } } });
|
|
52
52
|
}
|
|
53
53
|
else {
|
|
54
54
|
// source: https://github.com/git-lfs/git-lfs/blob/main/docs/api/batch.md "The object does not exist on the server."
|
|
55
|
-
objects.push(held ? { oid: o.oid, size: held
|
|
55
|
+
objects.push(held !== null ? { oid: o.oid, size: held, authenticated: true, actions: { download: { href: opts.href('download', o), ...(opts.header ? { header: opts.header } : {}), expires_in: 3600 } } } : { ...o, error: { code: 404, message: 'Object does not exist' } });
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
return lfsJson(200, { transfer: 'basic', objects, hash_algo: 'sha256' });
|
|
59
59
|
}
|
|
60
60
|
/** A basic transfer's upload (`PUT` of the object's bytes): kept when they hash to the oid and have the declared size
|
|
61
|
-
* (the size is the request's Content-Length when the caller has no other), else refused and nothing kept.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
* (the size is the request's Content-Length when the caller has no other), else refused and nothing kept. A body
|
|
62
|
+
* stream goes to the store's `putStream` as it arrives, read no further than the declared size; bytes, or a store
|
|
63
|
+
* without one, are checked whole. */
|
|
64
|
+
export async function lfsUpload(body, o, store) {
|
|
65
|
+
const refused = () => lfsJson(422, { message: `the object's bytes do not match ${o.oid} (${o.size} bytes)` });
|
|
66
|
+
if (body instanceof Uint8Array || body === null || !store.putStream) {
|
|
67
|
+
const bytes = body instanceof Uint8Array ? body : new Uint8Array(body === null ? new ArrayBuffer(0) : await new Response(body).arrayBuffer());
|
|
68
|
+
if (bytes.length !== o.size || (await lfsOid(bytes)) !== o.oid)
|
|
69
|
+
return refused();
|
|
70
|
+
await store.put(o.oid, bytes);
|
|
71
|
+
return new Response(null, { status: 200 });
|
|
72
|
+
}
|
|
73
|
+
const declared = Number(o.size);
|
|
74
|
+
let over = false;
|
|
75
|
+
async function* upTo(stream) {
|
|
76
|
+
const reader = stream.getReader();
|
|
77
|
+
let read = 0;
|
|
78
|
+
try {
|
|
79
|
+
for (;;) {
|
|
80
|
+
const { done, value } = await reader.read();
|
|
81
|
+
if (done)
|
|
82
|
+
return;
|
|
83
|
+
read += value.byteLength;
|
|
84
|
+
// a body longer than declared is refused without reading the rest of it
|
|
85
|
+
if (read > declared) {
|
|
86
|
+
over = true;
|
|
87
|
+
await reader.cancel();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
yield value;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
reader.releaseLock();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const kept = await store.putStream(o.oid, upTo(body), (w) => !over && w.size === declared && w.sha256 === o.oid);
|
|
98
|
+
return kept ? new Response(null, { status: 200 }) : refused();
|
|
67
99
|
}
|
package/dist/src/grpc-wire.js
CHANGED
|
@@ -7,6 +7,7 @@ import { parseExactJson } from "./exact-json.js";
|
|
|
7
7
|
// and what they answer is the pack's. Importing it does nothing.
|
|
8
8
|
import { acceptH2Connection } from "./h2.js";
|
|
9
9
|
import { nodeBuiltin, WORLD_BOOT_PATH } from "./serve-http.js";
|
|
10
|
+
import { worldBootAnswer } from "./world-boot.js";
|
|
10
11
|
import { decodeMessage, encodeMessage } from "./protobuf.js";
|
|
11
12
|
/** gRPC's status codes (https://grpc.github.io/grpc/core/md_doc_statuscodes.html). */
|
|
12
13
|
export const GrpcStatus = {
|
|
@@ -162,7 +163,7 @@ export function grpcOverFetch(fetch) {
|
|
|
162
163
|
export async function serveGrpc(options) {
|
|
163
164
|
const net = nodeBuiltin('node:net');
|
|
164
165
|
const worldBoot = globalThis.process?.env?.VOLTER_WORLD_BOOT_ID;
|
|
165
|
-
const fetch = worldBoot ? (request) => (new URL(request.url).pathname === WORLD_BOOT_PATH ? Promise.resolve(
|
|
166
|
+
const fetch = worldBoot ? (request) => (new URL(request.url).pathname === WORLD_BOOT_PATH ? Promise.resolve(worldBootAnswer(worldBoot)) : options.fetch(request)) : options.fetch;
|
|
166
167
|
const handler = grpcStreamHandler(options.schema, grpcOverFetch(fetch));
|
|
167
168
|
const server = net.createServer((socket) => { acceptH2Connection(socket, handler, fetch); });
|
|
168
169
|
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(options.port ?? 0, '127.0.0.1', () => resolve()); });
|
package/dist/src/h2.js
CHANGED
|
@@ -10,6 +10,43 @@ const MAX_CONCURRENT_STREAMS = 1000;
|
|
|
10
10
|
/** a request body larger than this resets its stream: a memory bound, not Temporal's message limit (the
|
|
11
11
|
* SDKs' gRPC default is 4 MiB; this server does not enforce that one) */
|
|
12
12
|
const MAX_REQUEST_BYTES = 64 * 1024 * 1024;
|
|
13
|
+
/** A chunked body (RFC 9112 §7.1) from `from`: its bytes once the last chunk and the trailer section have arrived,
|
|
14
|
+
* 'incomplete' before, 'bad' for a malformed size line or one past MAX_REQUEST_BYTES. Extensions and trailers are
|
|
15
|
+
* read past and not kept. */
|
|
16
|
+
function chunkedBody(buffer, from) {
|
|
17
|
+
const parts = [];
|
|
18
|
+
let at = from, total = 0;
|
|
19
|
+
for (;;) {
|
|
20
|
+
const line = buffer.indexOf('\r\n', at);
|
|
21
|
+
if (line < 0)
|
|
22
|
+
return buffer.length - at > 4096 ? 'bad' : 'incomplete';
|
|
23
|
+
const size = /^([0-9A-Fa-f]{1,8})[ \t]*(?:;.*)?$/su.exec(buffer.subarray(at, line).toString('latin1'));
|
|
24
|
+
if (!size)
|
|
25
|
+
return 'bad';
|
|
26
|
+
const length = parseInt(size[1], 16);
|
|
27
|
+
at = line + 2;
|
|
28
|
+
if (length === 0)
|
|
29
|
+
break;
|
|
30
|
+
total += length;
|
|
31
|
+
if (total > MAX_REQUEST_BYTES)
|
|
32
|
+
return 'bad';
|
|
33
|
+
if (buffer.length < at + length + 2)
|
|
34
|
+
return 'incomplete';
|
|
35
|
+
if (buffer[at + length] !== 0x0d || buffer[at + length + 1] !== 0x0a)
|
|
36
|
+
return 'bad';
|
|
37
|
+
parts.push(buffer.subarray(at, at + length));
|
|
38
|
+
at += length + 2;
|
|
39
|
+
}
|
|
40
|
+
// The trailer section: field lines up to an empty line.
|
|
41
|
+
for (;;) {
|
|
42
|
+
const line = buffer.indexOf('\r\n', at);
|
|
43
|
+
if (line < 0)
|
|
44
|
+
return buffer.length - at > 64 * 1024 ? 'bad' : 'incomplete';
|
|
45
|
+
if (line === at)
|
|
46
|
+
return Buffer.concat(parts);
|
|
47
|
+
at = line + 2;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
13
50
|
/** a header block (HEADERS + CONTINUATION) larger than this closes the connection */
|
|
14
51
|
const MAX_HEADER_BLOCK = 256 * 1024;
|
|
15
52
|
// ---- HPACK ------------------------------------------------------------------------------------
|
|
@@ -248,18 +285,34 @@ class H2Connection {
|
|
|
248
285
|
this.respondHttp1(new Response('bad header', { status: 400 }));
|
|
249
286
|
return;
|
|
250
287
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
288
|
+
// A request with a Transfer-Encoding has a body framed by it, and its Content-Length is not read (RFC 9112 §6.3):
|
|
289
|
+
// undici sends a streamed body (a World's fetch forwarding a request's own body, duplex 'half') chunked.
|
|
290
|
+
const coding = headers.get('transfer-encoding');
|
|
291
|
+
let body;
|
|
292
|
+
if (coding !== null) {
|
|
293
|
+
if (coding.split(',').map((each) => each.trim().toLowerCase()).at(-1) !== 'chunked') {
|
|
294
|
+
this.respondHttp1(new Response('a transfer coding other than chunked is not read', { status: 400 }));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const read = chunkedBody(this.buffer, end + 4);
|
|
298
|
+
if (read === 'bad') {
|
|
299
|
+
this.respondHttp1(new Response('bad chunked body', { status: 400 }));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (read === 'incomplete')
|
|
303
|
+
return;
|
|
304
|
+
body = read;
|
|
254
305
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
306
|
+
else {
|
|
307
|
+
const length = Number(headers.get('content-length') ?? 0);
|
|
308
|
+
if (!Number.isFinite(length) || length < 0 || length > MAX_REQUEST_BYTES) {
|
|
309
|
+
this.respondHttp1(new Response('bad content-length', { status: 400 }));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (this.buffer.length < end + 4 + length)
|
|
313
|
+
return;
|
|
314
|
+
body = this.buffer.subarray(end + 4, end + 4 + length);
|
|
259
315
|
}
|
|
260
|
-
if (this.buffer.length < end + 4 + length)
|
|
261
|
-
return;
|
|
262
|
-
const body = this.buffer.subarray(end + 4, end + 4 + length);
|
|
263
316
|
this.buffer = Buffer.alloc(0);
|
|
264
317
|
this.socket.removeAllListeners('data');
|
|
265
318
|
let request;
|
package/dist/src/head.d.ts
CHANGED
|
@@ -72,6 +72,8 @@ export declare function openRootCredential(root: BoundRoot, vendor?: string): Pr
|
|
|
72
72
|
* fingerprint, and a new one per grant would start the ledger over), and rotatedAt says when. `source` is the record
|
|
73
73
|
* the rotation began from: if another credential was set meanwhile, the rotation is dropped, never written over it. */
|
|
74
74
|
export declare function resealRootCredential(root: BoundRoot, payload: CredentialPayload, vendor?: string, source?: string | null): Promise<void>;
|
|
75
|
+
/** Beside a root's credential, the record of a rotation its seal failed to keep (`rootCustody`'s `unsealed`). */
|
|
76
|
+
export declare function unsealedMarkerOf(root: BoundRoot): string;
|
|
75
77
|
/** The root's custody for an exchange that rotates: the credential as sealed now, and the seal of a rotated one, which
|
|
76
78
|
* lands only over the record that was opened. */
|
|
77
79
|
export declare function rootCustody(root: BoundRoot, vendor?: string): CredentialCustody;
|
|
@@ -174,6 +176,9 @@ export declare function performEntries(opts: {
|
|
|
174
176
|
* `auto` is performed now. Returns the vendor's id when it minted one, so the answer carries it.
|
|
175
177
|
* Raises `RefusedWriteError` / `VendorWriteError` for the pack to answer in the vendor's shape.
|
|
176
178
|
*/
|
|
179
|
+
/** Whether `performAtHead` would perform an entry now (a twin whose root says `auto`, outside a pack fetch's deferred
|
|
180
|
+
* group): a caller appending several writes at once appends them one at a time then, each performed before the next. */
|
|
181
|
+
export declare function performsAtHeadNow(service: string, root?: string): boolean;
|
|
177
182
|
export declare function performAtHead(service: string, action: Pick<TwinAction, 'id'>, root?: string): Promise<{
|
|
178
183
|
performed: boolean;
|
|
179
184
|
externalId?: string;
|
package/dist/src/head.js
CHANGED
|
@@ -136,7 +136,10 @@ export async function resealRootCredential(root, payload, vendor = vendorOf(root
|
|
|
136
136
|
if (store.read(root.credential) !== held)
|
|
137
137
|
throw new Error(`the ${vendor} credential was set again while it rotated: the rotation is dropped`);
|
|
138
138
|
store.write(root.credential, `${JSON.stringify({ ...sealed, ...(before ? { fingerprint: before.fingerprint } : {}), rotatedAt: now }, null, 2)}\n`, { secret: true });
|
|
139
|
+
store.remove(unsealedMarkerOf(root));
|
|
139
140
|
}
|
|
141
|
+
/** Beside a root's credential, the record of a rotation its seal failed to keep (`rootCustody`'s `unsealed`). */
|
|
142
|
+
export function unsealedMarkerOf(root) { return root.credential.replace(/\.json$/, '.unsealed.json'); }
|
|
140
143
|
/** The root's custody for an exchange that rotates: the credential as sealed now, and the seal of a rotated one, which
|
|
141
144
|
* lands only over the record that was opened. */
|
|
142
145
|
export function rootCustody(root, vendor = vendorOf(root)) {
|
|
@@ -155,6 +158,9 @@ export function rootCustody(root, vendor = vendorOf(root)) {
|
|
|
155
158
|
return opened.payload;
|
|
156
159
|
},
|
|
157
160
|
seal: (credential) => resealRootCredential(root, credential, vendor, source),
|
|
161
|
+
// the vendor already spent the sealed secret: until a seal lands, the root renews only inside the process holding the
|
|
162
|
+
// new one, so the World keeps a record a person or `doctor` can read after that process ends
|
|
163
|
+
unsealed: (field, cause) => store.write(unsealedMarkerOf(root), `${JSON.stringify({ vendor, field, at: new Date().toISOString(), cause: cause.slice(0, 300) }, null, 2)}\n`),
|
|
158
164
|
};
|
|
159
165
|
}
|
|
160
166
|
/** The vendor a root belongs to: its credential is sealed under the vendor's name. */
|
|
@@ -464,6 +470,15 @@ async function performNow(opts) {
|
|
|
464
470
|
* `auto` is performed now. Returns the vendor's id when it minted one, so the answer carries it.
|
|
465
471
|
* Raises `RefusedWriteError` / `VendorWriteError` for the pack to answer in the vendor's shape.
|
|
466
472
|
*/
|
|
473
|
+
/** Whether `performAtHead` would perform an entry now (a twin whose root says `auto`, outside a pack fetch's deferred
|
|
474
|
+
* group): a caller appending several writes at once appends them one at a time then, each performed before the next. */
|
|
475
|
+
export function performsAtHeadNow(service, root) {
|
|
476
|
+
const request = currentCorrelationId();
|
|
477
|
+
if (request !== undefined && deferred.has(request))
|
|
478
|
+
return false;
|
|
479
|
+
const head = headOf(service, root);
|
|
480
|
+
return head.kind === 'real' && head.deploy === 'auto';
|
|
481
|
+
}
|
|
467
482
|
export async function performAtHead(service, action, root) {
|
|
468
483
|
// a pack fetch performs its request's entries as one group after the handler (performRequestGroup)
|
|
469
484
|
const request = currentCorrelationId();
|
package/dist/src/history.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ export declare function saveHistoryView(directory: string, view: HistoryView): s
|
|
|
51
51
|
* (and the view document itself) and each named directory's ancestry record (its generation). A read cache keyed by this sees an
|
|
52
52
|
* ancestor's retained history truncated, replaced or its directory re-created, which the full read
|
|
53
53
|
* refuses (`historyEntries`), instead of serving a tree folded from the history it replaced. */
|
|
54
|
-
export declare function inheritedHistoryKey(service: string, root?: string): string;
|
|
54
|
+
export declare function inheritedHistoryKey(service: string, root?: string, meta?: string | null): string;
|
|
55
55
|
export declare function inheritedHistory(service: string, root?: string): HistoryView | undefined;
|
|
56
56
|
/** Capture under the directory's history lock, as appends and complete observation batches are made. */
|
|
57
57
|
export declare function captureHistory(service: string, root?: string): {
|