instar 1.3.475 → 1.3.476
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/package.json
CHANGED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* throwaway-identity.mjs — mint genuinely-distinct, readable throwaway email
|
|
3
|
+
* identities for live-integration test harnesses (Slack, Discord, …).
|
|
4
|
+
*
|
|
5
|
+
* Backed by the mail.tm public disposable-mailbox API. Each minted inbox is a
|
|
6
|
+
* real, distinct, readable address — so a live test can use N of them as N
|
|
7
|
+
* GENUINELY DISTINCT principals (distinct addresses → distinct provider user
|
|
8
|
+
* IDs) with ZERO real accounts. This is the autonomous half of the
|
|
9
|
+
* test-identity provisioning the Live Integration Security-Test Harness needs;
|
|
10
|
+
* the only step it does NOT cover is an anti-bot signup CAPTCHA at workspace/
|
|
11
|
+
* account creation, which is a deliberate human-verification control (see
|
|
12
|
+
* docs/specs/JUDGMENT-PERMISSION-LIVE-RUN-RUNBOOK.md).
|
|
13
|
+
*
|
|
14
|
+
* Pure helpers (extractCode/extractLink/matchMessage) and HTTP helpers with an
|
|
15
|
+
* injectable `fetchImpl` so the unit test runs fully hermetic (no network).
|
|
16
|
+
*
|
|
17
|
+
* CLI: scripts/throwaway-identity.mjs (mint | wait). This file is the importable
|
|
18
|
+
* library; the CLI is a thin wrapper.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const DEFAULT_API_BASE = 'https://api.mail.tm';
|
|
22
|
+
|
|
23
|
+
/** Extract the first verification code (default: a 6-digit run) from a message body. */
|
|
24
|
+
export function extractCode(text, { pattern = /\b(\d{6})\b/ } = {}) {
|
|
25
|
+
if (typeof text !== 'string') return null;
|
|
26
|
+
const m = text.match(pattern);
|
|
27
|
+
return m ? (m[1] ?? m[0]) : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Extract the first URL (optionally matching a substring/regex) from a message body. */
|
|
31
|
+
export function extractLink(text, { match } = {}) {
|
|
32
|
+
if (typeof text !== 'string') return null;
|
|
33
|
+
const urls = text.match(/https?:\/\/[^\s"'<>)\]]+/g) || [];
|
|
34
|
+
if (!match) return urls[0] ?? null;
|
|
35
|
+
const test = match instanceof RegExp ? (u) => match.test(u) : (u) => u.includes(match);
|
|
36
|
+
return urls.find(test) ?? null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Does a mail.tm message summary match the caller's filter (subject/from substring or regex)? */
|
|
40
|
+
export function matchMessage(msg, { subject, from } = {}) {
|
|
41
|
+
if (!msg) return false;
|
|
42
|
+
const subjOk = matchField(msg.subject, subject);
|
|
43
|
+
const fromAddr = (msg.from && (msg.from.address || msg.from.name)) || '';
|
|
44
|
+
const fromOk = matchField(fromAddr, from);
|
|
45
|
+
return subjOk && fromOk;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function matchField(value, filter) {
|
|
49
|
+
if (filter === undefined || filter === null) return true;
|
|
50
|
+
const v = String(value ?? '');
|
|
51
|
+
return filter instanceof RegExp ? filter.test(v) : v.toLowerCase().includes(String(filter).toLowerCase());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function memberArray(json) {
|
|
55
|
+
// mail.tm is a Hydra/JSON-LD API: collections live under "hydra:member".
|
|
56
|
+
if (Array.isArray(json)) return json;
|
|
57
|
+
if (json && Array.isArray(json['hydra:member'])) return json['hydra:member'];
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function jsonFetch(fetchImpl, url, opts) {
|
|
62
|
+
const res = await fetchImpl(url, opts);
|
|
63
|
+
const text = await res.text();
|
|
64
|
+
let body = null;
|
|
65
|
+
try { body = text ? JSON.parse(text) : null; } catch { body = null; }
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
const detail = (body && (body['hydra:description'] || body.message || body.detail)) || text.slice(0, 200);
|
|
68
|
+
throw new Error(`mail.tm ${opts?.method || 'GET'} ${url} → ${res.status}: ${detail}`);
|
|
69
|
+
}
|
|
70
|
+
return body;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Mint a fresh throwaway inbox: pick a domain, create the account, get a token.
|
|
75
|
+
* Returns { address, password, token, accountId }. `rand` is injectable so the
|
|
76
|
+
* test is deterministic and the script never calls Math.random in a workflow.
|
|
77
|
+
*/
|
|
78
|
+
export async function createInbox({
|
|
79
|
+
fetchImpl = fetch,
|
|
80
|
+
apiBase = DEFAULT_API_BASE,
|
|
81
|
+
localPart,
|
|
82
|
+
rand = () => Math.floor(Math.random() * 1e10).toString(36),
|
|
83
|
+
} = {}) {
|
|
84
|
+
const domains = memberArray(await jsonFetch(fetchImpl, `${apiBase}/domains`));
|
|
85
|
+
const domain = domains.find((d) => d.isActive !== false)?.domain || domains[0]?.domain;
|
|
86
|
+
if (!domain) throw new Error('mail.tm: no available domain');
|
|
87
|
+
const local = localPart || `echo-${rand()}`;
|
|
88
|
+
const address = `${local}@${domain}`;
|
|
89
|
+
const password = `Echo!${rand()}A9`;
|
|
90
|
+
const account = await jsonFetch(fetchImpl, `${apiBase}/accounts`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: { 'Content-Type': 'application/json' },
|
|
93
|
+
body: JSON.stringify({ address, password }),
|
|
94
|
+
});
|
|
95
|
+
const tokenResp = await jsonFetch(fetchImpl, `${apiBase}/token`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: { 'Content-Type': 'application/json' },
|
|
98
|
+
body: JSON.stringify({ address, password }),
|
|
99
|
+
});
|
|
100
|
+
if (!tokenResp?.token) throw new Error('mail.tm: token request returned no token');
|
|
101
|
+
return { address, password, token: tokenResp.token, accountId: account?.id ?? null };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** List message summaries in the inbox (most-recent first, per mail.tm). */
|
|
105
|
+
export async function listMessages(token, { fetchImpl = fetch, apiBase = DEFAULT_API_BASE } = {}) {
|
|
106
|
+
const body = await jsonFetch(fetchImpl, `${apiBase}/messages`, {
|
|
107
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
108
|
+
});
|
|
109
|
+
return memberArray(body);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Fetch one message's full body (text + html). */
|
|
113
|
+
export async function getMessage(token, id, { fetchImpl = fetch, apiBase = DEFAULT_API_BASE } = {}) {
|
|
114
|
+
return jsonFetch(fetchImpl, `${apiBase}/messages/${id}`, {
|
|
115
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Poll the inbox until a message matches the filter, then return its full body.
|
|
121
|
+
* `sleep` and `now` are injectable so the test runs without real timers/clock.
|
|
122
|
+
* Throws on timeout. Returns the full message (with .text / .html / .subject).
|
|
123
|
+
*/
|
|
124
|
+
export async function waitForMessage(token, {
|
|
125
|
+
subject,
|
|
126
|
+
from,
|
|
127
|
+
timeoutMs = 120_000,
|
|
128
|
+
intervalMs = 3_000,
|
|
129
|
+
fetchImpl = fetch,
|
|
130
|
+
apiBase = DEFAULT_API_BASE,
|
|
131
|
+
sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
132
|
+
now = () => Date.now(),
|
|
133
|
+
} = {}) {
|
|
134
|
+
const deadline = now() + timeoutMs;
|
|
135
|
+
// First iteration runs immediately (no upfront sleep).
|
|
136
|
+
for (let first = true; ; first = false) {
|
|
137
|
+
if (!first) {
|
|
138
|
+
if (now() >= deadline) {
|
|
139
|
+
throw new Error(`waitForMessage: timed out after ${timeoutMs}ms (subject=${subject ?? '*'} from=${from ?? '*'})`);
|
|
140
|
+
}
|
|
141
|
+
await sleep(intervalMs);
|
|
142
|
+
}
|
|
143
|
+
const summaries = await listMessages(token, { fetchImpl, apiBase });
|
|
144
|
+
const hit = summaries.find((m) => matchMessage(m, { subject, from }));
|
|
145
|
+
if (hit) return getMessage(token, hit.id, { fetchImpl, apiBase });
|
|
146
|
+
if (first && now() >= deadline) {
|
|
147
|
+
throw new Error(`waitForMessage: timed out after ${timeoutMs}ms (subject=${subject ?? '*'} from=${from ?? '*'})`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* throwaway-identity.mjs — CLI for the disposable-identity helper (lib/throwaway-identity.mjs).
|
|
4
|
+
*
|
|
5
|
+
* node scripts/throwaway-identity.mjs mint
|
|
6
|
+
* → mints a fresh inbox; prints JSON { address, password, token, accountId }.
|
|
7
|
+
*
|
|
8
|
+
* node scripts/throwaway-identity.mjs wait <token> [--subject S] [--from F]
|
|
9
|
+
* [--code | --link [MATCH]] [--timeout MS]
|
|
10
|
+
* → polls the inbox until a matching message arrives, then prints either
|
|
11
|
+
* the full message JSON, or (with --code/--link) just the extracted value.
|
|
12
|
+
*
|
|
13
|
+
* Use for live-integration test-identity provisioning (the autonomous half — an
|
|
14
|
+
* anti-bot signup CAPTCHA at account creation is the human handoff).
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
createInbox,
|
|
18
|
+
waitForMessage,
|
|
19
|
+
extractCode,
|
|
20
|
+
extractLink,
|
|
21
|
+
} from './lib/throwaway-identity.mjs';
|
|
22
|
+
|
|
23
|
+
function parseFlags(args) {
|
|
24
|
+
const out = { _: [] };
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
const a = args[i];
|
|
27
|
+
if (a === '--code') out.code = true;
|
|
28
|
+
else if (a === '--link') { out.link = true; if (args[i + 1] && !args[i + 1].startsWith('--')) out.linkMatch = args[++i]; }
|
|
29
|
+
else if (a === '--subject') out.subject = args[++i];
|
|
30
|
+
else if (a === '--from') out.from = args[++i];
|
|
31
|
+
else if (a === '--timeout') out.timeout = parseInt(args[++i], 10);
|
|
32
|
+
else out._.push(a);
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function main() {
|
|
38
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
39
|
+
const flags = parseFlags(rest);
|
|
40
|
+
|
|
41
|
+
if (cmd === 'mint') {
|
|
42
|
+
const inbox = await createInbox();
|
|
43
|
+
process.stdout.write(JSON.stringify(inbox) + '\n');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (cmd === 'wait') {
|
|
48
|
+
const token = flags._[0];
|
|
49
|
+
if (!token) { console.error('usage: throwaway-identity.mjs wait <token> [--subject S] [--from F] [--code|--link [MATCH]] [--timeout MS]'); process.exit(1); }
|
|
50
|
+
const msg = await waitForMessage(token, {
|
|
51
|
+
subject: flags.subject,
|
|
52
|
+
from: flags.from,
|
|
53
|
+
timeoutMs: flags.timeout ?? 120_000,
|
|
54
|
+
});
|
|
55
|
+
const text = msg.text || msg.html?.join?.('\n') || (Array.isArray(msg.html) ? msg.html.join('\n') : msg.html) || '';
|
|
56
|
+
if (flags.code) {
|
|
57
|
+
const code = extractCode(text);
|
|
58
|
+
if (!code) { console.error('no verification code found in message'); process.exit(1); }
|
|
59
|
+
process.stdout.write(code + '\n');
|
|
60
|
+
} else if (flags.link) {
|
|
61
|
+
const link = extractLink(text, flags.linkMatch ? { match: flags.linkMatch } : {});
|
|
62
|
+
if (!link) { console.error('no link found in message'); process.exit(1); }
|
|
63
|
+
process.stdout.write(link + '\n');
|
|
64
|
+
} else {
|
|
65
|
+
process.stdout.write(JSON.stringify({ subject: msg.subject, from: msg.from, text }) + '\n');
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.error('usage: throwaway-identity.mjs <mint | wait>');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
import { pathToFileURL } from 'node:url';
|
|
75
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
76
|
+
main().catch((err) => { console.error(err instanceof Error ? err.message : String(err)); process.exit(1); });
|
|
77
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-06-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-06-10T19:09:40.028Z",
|
|
5
|
+
"instarVersion": "1.3.476",
|
|
6
6
|
"entryCount": 201,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Added a standalone test utility — `scripts/throwaway-identity.mjs` (+ `scripts/lib/`) — that
|
|
9
|
+
mints genuinely-distinct, readable throwaway email inboxes via the mail.tm public
|
|
10
|
+
disposable-mailbox API and extracts verification codes/links from them. It's the autonomous
|
|
11
|
+
half of provisioning distinct test identities for the Live Integration Security-Test Harness
|
|
12
|
+
(Slack today, any integration later): N distinct inboxes → N genuinely-distinct principals,
|
|
13
|
+
zero real accounts. No runtime code, gate, or config is touched.
|
|
14
|
+
|
|
15
|
+
## What to Tell Your User
|
|
16
|
+
|
|
17
|
+
Nothing changes in how the agent runs. This is a developer/test tool. It lets the agent set
|
|
18
|
+
up several genuinely-different throwaway test users (and read their email) on its own, so a
|
|
19
|
+
live integration test can use real distinct identities without anyone hand-creating email
|
|
20
|
+
accounts. The only step it intentionally leaves to a human is passing an anti-bot CAPTCHA at
|
|
21
|
+
workspace creation.
|
|
22
|
+
|
|
23
|
+
## Summary of New Capabilities
|
|
24
|
+
|
|
25
|
+
- `scripts/throwaway-identity.mjs mint` — create a fresh readable throwaway inbox.
|
|
26
|
+
- `scripts/throwaway-identity.mjs wait <token>` — await an email and extract its code/link.
|
|
27
|
+
- `scripts/lib/throwaway-identity.mjs` — importable helper (HTTP injectable for hermetic tests).
|
|
28
|
+
|
|
29
|
+
## Evidence
|
|
30
|
+
|
|
31
|
+
- 15 hermetic unit tests (`tests/unit/throwaway-identity.test.ts`) — pure extractors + the
|
|
32
|
+
mint / poll-until-match / timeout flow, with fetch + clock injected (no network).
|
|
33
|
+
- Live CLI smoke minted a real inbox + token. `tsc --noEmit` clean.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Side-Effects Review — throwaway-identity helper
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `throwaway-identity-helper`
|
|
4
|
+
**Date:** `2026-06-10`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Tier:** `1` (standalone test-tooling script + lib + hermetic test; no runtime/gate/config wiring)
|
|
7
|
+
**Second-pass reviewer:** `not required`
|
|
8
|
+
|
|
9
|
+
## Summary of the change
|
|
10
|
+
|
|
11
|
+
Adds `scripts/lib/throwaway-identity.mjs` (importable) + `scripts/throwaway-identity.mjs`
|
|
12
|
+
(CLI) — mints genuinely-distinct, readable throwaway email inboxes via the mail.tm public
|
|
13
|
+
disposable-mailbox API, and polls/extracts codes+links from them. The autonomous half of
|
|
14
|
+
test-identity provisioning for live-integration test harnesses (Slack/Discord/…). + a fully
|
|
15
|
+
hermetic unit test (injected fetch + clock, no network) and an ELI16.
|
|
16
|
+
|
|
17
|
+
## Decision-point inventory
|
|
18
|
+
|
|
19
|
+
None in runtime — it's standalone tooling, never imported by `src/` or wired into a gate.
|
|
20
|
+
Internal branch points (domain selection, message-match filter, timeout) are pure functions
|
|
21
|
+
covered by the test.
|
|
22
|
+
|
|
23
|
+
## 1. Over-block
|
|
24
|
+
|
|
25
|
+
Nothing is rejected at runtime. The tool only calls a public disposable-mail API and reads
|
|
26
|
+
inboxes it just created. It is not on any agent code path.
|
|
27
|
+
|
|
28
|
+
## 2. Under-block
|
|
29
|
+
|
|
30
|
+
It deliberately does NOT cover the anti-bot signup CAPTCHA at workspace/account creation —
|
|
31
|
+
that is a human-verification control and remains a ~30s human handoff (documented in the
|
|
32
|
+
live-run runbook). It is the email half only.
|
|
33
|
+
|
|
34
|
+
## 3. Level-of-abstraction fit
|
|
35
|
+
|
|
36
|
+
Right layer: a `scripts/` + `scripts/lib/` test utility beside the other dev/test scripts,
|
|
37
|
+
with the HTTP injected so the test is hermetic. Reusable by any integration's live harness,
|
|
38
|
+
not coupled to the permission gate.
|
|
39
|
+
|
|
40
|
+
## Migration / rollback
|
|
41
|
+
|
|
42
|
+
No migration (standalone tooling). Rollback = delete the two scripts + the test.
|
|
43
|
+
|
|
44
|
+
## Testing-integrity note
|
|
45
|
+
|
|
46
|
+
15 hermetic unit tests (pure extractors + the full mint / poll-until-match / timeout flow,
|
|
47
|
+
HTTP + clock injected). A separate live CLI smoke confirmed it mints a real inbox
|
|
48
|
+
(`echo-…@web-library.net` + token). `tsc --noEmit` clean.
|