@volter/world-core 3.0.164 → 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 (55) hide show
  1. package/dist/generated/pack-facts.json +44 -3660
  2. package/dist/pack-facts.cjs +4 -4
  3. package/dist/src/browser.d.ts +4 -0
  4. package/dist/src/browser.js +10 -0
  5. package/dist/src/derived-core.d.ts +45 -5
  6. package/dist/src/derived-core.js +53 -8
  7. package/dist/src/derived-real.js +144 -2
  8. package/dist/src/derived.d.ts +1 -1
  9. package/dist/src/executor.d.ts +2 -2
  10. package/dist/src/executor.js +13 -0
  11. package/dist/src/git/index.d.ts +1 -0
  12. package/dist/src/git/index.js +1 -0
  13. package/dist/src/git/smart-http-client.d.ts +28 -0
  14. package/dist/src/git/smart-http-client.js +95 -0
  15. package/dist/src/git/smart-http.d.ts +3 -0
  16. package/dist/src/git/smart-http.js +9 -0
  17. package/dist/src/head.d.ts +16 -1
  18. package/dist/src/head.js +52 -7
  19. package/dist/src/html.d.ts +29 -0
  20. package/dist/src/html.js +64 -0
  21. package/dist/src/index.d.ts +3 -1
  22. package/dist/src/index.js +3 -0
  23. package/dist/src/pack-fetch.js +41 -2
  24. package/dist/src/pack-paths.d.ts +2 -3
  25. package/dist/src/pack-paths.js +3 -13
  26. package/dist/src/packRegistry.d.ts +17 -1
  27. package/dist/src/runtime.d.ts +2 -0
  28. package/dist/src/runtime.js +3 -0
  29. package/dist/src/serve-http.d.ts +1 -1
  30. package/dist/src/serve-http.js +11 -2
  31. package/dist/src/smtp-server.d.ts +78 -0
  32. package/dist/src/smtp-server.js +160 -0
  33. package/dist/src/smtp.d.ts +29 -11
  34. package/dist/src/smtp.js +221 -52
  35. package/generated/pack-facts.json +44 -3660
  36. package/pack-facts.cjs +4 -4
  37. package/package.json +4 -2
  38. package/src/browser.ts +10 -0
  39. package/src/derived-core.ts +68 -12
  40. package/src/derived-real.ts +121 -2
  41. package/src/derived.ts +2 -2
  42. package/src/executor.ts +13 -2
  43. package/src/git/index.ts +1 -0
  44. package/src/git/smart-http-client.ts +99 -0
  45. package/src/git/smart-http.ts +7 -0
  46. package/src/head.ts +37 -6
  47. package/src/html.ts +72 -0
  48. package/src/index.ts +4 -1
  49. package/src/pack-fetch.ts +35 -2
  50. package/src/pack-paths.ts +3 -11
  51. package/src/packRegistry.ts +9 -1
  52. package/src/runtime.ts +3 -0
  53. package/src/serve-http.ts +11 -3
  54. package/src/smtp-server.ts +166 -0
  55. package/src/smtp.ts +199 -53
@@ -19,6 +19,12 @@ export type PushOutcome = {
19
19
  externalId: string;
20
20
  data?: Record<string, unknown>;
21
21
  }>;
22
+ /** the group's entries the vendor refused while it took the rest (a git push to a root without `atomic`, each ref its
23
+ * own outcome: ADR 0031); each settles refused with its reason */
24
+ refused?: Array<{
25
+ actionId: string;
26
+ reason: string;
27
+ }>;
22
28
  };
23
29
  /** What a perform is handed beside the executor: a resolver from a local id to the vendor's (an
24
30
  * entry authored against `twin-1` crosses against `REAL-42` once that subject's landing adopted it). */
@@ -140,6 +146,11 @@ export type DeployReport = {
140
146
  actionId: string;
141
147
  error: string;
142
148
  };
149
+ /** a group the vendor took in part: its entries it refused, each with its reason; nothing after it was attempted */
150
+ partial?: Array<{
151
+ actionId: string;
152
+ reason: string;
153
+ }>;
143
154
  /** entries after the refusal or failure, not attempted */
144
155
  skipped: string[];
145
156
  /** entries whose landed copy already said `deployed` — not performed again */
@@ -149,7 +160,7 @@ export type DeployReport = {
149
160
  };
150
161
  /** The branch entries a root twin still has to settle: `set` rows, not reverted, whose landed copy is absent or says
151
162
  * `failed`/`skipped` (a `deployed` or `refused` copy settles the entry), split into what is offered to the vendor (a
152
- * 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
163
+ * 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
153
164
  * clock or a seed made, and any write on a subject only the World's own writes made. */
154
165
  export declare function settlingEntries(service: string, root?: string): {
155
166
  offered: TwinAction[];
@@ -196,6 +207,10 @@ export declare function deferPerform<T>(request: string, fn: () => Promise<T>):
196
207
  export declare function performRequestGroup(service: string, request: string, root?: string): Promise<{
197
208
  ids: Map<string, string>;
198
209
  response?: RemoteExecuteResponse;
210
+ refused?: Array<{
211
+ actionId: string;
212
+ reason: string;
213
+ }>;
199
214
  } | RefusedWriteError | VendorWriteError>;
200
215
  /** Wrap a pack's fetch: a `RefusedWriteError` or `VendorWriteError` the head raised becomes the
201
216
  * vendor's own error body (the pack's `shape`), so an app under live use sees what the vendor
package/dist/src/head.js CHANGED
@@ -262,11 +262,12 @@ function receiptsByEntry(parent) {
262
262
  /** Whether an entry records a call of the vendor's surface (an operation or a GraphQL mutation): what a perform can send
263
263
  * again (the real-system adapters, "What the derived core records with a write"). */
264
264
  function surfaceWrite(e) {
265
- return typeof e.input?.operationId === 'string' || (e.input?.graphql !== undefined && e.input.graphql !== null);
265
+ // a push to a World repository is the vendor's too: a deploy sends it as a git push (derived-core.ts pushedInput)
266
+ return typeof e.input?.operationId === 'string' || (e.input?.graphql !== undefined && e.input.graphql !== null) || (e.input?.git !== undefined && e.input.git !== null);
266
267
  }
267
268
  /** The branch entries a root twin still has to settle: `set` rows, not reverted, whose landed copy is absent or says
268
269
  * `failed`/`skipped` (a `deployed` or `refused` copy settles the entry), split into what is offered to the vendor (a
269
- * 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
270
+ * 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
270
271
  * clock or a seed made, and any write on a subject only the World's own writes made. */
271
272
  export function settlingEntries(service, root) {
272
273
  const parent = parentEntries(service, root);
@@ -435,7 +436,14 @@ async function performNow(opts) {
435
436
  const answered = outcome.data !== null && typeof outcome.data === 'object' && !Array.isArray(outcome.data) ? outcome.data : {};
436
437
  const primary = resolved.find((a) => a.id === outcome.actionId) ?? resolved[0];
437
438
  const also = new Map((outcome.also ?? []).map((a) => [a.actionId, a]));
439
+ const refusedHere = new Map((outcome.refused ?? []).map((r) => [r.actionId, r.reason]));
438
440
  for (const action of resolved) {
441
+ const refusal = refusedHere.get(action.id);
442
+ if (refusal !== undefined) {
443
+ settle(action, at, { status: 'refused', reason: refusal });
444
+ refusedIds.add(action.subject.id);
445
+ continue;
446
+ }
439
447
  const answeredId = action === primary ? outcome.externalId : also.get(action.id)?.externalId;
440
448
  const externalId = answeredId || aliases.get(`${action.subject.type}:${action.subject.id}`);
441
449
  settle(action, at, { status: 'deployed', ...(action === primary && outcome.url ? { url: outcome.url } : {}) }, { ...action.fields, ...(action === primary ? answered : also.get(action.id)?.data ?? {}) }, externalId || undefined);
@@ -443,6 +451,10 @@ async function performNow(opts) {
443
451
  report.pushed += 1;
444
452
  report.deployed.push({ actionId: action.id, at, ...(externalId ? { externalId } : {}), ...(data !== undefined ? { data } : {}), ...(action === primary && outcome.response ? { response: outcome.response } : {}) });
445
453
  }
454
+ if (refusedHere.size) {
455
+ report.partial = [...refusedHere].map(([actionId, reason]) => ({ actionId, reason }));
456
+ stopped = true;
457
+ }
446
458
  }
447
459
  catch (error) {
448
460
  // a write the vendor refuses (a 4xx: it can never be sent as it is) settles the group refused, as a check's
@@ -545,24 +557,57 @@ export async function performRequestGroup(service, request, root) {
545
557
  }
546
558
  catch (error) {
547
559
  if (error instanceof DeploymentOrderError) {
548
- for (const e of [...group].reverse())
549
- revertAction({ service, actionId: e.id, occurredAt: worldNow(), ...(root !== undefined ? { root } : {}) });
560
+ revertRequest(service, request, group, root);
550
561
  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);
551
562
  }
552
563
  throw error instanceof HeadError ? error : new HeadError(error instanceof Error ? error.message : String(error));
553
564
  }
554
565
  if (report.refused || report.failed) {
555
- for (const e of [...group].reverse())
556
- revertAction({ service, actionId: e.id, occurredAt: worldNow(), ...(root !== undefined ? { root } : {}) });
566
+ revertRequest(service, request, group, root);
557
567
  return report.refused ? new RefusedWriteError(report.refused.check, report.refused.reason, report.refused.actionId) : new VendorWriteError(report.failed.error, report.failed.actionId);
558
568
  }
569
+ if (report.partial)
570
+ revertRequest(service, request, group.filter((e) => report.partial.some((r) => r.actionId === e.id)), root);
559
571
  for (const d of report.deployed) {
560
572
  const e = group.find((g) => g.id === d.actionId);
561
573
  if (e && d.externalId && d.externalId !== e.subject.id)
562
574
  ids.set(e.subject.id, d.externalId);
563
575
  }
564
576
  const response = report.deployed.find((entry) => entry.response)?.response;
565
- return { ids, ...(response ? { response } : {}) };
577
+ return { ids, ...(response ? { response } : {}), ...(report.partial ? { refused: report.partial } : {}) };
578
+ }
579
+ /** Revert what a request's refused entries made, latest first. An entry that is a declared push a screen recorded
580
+ * (`input.git`, ADR 0031) owns the request's writes after it up to the next such push, its push's effects (the
581
+ * reactions and vendor moves the pack runs on it), which go with it; any other refused entry goes alone, as the group
582
+ * it is in. */
583
+ function revertRequest(service, request, refused, root) {
584
+ const pushed = (e) => e.input?.git !== undefined && e.input.git !== null;
585
+ const undo = new Set();
586
+ if (refused.some(pushed)) {
587
+ const made = branchEntries(service, root).filter((e) => e.correlationId === request && e.op === 'set');
588
+ for (const r of refused) {
589
+ if (!pushed(r)) {
590
+ undo.add(r.id);
591
+ continue;
592
+ }
593
+ const at = made.findIndex((e) => e.id === r.id);
594
+ if (at < 0) {
595
+ undo.add(r.id);
596
+ continue;
597
+ }
598
+ undo.add(r.id);
599
+ for (const e of made.slice(at + 1)) {
600
+ if (pushed(e))
601
+ break;
602
+ undo.add(e.id);
603
+ }
604
+ }
605
+ }
606
+ else
607
+ for (const r of refused)
608
+ undo.add(r.id);
609
+ for (const e of branchEntries(service, root).filter((x) => undo.has(x.id)).reverse())
610
+ revertAction({ service, actionId: e.id, occurredAt: worldNow(), ...(root !== undefined ? { root } : {}) });
566
611
  }
567
612
  /** Wrap a pack's fetch: a `RefusedWriteError` or `VendorWriteError` the head raised becomes the
568
613
  * vendor's own error body (the pack's `shape`), so an app under live use sees what the vendor
@@ -0,0 +1,29 @@
1
+ export type HtmlAttribute = {
2
+ name: string;
3
+ value: string;
4
+ prefix?: string;
5
+ namespace?: string;
6
+ };
7
+ export type HtmlElement = {
8
+ type: 'element';
9
+ name: string;
10
+ namespace: string;
11
+ attributes: HtmlAttribute[];
12
+ children: HtmlNode[];
13
+ };
14
+ export type HtmlText = {
15
+ type: 'text';
16
+ value: string;
17
+ };
18
+ export type HtmlNode = HtmlElement | HtmlText;
19
+ export declare class HtmlError extends Error {
20
+ readonly code: 'parser-unavailable';
21
+ constructor(code: 'parser-unavailable');
22
+ }
23
+ /**
24
+ * A fragment parsed as WHATWG fragment parsing does in an HTML `<body>` context: elements and text, comments dropped,
25
+ * an HTML `<template>`'s content carried as that element's children. Importing this module does not load the grammar.
26
+ */
27
+ export declare function parseFragment(source: string): Promise<HtmlNode[]>;
28
+ /** Nodes serialized by the WHATWG HTML fragment serialization: its escaping, its void elements, its foreign names. */
29
+ export declare function serialize(nodes: HtmlNode[]): Promise<string>;
@@ -0,0 +1,64 @@
1
+ export class HtmlError extends Error {
2
+ code;
3
+ constructor(code) {
4
+ super('HTML parser parse5 7.3.0 is unavailable');
5
+ this.code = code;
6
+ this.name = 'HtmlError';
7
+ }
8
+ }
9
+ let grammar;
10
+ function loadGrammar() {
11
+ return grammar ??= import('parse5').catch(() => { grammar = undefined; throw new HtmlError('parser-unavailable'); });
12
+ }
13
+ /**
14
+ * A fragment parsed as WHATWG fragment parsing does in an HTML `<body>` context: elements and text, comments dropped,
15
+ * an HTML `<template>`'s content carried as that element's children. Importing this module does not load the grammar.
16
+ */
17
+ export async function parseFragment(source) {
18
+ const { parseFragment: parse, defaultTreeAdapter: tree, html } = await loadGrammar();
19
+ const convert = (nodes) => {
20
+ const out = [];
21
+ for (const node of nodes) {
22
+ if (node.nodeName === '#text')
23
+ out.push({ type: 'text', value: node.value });
24
+ else if ('tagName' in node) {
25
+ const element = node;
26
+ // only an HTML template has content; an SVG or MathML `template` is a foreign element with children
27
+ const children = element.tagName === 'template' && element.namespaceURI === html.NS.HTML
28
+ ? element.content.childNodes
29
+ : element.childNodes;
30
+ out.push({
31
+ type: 'element', name: element.tagName, namespace: element.namespaceURI,
32
+ attributes: element.attrs.map(attr => ({ name: attr.name, value: attr.value, ...(attr.prefix ? { prefix: attr.prefix } : {}), ...(attr.namespace ? { namespace: attr.namespace } : {}) })),
33
+ children: convert(children),
34
+ });
35
+ }
36
+ }
37
+ return out;
38
+ };
39
+ return convert(parse(tree.createElement('body', html.NS.HTML, []), source, {}).childNodes);
40
+ }
41
+ /** Nodes serialized by the WHATWG HTML fragment serialization: its escaping, its void elements, its foreign names. */
42
+ export async function serialize(nodes) {
43
+ const { serialize: write, defaultTreeAdapter: tree, html } = await loadGrammar();
44
+ const append = (parent, list) => {
45
+ for (const node of list) {
46
+ if (node.type === 'text') {
47
+ tree.insertText(parent, node.value);
48
+ continue;
49
+ }
50
+ const element = tree.createElement(node.name, node.namespace, node.attributes.map(({ name, value, prefix, namespace }) => ({ name, value, ...(prefix ? { prefix } : {}), ...(namespace ? { namespace } : {}) })));
51
+ tree.appendChild(parent, element);
52
+ if (node.name === 'template' && node.namespace === html.NS.HTML) {
53
+ const content = tree.createDocumentFragment();
54
+ tree.setTemplateContent(element, content);
55
+ append(content, node.children);
56
+ }
57
+ else
58
+ append(element, node.children);
59
+ }
60
+ };
61
+ const fragment = tree.createDocumentFragment();
62
+ append(fragment, nodes);
63
+ return write(fragment);
64
+ }
@@ -56,9 +56,10 @@ export type { TwinResource } from './serve.js';
56
56
  export { passwordHash, personOf, personWith, recordPerson, shownPersonFields, type Person } from './people.js';
57
57
  export { EngineDatabaseError, EngineUnavailableError, quoteIdent } from './managed-database.js';
58
58
  export type { Batch as EngineBatch, ManagedDatabase, Statement as EngineStatement, StatementResult as EngineStatementResult } from './managed-database.js';
59
- export type { Mail, SmtpRoute } from './smtp.js';
59
+ export type { Mail, MailReply } from './smtp.js';
60
60
  export * as s3 from './s3/wire.js';
61
61
  export * as xml from './xml.js';
62
+ export * as html from './html.js';
62
63
  export * as clickhouse from './clickhouse/index.js';
63
64
  export { sampleFromSchema } from './schema-sample.js';
64
65
  export * as openaiWire from './openai-wire.js';
@@ -70,6 +71,7 @@ export { placeholderPng } from './placeholder-image.js';
70
71
  export { imageDimensions, readImageHeader, readMp4Header } from './media-header.js';
71
72
  export { presignSigV4, signSigV4, verifySigV4, type SigV4Verdict } from './sigv4.js';
72
73
  export * as redis from './redis/index.js';
74
+ export * as smtpServer from './smtp-server.js';
73
75
  export * as mysql from './mysql/index.js';
74
76
  export * as live from './live/index.js';
75
77
  export * as mongodb from './mongodb/index.js';
package/dist/src/index.js CHANGED
@@ -51,6 +51,8 @@ export { EngineDatabaseError, EngineUnavailableError, quoteIdent } from "./manag
51
51
  export * as s3 from "./s3/wire.js";
52
52
  // Generic strict XML grammar; vendor interpretation remains in its consumer.
53
53
  export * as xml from "./xml.js";
54
+ // Generic WHATWG HTML grammar; what a pack keeps of received HTML remains the pack's policy.
55
+ export * as html from "./html.js";
54
56
  export * as clickhouse from "./clickhouse/index.js";
55
57
  export { sampleFromSchema } from "./schema-sample.js";
56
58
  export * as openaiWire from "./openai-wire.js";
@@ -62,6 +64,7 @@ export { placeholderPng } from "./placeholder-image.js";
62
64
  export { imageDimensions, readImageHeader, readMp4Header } from "./media-header.js";
63
65
  export { presignSigV4, signSigV4, verifySigV4 } from "./sigv4.js";
64
66
  export * as redis from "./redis/index.js";
67
+ export * as smtpServer from "./smtp-server.js";
65
68
  export * as mysql from "./mysql/index.js";
66
69
  export * as live from "./live/index.js";
67
70
  export * as mongodb from "./mongodb/index.js";
@@ -19,7 +19,9 @@ import { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER, runAsVendorMove } from "./
19
19
  import { parseScenarioDocument, ScenarioEngine, ScenarioError, statefulTwinManifest, twinManifest } from "./scenario.js";
20
20
  import { getActiveWorldStore } from "./world-store.js";
21
21
  import { runWithCorrelationId } from "./actions.js";
22
- import { deferPerform, performRequestGroup, RefusedWriteError, VendorWriteError } from "./head.js";
22
+ import { deferPerform, deployableEntries, performRequestGroup, RefusedWriteError, VendorWriteError } from "./head.js";
23
+ import { FLUSH, ZERO_SHA, pktLine, readPktLines } from "./git/smart-http.js";
24
+ import { concat } from "./git/objects.js";
23
25
  import { withCors } from "./cors.js";
24
26
  import { contentAddressOf } from "./content-route.js";
25
27
  import { getPack } from "./packRegistry.js";
@@ -27,7 +29,7 @@ import { hostRules } from '../host-routing.cjs';
27
29
  import { expireSocketWaits, socketUpgrade } from "./sockets.js";
28
30
  import { liveIngest } from "./live/ingest.js";
29
31
  import { readResourceBlob, removeResourceBlob, writeResourceBlob } from "./resource-blob.js";
30
- import { resourcesOfType } from "./derived-core.js";
32
+ import { gitRepositoryOf, resourcesOfType } from "./derived-core.js";
31
33
  import { worldNow } from "./world-clock.js";
32
34
  import { MACHINE_POOL, POOL_KINDS, resetFakePool } from "./machines.js";
33
35
  import { applyTwinWrite } from "./serve.js";
@@ -635,7 +637,44 @@ export function createPackFetch(parts, options = {}) {
635
637
  const { value: answer, appended } = await runWithCorrelationId(id, () => deferPerform(id, () => fetch(request)));
636
638
  if (!appended)
637
639
  return answer;
640
+ // a receive-pack's pushes (derived-core.ts pushedInput): what the World's refs held before and after each
641
+ const pushes = m.git ? deployableEntries(m.service, options.root).filter((e) => e.correlationId === id && e.input?.git).map((e) => ({ actionId: e.id, ...e.input.git })) : [];
638
642
  const performed = await runWithCorrelationId(id, () => performRequestGroup(m.service, id, options.root));
643
+ // under `auto`, what the root did is what the World keeps (ADR 0031): a ref it refused goes back to its value before
644
+ // the push, by compare-and-set under the ref lock (a later push built on it keeps its value), its push and effects
645
+ // are reverted (head.ts), and the client hears git's own report, `ok` or `ng` for each ref
646
+ if (pushes.length) {
647
+ const whole = performed instanceof RefusedWriteError ? performed.reason : performed instanceof VendorWriteError ? performed.message : undefined;
648
+ const refused = new Map();
649
+ for (const p of pushes) {
650
+ const reason = whole ?? performed.refused?.find((r) => r.actionId === p.actionId)?.reason;
651
+ if (reason !== undefined)
652
+ refused.set(p.actionId, reason);
653
+ }
654
+ if (refused.size) {
655
+ for (const p of [...pushes].reverse()) {
656
+ if (!refused.has(p.actionId))
657
+ continue;
658
+ const { refs } = gitRepositoryOf(m, p.name, options.root);
659
+ refs.withLock(() => {
660
+ if ((refs.get(p.ref) ?? ZERO_SHA) !== p.after)
661
+ return;
662
+ if (p.before === ZERO_SHA)
663
+ refs.delete(p.ref);
664
+ else
665
+ refs.set(p.ref, p.before);
666
+ });
667
+ }
668
+ const ng = new Map(pushes.filter((p) => refused.has(p.actionId)).map((p) => [p.ref, refused.get(p.actionId).replace(/\s+/g, ' ')]));
669
+ const report = concat(...readPktLines(new Uint8Array(await answer.arrayBuffer())).lines.map((line) => {
670
+ const ref = line.startsWith('ok ') ? line.slice(3).trim() : undefined;
671
+ return pktLine(ref !== undefined && ng.has(ref) ? `ng ${ref} ${ng.get(ref)}\n` : line);
672
+ }), FLUSH);
673
+ const headers = new Headers(answer.headers);
674
+ headers.delete('content-length');
675
+ return new Response(report, { status: answer.status, headers });
676
+ }
677
+ }
639
678
  // the vendor's refusal, as its status and words (perform names them `<status>: <message>`), in the vendor's error body
640
679
  if (performed instanceof RefusedWriteError) {
641
680
  const said = /^(\d{3}): ([\s\S]*)$/.exec(performed.reason);
@@ -8,9 +8,8 @@ export declare function installedVendorPacks(from: string, options?: {
8
8
  }): Map<string, string>;
9
9
  /** Node's nearest installed package, including linked packages and hoisted workspace dependencies. */
10
10
  export declare function installedPackageDir(from: string, name: string): string | undefined;
11
- /** A pack repository itself, or a platform checkout's P3 repository: the one `VOLTER_PACKS` names (a worktree's own
12
- * pinned packs, as the repository's scripts read it), else the one beside it. Applications do not acquire a
13
- * sibling checkout merely because one happens to be beside them. */
11
+ /** The pack repository a World is rooted in, when it is one (ADR 0034: a platform checkout finds no pack repository
12
+ * beside it; a pack reaches any other World as an installed or linked package). */
14
13
  export declare function packRepository(from: string): string | undefined;
15
14
  /** Resolve one vendor's package directory for host-side reads (assets, infrastructure launchers). */
16
15
  export declare function vendorPackageDir(from: string, vendor: string): string | undefined;
@@ -121,23 +121,13 @@ export function installedPackageDir(from, name) {
121
121
  dir = up;
122
122
  }
123
123
  }
124
- /** A pack repository itself, or a platform checkout's P3 repository: the one `VOLTER_PACKS` names (a worktree's own
125
- * pinned packs, as the repository's scripts read it), else the one beside it. Applications do not acquire a
126
- * sibling checkout merely because one happens to be beside them. */
124
+ /** The pack repository a World is rooted in, when it is one (ADR 0034: a platform checkout finds no pack repository
125
+ * beside it; a pack reaches any other World as an installed or linked package). */
127
126
  export function packRepository(from) {
128
127
  let dir = resolve(from);
129
128
  for (;;) {
130
129
  if (packageName(dir) === 'twin-packs-p3')
131
130
  return dir;
132
- if (packageName(join(dir, 'packages', 'world-core')) === '@volter/world-core') {
133
- const env = globalThis.process?.env?.VOLTER_PACKS;
134
- const named = env ? resolve(env) : undefined;
135
- if (named && packageName(named) === 'twin-packs-p3')
136
- return named;
137
- const sibling = join(dirname(dir), 'twin-packs-p3');
138
- if (packageName(sibling) === 'twin-packs-p3')
139
- return sibling;
140
- }
141
131
  const up = dirname(dir);
142
132
  if (up === dir)
143
133
  return undefined;
@@ -148,7 +138,7 @@ export function packRepository(from) {
148
138
  export function vendorPackageDir(from, vendor) {
149
139
  if (!/^[a-z][a-z0-9-]*$/.test(vendor))
150
140
  return undefined;
151
- const installed = installedVendorPacks(from).get(vendor);
141
+ const installed = installedVendorPacks(from, { vendors: [vendor] }).get(vendor);
152
142
  const repo = packRepository(from);
153
143
  const checkout = repo === undefined ? undefined : join(repo, vendor);
154
144
  if (!checkout || packageName(checkout) !== `@volter/twin-${vendor}` || !isDerivedPack(checkout))
@@ -118,7 +118,9 @@ export declare function pullOnSchedule<T>(vendor: PullVendor, pull: () => Promis
118
118
  * matches and no exclude does. Validated by scripts/pack-facts.ts: keys are `[a-z0-9-]+`, a
119
119
  * foreign key is never another pack's vendor id, belongs to one pack, and must appear in the
120
120
  * pack's `adoption.worldIds` (the world wires `<KEY>_TWIN_URL` from there); a pack must claim its
121
- * own vendor id too; no key is exclude-only; `exclude` is `true` or absent.
121
+ * own vendor id too; no key is exclude-only; `exclude` is `true` or absent. `basic` is a vendor fact for the executor: on
122
+ * a URL the rule names, the host takes the sealed token as HTTP Basic's password under this user (a vendor's git over
123
+ * HTTP whose API takes the same token as a bearer; ADR 0031).
122
124
  */
123
125
  export type HostRule = ({
124
126
  host: string;
@@ -131,6 +133,7 @@ export type HostRule = ({
131
133
  demandPathPattern?: string | false;
132
134
  key?: string;
133
135
  exclude?: true;
136
+ basic?: string;
134
137
  };
135
138
  export type RoundTripWrite = {
136
139
  method: string;
@@ -255,6 +258,19 @@ export type TwinPack = {
255
258
  scheme?: string;
256
259
  note: string;
257
260
  }>;
261
+ /** The vendor's own requests a World's generated seed makes for what its world.json declares (ADR 0033 §3), each a URL
262
+ * at the vendor's own host, defined by the twin's own screen form for that act: its sign-up, and its domain screen's
263
+ * add and verify with `record`, the one `/_twin/` stand-in, for a record at the domain's DNS host; `settles` is the
264
+ * World-clock time the vendor takes to settle a verified domain (`10m`). */
265
+ seed?: {
266
+ signUp?: string;
267
+ domain?: {
268
+ add: string;
269
+ record: string;
270
+ verify: string;
271
+ settles: string;
272
+ };
273
+ };
258
274
  stateSystem?: StateSystemAdapters;
259
275
  /** The pack has no vendor-backed half, and why (copied from the manifest by packOf). */
260
276
  vendorBacked?: {
@@ -55,6 +55,7 @@ export { installedPackageDir, installedVendorPacks, packVendor, packRepository,
55
55
  export * as git from './git/index.js';
56
56
  export * as s3 from './s3/wire.js';
57
57
  export * as xml from './xml.js';
58
+ export * as html from './html.js';
58
59
  export { PLACEHOLDER_REMOTE, beginPlaceholderPull, endPlaceholderPull, placeholderPullActive, placeholderPullMarkerPath, withPlaceholderPull, placeholderEventsFor } from './placeholder-remote.js';
59
60
  export { aliasesFrom, ownFields, subjectHistory, rebaseBranch, cutCheckpoint, appendParentEntry, wholeLog, branchEntries, branchLogPath, branchMetaPath, CHECKPOINT_EVERY, dropCheckpoint, foldEntries, landedCopy, landedIds, parentEntries, parentLogPath, position, readBranchMeta, readTree, toEntry, toEvent, unpushedEntries, writeBranchMeta } from './log.js';
60
61
  export type { BranchMeta, Entry, EntryOp, Receipt } from './log.js';
@@ -79,6 +80,7 @@ export { brandTokensResponse } from './brand-tokens.js';
79
80
  export { multipartBoundary, multipartParts, multipartEnvelope, inferMultipartBoundary, multipartDelimiterPattern, type MultipartPart, type MultipartFramedPart, type MultipartEnvelope } from './multipart.js';
80
81
  export { corsHeaders, withCors, type CorsDecl } from './cors.js';
81
82
  export { setInProcessTwins, vendorFetch, VendorUnreachableError, type InProcessTwin } from './vendor-call.js';
83
+ export { setInProcessMailServers } from './smtp.js';
82
84
  export { closeManagedDatabases, databaseSession, EngineDatabaseError, EngineUnavailableError, managedDatabase, quoteIdent, useDatabaseDriver, type DatabaseDriver, type DatabaseSession } from './managed-database.js';
83
85
  export type { Batch as EngineBatch, ManagedDatabase, Statement as EngineStatement, StatementResult as EngineStatementResult } from './managed-database.js';
84
86
  export { readResourceBlob, readResourceBlobRange, removeResourceBlob, resourceBlobFile, resourceBlobKey, resourceBlobSize, resourceChain, writeResourceBlob, writeResourceBlobStream } from './resource-blob.js';
@@ -78,6 +78,8 @@ export * as git from "./git/index.js";
78
78
  export * as s3 from "./s3/wire.js";
79
79
  // Generic strict XML grammar; vendor interpretation remains in its consumer.
80
80
  export * as xml from "./xml.js";
81
+ // Generic WHATWG HTML grammar; what a pack keeps of received HTML remains the pack's policy.
82
+ export * as html from "./html.js";
81
83
  // The push arm: the kernel's transaction over a pack's one `perform<Name>Action` (contract section of that name).
82
84
  // The placeholder remote (contract section of that name): the seed is a pull, never a pending write.
83
85
  export { PLACEHOLDER_REMOTE, beginPlaceholderPull, endPlaceholderPull, placeholderPullActive, placeholderPullMarkerPath, withPlaceholderPull, placeholderEventsFor } from "./placeholder-remote.js";
@@ -102,6 +104,7 @@ export { brandTokensResponse } from "./brand-tokens.js";
102
104
  export { multipartBoundary, multipartParts, multipartEnvelope, inferMultipartBoundary, multipartDelimiterPattern } from "./multipart.js";
103
105
  export { corsHeaders, withCors } from "./cors.js";
104
106
  export { setInProcessTwins, vendorFetch, VendorUnreachableError } from "./vendor-call.js";
107
+ export { setInProcessMailServers } from "./smtp.js";
105
108
  export { closeManagedDatabases, databaseSession, EngineDatabaseError, EngineUnavailableError, managedDatabase, quoteIdent, useDatabaseDriver } from "./managed-database.js";
106
109
  export { readResourceBlob, readResourceBlobRange, removeResourceBlob, resourceBlobFile, resourceBlobKey, resourceBlobSize, resourceChain, writeResourceBlob, writeResourceBlobStream } from "./resource-blob.js";
107
110
  export { assertStateRemovable, checkParent, stateGeneration, withAncestryLock, withHistoryLock, withHistoryLocks, withStateLock, withStateRemoval } from "./ancestry.js";
@@ -67,7 +67,7 @@ export declare function serveStream(options: {
67
67
  maxConnections?: number;
68
68
  }): Promise<{
69
69
  port: number;
70
- stop: () => void;
70
+ stop: () => Promise<void>;
71
71
  }>;
72
72
  export type WebSocketRelay = {
73
73
  url: string;
@@ -153,6 +153,7 @@ export async function serveStream(options) {
153
153
  throw new StreamListenerUnavailable('this runtime has no TCP listener (node:net)');
154
154
  const cap = options.maxConnections ?? options.stream.maxConnections;
155
155
  let open = 0;
156
+ const accepted = new Set();
156
157
  const server = net.createServer((socket) => {
157
158
  // past its cap a connection is closed before the stream sees it, so connections that never get anywhere cannot pile up
158
159
  if (cap !== undefined && open >= cap) {
@@ -160,7 +161,8 @@ export async function serveStream(options) {
160
161
  return;
161
162
  }
162
163
  open += 1;
163
- socket.once('close', () => { open -= 1; });
164
+ accepted.add(socket);
165
+ socket.once('close', () => { open -= 1; accepted.delete(socket); });
164
166
  const conn = options.stream({ write: (bytes) => { socket.write(bytes); }, end: () => { socket.end(); } }, socket.remoteAddress ?? '127.0.0.1');
165
167
  // a connection that says what waits (`backlog`: whole messages its work has not reached) is read no further past the
166
168
  // high-water mark until that work has settled, so a peer faster than the twin (a live ingest pushed faster than real
@@ -182,7 +184,14 @@ export async function serveStream(options) {
182
184
  });
183
185
  await new Promise((resolve, reject) => { server.once('error', reject); server.listen(options.port ?? 0, options.hostname ?? '127.0.0.1', () => resolve()); });
184
186
  const address = server.address();
185
- return { port: typeof address === 'object' && address ? address.port : options.port ?? 0, stop: () => { server.close(); } };
187
+ // stop is the listener's real retirement: it settles once the listener is closed and every connection it accepted has
188
+ // ended (each is closed here, so none holds the close open)
189
+ const stop = () => new Promise((resolve) => {
190
+ server.close(() => resolve());
191
+ for (const socket of accepted)
192
+ socket.destroy();
193
+ });
194
+ return { port: typeof address === 'object' && address ? address.port : options.port ?? 0, stop };
186
195
  }
187
196
  function builtin(name) {
188
197
  const get = process.getBuiltinModule;
@@ -0,0 +1,78 @@
1
+ import type { HandlerContext } from './derived-core.js';
2
+ import type { TwinStream } from './twin-fetch.js';
3
+ /** What a server's front answers the stream: its reply lines, the moved session, and whether the stream reads a
4
+ * message's data next (`data`), an AUTH response (`auth`) or commands, or closes. */
5
+ export type SmtpAnswer<S> = {
6
+ replies: string[];
7
+ session: S;
8
+ mode: 'command' | 'data' | 'auth';
9
+ close?: boolean;
10
+ };
11
+ /** The bridge between a connection and the pack's front: the header that carries the pack's key, the key, a fresh
12
+ * session, and the pack's answer when its front cannot answer (its words for a local error). */
13
+ export type SmtpBridge<S> = {
14
+ header: string;
15
+ key: () => string;
16
+ fresh: () => S;
17
+ failure: (session: S) => SmtpAnswer<S>;
18
+ };
19
+ /** A connection's bytes framed as SMTP frames them, each piece posted to the pack's front (`smtpFront`) through its
20
+ * fetch with the session the connection carries: commands are lines ended by CRLF; after a 354, the message's lines up
21
+ * to the lone dot, their leading dot removed; after a 334, one line of AUTH response. Pipelined commands are answered
22
+ * in order. */
23
+ export declare function smtpStream<S>(fetch: (request: Request) => Response | Promise<Response>, bridge: SmtpBridge<S>): TwinStream;
24
+ /** What a pack's front does with each piece the stream carries: the greeting, a command line, a message's data, an AUTH
25
+ * response (a server that takes none leaves it out). */
26
+ type SmtpServer<S> = {
27
+ open(): SmtpAnswer<S> | Promise<SmtpAnswer<S>>;
28
+ command(session: S, line: string): SmtpAnswer<S> | Promise<SmtpAnswer<S>>;
29
+ data(session: S, data: string): SmtpAnswer<S> | Promise<SmtpAnswer<S>>;
30
+ response?(session: S, response: string): SmtpAnswer<S> | Promise<SmtpAnswer<S>>;
31
+ };
32
+ /** The pack's front (its `around`): a request the stream signed with the pack's key is a piece of a connection, answered
33
+ * by the pack's server; any other request is the pack's own (its doors and screens). */
34
+ export declare function smtpFront<S>(ctx: HandlerContext, next: (request?: Request) => Promise<Response>, bridge: SmtpBridge<S>, server: SmtpServer<S>): Promise<Response>;
35
+ /** A command's verb (upper-cased) and its argument. */
36
+ export declare function smtpCommand(line: string): {
37
+ verb: string;
38
+ arg: string;
39
+ };
40
+ /** The session machine's word on a command from where a session stands (the manifest's states): a refusal's reply, or
41
+ * undefined to go on. */
42
+ export declare function smtpOrder(ctx: Pick<HandlerContext, 'legal'>, machine: string, stage: string, op: string, to?: string): string | undefined;
43
+ /** EHLO's multi-line 250 reply: the server's name, then each keyword. */
44
+ export declare const smtpEhlo: (keywords: string[]) => string[];
45
+ /** An address's domain, lower-cased. */
46
+ export declare const smtpDomain: (address: string) => string;
47
+ /** MAIL's argument: the sender and its SIZE, BODY and SMTPUTF8 parameters, or why it is refused (the pack words it). */
48
+ export declare function smtpMailFrom(arg: string): {
49
+ from: string;
50
+ size?: number;
51
+ smtputf8: boolean;
52
+ } | {
53
+ refused: 'syntax' | 'sender' | 'size';
54
+ } | {
55
+ refused: 'parameter';
56
+ parameter: string;
57
+ };
58
+ /** RCPT's argument: the recipient, or why it is refused (the pack words it). */
59
+ export declare function smtpRcptTo(arg: string): {
60
+ to: string;
61
+ } | {
62
+ refused: 'syntax' | 'recipient';
63
+ } | {
64
+ refused: 'parameter';
65
+ parameter: string;
66
+ };
67
+ /** A transaction's state cleared, the greeting kept (RSET, a new EHLO, the end of DATA): the session's `stage`,
68
+ * `greeted` and an empty `rcpt`, with whatever of the session the pack keeps across transactions (`kept`). */
69
+ export declare function smtpCleared<S extends {
70
+ stage: string;
71
+ rcpt: string[];
72
+ greeted?: 'helo' | 'ehlo';
73
+ }>(s: S, stage: string, kept: Omit<S, 'stage' | 'rcpt' | 'greeted' | 'from'>): Omit<S, 'stage' | 'rcpt' | 'greeted' | 'from'> & {
74
+ stage: string;
75
+ rcpt: string[];
76
+ greeted?: 'helo' | 'ehlo';
77
+ };
78
+ export {};