@oxygen-agent/cli 1.1014.1465 → 1.1014.1598

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 (85) hide show
  1. package/README.md +1 -1
  2. package/dist/command-families/program.js +2 -0
  3. package/dist/command-manifest.js +12 -2
  4. package/dist/commands/budget.js +2 -2
  5. package/dist/commands/capabilities.js +3 -2
  6. package/dist/commands/columns.js +1 -1
  7. package/dist/commands/companies.js +3 -0
  8. package/dist/commands/domains.js +122 -5
  9. package/dist/commands/feedback.js +1 -1
  10. package/dist/commands/feeds.js +34 -17
  11. package/dist/commands/mailboxes.js +19 -9
  12. package/dist/commands/publishing.js +1 -1
  13. package/dist/commands/senders.js +2 -2
  14. package/dist/commands/sequences.js +75 -12
  15. package/dist/commands/subscriptions.d.ts +10 -0
  16. package/dist/commands/subscriptions.js +203 -0
  17. package/dist/commands/tags.js +4 -4
  18. package/dist/customer-operation-commands.js +17 -0
  19. package/dist/mailbox-fleet-output.d.ts +3 -0
  20. package/dist/mailbox-fleet-output.js +80 -0
  21. package/dist/native-chat-host.d.ts +2 -0
  22. package/dist/native-chat-host.js +6 -0
  23. package/dist/workspace-placement-commands.js +67 -2
  24. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +9 -0
  25. package/node_modules/@oxygen/shared/dist/capability-discovery.js +59 -11
  26. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +2 -2
  27. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +18 -0
  28. package/node_modules/@oxygen/shared/dist/contracts/guided-activation.js +4 -4
  29. package/node_modules/@oxygen/shared/dist/contracts/native-chat.d.ts +120 -0
  30. package/node_modules/@oxygen/shared/dist/contracts/native-chat.js +35 -0
  31. package/node_modules/@oxygen/shared/dist/contracts/sender-limits.d.ts +9 -0
  32. package/node_modules/@oxygen/shared/dist/contracts/sender-limits.js +6 -1
  33. package/node_modules/@oxygen/shared/dist/contracts/sequence-list.d.ts +5 -0
  34. package/node_modules/@oxygen/shared/dist/contracts/sequence-list.js +2 -1
  35. package/node_modules/@oxygen/shared/dist/contracts/signal-search-compatibility.d.ts +78 -0
  36. package/node_modules/@oxygen/shared/dist/contracts/signal-search-compatibility.js +32 -0
  37. package/node_modules/@oxygen/shared/dist/dnc-rules.js +14 -0
  38. package/node_modules/@oxygen/shared/dist/email-delivery.d.ts +150 -0
  39. package/node_modules/@oxygen/shared/dist/email-delivery.js +246 -0
  40. package/node_modules/@oxygen/shared/dist/email-dsn.d.ts +1 -0
  41. package/node_modules/@oxygen/shared/dist/email-dsn.js +51 -14
  42. package/node_modules/@oxygen/shared/dist/email-health-summary.d.ts +29 -0
  43. package/node_modules/@oxygen/shared/dist/email-health-summary.js +28 -0
  44. package/node_modules/@oxygen/shared/dist/email-subscriptions.d.ts +50 -0
  45. package/node_modules/@oxygen/shared/dist/email-subscriptions.js +80 -0
  46. package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.d.ts +28 -0
  47. package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.js +47 -0
  48. package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +4 -16
  49. package/node_modules/@oxygen/shared/dist/error-redaction.js +8 -17
  50. package/node_modules/@oxygen/shared/dist/guided-activation.d.ts +15 -1
  51. package/node_modules/@oxygen/shared/dist/guided-activation.js +3 -1
  52. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +2 -0
  53. package/node_modules/@oxygen/shared/dist/mailbox-standby-rollout.d.ts +5 -0
  54. package/node_modules/@oxygen/shared/dist/mailbox-standby-rollout.js +9 -0
  55. package/node_modules/@oxygen/shared/dist/mailbox-standby.d.ts +43 -0
  56. package/node_modules/@oxygen/shared/dist/mailbox-standby.js +52 -0
  57. package/node_modules/@oxygen/shared/dist/native-chat-discovery.d.ts +5 -0
  58. package/node_modules/@oxygen/shared/dist/native-chat-discovery.js +27 -0
  59. package/node_modules/@oxygen/shared/dist/native-chat.d.ts +124 -0
  60. package/node_modules/@oxygen/shared/dist/native-chat.js +1 -0
  61. package/node_modules/@oxygen/shared/dist/onboarding-plays.d.ts +14 -3
  62. package/node_modules/@oxygen/shared/dist/onboarding-plays.js +26 -13
  63. package/node_modules/@oxygen/shared/dist/onboarding-research.d.ts +11 -0
  64. package/node_modules/@oxygen/shared/dist/onboarding-research.js +8 -0
  65. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +6 -0
  66. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +6 -0
  67. package/node_modules/@oxygen/shared/dist/select-options.js +1 -0
  68. package/node_modules/@oxygen/shared/dist/sequence-failures.js +6 -5
  69. package/node_modules/@oxygen/shared/dist/sequences.d.ts +8 -0
  70. package/node_modules/@oxygen/shared/dist/sequences.js +44 -2
  71. package/node_modules/@oxygen/shared/dist/tags.d.ts +7 -3
  72. package/node_modules/@oxygen/shared/dist/tags.js +31 -6
  73. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  74. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  75. package/node_modules/@oxygen/shared/dist/workspace-placement-bulk.d.ts +80 -0
  76. package/node_modules/@oxygen/shared/dist/workspace-placement-bulk.js +68 -0
  77. package/node_modules/@oxygen/shared/dist/workspace-placement-view.d.ts +2 -1
  78. package/node_modules/@oxygen/shared/dist/workspace-placement.d.ts +11 -1
  79. package/node_modules/@oxygen/shared/dist/workspace-placement.js +15 -4
  80. package/node_modules/@oxygen/shared/dist/write-rollout.d.ts +11 -0
  81. package/node_modules/@oxygen/shared/dist/write-rollout.js +59 -0
  82. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +176 -2
  83. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.js +50 -1
  84. package/node_modules/@oxygen/workflows/dist/index.js +108 -3
  85. package/package.json +1 -1
@@ -80,7 +80,7 @@ function dsnHtmlText(html) {
80
80
  }
81
81
  return parts.join("");
82
82
  }
83
- function readDsnBody(input) {
83
+ export function readDeliveryStatusNotificationBody(input) {
84
84
  if (typeof input.bodyText === "string" && input.bodyText.trim())
85
85
  return input.bodyText;
86
86
  if (typeof input.bodyHtml !== "string")
@@ -117,14 +117,37 @@ const MICROSOFT_BODY = /(microsoft exchange|exchange server|office\s?365)/i;
117
117
  // Enhanced status code (RFC 3463): class.subject.detail, where subject/detail can
118
118
  // be multi-digit (Google emits 5.7.708). Kept separate from the bare 3-digit SMTP
119
119
  // reply so a caller can tell "550" from "5.7.1".
120
- const ENHANCED_CODE = /\b([2-5]\.\d{1,3}\.\d{1,3})\b/;
120
+ const ENHANCED_CODE = /\b[2-5]\.\d{1,3}\.\d{1,3}\b/;
121
121
  const SMTP_REPLY_CODE = /\b([2-5]\d{2})\b/;
122
+ // Reported status fields outrank prose and SMTP examples in quoted help text.
123
+ // Exchange's 5.4.14 loop report includes a 5.1.1 help article and generic
124
+ // "Recipient Unknown" instructions before its actual status/diagnostic.
125
+ const REPORTED_ENHANCED_CODES = [
126
+ /(?:^|\n)[ \t]*Status[ \t]*:[ \t]*([2-5]\.\d{1,3}\.\d{1,3})\b/im,
127
+ /(?:^|\n)[ \t]*Diagnostic-Code[ \t]*:[ \t]*smtp[ \t]*;[ \t]*(?:[2-5]\d{2}[ -]+)?([2-5]\.\d{1,3}\.\d{1,3})\b/im,
128
+ /(?:^|\n)[ \t]*(?:Status[ \t]+code[ \t]*:?|Error[ \t]*:)[ \t]*(?:[2-5]\d{2}[ -]+)?([2-5]\.\d{1,3}\.\d{1,3})\b/im,
129
+ /\b(?:the response(?: from the remote server)? was|remote server returned)[ \t]*:?[ \t]*['"]?(?:[2-5]\d{2}[ -]+)?([2-5]\.\d{1,3}\.\d{1,3})\b/i,
130
+ /\b[2-5]\d{2}[ -]+([2-5]\.\d{1,3}\.\d{1,3})\b/,
131
+ /(?:^|\n)[ \t]*([2-5]\.\d{1,3}\.\d{1,3})\b/,
132
+ ];
133
+ function readEnhancedStatusCode(haystack) {
134
+ for (const pattern of REPORTED_ENHANCED_CODES) {
135
+ const code = pattern.exec(haystack)?.[1];
136
+ if (code)
137
+ return code;
138
+ }
139
+ // Unscoped codes can retain a policy/quota/routing verdict, but cannot prove
140
+ // an invalid destination: that code may only be in a generic help article.
141
+ return haystack.match(/\b[2-5]\.\d{1,3}\.\d{1,3}\b/g)
142
+ ?.find(code => !NO_SUCH_USER_CODE.test(code)) ?? null;
143
+ }
122
144
  const REPUTATION_BLOCK = /\b5\.7\.\d{1,3}\b/;
123
145
  const MAILBOX_FULL_CODE = /\b[45]\.2\.2\b/;
124
- const NO_SUCH_USER_CODE = /\b5\.1\.\d{1,3}\b/;
146
+ // 5.1.0 is a generic address-status failure, not proof the recipient is invalid.
147
+ const NO_SUCH_USER_CODE = /\b5\.1\.(?:[123]|10)\b/;
125
148
  const TRANSIENT_CODE = /(\b4\.\d{1,3}\.\d{1,3}\b|\b4\d{2}\b)/;
126
149
  const MAILBOX_FULL_TEXT = /(mailbox (?:is )?full|over quota|quota exceeded|insufficient storage)/i;
127
- const NO_SUCH_USER_TEXT = /(does ?n[o']?t exist|nosuchuser|no such user|user unknown|unknown user|address not found|recipient (?:address )?(?:not found|rejected)|invalid recipient)/i;
150
+ const NO_SUCH_USER_TEXT = /((?:address|account|recipient|user|mailbox|domain)[^\n.]{0,100}(?:does ?n[o']?t exist|could(?: not|n['’]t) be found)|nosuchuser|no such (?:user|recipient|mailbox)|(?:user|recipient) unknown|unknown (?:user|recipient)|(?:recipient |email )?address not found|recipient (?:address )?not found|invalid (?:recipient|email address)|no (?:valid )?MX(?: (?:record|records))?\b|\bNXDOMAIN\b)/i;
128
151
  const PROVIDER_REJECTED_TEXT = /(message rejected|has been blocked|\bblocked\b|\bbanned\b|\bspam\b|local policy violation|unsolicited|reputation)/i;
129
152
  const TRANSIENT_TEXT = /(temporar|will retry|try again later|rate ?limit|throttl)/i;
130
153
  // Greedy address capture: the character class already stops at whitespace and
@@ -155,21 +178,34 @@ function readProvider(from, haystack) {
155
178
  return "microsoft";
156
179
  return "unknown";
157
180
  }
158
- function readKind(subject, haystack, hasBody) {
181
+ function readKind(subject, haystack, hasBody, enhancedCode) {
159
182
  // Order is load-bearing. A Google "(Delay)" report can quote block-sounding
160
183
  // prose while the message is still queued, so the transient marker wins first;
161
184
  // an explicit enhanced code then beats keyword matching, because "blocked" and
162
185
  // "spam" appear in boilerplate that a 5.1.1 no-such-user report also carries.
163
186
  if (/\(delay\)/i.test(subject))
164
187
  return "delayed";
165
- if (REPUTATION_BLOCK.test(haystack))
166
- return "provider_rejected";
167
- if (MAILBOX_FULL_CODE.test(haystack) || MAILBOX_FULL_TEXT.test(haystack))
188
+ if (enhancedCode) {
189
+ if (REPUTATION_BLOCK.test(enhancedCode))
190
+ return "provider_rejected";
191
+ if (MAILBOX_FULL_CODE.test(enhancedCode))
192
+ return "mailbox_full";
193
+ if (NO_SUCH_USER_CODE.test(enhancedCode))
194
+ return "no_such_user";
195
+ if (enhancedCode.startsWith("4."))
196
+ return "delayed";
197
+ // An explicit generic/routing failure is not address-nonexistence evidence,
198
+ // even when the surrounding help text suggests checking the address.
199
+ return "other";
200
+ }
201
+ if (MAILBOX_FULL_TEXT.test(haystack))
168
202
  return "mailbox_full";
169
- if (NO_SUCH_USER_CODE.test(haystack) || NO_SUCH_USER_TEXT.test(haystack))
170
- return "no_such_user";
171
203
  if (PROVIDER_REJECTED_TEXT.test(haystack))
172
204
  return "provider_rejected";
205
+ if (ENHANCED_CODE.test(haystack))
206
+ return "other";
207
+ if (NO_SUCH_USER_TEXT.test(haystack))
208
+ return "no_such_user";
173
209
  if (TRANSIENT_CODE.test(haystack) || TRANSIENT_TEXT.test(haystack))
174
210
  return "delayed";
175
211
  // Nothing matched AND there was no body to match against: the sync stored the
@@ -186,13 +222,14 @@ function readKind(subject, haystack, hasBody) {
186
222
  export function classifyDeliveryStatusNotification(input) {
187
223
  const from = typeof input.fromAddress === "string" ? input.fromAddress : "";
188
224
  const subject = typeof input.subject === "string" ? input.subject : "";
189
- const body = readDsnBody(input);
225
+ const body = readDeliveryStatusNotificationBody(input);
190
226
  if (!DSN_FROM_PATTERN.test(from) && !DSN_SUBJECT_PATTERN.test(subject))
191
227
  return null;
192
228
  const haystack = `${subject}\n${body}`;
193
- const smtpCode = ENHANCED_CODE.exec(haystack)?.[1] ?? SMTP_REPLY_CODE.exec(haystack)?.[1] ?? null;
229
+ const enhancedCode = readEnhancedStatusCode(haystack);
230
+ const smtpCode = enhancedCode ?? (ENHANCED_CODE.test(haystack) ? null : SMTP_REPLY_CODE.exec(haystack)?.[1] ?? null);
194
231
  return {
195
- kind: readKind(subject, haystack, body.trim().length > 0),
232
+ kind: readKind(subject, haystack, body.trim().length > 0, enhancedCode),
196
233
  provider: readProvider(from, haystack),
197
234
  recipient: readRecipient(haystack),
198
235
  smtpCode,
@@ -206,7 +243,7 @@ export function classifyDeliveryStatusNotification(input) {
206
243
  export function isGoogleDeliveryStatusRejection(input) {
207
244
  if (classifyDeliveryStatusNotification(input)?.kind !== "provider_rejected")
208
245
  return false;
209
- const body = readDsnBody(input);
246
+ const body = readDeliveryStatusNotificationBody(input);
210
247
  const remote = /^Remote-MTA:\s*(?:dns|rfc822);\s*([a-z0-9.-]+)/im.exec(body)?.[1];
211
248
  if (remote) {
212
249
  return /(?:^|\.)(?:google|googlemail)\.com$/i.test(remote.replace(/\.$/, ""));
@@ -0,0 +1,29 @@
1
+ /** Last-known mailbox scores are advisory; coverage and evidence age qualify the fleet. */
2
+ export type EmailHealthState = "unknown" | "healthy" | "degraded" | "critical";
3
+ export declare const HEALTHY_SCORE_MIN = 80;
4
+ export declare const DEGRADED_SCORE_MIN = 50;
5
+ export declare function deriveEmailHealthState(score: number | null | undefined): EmailHealthState;
6
+ type HealthReading = {
7
+ healthScore: number | null;
8
+ healthState: EmailHealthState;
9
+ healthFreshness?: "current" | "stale" | "unmeasured";
10
+ };
11
+ export type FleetHealthSummary = {
12
+ /** Equal weight for each mailbox with a finite last-known score. Unknowns never become zeros. */
13
+ score: number | null;
14
+ scoreState: EmailHealthState;
15
+ fleetState: EmailHealthState;
16
+ worstMailboxState: EmailHealthState;
17
+ total: number;
18
+ scored: number;
19
+ current: number;
20
+ stale: number;
21
+ unverified: number;
22
+ unscored: number;
23
+ degraded: number;
24
+ critical: number;
25
+ coverage: "none" | "partial" | "complete";
26
+ freshness: "unmeasured" | "current" | "stale" | "mixed";
27
+ };
28
+ export declare function summarizeFleetHealth(readings: readonly HealthReading[]): FleetHealthSummary;
29
+ export {};
@@ -0,0 +1,28 @@
1
+ export const HEALTHY_SCORE_MIN = 80;
2
+ export const DEGRADED_SCORE_MIN = 50;
3
+ export function deriveEmailHealthState(score) {
4
+ if (score == null || !Number.isFinite(score))
5
+ return "unknown";
6
+ const value = Math.max(0, Math.min(100, score));
7
+ return value >= HEALTHY_SCORE_MIN ? "healthy" : value >= DEGRADED_SCORE_MIN ? "degraded" : "critical";
8
+ }
9
+ export function summarizeFleetHealth(readings) {
10
+ const scored = readings.filter(row => row.healthState !== "unknown" && row.healthScore !== null && Number.isFinite(row.healthScore));
11
+ const score = scored.length ? scored.reduce((sum, row) => sum + Math.max(0, Math.min(100, row.healthScore)), 0) / scored.length : null;
12
+ const current = scored.filter(row => row.healthFreshness === "current").length;
13
+ const stale = scored.filter(row => row.healthFreshness === "stale").length;
14
+ const unverified = scored.length - current - stale;
15
+ const scoreState = deriveEmailHealthState(score);
16
+ const critical = readings.filter(row => row.healthState === "critical").length;
17
+ const degraded = readings.filter(row => row.healthState === "degraded").length;
18
+ const coverage = scored.length === 0 ? "none" : scored.length === readings.length ? "complete" : "partial";
19
+ const freshness = scored.length === 0 ? "unmeasured" : current === scored.length ? "current" : stale === scored.length ? "stale" : "mixed";
20
+ // A strong recorded average cannot clear an unmeasured or stale fleet. Individual faults remain independent.
21
+ const fleetState = scoreState === "healthy" && (coverage !== "complete" || freshness !== "current") ? "unknown" : scoreState;
22
+ return {
23
+ score, scoreState, fleetState,
24
+ worstMailboxState: critical ? "critical" : degraded ? "degraded" : scored.length ? "healthy" : "unknown",
25
+ total: readings.length, scored: scored.length, current, stale, unverified,
26
+ unscored: readings.length - scored.length, degraded, critical, coverage, freshness,
27
+ };
28
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Subscription lists and consent for bulk email (sending-domain Sequences).
3
+ *
4
+ * A list is who agreed to hear from the workspace about one thing (the
5
+ * newsletter, product updates, onboarding). A subscription is one address on
6
+ * one list with a status; every change appends a consent event, which is the
7
+ * evidence a regulator or a mailbox provider can ask for. Sending-domain
8
+ * Sequences only send to `subscribed` addresses on their own list, on top of
9
+ * do-not-contact. Pure and browser-safe.
10
+ */
11
+ export declare const EMAIL_SUBSCRIPTION_LIST_KINDS: readonly ["marketing", "product_updates", "onboarding"];
12
+ export type EmailSubscriptionListKind = typeof EMAIL_SUBSCRIPTION_LIST_KINDS[number];
13
+ export declare const EMAIL_SUBSCRIPTION_STATUSES: readonly ["subscribed", "unsubscribed", "pending", "cleaned"];
14
+ export type EmailSubscriptionStatus = typeof EMAIL_SUBSCRIPTION_STATUSES[number];
15
+ /** Append-only consent event kinds. Status changes plus the provider facts that cause them. */
16
+ export declare const EMAIL_SUBSCRIPTION_EVENT_TYPES: readonly ["subscribed", "unsubscribed", "pending", "cleaned", "complaint", "hard_bounce"];
17
+ export type EmailSubscriptionEventType = typeof EMAIL_SUBSCRIPTION_EVENT_TYPES[number];
18
+ export declare const EMAIL_SUBSCRIPTION_LIST_KEY_RE: RegExp;
19
+ /** Most addresses one import call accepts. */
20
+ export declare const EMAIL_SUBSCRIPTION_IMPORT_LIMIT = 5000;
21
+ export declare class EmailSubscriptionInputError extends Error {
22
+ readonly code: string;
23
+ readonly field: string;
24
+ constructor(code: string, field: string, message: string);
25
+ }
26
+ export declare function isEmailSubscriptionStatus(value: unknown): value is EmailSubscriptionStatus;
27
+ export declare function isEmailSubscriptionListKind(value: unknown): value is EmailSubscriptionListKind;
28
+ /** The stored subscription key: trimmed, lowercased, one plausible address. */
29
+ export declare function normalizeSubscriptionEmail(value: unknown): string;
30
+ /**
31
+ * Consent evidence recorded with a subscription change. A change to
32
+ * `subscribed` must say where consent came from: the notice version and a hash
33
+ * of the exact notice text the person saw, or, for an import, the source and
34
+ * the person who vouches for it. IP addresses are stored hashed only.
35
+ */
36
+ export type EmailConsentEvidence = {
37
+ notice_version?: string | null;
38
+ /** sha256 hex of the exact consent notice text shown. */
39
+ notice_text_hash?: string | null;
40
+ form_url?: string | null;
41
+ /** sha256 hex of the subscriber's IP address; never the address itself. */
42
+ ip_hash?: string | null;
43
+ user_agent?: string | null;
44
+ /** Free-text provenance for imports, e.g. "Resend segment export 2026-10-20". */
45
+ basis?: string | null;
46
+ };
47
+ /** Normalize evidence; `requireForSubscribe` refuses a subscribe that names no basis. */
48
+ export declare function parseEmailConsentEvidence(value: unknown, options: {
49
+ requireForSubscribe: boolean;
50
+ }): EmailConsentEvidence;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Subscription lists and consent for bulk email (sending-domain Sequences).
3
+ *
4
+ * A list is who agreed to hear from the workspace about one thing (the
5
+ * newsletter, product updates, onboarding). A subscription is one address on
6
+ * one list with a status; every change appends a consent event, which is the
7
+ * evidence a regulator or a mailbox provider can ask for. Sending-domain
8
+ * Sequences only send to `subscribed` addresses on their own list, on top of
9
+ * do-not-contact. Pure and browser-safe.
10
+ */
11
+ export const EMAIL_SUBSCRIPTION_LIST_KINDS = ["marketing", "product_updates", "onboarding"];
12
+ export const EMAIL_SUBSCRIPTION_STATUSES = ["subscribed", "unsubscribed", "pending", "cleaned"];
13
+ /** Append-only consent event kinds. Status changes plus the provider facts that cause them. */
14
+ export const EMAIL_SUBSCRIPTION_EVENT_TYPES = [
15
+ "subscribed", "unsubscribed", "pending", "cleaned", "complaint", "hard_bounce",
16
+ ];
17
+ export const EMAIL_SUBSCRIPTION_LIST_KEY_RE = /^[a-z0-9][a-z0-9_-]{0,62}$/;
18
+ /** Most addresses one import call accepts. */
19
+ export const EMAIL_SUBSCRIPTION_IMPORT_LIMIT = 5000;
20
+ export class EmailSubscriptionInputError extends Error {
21
+ code;
22
+ field;
23
+ constructor(code, field, message) {
24
+ super(message);
25
+ this.code = code;
26
+ this.field = field;
27
+ this.name = "EmailSubscriptionInputError";
28
+ }
29
+ }
30
+ export function isEmailSubscriptionStatus(value) {
31
+ return typeof value === "string" && EMAIL_SUBSCRIPTION_STATUSES.includes(value);
32
+ }
33
+ export function isEmailSubscriptionListKind(value) {
34
+ return typeof value === "string" && EMAIL_SUBSCRIPTION_LIST_KINDS.includes(value);
35
+ }
36
+ /** The stored subscription key: trimmed, lowercased, one plausible address. */
37
+ export function normalizeSubscriptionEmail(value) {
38
+ const email = typeof value === "string" ? value.trim().toLowerCase() : "";
39
+ if (!email || email.length > 320 || !/^[^\s@<>"]+@[^\s@<>"]+\.[^\s@<>"]+$/.test(email)) {
40
+ throw new EmailSubscriptionInputError("invalid_subscription_email", "email", "Each subscription needs one valid email address.");
41
+ }
42
+ return email;
43
+ }
44
+ const SHA256_HEX = /^[a-f0-9]{64}$/;
45
+ function optionalText(value, field, max) {
46
+ if (value === undefined || value === null || value === "")
47
+ return null;
48
+ if (typeof value !== "string" || value.length > max) {
49
+ throw new EmailSubscriptionInputError("invalid_consent_evidence", `evidence.${field}`, `evidence.${field} must be text of at most ${max} characters.`);
50
+ }
51
+ return value.trim() || null;
52
+ }
53
+ function optionalHash(value, field) {
54
+ const text = optionalText(value, field, 64);
55
+ if (text === null)
56
+ return null;
57
+ if (!SHA256_HEX.test(text.toLowerCase())) {
58
+ throw new EmailSubscriptionInputError("invalid_consent_evidence", `evidence.${field}`, `evidence.${field} must be a sha256 hex digest.`);
59
+ }
60
+ return text.toLowerCase();
61
+ }
62
+ /** Normalize evidence; `requireForSubscribe` refuses a subscribe that names no basis. */
63
+ export function parseEmailConsentEvidence(value, options) {
64
+ const record = value && typeof value === "object" && !Array.isArray(value) ? value : {};
65
+ const evidence = {
66
+ notice_version: optionalText(record.notice_version, "notice_version", 80),
67
+ notice_text_hash: optionalHash(record.notice_text_hash, "notice_text_hash"),
68
+ form_url: optionalText(record.form_url, "form_url", 2048),
69
+ ip_hash: optionalHash(record.ip_hash, "ip_hash"),
70
+ user_agent: optionalText(record.user_agent, "user_agent", 512),
71
+ basis: optionalText(record.basis, "basis", 500),
72
+ };
73
+ if (options.requireForSubscribe) {
74
+ const notice = Boolean(evidence.notice_version && evidence.notice_text_hash);
75
+ if (!notice && !evidence.basis) {
76
+ throw new EmailSubscriptionInputError("consent_evidence_required", "evidence", "Subscribing someone needs consent evidence: notice_version with notice_text_hash, or a basis naming where consent came from.");
77
+ }
78
+ }
79
+ return evidence;
80
+ }
@@ -43,6 +43,34 @@ export declare function signUnsubscribeToken(payload: UnsubscribeTokenPayload, s
43
43
  * secret, tampered body, replayed garbage). Pure: the route maps null -> 401.
44
44
  */
45
45
  export declare function verifyUnsubscribeToken(token: string, secret: string): UnsubscribeTokenPayload | null;
46
+ /**
47
+ * v2: a subscription-list unsubscribe for bulk mail sent through a sending
48
+ * domain. `lst` names the list the recipient leaves; `scope` says whether the
49
+ * click leaves that one list or every list (and the do-not-contact ledger).
50
+ * v1 tokens are untouched: `verifyUnsubscribeToken` still accepts only v1.
51
+ */
52
+ export type UnsubscribeTokenPayloadV2 = {
53
+ v: 2;
54
+ org: string;
55
+ email: string;
56
+ /** Subscription list id the recipient leaves. */
57
+ lst: string;
58
+ /** `list` leaves only `lst`; `all` leaves every list and suppresses the address. */
59
+ scope: "list" | "all";
60
+ seq?: string;
61
+ enr?: string;
62
+ /** Sending-domain account id, for provenance. */
63
+ sd?: string;
64
+ iat: number;
65
+ };
66
+ export type AnyUnsubscribeTokenPayload = UnsubscribeTokenPayload | UnsubscribeTokenPayloadV2;
67
+ /** Sign a v2 (list-scoped) token. Same wire format and secret as v1. */
68
+ export declare function signUnsubscribeTokenV2(payload: UnsubscribeTokenPayloadV2, secret: string): string;
69
+ /**
70
+ * Verify a v1 OR v2 token. The one-click route uses this and branches on `v`;
71
+ * v1 callers keep `verifyUnsubscribeToken`, whose contract is unchanged.
72
+ */
73
+ export declare function verifyAnyUnsubscribeToken(token: string, secret: string): AnyUnsubscribeTokenPayload | null;
46
74
  /**
47
75
  * The HMAC signing secret for unsubscribe tokens, or null when unset. The send
48
76
  * path treats null as "omit the header" (a send with no one-click link still
@@ -49,6 +49,53 @@ export function verifyUnsubscribeToken(token, secret) {
49
49
  return null;
50
50
  return parseTokenPayload(body);
51
51
  }
52
+ /** Sign a v2 (list-scoped) token. Same wire format and secret as v1. */
53
+ export function signUnsubscribeTokenV2(payload, secret) {
54
+ const normalized = { ...payload, email: payload.email.trim().toLowerCase() };
55
+ return signPayload(normalized, secret);
56
+ }
57
+ function parseTokenPayloadV2(body) {
58
+ let parsed;
59
+ try {
60
+ parsed = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ if (!parsed || typeof parsed !== "object")
66
+ return null;
67
+ const candidate = parsed;
68
+ if (candidate.v !== 2)
69
+ return null;
70
+ const org = typeof candidate.org === "string" ? candidate.org.trim() : "";
71
+ const email = typeof candidate.email === "string" ? candidate.email.trim().toLowerCase() : "";
72
+ const lst = typeof candidate.lst === "string" ? candidate.lst.trim() : "";
73
+ if (!org || !email || !email.includes("@") || !lst)
74
+ return null;
75
+ if (candidate.scope !== "list" && candidate.scope !== "all")
76
+ return null;
77
+ return {
78
+ v: 2,
79
+ org,
80
+ email,
81
+ lst,
82
+ scope: candidate.scope,
83
+ ...(typeof candidate.seq === "string" && candidate.seq ? { seq: candidate.seq } : {}),
84
+ ...(typeof candidate.enr === "string" && candidate.enr ? { enr: candidate.enr } : {}),
85
+ ...(typeof candidate.sd === "string" && candidate.sd ? { sd: candidate.sd } : {}),
86
+ iat: typeof candidate.iat === "number" && Number.isFinite(candidate.iat) ? candidate.iat : 0,
87
+ };
88
+ }
89
+ /**
90
+ * Verify a v1 OR v2 token. The one-click route uses this and branches on `v`;
91
+ * v1 callers keep `verifyUnsubscribeToken`, whose contract is unchanged.
92
+ */
93
+ export function verifyAnyUnsubscribeToken(token, secret) {
94
+ const body = splitAndVerifyToken(token, secret);
95
+ if (body === null)
96
+ return null;
97
+ return parseTokenPayload(body) ?? parseTokenPayloadV2(body);
98
+ }
52
99
  /**
53
100
  * The HMAC signing secret for unsubscribe tokens, or null when unset. The send
54
101
  * path treats null as "omit the header" (a send with no one-click link still
@@ -78,22 +78,10 @@ export declare function readWorkflowStepFailure(error: unknown): WorkflowStepFai
78
78
  */
79
79
  export declare const AUTOMATION_ACTIONS_EXCEEDED_ERROR_CODE = "automation_actions_exceeded";
80
80
  export declare function isAutomationUsageQuotaError(error: unknown): boolean;
81
- /**
82
- * The credit-era sibling of the code above, and the one most callers actually want.
83
- *
84
- * `automation_actions_exceeded` can only be thrown when the plan carries a non-null
85
- * `monthlyAutomationActions`. Pricing Model 2.0 ([v1.330.0]) set that field to null on
86
- * every base plan, so on current plans the admission gate in captureAutomationActions()
87
- * is unreachable and assertAutomationActionsAvailable() refuses with `insufficient_credits`
88
- * instead — steps bill automationStepCredits() each out of the one credit pool.
89
- *
90
- * Both codes mean the same thing to a caller: "this automation work was REFUSED admission,
91
- * the customer must feel it, and the run must stop cleanly." Anything else is a metering
92
- * write that failed and must be stepped over. Matching only the quota code is how a
93
- * credit-exhausted cron ends up rethrowing out of the scheduler's enqueue loop instead of
94
- * taking the graceful pause path — abandoning the lease and every remaining due trigger
95
- * for that tenant's sweep.
96
- */
81
+ /** Stop dispatch on a known cap refusal or unavailable authoritative Workflow
82
+ * admission. Unavailable accounting can follow a lost commit acknowledgement;
83
+ * this predicate does not prove that the fee capture was not applied. Other
84
+ * legacy metering faults retain their best-effort behavior. */
97
85
  export declare const AUTOMATION_INSUFFICIENT_CREDITS_ERROR_CODE = "insufficient_credits";
98
86
  export declare function isAutomationAdmissionRefusal(error: unknown): boolean;
99
87
  /**
@@ -216,29 +216,20 @@ export function isAutomationUsageQuotaError(error) {
216
216
  && typeof error === "object"
217
217
  && error.code === AUTOMATION_ACTIONS_EXCEEDED_ERROR_CODE;
218
218
  }
219
- /**
220
- * The credit-era sibling of the code above, and the one most callers actually want.
221
- *
222
- * `automation_actions_exceeded` can only be thrown when the plan carries a non-null
223
- * `monthlyAutomationActions`. Pricing Model 2.0 ([v1.330.0]) set that field to null on
224
- * every base plan, so on current plans the admission gate in captureAutomationActions()
225
- * is unreachable and assertAutomationActionsAvailable() refuses with `insufficient_credits`
226
- * instead — steps bill automationStepCredits() each out of the one credit pool.
227
- *
228
- * Both codes mean the same thing to a caller: "this automation work was REFUSED admission,
229
- * the customer must feel it, and the run must stop cleanly." Anything else is a metering
230
- * write that failed and must be stepped over. Matching only the quota code is how a
231
- * credit-exhausted cron ends up rethrowing out of the scheduler's enqueue loop instead of
232
- * taking the graceful pause path — abandoning the lease and every remaining due trigger
233
- * for that tenant's sweep.
234
- */
219
+ /** Stop dispatch on a known cap refusal or unavailable authoritative Workflow
220
+ * admission. Unavailable accounting can follow a lost commit acknowledgement;
221
+ * this predicate does not prove that the fee capture was not applied. Other
222
+ * legacy metering faults retain their best-effort behavior. */
235
223
  export const AUTOMATION_INSUFFICIENT_CREDITS_ERROR_CODE = "insufficient_credits";
236
224
  export function isAutomationAdmissionRefusal(error) {
237
225
  if (!error || typeof error !== "object")
238
226
  return false;
239
227
  const code = error.code;
240
228
  return code === AUTOMATION_ACTIONS_EXCEEDED_ERROR_CODE
241
- || code === AUTOMATION_INSUFFICIENT_CREDITS_ERROR_CODE;
229
+ || code === AUTOMATION_INSUFFICIENT_CREDITS_ERROR_CODE
230
+ || code === "budget_policy_exceeded"
231
+ || code === "workspace_credit_cap_exceeded"
232
+ || code === "automation_admission_unavailable";
242
233
  }
243
234
  const MAX_CAUSE_DEPTH = 4;
244
235
  const MAX_CAUSE_MESSAGE_LENGTH = 200;
@@ -1,4 +1,16 @@
1
1
  export declare const GUIDED_ACTIVATION_PURPOSES: readonly ["lead_scan", "morning_briefing"];
2
+ /**
3
+ * The research that runs beside the onboarding chat: its own session and turn,
4
+ * so the person can talk to the Oxygent while it works. `results_turn_id` is
5
+ * the chat turn that reported it; `results_pending` means the chat was busy
6
+ * when it finished and the report goes out after that turn.
7
+ */
8
+ export type GuidedOnboardingResearch = {
9
+ session_id: string;
10
+ turn_id?: string;
11
+ results_turn_id?: string;
12
+ results_pending?: boolean;
13
+ };
2
14
  export type GuidedActivationPurpose = typeof GUIDED_ACTIVATION_PURPOSES[number];
3
15
  export type GuidedActivationRoutine = {
4
16
  activation_id: string;
@@ -54,13 +66,15 @@ export type GuidedActivation = {
54
66
  turn_id?: string;
55
67
  event_seq?: number;
56
68
  finished_at?: string;
69
+ /** Agent-led onboarding's background research (Philipp, 2026-10-20): its hidden session and turn, then the chat turn that reported it. */
70
+ research?: GuidedOnboardingResearch;
57
71
  } | null;
58
72
  last_fault: {
59
73
  code: string;
60
74
  at: string;
61
75
  } | null;
62
76
  };
63
- /** Unknown tiers and production-shaped data are always denied, independent of NODE_ENV. */
77
+ /** Production requires the explicit all-workspace rollout; the DEV pilot keeps its own switch. */
64
78
  export declare function guidedActivationEnabled(env?: Record<string, string | undefined>): boolean;
65
79
  export declare function guidedActivationWorkspaceAllowed(organizationId: string, env?: Record<string, string | undefined>): boolean;
66
80
  export declare function guidedActivationAvailableTo(env: Record<string, string | undefined>, viewer: {
@@ -3,8 +3,10 @@ import { isProductionData } from "./deploy-env.js";
3
3
  export const GUIDED_ACTIVATION_PURPOSES = ["lead_scan", "morning_briefing"];
4
4
  export const GUIDED_ACTIVATION_POINTERS = ["knowledge", "first_leads_table", "sequence", "sender", "oxygent", "direct_chat", "scan_routine", "briefing_routine"];
5
5
  export const GUIDED_ACTIVATION_REVISIONS = ["configuration", "sequence", "audience", "recipients", "sender", "copy", "knowledge", "schedule", "authority"];
6
- /** Unknown tiers and production-shaped data are always denied, independent of NODE_ENV. */
6
+ /** Production requires the explicit all-workspace rollout; the DEV pilot keeps its own switch. */
7
7
  export function guidedActivationEnabled(env = process.env) {
8
+ if (env.OXYGEN_DEPLOY_ENV === "selfhost-prod")
9
+ return env.OXYGEN_GUIDED_ACTIVATION_ENABLED === "all";
8
10
  return env.OXYGEN_DEPLOY_ENV === "selfhost-dev" && !isProductionData(env)
9
11
  && env.OXYGEN_GUIDED_ACTIVATION_ENABLED === "staff";
10
12
  }
@@ -46,6 +46,7 @@ export type LinkedInInviteStep = LinkedInStepBase & {
46
46
  };
47
47
  export type LinkedInWaitForConnectionStep = LinkedInStepBase & {
48
48
  kind: "wait_for_connection";
49
+ withdraw_on_timeout?: boolean;
49
50
  /** Days to wait for the invite to be accepted before acting on on_timeout. */
50
51
  timeout_days: number;
51
52
  /** What to do if the invite is never accepted. Defaults to "stop". */
@@ -53,6 +54,7 @@ export type LinkedInWaitForConnectionStep = LinkedInStepBase & {
53
54
  };
54
55
  export type LinkedInWaitStep = LinkedInStepBase & {
55
56
  kind: "wait";
57
+ working_days?: boolean;
56
58
  /** Fixed delay before the next step. Provide days and/or hours (>= 1 total). */
57
59
  days?: number;
58
60
  hours?: number;
@@ -0,0 +1,5 @@
1
+ export { mailboxStandbyWritesReady, assertMailboxStandbyWritesReady } from "./write-rollout.js";
2
+ export declare function assertMailboxStandbyMetadataWriteReady(metadata: Record<string, unknown> | null | undefined): void;
3
+ export declare function assertSequenceStandbyWriteReady(settings: {
4
+ auto_replace_mailboxes?: boolean;
5
+ } | null | undefined): void;
@@ -0,0 +1,9 @@
1
+ import { assertMailboxStandbyMetadataWritesReady, assertMailboxStandbyWritesReady } from "./write-rollout.js";
2
+ export { mailboxStandbyWritesReady, assertMailboxStandbyWritesReady } from "./write-rollout.js";
3
+ export function assertMailboxStandbyMetadataWriteReady(metadata) {
4
+ assertMailboxStandbyMetadataWritesReady(metadata);
5
+ }
6
+ export function assertSequenceStandbyWriteReady(settings) {
7
+ if (settings?.auto_replace_mailboxes === true)
8
+ assertMailboxStandbyWritesReady();
9
+ }
@@ -0,0 +1,43 @@
1
+ export type MailboxOutreachRole = "outreach" | "standby";
2
+ export type MailboxStandbyReadinessEvidence = {
3
+ observed_at: string;
4
+ confirmed_days: number;
5
+ warmup_running: boolean;
6
+ dns_ready: boolean;
7
+ auth_healthy: boolean;
8
+ warmup_provider: string;
9
+ warmup_vendor_ref: string;
10
+ ready_since?: string;
11
+ };
12
+ export declare const DEFAULT_MAILBOX_STANDBY_MINIMUM_DAYS = 14;
13
+ export declare const MAILBOX_STANDBY_EVIDENCE_MAX_AGE_MS: number;
14
+ export declare function mailboxOutreachRole(metadata: Record<string, unknown> | null | undefined): MailboxOutreachRole;
15
+ /** Campaign membership is authoritative DB evidence, never the promotion marker alone. */
16
+ export declare function isMailboxOutreachAllowed(input: {
17
+ metadata: Record<string, unknown> | null | undefined;
18
+ sequenceId?: string;
19
+ autoReplace?: boolean;
20
+ replacementSequenceIds?: string[];
21
+ }): boolean;
22
+ export declare function mailboxStandbyReadiness(input: {
23
+ metadata: Record<string, unknown>;
24
+ status: string;
25
+ warmupState: string;
26
+ warmupProvider: string | null;
27
+ warmupVendorRef: string | null;
28
+ senderProfileId: string | null;
29
+ senderProfileActive: boolean;
30
+ minimumDays: number;
31
+ now?: Date;
32
+ }): {
33
+ ready: boolean;
34
+ confirmed_days: number;
35
+ minimum_days: number;
36
+ blockers: string[];
37
+ };
38
+ /** The latest promoted descendant; waiting records never create a sending identity. */
39
+ export declare function resolveMailboxStandbyReplacement(rows: readonly {
40
+ oldMailboxId: string;
41
+ newMailboxId: string | null;
42
+ status: string;
43
+ }[], original: string): string | null;
@@ -0,0 +1,52 @@
1
+ export const DEFAULT_MAILBOX_STANDBY_MINIMUM_DAYS = 14;
2
+ export const MAILBOX_STANDBY_EVIDENCE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
3
+ export function mailboxOutreachRole(metadata) {
4
+ return metadata?.outreach_role === "standby" ? "standby" : "outreach";
5
+ }
6
+ /** Campaign membership is authoritative DB evidence, never the promotion marker alone. */
7
+ export function isMailboxOutreachAllowed(input) {
8
+ if (mailboxOutreachRole(input.metadata) === "standby")
9
+ return false;
10
+ if (!Object.hasOwn(input.metadata ?? {}, "standby_promotion"))
11
+ return true;
12
+ return input.autoReplace === true && !!input.sequenceId && input.replacementSequenceIds?.includes(input.sequenceId) === true;
13
+ }
14
+ export function mailboxStandbyReadiness(input) {
15
+ const raw = input.metadata.standby_readiness;
16
+ const evidence = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
17
+ const blockers = [];
18
+ const observed = Date.parse(evidence.observed_at ?? "");
19
+ const age = (input.now ?? new Date()).getTime() - observed;
20
+ if (!Number.isFinite(observed) || age > MAILBOX_STANDBY_EVIDENCE_MAX_AGE_MS || age < -300_000)
21
+ blockers.push("readiness_evidence_stale_or_missing");
22
+ if (!evidence.warmup_provider || !evidence.warmup_vendor_ref || evidence.warmup_provider !== input.warmupProvider || evidence.warmup_vendor_ref !== input.warmupVendorRef)
23
+ blockers.push("warmup_seat_unverified");
24
+ const days = typeof evidence.confirmed_days === "number" && Number.isInteger(evidence.confirmed_days) && evidence.confirmed_days >= 0 ? evidence.confirmed_days : 0;
25
+ if (days < input.minimumDays)
26
+ blockers.push("warmup_days_remaining");
27
+ if (evidence.warmup_running !== true || !["warming", "active"].includes(input.warmupState))
28
+ blockers.push("warmup_not_running");
29
+ if (evidence.dns_ready !== true)
30
+ blockers.push("dns_not_ready");
31
+ if (evidence.auth_healthy !== true || input.metadata.oauth_grant_failure || input.metadata.mailbox_deleted_at)
32
+ blockers.push("authorization_unverified");
33
+ if (input.status !== "active" || input.metadata.auto_pause)
34
+ blockers.push("mailbox_not_active_or_safety_paused");
35
+ if (!input.senderProfileId || !input.senderProfileActive)
36
+ blockers.push("sender_profile_required");
37
+ return { ready: blockers.length === 0, confirmed_days: days, minimum_days: input.minimumDays, blockers };
38
+ }
39
+ /** The latest promoted descendant; waiting records never create a sending identity. */
40
+ export function resolveMailboxStandbyReplacement(rows, original) {
41
+ const visited = new Set([original]);
42
+ let current = original;
43
+ while (true) {
44
+ const next = rows.find(row => row.status === "promoted" && row.newMailboxId && row.oldMailboxId === current)?.newMailboxId;
45
+ if (!next)
46
+ return current === original ? null : current;
47
+ if (visited.has(next))
48
+ return null;
49
+ visited.add(next);
50
+ current = next;
51
+ }
52
+ }