@volter/world-core 3.0.4 → 3.0.5
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/dist/src/derived-core.d.ts +23 -0
- package/dist/src/derived-real.js +37 -4
- package/dist/src/executor.js +9 -5
- package/dist/src/index.d.ts +1 -1
- package/dist/src/remote-execute.d.ts +4 -0
- package/package.json +1 -1
- package/src/derived-core.ts +24 -0
- package/src/derived-real.ts +31 -4
- package/src/executor.ts +6 -4
- package/src/index.ts +1 -1
- package/src/remote-execute.ts +4 -0
|
@@ -296,6 +296,21 @@ export type ScreenDecl = {
|
|
|
296
296
|
/** the vendor's documentation of the round trip or the screen */
|
|
297
297
|
source: string;
|
|
298
298
|
};
|
|
299
|
+
/** A protocol hook's arguments: the root's executor (it applies the sealed credential; `issued` carries one the vendor
|
|
300
|
+
* issued instead), the group and its first entry's recorded input, the perform's context, the derived perform of an
|
|
301
|
+
* input (`send`: the recorded request, or one the hook changed), the stored id the World's id of a type resolves to,
|
|
302
|
+
* the surface's base path, and the World's blobs of this twin. */
|
|
303
|
+
export type PerformHookArgs = {
|
|
304
|
+
execute: import('./remote-execute.js').RemoteExecute;
|
|
305
|
+
group: import('./actions.js').TwinAction[];
|
|
306
|
+
input: Record<string, unknown>;
|
|
307
|
+
ctx: import('./head.js').PerformContext;
|
|
308
|
+
send: (input: Record<string, unknown>) => Promise<import('./head.js').PushOutcome>;
|
|
309
|
+
resolve: (type: string, localId: string) => string;
|
|
310
|
+
basePath: string;
|
|
311
|
+
blob: (key: string) => Promise<Uint8Array | null>;
|
|
312
|
+
};
|
|
313
|
+
export type PerformHook = (args: PerformHookArgs) => Promise<import('./head.js').PushOutcome>;
|
|
299
314
|
export type DerivedManifest = {
|
|
300
315
|
/** The pack's descriptor, as data (docs/contributing/architecture.md, "The descriptor"), every field but `vendor`, which
|
|
301
316
|
* is the manifest's: the pack registers `packOf(manifest)`, so its index is a fixed file. A lane has none. */
|
|
@@ -384,6 +399,14 @@ export type DerivedManifest = {
|
|
|
384
399
|
vendorBacked?: {
|
|
385
400
|
none: string;
|
|
386
401
|
};
|
|
402
|
+
/** the resource the vendor's paths name the account by (Cloudflare's `accounts/{account_id}`): with a root, the
|
|
403
|
+
* World's account is the one account the root's credential reaches, which refresh observes ("The account a root
|
|
404
|
+
* is", architecture) */
|
|
405
|
+
account?: string;
|
|
406
|
+
/** a vendor protocol one recorded request cannot express, by the operation that ends it: a session whose answer
|
|
407
|
+
* issues the credential its next calls carry (Cloudflare's assets upload), performed by the hook in place of the
|
|
408
|
+
* derived request, which it composes (`send`) ("Protocols a request cannot express", architecture) */
|
|
409
|
+
performs?: Record<string, PerformHook>;
|
|
387
410
|
/** `withParam`: the error names the path parameter that held the unknown id */
|
|
388
411
|
notFound: {
|
|
389
412
|
status: number;
|
package/dist/src/derived-real.js
CHANGED
|
@@ -77,7 +77,9 @@ function resolvedDeep(value, resolve) {
|
|
|
77
77
|
const o = object(value);
|
|
78
78
|
return o ? Object.fromEntries(Object.entries(o).map(([k, v]) => [k, resolvedDeep(v, resolve)])) : value;
|
|
79
79
|
}
|
|
80
|
-
/** A multipart body: the recorded fields, then each recorded file part read back from the World's blob store.
|
|
80
|
+
/** A multipart body: the recorded fields, then each recorded file part read back from the World's blob store. An
|
|
81
|
+
* operation the vendor takes as multipart is sent so with no file part too (a Workers upload of assets alone carries
|
|
82
|
+
* only its `metadata` field). */
|
|
81
83
|
async function multipart(body, files, ctx) {
|
|
82
84
|
const boundary = `----volter${hashFieldValue({ body, files }).slice(0, 24)}`;
|
|
83
85
|
const chunks = [];
|
|
@@ -123,9 +125,9 @@ function unitOf(units, input) {
|
|
|
123
125
|
?? (input.graphql ? units[0] : undefined);
|
|
124
126
|
}
|
|
125
127
|
function performOf(units) {
|
|
126
|
-
|
|
128
|
+
// the request a group's first entry recorded (or one a protocol hook changed), sent to the vendor
|
|
129
|
+
const derived = async (execute, group, ctx, input) => {
|
|
127
130
|
const first = group[0];
|
|
128
|
-
const input = (first.input ?? {});
|
|
129
131
|
const unit = unitOf(units, input);
|
|
130
132
|
if (!unit)
|
|
131
133
|
throw new RefusedWriteError('vendor', `the write records no operation of the vendor's surface (${String(input.operationId ?? 'none')})`, first.id);
|
|
@@ -183,7 +185,7 @@ function performOf(units) {
|
|
|
183
185
|
const asCame = object(input.body);
|
|
184
186
|
const raw = asCame && typeof asCame.$text === 'string' ? { body: asCame.$text } : asCame && typeof asCame.$base64 === 'string' ? { body: new Uint8Array(Buffer.from(asCame.$base64, 'base64')) } : undefined;
|
|
185
187
|
const sent = raw ? { ...raw, ...(typeof input.contentType === 'string' ? { type: input.contentType } : {}) }
|
|
186
|
-
: files.length ? await multipart(body, files, ctx) : op ? encode(op, body) : { body: JSON.stringify(body), type: 'application/json' };
|
|
188
|
+
: files.length || op?.bodyEncoding === 'multipart' ? await multipart(body, files, ctx) : op ? encode(op, body) : { body: JSON.stringify(body), type: 'application/json' };
|
|
187
189
|
const headers = { ...(object(input.headers) ?? {}), ...(sent.type ? { 'content-type': sent.type } : {}) };
|
|
188
190
|
// a retried deploy makes nothing twice, and two Worlds deploying into one account never collide
|
|
189
191
|
if (m.idempotency)
|
|
@@ -228,6 +230,22 @@ function performOf(units) {
|
|
|
228
230
|
}
|
|
229
231
|
return { externalId, actionId: primary.id, data: obj, ...(also.length ? { also } : {}) };
|
|
230
232
|
};
|
|
233
|
+
return async (execute, group, ctx) => {
|
|
234
|
+
const input = (group[0].input ?? {});
|
|
235
|
+
const unit = unitOf(units, input);
|
|
236
|
+
// a protocol one request cannot express is the pack's hook, which composes the derived perform ("Protocols a
|
|
237
|
+
// request cannot express")
|
|
238
|
+
const hook = unit && typeof input.operationId === 'string' ? unit.manifest.performs?.[input.operationId] : undefined;
|
|
239
|
+
if (!hook || !unit)
|
|
240
|
+
return derived(execute, group, ctx, input);
|
|
241
|
+
return hook({
|
|
242
|
+
execute, group, input, ctx,
|
|
243
|
+
send: (changed) => derived(execute, group, ctx, changed),
|
|
244
|
+
resolve: (type, localId) => ctx.resolve(type, localId),
|
|
245
|
+
basePath: unit.surface.basePath ?? '',
|
|
246
|
+
blob: (key) => readResourceBlob(ctx.service ?? '', key, ctx.root),
|
|
247
|
+
});
|
|
248
|
+
};
|
|
231
249
|
}
|
|
232
250
|
/** Path parameters that name a subject by a field rather than its id, sent as the vendor's value of that field, read
|
|
233
251
|
* from the adopted landed copy ("Perform", step 1): a parent found by `parent.where` (a repository addressed as
|
|
@@ -400,6 +418,21 @@ function adopt(service, root, units, observed) {
|
|
|
400
418
|
list.push({ id: e.subject.id, fields: held.fields });
|
|
401
419
|
pending.set(e.subject.type, list);
|
|
402
420
|
}
|
|
421
|
+
// the account a root is ("The account a root is"): the root's credential reaches one account of the type the pack's
|
|
422
|
+
// paths name it by; observed alone, it is the World's own account (made by the World's door, so no deploy settled it)
|
|
423
|
+
for (const u of units) {
|
|
424
|
+
if (!u.manifest.account)
|
|
425
|
+
continue;
|
|
426
|
+
const type = storedType(u.manifest, u.manifest.account);
|
|
427
|
+
const seen = observed.filter((o) => o.type === type);
|
|
428
|
+
if (seen.length !== 1 || tree.has(`${type}:${seen[0].id}`))
|
|
429
|
+
continue;
|
|
430
|
+
const local = readTree(service, root).find((r) => r.type === type && r.deleted !== true && !aliases.has(`${type}:${String(r.id)}`) && String(r.id) !== seen[0].id);
|
|
431
|
+
if (local) {
|
|
432
|
+
seen[0].adopts = String(local.id);
|
|
433
|
+
aliases.set(`${type}:${String(local.id)}`, seen[0].id);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
403
436
|
if (!pending.size)
|
|
404
437
|
return;
|
|
405
438
|
const declOf = (type) => {
|
package/dist/src/executor.js
CHANGED
|
@@ -148,7 +148,7 @@ function budgeted(vendor, execute, token) {
|
|
|
148
148
|
}
|
|
149
149
|
/** The call made as the root's credential is applied by the strategy (header replacement when none). */
|
|
150
150
|
function direct(origin, credential, auth, vendorHosts) {
|
|
151
|
-
return async ({ method, path, headers, body, responseType, presigned }) => {
|
|
151
|
+
return async ({ method, path, headers, body, responseType, presigned, issued }) => {
|
|
152
152
|
// A PRESIGNED UPLOAD URL the vendor returned: its own signature is its authorization, so the
|
|
153
153
|
// sealed credential never goes with it, whatever host it names. A host other than the root's
|
|
154
154
|
// passes the same validation the root does; the root's own origin (a twin standing in for the
|
|
@@ -205,8 +205,12 @@ function direct(origin, credential, auth, vendorHosts) {
|
|
|
205
205
|
throw new Error('remote execute: path escaped the validated origin');
|
|
206
206
|
}
|
|
207
207
|
const outbound = new Headers(headers ?? {});
|
|
208
|
-
|
|
209
|
-
|
|
208
|
+
// a credential the vendor issued in this perform is the request's authorization; the sealed one stays home
|
|
209
|
+
if (issued !== undefined)
|
|
210
|
+
outbound.set('authorization', issued);
|
|
211
|
+
else
|
|
212
|
+
for (const [name, value] of Object.entries(credential.headers))
|
|
213
|
+
outbound.set(name, value);
|
|
210
214
|
// ── THE STRATEGIES BEYOND HEADER REPLACEMENT ──────────────────────────────────────────────
|
|
211
215
|
// Applied AFTER the anchoring check above, so nothing here can move the request off the
|
|
212
216
|
// validated origin: `searchParams.set` only ever touches the query, and a header is a header.
|
|
@@ -214,7 +218,7 @@ function direct(origin, credential, auth, vendorHosts) {
|
|
|
214
218
|
// FAIL CLOSED. A strategy with no secret must never send a request that merely LOOKS
|
|
215
219
|
// authenticated — that silence is the whole defect this exists to remove. The refusals name the
|
|
216
220
|
// strategy and the parameter, never the secret.
|
|
217
|
-
if (auth !== undefined) {
|
|
221
|
+
if (auth !== undefined && issued === undefined) {
|
|
218
222
|
const secret = credential.secret;
|
|
219
223
|
if (typeof secret !== 'string' || secret === '') {
|
|
220
224
|
throw new Error(`remote execute: the ${auth.in} credential strategy needs \`secret\` on the sealed credential, and it is absent`);
|
|
@@ -379,7 +383,7 @@ function exchangeExecute(origin, sealed, auth, vendorHosts, custody) {
|
|
|
379
383
|
return anchored({ ...request, headers });
|
|
380
384
|
};
|
|
381
385
|
return async (request) => {
|
|
382
|
-
if (request.presigned)
|
|
386
|
+
if (request.presigned || request.issued !== undefined)
|
|
383
387
|
return anchored(request);
|
|
384
388
|
const held = current();
|
|
385
389
|
const first = await call(request, held);
|
package/dist/src/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export declare const packOf: (manifest: {
|
|
|
14
14
|
}>) => PackDescriptor;
|
|
15
15
|
/** The pack registered, so the World finds it. */
|
|
16
16
|
export declare const registerPack: (pack: PackDescriptor) => PackDescriptor;
|
|
17
|
-
export type { Actor, CoreScope, DerivedManifest, ErrorSpec, LaneRoute, LanesDecl, VendorManifest, FieldRule, ResourceDecl, ScreenDecl, SeedCall, StateField, Transition } from './derived-core.js';
|
|
17
|
+
export type { Actor, CoreScope, DerivedManifest, PerformHook, PerformHookArgs, ErrorSpec, LaneRoute, LanesDecl, VendorManifest, FieldRule, ResourceDecl, ScreenDecl, SeedCall, StateField, Transition } from './derived-core.js';
|
|
18
18
|
export type { ApplicationAnswer, EndpointsDecl, EventRender, EventScheme, EventsDecl, EventTransport, EventValues, EventWrite } from './events.js';
|
|
19
19
|
export { verifyEvent } from './events.js';
|
|
20
20
|
export { deriveStateSystem, deriveVendorStateSystem } from './derived-real.js';
|
|
@@ -12,6 +12,10 @@ export type RemoteExecuteRequest = {
|
|
|
12
12
|
* the API, and the request goes WITHOUT the sealed credential. An upload the vendor authorizes
|
|
13
13
|
* with the API token (LinkedIn's images) is not presigned and cannot use this. */
|
|
14
14
|
presigned?: true;
|
|
15
|
+
/** An Authorization the vendor ISSUED earlier in the same perform (Cloudflare's assets upload session answers a JWT
|
|
16
|
+
* that its upload calls carry instead of the API token), on the root's own origin: it is sent as the request's
|
|
17
|
+
* Authorization and the sealed credential does not go with it. */
|
|
18
|
+
issued?: string;
|
|
15
19
|
};
|
|
16
20
|
export type RemoteExecuteResponse = {
|
|
17
21
|
status: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-core",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.5",
|
|
4
4
|
"description": "The kernel of Volter World: one log per twin, branches as pointers, checkpoints, the fold that keeps a twin current, the head that performs a write against the vendor, references, and the git plane. A twin package builds on it; the runtime serves it.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
package/src/derived-core.ts
CHANGED
|
@@ -239,6 +239,22 @@ export type ScreenDecl = {
|
|
|
239
239
|
source: string;
|
|
240
240
|
};
|
|
241
241
|
|
|
242
|
+
/** A protocol hook's arguments: the root's executor (it applies the sealed credential; `issued` carries one the vendor
|
|
243
|
+
* issued instead), the group and its first entry's recorded input, the perform's context, the derived perform of an
|
|
244
|
+
* input (`send`: the recorded request, or one the hook changed), the stored id the World's id of a type resolves to,
|
|
245
|
+
* the surface's base path, and the World's blobs of this twin. */
|
|
246
|
+
export type PerformHookArgs = {
|
|
247
|
+
execute: import('./remote-execute.ts').RemoteExecute;
|
|
248
|
+
group: import('./actions.ts').TwinAction[];
|
|
249
|
+
input: Record<string, unknown>;
|
|
250
|
+
ctx: import('./head.ts').PerformContext;
|
|
251
|
+
send: (input: Record<string, unknown>) => Promise<import('./head.ts').PushOutcome>;
|
|
252
|
+
resolve: (type: string, localId: string) => string;
|
|
253
|
+
basePath: string;
|
|
254
|
+
blob: (key: string) => Promise<Uint8Array | null>;
|
|
255
|
+
};
|
|
256
|
+
export type PerformHook = (args: PerformHookArgs) => Promise<import('./head.ts').PushOutcome>;
|
|
257
|
+
|
|
242
258
|
export type DerivedManifest = {
|
|
243
259
|
/** The pack's descriptor, as data (docs/contributing/architecture.md, "The descriptor"), every field but `vendor`, which
|
|
244
260
|
* is the manifest's: the pack registers `packOf(manifest)`, so its index is a fixed file. A lane has none. */
|
|
@@ -293,6 +309,14 @@ export type DerivedManifest = {
|
|
|
293
309
|
/** a pack whose wire has no derived perform (a socket, a line protocol, a managed database): binding a root is refused
|
|
294
310
|
* with this reason */
|
|
295
311
|
vendorBacked?: { none: string };
|
|
312
|
+
/** the resource the vendor's paths name the account by (Cloudflare's `accounts/{account_id}`): with a root, the
|
|
313
|
+
* World's account is the one account the root's credential reaches, which refresh observes ("The account a root
|
|
314
|
+
* is", architecture) */
|
|
315
|
+
account?: string;
|
|
316
|
+
/** a vendor protocol one recorded request cannot express, by the operation that ends it: a session whose answer
|
|
317
|
+
* issues the credential its next calls carry (Cloudflare's assets upload), performed by the hook in place of the
|
|
318
|
+
* derived request, which it composes (`send`) ("Protocols a request cannot express", architecture) */
|
|
319
|
+
performs?: Record<string, PerformHook>;
|
|
296
320
|
/** `withParam`: the error names the path parameter that held the unknown id */
|
|
297
321
|
notFound: { status: number; message: string; code?: string; kind?: string; withParam?: boolean };
|
|
298
322
|
/** the list envelope (placeholders `{data}`, `{has_more}`, `{url}`, `{first_id}`, `{last_id}`),
|
package/src/derived-real.ts
CHANGED
|
@@ -70,7 +70,9 @@ function resolvedDeep(value: unknown, resolve: (v: string) => string): unknown {
|
|
|
70
70
|
return o ? Object.fromEntries(Object.entries(o).map(([k, v]) => [k, resolvedDeep(v, resolve)])) : value;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
/** A multipart body: the recorded fields, then each recorded file part read back from the World's blob store.
|
|
73
|
+
/** A multipart body: the recorded fields, then each recorded file part read back from the World's blob store. An
|
|
74
|
+
* operation the vendor takes as multipart is sent so with no file part too (a Workers upload of assets alone carries
|
|
75
|
+
* only its `metadata` field). */
|
|
74
76
|
async function multipart(body: unknown, files: Row[], ctx: PerformContext): Promise<{ body: Uint8Array; type: string }> {
|
|
75
77
|
const boundary = `----volter${hashFieldValue({ body, files }).slice(0, 24)}`;
|
|
76
78
|
const chunks: Uint8Array[] = [];
|
|
@@ -110,9 +112,9 @@ function unitOf(units: Unit[], input: Row): Unit | undefined {
|
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
function performOf(units: Unit[]): PerformAction {
|
|
113
|
-
|
|
115
|
+
// the request a group's first entry recorded (or one a protocol hook changed), sent to the vendor
|
|
116
|
+
const derived = async (execute: RemoteExecute, group: TwinAction[], ctx: PerformContext, input: Row): Promise<PushOutcome> => {
|
|
114
117
|
const first = group[0]!;
|
|
115
|
-
const input = (first.input ?? {}) as Row;
|
|
116
118
|
const unit = unitOf(units, input);
|
|
117
119
|
if (!unit) throw new RefusedWriteError('vendor', `the write records no operation of the vendor's surface (${String(input.operationId ?? 'none')})`, first.id);
|
|
118
120
|
const { manifest: m, surface } = unit;
|
|
@@ -153,7 +155,7 @@ function performOf(units: Unit[]): PerformAction {
|
|
|
153
155
|
const asCame = object(input.body);
|
|
154
156
|
const raw = asCame && typeof asCame.$text === 'string' ? { body: asCame.$text as string } : asCame && typeof asCame.$base64 === 'string' ? { body: new Uint8Array(Buffer.from(asCame.$base64 as string, 'base64')) } : undefined;
|
|
155
157
|
const sent: { body?: string | Uint8Array; type?: string } = raw ? { ...raw, ...(typeof input.contentType === 'string' ? { type: input.contentType } : {}) }
|
|
156
|
-
: files.length ? await multipart(body, files, ctx) : op ? encode(op, body) : { body: JSON.stringify(body), type: 'application/json' };
|
|
158
|
+
: files.length || op?.bodyEncoding === 'multipart' ? await multipart(body, files, ctx) : op ? encode(op, body) : { body: JSON.stringify(body), type: 'application/json' };
|
|
157
159
|
const headers: Record<string, string> = { ...((object(input.headers) ?? {}) as Record<string, string>), ...(sent.type ? { 'content-type': sent.type } : {}) };
|
|
158
160
|
// a retried deploy makes nothing twice, and two Worlds deploying into one account never collide
|
|
159
161
|
if (m.idempotency) headers[m.idempotency.header] = hashFieldValue({ credential: ctx.credential ?? '', entry: first.id });
|
|
@@ -193,6 +195,21 @@ function performOf(units: Unit[]): PerformAction {
|
|
|
193
195
|
}
|
|
194
196
|
return { externalId, actionId: primary.id, data: obj, ...(also.length ? { also } : {}) };
|
|
195
197
|
};
|
|
198
|
+
return async (execute: RemoteExecute, group: TwinAction[], ctx: PerformContext): Promise<PushOutcome> => {
|
|
199
|
+
const input = (group[0]!.input ?? {}) as Row;
|
|
200
|
+
const unit = unitOf(units, input);
|
|
201
|
+
// a protocol one request cannot express is the pack's hook, which composes the derived perform ("Protocols a
|
|
202
|
+
// request cannot express")
|
|
203
|
+
const hook = unit && typeof input.operationId === 'string' ? unit.manifest.performs?.[input.operationId] : undefined;
|
|
204
|
+
if (!hook || !unit) return derived(execute, group, ctx, input);
|
|
205
|
+
return hook({
|
|
206
|
+
execute, group, input, ctx,
|
|
207
|
+
send: (changed) => derived(execute, group, ctx, changed),
|
|
208
|
+
resolve: (type, localId) => ctx.resolve(type, localId),
|
|
209
|
+
basePath: unit.surface.basePath ?? '',
|
|
210
|
+
blob: (key) => readResourceBlob(ctx.service ?? '', key, ctx.root),
|
|
211
|
+
});
|
|
212
|
+
};
|
|
196
213
|
}
|
|
197
214
|
|
|
198
215
|
/** Path parameters that name a subject by a field rather than its id, sent as the vendor's value of that field, read
|
|
@@ -336,6 +353,16 @@ function adopt(service: string, root: string | undefined, units: Unit[], observe
|
|
|
336
353
|
if (!list.some((p) => p.id === e.subject.id)) list.push({ id: e.subject.id, fields: held.fields });
|
|
337
354
|
pending.set(e.subject.type, list);
|
|
338
355
|
}
|
|
356
|
+
// the account a root is ("The account a root is"): the root's credential reaches one account of the type the pack's
|
|
357
|
+
// paths name it by; observed alone, it is the World's own account (made by the World's door, so no deploy settled it)
|
|
358
|
+
for (const u of units) {
|
|
359
|
+
if (!u.manifest.account) continue;
|
|
360
|
+
const type = storedType(u.manifest, u.manifest.account);
|
|
361
|
+
const seen = observed.filter((o) => o.type === type);
|
|
362
|
+
if (seen.length !== 1 || tree.has(`${type}:${seen[0]!.id}`)) continue;
|
|
363
|
+
const local = (readTree(service, root) as Row[]).find((r) => r.type === type && r.deleted !== true && !aliases.has(`${type}:${String(r.id)}`) && String(r.id) !== seen[0]!.id);
|
|
364
|
+
if (local) { seen[0]!.adopts = String(local.id); aliases.set(`${type}:${String(local.id)}`, seen[0]!.id); }
|
|
365
|
+
}
|
|
339
366
|
if (!pending.size) return;
|
|
340
367
|
const declOf = (type: string): { m: DerivedManifest; decl: ResourceDecl } | undefined => {
|
|
341
368
|
for (const u of units) { const hit = Object.entries(u.manifest.resources).find(([r]) => storedType(u.manifest, r) === type); if (hit) return { m: u.manifest, decl: hit[1] }; }
|
package/src/executor.ts
CHANGED
|
@@ -243,7 +243,7 @@ function budgeted(vendor: string, execute: RemoteExecute, token: string): Remote
|
|
|
243
243
|
|
|
244
244
|
/** The call made as the root's credential is applied by the strategy (header replacement when none). */
|
|
245
245
|
function direct(origin: URL, credential: CredentialPayload, auth?: Exclude<TwinAuthStrategy, { in: 'exchange' }>, vendorHosts?: readonly HostRule[]): RemoteExecute {
|
|
246
|
-
return async ({ method, path, headers, body, responseType, presigned }) => {
|
|
246
|
+
return async ({ method, path, headers, body, responseType, presigned, issued }) => {
|
|
247
247
|
// A PRESIGNED UPLOAD URL the vendor returned: its own signature is its authorization, so the
|
|
248
248
|
// sealed credential never goes with it, whatever host it names. A host other than the root's
|
|
249
249
|
// passes the same validation the root does; the root's own origin (a twin standing in for the
|
|
@@ -288,7 +288,9 @@ function direct(origin: URL, credential: CredentialPayload, auth?: Exclude<TwinA
|
|
|
288
288
|
if (target.origin !== origin.origin || !target.pathname.startsWith(origin.pathname.replace(/\/+$/, ''))) throw new Error('remote execute: path escaped the validated origin');
|
|
289
289
|
}
|
|
290
290
|
const outbound = new Headers(headers ?? {});
|
|
291
|
-
|
|
291
|
+
// a credential the vendor issued in this perform is the request's authorization; the sealed one stays home
|
|
292
|
+
if (issued !== undefined) outbound.set('authorization', issued);
|
|
293
|
+
else for (const [name, value] of Object.entries(credential.headers)) outbound.set(name, value);
|
|
292
294
|
|
|
293
295
|
// ── THE STRATEGIES BEYOND HEADER REPLACEMENT ──────────────────────────────────────────────
|
|
294
296
|
// Applied AFTER the anchoring check above, so nothing here can move the request off the
|
|
@@ -297,7 +299,7 @@ function direct(origin: URL, credential: CredentialPayload, auth?: Exclude<TwinA
|
|
|
297
299
|
// FAIL CLOSED. A strategy with no secret must never send a request that merely LOOKS
|
|
298
300
|
// authenticated — that silence is the whole defect this exists to remove. The refusals name the
|
|
299
301
|
// strategy and the parameter, never the secret.
|
|
300
|
-
if (auth !== undefined) {
|
|
302
|
+
if (auth !== undefined && issued === undefined) {
|
|
301
303
|
const secret = credential.secret;
|
|
302
304
|
if (typeof secret !== 'string' || secret === '') {
|
|
303
305
|
throw new Error(`remote execute: the ${auth.in} credential strategy needs \`secret\` on the sealed credential, and it is absent`);
|
|
@@ -437,7 +439,7 @@ function exchangeExecute(origin: URL, sealed: CredentialPayload, auth: Extract<T
|
|
|
437
439
|
return anchored({ ...request, headers });
|
|
438
440
|
};
|
|
439
441
|
return async (request) => {
|
|
440
|
-
if (request.presigned) return anchored(request);
|
|
442
|
+
if (request.presigned || request.issued !== undefined) return anchored(request);
|
|
441
443
|
const held = current();
|
|
442
444
|
const first = await call(request, held);
|
|
443
445
|
if (!retryOn.includes(first.status)) return first;
|
package/src/index.ts
CHANGED
|
@@ -21,7 +21,7 @@ export const packOf = (
|
|
|
21
21
|
export const registerPack = (pack: PackDescriptor): PackDescriptor => registerAny(pack);
|
|
22
22
|
|
|
23
23
|
// ── the manifest ───────────────────────────────────────────────────────────────────────────────
|
|
24
|
-
export type { Actor, CoreScope, DerivedManifest, ErrorSpec, LaneRoute, LanesDecl, VendorManifest, FieldRule, ResourceDecl, ScreenDecl, SeedCall, StateField, Transition } from './derived-core.ts';
|
|
24
|
+
export type { Actor, CoreScope, DerivedManifest, PerformHook, PerformHookArgs, ErrorSpec, LaneRoute, LanesDecl, VendorManifest, FieldRule, ResourceDecl, ScreenDecl, SeedCall, StateField, Transition } from './derived-core.ts';
|
|
25
25
|
export type { ApplicationAnswer, EndpointsDecl, EventRender, EventScheme, EventsDecl, EventTransport, EventValues, EventWrite } from './events.ts';
|
|
26
26
|
export { verifyEvent } from './events.ts';
|
|
27
27
|
export { deriveStateSystem, deriveVendorStateSystem } from './derived-real.ts';
|
package/src/remote-execute.ts
CHANGED
|
@@ -16,6 +16,10 @@ export type RemoteExecuteRequest = {
|
|
|
16
16
|
* the API, and the request goes WITHOUT the sealed credential. An upload the vendor authorizes
|
|
17
17
|
* with the API token (LinkedIn's images) is not presigned and cannot use this. */
|
|
18
18
|
presigned?: true;
|
|
19
|
+
/** An Authorization the vendor ISSUED earlier in the same perform (Cloudflare's assets upload session answers a JWT
|
|
20
|
+
* that its upload calls carry instead of the API token), on the root's own origin: it is sent as the request's
|
|
21
|
+
* Authorization and the sealed credential does not go with it. */
|
|
22
|
+
issued?: string;
|
|
19
23
|
};
|
|
20
24
|
export type RemoteExecuteResponse = {
|
|
21
25
|
status: number;
|