@oxygen-agent/cli 1.1014.1600 → 1.1014.1763

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 (127) hide show
  1. package/README.md +1 -1
  2. package/dist/command-families/program.js +4 -0
  3. package/dist/command-manifest.js +18 -3
  4. package/dist/command-support.js +1 -1
  5. package/dist/commands/admin.js +82 -1
  6. package/dist/commands/billing.js +1 -1
  7. package/dist/commands/copilot.js +29 -7
  8. package/dist/commands/custom-integrations.js +13 -0
  9. package/dist/commands/domains.js +7 -0
  10. package/dist/commands/email-assets.d.ts +3 -0
  11. package/dist/commands/email-assets.js +78 -0
  12. package/dist/commands/email-templates.d.ts +3 -0
  13. package/dist/commands/email-templates.js +101 -0
  14. package/dist/commands/inbox.js +1 -1
  15. package/dist/commands/integrations.js +5 -1
  16. package/dist/commands/mailboxes.js +26 -6
  17. package/dist/commands/managed-inboxes.js +2 -2
  18. package/dist/commands/senders.js +4 -4
  19. package/dist/commands/sequences.js +47 -12
  20. package/dist/commands/subscriptions.d.ts +2 -4
  21. package/dist/commands/subscriptions.js +297 -51
  22. package/dist/credential-storage/store.d.ts +10 -0
  23. package/dist/credential-storage/store.js +282 -0
  24. package/dist/credentials.d.ts +1 -3
  25. package/dist/credentials.js +3 -275
  26. package/dist/mailbox-fleet-output.js +11 -2
  27. package/dist/sequence-start-at.d.ts +7 -0
  28. package/dist/sequence-start-at.js +21 -0
  29. package/dist/sequences-brand-kit-command.d.ts +28 -0
  30. package/dist/sequences-brand-kit-command.js +83 -0
  31. package/dist/sequences-content-commands.d.ts +28 -0
  32. package/dist/sequences-content-commands.js +176 -0
  33. package/dist/sequences-newsletter-commands.d.ts +6 -0
  34. package/dist/sequences-newsletter-commands.js +108 -0
  35. package/dist/sequences-test-send-command.d.ts +3 -0
  36. package/dist/sequences-test-send-command.js +47 -0
  37. package/dist/workspace-placement-commands.js +3 -2
  38. package/node_modules/@oxygen/formula/dist/evaluate.js +2 -4
  39. package/node_modules/@oxygen/formula/dist/expression.d.ts +7 -5
  40. package/node_modules/@oxygen/formula/dist/expression.js +26 -4
  41. package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +6 -0
  42. package/node_modules/@oxygen/formula/dist/formula-functions.js +55 -0
  43. package/node_modules/@oxygen/shared/dist/billing.d.ts +2 -2
  44. package/node_modules/@oxygen/shared/dist/billing.js +5 -7
  45. package/node_modules/@oxygen/shared/dist/bulk-email-pacing.d.ts +105 -0
  46. package/node_modules/@oxygen/shared/dist/bulk-email-pacing.js +125 -0
  47. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +29 -0
  48. package/node_modules/@oxygen/shared/dist/capability-discovery.js +236 -18
  49. package/node_modules/@oxygen/shared/dist/clay-migration-auth-state.d.ts +7 -1
  50. package/node_modules/@oxygen/shared/dist/clay-migration-auth-state.js +6 -5
  51. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +1 -0
  52. package/node_modules/@oxygen/shared/dist/contracts/clay-migration.d.ts +40 -1
  53. package/node_modules/@oxygen/shared/dist/contracts/clay-migration.js +60 -6
  54. package/node_modules/@oxygen/shared/dist/contracts/copilot-attachment-read.d.ts +24 -0
  55. package/node_modules/@oxygen/shared/dist/contracts/copilot-attachment-read.js +7 -0
  56. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +2 -1
  57. package/node_modules/@oxygen/shared/dist/contracts/teammates.js +6 -1
  58. package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.js +1 -1
  59. package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.d.ts +1 -1
  60. package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.js +2 -2
  61. package/node_modules/@oxygen/shared/dist/email-brand-kit.d.ts +108 -0
  62. package/node_modules/@oxygen/shared/dist/email-brand-kit.js +214 -0
  63. package/node_modules/@oxygen/shared/dist/email-classification-scope.d.ts +35 -0
  64. package/node_modules/@oxygen/shared/dist/email-classification-scope.js +40 -0
  65. package/node_modules/@oxygen/shared/dist/email-delivery.d.ts +10 -0
  66. package/node_modules/@oxygen/shared/dist/email-delivery.js +14 -0
  67. package/node_modules/@oxygen/shared/dist/email-documents.d.ts +61 -0
  68. package/node_modules/@oxygen/shared/dist/email-documents.js +143 -0
  69. package/node_modules/@oxygen/shared/dist/email-dsn.js +52 -10
  70. package/node_modules/@oxygen/shared/dist/email-html-sanitize.d.ts +23 -0
  71. package/node_modules/@oxygen/shared/dist/email-html-sanitize.js +96 -0
  72. package/node_modules/@oxygen/shared/dist/email-subscriptions.d.ts +63 -1
  73. package/node_modules/@oxygen/shared/dist/email-subscriptions.js +149 -1
  74. package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.d.ts +41 -2
  75. package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.js +50 -1
  76. package/node_modules/@oxygen/shared/dist/native-chat.d.ts +9 -0
  77. package/node_modules/@oxygen/shared/dist/native-chat.js +12 -1
  78. package/node_modules/@oxygen/shared/dist/newsletter-analytics.d.ts +90 -0
  79. package/node_modules/@oxygen/shared/dist/newsletter-analytics.js +28 -0
  80. package/node_modules/@oxygen/shared/dist/newsletter-test-send.d.ts +20 -0
  81. package/node_modules/@oxygen/shared/dist/newsletter-test-send.js +46 -0
  82. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +11 -0
  83. package/node_modules/@oxygen/shared/dist/object-storage.js +12 -0
  84. package/node_modules/@oxygen/shared/dist/onboarding-research.d.ts +9 -2
  85. package/node_modules/@oxygen/shared/dist/onboarding-research.js +9 -2
  86. package/node_modules/@oxygen/shared/dist/plan-band.d.ts +9 -0
  87. package/node_modules/@oxygen/shared/dist/plan-band.js +17 -0
  88. package/node_modules/@oxygen/shared/dist/plan-capabilities.d.ts +4 -2
  89. package/node_modules/@oxygen/shared/dist/plan-capabilities.js +12 -1
  90. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +2 -0
  91. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +3 -0
  92. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.d.ts +5 -0
  93. package/node_modules/@oxygen/shared/dist/pricing-snapshot.generated.js +5 -0
  94. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +29 -0
  95. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +29 -0
  96. package/node_modules/@oxygen/shared/dist/recipes.d.ts +2 -0
  97. package/node_modules/@oxygen/shared/dist/recipes.js +13 -0
  98. package/node_modules/@oxygen/shared/dist/sending-domain-faults.d.ts +21 -0
  99. package/node_modules/@oxygen/shared/dist/sending-domain-faults.js +53 -0
  100. package/node_modules/@oxygen/shared/dist/sending-domain-rollout.d.ts +30 -0
  101. package/node_modules/@oxygen/shared/dist/sending-domain-rollout.js +75 -0
  102. package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +10 -1
  103. package/node_modules/@oxygen/shared/dist/sequence-failures.js +68 -6
  104. package/node_modules/@oxygen/shared/dist/sequence-lifecycle.d.ts +34 -3
  105. package/node_modules/@oxygen/shared/dist/sequence-lifecycle.js +57 -4
  106. package/node_modules/@oxygen/shared/dist/sequence-template.d.ts +17 -0
  107. package/node_modules/@oxygen/shared/dist/sequence-template.js +0 -0
  108. package/node_modules/@oxygen/shared/dist/sequences.d.ts +41 -0
  109. package/node_modules/@oxygen/shared/dist/sequences.js +86 -4
  110. package/node_modules/@oxygen/shared/dist/ses-identity-policy.d.ts +19 -0
  111. package/node_modules/@oxygen/shared/dist/ses-identity-policy.js +54 -0
  112. package/node_modules/@oxygen/shared/dist/skill-play-routing.d.ts +22 -0
  113. package/node_modules/@oxygen/shared/dist/skill-play-routing.js +75 -0
  114. package/node_modules/@oxygen/shared/dist/skill-plays.generated.d.ts +5 -0
  115. package/node_modules/@oxygen/shared/dist/skill-plays.generated.js +269 -0
  116. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +7 -0
  117. package/node_modules/@oxygen/shared/dist/spend-safety.js +7 -0
  118. package/node_modules/@oxygen/shared/dist/teammates.js +2 -1
  119. package/node_modules/@oxygen/shared/dist/tool-call-arguments.d.ts +24 -0
  120. package/node_modules/@oxygen/shared/dist/tool-call-arguments.js +68 -0
  121. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +15 -1
  122. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  123. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  124. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +2 -2
  125. package/node_modules/@oxygen/shared/dist/workspace-placement.d.ts +3 -0
  126. package/node_modules/@oxygen/shared/dist/workspace-placement.js +11 -0
  127. package/package.json +2 -2
@@ -8,7 +8,27 @@
8
8
  * Sequences only send to `subscribed` addresses on their own list, on top of
9
9
  * do-not-contact. Pure and browser-safe.
10
10
  */
11
- export const EMAIL_SUBSCRIPTION_LIST_KINDS = ["marketing", "product_updates", "onboarding"];
11
+ export const EMAIL_SUBSCRIPTION_LIST_KINDS = ["marketing", "product_updates", "onboarding", "topic"];
12
+ /**
13
+ * Topics: a `topic` list is a narrower subject inside an audience list
14
+ * (`parent_list_id`). A newsletter on a topic mails only the topic's
15
+ * subscribers. `opt_out` topics are joined together with their audience list
16
+ * (the same consent evidence, source `topic_default`); `opt_in` topics only by
17
+ * an explicit choice. Public lists show on the recipient's preference page;
18
+ * private ones only to someone already subscribed to them.
19
+ */
20
+ export const EMAIL_SUBSCRIPTION_LIST_VISIBILITIES = ["public", "private"];
21
+ export const EMAIL_SUBSCRIPTION_DEFAULTS = ["opt_in", "opt_out"];
22
+ /** The consent source of a topic subscription made by joining its audience list. */
23
+ export const TOPIC_DEFAULT_SOURCE = "topic_default";
24
+ /** The consent source of a change the recipient made on the hosted preference page. */
25
+ export const PREFERENCE_CENTER_SOURCE = "preference_center";
26
+ export function isEmailSubscriptionListVisibility(value) {
27
+ return typeof value === "string" && EMAIL_SUBSCRIPTION_LIST_VISIBILITIES.includes(value);
28
+ }
29
+ export function isEmailSubscriptionDefault(value) {
30
+ return typeof value === "string" && EMAIL_SUBSCRIPTION_DEFAULTS.includes(value);
31
+ }
12
32
  export const EMAIL_SUBSCRIPTION_STATUSES = ["subscribed", "unsubscribed", "pending", "cleaned"];
13
33
  /** Append-only consent event kinds. Status changes plus the provider facts that cause them. */
14
34
  export const EMAIL_SUBSCRIPTION_EVENT_TYPES = [
@@ -78,3 +98,131 @@ export function parseEmailConsentEvidence(value, options) {
78
98
  }
79
99
  return evidence;
80
100
  }
101
+ /**
102
+ * Contacts of a list: one subscription read together with the Records person it
103
+ * names (`email_subscriptions.person_id`, a People record). Records owns the
104
+ * person (name and the workspace's own People fields, the contact
105
+ * "properties"); the list owns status and consent.
106
+ */
107
+ export const EMAIL_CONTACT_PROPERTY_TYPES = ["string", "number"];
108
+ /** Most contacts one list page returns. */
109
+ export const EMAIL_CONTACTS_PAGE_LIMIT = 200;
110
+ /**
111
+ * Most import entries per call that carry a name or properties: each one is a
112
+ * Records write (create or match the person by email), so these batches are
113
+ * smaller than an address-only import. The CLI and web split larger files.
114
+ */
115
+ export const EMAIL_CONTACT_IMPORT_WITH_FIELDS_LIMIT = 1000;
116
+ export function isEmailContactPropertyType(value) {
117
+ return typeof value === "string" && EMAIL_CONTACT_PROPERTY_TYPES.includes(value);
118
+ }
119
+ /** Header names an import reads as the contact itself (or as export bookkeeping), never as a property. */
120
+ const CONTACT_CSV_RESERVED = new Set([
121
+ "email", "email_address", "e-mail", "person_id", "first_name", "last_name", "full_name",
122
+ "status", "source", "subscribed_at", "unsubscribed_at", "consent_at", "consent_source", "consent_basis",
123
+ "consent_notice_version", "consent_form_url",
124
+ ]);
125
+ /** Parse CSV text into rows of cells, honouring quoted fields ("" escapes, commas and newlines inside quotes). */
126
+ export function parseCsvCells(text) {
127
+ const rows = [];
128
+ let row = [];
129
+ let cell = "";
130
+ let quoted = false;
131
+ const input = text.replace(/^/, "");
132
+ for (let index = 0; index < input.length; index += 1) {
133
+ const char = input[index];
134
+ if (quoted) {
135
+ if (char === "\"" && input[index + 1] === "\"") {
136
+ cell += "\"";
137
+ index += 1;
138
+ }
139
+ else if (char === "\"")
140
+ quoted = false;
141
+ else
142
+ cell += char;
143
+ }
144
+ else if (char === "\"")
145
+ quoted = true;
146
+ else if (char === ",") {
147
+ row.push(cell.trim());
148
+ cell = "";
149
+ }
150
+ else if (char === "\n" || char === "\r") {
151
+ if (char === "\r" && input[index + 1] === "\n")
152
+ index += 1;
153
+ row.push(cell.trim());
154
+ cell = "";
155
+ if (row.some((value) => value !== ""))
156
+ rows.push(row);
157
+ row = [];
158
+ }
159
+ else
160
+ cell += char;
161
+ }
162
+ row.push(cell.trim());
163
+ if (row.some((value) => value !== ""))
164
+ rows.push(row);
165
+ return rows;
166
+ }
167
+ /**
168
+ * Contacts from a CSV with an email column (the shape `subscriptions export`
169
+ * writes): first_name and last_name go to the person, every other non-reserved
170
+ * column is a candidate property, named in `propertyColumns` so a caller can
171
+ * keep only the workspace's real properties.
172
+ */
173
+ export function parseContactsCsv(text) {
174
+ const [headerRow = [], ...body] = parseCsvCells(text);
175
+ const header = headerRow.map((cell) => cell.toLowerCase());
176
+ const emailIndex = header.findIndex((cell) => cell === "email" || cell === "email_address" || cell === "e-mail");
177
+ if (emailIndex < 0) {
178
+ throw new EmailSubscriptionInputError("invalid_request", "file", "The CSV needs an email column in its header row.");
179
+ }
180
+ const at = (name) => header.indexOf(name);
181
+ const personIndex = at("person_id");
182
+ const firstIndex = at("first_name");
183
+ const lastIndex = at("last_name");
184
+ const propertyColumns = header.flatMap((cell, index) => (cell && !CONTACT_CSV_RESERVED.has(cell) ? [{ key: cell, index }] : []));
185
+ const entries = body.map((cells) => {
186
+ const properties = Object.fromEntries(propertyColumns.flatMap(({ key, index }) => (cells[index] ? [[key, cells[index]]] : [])));
187
+ const read = (index) => (index >= 0 && cells[index] ? cells[index] : undefined);
188
+ const personId = read(personIndex);
189
+ const firstName = read(firstIndex);
190
+ const lastName = read(lastIndex);
191
+ return {
192
+ email: cells[emailIndex] ?? "",
193
+ ...(personId ? { person_id: personId } : {}),
194
+ ...(firstName ? { first_name: firstName } : {}),
195
+ ...(lastName ? { last_name: lastName } : {}),
196
+ ...(Object.keys(properties).length > 0 ? { properties } : {}),
197
+ };
198
+ });
199
+ return { entries, propertyColumns: propertyColumns.map((column) => column.key) };
200
+ }
201
+ /** Keep only properties the workspace has; report the columns dropped. */
202
+ export function keepContactProperties(entries, known) {
203
+ const columns = new Set(entries.flatMap((entry) => Object.keys(entry.properties ?? {})));
204
+ const ignored = [...columns].filter((column) => !known.has(column)).sort();
205
+ if (ignored.length === 0)
206
+ return { entries, ignored };
207
+ return {
208
+ ignored,
209
+ entries: entries.map((entry) => {
210
+ if (!entry.properties)
211
+ return entry;
212
+ const { properties, ...rest } = entry;
213
+ const kept = Object.fromEntries(Object.entries(properties).filter(([key]) => known.has(key)));
214
+ return Object.keys(kept).length > 0 ? { ...rest, properties: kept } : rest;
215
+ }),
216
+ };
217
+ }
218
+ function importRowHasFields(entry) {
219
+ return Boolean(entry.first_name || entry.last_name || (entry.properties && Object.keys(entry.properties).length > 0));
220
+ }
221
+ /** Split an import into calls the import route accepts (smaller batches once any entry carries a name or properties). */
222
+ export function chunkContactImport(entries) {
223
+ const size = entries.some(importRowHasFields) ? EMAIL_CONTACT_IMPORT_WITH_FIELDS_LIMIT : EMAIL_SUBSCRIPTION_IMPORT_LIMIT;
224
+ const chunks = [];
225
+ for (let index = 0; index < entries.length; index += size)
226
+ chunks.push(entries.slice(index, index + size));
227
+ return chunks.length > 0 ? chunks : [[]];
228
+ }
@@ -55,8 +55,11 @@ export type UnsubscribeTokenPayloadV2 = {
55
55
  email: string;
56
56
  /** Subscription list id the recipient leaves. */
57
57
  lst: string;
58
- /** `list` leaves only `lst`; `all` leaves every list and suppresses the address. */
59
- scope: "list" | "all";
58
+ /**
59
+ * `list` leaves only `lst`; `all` leaves every list and suppresses the
60
+ * address; `test` (a newsletter test send) changes nothing and says so.
61
+ */
62
+ scope: "list" | "all" | "test";
60
63
  seq?: string;
61
64
  enr?: string;
62
65
  /** Sending-domain account id, for provenance. */
@@ -71,6 +74,42 @@ export declare function signUnsubscribeTokenV2(payload: UnsubscribeTokenPayloadV
71
74
  * v1 callers keep `verifyUnsubscribeToken`, whose contract is unchanged.
72
75
  */
73
76
  export declare function verifyAnyUnsubscribeToken(token: string, secret: string): AnyUnsubscribeTokenPayload | null;
77
+ /**
78
+ * v2 `preferences`: the link to the hosted preference page of bulk mail
79
+ * (`{{preferences_url}}`). Same wire format and secret as the unsubscribe
80
+ * tokens, but a separate scope that `verifyAnyUnsubscribeToken` refuses, so a
81
+ * preferences link can never perform a one-click unsubscribe. `test` marks the
82
+ * link of a newsletter test send: the page says so and changes nothing.
83
+ */
84
+ export type PreferencesTokenPayload = {
85
+ v: 2;
86
+ org: string;
87
+ email: string;
88
+ /** The list the email was sent from; the page shows every list the address may manage. */
89
+ lst: string;
90
+ scope: "preferences";
91
+ test?: true;
92
+ seq?: string;
93
+ sd?: string;
94
+ iat: number;
95
+ };
96
+ /** A preferences link stops opening the page a year after the email that carried it. */
97
+ export declare const PREFERENCES_TOKEN_MAX_AGE_MS: number;
98
+ export declare function signPreferencesToken(payload: PreferencesTokenPayload, secret: string): string;
99
+ /**
100
+ * Verify a preferences token: signature, shape and age. Returns null for any
101
+ * problem, including an unsubscribe token (wrong scope) and a link older than
102
+ * PREFERENCES_TOKEN_MAX_AGE_MS; `expired` tells the page which copy to show.
103
+ */
104
+ export declare function verifyPreferencesToken(token: string, secret: string, nowMs?: number): {
105
+ payload: PreferencesTokenPayload;
106
+ expired: false;
107
+ } | {
108
+ payload: null;
109
+ expired: boolean;
110
+ };
111
+ /** The hosted preference page: org id in the path (re-checked against the token), token in `token`. */
112
+ export declare function buildPreferencesUrl(orgId: string, token: string, baseUrl: string): string;
74
113
  /**
75
114
  * The HMAC signing secret for unsubscribe tokens, or null when unset. The send
76
115
  * path treats null as "omit the header" (a send with no one-click link still
@@ -72,7 +72,7 @@ function parseTokenPayloadV2(body) {
72
72
  const lst = typeof candidate.lst === "string" ? candidate.lst.trim() : "";
73
73
  if (!org || !email || !email.includes("@") || !lst)
74
74
  return null;
75
- if (candidate.scope !== "list" && candidate.scope !== "all")
75
+ if (candidate.scope !== "list" && candidate.scope !== "all" && candidate.scope !== "test")
76
76
  return null;
77
77
  return {
78
78
  v: 2,
@@ -96,6 +96,55 @@ export function verifyAnyUnsubscribeToken(token, secret) {
96
96
  return null;
97
97
  return parseTokenPayload(body) ?? parseTokenPayloadV2(body);
98
98
  }
99
+ /** A preferences link stops opening the page a year after the email that carried it. */
100
+ export const PREFERENCES_TOKEN_MAX_AGE_MS = 365 * 24 * 60 * 60 * 1000;
101
+ export function signPreferencesToken(payload, secret) {
102
+ const normalized = { ...payload, email: payload.email.trim().toLowerCase() };
103
+ return signPayload(normalized, secret);
104
+ }
105
+ /**
106
+ * Verify a preferences token: signature, shape and age. Returns null for any
107
+ * problem, including an unsubscribe token (wrong scope) and a link older than
108
+ * PREFERENCES_TOKEN_MAX_AGE_MS; `expired` tells the page which copy to show.
109
+ */
110
+ export function verifyPreferencesToken(token, secret, nowMs = Date.now()) {
111
+ const body = splitAndVerifyToken(token, secret);
112
+ if (body === null)
113
+ return { payload: null, expired: false };
114
+ let parsed;
115
+ try {
116
+ parsed = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
117
+ }
118
+ catch {
119
+ return { payload: null, expired: false };
120
+ }
121
+ if (!parsed || typeof parsed !== "object")
122
+ return { payload: null, expired: false };
123
+ const candidate = parsed;
124
+ const org = typeof candidate.org === "string" ? candidate.org.trim() : "";
125
+ const email = typeof candidate.email === "string" ? candidate.email.trim().toLowerCase() : "";
126
+ const lst = typeof candidate.lst === "string" ? candidate.lst.trim() : "";
127
+ if (candidate.v !== 2 || candidate.scope !== "preferences" || !org || !email.includes("@") || !lst) {
128
+ return { payload: null, expired: false };
129
+ }
130
+ const iat = typeof candidate.iat === "number" && Number.isFinite(candidate.iat) ? candidate.iat : 0;
131
+ if (nowMs - iat > PREFERENCES_TOKEN_MAX_AGE_MS)
132
+ return { payload: null, expired: true };
133
+ return {
134
+ expired: false,
135
+ payload: {
136
+ v: 2, org, email, lst, scope: "preferences",
137
+ ...(candidate.test === true ? { test: true } : {}),
138
+ ...(typeof candidate.seq === "string" && candidate.seq ? { seq: candidate.seq } : {}),
139
+ ...(typeof candidate.sd === "string" && candidate.sd ? { sd: candidate.sd } : {}),
140
+ iat,
141
+ },
142
+ };
143
+ }
144
+ /** The hosted preference page: org id in the path (re-checked against the token), token in `token`. */
145
+ export function buildPreferencesUrl(orgId, token, baseUrl) {
146
+ return `${baseUrl}/email/preferences/${encodeURIComponent(orgId)}?token=${token}`;
147
+ }
99
148
  /**
100
149
  * The HMAC signing secret for unsubscribe tokens, or null when unset. The send
101
150
  * path treats null as "omit the header" (a send with no one-click link still
@@ -57,6 +57,7 @@ export type ChatMessage = {
57
57
  id: string | null;
58
58
  name: string;
59
59
  kind: "customer" | "staff" | "agent" | "system";
60
+ image_url?: string | null;
60
61
  };
61
62
  visibility: "public" | "internal";
62
63
  source: "web" | "cli" | "mcp";
@@ -78,6 +79,7 @@ export type ChatChannel = {
78
79
  id: string;
79
80
  organization_id: string;
80
81
  name: string;
82
+ icon_url?: string | null;
81
83
  updated_at: string;
82
84
  unread_count: number;
83
85
  open_count: number;
@@ -89,6 +91,7 @@ export type ChatMember = {
89
91
  id: string;
90
92
  name: string;
91
93
  email: string | null;
94
+ image_url?: string | null;
92
95
  };
93
96
  export type ChatChannelView = {
94
97
  channel: ChatChannel;
@@ -96,6 +99,10 @@ export type ChatChannelView = {
96
99
  members: ChatMember[];
97
100
  cursor: string;
98
101
  };
102
+ export type ChatThreadsView = Omit<ChatChannelView, "channel"> & {
103
+ initialized: boolean;
104
+ channel: ChatChannel | null;
105
+ };
99
106
  export type ChatThreadView = {
100
107
  thread: ChatThread;
101
108
  messages: ChatMessage[];
@@ -122,3 +129,5 @@ export type ChatThreadPatch = {
122
129
  required_roles?: string[];
123
130
  };
124
131
  };
132
+ /** Display-only URLs; never fetch these server-side or expose credential-bearing URLs. */
133
+ export declare function nativeChatImageUrl(value: unknown): string | null;
@@ -1 +1,12 @@
1
- export {};
1
+ /** Display-only URLs; never fetch these server-side or expose credential-bearing URLs. */
2
+ export function nativeChatImageUrl(value) {
3
+ if (typeof value !== "string" || value.length > 2_048)
4
+ return null;
5
+ try {
6
+ const url = new URL(value.trim());
7
+ return ["https:", "http:"].includes(url.protocol) && !url.username && !url.password ? url.href : null;
8
+ }
9
+ catch {
10
+ return null;
11
+ }
12
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Newsletter analytics: what one sending-domain Sequence's bulk mail did, read
3
+ * from its SES ledger (`ox_runs.bulk_email_send_jobs` + `ox_sequencer.bulk_email_events`).
4
+ *
5
+ * Opens are estimates: Apple Mail Privacy Protection and corporate link scanners
6
+ * fetch the tracking pixel and links for the recipient, so every surface labels
7
+ * opens "estimated", and events SES flags as bots are never counted. Rates are
8
+ * fractions (0..1) over delivered mail, except the bounce rate, which is over
9
+ * sent mail (a bounced message was never delivered); null when the denominator is 0.
10
+ *
11
+ * Pure and browser-safe: the route, the web panel, the CLI and MCP share it.
12
+ */
13
+ /** A recipient's furthest outcome, most significant first when several apply. */
14
+ export declare const NEWSLETTER_RECIPIENT_STATUSES: readonly ["queued", "sent", "delivered", "opened", "clicked", "bounced", "complained", "unsubscribed", "suppressed", "failed"];
15
+ export type NewsletterRecipientStatus = typeof NEWSLETTER_RECIPIENT_STATUSES[number];
16
+ export declare const NEWSLETTER_TIMELINE_INTERVALS: readonly ["hour", "day"];
17
+ export type NewsletterTimelineInterval = typeof NEWSLETTER_TIMELINE_INTERVALS[number];
18
+ /** The timeline covers the first seven days after the first send. */
19
+ export declare const NEWSLETTER_TIMELINE_DAYS = 7;
20
+ export declare const NEWSLETTER_RECIPIENTS_MAX_LIMIT = 200;
21
+ export declare const NEWSLETTER_RECIPIENTS_DEFAULT_LIMIT = 50;
22
+ export declare const NEWSLETTER_LINKS_LIMIT = 50;
23
+ export type NewsletterLinkStats = {
24
+ link: string;
25
+ unique_clicks: number;
26
+ total_clicks: number;
27
+ };
28
+ export type NewsletterTimelineBucket = {
29
+ at: string;
30
+ delivered: number;
31
+ /** Recipients whose first (non-bot) open fell in this bucket. */
32
+ opened: number;
33
+ /** Recipients whose first (non-bot) click fell in this bucket. */
34
+ clicked: number;
35
+ };
36
+ export type NewsletterAnalytics = {
37
+ recipients_targeted: number;
38
+ sent: number;
39
+ delivered: number;
40
+ opens: {
41
+ unique: number;
42
+ total: number;
43
+ estimated: true;
44
+ };
45
+ clicks: {
46
+ unique: number;
47
+ total: number;
48
+ };
49
+ bounces: {
50
+ hard: number;
51
+ soft: number;
52
+ };
53
+ complaints: number;
54
+ unsubscribes: number;
55
+ suppressed: number;
56
+ rates: {
57
+ open_rate: number | null;
58
+ click_rate: number | null;
59
+ bounce_rate: number | null;
60
+ complaint_rate: number | null;
61
+ unsubscribe_rate: number | null;
62
+ };
63
+ links: NewsletterLinkStats[];
64
+ timeline: {
65
+ interval: NewsletterTimelineInterval;
66
+ start: string | null;
67
+ buckets: NewsletterTimelineBucket[];
68
+ };
69
+ first_sent_at: string | null;
70
+ last_event_at: string | null;
71
+ };
72
+ export type NewsletterRecipient = {
73
+ email: string;
74
+ status: NewsletterRecipientStatus;
75
+ enrollment_id: string;
76
+ sent_at: string | null;
77
+ opens: number;
78
+ clicks: number;
79
+ last_event_at: string | null;
80
+ };
81
+ export type NewsletterRecipientsPage = {
82
+ recipients: NewsletterRecipient[];
83
+ status: NewsletterRecipientStatus | null;
84
+ total: number;
85
+ limit: number;
86
+ offset: number;
87
+ next_offset: number | null;
88
+ };
89
+ export declare function isNewsletterRecipientStatus(value: unknown): value is NewsletterRecipientStatus;
90
+ export declare function isNewsletterTimelineInterval(value: unknown): value is NewsletterTimelineInterval;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Newsletter analytics: what one sending-domain Sequence's bulk mail did, read
3
+ * from its SES ledger (`ox_runs.bulk_email_send_jobs` + `ox_sequencer.bulk_email_events`).
4
+ *
5
+ * Opens are estimates: Apple Mail Privacy Protection and corporate link scanners
6
+ * fetch the tracking pixel and links for the recipient, so every surface labels
7
+ * opens "estimated", and events SES flags as bots are never counted. Rates are
8
+ * fractions (0..1) over delivered mail, except the bounce rate, which is over
9
+ * sent mail (a bounced message was never delivered); null when the denominator is 0.
10
+ *
11
+ * Pure and browser-safe: the route, the web panel, the CLI and MCP share it.
12
+ */
13
+ /** A recipient's furthest outcome, most significant first when several apply. */
14
+ export const NEWSLETTER_RECIPIENT_STATUSES = [
15
+ "queued", "sent", "delivered", "opened", "clicked", "bounced", "complained", "unsubscribed", "suppressed", "failed",
16
+ ];
17
+ export const NEWSLETTER_TIMELINE_INTERVALS = ["hour", "day"];
18
+ /** The timeline covers the first seven days after the first send. */
19
+ export const NEWSLETTER_TIMELINE_DAYS = 7;
20
+ export const NEWSLETTER_RECIPIENTS_MAX_LIMIT = 200;
21
+ export const NEWSLETTER_RECIPIENTS_DEFAULT_LIMIT = 50;
22
+ export const NEWSLETTER_LINKS_LIMIT = 50;
23
+ export function isNewsletterRecipientStatus(value) {
24
+ return typeof value === "string" && NEWSLETTER_RECIPIENT_STATUSES.includes(value);
25
+ }
26
+ export function isNewsletterTimelineInterval(value) {
27
+ return typeof value === "string" && NEWSLETTER_TIMELINE_INTERVALS.includes(value);
28
+ }
@@ -0,0 +1,20 @@
1
+ import { CliHttpError } from "./cli-http-error.js";
2
+ export declare const NEWSLETTER_TEST_MAX_RECIPIENTS = 5;
3
+ /** Test sends (requests, not emails) a workspace may make per day. */
4
+ export declare const NEWSLETTER_TEST_DAILY_LIMIT = 20;
5
+ export declare const NEWSLETTER_TEST_SUBJECT_PREFIX = "[Test] ";
6
+ /** SES message tag every test email carries; event apply and workspace health skip it. */
7
+ export declare const NEWSLETTER_TEST_SES_TAG = "oxygen_test";
8
+ export declare const NEWSLETTER_TEST_FAULT_CODES: {
9
+ readonly recipientNotMember: "test_send_recipient_not_member";
10
+ readonly notNewsletter: "test_send_not_newsletter";
11
+ readonly unavailable: "test_send_unavailable";
12
+ readonly failed: "test_send_failed";
13
+ };
14
+ export declare function testSendRecipientNotMemberError(recipients: readonly string[]): CliHttpError;
15
+ export declare function testSendNotNewsletterError(): CliHttpError;
16
+ export declare function testSendUnavailableError(): CliHttpError;
17
+ export declare function testSendFailedError(recipients: ReadonlyArray<{
18
+ email: string;
19
+ status: string;
20
+ }>): CliHttpError;
@@ -0,0 +1,46 @@
1
+ import { CliHttpError } from "./cli-http-error.js";
2
+ // A newsletter test send: one email step rendered as delivery renders it, sent
3
+ // to at most five verified addresses of the workspace's own members. Web, CLI
4
+ // and MCP share these limits and refusals (ADR 0023: fixed copy, a machine code
5
+ // and a next step; none names SES or another workspace).
6
+ export const NEWSLETTER_TEST_MAX_RECIPIENTS = 5;
7
+ /** Test sends (requests, not emails) a workspace may make per day. */
8
+ export const NEWSLETTER_TEST_DAILY_LIMIT = 20;
9
+ export const NEWSLETTER_TEST_SUBJECT_PREFIX = "[Test] ";
10
+ /** SES message tag every test email carries; event apply and workspace health skip it. */
11
+ export const NEWSLETTER_TEST_SES_TAG = "oxygen_test";
12
+ export const NEWSLETTER_TEST_FAULT_CODES = {
13
+ recipientNotMember: "test_send_recipient_not_member",
14
+ notNewsletter: "test_send_not_newsletter",
15
+ unavailable: "test_send_unavailable",
16
+ failed: "test_send_failed",
17
+ };
18
+ export function testSendRecipientNotMemberError(recipients) {
19
+ return new CliHttpError(400, NEWSLETTER_TEST_FAULT_CODES.recipientNotMember, "Test emails go only to verified email addresses of this workspace's members.", {
20
+ details: {
21
+ field: "to",
22
+ recipients: [...recipients],
23
+ retryable: false,
24
+ next_step: "Send to your own address or a teammate's verified address; list them with `oxygen sequences test-send <sequence> --list-recipients`.",
25
+ },
26
+ exitCode: 1,
27
+ });
28
+ }
29
+ export function testSendNotNewsletterError() {
30
+ return new CliHttpError(409, NEWSLETTER_TEST_FAULT_CODES.notNewsletter, "Test sends are for Sequences that send through a sending domain.", {
31
+ details: { retryable: false, next_step: "Choose a sending domain in the Sequence's Sending settings, then send a test." },
32
+ exitCode: 1,
33
+ });
34
+ }
35
+ export function testSendUnavailableError() {
36
+ return new CliHttpError(503, NEWSLETTER_TEST_FAULT_CODES.unavailable, "Test emails can't be sent right now.", {
37
+ details: { retryable: true, next_step: "Try again in a few minutes." },
38
+ exitCode: 1,
39
+ });
40
+ }
41
+ export function testSendFailedError(recipients) {
42
+ return new CliHttpError(502, NEWSLETTER_TEST_FAULT_CODES.failed, "The test email could not be sent.", {
43
+ details: { recipients: [...recipients], retryable: true, next_step: "Check the sending domain is active, then try again." },
44
+ exitCode: 1,
45
+ });
46
+ }
@@ -103,11 +103,22 @@ export declare function readCopilotAttachmentObject(input: {
103
103
  contentType: string | null;
104
104
  truncated: boolean;
105
105
  }>;
106
+ /** Authenticated callers retain authorization; never mint a transferable GET URL. */
107
+ export declare function openCopilotAttachmentObject(input: {
108
+ storageKey: string;
109
+ etag: string;
110
+ window?: {
111
+ offset: number;
112
+ limit: number;
113
+ };
114
+ signal?: AbortSignal;
115
+ }): Promise<ReadableStream<Uint8Array>>;
106
116
  export declare function getCopilotAttachmentObjectMetadata(input: {
107
117
  storageKey: string;
108
118
  }): Promise<{
109
119
  contentLength: number | null;
110
120
  contentType: string | null;
121
+ etag: string | null;
111
122
  }>;
112
123
  /**
113
124
  * Read the LEADING BYTES of a staged import back out of the bucket.
@@ -252,10 +252,22 @@ export async function readCopilotAttachmentObject(input) {
252
252
  truncated,
253
253
  };
254
254
  }
255
+ /** Authenticated callers retain authorization; never mint a transferable GET URL. */
256
+ export async function openCopilotAttachmentObject(input) {
257
+ const { client, config } = resolveClient();
258
+ const result = await client.send(new GetObjectCommand({
259
+ Bucket: config.bucket, Key: input.storageKey, IfMatch: input.etag,
260
+ ...(input.window ? { Range: `bytes=${input.window.offset}-${input.window.offset + input.window.limit - 1}` } : {}),
261
+ }), input.signal ? { abortSignal: input.signal } : {});
262
+ if (!result.Body)
263
+ throw new Error("Attachment object had no body.");
264
+ return result.Body.transformToWebStream();
265
+ }
255
266
  export async function getCopilotAttachmentObjectMetadata(input) {
256
267
  const { client, config } = resolveClient();
257
268
  const result = await client.send(new HeadObjectCommand({ Bucket: config.bucket, Key: input.storageKey }));
258
269
  return {
270
+ etag: result.ETag ?? null,
259
271
  contentLength: typeof result.ContentLength === "number" ? result.ContentLength : null,
260
272
  contentType: typeof result.ContentType === "string" ? result.ContentType : null,
261
273
  };
@@ -21,8 +21,15 @@ export declare const ONBOARDING_RESEARCH_MODEL: {
21
21
  readonly level: "medium";
22
22
  readonly reasoningEffort: "low";
23
23
  };
24
- /** Provider credits OXYGEN pays for one workspace's onboarding research, over its lifetime. */
25
- export declare const ONBOARDING_RESEARCH_MAX_CREDITS = 300;
24
+ /**
25
+ * Provider credits OXYGEN pays for one workspace's onboarding research, over its
26
+ * lifetime, restarts included. Raised from 300 (Philipp, 2026-10-21: "we can
27
+ * increase the limit to make the research really good"): a full run took up to
28
+ * 121 credits, and the Oxygen Demo Workspace ran out on its eighth start.
29
+ */
30
+ export declare const ONBOARDING_RESEARCH_MAX_CREDITS = 2000;
31
+ /** What one onboarding run (one funded turn) may spend of it, so a run has room to go deep and a restart has room too. */
32
+ export declare const ONBOARDING_RESEARCH_RUN_MAX_CREDITS = 500;
26
33
  /** Ceiling on any one research call; a call that names a higher one is lowered to it. */
27
34
  export declare const ONBOARDING_RESEARCH_MAX_CREDITS_PER_CALL = 15;
28
35
  /** The onboarding turn's own and its sub-agents' inference, credited back once the turn settles. */
@@ -18,8 +18,15 @@ export const ONBOARDING_RESEARCH_SUBAGENT = "onboarding_researcher";
18
18
  * Reliable calls first, then speed: one place to change once measured.
19
19
  */
20
20
  export const ONBOARDING_RESEARCH_MODEL = { level: "medium", reasoningEffort: "low" };
21
- /** Provider credits OXYGEN pays for one workspace's onboarding research, over its lifetime. */
22
- export const ONBOARDING_RESEARCH_MAX_CREDITS = 300;
21
+ /**
22
+ * Provider credits OXYGEN pays for one workspace's onboarding research, over its
23
+ * lifetime, restarts included. Raised from 300 (Philipp, 2026-10-21: "we can
24
+ * increase the limit to make the research really good"): a full run took up to
25
+ * 121 credits, and the Oxygen Demo Workspace ran out on its eighth start.
26
+ */
27
+ export const ONBOARDING_RESEARCH_MAX_CREDITS = 2_000;
28
+ /** What one onboarding run (one funded turn) may spend of it, so a run has room to go deep and a restart has room too. */
29
+ export const ONBOARDING_RESEARCH_RUN_MAX_CREDITS = 500;
23
30
  /** Ceiling on any one research call; a call that names a higher one is lowered to it. */
24
31
  export const ONBOARDING_RESEARCH_MAX_CREDITS_PER_CALL = 15;
25
32
  /** The onboarding turn's own and its sub-agents' inference, credited back once the turn settles. */
@@ -94,6 +94,15 @@ export declare function resolveDefaultTriggerRunCreditCeilingForPlan(resolution:
94
94
  export declare function resolveDefaultConnectionCreditCeilingForPlan(resolution: PlanSpendResolution, options?: RepricingOptions): number;
95
95
  /** The same default for one standing or webhook table auto-run batch. */
96
96
  export declare function resolveDefaultAutoRunBatchCreditCeilingForPlan(resolution: PlanSpendResolution, options?: RepricingOptions): number | null;
97
+ /**
98
+ * PROPOSED defaults, not yet a product decision: newsletter (sending-domain)
99
+ * emails one workspace may reserve per UTC day. Newsletters are paid-only, so
100
+ * free is 0. The staff override and the workspace's share of the SES quota
101
+ * (`bulkEmailWorkspaceDailyAllowance`) can only lower this.
102
+ */
103
+ export declare const BULK_EMAIL_DAILY_CAP_PER_PLAN_BAND: Readonly<Record<PlanBand, number>>;
104
+ /** The largest plan cap: no sending domain's own daily ceiling may exceed it. */
105
+ export declare const BULK_EMAIL_MAX_DAILY_CAP: number;
97
106
  /**
98
107
  * Public webhook ingress (repricing 2026-09, decision L4.4, ratified
99
108
  * 2026-09-26): deliveries a minute that one webhook target (a Table endpoint,
@@ -203,6 +203,23 @@ export function resolveDefaultAutoRunBatchCreditCeilingForPlan(resolution, optio
203
203
  function bandSpendResolution(band) {
204
204
  return { planTier: band === "free" ? null : `oxygen_${band}`, limitsTier: PLAN_BAND_LIMITS_TIER[band] };
205
205
  }
206
+ /**
207
+ * PROPOSED defaults, not yet a product decision: newsletter (sending-domain)
208
+ * emails one workspace may reserve per UTC day. Newsletters are paid-only, so
209
+ * free is 0. The staff override and the workspace's share of the SES quota
210
+ * (`bulkEmailWorkspaceDailyAllowance`) can only lower this.
211
+ */
212
+ export const BULK_EMAIL_DAILY_CAP_PER_PLAN_BAND = Object.freeze({
213
+ free: 0,
214
+ "49": 5_000,
215
+ "99": 10_000,
216
+ "199": 25_000,
217
+ "499": 50_000,
218
+ "999": 100_000,
219
+ "1999": 250_000,
220
+ });
221
+ /** The largest plan cap: no sending domain's own daily ceiling may exceed it. */
222
+ export const BULK_EMAIL_MAX_DAILY_CAP = Math.max(...Object.values(BULK_EMAIL_DAILY_CAP_PER_PLAN_BAND));
206
223
  /**
207
224
  * Public webhook ingress (repricing 2026-09, decision L4.4, ratified
208
225
  * 2026-09-26): deliveries a minute that one webhook target (a Table endpoint,