@sellable/mcp 0.1.548 → 0.1.550

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.
@@ -19,8 +19,7 @@ export type CreateWorkflowTableResponse = {
19
19
  };
20
20
  export type AttachSequenceInput = {
21
21
  tableId: string;
22
- template?: Record<string, unknown>;
23
- templateRef?: "connection_only";
22
+ template: Record<string, unknown>;
24
23
  confirmed?: boolean;
25
24
  };
26
25
  export type AttachSequenceResponse = {
@@ -75,7 +74,6 @@ export declare const sequencerToolDefinitions: ({
75
74
  };
76
75
  tableId?: undefined;
77
76
  template?: undefined;
78
- templateRef?: undefined;
79
77
  confirmed?: undefined;
80
78
  campaignId?: undefined;
81
79
  currentStep?: undefined;
@@ -96,11 +94,6 @@ export declare const sequencerToolDefinitions: ({
96
94
  type: string;
97
95
  description: string;
98
96
  };
99
- templateRef: {
100
- type: string;
101
- enum: string[];
102
- description: string;
103
- };
104
97
  confirmed: {
105
98
  type: string;
106
99
  description: string;
@@ -140,7 +133,6 @@ export declare const sequencerToolDefinitions: ({
140
133
  sequenceActions?: undefined;
141
134
  tableId?: undefined;
142
135
  template?: undefined;
143
- templateRef?: undefined;
144
136
  };
145
137
  required: string[];
146
138
  };
@@ -53,17 +53,12 @@ export const sequencerToolDefinitions = [
53
53
  type: "object",
54
54
  description: "Sequence template object with version, entryNodeId, nodes, and branches",
55
55
  },
56
- templateRef: {
57
- type: "string",
58
- enum: ["connection_only"],
59
- description: "Optional backend-owned known template reference. Use connection_only for invite-only evergreen setup without duplicating app template JSON in MCP.",
60
- },
61
56
  confirmed: {
62
57
  type: "boolean",
63
58
  description: "Set true to overwrite existing sequence columns if the table already has them",
64
59
  },
65
60
  },
66
- required: ["tableId"],
61
+ required: ["tableId", "template"],
67
62
  },
68
63
  },
69
64
  {
@@ -112,53 +107,55 @@ export async function attachSequence(input) {
112
107
  const api = getApi();
113
108
  const template = input.template;
114
109
  const preValidationErrors = [];
115
- if (input.template && input.templateRef) {
116
- throw new Error("INVALID_TEMPLATE: Pass either template or templateRef, not both.");
110
+ if (!Array.isArray(template.nodes) || template.nodes.length === 0) {
111
+ preValidationErrors.push("Template must have a non-empty 'nodes' array.");
117
112
  }
118
- if (!input.template && !input.templateRef) {
119
- throw new Error("INVALID_TEMPLATE: Pass template or templateRef.");
113
+ if (typeof template.entryNodeId !== "string" ||
114
+ template.entryNodeId.trim().length === 0) {
115
+ preValidationErrors.push("Template must have an 'entryNodeId' string.");
120
116
  }
121
- if (input.template) {
122
- if (!Array.isArray(template.nodes) || template.nodes.length === 0) {
123
- preValidationErrors.push("Template must have a non-empty 'nodes' array.");
124
- }
125
- if (typeof template.entryNodeId !== "string" ||
126
- template.entryNodeId.trim().length === 0) {
127
- preValidationErrors.push("Template must have an 'entryNodeId' string.");
128
- }
129
- if (template.version !== 2) {
130
- preValidationErrors.push("Template version must be 2.");
131
- }
132
- if (Array.isArray(template.nodes) &&
133
- typeof template.entryNodeId === "string") {
134
- const nodeIds = new Set(template.nodes
135
- .map((node) => node && typeof node === "object"
136
- ? node.id
137
- : null)
138
- .filter((nodeId) => typeof nodeId === "string"));
139
- if (template.entryNodeId && !nodeIds.has(template.entryNodeId)) {
140
- preValidationErrors.push(`entryNodeId "${template.entryNodeId}" does not match any node id. Available node ids: ${Array.from(nodeIds).join(", ")}`);
141
- }
142
- }
143
- if (preValidationErrors.length > 0) {
144
- throw new Error("INVALID_TEMPLATE: Template has structural issues:\n" +
145
- preValidationErrors.map((issue) => ` - ${issue}`).join("\n") +
146
- "\n\nFix these issues and retry.");
117
+ if (template.version !== 2) {
118
+ preValidationErrors.push("Template version must be 2.");
119
+ }
120
+ if (Array.isArray(template.nodes) &&
121
+ typeof template.entryNodeId === "string") {
122
+ const nodeIds = new Set(template.nodes
123
+ .map((node) => node && typeof node === "object"
124
+ ? node.id
125
+ : null)
126
+ .filter((nodeId) => typeof nodeId === "string"));
127
+ if (template.entryNodeId && !nodeIds.has(template.entryNodeId)) {
128
+ preValidationErrors.push(`entryNodeId "${template.entryNodeId}" does not match any node id. Available node ids: ${Array.from(nodeIds).join(", ")}`);
147
129
  }
148
130
  }
131
+ if (preValidationErrors.length > 0) {
132
+ throw new Error("INVALID_TEMPLATE: Template has structural issues:\n" +
133
+ preValidationErrors.map((issue) => ` - ${issue}`).join("\n") +
134
+ "\n\nFix these issues and retry.");
135
+ }
149
136
  // Campaign-backed tables route to the campaigns endpoint so the MCP
150
137
  // tool persists the sequence to the same place the UI does
151
138
  // (`CampaignOffer.sequenceTemplate`). The workflow-tables endpoint
152
139
  // writes to `WorkflowTable.config.sequenceTemplate`, which the UI
153
140
  // doesn't read — hitting it on a campaign table is silently wrong.
154
- const campaignOfferId = await resolveCampaignOfferIdForTable(api, input.tableId);
141
+ let campaignOfferId = null;
142
+ try {
143
+ const tableMeta = await api.get(`/api/v3/workflow-tables/${input.tableId}?mode=meta`);
144
+ const rawCampaignId = tableMeta?.table?.config?.campaignOfferId;
145
+ if (typeof rawCampaignId === "string" && rawCampaignId.length > 0) {
146
+ campaignOfferId = rawCampaignId;
147
+ }
148
+ }
149
+ catch {
150
+ // Fall back to workflow-tables path on meta-fetch failure —
151
+ // non-campaign tables won't have the field set anyway.
152
+ }
155
153
  const endpoint = campaignOfferId
156
154
  ? `/api/v3/campaigns/${campaignOfferId}/sequence`
157
155
  : `/api/v3/workflow-tables/${input.tableId}/sequence`;
158
156
  try {
159
157
  return await api.put(endpoint, {
160
- ...(input.template ? { template: input.template } : {}),
161
- ...(input.templateRef ? { templateRef: input.templateRef } : {}),
158
+ template: input.template,
162
159
  confirmed: input.confirmed,
163
160
  });
164
161
  }
@@ -180,32 +177,6 @@ export async function attachSequence(input) {
180
177
  throw error;
181
178
  }
182
179
  }
183
- async function resolveCampaignOfferIdForTable(api, tableId) {
184
- try {
185
- const tableMeta = await api.get(`/api/v3/workflow-tables/${tableId}?mode=meta`);
186
- const rawCampaignId = tableMeta?.table?.config?.campaignOfferId;
187
- if (typeof rawCampaignId === "string" && rawCampaignId.length > 0) {
188
- return rawCampaignId;
189
- }
190
- }
191
- catch {
192
- // Continue to the inventory fallback below.
193
- }
194
- try {
195
- const tableInventory = await api.get("/api/v3/mcp/tables?limit=500");
196
- const matchingTable = tableInventory?.tables?.find((table) => {
197
- return table?.id === tableId;
198
- });
199
- const rawCampaignId = matchingTable?.campaignOfferId;
200
- if (typeof rawCampaignId === "string" && rawCampaignId.length > 0) {
201
- return rawCampaignId;
202
- }
203
- }
204
- catch {
205
- // Fall back to workflow-tables path on inventory failure.
206
- }
207
- return null;
208
- }
209
180
  export async function attachRecommendedSequence(input) {
210
181
  const api = getApi();
211
182
  try {
@@ -1,5 +1,4 @@
1
1
  type SetupEvergreenCampaignsInput = {
2
- workspaceId?: string;
3
2
  mode?: "plan" | "verify";
4
3
  depth?: "structure_only" | "customer_visible";
5
4
  handoffMode?: "create_campaign_goals";
@@ -16,9 +15,6 @@ type SetupEvergreenCampaignsInput = {
16
15
  planRevision?: string;
17
16
  selectedActionIds?: string[];
18
17
  receipts?: Array<Record<string, unknown>>;
19
- campaignSequenceOptions?: {
20
- mode: "connection_only";
21
- };
22
18
  };
23
19
  export declare const setupEvergreenCampaignsToolDefinitions: {
24
20
  name: string;
@@ -31,23 +27,6 @@ export declare const setupEvergreenCampaignsToolDefinitions: {
31
27
  enum: string[];
32
28
  description: string;
33
29
  };
34
- workspaceId: {
35
- type: string;
36
- description: string;
37
- };
38
- campaignSequenceOptions: {
39
- type: string;
40
- properties: {
41
- mode: {
42
- type: string;
43
- enum: string[];
44
- description: string;
45
- };
46
- };
47
- required: string[];
48
- additionalProperties: boolean;
49
- description: string;
50
- };
51
30
  depth: {
52
31
  type: string;
53
32
  enum: string[];
@@ -1,7 +1,7 @@
1
1
  import { getApi } from "../api.js";
2
- async function postSetupEvergreenCampaigns(body, workspaceId) {
2
+ async function postSetupEvergreenCampaigns(body) {
3
3
  const api = getApi();
4
- return api.post("/api/v3/mcp/setup-evergreen-campaigns", body, workspaceId ? { workspaceId } : undefined);
4
+ return api.post("/api/v3/mcp/setup-evergreen-campaigns", body);
5
5
  }
6
6
  export const setupEvergreenCampaignsToolDefinitions = [
7
7
  {
@@ -15,23 +15,6 @@ export const setupEvergreenCampaignsToolDefinitions = [
15
15
  enum: ["plan", "verify"],
16
16
  description: 'Defaults to "plan". Verify only validates worker receipts.',
17
17
  },
18
- workspaceId: {
19
- type: "string",
20
- description: "Optional explicit workspace id. Required for connection-only evergreen setup; it is also sent as the request workspace so concurrent threads cannot drift to another active workspace.",
21
- },
22
- campaignSequenceOptions: {
23
- type: "object",
24
- properties: {
25
- mode: {
26
- type: "string",
27
- enum: ["connection_only"],
28
- description: "Optional non-default sequence policy. connection_only means attach the canonical invite-only template and verify exactly send_invite, with no DM, InMail, View Profile, follow-up, launch, or send side effects.",
29
- },
30
- },
31
- required: ["mode"],
32
- additionalProperties: false,
33
- description: "Optional non-default evergreen sequence policy. Omit to keep the standard tier-recommended Premium/Sales Nav behavior.",
34
- },
35
18
  depth: {
36
19
  type: "string",
37
20
  enum: ["structure_only", "customer_visible"],
@@ -107,12 +90,10 @@ export const setupEvergreenCampaignsToolDefinitions = [
107
90
  ];
108
91
  export function setupEvergreenCampaigns(input) {
109
92
  return postSetupEvergreenCampaigns({
110
- workspaceId: input.workspaceId,
111
93
  mode: input.mode,
112
94
  yolo: input.mode === "verify" ? undefined : input.yolo,
113
95
  depth: input.depth,
114
96
  handoffMode: input.mode === "verify" ? undefined : input.handoffMode,
115
- campaignSequenceOptions: input.campaignSequenceOptions,
116
97
  allConnectedSenders: input.allConnectedSenders,
117
98
  selectedSenderIds: input.selectedSenderIds,
118
99
  postEngagerSenderIds: input.postEngagerSenderIds,
@@ -121,5 +102,5 @@ export function setupEvergreenCampaigns(input) {
121
102
  planRevision: input.planRevision,
122
103
  selectedActionIds: input.selectedActionIds,
123
104
  receipts: input.receipts,
124
- }, input.workspaceId);
105
+ });
125
106
  }
@@ -12,7 +12,7 @@ export interface WorkspaceContext {
12
12
  executionMode: WorkspaceExecutionMode;
13
13
  toolName?: string;
14
14
  runId: string;
15
- workspaceResolution: "explicit";
15
+ workspaceResolution: "explicit" | "locked_profile";
16
16
  }
17
17
  export interface WorkspaceRequiredResult {
18
18
  ok: false;
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { getLockedWorkspaceId, resolveWorkspaceIdForRequest, } from "../auth.js";
2
3
  export function workspaceRequired(params) {
3
4
  const toolLabel = params.toolName ? ` for ${params.toolName}` : "";
4
5
  return {
@@ -15,11 +16,13 @@ export function normalizeExplicitWorkspaceId(value) {
15
16
  return typeof value === "string" && value.trim() ? value.trim() : null;
16
17
  }
17
18
  export function workspaceRequestOptions(workspaceId) {
18
- const normalized = normalizeExplicitWorkspaceId(workspaceId);
19
+ const normalized = resolveWorkspaceIdForRequest(normalizeExplicitWorkspaceId(workspaceId), "workspace request options");
19
20
  return normalized ? Object.freeze({ workspaceId: normalized }) : undefined;
20
21
  }
21
22
  export function createWorkspaceContext(input) {
22
- const workspaceId = normalizeExplicitWorkspaceId(input.workspaceId);
23
+ const explicitWorkspaceId = normalizeExplicitWorkspaceId(input.workspaceId);
24
+ const lockedWorkspaceId = getLockedWorkspaceId();
25
+ const workspaceId = resolveWorkspaceIdForRequest(explicitWorkspaceId, input.toolName || "workspace context");
23
26
  if (!workspaceId) {
24
27
  return workspaceRequired({
25
28
  executionMode: input.executionMode,
@@ -33,7 +36,9 @@ export function createWorkspaceContext(input) {
33
36
  executionMode: input.executionMode,
34
37
  toolName: input.toolName,
35
38
  runId: input.runId?.trim() || randomUUID(),
36
- workspaceResolution: "explicit",
39
+ workspaceResolution: lockedWorkspaceId && !explicitWorkspaceId
40
+ ? "locked_profile"
41
+ : "explicit",
37
42
  });
38
43
  return { ok: true, context };
39
44
  }
@@ -4,7 +4,7 @@ import { lstat, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises
4
4
  import { tmpdir } from "node:os";
5
5
  import path from "node:path";
6
6
  import { getApi } from "../api.js";
7
- import { getConfig } from "../auth.js";
7
+ import { getConfig, getEffectiveConfiguredWorkspaceId } from "../auth.js";
8
8
  const MAX_FILTER_IDS = 100;
9
9
  const MAX_QUERY_LENGTH = 8000;
10
10
  export const workspaceExportToolDefinitions = [
@@ -340,7 +340,7 @@ async function writeJsonAtomic(filePath, value) {
340
340
  }
341
341
  export async function exportWorkspaceCsv(input = {}) {
342
342
  const config = getConfig();
343
- const workspaceId = config.activeWorkspaceId || config.workspaceId || null;
343
+ const workspaceId = getEffectiveConfiguredWorkspaceId(config);
344
344
  if (!workspaceId) {
345
345
  throw new Error("No active workspace selected. Run list_workspaces then set_active_workspace before export_workspace_csv.");
346
346
  }
@@ -140,21 +140,56 @@ export declare const workspaceToolDefinitions: ({
140
140
  })[];
141
141
  export declare function listWorkspaces(): Promise<{
142
142
  workspaces: WorkspaceSummary[];
143
+ workspaceLock: {
144
+ enabled: boolean;
145
+ workspaceId?: undefined;
146
+ hiddenWorkspaceCount?: undefined;
147
+ };
148
+ } | {
149
+ workspaces: WorkspaceSummary[];
150
+ workspaceLock: {
151
+ enabled: boolean;
152
+ workspaceId: string;
153
+ hiddenWorkspaceCount: number;
154
+ };
143
155
  }>;
144
156
  export declare function getActiveWorkspace(): {
145
157
  activeWorkspaceId: string | null;
146
158
  activeWorkspaceName: string | null;
159
+ workspaceLock: {
160
+ enabled: boolean;
161
+ workspaceId: string;
162
+ } | {
163
+ enabled: boolean;
164
+ workspaceId?: undefined;
165
+ };
147
166
  };
148
167
  export declare function getWorkspace(workspaceId: string): Promise<{
168
+ ok: boolean;
169
+ error: string;
170
+ workspace?: undefined;
171
+ } | {
149
172
  workspace: WorkspaceDetails;
173
+ ok?: undefined;
174
+ error?: undefined;
150
175
  }>;
151
176
  export declare function setActiveWorkspace(workspaceId: string, userConfirmed?: boolean): Promise<{
152
177
  ok: boolean;
178
+ code: string;
179
+ activeWorkspaceId: string;
180
+ requestedWorkspaceId: string;
153
181
  error: string;
154
182
  requiresConfirmation?: undefined;
155
- activeWorkspaceId?: undefined;
156
183
  activeWorkspaceName?: undefined;
184
+ requestedWorkspaceName?: undefined;
185
+ } | {
186
+ ok: boolean;
187
+ error: string;
188
+ code?: undefined;
189
+ activeWorkspaceId?: undefined;
157
190
  requestedWorkspaceId?: undefined;
191
+ requiresConfirmation?: undefined;
192
+ activeWorkspaceName?: undefined;
158
193
  requestedWorkspaceName?: undefined;
159
194
  } | {
160
195
  ok: boolean;
@@ -164,22 +199,33 @@ export declare function setActiveWorkspace(workspaceId: string, userConfirmed?:
164
199
  requestedWorkspaceId: string;
165
200
  requestedWorkspaceName: string;
166
201
  error: string;
202
+ code?: undefined;
167
203
  } | {
168
204
  ok: boolean;
169
205
  activeWorkspaceId: string;
170
206
  activeWorkspaceName: string;
207
+ code?: undefined;
208
+ requestedWorkspaceId?: undefined;
171
209
  error?: undefined;
172
210
  requiresConfirmation?: undefined;
173
- requestedWorkspaceId?: undefined;
174
211
  requestedWorkspaceName?: undefined;
175
212
  }>;
176
213
  export declare function createWorkspace(name: string): Promise<{
214
+ ok: boolean;
215
+ code: string;
216
+ activeWorkspaceId: string;
217
+ error: string;
218
+ workspace?: undefined;
219
+ } | {
177
220
  workspace: {
178
221
  id: string;
179
222
  name: string;
180
223
  slug: string;
181
224
  };
182
225
  activeWorkspaceId: string;
226
+ ok?: undefined;
227
+ code?: undefined;
228
+ error?: undefined;
183
229
  }>;
184
230
  export declare function addTeammate(input: {
185
231
  workspaceId?: string;
@@ -1,5 +1,5 @@
1
1
  import { getApi, resetApi } from "../api.js";
2
- import { getConfig, updateActiveWorkspace } from "../auth.js";
2
+ import { getConfig, getEffectiveConfiguredWorkspaceId, getLockedWorkspaceId, resolveWorkspaceIdForRequest, updateActiveWorkspace, } from "../auth.js";
3
3
  export const workspaceToolDefinitions = [
4
4
  {
5
5
  name: "list_workspaces",
@@ -81,21 +81,53 @@ export const workspaceToolDefinitions = [
81
81
  export async function listWorkspaces() {
82
82
  const api = getApi();
83
83
  const { workspaces } = await api.get("/api/v3/workspaces");
84
- return { workspaces };
84
+ const lockedWorkspaceId = getLockedWorkspaceId();
85
+ if (!lockedWorkspaceId) {
86
+ return { workspaces, workspaceLock: { enabled: false } };
87
+ }
88
+ return {
89
+ workspaces: workspaces.filter((ws) => ws.id === lockedWorkspaceId),
90
+ workspaceLock: {
91
+ enabled: true,
92
+ workspaceId: lockedWorkspaceId,
93
+ hiddenWorkspaceCount: workspaces.filter((ws) => ws.id !== lockedWorkspaceId).length,
94
+ },
95
+ };
85
96
  }
86
97
  export function getActiveWorkspace() {
87
98
  const config = getConfig();
99
+ const lockedWorkspaceId = getLockedWorkspaceId();
88
100
  return {
89
- activeWorkspaceId: config.activeWorkspaceId || config.workspaceId || null,
101
+ activeWorkspaceId: getEffectiveConfiguredWorkspaceId(config),
90
102
  activeWorkspaceName: config.activeWorkspaceName || null,
103
+ workspaceLock: lockedWorkspaceId
104
+ ? { enabled: true, workspaceId: lockedWorkspaceId }
105
+ : { enabled: false },
91
106
  };
92
107
  }
93
108
  export async function getWorkspace(workspaceId) {
109
+ const allowedWorkspaceId = resolveWorkspaceIdForRequest(workspaceId, "get_workspace");
110
+ if (!allowedWorkspaceId) {
111
+ return {
112
+ ok: false,
113
+ error: "No active workspace selected.",
114
+ };
115
+ }
94
116
  const api = getApi();
95
- const { workspace } = await api.get(`/api/v3/workspaces/${encodeURIComponent(workspaceId)}`);
117
+ const { workspace } = await api.get(`/api/v3/workspaces/${encodeURIComponent(allowedWorkspaceId)}`);
96
118
  return { workspace };
97
119
  }
98
120
  export async function setActiveWorkspace(workspaceId, userConfirmed) {
121
+ const lockedWorkspaceId = getLockedWorkspaceId();
122
+ if (lockedWorkspaceId && workspaceId !== lockedWorkspaceId) {
123
+ return {
124
+ ok: false,
125
+ code: "WORKSPACE_LOCKED",
126
+ activeWorkspaceId: lockedWorkspaceId,
127
+ requestedWorkspaceId: workspaceId,
128
+ error: "This Hermes profile is locked to its customer workspace and cannot switch workspaces.",
129
+ };
130
+ }
99
131
  const api = getApi();
100
132
  const { workspaces } = await api.get("/api/v3/workspaces");
101
133
  const match = workspaces.find((ws) => ws.id === workspaceId);
@@ -131,6 +163,15 @@ export async function setActiveWorkspace(workspaceId, userConfirmed) {
131
163
  };
132
164
  }
133
165
  export async function createWorkspace(name) {
166
+ const lockedWorkspaceId = getLockedWorkspaceId();
167
+ if (lockedWorkspaceId) {
168
+ return {
169
+ ok: false,
170
+ code: "WORKSPACE_LOCKED",
171
+ activeWorkspaceId: lockedWorkspaceId,
172
+ error: "This Hermes profile is locked to its customer workspace and cannot create or switch workspaces. Use the sellable-admin profile for provisioning.",
173
+ };
174
+ }
134
175
  const api = getApi();
135
176
  const { workspace } = await api.post("/api/v3/workspaces", { name });
136
177
  updateActiveWorkspace({
@@ -146,7 +187,9 @@ export async function createWorkspace(name) {
146
187
  export async function addTeammate(input) {
147
188
  const args = input || {};
148
189
  const config = getConfig();
149
- const workspaceId = args.workspaceId || config.activeWorkspaceId || config.workspaceId;
190
+ const workspaceId = resolveWorkspaceIdForRequest(args.workspaceId ||
191
+ getEffectiveConfiguredWorkspaceId(config) ||
192
+ undefined, "add_teammate");
150
193
  if (!workspaceId) {
151
194
  return {
152
195
  ok: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.548",
3
+ "version": "0.1.550",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code, Codex, and Hermes campaign workflows",
6
6
  "main": "dist/index.js",
@@ -59,11 +59,6 @@ The tail MUST verify:
59
59
 
60
60
  - `attach_recommended_sequence` is the only way to pick a template in
61
61
  the autonomous tail. Do NOT hand-wire a sequence at Step 16.
62
- - Exception: when the parent `$sellable:create-evergreen-campaigns` plan
63
- explicitly carries `campaignSequenceOptions:{ mode:"connection_only" }`,
64
- do not call `attach_recommended_sequence`. Use the backend-owned
65
- `attach_sequence({ tableId, templateRef:"connection_only" })` path and prove
66
- `sequenceReceipt.actionTypes:["send_invite"]`.
67
62
  - Tier mismatch at attach time is an escalation, NOT a silent fallback.
68
63
  - A Standard sender MUST NOT be attached to an INMAIL_OPEN template.
69
64
  This would produce sends that fail at Unipile with `not_authorized`.