primcli 1.74.1 → 3.0.0-preview.1

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.
Files changed (31) hide show
  1. package/README.md +165 -72
  2. package/bin/claude-pending-mail.mjs +1 -1
  3. package/bin/run.js +14 -0
  4. package/dist/{api-command-fKP3NYa7.js → api-command-ctHBqTL5.js} +825 -95
  5. package/dist/{cli-config-C0Y_Ppol.js → cli-config-Ms_0vNjq.js} +66 -28
  6. package/dist/{client.gen-CaaMP86o.js → client.gen-6rZvmHfx.js} +7 -14
  7. package/dist/{json-output-Ce10AsSb.js → json-output-BzSBR-KX.js} +1 -2
  8. package/dist/{listen-background-cD6n8VYO.js → listen-background-C0F9gNDD.js} +2 -3
  9. package/dist/{machine-session-B6V5-grn.js → machine-session-Cp_eQ9t_.js} +164 -99
  10. package/dist/{notify-session-DJAb9SGm.js → notify-session-BN0JTPZ3.js} +341 -31
  11. package/dist/oclif/auto-signal-worker.js +5 -6
  12. package/dist/oclif/auto-signals.js +4 -5
  13. package/dist/oclif/global-org-flag.js +76 -0
  14. package/dist/oclif/help.js +127 -0
  15. package/dist/oclif/index.js +93239 -31954
  16. package/dist/oclif/listen-supervisor.js +1 -1
  17. package/dist/oclif/proxy-auto-detect.js +2 -3
  18. package/dist/oclif/receiver-heal.js +6 -6
  19. package/dist/oclif/root-signup-hint.js +3 -4
  20. package/dist/receiver.d.ts +95 -0
  21. package/dist/receiver.js +569 -0
  22. package/dist/{repeats-BL7ZC-_6-B_ESsWTH.js → repeats-Dj_o6c-X-Bf9G0xds.js} +4617 -1744
  23. package/dist/{scoped-chat-DAiOb622.js → scoped-chat-DZ42V3jq.js} +3 -5
  24. package/dist/{shared-mail-files-CZfm7E2k.js → shared-mail-files-BAQSGxxz.js} +2 -4
  25. package/dist/{shared-mail-watch-DyQB0MK0.js → shared-mail-watch-BAPMsFNv.js} +2 -3
  26. package/dist/{signal-command-Cte4mAL3.js → signal-command-CRUMUCNa.js} +19 -27
  27. package/dist/skills/primitive-connect/references/connect.md +3 -1
  28. package/dist/skills/primitive-connect/references/contacts.md +9 -4
  29. package/dist/skills/primitive-connect.json +4 -4
  30. package/man/primitive.1 +44 -3
  31. package/package.json +65 -15
@@ -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 };