@oxygen-agent/cli 1.696.2 → 1.717.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +1 -1
  2. package/dist/help.js +2 -0
  3. package/dist/http-client.js +11 -1
  4. package/dist/index.js +524 -86
  5. package/dist/transcript.js +2 -2
  6. package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +4 -0
  7. package/node_modules/@oxygen/shared/dist/billing.d.ts +1 -1
  8. package/node_modules/@oxygen/shared/dist/billing.js +7 -6
  9. package/node_modules/@oxygen/shared/dist/dnc-identities.d.ts +53 -0
  10. package/node_modules/@oxygen/shared/dist/dnc-identities.js +175 -0
  11. package/node_modules/@oxygen/shared/dist/file-import.d.ts +36 -1
  12. package/node_modules/@oxygen/shared/dist/file-import.js +80 -1
  13. package/node_modules/@oxygen/shared/dist/image-sniff.d.ts +39 -0
  14. package/node_modules/@oxygen/shared/dist/image-sniff.js +75 -0
  15. package/node_modules/@oxygen/shared/dist/import-limits.d.ts +28 -0
  16. package/node_modules/@oxygen/shared/dist/import-limits.js +30 -0
  17. package/node_modules/@oxygen/shared/dist/index.d.ts +2 -0
  18. package/node_modules/@oxygen/shared/dist/index.js +2 -0
  19. package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +10 -4
  20. package/node_modules/@oxygen/shared/dist/mailbox-import.js +18 -10
  21. package/node_modules/@oxygen/shared/dist/member-columns.d.ts +64 -0
  22. package/node_modules/@oxygen/shared/dist/member-columns.js +111 -0
  23. package/node_modules/@oxygen/shared/dist/microsoft-consent-url.js +1 -1
  24. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +65 -0
  25. package/node_modules/@oxygen/shared/dist/object-storage.js +100 -0
  26. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +2 -1
  27. package/node_modules/@oxygen/shared/dist/plan-limits.js +12 -2
  28. package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +9 -0
  29. package/node_modules/@oxygen/shared/dist/spend-safety.js +10 -0
  30. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  31. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  32. package/node_modules/@oxygen/shared/package.json +20 -0
  33. package/node_modules/@oxygen/workflows/dist/graph/lint.js +79 -11
  34. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +679 -0
  35. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.js +80 -1
  36. package/node_modules/@oxygen/workflows/dist/graph/remap.js +8 -1
  37. package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +72 -3
  38. package/node_modules/@oxygen/workflows/dist/graph/types.js +12 -0
  39. package/node_modules/@oxygen/workflows/dist/index.d.ts +16 -0
  40. package/node_modules/@oxygen/workflows/dist/index.js +45 -3
  41. package/node_modules/@oxygen/workflows/dist/portable.d.ts +43 -0
  42. package/node_modules/@oxygen/workflows/dist/portable.js +319 -0
  43. package/node_modules/@oxygen/workflows/dist/usage-estimate.js +18 -4
  44. package/package.json +1 -1
@@ -34,6 +34,8 @@ export type MailboxImportValidationSummary = {
34
34
  };
35
35
  export type MailboxOAuthProviderReviewPlan = {
36
36
  mailboxes: number;
37
+ authorization_mode: "individual" | "tenant";
38
+ batch_size: number;
37
39
  batches: number;
38
40
  batch_sizes: number[];
39
41
  };
@@ -42,7 +44,9 @@ export type MailboxOAuthReviewPlan = {
42
44
  scope: "validated_addresses";
43
45
  directory_discovery: false;
44
46
  existing_grants_checked: false;
47
+ /** Backward-compatible individual OAuth review size. */
45
48
  batch_size: number;
49
+ tenant_batch_size: number;
46
50
  total_batches: number;
47
51
  providers: {
48
52
  google: MailboxOAuthProviderReviewPlan;
@@ -75,10 +79,12 @@ export declare function summarizeMailboxImportValidation(mailboxes: readonly Nor
75
79
  sourceProvider: string | null;
76
80
  }): MailboxImportValidationSummary;
77
81
  /**
78
- * Build the deterministic, provider-specific review queue for exact-account
79
- * OAuth. This is safe during local file validation: it uses only normalized
80
- * provider counts, performs no directory discovery, and does not claim to know
81
- * whether a workspace already holds a usable grant for an address.
82
+ * Build the deterministic, provider-specific review queue for exact mailbox
83
+ * authorization. This is safe during local file validation: it uses only
84
+ * normalized provider counts, performs no directory discovery, and does not
85
+ * claim to know whether a workspace already holds a usable grant for an address.
86
+ * Multi-mailbox Microsoft imports recommend one tenant-admin batch; a single
87
+ * Microsoft mailbox and all Google mailboxes retain individual OAuth.
82
88
  */
83
89
  export declare function planMailboxOAuthReviews(mailboxes: readonly Pick<NormalizedMailboxImportRow, "provider">[]): MailboxOAuthReviewPlan;
84
90
  export declare function normalizeMailboxImportVendor(raw: string | null | undefined, from: string | null | undefined): string | null;
@@ -208,35 +208,43 @@ export function summarizeMailboxImportValidation(mailboxes, input) {
208
208
  };
209
209
  }
210
210
  /**
211
- * Build the deterministic, provider-specific review queue for exact-account
212
- * OAuth. This is safe during local file validation: it uses only normalized
213
- * provider counts, performs no directory discovery, and does not claim to know
214
- * whether a workspace already holds a usable grant for an address.
211
+ * Build the deterministic, provider-specific review queue for exact mailbox
212
+ * authorization. This is safe during local file validation: it uses only
213
+ * normalized provider counts, performs no directory discovery, and does not
214
+ * claim to know whether a workspace already holds a usable grant for an address.
215
+ * Multi-mailbox Microsoft imports recommend one tenant-admin batch; a single
216
+ * Microsoft mailbox and all Google mailboxes retain individual OAuth.
215
217
  */
216
218
  export function planMailboxOAuthReviews(mailboxes) {
217
219
  const counts = { google: 0, microsoft: 0 };
218
220
  for (const mailbox of mailboxes)
219
221
  counts[mailbox.provider] += 1;
220
- const google = planProviderOAuthReviews(counts.google);
221
- const microsoft = planProviderOAuthReviews(counts.microsoft);
222
+ const google = planProviderOAuthReviews(counts.google, MAILBOX_OAUTH_REVIEW_BATCH_SIZE, "individual");
223
+ const microsoftMode = counts.microsoft > 1 ? "tenant" : "individual";
224
+ const microsoft = planProviderOAuthReviews(counts.microsoft, microsoftMode === "tenant"
225
+ ? MAILBOX_IMPORT_ROW_LIMIT
226
+ : MAILBOX_OAUTH_REVIEW_BATCH_SIZE, microsoftMode);
222
227
  return {
223
228
  authorization: "exact_account_oauth",
224
229
  scope: "validated_addresses",
225
230
  directory_discovery: false,
226
231
  existing_grants_checked: false,
227
232
  batch_size: MAILBOX_OAUTH_REVIEW_BATCH_SIZE,
233
+ tenant_batch_size: MAILBOX_IMPORT_ROW_LIMIT,
228
234
  total_batches: google.batches + microsoft.batches,
229
235
  providers: { google, microsoft },
230
236
  };
231
237
  }
232
- function planProviderOAuthReviews(mailboxes) {
233
- const fullBatches = Math.floor(mailboxes / MAILBOX_OAUTH_REVIEW_BATCH_SIZE);
234
- const remainder = mailboxes % MAILBOX_OAUTH_REVIEW_BATCH_SIZE;
235
- const batchSizes = Array.from({ length: fullBatches }, () => MAILBOX_OAUTH_REVIEW_BATCH_SIZE);
238
+ function planProviderOAuthReviews(mailboxes, batchSize, authorizationMode) {
239
+ const fullBatches = Math.floor(mailboxes / batchSize);
240
+ const remainder = mailboxes % batchSize;
241
+ const batchSizes = Array.from({ length: fullBatches }, () => batchSize);
236
242
  if (remainder > 0)
237
243
  batchSizes.push(remainder);
238
244
  return {
239
245
  mailboxes,
246
+ authorization_mode: authorizationMode,
247
+ batch_size: batchSize,
240
248
  batches: batchSizes.length,
241
249
  batch_sizes: batchSizes,
242
250
  };
@@ -0,0 +1,64 @@
1
+ /** Canonical semantic for a member-reference column on any object. */
2
+ export declare const MEMBER_COLUMN_SEMANTIC = "member";
3
+ export declare function isMemberColumnSemantic(semanticType: string | null | undefined): boolean;
4
+ /**
5
+ * One workspace member, as every surface needs them: the email is the stored
6
+ * cell value and the join key, `userId` is the control-db id for anything that
7
+ * has to talk to the control plane, and name/image are display only.
8
+ *
9
+ * `isActive` false means the row resolved out of a historical membership that
10
+ * no longer holds — the cell still renders the person rather than going blank,
11
+ * because "Sarah owned this before she left" is information, not an error.
12
+ */
13
+ export type WorkspaceMemberRef = {
14
+ email: string;
15
+ userId: string | null;
16
+ name: string | null;
17
+ imageUrl: string | null;
18
+ isActive: boolean;
19
+ };
20
+ /** Email (lowercased) → member. The shape the API layer puts on an envelope. */
21
+ export type MemberDirectory = Record<string, WorkspaceMemberRef>;
22
+ /**
23
+ * Canonical form of a member cell value: a lowercased, trimmed email.
24
+ *
25
+ * Anything that is not an email is preserved verbatim (trimmed) rather than
26
+ * rejected — existing `owner` columns are free text and hold real names typed
27
+ * by real founders. Losing "Philipp" on the way to a typed column would be a
28
+ * silent data deletion; carrying it as an unresolved value is honest and lets
29
+ * the owner picker repair it one click at a time.
30
+ */
31
+ export declare function normalizeMemberCellValue(raw: unknown): string | null;
32
+ /** Build the lookup the render + validation paths use, keyed on lowercased email. */
33
+ export declare function buildMemberDirectory(members: readonly WorkspaceMemberRef[]): MemberDirectory;
34
+ export type ResolvedMemberCell = {
35
+ /** The canonical stored value, or null for an empty cell. */
36
+ value: string | null;
37
+ /** The directory hit, when there is one. */
38
+ member: WorkspaceMemberRef | null;
39
+ /**
40
+ * A non-empty value that no directory entry matched: a legacy free-text
41
+ * owner, a member who left, or a changed address. Renders as a plain grey
42
+ * chip so it reads as "unrecognised", never as a normal member.
43
+ */
44
+ unresolved: boolean;
45
+ };
46
+ /**
47
+ * Resolve one stored cell value against a directory. Pure, total, and never
48
+ * throws: a missing directory (the API layer could not reach the control plane)
49
+ * degrades every cell to `unresolved`, which shows the raw value instead of
50
+ * blanking a column — the same posture the activity timeline takes when link
51
+ * decoration fails.
52
+ */
53
+ export declare function resolveMemberCell(raw: unknown, directory: MemberDirectory | null | undefined): ResolvedMemberCell;
54
+ /**
55
+ * Validate a member value on WRITE, resolving the several things a human or an
56
+ * agent might reasonably pass — the member's email, or their display name when
57
+ * it is unambiguous — down to the canonical stored email.
58
+ *
59
+ * Returns null when nothing matched, so the caller can raise a typed error
60
+ * naming the workspace's actual members. Deliberately NOT lenient: a write is
61
+ * the one moment we can keep the column clean, and silently storing an
62
+ * unrecognised assignee is how an owner column rots back into free text.
63
+ */
64
+ export declare function resolveMemberWriteValue(raw: unknown, directory: MemberDirectory): WorkspaceMemberRef | null;
@@ -0,0 +1,111 @@
1
+ // Member-reference cells: an attribute whose value is a PERSON IN THIS
2
+ // WORKSPACE — the record's owner/assignee. Attio calls the type an "actor
3
+ // reference"; HubSpot calls the attribute "Deal owner". We ship one concept
4
+ // under one name so a founder never has to hold both.
5
+ //
6
+ // STORAGE IS THE MEMBER'S EMAIL, in a `text` column, riding the same
7
+ // semantic-over-text pattern as `markdown` and `image`: no new dataType, no new
8
+ // WorkspaceColumnKind, so every existing filter, sort, CSV export, formula and
9
+ // provider mapping keeps working unchanged.
10
+ //
11
+ // Email rather than the control-db user id, deliberately, and this is the one
12
+ // place the two conventions diverge:
13
+ // - Collab (approvals) stores `assignee_user_ids` uuids and speaks email only
14
+ // at its API boundary. Correct there — those rows are control-plane-linked
15
+ // and never leave the product.
16
+ // - A CRM cell DOES leave the product. It lands in CSV exports, AI-column
17
+ // prompts, formula expressions, and outbound HubSpot/Attio owner mappings,
18
+ // all of which match on email. A uuid in any of those is an unreadable
19
+ // token that no downstream system can resolve.
20
+ // The consequence to accept: an email is not an immutable handle. A member who
21
+ // changes address leaves cells pointing at their old one, which is why
22
+ // `resolveMemberCell` reports an unresolved value rather than dropping it.
23
+ //
24
+ // The DIRECTORY — who is a member, their display name, their avatar — is
25
+ // control-plane state. tenant-db must never reach across that boundary, so
26
+ // nothing here queries anything: the API layer supplies a directory and these
27
+ // helpers stay pure.
28
+ /** Canonical semantic for a member-reference column on any object. */
29
+ export const MEMBER_COLUMN_SEMANTIC = "member";
30
+ // `crm.owner` predates this type and already means exactly "a person in this
31
+ // workspace owns this record" on the three standard objects. Treating it as a
32
+ // member semantic rather than migrating it to `member` is what makes this
33
+ // change reach every existing workspace with no column rewrite at all — the
34
+ // stored VALUES converge on their own as owners are re-picked, and an
35
+ // unresolved legacy string stays visible in the meantime.
36
+ const MEMBER_COLUMN_SEMANTIC_ALIASES = ["crm.owner", "crm.assignee"];
37
+ export function isMemberColumnSemantic(semanticType) {
38
+ if (!semanticType)
39
+ return false;
40
+ return (semanticType === MEMBER_COLUMN_SEMANTIC ||
41
+ MEMBER_COLUMN_SEMANTIC_ALIASES.includes(semanticType));
42
+ }
43
+ /**
44
+ * Canonical form of a member cell value: a lowercased, trimmed email.
45
+ *
46
+ * Anything that is not an email is preserved verbatim (trimmed) rather than
47
+ * rejected — existing `owner` columns are free text and hold real names typed
48
+ * by real founders. Losing "Philipp" on the way to a typed column would be a
49
+ * silent data deletion; carrying it as an unresolved value is honest and lets
50
+ * the owner picker repair it one click at a time.
51
+ */
52
+ export function normalizeMemberCellValue(raw) {
53
+ if (typeof raw !== "string")
54
+ return null;
55
+ const trimmed = raw.trim();
56
+ if (!trimmed)
57
+ return null;
58
+ return looksLikeEmail(trimmed) ? trimmed.toLowerCase() : trimmed;
59
+ }
60
+ /** Build the lookup the render + validation paths use, keyed on lowercased email. */
61
+ export function buildMemberDirectory(members) {
62
+ const directory = {};
63
+ for (const member of members) {
64
+ const email = normalizeMemberCellValue(member.email);
65
+ if (!email || !looksLikeEmail(email))
66
+ continue;
67
+ directory[email] = { ...member, email };
68
+ }
69
+ return directory;
70
+ }
71
+ /**
72
+ * Resolve one stored cell value against a directory. Pure, total, and never
73
+ * throws: a missing directory (the API layer could not reach the control plane)
74
+ * degrades every cell to `unresolved`, which shows the raw value instead of
75
+ * blanking a column — the same posture the activity timeline takes when link
76
+ * decoration fails.
77
+ */
78
+ export function resolveMemberCell(raw, directory) {
79
+ const value = normalizeMemberCellValue(raw);
80
+ if (!value)
81
+ return { value: null, member: null, unresolved: false };
82
+ const member = directory?.[value.toLowerCase()] ?? null;
83
+ return { value, member: member ?? null, unresolved: !member };
84
+ }
85
+ /**
86
+ * Validate a member value on WRITE, resolving the several things a human or an
87
+ * agent might reasonably pass — the member's email, or their display name when
88
+ * it is unambiguous — down to the canonical stored email.
89
+ *
90
+ * Returns null when nothing matched, so the caller can raise a typed error
91
+ * naming the workspace's actual members. Deliberately NOT lenient: a write is
92
+ * the one moment we can keep the column clean, and silently storing an
93
+ * unrecognised assignee is how an owner column rots back into free text.
94
+ */
95
+ export function resolveMemberWriteValue(raw, directory) {
96
+ const value = normalizeMemberCellValue(raw);
97
+ if (!value)
98
+ return null;
99
+ const byEmail = directory[value.toLowerCase()];
100
+ if (byEmail)
101
+ return byEmail;
102
+ // Name match, only when exactly one member answers to it. Two people called
103
+ // "Alex" must fail rather than have the write pick one of them.
104
+ const needle = value.toLowerCase();
105
+ const byName = Object.values(directory).filter((member) => member.name && member.name.trim().toLowerCase() === needle);
106
+ return byName.length === 1 ? (byName[0] ?? null) : null;
107
+ }
108
+ function looksLikeEmail(value) {
109
+ const at = value.indexOf("@");
110
+ return at > 0 && at < value.length - 1 && !/\s/.test(value);
111
+ }
@@ -1,5 +1,5 @@
1
1
  const MICROSOFT_LOGIN_HOST = "login.microsoftonline.com";
2
- const ADMIN_CONSENT_PATH = /^\/(?:[^/]+\/)?adminconsent\/?$/i;
2
+ const ADMIN_CONSENT_PATH = /^\/(?:[^/]+\/)?(?:v2\.0\/)?adminconsent\/?$/i;
3
3
  /**
4
4
  * Normalize a Microsoft Entra admin-consent URL for an operator handoff.
5
5
  *
@@ -11,6 +11,22 @@ export declare function buildCopilotAttachmentObjectKey(input: {
11
11
  organizationId: string;
12
12
  fileName?: string | null;
13
13
  }): string;
14
+ /**
15
+ * Mailbox avatars, which are unlike every other namespace here: their bytes are
16
+ * served back to an UNAUTHENTICATED third party (the inbox vendor fetches the
17
+ * URL server-side when it provisions the mailbox, possibly hours later).
18
+ *
19
+ * The object itself stays private — the bucket also holds customer lead lists —
20
+ * and a proxy route on our own domain reads it. So the two identifiers in the
21
+ * key are the whole access-control story: the org id scopes it, and the random
22
+ * UUID makes it unguessable. The file name is NOT caller-supplied; it comes from
23
+ * the sniffed image type, so the key can never carry attacker-chosen text.
24
+ */
25
+ export declare function buildInboxAvatarObjectKey(input: {
26
+ organizationId: string;
27
+ fileName: string;
28
+ }): string;
29
+ export declare function isInboxAvatarObjectKeyForOrganization(key: string, organizationId: string): boolean;
14
30
  export declare function isImportObjectKeyForOrganization(key: string, organizationId: string): boolean;
15
31
  export declare function isCopilotAttachmentObjectKeyForOrganization(key: string, organizationId: string): boolean;
16
32
  export declare function isPublishingMediaObjectKeyForOrganization(key: string, organizationId: string): boolean;
@@ -61,6 +77,27 @@ export declare function getCopilotAttachmentObjectMetadata(input: {
61
77
  contentLength: number | null;
62
78
  contentType: string | null;
63
79
  }>;
80
+ /**
81
+ * Read the LEADING BYTES of a staged import back out of the bucket.
82
+ *
83
+ * Bounded by `maxBytes` on purpose: the callers derive a header row, a column
84
+ * type sample and a row estimate, none of which needs the whole object — and a
85
+ * 100MB import must never become a 100MB buffer inside a Vercel handler. Same
86
+ * ranged-read shape as `readCopilotAttachmentObject`.
87
+ *
88
+ * Note on Content-Type: objects under `imports/` are written by a presigned PUT,
89
+ * and the S3 presigner marks content-type UNSIGNABLE — so the stored type is
90
+ * whatever the client claimed and is not covered by the signature. Never add a
91
+ * download, redirect or presigned-GET route over this prefix without pinning
92
+ * `ResponseContentType` and `ResponseContentDisposition: attachment`.
93
+ */
94
+ export declare function readImportObject(input: {
95
+ storageKey: string;
96
+ maxBytes: number;
97
+ }): Promise<{
98
+ body: Uint8Array;
99
+ truncated: boolean;
100
+ }>;
64
101
  export declare function getImportObjectMetadata(input: {
65
102
  storageKey: string;
66
103
  }): Promise<{
@@ -82,6 +119,34 @@ export declare function presignPublishingMediaDownload(input: {
82
119
  storageKey: string;
83
120
  expiresInSeconds?: number;
84
121
  }): Promise<string>;
122
+ export declare function presignInboxAvatarUpload(input: {
123
+ organizationId: string;
124
+ fileName: string;
125
+ contentType: string;
126
+ contentLength: number;
127
+ }): Promise<PresignedImportUpload>;
128
+ /**
129
+ * Read an avatar back, bounded. `maxBytes` is a hard stop rather than a hint:
130
+ * this is called from an unauthenticated route, so an object that somehow grew
131
+ * past the upload ceiling must not be able to turn one request into an
132
+ * unbounded buffer.
133
+ */
134
+ export declare function readInboxAvatarObject(input: {
135
+ storageKey: string;
136
+ maxBytes: number;
137
+ }): Promise<{
138
+ body: Uint8Array;
139
+ contentLength: number | null;
140
+ truncated: boolean;
141
+ }>;
142
+ export declare function getInboxAvatarObjectMetadata(input: {
143
+ storageKey: string;
144
+ }): Promise<{
145
+ contentLength: number | null;
146
+ }>;
147
+ export declare function deleteInboxAvatarObject(input: {
148
+ storageKey: string;
149
+ }): Promise<void>;
85
150
  export declare function deleteImportObject(input: {
86
151
  storageKey: string;
87
152
  }): Promise<void>;
@@ -69,6 +69,23 @@ export function buildCopilotAttachmentObjectKey(input) {
69
69
  const safeName = sanitizeFileName(input.fileName) || "attachment";
70
70
  return `copilot/${input.organizationId}/${randomUUID()}/${safeName}`;
71
71
  }
72
+ /**
73
+ * Mailbox avatars, which are unlike every other namespace here: their bytes are
74
+ * served back to an UNAUTHENTICATED third party (the inbox vendor fetches the
75
+ * URL server-side when it provisions the mailbox, possibly hours later).
76
+ *
77
+ * The object itself stays private — the bucket also holds customer lead lists —
78
+ * and a proxy route on our own domain reads it. So the two identifiers in the
79
+ * key are the whole access-control story: the org id scopes it, and the random
80
+ * UUID makes it unguessable. The file name is NOT caller-supplied; it comes from
81
+ * the sniffed image type, so the key can never carry attacker-chosen text.
82
+ */
83
+ export function buildInboxAvatarObjectKey(input) {
84
+ return `inbox-avatars/${input.organizationId}/${randomUUID()}/${input.fileName}`;
85
+ }
86
+ export function isInboxAvatarObjectKeyForOrganization(key, organizationId) {
87
+ return key.startsWith(`inbox-avatars/${organizationId}/`);
88
+ }
72
89
  export function isImportObjectKeyForOrganization(key, organizationId) {
73
90
  return key.startsWith(`imports/${organizationId}/`);
74
91
  }
@@ -176,6 +193,34 @@ export async function getCopilotAttachmentObjectMetadata(input) {
176
193
  contentType: typeof result.ContentType === "string" ? result.ContentType : null,
177
194
  };
178
195
  }
196
+ /**
197
+ * Read the LEADING BYTES of a staged import back out of the bucket.
198
+ *
199
+ * Bounded by `maxBytes` on purpose: the callers derive a header row, a column
200
+ * type sample and a row estimate, none of which needs the whole object — and a
201
+ * 100MB import must never become a 100MB buffer inside a Vercel handler. Same
202
+ * ranged-read shape as `readCopilotAttachmentObject`.
203
+ *
204
+ * Note on Content-Type: objects under `imports/` are written by a presigned PUT,
205
+ * and the S3 presigner marks content-type UNSIGNABLE — so the stored type is
206
+ * whatever the client claimed and is not covered by the signature. Never add a
207
+ * download, redirect or presigned-GET route over this prefix without pinning
208
+ * `ResponseContentType` and `ResponseContentDisposition: attachment`.
209
+ */
210
+ export async function readImportObject(input) {
211
+ const { client, config } = resolveClient();
212
+ const result = await client.send(new GetObjectCommand({
213
+ Bucket: config.bucket,
214
+ Key: input.storageKey,
215
+ // One byte past the cap, so a full read is distinguishable from a file
216
+ // that happens to be exactly maxBytes long.
217
+ Range: `bytes=0-${input.maxBytes}`,
218
+ }));
219
+ const raw = await result.Body?.transformToByteArray();
220
+ const body = raw ?? new Uint8Array();
221
+ const truncated = body.byteLength > input.maxBytes;
222
+ return { body: truncated ? body.subarray(0, input.maxBytes) : body, truncated };
223
+ }
179
224
  export async function getImportObjectMetadata(input) {
180
225
  const { client, config } = resolveClient();
181
226
  const result = await client.send(new HeadObjectCommand({ Bucket: config.bucket, Key: input.storageKey }));
@@ -222,6 +267,61 @@ export async function presignPublishingMediaDownload(input) {
222
267
  expiresIn: input.expiresInSeconds ?? 60 * 60,
223
268
  });
224
269
  }
270
+ export async function presignInboxAvatarUpload(input) {
271
+ const { client, config } = resolveClient();
272
+ const storageKey = buildInboxAvatarObjectKey({
273
+ organizationId: input.organizationId,
274
+ fileName: input.fileName,
275
+ });
276
+ const command = new PutObjectCommand({
277
+ Bucket: config.bucket,
278
+ Key: storageKey,
279
+ ContentLength: input.contentLength,
280
+ ContentType: input.contentType,
281
+ });
282
+ const uploadUrl = await getSignedUrl(client, command, { expiresIn: PRESIGN_EXPIRY_SECONDS });
283
+ return {
284
+ uploadUrl,
285
+ bucket: config.bucket,
286
+ storageKey,
287
+ contentLength: input.contentLength,
288
+ provider: "s3",
289
+ expiresInSeconds: PRESIGN_EXPIRY_SECONDS,
290
+ };
291
+ }
292
+ /**
293
+ * Read an avatar back, bounded. `maxBytes` is a hard stop rather than a hint:
294
+ * this is called from an unauthenticated route, so an object that somehow grew
295
+ * past the upload ceiling must not be able to turn one request into an
296
+ * unbounded buffer.
297
+ */
298
+ export async function readInboxAvatarObject(input) {
299
+ const { client, config } = resolveClient();
300
+ const result = await client.send(new GetObjectCommand({
301
+ Bucket: config.bucket,
302
+ Key: input.storageKey,
303
+ Range: `bytes=0-${input.maxBytes}`,
304
+ }));
305
+ const raw = await result.Body?.transformToByteArray();
306
+ const body = raw ?? new Uint8Array();
307
+ const truncated = body.byteLength > input.maxBytes;
308
+ return {
309
+ body: truncated ? body.subarray(0, input.maxBytes) : body,
310
+ contentLength: typeof result.ContentLength === "number" ? result.ContentLength : null,
311
+ truncated,
312
+ };
313
+ }
314
+ export async function getInboxAvatarObjectMetadata(input) {
315
+ const { client, config } = resolveClient();
316
+ const result = await client.send(new HeadObjectCommand({ Bucket: config.bucket, Key: input.storageKey }));
317
+ return {
318
+ contentLength: typeof result.ContentLength === "number" ? result.ContentLength : null,
319
+ };
320
+ }
321
+ export async function deleteInboxAvatarObject(input) {
322
+ const { client, config } = resolveClient();
323
+ await client.send(new DeleteObjectCommand({ Bucket: config.bucket, Key: input.storageKey }));
324
+ }
225
325
  export async function deleteImportObject(input) {
226
326
  const { client, config } = resolveClient();
227
327
  await client.send(new DeleteObjectCommand({ Bucket: config.bucket, Key: input.storageKey }));
@@ -70,6 +70,7 @@ export type PlanLimits = {
70
70
  maxActionsPerRun: number;
71
71
  };
72
72
  };
73
+ export { VERCEL_REQUEST_BODY_LIMIT_BYTES } from "./import-limits.js";
73
74
  /**
74
75
  * The per-rung limit matrix. `ai_live` deliberately equals `tool_live` at every
75
76
  * rung — one "live actions" mental model; the per-call cost asymmetry between
@@ -164,7 +165,7 @@ export declare const PLAN_LIMITS: {
164
165
  };
165
166
  };
166
167
  readonly import: {
167
- readonly maxRowsPerFile: 50000;
168
+ readonly maxRowsPerFile: 250000;
168
169
  readonly maxFileBytes: number;
169
170
  readonly maxDirectCsvBodyBytes: 10000000;
170
171
  };
@@ -9,6 +9,10 @@ export const LIMITS_TIER_ORDER = [
9
9
  const HOUR = 60 * 60;
10
10
  const DAY = 24 * HOUR;
11
11
  const MIB = 1024 * 1024;
12
+ // The platform request-body ceiling belongs to the same question as the import
13
+ // ceilings below ("how big can an import be"), but it must stay importable from a
14
+ // browser bundle, so it is defined in ./import-limits and re-exported here.
15
+ export { VERCEL_REQUEST_BODY_LIMIT_BYTES } from "./import-limits.js";
12
16
  /**
13
17
  * The per-rung limit matrix. `ai_live` deliberately equals `tool_live` at every
14
18
  * rung — one "live actions" mental model; the per-call cost asymmetry between
@@ -34,8 +38,14 @@ export const PLAN_LIMITS = {
34
38
  import: { requestsPerMinute: 10, orgRequests: { limit: 100, windowSeconds: HOUR } },
35
39
  },
36
40
  import: {
37
- maxRowsPerFile: 50_000,
38
- maxFileBytes: 25 * MIB,
41
+ // Raised from 50k/25MiB in v1.717.0, when the web import modal moved onto
42
+ // the staged upload path. The old ceiling was set when every import had to
43
+ // squeeze through a request body; now the bytes go straight to object
44
+ // storage and the worker streams them, so the limit can describe what a
45
+ // trial workspace should be allowed to load rather than what the transport
46
+ // could survive.
47
+ maxRowsPerFile: 250_000,
48
+ maxFileBytes: 100 * MIB,
39
49
  maxDirectCsvBodyBytes: 10_000_000,
40
50
  },
41
51
  agents: {
@@ -42,6 +42,15 @@ export declare const DEFAULT_BYOK_COLUMN_RUN_MAX_ROWS: Record<PlanTier, number |
42
42
  */
43
43
  export declare const DEFAULT_BYOK_PROVIDER_DAILY_CALL_CAP: Record<PlanTier, number | null>;
44
44
  export declare const BYOK_PROVIDER_DAILY_WINDOW_SECONDS: number;
45
+ export declare const BYOK_PROVIDER_DAILY_CAP_ENFORCEMENT_ENV = "OXYGEN_BYOK_DAILY_CAPS";
46
+ export type ByokProviderDailyCapEnforcementMode = "observe_only" | "enforced";
47
+ /**
48
+ * Runtime mode for the plan-scaled BYOK daily threshold. The default is
49
+ * deliberately observe-only while provider-volume telemetry is calibrated;
50
+ * explicit workspace provider policies remain independently enforced by the
51
+ * normal provider rate limiter.
52
+ */
53
+ export declare function resolveByokProviderDailyCapEnforcementMode(configured?: string | undefined): ByokProviderDailyCapEnforcementMode;
45
54
  /**
46
55
  * Implicit org-level DAILY budget guard, evaluated only when the org has no
47
56
  * explicit org-scope daily budget policy and the plan has finite positive
@@ -58,6 +58,16 @@ export const DEFAULT_BYOK_PROVIDER_DAILY_CALL_CAP = {
58
58
  enterprise: null,
59
59
  };
60
60
  export const BYOK_PROVIDER_DAILY_WINDOW_SECONDS = 24 * 60 * 60;
61
+ export const BYOK_PROVIDER_DAILY_CAP_ENFORCEMENT_ENV = "OXYGEN_BYOK_DAILY_CAPS";
62
+ /**
63
+ * Runtime mode for the plan-scaled BYOK daily threshold. The default is
64
+ * deliberately observe-only while provider-volume telemetry is calibrated;
65
+ * explicit workspace provider policies remain independently enforced by the
66
+ * normal provider rate limiter.
67
+ */
68
+ export function resolveByokProviderDailyCapEnforcementMode(configured = process.env[BYOK_PROVIDER_DAILY_CAP_ENFORCEMENT_ENV]) {
69
+ return configured === "enforce" ? "enforced" : "observe_only";
70
+ }
61
71
  /**
62
72
  * Implicit org-level DAILY budget guard, evaluated only when the org has no
63
73
  * explicit org-scope daily budget policy and the plan has finite positive
@@ -1,3 +1,3 @@
1
- export declare const OXYGEN_VERSION = "1.696.2";
1
+ export declare const OXYGEN_VERSION = "1.717.1";
2
2
  export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
3
3
  export declare const MANAGED_INBOX_MINIMUM_CLI_VERSION = "1.326.2";
@@ -1,4 +1,4 @@
1
- export const OXYGEN_VERSION = "1.696.2";
1
+ export const OXYGEN_VERSION = "1.717.1";
2
2
  // The GLOBAL CLI compatibility floor: the oldest CLI allowed to call any
3
3
  // operational route. Raising it hard-rejects every older CLI from the entire
4
4
  // product, so it obeys one law, enforced by scripts/ci/cli-min-version-gate.mjs:
@@ -21,6 +21,16 @@
21
21
  "import": "./dist/file-import.js",
22
22
  "default": "./dist/file-import.js"
23
23
  },
24
+ "./import-limits": {
25
+ "types": "./dist/import-limits.d.ts",
26
+ "import": "./dist/import-limits.js",
27
+ "default": "./dist/import-limits.js"
28
+ },
29
+ "./image-sniff": {
30
+ "types": "./dist/image-sniff.d.ts",
31
+ "import": "./dist/image-sniff.js",
32
+ "default": "./dist/image-sniff.js"
33
+ },
24
34
  "./mailbox-import": {
25
35
  "types": "./dist/mailbox-import.d.ts",
26
36
  "import": "./dist/mailbox-import.js",
@@ -36,6 +46,11 @@
36
46
  "import": "./dist/cell-format.js",
37
47
  "default": "./dist/cell-format.js"
38
48
  },
49
+ "./member-columns": {
50
+ "types": "./dist/member-columns.d.ts",
51
+ "import": "./dist/member-columns.js",
52
+ "default": "./dist/member-columns.js"
53
+ },
39
54
  "./column-types": {
40
55
  "types": "./dist/column-types.d.ts",
41
56
  "import": "./dist/column-types.js",
@@ -56,6 +71,11 @@
56
71
  "import": "./dist/suppression-entries.js",
57
72
  "default": "./dist/suppression-entries.js"
58
73
  },
74
+ "./dnc-identities": {
75
+ "types": "./dist/dnc-identities.d.ts",
76
+ "import": "./dist/dnc-identities.js",
77
+ "default": "./dist/dnc-identities.js"
78
+ },
59
79
  "./cli-result": {
60
80
  "types": "./dist/cli-result.d.ts",
61
81
  "import": "./dist/cli-result.js",