humanish 0.20.4 → 0.21.0
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/comms-email-catch.d.ts +53 -0
- package/dist/comms-email-catch.js +209 -0
- package/dist/comms-email-catch.js.map +1 -0
- package/dist/comms-faux-inbox.d.ts +37 -0
- package/dist/comms-faux-inbox.js +157 -0
- package/dist/comms-faux-inbox.js.map +1 -0
- package/dist/comms-types.d.ts +63 -0
- package/dist/comms-types.js +10 -0
- package/dist/comms-types.js.map +1 -0
- package/dist/concurrent-shared-world-lab.js +9 -2
- package/dist/concurrent-shared-world-lab.js.map +1 -1
- package/dist/cua-actor-lab.d.ts +14 -0
- package/dist/cua-actor-lab.js +24 -2
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/init-templates.js +7 -4
- package/dist/init-templates.js.map +1 -1
- package/dist/run.d.ts +15 -0
- package/dist/run.js +9 -0
- package/dist/run.js.map +1 -1
- package/docs/architecture/external-public-shared-world.md +8 -5
- package/docs/contracts/run-bundle.md +7 -1
- package/docs/contracts/schemas.md +1 -1
- package/docs/goals/current.md +1 -1
- package/docs/product/cineguessr-3player-external-public.md +14 -5
- package/docs/ramp/README.md +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ServerResponse } from "node:http";
|
|
2
|
+
import type { CommsChannel } from "./comms-types.js";
|
|
3
|
+
/** One send, normalized across providers. `body` is html-preferred, else text. */
|
|
4
|
+
export interface NormalizedSend {
|
|
5
|
+
from: string;
|
|
6
|
+
to: string[];
|
|
7
|
+
subject?: string;
|
|
8
|
+
body: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A provider wire-shape adapter. Vendor-neutral seam: the server owns the HTTP + routing + bus
|
|
12
|
+
* delivery; a profile only knows how ONE provider (or a custom app) shapes its send request.
|
|
13
|
+
*/
|
|
14
|
+
export interface EmailSendProfile {
|
|
15
|
+
/** Descriptive name (e.g. "generic", "sendgrid"). Not a dependency — just a label. */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Request paths (method POST) this profile accepts a send on. */
|
|
18
|
+
sendPaths: string[];
|
|
19
|
+
/** Parse a POST body (already JSON-parsed) into zero+ normalized sends (a batch yields many). */
|
|
20
|
+
parse(path: string, body: unknown): NormalizedSend[];
|
|
21
|
+
/** Optional provider-faithful success response. Default: 200 `{ id }` (single) / `{ data:[{id}] }`. */
|
|
22
|
+
respond?: (res: ServerResponse, ids: string[], batch: boolean) => void;
|
|
23
|
+
}
|
|
24
|
+
export interface EmailCatchServer {
|
|
25
|
+
/** Point the app's email-API base URL here (loopback only). */
|
|
26
|
+
readonly url: string;
|
|
27
|
+
readonly port: number;
|
|
28
|
+
/** Every normalized send accepted this run (runtime-only; for inspection/tests). */
|
|
29
|
+
readonly received: NormalizedSend[];
|
|
30
|
+
close(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
export interface EmailCatchOptions {
|
|
33
|
+
/** Bind host. Default 127.0.0.1 (loopback ONLY). */
|
|
34
|
+
host?: string;
|
|
35
|
+
/** Port. Default 0 (ephemeral). */
|
|
36
|
+
port?: number;
|
|
37
|
+
/** Wire-shape profiles, tried in order by path. Default [genericEmailProfile, sendgridEmailProfile]. */
|
|
38
|
+
profiles?: EmailSendProfile[];
|
|
39
|
+
/** Deterministic id for responses (tests). Default a zero-padded counter. */
|
|
40
|
+
idFor?: (n: number) => string;
|
|
41
|
+
}
|
|
42
|
+
/** The flat-JSON shape: `{ from, to, subject, html, text }`. Matches Resend (POST /emails) AND a
|
|
43
|
+
* custom app that sends the common shape AND Postmark's TitleCase keys (From/To/HtmlBody/…). */
|
|
44
|
+
export declare const genericEmailProfile: EmailSendProfile;
|
|
45
|
+
/** SendGrid's nested shape: `from.email`, `personalizations[].to[].email`, `content[].value`. Proves
|
|
46
|
+
* the seam handles a structurally different vendor, not just a field-name rename. */
|
|
47
|
+
export declare const sendgridEmailProfile: EmailSendProfile;
|
|
48
|
+
export declare const DEFAULT_EMAIL_PROFILES: EmailSendProfile[];
|
|
49
|
+
/**
|
|
50
|
+
* Start the vendor-neutral email catch, routing accepted sends into `channel`. Returns the loopback
|
|
51
|
+
* URL to hand the app as its email-API base URL. `close()` in a finally (mirror by-id teardown).
|
|
52
|
+
*/
|
|
53
|
+
export declare function startEmailCatchServer(channel: CommsChannel, options?: EmailCatchOptions): Promise<EmailCatchServer>;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// A VENDOR-NEUTRAL loopback catch server for email-send APIs (#297). An app hardwired to a hosted
|
|
2
|
+
// email provider is redirected into the faux bus with ONE env var and no code change: point the app's
|
|
3
|
+
// API base URL at this server. The catch does not depend on, or name itself after, any one vendor —
|
|
4
|
+
// it normalizes each provider's distinct wire shape (Resend's flat body, SendGrid's nested
|
|
5
|
+
// personalizations, Postmark's TitleCase, a custom app's own JSON) to one shape via pluggable
|
|
6
|
+
// PROFILES, and routes the result into a CommsChannel. Resend-compatible and SendGrid-compatible out
|
|
7
|
+
// of the box; extend with a custom profile for anything else. (There is no standardized email-send
|
|
8
|
+
// request shape across vendors, so normalization is by design, not a shortcut.)
|
|
9
|
+
//
|
|
10
|
+
// 127.0.0.1 only; request bodies are never logged; request size is capped.
|
|
11
|
+
import { createServer } from "node:http";
|
|
12
|
+
// ---------------------------------------------------------------- shared parse helpers
|
|
13
|
+
function str(value) {
|
|
14
|
+
return typeof value === "string" ? value : "";
|
|
15
|
+
}
|
|
16
|
+
function optStr(value) {
|
|
17
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
18
|
+
}
|
|
19
|
+
/** "Name <email@host>" → "email@host"; a bare address passes through. */
|
|
20
|
+
function bareEmail(value) {
|
|
21
|
+
const angle = value.match(/<([^>]+)>/);
|
|
22
|
+
return (angle ? angle[1] ?? value : value).trim();
|
|
23
|
+
}
|
|
24
|
+
/** A recipient field → address strings: an array, a single, or a comma-separated string (Postmark). */
|
|
25
|
+
function toAddresses(value) {
|
|
26
|
+
if (Array.isArray(value))
|
|
27
|
+
return value.map((entry) => bareEmail(String(entry)));
|
|
28
|
+
if (typeof value === "string")
|
|
29
|
+
return value.split(",").map((part) => bareEmail(part)).filter((part) => part.length > 0);
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
// ---------------------------------------------------------------- built-in profiles
|
|
33
|
+
/** The flat-JSON shape: `{ from, to, subject, html, text }`. Matches Resend (POST /emails) AND a
|
|
34
|
+
* custom app that sends the common shape AND Postmark's TitleCase keys (From/To/HtmlBody/…). */
|
|
35
|
+
export const genericEmailProfile = {
|
|
36
|
+
name: "generic",
|
|
37
|
+
sendPaths: ["/emails", "/emails/batch", "/email", "/email/batch", "/send"],
|
|
38
|
+
parse(_path, body) {
|
|
39
|
+
const items = Array.isArray(body) ? body : [body];
|
|
40
|
+
const out = [];
|
|
41
|
+
for (const item of items) {
|
|
42
|
+
if (typeof item !== "object" || item === null)
|
|
43
|
+
continue;
|
|
44
|
+
const rec = item;
|
|
45
|
+
const from = str(rec.from ?? rec.From);
|
|
46
|
+
const to = toAddresses(rec.to ?? rec.To);
|
|
47
|
+
const subject = optStr(rec.subject ?? rec.Subject);
|
|
48
|
+
const bodyValue = str(rec.html ?? rec.HtmlBody ?? rec.text ?? rec.TextBody);
|
|
49
|
+
if (to.length === 0 && from === "" && bodyValue === "")
|
|
50
|
+
continue;
|
|
51
|
+
out.push({ from, to, ...(subject === undefined ? {} : { subject }), body: bodyValue });
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
/** SendGrid's nested shape: `from.email`, `personalizations[].to[].email`, `content[].value`. Proves
|
|
57
|
+
* the seam handles a structurally different vendor, not just a field-name rename. */
|
|
58
|
+
export const sendgridEmailProfile = {
|
|
59
|
+
name: "sendgrid",
|
|
60
|
+
sendPaths: ["/v3/mail/send"],
|
|
61
|
+
parse(_path, body) {
|
|
62
|
+
if (typeof body !== "object" || body === null)
|
|
63
|
+
return [];
|
|
64
|
+
const rec = body;
|
|
65
|
+
const fromObj = rec.from;
|
|
66
|
+
const from = typeof fromObj === "object" && fromObj !== null ? str(fromObj.email) : str(fromObj);
|
|
67
|
+
const subject = optStr(rec.subject);
|
|
68
|
+
const content = Array.isArray(rec.content) ? rec.content : [];
|
|
69
|
+
const isPart = (part) => typeof part === "object" && part !== null;
|
|
70
|
+
const chosen = content.find((part) => isPart(part) && part.type === "text/html") ??
|
|
71
|
+
content.find((part) => isPart(part) && part.type === "text/plain");
|
|
72
|
+
const bodyValue = isPart(chosen) ? str(chosen.value) : "";
|
|
73
|
+
const personalizations = Array.isArray(rec.personalizations) ? rec.personalizations : [];
|
|
74
|
+
const to = [];
|
|
75
|
+
for (const personalization of personalizations) {
|
|
76
|
+
if (!isPart(personalization))
|
|
77
|
+
continue;
|
|
78
|
+
const recipients = Array.isArray(personalization.to) ? personalization.to : [];
|
|
79
|
+
for (const recipient of recipients) {
|
|
80
|
+
if (!isPart(recipient))
|
|
81
|
+
continue;
|
|
82
|
+
const email = str(recipient.email);
|
|
83
|
+
if (email.length > 0)
|
|
84
|
+
to.push(email);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (to.length === 0)
|
|
88
|
+
return [];
|
|
89
|
+
return [{ from, to, ...(subject === undefined ? {} : { subject }), body: bodyValue }];
|
|
90
|
+
},
|
|
91
|
+
respond(res, ids, _batch) {
|
|
92
|
+
// SendGrid answers 202 with the id in a header and an empty body.
|
|
93
|
+
res.statusCode = 202;
|
|
94
|
+
if (ids[0] !== undefined)
|
|
95
|
+
res.setHeader("x-message-id", ids[0]);
|
|
96
|
+
res.end();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
export const DEFAULT_EMAIL_PROFILES = [genericEmailProfile, sendgridEmailProfile];
|
|
100
|
+
// ---------------------------------------------------------------- server
|
|
101
|
+
class BodyTooLargeError extends Error {
|
|
102
|
+
}
|
|
103
|
+
function readBody(req, limit) {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
let size = 0;
|
|
106
|
+
const chunks = [];
|
|
107
|
+
req.on("data", (chunk) => {
|
|
108
|
+
size += chunk.length;
|
|
109
|
+
if (size > limit) {
|
|
110
|
+
// Reject WITHOUT destroying the socket here: the handler responds 413 cleanly first (a
|
|
111
|
+
// write-after-destroy would otherwise reset the connection), then tears the request down.
|
|
112
|
+
reject(new BodyTooLargeError("request body too large"));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
chunks.push(chunk);
|
|
116
|
+
});
|
|
117
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
118
|
+
req.on("error", reject);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const MAX_BODY_BYTES = 5 * 1024 * 1024;
|
|
122
|
+
/**
|
|
123
|
+
* Start the vendor-neutral email catch, routing accepted sends into `channel`. Returns the loopback
|
|
124
|
+
* URL to hand the app as its email-API base URL. `close()` in a finally (mirror by-id teardown).
|
|
125
|
+
*/
|
|
126
|
+
export async function startEmailCatchServer(channel, options = {}) {
|
|
127
|
+
const host = options.host ?? "127.0.0.1";
|
|
128
|
+
const profiles = options.profiles ?? DEFAULT_EMAIL_PROFILES;
|
|
129
|
+
const received = [];
|
|
130
|
+
let idCounter = 0;
|
|
131
|
+
const idFor = options.idFor ?? ((n) => `humanish-catch-${n.toString().padStart(6, "0")}`);
|
|
132
|
+
const respondJson = (res, status, value) => {
|
|
133
|
+
res.statusCode = status;
|
|
134
|
+
res.setHeader("content-type", "application/json");
|
|
135
|
+
res.end(JSON.stringify(value));
|
|
136
|
+
};
|
|
137
|
+
const handle = async (req, res) => {
|
|
138
|
+
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
139
|
+
if (req.method === "GET" && (path === "/" || path === "/health")) {
|
|
140
|
+
respondJson(res, 200, { ok: true, service: "humanish-email-catch", channel: channel.channel, profiles: profiles.map((p) => p.name) });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const profile = req.method === "POST" ? profiles.find((candidate) => candidate.sendPaths.includes(path)) : undefined;
|
|
144
|
+
if (profile === undefined) {
|
|
145
|
+
respondJson(res, 404, { error: "not found" });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
let raw;
|
|
149
|
+
try {
|
|
150
|
+
raw = await readBody(req, MAX_BODY_BYTES);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (!res.headersSent)
|
|
154
|
+
respondJson(res, error instanceof BodyTooLargeError ? 413 : 400, { error: "request body could not be read" });
|
|
155
|
+
req.destroy();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
let parsed;
|
|
159
|
+
try {
|
|
160
|
+
parsed = JSON.parse(raw.length > 0 ? raw : "{}");
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
respondJson(res, 422, { error: "invalid JSON body" });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const sends = profile.parse(path, parsed);
|
|
167
|
+
if (sends.length === 0) {
|
|
168
|
+
// A well-formed request that names no deliverable recipient (empty body, or a shape the profile
|
|
169
|
+
// could not resolve to a send) — return a bad-request rather than a fabricated success id.
|
|
170
|
+
respondJson(res, 422, { error: "no deliverable message in request" });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const ids = [];
|
|
174
|
+
for (const send of sends) {
|
|
175
|
+
received.push(send);
|
|
176
|
+
const inbound = {
|
|
177
|
+
from: send.from,
|
|
178
|
+
to: send.to,
|
|
179
|
+
...(send.subject === undefined ? {} : { subject: send.subject }),
|
|
180
|
+
body: send.body
|
|
181
|
+
};
|
|
182
|
+
await channel.deliverRaw(inbound);
|
|
183
|
+
idCounter += 1;
|
|
184
|
+
ids.push(idFor(idCounter));
|
|
185
|
+
}
|
|
186
|
+
const batch = Array.isArray(parsed) || path.endsWith("/batch");
|
|
187
|
+
if (profile.respond)
|
|
188
|
+
profile.respond(res, ids, batch);
|
|
189
|
+
else if (batch)
|
|
190
|
+
respondJson(res, 200, { data: ids.map((id) => ({ id })) });
|
|
191
|
+
else
|
|
192
|
+
respondJson(res, 200, { id: ids[0] ?? idFor(idCounter) });
|
|
193
|
+
};
|
|
194
|
+
const server = createServer((req, res) => {
|
|
195
|
+
void handle(req, res).catch(() => {
|
|
196
|
+
if (!res.headersSent)
|
|
197
|
+
respondJson(res, 500, { error: "catch server error" });
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
await new Promise((resolve) => server.listen(options.port ?? 0, host, () => resolve()));
|
|
201
|
+
const port = server.address().port;
|
|
202
|
+
return {
|
|
203
|
+
url: `http://${host}:${port}`,
|
|
204
|
+
port,
|
|
205
|
+
received,
|
|
206
|
+
close: () => new Promise((resolve) => server.close(() => resolve()))
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=comms-email-catch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comms-email-catch.js","sourceRoot":"","sources":["../src/comms-email-catch.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CommsAddress, CommsChannel, CommsChannelKind, CommsMessage, InboundRaw, OutboundMessage } from "./comms-types.js";
|
|
2
|
+
/** Extract actionable http(s) links from a message body (href="…" and bare URLs), de-duped, in order.
|
|
3
|
+
* The verification magic-link the persona would tap. Pure; body is runtime-only. */
|
|
4
|
+
export declare function extractLinks(body: string): string[];
|
|
5
|
+
/** Extract OTP-shaped tokens from a message body. Labeled codes ("your code is 481920",
|
|
6
|
+
* "verification code 8A3F2K") are high-precision and preferred; if none are labeled, fall back to an
|
|
7
|
+
* isolated 4–8 digit run (a bare OTP). Pure; tokens are runtime-only literal-scrub targets. */
|
|
8
|
+
export declare function extractOtpCodes(body: string): string[];
|
|
9
|
+
export interface FauxInboxOptions {
|
|
10
|
+
/** "email" (default) or "sms" — the address shape + surface differ; machinery is identical. */
|
|
11
|
+
channel?: CommsChannelKind;
|
|
12
|
+
/** Email domain for minted addresses. Default example.test (an RFC 6761 reserved, unroutable test
|
|
13
|
+
* domain — public-safe; override to a branded reserved domain once it's added to the email allowlist). */
|
|
14
|
+
domain?: string;
|
|
15
|
+
/** Injected clock (ms) for deterministic tests. Default Date.now. */
|
|
16
|
+
now?: () => number;
|
|
17
|
+
}
|
|
18
|
+
/** The in-process faux adapter. Implements the same CommsChannel port a real provider adapter would. */
|
|
19
|
+
export declare class FauxInbox implements CommsChannel {
|
|
20
|
+
readonly channel: CommsChannelKind;
|
|
21
|
+
readonly kind: "faux";
|
|
22
|
+
private readonly domain;
|
|
23
|
+
private readonly clock;
|
|
24
|
+
private readonly byActor;
|
|
25
|
+
private readonly byValue;
|
|
26
|
+
private readonly queues;
|
|
27
|
+
private counter;
|
|
28
|
+
constructor(options?: FauxInboxOptions);
|
|
29
|
+
provision(actorId: string): Promise<CommsAddress>;
|
|
30
|
+
private route;
|
|
31
|
+
send(message: OutboundMessage): Promise<CommsMessage>;
|
|
32
|
+
deliverRaw(inbound: InboundRaw): Promise<CommsMessage[]>;
|
|
33
|
+
poll(address: CommsAddress, since?: number): Promise<CommsMessage[]>;
|
|
34
|
+
teardown(): Promise<void>;
|
|
35
|
+
/** Inspection helper (tests / a surface): every inbox currently provisioned. */
|
|
36
|
+
addresses(): CommsAddress[];
|
|
37
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// The FAUX in-process email/SMS bus (#297 Stage 2). Deterministic, $0, offline, public-safe: a
|
|
2
|
+
// message an app-under-test "sends" (via an ingress like the vendor-neutral email catch) is routed to the
|
|
3
|
+
// addressed actor inbox and read back through the same CommsChannel port a real provider adapter
|
|
4
|
+
// would implement. Nothing leaves the process. See comms-types.ts for the port + public-safety notes.
|
|
5
|
+
import { digestText } from "./redaction.js";
|
|
6
|
+
/** Extract actionable http(s) links from a message body (href="…" and bare URLs), de-duped, in order.
|
|
7
|
+
* The verification magic-link the persona would tap. Pure; body is runtime-only. */
|
|
8
|
+
export function extractLinks(body) {
|
|
9
|
+
if (typeof body !== "string" || body.length === 0)
|
|
10
|
+
return [];
|
|
11
|
+
const out = [];
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
const push = (raw) => {
|
|
14
|
+
const url = raw.trim().replace(/[).,;'"]+$/, "");
|
|
15
|
+
if (/^https?:\/\//i.test(url) && !seen.has(url)) {
|
|
16
|
+
seen.add(url);
|
|
17
|
+
out.push(url);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
let m;
|
|
21
|
+
const href = /href\s*=\s*["']([^"']+)["']/gi;
|
|
22
|
+
while ((m = href.exec(body)) !== null)
|
|
23
|
+
push(m[1] ?? "");
|
|
24
|
+
const bare = /https?:\/\/[^\s"'<>)\]]+/gi;
|
|
25
|
+
while ((m = bare.exec(body)) !== null)
|
|
26
|
+
push(m[0]);
|
|
27
|
+
return out.slice(0, 50);
|
|
28
|
+
}
|
|
29
|
+
function stripTags(html) {
|
|
30
|
+
return html
|
|
31
|
+
.replace(/<style[\s\S]*?<\/style>/gi, " ")
|
|
32
|
+
.replace(/<script[\s\S]*?<\/script>/gi, " ")
|
|
33
|
+
.replace(/<[^>]+>/g, " ")
|
|
34
|
+
.replace(/ /gi, " ")
|
|
35
|
+
.replace(/\s+/g, " ")
|
|
36
|
+
.trim();
|
|
37
|
+
}
|
|
38
|
+
/** Extract OTP-shaped tokens from a message body. Labeled codes ("your code is 481920",
|
|
39
|
+
* "verification code 8A3F2K") are high-precision and preferred; if none are labeled, fall back to an
|
|
40
|
+
* isolated 4–8 digit run (a bare OTP). Pure; tokens are runtime-only literal-scrub targets. */
|
|
41
|
+
export function extractOtpCodes(body) {
|
|
42
|
+
if (typeof body !== "string" || body.length === 0)
|
|
43
|
+
return [];
|
|
44
|
+
const text = stripTags(body);
|
|
45
|
+
const labeled = [];
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
const push = (list, code) => {
|
|
48
|
+
const c = code.toUpperCase();
|
|
49
|
+
if (c && !seen.has(c)) {
|
|
50
|
+
seen.add(c);
|
|
51
|
+
list.push(c);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
// The alphanumeric alternative requires at least one DIGIT (lookahead) so a labeled prose word like
|
|
55
|
+
// "your code is INVALID" isn't captured as a code; pure-digit codes (4–8) match directly.
|
|
56
|
+
const labeledRe = /(?:one[-\s]?time\s+(?:pass)?code|verification\s+code|security\s+code|access\s+code|login\s+code|confirmation\s+code|passcode|\bOTP\b|\bPIN\b|\bcode\b)\D{0,15}\b([0-9]{4,8}|(?=[A-Za-z0-9]*[0-9])[A-Z0-9]{6,8})\b/gi;
|
|
57
|
+
let m;
|
|
58
|
+
while ((m = labeledRe.exec(text)) !== null)
|
|
59
|
+
push(labeled, m[1] ?? "");
|
|
60
|
+
if (labeled.length > 0)
|
|
61
|
+
return labeled.slice(0, 10);
|
|
62
|
+
// Fallback: an isolated 4–8 digit run (a bare, unlabeled OTP), not embedded in a longer token.
|
|
63
|
+
const bare = [];
|
|
64
|
+
const bareRe = /(?<![0-9A-Za-z])([0-9]{4,8})(?![0-9A-Za-z])/g;
|
|
65
|
+
while ((m = bareRe.exec(text)) !== null)
|
|
66
|
+
push(bare, m[1] ?? "");
|
|
67
|
+
return bare.slice(0, 10);
|
|
68
|
+
}
|
|
69
|
+
function sanitizeLocalPart(actorId) {
|
|
70
|
+
return actorId.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "actor";
|
|
71
|
+
}
|
|
72
|
+
function smsAddressFor(actorId) {
|
|
73
|
+
const digits = digestText(actorId, 16).replace(/[a-f]/g, (c) => String(c.charCodeAt(0) % 10)).slice(0, 7);
|
|
74
|
+
return `+1555${digits}`;
|
|
75
|
+
}
|
|
76
|
+
/** The in-process faux adapter. Implements the same CommsChannel port a real provider adapter would. */
|
|
77
|
+
export class FauxInbox {
|
|
78
|
+
channel;
|
|
79
|
+
kind = "faux";
|
|
80
|
+
domain;
|
|
81
|
+
clock;
|
|
82
|
+
byActor = new Map();
|
|
83
|
+
byValue = new Map();
|
|
84
|
+
queues = new Map();
|
|
85
|
+
counter = 0;
|
|
86
|
+
constructor(options = {}) {
|
|
87
|
+
this.channel = options.channel ?? "email";
|
|
88
|
+
this.domain = options.domain ?? "example.test";
|
|
89
|
+
this.clock = options.now ?? (() => Date.now());
|
|
90
|
+
}
|
|
91
|
+
async provision(actorId) {
|
|
92
|
+
const existing = this.byActor.get(actorId);
|
|
93
|
+
if (existing)
|
|
94
|
+
return existing;
|
|
95
|
+
const value = this.channel === "sms" ? smsAddressFor(actorId) : `${sanitizeLocalPart(actorId)}@${this.domain}`;
|
|
96
|
+
// Address collision guard: two distinct actor ids can sanitize to the same local part. Reuse the
|
|
97
|
+
// existing inbox rather than resetting its queue (which would drop already-delivered mail). Both
|
|
98
|
+
// actors then share it — a faux-world edge; declare distinct addresses to avoid it.
|
|
99
|
+
const prior = this.byValue.get(value.toLowerCase());
|
|
100
|
+
if (prior) {
|
|
101
|
+
this.byActor.set(actorId, prior);
|
|
102
|
+
return prior;
|
|
103
|
+
}
|
|
104
|
+
const address = { channel: this.channel, actorId, value, digest: digestText(value, 16) };
|
|
105
|
+
this.byActor.set(actorId, address);
|
|
106
|
+
this.byValue.set(value.toLowerCase(), address);
|
|
107
|
+
this.queues.set(value.toLowerCase(), []);
|
|
108
|
+
return address;
|
|
109
|
+
}
|
|
110
|
+
route(from, to, subject, body) {
|
|
111
|
+
const at = this.clock();
|
|
112
|
+
const message = {
|
|
113
|
+
id: `comms-${(this.counter += 1).toString().padStart(4, "0")}`,
|
|
114
|
+
channel: this.channel,
|
|
115
|
+
from,
|
|
116
|
+
to,
|
|
117
|
+
...(subject === undefined ? {} : { subject }),
|
|
118
|
+
body,
|
|
119
|
+
links: extractLinks(body),
|
|
120
|
+
codes: extractOtpCodes(body),
|
|
121
|
+
sentAt: at,
|
|
122
|
+
deliveredAt: at
|
|
123
|
+
};
|
|
124
|
+
for (const addr of to) {
|
|
125
|
+
const queue = this.queues.get(addr.value.toLowerCase());
|
|
126
|
+
if (queue)
|
|
127
|
+
queue.push(message);
|
|
128
|
+
}
|
|
129
|
+
return message;
|
|
130
|
+
}
|
|
131
|
+
async send(message) {
|
|
132
|
+
return this.route(message.from.value, message.to, message.subject, message.body);
|
|
133
|
+
}
|
|
134
|
+
async deliverRaw(inbound) {
|
|
135
|
+
const to = (inbound.to ?? [])
|
|
136
|
+
.map((raw) => this.byValue.get(String(raw).trim().toLowerCase()))
|
|
137
|
+
.filter((address) => address !== undefined);
|
|
138
|
+
if (to.length === 0)
|
|
139
|
+
return []; // no provisioned inbox matched → nothing to deliver to
|
|
140
|
+
return [this.route(inbound.from, to, inbound.subject, inbound.body)];
|
|
141
|
+
}
|
|
142
|
+
async poll(address, since = 0) {
|
|
143
|
+
const queue = this.queues.get(address.value.toLowerCase()) ?? [];
|
|
144
|
+
return queue.filter((message) => message.deliveredAt > since);
|
|
145
|
+
}
|
|
146
|
+
async teardown() {
|
|
147
|
+
this.byActor.clear();
|
|
148
|
+
this.byValue.clear();
|
|
149
|
+
this.queues.clear();
|
|
150
|
+
this.counter = 0;
|
|
151
|
+
}
|
|
152
|
+
/** Inspection helper (tests / a surface): every inbox currently provisioned. */
|
|
153
|
+
addresses() {
|
|
154
|
+
return [...this.byActor.values()];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=comms-faux-inbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comms-faux-inbox.js","sourceRoot":"","sources":["../src/comms-faux-inbox.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export type CommsChannelKind = "email" | "sms";
|
|
2
|
+
/** One actor's inbox identity. `value` is runtime-only; `digest` is the only form meant to persist. */
|
|
3
|
+
export interface CommsAddress {
|
|
4
|
+
channel: CommsChannelKind;
|
|
5
|
+
/** Which lane owns this inbox. */
|
|
6
|
+
actorId: string;
|
|
7
|
+
/** Runtime-only raw address, e.g. patient-07@example.test | +15550137. */
|
|
8
|
+
value: string;
|
|
9
|
+
/** sha256-short(value) — the only form persisted (redaction.digestText). */
|
|
10
|
+
digest: string;
|
|
11
|
+
}
|
|
12
|
+
/** A message that arrived to (or was sent from) an inbox. Body/links/codes are runtime-only. */
|
|
13
|
+
export interface CommsMessage {
|
|
14
|
+
id: string;
|
|
15
|
+
channel: CommsChannelKind;
|
|
16
|
+
/** Raw sender — an app-under-test address, or another actor's address. Runtime-only. */
|
|
17
|
+
from: string;
|
|
18
|
+
/** Resolved recipient inboxes this message was delivered to. */
|
|
19
|
+
to: CommsAddress[];
|
|
20
|
+
subject?: string;
|
|
21
|
+
/** Runtime-only for real; local-only for faux (never a share path without redaction — #108). */
|
|
22
|
+
body: string;
|
|
23
|
+
/** Actionable links extracted from the body (magic-link / invite / reset). Runtime-only. */
|
|
24
|
+
links: string[];
|
|
25
|
+
/** OTP-shaped tokens extracted from the body. Runtime-only; literal-scrub targets. */
|
|
26
|
+
codes: string[];
|
|
27
|
+
sentAt: number;
|
|
28
|
+
deliveredAt: number;
|
|
29
|
+
}
|
|
30
|
+
/** An actor sending OUT (a reply/compose); recipients are known CommsAddresses. */
|
|
31
|
+
export interface OutboundMessage {
|
|
32
|
+
from: CommsAddress;
|
|
33
|
+
to: CommsAddress[];
|
|
34
|
+
subject?: string;
|
|
35
|
+
body: string;
|
|
36
|
+
}
|
|
37
|
+
/** A raw inbound from an INGRESS (the vendor-neutral email catch, an SMTP sink, …): recipients are
|
|
38
|
+
* raw address strings the bus resolves against its provisioned inboxes. */
|
|
39
|
+
export interface InboundRaw {
|
|
40
|
+
from: string;
|
|
41
|
+
to: string[];
|
|
42
|
+
subject?: string;
|
|
43
|
+
body: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The port. Faux (in-process) + real-email + real-sms adapters implement it identically. Async so a
|
|
47
|
+
* real (network) adapter fits without changing callers; the faux adapter just resolves immediately.
|
|
48
|
+
*/
|
|
49
|
+
export interface CommsChannel {
|
|
50
|
+
readonly channel: CommsChannelKind;
|
|
51
|
+
readonly kind: "faux" | "real";
|
|
52
|
+
/** Mint/allocate an inbox for an actor (address auto-generated). Idempotent per actor. */
|
|
53
|
+
provision(actorId: string): Promise<CommsAddress>;
|
|
54
|
+
/** Route a composed message from one actor to addressed inboxes. Returns the delivered record. */
|
|
55
|
+
send(message: OutboundMessage): Promise<CommsMessage>;
|
|
56
|
+
/** Route a raw ingress delivery (app-under-test → recipient strings). Returns the messages that
|
|
57
|
+
* matched a provisioned inbox (unmatched recipients are dropped — no inbox to deliver to). */
|
|
58
|
+
deliverRaw(inbound: InboundRaw): Promise<CommsMessage[]>;
|
|
59
|
+
/** New messages delivered to `address` since `since` (exclusive), oldest-first. Drives the surface. */
|
|
60
|
+
poll(address: CommsAddress, since?: number): Promise<CommsMessage[]>;
|
|
61
|
+
/** Release inboxes BY id (mirror the by-id sandbox teardown rail). */
|
|
62
|
+
teardown(): Promise<void>;
|
|
63
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The addressed message bus (#297) — the seam that makes "off-app" comms (email/SMS the persona
|
|
2
|
+
// actually lives in) a first-class, persona-driven testable surface. A single port, addressed by
|
|
3
|
+
// actor; faux (in-process) and real (provider-backed) adapters implement it identically, so the
|
|
4
|
+
// persona surface + evidence writer consume it without knowing which is behind it.
|
|
5
|
+
//
|
|
6
|
+
// PUBLIC-SAFETY: raw address values, message bodies, links, and codes are RUNTIME-ONLY. Only the
|
|
7
|
+
// address DIGEST (sha256-short, via redaction.digestText) is ever meant to reach a persisted bundle;
|
|
8
|
+
// a verification link / OTP has "no secret shape" (like the lobby code) → literal-scrub + digest.
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=comms-types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comms-types.js","sourceRoot":"","sources":["../src/comms-types.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,iGAAiG;AACjG,gGAAgG;AAChG,mFAAmF;AACnF,EAAE;AACF,iGAAiG;AACjG,qGAAqG;AACrG,kGAAkG"}
|
|
@@ -39,7 +39,7 @@ import { adapterScoreFailureMessage, applyBrowserAdapterHooks } from "./adapter-
|
|
|
39
39
|
import { actorRegistry, isCuaActorDescriptor } from "./actor-registry.js";
|
|
40
40
|
import { toErrorMessage } from "./command-failure.js";
|
|
41
41
|
import { mapWithConcurrency } from "./concurrency.js";
|
|
42
|
-
import { commandDigestOf, composeLaneInstructions, defaultPackLocalTree, provisionCloneSubject, provisionLocalTreeSubject, resolveLaneDevice, resolveSubjectState, runCuaLane } from "./cua-actor-lab.js";
|
|
42
|
+
import { commandDigestOf, composeLaneInstructions, defaultPackLocalTree, provisionCloneSubject, provisionLocalTreeSubject, declaredScreenForRender, resolveLaneDevice, resolveSubjectState, runCuaLane } from "./cua-actor-lab.js";
|
|
43
43
|
import { createDesktopSandbox, loadE2BDesktopModule } from "./e2b-desktop-launch.js";
|
|
44
44
|
import { concurrentSharedWorldValidationReason, externalPublicSharedWorldValidationReason } from "./lab-config.js";
|
|
45
45
|
import { buildObserverData } from "./observer-data.js";
|
|
@@ -1379,8 +1379,15 @@ export function buildConcurrentSharedWorldBundle(args) {
|
|
|
1379
1379
|
? "Actor desktop is running; the attached Observer hydrates the runtime stream URL without persisting it."
|
|
1380
1380
|
: "Contract actor only: dry-run produced the evidence shape without launching a desktop or spending provider tokens.");
|
|
1381
1381
|
const traceScreenshotMode = session?.trace.redaction.screenshots;
|
|
1382
|
+
// Include `declared` on the no-outcome fallback too (dry-run, skipped lane): otherwise an
|
|
1383
|
+
// ABSENT declared means either "the preset rendered faithfully" or "there was no live
|
|
1384
|
+
// outcome", and a dry-run bundle keeps the self-confirming shape this field exists to kill.
|
|
1385
|
+
const fallbackDeclared = declaredScreenForRender(spec.devicePreset, spec.deviceName, spec.resolution);
|
|
1382
1386
|
const desktopGeometry = outcome?.desktopGeometry ?? {
|
|
1383
|
-
screen: {
|
|
1387
|
+
screen: {
|
|
1388
|
+
requested: { width: spec.resolution[0], height: spec.resolution[1] },
|
|
1389
|
+
...(fallbackDeclared ? { declared: fallbackDeclared } : {})
|
|
1390
|
+
}
|
|
1384
1391
|
};
|
|
1385
1392
|
const screenshotMode = traceScreenshotMode === "raw" || traceScreenshotMode === "blurred"
|
|
1386
1393
|
? traceScreenshotMode
|