@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.
- package/db/drizzle/0060_webhook_source_strict_schemas.sql +24 -0
- package/db/drizzle/meta/_journal.json +7 -0
- package/dist/commands/webhooks.d.ts.map +1 -1
- package/dist/index.js +22 -11
- package/dist/sdk/client.d.ts +7 -0
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/types.generated.d.ts +106 -18
- package/dist/sdk/types.generated.d.ts.map +1 -1
- package/dist/server/index.js +76 -27
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -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
|
-
|
|
37826
|
-
|
|
37827
|
-
|
|
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.
|
|
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"
|
|
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
|
|
363739
|
+
if (!mapping) {
|
|
363726
363740
|
return fallback;
|
|
363727
363741
|
}
|
|
363728
|
-
const
|
|
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.
|
|
370956
|
-
|
|
370957
|
-
|
|
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
|
-
|
|
370960
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
387861
|
+
return { valid: false, errors: errors4, reason: reason2 };
|
|
387813
387862
|
}
|
|
387814
387863
|
};
|
|
387815
387864
|
}
|