primcli 1.75.0 → 3.0.0-preview.10
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 +131 -68
- package/bin/claude-pending-mail.mjs +1 -1
- package/bin/run.js +14 -0
- package/dist/api-command-D0KHkqhA.js +56084 -0
- package/dist/{cli-config-B80p0pLk.js → cli-config-Ms_0vNjq.js} +59 -16
- package/dist/{json-output-BzSBR-KX.js → json-output-ch756TXV.js} +14 -1
- package/dist/{listen-background-C0F9gNDD.js → listen-background-DyBYca28.js} +323 -36
- package/dist/{machine-session-Cq_nD5eF.js → machine-session-CI8tnGzz.js} +27 -14
- package/dist/{notify-session-CbY4FYUt.js → notify-session-DN8tSfAi.js} +335 -5
- package/dist/oclif/auto-signal-worker.js +3 -3
- package/dist/oclif/auto-signals.js +2 -2
- package/dist/oclif/command-not-found.js +62 -0
- package/dist/oclif/global-org-flag.js +76 -0
- package/dist/oclif/index.js +61145 -36959
- package/dist/oclif/listen-supervisor.js +1 -1
- package/dist/oclif/proxy-auto-detect.js +1 -1
- package/dist/oclif/receiver-heal.js +5 -4
- package/dist/oclif/root-signup-hint.js +17 -17
- package/dist/receiver.d.ts +95 -0
- package/dist/receiver.js +569 -0
- package/dist/{repeats-Dj_o6c-X-LyCkKYjp.js → repeats-Dj_o6c-X-CDaE-kdZ.js} +3981 -930
- package/dist/{scoped-chat-B0thCwME.js → scoped-chat-C4TDfZND.js} +1 -1
- package/dist/{signal-command-DCPsJBdz.js → signal-command-f5QeK45s.js} +9 -8
- package/man/primitive.1 +50 -0
- package/package.json +68 -11
- package/dist/api-command-rlS3eTlZ.js +0 -2938
package/dist/receiver.js
ADDED
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
// src/oclif/contact-rule-matcher.ts
|
|
2
|
+
const localPart = /^[a-z0-9!#$%&'+/=^_`{|}~-]+(?:\.[a-z0-9!#$%&'+/=^_`{|}~-]+)*$/;
|
|
3
|
+
const patternPart = /^[a-z0-9.!#$%&'+/=^_`{|}~*-]+$/;
|
|
4
|
+
const label = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
5
|
+
function domain(value) {
|
|
6
|
+
const labels = value.split(".");
|
|
7
|
+
return value.length <= 253 && labels.length >= 2 && labels.every((part) => label.test(part)) && /[a-z]/.test(labels.at(-1) ?? "");
|
|
8
|
+
}
|
|
9
|
+
function domainPattern(value) {
|
|
10
|
+
return domain(value.startsWith("*.") ? value.slice(2) : value);
|
|
11
|
+
}
|
|
12
|
+
function splitAddress(value) {
|
|
13
|
+
const parts = value.split("@");
|
|
14
|
+
const [local, host] = parts;
|
|
15
|
+
return parts.length === 2 && local && host ? [local, host] : null;
|
|
16
|
+
}
|
|
17
|
+
/** Restricted selectors are canonical data, never regular expressions. */
|
|
18
|
+
function canonicalContactSelector(input) {
|
|
19
|
+
const invalid = () => /* @__PURE__ */ new Error("Invalid contact approval selector.");
|
|
20
|
+
const trimmed = input.value.trim();
|
|
21
|
+
for (const character of trimmed) if (character.charCodeAt(0) > 127) throw invalid();
|
|
22
|
+
const value = trimmed.toLowerCase();
|
|
23
|
+
if (!value || value.length > 254) throw invalid();
|
|
24
|
+
if (input.kind === "domain") {
|
|
25
|
+
if (!domainPattern(value)) throw invalid();
|
|
26
|
+
return {
|
|
27
|
+
kind: input.kind,
|
|
28
|
+
value
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const parts = splitAddress(value);
|
|
32
|
+
if (!parts) throw invalid();
|
|
33
|
+
const [local, host] = parts;
|
|
34
|
+
if (local.length > 64 || local.startsWith(".") || local.endsWith(".") || local.includes("..")) throw invalid();
|
|
35
|
+
if (input.kind === "address") {
|
|
36
|
+
if (!localPart.test(local) || !domain(host)) throw invalid();
|
|
37
|
+
} else if (input.kind === "pattern") {
|
|
38
|
+
if (!patternPart.test(local) || local !== "*" && !localPart.test(local.endsWith("*") ? local.slice(0, -1) : local) || !domainPattern(host) || local.includes("*") && (!local.endsWith("*") || local.slice(0, -1).includes("*"))) throw invalid();
|
|
39
|
+
} else throw invalid();
|
|
40
|
+
return {
|
|
41
|
+
kind: input.kind,
|
|
42
|
+
value
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function matchesDomain(pattern, value) {
|
|
46
|
+
if (!pattern.startsWith("*.")) return pattern === value;
|
|
47
|
+
const suffix = pattern.slice(1);
|
|
48
|
+
return value.length > suffix.length && value.endsWith(suffix);
|
|
49
|
+
}
|
|
50
|
+
function matchesLocal(pattern, value) {
|
|
51
|
+
return pattern.endsWith("*") ? value.startsWith(pattern.slice(0, -1)) : value === pattern;
|
|
52
|
+
}
|
|
53
|
+
function matchesContactPattern(pattern, sender) {
|
|
54
|
+
return matchesContactSelector({
|
|
55
|
+
kind: "pattern",
|
|
56
|
+
value: pattern
|
|
57
|
+
}, sender);
|
|
58
|
+
}
|
|
59
|
+
/** Malformed sender identities never match; malformed stored rules fail closed. */
|
|
60
|
+
function matchesContactSelector(input, sender) {
|
|
61
|
+
const selector = canonicalContactSelector(input);
|
|
62
|
+
let address;
|
|
63
|
+
try {
|
|
64
|
+
address = canonicalContactSelector({
|
|
65
|
+
kind: "address",
|
|
66
|
+
value: sender
|
|
67
|
+
}).value;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if (selector.kind === "address") return selector.value === address;
|
|
72
|
+
const [local, host] = splitAddress(address) ?? [];
|
|
73
|
+
if (!local || !host) return false;
|
|
74
|
+
if (selector.kind === "domain") return matchesDomain(selector.value, host);
|
|
75
|
+
const [localPattern, hostPattern] = splitAddress(selector.value) ?? [];
|
|
76
|
+
return Boolean(localPattern && hostPattern && matchesDomain(hostPattern, host) && matchesLocal(localPattern, local));
|
|
77
|
+
}
|
|
78
|
+
// src/oclif/listen-state.ts
|
|
79
|
+
var ListenStateError = class extends Error {};
|
|
80
|
+
// src/oclif/shared-mail-files.ts
|
|
81
|
+
var SharedMailStateError = class extends ListenStateError {};
|
|
82
|
+
const invalidSharedMail = () => new SharedMailStateError("Shared mail state is invalid or inconsistent. Preserve it before retrying to avoid duplicate delivery.");
|
|
83
|
+
function mailId(value) {
|
|
84
|
+
if (typeof value !== "string" || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(value)) throw invalidSharedMail();
|
|
85
|
+
return value.toLowerCase();
|
|
86
|
+
}
|
|
87
|
+
function mailString(value, max = 256) {
|
|
88
|
+
if (typeof value !== "string" || !value || value.length > max || Array.from(value).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)) throw invalidSharedMail();
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
function mailAddress(value) {
|
|
92
|
+
const address = mailString(value, 254).toLowerCase();
|
|
93
|
+
if (!/^[^\s<>@]+@[^\s<>@]+\.[^\s<>@]+$/.test(address)) throw invalidSharedMail();
|
|
94
|
+
return address;
|
|
95
|
+
}
|
|
96
|
+
function mailTime(value) {
|
|
97
|
+
const time = mailString(value, 40);
|
|
98
|
+
if (!Number.isFinite(Date.parse(time))) throw invalidSharedMail();
|
|
99
|
+
return new Date(time).toISOString();
|
|
100
|
+
}
|
|
101
|
+
// src/oclif/contact-policy.ts
|
|
102
|
+
function record(value) {
|
|
103
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid contact policy.");
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
function fingerprint(value) {
|
|
107
|
+
if (typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)) throw new Error("Invalid contact policy version.");
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
function document(value, organization) {
|
|
111
|
+
const row = record(value);
|
|
112
|
+
if (!Array.isArray(row.rules) || row.rules.length > 100 || !(typeof row.allow_contact_requests === "boolean" || !organization && row.allow_contact_requests === null)) throw new Error("Invalid contact policy document.");
|
|
113
|
+
const seen = /* @__PURE__ */ new Set();
|
|
114
|
+
const rules = row.rules.map((value) => {
|
|
115
|
+
const rule = record(value);
|
|
116
|
+
if (typeof rule.pattern !== "string" || rule.effect !== "allow" && rule.effect !== "silence") throw new Error("Invalid contact policy rule.");
|
|
117
|
+
const pattern = canonicalContactSelector({
|
|
118
|
+
kind: "pattern",
|
|
119
|
+
value: rule.pattern
|
|
120
|
+
}).value;
|
|
121
|
+
const key = `${pattern}:${rule.effect}`;
|
|
122
|
+
if (pattern !== rule.pattern || seen.has(key)) throw new Error("Invalid contact policy rule.");
|
|
123
|
+
seen.add(key);
|
|
124
|
+
if (rule.effect === "silence") {
|
|
125
|
+
if (rule.notify_since !== null || rule.notification_generation !== null) throw new Error("Invalid silence rule.");
|
|
126
|
+
return {
|
|
127
|
+
pattern,
|
|
128
|
+
effect: "silence",
|
|
129
|
+
notify_since: null,
|
|
130
|
+
notification_generation: null
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
pattern,
|
|
135
|
+
effect: "allow",
|
|
136
|
+
notify_since: mailTime(rule.notify_since),
|
|
137
|
+
notification_generation: mailId(rule.notification_generation)
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
const version = row.version === null ? null : mailId(row.version);
|
|
141
|
+
const updatedAt = row.updated_at === null ? null : mailTime(row.updated_at);
|
|
142
|
+
if (version === null !== (updatedAt === null)) throw new Error("Invalid contact policy document version.");
|
|
143
|
+
const requests = row.allow_contact_requests;
|
|
144
|
+
if (requests !== true && (row.contact_request_since !== null || row.contact_request_generation !== null)) throw new Error("Invalid contact request activation.");
|
|
145
|
+
if (version === null && (rules.length || requests !== (organization ? false : null))) throw new Error("Invalid missing contact policy document.");
|
|
146
|
+
return {
|
|
147
|
+
rules,
|
|
148
|
+
allow_contact_requests: requests,
|
|
149
|
+
contact_request_since: requests === true ? mailTime(row.contact_request_since) : null,
|
|
150
|
+
contact_request_generation: requests === true ? mailId(row.contact_request_generation) : null,
|
|
151
|
+
version,
|
|
152
|
+
updated_at: updatedAt
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function parseAgentContactPolicy(value, recipient) {
|
|
156
|
+
const row = record(value);
|
|
157
|
+
if (row.agent_address !== recipient || typeof row.allow_contact_requests !== "boolean") throw new Error("Contact policy recipient mismatch.");
|
|
158
|
+
const org = document(row.org_policy, true);
|
|
159
|
+
const agent = document(row.agent_policy, false);
|
|
160
|
+
if (row.allow_contact_requests !== (agent.allow_contact_requests ?? org.allow_contact_requests)) throw new Error("Inconsistent contact request policy.");
|
|
161
|
+
if (!row.allow_contact_requests && (row.contact_request_since !== null || row.contact_request_generation !== null)) throw new Error("Invalid effective contact request activation.");
|
|
162
|
+
return {
|
|
163
|
+
agent_address: recipient,
|
|
164
|
+
org_policy: org,
|
|
165
|
+
agent_policy: agent,
|
|
166
|
+
effective_version: fingerprint(row.effective_version),
|
|
167
|
+
effective_since: mailTime(row.effective_since),
|
|
168
|
+
allow_contact_requests: row.allow_contact_requests,
|
|
169
|
+
contact_request_since: row.allow_contact_requests ? mailTime(row.contact_request_since) : null,
|
|
170
|
+
contact_request_generation: row.allow_contact_requests ? fingerprint(row.contact_request_generation) : null
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
const latest = (left, right) => Date.parse(left) >= Date.parse(right) ? left : right;
|
|
174
|
+
const arrived = (received, since) => Date.parse(received) >= Date.parse(since);
|
|
175
|
+
/** Owner rules are unordered. Agent matches take precedence; silence wins ties. */
|
|
176
|
+
function evaluateContactPolicy(options) {
|
|
177
|
+
const { policy, sender, receivedAt, membership } = options;
|
|
178
|
+
if (membership?.notify === false) return {
|
|
179
|
+
kind: "silent",
|
|
180
|
+
source: "membership"
|
|
181
|
+
};
|
|
182
|
+
for (const source of ["agent", "org"]) {
|
|
183
|
+
const matches = policy[`${source}_policy`].rules.filter((rule) => matchesContactPattern(rule.pattern, sender));
|
|
184
|
+
if (!matches.length) continue;
|
|
185
|
+
if (matches.some((rule) => rule.effect === "silence")) return {
|
|
186
|
+
kind: "silent",
|
|
187
|
+
source
|
|
188
|
+
};
|
|
189
|
+
for (const rule of matches.sort((a, b) => a.pattern < b.pattern ? -1 : a.pattern > b.pattern ? 1 : 0)) {
|
|
190
|
+
if (!rule.notify_since || !rule.notification_generation) throw new Error("Missing rule activation.");
|
|
191
|
+
const since = latest(rule.notify_since, policy.effective_since);
|
|
192
|
+
if (arrived(receivedAt, since)) return {
|
|
193
|
+
kind: "allowed",
|
|
194
|
+
source,
|
|
195
|
+
pattern: rule.pattern,
|
|
196
|
+
generation: rule.notification_generation,
|
|
197
|
+
notifySince: since
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
kind: "silent",
|
|
202
|
+
source
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (membership?.notify === true) {
|
|
206
|
+
const since = membership.notifySince;
|
|
207
|
+
return arrived(receivedAt, since) ? {
|
|
208
|
+
kind: "allowed",
|
|
209
|
+
source: "membership",
|
|
210
|
+
generation: membership.generation,
|
|
211
|
+
notifySince: since
|
|
212
|
+
} : {
|
|
213
|
+
kind: "silent",
|
|
214
|
+
source: "membership"
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (options.contactRequests && policy.allow_contact_requests && policy.contact_request_since && policy.contact_request_generation) {
|
|
218
|
+
const since = latest(policy.contact_request_since, policy.effective_since);
|
|
219
|
+
if (arrived(receivedAt, since)) return {
|
|
220
|
+
kind: "request",
|
|
221
|
+
generation: policy.contact_request_generation,
|
|
222
|
+
notifySince: since
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
kind: "silent",
|
|
227
|
+
source: "default"
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
// src/oclif/notify-session-content.ts
|
|
231
|
+
var NotificationRetryError = class extends ListenStateError {};
|
|
232
|
+
const CONTACT_POLICY_RETRY_MIN_MS = 1e3;
|
|
233
|
+
const CONTACT_POLICY_RETRY_MAX_MS = 3e4;
|
|
234
|
+
var NetworkAdmissionPendingError = class extends NotificationRetryError {
|
|
235
|
+
constructor() {
|
|
236
|
+
super("Network mail admission is pending delivery proof.");
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
var ContactPolicyReadRetryError = class extends NotificationRetryError {
|
|
240
|
+
constructor() {
|
|
241
|
+
super("Contact notification policy is temporarily unavailable. Mail remains pending while permissions are refreshed.");
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
/** A network decision is trusted only when every field agrees with the others. */
|
|
245
|
+
function consistentNetworkAdmission(decision) {
|
|
246
|
+
const relation = decision.sender_relation;
|
|
247
|
+
return typeof decision.allowed === "boolean" && typeof decision.pending === "boolean" && typeof decision.member_policy_required === "boolean" && (relation === void 0 || decision.member_policy_required && (relation === "owner" || relation === "member")) && !(decision.member_policy_required && decision.allowed && !relation) && !(decision.pending && (decision.allowed || decision.allowed_since !== null)) && (decision.allowed ? typeof decision.allowed_since === "string" : decision.allowed_since === null);
|
|
248
|
+
}
|
|
249
|
+
const unavailable = () => new ListenStateError("Contact notification preferences are unavailable or invalid. No contact notification was submitted; restart after access is restored.");
|
|
250
|
+
const changed = () => new NotificationRetryError("Contact notification permission changed or expired before dispatch.");
|
|
251
|
+
/** Read one connected address's policy. Partial pages never authorize delivery. */
|
|
252
|
+
function createNotificationContactPolicy(options) {
|
|
253
|
+
const recipient = mailAddress(options.recipient);
|
|
254
|
+
const now = options.now ?? (() => performance.now());
|
|
255
|
+
let snapshot;
|
|
256
|
+
let retryAt = 0;
|
|
257
|
+
let retryDelay = CONTACT_POLICY_RETRY_MIN_MS;
|
|
258
|
+
function fresh(value) {
|
|
259
|
+
if (!value) return false;
|
|
260
|
+
const elapsed = now() - value.startedAt;
|
|
261
|
+
return elapsed >= 0 && elapsed < 3e4;
|
|
262
|
+
}
|
|
263
|
+
async function refreshOnce(signal) {
|
|
264
|
+
snapshot = void 0;
|
|
265
|
+
signal.throwIfAborted();
|
|
266
|
+
if (now() < retryAt) throw new ContactPolicyReadRetryError();
|
|
267
|
+
const startedAt = now();
|
|
268
|
+
const peers = /* @__PURE__ */ new Set();
|
|
269
|
+
let cursor;
|
|
270
|
+
try {
|
|
271
|
+
const next = {
|
|
272
|
+
startedAt,
|
|
273
|
+
senders: /* @__PURE__ */ new Map(),
|
|
274
|
+
policy: parseAgentContactPolicy(await options.readPolicy(signal), recipient)
|
|
275
|
+
};
|
|
276
|
+
do {
|
|
277
|
+
signal.throwIfAborted();
|
|
278
|
+
const page = await options.readPage(cursor, signal);
|
|
279
|
+
signal.throwIfAborted();
|
|
280
|
+
if (!fresh(next) || !Array.isArray(page.data) || page.data.length > 100) throw unavailable();
|
|
281
|
+
let previous = cursor;
|
|
282
|
+
for (const value of page.data) {
|
|
283
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw unavailable();
|
|
284
|
+
const row = value;
|
|
285
|
+
const peer = mailAddress(row.contact_address);
|
|
286
|
+
if (row.agent_address !== recipient || row.contact_address !== peer || previous !== void 0 && peer <= previous || peers.has(peer) || typeof row.notify !== "boolean") throw unavailable();
|
|
287
|
+
mailId(row.version);
|
|
288
|
+
peers.add(peer);
|
|
289
|
+
previous = peer;
|
|
290
|
+
if (row.notify) next.senders.set(peer, {
|
|
291
|
+
notify: true,
|
|
292
|
+
generation: mailId(row.notification_generation),
|
|
293
|
+
notifySince: mailTime(row.notify_since)
|
|
294
|
+
});
|
|
295
|
+
else if (row.notification_generation !== null || row.notify_since !== null) throw unavailable();
|
|
296
|
+
else next.senders.set(peer, { notify: false });
|
|
297
|
+
}
|
|
298
|
+
if (page.cursor === null) cursor = void 0;
|
|
299
|
+
else {
|
|
300
|
+
const following = mailAddress(page.cursor);
|
|
301
|
+
if (!page.data.length || following !== previous) throw unavailable();
|
|
302
|
+
cursor = following;
|
|
303
|
+
}
|
|
304
|
+
} while (cursor !== void 0);
|
|
305
|
+
if (!fresh(next)) throw unavailable();
|
|
306
|
+
snapshot = next;
|
|
307
|
+
retryAt = 0;
|
|
308
|
+
retryDelay = CONTACT_POLICY_RETRY_MIN_MS;
|
|
309
|
+
return next;
|
|
310
|
+
} catch (error) {
|
|
311
|
+
snapshot = void 0;
|
|
312
|
+
signal.throwIfAborted();
|
|
313
|
+
if (error instanceof ContactPolicyReadRetryError) {
|
|
314
|
+
retryAt = now() + retryDelay;
|
|
315
|
+
retryDelay = Math.min(retryDelay * 2, CONTACT_POLICY_RETRY_MAX_MS);
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
throw unavailable();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function admission(value, sender, receivedAt, solicited = false) {
|
|
322
|
+
const decision = evaluateContactPolicy({
|
|
323
|
+
policy: value.policy,
|
|
324
|
+
sender,
|
|
325
|
+
receivedAt,
|
|
326
|
+
membership: value.senders.get(sender),
|
|
327
|
+
contactRequests: options.contactRequests === true
|
|
328
|
+
});
|
|
329
|
+
if (decision.kind === "silent") {
|
|
330
|
+
if (!solicited || decision.source !== "default") return null;
|
|
331
|
+
return {
|
|
332
|
+
sender,
|
|
333
|
+
receivedAt,
|
|
334
|
+
generation: value.policy.effective_version,
|
|
335
|
+
notifySince: value.policy.effective_since,
|
|
336
|
+
kind: "response",
|
|
337
|
+
effectiveVersion: value.policy.effective_version
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
sender,
|
|
342
|
+
receivedAt,
|
|
343
|
+
generation: decision.generation,
|
|
344
|
+
notifySince: decision.notifySince,
|
|
345
|
+
kind: solicited ? "response" : decision.kind,
|
|
346
|
+
effectiveVersion: value.policy.effective_version
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
async function admissionWithNetwork(value, sender, receivedAt, signal, emailId, networkReader = options.readNetworkAdmission, solicited = false) {
|
|
350
|
+
const contact = admission(value, sender, receivedAt, solicited);
|
|
351
|
+
const decision = evaluateContactPolicy({
|
|
352
|
+
policy: value.policy,
|
|
353
|
+
sender,
|
|
354
|
+
receivedAt,
|
|
355
|
+
membership: value.senders.get(sender),
|
|
356
|
+
contactRequests: options.contactRequests === true
|
|
357
|
+
});
|
|
358
|
+
if (!networkReader) return contact;
|
|
359
|
+
if (!emailId) throw unavailable();
|
|
360
|
+
const network = await networkReader(emailId, sender, signal);
|
|
361
|
+
signal.throwIfAborted();
|
|
362
|
+
if (!consistentNetworkAdmission(network)) throw unavailable();
|
|
363
|
+
if (network.member_policy_required) {
|
|
364
|
+
if (network.pending) throw new NetworkAdmissionPendingError();
|
|
365
|
+
if (!network.allowed) return null;
|
|
366
|
+
if (decision.kind === "silent" && decision.source !== "default") return null;
|
|
367
|
+
return networkAdmission(value, sender, receivedAt, emailId, network, solicited);
|
|
368
|
+
}
|
|
369
|
+
const scopedContact = contact ? {
|
|
370
|
+
...contact,
|
|
371
|
+
emailId
|
|
372
|
+
} : null;
|
|
373
|
+
if (contact?.kind === "allowed" || contact?.kind === "response") return scopedContact;
|
|
374
|
+
if (decision.kind !== "request" && !(decision.kind === "silent" && decision.source === "default")) return scopedContact;
|
|
375
|
+
if (network.pending) throw new NetworkAdmissionPendingError();
|
|
376
|
+
if (!network.allowed) return scopedContact;
|
|
377
|
+
return networkAdmission(value, sender, receivedAt, emailId, network, solicited);
|
|
378
|
+
}
|
|
379
|
+
function networkAdmission(value, sender, receivedAt, emailId, network, solicited) {
|
|
380
|
+
const networkSince = network.allowed_since && mailTime(network.allowed_since);
|
|
381
|
+
if (!networkSince) throw unavailable();
|
|
382
|
+
const since = Date.parse(value.policy.effective_since) > Date.parse(networkSince) ? value.policy.effective_since : networkSince;
|
|
383
|
+
if (Date.parse(receivedAt) < Date.parse(since)) return null;
|
|
384
|
+
return {
|
|
385
|
+
sender,
|
|
386
|
+
emailId,
|
|
387
|
+
receivedAt,
|
|
388
|
+
generation: since,
|
|
389
|
+
notifySince: since,
|
|
390
|
+
kind: solicited ? "response" : "allowed",
|
|
391
|
+
effectiveVersion: value.policy.effective_version,
|
|
392
|
+
source: "network",
|
|
393
|
+
...network.sender_relation ? { senderRelation: network.sender_relation } : {}
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function permits(prior, current) {
|
|
397
|
+
return current !== null && current.emailId === prior.emailId && current.kind === prior.kind && current.source === prior.source && current.senderRelation === prior.senderRelation && current.generation === prior.generation && current.notifySince === prior.notifySince && current.effectiveVersion === prior.effectiveVersion;
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
refresh: refreshOnce,
|
|
401
|
+
members() {
|
|
402
|
+
if (!fresh(snapshot)) throw unavailable();
|
|
403
|
+
return snapshot.senders.keys();
|
|
404
|
+
},
|
|
405
|
+
async admit(sender, receivedAt, signal, emailId) {
|
|
406
|
+
signal.throwIfAborted();
|
|
407
|
+
let peer;
|
|
408
|
+
try {
|
|
409
|
+
peer = mailAddress(sender);
|
|
410
|
+
} catch {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
const received = mailTime(receivedAt);
|
|
414
|
+
const inboundId = emailId === void 0 ? void 0 : mailId(emailId);
|
|
415
|
+
const reader = options.readNetworkAdmission;
|
|
416
|
+
let networkResult;
|
|
417
|
+
const networkReader = reader ? (id, address, abort) => networkResult ??= reader(id, address, abort) : void 0;
|
|
418
|
+
const cached = fresh(snapshot);
|
|
419
|
+
let current = cached && snapshot ? snapshot : await refreshOnce(signal);
|
|
420
|
+
let allowed = await admissionWithNetwork(current, peer, received, signal, inboundId, networkReader);
|
|
421
|
+
if (cached && allowed?.kind !== "allowed") {
|
|
422
|
+
current = await refreshOnce(signal);
|
|
423
|
+
allowed = await admissionWithNetwork(current, peer, received, signal, inboundId, networkReader);
|
|
424
|
+
}
|
|
425
|
+
return allowed;
|
|
426
|
+
},
|
|
427
|
+
async recheck(prior, signal) {
|
|
428
|
+
const current = await refreshOnce(signal);
|
|
429
|
+
const candidate = await admissionWithNetwork(current, prior.sender, prior.receivedAt, signal, prior.emailId, options.readNetworkAdmission, prior.kind === "response");
|
|
430
|
+
if (!permits(prior, candidate)) throw changed();
|
|
431
|
+
return () => {
|
|
432
|
+
signal.throwIfAborted();
|
|
433
|
+
if (!fresh(snapshot) || snapshot !== current || !permits(prior, candidate)) throw changed();
|
|
434
|
+
};
|
|
435
|
+
},
|
|
436
|
+
async admitResponse(sender, receivedAt, signal, emailId) {
|
|
437
|
+
return admissionWithNetwork(await refreshOnce(signal), mailAddress(sender), mailTime(receivedAt), signal, emailId, options.readNetworkAdmission, true);
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
// src/receiver.ts
|
|
442
|
+
/**
|
|
443
|
+
* Supported API for receivers that wake a coding session on Primitive mail.
|
|
444
|
+
* Import it as `primitive/receiver`. See "Supported receiver API" in the
|
|
445
|
+
* package README.
|
|
446
|
+
*/
|
|
447
|
+
const RETRYABLE = {
|
|
448
|
+
policy_read_failed: true,
|
|
449
|
+
network_admission_pending: true,
|
|
450
|
+
permission_changed: true,
|
|
451
|
+
policy_unavailable: false
|
|
452
|
+
};
|
|
453
|
+
var WakePolicyError = class extends Error {
|
|
454
|
+
code;
|
|
455
|
+
/** Whether the same email may be admitted later without operator action. */
|
|
456
|
+
retryable;
|
|
457
|
+
constructor(code, options) {
|
|
458
|
+
super(`Wake policy: ${code}`, options);
|
|
459
|
+
this.name = "WakePolicyError";
|
|
460
|
+
this.code = code;
|
|
461
|
+
this.retryable = RETRYABLE[code];
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
function publicError(error, signal) {
|
|
465
|
+
if (signal.aborted) return signal.reason;
|
|
466
|
+
if (error instanceof WakePolicyError) return error;
|
|
467
|
+
if (error instanceof NetworkAdmissionPendingError) return new WakePolicyError("network_admission_pending");
|
|
468
|
+
if (error instanceof ContactPolicyReadRetryError) return new WakePolicyError("policy_read_failed");
|
|
469
|
+
if (error instanceof NotificationRetryError) return new WakePolicyError("permission_changed");
|
|
470
|
+
return new WakePolicyError("policy_unavailable", { cause: error });
|
|
471
|
+
}
|
|
472
|
+
function transient(read) {
|
|
473
|
+
return async (...args) => {
|
|
474
|
+
try {
|
|
475
|
+
return await read(...args);
|
|
476
|
+
} catch (error) {
|
|
477
|
+
if (error instanceof WakePolicyError && error.code === "policy_read_failed") throw new ContactPolicyReadRetryError();
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* The policy backs off its own reads but not the network read, which runs per
|
|
484
|
+
* email. Give that read the same backoff so a receiver that retries right away
|
|
485
|
+
* does not keep calling a failing server.
|
|
486
|
+
*/
|
|
487
|
+
function paced(read) {
|
|
488
|
+
let retryAt = 0;
|
|
489
|
+
let retryDelay = CONTACT_POLICY_RETRY_MIN_MS;
|
|
490
|
+
return async (...args) => {
|
|
491
|
+
if (performance.now() < retryAt) throw new ContactPolicyReadRetryError();
|
|
492
|
+
try {
|
|
493
|
+
const value = await read(...args);
|
|
494
|
+
retryAt = 0;
|
|
495
|
+
retryDelay = CONTACT_POLICY_RETRY_MIN_MS;
|
|
496
|
+
return value;
|
|
497
|
+
} catch (error) {
|
|
498
|
+
if (error instanceof ContactPolicyReadRetryError) {
|
|
499
|
+
retryAt = performance.now() + retryDelay;
|
|
500
|
+
retryDelay = Math.min(retryDelay * 2, CONTACT_POLICY_RETRY_MAX_MS);
|
|
501
|
+
}
|
|
502
|
+
throw error;
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function inbound(email) {
|
|
507
|
+
try {
|
|
508
|
+
return {
|
|
509
|
+
emailId: mailId(email.emailId),
|
|
510
|
+
receivedAt: mailTime(email.receivedAt)
|
|
511
|
+
};
|
|
512
|
+
} catch {
|
|
513
|
+
throw new TypeError("admit needs an email ID (UUID) and an ISO 8601 received time.");
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/** Create the contact admission policy for one connected address. */
|
|
517
|
+
function createWakePolicy(options) {
|
|
518
|
+
let recipient;
|
|
519
|
+
try {
|
|
520
|
+
recipient = mailAddress(options.recipient);
|
|
521
|
+
} catch {
|
|
522
|
+
throw new TypeError("createWakePolicy needs a recipient email address.");
|
|
523
|
+
}
|
|
524
|
+
const policy = createNotificationContactPolicy({
|
|
525
|
+
recipient,
|
|
526
|
+
contactRequests: options.contactRequests === true,
|
|
527
|
+
readPolicy: transient(options.readContactPolicy),
|
|
528
|
+
readPage: transient(options.readContactPage),
|
|
529
|
+
readNetworkAdmission: paced(transient(options.readNetworkAdmission))
|
|
530
|
+
});
|
|
531
|
+
const issued = /* @__PURE__ */ new WeakMap();
|
|
532
|
+
return {
|
|
533
|
+
async admit(email, signal) {
|
|
534
|
+
const { emailId, receivedAt } = inbound(email);
|
|
535
|
+
let admitted;
|
|
536
|
+
try {
|
|
537
|
+
admitted = await policy.admit(email.sender, receivedAt, signal, emailId);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
throw publicError(error, signal);
|
|
540
|
+
}
|
|
541
|
+
if (admitted === null || admitted.kind === "response") return { kind: "denied" };
|
|
542
|
+
const admission = Object.freeze({
|
|
543
|
+
kind: admitted.kind,
|
|
544
|
+
emailId,
|
|
545
|
+
sender: admitted.sender,
|
|
546
|
+
receivedAt
|
|
547
|
+
});
|
|
548
|
+
issued.set(admission, admitted);
|
|
549
|
+
return admission;
|
|
550
|
+
},
|
|
551
|
+
async recheck(admission, signal) {
|
|
552
|
+
const prior = issued.get(admission);
|
|
553
|
+
if (!prior) throw new TypeError("recheck takes an admission returned by this policy's admit.");
|
|
554
|
+
try {
|
|
555
|
+
const confirm = await policy.recheck(prior, signal);
|
|
556
|
+
return () => {
|
|
557
|
+
try {
|
|
558
|
+
confirm();
|
|
559
|
+
} catch (error) {
|
|
560
|
+
throw publicError(error, signal);
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
} catch (error) {
|
|
564
|
+
throw publicError(error, signal);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
export { WakePolicyError, createWakePolicy };
|