@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.
@@ -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 named cookie, without interpreting any other vendor's cookie. */
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
- /** The named cookie, without interpreting any other vendor's cookie. */
2
- export function browserSessionToken(request, name) {
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
- try {
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: what the vendor asks it (`ctx.ask`) is answered by it (events.ts) */
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. */
@@ -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 in this process: two mints before the first is written (a message
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 Map();
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 = lastSnowflake.get(key) ?? -1n;
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
- lastSnowflake.set(key, id);
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() });
@@ -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 full = object(op.answers?.key ? at(parse(answer), op.answers.key) : parse(answer)) ?? {};
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) {
@@ -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 what the vendor asks the application (`ctx.ask`) before the World's application route does, given to a
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>>): Promise<EventDelivery[]>;
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[]>;
@@ -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, transport, values, type)));
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
- for (const packet of recorded)
368
- setTimeout(() => {
369
- // Persistence failure leaves a queued/sending claim for explicit recovery;
370
- // it must not fail the already-committed vendor write or cause a blind resend.
371
- void withWorldStore(store, () => sendRecordedEvent(service, packet.type, packet.id, deliveryRoot, transport, trace)).catch(() => { });
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. `nosniff` stops a browser second-guessing the content type the vendor gave.
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, contentType: string, cacheControl?: string): Response;
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. `nosniff` stops a browser second-guessing the content type the vendor gave.
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
- const body = bytes.slice(start, end + 1);
51
- return new Response(request.method === 'HEAD' ? null : new Blob([body]), {
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(body.length), ...(partial ? { 'content-range': `bytes ${start}-${end}/${total}` } : {}) },
77
+ headers: { ...base, 'content-length': String(end + 1 - start), ...(partial ? { 'content-range': `bytes ${start}-${end}/${total}` } : {}) },
54
78
  });
55
79
  }
@@ -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, untyped. */
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
+ }
@@ -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 (a life's `application` answers): this fetch's `ctx.ask` is answered by it */
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;