@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.
Files changed (85) hide show
  1. package/README.md +1 -1
  2. package/dist/command-families/program.js +2 -0
  3. package/dist/command-manifest.js +12 -2
  4. package/dist/commands/budget.js +2 -2
  5. package/dist/commands/capabilities.js +3 -2
  6. package/dist/commands/columns.js +1 -1
  7. package/dist/commands/companies.js +3 -0
  8. package/dist/commands/domains.js +122 -5
  9. package/dist/commands/feedback.js +1 -1
  10. package/dist/commands/feeds.js +34 -17
  11. package/dist/commands/mailboxes.js +19 -9
  12. package/dist/commands/publishing.js +1 -1
  13. package/dist/commands/senders.js +2 -2
  14. package/dist/commands/sequences.js +75 -12
  15. package/dist/commands/subscriptions.d.ts +10 -0
  16. package/dist/commands/subscriptions.js +203 -0
  17. package/dist/commands/tags.js +4 -4
  18. package/dist/customer-operation-commands.js +17 -0
  19. package/dist/mailbox-fleet-output.d.ts +3 -0
  20. package/dist/mailbox-fleet-output.js +80 -0
  21. package/dist/native-chat-host.d.ts +2 -0
  22. package/dist/native-chat-host.js +6 -0
  23. package/dist/workspace-placement-commands.js +67 -2
  24. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +9 -0
  25. package/node_modules/@oxygen/shared/dist/capability-discovery.js +59 -11
  26. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +2 -2
  27. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +18 -0
  28. package/node_modules/@oxygen/shared/dist/contracts/guided-activation.js +4 -4
  29. package/node_modules/@oxygen/shared/dist/contracts/native-chat.d.ts +120 -0
  30. package/node_modules/@oxygen/shared/dist/contracts/native-chat.js +35 -0
  31. package/node_modules/@oxygen/shared/dist/contracts/sender-limits.d.ts +9 -0
  32. package/node_modules/@oxygen/shared/dist/contracts/sender-limits.js +6 -1
  33. package/node_modules/@oxygen/shared/dist/contracts/sequence-list.d.ts +5 -0
  34. package/node_modules/@oxygen/shared/dist/contracts/sequence-list.js +2 -1
  35. package/node_modules/@oxygen/shared/dist/contracts/signal-search-compatibility.d.ts +78 -0
  36. package/node_modules/@oxygen/shared/dist/contracts/signal-search-compatibility.js +32 -0
  37. package/node_modules/@oxygen/shared/dist/dnc-rules.js +14 -0
  38. package/node_modules/@oxygen/shared/dist/email-delivery.d.ts +150 -0
  39. package/node_modules/@oxygen/shared/dist/email-delivery.js +246 -0
  40. package/node_modules/@oxygen/shared/dist/email-dsn.d.ts +1 -0
  41. package/node_modules/@oxygen/shared/dist/email-dsn.js +51 -14
  42. package/node_modules/@oxygen/shared/dist/email-health-summary.d.ts +29 -0
  43. package/node_modules/@oxygen/shared/dist/email-health-summary.js +28 -0
  44. package/node_modules/@oxygen/shared/dist/email-subscriptions.d.ts +50 -0
  45. package/node_modules/@oxygen/shared/dist/email-subscriptions.js +80 -0
  46. package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.d.ts +28 -0
  47. package/node_modules/@oxygen/shared/dist/email-unsubscribe-token.js +47 -0
  48. package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +4 -16
  49. package/node_modules/@oxygen/shared/dist/error-redaction.js +8 -17
  50. package/node_modules/@oxygen/shared/dist/guided-activation.d.ts +15 -1
  51. package/node_modules/@oxygen/shared/dist/guided-activation.js +3 -1
  52. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +2 -0
  53. package/node_modules/@oxygen/shared/dist/mailbox-standby-rollout.d.ts +5 -0
  54. package/node_modules/@oxygen/shared/dist/mailbox-standby-rollout.js +9 -0
  55. package/node_modules/@oxygen/shared/dist/mailbox-standby.d.ts +43 -0
  56. package/node_modules/@oxygen/shared/dist/mailbox-standby.js +52 -0
  57. package/node_modules/@oxygen/shared/dist/native-chat-discovery.d.ts +5 -0
  58. package/node_modules/@oxygen/shared/dist/native-chat-discovery.js +27 -0
  59. package/node_modules/@oxygen/shared/dist/native-chat.d.ts +124 -0
  60. package/node_modules/@oxygen/shared/dist/native-chat.js +1 -0
  61. package/node_modules/@oxygen/shared/dist/onboarding-plays.d.ts +14 -3
  62. package/node_modules/@oxygen/shared/dist/onboarding-plays.js +26 -13
  63. package/node_modules/@oxygen/shared/dist/onboarding-research.d.ts +11 -0
  64. package/node_modules/@oxygen/shared/dist/onboarding-research.js +8 -0
  65. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +6 -0
  66. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +6 -0
  67. package/node_modules/@oxygen/shared/dist/select-options.js +1 -0
  68. package/node_modules/@oxygen/shared/dist/sequence-failures.js +6 -5
  69. package/node_modules/@oxygen/shared/dist/sequences.d.ts +8 -0
  70. package/node_modules/@oxygen/shared/dist/sequences.js +44 -2
  71. package/node_modules/@oxygen/shared/dist/tags.d.ts +7 -3
  72. package/node_modules/@oxygen/shared/dist/tags.js +31 -6
  73. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  74. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  75. package/node_modules/@oxygen/shared/dist/workspace-placement-bulk.d.ts +80 -0
  76. package/node_modules/@oxygen/shared/dist/workspace-placement-bulk.js +68 -0
  77. package/node_modules/@oxygen/shared/dist/workspace-placement-view.d.ts +2 -1
  78. package/node_modules/@oxygen/shared/dist/workspace-placement.d.ts +11 -1
  79. package/node_modules/@oxygen/shared/dist/workspace-placement.js +15 -4
  80. package/node_modules/@oxygen/shared/dist/write-rollout.d.ts +11 -0
  81. package/node_modules/@oxygen/shared/dist/write-rollout.js +59 -0
  82. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.d.ts +176 -2
  83. package/node_modules/@oxygen/workflows/dist/graph/manifest-schema.js +50 -1
  84. package/node_modules/@oxygen/workflows/dist/index.js +108 -3
  85. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  // Workspace tags (Tags primitive, ADR 0010; registry per ADR 0017): free-form
2
- // lowercase labels shared across primitives so campaign knowledge links across
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: lowercase, trim, drop non-strings/empties, dedupe,
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
- return Array.from(new Set(values.map((tag) => (typeof tag === "string" ? tag.trim().toLowerCase() : "")).filter(Boolean))).slice(0, MAX_TAGS_PER_ITEM);
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
- * Note this lowercases and trims but does NOT slugify — "Q3 Outbound" becomes
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().toLowerCase();
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.1465";
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.1465";
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 self = (sender, receiver) => sender.id === receiver.id || sender.address.toLowerCase() === receiver.address.toLowerCase();
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: createHash("sha256").update(JSON.stringify(plan)).digest("hex") };
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: "A node + edge workflow graph. Exactly one trigger node; every node reachable from it; cycles are illegal except a single back-edge from inside a loop's body to that loop node.";
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: "A node + edge workflow graph. Exactly one trigger node; every node reachable from it; cycles are illegal except a single back-edge from inside a loop's body to that loop node.";
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";