@sensigo/realm-cli 0.6.4 → 0.7.1

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,122 @@
1
+ // Dedup stores — at-least-once duplicate suppression for webhook events.
2
+ // FileDedupStore survives process restarts; InMemoryDedupStore does not.
3
+ import { createHash } from 'node:crypto';
4
+ import { mkdirSync, openSync, closeSync, statSync, readdirSync, rmSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ const ONE_HOUR_MS = 3_600_000;
7
+ /**
8
+ * File-backed dedup store. Records are empty files whose mtime is the only data.
9
+ * Layout: <baseDir>/<workflowId>/<YYYYMMDDHH>/<sha256(key)[:32]>.
10
+ *
11
+ * Safe across concurrent `realm listen` processes via O_EXCL (openSync 'wx').
12
+ *
13
+ * Note: `check` requires `ttlMs` in practice — it scans floor(ttlMs/3_600_000)+2
14
+ * hour buckets. It is declared with a default of 0 only to satisfy the DedupStore
15
+ * interface (which types ttlMs as optional); real callers always pass the TTL.
16
+ */
17
+ export class FileDedupStore {
18
+ dir;
19
+ lastCleanupErrorAt = 0;
20
+ constructor(baseDir, workflowId) {
21
+ this.dir = join(baseDir, workflowId);
22
+ }
23
+ hashKey(key) {
24
+ return createHash('sha256').update(key).digest('hex').slice(0, 32);
25
+ }
26
+ bucketName(ms) {
27
+ const d = new Date(ms);
28
+ const year = String(d.getUTCFullYear()).padStart(4, '0');
29
+ const month = String(d.getUTCMonth() + 1).padStart(2, '0');
30
+ const day = String(d.getUTCDate()).padStart(2, '0');
31
+ const hour = String(d.getUTCHours()).padStart(2, '0');
32
+ return `${year}${month}${day}${hour}`;
33
+ }
34
+ check(key, ttlMs = 0) {
35
+ const hash = this.hashKey(key);
36
+ const now = Date.now();
37
+ const buckets = Math.floor(ttlMs / ONE_HOUR_MS) + 2;
38
+ for (let i = 0; i < buckets; i++) {
39
+ const bucket = this.bucketName(now - i * ONE_HOUR_MS);
40
+ const filePath = join(this.dir, bucket, hash);
41
+ const stat = statSync(filePath, { throwIfNoEntry: false });
42
+ if (stat !== undefined && now - stat.mtime.getTime() < ttlMs) {
43
+ return true;
44
+ }
45
+ }
46
+ return false;
47
+ }
48
+ record(key, _ttlMs) {
49
+ const hash = this.hashKey(key);
50
+ const bucketDir = join(this.dir, this.bucketName(Date.now()));
51
+ const filePath = join(bucketDir, hash);
52
+ mkdirSync(bucketDir, { recursive: true });
53
+ try {
54
+ const fd = openSync(filePath, 'wx');
55
+ closeSync(fd);
56
+ }
57
+ catch (err) {
58
+ // Concurrent at-least-once delivery may race to create the same file.
59
+ if (err.code === 'EEXIST') {
60
+ return;
61
+ }
62
+ throw err;
63
+ }
64
+ }
65
+ cleanup(maxAgeMs) {
66
+ try {
67
+ const now = Date.now();
68
+ let entries;
69
+ try {
70
+ entries = readdirSync(this.dir);
71
+ }
72
+ catch (err) {
73
+ if (err.code === 'ENOENT') {
74
+ return;
75
+ }
76
+ throw err;
77
+ }
78
+ for (const name of entries) {
79
+ if (!/^\d{10}$/.test(name)) {
80
+ continue;
81
+ }
82
+ const year = parseInt(name.slice(0, 4), 10);
83
+ const month = parseInt(name.slice(4, 6), 10) - 1;
84
+ const day = parseInt(name.slice(6, 8), 10);
85
+ const hour = parseInt(name.slice(8, 10), 10);
86
+ const bucketEndMs = new Date(Date.UTC(year, month, day, hour + 1)).getTime();
87
+ if (bucketEndMs < now - maxAgeMs) {
88
+ rmSync(join(this.dir, name), { recursive: true, force: true });
89
+ }
90
+ }
91
+ }
92
+ catch (err) {
93
+ const now = Date.now();
94
+ if (now - this.lastCleanupErrorAt >= 60_000) {
95
+ this.lastCleanupErrorAt = now;
96
+ console.error(`realm dedup cleanup error in ${this.dir}:`, err);
97
+ }
98
+ }
99
+ }
100
+ }
101
+ export class InMemoryDedupStore {
102
+ map = new Map(); // key → expiry timestamp
103
+ check(key) {
104
+ this.evictExpired();
105
+ const expiry = this.map.get(key);
106
+ return expiry !== undefined && Date.now() < expiry;
107
+ }
108
+ record(key, ttlMs) {
109
+ this.map.set(key, Date.now() + ttlMs);
110
+ }
111
+ cleanup(_maxAgeMs) {
112
+ this.evictExpired();
113
+ }
114
+ evictExpired() {
115
+ const now = Date.now();
116
+ for (const [k, expiry] of this.map) {
117
+ if (now >= expiry)
118
+ this.map.delete(k);
119
+ }
120
+ }
121
+ }
122
+ //# sourceMappingURL=dedup-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dedup-store.js","sourceRoot":"","sources":["../../src/lib/dedup-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ import type { DedupConfig } from '@sensigo/realm';
2
+ export interface WebhookPayload {
3
+ headers: Record<string, string>;
4
+ body: unknown;
5
+ }
6
+ /**
7
+ * Resolves a dot-notation path into an object.
8
+ * Numeric segments (all-digit strings) index into arrays (zero-based).
9
+ * Returns undefined on any resolution failure. Never throws.
10
+ *
11
+ * Rules:
12
+ * - Empty path returns the root object.
13
+ * - On an array, an all-digit in-bounds segment descends by index; any other
14
+ * segment (non-numeric or out-of-bounds index) resolves to undefined.
15
+ * - On a plain object, the segment is always a property key — even all-digit
16
+ * segments are string keys, not array indices.
17
+ * - On null/undefined/primitive, resolution stops with undefined.
18
+ */
19
+ export declare function resolveDotPath(obj: unknown, path: string): unknown;
20
+ /**
21
+ * Extracts run params from a webhook payload using params_map.
22
+ * Logs a warn via the injected logger for each undefined result.
23
+ * Returns a partial record — missing keys are absent, not null.
24
+ */
25
+ export declare function extractParams(payload: WebhookPayload, paramsMap: Record<string, string>, logger: {
26
+ warn: (msg: string, data?: unknown) => void;
27
+ }): Record<string, unknown>;
28
+ /**
29
+ * Extracts the dedup ID from a webhook payload.
30
+ * Returns the value coerced to string, or undefined if absent/null.
31
+ * Numeric values are valid dedup IDs (stringified).
32
+ */
33
+ export declare function extractDedupId(payload: WebhookPayload, dedupConfig: DedupConfig): string | undefined;
34
+ //# sourceMappingURL=webhook-params.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-params.d.ts","sourceRoot":"","sources":["../../src/lib/webhook-params.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,OAAO,CAAC;CACf;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAgClE;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACjC,MAAM,EAAE;IAAE,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE,GACtD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAczB;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,cAAc,EACvB,WAAW,EAAE,WAAW,GACvB,MAAM,GAAG,SAAS,CAOpB"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Resolves a dot-notation path into an object.
3
+ * Numeric segments (all-digit strings) index into arrays (zero-based).
4
+ * Returns undefined on any resolution failure. Never throws.
5
+ *
6
+ * Rules:
7
+ * - Empty path returns the root object.
8
+ * - On an array, an all-digit in-bounds segment descends by index; any other
9
+ * segment (non-numeric or out-of-bounds index) resolves to undefined.
10
+ * - On a plain object, the segment is always a property key — even all-digit
11
+ * segments are string keys, not array indices.
12
+ * - On null/undefined/primitive, resolution stops with undefined.
13
+ */
14
+ export function resolveDotPath(obj, path) {
15
+ if (path === '') {
16
+ return obj;
17
+ }
18
+ let current = obj;
19
+ for (const segment of path.split('.')) {
20
+ if (current === null || current === undefined) {
21
+ return undefined;
22
+ }
23
+ if (Array.isArray(current)) {
24
+ if (/^\d+$/.test(segment)) {
25
+ const index = Number(segment);
26
+ if (index >= 0 && index < current.length) {
27
+ current = current[index];
28
+ continue;
29
+ }
30
+ }
31
+ return undefined;
32
+ }
33
+ if (typeof current === 'object') {
34
+ current = current[segment];
35
+ continue;
36
+ }
37
+ // primitive — cannot descend further
38
+ return undefined;
39
+ }
40
+ return current;
41
+ }
42
+ /**
43
+ * Extracts run params from a webhook payload using params_map.
44
+ * Logs a warn via the injected logger for each undefined result.
45
+ * Returns a partial record — missing keys are absent, not null.
46
+ */
47
+ export function extractParams(payload, paramsMap, logger) {
48
+ const root = { headers: payload.headers, body: payload.body };
49
+ const result = {};
50
+ for (const [paramName, dotPath] of Object.entries(paramsMap)) {
51
+ const value = resolveDotPath(root, dotPath);
52
+ if (value === undefined) {
53
+ logger.warn('webhook params: could not resolve path', { path: dotPath });
54
+ }
55
+ else {
56
+ result[paramName] = value;
57
+ }
58
+ }
59
+ return result;
60
+ }
61
+ /**
62
+ * Extracts the dedup ID from a webhook payload.
63
+ * Returns the value coerced to string, or undefined if absent/null.
64
+ * Numeric values are valid dedup IDs (stringified).
65
+ */
66
+ export function extractDedupId(payload, dedupConfig) {
67
+ const root = { headers: payload.headers, body: payload.body };
68
+ const value = resolveDotPath(root, dedupConfig.id_from);
69
+ if (value === null || value === undefined) {
70
+ return undefined;
71
+ }
72
+ return String(value);
73
+ }
74
+ //# sourceMappingURL=webhook-params.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-params.js","sourceRoot":"","sources":["../../src/lib/webhook-params.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,cAAc,CAAC,GAAY,EAAE,IAAY;IACvD,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;QAChB,OAAO,GAAG,CAAC;IACb,CAAC;IAED,IAAI,OAAO,GAAY,GAAG,CAAC;IAC3B,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACtC,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC9C,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3B,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC1B,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;gBAC9B,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;oBACzC,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;oBACzB,SAAS;gBACX,CAAC;YACH,CAAC;YACD,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;YAChC,OAAO,GAAI,OAAmC,CAAC,OAAO,CAAC,CAAC;YACxD,SAAS;QACX,CAAC;QAED,qCAAqC;QACrC,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAC3B,OAAuB,EACvB,SAAiC,EACjC,MAAuD;IAEvD,MAAM,IAAI,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC;IAC9D,MAAM,MAAM,GAA4B,EAAE,CAAC;IAE3C,KAAK,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7D,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC5C,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,MAAM,CAAC,IAAI,CAAC,wCAAwC,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAC3E,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,SAAS,CAAC,GAAG,KAAK,CAAC;QAC5B,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAuB,EACvB,WAAwB;IAExB,MAAM,IAAI,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC;IAC9D,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACxD,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QAC1C,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Verifies a GitHub webhook signature.
3
+ * Header format: 'sha256=<hex-digest>'
4
+ */
5
+ export declare function verifyGithub(rawBody: Buffer, header: string, secret: string): boolean;
6
+ /**
7
+ * Verifies a shared-secret (header token) webhook — the primary model for sources that
8
+ * authenticate via a user-configured header (e.g. Gorgias: `Authorization: Bearer <token>`).
9
+ *
10
+ * `secret` is the EXACT expected header value the operator configured on the source integration
11
+ * (e.g. 'Bearer abc123' or just 'abc123'). No prefix stripping — an exact, timing-safe compare
12
+ * keeps it unambiguous. `headers` is the request header map (Node lowercases header names);
13
+ * `headerName` is matched case-insensitively. Pure, never throws, returns false on any malformed,
14
+ * missing, or mismatched input.
15
+ */
16
+ export declare function verifySharedSecret(headers: Record<string, string>, headerName: string, secret: string): boolean;
17
+ /**
18
+ * Verifies a Stripe webhook signature.
19
+ * Header format: 't=<timestamp>,v1=<hex-digest>[,v1=<hex-digest>...]'
20
+ * Signed payload: '<timestamp>.<rawBody>'
21
+ */
22
+ export declare function verifyStripe(rawBody: Buffer, header: string, secret: string, maxAgeSeconds?: number): boolean;
23
+ /**
24
+ * Verifies a generic HMAC webhook signature.
25
+ */
26
+ export declare function verifyHmac(rawBody: Buffer, header: string, secret: string, opts: {
27
+ algorithm?: 'sha1' | 'sha256' | 'sha512';
28
+ encoding?: 'hex' | 'base64';
29
+ timestamp_header?: string;
30
+ max_age_seconds?: number;
31
+ headers: Record<string, string>;
32
+ }): boolean;
33
+ //# sourceMappingURL=webhook-verifiers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-verifiers.d.ts","sourceRoot":"","sources":["../../src/lib/webhook-verifiers.ts"],"names":[],"mappings":"AAiBA;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAcrF;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,GACb,OAAO,CAsBT;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAwDT;AAED;;GAEG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,IAAI,EAAE;IACJ,SAAS,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACzC,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC,GACA,OAAO,CA8BT"}
@@ -0,0 +1,160 @@
1
+ // Webhook signature verifiers — pure functions, no I/O, no logging.
2
+ // Every comparison uses timingSafeEqual; every function returns false (never throws)
3
+ // on any malformed, missing, or mismatched input.
4
+ import { createHmac, timingSafeEqual } from 'node:crypto';
5
+ /**
6
+ * Length-safe constant-time comparison of two buffers.
7
+ * timingSafeEqual throws when lengths differ, so guard the length first.
8
+ * The length of an attacker-supplied signature is not itself a secret.
9
+ */
10
+ function safeEqual(a, b) {
11
+ if (a.length !== b.length) {
12
+ return false;
13
+ }
14
+ return timingSafeEqual(a, b);
15
+ }
16
+ /**
17
+ * Verifies a GitHub webhook signature.
18
+ * Header format: 'sha256=<hex-digest>'
19
+ */
20
+ export function verifyGithub(rawBody, header, secret) {
21
+ try {
22
+ if (typeof header !== 'string' || !header.startsWith('sha256=')) {
23
+ return false;
24
+ }
25
+ const provided = header.slice('sha256='.length);
26
+ if (provided === '') {
27
+ return false;
28
+ }
29
+ const computed = createHmac('sha256', secret).update(rawBody).digest('hex');
30
+ return safeEqual(Buffer.from(computed), Buffer.from(provided));
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ }
36
+ /**
37
+ * Verifies a shared-secret (header token) webhook — the primary model for sources that
38
+ * authenticate via a user-configured header (e.g. Gorgias: `Authorization: Bearer <token>`).
39
+ *
40
+ * `secret` is the EXACT expected header value the operator configured on the source integration
41
+ * (e.g. 'Bearer abc123' or just 'abc123'). No prefix stripping — an exact, timing-safe compare
42
+ * keeps it unambiguous. `headers` is the request header map (Node lowercases header names);
43
+ * `headerName` is matched case-insensitively. Pure, never throws, returns false on any malformed,
44
+ * missing, or mismatched input.
45
+ */
46
+ export function verifySharedSecret(headers, headerName, secret) {
47
+ try {
48
+ if (typeof headerName !== 'string' || headerName === '') {
49
+ return false;
50
+ }
51
+ if (typeof secret !== 'string' || secret === '') {
52
+ return false;
53
+ }
54
+ if (typeof headers !== 'object' || headers === null) {
55
+ return false;
56
+ }
57
+ // Own-property lookup (defense-in-depth): a polluted Object.prototype[<header-name>]
58
+ // can never satisfy the compare, even if `headers` was not built with a null prototype.
59
+ const key = headerName.toLowerCase();
60
+ const provided = Object.hasOwn(headers, key) ? headers[key] : undefined;
61
+ if (typeof provided !== 'string' || provided === '') {
62
+ return false;
63
+ }
64
+ return safeEqual(Buffer.from(provided), Buffer.from(secret));
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ /**
71
+ * Verifies a Stripe webhook signature.
72
+ * Header format: 't=<timestamp>,v1=<hex-digest>[,v1=<hex-digest>...]'
73
+ * Signed payload: '<timestamp>.<rawBody>'
74
+ */
75
+ export function verifyStripe(rawBody, header, secret, maxAgeSeconds) {
76
+ try {
77
+ if (typeof header !== 'string' || header === '') {
78
+ return false;
79
+ }
80
+ let timestamp;
81
+ const signatures = [];
82
+ for (const segment of header.split(',')) {
83
+ const eq = segment.indexOf('=');
84
+ if (eq === -1) {
85
+ continue;
86
+ }
87
+ const key = segment.slice(0, eq).trim();
88
+ const value = segment.slice(eq + 1).trim();
89
+ if (key === 't') {
90
+ timestamp = value;
91
+ }
92
+ else if (key === 'v1') {
93
+ signatures.push(value);
94
+ }
95
+ }
96
+ if (timestamp === undefined || timestamp === '' || signatures.length === 0) {
97
+ return false;
98
+ }
99
+ const signedPayload = Buffer.concat([Buffer.from(`${timestamp}.`), rawBody]);
100
+ const computed = createHmac('sha256', secret).update(signedPayload).digest('hex');
101
+ const computedBuf = Buffer.from(computed);
102
+ let matched = false;
103
+ for (const sig of signatures) {
104
+ if (safeEqual(computedBuf, Buffer.from(sig))) {
105
+ matched = true;
106
+ break;
107
+ }
108
+ }
109
+ if (!matched) {
110
+ return false;
111
+ }
112
+ // Age check happens AFTER signature verification.
113
+ if (maxAgeSeconds !== undefined) {
114
+ const ts = parseInt(timestamp, 10);
115
+ if (Number.isNaN(ts)) {
116
+ return false;
117
+ }
118
+ if (Math.floor(Date.now() / 1000) - ts > maxAgeSeconds) {
119
+ return false;
120
+ }
121
+ }
122
+ return true;
123
+ }
124
+ catch {
125
+ return false;
126
+ }
127
+ }
128
+ /**
129
+ * Verifies a generic HMAC webhook signature.
130
+ */
131
+ export function verifyHmac(rawBody, header, secret, opts) {
132
+ try {
133
+ if (typeof header !== 'string' || header === '') {
134
+ return false;
135
+ }
136
+ const algorithm = opts.algorithm ?? 'sha256';
137
+ const encoding = opts.encoding ?? 'hex';
138
+ const computed = createHmac(algorithm, secret).update(rawBody).digest(encoding);
139
+ if (opts.timestamp_header !== undefined) {
140
+ const tsValue = opts.headers[opts.timestamp_header];
141
+ if (tsValue === undefined) {
142
+ return false;
143
+ }
144
+ if (opts.max_age_seconds !== undefined) {
145
+ const ts = parseInt(tsValue, 10);
146
+ if (Number.isNaN(ts)) {
147
+ return false;
148
+ }
149
+ if (Math.floor(Date.now() / 1000) - ts > opts.max_age_seconds) {
150
+ return false;
151
+ }
152
+ }
153
+ }
154
+ return safeEqual(Buffer.from(computed), Buffer.from(header));
155
+ }
156
+ catch {
157
+ return false;
158
+ }
159
+ }
160
+ //# sourceMappingURL=webhook-verifiers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-verifiers.js","sourceRoot":"","sources":["../../src/lib/webhook-verifiers.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,qFAAqF;AACrF,kDAAkD;AAClD,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE1D;;;;GAIG;AACH,SAAS,SAAS,CAAC,CAAS,EAAE,CAAS;IACrC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;QAC1B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC/B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,MAAc,EAAE,MAAc;IAC1E,IAAI,CAAC;QACH,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAChE,OAAO,KAAK,CAAC;QACf,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAChD,IAAI,QAAQ,KAAK,EAAE,EAAE,CAAC;YACpB,OAAO,KAAK,CAAC;QACf,CAAC;QACD,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC5E,OAAO,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IACjE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAChC,OAA+B,EAC/B,UAAkB,EAClB,MAAc;IAEd,IAAI,CAAC;QACH,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,KAAK,EAAE,EAAE,CAAC;YACxD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC;YAChD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;YACpD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,qFAAqF;QACrF,wFAAwF;QACxF,MAAM,GAAG,GAAG,UAAU,CAAC,WAAW,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACxE,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,EAAE,EAAE,CAAC;YACpD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,OAAO,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAC1B,OAAe,EACf,MAAc,EACd,MAAc,EACd,aAAsB;IAEtB,IAAI,CAAC;QACH,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC;YAChD,OAAO,KAAK,CAAC;QACf,CAAC;QAED,IAAI,SAA6B,CAAC;QAClC,MAAM,UAAU,GAAa,EAAE,CAAC;QAChC,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;YACxC,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAChC,IAAI,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC;gBACd,SAAS;YACX,CAAC;YACD,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;YACxC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAC3C,IAAI,GAAG,KAAK,GAAG,EAAE,CAAC;gBAChB,SAAS,GAAG,KAAK,CAAC;YACpB,CAAC;iBAAM,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;gBACxB,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACzB,CAAC;QACH,CAAC;QAED,IAAI,SAAS,KAAK,SAAS,IAAI,SAAS,KAAK,EAAE,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3E,OAAO,KAAK,CAAC;QACf,CAAC;QAED,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,SAAS,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;QAC7E,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAClF,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAE1C,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;YAC7B,IAAI,SAAS,CAAC,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;gBAC7C,OAAO,GAAG,IAAI,CAAC;gBACf,MAAM;YACR,CAAC;QACH,CAAC;QACD,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,KAAK,CAAC;QACf,CAAC;QAED,kDAAkD;QAClD,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;YAChC,MAAM,EAAE,GAAG,QAAQ,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;YACnC,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;gBACrB,OAAO,KAAK,CAAC;YACf,CAAC;YACD,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,aAAa,EAAE,CAAC;gBACvD,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,UAAU,CACxB,OAAe,EACf,MAAc,EACd,MAAc,EACd,IAMC;IAED,IAAI,CAAC;QACH,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC;YAChD,OAAO,KAAK,CAAC;QACf,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,QAAQ,CAAC;QAC7C,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,KAAK,CAAC;QACxC,MAAM,QAAQ,GAAG,UAAU,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAEhF,IAAI,IAAI,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;YACxC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;YACpD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;gBAC1B,OAAO,KAAK,CAAC;YACf,CAAC;YACD,IAAI,IAAI,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;gBACvC,MAAM,EAAE,GAAG,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;gBACjC,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC;oBACrB,OAAO,KAAK,CAAC;gBACf,CAAC;gBACD,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;oBAC9D,OAAO,KAAK,CAAC;gBACf,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sensigo/realm-cli",
3
- "version": "0.6.4",
3
+ "version": "0.7.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -61,14 +61,14 @@
61
61
  "vitest": "^4.1.0"
62
62
  },
63
63
  "dependencies": {
64
- "@sensigo/realm": "^0.6.4",
65
- "@sensigo/realm-mcp": "^0.6.4",
66
- "@sensigo/realm-testing": "^0.6.4",
64
+ "@sensigo/realm": "^0.7.1",
65
+ "@sensigo/realm-mcp": "^0.7.1",
66
+ "@sensigo/realm-testing": "^0.7.1",
67
67
  "@modelcontextprotocol/sdk": "1.29.0",
68
68
  "chalk": "^5.0.0",
69
69
  "commander": "^14.0.3",
70
70
  "dotenv": "^17.4.0",
71
- "js-yaml": "^4.1.1"
71
+ "js-yaml": "^4.2.0"
72
72
  },
73
73
  "bin": {
74
74
  "realm": "./dist/index.js"