@idriszade/source-webhook 0.1.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/LICENSE +21 -0
- package/README.md +30 -0
- package/dist/async-queue.d.ts +19 -0
- package/dist/async-queue.d.ts.map +1 -0
- package/dist/async-queue.js +70 -0
- package/dist/async-queue.js.map +1 -0
- package/dist/handler.d.ts +14 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +58 -0
- package/dist/handler.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/webhook-source.d.ts +33 -0
- package/dist/webhook-source.d.ts.map +1 -0
- package/dist/webhook-source.js +102 -0
- package/dist/webhook-source.js.map +1 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Idris Idriszade
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @idriszade/source-webhook
|
|
2
|
+
|
|
3
|
+
Source adapter for receiving inbound webhooks via Hono — exposes a pre-wired Hono `app` and a raw web-standards `(req: Request) => Promise<Response>` handler. Verifies HMAC signatures via core's `verify()`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @idriszade/source-webhook
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`hono` and `@hono/zod-validator` are bundled as direct dependencies. Mount `app` (or `handler`) on Node, Bun, Cloudflare Workers, Vercel Edge, or Lambda.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { createWebhookSource } from '@idriszade/source-webhook';
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
|
|
19
|
+
const webhook = createWebhookSource({
|
|
20
|
+
path: '/webhooks/orders',
|
|
21
|
+
secret: process.env.WEBHOOK_SECRET!,
|
|
22
|
+
schema: z.object({ orderId: z.string(), total: z.number() }),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// In your server: webhook.app (Hono) or webhook.handler (raw fetch)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Reference
|
|
29
|
+
|
|
30
|
+
Canonical API surface: [`docs/spec-adapters.md`](../../docs/spec-adapters.md). Core types: [`docs/spec-api-surface.md`](../../docs/spec-api-surface.md).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal async queue used as an internal buffer for incoming webhook atoms.
|
|
3
|
+
* Items pushed before iteration starts are held and yielded in order.
|
|
4
|
+
* If iteration is already running, the next item wakes the pending resolver.
|
|
5
|
+
*/
|
|
6
|
+
export declare class AsyncQueue<T> {
|
|
7
|
+
private items;
|
|
8
|
+
private resolvers;
|
|
9
|
+
private closed;
|
|
10
|
+
push(item: T): void;
|
|
11
|
+
close(): void;
|
|
12
|
+
/**
|
|
13
|
+
* Returns a snapshot of the current buffer and clears it.
|
|
14
|
+
* Used by fetch() for a non-blocking drain.
|
|
15
|
+
*/
|
|
16
|
+
drain(): T[];
|
|
17
|
+
[Symbol.asyncIterator](signal?: AbortSignal): AsyncIterableIterator<T>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=async-queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-queue.d.ts","sourceRoot":"","sources":["../src/async-queue.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,qBAAa,UAAU,CAAC,CAAC;IACvB,OAAO,CAAC,KAAK,CAAW;IACxB,OAAO,CAAC,SAAS,CAAiD;IAClE,OAAO,CAAC,MAAM,CAAS;IAEvB,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI;IAUnB,KAAK,IAAI,IAAI;IAQb;;;OAGG;IACH,KAAK,IAAI,CAAC,EAAE;IAML,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,qBAAqB,CAAC,CAAC,CAAC;CAkC9E"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal async queue used as an internal buffer for incoming webhook atoms.
|
|
3
|
+
* Items pushed before iteration starts are held and yielded in order.
|
|
4
|
+
* If iteration is already running, the next item wakes the pending resolver.
|
|
5
|
+
*/
|
|
6
|
+
export class AsyncQueue {
|
|
7
|
+
items = [];
|
|
8
|
+
resolvers = [];
|
|
9
|
+
closed = false;
|
|
10
|
+
push(item) {
|
|
11
|
+
if (this.closed)
|
|
12
|
+
return;
|
|
13
|
+
const resolver = this.resolvers.shift();
|
|
14
|
+
if (resolver !== undefined) {
|
|
15
|
+
resolver({ value: item, done: false });
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
this.items.push(item);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
close() {
|
|
22
|
+
this.closed = true;
|
|
23
|
+
for (const resolver of this.resolvers) {
|
|
24
|
+
resolver({ value: undefined, done: true });
|
|
25
|
+
}
|
|
26
|
+
this.resolvers = [];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Returns a snapshot of the current buffer and clears it.
|
|
30
|
+
* Used by fetch() for a non-blocking drain.
|
|
31
|
+
*/
|
|
32
|
+
drain() {
|
|
33
|
+
const snapshot = this.items.slice();
|
|
34
|
+
this.items = [];
|
|
35
|
+
return snapshot;
|
|
36
|
+
}
|
|
37
|
+
async *[Symbol.asyncIterator](signal) {
|
|
38
|
+
while (true) {
|
|
39
|
+
if (signal?.aborted)
|
|
40
|
+
return;
|
|
41
|
+
if (this.items.length > 0) {
|
|
42
|
+
yield this.items.shift();
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (this.closed)
|
|
46
|
+
return;
|
|
47
|
+
const next = await new Promise((resolve) => {
|
|
48
|
+
if (signal?.aborted) {
|
|
49
|
+
resolve({ value: undefined, done: true });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const abortHandler = () => {
|
|
53
|
+
const idx = this.resolvers.indexOf(resolve);
|
|
54
|
+
if (idx !== -1)
|
|
55
|
+
this.resolvers.splice(idx, 1);
|
|
56
|
+
resolve({ value: undefined, done: true });
|
|
57
|
+
};
|
|
58
|
+
signal?.addEventListener('abort', abortHandler, { once: true });
|
|
59
|
+
this.resolvers.push((result) => {
|
|
60
|
+
signal?.removeEventListener('abort', abortHandler);
|
|
61
|
+
resolve(result);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
if (next.done)
|
|
65
|
+
return;
|
|
66
|
+
yield next.value;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=async-queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-queue.js","sourceRoot":"","sources":["../src/async-queue.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,OAAO,UAAU;IACb,KAAK,GAAQ,EAAE,CAAC;IAChB,SAAS,GAA8C,EAAE,CAAC;IAC1D,MAAM,GAAG,KAAK,CAAC;IAEvB,IAAI,CAAC,IAAO;QACV,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO;QACxB,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;QACxC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,QAAQ,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACzC,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;IAED,KAAK;QACH,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACtC,QAAQ,CAAC,EAAE,KAAK,EAAE,SAAyB,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,CAAC;QACD,IAAI,CAAC,SAAS,GAAG,EAAE,CAAC;IACtB,CAAC;IAED;;;OAGG;IACH,KAAK;QACH,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACpC,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;QAChB,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,MAAoB;QAChD,OAAO,IAAI,EAAE,CAAC;YACZ,IAAI,MAAM,EAAE,OAAO;gBAAE,OAAO;YAE5B,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC1B,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,EAAO,CAAC;gBAC9B,SAAS;YACX,CAAC;YAED,IAAI,IAAI,CAAC,MAAM;gBAAE,OAAO;YAExB,MAAM,IAAI,GAAG,MAAM,IAAI,OAAO,CAAoB,CAAC,OAAO,EAAE,EAAE;gBAC5D,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;oBACpB,OAAO,CAAC,EAAE,KAAK,EAAE,SAAyB,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;oBAC1D,OAAO;gBACT,CAAC;gBAED,MAAM,YAAY,GAAG,GAAS,EAAE;oBAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;oBAC5C,IAAI,GAAG,KAAK,CAAC,CAAC;wBAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;oBAC9C,OAAO,CAAC,EAAE,KAAK,EAAE,SAAyB,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;gBAC5D,CAAC,CAAC;gBAEF,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;gBAChE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;oBAC7B,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;oBACnD,OAAO,CAAC,MAAM,CAAC,CAAC;gBAClB,CAAC,CAAC,CAAC;YACL,CAAC,CAAC,CAAC;YAEH,IAAI,IAAI,CAAC,IAAI;gBAAE,OAAO;YACtB,MAAM,IAAI,CAAC,KAAK,CAAC;QACnB,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ZodType } from 'zod';
|
|
2
|
+
export interface RawHandlerConfig<O> {
|
|
3
|
+
secret: string;
|
|
4
|
+
schema: ZodType<O>;
|
|
5
|
+
onAtom: (data: O, idempotencyKey?: string) => void;
|
|
6
|
+
verify?: {
|
|
7
|
+
tolerance?: number;
|
|
8
|
+
acceptedAlgorithms?: ('v1' | 'v2')[];
|
|
9
|
+
headerName?: string;
|
|
10
|
+
};
|
|
11
|
+
idempotencyHeader?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function createRawHandler<O>(req: Request, cfg: RawHandlerConfig<O>): Promise<Response>;
|
|
14
|
+
//# sourceMappingURL=handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAKnC,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACnB,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,cAAc,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACnD,MAAM,CAAC,EAAE;QACP,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,kBAAkB,CAAC,EAAE,CAAC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;QACrC,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AASD,wBAAsB,gBAAgB,CAAC,CAAC,EACtC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,gBAAgB,CAAC,CAAC,CAAC,GACvB,OAAO,CAAC,QAAQ,CAAC,CA+CnB"}
|
package/dist/handler.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw web-standards handler that can be used without Hono.
|
|
3
|
+
* Validates HMAC, parses body with a Zod schema, and calls an onAtom callback.
|
|
4
|
+
*
|
|
5
|
+
* This is intentionally a thin function so callers that already have a
|
|
6
|
+
* web-standards Request/Response runtime (e.g. Cloudflare Workers, Deno Deploy)
|
|
7
|
+
* can use it directly without pulling in Hono.
|
|
8
|
+
*/
|
|
9
|
+
import { verify } from '@idriszade/core';
|
|
10
|
+
const DEFAULT_HEADER_NAME = 'X-Pipeline-Kit-Signature';
|
|
11
|
+
const DEFAULT_TOLERANCE_MS = 300_000;
|
|
12
|
+
function jsonError(status, code, message) {
|
|
13
|
+
return new Response(JSON.stringify({ error: { code, message } }), {
|
|
14
|
+
status,
|
|
15
|
+
headers: { 'Content-Type': 'application/json' },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export async function createRawHandler(req, cfg) {
|
|
19
|
+
const headerName = cfg.verify?.headerName ?? DEFAULT_HEADER_NAME;
|
|
20
|
+
const tolerance = cfg.verify?.tolerance ?? DEFAULT_TOLERANCE_MS;
|
|
21
|
+
const acceptedAlgorithms = cfg.verify?.acceptedAlgorithms ?? ['v1'];
|
|
22
|
+
const sigHeader = req.headers.get(headerName);
|
|
23
|
+
if (!sigHeader) {
|
|
24
|
+
return jsonError(400, 'missing_signature', `Missing required header: ${headerName}`);
|
|
25
|
+
}
|
|
26
|
+
const rawBody = await req.text();
|
|
27
|
+
const verifyResult = verify(rawBody, sigHeader, cfg.secret, {
|
|
28
|
+
tolerance,
|
|
29
|
+
acceptedAlgorithms,
|
|
30
|
+
});
|
|
31
|
+
if (verifyResult.error !== null) {
|
|
32
|
+
const e = verifyResult.error;
|
|
33
|
+
if (e.type !== 'invalid_payload') {
|
|
34
|
+
return jsonError(400, e.code, e.message);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
let rawParsed;
|
|
38
|
+
try {
|
|
39
|
+
rawParsed = JSON.parse(rawBody);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return jsonError(400, 'json_parse', 'Request body is not valid JSON');
|
|
43
|
+
}
|
|
44
|
+
const parsed = cfg.schema.safeParse(rawParsed);
|
|
45
|
+
if (!parsed.success) {
|
|
46
|
+
const message = parsed.error.issues.map((i) => i.message).join('; ');
|
|
47
|
+
return jsonError(400, 'schema_mismatch', message);
|
|
48
|
+
}
|
|
49
|
+
const idempotencyKey = cfg.idempotencyHeader
|
|
50
|
+
? (req.headers.get(cfg.idempotencyHeader) ?? undefined)
|
|
51
|
+
: undefined;
|
|
52
|
+
cfg.onAtom(parsed.data, idempotencyKey);
|
|
53
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
54
|
+
status: 200,
|
|
55
|
+
headers: { 'Content-Type': 'application/json' },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.js","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { RawHandlerConfig } from './handler.js';
|
|
2
|
+
export { createRawHandler } from './handler.js';
|
|
3
|
+
export type { WebhookHandler, WebhookSource, WebhookSourceConfig, WebhookVerifyConfig, } from './webhook-source.js';
|
|
4
|
+
export { createWebhookSource } from './webhook-source.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,YAAY,EACV,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAOhD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type RetryPolicy, type Source } from '@idriszade/core';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import type { ZodType } from 'zod';
|
|
4
|
+
export type WebhookHandler = (req: Request) => Promise<Response>;
|
|
5
|
+
export interface WebhookVerifyConfig {
|
|
6
|
+
/** Timestamp drift tolerance in ms. Default 300_000 (5 min). */
|
|
7
|
+
tolerance?: number;
|
|
8
|
+
/** HMAC algorithm variants to accept. Default ['v1']. */
|
|
9
|
+
acceptedAlgorithms?: ('v1' | 'v2')[];
|
|
10
|
+
/** Header to read the signature from. Default 'X-Pipeline-Kit-Signature'. */
|
|
11
|
+
headerName?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface WebhookSourceConfig<O> {
|
|
14
|
+
id?: string;
|
|
15
|
+
/** HTTP path to register, e.g. '/webhooks/stripe'. */
|
|
16
|
+
path: string;
|
|
17
|
+
/** HMAC secret used to verify incoming requests. */
|
|
18
|
+
secret: string;
|
|
19
|
+
/** Zod schema to parse and validate the request body. */
|
|
20
|
+
schema: ZodType<O>;
|
|
21
|
+
verify?: WebhookVerifyConfig;
|
|
22
|
+
/** Header name whose value is used as idempotency key on buffered atoms. */
|
|
23
|
+
idempotencyHeader?: string;
|
|
24
|
+
retryPolicy?: Partial<RetryPolicy>;
|
|
25
|
+
}
|
|
26
|
+
export interface WebhookSource<O> extends Source<O> {
|
|
27
|
+
/** Pre-wired Hono app with the POST route registered. */
|
|
28
|
+
readonly app: Hono;
|
|
29
|
+
/** Raw web-standards handler — delegates to the Hono app. */
|
|
30
|
+
readonly handler: WebhookHandler;
|
|
31
|
+
}
|
|
32
|
+
export declare function createWebhookSource<O>(config: WebhookSourceConfig<O>): WebhookSource<O>;
|
|
33
|
+
//# sourceMappingURL=webhook-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-source.d.ts","sourceRoot":"","sources":["../src/webhook-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,WAAW,EAChB,KAAK,MAAM,EAKZ,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAMnC,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEjE,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,kBAAkB,CAAC,EAAE,CAAC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;IACrC,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB,CAAC,CAAC;IACpC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACnB,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,4EAA4E;IAC5E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,aAAa,CAAC,CAAC,CAAE,SAAQ,MAAM,CAAC,CAAC,CAAC;IACjD,yDAAyD;IACzD,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC;IACnB,6DAA6D;IAC7D,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;CAClC;AASD,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CA4GvF"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { atom as atomId, ok, src as srcId, verify, } from '@idriszade/core';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { AsyncQueue } from './async-queue.js';
|
|
4
|
+
const DEFAULT_HEADER_NAME = 'X-Pipeline-Kit-Signature';
|
|
5
|
+
const DEFAULT_TOLERANCE_MS = 300_000;
|
|
6
|
+
function jsonError(status, code, message) {
|
|
7
|
+
return new Response(JSON.stringify({ error: { code, message } }), {
|
|
8
|
+
status,
|
|
9
|
+
headers: { 'Content-Type': 'application/json' },
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export function createWebhookSource(config) {
|
|
13
|
+
const resolvedId = config.id ?? srcId();
|
|
14
|
+
const headerName = config.verify?.headerName ?? DEFAULT_HEADER_NAME;
|
|
15
|
+
const tolerance = config.verify?.tolerance ?? DEFAULT_TOLERANCE_MS;
|
|
16
|
+
const acceptedAlgorithms = config.verify?.acceptedAlgorithms ?? ['v1'];
|
|
17
|
+
// Internal buffer shared between the HTTP handler and iter/fetch callers.
|
|
18
|
+
const queue = new AsyncQueue();
|
|
19
|
+
const app = new Hono();
|
|
20
|
+
app.post(config.path, async (c) => {
|
|
21
|
+
const sigHeader = c.req.header(headerName);
|
|
22
|
+
if (!sigHeader) {
|
|
23
|
+
return jsonError(400, 'missing_signature', `Missing required header: ${headerName}`);
|
|
24
|
+
}
|
|
25
|
+
const rawBody = await c.req.text();
|
|
26
|
+
// HMAC verification — we re-use core's verify but only for sig + timestamp.
|
|
27
|
+
// The schema parse below validates the actual payload shape, so we tolerate
|
|
28
|
+
// non-PipelineKitEvent bodies by calling the lower-level path.
|
|
29
|
+
// core's verify() also JSON.parses and type-checks for PipelineKitEvent.
|
|
30
|
+
// For arbitrary schemas we just need sig + timestamp check, then let Zod run.
|
|
31
|
+
const verifyResult = verify(rawBody, sigHeader, config.secret, {
|
|
32
|
+
tolerance,
|
|
33
|
+
acceptedAlgorithms,
|
|
34
|
+
});
|
|
35
|
+
if (verifyResult.error !== null) {
|
|
36
|
+
const e = verifyResult.error;
|
|
37
|
+
if (e.type === 'expired_timestamp') {
|
|
38
|
+
return jsonError(400, e.code, e.message);
|
|
39
|
+
}
|
|
40
|
+
if (e.type === 'missing_secret' || e.type === 'malformed_header') {
|
|
41
|
+
return jsonError(400, e.code, e.message);
|
|
42
|
+
}
|
|
43
|
+
if (e.type === 'invalid_signature') {
|
|
44
|
+
return jsonError(400, e.code, e.message);
|
|
45
|
+
}
|
|
46
|
+
// invalid_payload from JSON/event-type check is fine — we'll re-parse with Zod below.
|
|
47
|
+
if (e.type !== 'invalid_payload') {
|
|
48
|
+
return jsonError(400, e.code, e.message);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Parse raw JSON for schema validation.
|
|
52
|
+
let rawParsed;
|
|
53
|
+
try {
|
|
54
|
+
rawParsed = JSON.parse(rawBody);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return jsonError(400, 'json_parse', 'Request body is not valid JSON');
|
|
58
|
+
}
|
|
59
|
+
const parsed = config.schema.safeParse(rawParsed);
|
|
60
|
+
if (!parsed.success) {
|
|
61
|
+
const message = parsed.error.issues.map((i) => i.message).join('; ');
|
|
62
|
+
return jsonError(400, 'schema_mismatch', message);
|
|
63
|
+
}
|
|
64
|
+
const idempotencyKey = config.idempotencyHeader
|
|
65
|
+
? c.req.header(config.idempotencyHeader)
|
|
66
|
+
: undefined;
|
|
67
|
+
const atomItem = {
|
|
68
|
+
id: atomId(),
|
|
69
|
+
object: 'atom',
|
|
70
|
+
created_at: new Date().toISOString(),
|
|
71
|
+
metadata: idempotencyKey ? { idempotency_key: idempotencyKey } : {},
|
|
72
|
+
data: parsed.data,
|
|
73
|
+
source_id: resolvedId,
|
|
74
|
+
};
|
|
75
|
+
queue.push(atomItem);
|
|
76
|
+
return new Response(JSON.stringify({ id: atomItem.id, object: 'atom' }), {
|
|
77
|
+
status: 200,
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
async function* iterImpl(_query, ctx) {
|
|
82
|
+
for await (const item of queue[Symbol.asyncIterator](ctx.signal)) {
|
|
83
|
+
yield item;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function fetchImpl(_query, _ctx) {
|
|
87
|
+
// Non-blocking snapshot drain — returns whatever is buffered and clears it.
|
|
88
|
+
const buffered = queue.drain();
|
|
89
|
+
return ok(buffered);
|
|
90
|
+
}
|
|
91
|
+
const handler = (req) => Promise.resolve(app.fetch(req));
|
|
92
|
+
return {
|
|
93
|
+
id: resolvedId,
|
|
94
|
+
schema: config.schema,
|
|
95
|
+
retryPolicy: config.retryPolicy,
|
|
96
|
+
app,
|
|
97
|
+
handler,
|
|
98
|
+
iter: iterImpl,
|
|
99
|
+
fetch: fetchImpl,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=webhook-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-source.js","sourceRoot":"","sources":["../src/webhook-source.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@idriszade/source-webhook",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pipeline-kit Source adapter for receiving inbound webhooks via Hono",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"pipeline-kit",
|
|
22
|
+
"source",
|
|
23
|
+
"automation"
|
|
24
|
+
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@hono/zod-validator": "^0.7.6",
|
|
27
|
+
"hono": "^4.9.0",
|
|
28
|
+
"zod": "^4.4.3",
|
|
29
|
+
"@idriszade/core": "0.1.0"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc",
|
|
37
|
+
"typecheck": "tsc --noEmit"
|
|
38
|
+
}
|
|
39
|
+
}
|