aiden-runtime 4.1.5 → 4.6.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/README.md +265 -847
- package/dist/api/server.js +32 -5
- package/dist/cli/v4/aidenCLI.js +536 -152
- package/dist/cli/v4/callbacks.js +170 -0
- package/dist/cli/v4/chatSession.js +245 -3
- package/dist/cli/v4/commands/_runtimeToggleHelpers.js +94 -0
- package/dist/cli/v4/commands/browserDepth.js +45 -0
- package/dist/cli/v4/commands/cron.js +264 -0
- package/dist/cli/v4/commands/daemon.js +541 -0
- package/dist/cli/v4/commands/daemonStatus.js +253 -0
- package/dist/cli/v4/commands/fanout.js +42 -59
- package/dist/cli/v4/commands/help.js +13 -0
- package/dist/cli/v4/commands/index.js +35 -1
- package/dist/cli/v4/commands/mcp.js +80 -54
- package/dist/cli/v4/commands/plannerGuard.js +53 -0
- package/dist/cli/v4/commands/recovery.js +122 -0
- package/dist/cli/v4/commands/runs.js +223 -0
- package/dist/cli/v4/commands/sandbox.js +48 -0
- package/dist/cli/v4/commands/spawnPause.js +93 -0
- package/dist/cli/v4/commands/suggestions.js +68 -0
- package/dist/cli/v4/commands/tce.js +41 -0
- package/dist/cli/v4/commands/trigger.js +378 -0
- package/dist/cli/v4/commands/update.js +95 -3
- package/dist/cli/v4/daemonAgentBuilder.js +145 -0
- package/dist/cli/v4/defaultSoul.js +1 -1
- package/dist/cli/v4/display/capabilityCard.js +26 -0
- package/dist/cli/v4/display.js +18 -8
- package/dist/cli/v4/replyRenderer.js +31 -23
- package/dist/cli/v4/updateBootPrompt.js +170 -0
- package/dist/core/playwrightBridge.js +129 -0
- package/dist/core/v4/aidenAgent.js +527 -5
- package/dist/core/v4/browserState.js +436 -0
- package/dist/core/v4/checkpoint.js +79 -0
- package/dist/core/v4/daemon/bootstrap.js +651 -0
- package/dist/core/v4/daemon/cleanShutdown.js +154 -0
- package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
- package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
- package/dist/core/v4/daemon/cron/migration.js +199 -0
- package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
- package/dist/core/v4/daemon/daemonConfig.js +90 -0
- package/dist/core/v4/daemon/db/connection.js +106 -0
- package/dist/core/v4/daemon/db/migrations.js +362 -0
- package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
- package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
- package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
- package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
- package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
- package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
- package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
- package/dist/core/v4/daemon/dispatcher/index.js +53 -0
- package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
- package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
- package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
- package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
- package/dist/core/v4/daemon/drain.js +156 -0
- package/dist/core/v4/daemon/eventLoopLag.js +73 -0
- package/dist/core/v4/daemon/health.js +159 -0
- package/dist/core/v4/daemon/idempotencyStore.js +204 -0
- package/dist/core/v4/daemon/index.js +179 -0
- package/dist/core/v4/daemon/instanceTracker.js +99 -0
- package/dist/core/v4/daemon/resourceRegistry.js +150 -0
- package/dist/core/v4/daemon/restartCode.js +32 -0
- package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
- package/dist/core/v4/daemon/runStore.js +144 -0
- package/dist/core/v4/daemon/runtimeLock.js +167 -0
- package/dist/core/v4/daemon/signals.js +50 -0
- package/dist/core/v4/daemon/supervisor.js +272 -0
- package/dist/core/v4/daemon/triggerBus.js +279 -0
- package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
- package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
- package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
- package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
- package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
- package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
- package/dist/core/v4/daemon/triggers/email/index.js +332 -0
- package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
- package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
- package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
- package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
- package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
- package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
- package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
- package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
- package/dist/core/v4/daemon/triggers/webhook.js +376 -0
- package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
- package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
- package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
- package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
- package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
- package/dist/core/v4/daemon/types.js +15 -0
- package/dist/core/v4/dockerSession.js +461 -0
- package/dist/core/v4/dryRun.js +117 -0
- package/dist/core/v4/failureClassifier.js +779 -0
- package/dist/core/v4/providerFallback.js +35 -2
- package/dist/core/v4/recoveryReport.js +449 -0
- package/dist/core/v4/runtimeToggles.js +214 -0
- package/dist/core/v4/sandboxConfig.js +285 -0
- package/dist/core/v4/sandboxFs.js +316 -0
- package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
- package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
- package/dist/core/v4/subagent/childBuilder.js +391 -0
- package/dist/core/v4/subagent/fanout.js +75 -51
- package/dist/core/v4/subagent/spawnPause.js +191 -0
- package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
- package/dist/core/v4/suggestionCatalog.js +41 -0
- package/dist/core/v4/suggestionEngine.js +210 -0
- package/dist/core/v4/toolRegistry.js +37 -3
- package/dist/core/v4/turnState.js +587 -0
- package/dist/core/v4/update/checkUpdate.js +63 -3
- package/dist/core/v4/update/installMethodDetect.js +115 -0
- package/dist/core/v4/update/registryClient.js +121 -0
- package/dist/core/v4/update/skipState.js +75 -0
- package/dist/core/v4/verifier.js +448 -0
- package/dist/core/version.js +1 -1
- package/dist/moat/plannerGuard.js +29 -0
- package/dist/providers/v4/anthropicAdapter.js +31 -3
- package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
- package/dist/providers/v4/codexResponsesAdapter.js +25 -2
- package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
- package/dist/tools/v4/browser/_observer.js +224 -0
- package/dist/tools/v4/browser/browserBlocker.js +396 -0
- package/dist/tools/v4/browser/browserClick.js +18 -1
- package/dist/tools/v4/browser/browserClose.js +18 -1
- package/dist/tools/v4/browser/browserExtract.js +5 -1
- package/dist/tools/v4/browser/browserFill.js +17 -1
- package/dist/tools/v4/browser/browserGetUrl.js +5 -1
- package/dist/tools/v4/browser/browserNavigate.js +16 -1
- package/dist/tools/v4/browser/browserScreenshot.js +5 -1
- package/dist/tools/v4/browser/browserScroll.js +18 -1
- package/dist/tools/v4/browser/browserType.js +17 -1
- package/dist/tools/v4/browser/captchaCheck.js +5 -1
- package/dist/tools/v4/executeCode.js +1 -0
- package/dist/tools/v4/files/fileCopy.js +56 -2
- package/dist/tools/v4/files/fileDelete.js +38 -1
- package/dist/tools/v4/files/fileList.js +12 -1
- package/dist/tools/v4/files/fileMove.js +59 -2
- package/dist/tools/v4/files/filePatch.js +43 -1
- package/dist/tools/v4/files/fileRead.js +12 -1
- package/dist/tools/v4/files/fileWrite.js +41 -1
- package/dist/tools/v4/index.js +88 -61
- package/dist/tools/v4/memory/memoryAdd.js +14 -0
- package/dist/tools/v4/memory/memoryRemove.js +14 -0
- package/dist/tools/v4/memory/memoryReplace.js +15 -0
- package/dist/tools/v4/memory/sessionSummary.js +12 -0
- package/dist/tools/v4/process/processKill.js +19 -0
- package/dist/tools/v4/process/processList.js +1 -0
- package/dist/tools/v4/process/processLogRead.js +1 -0
- package/dist/tools/v4/process/processSpawn.js +13 -0
- package/dist/tools/v4/process/processWait.js +1 -0
- package/dist/tools/v4/sessions/recallSession.js +1 -0
- package/dist/tools/v4/sessions/sessionList.js +1 -0
- package/dist/tools/v4/sessions/sessionSearch.js +1 -0
- package/dist/tools/v4/skills/lookupToolSchema.js +7 -0
- package/dist/tools/v4/skills/skillManage.js +13 -0
- package/dist/tools/v4/skills/skillView.js +1 -0
- package/dist/tools/v4/skills/skillsList.js +1 -0
- package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
- package/dist/tools/v4/subagent/subagentFanout.js +54 -1
- package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
- package/dist/tools/v4/system/appClose.js +13 -0
- package/dist/tools/v4/system/appInput.js +13 -0
- package/dist/tools/v4/system/appLaunch.js +13 -0
- package/dist/tools/v4/system/clipboardRead.js +1 -0
- package/dist/tools/v4/system/clipboardWrite.js +14 -0
- package/dist/tools/v4/system/mediaKey.js +12 -0
- package/dist/tools/v4/system/mediaSessions.js +1 -0
- package/dist/tools/v4/system/mediaTransport.js +13 -0
- package/dist/tools/v4/system/naturalEvents.js +1 -0
- package/dist/tools/v4/system/nowPlaying.js +1 -0
- package/dist/tools/v4/system/osProcessList.js +1 -0
- package/dist/tools/v4/system/screenshot.js +1 -0
- package/dist/tools/v4/system/systemInfo.js +1 -0
- package/dist/tools/v4/system/volumeSet.js +17 -0
- package/dist/tools/v4/terminal/shellExec.js +81 -9
- package/dist/tools/v4/web/deepResearch.js +1 -0
- package/dist/tools/v4/web/openUrl.js +1 -0
- package/dist/tools/v4/web/webFetch.js +1 -0
- package/dist/tools/v4/web/webPage.js +1 -0
- package/dist/tools/v4/web/webSearch.js +1 -0
- package/dist/tools/v4/web/youtubeSearch.js +1 -0
- package/package.json +13 -3
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/triggerBus.ts — v4.5 Phase 1: durable trigger bus.
|
|
10
|
+
*
|
|
11
|
+
* The CENTRAL persistence layer for the daemon. Every accepted
|
|
12
|
+
* trigger (file watcher fire, webhook delivery, email match,
|
|
13
|
+
* scheduled job tick) normalizes into a `trigger_events` row
|
|
14
|
+
* BEFORE returning 202 Accepted / marking the email seen /
|
|
15
|
+
* enqueuing schedule. ONE durable queue, not 4 parallel half-
|
|
16
|
+
* baked persistence layers.
|
|
17
|
+
*
|
|
18
|
+
* Concurrency semantics:
|
|
19
|
+
* - `insert()` uses INSERT OR IGNORE against the partial unique
|
|
20
|
+
* index on (source, idempotency_key). Duplicates return the
|
|
21
|
+
* existing id with `inserted: false`.
|
|
22
|
+
* - `claim()` atomically picks the oldest pending event and
|
|
23
|
+
* sets status='claimed' + claim_owner + claim_expires_at. A
|
|
24
|
+
* per-claim nonce (claimToken) is returned; subsequent
|
|
25
|
+
* release/markDone/markFailed validate it to prevent double-
|
|
26
|
+
* completion races across sibling daemons.
|
|
27
|
+
* - `reclaimExpired()` returns to 'pending' any claimed row
|
|
28
|
+
* whose lease has elapsed (called by the daemon ticker every
|
|
29
|
+
* 30s + on boot).
|
|
30
|
+
* - `markFailed()` increments attempts; on attempts >=
|
|
31
|
+
* maxAttempts the row moves to 'dead_letter' instead of
|
|
32
|
+
* returning to pending.
|
|
33
|
+
*/
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.DEFAULT_MAX_ATTEMPTS = exports.DEFAULT_CLAIM_LEASE_MS = void 0;
|
|
36
|
+
exports.createTriggerBus = createTriggerBus;
|
|
37
|
+
const node_crypto_1 = require("node:crypto");
|
|
38
|
+
exports.DEFAULT_CLAIM_LEASE_MS = 5 * 60000;
|
|
39
|
+
exports.DEFAULT_MAX_ATTEMPTS = 3;
|
|
40
|
+
function rowToTs(r) {
|
|
41
|
+
return {
|
|
42
|
+
id: r.id,
|
|
43
|
+
source: r.source,
|
|
44
|
+
sourceKey: r.source_key,
|
|
45
|
+
idempotencyKey: r.idempotency_key,
|
|
46
|
+
payload: safeJsonParse(r.payload_json),
|
|
47
|
+
status: r.status,
|
|
48
|
+
attempts: r.attempts,
|
|
49
|
+
claimOwner: r.claim_owner,
|
|
50
|
+
claimExpiresAt: r.claim_expires_at,
|
|
51
|
+
lastError: r.last_error,
|
|
52
|
+
createdAt: r.created_at,
|
|
53
|
+
updatedAt: r.updated_at,
|
|
54
|
+
completedAt: r.completed_at,
|
|
55
|
+
runId: r.run_id,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function safeJsonParse(s) {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(s);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function createTriggerBus(opts) {
|
|
67
|
+
const db = opts.db;
|
|
68
|
+
// In-memory map of valid claim tokens. Stored separately from
|
|
69
|
+
// SQLite so cross-daemon claim attempts can't forge a token by
|
|
70
|
+
// reading the row. Token is wiped on markDone/release/markFailed.
|
|
71
|
+
const activeClaims = new Map();
|
|
72
|
+
return {
|
|
73
|
+
insert(ev) {
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
const payloadJson = JSON.stringify(ev.payload ?? {});
|
|
76
|
+
const result = db
|
|
77
|
+
.prepare(`INSERT OR IGNORE INTO trigger_events
|
|
78
|
+
(source, source_key, idempotency_key, payload_json,
|
|
79
|
+
status, attempts, created_at, updated_at)
|
|
80
|
+
VALUES (?, ?, ?, ?, 'pending', 0, ?, ?)`)
|
|
81
|
+
.run(ev.source, ev.sourceKey, ev.idempotencyKey ?? null, payloadJson, now, now);
|
|
82
|
+
if (result.changes > 0) {
|
|
83
|
+
return { id: Number(result.lastInsertRowid), inserted: true };
|
|
84
|
+
}
|
|
85
|
+
// Dedup hit — return the existing id.
|
|
86
|
+
const existing = db
|
|
87
|
+
.prepare('SELECT id FROM trigger_events WHERE source = ? AND idempotency_key = ?')
|
|
88
|
+
.get(ev.source, ev.idempotencyKey ?? null);
|
|
89
|
+
if (!existing) {
|
|
90
|
+
// Defensive — shouldn't happen unless idempotency_key was null,
|
|
91
|
+
// in which case INSERT OR IGNORE wouldn't have skipped.
|
|
92
|
+
throw new Error('triggerBus.insert: INSERT OR IGNORE produced no row but no existing match found');
|
|
93
|
+
}
|
|
94
|
+
return { id: existing.id, inserted: false };
|
|
95
|
+
},
|
|
96
|
+
claim(opts2 = { ownerId: '' }) {
|
|
97
|
+
const leaseMs = opts2.leaseMs ?? exports.DEFAULT_CLAIM_LEASE_MS;
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
const expires = now + leaseMs;
|
|
100
|
+
const claimToken = (0, node_crypto_1.randomUUID)();
|
|
101
|
+
const tx = db.transaction(() => {
|
|
102
|
+
// Pick the oldest pending event matching the optional source filter.
|
|
103
|
+
// v4.5 Phase 7 — honour cooldown: skip pending rows whose
|
|
104
|
+
// claim_expires_at is still in the future (re-set by
|
|
105
|
+
// markFailed with cooldownMs to delay re-claim).
|
|
106
|
+
const sql = opts2.source
|
|
107
|
+
? `SELECT id FROM trigger_events
|
|
108
|
+
WHERE status = 'pending' AND source = ?
|
|
109
|
+
AND (claim_expires_at IS NULL OR claim_expires_at <= ?)
|
|
110
|
+
ORDER BY created_at LIMIT 1`
|
|
111
|
+
: `SELECT id FROM trigger_events
|
|
112
|
+
WHERE status = 'pending'
|
|
113
|
+
AND (claim_expires_at IS NULL OR claim_expires_at <= ?)
|
|
114
|
+
ORDER BY created_at LIMIT 1`;
|
|
115
|
+
const candidate = (opts2.source
|
|
116
|
+
? db.prepare(sql).get(opts2.source, now)
|
|
117
|
+
: db.prepare(sql).get(now));
|
|
118
|
+
if (!candidate)
|
|
119
|
+
return null;
|
|
120
|
+
const upd = db
|
|
121
|
+
.prepare(`UPDATE trigger_events
|
|
122
|
+
SET status = 'claimed',
|
|
123
|
+
claim_owner = ?,
|
|
124
|
+
claim_expires_at = ?,
|
|
125
|
+
updated_at = ?,
|
|
126
|
+
attempts = attempts + 1
|
|
127
|
+
WHERE id = ? AND status = 'pending'`)
|
|
128
|
+
.run(opts2.ownerId, expires, now, candidate.id);
|
|
129
|
+
if (upd.changes === 0)
|
|
130
|
+
return null; // race lost
|
|
131
|
+
const row = db
|
|
132
|
+
.prepare('SELECT * FROM trigger_events WHERE id = ?')
|
|
133
|
+
.get(candidate.id);
|
|
134
|
+
return rowToTs(row);
|
|
135
|
+
});
|
|
136
|
+
const row = tx();
|
|
137
|
+
if (!row)
|
|
138
|
+
return null;
|
|
139
|
+
activeClaims.set(row.id, claimToken);
|
|
140
|
+
return { ...row, claimToken };
|
|
141
|
+
},
|
|
142
|
+
renewClaim(eventId, claimToken, extendMs) {
|
|
143
|
+
if (activeClaims.get(eventId) !== claimToken)
|
|
144
|
+
return false;
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
const upd = db
|
|
147
|
+
.prepare(`UPDATE trigger_events
|
|
148
|
+
SET claim_expires_at = ?,
|
|
149
|
+
updated_at = ?
|
|
150
|
+
WHERE id = ? AND status = 'claimed'`)
|
|
151
|
+
.run(now + extendMs, now, eventId);
|
|
152
|
+
return upd.changes > 0;
|
|
153
|
+
},
|
|
154
|
+
release(eventId, claimToken) {
|
|
155
|
+
if (activeClaims.get(eventId) !== claimToken)
|
|
156
|
+
return;
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
db.prepare(`UPDATE trigger_events
|
|
159
|
+
SET status = 'pending',
|
|
160
|
+
claim_owner = NULL,
|
|
161
|
+
claim_expires_at = NULL,
|
|
162
|
+
updated_at = ?
|
|
163
|
+
WHERE id = ? AND status = 'claimed'`).run(now, eventId);
|
|
164
|
+
activeClaims.delete(eventId);
|
|
165
|
+
},
|
|
166
|
+
markDone(eventId, claimToken, runId) {
|
|
167
|
+
if (activeClaims.get(eventId) !== claimToken)
|
|
168
|
+
return;
|
|
169
|
+
const now = Date.now();
|
|
170
|
+
db.prepare(`UPDATE trigger_events
|
|
171
|
+
SET status = 'done',
|
|
172
|
+
claim_owner = NULL,
|
|
173
|
+
claim_expires_at = NULL,
|
|
174
|
+
updated_at = ?,
|
|
175
|
+
completed_at = ?,
|
|
176
|
+
run_id = COALESCE(?, run_id)
|
|
177
|
+
WHERE id = ? AND status = 'claimed'`).run(now, now, runId ?? null, eventId);
|
|
178
|
+
activeClaims.delete(eventId);
|
|
179
|
+
},
|
|
180
|
+
markFailed(eventId, claimToken, error, opts2 = {}) {
|
|
181
|
+
if (activeClaims.get(eventId) !== claimToken)
|
|
182
|
+
return;
|
|
183
|
+
const max = opts2.maxAttempts ?? exports.DEFAULT_MAX_ATTEMPTS;
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
const truncated = error.length > 1024 ? error.slice(0, 1024) + '…' : error;
|
|
186
|
+
// v4.5 Phase 7 — optional cooldown delays re-claim. When set,
|
|
187
|
+
// we stash `now + cooldownMs` in `claim_expires_at` while
|
|
188
|
+
// status='pending'. The claim picker filters out pending rows
|
|
189
|
+
// whose claim_expires_at is in the future (added below). Bus
|
|
190
|
+
// poll loop will pick the row up naturally once cooldown
|
|
191
|
+
// elapses — no explicit sleep needed.
|
|
192
|
+
const cooldownUntil = opts2.cooldownMs && opts2.cooldownMs > 0
|
|
193
|
+
? now + opts2.cooldownMs
|
|
194
|
+
: null;
|
|
195
|
+
const tx = db.transaction(() => {
|
|
196
|
+
const row = db
|
|
197
|
+
.prepare('SELECT attempts FROM trigger_events WHERE id = ?')
|
|
198
|
+
.get(eventId);
|
|
199
|
+
if (!row)
|
|
200
|
+
return;
|
|
201
|
+
// attempts was already incremented at claim time. Move to
|
|
202
|
+
// dead_letter when the count hits max, else return to pending.
|
|
203
|
+
if (row.attempts >= max) {
|
|
204
|
+
db.prepare(`UPDATE trigger_events
|
|
205
|
+
SET status = 'dead_letter',
|
|
206
|
+
claim_owner = NULL,
|
|
207
|
+
claim_expires_at = NULL,
|
|
208
|
+
last_error = ?,
|
|
209
|
+
updated_at = ?,
|
|
210
|
+
completed_at = ?
|
|
211
|
+
WHERE id = ?`).run(truncated, now, now, eventId);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
db.prepare(`UPDATE trigger_events
|
|
215
|
+
SET status = 'pending',
|
|
216
|
+
claim_owner = NULL,
|
|
217
|
+
claim_expires_at = ?,
|
|
218
|
+
last_error = ?,
|
|
219
|
+
updated_at = ?
|
|
220
|
+
WHERE id = ?`).run(cooldownUntil, truncated, now, eventId);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
tx();
|
|
224
|
+
activeClaims.delete(eventId);
|
|
225
|
+
},
|
|
226
|
+
reclaimExpired(now) {
|
|
227
|
+
const cutoff = now ?? Date.now();
|
|
228
|
+
const upd = db
|
|
229
|
+
.prepare(`UPDATE trigger_events
|
|
230
|
+
SET status = 'pending',
|
|
231
|
+
claim_owner = NULL,
|
|
232
|
+
claim_expires_at = NULL,
|
|
233
|
+
last_error = COALESCE(last_error, 'claim lease expired'),
|
|
234
|
+
updated_at = ?
|
|
235
|
+
WHERE status = 'claimed'
|
|
236
|
+
AND claim_expires_at IS NOT NULL
|
|
237
|
+
AND claim_expires_at < ?`)
|
|
238
|
+
.run(cutoff, cutoff);
|
|
239
|
+
return { reclaimed: upd.changes };
|
|
240
|
+
},
|
|
241
|
+
deadLetter(eventId, reason) {
|
|
242
|
+
const now = Date.now();
|
|
243
|
+
db.prepare(`UPDATE trigger_events
|
|
244
|
+
SET status = 'dead_letter',
|
|
245
|
+
claim_owner = NULL,
|
|
246
|
+
claim_expires_at = NULL,
|
|
247
|
+
last_error = ?,
|
|
248
|
+
updated_at = ?,
|
|
249
|
+
completed_at = ?
|
|
250
|
+
WHERE id = ?`).run(reason.length > 1024 ? reason.slice(0, 1024) + '…' : reason, now, now, eventId);
|
|
251
|
+
activeClaims.delete(eventId);
|
|
252
|
+
},
|
|
253
|
+
stats() {
|
|
254
|
+
const counts = db
|
|
255
|
+
.prepare(`SELECT status, COUNT(*) AS c FROM trigger_events GROUP BY status`)
|
|
256
|
+
.all();
|
|
257
|
+
const m = {};
|
|
258
|
+
for (const r of counts)
|
|
259
|
+
m[r.status] = r.c;
|
|
260
|
+
const oldest = db
|
|
261
|
+
.prepare(`SELECT MIN(created_at) AS t FROM trigger_events WHERE status = 'pending'`)
|
|
262
|
+
.get();
|
|
263
|
+
const oldestPendingMs = oldest.t != null ? Date.now() - oldest.t : null;
|
|
264
|
+
return {
|
|
265
|
+
pending: m.pending ?? 0,
|
|
266
|
+
claimed: m.claimed ?? 0,
|
|
267
|
+
running: m.running ?? 0,
|
|
268
|
+
deadLetter: m.dead_letter ?? 0,
|
|
269
|
+
oldestPendingMs,
|
|
270
|
+
};
|
|
271
|
+
},
|
|
272
|
+
get(eventId) {
|
|
273
|
+
const r = db
|
|
274
|
+
.prepare('SELECT * FROM trigger_events WHERE id = ?')
|
|
275
|
+
.get(eventId);
|
|
276
|
+
return r ? rowToTs(r) : null;
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/triggers/email/allowlist.ts — v4.5 Phase 4a.
|
|
10
|
+
*
|
|
11
|
+
* Compiles a per-trigger sender allowlist into a fast matcher.
|
|
12
|
+
* Glob syntax supported:
|
|
13
|
+
* - Exact: user@example.com
|
|
14
|
+
* - Domain: *@example.com
|
|
15
|
+
* - Prefix: alerts-*@example.com
|
|
16
|
+
* - Both: *-alerts-*@*.example.com
|
|
17
|
+
*
|
|
18
|
+
* Allowlist semantics:
|
|
19
|
+
* - Default (empty list) = REJECT all. Trigger is functional only
|
|
20
|
+
* after at least one --allow-sender is registered. This matches
|
|
21
|
+
* the explicit-over-implicit security posture.
|
|
22
|
+
* - Applied AFTER the automated-sender filter (we never trust
|
|
23
|
+
* `noreply@<allowed-domain>`).
|
|
24
|
+
* - Case-insensitive (RFC 5321: local-part is technically case-
|
|
25
|
+
* sensitive, but in practice IS not).
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.compileSenderAllowlist = compileSenderAllowlist;
|
|
29
|
+
/**
|
|
30
|
+
* Compile a list of address-pattern strings into a matcher.
|
|
31
|
+
* Empty list → matcher rejects every address (the explicit-allow
|
|
32
|
+
* default per Q-P4-1).
|
|
33
|
+
*/
|
|
34
|
+
function compileSenderAllowlist(patterns) {
|
|
35
|
+
if (patterns.length === 0) {
|
|
36
|
+
return {
|
|
37
|
+
isAllowed: () => false,
|
|
38
|
+
size: () => 0,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const regexes = patterns
|
|
42
|
+
.map((p) => p.trim().toLowerCase())
|
|
43
|
+
.filter((p) => p.length > 0)
|
|
44
|
+
.map(patternToRegex);
|
|
45
|
+
return {
|
|
46
|
+
isAllowed(fromAddress) {
|
|
47
|
+
const addr = (fromAddress ?? '').trim().toLowerCase();
|
|
48
|
+
if (!addr)
|
|
49
|
+
return false;
|
|
50
|
+
for (const re of regexes)
|
|
51
|
+
if (re.test(addr))
|
|
52
|
+
return true;
|
|
53
|
+
return false;
|
|
54
|
+
},
|
|
55
|
+
size: () => regexes.length,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Convert a sender glob pattern into a case-insensitive RegExp
|
|
60
|
+
* anchored at both ends. Only `*` is supported (matches zero or
|
|
61
|
+
* more characters that are NOT a separator we care about — but
|
|
62
|
+
* since email addresses don't have hierarchical separators like
|
|
63
|
+
* filesystems do, `*` is greedy across the whole local-part or
|
|
64
|
+
* domain). Escape every other regex meta-char.
|
|
65
|
+
*/
|
|
66
|
+
function patternToRegex(pat) {
|
|
67
|
+
// Escape regex specials except `*`, then convert `*` to `.*`.
|
|
68
|
+
const escaped = pat.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
|
69
|
+
return new RegExp('^' + escaped + '$');
|
|
70
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/triggers/email/automatedSender.ts — v4.5 Phase 4a.
|
|
10
|
+
*
|
|
11
|
+
* Detects mail from automated systems (noreply addresses, bounce
|
|
12
|
+
* notifications, mailing-list digests, calendar invites, etc.).
|
|
13
|
+
*
|
|
14
|
+
* Used at TWO points:
|
|
15
|
+
* 1. INGRESS — mark UID seen + skip (don't kick off an agent turn
|
|
16
|
+
* to "respond" to a bounce notification).
|
|
17
|
+
* 2. OUTBOUND — any future auto-reply path (Phase 5+) MUST NEVER
|
|
18
|
+
* reply to a sender flagged automated. This is the canonical
|
|
19
|
+
* defense against mail loops (the gateway's notification system
|
|
20
|
+
* bounces our reply, we reply to the bounce, ...).
|
|
21
|
+
*
|
|
22
|
+
* Two signal sources combined:
|
|
23
|
+
* - Substring match on the From / Reply-To address (noreply, etc.)
|
|
24
|
+
* - RFC headers: Auto-Submitted, Precedence, X-Auto-Response-Suppress,
|
|
25
|
+
* List-Unsubscribe.
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.AUTOMATED_HEADERS = exports.NOREPLY_PATTERNS = void 0;
|
|
29
|
+
exports.isAutomatedSender = isAutomatedSender;
|
|
30
|
+
/** Address substrings — case-insensitive — that signal automated. */
|
|
31
|
+
exports.NOREPLY_PATTERNS = Object.freeze([
|
|
32
|
+
'noreply',
|
|
33
|
+
'no-reply',
|
|
34
|
+
'no_reply',
|
|
35
|
+
'donotreply',
|
|
36
|
+
'do-not-reply',
|
|
37
|
+
'mailer-daemon',
|
|
38
|
+
'postmaster',
|
|
39
|
+
'bounce',
|
|
40
|
+
'notifications@',
|
|
41
|
+
'automated@',
|
|
42
|
+
'auto-confirm',
|
|
43
|
+
'auto-reply',
|
|
44
|
+
'automailer',
|
|
45
|
+
]);
|
|
46
|
+
/** RFC headers that signal bulk/automated mail. */
|
|
47
|
+
exports.AUTOMATED_HEADERS = Object.freeze([
|
|
48
|
+
{ header: 'Auto-Submitted', test: (v) => v.toLowerCase() !== 'no' },
|
|
49
|
+
{ header: 'Precedence', test: (v) => ['bulk', 'list', 'junk'].includes(v.toLowerCase()) },
|
|
50
|
+
{ header: 'X-Auto-Response-Suppress', test: (v) => v.length > 0 },
|
|
51
|
+
{ header: 'List-Unsubscribe', test: (v) => v.length > 0 },
|
|
52
|
+
]);
|
|
53
|
+
/**
|
|
54
|
+
* Return true when the sender appears to be an automated system.
|
|
55
|
+
*
|
|
56
|
+
* - `fromAddress` is the bare address (e.g. 'noreply@github.com'),
|
|
57
|
+
* NOT a display-name-decorated form. Caller should extract via
|
|
58
|
+
* mailparser's `parsed.from?.value[0]?.address`.
|
|
59
|
+
* - `headers` is a case-INSENSITIVE map. mailparser provides
|
|
60
|
+
* `parsed.headers` as a Map with lowercased keys; pass that
|
|
61
|
+
* normalized to `Record<string, string>` (multi-value headers
|
|
62
|
+
* pick the first value).
|
|
63
|
+
*/
|
|
64
|
+
function isAutomatedSender(fromAddress, headers) {
|
|
65
|
+
const addr = (fromAddress ?? '').toLowerCase();
|
|
66
|
+
for (const p of exports.NOREPLY_PATTERNS)
|
|
67
|
+
if (addr.includes(p))
|
|
68
|
+
return true;
|
|
69
|
+
for (const r of exports.AUTOMATED_HEADERS) {
|
|
70
|
+
// Match case-insensitively against both the exact-case header
|
|
71
|
+
// name and the lowercased form so callers don't have to remember
|
|
72
|
+
// mailparser's specific quirk.
|
|
73
|
+
const v = headers[r.header] ?? headers[r.header.toLowerCase()];
|
|
74
|
+
if (typeof v === 'string' && v.length > 0 && r.test(v))
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/triggers/email/emailSeenStore.ts — v4.5 Phase 4a.
|
|
10
|
+
*
|
|
11
|
+
* Writer + reader for the `email_seen` table. Cross-restart authority
|
|
12
|
+
* for "have we processed this UID before?" + forensic audit trail.
|
|
13
|
+
*
|
|
14
|
+
* UNIQUE(route_id, uid_validity, uid) at the schema layer dedups
|
|
15
|
+
* concurrent or repeated writes for the same UID. The status field
|
|
16
|
+
* records WHY a UID was seen even when no trigger_event was emitted
|
|
17
|
+
* (skipped due to automated sender, allowlist, subject filter, etc.).
|
|
18
|
+
*
|
|
19
|
+
* Retention: AIDEN_DAEMON_EMAIL_RETENTION_DAYS env (default 30).
|
|
20
|
+
* Sweep runs on boot + daily.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.createEmailSeenStore = createEmailSeenStore;
|
|
24
|
+
function rowToTs(r) {
|
|
25
|
+
return {
|
|
26
|
+
id: r.id,
|
|
27
|
+
routeId: r.route_id,
|
|
28
|
+
mailbox: r.mailbox,
|
|
29
|
+
uidValidity: r.uid_validity,
|
|
30
|
+
uid: r.uid,
|
|
31
|
+
messageId: r.message_id,
|
|
32
|
+
fromAddress: r.from_address,
|
|
33
|
+
subject: r.subject,
|
|
34
|
+
receivedAt: r.received_at,
|
|
35
|
+
processedAt: r.processed_at,
|
|
36
|
+
triggerEventId: r.trigger_event_id,
|
|
37
|
+
status: r.status,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function createEmailSeenStore(opts) {
|
|
41
|
+
const db = opts.db;
|
|
42
|
+
return {
|
|
43
|
+
record(input) {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
// INSERT OR IGNORE returns 0 rows changed on conflict; in that
|
|
46
|
+
// case we look up the existing row id.
|
|
47
|
+
const r = db
|
|
48
|
+
.prepare(`INSERT OR IGNORE INTO email_seen
|
|
49
|
+
(route_id, mailbox, uid_validity, uid, message_id,
|
|
50
|
+
from_address, subject, received_at, processed_at,
|
|
51
|
+
trigger_event_id, status)
|
|
52
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
53
|
+
.run(input.routeId, input.mailbox, input.uidValidity, input.uid, input.messageId, input.fromAddress, input.subject, input.receivedAt, now, input.triggerEventId, input.status);
|
|
54
|
+
if (r.changes > 0)
|
|
55
|
+
return Number(r.lastInsertRowid);
|
|
56
|
+
const existing = db
|
|
57
|
+
.prepare(`SELECT id FROM email_seen WHERE route_id = ? AND uid_validity = ? AND uid = ?`)
|
|
58
|
+
.get(input.routeId, input.uidValidity, input.uid);
|
|
59
|
+
if (!existing) {
|
|
60
|
+
throw new Error('emailSeenStore.record: INSERT OR IGNORE skipped + no existing row found');
|
|
61
|
+
}
|
|
62
|
+
return existing.id;
|
|
63
|
+
},
|
|
64
|
+
get(routeId, uidValidity, uid) {
|
|
65
|
+
const r = db
|
|
66
|
+
.prepare(`SELECT * FROM email_seen WHERE route_id = ? AND uid_validity = ? AND uid = ?`)
|
|
67
|
+
.get(routeId, uidValidity, uid);
|
|
68
|
+
return r ? rowToTs(r) : null;
|
|
69
|
+
},
|
|
70
|
+
list(routeId, limit = 100) {
|
|
71
|
+
const rows = db
|
|
72
|
+
.prepare(`SELECT * FROM email_seen WHERE route_id = ?
|
|
73
|
+
ORDER BY received_at DESC LIMIT ?`)
|
|
74
|
+
.all(routeId, limit);
|
|
75
|
+
return rows.map(rowToTs);
|
|
76
|
+
},
|
|
77
|
+
sweep(retentionDays, now) {
|
|
78
|
+
const cutoff = (now ?? Date.now()) - retentionDays * 24 * 60 * 60 * 1000;
|
|
79
|
+
const r = db
|
|
80
|
+
.prepare(`DELETE FROM email_seen WHERE received_at < ?`)
|
|
81
|
+
.run(cutoff);
|
|
82
|
+
return { deleted: r.changes };
|
|
83
|
+
},
|
|
84
|
+
countForRoute(routeId) {
|
|
85
|
+
const r = db
|
|
86
|
+
.prepare(`SELECT COUNT(*) AS c FROM email_seen WHERE route_id = ?`)
|
|
87
|
+
.get(routeId);
|
|
88
|
+
return r.c;
|
|
89
|
+
},
|
|
90
|
+
isMessageIdSeen(messageId) {
|
|
91
|
+
if (!messageId)
|
|
92
|
+
return false;
|
|
93
|
+
const r = db
|
|
94
|
+
.prepare(`SELECT 1 FROM email_seen WHERE message_id = ? LIMIT 1`)
|
|
95
|
+
.get(messageId);
|
|
96
|
+
return r !== undefined;
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/triggers/email/emailSpec.ts — v4.5 Phase 4a.
|
|
10
|
+
*
|
|
11
|
+
* Typed spec for source='email' triggers, stored in
|
|
12
|
+
* triggers.spec_json. Parse + validation + defaults.
|
|
13
|
+
*
|
|
14
|
+
* Required:
|
|
15
|
+
* - name
|
|
16
|
+
* - imap.{host, user, password}
|
|
17
|
+
* - allowedSenders (at least one entry) — Q-P4-1 (a) reject-all
|
|
18
|
+
* default; users opt in to specific senders. Glob-style patterns
|
|
19
|
+
* supported (see allowlist.ts).
|
|
20
|
+
*
|
|
21
|
+
* Password handling: stored RAW in spec_json. daemon.db is chmod 600
|
|
22
|
+
* on POSIX (see db/connection.ts since Phase 3). Encryption-at-rest
|
|
23
|
+
* deferred to v4.6+ per Q-P4-4 (a). aiden trigger show deliberately
|
|
24
|
+
* omits the password.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.DEFAULT_IMAP = exports.DEFAULT_EMAIL_SPEC = void 0;
|
|
28
|
+
exports.parseEmailSpec = parseEmailSpec;
|
|
29
|
+
exports.DEFAULT_EMAIL_SPEC = {
|
|
30
|
+
mailbox: 'INBOX',
|
|
31
|
+
pollIntervalMs: 15000,
|
|
32
|
+
maxBodyBytes: 1048576,
|
|
33
|
+
deliverOnly: false,
|
|
34
|
+
attachmentPolicy: 'skip',
|
|
35
|
+
};
|
|
36
|
+
exports.DEFAULT_IMAP = {
|
|
37
|
+
port: 993,
|
|
38
|
+
tls: true,
|
|
39
|
+
authTimeoutMs: 10000,
|
|
40
|
+
};
|
|
41
|
+
function parseEmailSpec(raw) {
|
|
42
|
+
const obj = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
43
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
44
|
+
throw new Error('EmailSpec: input must be an object');
|
|
45
|
+
}
|
|
46
|
+
const o = obj;
|
|
47
|
+
const name = typeof o.name === 'string' && o.name.length > 0 ? o.name : '';
|
|
48
|
+
if (!name)
|
|
49
|
+
throw new Error('EmailSpec: name required');
|
|
50
|
+
const imapRaw = (o.imap && typeof o.imap === 'object') ? o.imap : {};
|
|
51
|
+
const imap = {
|
|
52
|
+
host: requireStr(imapRaw.host, 'imap.host'),
|
|
53
|
+
user: requireStr(imapRaw.user, 'imap.user'),
|
|
54
|
+
password: requireStr(imapRaw.password, 'imap.password'),
|
|
55
|
+
port: sanitizeNum(imapRaw.port, exports.DEFAULT_IMAP.port, 1),
|
|
56
|
+
tls: typeof imapRaw.tls === 'boolean' ? imapRaw.tls : exports.DEFAULT_IMAP.tls,
|
|
57
|
+
authTimeoutMs: sanitizeNum(imapRaw.authTimeoutMs, exports.DEFAULT_IMAP.authTimeoutMs, 1000),
|
|
58
|
+
};
|
|
59
|
+
const allowedSenders = Array.isArray(o.allowedSenders)
|
|
60
|
+
? o.allowedSenders.filter((s) => typeof s === 'string' && s.trim().length > 0)
|
|
61
|
+
: [];
|
|
62
|
+
if (allowedSenders.length === 0) {
|
|
63
|
+
throw new Error('EmailSpec: allowedSenders required (at least one --allow-sender entry). Use "*@your-domain.com" for a whole domain.');
|
|
64
|
+
}
|
|
65
|
+
const allowedSubjectPatterns = Array.isArray(o.allowedSubjectPatterns)
|
|
66
|
+
? o.allowedSubjectPatterns.filter((s) => typeof s === 'string')
|
|
67
|
+
: undefined;
|
|
68
|
+
if (allowedSubjectPatterns) {
|
|
69
|
+
// Compile-test each pattern at parse time so bad regexes surface early.
|
|
70
|
+
for (const p of allowedSubjectPatterns) {
|
|
71
|
+
try {
|
|
72
|
+
new RegExp(p);
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
throw new Error(`EmailSpec: allowedSubjectPatterns has invalid regex ${JSON.stringify(p)}: ${e instanceof Error ? e.message : String(e)}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
name,
|
|
81
|
+
imap,
|
|
82
|
+
mailbox: typeof o.mailbox === 'string' && o.mailbox.length > 0 ? o.mailbox : exports.DEFAULT_EMAIL_SPEC.mailbox,
|
|
83
|
+
pollIntervalMs: sanitizeNum(o.pollIntervalMs, exports.DEFAULT_EMAIL_SPEC.pollIntervalMs, 1000),
|
|
84
|
+
allowedSenders,
|
|
85
|
+
allowedSubjectPatterns,
|
|
86
|
+
maxBodyBytes: sanitizeNum(o.maxBodyBytes, exports.DEFAULT_EMAIL_SPEC.maxBodyBytes, 1024),
|
|
87
|
+
promptTemplate: typeof o.promptTemplate === 'string' ? o.promptTemplate : undefined,
|
|
88
|
+
deliverOnly: typeof o.deliverOnly === 'boolean' ? o.deliverOnly : exports.DEFAULT_EMAIL_SPEC.deliverOnly,
|
|
89
|
+
attachmentPolicy: sanitizeAttachmentPolicy(o.attachmentPolicy),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function requireStr(v, label) {
|
|
93
|
+
if (typeof v !== 'string' || v.length === 0) {
|
|
94
|
+
throw new Error(`EmailSpec: ${label} required (non-empty string)`);
|
|
95
|
+
}
|
|
96
|
+
return v;
|
|
97
|
+
}
|
|
98
|
+
function sanitizeNum(v, fallback, min) {
|
|
99
|
+
if (typeof v !== 'number' || !Number.isFinite(v) || v < min)
|
|
100
|
+
return fallback;
|
|
101
|
+
return v;
|
|
102
|
+
}
|
|
103
|
+
function sanitizeAttachmentPolicy(v) {
|
|
104
|
+
if (v === 'skip' || v === 'inline-text' || v === 'save-to-tmp')
|
|
105
|
+
return v;
|
|
106
|
+
return exports.DEFAULT_EMAIL_SPEC.attachmentPolicy;
|
|
107
|
+
}
|