@volter/world-core 3.0.163 → 3.0.165

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.
Files changed (74) hide show
  1. package/dist/generated/pack-facts.json +44 -3660
  2. package/dist/pack-facts.cjs +4 -4
  3. package/dist/src/browser-session.d.ts +7 -1
  4. package/dist/src/browser-session.js +21 -10
  5. package/dist/src/browser.d.ts +4 -0
  6. package/dist/src/browser.js +10 -0
  7. package/dist/src/derived-core.d.ts +46 -6
  8. package/dist/src/derived-core.js +60 -13
  9. package/dist/src/derived-real.js +159 -5
  10. package/dist/src/derived.d.ts +1 -1
  11. package/dist/src/events.d.ts +6 -3
  12. package/dist/src/events.js +52 -7
  13. package/dist/src/executor.d.ts +2 -2
  14. package/dist/src/executor.js +13 -0
  15. package/dist/src/file-response.d.ts +6 -2
  16. package/dist/src/file-response.js +29 -5
  17. package/dist/src/git/index.d.ts +1 -0
  18. package/dist/src/git/index.js +1 -0
  19. package/dist/src/git/smart-http-client.d.ts +28 -0
  20. package/dist/src/git/smart-http-client.js +95 -0
  21. package/dist/src/git/smart-http.d.ts +3 -0
  22. package/dist/src/git/smart-http.js +9 -0
  23. package/dist/src/head.d.ts +16 -1
  24. package/dist/src/head.js +52 -7
  25. package/dist/src/html.d.ts +29 -0
  26. package/dist/src/html.js +64 -0
  27. package/dist/src/index.d.ts +5 -2
  28. package/dist/src/index.js +5 -1
  29. package/dist/src/managed-database.d.ts +4 -2
  30. package/dist/src/managed-database.js +1 -0
  31. package/dist/src/media-header.d.ts +34 -0
  32. package/dist/src/media-header.js +129 -0
  33. package/dist/src/pack-fetch.d.ts +24 -3
  34. package/dist/src/pack-fetch.js +155 -96
  35. package/dist/src/pack-paths.d.ts +2 -3
  36. package/dist/src/pack-paths.js +3 -13
  37. package/dist/src/packRegistry.d.ts +17 -1
  38. package/dist/src/request-body.d.ts +2 -3
  39. package/dist/src/request-body.js +2 -3
  40. package/dist/src/runtime.d.ts +4 -1
  41. package/dist/src/runtime.js +5 -1
  42. package/dist/src/serve-http.d.ts +2 -2
  43. package/dist/src/serve-http.js +104 -19
  44. package/dist/src/smtp-server.d.ts +78 -0
  45. package/dist/src/smtp-server.js +160 -0
  46. package/dist/src/smtp.d.ts +29 -11
  47. package/dist/src/smtp.js +221 -52
  48. package/generated/pack-facts.json +44 -3660
  49. package/pack-facts.cjs +4 -4
  50. package/package.json +4 -2
  51. package/src/browser-session.ts +17 -5
  52. package/src/browser.ts +10 -0
  53. package/src/derived-core.ts +77 -19
  54. package/src/derived-real.ts +131 -4
  55. package/src/derived.ts +2 -2
  56. package/src/events.ts +54 -9
  57. package/src/executor.ts +13 -2
  58. package/src/file-response.ts +28 -6
  59. package/src/git/index.ts +1 -0
  60. package/src/git/smart-http-client.ts +99 -0
  61. package/src/git/smart-http.ts +7 -0
  62. package/src/head.ts +37 -6
  63. package/src/html.ts +72 -0
  64. package/src/index.ts +6 -2
  65. package/src/managed-database.ts +4 -3
  66. package/src/media-header.ts +108 -0
  67. package/src/pack-fetch.ts +142 -87
  68. package/src/pack-paths.ts +3 -11
  69. package/src/packRegistry.ts +9 -1
  70. package/src/request-body.ts +2 -3
  71. package/src/runtime.ts +5 -1
  72. package/src/serve-http.ts +81 -19
  73. package/src/smtp-server.ts +166 -0
  74. package/src/smtp.ts +199 -53
package/src/executor.ts CHANGED
@@ -248,8 +248,8 @@ async function awsSigV4Headers(input: {
248
248
  /** `vendorHosts`: the pack's declared vendor host rules (its descriptor's `hosts`). An absolute https URL
249
249
  * on one of them — an exact `host` rule whose `pathPattern` the path matches — is a second host of the
250
250
  * same vendor that takes the credential (LinkedIn's image uploads on www.linkedin.com/dms-uploads,
251
- * Instagram's rupload host); it is sent credentialed like an anchored call. Any other absolute URL is
252
- * refused, as before. */
251
+ * Instagram's rupload host); it is sent credentialed like an anchored call, as HTTP Basic when a rule naming
252
+ * it names a `basic` user. Any other absolute URL is refused. */
253
253
  /** Where a credential is kept (the head's and refresh's root: head.ts rootCustody): `key` names the credential stably
254
254
  * across rotations and anew when another is sealed in its place (the token cache is keyed by it, not by the secret's value), `open` reads it as sealed now, `seal` keeps a rotated one. */
255
255
  /** `unsealed`: where a rotation the seal failed to keep is recorded durably (the field and the cause, never a value), since
@@ -326,6 +326,8 @@ function direct(origin: URL, credential: CredentialPayload, auth?: Exclude<TwinA
326
326
  let target: URL;
327
327
  // the vendor's own URL, where a loopback root stands in for it: what a signature is made over
328
328
  let vendorUrl: URL | undefined;
329
+ // the user a host that takes the token as HTTP Basic's password names it under (HostRule.basic)
330
+ let basicUser: string | undefined;
329
331
  if (/^https:\/\//i.test(path) && vendorHosts?.length) {
330
332
  // a second host the pack declares for this vendor, on a path its rule names: validated as the root is
331
333
  target = new URL(path);
@@ -335,6 +337,7 @@ function direct(origin: URL, credential: CredentialPayload, auth?: Exclude<TwinA
335
337
  && (r.pathPattern === undefined || new RegExp(r.pathPattern).test(target.pathname));
336
338
  const declared = vendorHosts.some((r) => !r.exclude && named(r)) && !vendorHosts.some((r) => r.exclude && named(r));
337
339
  if (!declared) throw new Error(`remote execute: ${target.host}${target.pathname.slice(0, 64)} is not a host this vendor's pack declares for this path`);
340
+ basicUser = vendorHosts.find((r) => !r.exclude && r.basic !== undefined && named(r))?.basic;
338
341
  // A loopback root is the vendor substitute. Preserve the vendor Host while retaining
339
342
  // the already-validated root's mounted path; an absolute URL never escapes that root.
340
343
  if (isLoopbackHost(origin.hostname)) {
@@ -352,6 +355,14 @@ function direct(origin: URL, credential: CredentialPayload, auth?: Exclude<TwinA
352
355
  // a credential the vendor issued in this perform is the request's authorization; the sealed one stays home
353
356
  if (issued !== undefined) outbound.set('authorization', issued);
354
357
  else for (const [name, value] of Object.entries(credential.headers)) outbound.set(name, value);
358
+ // A HOST THAT TAKES THE TOKEN AS HTTP BASIC: the sealed authorization header's credentials, after its scheme, are
359
+ // Basic's password under the rule's user; one sealed as Basic goes as it is, and none sealed refuses before egress
360
+ if (issued === undefined && basicUser !== undefined) {
361
+ const sealed = outbound.get('authorization') ?? '';
362
+ const token = /^\S+\s+(\S.*)$/.exec(sealed)?.[1];
363
+ if (token === undefined) throw new Error(`remote execute: ${target.host} takes the credential as HTTP Basic, and the sealed credential carries no authorization header`);
364
+ if (!/^basic\s/i.test(sealed)) outbound.set('authorization', `Basic ${btoa(`${basicUser}:${token}`)}`);
365
+ }
355
366
  // the vendor's host, kept over any the credential carries (a loopback twin routes by it, as the World's proxy's)
356
367
  if (vendorUrl) { outbound.set('host', vendorUrl.host); outbound.set('x-volter-twin-original-host', vendorUrl.host); }
357
368
 
@@ -25,10 +25,16 @@ export function fileResponse(path: string, init?: ResponseInit): Response {
25
25
  * Stored bytes as a response, honouring a single `Range: bytes=` as RFC 9110 §14 has it: every satisfiable range is a
26
26
  * 206 with its Content-Range, `bytes=0-` (the whole body) included; a range that does not parse, a first-byte-pos past
27
27
  * its last-byte-pos (`bytes=5-2`) included, is ignored and the whole body is a 200; a well-formed range that starts past
28
- * the end (any range of an empty body) is a 416. `nosniff` stops a browser second-guessing the content type the vendor gave.
28
+ * the end (any range of an empty body) is a 416. Lazy sources supply inclusive ranges and stream in bounded reads.
29
+ * `nosniff` stops a browser second-guessing the content type the vendor gave.
29
30
  */
30
- export function bytesResponse(request: Request, bytes: Uint8Array, contentType: string, cacheControl = 'public, max-age=604800, immutable'): Response {
31
- const total = bytes.length;
31
+ export function bytesResponse(
32
+ request: Request,
33
+ bytes: Uint8Array | { size: number; range(start: number, end: number): Promise<Uint8Array | null> },
34
+ contentType: string,
35
+ cacheControl = 'public, max-age=604800, immutable',
36
+ ): Response {
37
+ const total = bytes instanceof Uint8Array ? bytes.length : bytes.size;
32
38
  const base = { 'content-type': contentType, 'x-content-type-options': 'nosniff', 'accept-ranges': 'bytes', 'cache-control': cacheControl };
33
39
  const range = /^bytes=(\d*)-(\d*)$/.exec((request.headers.get('range') ?? '').trim());
34
40
  let start = 0;
@@ -44,9 +50,25 @@ export function bytesResponse(request: Request, bytes: Uint8Array, contentType:
44
50
  // every satisfiable range is answered as one, the whole body's `bytes=0-` included: a media player (Safari's) reads a
45
51
  // 206 with its Content-Range as the server taking ranges, and a 200 as one it cannot seek in
46
52
  const partial = valid;
47
- const body = bytes.slice(start, end + 1);
48
- return new Response(request.method === 'HEAD' ? null : new Blob([body]), {
53
+ let body: BodyInit | null = null;
54
+ if (request.method !== 'HEAD') {
55
+ if (bytes instanceof Uint8Array) body = new Blob([bytes.slice(start, end + 1)]);
56
+ else {
57
+ let at = start;
58
+ body = new ReadableStream<Uint8Array>({
59
+ async pull(controller) {
60
+ if (at > end) { controller.close(); return; }
61
+ const last = Math.min(at + 8 * 1024 * 1024 - 1, end);
62
+ const chunk = await bytes.range(at, last);
63
+ if (!chunk) { controller.error(new Error('stored bytes are gone')); return; }
64
+ controller.enqueue(chunk);
65
+ at = last + 1;
66
+ },
67
+ });
68
+ }
69
+ }
70
+ return new Response(body, {
49
71
  status: partial ? 206 : 200,
50
- headers: { ...base, 'content-length': String(body.length), ...(partial ? { 'content-range': `bytes ${start}-${end}/${total}` } : {}) },
72
+ headers: { ...base, 'content-length': String(end + 1 - start), ...(partial ? { 'content-range': `bytes ${start}-${end}/${total}` } : {}) },
51
73
  });
52
74
  }
package/src/git/index.ts CHANGED
@@ -6,3 +6,4 @@ export * from './smart-http.ts';
6
6
  export * from './history.ts';
7
7
  export * from './lfs.ts';
8
8
  export * from './format.ts';
9
+ export * from './smart-http-client.ts';
@@ -0,0 +1,99 @@
1
+ // THE CLIENT HALF OF SMART HTTP (gitprotocol-http, protocol v0): what `git fetch` and `git push` send to a server,
2
+ // over a transport the caller supplies. The library opens no socket, holds no credential and names no host; a root's
3
+ // refresh and deploy reach the vendor's git through it over the kernel executor (derived-real.ts, ADR 0031). It asks
4
+ // only for what it reads: no side-band (the pack arrives bare), no multi_ack (one answer), no thin pack (every delta
5
+ // base is in the pack or the store), ofs-delta (readPack resolves both delta kinds), and on a push report-status, with
6
+ // atomic when the server offers it.
7
+ import { concat, type GitObjectStore } from './objects.ts';
8
+ import { readPack } from './pack.ts';
9
+ import { FLUSH, ZERO_SHA, packObjects, pktLine, readPktLines, type Service } from './smart-http.ts';
10
+
11
+ const DEC = new TextDecoder();
12
+ const SHA = /^[0-9a-f]{40}$/;
13
+
14
+ /** One exchange: `GET info/refs?service=<service>` when there is no body, `POST <service>` when there is. It answers
15
+ * the response's bytes, and throws when the server did not answer 200. */
16
+ export type SmartHttpTransport = (request: { service: Service; body?: Uint8Array }) => Promise<Uint8Array>;
17
+
18
+ /** What a server advertises: its refs (peeled `^{}` lines dropped), the ref its HEAD names, and its capabilities. */
19
+ export type RemoteRefs = { refs: Map<string, string>; head: string | null; caps: string[] };
20
+
21
+ /** `GET info/refs?service=<service>`, read. */
22
+ export async function lsRemote(transport: SmartHttpTransport, service: Service): Promise<RemoteRefs> {
23
+ let { lines, rest } = readPktLines(await transport({ service }));
24
+ if (lines[0]?.startsWith('# service=')) ({ lines, rest } = readPktLines(rest));
25
+ const refs = new Map<string, string>(); let head: string | null = null; let caps: string[] = [];
26
+ for (const raw of lines) {
27
+ const [line, offered] = raw.replace(/\n$/, '').split('\0') as [string, string | undefined];
28
+ if (line.startsWith('ERR ')) throw new Error(`git ${service}: ${line.slice(4)}`);
29
+ if (offered !== undefined) {
30
+ caps = offered.split(' ').filter(Boolean);
31
+ const symref = caps.find((c) => c.startsWith('symref=HEAD:'));
32
+ if (symref) head = symref.slice('symref=HEAD:'.length);
33
+ }
34
+ const sha = line.slice(0, 40); const name = line.slice(41);
35
+ if (!SHA.test(sha) || sha === ZERO_SHA || name === 'HEAD' || name.endsWith('^{}')) continue;
36
+ refs.set(name, sha);
37
+ }
38
+ return { refs, head, caps };
39
+ }
40
+
41
+ const isPack = (bytes: Uint8Array, at: number): boolean => DEC.decode(bytes.subarray(at, at + 4)) === 'PACK';
42
+
43
+ /** `POST git-upload-pack` with `done`: every object `wants` reach and `haves` do not, read into `store`. */
44
+ export async function fetchPack(transport: SmartHttpTransport, store: GitObjectStore, wants: string[], haves: string[]): Promise<void> {
45
+ if (wants.length === 0) return;
46
+ const answer = await transport({
47
+ service: 'git-upload-pack',
48
+ body: concat(
49
+ pktLine(`want ${wants[0]} ofs-delta no-progress agent=volter-twin\n`),
50
+ ...wants.slice(1).map((w) => pktLine(`want ${w}\n`)),
51
+ FLUSH,
52
+ ...haves.map((h) => pktLine(`have ${h}\n`)),
53
+ pktLine('done\n'),
54
+ ),
55
+ });
56
+ // the negotiation's ACK / NAK lines, then the pack
57
+ let at = 0;
58
+ while (at + 4 <= answer.length && !isPack(answer, at)) {
59
+ const len = parseInt(DEC.decode(answer.subarray(at, at + 4)), 16);
60
+ if (Number.isNaN(len)) break;
61
+ if (len < 4) { at += 4; continue; }
62
+ const line = DEC.decode(answer.subarray(at + 4, at + len));
63
+ if (line.startsWith('ERR ')) throw new Error(`git upload-pack: ${line.slice(4).trim()}`);
64
+ at += len;
65
+ }
66
+ if (!isPack(answer, at)) throw new Error('git upload-pack: the server answered no pack');
67
+ await readPack(answer.subarray(at), store);
68
+ }
69
+
70
+ /** One ref update a push asks for: `old` is the server's value as advertised (ZERO_SHA to create), `new` the object
71
+ * to point it at (ZERO_SHA to delete). */
72
+ export type PushCommand = { name: string; old: string; new: string };
73
+
74
+ /** `POST git-receive-pack`: the commands and the pack of what they need beyond `haves`, atomic when `atomic` (the
75
+ * server offered it). Answers each command's outcome by ref name: null when the server took it, else its reason. */
76
+ export async function sendPack(transport: SmartHttpTransport, store: GitObjectStore, commands: PushCommand[], haves: string[], atomic = false): Promise<Map<string, string | null>> {
77
+ const updates = commands.filter((c) => c.new !== ZERO_SHA).map((c) => c.new);
78
+ const caps = `report-status${atomic ? ' atomic' : ''} agent=volter-twin`;
79
+ const answer = await transport({
80
+ service: 'git-receive-pack',
81
+ body: concat(
82
+ ...commands.map((c, i) => pktLine(`${c.old} ${c.new} ${c.name}${i === 0 ? `\0${caps}` : ''}\n`)),
83
+ FLUSH,
84
+ // a push of deletions alone sends no pack (gitprotocol-pack, "Pushing data to a server")
85
+ ...(updates.length ? [await packObjects(store, updates, haves)] : []),
86
+ ),
87
+ });
88
+ const report = readPktLines(answer).lines.map((l) => l.replace(/\n$/, ''));
89
+ const failure = report.find((l) => l.startsWith('ERR '));
90
+ if (failure) throw new Error(`git receive-pack: ${failure.slice(4)}`);
91
+ const unpack = report.find((l) => l.startsWith('unpack '))?.slice('unpack '.length);
92
+ if (unpack === undefined) throw new Error('git receive-pack: the server sent no report');
93
+ const out = new Map<string, string | null>(commands.map((c) => [c.name, unpack === 'ok' ? 'the server reported nothing for it' : `unpacker error: ${unpack}`]));
94
+ for (const l of report) {
95
+ if (l.startsWith('ok ')) out.set(l.slice(3), null);
96
+ else if (l.startsWith('ng ')) { const rest = l.slice(3); const sp = rest.indexOf(' '); out.set(sp === -1 ? rest : rest.slice(0, sp), sp === -1 ? 'refused' : rest.slice(sp + 1)); }
97
+ }
98
+ return out;
99
+ }
@@ -68,6 +68,13 @@ async function closure(store: GitObjectStore, roots: string[], seen: Set<string>
68
68
  }
69
69
  async function reachable(store: GitObjectStore, roots: string[]): Promise<Set<string>> { const seen = new Set<string>(); await closure(store, roots, seen); return seen; }
70
70
 
71
+ /** The pack of every object `wants` reach that the `haves` the store holds do not: what a push sends (a client's
72
+ * send-pack, smart-http-client.ts). */
73
+ export async function packObjects(store: GitObjectStore, wants: string[], haves: string[]): Promise<Uint8Array> {
74
+ const known: string[] = []; for (const h of haves) if (await store.has(h)) known.push(h);
75
+ return writePack(await closure(store, wants, await reachable(store, known)));
76
+ }
77
+
71
78
  /**
72
79
  * A shallow request's commits (gitprotocol-pack, "Shallow requests"): every commit within `depth` of a want (a want is
73
80
  * depth 1), and the boundary, the commits whose parents the client will not have. The client's own shallow commits it
package/src/head.ts CHANGED
@@ -34,6 +34,9 @@ export type PushOutcome = {
34
34
  actionId?: string;
35
35
  /** the group's other entries the answer named an id for (a resource's `companions`) */
36
36
  also?: Array<{ actionId: string; externalId: string; data?: Record<string, unknown> }>;
37
+ /** the group's entries the vendor refused while it took the rest (a git push to a root without `atomic`, each ref its
38
+ * own outcome: ADR 0031); each settles refused with its reason */
39
+ refused?: Array<{ actionId: string; reason: string }>;
37
40
  };
38
41
  /** What a perform is handed beside the executor: a resolver from a local id to the vendor's (an
39
42
  * entry authored against `twin-1` crosses against `REAL-42` once that subject's landing adopted it). */
@@ -269,6 +272,8 @@ export type DeployReport = {
269
272
  refused?: { actionId: string; check: string; reason: string };
270
273
  /** the first failure, with the vendor's words; nothing after it was attempted */
271
274
  failed?: { actionId: string; error: string };
275
+ /** a group the vendor took in part: its entries it refused, each with its reason; nothing after it was attempted */
276
+ partial?: Array<{ actionId: string; reason: string }>;
272
277
  /** entries after the refusal or failure, not attempted */
273
278
  skipped: string[];
274
279
  /** entries whose landed copy already said `deployed` — not performed again */
@@ -287,12 +292,13 @@ function receiptsByEntry(parent: Entry[]): Map<string, Receipt> {
287
292
  /** Whether an entry records a call of the vendor's surface (an operation or a GraphQL mutation): what a perform can send
288
293
  * again (the real-system adapters, "What the derived core records with a write"). */
289
294
  function surfaceWrite(e: { input?: Record<string, unknown> }): boolean {
290
- return typeof e.input?.operationId === 'string' || (e.input?.graphql !== undefined && e.input.graphql !== null);
295
+ // a push to a World repository is the vendor's too: a deploy sends it as a git push (derived-core.ts pushedInput)
296
+ return typeof e.input?.operationId === 'string' || (e.input?.graphql !== undefined && e.input.graphql !== null) || (e.input?.git !== undefined && e.input.git !== null);
291
297
  }
292
298
 
293
299
  /** The branch entries a root twin still has to settle: `set` rows, not reverted, whose landed copy is absent or says
294
300
  * `failed`/`skipped` (a `deployed` or `refused` copy settles the entry), split into what is offered to the vendor (a
295
- * surface write on a subject the vendor can have) and what is the World's own: a write a door, a screen, the gap, the
301
+ * surface write or a git push on a subject the vendor can have) and what is the World's own: a write a door, a screen, the gap, the
296
302
  * clock or a seed made, and any write on a subject only the World's own writes made. */
297
303
  export function settlingEntries(service: string, root?: string): { offered: TwinAction[]; worldOnly: TwinAction[] } {
298
304
  const parent = parentEntries(service, root);
@@ -436,7 +442,10 @@ async function performNow(opts: Parameters<typeof performEntries>[0]): Promise<D
436
442
  const answered = outcome.data !== null && typeof outcome.data === 'object' && !Array.isArray(outcome.data) ? (outcome.data as Record<string, unknown>) : {};
437
443
  const primary = resolved.find((a) => a.id === outcome.actionId) ?? resolved[0]!;
438
444
  const also = new Map((outcome.also ?? []).map((a) => [a.actionId, a]));
445
+ const refusedHere = new Map((outcome.refused ?? []).map((r) => [r.actionId, r.reason]));
439
446
  for (const action of resolved) {
447
+ const refusal = refusedHere.get(action.id);
448
+ if (refusal !== undefined) { settle(action, at, { status: 'refused', reason: refusal }); refusedIds.add(action.subject.id); continue; }
440
449
  const answeredId = action === primary ? outcome.externalId : also.get(action.id)?.externalId;
441
450
  const externalId = answeredId || aliases.get(`${action.subject.type}:${action.subject.id}`);
442
451
  settle(action, at, { status: 'deployed', ...(action === primary && outcome.url ? { url: outcome.url } : {}) }, { ...action.fields, ...(action === primary ? answered : also.get(action.id)?.data ?? {}) }, externalId || undefined);
@@ -444,6 +453,7 @@ async function performNow(opts: Parameters<typeof performEntries>[0]): Promise<D
444
453
  report.pushed += 1;
445
454
  report.deployed.push({ actionId: action.id, at, ...(externalId ? { externalId } : {}), ...(data !== undefined ? { data } : {}), ...(action === primary && outcome.response ? { response: outcome.response } : {}) });
446
455
  }
456
+ if (refusedHere.size) { report.partial = [...refusedHere].map(([actionId, reason]) => ({ actionId, reason })); stopped = true; }
447
457
  } catch (error) {
448
458
  // a write the vendor refuses (a 4xx: it can never be sent as it is) settles the group refused, as a check's
449
459
  // refusal does, so the next deploy moves on to the group after it; anything else failed and stays offered
@@ -515,7 +525,7 @@ export async function deferPerform<T>(request: string, fn: () => Promise<T>): Pr
515
525
  /** Under `auto`, perform the entries one request made as one call (the real-system adapters, "Perform"): the ids the
516
526
  * vendor gave them by local id, or the vendor's refusal or failure, the request's entries reverted as performAtHead
517
527
  * reverts one. Nothing when the head is not real and auto, or the request wrote nothing the vendor is offered. */
518
- export async function performRequestGroup(service: string, request: string, root?: string): Promise<{ ids: Map<string, string>; response?: RemoteExecuteResponse } | RefusedWriteError | VendorWriteError> {
528
+ export async function performRequestGroup(service: string, request: string, root?: string): Promise<{ ids: Map<string, string>; response?: RemoteExecuteResponse; refused?: Array<{ actionId: string; reason: string }> } | RefusedWriteError | VendorWriteError> {
519
529
  const head = headOf(service, root);
520
530
  const ids = new Map<string, string>();
521
531
  if (head.kind !== 'real' || head.deploy !== 'auto') return { ids };
@@ -525,21 +535,42 @@ export async function performRequestGroup(service: string, request: string, root
525
535
  try { report = await performEntries({ service, actionIds: group.map((e) => e.id), ...(root !== undefined ? { root } : {}) }); }
526
536
  catch (error) {
527
537
  if (error instanceof DeploymentOrderError) {
528
- for (const e of [...group].reverse()) revertAction({ service, actionId: e.id, occurredAt: worldNow(), ...(root !== undefined ? { root } : {}) });
538
+ revertRequest(service, request, group, root);
529
539
  return new VendorWriteError(`An earlier ${service} write is still pending; this request was rolled back before reaching the vendor. Retry after that write finishes.`, group[0]!.id);
530
540
  }
531
541
  throw error instanceof HeadError ? error : new HeadError(error instanceof Error ? error.message : String(error));
532
542
  }
533
543
  if (report.refused || report.failed) {
534
- for (const e of [...group].reverse()) revertAction({ service, actionId: e.id, occurredAt: worldNow(), ...(root !== undefined ? { root } : {}) });
544
+ revertRequest(service, request, group, root);
535
545
  return report.refused ? new RefusedWriteError(report.refused.check, report.refused.reason, report.refused.actionId) : new VendorWriteError(report.failed!.error, report.failed!.actionId);
536
546
  }
547
+ if (report.partial) revertRequest(service, request, group.filter((e) => report.partial!.some((r) => r.actionId === e.id)), root);
537
548
  for (const d of report.deployed) {
538
549
  const e = group.find((g) => g.id === d.actionId);
539
550
  if (e && d.externalId && d.externalId !== e.subject.id) ids.set(e.subject.id, d.externalId);
540
551
  }
541
552
  const response = report.deployed.find((entry) => entry.response)?.response;
542
- return { ids, ...(response ? { response } : {}) };
553
+ return { ids, ...(response ? { response } : {}), ...(report.partial ? { refused: report.partial } : {}) };
554
+ }
555
+
556
+ /** Revert what a request's refused entries made, latest first. An entry that is a declared push a screen recorded
557
+ * (`input.git`, ADR 0031) owns the request's writes after it up to the next such push, its push's effects (the
558
+ * reactions and vendor moves the pack runs on it), which go with it; any other refused entry goes alone, as the group
559
+ * it is in. */
560
+ function revertRequest(service: string, request: string, refused: TwinAction[], root?: string): void {
561
+ const pushed = (e: { input?: Record<string, unknown> }): boolean => e.input?.git !== undefined && e.input.git !== null;
562
+ const undo = new Set<string>();
563
+ if (refused.some(pushed)) {
564
+ const made = (branchEntries(service, root) as TwinAction[]).filter((e) => e.correlationId === request && e.op === 'set');
565
+ for (const r of refused) {
566
+ if (!pushed(r)) { undo.add(r.id); continue; }
567
+ const at = made.findIndex((e) => e.id === r.id);
568
+ if (at < 0) { undo.add(r.id); continue; }
569
+ undo.add(r.id);
570
+ for (const e of made.slice(at + 1)) { if (pushed(e)) break; undo.add(e.id); }
571
+ }
572
+ } else for (const r of refused) undo.add(r.id);
573
+ for (const e of branchEntries(service, root).filter((x) => undo.has(x.id)).reverse()) revertAction({ service, actionId: e.id, occurredAt: worldNow(), ...(root !== undefined ? { root } : {}) });
543
574
  }
544
575
 
545
576
  /** Wrap a pack's fetch: a `RefusedWriteError` or `VendorWriteError` the head raised becomes the
package/src/html.ts ADDED
@@ -0,0 +1,72 @@
1
+ // The WHATWG HTML grammar is shared mechanics; what a pack keeps of a document is the pack's own policy.
2
+ // The exact optional dependency belongs to this kernel, so Node and browser bundles resolve one parser.
3
+ // Source: parse5 7.3.0 (parseFragment, serialize, defaultTreeAdapter).
4
+ //
5
+ // A fragment is parsed in an HTML `<body>` context. Elements keep their namespace (HTML, SVG or MathML) and attributes
6
+ // their prefix and namespace, and serialization reproduces what the tree holds: a foreign subtree survives parsing, and
7
+ // a pack's policy may drop it.
8
+ export type HtmlAttribute = { name: string; value: string; prefix?: string; namespace?: string };
9
+ export type HtmlElement = { type: 'element'; name: string; namespace: string; attributes: HtmlAttribute[]; children: HtmlNode[] };
10
+ export type HtmlText = { type: 'text'; value: string };
11
+ export type HtmlNode = HtmlElement | HtmlText;
12
+ export class HtmlError extends Error {
13
+ constructor(readonly code: 'parser-unavailable') {
14
+ super('HTML parser parse5 7.3.0 is unavailable');
15
+ this.name = 'HtmlError';
16
+ }
17
+ }
18
+ type Grammar = typeof import('parse5');
19
+ type Tree = import('parse5').DefaultTreeAdapterMap;
20
+ let grammar: Promise<Grammar> | undefined;
21
+ function loadGrammar(): Promise<Grammar> {
22
+ return grammar ??= import('parse5').catch(() => { grammar = undefined; throw new HtmlError('parser-unavailable'); });
23
+ }
24
+
25
+ /**
26
+ * A fragment parsed as WHATWG fragment parsing does in an HTML `<body>` context: elements and text, comments dropped,
27
+ * an HTML `<template>`'s content carried as that element's children. Importing this module does not load the grammar.
28
+ */
29
+ export async function parseFragment(source: string): Promise<HtmlNode[]> {
30
+ const { parseFragment: parse, defaultTreeAdapter: tree, html } = await loadGrammar();
31
+ const convert = (nodes: Tree['node'][]): HtmlNode[] => {
32
+ const out: HtmlNode[] = [];
33
+ for (const node of nodes) {
34
+ if (node.nodeName === '#text') out.push({ type: 'text', value: (node as Tree['textNode']).value });
35
+ else if ('tagName' in node) {
36
+ const element = node as Tree['element'];
37
+ // only an HTML template has content; an SVG or MathML `template` is a foreign element with children
38
+ const children = element.tagName === 'template' && element.namespaceURI === html.NS.HTML
39
+ ? (element as Tree['template']).content.childNodes
40
+ : element.childNodes;
41
+ out.push({
42
+ type: 'element', name: element.tagName, namespace: element.namespaceURI,
43
+ attributes: element.attrs.map(attr => ({ name: attr.name, value: attr.value, ...(attr.prefix ? { prefix: attr.prefix } : {}), ...(attr.namespace ? { namespace: attr.namespace } : {}) })),
44
+ children: convert(children),
45
+ });
46
+ }
47
+ }
48
+ return out;
49
+ };
50
+ return convert(parse(tree.createElement('body', html.NS.HTML, []), source, {}).childNodes);
51
+ }
52
+
53
+ /** Nodes serialized by the WHATWG HTML fragment serialization: its escaping, its void elements, its foreign names. */
54
+ export async function serialize(nodes: HtmlNode[]): Promise<string> {
55
+ const { serialize: write, defaultTreeAdapter: tree, html } = await loadGrammar();
56
+ const append = (parent: Tree['parentNode'], list: HtmlNode[]): void => {
57
+ for (const node of list) {
58
+ if (node.type === 'text') { tree.insertText(parent, node.value); continue; }
59
+ const element = tree.createElement(node.name, node.namespace as import('parse5').html.NS,
60
+ node.attributes.map(({ name, value, prefix, namespace }) => ({ name, value, ...(prefix ? { prefix } : {}), ...(namespace ? { namespace } : {}) })));
61
+ tree.appendChild(parent, element);
62
+ if (node.name === 'template' && node.namespace === html.NS.HTML) {
63
+ const content = tree.createDocumentFragment();
64
+ tree.setTemplateContent(element as Tree['template'], content);
65
+ append(content, node.children);
66
+ } else append(element, node.children);
67
+ }
68
+ };
69
+ const fragment = tree.createDocumentFragment();
70
+ append(fragment, nodes);
71
+ return write(fragment);
72
+ }
package/src/index.ts CHANGED
@@ -76,11 +76,13 @@ export { passwordHash, personOf, personWith, recordPerson, shownPersonFields, ty
76
76
  // the World's managed Postgres a handler reaches as `ctx.engine` (managed-database.ts), and a mail's route (`ctx.mail`)
77
77
  export { EngineDatabaseError, EngineUnavailableError, quoteIdent } from './managed-database.ts';
78
78
  export type { Batch as EngineBatch, ManagedDatabase, Statement as EngineStatement, StatementResult as EngineStatementResult } from './managed-database.ts';
79
- export type { Mail, SmtpRoute } from './smtp.ts';
79
+ export type { Mail, MailReply } from './smtp.ts';
80
80
  // S3's wire (contract "S3's wire is a kernel library too") and the git plane (contract "The git plane is a kernel library")
81
81
  export * as s3 from './s3/wire.ts';
82
82
  // Generic strict XML grammar; vendor interpretation remains in its consumer.
83
83
  export * as xml from './xml.ts';
84
+ // Generic WHATWG HTML grammar; what a pack keeps of received HTML remains the pack's policy.
85
+ export * as html from './html.ts';
84
86
  export * as clickhouse from './clickhouse/index.ts';
85
87
  export { sampleFromSchema } from './schema-sample.ts';
86
88
  export * as openaiWire from './openai-wire.ts';
@@ -89,8 +91,10 @@ export * as protobuf from './protobuf.ts';
89
91
  export * as grpc from './grpc-wire.ts';
90
92
  export { acceptH2Connection, type H2Handler, type Http1Handler } from './h2.ts';
91
93
  export { placeholderPng } from './placeholder-image.ts';
94
+ export { imageDimensions, readImageHeader, readMp4Header } from './media-header.ts';
92
95
  export { presignSigV4, signSigV4, verifySigV4, type SigV4Verdict } from './sigv4.ts';
93
96
  export * as redis from './redis/index.ts';
97
+ export * as smtpServer from './smtp-server.ts';
94
98
  export * as mysql from './mysql/index.ts';
95
99
  export * as live from './live/index.ts';
96
100
  export * as mongodb from './mongodb/index.ts';
@@ -102,7 +106,7 @@ export type { PackScenarioAdapter, ScenarioDecision, ScenarioDocument, ScenarioE
102
106
 
103
107
  export { verifyAttestationBundle } from './attestation.ts';
104
108
 
105
- export { browserSessionToken, startBrowserSession, endBrowserSession } from './browser-session.ts';
109
+ export { cookieOf, signedIn, browserSessionToken, startBrowserSession, endBrowserSession } from './browser-session.ts';
106
110
 
107
111
  export { readHostPort } from './host-port.ts';
108
112
  export { assertBrowserName, browserOrigin, validateBrowserState, validateBrowserStorageState, validateBrowserContext, browserAcceptLanguage, cookieMatchesHost, type BrowserCookie, type BrowserState, type BrowserStorageState, type BrowserContext, type BrowserIndexedDB } from './browser-state.ts';
@@ -254,10 +254,10 @@ export function managedDatabase(url: string, options: { readOnly?: boolean } = {
254
254
  * walk/journey-kit.ts), its statements sent as its client sends them: a text alone as one simple Query (a migration
255
255
  * file, a `BEGIN`), a text with values as one extended-protocol statement, the values bind parameters. A transaction it
256
256
  * opens stays open across its statements until it ends it, as on the application's own connection. Each statement's
257
- * answer is its results in Postgres's text format, untyped. */
258
- export type DatabaseSession = { query(text: string, values?: ReadonlyArray<string | null>): Promise<StatementResult[]>; end(): Promise<void> };
257
+ * answer is its results in Postgres's text format, with native column type OIDs retained beside the values. */
258
+ export type DatabaseSession = { query(text: string, values?: ReadonlyArray<string | null>): Promise<Array<StatementResult & { fieldTypes: number[] }>>; end(): Promise<void> };
259
259
 
260
- type PgResult = { command?: string | null; rowCount?: number | null; fields?: Array<{ name: string }>; rows?: Array<Array<string | null>> };
260
+ type PgResult = { command?: string | null; rowCount?: number | null; fields?: Array<{ name: string; dataTypeID: number }>; rows?: Array<Array<string | null>> };
261
261
 
262
262
  /** The World's Postgres at `url` as one connection held until `end()`. */
263
263
  export async function databaseSession(url: string): Promise<DatabaseSession> {
@@ -277,6 +277,7 @@ export async function databaseSession(url: string): Promise<DatabaseSession> {
277
277
  const answer = (await (client.query({ text, ...(values === undefined ? {} : { values }), rowMode: 'array', types: raw }) as Promise<PgResult | PgResult[]>));
278
278
  return (Array.isArray(answer) ? answer : [answer]).map((r) => ({
279
279
  fields: (r.fields ?? []).map((f) => f.name),
280
+ fieldTypes: (r.fields ?? []).map((f) => f.dataTypeID),
280
281
  rows: r.rows ?? [],
281
282
  command: [r.command ?? '', r.command === 'INSERT' ? '0' : '', r.rowCount ?? ''].filter((part) => part !== '').join(' '),
282
283
  }));
@@ -0,0 +1,108 @@
1
+ // File-format headers only: no decoding, vendor limits, processing or rendition policy.
2
+ type ImageType = 'image/png' | 'image/gif' | 'image/jpeg' | 'image/webp';
3
+ type Dimensions = { width: number; height: number };
4
+ type ImageHeader = Dimensions & { jpeg?: { frameEnd: number; nonMarkerBytes: boolean; standaloneMarkers: boolean; fillBytes: boolean; shortSegments: boolean } };
5
+
6
+ const tag = (bytes: Uint8Array, at: number): string => String.fromCharCode(...bytes.subarray(at, at + 4));
7
+
8
+ /** Dimensions in a caller-declared image format, or undefined for an incomplete header. */
9
+ export function imageDimensions(bytes: Uint8Array, type: ImageType): ImageHeader | undefined {
10
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
11
+ if (type === 'image/png') return bytes.length >= 24 ? { width: view.getUint32(16), height: view.getUint32(20) } : undefined;
12
+ if (type === 'image/gif') return bytes.length >= 10 ? { width: view.getUint16(6, true), height: view.getUint16(8, true) } : undefined;
13
+ if (type === 'image/webp') {
14
+ if (bytes.length < 30) return undefined;
15
+ const chunk = tag(bytes, 12);
16
+ if (chunk === 'VP8 ') return { width: view.getUint16(26, true) & 0x3fff, height: view.getUint16(28, true) & 0x3fff };
17
+ if (chunk === 'VP8L') {
18
+ const bits = view.getUint32(21, true);
19
+ return { width: 1 + (bits & 0x3fff), height: 1 + ((bits >>> 14) & 0x3fff) };
20
+ }
21
+ if (chunk === 'VP8X') {
22
+ const u24 = (at: number): number => bytes[at]! | (bytes[at + 1]! << 8) | (bytes[at + 2]! << 16);
23
+ return { width: 1 + u24(24), height: 1 + u24(27) };
24
+ }
25
+ return undefined;
26
+ }
27
+ let at = 2;
28
+ const jpeg = { frameEnd: 0, nonMarkerBytes: false, standaloneMarkers: false, fillBytes: false, shortSegments: false };
29
+ while (at + 4 <= bytes.length) {
30
+ if (bytes[at] !== 0xff) { jpeg.nonMarkerBytes = true; at += 1; continue; }
31
+ const marker = bytes[at + 1]!;
32
+ if (marker === 0xff) { jpeg.fillBytes = true; at += 1; continue; }
33
+ if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) { jpeg.standaloneMarkers = true; at += 2; continue; }
34
+ const length = view.getUint16(at + 2);
35
+ if (length < 2) jpeg.shortSegments = true;
36
+ if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
37
+ return at + 9 <= bytes.length ? { width: view.getUint16(at + 7), height: view.getUint16(at + 5), jpeg: { ...jpeg, frameEnd: at + 9 } } : undefined;
38
+ }
39
+ if (marker === 0xda) return undefined;
40
+ at += 2 + length;
41
+ }
42
+ return undefined;
43
+ }
44
+
45
+ /** An image's signature and dimensions, without deciding whether a vendor accepts that format or size. */
46
+ export function readImageHeader(bytes: Uint8Array): (ImageHeader & { type: ImageType }) | undefined {
47
+ let type: ImageType;
48
+ if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) type = 'image/png';
49
+ else if (tag(bytes, 0).startsWith('GIF')) type = 'image/gif';
50
+ else if (bytes[0] === 0xff && bytes[1] === 0xd8) type = 'image/jpeg';
51
+ else if (tag(bytes, 0) === 'RIFF' && tag(bytes, 8) === 'WEBP') type = 'image/webp';
52
+ else return undefined;
53
+ const dimensions = imageDimensions(bytes, type);
54
+ return dimensions ? { type, ...dimensions } : undefined;
55
+ }
56
+
57
+ /** ISO BMFF's bounded top-level boxes and movie headers, with raw duration and 16.16 track dimensions. */
58
+ export function readMp4Header(bytes: Uint8Array): {
59
+ fileType: boolean;
60
+ hasMediaData: boolean;
61
+ movies: Array<{ headers: Array<{ timescale: number; duration: number; payloadSize: number }>; tracks: Dimensions[] }>;
62
+ } {
63
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
64
+ function* boxes(from: number, to: number): Generator<{ type: string; start: number; end: number }> {
65
+ let at = from;
66
+ while (at + 8 <= to) {
67
+ let size = view.getUint32(at);
68
+ const type = tag(bytes, at + 4);
69
+ let header = 8;
70
+ if (size === 1) {
71
+ if (at + 16 > to) return;
72
+ size = Number(view.getBigUint64(at + 8));
73
+ header = 16;
74
+ }
75
+ if (size === 0) size = to - at;
76
+ if (!Number.isSafeInteger(size) || size < header || size > to - at) return;
77
+ yield { type, start: at + header, end: at + size };
78
+ at += size;
79
+ }
80
+ }
81
+ const result: ReturnType<typeof readMp4Header> = { fileType: bytes.length >= 8 && tag(bytes, 4) === 'ftyp', hasMediaData: false, movies: [] };
82
+ for (const box of boxes(0, bytes.length)) {
83
+ if (box.type === 'mdat' && box.end > box.start) result.hasMediaData = true;
84
+ if (box.type !== 'moov') continue;
85
+ const movie: (typeof result.movies)[number] = { headers: [], tracks: [] };
86
+ result.movies.push(movie);
87
+ for (const child of boxes(box.start, box.end)) {
88
+ const { type, start, end } = child;
89
+ if (type === 'mvhd' && start < end) {
90
+ const version = bytes[start];
91
+ const payloadSize = end - start;
92
+ if (version === 0 && payloadSize >= 20) movie.headers.push({ timescale: view.getUint32(start + 12), duration: view.getUint32(start + 16), payloadSize });
93
+ if (version === 1 && payloadSize >= 32) movie.headers.push({ timescale: view.getUint32(start + 20), duration: Number(view.getBigUint64(start + 24)), payloadSize });
94
+ }
95
+ if (type !== 'trak') continue;
96
+ for (const track of boxes(start, end)) {
97
+ if (track.type !== 'tkhd' || track.start >= track.end) continue;
98
+ const version = bytes[track.start];
99
+ const at = version === 1 ? track.start + 88 : version === 0 ? track.start + 76 : track.end;
100
+ if (at + 8 > track.end) continue;
101
+ const width = view.getUint32(at) / 65536;
102
+ const height = view.getUint32(at + 4) / 65536;
103
+ movie.tracks.push({ width, height });
104
+ }
105
+ }
106
+ }
107
+ return result;
108
+ }