@oxygen-agent/cli 1.1014.1035 → 1.1014.1132
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-manifest.js +1 -0
- package/dist/commands/columns.js +1 -1
- package/dist/commands/deliverability.js +2 -0
- package/dist/commands/sequences.js +30 -2
- package/dist/commands/viewers.js +1 -2
- package/dist/credentials.js +1 -1
- package/dist/customer-operation-commands.js +62 -3
- package/dist/help.js +1 -1
- package/dist/index.d.ts +0 -97
- package/dist/index.js +0 -98
- package/dist/workspace-placement-commands.d.ts +3 -0
- package/dist/workspace-placement-commands.js +42 -0
- package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +6 -1
- package/node_modules/@oxygen/shared/dist/agent-sessions.js +18 -6
- package/node_modules/@oxygen/shared/dist/agents-enabled.d.ts +8 -9
- package/node_modules/@oxygen/shared/dist/agents-enabled.js +12 -8
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +24 -6
- package/node_modules/@oxygen/shared/dist/column-autofill.js +2 -2
- package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/guided-activation.js +5 -5
- package/node_modules/@oxygen/shared/dist/contracts/guided-company-files.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-company-files.js +53 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-first-sourcing.d.ts +120 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-first-sourcing.js +48 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-onboarding-journey.d.ts +267 -0
- package/node_modules/@oxygen/shared/dist/contracts/guided-onboarding-journey.js +63 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +9 -4
- package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.js +3 -0
- package/node_modules/@oxygen/shared/dist/contracts/teammates.js +9 -8
- package/node_modules/@oxygen/shared/dist/data-suppliers.d.ts +32 -2
- package/node_modules/@oxygen/shared/dist/data-suppliers.js +44 -4
- package/node_modules/@oxygen/shared/dist/email-tracking-token.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/email-tracking-token.js +1 -0
- package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +3 -4
- package/node_modules/@oxygen/shared/dist/feature-gates.js +3 -4
- package/node_modules/@oxygen/shared/dist/file-import.d.ts +46 -0
- package/node_modules/@oxygen/shared/dist/file-import.js +138 -0
- package/node_modules/@oxygen/shared/dist/guided-activation.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/guided-company-files.d.ts +13 -0
- package/node_modules/@oxygen/shared/dist/guided-company-files.js +14 -0
- package/node_modules/@oxygen/shared/dist/guided-first-sourcing.d.ts +116 -0
- package/node_modules/@oxygen/shared/dist/guided-first-sourcing.js +11 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-answers.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-answers.js +42 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-company-context.d.ts +22 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-company-context.js +38 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.d.ts +147 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.fixture.d.ts +12 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.fixture.js +83 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.js +44 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-prospects.d.ts +7 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-prospects.js +13 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-signals.d.ts +4 -0
- package/node_modules/@oxygen/shared/dist/guided-onboarding-signals.js +34 -0
- package/node_modules/@oxygen/shared/dist/guided-trial-billing.d.ts +26 -0
- package/node_modules/@oxygen/shared/dist/guided-trial-billing.js +19 -0
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +108 -118
- package/node_modules/@oxygen/shared/dist/knowledge-links.d.ts +29 -0
- package/node_modules/@oxygen/shared/dist/knowledge-links.js +84 -1
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +5 -14
- package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +22 -5
- package/node_modules/@oxygen/shared/dist/managed-scraper-backend.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/managed-scraper-backend.js +17 -0
- package/node_modules/@oxygen/shared/dist/teammates.d.ts +7 -5
- package/node_modules/@oxygen/shared/dist/teammates.js +11 -20
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +88 -0
- 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-view.d.ts +150 -0
- package/node_modules/@oxygen/shared/dist/workspace-placement-view.js +102 -0
- package/node_modules/@oxygen/shared/dist/workspace-placement.d.ts +84 -0
- package/node_modules/@oxygen/shared/dist/workspace-placement.js +107 -0
- package/package.json +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { CliHttpError } from "./cli-http-error.js";
|
|
2
|
+
import { placementReceiverLabel } from "./workspace-placement.js";
|
|
3
|
+
export function workspacePlacementView(input, webUrl) {
|
|
4
|
+
const variants = new Map();
|
|
5
|
+
const probes = input.plan.probes.map(p => {
|
|
6
|
+
let v = variants.get(p.variant.key);
|
|
7
|
+
if (!v) {
|
|
8
|
+
v = { key: p.variant.key, planned: 0, observed: 0, google_primary: 0, google_promotions: 0, google_social: 0, google_updates: 0, google_forums: 0, microsoft_focused: 0, microsoft_other: 0, spam: 0, other_folder: 0, unknown_folder: 0, unknown_category: 0, not_observed: 0, failed: 0, send_unknown: 0, read_blocked: 0 };
|
|
9
|
+
variants.set(v.key, v);
|
|
10
|
+
}
|
|
11
|
+
v.planned++;
|
|
12
|
+
const out = input.outcomes[p.id];
|
|
13
|
+
const o = out?.observation;
|
|
14
|
+
if (o) {
|
|
15
|
+
v.observed++;
|
|
16
|
+
if (o.folder === "spam")
|
|
17
|
+
v.spam++;
|
|
18
|
+
else if (o.folder === "other")
|
|
19
|
+
v.other_folder++;
|
|
20
|
+
else if (o.folder === "unknown")
|
|
21
|
+
v.unknown_folder++;
|
|
22
|
+
else if (o.folder === "inbox") {
|
|
23
|
+
if (p.receiver.provider === "google" && o.category === "primary")
|
|
24
|
+
v.google_primary++;
|
|
25
|
+
else if (p.receiver.provider === "google" && o.category === "promotions")
|
|
26
|
+
v.google_promotions++;
|
|
27
|
+
else if (p.receiver.provider === "google" && o.category === "social")
|
|
28
|
+
v.google_social++;
|
|
29
|
+
else if (p.receiver.provider === "google" && o.category === "updates")
|
|
30
|
+
v.google_updates++;
|
|
31
|
+
else if (p.receiver.provider === "google" && o.category === "forums")
|
|
32
|
+
v.google_forums++;
|
|
33
|
+
else if (p.receiver.provider === "microsoft" && o.category === "focused")
|
|
34
|
+
v.microsoft_focused++;
|
|
35
|
+
else if (p.receiver.provider === "microsoft" && o.category === "other")
|
|
36
|
+
v.microsoft_other++;
|
|
37
|
+
else if (o.category === "unknown")
|
|
38
|
+
v.unknown_category++;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else if (out?.state === "not_observed")
|
|
42
|
+
v.not_observed++;
|
|
43
|
+
else if (out?.state === "send_failed")
|
|
44
|
+
v.failed++;
|
|
45
|
+
else if (out?.state === "send_unknown")
|
|
46
|
+
v.send_unknown++;
|
|
47
|
+
else if (out?.state === "read_blocked")
|
|
48
|
+
v.read_blocked++;
|
|
49
|
+
const state = out?.state ?? (input.deliveries?.[p.id]?.status === "sent" ? "awaiting_results" : input.deliveries?.[p.id]?.status ?? "pending");
|
|
50
|
+
return { ...workspacePlacementProbeMeta(p), state, status_label: workspacePlacementStateLabel(state), ...(input.deliveries?.[p.id] ? { delivery: input.deliveries[p.id] } : {}), ...(o ? { observation: p.receiver.network_id ? { ...o, receiver_message_id: "private" } : o } : {}) };
|
|
51
|
+
});
|
|
52
|
+
const settled = input.plan.probes.filter(p => input.outcomes[p.id]).length;
|
|
53
|
+
return { id: input.id, provider: "internal", billing_mode: "internal", credits_used: 0, status: settled < probes.length ? "running" : probes.every(p => p.state === "observed") ? "completed" : "incomplete",
|
|
54
|
+
created_at: new Date(input.created_at).toISOString(), message_count: probes.length, variants: [...variants.values()], probes, plan_hash: input.plan.plan_hash, web_url: webUrl,
|
|
55
|
+
directional_note: "Observed placement in participating receiving accounts; not a campaign placement guarantee. Primary and Focused are separate provider classifications." };
|
|
56
|
+
}
|
|
57
|
+
export function workspacePlacementStateLabel(state) {
|
|
58
|
+
return { pending: "Queued", leased: "Preparing send", sent: "Awaiting receiver observation", awaiting_results: "Awaiting receiver observation", observed: "Observed", not_observed: "Not observed within two hours", send_failed: "Send failed", send_unknown: "Send outcome unknown; do not resend", read_blocked: "Receiver could not be checked", uncertain: "Send outcome unknown; do not resend", canceled: "Send canceled", failed: "Send failed" }[state] ?? "Pending";
|
|
59
|
+
}
|
|
60
|
+
function pageNumber(value, fallback, max, field) {
|
|
61
|
+
if (value === undefined)
|
|
62
|
+
return fallback;
|
|
63
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > max)
|
|
64
|
+
throw new CliHttpError(400, "workspace_test_page_invalid", `${field} must be an integer between 0 and ${max}.`);
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
export function workspacePlacementProbeMeta(probe) {
|
|
68
|
+
return { id: probe.id, sender: probe.sender.address, sender_name: probe.sender.display_name ?? null, receiver: placementReceiverLabel(probe.receiver), receiver_provider: probe.receiver.provider, variant: probe.variant.key, subject: probe.variant.subject,
|
|
69
|
+
body_text_characters: Array.from(probe.variant.body_text).length, body_html_characters: Array.from(probe.variant.body_html ?? "").length };
|
|
70
|
+
}
|
|
71
|
+
export function workspacePlacementCopyPage(plan, input) {
|
|
72
|
+
const id = typeof input.probeId === "string" ? input.probeId.toLowerCase() : null;
|
|
73
|
+
const probe = plan.probes.find(p => p.id === id);
|
|
74
|
+
if (!probe)
|
|
75
|
+
throw new CliHttpError(400, "workspace_test_page_invalid", "Choose a probe from this workspace test.");
|
|
76
|
+
const part = input.part ?? "body_text";
|
|
77
|
+
if (part !== "body_text" && part !== "body_html")
|
|
78
|
+
throw new CliHttpError(400, "workspace_test_page_invalid", "body_part must be body_text or body_html.");
|
|
79
|
+
const chars = Array.from(probe.variant[part] ?? "");
|
|
80
|
+
const offset = pageNumber(input.offset, 0, chars.length, "offset");
|
|
81
|
+
const limit = pageNumber(input.limit, 3000, 3000, "copy_limit");
|
|
82
|
+
if (limit < 1)
|
|
83
|
+
throw new CliHttpError(400, "workspace_test_page_invalid", "copy_limit must be at least 1.");
|
|
84
|
+
const end = Math.min(chars.length, offset + limit);
|
|
85
|
+
return { id: probe.id, variant: probe.variant.key, sender: probe.sender.address, sender_name: probe.sender.display_name ?? null, receiver: placementReceiverLabel(probe.receiver), subject: probe.variant.subject, part, offset, total_characters: chars.length, text: chars.slice(offset, end).join(""), has_more: end < chars.length, next_offset: end < chars.length ? end : null };
|
|
86
|
+
}
|
|
87
|
+
export function workspacePlacementProbePage(probes, input, defaultLimit = 10) {
|
|
88
|
+
const limit = pageNumber(input.limit, defaultLimit, 20, "limit");
|
|
89
|
+
if (limit < 1)
|
|
90
|
+
throw new CliHttpError(400, "workspace_test_page_invalid", "limit must be at least 1.");
|
|
91
|
+
const cursor = typeof input.cursor === "string" ? input.cursor.toLowerCase() : null;
|
|
92
|
+
const anchor = cursor === null ? -1 : probes.findIndex(p => p.id === cursor);
|
|
93
|
+
if (input.cursor !== undefined && anchor < 0)
|
|
94
|
+
throw new CliHttpError(400, "workspace_test_page_invalid", "Use a cursor from this workspace test.");
|
|
95
|
+
const rows = probes.slice(anchor + 1, anchor + 1 + limit);
|
|
96
|
+
const more = anchor + 1 + rows.length < probes.length;
|
|
97
|
+
return { probes: rows, probe_count: probes.length, has_more: more, next_cursor: more ? rows.at(-1).id : null };
|
|
98
|
+
}
|
|
99
|
+
export function workspacePlacementPreviewPage(plan, input = {}) {
|
|
100
|
+
const { probes: _probes, ...metadata } = plan;
|
|
101
|
+
return { ...metadata, ...workspacePlacementProbePage(plan.probes.map(workspacePlacementProbeMeta), input, 5) };
|
|
102
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export type WorkspacePlacementVariant = {
|
|
2
|
+
key: string;
|
|
3
|
+
subject: string;
|
|
4
|
+
body_text: string;
|
|
5
|
+
body_html?: string;
|
|
6
|
+
};
|
|
7
|
+
export type WorkspacePlacementMailbox = {
|
|
8
|
+
id: string;
|
|
9
|
+
address: string;
|
|
10
|
+
provider: "google" | "microsoft";
|
|
11
|
+
transport: string;
|
|
12
|
+
display_name?: string | null;
|
|
13
|
+
signature_html: string | null;
|
|
14
|
+
organization_id?: string;
|
|
15
|
+
network_id?: string;
|
|
16
|
+
};
|
|
17
|
+
export type WorkspacePlacementProbe = {
|
|
18
|
+
id: string;
|
|
19
|
+
sender: WorkspacePlacementMailbox;
|
|
20
|
+
receiver: WorkspacePlacementMailbox;
|
|
21
|
+
variant: WorkspacePlacementVariant;
|
|
22
|
+
};
|
|
23
|
+
export type WorkspacePlacementPlan = {
|
|
24
|
+
request_id: string;
|
|
25
|
+
organization_id: string;
|
|
26
|
+
provider: "internal";
|
|
27
|
+
billing_mode: "internal";
|
|
28
|
+
credits: 0;
|
|
29
|
+
allow_paused_probe: boolean;
|
|
30
|
+
receiver_scope?: "workspace" | "network";
|
|
31
|
+
rendered_at?: string;
|
|
32
|
+
rendering?: {
|
|
33
|
+
tracking_opens: boolean;
|
|
34
|
+
tracking_clicks: boolean;
|
|
35
|
+
tracking_host: string | null;
|
|
36
|
+
};
|
|
37
|
+
probes: WorkspacePlacementProbe[];
|
|
38
|
+
excluded_self_sends: number;
|
|
39
|
+
message_count: number;
|
|
40
|
+
plan_hash: string;
|
|
41
|
+
};
|
|
42
|
+
export declare function workspacePlacementId(value: unknown): value is string;
|
|
43
|
+
export declare function readWorkspacePlacementVariants(value: unknown): WorkspacePlacementVariant[];
|
|
44
|
+
export declare function readWorkspacePlacementMailboxIds(value: unknown): string[];
|
|
45
|
+
export declare function buildWorkspacePlacementPlan(input: {
|
|
46
|
+
requestId: string;
|
|
47
|
+
organizationId: string;
|
|
48
|
+
senders: WorkspacePlacementMailbox[];
|
|
49
|
+
receivers: WorkspacePlacementMailbox[];
|
|
50
|
+
variants: WorkspacePlacementVariant[];
|
|
51
|
+
allowPausedProbe: boolean;
|
|
52
|
+
renderedAt?: string;
|
|
53
|
+
rendering?: WorkspacePlacementPlan["rendering"];
|
|
54
|
+
network?: boolean;
|
|
55
|
+
renderVariant?: (variant: WorkspacePlacementVariant, sender: WorkspacePlacementMailbox, probeId: string) => WorkspacePlacementVariant;
|
|
56
|
+
}): WorkspacePlacementPlan;
|
|
57
|
+
export type WorkspacePlacementObservation = {
|
|
58
|
+
folder: "inbox" | "spam" | "other" | "unknown";
|
|
59
|
+
category: "primary" | "promotions" | "social" | "updates" | "forums" | "focused" | "other" | "unknown";
|
|
60
|
+
observed_at: string;
|
|
61
|
+
receiver_message_id: string;
|
|
62
|
+
};
|
|
63
|
+
export declare function googleWorkspacePlacement(labels: string[], messageId: string, observedAt: string): WorkspacePlacementObservation;
|
|
64
|
+
export declare function microsoftWorkspacePlacement(input: {
|
|
65
|
+
messageId: string;
|
|
66
|
+
parentFolderId: string | null;
|
|
67
|
+
inboxId: string;
|
|
68
|
+
junkId: string;
|
|
69
|
+
classification: unknown;
|
|
70
|
+
observedAt: string;
|
|
71
|
+
}): WorkspacePlacementObservation;
|
|
72
|
+
/** One exact payload owner for preview, transactional enqueue and drain verification. */
|
|
73
|
+
export declare function workspacePlacementPayload(probe: WorkspacePlacementProbe): {
|
|
74
|
+
kind: "email_send";
|
|
75
|
+
to: string;
|
|
76
|
+
subject: string;
|
|
77
|
+
body: string;
|
|
78
|
+
body_html?: string;
|
|
79
|
+
apply_mailbox_signature: boolean;
|
|
80
|
+
diagnostic_probe_id: string;
|
|
81
|
+
};
|
|
82
|
+
/** Foreign identities remain private even in previews, copy pages and manifests. */
|
|
83
|
+
export declare function placementReceiverLabel(receiver: WorkspacePlacementMailbox): string;
|
|
84
|
+
export declare function publicWorkspacePlacementPlan(plan: WorkspacePlacementPlan): WorkspacePlacementPlan;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { CliHttpError } from "./cli-http-error.js";
|
|
3
|
+
const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;
|
|
4
|
+
export function workspacePlacementId(value) { return typeof value === "string" && UUID.test(value); }
|
|
5
|
+
function invalid(message) { throw new CliHttpError(400, "workspace_test_invalid", message); }
|
|
6
|
+
export function readWorkspacePlacementVariants(value) {
|
|
7
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 5)
|
|
8
|
+
invalid("Choose between one and five copy variants.");
|
|
9
|
+
const keys = new Set();
|
|
10
|
+
return value.map(raw => {
|
|
11
|
+
if (!raw || typeof raw !== "object")
|
|
12
|
+
invalid("Each variant needs a key, subject and body_text.");
|
|
13
|
+
const v = raw;
|
|
14
|
+
if (typeof v.key !== "string" || !/^[a-zA-Z0-9_-]{1,40}$/.test(v.key) || keys.has(v.key))
|
|
15
|
+
invalid("Variant keys must be unique short names using letters, numbers, underscores or hyphens.");
|
|
16
|
+
if (typeof v.subject !== "string" || !v.subject.trim() || /[\r\n]/.test(v.subject) || v.subject.length > 998)
|
|
17
|
+
invalid("Each subject must be a non-empty single line, at most 998 characters.");
|
|
18
|
+
if (typeof v.body_text !== "string" || !v.body_text.trim() || v.body_text.length > 100_000)
|
|
19
|
+
invalid("Each body_text must contain between one and 100,000 characters.");
|
|
20
|
+
if (v.body_html !== undefined && (typeof v.body_html !== "string" || !v.body_html.trim() || v.body_html.length > 200_000))
|
|
21
|
+
invalid("body_html must contain between one and 200,000 characters when supplied.");
|
|
22
|
+
if (/\{\{[^}]+\}\}/.test(v.subject + v.body_text + (v.body_html ?? "")))
|
|
23
|
+
invalid("Resolve personalization variables before testing the exact copy.");
|
|
24
|
+
keys.add(v.key);
|
|
25
|
+
return { key: v.key, subject: v.subject, body_text: v.body_text, ...(typeof v.body_html === "string" ? { body_html: v.body_html } : {}) };
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function readWorkspacePlacementMailboxIds(value) {
|
|
29
|
+
if (!Array.isArray(value) || !value.length || value.length > 50 || value.some(v => !workspacePlacementId(v)))
|
|
30
|
+
invalid("Select one to 50 exact mailbox IDs from this workspace.");
|
|
31
|
+
return [...new Set(value.map(id => id.toLowerCase()))].sort();
|
|
32
|
+
}
|
|
33
|
+
function probeId(value) {
|
|
34
|
+
const h = createHash("sha256").update(value).digest("hex");
|
|
35
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-a${h.slice(17, 20)}-${h.slice(20, 32)}`;
|
|
36
|
+
}
|
|
37
|
+
export function buildWorkspacePlacementPlan(input) {
|
|
38
|
+
if (!workspacePlacementId(input.requestId) || !workspacePlacementId(input.organizationId))
|
|
39
|
+
invalid("The workspace test request is invalid.");
|
|
40
|
+
const requestId = input.requestId.toLowerCase();
|
|
41
|
+
const organizationId = input.organizationId.toLowerCase();
|
|
42
|
+
const variants = readWorkspacePlacementVariants(input.variants);
|
|
43
|
+
const senders = [...input.senders].sort((a, b) => a.id.localeCompare(b.id));
|
|
44
|
+
const receivers = [...input.receivers].sort((a, b) => a.id.localeCompare(b.id));
|
|
45
|
+
const self = (sender, receiver) => sender.id === receiver.id || sender.address.toLowerCase() === receiver.address.toLowerCase();
|
|
46
|
+
let excluded = 0, pairs = 0;
|
|
47
|
+
for (const sender of senders)
|
|
48
|
+
for (const receiver of receivers) {
|
|
49
|
+
if (self(sender, receiver))
|
|
50
|
+
excluded += variants.length;
|
|
51
|
+
else
|
|
52
|
+
pairs++;
|
|
53
|
+
}
|
|
54
|
+
if ((input.network ? senders.length * variants.length * new Set(receivers.map(r => r.provider)).size : pairs * variants.length) < 1 || (input.network ? senders.length * variants.length * new Set(receivers.map(r => r.provider)).size : pairs * variants.length) > 50)
|
|
55
|
+
invalid("A workspace test must send between one and 50 messages after excluding self-sends.");
|
|
56
|
+
const probes = [];
|
|
57
|
+
// Reject oversized matrices before signatures/HTML/tracking allocate per-probe content.
|
|
58
|
+
// Rotate variants per receiver: no variant always gets the first submission.
|
|
59
|
+
for (const [senderIndex, sender] of senders.entries()) {
|
|
60
|
+
for (const [index, receiver] of receivers.entries()) {
|
|
61
|
+
if (self(sender, receiver))
|
|
62
|
+
continue;
|
|
63
|
+
for (let offset = 0; offset < variants.length; offset++) {
|
|
64
|
+
const original = variants[(offset + index) % variants.length];
|
|
65
|
+
if (input.network) {
|
|
66
|
+
const group = receivers.filter(r => r.provider === receiver.provider);
|
|
67
|
+
const sequence = senderIndex * variants.length + variants.indexOf(original);
|
|
68
|
+
if (group[sequence % group.length].id !== receiver.id)
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const id = probeId(`${organizationId}:${requestId}:${sender.id}:${receiver.id}:${original.key}`);
|
|
72
|
+
const variant = input.renderVariant ? input.renderVariant(original, sender, id) : original;
|
|
73
|
+
probes.push({ id, sender, receiver, variant });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!probes.length || probes.length > 50)
|
|
78
|
+
invalid("A workspace test must send between one and 50 messages after excluding self-sends.");
|
|
79
|
+
const plan = { request_id: requestId, organization_id: organizationId, provider: "internal",
|
|
80
|
+
...(input.network ? { receiver_scope: "network" } : {}), billing_mode: "internal", credits: 0, allow_paused_probe: input.allowPausedProbe,
|
|
81
|
+
...(input.renderedAt ? { rendered_at: input.renderedAt } : {}), ...(input.rendering ? { rendering: input.rendering } : {}),
|
|
82
|
+
probes, excluded_self_sends: excluded, message_count: probes.length };
|
|
83
|
+
return { ...plan, plan_hash: createHash("sha256").update(JSON.stringify(plan)).digest("hex") };
|
|
84
|
+
}
|
|
85
|
+
export function googleWorkspacePlacement(labels, messageId, observedAt) {
|
|
86
|
+
const categories = ["PERSONAL", "PROMOTIONS", "SOCIAL", "UPDATES", "FORUMS"].filter(k => labels.includes(`CATEGORY_${k}`));
|
|
87
|
+
return { folder: labels.includes("SPAM") ? "spam" : labels.includes("INBOX") ? "inbox" : "other",
|
|
88
|
+
category: categories.length === 1 ? { PERSONAL: "primary", PROMOTIONS: "promotions", SOCIAL: "social", UPDATES: "updates", FORUMS: "forums" }[categories[0]] : "unknown",
|
|
89
|
+
observed_at: observedAt, receiver_message_id: messageId };
|
|
90
|
+
}
|
|
91
|
+
export function microsoftWorkspacePlacement(input) {
|
|
92
|
+
return { folder: input.parentFolderId === input.junkId ? "spam" : input.parentFolderId === input.inboxId ? "inbox" : input.parentFolderId ? "other" : "unknown",
|
|
93
|
+
category: input.classification === "focused" || input.classification === "other" ? input.classification : "unknown",
|
|
94
|
+
observed_at: input.observedAt, receiver_message_id: input.messageId };
|
|
95
|
+
}
|
|
96
|
+
/** One exact payload owner for preview, transactional enqueue and drain verification. */
|
|
97
|
+
export function workspacePlacementPayload(probe) {
|
|
98
|
+
return { kind: "email_send", to: probe.receiver.address, subject: probe.variant.subject, body: probe.variant.body_text,
|
|
99
|
+
...(probe.variant.body_html ? { body_html: probe.variant.body_html } : {}), apply_mailbox_signature: false, diagnostic_probe_id: probe.id };
|
|
100
|
+
}
|
|
101
|
+
/** Foreign identities remain private even in previews, copy pages and manifests. */
|
|
102
|
+
export function placementReceiverLabel(receiver) {
|
|
103
|
+
return receiver.network_id ? `${receiver.provider === "google" ? "Google" : "Microsoft"} receiver ${receiver.network_id.slice(0, 8)}` : receiver.address;
|
|
104
|
+
}
|
|
105
|
+
export function publicWorkspacePlacementPlan(plan) {
|
|
106
|
+
return { ...plan, probes: plan.probes.map(p => ({ ...p, receiver: p.receiver.network_id ? { id: p.receiver.network_id, address: placementReceiverLabel(p.receiver), provider: p.receiver.provider, transport: "network", signature_html: null } : p.receiver })) };
|
|
107
|
+
}
|