humanish 0.20.3 → 0.20.5
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-faux-inbox.d.ts +37 -0
- package/dist/comms-faux-inbox.js +147 -0
- package/dist/comms-faux-inbox.js.map +1 -0
- package/dist/comms-resend-catch.d.ts +30 -0
- package/dist/comms-resend-catch.js +120 -0
- package/dist/comms-resend-catch.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/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/observer-assets.js +168 -2
- package/dist/observer-assets.js.map +1 -1
- package/docs/contracts/schemas.md +1 -1
- package/docs/goals/current.md +1 -1
- package/docs/ramp/README.md +1 -1
- package/package.json +1 -1
|
@@ -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,147 @@
|
|
|
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 Resend 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
|
+
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-Z0-9]{6,8})\b/gi;
|
|
55
|
+
let m;
|
|
56
|
+
while ((m = labeledRe.exec(text)) !== null)
|
|
57
|
+
push(labeled, m[1] ?? "");
|
|
58
|
+
if (labeled.length > 0)
|
|
59
|
+
return labeled.slice(0, 10);
|
|
60
|
+
// Fallback: an isolated 4–8 digit run (a bare, unlabeled OTP), not embedded in a longer token.
|
|
61
|
+
const bare = [];
|
|
62
|
+
const bareRe = /(?<![0-9A-Za-z])([0-9]{4,8})(?![0-9A-Za-z])/g;
|
|
63
|
+
while ((m = bareRe.exec(text)) !== null)
|
|
64
|
+
push(bare, m[1] ?? "");
|
|
65
|
+
return bare.slice(0, 10);
|
|
66
|
+
}
|
|
67
|
+
function sanitizeLocalPart(actorId) {
|
|
68
|
+
return actorId.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "actor";
|
|
69
|
+
}
|
|
70
|
+
function smsAddressFor(actorId) {
|
|
71
|
+
const digits = digestText(actorId, 16).replace(/[a-f]/g, (c) => String(c.charCodeAt(0) % 10)).slice(0, 7);
|
|
72
|
+
return `+1555${digits}`;
|
|
73
|
+
}
|
|
74
|
+
/** The in-process faux adapter. Implements the same CommsChannel port a real provider adapter would. */
|
|
75
|
+
export class FauxInbox {
|
|
76
|
+
channel;
|
|
77
|
+
kind = "faux";
|
|
78
|
+
domain;
|
|
79
|
+
clock;
|
|
80
|
+
byActor = new Map();
|
|
81
|
+
byValue = new Map();
|
|
82
|
+
queues = new Map();
|
|
83
|
+
counter = 0;
|
|
84
|
+
constructor(options = {}) {
|
|
85
|
+
this.channel = options.channel ?? "email";
|
|
86
|
+
this.domain = options.domain ?? "example.test";
|
|
87
|
+
this.clock = options.now ?? (() => Date.now());
|
|
88
|
+
}
|
|
89
|
+
async provision(actorId) {
|
|
90
|
+
const existing = this.byActor.get(actorId);
|
|
91
|
+
if (existing)
|
|
92
|
+
return existing;
|
|
93
|
+
const value = this.channel === "sms" ? smsAddressFor(actorId) : `${sanitizeLocalPart(actorId)}@${this.domain}`;
|
|
94
|
+
const address = { channel: this.channel, actorId, value, digest: digestText(value, 16) };
|
|
95
|
+
this.byActor.set(actorId, address);
|
|
96
|
+
this.byValue.set(value.toLowerCase(), address);
|
|
97
|
+
this.queues.set(value.toLowerCase(), []);
|
|
98
|
+
return address;
|
|
99
|
+
}
|
|
100
|
+
route(from, to, subject, body) {
|
|
101
|
+
const at = this.clock();
|
|
102
|
+
const message = {
|
|
103
|
+
id: `comms-${(this.counter += 1).toString().padStart(4, "0")}`,
|
|
104
|
+
channel: this.channel,
|
|
105
|
+
from,
|
|
106
|
+
to,
|
|
107
|
+
...(subject === undefined ? {} : { subject }),
|
|
108
|
+
body,
|
|
109
|
+
links: extractLinks(body),
|
|
110
|
+
codes: extractOtpCodes(body),
|
|
111
|
+
sentAt: at,
|
|
112
|
+
deliveredAt: at
|
|
113
|
+
};
|
|
114
|
+
for (const addr of to) {
|
|
115
|
+
const queue = this.queues.get(addr.value.toLowerCase());
|
|
116
|
+
if (queue)
|
|
117
|
+
queue.push(message);
|
|
118
|
+
}
|
|
119
|
+
return message;
|
|
120
|
+
}
|
|
121
|
+
async send(message) {
|
|
122
|
+
return this.route(message.from.value, message.to, message.subject, message.body);
|
|
123
|
+
}
|
|
124
|
+
async deliverRaw(inbound) {
|
|
125
|
+
const to = (inbound.to ?? [])
|
|
126
|
+
.map((raw) => this.byValue.get(String(raw).trim().toLowerCase()))
|
|
127
|
+
.filter((address) => address !== undefined);
|
|
128
|
+
if (to.length === 0)
|
|
129
|
+
return []; // no provisioned inbox matched → nothing to deliver to
|
|
130
|
+
return [this.route(inbound.from, to, inbound.subject, inbound.body)];
|
|
131
|
+
}
|
|
132
|
+
async poll(address, since = 0) {
|
|
133
|
+
const queue = this.queues.get(address.value.toLowerCase()) ?? [];
|
|
134
|
+
return queue.filter((message) => message.deliveredAt > since);
|
|
135
|
+
}
|
|
136
|
+
async teardown() {
|
|
137
|
+
this.byActor.clear();
|
|
138
|
+
this.byValue.clear();
|
|
139
|
+
this.queues.clear();
|
|
140
|
+
this.counter = 0;
|
|
141
|
+
}
|
|
142
|
+
/** Inspection helper (tests / a surface): every inbox currently provisioned. */
|
|
143
|
+
addresses() {
|
|
144
|
+
return [...this.byActor.values()];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
//# 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,30 @@
|
|
|
1
|
+
import type { CommsChannel } from "./comms-types.js";
|
|
2
|
+
/** A Resend send payload we accepted, normalized (runtime-only; for inspection/tests). */
|
|
3
|
+
export interface ResendEmailPayload {
|
|
4
|
+
from: string;
|
|
5
|
+
to: string[];
|
|
6
|
+
subject?: string;
|
|
7
|
+
html?: string;
|
|
8
|
+
text?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ResendCatchServer {
|
|
11
|
+
/** Point the app here: `RESEND_BASE_URL=<url>` (loopback only). */
|
|
12
|
+
readonly url: string;
|
|
13
|
+
readonly port: number;
|
|
14
|
+
/** Every payload accepted this run (runtime-only). */
|
|
15
|
+
readonly received: ResendEmailPayload[];
|
|
16
|
+
close(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export interface ResendCatchOptions {
|
|
19
|
+
/** Bind host. Default 127.0.0.1 (loopback ONLY — never bind a public interface). */
|
|
20
|
+
host?: string;
|
|
21
|
+
/** Port. Default 0 (ephemeral — read the chosen port off the returned server). */
|
|
22
|
+
port?: number;
|
|
23
|
+
/** Deterministic id for the Resend-shaped response (tests). Default a zero-padded counter. */
|
|
24
|
+
idFor?: (n: number) => string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Start the Resend-shaped catch server, routing accepted sends into `channel`. Returns the loopback
|
|
28
|
+
* URL to hand the app as `RESEND_BASE_URL`. `close()` in a finally (mirror by-id teardown).
|
|
29
|
+
*/
|
|
30
|
+
export declare function startResendCatchServer(channel: CommsChannel, options?: ResendCatchOptions): Promise<ResendCatchServer>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// A LOOPBACK catch server that speaks the Resend `POST /emails` wire shape, so an app hardwired to
|
|
2
|
+
// Resend is redirected into the faux bus with ONE env var and no code change: point the app at this
|
|
3
|
+
// server via `RESEND_BASE_URL=<url>` (the official `resend` Node SDK reads that env var / a `baseUrl`
|
|
4
|
+
// option). The app's real verification email POSTs here instead of api.resend.com; we route it into
|
|
5
|
+
// the CommsChannel and the persona reads it. Nothing is delivered onward. (#297 — the API-first
|
|
6
|
+
// analog of pointing SMTP at a local catcher, since Resend is HTTPS-first and its own SMTP delivers
|
|
7
|
+
// for real.) 127.0.0.1 only; bodies are never logged.
|
|
8
|
+
import { createServer } from "node:http";
|
|
9
|
+
/** "Name <email@host>" → "email@host"; a bare address passes through. */
|
|
10
|
+
function bareEmail(value) {
|
|
11
|
+
const angle = value.match(/<([^>]+)>/);
|
|
12
|
+
return (angle ? angle[1] ?? value : value).trim();
|
|
13
|
+
}
|
|
14
|
+
function asStringArray(value) {
|
|
15
|
+
if (Array.isArray(value))
|
|
16
|
+
return value.map((entry) => String(entry));
|
|
17
|
+
if (typeof value === "string")
|
|
18
|
+
return [value];
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
function normalizeResendPayload(payload) {
|
|
22
|
+
return {
|
|
23
|
+
from: typeof payload.from === "string" ? payload.from : "",
|
|
24
|
+
to: asStringArray(payload.to).map(bareEmail),
|
|
25
|
+
...(typeof payload.subject === "string" ? { subject: payload.subject } : {}),
|
|
26
|
+
...(typeof payload.html === "string" ? { html: payload.html } : {}),
|
|
27
|
+
...(typeof payload.text === "string" ? { text: payload.text } : {})
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function readBody(req, limit) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
let size = 0;
|
|
33
|
+
const chunks = [];
|
|
34
|
+
req.on("data", (chunk) => {
|
|
35
|
+
size += chunk.length;
|
|
36
|
+
if (size > limit) {
|
|
37
|
+
req.destroy();
|
|
38
|
+
reject(new Error("request body too large"));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
chunks.push(chunk);
|
|
42
|
+
});
|
|
43
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
44
|
+
req.on("error", reject);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const MAX_BODY_BYTES = 5 * 1024 * 1024;
|
|
48
|
+
/**
|
|
49
|
+
* Start the Resend-shaped catch server, routing accepted sends into `channel`. Returns the loopback
|
|
50
|
+
* URL to hand the app as `RESEND_BASE_URL`. `close()` in a finally (mirror by-id teardown).
|
|
51
|
+
*/
|
|
52
|
+
export async function startResendCatchServer(channel, options = {}) {
|
|
53
|
+
const host = options.host ?? "127.0.0.1";
|
|
54
|
+
const received = [];
|
|
55
|
+
let idCounter = 0;
|
|
56
|
+
const idFor = options.idFor ?? ((n) => `humanish-catch-${n.toString().padStart(6, "0")}`);
|
|
57
|
+
const respondJson = (res, status, value) => {
|
|
58
|
+
res.statusCode = status;
|
|
59
|
+
res.setHeader("content-type", "application/json");
|
|
60
|
+
res.end(JSON.stringify(value));
|
|
61
|
+
};
|
|
62
|
+
const handle = async (req, res) => {
|
|
63
|
+
const path = (req.url ?? "/").split("?")[0];
|
|
64
|
+
if (req.method === "GET" && (path === "/" || path === "/health")) {
|
|
65
|
+
respondJson(res, 200, { ok: true, service: "humanish-resend-catch", channel: channel.channel });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Resend's send endpoints: POST /emails (single) and /emails/batch. Batch is an array of sends.
|
|
69
|
+
if (req.method === "POST" && (path === "/emails" || path === "/emails/batch")) {
|
|
70
|
+
const raw = await readBody(req, MAX_BODY_BYTES);
|
|
71
|
+
let parsed;
|
|
72
|
+
try {
|
|
73
|
+
parsed = JSON.parse(raw.length > 0 ? raw : "{}");
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
respondJson(res, 422, { error: "invalid JSON body" });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
80
|
+
const ids = [];
|
|
81
|
+
for (const item of items) {
|
|
82
|
+
if (typeof item !== "object" || item === null)
|
|
83
|
+
continue;
|
|
84
|
+
const payload = normalizeResendPayload(item);
|
|
85
|
+
received.push(payload);
|
|
86
|
+
const inbound = {
|
|
87
|
+
from: payload.from,
|
|
88
|
+
to: payload.to,
|
|
89
|
+
...(payload.subject === undefined ? {} : { subject: payload.subject }),
|
|
90
|
+
body: payload.html ?? payload.text ?? ""
|
|
91
|
+
};
|
|
92
|
+
await channel.deliverRaw(inbound);
|
|
93
|
+
idCounter += 1;
|
|
94
|
+
ids.push(idFor(idCounter));
|
|
95
|
+
}
|
|
96
|
+
// Resend-shaped response: a single send returns { id }; a batch returns { data: [{ id }, …] }.
|
|
97
|
+
if (path === "/emails/batch")
|
|
98
|
+
respondJson(res, 200, { data: ids.map((id) => ({ id })) });
|
|
99
|
+
else
|
|
100
|
+
respondJson(res, 200, { id: ids[0] ?? idFor(idCounter) });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
respondJson(res, 404, { error: "not found" });
|
|
104
|
+
};
|
|
105
|
+
const server = createServer((req, res) => {
|
|
106
|
+
void handle(req, res).catch(() => {
|
|
107
|
+
if (!res.headersSent)
|
|
108
|
+
respondJson(res, 500, { error: "catch server error" });
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
await new Promise((resolve) => server.listen(options.port ?? 0, host, () => resolve()));
|
|
112
|
+
const port = server.address().port;
|
|
113
|
+
return {
|
|
114
|
+
url: `http://${host}:${port}`,
|
|
115
|
+
port,
|
|
116
|
+
received,
|
|
117
|
+
close: () => new Promise((resolve) => server.close(() => resolve()))
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=comms-resend-catch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comms-resend-catch.js","sourceRoot":"","sources":["../src/comms-resend-catch.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 Resend catch, an SMTP sink, …): recipients are raw address
|
|
38
|
+
* 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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,11 @@ export type { FetchLike, OpenAiResponsesProviderOptions } from "./openai-respons
|
|
|
17
17
|
export { adapterScoreFailureMessage, applyAdapterScoreFailureToReview, applyBrowserAdapterHooks } from "./adapter-extension.js";
|
|
18
18
|
export type { BrowserAdapterBackend, BrowserLabAdapterHooks, BrowserLabScoringContext } from "./adapter-extension.js";
|
|
19
19
|
export type { RedactionHooks } from "./redaction.js";
|
|
20
|
+
export { FauxInbox, extractLinks, extractOtpCodes } from "./comms-faux-inbox.js";
|
|
21
|
+
export type { FauxInboxOptions } from "./comms-faux-inbox.js";
|
|
22
|
+
export { startResendCatchServer } from "./comms-resend-catch.js";
|
|
23
|
+
export type { ResendCatchOptions, ResendCatchServer, ResendEmailPayload } from "./comms-resend-catch.js";
|
|
24
|
+
export type { CommsAddress, CommsChannel, CommsChannelKind, CommsMessage, InboundRaw, OutboundMessage } from "./comms-types.js";
|
|
20
25
|
export { DESKTOP_RATE, MODEL_RATES, PRICING_SCHEMA, estimateActorCost, estimateDesktopCost } from "./pricing.js";
|
|
21
26
|
export type { ActorEstimatedCost, DesktopCostEstimate, DesktopRate, ModelRate } from "./pricing.js";
|
|
22
27
|
export { normalizeCliArgv } from "./argv.js";
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,11 @@ export { createE2BDesktopExecutor } from "./e2b-desktop-executor.js";
|
|
|
7
7
|
export { loadE2BDesktopModule } from "./e2b-desktop-launch.js";
|
|
8
8
|
export { DEFAULT_OPENAI_CU_MODEL, OPENAI_RESPONSES_CU_CAPABILITIES, createOpenAiResponsesProvider } from "./openai-responses-cu.js";
|
|
9
9
|
export { adapterScoreFailureMessage, applyAdapterScoreFailureToReview, applyBrowserAdapterHooks } from "./adapter-extension.js";
|
|
10
|
+
// Addressed comms bus (#297): faux email/SMS inboxes + the Resend-shaped ingress that redirects an
|
|
11
|
+
// API-first app into the faux bus with one env var. Real (provider-backed) adapters implement the
|
|
12
|
+
// same CommsChannel port.
|
|
13
|
+
export { FauxInbox, extractLinks, extractOtpCodes } from "./comms-faux-inbox.js";
|
|
14
|
+
export { startResendCatchServer } from "./comms-resend-catch.js";
|
|
10
15
|
export { DESKTOP_RATE, MODEL_RATES, PRICING_SCHEMA, estimateActorCost, estimateDesktopCost } from "./pricing.js";
|
|
11
16
|
export { normalizeCliArgv } from "./argv.js";
|
|
12
17
|
export { CODEX_APP_SERVER_UI_SCHEMA, startCodexAppServerUi } from "./codex-app-server-ui.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/observer-assets.js
CHANGED
|
@@ -417,6 +417,20 @@ a { color: inherit; text-decoration: none; }
|
|
|
417
417
|
/* ============================================================ STREAM SURFACES */
|
|
418
418
|
.surface-fill { position: absolute; inset: 0; width: 100%; height: 100%; }
|
|
419
419
|
.surface-screenshot { object-fit: contain; object-position: top center; }
|
|
420
|
+
/* Replay scrubber (#292 first slice): a per-turn keyframe timeline built from persisted frames + trace. */
|
|
421
|
+
.replay { position: absolute; inset: 0; display: flex; flex-direction: column; background: #0b0d10; }
|
|
422
|
+
.replay-stage { position: relative; flex: 1 1 auto; min-height: 0; }
|
|
423
|
+
.replay-badge { position: absolute; top: 6px; right: 8px; z-index: 2; font-size: 9px; text-transform: uppercase; letter-spacing: .04em; color: var(--text-2); background: rgba(0,0,0,.55); border: 1px solid var(--line-2); border-radius: 999px; padding: 2px 7px; }
|
|
424
|
+
.replay-bar { flex: 0 0 auto; display: flex; align-items: center; gap: 6px; padding: 6px 8px; border-top: 1px solid var(--line-2); background: var(--surface-2); }
|
|
425
|
+
.replay-btn { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 7px; border: 1px solid var(--line-2); background: transparent; color: var(--text-1); cursor: pointer; }
|
|
426
|
+
.replay-btn:hover { background: var(--surface-3); }
|
|
427
|
+
.replay-range { flex: 1 1 auto; accent-color: var(--accent-color); cursor: pointer; height: 4px; min-width: 40px; }
|
|
428
|
+
.replay-count { flex: 0 0 auto; font-size: 11px; color: var(--text-2); min-width: 44px; text-align: right; }
|
|
429
|
+
.replay-info { flex: 0 0 auto; max-height: 32%; overflow-y: auto; padding: 8px 10px; border-top: 1px solid var(--line-2); background: var(--surface-1); font-size: 12px; line-height: 1.5; color: var(--text-1); }
|
|
430
|
+
.replay-acts { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 6px; }
|
|
431
|
+
.replay-act { font-family: var(--mono); font-size: 10px; color: var(--text-2); background: var(--surface-3); border: 1px solid var(--line-2); border-radius: 5px; padding: 2px 6px; }
|
|
432
|
+
.replay-narr { white-space: pre-wrap; color: var(--text-1); }
|
|
433
|
+
.replay-dim { color: var(--text-2); font-style: italic; }
|
|
420
434
|
.live-stream-mount { position: absolute; inset: 0; overflow: hidden; background: #000; }
|
|
421
435
|
.live-stream-overlay { position: absolute; overflow: hidden; pointer-events: none; z-index: 2; }
|
|
422
436
|
.live-stream-overlay[data-focus="true"] .bw-lab-dock { max-height: 86px; }
|
|
@@ -1143,6 +1157,12 @@ export function observerClientJs() {
|
|
|
1143
1157
|
motion: readPref("motion", "full")
|
|
1144
1158
|
};
|
|
1145
1159
|
var artifactCache = {};
|
|
1160
|
+
// Replay-scrubber state: per-stream current frame index + auto-advance timer. Kept OUTSIDE the S
|
|
1161
|
+
// render-state so a data-poll re-render preserves the scrub position; indices survive because
|
|
1162
|
+
// render() reads scrubIndexFor() (which defaults to the last frame — also fixing the "finished lane
|
|
1163
|
+
// cuts to black" symptom). Timers are cleared on every render() so playback never orphans.
|
|
1164
|
+
var scrubState = {};
|
|
1165
|
+
var scrubTimers = {};
|
|
1146
1166
|
var liveStreamFrames = {};
|
|
1147
1167
|
var liveStreamHost = null;
|
|
1148
1168
|
var liveStreamLayoutRaf = null;
|
|
@@ -1198,7 +1218,9 @@ export function observerClientJs() {
|
|
|
1198
1218
|
lock: '<rect x="4.5" y="10" width="15" height="10" rx="2"' + P + '/><path d="M8 10V7a4 4 0 0 1 8 0v3"' + P + '/>',
|
|
1199
1219
|
caret: '<path d="m6 9 6 6 6-6"' + P + '/>',
|
|
1200
1220
|
filter: '<path d="M3 5h18l-7 8.2V20l-4-2.2v-4.6L3 5Z"' + P + '/>',
|
|
1201
|
-
panelRight: '<rect x="3" y="4" width="18" height="16" rx="2"' + P + '/><path d="M15 4v16"' + P + '/>'
|
|
1221
|
+
panelRight: '<rect x="3" y="4" width="18" height="16" rx="2"' + P + '/><path d="M15 4v16"' + P + '/>',
|
|
1222
|
+
play: '<path d="M7 5l12 7-12 7z"' + P + '/>',
|
|
1223
|
+
pause: '<path d="M9 5v14M15 5v14"' + P + '/>'
|
|
1202
1224
|
};
|
|
1203
1225
|
function icon(name, size) {
|
|
1204
1226
|
var s = size || 18;
|
|
@@ -1605,14 +1627,147 @@ export function observerClientJs() {
|
|
|
1605
1627
|
}
|
|
1606
1628
|
return '<div class="wait"><div class="wait-inner">' + inner + '</div></div>';
|
|
1607
1629
|
}
|
|
1630
|
+
// ---------------------------------------------------------------- replay scrubber
|
|
1631
|
+
// A durable per-turn keyframe replay built PURELY from the artifacts already in the bundle: the
|
|
1632
|
+
// ordered actor.items (reasoning/message/ui_action) sequenced against each persisted screenshot.
|
|
1633
|
+
// No new capture, no schema change, no new privacy surface — it reuses the frames' existing
|
|
1634
|
+
// gitignored / publishable:false posture. (#292 first slice.)
|
|
1635
|
+
function replayFrames(s) {
|
|
1636
|
+
var actor = s && s.actor;
|
|
1637
|
+
var items = actor && actor.items;
|
|
1638
|
+
if (!items || !items.length) return [];
|
|
1639
|
+
var frames = [];
|
|
1640
|
+
var narration = [];
|
|
1641
|
+
var actions = [];
|
|
1642
|
+
for (var i = 0; i < items.length; i += 1) {
|
|
1643
|
+
var it = items[i];
|
|
1644
|
+
if (!it) continue;
|
|
1645
|
+
if (it.kind === "reasoning" || it.kind === "message") { if (it.text) narration.push(String(it.text)); }
|
|
1646
|
+
else if (it.kind === "ui_action") { if (it.title) actions.push(String(it.title)); }
|
|
1647
|
+
else if (it.kind === "screenshot" && it.screenshotRef && it.screenshotRef.path) {
|
|
1648
|
+
frames.push({
|
|
1649
|
+
src: it.screenshotRef.path,
|
|
1650
|
+
label: it.title || ("frame " + (frames.length + 1)),
|
|
1651
|
+
redaction: (it.screenshotRef.redaction && it.screenshotRef.redaction !== "none") ? it.screenshotRef.redaction : "raw",
|
|
1652
|
+
narration: narration,
|
|
1653
|
+
actions: actions
|
|
1654
|
+
});
|
|
1655
|
+
narration = [];
|
|
1656
|
+
actions = [];
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
return frames;
|
|
1660
|
+
}
|
|
1661
|
+
function streamById(id) {
|
|
1662
|
+
var ss = currentData.streams || [];
|
|
1663
|
+
for (var i = 0; i < ss.length; i += 1) { if (ss[i].id === id) return ss[i]; }
|
|
1664
|
+
return null;
|
|
1665
|
+
}
|
|
1666
|
+
function scrubIndexOf(id, frames) {
|
|
1667
|
+
var v = scrubState[id];
|
|
1668
|
+
if (v == null) return frames.length ? frames.length - 1 : 0; // default: the last real frame
|
|
1669
|
+
v = Number(v);
|
|
1670
|
+
if (!(v >= 0)) v = 0;
|
|
1671
|
+
if (v > frames.length - 1) v = frames.length - 1;
|
|
1672
|
+
return v;
|
|
1673
|
+
}
|
|
1674
|
+
function frameInfoHtml(f) {
|
|
1675
|
+
if (!f) return "";
|
|
1676
|
+
var out = "";
|
|
1677
|
+
var acts = (f.actions || []).filter(Boolean);
|
|
1678
|
+
var narr = (f.narration || []).filter(Boolean);
|
|
1679
|
+
if (acts.length) out += '<div class="replay-acts">' + acts.map(function (a) { return '<span class="replay-act">' + esc(a) + '</span>'; }).join("") + '</div>';
|
|
1680
|
+
if (narr.length) out += '<div class="replay-narr">' + esc(narr.join(NL + NL)) + '</div>';
|
|
1681
|
+
if (!out) out = '<div class="replay-narr replay-dim">No narration recorded before this frame.</div>';
|
|
1682
|
+
return out;
|
|
1683
|
+
}
|
|
1684
|
+
function replayScrubber(s) {
|
|
1685
|
+
var frames = replayFrames(s);
|
|
1686
|
+
var id = s.id;
|
|
1687
|
+
var idx = scrubIndexOf(id, frames);
|
|
1688
|
+
var f = frames[idx];
|
|
1689
|
+
var playing = !!scrubTimers[id];
|
|
1690
|
+
return '<div class="replay" data-stream="' + esc(id) + '">'
|
|
1691
|
+
+ '<div class="replay-stage"><img class="surface-fill surface-screenshot" id="replay-img-' + esc(id) + '" src="' + esc(runArtifactHref(f && f.src)) + '" alt="replay frame ' + (idx + 1) + '"/>'
|
|
1692
|
+
+ '<span class="replay-badge" id="replay-badge-' + esc(id) + '" title="frame capture fidelity">' + esc(f ? f.redaction : "raw") + '</span></div>'
|
|
1693
|
+
+ '<div class="replay-bar">'
|
|
1694
|
+
+ '<button class="replay-btn" id="replay-play-' + esc(id) + '" data-action="scrub:' + esc(id) + ':play" aria-label="Play replay">' + icon(playing ? "pause" : "play", 13) + '</button>'
|
|
1695
|
+
+ '<button class="replay-btn" data-action="scrub:' + esc(id) + ':-1" aria-label="Previous frame">' + icon("chevL", 13) + '</button>'
|
|
1696
|
+
+ '<input class="replay-range" type="range" data-role="scrub" data-stream="' + esc(id) + '" min="0" max="' + (frames.length - 1) + '" step="1" value="' + idx + '" aria-label="Scrub frames"/>'
|
|
1697
|
+
+ '<button class="replay-btn" data-action="scrub:' + esc(id) + ':1" aria-label="Next frame">' + icon("chevR", 13) + '</button>'
|
|
1698
|
+
+ '<span class="replay-count mono" id="replay-count-' + esc(id) + '">' + (idx + 1) + '/' + frames.length + '</span>'
|
|
1699
|
+
+ '</div>'
|
|
1700
|
+
+ '<div class="replay-info" id="replay-info-' + esc(id) + '">' + frameInfoHtml(f) + '</div>'
|
|
1701
|
+
+ '</div>';
|
|
1702
|
+
}
|
|
1703
|
+
function updateScrubDom(id) {
|
|
1704
|
+
var s = streamById(id);
|
|
1705
|
+
if (!s) return;
|
|
1706
|
+
var frames = replayFrames(s);
|
|
1707
|
+
if (!frames.length) return;
|
|
1708
|
+
var idx = scrubIndexOf(id, frames);
|
|
1709
|
+
var f = frames[idx];
|
|
1710
|
+
var img = document.getElementById("replay-img-" + id);
|
|
1711
|
+
if (img) img.setAttribute("src", runArtifactHref(f && f.src));
|
|
1712
|
+
var badge = document.getElementById("replay-badge-" + id);
|
|
1713
|
+
if (badge) badge.textContent = f ? f.redaction : "raw";
|
|
1714
|
+
var cnt = document.getElementById("replay-count-" + id);
|
|
1715
|
+
if (cnt) cnt.textContent = (idx + 1) + "/" + frames.length;
|
|
1716
|
+
var info = document.getElementById("replay-info-" + id);
|
|
1717
|
+
if (info) info.innerHTML = frameInfoHtml(f);
|
|
1718
|
+
var range = app.querySelector('.replay-range[data-stream="' + id + '"]');
|
|
1719
|
+
if (range && String(range.value) !== String(idx)) range.value = idx;
|
|
1720
|
+
}
|
|
1721
|
+
function syncPlayBtn(id) {
|
|
1722
|
+
var btn = document.getElementById("replay-play-" + id);
|
|
1723
|
+
if (btn) btn.innerHTML = icon(scrubTimers[id] ? "pause" : "play", 13);
|
|
1724
|
+
}
|
|
1725
|
+
function stopScrubPlay(id) {
|
|
1726
|
+
if (scrubTimers[id]) { clearInterval(scrubTimers[id]); delete scrubTimers[id]; syncPlayBtn(id); }
|
|
1727
|
+
}
|
|
1728
|
+
function stopAllScrubTimers() {
|
|
1729
|
+
for (var k in scrubTimers) { if (scrubTimers.hasOwnProperty(k)) clearInterval(scrubTimers[k]); }
|
|
1730
|
+
scrubTimers = {};
|
|
1731
|
+
}
|
|
1732
|
+
function toggleScrubPlay(id) {
|
|
1733
|
+
if (scrubTimers[id]) { stopScrubPlay(id); return; }
|
|
1734
|
+
var s = streamById(id);
|
|
1735
|
+
var frames = s ? replayFrames(s) : [];
|
|
1736
|
+
if (frames.length < 2) return;
|
|
1737
|
+
if (scrubIndexOf(id, frames) >= frames.length - 1) { scrubState[id] = 0; updateScrubDom(id); } // restart from the top
|
|
1738
|
+
scrubTimers[id] = setInterval(function () {
|
|
1739
|
+
var st = streamById(id);
|
|
1740
|
+
var fr = st ? replayFrames(st) : [];
|
|
1741
|
+
if (fr.length < 2) { stopScrubPlay(id); return; }
|
|
1742
|
+
var cur = scrubIndexOf(id, fr);
|
|
1743
|
+
if (cur >= fr.length - 1) { stopScrubPlay(id); return; }
|
|
1744
|
+
scrubState[id] = cur + 1;
|
|
1745
|
+
updateScrubDom(id);
|
|
1746
|
+
}, 850);
|
|
1747
|
+
syncPlayBtn(id);
|
|
1748
|
+
}
|
|
1749
|
+
function handleScrub(id, op) {
|
|
1750
|
+
var s = streamById(id);
|
|
1751
|
+
var frames = s ? replayFrames(s) : [];
|
|
1752
|
+
if (!frames.length) return;
|
|
1753
|
+
if (op === "play") { toggleScrubPlay(id); return; }
|
|
1754
|
+
stopScrubPlay(id);
|
|
1755
|
+
var idx = scrubIndexOf(id, frames) + Number(op);
|
|
1756
|
+
if (idx < 0) idx = 0;
|
|
1757
|
+
if (idx > frames.length - 1) idx = frames.length - 1;
|
|
1758
|
+
scrubState[id] = idx;
|
|
1759
|
+
updateScrubDom(id);
|
|
1760
|
+
}
|
|
1608
1761
|
function browserSurface(s, focus) {
|
|
1609
1762
|
var live = tone(s.status) === "running";
|
|
1610
1763
|
var route = laneRoute(s) || "(local)";
|
|
1611
1764
|
var liveUrl = browserLiveUrl(s);
|
|
1612
1765
|
var shot = browserShot(s);
|
|
1613
1766
|
var dock = focus ? browserLabDock(s, shot) : "";
|
|
1767
|
+
var showReplay = focus && !(liveUrl && S.media === "live") && replayFrames(s).length >= 2;
|
|
1614
1768
|
var body;
|
|
1615
1769
|
if (liveUrl && S.media === "live") body = liveStreamMount(s, liveUrl);
|
|
1770
|
+
else if (showReplay) body = replayScrubber(s);
|
|
1616
1771
|
else if (shot) body = '<img class="surface-fill surface-screenshot" src="' + esc(shot) + '" alt="' + (s.desktopGeometry ? "desktop screenshot" : "browser screenshot") + '"/>';
|
|
1617
1772
|
else body = '<div class="bw-app-wait"><div class="wait-spinner" style="width:24px;height:24px"></div>'
|
|
1618
1773
|
+ '<div class="mono" style="font-size:9px">' + esc(route) + '</div>'
|
|
@@ -2495,6 +2650,7 @@ export function observerClientJs() {
|
|
|
2495
2650
|
}
|
|
2496
2651
|
|
|
2497
2652
|
function render() {
|
|
2653
|
+
stopAllScrubTimers(); // a full rebuild replaces the scrubber DOM; never leave a timer ticking on stale nodes
|
|
2498
2654
|
var docEl = document.documentElement;
|
|
2499
2655
|
docEl.setAttribute("data-theme", S.theme);
|
|
2500
2656
|
docEl.setAttribute("data-motion", S.motion);
|
|
@@ -2561,6 +2717,7 @@ export function observerClientJs() {
|
|
|
2561
2717
|
var arg = parts[1];
|
|
2562
2718
|
var arg2 = parts[2];
|
|
2563
2719
|
switch (cmd) {
|
|
2720
|
+
case "scrub": handleScrub(arg, arg2); break;
|
|
2564
2721
|
case "open": openFocus(arg); break;
|
|
2565
2722
|
case "select": S.focusedId = arg; writeHash(arg); render(); break;
|
|
2566
2723
|
case "exit-focus": exitFocus(); break;
|
|
@@ -2615,7 +2772,16 @@ export function observerClientJs() {
|
|
|
2615
2772
|
});
|
|
2616
2773
|
app.addEventListener("input", function (e) {
|
|
2617
2774
|
var t = e.target;
|
|
2618
|
-
if (t
|
|
2775
|
+
if (!t || !t.getAttribute) return;
|
|
2776
|
+
if (t.getAttribute("data-role") === "search") { S.query = t.value; render(); return; }
|
|
2777
|
+
if (t.getAttribute("data-role") === "scrub") {
|
|
2778
|
+
// Direct DOM update (NOT a full render) so dragging the slider stays smooth and the slider
|
|
2779
|
+
// element is not rebuilt mid-drag. Position is stashed in scrubState so a later render restores it.
|
|
2780
|
+
var sid = t.getAttribute("data-stream");
|
|
2781
|
+
stopScrubPlay(sid);
|
|
2782
|
+
scrubState[sid] = Number(t.value);
|
|
2783
|
+
updateScrubDom(sid);
|
|
2784
|
+
}
|
|
2619
2785
|
});
|
|
2620
2786
|
document.addEventListener("mousedown", function (e) {
|
|
2621
2787
|
if (openDd) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observer-assets.js","sourceRoot":"","sources":["../src/observer-assets.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,gFAAgF;AAChF,+EAA+E;AAC/E,iEAAiE;AACjE,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,4EAA4E;AAC5E,+CAA+C;AAE/C,MAAM,UAAU,WAAW;IACzB,OAAO
|
|
1
|
+
{"version":3,"file":"observer-assets.js","sourceRoot":"","sources":["../src/observer-assets.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,gFAAgF;AAChF,+EAA+E;AAC/E,iEAAiE;AACjE,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,4EAA4E;AAC5E,+CAA+C;AAE/C,MAAM,UAAU,WAAW;IACzB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsjCR,CAAC;AACF,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmxDR,CAAC;AACF,CAAC"}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Date: 2026-06-02 (current-state note updated 2026-07-14)
|
|
4
4
|
|
|
5
5
|
Status: reference map for the major contracts shipped through source version
|
|
6
|
-
`0.20.
|
|
6
|
+
`0.20.5`; it is not an exhaustive inventory of command/result envelopes. Exported types,
|
|
7
7
|
schema constants, parsers, and validators in `src/` are authoritative. Rows
|
|
8
8
|
marked "reserved" name layering intent only — no code emits or validates them
|
|
9
9
|
yet. Do not emit a reserved schema.
|
package/docs/goals/current.md
CHANGED
|
@@ -16,7 +16,7 @@ Humanish should be the open-source CLI that lets a maintainer ask:
|
|
|
16
16
|
The answer should be observable, verifiable, public-safe, and easy to turn into
|
|
17
17
|
actionable feedback.
|
|
18
18
|
|
|
19
|
-
## Current Program Truth (source `0.20.
|
|
19
|
+
## Current Program Truth (source `0.20.5`)
|
|
20
20
|
|
|
21
21
|
The package source and repository implementation in this tree agree on these
|
|
22
22
|
points:
|
package/docs/ramp/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Status: public-safe contributor and agent ramp.
|
|
4
4
|
|
|
5
|
-
Package/source version in this tree: `0.20.
|
|
5
|
+
Package/source version in this tree: `0.20.5` (2026-08-03). The containment boundary introduced in
|
|
6
6
|
`0.15.1` remains in force: managed run and output paths bind to validated
|
|
7
7
|
physical filesystem identities, and stored provider IDs are evidence, not
|
|
8
8
|
cleanup authority. The bundled OSS meta-lab is dry-run only until
|
package/package.json
CHANGED