@oxygen-agent/cli 1.146.1 → 1.160.18

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.
@@ -0,0 +1,318 @@
1
+ // Structured, non-secret attribution for Postgres / drizzle query failures.
2
+ //
3
+ // Background (OXY-44/OXY-46): worker error spans and logs previously carried
4
+ // only the raw drizzle `Failed query: <sql>...` message (truncated to ~500
5
+ // chars in Axiom), so an operator could not tell a Neon connect timeout from a
6
+ // statement timeout, schema drift, or another SQLSTATE without guessing from
7
+ // the truncated SQL. This classifier turns any pg/drizzle error into a small
8
+ // set of structured, groupable fields:
9
+ //
10
+ // - SQLSTATE / pg error code (e.g. "57014", "42P01")
11
+ // - normalized cause category (e.g. "statement_timeout", "schema_drift")
12
+ // - query kind + table (e.g. "select" on "workflow_triggers")
13
+ // - whether a retry could succeed (transient)
14
+ //
15
+ // HARD CONSTRAINT: customer row data and SQL *parameter values* must never leak
16
+ // into telemetry. We therefore only ever read schema-level identifiers (the
17
+ // table name) and the leading SQL verb from the *parameter-free* SQL, and we
18
+ // always strip drizzle's trailing `\nparams: [...]` tail. We never emit the SQL
19
+ // text itself or the parameter array.
20
+ //
21
+ // drizzle (>=0.36) wraps driver failures as a DrizzleQueryError whose message is
22
+ // `Failed query: <sql>\nparams: <params>`, with the original pg error on
23
+ // `.cause` (so the SQLSTATE lives on `error.cause.code`, not `error.code`).
24
+ // Connection-level failures (Neon cold-start connect timeouts, socket resets)
25
+ // surface before a query with a Node errno on `code`/`errno` and no SQLSTATE.
26
+ // SQLSTATEs that mean the code expects schema the database does not have — the
27
+ // "schema drift" signal OXY-44 specifically called out (undefined table/column/
28
+ // function/object/schema). Distinct from generic class-42 syntax/access errors.
29
+ const SCHEMA_DRIFT_SQLSTATES = new Set([
30
+ "42P01", // undefined_table
31
+ "42703", // undefined_column
32
+ "42883", // undefined_function
33
+ "42704", // undefined_object
34
+ "42P02", // undefined_parameter
35
+ "3F000", // invalid_schema_name
36
+ ]);
37
+ // Node/undici errnos. These appear on `.code` (or `.errno`) for failures that
38
+ // happen before/around the TCP+TLS handshake, so they carry no SQLSTATE.
39
+ const CONNECT_TIMEOUT_ERRNOS = new Set(["ETIMEDOUT", "UND_ERR_CONNECT_TIMEOUT"]);
40
+ const CONNECTION_ERRNOS = new Set([
41
+ "ECONNRESET",
42
+ "ECONNREFUSED",
43
+ "ENOTFOUND",
44
+ "EAI_AGAIN",
45
+ "EPIPE",
46
+ "ENETUNREACH",
47
+ "UND_ERR_SOCKET",
48
+ ]);
49
+ const CONNECT_TIMEOUT_MESSAGE = /timeout exceeded when trying to connect|connection terminated due to connection timeout|connect(?:ion)?\s+time(?:d?\s?)out/i;
50
+ const CONNECTION_MESSAGE = /connection terminated|connection reset|socket hang up|fetch failed|getaddrinfo|network timeout/i;
51
+ // Categories from which a retry on a later tick could plausibly succeed. This is
52
+ // a SQLSTATE-aware retry-eligibility signal and is intentionally independent of
53
+ // the worker span's conservative `outcome` gate (isTransientPersistenceError),
54
+ // which only suppresses error-rate monitors for self-healing connect failures.
55
+ const TRANSIENT_CAUSES = new Set([
56
+ "connect_timeout",
57
+ "connection",
58
+ "statement_timeout",
59
+ "admin_shutdown",
60
+ "too_many_connections",
61
+ "insufficient_resources",
62
+ "serialization",
63
+ "deadlock",
64
+ "transaction_rollback",
65
+ ]);
66
+ /**
67
+ * Classifies a pg/drizzle error into structured, non-secret attribution.
68
+ * Returns null when the error carries no SQL/connection signal at all (so
69
+ * non-DB errors never gain spurious `db.*` attributes). Never throws.
70
+ */
71
+ export function describeSqlError(error) {
72
+ try {
73
+ const sqlstate = readSqlstate(error);
74
+ const errno = readErrno(error);
75
+ const message = readMessage(error);
76
+ const drizzle = isDrizzleQueryError(error, message);
77
+ let cause = sqlstate ? categoryFromSqlstate(sqlstate) ?? "unknown" : "unknown";
78
+ if (cause === "unknown") {
79
+ if (errno && CONNECT_TIMEOUT_ERRNOS.has(errno))
80
+ cause = "connect_timeout";
81
+ else if (errno && CONNECTION_ERRNOS.has(errno))
82
+ cause = "connection";
83
+ else if (CONNECT_TIMEOUT_MESSAGE.test(message))
84
+ cause = "connect_timeout";
85
+ else if (CONNECTION_MESSAGE.test(message))
86
+ cause = "connection";
87
+ }
88
+ const hasSignal = Boolean(sqlstate) || Boolean(errno) || cause !== "unknown" || drizzle;
89
+ if (!hasSignal)
90
+ return null;
91
+ const { kind, table } = extractQueryShape(readParamFreeSql(error, message));
92
+ // severity/schema/column/constraint/statusCode are schema-level identifiers
93
+ // or transport status codes — safe to surface. We deliberately do NOT read
94
+ // pg's `detail`, `hint`, or `where`: those embed customer row values (e.g.
95
+ // "Key (email)=(a@b.com) already exists") and would leak data (OXY-38).
96
+ return {
97
+ pgCode: sqlstate,
98
+ cause,
99
+ queryKind: kind,
100
+ table: table ?? readTableField(error),
101
+ transient: TRANSIENT_CAUSES.has(cause),
102
+ severity: readSeverity(error),
103
+ schema: readIdentifierField(error, "schema"),
104
+ column: readIdentifierField(error, "column"),
105
+ constraint: readIdentifierField(error, "constraint"),
106
+ statusCode: readStatusCode(error),
107
+ };
108
+ }
109
+ catch {
110
+ return null;
111
+ }
112
+ }
113
+ /** Log-field shape (snake_case) for merging into `errorFields(error)` payloads. */
114
+ export function sqlErrorFields(error) {
115
+ const a = describeSqlError(error);
116
+ if (!a)
117
+ return {};
118
+ return {
119
+ ...(a.pgCode ? { sql_error_pg_code: a.pgCode } : {}),
120
+ sql_error_cause: a.cause,
121
+ ...(a.queryKind ? { sql_error_query_kind: a.queryKind } : {}),
122
+ ...(a.table ? { sql_error_table: a.table } : {}),
123
+ sql_error_transient: a.transient,
124
+ ...(a.severity ? { sql_error_severity: a.severity } : {}),
125
+ ...(a.schema ? { sql_error_schema: a.schema } : {}),
126
+ ...(a.column ? { sql_error_column: a.column } : {}),
127
+ ...(a.constraint ? { sql_error_constraint: a.constraint } : {}),
128
+ ...(a.statusCode !== null ? { sql_error_status: a.statusCode } : {}),
129
+ };
130
+ }
131
+ /**
132
+ * Strips drizzle's `\nparams: <values>` segment from an error message or stack
133
+ * so SQL parameter values never reach logs/telemetry (OXY-46). Preserves the SQL
134
+ * text itself and any following stack frames; a no-op on non-drizzle text.
135
+ *
136
+ * drizzle's DrizzleQueryError carries the parameter array inside its `.message`
137
+ * (and therefore its `.stack`) as `Failed query: <sql>\nparams: <values>`, so
138
+ * the generic error serializers would otherwise emit those values verbatim.
139
+ */
140
+ export function redactSqlParameters(text) {
141
+ if (!text.includes("Failed query:"))
142
+ return text;
143
+ return text.replace(/\n[ \t]*params[ \t]*:[\s\S]*?(?=\n[ \t]*at\s|$)/i, "");
144
+ }
145
+ /** Telemetry-attribute shape (dotted keys) for span error attribution. */
146
+ export function sqlErrorTelemetryAttributes(error) {
147
+ const a = describeSqlError(error);
148
+ if (!a)
149
+ return {};
150
+ return {
151
+ ...(a.pgCode ? { "db.pg_code": a.pgCode } : {}),
152
+ "db.error.cause": a.cause,
153
+ ...(a.queryKind ? { "db.error.query_kind": a.queryKind } : {}),
154
+ ...(a.table ? { "db.error.table": a.table } : {}),
155
+ "db.error.transient": a.transient,
156
+ ...(a.severity ? { "db.error.severity": a.severity } : {}),
157
+ ...(a.schema ? { "db.error.schema": a.schema } : {}),
158
+ ...(a.column ? { "db.error.column": a.column } : {}),
159
+ ...(a.constraint ? { "db.error.constraint": a.constraint } : {}),
160
+ ...(a.statusCode !== null ? { "db.error.status": a.statusCode } : {}),
161
+ };
162
+ }
163
+ function categoryFromSqlstate(code) {
164
+ if (code === "57014")
165
+ return "statement_timeout";
166
+ if (code === "53300")
167
+ return "too_many_connections";
168
+ if (code === "40001")
169
+ return "serialization";
170
+ if (code === "40P01")
171
+ return "deadlock";
172
+ if (SCHEMA_DRIFT_SQLSTATES.has(code))
173
+ return "schema_drift";
174
+ switch (code.slice(0, 2)) {
175
+ case "08": return "connection";
176
+ case "28": return "auth";
177
+ case "22": return "data_exception";
178
+ case "23": return "integrity_constraint";
179
+ case "40": return "transaction_rollback";
180
+ case "42": return "syntax_or_access";
181
+ case "53": return "insufficient_resources";
182
+ case "57": return "admin_shutdown";
183
+ case "58": return "internal";
184
+ case "XX": return "internal";
185
+ case "3F": return "schema_drift";
186
+ default: return null;
187
+ }
188
+ }
189
+ // Walk the Error.cause chain (bounded) and return the first non-null value the
190
+ // reader extracts. drizzle wraps the pg error on `.cause`, and custom wrappers
191
+ // can nest deeper, so a single-level read misses the actionable DB fields when
192
+ // the chain is two or more deep (OXY-38).
193
+ const MAX_CAUSE_DEPTH = 5;
194
+ function readCauseChain(error, read) {
195
+ let current = error;
196
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH; depth++) {
197
+ if (!current || typeof current !== "object")
198
+ return null;
199
+ const value = read(current);
200
+ if (value !== null)
201
+ return value;
202
+ current = current.cause;
203
+ }
204
+ return null;
205
+ }
206
+ function readSqlstate(error) {
207
+ return readCauseChain(error, (obj) => {
208
+ const value = obj.code;
209
+ return typeof value === "string"
210
+ && /^[0-9A-Z]{5}$/.test(value)
211
+ && !CONNECTION_ERRNOS.has(value)
212
+ && !CONNECT_TIMEOUT_ERRNOS.has(value)
213
+ ? value
214
+ : null;
215
+ });
216
+ }
217
+ function readErrno(error) {
218
+ return readCauseChain(error, (obj) => {
219
+ for (const key of ["code", "errno"]) {
220
+ const value = obj[key];
221
+ if (typeof value === "string" && (CONNECTION_ERRNOS.has(value) || CONNECT_TIMEOUT_ERRNOS.has(value))) {
222
+ return value;
223
+ }
224
+ }
225
+ return null;
226
+ });
227
+ }
228
+ // pg sets `error.table` for some failures (notably constraint violations). Used
229
+ // only as a fallback when the table cannot be parsed from the SQL. Identifiers
230
+ // are schema metadata, never customer row data.
231
+ function readTableField(error) {
232
+ return readIdentifierField(error, "table");
233
+ }
234
+ // pg sets schema/column/constraint/table to identifier names (never row
235
+ // values), so they are safe to surface for triage. Guarded by isSafeIdentifier
236
+ // as defence in depth against an unexpected driver placing free text there.
237
+ function readIdentifierField(error, key) {
238
+ return readCauseChain(error, (obj) => {
239
+ const value = obj[key];
240
+ return typeof value === "string" && isSafeIdentifier(value) ? value : null;
241
+ });
242
+ }
243
+ function readSeverity(error) {
244
+ return readCauseChain(error, (obj) => {
245
+ const value = obj.severity;
246
+ // A short keyword (ERROR/FATAL/PANIC/WARNING), possibly localized. Cap the
247
+ // length and restrict to letters/spaces so it can never carry parameter
248
+ // values even if a driver mislabels a field.
249
+ return typeof value === "string" && value.length > 0 && value.length <= 32 && /^[A-Za-z _]+$/.test(value)
250
+ ? value
251
+ : null;
252
+ });
253
+ }
254
+ function readStatusCode(error) {
255
+ return readCauseChain(error, (obj) => {
256
+ for (const key of ["statusCode", "status"]) {
257
+ const value = obj[key];
258
+ if (typeof value === "number" && Number.isInteger(value) && value >= 100 && value <= 599) {
259
+ return value;
260
+ }
261
+ }
262
+ return null;
263
+ });
264
+ }
265
+ function isDrizzleQueryError(error, message) {
266
+ return message.startsWith("Failed query:") || typeof readNestedString(error, ["query"]) === "string";
267
+ }
268
+ /**
269
+ * Returns the parameter-free SQL for shape extraction: drizzle's `.query`
270
+ * (already param-free) when present, else the message with the `Failed query:`
271
+ * prefix removed and the `\nparams: [...]` tail stripped. Never returns the
272
+ * parameter array.
273
+ */
274
+ function readParamFreeSql(error, message) {
275
+ const query = readNestedString(error, ["query"]);
276
+ if (query)
277
+ return query;
278
+ let sql = message;
279
+ const prefixIndex = sql.indexOf("Failed query:");
280
+ if (prefixIndex >= 0)
281
+ sql = sql.slice(prefixIndex + "Failed query:".length);
282
+ const paramsIndex = sql.search(/\n\s*params\s*:/i);
283
+ if (paramsIndex >= 0)
284
+ sql = sql.slice(0, paramsIndex);
285
+ return sql.trim();
286
+ }
287
+ const SQL_VERB = /^(select|insert|update|delete|with|merge|create|alter|drop|truncate)\b/i;
288
+ // Identifier after a table-introducing keyword: optionally double-quoted, with an
289
+ // optional `schema.table` qualification. Stops at the first non-identifier char,
290
+ // so it can never capture a value, a paren-subquery, or whitespace.
291
+ const TABLE_AFTER_KEYWORD = /\b(?:from|into|update|join|table)\s+("?[A-Za-z_][\w$]*"?(?:\s*\.\s*"?[A-Za-z_][\w$]*"?)?)/i;
292
+ function extractQueryShape(sql) {
293
+ const trimmed = sql.replace(/^[\s(]+/, "");
294
+ const kindMatch = SQL_VERB.exec(trimmed);
295
+ const kind = kindMatch?.[1]?.toLowerCase() ?? null;
296
+ const tableMatch = TABLE_AFTER_KEYWORD.exec(trimmed);
297
+ const rawTable = tableMatch?.[1]?.replace(/["\s]/g, "") ?? null;
298
+ const table = rawTable && isSafeIdentifier(rawTable) ? rawTable : null;
299
+ return { kind, table };
300
+ }
301
+ function isSafeIdentifier(value) {
302
+ return value.length > 0 && value.length <= 128 && /^[A-Za-z_][\w$]*(?:\.[A-Za-z_][\w$]*)?$/.test(value);
303
+ }
304
+ function readMessage(error) {
305
+ if (error instanceof Error)
306
+ return error.message ?? "";
307
+ const value = readNestedString(error, ["message"]);
308
+ return value ?? "";
309
+ }
310
+ function readNestedString(error, path) {
311
+ let current = error;
312
+ for (const part of path) {
313
+ if (!current || typeof current !== "object")
314
+ return null;
315
+ current = current[part];
316
+ }
317
+ return typeof current === "string" ? current : null;
318
+ }
@@ -1,6 +1,7 @@
1
1
  import { SpanStatusCode, metrics, trace, } from "@opentelemetry/api";
2
2
  import { errorId } from "./log.js";
3
3
  import { normalizeTelemetryAttributes } from "./redaction.js";
4
+ import { redactSqlParameters, sqlErrorTelemetryAttributes } from "./sql-error.js";
4
5
  import { OXYGEN_VERSION } from "./version.js";
5
6
  const counterCache = new Map();
6
7
  const histogramCache = new Map();
@@ -20,7 +21,13 @@ export async function withTelemetrySpan(tracerName, name, attributes, fn, option
20
21
  }
21
22
  else {
22
23
  span.setStatus({ code: SpanStatusCode.ERROR });
23
- span.setAttributes(normalizeTelemetryAttributes(errorTelemetryAttributes(error)));
24
+ // Overwrite the initial `outcome: "started"` so ERROR-status spans are
25
+ // never left mislabelled as still-running; dashboards/reports that
26
+ // group by outcome would otherwise misclassify real faults (OXY-32).
27
+ span.setAttributes(normalizeTelemetryAttributes({
28
+ ...errorTelemetryAttributes(error),
29
+ outcome: "error",
30
+ }));
24
31
  }
25
32
  throw error;
26
33
  }
@@ -69,9 +76,17 @@ export function commonTelemetryAttributes(attributes) {
69
76
  "deployment.environment": workerProcess
70
77
  ? process.env.FLY_ENVIRONMENT ?? process.env.NODE_ENV ?? null
71
78
  : process.env.VERCEL_ENV ?? process.env.NODE_ENV ?? null,
79
+ // deployment.sha stays the deploy artifact id: the Fly registry image ref on
80
+ // the worker (release cross-reference), VERCEL_GIT_COMMIT_SHA on the web. The
81
+ // worker's image ref is NOT a git commit, so deployment.git_sha carries the
82
+ // resolved commit separately and is the single field that means "the commit"
83
+ // on both surfaces (OXY-61). On the web both happen to equal the Vercel SHA.
72
84
  "deployment.sha": workerProcess
73
85
  ? process.env.FLY_IMAGE_REF ?? process.env.VERCEL_GIT_COMMIT_SHA ?? null
74
86
  : process.env.VERCEL_GIT_COMMIT_SHA ?? process.env.FLY_IMAGE_REF ?? null,
87
+ "deployment.git_sha": workerProcess
88
+ ? process.env.OXYGEN_GIT_SHA ?? process.env.VERCEL_GIT_COMMIT_SHA ?? null
89
+ : process.env.VERCEL_GIT_COMMIT_SHA ?? process.env.OXYGEN_GIT_SHA ?? null,
75
90
  "cloud.region": workerProcess
76
91
  ? process.env.FLY_REGION ?? process.env.VERCEL_REGION ?? null
77
92
  : process.env.VERCEL_REGION ?? process.env.FLY_REGION ?? null,
@@ -104,19 +119,27 @@ function getHistogram(name) {
104
119
  return histogram;
105
120
  }
106
121
  function errorTelemetryAttributes(error) {
122
+ // Structured SQL attribution (SQLSTATE, cause category, query kind/table,
123
+ // transient) when the error is a pg/drizzle failure; {} otherwise, so non-DB
124
+ // spans are unchanged. Never includes SQL text or parameter values (OXY-46).
125
+ const sqlAttributes = sqlErrorTelemetryAttributes(error);
107
126
  if (error instanceof Error) {
108
127
  // pg connect timeouts (and some driver errors) surface with an empty
109
128
  // message; fall back to the error name so spans are never message-less.
110
- const message = error.message.trim() ? error.message : error.name || "unknown_error";
129
+ // Strip drizzle's `\nparams: <values>` tail so parameter values stay out of
130
+ // telemetry (OXY-46).
131
+ const message = error.message.trim() ? redactSqlParameters(error.message) : error.name || "unknown_error";
111
132
  return {
112
133
  "error.id": errorId(error),
113
134
  "error.name": error.name,
114
135
  "error.message": message,
136
+ ...sqlAttributes,
115
137
  };
116
138
  }
117
- const text = String(error).trim();
139
+ const text = redactSqlParameters(String(error)).trim();
118
140
  return {
119
141
  "error.id": "non_error",
120
142
  "error.message": text || "unknown_error",
143
+ ...sqlAttributes,
121
144
  };
122
145
  }
@@ -1,2 +1,2 @@
1
- export declare const OXYGEN_VERSION = "1.146.1";
2
- export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.135.0";
1
+ export declare const OXYGEN_VERSION = "1.160.18";
2
+ export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.154.0";
@@ -1,3 +1,6 @@
1
- export const OXYGEN_VERSION = "1.146.1";
1
+ export const OXYGEN_VERSION = "1.160.18";
2
2
  // Bump this only when deployed CLI/API contracts require a newer CLI.
3
- export const OXYGEN_MINIMUM_CLI_VERSION = "1.135.0";
3
+ // 1.154.0: LinkedIn → Sequencer rename moved the CLI/API/MCP surface
4
+ // (oxygen sequences|inbox|senders, /api/cli/{sequences,inbox,senders}) and
5
+ // removed the old /api/cli/linkedin/* routes — older CLIs would 404.
6
+ export const OXYGEN_MINIMUM_CLI_VERSION = "1.154.0";
@@ -334,7 +334,6 @@ export declare function branchStep(input: {
334
334
  export declare function isWorkflowDefinition(value: unknown): value is WorkflowDefinition;
335
335
  export declare function isWorkflowManifest(value: unknown): value is WorkflowManifest;
336
336
  export declare function isRecipeManifest(value: unknown): value is RecipeManifest;
337
- export declare function isDurableRecipeManifest(value: unknown): value is RecipeManifest;
338
337
  export declare function isAnyWorkflowManifest(value: unknown): value is AnyWorkflowManifest;
339
338
  export declare function compileWorkflowDefinition(// skipcq: JS-R1005 -- compiler validates workflow metadata, trigger, steps, branch targets, and defaults together.
340
339
  definition: WorkflowDefinition, options?: {
@@ -533,24 +532,6 @@ export declare const workflowManifestSchema: {
533
532
  readonly additionalProperties: true;
534
533
  readonly required: readonly ["manifest_version", "workflow", "steps", "source_hash", "compiler_version"];
535
534
  };
536
- export declare function getWorkflowApplySchema(): {
537
- readonly $schema: "https://json-schema.org/draft/2020-12/schema";
538
- readonly title: "OXYGEN Workflow Apply Input";
539
- readonly type: "object";
540
- readonly additionalProperties: false;
541
- readonly properties: {
542
- readonly manifest: {
543
- readonly $ref: "#/$defs/manifest";
544
- };
545
- };
546
- readonly required: readonly ["manifest"];
547
- readonly $defs: {
548
- readonly manifest: {
549
- readonly type: "object";
550
- readonly additionalProperties: true;
551
- };
552
- };
553
- };
554
535
  export declare function getWorkflowSchema(subject?: "apply" | "call" | "event" | "trigger" | "manifest" | "all"): {
555
536
  readonly $schema: "https://json-schema.org/draft/2020-12/schema";
556
537
  readonly title: "OXYGEN Workflow Apply Input";
@@ -31,6 +31,10 @@ const UNSAFE_RECIPE_BUNDLE_PATTERNS = [
31
31
  { token: "setTimeout", pattern: /\bsetTimeout\b/ },
32
32
  { token: "setInterval", pattern: /\bsetInterval\b/ }, // skipcq: SCT-A000
33
33
  ];
34
+ /** A valid workflow / recipe / trigger status. Optional everywhere it appears. */
35
+ function isValidWorkflowStatus(value) {
36
+ return value === "active" || value === "disabled";
37
+ }
34
38
  export function defineWorkflow(input) {
35
39
  return {
36
40
  __oxygen_workflow_definition: true,
@@ -140,9 +144,6 @@ export function isRecipeManifest(value) {
140
144
  && value.manifest_version === WORKFLOW_MANIFEST_VERSION
141
145
  && value.compiler_version === DURABLE_RECIPE_COMPILER_VERSION;
142
146
  }
143
- export function isDurableRecipeManifest(value) {
144
- return isRecipeManifest(value);
145
- }
146
147
  export function isAnyWorkflowManifest(value) {
147
148
  return isWorkflowManifest(value) || isRecipeManifest(value);
148
149
  }
@@ -248,7 +249,7 @@ value, options = {}) {
248
249
  add("$.workflow.id", "invalid_workflow_id", "Workflow id is required.");
249
250
  if (!isNonEmptyString(workflow.name))
250
251
  add("$.workflow.name", "invalid_workflow_name", "Workflow name is required.");
251
- if (workflow.status !== undefined && workflow.status !== "active" && workflow.status !== "disabled") {
252
+ if (workflow.status !== undefined && !isValidWorkflowStatus(workflow.status)) {
252
253
  add("$.workflow.status", "invalid_workflow_status", "Workflow status must be active or disabled.");
253
254
  }
254
255
  }
@@ -371,7 +372,7 @@ value, options = {}) {
371
372
  add("$.workflow.id", "invalid_workflow_id", "Recipe id is required.");
372
373
  if (!isNonEmptyString(workflow.name))
373
374
  add("$.workflow.name", "invalid_workflow_name", "Recipe name is required.");
374
- if (workflow.status !== undefined && workflow.status !== "active" && workflow.status !== "disabled") {
375
+ if (workflow.status !== undefined && !isValidWorkflowStatus(workflow.status)) {
375
376
  add("$.workflow.status", "invalid_workflow_status", "Recipe status must be active or disabled.");
376
377
  }
377
378
  }
@@ -904,9 +905,6 @@ export const workflowManifestSchema = {
904
905
  additionalProperties: true,
905
906
  required: ["manifest_version", "workflow", "steps", "source_hash", "compiler_version"],
906
907
  };
907
- export function getWorkflowApplySchema() {
908
- return workflowApplySchema;
909
- }
910
908
  export function getWorkflowSchema(subject = "apply") {
911
909
  const schemas = {
912
910
  apply: workflowApplySchema,
@@ -1042,13 +1040,20 @@ function formatSchemaTypes(types) {
1042
1040
  function jsonEqual(left, right) {
1043
1041
  return JSON.stringify(left) === JSON.stringify(right);
1044
1042
  }
1043
+ // Optional idempotency_key_path (webhook + event triggers): when present it must
1044
+ // be a non-empty string. `label` distinguishes the trigger type in the message.
1045
+ function validateOptionalIdempotencyKeyPath(value, path, label, add) {
1046
+ if (value !== undefined && value !== null && !isNonEmptyString(value)) {
1047
+ add(`${path}.idempotency_key_path`, "invalid_idempotency_key_path", `${label} idempotency path is invalid.`);
1048
+ }
1049
+ }
1045
1050
  function validateTrigger(// skipcq: JS-R1005
1046
1051
  value, path, add) {
1047
1052
  if (!isRecord(value)) {
1048
1053
  add(path, "invalid_trigger", "Workflow trigger must be an object.");
1049
1054
  return;
1050
1055
  }
1051
- if (value.status !== undefined && value.status !== "active" && value.status !== "disabled") {
1056
+ if (value.status !== undefined && !isValidWorkflowStatus(value.status)) {
1052
1057
  add(`${path}.status`, "invalid_trigger_status", "Trigger status must be active or disabled.");
1053
1058
  }
1054
1059
  if (value.type === "api")
@@ -1057,11 +1062,7 @@ value, path, add) {
1057
1062
  if (!isNonEmptyString(value.trigger_id)) {
1058
1063
  add(`${path}.trigger_id`, "invalid_trigger_id", "Webhook trigger_id is required.");
1059
1064
  }
1060
- if (value.idempotency_key_path !== undefined
1061
- && value.idempotency_key_path !== null
1062
- && !isNonEmptyString(value.idempotency_key_path)) {
1063
- add(`${path}.idempotency_key_path`, "invalid_idempotency_key_path", "Webhook idempotency path is invalid.");
1064
- }
1065
+ validateOptionalIdempotencyKeyPath(value.idempotency_key_path, path, "Webhook", add);
1065
1066
  return;
1066
1067
  }
1067
1068
  if (value.type === "cron") {
@@ -1085,11 +1086,7 @@ value, path, add) {
1085
1086
  if (!isNonEmptyString(value.event)) {
1086
1087
  add(`${path}.event`, "invalid_event_type", "Event trigger event is required.");
1087
1088
  }
1088
- if (value.idempotency_key_path !== undefined
1089
- && value.idempotency_key_path !== null
1090
- && !isNonEmptyString(value.idempotency_key_path)) {
1091
- add(`${path}.idempotency_key_path`, "invalid_idempotency_key_path", "Event idempotency path is invalid.");
1092
- }
1089
+ validateOptionalIdempotencyKeyPath(value.idempotency_key_path, path, "Event", add);
1093
1090
  if (value.filters !== undefined)
1094
1091
  validateEventFilters(value.filters, `${path}.filters`, add);
1095
1092
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.146.1",
3
+ "version": "1.160.18",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",