primcli 1.75.0 → 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.
@@ -1,7 +1,7 @@
1
- import { F as agentProfileName, M as AGENT_PROFILE_ENV, P as agentProfileDirectory, z as loadConnectedAgentProfile } from "../cli-config-B80p0pLk.js";
1
+ import { N as agentProfileDirectory, P as agentProfileName, R as loadConnectedAgentProfile, j as AGENT_PROFILE_ENV } from "../cli-config-Ms_0vNjq.js";
2
2
  import { a as claimBackgroundListenRestart, n as backgroundListenRestartable, r as backgroundListenStatus } from "../listen-background-C0F9gNDD.js";
3
- import { l as SESSION_UUID, t as notificationScope } from "../notify-session-CbY4FYUt.js";
4
- import { xt as detectAgentRuntime } from "../api-command-rlS3eTlZ.js";
3
+ import { d as SESSION_UUID, t as notificationScope } from "../notify-session-BN0JTPZ3.js";
4
+ import { St as detectAgentRuntime } from "../api-command-ctHBqTL5.js";
5
5
  import { d as readMailJson } from "../shared-mail-files-BAQSGxxz.js";
6
6
  import { join } from "node:path";
7
7
  import { spawn } from "node:child_process";
@@ -1,4 +1,4 @@
1
- import { S as normalizeApiBaseUrl, c as resolveConfigEnvironment, i as loadCliConfig } from "../cli-config-B80p0pLk.js";
1
+ import { c as resolveConfigEnvironment, i as loadCliConfig, x as normalizeApiBaseUrl } from "../cli-config-Ms_0vNjq.js";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { homedir } from "node:os";
@@ -19,7 +19,7 @@ function readRootCredentials(configDir) {
19
19
  }
20
20
  try {
21
21
  const parsed = JSON.parse(raw);
22
- if (parsed.auth_method !== "oauth") return null;
22
+ if (parsed.auth_method !== "oauth" && parsed.auth_method !== "user_grant") return null;
23
23
  if (typeof parsed.access_token !== "string" || !parsed.access_token.trim()) return null;
24
24
  const apiBaseUrl = parsed.api_base_url ?? parsed.api_base_url_1;
25
25
  if (typeof apiBaseUrl !== "string" || !apiBaseUrl.trim()) return null;
@@ -0,0 +1,95 @@
1
+ /** One page of `listAgentContacts` (`primitive agent contacts list`). */
2
+ export type ContactPage = {
3
+ readonly data: readonly unknown[];
4
+ /** The cursor for the next page, or null on the last page. */
5
+ readonly cursor: string | null;
6
+ };
7
+ /**
8
+ * The `checkDefaultNetworkContactAdmission` result for one email
9
+ * (`primitive agent-networks check-default-network-contact-admission`).
10
+ */
11
+ export type NetworkAdmission = {
12
+ readonly allowed: boolean;
13
+ readonly allowed_since: string | null;
14
+ readonly pending: boolean;
15
+ readonly member_policy_required: boolean;
16
+ readonly sender_relation?: "owner" | "member" | undefined;
17
+ };
18
+ /**
19
+ * Reads the policy from the API as the connected address. A reader that hits
20
+ * a temporary failure, such as a rate limit, a 5xx response or a dropped
21
+ * connection, throws `new WakePolicyError("policy_read_failed")`. Any other
22
+ * error from a reader makes the policy unavailable.
23
+ */
24
+ export type WakePolicyOptions = {
25
+ /** The connected address that receives the mail. */
26
+ readonly recipient: string;
27
+ /**
28
+ * Admit first mail from unknown senders as `request` when the owner allows
29
+ * contact requests. Defaults to false.
30
+ */
31
+ readonly contactRequests?: boolean | undefined;
32
+ /** The `data` of `getAgentContactPolicy` for the recipient. */
33
+ readonly readContactPolicy: (signal: AbortSignal) => Promise<unknown>;
34
+ /** One page of the recipient's contacts, 100 rows at most. */
35
+ readonly readContactPage: (cursor: string | undefined, signal: AbortSignal) => Promise<ContactPage>;
36
+ /** The network admission decision for one exact email. */
37
+ readonly readNetworkAdmission: (emailId: string, sender: string, signal: AbortSignal) => Promise<NetworkAdmission>;
38
+ };
39
+ /** The facts of one received email, from the email detail. */
40
+ export type InboundEmail = {
41
+ readonly emailId: string;
42
+ /** The authenticated sender address. Check it before admission. */
43
+ readonly sender: string;
44
+ readonly receivedAt: string;
45
+ };
46
+ /**
47
+ * `allowed` may wake the session. `request` is a first-contact request from
48
+ * an unknown sender: show it as a request, never as a task.
49
+ */
50
+ export type WakeAdmission = (InboundEmail & {
51
+ readonly kind: "allowed";
52
+ }) | (InboundEmail & {
53
+ readonly kind: "request";
54
+ });
55
+ /** `denied` must not wake the session. */
56
+ export type WakeDecision = WakeAdmission | {
57
+ readonly kind: "denied";
58
+ };
59
+ /**
60
+ * Call synchronously immediately before you wake the session. It throws
61
+ * `WakePolicyError` with `permission_changed` when the permission it confirms
62
+ * was refreshed or is older than 30 seconds; recheck and try again.
63
+ */
64
+ export type ConfirmWake = () => void;
65
+ export type WakePolicy = {
66
+ /** Decide whether one email may wake the session. */
67
+ admit(email: InboundEmail, signal: AbortSignal): Promise<WakeDecision>;
68
+ /**
69
+ * Read the policy again for an admission from this policy's `admit`, right
70
+ * before dispatch. Throws `permission_changed` if the admission no longer
71
+ * holds.
72
+ */
73
+ recheck(admission: WakeAdmission, signal: AbortSignal): Promise<ConfirmWake>;
74
+ };
75
+ /**
76
+ * - `policy_read_failed`: a policy read failed temporarily. Retry later; the
77
+ * policy paces repeated reads itself.
78
+ * - `network_admission_pending`: the server has not settled network admission
79
+ * for this email. Retry in a few seconds.
80
+ * - `permission_changed`: permission changed or expired before dispatch.
81
+ * Admit the email again.
82
+ * - `policy_unavailable`: the policy is invalid or access is lost. Do not
83
+ * wake; retrying will not help until access is restored.
84
+ */
85
+ export type WakePolicyErrorCode = "policy_read_failed" | "network_admission_pending" | "permission_changed" | "policy_unavailable";
86
+ export declare class WakePolicyError extends Error {
87
+ readonly code: WakePolicyErrorCode;
88
+ /** Whether the same email may be admitted later without operator action. */
89
+ readonly retryable: boolean;
90
+ constructor(code: WakePolicyErrorCode, options?: {
91
+ cause?: unknown;
92
+ });
93
+ }
94
+ /** Create the contact admission policy for one connected address. */
95
+ export declare function createWakePolicy(options: WakePolicyOptions): WakePolicy;
@@ -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 };