@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.
- package/README.md +1 -1
- package/dist/help.js +2 -0
- package/dist/http-client.js +11 -1
- package/dist/index.js +524 -86
- package/dist/transcript.js +2 -2
- package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/billing.js +7 -6
- package/node_modules/@oxygen/shared/dist/dnc-identities.d.ts +53 -0
- package/node_modules/@oxygen/shared/dist/dnc-identities.js +175 -0
- package/node_modules/@oxygen/shared/dist/file-import.d.ts +36 -1
- package/node_modules/@oxygen/shared/dist/file-import.js +80 -1
- package/node_modules/@oxygen/shared/dist/image-sniff.d.ts +39 -0
- package/node_modules/@oxygen/shared/dist/image-sniff.js +75 -0
- package/node_modules/@oxygen/shared/dist/import-limits.d.ts +28 -0
- package/node_modules/@oxygen/shared/dist/import-limits.js +30 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/index.js +2 -0
- package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +10 -4
- package/node_modules/@oxygen/shared/dist/mailbox-import.js +18 -10
- package/node_modules/@oxygen/shared/dist/member-columns.d.ts +64 -0
- package/node_modules/@oxygen/shared/dist/member-columns.js +111 -0
- package/node_modules/@oxygen/shared/dist/microsoft-consent-url.js +1 -1
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +65 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +100 -0
- package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +2 -1
- package/node_modules/@oxygen/shared/dist/plan-limits.js +12 -2
- package/node_modules/@oxygen/shared/dist/spend-safety.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/spend-safety.js +10 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/package.json +20 -0
- package/node_modules/@oxygen/workflows/dist/graph/lint.js +79 -11
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +679 -0
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.js +80 -1
- package/node_modules/@oxygen/workflows/dist/graph/remap.js +8 -1
- package/node_modules/@oxygen/workflows/dist/graph/types.d.ts +72 -3
- package/node_modules/@oxygen/workflows/dist/graph/types.js +12 -0
- package/node_modules/@oxygen/workflows/dist/index.d.ts +16 -0
- package/node_modules/@oxygen/workflows/dist/index.js +45 -3
- package/node_modules/@oxygen/workflows/dist/portable.d.ts +43 -0
- package/node_modules/@oxygen/workflows/dist/portable.js +319 -0
- package/node_modules/@oxygen/workflows/dist/usage-estimate.js +18 -4
- 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
|
|
79
|
-
*
|
|
80
|
-
* provider counts, performs no directory discovery, and does not
|
|
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
|
|
212
|
-
*
|
|
213
|
-
* provider counts, performs no directory discovery, and does not
|
|
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
|
|
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 /
|
|
234
|
-
const remainder = mailboxes %
|
|
235
|
-
const batchSizes = Array.from({ length: fullBatches }, () =>
|
|
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:
|
|
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
|
-
|
|
38
|
-
|
|
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,4 +1,4 @@
|
|
|
1
|
-
export const OXYGEN_VERSION = "1.
|
|
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",
|