@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.
- package/dist/generated/pack-facts.json +44 -3660
- package/dist/pack-facts.cjs +4 -4
- package/dist/src/browser.d.ts +4 -0
- package/dist/src/browser.js +10 -0
- package/dist/src/derived-core.d.ts +45 -5
- package/dist/src/derived-core.js +53 -8
- package/dist/src/derived-real.js +144 -2
- package/dist/src/derived.d.ts +1 -1
- package/dist/src/executor.d.ts +2 -2
- package/dist/src/executor.js +13 -0
- package/dist/src/git/index.d.ts +1 -0
- package/dist/src/git/index.js +1 -0
- package/dist/src/git/smart-http-client.d.ts +28 -0
- package/dist/src/git/smart-http-client.js +95 -0
- package/dist/src/git/smart-http.d.ts +3 -0
- package/dist/src/git/smart-http.js +9 -0
- package/dist/src/head.d.ts +16 -1
- package/dist/src/head.js +52 -7
- package/dist/src/html.d.ts +29 -0
- package/dist/src/html.js +64 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +3 -0
- package/dist/src/pack-fetch.js +41 -2
- package/dist/src/pack-paths.d.ts +2 -3
- package/dist/src/pack-paths.js +3 -13
- package/dist/src/packRegistry.d.ts +17 -1
- package/dist/src/runtime.d.ts +2 -0
- package/dist/src/runtime.js +3 -0
- package/dist/src/serve-http.d.ts +1 -1
- package/dist/src/serve-http.js +11 -2
- package/dist/src/smtp-server.d.ts +78 -0
- package/dist/src/smtp-server.js +160 -0
- package/dist/src/smtp.d.ts +29 -11
- package/dist/src/smtp.js +221 -52
- package/generated/pack-facts.json +44 -3660
- package/pack-facts.cjs +4 -4
- package/package.json +4 -2
- package/src/browser.ts +10 -0
- package/src/derived-core.ts +68 -12
- package/src/derived-real.ts +121 -2
- package/src/derived.ts +2 -2
- package/src/executor.ts +13 -2
- package/src/git/index.ts +1 -0
- package/src/git/smart-http-client.ts +99 -0
- package/src/git/smart-http.ts +7 -0
- package/src/head.ts +37 -6
- package/src/html.ts +72 -0
- package/src/index.ts +4 -1
- package/src/pack-fetch.ts +35 -2
- package/src/pack-paths.ts +3 -11
- package/src/packRegistry.ts +9 -1
- package/src/runtime.ts +3 -0
- package/src/serve-http.ts +11 -3
- package/src/smtp-server.ts +166 -0
- package/src/smtp.ts +199 -53
package/dist/src/head.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>;
|
package/dist/src/html.js
ADDED
|
@@ -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
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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,
|
|
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";
|
package/dist/src/pack-fetch.js
CHANGED
|
@@ -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);
|
package/dist/src/pack-paths.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
12
|
-
*
|
|
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;
|
package/dist/src/pack-paths.js
CHANGED
|
@@ -121,23 +121,13 @@ export function installedPackageDir(from, name) {
|
|
|
121
121
|
dir = up;
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
|
-
/**
|
|
125
|
-
*
|
|
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?: {
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -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';
|
package/dist/src/runtime.js
CHANGED
|
@@ -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";
|
package/dist/src/serve-http.d.ts
CHANGED
package/dist/src/serve-http.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {};
|