@end-close/relay 0.12.0
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/COMPATIBILITY.md +39 -0
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/dist/config/schema.d.ts +993 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +156 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/crypto/at-rest.d.ts +6 -0
- package/dist/crypto/at-rest.d.ts.map +1 -0
- package/dist/crypto/at-rest.js +19 -0
- package/dist/crypto/at-rest.js.map +1 -0
- package/dist/crypto/keys.d.ts +2 -0
- package/dist/crypto/keys.d.ts.map +1 -0
- package/dist/crypto/keys.js +12 -0
- package/dist/crypto/keys.js.map +1 -0
- package/dist/engine/codec.d.ts +12 -0
- package/dist/engine/codec.d.ts.map +1 -0
- package/dist/engine/codec.js +21 -0
- package/dist/engine/codec.js.map +1 -0
- package/dist/engine/hooks.d.ts +84 -0
- package/dist/engine/hooks.d.ts.map +1 -0
- package/dist/engine/hooks.js +34 -0
- package/dist/engine/hooks.js.map +1 -0
- package/dist/engine/ingest.d.ts +39 -0
- package/dist/engine/ingest.d.ts.map +1 -0
- package/dist/engine/ingest.js +131 -0
- package/dist/engine/ingest.js.map +1 -0
- package/dist/engine/memory-store.d.ts +35 -0
- package/dist/engine/memory-store.d.ts.map +1 -0
- package/dist/engine/memory-store.js +205 -0
- package/dist/engine/memory-store.js.map +1 -0
- package/dist/engine/relay.d.ts +116 -0
- package/dist/engine/relay.d.ts.map +1 -0
- package/dist/engine/relay.js +180 -0
- package/dist/engine/relay.js.map +1 -0
- package/dist/engine/secrets.d.ts +18 -0
- package/dist/engine/secrets.d.ts.map +1 -0
- package/dist/engine/secrets.js +42 -0
- package/dist/engine/secrets.js.map +1 -0
- package/dist/engine/settings.d.ts +29 -0
- package/dist/engine/settings.d.ts.map +1 -0
- package/dist/engine/settings.js +13 -0
- package/dist/engine/settings.js.map +1 -0
- package/dist/engine/store.d.ts +141 -0
- package/dist/engine/store.d.ts.map +1 -0
- package/dist/engine/store.js +52 -0
- package/dist/engine/store.js.map +1 -0
- package/dist/forward/backoff.d.ts +3 -0
- package/dist/forward/backoff.d.ts.map +1 -0
- package/dist/forward/backoff.js +10 -0
- package/dist/forward/backoff.js.map +1 -0
- package/dist/forward/dispatcher.d.ts +98 -0
- package/dist/forward/dispatcher.d.ts.map +1 -0
- package/dist/forward/dispatcher.js +444 -0
- package/dist/forward/dispatcher.js.map +1 -0
- package/dist/forward/endclose-client.d.ts +42 -0
- package/dist/forward/endclose-client.d.ts.map +1 -0
- package/dist/forward/endclose-client.js +76 -0
- package/dist/forward/endclose-client.js.map +1 -0
- package/dist/forward/enrich.d.ts +39 -0
- package/dist/forward/enrich.d.ts.map +1 -0
- package/dist/forward/enrich.js +77 -0
- package/dist/forward/enrich.js.map +1 -0
- package/dist/forward/mapper.d.ts +50 -0
- package/dist/forward/mapper.d.ts.map +1 -0
- package/dist/forward/mapper.js +141 -0
- package/dist/forward/mapper.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest/adapters/generic-hmac.d.ts +3 -0
- package/dist/ingest/adapters/generic-hmac.d.ts.map +1 -0
- package/dist/ingest/adapters/generic-hmac.js +60 -0
- package/dist/ingest/adapters/generic-hmac.js.map +1 -0
- package/dist/ingest/adapters/payabli.d.ts +3 -0
- package/dist/ingest/adapters/payabli.d.ts.map +1 -0
- package/dist/ingest/adapters/payabli.js +57 -0
- package/dist/ingest/adapters/payabli.js.map +1 -0
- package/dist/ingest/adapters/registry.d.ts +4 -0
- package/dist/ingest/adapters/registry.d.ts.map +1 -0
- package/dist/ingest/adapters/registry.js +16 -0
- package/dist/ingest/adapters/registry.js.map +1 -0
- package/dist/ingest/adapters/types.d.ts +28 -0
- package/dist/ingest/adapters/types.d.ts.map +1 -0
- package/dist/ingest/adapters/types.js +5 -0
- package/dist/ingest/adapters/types.js.map +1 -0
- package/dist/logger.d.ts +12 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +14 -0
- package/dist/logger.js.map +1 -0
- package/dist/mask/defaults.d.ts +7 -0
- package/dist/mask/defaults.d.ts.map +1 -0
- package/dist/mask/defaults.js +51 -0
- package/dist/mask/defaults.js.map +1 -0
- package/dist/mask/paths.d.ts +7 -0
- package/dist/mask/paths.d.ts.map +1 -0
- package/dist/mask/paths.js +36 -0
- package/dist/mask/paths.js.map +1 -0
- package/dist/util/payload-shape.d.ts +6 -0
- package/dist/util/payload-shape.d.ts.map +1 -0
- package/dist/util/payload-shape.js +19 -0
- package/dist/util/payload-shape.js.map +1 -0
- package/dist/util/strings.d.ts +3 -0
- package/dist/util/strings.d.ts.map +1 -0
- package/dist/util/strings.js +7 -0
- package/dist/util/strings.js.map +1 -0
- package/package.json +57 -0
- package/src/config/schema.ts +177 -0
- package/src/crypto/at-rest.ts +21 -0
- package/src/crypto/keys.ts +13 -0
- package/src/engine/codec.ts +30 -0
- package/src/engine/hooks.ts +76 -0
- package/src/engine/ingest.ts +194 -0
- package/src/engine/memory-store.ts +232 -0
- package/src/engine/relay.ts +294 -0
- package/src/engine/secrets.ts +51 -0
- package/src/engine/settings.ts +42 -0
- package/src/engine/store.ts +188 -0
- package/src/forward/backoff.ts +10 -0
- package/src/forward/dispatcher.ts +504 -0
- package/src/forward/endclose-client.ts +102 -0
- package/src/forward/enrich.ts +106 -0
- package/src/forward/mapper.ts +191 -0
- package/src/index.ts +47 -0
- package/src/ingest/adapters/generic-hmac.ts +70 -0
- package/src/ingest/adapters/payabli.ts +67 -0
- package/src/ingest/adapters/registry.ts +21 -0
- package/src/ingest/adapters/types.ts +33 -0
- package/src/logger.ts +26 -0
- package/src/mask/defaults.ts +56 -0
- package/src/mask/paths.ts +36 -0
- package/src/util/payload-shape.ts +19 -0
- package/src/util/strings.ts +7 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { keyNameIsSensitive } from '../mask/defaults.js';
|
|
3
|
+
// Dot-notation path into the webhook payload, e.g. "batchId", "batch.id",
|
|
4
|
+
// "transactions.*.id" (a `*` segment fans out over an array).
|
|
5
|
+
const dotPath = z
|
|
6
|
+
.string()
|
|
7
|
+
.regex(/^[^\s.]+(\.[^\s.]+)*$/, 'must be a dot-notation path like batchId or batch.id');
|
|
8
|
+
export const transformNameSchema = z.enum(['trim', 'lowercase', 'hash']);
|
|
9
|
+
// A mapped field: either a bare source path, or an object with optional transforms
|
|
10
|
+
// (single or array, applied in order).
|
|
11
|
+
// external_id: transferId
|
|
12
|
+
// customer_email: { source: CustomerEmail, transform: hash }
|
|
13
|
+
// customer_email: { source: CustomerEmail, transform: [trim, lowercase, hash] }
|
|
14
|
+
// Strict, so a stray key (a typo, or `enrich` where it is not allowed) is an error
|
|
15
|
+
// rather than silently ignored.
|
|
16
|
+
export const fieldRefSchema = z.union([
|
|
17
|
+
dotPath,
|
|
18
|
+
z
|
|
19
|
+
.object({
|
|
20
|
+
source: dotPath,
|
|
21
|
+
transform: z.union([transformNameSchema, z.array(transformNameSchema).min(1)]).optional(),
|
|
22
|
+
})
|
|
23
|
+
.strict(),
|
|
24
|
+
]);
|
|
25
|
+
// Name of an enrichment registered by the host via createRelay({ enrichments }). Only
|
|
26
|
+
// `description` and `metadata` entries may name one: the value at `source` (after any
|
|
27
|
+
// transforms) is handed to the host function and its return value is what is forwarded.
|
|
28
|
+
export const enrichNameSchema = z
|
|
29
|
+
.string()
|
|
30
|
+
.regex(/^[a-z][a-z0-9_]*$/, 'enrichment name must be a lowercase snake_case identifier');
|
|
31
|
+
// resident_name: { source: PayorId, enrich: resident_name }
|
|
32
|
+
// resident_unit: { source: PayorId, transform: trim, enrich: resident_unit }
|
|
33
|
+
export const enrichableFieldRefSchema = z.union([
|
|
34
|
+
dotPath,
|
|
35
|
+
z
|
|
36
|
+
.object({
|
|
37
|
+
source: dotPath,
|
|
38
|
+
transform: z.union([transformNameSchema, z.array(transformNameSchema).min(1)]).optional(),
|
|
39
|
+
enrich: enrichNameSchema.optional(),
|
|
40
|
+
})
|
|
41
|
+
.strict(),
|
|
42
|
+
]);
|
|
43
|
+
export function refSource(ref) {
|
|
44
|
+
return typeof ref === 'string' ? ref : ref.source;
|
|
45
|
+
}
|
|
46
|
+
export function refTransforms(ref) {
|
|
47
|
+
if (typeof ref === 'string' || ref.transform === undefined)
|
|
48
|
+
return [];
|
|
49
|
+
return Array.isArray(ref.transform) ? ref.transform : [ref.transform];
|
|
50
|
+
}
|
|
51
|
+
/** The enrichment a field names, if any. */
|
|
52
|
+
export function refEnrichment(ref) {
|
|
53
|
+
return typeof ref === 'string' ? undefined : ref.enrich;
|
|
54
|
+
}
|
|
55
|
+
const dateRefSchema = z.union([
|
|
56
|
+
dotPath,
|
|
57
|
+
z.object({
|
|
58
|
+
source: dotPath,
|
|
59
|
+
format: z.enum(['iso8601', 'mdy_hms']).default('iso8601'),
|
|
60
|
+
}),
|
|
61
|
+
]);
|
|
62
|
+
// A secret reference: the NAME the host's SecretResolver resolves (an env var in the
|
|
63
|
+
// application). The value itself never appears in config or the database. The field is
|
|
64
|
+
// deliberately named as a reference so a value is never mistaken for it.
|
|
65
|
+
const secretRef = z.string().min(1);
|
|
66
|
+
export const payabliAuthSchema = z.object({
|
|
67
|
+
mode: z.literal('static_header'),
|
|
68
|
+
header: z.string().default('authorization'),
|
|
69
|
+
secret_env: secretRef,
|
|
70
|
+
allowed_ips: z.array(z.string()).default([]),
|
|
71
|
+
});
|
|
72
|
+
export const genericHmacAuthSchema = z.object({
|
|
73
|
+
mode: z.literal('hmac'),
|
|
74
|
+
header: z.string(),
|
|
75
|
+
algorithm: z.enum(['sha256', 'sha512']).default('sha256'),
|
|
76
|
+
secret_env: secretRef,
|
|
77
|
+
// What gets signed: '{body}' or '{timestamp}.{body}'
|
|
78
|
+
signed_content: z.enum(['body', 'timestamp.body']).default('body'),
|
|
79
|
+
timestamp_header: z.string().optional(),
|
|
80
|
+
tolerance_seconds: z.number().int().positive().default(300),
|
|
81
|
+
// Where this processor keeps its stable event ID / event type in the payload.
|
|
82
|
+
event_id: dotPath.optional(),
|
|
83
|
+
event_type: dotPath.optional(),
|
|
84
|
+
});
|
|
85
|
+
// How a webhook event becomes an End Close record. This block is the complete answer to
|
|
86
|
+
// "what leaves our network": only fields named here are forwarded, ever.
|
|
87
|
+
export const recordMapSchema = z
|
|
88
|
+
.object({
|
|
89
|
+
data_stream_key: z.string().min(1),
|
|
90
|
+
external_id: fieldRefSchema,
|
|
91
|
+
// Source of a string amount like "3,762.87"; converted to integer cents.
|
|
92
|
+
amount: fieldRefSchema,
|
|
93
|
+
direction: z.enum(['credit', 'debit']),
|
|
94
|
+
// Optional payload timestamp. Absent -> the record is dated by receive time.
|
|
95
|
+
date: dateRefSchema.optional(),
|
|
96
|
+
description: enrichableFieldRefSchema.optional(),
|
|
97
|
+
currency: z
|
|
98
|
+
.string()
|
|
99
|
+
.regex(/^[A-Za-z]{3}$/)
|
|
100
|
+
.optional(),
|
|
101
|
+
// Extra fields to forward, as output_name: source. Output names are what End Close
|
|
102
|
+
// property definitions see, so choose clean snake_case names.
|
|
103
|
+
metadata: z.record(z.string().regex(/^[a-z][a-z0-9_]*$/), enrichableFieldRefSchema).default({}),
|
|
104
|
+
})
|
|
105
|
+
.superRefine((map, ctx) => {
|
|
106
|
+
for (const [outputKey, ref] of Object.entries(map.metadata)) {
|
|
107
|
+
const sourceLeaf = refSource(ref).split('.').at(-1);
|
|
108
|
+
const hashed = refTransforms(ref).includes('hash');
|
|
109
|
+
const enriched = refEnrichment(ref) !== undefined;
|
|
110
|
+
for (const name of [outputKey, sourceLeaf]) {
|
|
111
|
+
if (!keyNameIsSensitive(name))
|
|
112
|
+
continue;
|
|
113
|
+
// `hash` protects the source value. An enriched field forwards whatever the host
|
|
114
|
+
// returns, so hashing the input does not make a sensitive output name safe.
|
|
115
|
+
if (hashed && !(enriched && name === outputKey))
|
|
116
|
+
continue;
|
|
117
|
+
ctx.addIssue({
|
|
118
|
+
code: z.ZodIssueCode.custom,
|
|
119
|
+
path: ['metadata', outputKey],
|
|
120
|
+
message: enriched && name === outputKey
|
|
121
|
+
? `"${name}" matches the hard denylist (cvv/ssn/account number/...); an enriched field cannot use it as an output name`
|
|
122
|
+
: `"${name}" matches the hard denylist (cvv/ssn/account number/...); it cannot be forwarded in clear — use transform: hash or remove it`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
/** Built-in processor adapters. Hosts may register more via createRelay({ adapters }). */
|
|
128
|
+
export const BUILTIN_SOURCES = ['payabli', 'generic_hmac'];
|
|
129
|
+
export const routeSchema = z.object({
|
|
130
|
+
id: z
|
|
131
|
+
.string()
|
|
132
|
+
.regex(/^[a-z0-9][a-z0-9-_]*$/, 'route id must be a lowercase slug'),
|
|
133
|
+
// The processor adapter name: a built-in, or one registered by the host.
|
|
134
|
+
source: z.string().regex(/^[a-z0-9][a-z0-9_-]*$/, 'source must be a lowercase slug'),
|
|
135
|
+
auth: z.discriminatedUnion('mode', [payabliAuthSchema, genericHmacAuthSchema]),
|
|
136
|
+
// Payload event types accepted by this route (adapter-extracted; glob '*' allowed).
|
|
137
|
+
// Non-matching events persist locally as dropped_by_filter and are never forwarded.
|
|
138
|
+
events: z.array(z.string()).min(1).optional(),
|
|
139
|
+
map: recordMapSchema,
|
|
140
|
+
max_body_bytes: z
|
|
141
|
+
.number()
|
|
142
|
+
.int()
|
|
143
|
+
.positive()
|
|
144
|
+
.max(10 * 1024 * 1024)
|
|
145
|
+
.default(1024 * 1024),
|
|
146
|
+
});
|
|
147
|
+
// The config document is ROUTES ONLY (strict: unknown top-level keys are rejected).
|
|
148
|
+
// Everything else (End Close endpoint, ports, dispatch/retention tuning) is a boot-time
|
|
149
|
+
// runtime setting from the environment — see src/config/runtime.ts. Consequence: every
|
|
150
|
+
// applied config change takes effect live; there is no "restart pending" state.
|
|
151
|
+
export const relayConfigSchema = z
|
|
152
|
+
.object({
|
|
153
|
+
routes: z.array(routeSchema).min(1),
|
|
154
|
+
})
|
|
155
|
+
.strict();
|
|
156
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"at-rest.d.ts","sourceRoot":"","sources":["../../src/crypto/at-rest.ts"],"names":[],"mappings":"AAOA,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAK1F;AAED,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAM3E"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
|
|
2
|
+
// AES-256-GCM with a per-row random IV. Ciphertext layout: <ciphertext><16-byte auth tag>.
|
|
3
|
+
const ALGO = 'aes-256-gcm';
|
|
4
|
+
const IV_BYTES = 12;
|
|
5
|
+
const TAG_BYTES = 16;
|
|
6
|
+
export function encrypt(key, plaintext) {
|
|
7
|
+
const iv = randomBytes(IV_BYTES);
|
|
8
|
+
const cipher = createCipheriv(ALGO, key, iv);
|
|
9
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);
|
|
10
|
+
return { ciphertext, iv };
|
|
11
|
+
}
|
|
12
|
+
export function decrypt(key, ciphertext, iv) {
|
|
13
|
+
const tag = ciphertext.subarray(ciphertext.length - TAG_BYTES);
|
|
14
|
+
const body = ciphertext.subarray(0, ciphertext.length - TAG_BYTES);
|
|
15
|
+
const decipher = createDecipheriv(ALGO, key, iv);
|
|
16
|
+
decipher.setAuthTag(tag);
|
|
17
|
+
return Buffer.concat([decipher.update(body), decipher.final()]);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=at-rest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"at-rest.js","sourceRoot":"","sources":["../../src/crypto/at-rest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAE3E,2FAA2F;AAC3F,MAAM,IAAI,GAAG,aAAa,CAAA;AAC1B,MAAM,QAAQ,GAAG,EAAE,CAAA;AACnB,MAAM,SAAS,GAAG,EAAE,CAAA;AAEpB,MAAM,UAAU,OAAO,CAAC,GAAW,EAAE,SAAiB;IACpD,MAAM,EAAE,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAA;IAChC,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,CAAA;IAC5C,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAA;IACjG,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,CAAA;AAC3B,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,GAAW,EAAE,UAAkB,EAAE,EAAU;IACjE,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IAC9D,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,EAAE,UAAU,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IAClE,MAAM,QAAQ,GAAG,gBAAgB,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,CAAA;IAChD,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAA;IACxB,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;AACjE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/crypto/keys.ts"],"names":[],"mappings":"AAOA,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAK1E"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
// RELAY_DATA_KEY and MASKING_HMAC_KEY are operator-supplied strings (32+ chars of entropy
|
|
3
|
+
// recommended). We derive fixed-length keys by hashing so any string works, but refuse
|
|
4
|
+
// obviously weak values.
|
|
5
|
+
const MIN_KEY_CHARS = 16;
|
|
6
|
+
export function deriveKey(envName, raw) {
|
|
7
|
+
if (!raw || raw.length < MIN_KEY_CHARS) {
|
|
8
|
+
throw new Error(`${envName} must be set to at least ${MIN_KEY_CHARS} characters`);
|
|
9
|
+
}
|
|
10
|
+
return createHash('sha256').update(raw, 'utf8').digest();
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../../src/crypto/keys.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,0FAA0F;AAC1F,uFAAuF;AACvF,yBAAyB;AACzB,MAAM,aAAa,GAAG,EAAE,CAAA;AAExB,MAAM,UAAU,SAAS,CAAC,OAAe,EAAE,GAAuB;IAChE,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,GAAG,aAAa,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,GAAG,OAAO,4BAA4B,aAAa,aAAa,CAAC,CAAA;IACnF,CAAC;IACD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,MAAM,EAAE,CAAA;AAC1D,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface PayloadCodec {
|
|
2
|
+
encode(plain: Buffer): {
|
|
3
|
+
payload: Buffer;
|
|
4
|
+
iv: Buffer | null;
|
|
5
|
+
};
|
|
6
|
+
decode(payload: Buffer, iv: Buffer | null): Buffer;
|
|
7
|
+
}
|
|
8
|
+
/** AES-256-GCM under a 32-byte key (see crypto/keys.ts deriveKey). */
|
|
9
|
+
export declare function aesGcmCodec(key: Buffer): PayloadCodec;
|
|
10
|
+
/** Stores payloads as-is. Only appropriate when the store itself is encrypted or trusted. */
|
|
11
|
+
export declare const plainCodec: PayloadCodec;
|
|
12
|
+
//# sourceMappingURL=codec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../../src/engine/codec.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAA;IAC7D,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAAA;CACnD;AAED,sEAAsE;AACtE,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAWrD;AAED,6FAA6F;AAC7F,eAAO,MAAM,UAAU,EAAE,YAGxB,CAAA"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { encrypt, decrypt } from '../crypto/at-rest.js';
|
|
2
|
+
/** AES-256-GCM under a 32-byte key (see crypto/keys.ts deriveKey). */
|
|
3
|
+
export function aesGcmCodec(key) {
|
|
4
|
+
return {
|
|
5
|
+
encode(plain) {
|
|
6
|
+
const { ciphertext, iv } = encrypt(key, plain);
|
|
7
|
+
return { payload: ciphertext, iv };
|
|
8
|
+
},
|
|
9
|
+
decode(payload, iv) {
|
|
10
|
+
if (!iv)
|
|
11
|
+
throw new Error('encrypted payload has no iv');
|
|
12
|
+
return decrypt(key, payload, iv);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Stores payloads as-is. Only appropriate when the store itself is encrypted or trusted. */
|
|
17
|
+
export const plainCodec = {
|
|
18
|
+
encode: (plain) => ({ payload: plain, iv: null }),
|
|
19
|
+
decode: (payload) => payload,
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=codec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codec.js","sourceRoot":"","sources":["../../src/engine/codec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAWvD,sEAAsE;AACtE,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,OAAO;QACL,MAAM,CAAC,KAAK;YACV,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;YAC9C,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE,CAAA;QACpC,CAAC;QACD,MAAM,CAAC,OAAO,EAAE,EAAE;YAChB,IAAI,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAA;YACvD,OAAO,OAAO,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC,CAAA;QAClC,CAAC;KACF,CAAA;AACH,CAAC;AAED,6FAA6F;AAC7F,MAAM,CAAC,MAAM,UAAU,GAAiB;IACtC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACjD,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO;CAC7B,CAAA"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export type IngestOutcome = 'accepted' | 'duplicate' | 'filtered' | 'rejected_auth' | 'rejected_size' | 'rejected_json' | 'panic';
|
|
2
|
+
export type ForwardResult = 'delivered' | 'retried' | 'parked';
|
|
3
|
+
/**
|
|
4
|
+
* applied: the field was filled. omitted: the enrichment returned undefined. failed: it
|
|
5
|
+
* threw or timed out (the event retries). rejected: its result or the reference was
|
|
6
|
+
* invalid (the event parks).
|
|
7
|
+
*/
|
|
8
|
+
export type EnrichOutcome = 'applied' | 'omitted' | 'failed' | 'rejected';
|
|
9
|
+
export type EngineErrorKind = 'dispatch_cycle' | 'ingest_persist' | 'prune' | 'recover_delivering';
|
|
10
|
+
export interface RelayEvents {
|
|
11
|
+
/** Every ingest request that reached a known route, by outcome. */
|
|
12
|
+
ingest: {
|
|
13
|
+
routeId: string;
|
|
14
|
+
outcome: IngestOutcome;
|
|
15
|
+
eventType: string | null;
|
|
16
|
+
bodyBytes: number;
|
|
17
|
+
};
|
|
18
|
+
/** A new event row was persisted (not emitted for duplicates). */
|
|
19
|
+
stored: {
|
|
20
|
+
routeId: string;
|
|
21
|
+
id: string;
|
|
22
|
+
filtered: boolean;
|
|
23
|
+
};
|
|
24
|
+
/** Per-event forwarding results. */
|
|
25
|
+
forward: {
|
|
26
|
+
routeId: string;
|
|
27
|
+
result: ForwardResult;
|
|
28
|
+
count: number;
|
|
29
|
+
};
|
|
30
|
+
/** Per-event outcome of a dispatch attempt, keyed by the store id `ingest` returned. */
|
|
31
|
+
settled: {
|
|
32
|
+
id: string;
|
|
33
|
+
routeId: string;
|
|
34
|
+
result: ForwardResult;
|
|
35
|
+
error?: string;
|
|
36
|
+
};
|
|
37
|
+
/** One event confirmed delivered; carries the timestamps for lag measurement. */
|
|
38
|
+
delivered: {
|
|
39
|
+
routeId: string;
|
|
40
|
+
receivedAt: string;
|
|
41
|
+
deliveredAt: string;
|
|
42
|
+
};
|
|
43
|
+
/** One host enrichment call for one field of one event. Names only, never values. */
|
|
44
|
+
enrich: {
|
|
45
|
+
routeId: string;
|
|
46
|
+
id: string;
|
|
47
|
+
field: string;
|
|
48
|
+
enrichment: string;
|
|
49
|
+
result: EnrichOutcome;
|
|
50
|
+
error?: string;
|
|
51
|
+
};
|
|
52
|
+
'batch.forwarded': {
|
|
53
|
+
routeId: string;
|
|
54
|
+
events: number;
|
|
55
|
+
bulkRequestId: string;
|
|
56
|
+
};
|
|
57
|
+
/** A whole batch was permanently rejected by End Close. */
|
|
58
|
+
'batch.parked': {
|
|
59
|
+
routeId: string;
|
|
60
|
+
status: number;
|
|
61
|
+
events: number;
|
|
62
|
+
};
|
|
63
|
+
prune: {
|
|
64
|
+
wiped: number;
|
|
65
|
+
deleted: number;
|
|
66
|
+
};
|
|
67
|
+
/** An operational failure inside the engine (already logged). */
|
|
68
|
+
error: {
|
|
69
|
+
kind: EngineErrorKind;
|
|
70
|
+
error: unknown;
|
|
71
|
+
op?: string;
|
|
72
|
+
routeId?: string;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export type RelayEventName = keyof RelayEvents;
|
|
76
|
+
export type RelayHandler<E extends RelayEventName> = (payload: RelayEvents[E]) => void;
|
|
77
|
+
export declare class RelayHooks {
|
|
78
|
+
private handlers;
|
|
79
|
+
/** Subscribe; returns an unsubscribe function. */
|
|
80
|
+
on<E extends RelayEventName>(name: E, handler: RelayHandler<E>): () => void;
|
|
81
|
+
/** Handlers must not throw; a throwing handler is isolated so it cannot break the engine. */
|
|
82
|
+
emit<E extends RelayEventName>(name: E, payload: RelayEvents[E]): void;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/engine/hooks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Typed, metadata-only events the engine emits. Hosts subscribe to drive metrics,
|
|
2
|
+
// telemetry, or their own observability; the engine itself depends on none of them.
|
|
3
|
+
// Payloads carry route ids, counts, timestamps, field names and error kinds — never
|
|
4
|
+
// webhook data or enriched values.
|
|
5
|
+
export class RelayHooks {
|
|
6
|
+
// Internally untyped; the public on/emit signatures enforce payload types.
|
|
7
|
+
handlers = new Map();
|
|
8
|
+
/** Subscribe; returns an unsubscribe function. */
|
|
9
|
+
on(name, handler) {
|
|
10
|
+
let set = this.handlers.get(name);
|
|
11
|
+
if (!set) {
|
|
12
|
+
set = new Set();
|
|
13
|
+
this.handlers.set(name, set);
|
|
14
|
+
}
|
|
15
|
+
const h = handler;
|
|
16
|
+
set.add(h);
|
|
17
|
+
return () => set.delete(h);
|
|
18
|
+
}
|
|
19
|
+
/** Handlers must not throw; a throwing handler is isolated so it cannot break the engine. */
|
|
20
|
+
emit(name, payload) {
|
|
21
|
+
const set = this.handlers.get(name);
|
|
22
|
+
if (!set)
|
|
23
|
+
return;
|
|
24
|
+
for (const h of set) {
|
|
25
|
+
try {
|
|
26
|
+
h(payload);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// observers never affect ingest or dispatch
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=hooks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../src/engine/hooks.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,oFAAoF;AACpF,oFAAoF;AACpF,mCAAmC;AA4CnC,MAAM,OAAO,UAAU;IACrB,2EAA2E;IACnE,QAAQ,GAAG,IAAI,GAAG,EAAmD,CAAA;IAE7E,kDAAkD;IAClD,EAAE,CAA2B,IAAO,EAAE,OAAwB;QAC5D,IAAI,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACjC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,GAAG,GAAG,IAAI,GAAG,EAAE,CAAA;YACf,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;QAC9B,CAAC;QACD,MAAM,CAAC,GAAG,OAAqC,CAAA;QAC/C,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QACV,OAAO,GAAG,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;IAC5B,CAAC;IAED,6FAA6F;IAC7F,IAAI,CAA2B,IAAO,EAAE,OAAuB;QAC7D,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACnC,IAAI,CAAC,GAAG;YAAE,OAAM;QAChB,KAAK,MAAM,CAAC,IAAI,GAAG,EAAE,CAAC;YACpB,IAAI,CAAC;gBACH,CAAC,CAAC,OAAO,CAAC,CAAA;YACZ,CAAC;YAAC,MAAM,CAAC;gBACP,4CAA4C;YAC9C,CAAC;QACH,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { EventEmitter } from 'node:events';
|
|
2
|
+
import { type ControlStore, type EventStore, type RouteProvider } from './store.js';
|
|
3
|
+
import type { PayloadCodec } from './codec.js';
|
|
4
|
+
import type { ProcessorAdapter, RawRequest } from '../ingest/adapters/types.js';
|
|
5
|
+
import { RelayHooks, type IngestOutcome } from './hooks.js';
|
|
6
|
+
import { type Logger } from '../logger.js';
|
|
7
|
+
import type { SecretResolver } from './secrets.js';
|
|
8
|
+
export interface IngestDeps {
|
|
9
|
+
store: EventStore;
|
|
10
|
+
control: ControlStore;
|
|
11
|
+
routes: RouteProvider;
|
|
12
|
+
secrets: SecretResolver;
|
|
13
|
+
/** How buffered payloads are stored (encrypted or plain). */
|
|
14
|
+
codec: PayloadCodec;
|
|
15
|
+
/** Emits 'event' whenever a new deliverable event lands, so the dispatcher wakes immediately. */
|
|
16
|
+
signal?: EventEmitter;
|
|
17
|
+
hooks?: RelayHooks;
|
|
18
|
+
logger?: Logger;
|
|
19
|
+
/** Additional processor adapters keyed by route `source` (override built-ins by name). */
|
|
20
|
+
adapters?: Record<string, ProcessorAdapter>;
|
|
21
|
+
}
|
|
22
|
+
export type IngestResultOutcome = IngestOutcome | 'unknown_route' | 'secret_unavailable' | 'unknown_source' | 'persist_failed' | 'unavailable';
|
|
23
|
+
/** HTTP-shaped so any host can answer the processor without knowing the engine's rules. */
|
|
24
|
+
export interface IngestResult {
|
|
25
|
+
status: 200 | 400 | 401 | 404 | 413 | 500 | 503;
|
|
26
|
+
body: {
|
|
27
|
+
status: string;
|
|
28
|
+
} | {
|
|
29
|
+
error: string;
|
|
30
|
+
};
|
|
31
|
+
outcome: IngestResultOutcome;
|
|
32
|
+
/** Store id of the buffered event; present for `accepted` and `filtered`. Correlate with the `settled` hook. */
|
|
33
|
+
id?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function eventTypeMatches(patterns: string[], eventType: string | null): boolean;
|
|
36
|
+
/** Idempotency key for one inbound event: stable across redeliveries of the same processor event. */
|
|
37
|
+
export declare function eventIdempotencyKey(source: string, eventId: string): string;
|
|
38
|
+
export declare function ingestWebhook(deps: IngestDeps, routeId: string, raw: RawRequest): Promise<IngestResult>;
|
|
39
|
+
//# sourceMappingURL=ingest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ingest.d.ts","sourceRoot":"","sources":["../../src/engine/ingest.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAqC,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAA;AACtH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAA;AAE/E,OAAO,EAAE,UAAU,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAA;AAC3D,OAAO,EAAc,KAAK,MAAM,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAWlD,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,UAAU,CAAA;IACjB,OAAO,EAAE,YAAY,CAAA;IACrB,MAAM,EAAE,aAAa,CAAA;IACrB,OAAO,EAAE,cAAc,CAAA;IACvB,6DAA6D;IAC7D,KAAK,EAAE,YAAY,CAAA;IACnB,iGAAiG;IACjG,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;CAC5C;AAED,MAAM,MAAM,mBAAmB,GAC3B,aAAa,GACb,eAAe,GACf,oBAAoB,GACpB,gBAAgB,GAChB,gBAAgB,GAChB,aAAa,CAAA;AAEjB,2FAA2F;AAC3F,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;IAC/C,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;IAC5C,OAAO,EAAE,mBAAmB,CAAA;IAC5B,gHAAgH;IAChH,EAAE,CAAC,EAAE,MAAM,CAAA;CACZ;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAMtF;AAED,qGAAqG;AACrG,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE3E;AAED,wBAAsB,aAAa,CACjC,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,UAAU,GACd,OAAO,CAAC,YAAY,CAAC,CAgBvB"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { StoreError, StoreUnavailableError } from './store.js';
|
|
3
|
+
import { adapterFor, hasAdapter } from '../ingest/adapters/registry.js';
|
|
4
|
+
import { RelayHooks } from './hooks.js';
|
|
5
|
+
import { noopLogger } from '../logger.js';
|
|
6
|
+
import { jsonTopLevelKeys, requestHeaderNames } from '../util/payload-shape.js';
|
|
7
|
+
import { escapeRe } from '../util/strings.js';
|
|
8
|
+
// The framework-agnostic ingest path: verify → encrypt → persist → ack. A host adapts its
|
|
9
|
+
// HTTP request into a RawRequest and maps the IngestResult back onto its response.
|
|
10
|
+
// Headers persisted alongside the payload for debugging/replay. Auth headers are
|
|
11
|
+
// deliberately excluded — secrets never reach the database.
|
|
12
|
+
const PERSISTED_HEADERS = ['content-type', 'user-agent'];
|
|
13
|
+
export function eventTypeMatches(patterns, eventType) {
|
|
14
|
+
if (patterns.length === 0)
|
|
15
|
+
return true;
|
|
16
|
+
if (eventType === null)
|
|
17
|
+
return false;
|
|
18
|
+
return patterns.some((p) => p.includes('*') ? new RegExp(`^${p.split('*').map(escapeRe).join('.*')}$`).test(eventType) : p === eventType);
|
|
19
|
+
}
|
|
20
|
+
/** Idempotency key for one inbound event: stable across redeliveries of the same processor event. */
|
|
21
|
+
export function eventIdempotencyKey(source, eventId) {
|
|
22
|
+
return 'sha256:' + createHash('sha256').update(`${source}:${eventId}`).digest('hex');
|
|
23
|
+
}
|
|
24
|
+
export async function ingestWebhook(deps, routeId, raw) {
|
|
25
|
+
const hooks = deps.hooks ?? new RelayHooks();
|
|
26
|
+
const logger = deps.logger ?? noopLogger;
|
|
27
|
+
try {
|
|
28
|
+
return await ingestInner(deps, routeId, raw, hooks, logger);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
// Any store failure (route lookup, killswitch, insert): 503 when the store says it is
|
|
32
|
+
// temporarily unavailable so the processor retries, 500 otherwise. Never the raw error.
|
|
33
|
+
if (!(err instanceof StoreError))
|
|
34
|
+
throw err;
|
|
35
|
+
logger.error('ingest store failure', { route: routeId, op: err.op, error: err.message });
|
|
36
|
+
hooks.emit('error', { kind: 'ingest_persist', error: err, routeId, op: err.op });
|
|
37
|
+
if (err instanceof StoreUnavailableError) {
|
|
38
|
+
return { status: 503, body: { error: 'temporarily unavailable' }, outcome: 'unavailable' };
|
|
39
|
+
}
|
|
40
|
+
return { status: 500, body: { error: 'internal error' }, outcome: 'persist_failed' };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function ingestInner(deps, routeId, raw, hooks, logger) {
|
|
44
|
+
const { store, control, routes, secrets, codec, signal } = deps;
|
|
45
|
+
const route = await routes.get(routeId);
|
|
46
|
+
if (!route)
|
|
47
|
+
return { status: 404, body: { error: 'unknown route' }, outcome: 'unknown_route' };
|
|
48
|
+
const { rawBody } = raw;
|
|
49
|
+
const bodyBytes = Buffer.isBuffer(rawBody) ? rawBody.length : 0;
|
|
50
|
+
const ingested = (outcome, eventType = null) => hooks.emit('ingest', { routeId, outcome, eventType, bodyBytes });
|
|
51
|
+
// Panic refuses at the door; the processor's own retries carry the window.
|
|
52
|
+
if ((await control.getKillswitch()) === 'panic') {
|
|
53
|
+
ingested('panic');
|
|
54
|
+
return { status: 503, body: { error: 'relay is in panic mode' }, outcome: 'panic' };
|
|
55
|
+
}
|
|
56
|
+
if (!Buffer.isBuffer(rawBody) || rawBody.length === 0) {
|
|
57
|
+
ingested('rejected_json');
|
|
58
|
+
return { status: 400, body: { error: 'empty body' }, outcome: 'rejected_json' };
|
|
59
|
+
}
|
|
60
|
+
if (rawBody.length > route.max_body_bytes) {
|
|
61
|
+
ingested('rejected_size');
|
|
62
|
+
return { status: 413, body: { error: 'body too large' }, outcome: 'rejected_size' };
|
|
63
|
+
}
|
|
64
|
+
if (!hasAdapter(route.source, deps.adapters)) {
|
|
65
|
+
logger.error('ingest route has no adapter', { route: routeId, source: route.source });
|
|
66
|
+
return { status: 500, body: { error: 'internal error' }, outcome: 'unknown_source' };
|
|
67
|
+
}
|
|
68
|
+
const adapter = adapterFor(route.source, deps.adapters);
|
|
69
|
+
const secret = secrets.resolve(route.auth.secret_env);
|
|
70
|
+
if (secret === undefined) {
|
|
71
|
+
logger.error('ingest secret unavailable', { route: routeId, secret_env: route.auth.secret_env });
|
|
72
|
+
return { status: 500, body: { error: 'internal error' }, outcome: 'secret_unavailable' };
|
|
73
|
+
}
|
|
74
|
+
const verdict = adapter.verify(raw, route, { secret });
|
|
75
|
+
if (!verdict.ok) {
|
|
76
|
+
ingested('rejected_auth');
|
|
77
|
+
logger.warn('ingest rejected', { route: routeId, reason: verdict.reason });
|
|
78
|
+
return { status: 401, body: { error: 'verification failed' }, outcome: 'rejected_auth' };
|
|
79
|
+
}
|
|
80
|
+
let body;
|
|
81
|
+
try {
|
|
82
|
+
body = JSON.parse(rawBody.toString('utf8'));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
ingested('rejected_json');
|
|
86
|
+
return { status: 400, body: { error: 'invalid JSON' }, outcome: 'rejected_json' };
|
|
87
|
+
}
|
|
88
|
+
const eventId = adapter.extractEventId(body, raw, route);
|
|
89
|
+
const eventType = adapter.extractEventType(body, raw, route);
|
|
90
|
+
const filtered = Boolean(route.events && !eventTypeMatches(route.events, eventType));
|
|
91
|
+
// Shape-only debug metadata (no values).
|
|
92
|
+
logger.debug('ingest shape', {
|
|
93
|
+
route: routeId,
|
|
94
|
+
event_type: eventType,
|
|
95
|
+
body_bytes: rawBody.length,
|
|
96
|
+
remote_ip: raw.remoteIp,
|
|
97
|
+
header_names: requestHeaderNames(raw.headers),
|
|
98
|
+
payload_keys: jsonTopLevelKeys(body),
|
|
99
|
+
});
|
|
100
|
+
const { payload, iv } = codec.encode(rawBody);
|
|
101
|
+
const headersJson = JSON.stringify(Object.fromEntries(PERSISTED_HEADERS.map((h) => [h, raw.headers[h]]).filter(([, v]) => v !== undefined)));
|
|
102
|
+
const inserted = await store.insert({
|
|
103
|
+
route_id: routeId,
|
|
104
|
+
source: route.source,
|
|
105
|
+
event_id: eventId,
|
|
106
|
+
event_type: eventType,
|
|
107
|
+
payload,
|
|
108
|
+
payload_iv: iv,
|
|
109
|
+
headers_json: headersJson,
|
|
110
|
+
received_at: new Date().toISOString(),
|
|
111
|
+
status: filtered ? 'dropped_by_filter' : 'pending',
|
|
112
|
+
idempotency_key: eventIdempotencyKey(route.source, eventId),
|
|
113
|
+
});
|
|
114
|
+
if (inserted.duplicate) {
|
|
115
|
+
ingested('duplicate', eventType);
|
|
116
|
+
logger.info('duplicate event acked', { route: routeId, event_type: eventType });
|
|
117
|
+
return { status: 200, body: { status: 'duplicate' }, outcome: 'duplicate' };
|
|
118
|
+
}
|
|
119
|
+
ingested(filtered ? 'filtered' : 'accepted', eventType);
|
|
120
|
+
hooks.emit('stored', { routeId, id: inserted.id, filtered });
|
|
121
|
+
logger.info('event ingested', { route: routeId, event_type: eventType, filtered });
|
|
122
|
+
if (!filtered)
|
|
123
|
+
signal?.emit('event', routeId);
|
|
124
|
+
return {
|
|
125
|
+
status: 200,
|
|
126
|
+
body: { status: filtered ? 'filtered' : 'accepted' },
|
|
127
|
+
outcome: filtered ? 'filtered' : 'accepted',
|
|
128
|
+
id: inserted.id,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=ingest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ingest.js","sourceRoot":"","sources":["../../src/engine/ingest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,OAAO,EAAE,UAAU,EAAE,qBAAqB,EAA0D,MAAM,YAAY,CAAA;AAEtH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAA;AAGvE,OAAO,EAAE,UAAU,EAAsB,MAAM,YAAY,CAAA;AAC3D,OAAO,EAAE,UAAU,EAAe,MAAM,cAAc,CAAA;AAEtD,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAC/E,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAE7C,0FAA0F;AAC1F,mFAAmF;AAEnF,iFAAiF;AACjF,4DAA4D;AAC5D,MAAM,iBAAiB,GAAG,CAAC,cAAc,EAAE,YAAY,CAAC,CAAA;AAkCxD,MAAM,UAAU,gBAAgB,CAAC,QAAkB,EAAE,SAAwB;IAC3E,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACtC,IAAI,SAAS,KAAK,IAAI;QAAE,OAAO,KAAK,CAAA;IACpC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CACzB,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,CAC7G,CAAA;AACH,CAAC;AAED,qGAAqG;AACrG,MAAM,UAAU,mBAAmB,CAAC,MAAc,EAAE,OAAe;IACjE,OAAO,SAAS,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,MAAM,IAAI,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AACtF,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,IAAgB,EAChB,OAAe,EACf,GAAe;IAEf,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,UAAU,EAAE,CAAA;IAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,UAAU,CAAA;IACxC,IAAI,CAAC;QACH,OAAO,MAAM,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;IAC7D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,sFAAsF;QACtF,wFAAwF;QACxF,IAAI,CAAC,CAAC,GAAG,YAAY,UAAU,CAAC;YAAE,MAAM,GAAG,CAAA;QAC3C,MAAM,CAAC,KAAK,CAAC,sBAAsB,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAA;QACxF,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAA;QAChF,IAAI,GAAG,YAAY,qBAAqB,EAAE,CAAC;YACzC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,yBAAyB,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAA;QAC5F,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAA;IACtF,CAAC;AACH,CAAC;AAED,KAAK,UAAU,WAAW,CACxB,IAAgB,EAChB,OAAe,EACf,GAAe,EACf,KAAiB,EACjB,MAAc;IAEd,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;IAE/D,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;IACvC,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,EAAE,OAAO,EAAE,eAAe,EAAE,CAAA;IAE9F,MAAM,EAAE,OAAO,EAAE,GAAG,GAAG,CAAA;IACvB,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;IAC/D,MAAM,QAAQ,GAAG,CAAC,OAAsB,EAAE,YAA2B,IAAI,EAAE,EAAE,CAC3E,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC,CAAA;IAElE,2EAA2E;IAC3E,IAAI,CAAC,MAAM,OAAO,CAAC,aAAa,EAAE,CAAC,KAAK,OAAO,EAAE,CAAC;QAChD,QAAQ,CAAC,OAAO,CAAC,CAAA;QACjB,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,wBAAwB,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAA;IACrF,CAAC;IAED,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtD,QAAQ,CAAC,eAAe,CAAC,CAAA;QACzB,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,eAAe,EAAE,CAAA;IACjF,CAAC;IACD,IAAI,OAAO,CAAC,MAAM,GAAG,KAAK,CAAC,cAAc,EAAE,CAAC;QAC1C,QAAQ,CAAC,eAAe,CAAC,CAAA;QACzB,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,OAAO,EAAE,eAAe,EAAE,CAAA;IACrF,CAAC;IAED,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7C,MAAM,CAAC,KAAK,CAAC,6BAA6B,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAA;QACrF,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAA;IACtF,CAAC;IACD,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;IACvD,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IACrD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,CAAC,KAAK,CAAC,2BAA2B,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,CAAA;QAChG,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,OAAO,EAAE,oBAAoB,EAAE,CAAA;IAC1F,CAAC;IACD,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;IACtD,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;QAChB,QAAQ,CAAC,eAAe,CAAC,CAAA;QACzB,MAAM,CAAC,IAAI,CAAC,iBAAiB,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;QAC1E,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,qBAAqB,EAAE,EAAE,OAAO,EAAE,eAAe,EAAE,CAAA;IAC1F,CAAC;IAED,IAAI,IAAU,CAAA;IACd,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAS,CAAA;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,QAAQ,CAAC,eAAe,CAAC,CAAA;QACzB,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,OAAO,EAAE,eAAe,EAAE,CAAA;IACnF,CAAC;IAED,MAAM,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAA;IACxD,MAAM,SAAS,GAAG,OAAO,CAAC,gBAAgB,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAA;IAC5D,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAA;IACpF,yCAAyC;IACzC,MAAM,CAAC,KAAK,CAAC,cAAc,EAAE;QAC3B,KAAK,EAAE,OAAO;QACd,UAAU,EAAE,SAAS;QACrB,UAAU,EAAE,OAAO,CAAC,MAAM;QAC1B,SAAS,EAAE,GAAG,CAAC,QAAQ;QACvB,YAAY,EAAE,kBAAkB,CAAC,GAAG,CAAC,OAAkC,CAAC;QACxE,YAAY,EAAE,gBAAgB,CAAC,IAAI,CAAC;KACrC,CAAC,CAAA;IAEF,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAC7C,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,CAChC,MAAM,CAAC,WAAW,CAChB,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CACrF,CACF,CAAA;IAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC;QAClC,QAAQ,EAAE,OAAO;QACjB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,QAAQ,EAAE,OAAO;QACjB,UAAU,EAAE,SAAS;QACrB,OAAO;QACP,UAAU,EAAE,EAAE;QACd,YAAY,EAAE,WAAW;QACzB,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACrC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,SAAS;QAClD,eAAe,EAAE,mBAAmB,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC;KAC5D,CAAC,CAAA;IAEF,IAAI,QAAQ,CAAC,SAAS,EAAE,CAAC;QACvB,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,CAAA;QAChC,MAAM,CAAC,IAAI,CAAC,uBAAuB,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,CAAA;QAC/E,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE,CAAA;IAC7E,CAAC;IAED,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,EAAE,SAAS,CAAC,CAAA;IACvD,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAA;IAC5D,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAA;IAClF,IAAI,CAAC,QAAQ;QAAE,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;IAC7C,OAAO;QACL,MAAM,EAAE,GAAG;QACX,IAAI,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,EAAE;QACpD,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU;QAC3C,EAAE,EAAE,QAAQ,CAAC,EAAE;KAChB,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { EventRecord, EventStatus, EventStore, EventStoreAdmin, EventSummary, InsertResult, Lease, NewEvent, RouteStats } from './store.js';
|
|
2
|
+
export declare class MemoryEventStore implements EventStore, EventStoreAdmin {
|
|
3
|
+
private rows;
|
|
4
|
+
private byKey;
|
|
5
|
+
private nextId;
|
|
6
|
+
insert(e: NewEvent): Promise<InsertResult>;
|
|
7
|
+
routesWithDueEvents(now: string): Promise<string[]>;
|
|
8
|
+
claimDue(routeId: string, now: string, limit: number, lease: Lease): Promise<EventRecord[]>;
|
|
9
|
+
markDelivered(ids: string[], deliveredAt: string, bulkRequestId: string | null): Promise<void>;
|
|
10
|
+
markFailed(ids: string[], nextAttemptAt: string, error: string): Promise<void>;
|
|
11
|
+
markParked(ids: string[], error: string): Promise<void>;
|
|
12
|
+
releaseDelivering(ids: string[], nextAttemptAt: string, error: string): Promise<number>;
|
|
13
|
+
recoverDelivering(now: string, owner?: string): Promise<number>;
|
|
14
|
+
parkExpired(now: string, maxAgeMs: number): Promise<number>;
|
|
15
|
+
pruneBatch(now: string, deliveredDays: number, ledgerDays: number, limit: number): Promise<{
|
|
16
|
+
wiped: number;
|
|
17
|
+
deleted: number;
|
|
18
|
+
}>;
|
|
19
|
+
getById(id: string): Promise<EventRecord | undefined>;
|
|
20
|
+
list(filter: {
|
|
21
|
+
status?: EventStatus;
|
|
22
|
+
route?: string;
|
|
23
|
+
limit?: number;
|
|
24
|
+
}): Promise<EventSummary[]>;
|
|
25
|
+
countByStatus(): Promise<Record<string, number>>;
|
|
26
|
+
perRouteStats(): Promise<RouteStats[]>;
|
|
27
|
+
replay(id: string): Promise<boolean>;
|
|
28
|
+
replayAllParked(): Promise<number>;
|
|
29
|
+
private requeue;
|
|
30
|
+
private isDue;
|
|
31
|
+
private pick;
|
|
32
|
+
private clearLease;
|
|
33
|
+
}
|
|
34
|
+
export declare function memoryStore(): MemoryEventStore;
|
|
35
|
+
//# sourceMappingURL=memory-store.d.ts.map
|