@volter/world-core 3.0.163 → 3.0.164
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/browser-session.d.ts +7 -1
- package/dist/src/browser-session.js +21 -10
- package/dist/src/derived-core.d.ts +1 -1
- package/dist/src/derived-core.js +7 -5
- package/dist/src/derived-real.js +15 -3
- package/dist/src/events.d.ts +6 -3
- package/dist/src/events.js +52 -7
- package/dist/src/file-response.d.ts +6 -2
- package/dist/src/file-response.js +29 -5
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +2 -1
- package/dist/src/managed-database.d.ts +4 -2
- package/dist/src/managed-database.js +1 -0
- package/dist/src/media-header.d.ts +34 -0
- package/dist/src/media-header.js +129 -0
- package/dist/src/pack-fetch.d.ts +24 -3
- package/dist/src/pack-fetch.js +114 -94
- package/dist/src/request-body.d.ts +2 -3
- package/dist/src/request-body.js +2 -3
- package/dist/src/runtime.d.ts +2 -1
- package/dist/src/runtime.js +2 -1
- package/dist/src/serve-http.d.ts +1 -1
- package/dist/src/serve-http.js +93 -17
- package/package.json +1 -1
- package/src/browser-session.ts +17 -5
- package/src/derived-core.ts +9 -7
- package/src/derived-real.ts +10 -2
- package/src/events.ts +54 -9
- package/src/file-response.ts +28 -6
- package/src/index.ts +2 -1
- package/src/managed-database.ts +4 -3
- package/src/media-header.ts +108 -0
- package/src/pack-fetch.ts +107 -85
- package/src/request-body.ts +2 -3
- package/src/runtime.ts +2 -1
- package/src/serve-http.ts +70 -16
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import type { HandlerContext } from './derived-core.js';
|
|
2
|
+
import { type Person } from './people.js';
|
|
2
3
|
type Writer = Pick<HandlerContext, 'record' | 'secret' | 'occurredAt'>;
|
|
3
|
-
/** The
|
|
4
|
+
/** The value of the cookie `name` the request carries, as a browser sends it back. */
|
|
5
|
+
export declare function cookieOf(request: Request, name: string): string | undefined;
|
|
6
|
+
/** The named session cookie; malformed encodings do not name a session. */
|
|
4
7
|
export declare function browserSessionToken(request: Request, name: string): string | undefined;
|
|
8
|
+
/** The person the request's session cookie names, or undefined when nobody is signed in; with `scope`, only a session
|
|
9
|
+
* begun in it (a vendor's sandbox dashboard and its production one sign a person in apart). */
|
|
10
|
+
export declare function signedIn(ctx: HandlerContext, cookie: string, scope?: string): Person | undefined;
|
|
5
11
|
/** A new, independent sign-in, returning the cookie the vendor places on its response. */
|
|
6
12
|
export declare function startBrowserSession(ctx: Writer, email: string, cookie: string, scope?: string): Promise<string>;
|
|
7
13
|
/** End the session the request holds, then clear its cookie. Unknown cookies create no rows. */
|
|
@@ -1,18 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { personOf } from "./people.js";
|
|
2
|
+
/** The value of the cookie `name` the request carries, as a browser sends it back. */
|
|
3
|
+
export function cookieOf(request, name) {
|
|
3
4
|
for (const part of (request.headers.get('cookie') ?? '').split(';')) {
|
|
4
5
|
const at = part.indexOf('=');
|
|
5
|
-
if (at > 0 && part.slice(0, at).trim() === name)
|
|
6
|
-
|
|
7
|
-
return decodeURIComponent(part.slice(at + 1).trim());
|
|
8
|
-
}
|
|
9
|
-
catch {
|
|
10
|
-
return undefined;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
6
|
+
if (at > 0 && part.slice(0, at).trim() === name)
|
|
7
|
+
return decodeURIComponent(part.slice(at + 1).trim());
|
|
13
8
|
}
|
|
14
9
|
return undefined;
|
|
15
10
|
}
|
|
11
|
+
/** The named session cookie; malformed encodings do not name a session. */
|
|
12
|
+
export function browserSessionToken(request, name) {
|
|
13
|
+
try {
|
|
14
|
+
return cookieOf(request, name);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** The person the request's session cookie names, or undefined when nobody is signed in; with `scope`, only a session
|
|
21
|
+
* begun in it (a vendor's sandbox dashboard and its production one sign a person in apart). */
|
|
22
|
+
export function signedIn(ctx, cookie, scope) {
|
|
23
|
+
const token = cookieOf(ctx.call.request, cookie);
|
|
24
|
+
const session = token ? ctx.rowsRaw('_web_session').find((s) => s.token === token && (scope === undefined || s.scope === scope)) : undefined;
|
|
25
|
+
return session ? personOf(ctx, String(session.email)) : undefined;
|
|
26
|
+
}
|
|
16
27
|
/** A new, independent sign-in, returning the cookie the vendor places on its response. */
|
|
17
28
|
export async function startBrowserSession(ctx, email, cookie, scope) {
|
|
18
29
|
const issued = await ctx.record('_web_session_issue', { email, ...(scope !== undefined ? { scope } : {}) });
|
|
@@ -914,7 +914,7 @@ export type CoreScope = {
|
|
|
914
914
|
readOnly?: boolean;
|
|
915
915
|
/** `semantics/tenant.ts`: the tenant the caller acts in (the manifest's `tenant`), or undefined for none */
|
|
916
916
|
tenant?: (ctx: HandlerContext) => string | undefined | Promise<string | undefined>;
|
|
917
|
-
/** the application a walk stands in for:
|
|
917
|
+
/** the application a walk stands in for: vendor requests and declared events are answered by it (events.ts) */
|
|
918
918
|
application?: ApplicationStandIn;
|
|
919
919
|
};
|
|
920
920
|
/** World-owned generated values use the same private seed and log as pack secrets. */
|
package/dist/src/derived-core.js
CHANGED
|
@@ -623,9 +623,9 @@ function mintId(m, resource, root, at, snapshot) {
|
|
|
623
623
|
* increment. One space over every snowflake resource of the vendor, as its one generator's: the instant's first, or one
|
|
624
624
|
* past the greatest the World holds when that is later (a frozen clock, or several ids in one millisecond), so ids
|
|
625
625
|
* rise with time and never repeat. */
|
|
626
|
-
/** The last snowflake minted per vendor and World
|
|
626
|
+
/** The last snowflake minted per store, vendor and World: two mints before the first is written (a message
|
|
627
627
|
* and its attachment) must not meet. */
|
|
628
|
-
const lastSnowflake = new
|
|
628
|
+
const lastSnowflake = new WeakMap();
|
|
629
629
|
function mintSnowflake(m, epoch, root, at, snapshot) {
|
|
630
630
|
const ms = BigInt(Date.parse(at) || 0);
|
|
631
631
|
const base = ms > epoch ? (ms - epoch) << 22n : 0n;
|
|
@@ -640,11 +640,13 @@ function mintSnowflake(m, epoch, root, at, snapshot) {
|
|
|
640
640
|
max = v;
|
|
641
641
|
}
|
|
642
642
|
}
|
|
643
|
+
const store = getActiveWorldStore();
|
|
644
|
+
const reservations = lastSnowflake.get(store) ?? lastSnowflake.set(store, new Map()).get(store);
|
|
643
645
|
const key = `${m.service}\0${root ?? ''}`;
|
|
644
|
-
const last =
|
|
646
|
+
const last = reservations.get(key) ?? -1n;
|
|
645
647
|
const floor = max > last ? max : last;
|
|
646
648
|
const id = base > floor ? base : floor + 1n;
|
|
647
|
-
|
|
649
|
+
reservations.set(key, id);
|
|
648
650
|
return String(id);
|
|
649
651
|
}
|
|
650
652
|
/** The next KSUID for a resource (Clerk's and Svix's ids, `ksuidFrom`): the World's instant, then the type's mint
|
|
@@ -947,7 +949,7 @@ async function finishWrite(m, call, resource, row, result, operation, params, ro
|
|
|
947
949
|
const endpoint = (kind) => ({ ...kind, storedAs: storedType(m, kind.storedAs), ...(kind.joins ? { joins: Object.fromEntries(Object.entries(kind.joins).map(([name, join]) => [name, { ...join, storedAs: storedType(m, join.storedAs) }])) } : {}) });
|
|
948
950
|
const events = { ...m.events, ...(m.events.store ? { store: storedType(m, m.events.store) } : {}), endpoints: 'storedAs' in m.events.endpoints ? endpoint(m.events.endpoints) : m.events.endpoints.map(endpoint) };
|
|
949
951
|
const write = { operation, storedType: m.resources[resource]?.storedAs ?? resource, body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone() };
|
|
950
|
-
await deliverEvents(m.service, events, write, async (type) => (events.render ? events.render(await context(), write, type) : body), undefined, events.values ? async (type) => events.values(await context(), write, type) : undefined);
|
|
952
|
+
await deliverEvents(m.service, events, write, async (type) => (events.render ? events.render(await context(), write, type) : body), undefined, events.values ? async (type) => events.values(await context(), write, type) : undefined, scopeOfCall.get(call)?.application);
|
|
951
953
|
}
|
|
952
954
|
if (m.sockets?.length)
|
|
953
955
|
socketWrite(m.service, root, { operation, storedType: m.resources[resource]?.storedAs ?? resource, body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone() });
|
package/dist/src/derived-real.js
CHANGED
|
@@ -682,10 +682,10 @@ function refreshOf(units, service) {
|
|
|
682
682
|
const sent = asked.length ? `${target}${target.includes('?') ? '&' : '?'}${new URLSearchParams(asked)}` : target;
|
|
683
683
|
const answer = await execute({ method: op.method.toUpperCase(), path: sentTo(m, sent, subject), ...onLane, headers: fixedHeaders(op) });
|
|
684
684
|
// a door's subject the vendor does not show the root (another bot's private chat) stays as the World holds it
|
|
685
|
-
if (!rooted.has(key) && answer.status >= 400 && answer.status !== 429)
|
|
686
|
-
continue;
|
|
687
685
|
if (answer.status === 401)
|
|
688
686
|
throw new Error(`401: the vendor refused the root's credential (${op.method.toUpperCase()} ${path}); nothing folded`);
|
|
687
|
+
if (!rooted.has(key) && answer.status >= 400 && answer.status !== 429)
|
|
688
|
+
continue;
|
|
689
689
|
if (answer.status === 403) {
|
|
690
690
|
unread.push({ type, path, status: 403 });
|
|
691
691
|
refused = true;
|
|
@@ -700,7 +700,19 @@ function refreshOf(units, service) {
|
|
|
700
700
|
continue;
|
|
701
701
|
if (answer.status >= 400 || refusedIn(m, parse(answer)))
|
|
702
702
|
throw new Error(`${m.vendor}: ${resource} refresh ${op.id} (${op.method.toUpperCase()} ${op.path}): ${answer.status}: ${refusalWords(m, parse(answer))}`);
|
|
703
|
-
const
|
|
703
|
+
const parsed = parse(answer);
|
|
704
|
+
let full;
|
|
705
|
+
if (op.answers?.list) {
|
|
706
|
+
const items = itemsOf(op, parsed);
|
|
707
|
+
const matching = items.find((item) => (scopeIdOf(scope, item) ?? storedIdOf(decl, item)) === vendorId);
|
|
708
|
+
if (!matching && items.length)
|
|
709
|
+
throw new Error(`${m.vendor}: ${resource} refresh ${op.id} omitted the requested subject; nothing folded`);
|
|
710
|
+
if (!matching)
|
|
711
|
+
continue;
|
|
712
|
+
full = matching;
|
|
713
|
+
}
|
|
714
|
+
else
|
|
715
|
+
full = object(op.answers?.key ? at(parsed, op.answers.key) : parsed) ?? {};
|
|
704
716
|
const local = m.worldFields?.[resource] ?? {};
|
|
705
717
|
const fields = Object.fromEntries(Object.entries(full).filter(([k]) => !(k in local)));
|
|
706
718
|
if (decl.parent) {
|
package/dist/src/events.d.ts
CHANGED
|
@@ -164,6 +164,8 @@ export declare function eventTypeOf(decl: Pick<EventsDecl, 'types' | 'known'>, o
|
|
|
164
164
|
/** Whether an endpoint's list of event types takes a type: every type when the list is empty, `*` or `<family>.*`
|
|
165
165
|
* its patterns, else the type itself. */
|
|
166
166
|
export declare function takesEvent(list: unknown, type: string): boolean;
|
|
167
|
+
/** Await already-scheduled first dispatches and consume their retained persistence failures. */
|
|
168
|
+
export declare function settleEventDeliveries(root: string): Promise<void>;
|
|
167
169
|
/** Who watches signed packets before dispatch (a walk reading each as its receiver would). This is not evidence of
|
|
168
170
|
* transport or acceptance. Set and restored by the walk, as its World store is; unset, nothing watches. */
|
|
169
171
|
export type DeliveryObserver = (delivery: EventDelivery & {
|
|
@@ -179,13 +181,14 @@ export type ApplicationAnswer = {
|
|
|
179
181
|
body: string;
|
|
180
182
|
missed?: 'timeout' | 'unreachable';
|
|
181
183
|
};
|
|
182
|
-
/** Who answers
|
|
184
|
+
/** Who answers a vendor's requests to the application before the World's application route does, given to a
|
|
183
185
|
* pack's fetch (`PackFetchOptions.application`) and belonging to that fetch alone: an answer it gives is the
|
|
184
186
|
* application's; `'unreachable'` is a question nobody answers (a walk's life, a sealed World, takes no other); undefined
|
|
185
187
|
* is a question it does not take, which goes on as any other does: the application route, the World's egress rule, the
|
|
186
188
|
* network. A walk's stand-in answers every question (its life's answers, else unreachable); a hosted World's router
|
|
187
189
|
* answers only what it can answer truthfully (WorldDoors.askInWorld) and leaves the rest. `bodyError` models a
|
|
188
|
-
* disconnect after the response headers; a caller needing the complete body receives no complete answer
|
|
190
|
+
* disconnect after the response headers; a caller needing the complete body receives no complete answer, while an
|
|
191
|
+
* event dispatch acknowledges the response status without reading its body. */
|
|
189
192
|
export type ApplicationStandIn = (url: string, init: RequestInit) => Promise<{
|
|
190
193
|
status: number;
|
|
191
194
|
headers?: Record<string, string>;
|
|
@@ -198,4 +201,4 @@ export type ApplicationStandIn = (url: string, init: RequestInit) => Promise<{
|
|
|
198
201
|
export declare function askApplication(url: string, init: RequestInit, within: number, standIn?: ApplicationStandIn): Promise<ApplicationAnswer>;
|
|
199
202
|
/** Every event a write sends, rendered, stored when the vendor keeps its events, signed, delivered to each endpoint
|
|
200
203
|
* that takes it, and recorded. */
|
|
201
|
-
export declare function deliverEvents(service: string, decl: EventsDecl, write: EventWrite, data: (type: string) => Promise<Record<string, unknown>>, transport?: EventTransport, values?: (type: string) => Promise<Record<string, unknown
|
|
204
|
+
export declare function deliverEvents(service: string, decl: EventsDecl, write: EventWrite, data: (type: string) => Promise<Record<string, unknown>>, transport?: EventTransport, values?: (type: string) => Promise<Record<string, unknown>>, application?: ApplicationStandIn): Promise<EventDelivery[]>;
|
package/dist/src/events.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// Imports stay free of side effects: the delivery's transport (Node's http, the World's egress policy) is loaded when a
|
|
8
8
|
// delivery is made, never when a browser client imports the kernel.
|
|
9
9
|
import { createHash, createHmac } from 'node:crypto';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
10
11
|
import { applyTwinWrite, applyTwinWriteAtomic, twinResources } from "./serve.js";
|
|
11
12
|
import { equalSecrets as same, ksuidFrom } from "./signing.js";
|
|
12
13
|
import { deliveryTraceHeaders } from "./trace-context.js";
|
|
@@ -223,6 +224,28 @@ const worldTransport = async (url, body, headers) => {
|
|
|
223
224
|
return 0;
|
|
224
225
|
}
|
|
225
226
|
};
|
|
227
|
+
const pendingDeliveries = new WeakMap();
|
|
228
|
+
function forgetDeliveries(store, root, deliveries) {
|
|
229
|
+
const roots = pendingDeliveries.get(store), pending = roots?.get(root);
|
|
230
|
+
if (!pending)
|
|
231
|
+
return;
|
|
232
|
+
for (const delivery of deliveries)
|
|
233
|
+
pending.delete(delivery);
|
|
234
|
+
if (!pending.size)
|
|
235
|
+
roots.delete(root);
|
|
236
|
+
if (!roots.size)
|
|
237
|
+
pendingDeliveries.delete(store);
|
|
238
|
+
}
|
|
239
|
+
/** Await already-scheduled first dispatches and consume their retained persistence failures. */
|
|
240
|
+
export async function settleEventDeliveries(root) {
|
|
241
|
+
const store = getActiveWorldStore(), path = resolve(root);
|
|
242
|
+
const deliveries = [...(pendingDeliveries.get(store)?.get(path) ?? [])];
|
|
243
|
+
const outcomes = await Promise.allSettled(deliveries);
|
|
244
|
+
forgetDeliveries(store, path, deliveries);
|
|
245
|
+
const failures = outcomes.flatMap((outcome) => outcome.status === 'rejected' ? [outcome.reason] : []);
|
|
246
|
+
if (failures.length)
|
|
247
|
+
throw new AggregateError(failures, 'Declared-event dispatch failed');
|
|
248
|
+
}
|
|
226
249
|
let deliveryObserver;
|
|
227
250
|
export function setDeliveryObserver(observer) {
|
|
228
251
|
const previous = deliveryObserver;
|
|
@@ -266,12 +289,16 @@ export async function askApplication(url, init, within, standIn) {
|
|
|
266
289
|
}
|
|
267
290
|
/** Every event a write sends, rendered, stored when the vendor keeps its events, signed, delivered to each endpoint
|
|
268
291
|
* that takes it, and recorded. */
|
|
269
|
-
export async function deliverEvents(service, decl, write, data, transport = worldTransport, values) {
|
|
292
|
+
export async function deliverEvents(service, decl, write, data, transport = worldTransport, values, application) {
|
|
270
293
|
if (write.storedType === decl.store)
|
|
271
294
|
return [];
|
|
295
|
+
const dispatch = application ? async (url, body, headers) => {
|
|
296
|
+
const answer = await application(url, { method: 'POST', headers: { 'content-type': 'application/json', ...headers }, body });
|
|
297
|
+
return answer === undefined ? transport(url, body, headers) : answer === 'unreachable' ? 0 : answer.status;
|
|
298
|
+
} : transport;
|
|
272
299
|
const sent = [];
|
|
273
300
|
for (const type of eventTypesOf(decl, write.operation))
|
|
274
|
-
sent.push(...(await deliverEventType(service, decl, write, data,
|
|
301
|
+
sent.push(...(await deliverEventType(service, decl, write, data, dispatch, values, type)));
|
|
275
302
|
return sent;
|
|
276
303
|
}
|
|
277
304
|
/** One event type a write sends: to the live endpoints that take it, stored when the vendor keeps its events. */
|
|
@@ -364,11 +391,29 @@ async function deliverEventType(service, decl, write, data, transport, values, t
|
|
|
364
391
|
const store = getActiveWorldStore(), deliveryRoot = worldPaths(service, write.root).root;
|
|
365
392
|
for (const d of sent)
|
|
366
393
|
deliveryObserver?.({ ...d, service });
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
394
|
+
const roots = pendingDeliveries.get(store) ?? new Map();
|
|
395
|
+
pendingDeliveries.set(store, roots);
|
|
396
|
+
const pending = roots.get(deliveryRoot) ?? new Set();
|
|
397
|
+
roots.set(deliveryRoot, pending);
|
|
398
|
+
for (const packet of recorded) {
|
|
399
|
+
let finish;
|
|
400
|
+
let fail;
|
|
401
|
+
const dispatched = new Promise((done, reject) => { finish = done; fail = reject; });
|
|
402
|
+
// Live writes return before dispatch; an explicit settlement reports persistence failures.
|
|
403
|
+
void dispatched.catch(() => { });
|
|
404
|
+
pending.add(dispatched);
|
|
405
|
+
setTimeout(async () => {
|
|
406
|
+
try {
|
|
407
|
+
await withWorldStore(store, () => sendRecordedEvent(service, packet.type, packet.id, deliveryRoot, transport, trace));
|
|
408
|
+
finish();
|
|
409
|
+
forgetDeliveries(store, deliveryRoot, [dispatched]);
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
// Keep the rejection until explicit settlement consumes it. The queued/sending claim remains for
|
|
413
|
+
// native recovery; this must not fail the already-committed vendor write or cause a blind resend.
|
|
414
|
+
fail(error);
|
|
415
|
+
}
|
|
372
416
|
}, 0);
|
|
417
|
+
}
|
|
373
418
|
return sent;
|
|
374
419
|
}
|
|
@@ -5,6 +5,10 @@ export declare function fileResponse(path: string, init?: ResponseInit): Respons
|
|
|
5
5
|
* Stored bytes as a response, honouring a single `Range: bytes=` as RFC 9110 §14 has it: every satisfiable range is a
|
|
6
6
|
* 206 with its Content-Range, `bytes=0-` (the whole body) included; a range that does not parse, a first-byte-pos past
|
|
7
7
|
* its last-byte-pos (`bytes=5-2`) included, is ignored and the whole body is a 200; a well-formed range that starts past
|
|
8
|
-
* the end (any range of an empty body) is a 416.
|
|
8
|
+
* the end (any range of an empty body) is a 416. Lazy sources supply inclusive ranges and stream in bounded reads.
|
|
9
|
+
* `nosniff` stops a browser second-guessing the content type the vendor gave.
|
|
9
10
|
*/
|
|
10
|
-
export declare function bytesResponse(request: Request, bytes: Uint8Array
|
|
11
|
+
export declare function bytesResponse(request: Request, bytes: Uint8Array | {
|
|
12
|
+
size: number;
|
|
13
|
+
range(start: number, end: number): Promise<Uint8Array | null>;
|
|
14
|
+
}, contentType: string, cacheControl?: string): Response;
|
|
@@ -24,10 +24,11 @@ export function fileResponse(path, init) {
|
|
|
24
24
|
* Stored bytes as a response, honouring a single `Range: bytes=` as RFC 9110 §14 has it: every satisfiable range is a
|
|
25
25
|
* 206 with its Content-Range, `bytes=0-` (the whole body) included; a range that does not parse, a first-byte-pos past
|
|
26
26
|
* its last-byte-pos (`bytes=5-2`) included, is ignored and the whole body is a 200; a well-formed range that starts past
|
|
27
|
-
* the end (any range of an empty body) is a 416.
|
|
27
|
+
* the end (any range of an empty body) is a 416. Lazy sources supply inclusive ranges and stream in bounded reads.
|
|
28
|
+
* `nosniff` stops a browser second-guessing the content type the vendor gave.
|
|
28
29
|
*/
|
|
29
30
|
export function bytesResponse(request, bytes, contentType, cacheControl = 'public, max-age=604800, immutable') {
|
|
30
|
-
const total = bytes.length;
|
|
31
|
+
const total = bytes instanceof Uint8Array ? bytes.length : bytes.size;
|
|
31
32
|
const base = { 'content-type': contentType, 'x-content-type-options': 'nosniff', 'accept-ranges': 'bytes', 'cache-control': cacheControl };
|
|
32
33
|
const range = /^bytes=(\d*)-(\d*)$/.exec((request.headers.get('range') ?? '').trim());
|
|
33
34
|
let start = 0;
|
|
@@ -47,9 +48,32 @@ export function bytesResponse(request, bytes, contentType, cacheControl = 'publi
|
|
|
47
48
|
// every satisfiable range is answered as one, the whole body's `bytes=0-` included: a media player (Safari's) reads a
|
|
48
49
|
// 206 with its Content-Range as the server taking ranges, and a 200 as one it cannot seek in
|
|
49
50
|
const partial = valid;
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
let body = null;
|
|
52
|
+
if (request.method !== 'HEAD') {
|
|
53
|
+
if (bytes instanceof Uint8Array)
|
|
54
|
+
body = new Blob([bytes.slice(start, end + 1)]);
|
|
55
|
+
else {
|
|
56
|
+
let at = start;
|
|
57
|
+
body = new ReadableStream({
|
|
58
|
+
async pull(controller) {
|
|
59
|
+
if (at > end) {
|
|
60
|
+
controller.close();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const last = Math.min(at + 8 * 1024 * 1024 - 1, end);
|
|
64
|
+
const chunk = await bytes.range(at, last);
|
|
65
|
+
if (!chunk) {
|
|
66
|
+
controller.error(new Error('stored bytes are gone'));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
controller.enqueue(chunk);
|
|
70
|
+
at = last + 1;
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return new Response(body, {
|
|
52
76
|
status: partial ? 206 : 200,
|
|
53
|
-
headers: { ...base, 'content-length': String(
|
|
77
|
+
headers: { ...base, 'content-length': String(end + 1 - start), ...(partial ? { 'content-range': `bytes ${start}-${end}/${total}` } : {}) },
|
|
54
78
|
});
|
|
55
79
|
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -67,6 +67,7 @@ export * as protobuf from './protobuf.js';
|
|
|
67
67
|
export * as grpc from './grpc-wire.js';
|
|
68
68
|
export { acceptH2Connection, type H2Handler, type Http1Handler } from './h2.js';
|
|
69
69
|
export { placeholderPng } from './placeholder-image.js';
|
|
70
|
+
export { imageDimensions, readImageHeader, readMp4Header } from './media-header.js';
|
|
70
71
|
export { presignSigV4, signSigV4, verifySigV4, type SigV4Verdict } from './sigv4.js';
|
|
71
72
|
export * as redis from './redis/index.js';
|
|
72
73
|
export * as mysql from './mysql/index.js';
|
|
@@ -76,7 +77,7 @@ export * as git from './git/index.js';
|
|
|
76
77
|
export { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError } from './scenario.js';
|
|
77
78
|
export type { PackScenarioAdapter, ScenarioDecision, ScenarioDocument, ScenarioExtractorSpec, ScenarioFault, ScenarioFaultResult, ScenarioFeatures, ScenarioHandler, ScenarioMatcher, ScenarioMissRecord, ScenarioStatus } from './scenario.js';
|
|
78
79
|
export { verifyAttestationBundle } from './attestation.js';
|
|
79
|
-
export { browserSessionToken, startBrowserSession, endBrowserSession } from './browser-session.js';
|
|
80
|
+
export { cookieOf, signedIn, browserSessionToken, startBrowserSession, endBrowserSession } from './browser-session.js';
|
|
80
81
|
export { readHostPort } from './host-port.js';
|
|
81
82
|
export { assertBrowserName, browserOrigin, validateBrowserState, validateBrowserStorageState, validateBrowserContext, browserAcceptLanguage, cookieMatchesHost, type BrowserCookie, type BrowserState, type BrowserStorageState, type BrowserContext, type BrowserIndexedDB } from './browser-state.js';
|
|
82
83
|
export * as managedOriginalInputs from './managed-original-inputs.js';
|
package/dist/src/index.js
CHANGED
|
@@ -59,6 +59,7 @@ export * as protobuf from "./protobuf.js";
|
|
|
59
59
|
export * as grpc from "./grpc-wire.js";
|
|
60
60
|
export { acceptH2Connection } from "./h2.js";
|
|
61
61
|
export { placeholderPng } from "./placeholder-image.js";
|
|
62
|
+
export { imageDimensions, readImageHeader, readMp4Header } from "./media-header.js";
|
|
62
63
|
export { presignSigV4, signSigV4, verifySigV4 } from "./sigv4.js";
|
|
63
64
|
export * as redis from "./redis/index.js";
|
|
64
65
|
export * as mysql from "./mysql/index.js";
|
|
@@ -68,7 +69,7 @@ export * as git from "./git/index.js";
|
|
|
68
69
|
// ── the scenario grammar (architecture, "Behaviour") ───────────────────────────────────────────
|
|
69
70
|
export { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError } from "./scenario.js";
|
|
70
71
|
export { verifyAttestationBundle } from "./attestation.js";
|
|
71
|
-
export { browserSessionToken, startBrowserSession, endBrowserSession } from "./browser-session.js";
|
|
72
|
+
export { cookieOf, signedIn, browserSessionToken, startBrowserSession, endBrowserSession } from "./browser-session.js";
|
|
72
73
|
export { readHostPort } from "./host-port.js";
|
|
73
74
|
export { assertBrowserName, browserOrigin, validateBrowserState, validateBrowserStorageState, validateBrowserContext, browserAcceptLanguage, cookieMatchesHost } from "./browser-state.js";
|
|
74
75
|
export * as managedOriginalInputs from "./managed-original-inputs.js";
|
|
@@ -76,9 +76,11 @@ export declare function managedDatabase(url: string, options?: {
|
|
|
76
76
|
* walk/journey-kit.ts), its statements sent as its client sends them: a text alone as one simple Query (a migration
|
|
77
77
|
* file, a `BEGIN`), a text with values as one extended-protocol statement, the values bind parameters. A transaction it
|
|
78
78
|
* opens stays open across its statements until it ends it, as on the application's own connection. Each statement's
|
|
79
|
-
* answer is its results in Postgres's text format,
|
|
79
|
+
* answer is its results in Postgres's text format, with native column type OIDs retained beside the values. */
|
|
80
80
|
export type DatabaseSession = {
|
|
81
|
-
query(text: string, values?: ReadonlyArray<string | null>): Promise<StatementResult
|
|
81
|
+
query(text: string, values?: ReadonlyArray<string | null>): Promise<Array<StatementResult & {
|
|
82
|
+
fieldTypes: number[];
|
|
83
|
+
}>>;
|
|
82
84
|
end(): Promise<void>;
|
|
83
85
|
};
|
|
84
86
|
/** The World's Postgres at `url` as one connection held until `end()`. */
|
|
@@ -255,6 +255,7 @@ export async function databaseSession(url) {
|
|
|
255
255
|
const answer = (await client.query({ text, ...(values === undefined ? {} : { values }), rowMode: 'array', types: raw }));
|
|
256
256
|
return (Array.isArray(answer) ? answer : [answer]).map((r) => ({
|
|
257
257
|
fields: (r.fields ?? []).map((f) => f.name),
|
|
258
|
+
fieldTypes: (r.fields ?? []).map((f) => f.dataTypeID),
|
|
258
259
|
rows: r.rows ?? [],
|
|
259
260
|
command: [r.command ?? '', r.command === 'INSERT' ? '0' : '', r.rowCount ?? ''].filter((part) => part !== '').join(' '),
|
|
260
261
|
}));
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
type ImageType = 'image/png' | 'image/gif' | 'image/jpeg' | 'image/webp';
|
|
2
|
+
type Dimensions = {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
};
|
|
6
|
+
type ImageHeader = Dimensions & {
|
|
7
|
+
jpeg?: {
|
|
8
|
+
frameEnd: number;
|
|
9
|
+
nonMarkerBytes: boolean;
|
|
10
|
+
standaloneMarkers: boolean;
|
|
11
|
+
fillBytes: boolean;
|
|
12
|
+
shortSegments: boolean;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
/** Dimensions in a caller-declared image format, or undefined for an incomplete header. */
|
|
16
|
+
export declare function imageDimensions(bytes: Uint8Array, type: ImageType): ImageHeader | undefined;
|
|
17
|
+
/** An image's signature and dimensions, without deciding whether a vendor accepts that format or size. */
|
|
18
|
+
export declare function readImageHeader(bytes: Uint8Array): (ImageHeader & {
|
|
19
|
+
type: ImageType;
|
|
20
|
+
}) | undefined;
|
|
21
|
+
/** ISO BMFF's bounded top-level boxes and movie headers, with raw duration and 16.16 track dimensions. */
|
|
22
|
+
export declare function readMp4Header(bytes: Uint8Array): {
|
|
23
|
+
fileType: boolean;
|
|
24
|
+
hasMediaData: boolean;
|
|
25
|
+
movies: Array<{
|
|
26
|
+
headers: Array<{
|
|
27
|
+
timescale: number;
|
|
28
|
+
duration: number;
|
|
29
|
+
payloadSize: number;
|
|
30
|
+
}>;
|
|
31
|
+
tracks: Dimensions[];
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const tag = (bytes, at) => String.fromCharCode(...bytes.subarray(at, at + 4));
|
|
2
|
+
/** Dimensions in a caller-declared image format, or undefined for an incomplete header. */
|
|
3
|
+
export function imageDimensions(bytes, type) {
|
|
4
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
5
|
+
if (type === 'image/png')
|
|
6
|
+
return bytes.length >= 24 ? { width: view.getUint32(16), height: view.getUint32(20) } : undefined;
|
|
7
|
+
if (type === 'image/gif')
|
|
8
|
+
return bytes.length >= 10 ? { width: view.getUint16(6, true), height: view.getUint16(8, true) } : undefined;
|
|
9
|
+
if (type === 'image/webp') {
|
|
10
|
+
if (bytes.length < 30)
|
|
11
|
+
return undefined;
|
|
12
|
+
const chunk = tag(bytes, 12);
|
|
13
|
+
if (chunk === 'VP8 ')
|
|
14
|
+
return { width: view.getUint16(26, true) & 0x3fff, height: view.getUint16(28, true) & 0x3fff };
|
|
15
|
+
if (chunk === 'VP8L') {
|
|
16
|
+
const bits = view.getUint32(21, true);
|
|
17
|
+
return { width: 1 + (bits & 0x3fff), height: 1 + ((bits >>> 14) & 0x3fff) };
|
|
18
|
+
}
|
|
19
|
+
if (chunk === 'VP8X') {
|
|
20
|
+
const u24 = (at) => bytes[at] | (bytes[at + 1] << 8) | (bytes[at + 2] << 16);
|
|
21
|
+
return { width: 1 + u24(24), height: 1 + u24(27) };
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
let at = 2;
|
|
26
|
+
const jpeg = { frameEnd: 0, nonMarkerBytes: false, standaloneMarkers: false, fillBytes: false, shortSegments: false };
|
|
27
|
+
while (at + 4 <= bytes.length) {
|
|
28
|
+
if (bytes[at] !== 0xff) {
|
|
29
|
+
jpeg.nonMarkerBytes = true;
|
|
30
|
+
at += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const marker = bytes[at + 1];
|
|
34
|
+
if (marker === 0xff) {
|
|
35
|
+
jpeg.fillBytes = true;
|
|
36
|
+
at += 1;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {
|
|
40
|
+
jpeg.standaloneMarkers = true;
|
|
41
|
+
at += 2;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const length = view.getUint16(at + 2);
|
|
45
|
+
if (length < 2)
|
|
46
|
+
jpeg.shortSegments = true;
|
|
47
|
+
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
48
|
+
return at + 9 <= bytes.length ? { width: view.getUint16(at + 7), height: view.getUint16(at + 5), jpeg: { ...jpeg, frameEnd: at + 9 } } : undefined;
|
|
49
|
+
}
|
|
50
|
+
if (marker === 0xda)
|
|
51
|
+
return undefined;
|
|
52
|
+
at += 2 + length;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
/** An image's signature and dimensions, without deciding whether a vendor accepts that format or size. */
|
|
57
|
+
export function readImageHeader(bytes) {
|
|
58
|
+
let type;
|
|
59
|
+
if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47)
|
|
60
|
+
type = 'image/png';
|
|
61
|
+
else if (tag(bytes, 0).startsWith('GIF'))
|
|
62
|
+
type = 'image/gif';
|
|
63
|
+
else if (bytes[0] === 0xff && bytes[1] === 0xd8)
|
|
64
|
+
type = 'image/jpeg';
|
|
65
|
+
else if (tag(bytes, 0) === 'RIFF' && tag(bytes, 8) === 'WEBP')
|
|
66
|
+
type = 'image/webp';
|
|
67
|
+
else
|
|
68
|
+
return undefined;
|
|
69
|
+
const dimensions = imageDimensions(bytes, type);
|
|
70
|
+
return dimensions ? { type, ...dimensions } : undefined;
|
|
71
|
+
}
|
|
72
|
+
/** ISO BMFF's bounded top-level boxes and movie headers, with raw duration and 16.16 track dimensions. */
|
|
73
|
+
export function readMp4Header(bytes) {
|
|
74
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
75
|
+
function* boxes(from, to) {
|
|
76
|
+
let at = from;
|
|
77
|
+
while (at + 8 <= to) {
|
|
78
|
+
let size = view.getUint32(at);
|
|
79
|
+
const type = tag(bytes, at + 4);
|
|
80
|
+
let header = 8;
|
|
81
|
+
if (size === 1) {
|
|
82
|
+
if (at + 16 > to)
|
|
83
|
+
return;
|
|
84
|
+
size = Number(view.getBigUint64(at + 8));
|
|
85
|
+
header = 16;
|
|
86
|
+
}
|
|
87
|
+
if (size === 0)
|
|
88
|
+
size = to - at;
|
|
89
|
+
if (!Number.isSafeInteger(size) || size < header || size > to - at)
|
|
90
|
+
return;
|
|
91
|
+
yield { type, start: at + header, end: at + size };
|
|
92
|
+
at += size;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const result = { fileType: bytes.length >= 8 && tag(bytes, 4) === 'ftyp', hasMediaData: false, movies: [] };
|
|
96
|
+
for (const box of boxes(0, bytes.length)) {
|
|
97
|
+
if (box.type === 'mdat' && box.end > box.start)
|
|
98
|
+
result.hasMediaData = true;
|
|
99
|
+
if (box.type !== 'moov')
|
|
100
|
+
continue;
|
|
101
|
+
const movie = { headers: [], tracks: [] };
|
|
102
|
+
result.movies.push(movie);
|
|
103
|
+
for (const child of boxes(box.start, box.end)) {
|
|
104
|
+
const { type, start, end } = child;
|
|
105
|
+
if (type === 'mvhd' && start < end) {
|
|
106
|
+
const version = bytes[start];
|
|
107
|
+
const payloadSize = end - start;
|
|
108
|
+
if (version === 0 && payloadSize >= 20)
|
|
109
|
+
movie.headers.push({ timescale: view.getUint32(start + 12), duration: view.getUint32(start + 16), payloadSize });
|
|
110
|
+
if (version === 1 && payloadSize >= 32)
|
|
111
|
+
movie.headers.push({ timescale: view.getUint32(start + 20), duration: Number(view.getBigUint64(start + 24)), payloadSize });
|
|
112
|
+
}
|
|
113
|
+
if (type !== 'trak')
|
|
114
|
+
continue;
|
|
115
|
+
for (const track of boxes(start, end)) {
|
|
116
|
+
if (track.type !== 'tkhd' || track.start >= track.end)
|
|
117
|
+
continue;
|
|
118
|
+
const version = bytes[track.start];
|
|
119
|
+
const at = version === 1 ? track.start + 88 : version === 0 ? track.start + 76 : track.end;
|
|
120
|
+
if (at + 8 > track.end)
|
|
121
|
+
continue;
|
|
122
|
+
const width = view.getUint32(at) / 65536;
|
|
123
|
+
const height = view.getUint32(at + 4) / 65536;
|
|
124
|
+
movie.tracks.push({ width, height });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
package/dist/src/pack-fetch.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type DerivedManifest, type Handler, type HandlerContext, type ScreenDecl, type VendorManifest } from './derived-core.js';
|
|
2
|
-
import { type DerivedFetch, type DerivedSurface, type BoardFrame } from './derived.js';
|
|
1
|
+
import { type DerivedManifest, type Handler, type HandlerContext, type LanesDecl, type ScreenDecl, type VendorManifest } from './derived-core.js';
|
|
2
|
+
import { compileSurface, type DerivedFetch, type DerivedSurface, type BoardFrame } from './derived.js';
|
|
3
3
|
import { type PackScenarioAdapter } from './scenario.js';
|
|
4
4
|
import type { ApplicationStandIn } from './events.js';
|
|
5
5
|
import { type SocketEngine } from './sockets.js';
|
|
@@ -83,13 +83,34 @@ export type PackFetchOptions = {
|
|
|
83
83
|
clock?: () => string;
|
|
84
84
|
database?: string;
|
|
85
85
|
scenarioPath?: string;
|
|
86
|
-
/** the application a walk stands in for
|
|
86
|
+
/** the application a walk stands in for: this fetch's requests and declared events are answered by it */
|
|
87
87
|
application?: ApplicationStandIn;
|
|
88
88
|
};
|
|
89
|
+
/** The host a request is for: the vendor host the World's injector names, else its own. */
|
|
90
|
+
export declare const hostOf: (request: Request) => string;
|
|
89
91
|
/** Where a workspace asked `?show=<type>:<id>` opens the record (architecture, "Workspace"): the built workspace page
|
|
90
92
|
* that shows that type, its parameter the id; else the one that lists the type; else the workspace's home (its root
|
|
91
93
|
* page). Every other parameter a page's path names is the asking page's, and a page that cannot be filled is passed
|
|
92
94
|
* over. Undefined when nothing is asked; the path (with no `show`) otherwise. */
|
|
93
95
|
export declare function shownRecord(url: URL, workspaces: ReadonlyArray<ScreenDecl>, params: Record<string, string>): string | undefined;
|
|
96
|
+
/** A declared World door, matched by the same normalized path for serving and response ownership. */
|
|
97
|
+
export declare function worldDoorMatcher<Door extends {
|
|
98
|
+
method: string;
|
|
99
|
+
path: string;
|
|
100
|
+
}>(doors: ReadonlyArray<Door>): (method: string, path: string) => {
|
|
101
|
+
door: Door;
|
|
102
|
+
params: Record<string, string>;
|
|
103
|
+
} | undefined;
|
|
104
|
+
/** A request with its path's repeated slashes merged, as a front that merges them routes it (the manifest's
|
|
105
|
+
* `mergeSlashes`); one with none is itself. */
|
|
106
|
+
export declare function slashesMerged(request: Request): Request;
|
|
107
|
+
/** The selected unit's request transformation, shared by serving and response judgment. */
|
|
108
|
+
export declare function unitRequestRouter(m: Pick<DerivedManifest, 'pathPrefix' | 'basePathAlias' | 'pathFromHost' | 'hostParams' | 'headerParams'>, surface: DerivedSurface, routes: ReturnType<typeof compileSurface>): (request: Request) => Request;
|
|
109
|
+
/** A pack's HTTP surface, as the kernel serves it from the pack's parts. */
|
|
94
110
|
export declare function createPackFetch(parts: PackParts, options?: PackFetchOptions): DerivedFetch;
|
|
111
|
+
/** The lane selected by the vendor gateway, shared by serving and response judgment. */
|
|
112
|
+
export declare function laneSelector(decl: LanesDecl, lanes: Record<string, {
|
|
113
|
+
manifest: LanePart['manifest'];
|
|
114
|
+
fetch: Pick<LanePart['fetch'], 'serves' | 'contentHosts'>;
|
|
115
|
+
}>, rootServes?: (method: string, path: string) => boolean): (request: Request) => string | undefined;
|
|
95
116
|
export declare function createVendorFetch(manifest: VendorManifest, lanes: Record<string, LanePart>): DerivedFetch;
|