@oxygen-agent/cli 1.1014.1465 → 1.1014.1598
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/command-families/program.js +2 -0
- package/dist/command-manifest.js +12 -2
- package/dist/commands/budget.js +2 -2
- package/dist/commands/capabilities.js +3 -2
- package/dist/commands/columns.js +1 -1
- package/dist/commands/companies.js +3 -0
- package/dist/commands/domains.js +122 -5
- package/dist/commands/feedback.js +1 -1
- package/dist/commands/feeds.js +34 -17
- package/dist/commands/mailboxes.js +19 -9
- package/dist/commands/publishing.js +1 -1
- package/dist/commands/senders.js +2 -2
- package/dist/commands/sequences.js +75 -12
- package/dist/commands/subscriptions.d.ts +10 -0
- package/dist/commands/subscriptions.js +203 -0
- package/dist/commands/tags.js +4 -4
- package/dist/customer-operation-commands.js +17 -0
- package/dist/mailbox-fleet-output.d.ts +3 -0
- package/dist/mailbox-fleet-output.js +80 -0
- package/dist/native-chat-host.d.ts +2 -0
- package/dist/native-chat-host.js +6 -0
- package/dist/workspace-placement-commands.js +67 -2
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +59 -11
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +2 -2
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +18 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-activation.js +4 -4
- package/node_modules/@oxygen/shared/dist/contracts/native-chat.d.ts +120 -0
- package/node_modules/@oxygen/shared/dist/contracts/native-chat.js +35 -0
- package/node_modules/@oxygen/shared/dist/contracts/sender-limits.d.ts +9 -0
- package/node_modules/@oxygen/shared/dist/contracts/sender-limits.js +6 -1
- package/node_modules/@oxygen/shared/dist/contracts/sequence-list.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/contracts/sequence-list.js +2 -1
- package/node_modules/@oxygen/shared/dist/contracts/signal-search-compatibility.d.ts +78 -0
- package/node_modules/@oxygen/shared/dist/contracts/signal-search-compatibility.js +32 -0
- package/node_modules/@oxygen/shared/dist/dnc-rules.js +14 -0
- package/node_modules/@oxygen/shared/dist/email-delivery.d.ts +150 -0
- package/node_modules/@oxygen/shared/dist/email-delivery.js +246 -0
- package/node_modules/@oxygen/shared/dist/email-dsn.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/email-dsn.js +51 -14
- package/node_modules/@oxygen/shared/dist/email-health-summary.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/email-health-summary.js +28 -0
- package/node_modules/@oxygen/shared/dist/email-subscriptions.d.ts +50 -0
- package/node_modules/@oxygen/shared/dist/email-subscriptions.js +80 -0
- package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.d.ts +28 -0
- package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.js +47 -0
- package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +4 -16
- package/node_modules/@oxygen/shared/dist/error-redaction.js +8 -17
- package/node_modules/@oxygen/shared/dist/guided-activation.d.ts +15 -1
- package/node_modules/@oxygen/shared/dist/guided-activation.js +3 -1
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/mailbox-standby-rollout.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/mailbox-standby-rollout.js +9 -0
- package/node_modules/@oxygen/shared/dist/mailbox-standby.d.ts +43 -0
- package/node_modules/@oxygen/shared/dist/mailbox-standby.js +52 -0
- package/node_modules/@oxygen/shared/dist/native-chat-discovery.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/native-chat-discovery.js +27 -0
- package/node_modules/@oxygen/shared/dist/native-chat.d.ts +124 -0
- package/node_modules/@oxygen/shared/dist/native-chat.js +1 -0
- package/node_modules/@oxygen/shared/dist/onboarding-plays.d.ts +14 -3
- package/node_modules/@oxygen/shared/dist/onboarding-plays.js +26 -13
- package/node_modules/@oxygen/shared/dist/onboarding-research.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/onboarding-research.js +8 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/product-analytics-events.js +6 -0
- package/node_modules/@oxygen/shared/dist/select-options.js +1 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +6 -5
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/sequences.js +44 -2
- package/node_modules/@oxygen/shared/dist/tags.d.ts +7 -3
- package/node_modules/@oxygen/shared/dist/tags.js +31 -6
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/workspace-placement-bulk.d.ts +80 -0
- package/node_modules/@oxygen/shared/dist/workspace-placement-bulk.js +68 -0
- package/node_modules/@oxygen/shared/dist/workspace-placement-view.d.ts +2 -1
- package/node_modules/@oxygen/shared/dist/workspace-placement.d.ts +11 -1
- package/node_modules/@oxygen/shared/dist/workspace-placement.js +15 -4
- package/node_modules/@oxygen/shared/dist/write-rollout.d.ts +11 -0
- package/node_modules/@oxygen/shared/dist/write-rollout.js +59 -0
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +176 -2
- package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.js +50 -1
- package/node_modules/@oxygen/workflows/dist/index.js +108 -3
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Workspace tags (Tags primitive, ADR 0010; registry per ADR 0017): free-form
|
|
2
|
-
//
|
|
2
|
+
// case-preserving labels shared across primitives so campaign knowledge links across
|
|
3
3
|
// stores — a sequence, its campaign-learnings wiki page, the source table, and
|
|
4
4
|
// the workflow that feeds it can all carry `q3-outbound`. Carrier storage stays
|
|
5
5
|
// per-store (`tags text[]` + GIN, the 0009/0025 pattern); this module is the one
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// `ox_context.workspace_tags`) OR CARRIED (some store's tags array holds it).
|
|
11
11
|
// The registry is an ANNOTATION table — description, color override, pinned —
|
|
12
12
|
// and never records which items carry a tag. Drift is legal in both directions.
|
|
13
|
+
import { casePreservingTagWritesReady } from "./write-rollout.js";
|
|
13
14
|
/** Primitive kinds that participate in the workspace tag union today. */
|
|
14
15
|
export const TAG_KINDS = [
|
|
15
16
|
"knowledge_page",
|
|
@@ -120,12 +121,24 @@ export function tagKindHeading(kind) {
|
|
|
120
121
|
/** Hard cap per tagged item — matches the wiki's long-standing limit. */
|
|
121
122
|
export const MAX_TAGS_PER_ITEM = 50;
|
|
122
123
|
/**
|
|
123
|
-
* Normalize a raw tag array:
|
|
124
|
+
* Normalize a raw tag array: preserve case, trim, drop non-strings/empties, dedupe,
|
|
124
125
|
* cap at {@link MAX_TAGS_PER_ITEM}. Callers that must reject non-arrays with a
|
|
125
126
|
* typed error (the wiki does) validate the shape first and delegate here.
|
|
126
127
|
*/
|
|
127
128
|
export function normalizeTagList(values) {
|
|
128
|
-
|
|
129
|
+
const seen = new Set();
|
|
130
|
+
const result = [];
|
|
131
|
+
for (const value of values) {
|
|
132
|
+
const tag = typeof value === "string" ? value.trim() : "";
|
|
133
|
+
const key = tag.toLowerCase();
|
|
134
|
+
if (!tag || seen.has(key))
|
|
135
|
+
continue;
|
|
136
|
+
seen.add(key);
|
|
137
|
+
result.push(tag);
|
|
138
|
+
if (result.length === MAX_TAGS_PER_ITEM)
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
129
142
|
}
|
|
130
143
|
/** Lenient variant: `undefined`/`null`/non-arrays normalize to `[]`. */
|
|
131
144
|
export function normalizeTags(value) {
|
|
@@ -133,6 +146,19 @@ export function normalizeTags(value) {
|
|
|
133
146
|
return [];
|
|
134
147
|
return normalizeTagList(value);
|
|
135
148
|
}
|
|
149
|
+
/** Persistence-only normalization; browser label formatting does not activate writes. */
|
|
150
|
+
export function normalizeStoredTagList(values) {
|
|
151
|
+
const labels = normalizeTagList(values);
|
|
152
|
+
return casePreservingTagWritesReady() ? labels : labels.map(label => label.toLowerCase());
|
|
153
|
+
}
|
|
154
|
+
export function normalizeStoredTags(value) {
|
|
155
|
+
return Array.isArray(value) ? normalizeStoredTagList(value) : [];
|
|
156
|
+
}
|
|
157
|
+
/** Merge/rename targets must use the same rollout representation as ordinary writes. */
|
|
158
|
+
export function normalizeStoredTagLabel(value) {
|
|
159
|
+
const label = value.trim();
|
|
160
|
+
return casePreservingTagWritesReady() ? label : label.toLowerCase();
|
|
161
|
+
}
|
|
136
162
|
/**
|
|
137
163
|
* Registry-only caps (ADR 0017). Deliberately NOT enforced by
|
|
138
164
|
* {@link normalizeTagList}: that runs on every writer's hot path, so adding a
|
|
@@ -148,13 +174,12 @@ export const MAX_TAG_DESCRIPTION_LENGTH = 500;
|
|
|
148
174
|
* over {@link MAX_TAG_LABEL_LENGTH}. Returns `null` so callers can raise their
|
|
149
175
|
* own typed error.
|
|
150
176
|
*
|
|
151
|
-
*
|
|
152
|
-
* "q3 outbound", space intact. Any character is legal in a tag.
|
|
177
|
+
* Keeps the entered case and spaces; identity comparisons are case-insensitive. Any character is legal in a tag.
|
|
153
178
|
*/
|
|
154
179
|
export function normalizeTagLabel(value) {
|
|
155
180
|
if (typeof value !== "string")
|
|
156
181
|
return null;
|
|
157
|
-
const normalized = value.trim()
|
|
182
|
+
const normalized = value.trim();
|
|
158
183
|
if (!normalized || normalized.length > MAX_TAG_LABEL_LENGTH)
|
|
159
184
|
return null;
|
|
160
185
|
return normalized;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const OXYGEN_BUILD_VERSION = "1.1014.
|
|
1
|
+
export declare const OXYGEN_BUILD_VERSION = "1.1014.1598";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// GENERATED by scripts/ci/version-stamp.mjs from VERSION + git history. Do not edit; do not commit.
|
|
2
|
-
export const OXYGEN_BUILD_VERSION = "1.1014.
|
|
2
|
+
export const OXYGEN_BUILD_VERSION = "1.1014.1598";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type WorkspacePlacementMailbox, type WorkspacePlacementPlan, type WorkspacePlacementVariant } from "./workspace-placement.js";
|
|
2
|
+
import { type WorkspacePlacementView } from "./workspace-placement-view.js";
|
|
3
|
+
export declare const MAX_BULK_PLACEMENT_MESSAGES = 20000;
|
|
4
|
+
export type BulkPlacementSkippedSender = {
|
|
5
|
+
id: string;
|
|
6
|
+
address: string;
|
|
7
|
+
reason: string;
|
|
8
|
+
};
|
|
9
|
+
export type BulkPlacementCoverage = Record<"google" | "microsoft", {
|
|
10
|
+
receivers: number;
|
|
11
|
+
planned: number;
|
|
12
|
+
}>;
|
|
13
|
+
export type WorkspacePlacementBulkPlan = {
|
|
14
|
+
request_id: string;
|
|
15
|
+
organization_id: string;
|
|
16
|
+
plan_hash: string;
|
|
17
|
+
rendered_at: string;
|
|
18
|
+
message_count: number;
|
|
19
|
+
child_count: number;
|
|
20
|
+
sender_count: number;
|
|
21
|
+
receiver_providers: Array<"google" | "microsoft">;
|
|
22
|
+
skipped_senders: BulkPlacementSkippedSender[];
|
|
23
|
+
coverage: BulkPlacementCoverage;
|
|
24
|
+
children: WorkspacePlacementPlan[];
|
|
25
|
+
};
|
|
26
|
+
export type WorkspacePlacementBulkPreview = Omit<WorkspacePlacementBulkPlan, "organization_id" | "children" | "skipped_senders"> & {
|
|
27
|
+
skipped_senders: BulkPlacementSkippedSender[];
|
|
28
|
+
skipped_sender_count: number;
|
|
29
|
+
skipped_has_more: boolean;
|
|
30
|
+
next_skipped_cursor: string | null;
|
|
31
|
+
probes: Array<{
|
|
32
|
+
id: string;
|
|
33
|
+
sender_mailbox_id: string;
|
|
34
|
+
sender: string;
|
|
35
|
+
sender_name: string | null;
|
|
36
|
+
sender_provider: "google" | "microsoft";
|
|
37
|
+
sender_transport: string;
|
|
38
|
+
receiver_mailbox_id: string;
|
|
39
|
+
receiver: string;
|
|
40
|
+
receiver_provider: "google" | "microsoft";
|
|
41
|
+
variant: string;
|
|
42
|
+
subject: string;
|
|
43
|
+
body_text_characters: number;
|
|
44
|
+
body_html_characters: number;
|
|
45
|
+
}>;
|
|
46
|
+
probe_count: number;
|
|
47
|
+
has_more: boolean;
|
|
48
|
+
next_cursor: string | null;
|
|
49
|
+
};
|
|
50
|
+
export type WorkspacePlacementBulkView = Omit<WorkspacePlacementBulkPreview, "probes"> & {
|
|
51
|
+
id: string;
|
|
52
|
+
status: WorkspacePlacementView["status"];
|
|
53
|
+
created_at: string;
|
|
54
|
+
web_url: string;
|
|
55
|
+
variants: WorkspacePlacementView["variants"];
|
|
56
|
+
probes: WorkspacePlacementView["probes"];
|
|
57
|
+
directional_note: string;
|
|
58
|
+
};
|
|
59
|
+
export declare function bulkPlacementId(value: string): string;
|
|
60
|
+
export declare function readBulkPlacementProviders(value: unknown): Array<"google" | "microsoft">;
|
|
61
|
+
/** Own-account rotation, never a sender × receiver Cartesian product. */
|
|
62
|
+
export declare function buildWorkspacePlacementBulkPlan(input: {
|
|
63
|
+
requestId: string;
|
|
64
|
+
organizationId: string;
|
|
65
|
+
renderedAt: string;
|
|
66
|
+
senders: WorkspacePlacementMailbox[];
|
|
67
|
+
receivers: WorkspacePlacementMailbox[];
|
|
68
|
+
receiverProviders: Array<"google" | "microsoft">;
|
|
69
|
+
variants: WorkspacePlacementVariant[];
|
|
70
|
+
allowPausedProbe: boolean;
|
|
71
|
+
rendering: NonNullable<WorkspacePlacementPlan["rendering"]>;
|
|
72
|
+
skippedSenders?: BulkPlacementSkippedSender[];
|
|
73
|
+
renderVariant?: (variant: WorkspacePlacementVariant, sender: WorkspacePlacementMailbox, probeId: string) => WorkspacePlacementVariant;
|
|
74
|
+
}): WorkspacePlacementBulkPlan;
|
|
75
|
+
export declare function bulkPlacementSkippedPage(plan: WorkspacePlacementBulkPlan, cursor?: unknown): {
|
|
76
|
+
skipped_senders: BulkPlacementSkippedSender[];
|
|
77
|
+
skipped_sender_count: number;
|
|
78
|
+
skipped_has_more: boolean;
|
|
79
|
+
next_skipped_cursor: string | null;
|
|
80
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { CliHttpError } from "./cli-http-error.js";
|
|
3
|
+
import { readWorkspacePlacementVariants, workspacePlacementId } from "./workspace-placement.js";
|
|
4
|
+
export const MAX_BULK_PLACEMENT_MESSAGES = 20_000;
|
|
5
|
+
export function bulkPlacementId(value) {
|
|
6
|
+
const h = createHash("sha256").update(value).digest("hex");
|
|
7
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-a${h.slice(17, 20)}-${h.slice(20, 32)}`;
|
|
8
|
+
}
|
|
9
|
+
export function readBulkPlacementProviders(value) {
|
|
10
|
+
if (value === undefined)
|
|
11
|
+
return ["google", "microsoft"];
|
|
12
|
+
if (!Array.isArray(value) || !value.length || value.length > 2 || new Set(value).size !== value.length || value.some(p => p !== "google" && p !== "microsoft"))
|
|
13
|
+
throw new CliHttpError(400, "invalid_request", "receiver_providers must select Google, Microsoft, or both.");
|
|
14
|
+
return [...value].sort();
|
|
15
|
+
}
|
|
16
|
+
/** Own-account rotation, never a sender × receiver Cartesian product. */
|
|
17
|
+
export function buildWorkspacePlacementBulkPlan(input) {
|
|
18
|
+
if (!workspacePlacementId(input.requestId) || !workspacePlacementId(input.organizationId))
|
|
19
|
+
throw new CliHttpError(400, "workspace_test_invalid", "The deliverability test request is invalid.");
|
|
20
|
+
const requestId = input.requestId.toLowerCase(), organizationId = input.organizationId.toLowerCase();
|
|
21
|
+
const variants = readWorkspacePlacementVariants(input.variants), providers = readBulkPlacementProviders(input.receiverProviders);
|
|
22
|
+
const senders = [...input.senders].sort((a, b) => a.id.localeCompare(b.id));
|
|
23
|
+
if (!senders.length)
|
|
24
|
+
throw new CliHttpError(409, "workspace_test_no_eligible_senders", "No eligible sending mailboxes are available. Connect mailboxes with send permission or explicitly include paused mailboxes.", { details: { skipped_senders: input.skippedSenders?.slice(0, 20) ?? [], skipped_sender_count: input.skippedSenders?.length ?? 0 } });
|
|
25
|
+
const count = senders.length * variants.length * providers.length;
|
|
26
|
+
if (count > MAX_BULK_PLACEMENT_MESSAGES)
|
|
27
|
+
throw new CliHttpError(400, "workspace_test_bulk_limit", "This batch exceeds 20,000 messages. Choose fewer copy variants or receiving providers.");
|
|
28
|
+
const coverage = { google: { receivers: 0, planned: 0 }, microsoft: { receivers: 0, planned: 0 } };
|
|
29
|
+
const pools = new Map(providers.map(provider => {
|
|
30
|
+
const rows = [...input.receivers].filter(m => m.provider === provider).sort((a, b) => a.id.localeCompare(b.id));
|
|
31
|
+
coverage[provider].receivers = rows.length;
|
|
32
|
+
return [provider, rows];
|
|
33
|
+
}));
|
|
34
|
+
// Check coverage before rendering signatures/tracking. One sole own receiver
|
|
35
|
+
// cannot cover its own sender, even when that provider has a nonempty pool.
|
|
36
|
+
const missing = senders.flatMap(sender => providers.filter(p => !pools.get(p).some(r => r.id !== sender.id && r.address.toLowerCase() !== sender.address.toLowerCase())).map(provider => ({ sender: sender.address, provider })));
|
|
37
|
+
if (missing.length)
|
|
38
|
+
throw new CliHttpError(409, "workspace_test_receiver_coverage_missing", "Connected receiving mailboxes cannot cover every sender and requested provider without self-sends. Connect another receiving mailbox or change receiver_providers.", { details: { missing_coverage: missing.slice(0, 20), missing_coverage_count: missing.length, coverage } });
|
|
39
|
+
const probes = [];
|
|
40
|
+
let renderedBytes = 0;
|
|
41
|
+
for (const [senderIndex, sender] of senders.entries())
|
|
42
|
+
for (const [variantIndex, original] of variants.entries())
|
|
43
|
+
for (const provider of providers) {
|
|
44
|
+
const pool = pools.get(provider).filter(r => r.id !== sender.id && r.address.toLowerCase() !== sender.address.toLowerCase());
|
|
45
|
+
const receiver = pool[(senderIndex * variants.length + variantIndex) % pool.length];
|
|
46
|
+
const id = bulkPlacementId(`${organizationId}:${requestId}:${sender.id}:${receiver.id}:${original.key}`);
|
|
47
|
+
const variant = input.renderVariant ? input.renderVariant(original, sender, id) : original;
|
|
48
|
+
renderedBytes += Buffer.byteLength(JSON.stringify(variant));
|
|
49
|
+
if (renderedBytes > 32 * 1024 * 1024)
|
|
50
|
+
throw new CliHttpError(400, "workspace_test_bulk_copy_limit", "Rendered copy exceeds the batch review limit. Shorten the copy or choose fewer variants.");
|
|
51
|
+
probes.push({ id, sender, receiver, variant });
|
|
52
|
+
coverage[provider].planned++;
|
|
53
|
+
}
|
|
54
|
+
const children = [];
|
|
55
|
+
for (let offset = 0; offset < probes.length; offset += 50) {
|
|
56
|
+
const child = { request_id: bulkPlacementId(`${organizationId}:${requestId}:child:${offset / 50}`), organization_id: organizationId, provider: "internal", billing_mode: "internal", credits: 0, allow_paused_probe: input.allowPausedProbe, receiver_scope: "workspace", rendered_at: input.renderedAt, rendering: input.rendering, probes: probes.slice(offset, offset + 50), excluded_self_sends: 0, message_count: Math.min(50, probes.length - offset) };
|
|
57
|
+
children.push({ ...child, plan_hash: createHash("sha256").update(JSON.stringify(child)).digest("hex") });
|
|
58
|
+
}
|
|
59
|
+
const plan = { request_id: requestId, organization_id: organizationId, rendered_at: input.renderedAt, message_count: count, child_count: children.length, sender_count: senders.length, receiver_providers: providers, skipped_senders: [...(input.skippedSenders ?? [])].sort((a, b) => a.id.localeCompare(b.id)), coverage, children };
|
|
60
|
+
return { ...plan, plan_hash: createHash("sha256").update(JSON.stringify(plan)).digest("hex") };
|
|
61
|
+
}
|
|
62
|
+
export function bulkPlacementSkippedPage(plan, cursor) {
|
|
63
|
+
if (cursor !== undefined && (typeof cursor !== "string" || !plan.skipped_senders.some(s => s.id === cursor)))
|
|
64
|
+
throw new CliHttpError(400, "workspace_test_page_invalid", "Use the skipped-sender cursor from this deliverability test.");
|
|
65
|
+
const start = cursor === undefined ? 0 : plan.skipped_senders.findIndex(s => s.id === cursor) + 1;
|
|
66
|
+
const skipped = plan.skipped_senders.slice(start, start + 20), more = start + skipped.length < plan.skipped_senders.length;
|
|
67
|
+
return { skipped_senders: skipped, skipped_sender_count: plan.skipped_senders.length, skipped_has_more: more, next_skipped_cursor: more ? skipped.at(-1).id : null };
|
|
68
|
+
}
|
|
@@ -123,7 +123,8 @@ export declare function workspacePlacementPreviewPage(plan: WorkspacePlacementPl
|
|
|
123
123
|
billing_mode: "internal";
|
|
124
124
|
credits: 0;
|
|
125
125
|
allow_paused_probe: boolean;
|
|
126
|
-
receiver_scope?: "workspace" | "network";
|
|
126
|
+
receiver_scope?: "workspace" | "network" | "staff";
|
|
127
|
+
staff?: import("./workspace-placement.js").StaffPlacementGrant;
|
|
127
128
|
rendered_at?: string;
|
|
128
129
|
rendering?: {
|
|
129
130
|
tracking_opens: boolean;
|
|
@@ -13,6 +13,13 @@ export type WorkspacePlacementMailbox = {
|
|
|
13
13
|
signature_html: string | null;
|
|
14
14
|
organization_id?: string;
|
|
15
15
|
network_id?: string;
|
|
16
|
+
environment?: "dev" | "prod";
|
|
17
|
+
};
|
|
18
|
+
export type StaffPlacementGrant = {
|
|
19
|
+
actor_id: string;
|
|
20
|
+
source_environment: "dev" | "prod";
|
|
21
|
+
receiver_environment: "dev" | "prod";
|
|
22
|
+
receiver_workspace_id: string;
|
|
16
23
|
};
|
|
17
24
|
export type WorkspacePlacementProbe = {
|
|
18
25
|
id: string;
|
|
@@ -27,7 +34,8 @@ export type WorkspacePlacementPlan = {
|
|
|
27
34
|
billing_mode: "internal";
|
|
28
35
|
credits: 0;
|
|
29
36
|
allow_paused_probe: boolean;
|
|
30
|
-
receiver_scope?: "workspace" | "network";
|
|
37
|
+
receiver_scope?: "workspace" | "network" | "staff";
|
|
38
|
+
staff?: StaffPlacementGrant;
|
|
31
39
|
rendered_at?: string;
|
|
32
40
|
rendering?: {
|
|
33
41
|
tracking_opens: boolean;
|
|
@@ -42,6 +50,7 @@ export type WorkspacePlacementPlan = {
|
|
|
42
50
|
export declare function workspacePlacementId(value: unknown): value is string;
|
|
43
51
|
export declare function readWorkspacePlacementVariants(value: unknown): WorkspacePlacementVariant[];
|
|
44
52
|
export declare function readWorkspacePlacementMailboxIds(value: unknown): string[];
|
|
53
|
+
export declare function workspacePlacementPlanHash(plan: Omit<WorkspacePlacementPlan, "plan_hash">): string;
|
|
45
54
|
export declare function buildWorkspacePlacementPlan(input: {
|
|
46
55
|
requestId: string;
|
|
47
56
|
organizationId: string;
|
|
@@ -52,6 +61,7 @@ export declare function buildWorkspacePlacementPlan(input: {
|
|
|
52
61
|
renderedAt?: string;
|
|
53
62
|
rendering?: WorkspacePlacementPlan["rendering"];
|
|
54
63
|
network?: boolean;
|
|
64
|
+
staff?: StaffPlacementGrant;
|
|
55
65
|
renderVariant?: (variant: WorkspacePlacementVariant, sender: WorkspacePlacementMailbox, probeId: string) => WorkspacePlacementVariant;
|
|
56
66
|
}): WorkspacePlacementPlan;
|
|
57
67
|
export type WorkspacePlacementObservation = {
|
|
@@ -38,6 +38,16 @@ function probeId(value) {
|
|
|
38
38
|
const h = createHash("sha256").update(value).digest("hex");
|
|
39
39
|
return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-a${h.slice(17, 20)}-${h.slice(20, 32)}`;
|
|
40
40
|
}
|
|
41
|
+
function canonicalPlacementValue(value) {
|
|
42
|
+
if (Array.isArray(value))
|
|
43
|
+
return value.map(canonicalPlacementValue);
|
|
44
|
+
if (value && typeof value === "object")
|
|
45
|
+
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => [key, canonicalPlacementValue(item)]));
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
export function workspacePlacementPlanHash(plan) {
|
|
49
|
+
return createHash("sha256").update(JSON.stringify(plan.receiver_scope === "staff" ? canonicalPlacementValue(plan) : plan)).digest("hex");
|
|
50
|
+
}
|
|
41
51
|
export function buildWorkspacePlacementPlan(input) {
|
|
42
52
|
if (!workspacePlacementId(input.requestId) || !workspacePlacementId(input.organizationId))
|
|
43
53
|
invalid("The workspace test request is invalid.");
|
|
@@ -46,7 +56,8 @@ export function buildWorkspacePlacementPlan(input) {
|
|
|
46
56
|
const variants = readWorkspacePlacementVariants(input.variants);
|
|
47
57
|
const senders = [...input.senders].sort((a, b) => a.id.localeCompare(b.id));
|
|
48
58
|
const receivers = [...input.receivers].sort((a, b) => a.id.localeCompare(b.id));
|
|
49
|
-
const
|
|
59
|
+
const identity = (box) => `${box.environment ?? input.staff?.source_environment ?? ""}:${box.organization_id ?? organizationId}:${box.id}`;
|
|
60
|
+
const self = (sender, receiver) => identity(sender) === identity(receiver) || sender.address.toLowerCase() === receiver.address.toLowerCase();
|
|
50
61
|
let excluded = 0, pairs = 0;
|
|
51
62
|
for (const sender of senders)
|
|
52
63
|
for (const receiver of receivers) {
|
|
@@ -72,7 +83,7 @@ export function buildWorkspacePlacementPlan(input) {
|
|
|
72
83
|
if (group[sequence % group.length].id !== receiver.id)
|
|
73
84
|
continue;
|
|
74
85
|
}
|
|
75
|
-
const id = probeId(`${organizationId}:${requestId}:${sender.id}:${receiver.id}:${original.key}`);
|
|
86
|
+
const id = probeId(input.staff ? `${organizationId}:${requestId}:${identity(sender)}:${identity(receiver)}:${original.key}` : `${organizationId}:${requestId}:${sender.id}:${receiver.id}:${original.key}`);
|
|
76
87
|
const variant = input.renderVariant ? input.renderVariant(original, sender, id) : original;
|
|
77
88
|
probes.push({ id, sender, receiver, variant });
|
|
78
89
|
}
|
|
@@ -81,10 +92,10 @@ export function buildWorkspacePlacementPlan(input) {
|
|
|
81
92
|
if (!probes.length || probes.length > 50)
|
|
82
93
|
invalid("A workspace test must send between one and 50 messages after excluding self-sends.");
|
|
83
94
|
const plan = { request_id: requestId, organization_id: organizationId, provider: "internal",
|
|
84
|
-
...(input.network ? { receiver_scope: "network" } : {}), billing_mode: "internal", credits: 0, allow_paused_probe: input.allowPausedProbe,
|
|
95
|
+
...(input.network ? { receiver_scope: "network" } : input.staff ? { receiver_scope: "staff", staff: input.staff } : {}), billing_mode: "internal", credits: 0, allow_paused_probe: input.allowPausedProbe,
|
|
85
96
|
...(input.renderedAt ? { rendered_at: input.renderedAt } : {}), ...(input.rendering ? { rendering: input.rendering } : {}),
|
|
86
97
|
probes, excluded_self_sends: excluded, message_count: probes.length };
|
|
87
|
-
return { ...plan, plan_hash:
|
|
98
|
+
return { ...plan, plan_hash: workspacePlacementPlanHash(plan) };
|
|
88
99
|
}
|
|
89
100
|
export function googleWorkspacePlacement(labels, messageId, observedAt) {
|
|
90
101
|
const categories = ["PERSONAL", "PROMOTIONS", "SOCIAL", "UPDATES", "FORUMS"].filter(k => labels.includes(`CATEGORY_${k}`));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Writes become compatible only after every consuming role has adopted the new source. */
|
|
2
|
+
export declare function casePreservingTagWritesReady(env?: NodeJS.ProcessEnv): boolean;
|
|
3
|
+
export declare function campaignMetadataWritesReady(env?: NodeJS.ProcessEnv): boolean;
|
|
4
|
+
export declare function assertCampaignMetadataWritesReady(field: string, env?: NodeJS.ProcessEnv): void;
|
|
5
|
+
export declare function mailboxStandbyWritesReady(env?: NodeJS.ProcessEnv): boolean;
|
|
6
|
+
export declare function assertMailboxStandbyWritesReady(): void;
|
|
7
|
+
export declare function assertMailboxStandbyMetadataWritesReady(metadata: Record<string, unknown> | null | undefined): void;
|
|
8
|
+
export declare function staffPlacementWritesReady(env?: NodeJS.ProcessEnv): boolean;
|
|
9
|
+
export declare function assertStaffPlacementWritesReady(env?: NodeJS.ProcessEnv): void;
|
|
10
|
+
export declare function assertPublishingApprovalPolicyWriteReady(policy: unknown): void;
|
|
11
|
+
export declare function assertSequenceMetadataWritesReady(definition: unknown): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { OxygenError } from "./cli-result.js";
|
|
2
|
+
import { isProductionData } from "./deploy-env.js";
|
|
3
|
+
/** Writes become compatible only after every consuming role has adopted the new source. */
|
|
4
|
+
export function casePreservingTagWritesReady(env = process.env) {
|
|
5
|
+
return !isProductionData(env) || env.OXYGEN_CASE_PRESERVING_TAG_WRITES_READY === "1";
|
|
6
|
+
}
|
|
7
|
+
export function campaignMetadataWritesReady(env = process.env) {
|
|
8
|
+
return !isProductionData(env) || env.OXYGEN_CAMPAIGN_METADATA_WRITES_READY === "1";
|
|
9
|
+
}
|
|
10
|
+
export function assertCampaignMetadataWritesReady(field, env = process.env) {
|
|
11
|
+
if (campaignMetadataWritesReady(env))
|
|
12
|
+
return;
|
|
13
|
+
throw new OxygenError("feature_rollout_pending", "This setting is not available yet.", {
|
|
14
|
+
details: { field, next_step: "Try again after the workspace update is available." }, exitCode: 1,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export function mailboxStandbyWritesReady(env = process.env) {
|
|
18
|
+
return campaignMetadataWritesReady(env)
|
|
19
|
+
&& (!isProductionData(env) || env.OXYGEN_MAILBOX_STANDBY_WRITES_READY === "1");
|
|
20
|
+
}
|
|
21
|
+
export function assertMailboxStandbyWritesReady() {
|
|
22
|
+
if (mailboxStandbyWritesReady())
|
|
23
|
+
return;
|
|
24
|
+
throw new OxygenError("feature_rollout_pending", "Mailbox standby is not available yet.", {
|
|
25
|
+
details: { field: "mailbox_standby", next_step: "Try again after the workspace update is available." }, exitCode: 1,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function assertMailboxStandbyMetadataWritesReady(metadata) {
|
|
29
|
+
if (Object.hasOwn(metadata ?? {}, "outreach_role") || Object.hasOwn(metadata ?? {}, "standby_promotion")) {
|
|
30
|
+
assertMailboxStandbyWritesReady();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function staffPlacementWritesReady(env = process.env) {
|
|
34
|
+
return !isProductionData(env) || env.OXYGEN_STAFF_PLACEMENT_WRITES_READY === "1";
|
|
35
|
+
}
|
|
36
|
+
export function assertStaffPlacementWritesReady(env = process.env) {
|
|
37
|
+
if (staffPlacementWritesReady(env))
|
|
38
|
+
return;
|
|
39
|
+
throw new OxygenError("feature_rollout_pending", "Staff placement tests are not available yet.", {
|
|
40
|
+
details: { field: "staff_placement", next_step: "Try again after the workspace update is available." }, exitCode: 1,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export function assertPublishingApprovalPolicyWriteReady(policy) {
|
|
44
|
+
if (policy && typeof policy === "object" && "mode" in policy && policy.mode === "owner") {
|
|
45
|
+
assertCampaignMetadataWritesReady("publishing_approval");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function assertSequenceMetadataWritesReady(definition) {
|
|
49
|
+
if (!definition || typeof definition !== "object" || !("steps" in definition) || !Array.isArray(definition.steps))
|
|
50
|
+
return;
|
|
51
|
+
for (const step of definition.steps) {
|
|
52
|
+
if (!step || typeof step !== "object")
|
|
53
|
+
continue;
|
|
54
|
+
if (step.kind === "wait" && step.working_days === true)
|
|
55
|
+
assertCampaignMetadataWritesReady("working_days");
|
|
56
|
+
if (step.kind === "wait_for_connection" && step.withdraw_on_timeout === true)
|
|
57
|
+
assertCampaignMetadataWritesReady("withdraw_on_timeout");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -150,7 +150,7 @@ export declare const workflowCodeV1Schema: {
|
|
|
150
150
|
export declare const workflowGraphManifestSchema: {
|
|
151
151
|
readonly $schema: "https://json-schema.org/draft/2020-12/schema";
|
|
152
152
|
readonly title: "OXYGEN Workflow Graph Manifest (oxygen-workflows-v2)";
|
|
153
|
-
readonly description:
|
|
153
|
+
readonly description: string;
|
|
154
154
|
readonly type: "object";
|
|
155
155
|
readonly additionalProperties: true;
|
|
156
156
|
readonly properties: {
|
|
@@ -923,6 +923,93 @@ export declare const workflowGraphManifestSchema: {
|
|
|
923
923
|
};
|
|
924
924
|
};
|
|
925
925
|
readonly required: readonly ["manifest_version", "workflow", "nodes", "edges", "source_hash", "compiler_version"];
|
|
926
|
+
readonly examples: readonly [{
|
|
927
|
+
readonly manifest_version: 2;
|
|
928
|
+
readonly compiler_version: "oxygen-workflows-v2";
|
|
929
|
+
readonly workflow: {
|
|
930
|
+
readonly id: "form-signups";
|
|
931
|
+
readonly name: "Form signups";
|
|
932
|
+
readonly status: "disabled";
|
|
933
|
+
};
|
|
934
|
+
readonly trigger: {
|
|
935
|
+
readonly type: "webhook";
|
|
936
|
+
readonly trigger_id: "form-signup";
|
|
937
|
+
readonly status: "disabled";
|
|
938
|
+
readonly secret_required: true;
|
|
939
|
+
};
|
|
940
|
+
readonly input_schema: {
|
|
941
|
+
readonly type: "object";
|
|
942
|
+
readonly properties: {
|
|
943
|
+
readonly name: {
|
|
944
|
+
readonly type: "string";
|
|
945
|
+
};
|
|
946
|
+
readonly email: {
|
|
947
|
+
readonly type: "string";
|
|
948
|
+
};
|
|
949
|
+
};
|
|
950
|
+
readonly required: readonly ["name", "email"];
|
|
951
|
+
};
|
|
952
|
+
readonly nodes: readonly [{
|
|
953
|
+
readonly id: "trigger";
|
|
954
|
+
readonly name: "Form signup";
|
|
955
|
+
readonly kind: "trigger";
|
|
956
|
+
readonly ui: {
|
|
957
|
+
readonly x: 0;
|
|
958
|
+
readonly y: 0;
|
|
959
|
+
};
|
|
960
|
+
}, {
|
|
961
|
+
readonly id: "row";
|
|
962
|
+
readonly name: "Map row";
|
|
963
|
+
readonly kind: "set";
|
|
964
|
+
readonly ui: {
|
|
965
|
+
readonly x: 240;
|
|
966
|
+
readonly y: 0;
|
|
967
|
+
};
|
|
968
|
+
readonly fields: readonly [{
|
|
969
|
+
readonly key: "name";
|
|
970
|
+
readonly value: {
|
|
971
|
+
readonly type: "ref";
|
|
972
|
+
readonly path: "trigger.input.name";
|
|
973
|
+
};
|
|
974
|
+
}, {
|
|
975
|
+
readonly key: "email";
|
|
976
|
+
readonly value: {
|
|
977
|
+
readonly type: "ref";
|
|
978
|
+
readonly path: "trigger.input.email";
|
|
979
|
+
};
|
|
980
|
+
}];
|
|
981
|
+
}, {
|
|
982
|
+
readonly id: "add_row";
|
|
983
|
+
readonly name: "Add row";
|
|
984
|
+
readonly kind: "tool";
|
|
985
|
+
readonly ui: {
|
|
986
|
+
readonly x: 480;
|
|
987
|
+
readonly y: 0;
|
|
988
|
+
};
|
|
989
|
+
readonly tool: "oxygen_table_rows_insert";
|
|
990
|
+
readonly effect: "external_write";
|
|
991
|
+
readonly params: {
|
|
992
|
+
readonly table: {
|
|
993
|
+
readonly type: "literal";
|
|
994
|
+
readonly value: "signups";
|
|
995
|
+
};
|
|
996
|
+
readonly values: {
|
|
997
|
+
readonly type: "ref";
|
|
998
|
+
readonly path: "steps.row.output";
|
|
999
|
+
};
|
|
1000
|
+
};
|
|
1001
|
+
}];
|
|
1002
|
+
readonly edges: readonly [{
|
|
1003
|
+
readonly id: "trigger-row";
|
|
1004
|
+
readonly source: "trigger";
|
|
1005
|
+
readonly target: "row";
|
|
1006
|
+
}, {
|
|
1007
|
+
readonly id: "row-add_row";
|
|
1008
|
+
readonly source: "row";
|
|
1009
|
+
readonly target: "add_row";
|
|
1010
|
+
}];
|
|
1011
|
+
readonly source_hash: "draft";
|
|
1012
|
+
}];
|
|
926
1013
|
};
|
|
927
1014
|
export declare const portableWorkflowDefinitionSchema: {
|
|
928
1015
|
readonly $schema: "https://json-schema.org/draft/2020-12/schema";
|
|
@@ -940,7 +1027,7 @@ export declare const portableWorkflowDefinitionSchema: {
|
|
|
940
1027
|
readonly graph: {
|
|
941
1028
|
readonly $schema: "https://json-schema.org/draft/2020-12/schema";
|
|
942
1029
|
readonly title: "OXYGEN Workflow Graph Manifest (oxygen-workflows-v2)";
|
|
943
|
-
readonly description:
|
|
1030
|
+
readonly description: string;
|
|
944
1031
|
readonly type: "object";
|
|
945
1032
|
readonly additionalProperties: true;
|
|
946
1033
|
readonly properties: {
|
|
@@ -1713,6 +1800,93 @@ export declare const portableWorkflowDefinitionSchema: {
|
|
|
1713
1800
|
};
|
|
1714
1801
|
};
|
|
1715
1802
|
readonly required: readonly ["manifest_version", "workflow", "nodes", "edges", "source_hash", "compiler_version"];
|
|
1803
|
+
readonly examples: readonly [{
|
|
1804
|
+
readonly manifest_version: 2;
|
|
1805
|
+
readonly compiler_version: "oxygen-workflows-v2";
|
|
1806
|
+
readonly workflow: {
|
|
1807
|
+
readonly id: "form-signups";
|
|
1808
|
+
readonly name: "Form signups";
|
|
1809
|
+
readonly status: "disabled";
|
|
1810
|
+
};
|
|
1811
|
+
readonly trigger: {
|
|
1812
|
+
readonly type: "webhook";
|
|
1813
|
+
readonly trigger_id: "form-signup";
|
|
1814
|
+
readonly status: "disabled";
|
|
1815
|
+
readonly secret_required: true;
|
|
1816
|
+
};
|
|
1817
|
+
readonly input_schema: {
|
|
1818
|
+
readonly type: "object";
|
|
1819
|
+
readonly properties: {
|
|
1820
|
+
readonly name: {
|
|
1821
|
+
readonly type: "string";
|
|
1822
|
+
};
|
|
1823
|
+
readonly email: {
|
|
1824
|
+
readonly type: "string";
|
|
1825
|
+
};
|
|
1826
|
+
};
|
|
1827
|
+
readonly required: readonly ["name", "email"];
|
|
1828
|
+
};
|
|
1829
|
+
readonly nodes: readonly [{
|
|
1830
|
+
readonly id: "trigger";
|
|
1831
|
+
readonly name: "Form signup";
|
|
1832
|
+
readonly kind: "trigger";
|
|
1833
|
+
readonly ui: {
|
|
1834
|
+
readonly x: 0;
|
|
1835
|
+
readonly y: 0;
|
|
1836
|
+
};
|
|
1837
|
+
}, {
|
|
1838
|
+
readonly id: "row";
|
|
1839
|
+
readonly name: "Map row";
|
|
1840
|
+
readonly kind: "set";
|
|
1841
|
+
readonly ui: {
|
|
1842
|
+
readonly x: 240;
|
|
1843
|
+
readonly y: 0;
|
|
1844
|
+
};
|
|
1845
|
+
readonly fields: readonly [{
|
|
1846
|
+
readonly key: "name";
|
|
1847
|
+
readonly value: {
|
|
1848
|
+
readonly type: "ref";
|
|
1849
|
+
readonly path: "trigger.input.name";
|
|
1850
|
+
};
|
|
1851
|
+
}, {
|
|
1852
|
+
readonly key: "email";
|
|
1853
|
+
readonly value: {
|
|
1854
|
+
readonly type: "ref";
|
|
1855
|
+
readonly path: "trigger.input.email";
|
|
1856
|
+
};
|
|
1857
|
+
}];
|
|
1858
|
+
}, {
|
|
1859
|
+
readonly id: "add_row";
|
|
1860
|
+
readonly name: "Add row";
|
|
1861
|
+
readonly kind: "tool";
|
|
1862
|
+
readonly ui: {
|
|
1863
|
+
readonly x: 480;
|
|
1864
|
+
readonly y: 0;
|
|
1865
|
+
};
|
|
1866
|
+
readonly tool: "oxygen_table_rows_insert";
|
|
1867
|
+
readonly effect: "external_write";
|
|
1868
|
+
readonly params: {
|
|
1869
|
+
readonly table: {
|
|
1870
|
+
readonly type: "literal";
|
|
1871
|
+
readonly value: "signups";
|
|
1872
|
+
};
|
|
1873
|
+
readonly values: {
|
|
1874
|
+
readonly type: "ref";
|
|
1875
|
+
readonly path: "steps.row.output";
|
|
1876
|
+
};
|
|
1877
|
+
};
|
|
1878
|
+
}];
|
|
1879
|
+
readonly edges: readonly [{
|
|
1880
|
+
readonly id: "trigger-row";
|
|
1881
|
+
readonly source: "trigger";
|
|
1882
|
+
readonly target: "row";
|
|
1883
|
+
}, {
|
|
1884
|
+
readonly id: "row-add_row";
|
|
1885
|
+
readonly source: "row";
|
|
1886
|
+
readonly target: "add_row";
|
|
1887
|
+
}];
|
|
1888
|
+
readonly source_hash: "draft";
|
|
1889
|
+
}];
|
|
1716
1890
|
};
|
|
1717
1891
|
readonly requirements: {
|
|
1718
1892
|
readonly type: "object";
|