@automagik/omni 2.260908.2 → 2.260908.4

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.
@@ -31173,7 +31173,7 @@ function zodToEventJsonSchema(schema, name) {
31173
31173
  }
31174
31174
  return artifact;
31175
31175
  }
31176
- var import_ajv, SCHEMA_VALIDATION_FAILED = "schema_validation_failed", ajv, compiled;
31176
+ var import_ajv, SCHEMA_VALIDATION_FAILED = "schema_validation_failed", SCHEMA_NOT_REGISTERED = "schema_not_registered", ajv, compiled;
31177
31177
  var init_schema_registry = __esm(() => {
31178
31178
  init_esm();
31179
31179
  import_ajv = __toESM(require_ajv(), 1);
@@ -37807,6 +37807,16 @@ async function releaseEmissionClaim(deps, claimedEventId, eventType) {
37807
37807
  });
37808
37808
  });
37809
37809
  }
37810
+ async function enforceEmitEventSchema(deps, eventType, payload, trustedTenantId) {
37811
+ if (!deps.validateEmitEvent)
37812
+ return null;
37813
+ const verdict = await deps.validateEmitEvent(eventType, payload, trustedTenantId);
37814
+ if (verdict.valid)
37815
+ return null;
37816
+ const detail = verdict.errors?.length ? `: ${verdict.errors.join("; ")}` : "";
37817
+ logger2.warn("Emit event refused by schema registry", { eventType, errors: verdict.errors });
37818
+ return `${verdict.reason ?? SCHEMA_VALIDATION_FAILED}${detail}`;
37819
+ }
37810
37820
  async function executeEmitEventAction(config2, context2, deps, trustedTenantId, actionIndex = 0, provenance) {
37811
37821
  try {
37812
37822
  if (!deps.eventBus) {
@@ -37822,13 +37832,9 @@ async function executeEmitEventAction(config2, context2, deps, trustedTenantId,
37822
37832
  payload = context2.payload;
37823
37833
  }
37824
37834
  const eventType = substituteTemplate(config2.eventType, context2);
37825
- if (deps.validateEmitEvent) {
37826
- const verdict = await deps.validateEmitEvent(eventType, payload, trustedTenantId);
37827
- if (!verdict.valid) {
37828
- const detail = verdict.errors?.length ? `: ${verdict.errors.join("; ")}` : "";
37829
- logger2.warn("Emit event refused by schema registry", { eventType, errors: verdict.errors });
37830
- return { success: false, error: `${SCHEMA_VALIDATION_FAILED}${detail}` };
37831
- }
37835
+ const schemaError = await enforceEmitEventSchema(deps, eventType, payload, trustedTenantId);
37836
+ if (schemaError !== null) {
37837
+ return { success: false, error: schemaError };
37832
37838
  }
37833
37839
  const correlationId = context2.event?.metadata.correlationId ?? context2.payload.correlationId ?? undefined;
37834
37840
  const causationId = context2.event?.id ?? provenance?.parentEventId;
@@ -44136,6 +44142,7 @@ __export(exports_src, {
44136
44142
  SETTING_VALUE_TYPES: () => SETTING_VALUE_TYPES,
44137
44143
  SECRET_BOX_VERSION: () => SECRET_BOX_VERSION,
44138
44144
  SCHEMA_VALIDATION_FAILED: () => SCHEMA_VALIDATION_FAILED,
44145
+ SCHEMA_NOT_REGISTERED: () => SCHEMA_NOT_REGISTERED,
44139
44146
  RuleTypeSchema: () => RuleTypeSchema,
44140
44147
  ReplyFilterModeSchema: () => ReplyFilterModeSchema,
44141
44148
  RULE_TYPES: () => RULE_TYPES,
@@ -173707,6 +173714,7 @@ var init_schema2 = __esm(() => {
173707
173714
  signatureSecret: text("signature_secret"),
173708
173715
  idempotencyKeyTemplate: text("idempotency_key_template").notNull().default("{source}:{sha256(body)}"),
173709
173716
  eventTypeMapping: jsonb("event_type_mapping").$type(),
173717
+ strictSchemas: boolean2("strict_schemas").notNull().default(false),
173710
173718
  enabled: boolean2("enabled").notNull().default(true),
173711
173719
  lastReceivedAt: timestamp("last_received_at", { withTimezone: true }),
173712
173720
  totalReceived: integer("total_received").notNull().default(0),
@@ -250967,7 +250975,7 @@ var init_sentry_scrub = __esm(() => {
250967
250975
  var require_package7 = __commonJS((exports, module) => {
250968
250976
  module.exports = {
250969
250977
  name: "@omni/api",
250970
- version: "2.260908.2",
250978
+ version: "2.260908.4",
250971
250979
  private: true,
250972
250980
  type: "module",
250973
250981
  exports: {
@@ -358528,7 +358536,7 @@ class DeadLetterService {
358528
358536
  eventType: input.eventType,
358529
358537
  subject: input.subject,
358530
358538
  payload: input.payload,
358531
- error: `${SCHEMA_VALIDATION_FAILED}: ${input.errors.join("; ")}`,
358539
+ error: `${input.reason ?? SCHEMA_VALIDATION_FAILED}: ${input.errors.join("; ")}`,
358532
358540
  stack: null,
358533
358541
  autoRetryCount: 0,
358534
358542
  nextAutoRetryAt: null,
@@ -363650,8 +363658,14 @@ function sha256Hex(input) {
363650
363658
  function resolvePayloadPath(payload, path3) {
363651
363659
  let current = payload;
363652
363660
  for (const part of path3.split(".")) {
363653
- if (current === null || typeof current !== "object" || Array.isArray(current))
363661
+ if (current === null || typeof current !== "object")
363654
363662
  return;
363663
+ if (Array.isArray(current)) {
363664
+ if (!/^(0|[1-9]\d*)$/.test(part))
363665
+ return;
363666
+ current = current[Number(part)];
363667
+ continue;
363668
+ }
363655
363669
  current = current[part];
363656
363670
  }
363657
363671
  if (typeof current === "string")
@@ -363720,12 +363734,13 @@ function sanitizeEventToken(raw2) {
363720
363734
  const token = raw2.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
363721
363735
  return token.length > 0 ? token : null;
363722
363736
  }
363723
- function resolveWebhookEventType(sourceName, mapping, headers) {
363737
+ function resolveWebhookEventType(sourceName, mapping, headers, payload = {}) {
363724
363738
  const fallback = `custom.webhook.${sourceName}`;
363725
- if (!mapping || mapping.source !== "header") {
363739
+ if (!mapping) {
363726
363740
  return fallback;
363727
363741
  }
363728
- const token = sanitizeEventToken(headers[mapping.header.toLowerCase()]);
363742
+ const raw2 = mapping.source === "header" ? headers[mapping.header.toLowerCase()] : resolvePayloadPath(payload, mapping.path);
363743
+ const token = sanitizeEventToken(raw2);
363729
363744
  return token ? `custom.${sourceName}.${token}` : fallback;
363730
363745
  }
363731
363746
 
@@ -363864,13 +363879,13 @@ class WebhookService {
363864
363879
  }
363865
363880
  }
363866
363881
  }
363867
- const eventType = resolveWebhookEventType(sourceName, source.eventTypeMapping, headers);
363882
+ const eventType = resolveWebhookEventType(sourceName, source.eventTypeMapping, headers, payload);
363868
363883
  const eventPayload = {
363869
363884
  source: sourceName,
363870
363885
  ...payload
363871
363886
  };
363872
363887
  const eventId = generateId();
363873
- await this.enforceRegisteredSchema(eventType, eventPayload, eventId, `webhook '${sourceName}'`);
363888
+ await this.enforceRegisteredSchema(eventType, eventPayload, eventId, `webhook '${sourceName}'`, source.strictSchemas);
363874
363889
  const idempotencyKey = deriveIdempotencyKey({
363875
363890
  template: source.idempotencyKeyTemplate,
363876
363891
  sourceName,
@@ -363963,11 +363978,24 @@ class WebhookService {
363963
363978
  throw unauthorized("Invalid webhook signature");
363964
363979
  }
363965
363980
  }
363966
- async enforceRegisteredSchema(eventType, payload, eventId, origin) {
363981
+ async enforceRegisteredSchema(eventType, payload, eventId, origin, strict = false) {
363967
363982
  if (!this.eventSchemas) {
363968
363983
  return;
363969
363984
  }
363970
363985
  const verdict = await this.eventSchemas.validate(eventType, payload);
363986
+ if (strict && !verdict.registered) {
363987
+ const errors4 = [`no enabled schema is registered for event type '${eventType}'`];
363988
+ await this.deadLetters?.createSchemaValidationFailure({
363989
+ eventId,
363990
+ eventType,
363991
+ subject: eventType,
363992
+ payload,
363993
+ errors: errors4,
363994
+ reason: SCHEMA_NOT_REGISTERED
363995
+ });
363996
+ log112.warn("Webhook refused by strict schema mode: type not registered", { eventType, origin });
363997
+ throw new ValidationError2(`${SCHEMA_NOT_REGISTERED}: '${eventType}' has no enabled registered schema and the source enforces strict schemas (${origin})`, undefined, { eventType, errors: errors4 });
363998
+ }
363971
363999
  if (verdict.valid) {
363972
364000
  return;
363973
364001
  }
@@ -367846,7 +367874,7 @@ function registerEventSchemaSchemas(registry4) {
367846
367874
  operationId: "registerEventSchema",
367847
367875
  tags: ["Events"],
367848
367876
  summary: "Register or revise an event schema",
367849
- description: "Register a JSON Schema for an event type. Once registered, the webhook ingress and automation " + "emit_event validate payloads of this type before publishing; invalid payloads are dead-lettered with " + "reason schema_validation_failed. Revising an existing registration must be additive-optional " + "(the evolution rule) \u2014 an incompatible change is refused with 409 and must ship as a new versioned " + "event type (e.g. custom.github.push.v2).",
367877
+ description: "Register a JSON Schema for an event type. Once registered, the webhook ingress and automation " + "emit_event validate payloads of this type before publishing; invalid payloads are dead-lettered with " + "reason schema_validation_failed. Unregistered types pass through unless the emitter is strict " + "(webhook_sources.strictSchemas, or OMNI_STRICT_EMIT_EVENT_SCHEMAS for emit_event), in which case " + "they are dead-lettered with reason schema_not_registered. " + "Revising an existing registration must be additive-optional " + "(the evolution rule) \u2014 an incompatible change is refused with 409 and must ship as a new versioned " + "event type (e.g. custom.github.push.v2).",
367850
367878
  request: { body: { content: { "application/json": { schema: RegisterEventSchemaSchema } } } },
367851
367879
  responses: {
367852
367880
  201: {
@@ -370952,14 +370980,24 @@ var init_webhooks2 = __esm(() => {
370952
370980
  message: "prefix is not applicable to algorithm 'token-match'",
370953
370981
  path: ["prefix"]
370954
370982
  });
370955
- WebhookEventTypeMappingSchema = exports_external.object({
370956
- source: exports_external.literal("header").openapi({
370957
- description: "Where the semantic event name is read from (only headers for now)"
370983
+ WebhookEventTypeMappingSchema = exports_external.discriminatedUnion("source", [
370984
+ exports_external.object({
370985
+ source: exports_external.literal("header").openapi({
370986
+ description: "Read the semantic event name from a request header"
370987
+ }),
370988
+ header: exports_external.string().min(1).max(200).openapi({
370989
+ description: "Header carrying the semantic event name (e.g. X-GitHub-Event). 1-200 characters"
370990
+ })
370958
370991
  }),
370959
- header: exports_external.string().min(1).max(200).openapi({
370960
- description: "Header carrying the semantic event name (e.g. X-GitHub-Event). 1-200 characters"
370992
+ exports_external.object({
370993
+ source: exports_external.literal("body").openapi({
370994
+ description: "Read the semantic event name from the JSON body (body-first providers like ClickUp, #984)"
370995
+ }),
370996
+ path: exports_external.string().min(1).max(200).openapi({
370997
+ description: 'Dot-path to the event name in the payload (e.g. "event"); numeric segments index arrays. 1-200 characters'
370998
+ })
370961
370999
  })
370962
- });
371000
+ ]);
370963
371001
  WebhookSourceSchema = exports_external.object({
370964
371002
  id: exports_external.string().uuid().openapi({ description: "Source UUID" }),
370965
371003
  name: exports_external.string().openapi({ description: "Source name" }),
@@ -370972,6 +371010,9 @@ var init_webhooks2 = __esm(() => {
370972
371010
  hasSignatureSecret: exports_external.boolean().openapi({ description: "Whether a signature secret is stored (secret is write-only)" }),
370973
371011
  idempotencyKeyTemplate: exports_external.string().openapi({ description: "Idempotency key derivation template" }),
370974
371012
  totalDuplicates: exports_external.number().int().openapi({ description: "Redeliveries acked without creating a second event" }),
371013
+ strictSchemas: exports_external.boolean().openapi({
371014
+ description: "Strict schema mode: deliveries resolving to an event type with no enabled registered schema are " + "refused and dead-lettered with reason schema_not_registered"
371015
+ }),
370975
371016
  enabled: exports_external.boolean().openapi({ description: "Whether enabled" }),
370976
371017
  lastReceivedAt: exports_external.string().datetime().nullable().openapi({ description: "When the last webhook was received" }),
370977
371018
  totalReceived: exports_external.number().int().openapi({ description: "Total webhooks received" }),
@@ -370997,6 +371038,9 @@ var init_webhooks2 = __esm(() => {
370997
371038
  description: "Shared secret used by signatureConfig (write-only, never returned; 8-512 characters). Cannot be set " + "without a signatureConfig (given in the same request, or already stored on update); null clears it."
370998
371039
  }),
370999
371040
  idempotencyKeyTemplate: IdempotencyKeyTemplateSchema.optional(),
371041
+ strictSchemas: exports_external.boolean().optional().openapi({
371042
+ description: "Strict schema mode (RFC #925 G1 policy switch): when true, a delivery resolving to an event type " + "with no enabled registered schema is refused and dead-lettered with reason schema_not_registered " + "(manual retry only) instead of passing through. Defaults to false (opt-in pass-through). " + "Recommended true for NEW sources \u2014 they have no legacy emitters to grandfather."
371043
+ }),
371000
371044
  eventTypeMapping: WebhookEventTypeMappingSchema.nullable().optional().openapi({
371001
371045
  description: "Semantic event-type extraction (e.g. header X-GitHub-Event: push emits custom.{source}.push). " + "Null or absent keeps the legacy collapsed custom.webhook.{source} type for every delivery."
371002
371046
  }),
@@ -387792,16 +387836,21 @@ function buildAutomationEngineDeps(services, db3, options = {}) {
387792
387836
  },
387793
387837
  validateEmitEvent: async (eventType, payload, trustedTenantId = null) => {
387794
387838
  const verdict = await runTenantWorkDb(db3, trustedTenantId, () => services.eventSchemas.validate(eventType, payload));
387795
- if (verdict.valid) {
387839
+ const strictEmit = process.env.OMNI_STRICT_EMIT_EVENT_SCHEMAS === "true";
387840
+ const refusedUnregistered = strictEmit && !verdict.registered;
387841
+ if (verdict.valid && !refusedUnregistered) {
387796
387842
  return { valid: true };
387797
387843
  }
387844
+ const errors4 = refusedUnregistered ? [`no enabled schema is registered for event type '${eventType}'`] : verdict.errors;
387845
+ const reason2 = refusedUnregistered ? SCHEMA_NOT_REGISTERED : undefined;
387798
387846
  try {
387799
387847
  await runTenantWorkDb(db3, trustedTenantId, () => services.deadLetters.createSchemaValidationFailure({
387800
387848
  eventId: generateId(),
387801
387849
  eventType,
387802
387850
  subject: eventType,
387803
387851
  payload,
387804
- errors: verdict.errors
387852
+ errors: errors4,
387853
+ reason: reason2
387805
387854
  }));
387806
387855
  } catch (error3) {
387807
387856
  log130.error("Failed to dead-letter refused emit_event payload", {
@@ -387809,7 +387858,7 @@ function buildAutomationEngineDeps(services, db3, options = {}) {
387809
387858
  error: error3 instanceof Error ? error3.message : String(error3)
387810
387859
  });
387811
387860
  }
387812
- return { valid: false, errors: verdict.errors };
387861
+ return { valid: false, errors: errors4, reason: reason2 };
387813
387862
  }
387814
387863
  };
387815
387864
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automagik/omni",
3
- "version": "2.260908.2",
3
+ "version": "2.260908.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/automagik-dev/omni.git",