@shivaedev/webhooks 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -8,14 +8,12 @@ A sender can retry or send concurrent copies. Your application declares which ev
8
8
 
9
9
  ```ts
10
10
  const sources = {
11
- github: githubSource({
12
- key: Config.Redacted("GITHUB_WEBHOOK_SECRET"),
13
- events: {
14
- issues: webhookEvent(IssueEvent, ({ issue }) => [
15
- webhookJob(IndexIssue, { number: issue.number }),
16
- ]),
17
- },
18
- }),
11
+ github: githubSource({
12
+ key: Config.Redacted("GITHUB_WEBHOOK_SECRET"),
13
+ events: {
14
+ issues: webhookEvent(IssueEvent, ({ issue }) => [webhookJob(IndexIssue, { number: issue.number })]),
15
+ },
16
+ }),
19
17
  };
20
18
  ```
21
19
 
@@ -41,48 +39,46 @@ import { webhookReceipts } from "@shivaedev/webhooks/webhookReceipts.ts";
41
39
 
42
40
  const Jobs = makePgBoss("app/Jobs");
43
41
  const IndexIssue = defineQueue({
44
- name: "index-issue",
45
- schema: Schema.Struct({ number: Schema.Number }),
42
+ name: "index-issue",
43
+ schema: Schema.Struct({ number: Schema.Number }),
46
44
  });
47
45
  const IssueEvent = Schema.Struct({
48
- issue: Schema.Struct({ number: Schema.Number }),
46
+ issue: Schema.Struct({ number: Schema.Number }),
49
47
  });
50
48
  const sources = {
51
- github: githubSource({
52
- key: Config.Redacted("GITHUB_WEBHOOK_SECRET"),
53
- events: {
54
- issues: webhookEvent(IssueEvent, ({ issue }) => [
55
- webhookJob(IndexIssue, { number: issue.number }),
56
- ]),
57
- },
58
- }),
49
+ github: githubSource({
50
+ key: Config.Redacted("GITHUB_WEBHOOK_SECRET"),
51
+ events: {
52
+ issues: webhookEvent(IssueEvent, ({ issue }) => [webhookJob(IndexIssue, { number: issue.number })]),
53
+ },
54
+ }),
59
55
  };
60
56
 
61
57
  const Host = appHost({
62
- config: { databaseUrl },
63
- migrations: {
64
- loader,
65
- components: [pgBossSchema(), webhookReceipts],
66
- },
67
- application: Effect.gen(function* () {
68
- const sql = yield* SqlClient.SqlClient;
69
- const jobs = Jobs.layer({
70
- connectionString: Redacted.value(databaseUrl),
71
- jobs: [IndexIssue],
72
- migrate: false,
73
- });
74
- return {
75
- group: Api,
76
- handlers,
77
- sessions,
78
- services: jobs,
79
- routes: webhookRoute({ basePath: "/webhooks", jobs: Jobs, sources }).pipe(
80
- Layer.provide(jobs),
81
- Layer.provide(Layer.succeed(SqlClient.SqlClient, sql)),
82
- Layer.orDie,
83
- ),
84
- };
85
- }),
58
+ config: { databaseUrl },
59
+ migrations: {
60
+ loader,
61
+ components: [pgBossSchema(), webhookReceipts],
62
+ },
63
+ application: Effect.gen(function* () {
64
+ const sql = yield* SqlClient.SqlClient;
65
+ const jobs = Jobs.layer({
66
+ connectionString: Redacted.value(databaseUrl),
67
+ jobs: [IndexIssue],
68
+ migrate: false,
69
+ });
70
+ return {
71
+ group: Api,
72
+ handlers,
73
+ sessions,
74
+ services: jobs,
75
+ routes: webhookRoute({ basePath: "/webhooks", jobs: Jobs, sources }).pipe(
76
+ Layer.provide(jobs),
77
+ Layer.provide(Layer.succeed(SqlClient.SqlClient, sql)),
78
+ Layer.orDie,
79
+ ),
80
+ };
81
+ }),
86
82
  });
87
83
  ```
88
84
 
@@ -90,17 +86,17 @@ const Host = appHost({
90
86
 
91
87
  ### Answers and retries
92
88
 
93
- | Request | Status | Body |
94
- | --- | --- | --- |
95
- | Registered event maps to jobs | 202 | `queued` |
96
- | Unlisted event, or mapping returns no jobs | 200 | `ignored` |
97
- | Source and delivery ID already recorded | 200 | `already queued` |
98
- | Unregistered source | 404 | Empty |
99
- | Missing or wrong signature | 401 | Empty |
100
- | Missing delivery ID or event header | 400 | `Invalid delivery` |
101
- | Payload rejected by its Schema | 400 | Empty |
102
- | Body exceeds the host limit | 413 | Empty |
103
- | Database or enqueue failure | 503 | Empty |
89
+ | Request | Status | Body |
90
+ | ------------------------------------------ | ------ | ------------------ |
91
+ | Registered event maps to jobs | 202 | `queued` |
92
+ | Unlisted event, or mapping returns no jobs | 200 | `ignored` |
93
+ | Source and delivery ID already recorded | 200 | `already queued` |
94
+ | Unregistered source | 404 | Empty |
95
+ | Missing or wrong signature | 401 | Empty |
96
+ | Missing delivery ID or event header | 400 | `Invalid delivery` |
97
+ | Payload rejected by its Schema | 400 | Empty |
98
+ | Body exceeds the host limit | 413 | Empty |
99
+ | Database or enqueue failure | 503 | Empty |
104
100
 
105
101
  Refused requests create no receipt and no jobs. Signatures are checked before decoding; unlisted events are verified and recorded without decoding. The signing key, signature and raw body stay out of responses, logs and spans.
106
102
 
@@ -1 +1 @@
1
- {"version":3,"file":"receive.d.ts","sourceRoot":"","sources":["../../src/internal/receive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAU,KAAK,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAE1E,eAAO,MAAM,OAAO,GAAgC,CAAC,4fA6BnD,CAAC"}
1
+ {"version":3,"file":"receive.d.ts","sourceRoot":"","sources":["../../src/internal/receive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAU,KAAK,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAE1E,eAAO,MAAM,OAAO,GAAgC,CAAC,4fA+BnD,CAAC"}
@@ -1,12 +1,13 @@
1
1
  import { Effect, Option } from "effect";
2
2
  import { HttpServerResponse } from "effect/http";
3
+ import * as HttpStatus from "effect/http/HttpStatus";
3
4
  import { SqlClient } from "effect/sql";
4
5
  import { enqueueWebhookJobs } from "#registration.ts";
5
6
  export const receive = Effect.fnUntraced(function* (name, source, key, body, headers, jobs) {
6
7
  yield* source.strategy.verify(body, headers, key);
7
8
  const { id, event } = source.strategy.delivery(headers);
8
9
  if (!(id && event)) {
9
- return HttpServerResponse.text("Invalid delivery", { status: 400 });
10
+ return HttpServerResponse.text("Invalid delivery", { status: HttpStatus.fromLiteral("BadRequest") });
10
11
  }
11
12
  const definition = Object.hasOwn(source.events, event) ? source.events[event] : undefined;
12
13
  const sql = yield* SqlClient.SqlClient;
@@ -15,12 +16,14 @@ export const receive = Effect.fnUntraced(function* (name, source, key, body, hea
15
16
  values (${name}, ${id}, ${event}, 'ignored', '[]'::jsonb)
16
17
  on conflict (source, delivery_id) do nothing returning delivery_id`;
17
18
  if (inserted.length === 0) {
18
- return HttpServerResponse.text("already queued", { status: 200 });
19
+ return HttpServerResponse.text("already queued", { status: HttpStatus.fromLiteral("OK") });
19
20
  }
20
21
  const requests = definition === undefined ? [] : yield* definition.decode(body);
21
22
  const ids = (yield* enqueueWebhookJobs(requests, jobs)).flatMap(Option.toArray);
22
23
  yield* sql `update webhook_receipts set job_ids = ${JSON.stringify(ids)}::jsonb, outcome = ${requests.length === 0 ? "ignored" : "queued"} where source = ${name} and delivery_id = ${id}`;
23
- return HttpServerResponse.text(requests.length === 0 ? "ignored" : "queued", { status: requests.length === 0 ? 200 : 202 });
24
+ return HttpServerResponse.text(requests.length === 0 ? "ignored" : "queued", {
25
+ status: requests.length === 0 ? HttpStatus.fromLiteral("OK") : HttpStatus.fromLiteral("Accepted"),
26
+ });
24
27
  }));
25
28
  });
26
29
  //# sourceMappingURL=receive.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"receive.js","sourceRoot":"","sources":["../../src/internal/receive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAiB,MAAM,QAAQ,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,EAAE,kBAAkB,EAAsB,MAAM,kBAAkB,CAAC;AAE1E,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EACjD,IAAY,EACZ,MAAwB,EACxB,GAA8B,EAC9B,IAAgB,EAChB,OAAyC,EACzC,IAAmB;IAEnB,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;IAClD,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACxD,IAAI,CAAC,CAAC,EAAE,IAAI,KAAK,CAAC,EAAE,CAAC;QACpB,OAAO,kBAAkB,CAAC,IAAI,CAAC,kBAAkB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;IACrE,CAAC;IACD,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC1F,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC;IACvC,OAAO,KAAK,CAAC,CAAC,GAAG,CAAC,eAAe,CAChC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,GAAG,CAAA;cAChB,IAAI,KAAK,EAAE,KAAK,KAAK;uEACoC,CAAC;QACrE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAO,kBAAkB,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QACnE,CAAC;QACD,MAAM,QAAQ,GAAG,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAChF,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,kBAAkB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAChF,KAAK,CAAC,CAAC,GAAG,CAAA,yCAAyC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,sBAAsB,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,mBAAmB,IAAI,sBAAsB,EAAE,EAAE,CAAC;QAC1L,OAAO,kBAAkB,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;IAC7H,CAAC,CAAC,CACF,CAAC;AACH,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"receive.js","sourceRoot":"","sources":["../../src/internal/receive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAiB,MAAM,QAAQ,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC,OAAO,EAAE,kBAAkB,EAAsB,MAAM,kBAAkB,CAAC;AAE1E,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EACjD,IAAY,EACZ,MAAwB,EACxB,GAA8B,EAC9B,IAAgB,EAChB,OAAyC,EACzC,IAAmB;IAEnB,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;IAClD,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACxD,IAAI,CAAC,CAAC,EAAE,IAAI,KAAK,CAAC,EAAE,CAAC;QACpB,OAAO,kBAAkB,CAAC,IAAI,CAAC,kBAAkB,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,WAAW,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IACtG,CAAC;IACD,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC1F,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC;IACvC,OAAO,KAAK,CAAC,CAAC,GAAG,CAAC,eAAe,CAChC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,GAAG,CAAA;cAChB,IAAI,KAAK,EAAE,KAAK,KAAK;uEACoC,CAAC;QACrE,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAO,kBAAkB,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC5F,CAAC;QACD,MAAM,QAAQ,GAAG,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAChF,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,kBAAkB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAChF,KAAK,CAAC,CAAC,GAAG,CAAA,yCAAyC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,sBAAsB,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,mBAAmB,IAAI,sBAAsB,EAAE,EAAE,CAAC;QAC1L,OAAO,kBAAkB,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,EAAE;YAC5E,MAAM,EAAE,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,WAAW,CAAC,UAAU,CAAC;SACjG,CAAC,CAAC;IACJ,CAAC,CAAC,CACF,CAAC;AACH,CAAC,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../src/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAU,KAAK,EAAiB,MAAM,QAAQ,CAAC;AACpE,OAAO,EAAE,UAAU,EAAsB,MAAM,aAAa,CAAC;AAC7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAEvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,MAAM,WAAW,mBAAmB,CAAC,KAAK,EAAE,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,IAAI,MAAM,EAAE,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC;IACjD,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAC7D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,mBAAmB,CAAC,KAAK,EAAE,CAAC,CAAC,oKA6B5E"}
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../src/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAU,KAAK,EAAiB,MAAM,QAAQ,CAAC;AACpE,OAAO,EAAE,UAAU,EAAsB,MAAM,aAAa,CAAC;AAE7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAEvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,MAAM,WAAW,mBAAmB,CAAC,KAAK,EAAE,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,IAAI,MAAM,EAAE,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC;IACjD,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAC7D;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,mBAAmB,CAAC,KAAK,EAAE,CAAC,CAAC,oKA+B5E"}
package/dist/route.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Effect, Layer } from "effect";
2
2
  import { HttpRouter, HttpServerResponse } from "effect/http";
3
+ import * as HttpStatus from "effect/http/HttpStatus";
3
4
  import { readBoundedBody } from "@shivaedev/http/server/boundedBody.ts";
4
5
  import { receive } from "#internal/receive.ts";
5
6
  export function webhookRoute(options) {
@@ -16,11 +17,11 @@ export function webhookRoute(options) {
16
17
  const source = Object.hasOwn(options.sources, name) ? options.sources[name] : undefined;
17
18
  const key = keys.get(name);
18
19
  if (source === undefined || key === undefined) {
19
- return HttpServerResponse.empty({ status: 404 });
20
+ return HttpServerResponse.empty({ status: HttpStatus.fromLiteral("NotFound") });
20
21
  }
21
22
  const body = new Uint8Array(yield* readBoundedBody(request));
22
23
  return yield* receive(name, source, key, body, request.headers, jobs).pipe(Effect.provideContext(context));
23
- }).pipe(Effect.catchTag("RequestBodyTooLarge", () => Effect.succeed(HttpServerResponse.empty({ status: 413 }))), Effect.catchTag("WebhookUnauthorized", () => Effect.succeed(HttpServerResponse.empty({ status: 401 }))), Effect.catchTag("SchemaError", () => Effect.succeed(HttpServerResponse.empty({ status: 400 }))), Effect.catch(() => Effect.succeed(HttpServerResponse.empty({ status: 503 })))));
24
+ }).pipe(Effect.catchTag("RequestBodyTooLarge", () => Effect.succeed(HttpServerResponse.empty({ status: HttpStatus.fromLiteral("PayloadTooLarge") }))), Effect.catchTag("WebhookUnauthorized", () => Effect.succeed(HttpServerResponse.empty({ status: HttpStatus.fromLiteral("Unauthorized") }))), Effect.catchTag("SchemaError", () => Effect.succeed(HttpServerResponse.empty({ status: HttpStatus.fromLiteral("BadRequest") }))), Effect.catch(() => Effect.succeed(HttpServerResponse.empty({ status: HttpStatus.fromLiteral("ServiceUnavailable") })))));
24
25
  }).pipe(Effect.orDie));
25
26
  }
26
27
  //# sourceMappingURL=route.js.map
package/dist/route.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"route.js","sourceRoot":"","sources":["../src/route.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"route.js","sourceRoot":"","sources":["../src/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,MAAM,EAAE,KAAK,EAAiB,MAAM,QAAQ,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAC;AAErD,OAAO,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAC;AAExE,OAAO,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAS/C,MAAM,UAAU,YAAY,CAAW,OAAsC;IAC5E,OAAO,KAAK,CAAC,MAAM,CAClB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,EAAmC,CAAC;QACzE,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;QACjC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAqC,CAAC;QAC1D,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9D,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACnC,CAAC;QACD,OAAO,UAAU,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE,EAAE,CAClG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YACnB,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,YAAY,CAAC;YAClD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC;YACjC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YACxF,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC3B,IAAI,MAAM,KAAK,SAAS,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;gBAC/C,OAAO,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,WAAW,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;YACjF,CAAC;YACD,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;YAC7D,OAAO,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;QAC5G,CAAC,CAAC,CAAC,IAAI,CACN,MAAM,CAAC,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE,CAC3C,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,WAAW,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAC,CAC/F,EACD,MAAM,CAAC,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,WAAW,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC,EAC1I,MAAM,CAAC,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,WAAW,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC,EAChI,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,WAAW,CAAC,oBAAoB,CAAC,EAAE,CAAC,CAAC,CAAC,CACtH,CACD,CAAC;IACH,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CACrB,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/webhooks",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Verified webhook receipts and transactional job delivery over Effect HTTP",
5
5
  "keywords": [
6
6
  "effect"
@@ -45,17 +45,17 @@
45
45
  "README.md"
46
46
  ],
47
47
  "dependencies": {
48
- "@shivaedev/github": "0.7.0",
49
- "@shivaedev/http": "0.1.0",
50
- "@shivaedev/jobs": "0.21.0",
51
- "@shivaedev/sql": "0.23.0"
48
+ "@shivaedev/github": "0.7.1",
49
+ "@shivaedev/http": "0.1.1",
50
+ "@shivaedev/jobs": "0.22.0",
51
+ "@shivaedev/sql": "0.24.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@effect/platform-node": "4.0.5",
55
- "@shivaedev/quality": "0.42.0",
56
- "@shivaedev/server": "0.2.0",
57
- "@shivaedev/test-sql": "0.10.1",
58
- "@shivaedev/test-story": "0.21.1",
55
+ "@shivaedev/quality": "0.44.0",
56
+ "@shivaedev/server": "0.3.1",
57
+ "@shivaedev/test-sql": "0.10.2",
58
+ "@shivaedev/test-story": "0.22.0",
59
59
  "@types/node": "24.19.2",
60
60
  "effect": "4.0.5",
61
61
  "pg-boss": "12.38.0",
@@ -1,5 +1,6 @@
1
1
  import { Effect, Option, type Redacted } from "effect";
2
2
  import { HttpServerResponse } from "effect/http";
3
+ import * as HttpStatus from "effect/http/HttpStatus";
3
4
  import { SqlClient } from "effect/sql";
4
5
  import type { PgBossService } from "@shivaedev/jobs/server/service.ts";
5
6
  import { enqueueWebhookJobs, type WebhookSource } from "#registration.ts";
@@ -15,7 +16,7 @@ export const receive = Effect.fnUntraced(function* <R>(
15
16
  yield* source.strategy.verify(body, headers, key);
16
17
  const { id, event } = source.strategy.delivery(headers);
17
18
  if (!(id && event)) {
18
- return HttpServerResponse.text("Invalid delivery", { status: 400 });
19
+ return HttpServerResponse.text("Invalid delivery", { status: HttpStatus.fromLiteral("BadRequest") });
19
20
  }
20
21
  const definition = Object.hasOwn(source.events, event) ? source.events[event] : undefined;
21
22
  const sql = yield* SqlClient.SqlClient;
@@ -25,12 +26,14 @@ export const receive = Effect.fnUntraced(function* <R>(
25
26
  values (${name}, ${id}, ${event}, 'ignored', '[]'::jsonb)
26
27
  on conflict (source, delivery_id) do nothing returning delivery_id`;
27
28
  if (inserted.length === 0) {
28
- return HttpServerResponse.text("already queued", { status: 200 });
29
+ return HttpServerResponse.text("already queued", { status: HttpStatus.fromLiteral("OK") });
29
30
  }
30
31
  const requests = definition === undefined ? [] : yield* definition.decode(body);
31
32
  const ids = (yield* enqueueWebhookJobs(requests, jobs)).flatMap(Option.toArray);
32
33
  yield* sql`update webhook_receipts set job_ids = ${JSON.stringify(ids)}::jsonb, outcome = ${requests.length === 0 ? "ignored" : "queued"} where source = ${name} and delivery_id = ${id}`;
33
- return HttpServerResponse.text(requests.length === 0 ? "ignored" : "queued", { status: requests.length === 0 ? 200 : 202 });
34
+ return HttpServerResponse.text(requests.length === 0 ? "ignored" : "queued", {
35
+ status: requests.length === 0 ? HttpStatus.fromLiteral("OK") : HttpStatus.fromLiteral("Accepted"),
36
+ });
34
37
  }),
35
38
  );
36
39
  });
package/src/route.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type Context, Effect, Layer, type Redacted } from "effect";
2
2
  import { HttpRouter, HttpServerResponse } from "effect/http";
3
+ import * as HttpStatus from "effect/http/HttpStatus";
3
4
  import type { SqlClient } from "effect/sql";
4
5
  import { readBoundedBody } from "@shivaedev/http/server/boundedBody.ts";
5
6
  import type { PgBossService } from "@shivaedev/jobs/server/service.ts";
@@ -28,15 +29,17 @@ export function webhookRoute<TJobs, R>(options: WebhookRouteOptions<TJobs, R>) {
28
29
  const source = Object.hasOwn(options.sources, name) ? options.sources[name] : undefined;
29
30
  const key = keys.get(name);
30
31
  if (source === undefined || key === undefined) {
31
- return HttpServerResponse.empty({ status: 404 });
32
+ return HttpServerResponse.empty({ status: HttpStatus.fromLiteral("NotFound") });
32
33
  }
33
34
  const body = new Uint8Array(yield* readBoundedBody(request));
34
35
  return yield* receive(name, source, key, body, request.headers, jobs).pipe(Effect.provideContext(context));
35
36
  }).pipe(
36
- Effect.catchTag("RequestBodyTooLarge", () => Effect.succeed(HttpServerResponse.empty({ status: 413 }))),
37
- Effect.catchTag("WebhookUnauthorized", () => Effect.succeed(HttpServerResponse.empty({ status: 401 }))),
38
- Effect.catchTag("SchemaError", () => Effect.succeed(HttpServerResponse.empty({ status: 400 }))),
39
- Effect.catch(() => Effect.succeed(HttpServerResponse.empty({ status: 503 }))),
37
+ Effect.catchTag("RequestBodyTooLarge", () =>
38
+ Effect.succeed(HttpServerResponse.empty({ status: HttpStatus.fromLiteral("PayloadTooLarge") })),
39
+ ),
40
+ Effect.catchTag("WebhookUnauthorized", () => Effect.succeed(HttpServerResponse.empty({ status: HttpStatus.fromLiteral("Unauthorized") }))),
41
+ Effect.catchTag("SchemaError", () => Effect.succeed(HttpServerResponse.empty({ status: HttpStatus.fromLiteral("BadRequest") }))),
42
+ Effect.catch(() => Effect.succeed(HttpServerResponse.empty({ status: HttpStatus.fromLiteral("ServiceUnavailable") }))),
40
43
  ),
41
44
  );
42
45
  }).pipe(Effect.orDie),