@happyvertical/smrt-webhooks 0.0.0-stage → 0.55.10

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.
Files changed (43) hide show
  1. package/AGENTS.md +23 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +141 -2
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/admin.d.ts +25 -0
  8. package/dist/admin.d.ts.map +1 -0
  9. package/dist/admin.js +2 -0
  10. package/dist/change-feed.d.ts +25 -0
  11. package/dist/change-feed.d.ts.map +1 -0
  12. package/dist/chunks/admin-413jve1I.js +318 -0
  13. package/dist/chunks/admin-413jve1I.js.map +1 -0
  14. package/dist/chunks/dispatcher-C5AqtgtK.js +248 -0
  15. package/dist/chunks/dispatcher-C5AqtgtK.js.map +1 -0
  16. package/dist/dispatcher.d.ts +23 -0
  17. package/dist/dispatcher.d.ts.map +1 -0
  18. package/dist/events.d.ts +6 -0
  19. package/dist/events.d.ts.map +1 -0
  20. package/dist/index.d.ts +11 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +148 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/manifest.json +1032 -0
  25. package/dist/models.d.ts +43 -0
  26. package/dist/models.d.ts.map +1 -0
  27. package/dist/recipes.d.ts +23 -0
  28. package/dist/recipes.d.ts.map +1 -0
  29. package/dist/security.d.ts +15 -0
  30. package/dist/security.d.ts.map +1 -0
  31. package/dist/smrt-knowledge.json +418 -0
  32. package/dist/store.d.ts +26 -0
  33. package/dist/store.d.ts.map +1 -0
  34. package/dist/svelte/WebhookAdmin.svelte +113 -0
  35. package/dist/svelte/WebhookAdmin.svelte.d.ts +22 -0
  36. package/dist/svelte/WebhookAdmin.svelte.d.ts.map +1 -0
  37. package/dist/svelte/index.d.ts +2 -0
  38. package/dist/svelte/index.d.ts.map +1 -0
  39. package/dist/svelte/index.js +1 -0
  40. package/dist/types.d.ts +27 -0
  41. package/dist/types.d.ts.map +1 -0
  42. package/dist/types.js +0 -0
  43. package/package.json +75 -3
package/AGENTS.md ADDED
@@ -0,0 +1,23 @@
1
+ # @happyvertical/smrt-webhooks
2
+
3
+ Server-only outbound webhooks. `change-feed.ts` owns durable model publication
4
+ with tenant/runtime cursors and retained-history recovery; `events.ts` is the
5
+ optional non-atomic hook path. Never enable both for the same models. `store.ts` owns tenant-bound SQL and atomic
6
+ outbox/job enqueue; `dispatcher.ts` owns signing and job execution. Always use
7
+ `requireTenant()` plus explicit tenant predicates, including raw CAS writes.
8
+
9
+ `security.ts` validates every DNS result, pins HTTPS lookup, disables pooling,
10
+ and bounds DNS/request time. Never substitute a preflight DNS check followed by
11
+ fetch, follow redirects, or persist raw transport errors. Preserve exact stored
12
+ payload bytes for signing. HTTP is at-least-once; receivers deduplicate the
13
+ stable delivery ID. Do not promise exactly-once network effects.
14
+
15
+ Admin projections are allowlists. Secrets and payloads are sensitive fields;
16
+ generated API/CLI/MCP access is disabled. The host authorizes administrators via
17
+ `WebhookAdminService`; Svelte callbacks must use authenticated, CSRF-protected
18
+ host routes. No credentials in the browser read model.
19
+
20
+ Validate: `pnpm test`, `pnpm typecheck`, `pnpm check`, `pnpm test:e2e` within this
21
+ package after building its dependencies. E2E uses the maintained Playwright
22
+ harness under `e2e/`; no external webhook server or network credential is needed.
23
+ See README.md for migration, lifecycle-hook boundaries and worker setup.
package/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ @AGENTS.md
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright <2025> <Happy Vertical Corporation>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,142 @@
1
- # Temporary Holding Version
1
+ # Outbound webhooks
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `@happyvertical/smrt-webhooks` delivers tenant-scoped create, update, delete and
4
+ custom events to public HTTPS endpoints, with an HMAC signature, a durable jobs
5
+ queue, retry/backoff, and a safe delivery log.
6
+
7
+ ## Server setup
8
+
9
+ Migrate the package models and jobs system tables with the normal application
10
+ migration flow before starting workers. Runtime code never creates application
11
+ schema. Run this only in a Node server; import the UI from `/svelte` separately.
12
+
13
+ ```ts
14
+ import {
15
+ WebhookDispatcher, WebhookAdminService,
16
+ registerWebhookRuntime, WebhookChangeFeed,
17
+ } from '@happyvertical/smrt-webhooks';
18
+ import { TaskRunner } from '@happyvertical/smrt-jobs';
19
+
20
+ const webhooks = new WebhookDispatcher({ db, runtime: 'application-webhooks' });
21
+ registerWebhookRuntime('application-webhooks', webhooks);
22
+ const events = new WebhookChangeFeed(webhooks, { orders: '@example/app:Order' });
23
+ // Poll from a host scheduler inside each authenticated tenant context.
24
+ await events.poll();
25
+ const admin = new WebhookAdminService(webhooks.store, async () =>
26
+ currentUserHasPermission('webhooks:admin'));
27
+ const runner = new TaskRunner({ queues: ['webhooks'] });
28
+ await runner.initialize(db);
29
+ await runner.start();
30
+ ```
31
+
32
+ Register the same runtime name in every worker process before starting jobs.
33
+ The runner restores captured tenant context; all service calls require the
34
+ host's authenticated `withTenant` context. Do not derive that context from a
35
+ client-supplied tenant ID. Unregister optional hooks and call `unregisterWebhookRuntime()` on shutdown. The server entry is intentionally not browser-safe.
36
+
37
+ Lifecycle subscriptions use qualified model names. The recommended durable path is
38
+ `WebhookChangeFeed`: map physical application tables to model names and poll in
39
+ each tenant context. It reads s-m-r-t's committed change feed (including delete
40
+ tombstones), publishes identity-only `{ id }` payloads, and advances a persisted
41
+ tenant/runtime cursor only after outbox/jobs are durable. Partial failure or
42
+ process death before cursor advancement replays stable change-sequence IDs,
43
+ which deduplicate against already enqueued deliveries. Global records and other
44
+ tenants are excluded. Shared STI tables must map to their base model: the feed
45
+ does not encode a child discriminator. Custom SQL that bypasses s-m-r-t persistence
46
+ must explicitly append its domain event or core change-feed entry.
47
+
48
+ Poll more frequently than core change-feed retention. A pruned cursor raises
49
+ `WebhookHistoryExpiredError` and stops rather than silently dropping history.
50
+ After an operator reconciles source state and publishes any recoverable missing
51
+ events, call `resumeAfterResync(error.resumeCursor)`. Lost historical transitions
52
+ cannot be reconstructed from a current-state snapshot alone; retain domain event
53
+ history if that guarantee is required. Keep one stable model mapping per runtime;
54
+ adding tables after its cursor has advanced does not backfill their older events.
55
+
56
+ `registerWebhookModelEvents` is an alternative low-latency hook binding with an
57
+ explicit model allowlist. It also publishes only identities, but source writes
58
+ and hooks are not atomic: a crash between source commit and hook enqueue can
59
+ miss an event. Use the durable change-feed path when that gap is unacceptable.
60
+ Do not enable both paths for the same models: their event IDs represent different
61
+ publication streams. Neither path promises atomic network effects with source
62
+ writes. Hooks propagate enqueue failures; re-saving a source object does not
63
+ reconstruct a missing historical create/update event.
64
+
65
+ For custom events, authorize the data projection on the server:
66
+
67
+ ```ts
68
+ await webhooks.dispatch({
69
+ id: durableDomainEventId, event: 'custom.paid', model: '@example/app:Invoice',
70
+ occurredAt: new Date().toISOString(), data: { id: invoiceId },
71
+ });
72
+ ```
73
+
74
+ Empty event/model filters mean all events/models. Unknown nonempty custom names
75
+ are supported; malformed envelopes and bodies over 256 KiB are rejected.
76
+
77
+ ## Delivery and recovery
78
+
79
+ A unique `(tenant, subscription, event ID)` outbox row and its jobs enqueue commit
80
+ in one database transaction. Repeated dispatch is a no-op for that identity.
81
+ Jobs contain only runtime, opaque tenant/delivery IDs, and a replay generation. Five attempts use jobs'
82
+ exponential retry (1 second initial delay, multiplier 2, 5 minute cap). A delivery
83
+ lease prevents overlapping HTTP attempts; an expired lease permits recovery
84
+ after worker death. Terminal writes require the current lease token. Disabled
85
+ subscriptions cancel unsent deliveries. Secrets are read at delivery time, so
86
+ rotation applies to subsequent attempts.
87
+
88
+ Call `store.reconcile()` from a tenant-scoped operator sweep to turn exhausted
89
+ or cancelled jobs into replayable failed deliveries. Admin refresh also runs
90
+ reconciliation. `admin.replay(id)` atomically creates a new job only for a failed
91
+ delivery; repeated replay is a no-op. Delivered events are never replayed by this
92
+ API. Receivers **must deduplicate `x-smrt-delivery`**: if HTTP succeeds and the
93
+ process dies before the outcome commits, recovery can send the same request
94
+ again. The package does not claim exactly-once HTTP effects.
95
+
96
+ ## Security contract
97
+
98
+ Only HTTPS port 443 is allowed, without credentials or fragments. Every resolved
99
+ DNS address must be public unicast. The socket lookup is pinned to a validated
100
+ answer while TLS still verifies the original hostname. Redirects and connection
101
+ pooling are disabled. DNS and request deadlines are 5 and 10 seconds. Special,
102
+ private, mapped, transition and documentation IP ranges are conservatively
103
+ rejected. Response bodies are never buffered or logged. A transport override is
104
+ a **trusted server-only integration seam** and must preserve those requirements.
105
+
106
+ `x-smrt-signature` is `sha256=<hex HMAC-SHA256(secret, timestamp + '.' + body)>`,
107
+ where timestamp is the exact `x-smrt-timestamp` header and body is the exact
108
+ persisted UTF-8 request body. Verify in constant time, bound timestamp age, and
109
+ deduplicate the delivery ID. Keys require at least 32 bytes of entropy-bearing
110
+ secret material (generate them cryptographically; length alone is not entropy).
111
+
112
+ Secrets and payloads are durable sensitive data: use protected database access,
113
+ backups and encryption at rest. Never serialize model instances with `toJSON`
114
+ for clients; use the admin service or `toPublicJSON`. The admin view omits secrets,
115
+ payloads and lease tokens, and transport errors are fixed safe strings.
116
+
117
+ ## Admin UI and recipe
118
+
119
+ `WebhookAdmin` from `/svelte` uses smrt-ui Button, Card, Input and Alert primitives.
120
+ Supply `subscriptions`, `deliveries`, and async `onCreate`, `onToggle`, `onReplay`
121
+ callbacks backed by `WebhookAdminService`. Refresh the view after mutations.
122
+ The host owns login, administrator permissions, and CSRF protection. The component
123
+ has busy/error states and never receives persisted signing secrets.
124
+
125
+ The `integrations.webhooks` recipe declares `runtime: 'server'` and the
126
+ `@happyvertical/smrt-webhooks/svelte#WebhookAdmin` settings panel in the
127
+ Integrations group. The host resolves that export and supplies the authenticated
128
+ callbacks and safe read models described above. It intentionally declares no
129
+ model navigation: generated CRUD is disabled for these sensitive internal
130
+ models. It declares no provider or demo seed because transport configuration and
131
+ signing secrets belong to the server; selecting the recipe does not configure a
132
+ worker or authorize a browser to dispatch webhooks. The browser capability gate
133
+ records this intentional server-only root against #3717; the separate `/svelte`
134
+ export contains the administration component, not the delivery transport.
135
+
136
+ ## Validation
137
+
138
+ `pnpm test` exercises persistence, rollback, tenancy, retries, lease fencing,
139
+ lifecycle events, redaction, HMAC and DNS pinning. `pnpm check` and
140
+ `pnpm typecheck` check public TypeScript/Svelte contracts. `pnpm test:e2e` runs the
141
+ maintained Chromium admin interaction harness. Build with
142
+ `pnpm exec turbo build --filter=@happyvertical/smrt-webhooks...` from the root.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=__smrt-register__.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"__smrt-register__.d.ts","sourceRoot":"","sources":["../src/__smrt-register__.ts"],"names":[],"mappings":""}
@@ -0,0 +1,25 @@
1
+ import type { WebhookStore } from './store.js';
2
+ import type { WebhookDeliveryRecord, WebhookSubscriptionRecord } from './types.js';
3
+ export interface WebhookAdminView {
4
+ subscriptions: WebhookSubscriptionRecord[];
5
+ deliveries: WebhookDeliveryRecord[];
6
+ }
7
+ /** Allowlisted projection; never spread persistence objects into a browser response. */
8
+ export declare function webhookAdminView(subscriptions: WebhookSubscriptionRecord[], deliveries: WebhookDeliveryRecord[]): WebhookAdminView;
9
+ /** Host supplies its administrator permission check and authenticated tenant context. */
10
+ export declare class WebhookAdminService {
11
+ private readonly store;
12
+ private readonly authorize;
13
+ constructor(store: WebhookStore, authorize: () => Promise<boolean>);
14
+ private guard;
15
+ view(): Promise<WebhookAdminView>;
16
+ create(input: {
17
+ url: string;
18
+ events: string[];
19
+ models: string[];
20
+ secret: string;
21
+ }): Promise<void>;
22
+ toggle(id: string, enabled: boolean): Promise<void>;
23
+ replay(id: string): Promise<boolean>;
24
+ }
25
+ //# sourceMappingURL=admin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin.d.ts","sourceRoot":"","sources":["../src/admin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EACV,qBAAqB,EACrB,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AACpB,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,yBAAyB,EAAE,CAAC;IAC3C,UAAU,EAAE,qBAAqB,EAAE,CAAC;CACrC;AACD,wFAAwF;AACxF,wBAAgB,gBAAgB,CAC9B,aAAa,EAAE,yBAAyB,EAAE,EAC1C,UAAU,EAAE,qBAAqB,EAAE,GAClC,gBAAgB,CAqBlB;AACD,yFAAyF;AACzF,qBAAa,mBAAmB;IAE5B,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBADT,KAAK,EAAE,YAAY,EACnB,SAAS,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC;YAEtC,KAAK;IAKb,IAAI,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAUjC,MAAM,CAAC,KAAK,EAAE;QAClB,GAAG,EAAE,MAAM,CAAC;QACZ,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;KAChB,GAAG,OAAO,CAAC,IAAI,CAAC;IAeX,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAQnD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAI3C"}
package/dist/admin.js ADDED
@@ -0,0 +1,2 @@
1
+ import { n as webhookAdminView, t as WebhookAdminService } from "./chunks/admin-413jve1I.js";
2
+ export { WebhookAdminService, webhookAdminView };
@@ -0,0 +1,25 @@
1
+ import type { WebhookDispatcher } from './dispatcher.js';
2
+ export declare class WebhookHistoryExpiredError extends Error {
3
+ readonly resumeCursor: number;
4
+ constructor(resumeCursor: number);
5
+ }
6
+ /** Durable, replayable lifecycle publication from SMRT's committed change feed.
7
+ * Host scheduling calls poll() in each authenticated tenant context. The mapping
8
+ * is an explicit physical-table allowlist; shared STI tables represent the base model.
9
+ */
10
+ export declare class WebhookChangeFeed {
11
+ private readonly dispatcher;
12
+ private readonly models;
13
+ constructor(dispatcher: WebhookDispatcher, models: Readonly<Record<string, string>>);
14
+ /** Cursor only advances after all matched changes have durable outbox/jobs.
15
+ * A crash or partial failure replays the same stable identities on the next poll.
16
+ * History pruning fails loudly: the host must reconcile source state before reset.
17
+ */
18
+ poll(): Promise<{
19
+ cursor: number;
20
+ published: number;
21
+ }>;
22
+ /** Operator-only recovery: call after reconciling retained source state externally. */
23
+ resumeAfterResync(cursor: number): Promise<void>;
24
+ }
25
+ //# sourceMappingURL=change-feed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"change-feed.d.ts","sourceRoot":"","sources":["../src/change-feed.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,qBAAa,0BAA2B,SAAQ,KAAK;IACvC,QAAQ,CAAC,YAAY,EAAE,MAAM;gBAApB,YAAY,EAAE,MAAM;CAK1C;AAED;;;GAGG;AACH,qBAAa,iBAAiB;IAE1B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,UAAU,EAAE,iBAAiB,EAC7B,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAS3D;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAyD5D,uFAAuF;IACjF,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAYvD"}
@@ -0,0 +1,318 @@
1
+ import { SmrtCollection, SmrtObject, field, foreignKey, smrt } from "@happyvertical/smrt-core";
2
+ import { TenantScoped, requireTenant, tenantId } from "@happyvertical/smrt-tenancy";
3
+ import { backgroundEligible } from "@happyvertical/smrt-jobs";
4
+ import { lookup } from "node:dns/promises";
5
+ import { request } from "node:https";
6
+ import { isIP } from "node:net";
7
+ //#region src/security.ts
8
+ function isPublicAddress(address) {
9
+ if (isIP(address) === 4) {
10
+ const [a, b, c] = address.split(".").map(Number);
11
+ return !(a === 0 || a === 10 || a === 127 || a >= 224 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && (b === 0 || b === 168 || b === 88 && c === 99) || a === 198 && (b === 18 || b === 19 || b === 51 && c === 100) || a === 203 && b === 0 && c === 113);
12
+ }
13
+ if (isIP(address) !== 6) return false;
14
+ const normalized = new URL(`https://[${address}]/`).hostname.slice(1, -1);
15
+ const first = Number.parseInt(normalized.split(":")[0], 16);
16
+ return first >= 8192 && first < 16384 && first !== 8194 && !(first === 8193 && Number.parseInt(normalized.split(":")[1] || "0", 16) < 512) && !normalized.startsWith("2001:db8:") && first !== 16383;
17
+ }
18
+ function assertWebhookUrl(value) {
19
+ let url;
20
+ try {
21
+ url = new URL(value);
22
+ } catch {
23
+ throw new Error("Webhook URL must be an absolute HTTPS URL.");
24
+ }
25
+ const host = url.hostname.replace(/^\[|\]$/g, "");
26
+ if (url.protocol !== "https:" || url.username || url.password || url.hash || url.port && url.port !== "443" || host === "localhost" || host.endsWith(".localhost") || isIP(host) && !isPublicAddress(host)) throw new Error("Webhook URL must use public HTTPS on port 443 without credentials or a fragment.");
27
+ return url;
28
+ }
29
+ function assertHmacSecret(secret) {
30
+ if (typeof secret !== "string" || Buffer.byteLength(secret) < 32 || Buffer.byteLength(secret) > 4096) throw new Error("Webhook signing secret must contain 32 to 4096 bytes.");
31
+ }
32
+ var sendPublicWebhook = async ({ url, body, headers }) => {
33
+ const target = assertWebhookUrl(url);
34
+ const host = target.hostname.replace(/^\[|\]$/g, "");
35
+ let dnsTimer;
36
+ const answers = isIP(host) ? [{
37
+ address: host,
38
+ family: isIP(host)
39
+ }] : await Promise.race([lookup(host, {
40
+ all: true,
41
+ verbatim: true
42
+ }), new Promise((_, reject) => {
43
+ dnsTimer = setTimeout(() => reject(/* @__PURE__ */ new Error("Webhook DNS timeout.")), 5e3);
44
+ dnsTimer.unref();
45
+ })]).finally(() => clearTimeout(dnsTimer));
46
+ if (!answers.length || answers.some(({ address }) => !isPublicAddress(address))) throw new Error("Webhook destination is not public.");
47
+ const pinned = answers[0];
48
+ return new Promise((resolve, reject) => {
49
+ const req = request(target, {
50
+ method: "POST",
51
+ agent: false,
52
+ headers: {
53
+ ...headers,
54
+ "content-length": Buffer.byteLength(body).toString()
55
+ },
56
+ lookup: (_hostname, options, callback) => {
57
+ if (options.all) callback(null, [pinned]);
58
+ else callback(null, pinned.address, pinned.family);
59
+ }
60
+ }, (response) => {
61
+ const status = response.statusCode ?? 0;
62
+ response.destroy();
63
+ resolve(status);
64
+ });
65
+ const timer = setTimeout(() => req.destroy(/* @__PURE__ */ new Error("Webhook request timeout.")), 1e4);
66
+ req.on("close", () => clearTimeout(timer));
67
+ req.on("error", () => reject(/* @__PURE__ */ new Error("Webhook transport failed.")));
68
+ req.end(body);
69
+ });
70
+ };
71
+ //#endregion
72
+ //#region src/models.ts
73
+ var __defProp = Object.defineProperty;
74
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
75
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
76
+ enumerable: true,
77
+ configurable: true,
78
+ writable: true,
79
+ value
80
+ }) : obj[key] = value;
81
+ var __decorateClass = (decorators, target, key, kind) => {
82
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
83
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
84
+ if (kind && result) __defProp(target, key, result);
85
+ return result;
86
+ };
87
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
88
+ var WebhookSubscription = class extends SmrtObject {
89
+ tenantId = "";
90
+ url = "";
91
+ events = [];
92
+ models = [];
93
+ secret = "";
94
+ enabled = true;
95
+ async save() {
96
+ assertWebhookUrl(this.url);
97
+ assertHmacSecret(this.secret);
98
+ for (const values of [this.events, this.models]) if (!Array.isArray(values) || values.length > 100 || values.some((v) => typeof v !== "string" || !v || v.length > 200)) throw new Error("Webhook filters must be lists of nonempty names.");
99
+ return super.save();
100
+ }
101
+ };
102
+ __publicField(WebhookSubscription, "__smrtPackage__", "@happyvertical/smrt-webhooks");
103
+ __decorateClass([tenantId()], WebhookSubscription.prototype, "tenantId", 2);
104
+ __decorateClass([field({
105
+ type: "text",
106
+ required: true
107
+ })], WebhookSubscription.prototype, "url", 2);
108
+ __decorateClass([field({ type: "json" })], WebhookSubscription.prototype, "events", 2);
109
+ __decorateClass([field({ type: "json" })], WebhookSubscription.prototype, "models", 2);
110
+ __decorateClass([field({
111
+ type: "text",
112
+ sensitive: true,
113
+ required: true
114
+ })], WebhookSubscription.prototype, "secret", 2);
115
+ __decorateClass([field({
116
+ type: "boolean",
117
+ default: true
118
+ })], WebhookSubscription.prototype, "enabled", 2);
119
+ WebhookSubscription = __decorateClass([smrt({
120
+ tableName: "_smrt_webhook_subscriptions",
121
+ api: false,
122
+ cli: false,
123
+ mcp: false
124
+ }), TenantScoped({ mode: "required" })], WebhookSubscription);
125
+ var WebhookSubscriptionCollection = class extends SmrtCollection {
126
+ static _itemClass = WebhookSubscription;
127
+ };
128
+ var WebhookDelivery = class extends SmrtObject {
129
+ tenantId = "";
130
+ subscriptionId = "";
131
+ eventId = "";
132
+ payload = "";
133
+ status = "pending";
134
+ generation = 0;
135
+ attempts = 0;
136
+ responseStatus = null;
137
+ error = null;
138
+ leaseToken = null;
139
+ leaseUntil = null;
140
+ };
141
+ __publicField(WebhookDelivery, "__smrtPackage__", "@happyvertical/smrt-webhooks");
142
+ __decorateClass([tenantId()], WebhookDelivery.prototype, "tenantId", 2);
143
+ __decorateClass([foreignKey(WebhookSubscription, {
144
+ onUpdate: "RESTRICT",
145
+ onDelete: "RESTRICT"
146
+ })], WebhookDelivery.prototype, "subscriptionId", 2);
147
+ __decorateClass([field({
148
+ type: "text",
149
+ required: true
150
+ })], WebhookDelivery.prototype, "eventId", 2);
151
+ __decorateClass([field({
152
+ type: "text",
153
+ required: true,
154
+ sensitive: true
155
+ })], WebhookDelivery.prototype, "payload", 2);
156
+ __decorateClass([field({
157
+ type: "text",
158
+ default: "pending"
159
+ })], WebhookDelivery.prototype, "status", 2);
160
+ __decorateClass([field({
161
+ type: "integer",
162
+ default: 0
163
+ })], WebhookDelivery.prototype, "generation", 2);
164
+ __decorateClass([field({
165
+ type: "integer",
166
+ default: 0
167
+ })], WebhookDelivery.prototype, "attempts", 2);
168
+ __decorateClass([field({
169
+ type: "integer",
170
+ nullable: true
171
+ })], WebhookDelivery.prototype, "responseStatus", 2);
172
+ __decorateClass([field({
173
+ type: "text",
174
+ nullable: true
175
+ })], WebhookDelivery.prototype, "error", 2);
176
+ __decorateClass([field({
177
+ type: "text",
178
+ nullable: true,
179
+ sensitive: true
180
+ })], WebhookDelivery.prototype, "leaseToken", 2);
181
+ __decorateClass([field({
182
+ type: "datetime",
183
+ nullable: true
184
+ })], WebhookDelivery.prototype, "leaseUntil", 2);
185
+ WebhookDelivery = __decorateClass([smrt({
186
+ tableName: "_smrt_webhook_deliveries",
187
+ api: false,
188
+ cli: false,
189
+ mcp: false,
190
+ indexes: [{
191
+ name: "_smrt_webhook_delivery_identity",
192
+ columns: [
193
+ "tenantId",
194
+ "subscriptionId",
195
+ "eventId"
196
+ ],
197
+ unique: true
198
+ }]
199
+ }), TenantScoped({ mode: "required" })], WebhookDelivery);
200
+ var WebhookDeliveryCollection = class extends SmrtCollection {
201
+ static _itemClass = WebhookDelivery;
202
+ };
203
+ var WebhookDeliveryTask = class extends SmrtObject {
204
+ async deliver(args = {}, _context) {
205
+ const { runWebhookDeliveryJob } = await import("./dispatcher-C5AqtgtK.js").then((n) => n.r);
206
+ await runWebhookDeliveryJob(args);
207
+ }
208
+ };
209
+ __publicField(WebhookDeliveryTask, "__smrtPackage__", "@happyvertical/smrt-webhooks");
210
+ __decorateClass([backgroundEligible()], WebhookDeliveryTask.prototype, "deliver", 1);
211
+ WebhookDeliveryTask = __decorateClass([smrt({
212
+ tableName: "_smrt_webhook_delivery_tasks",
213
+ api: false,
214
+ cli: false,
215
+ mcp: false
216
+ })], WebhookDeliveryTask);
217
+ var WebhookCursor = class extends SmrtObject {
218
+ tenantId = "";
219
+ runtime = "";
220
+ position = 0;
221
+ };
222
+ __publicField(WebhookCursor, "__smrtPackage__", "@happyvertical/smrt-webhooks");
223
+ __decorateClass([tenantId()], WebhookCursor.prototype, "tenantId", 2);
224
+ __decorateClass([field({
225
+ type: "text",
226
+ required: true
227
+ })], WebhookCursor.prototype, "runtime", 2);
228
+ __decorateClass([field({
229
+ type: "integer",
230
+ default: 0
231
+ })], WebhookCursor.prototype, "position", 2);
232
+ WebhookCursor = __decorateClass([smrt({
233
+ tableName: "_smrt_webhook_cursors",
234
+ api: false,
235
+ cli: false,
236
+ mcp: false,
237
+ indexes: [{
238
+ name: "_smrt_webhook_cursor_identity",
239
+ columns: ["tenantId", "runtime"],
240
+ unique: true
241
+ }]
242
+ }), TenantScoped({ mode: "required" })], WebhookCursor);
243
+ //#endregion
244
+ //#region src/admin.ts
245
+ function webhookAdminView(subscriptions, deliveries) {
246
+ return {
247
+ subscriptions: subscriptions.map((s) => ({
248
+ id: s.id,
249
+ tenantId: s.tenantId,
250
+ url: s.url,
251
+ models: s.models,
252
+ events: s.events,
253
+ enabled: s.enabled
254
+ })),
255
+ deliveries: deliveries.map((d) => ({
256
+ id: d.id,
257
+ tenantId: d.tenantId,
258
+ subscriptionId: d.subscriptionId,
259
+ eventId: d.eventId,
260
+ status: d.status,
261
+ attempts: d.attempts,
262
+ responseStatus: d.responseStatus,
263
+ error: d.error
264
+ }))
265
+ };
266
+ }
267
+ var WebhookAdminService = class {
268
+ constructor(store, authorize) {
269
+ this.store = store;
270
+ this.authorize = authorize;
271
+ }
272
+ store;
273
+ authorize;
274
+ async guard() {
275
+ requireTenant();
276
+ if (!await this.authorize()) throw new Error("Webhook administration forbidden.");
277
+ }
278
+ async view() {
279
+ await this.guard();
280
+ await this.store.reconcile();
281
+ const subscriptions = await this.store.subscriptions();
282
+ const deliveries = await this.store.deliveries();
283
+ return webhookAdminView(subscriptions.map((s) => ({
284
+ ...s,
285
+ id: s.id ?? ""
286
+ })), deliveries.map((d) => ({
287
+ ...d,
288
+ id: d.id ?? ""
289
+ })));
290
+ }
291
+ async create(input) {
292
+ await this.guard();
293
+ await (await (await WebhookSubscriptionCollection.create({ db: this.store.db })).create({
294
+ tenantId: requireTenant().tenantId,
295
+ url: input.url,
296
+ events: input.events,
297
+ models: input.models,
298
+ secret: input.secret,
299
+ enabled: true
300
+ })).save();
301
+ }
302
+ async toggle(id, enabled) {
303
+ await this.guard();
304
+ if (typeof enabled !== "boolean") throw new Error("Invalid enabled value.");
305
+ const sub = await this.store.subscription(id);
306
+ if (!sub) throw new Error("Webhook subscription not found.");
307
+ sub.enabled = enabled;
308
+ await sub.save();
309
+ }
310
+ async replay(id) {
311
+ await this.guard();
312
+ return this.store.replay(id);
313
+ }
314
+ };
315
+ //#endregion
316
+ export { WebhookDeliveryCollection as a, WebhookSubscriptionCollection as c, isPublicAddress as d, sendPublicWebhook as f, WebhookDelivery as i, assertHmacSecret as l, webhookAdminView as n, WebhookDeliveryTask as o, WebhookCursor as r, WebhookSubscription as s, WebhookAdminService as t, assertWebhookUrl as u };
317
+
318
+ //# sourceMappingURL=admin-413jve1I.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-413jve1I.js","names":[],"sources":["../../src/security.ts","../../src/models.ts","../../src/admin.ts"],"sourcesContent":["import { lookup } from 'node:dns/promises';\nimport { request } from 'node:https';\nimport { isIP } from 'node:net';\n\n/** Conservative public-unicast allowlist. Transition/mapped IPv6 is excluded. */\nexport function isPublicAddress(address: string): boolean {\n if (isIP(address) === 4) {\n const [a, b, c] = address.split('.').map(Number);\n return !(\n a === 0 ||\n a === 10 ||\n a === 127 ||\n a >= 224 ||\n (a === 100 && b >= 64 && b <= 127) ||\n (a === 169 && b === 254) ||\n (a === 172 && b >= 16 && b <= 31) ||\n (a === 192 && (b === 0 || b === 168 || (b === 88 && c === 99))) ||\n (a === 198 && (b === 18 || b === 19 || (b === 51 && c === 100))) ||\n (a === 203 && b === 0 && c === 113)\n );\n }\n if (isIP(address) !== 6) return false;\n const normalized = new URL(`https://[${address}]/`).hostname.slice(1, -1);\n // Only global unicast 2000::/3; exclude IETF special-purpose, documentation,\n // 6to4, and the newer documentation block (3fff::/20).\n const first = Number.parseInt(normalized.split(':')[0], 16);\n return (\n first >= 0x2000 &&\n first < 0x4000 &&\n first !== 0x2002 &&\n !(\n first === 0x2001 &&\n Number.parseInt(normalized.split(':')[1] || '0', 16) < 0x200\n ) &&\n !normalized.startsWith('2001:db8:') &&\n first !== 0x3fff\n );\n}\n\nexport function assertWebhookUrl(value: string): URL {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n throw new Error('Webhook URL must be an absolute HTTPS URL.');\n }\n const host = url.hostname.replace(/^\\[|\\]$/g, '');\n if (\n url.protocol !== 'https:' ||\n url.username ||\n url.password ||\n url.hash ||\n (url.port && url.port !== '443') ||\n host === 'localhost' ||\n host.endsWith('.localhost') ||\n (isIP(host) && !isPublicAddress(host))\n ) {\n throw new Error(\n 'Webhook URL must use public HTTPS on port 443 without credentials or a fragment.',\n );\n }\n return url;\n}\n\nexport function assertHmacSecret(secret: string): void {\n if (\n typeof secret !== 'string' ||\n Buffer.byteLength(secret) < 32 ||\n Buffer.byteLength(secret) > 4096\n ) {\n throw new Error('Webhook signing secret must contain 32 to 4096 bytes.');\n }\n}\n\nexport interface WebhookRequest {\n url: string;\n body: string;\n headers: Record<string, string>;\n}\nexport type WebhookTransport = (request: WebhookRequest) => Promise<number>;\n\n/** Validate every DNS answer, then pin the socket lookup; TLS still verifies the hostname.\n * No proxy, redirects, pooled sockets, response body buffering, or second DNS lookup.\n */\nexport const sendPublicWebhook: WebhookTransport = async ({\n url,\n body,\n headers,\n}) => {\n const target = assertWebhookUrl(url);\n const host = target.hostname.replace(/^\\[|\\]$/g, '');\n let dnsTimer: ReturnType<typeof setTimeout> | undefined;\n const answers = isIP(host)\n ? [{ address: host, family: isIP(host) }]\n : await Promise.race([\n lookup(host, { all: true, verbatim: true }),\n new Promise<never>((_, reject) => {\n dnsTimer = setTimeout(\n () => reject(new Error('Webhook DNS timeout.')),\n 5000,\n );\n dnsTimer.unref();\n }),\n ]).finally(() => clearTimeout(dnsTimer));\n if (\n !answers.length ||\n answers.some(({ address }) => !isPublicAddress(address))\n ) {\n throw new Error('Webhook destination is not public.');\n }\n const pinned = answers[0];\n return new Promise<number>((resolve, reject) => {\n const req = request(\n target,\n {\n method: 'POST',\n agent: false,\n headers: {\n ...headers,\n 'content-length': Buffer.byteLength(body).toString(),\n },\n lookup: (_hostname, options, callback) => {\n if (options.all) callback(null, [pinned]);\n else callback(null, pinned.address, pinned.family);\n },\n },\n (response) => {\n const status = response.statusCode ?? 0;\n response.destroy();\n resolve(status);\n },\n );\n const timer = setTimeout(\n () => req.destroy(new Error('Webhook request timeout.')),\n 10000,\n );\n req.on('close', () => clearTimeout(timer));\n req.on('error', () => reject(new Error('Webhook transport failed.')));\n req.end(body);\n });\n};\n","import {\n field,\n foreignKey,\n SmrtCollection,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport {\n backgroundEligible,\n type JobExecutionContext,\n} from '@happyvertical/smrt-jobs';\nimport { TenantScoped, tenantId } from '@happyvertical/smrt-tenancy';\nimport { assertHmacSecret, assertWebhookUrl } from './security.js';\n\n/** Server-only subscription. Hosts authorize administration before accessing it. */\n@smrt({\n tableName: '_smrt_webhook_subscriptions',\n api: false,\n cli: false,\n mcp: false,\n})\n@TenantScoped({ mode: 'required' })\nexport class WebhookSubscription extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-webhooks\";\n @tenantId() tenantId: string = '';\n @field({ type: 'text', required: true }) url: string = '';\n @field({ type: 'json' }) events: string[] = [];\n @field({ type: 'json' }) models: string[] = [];\n @field({ type: 'text', sensitive: true, required: true }) secret: string = '';\n @field({ type: 'boolean', default: true }) enabled: boolean = true;\n\n override async save(): Promise<this> {\n assertWebhookUrl(this.url);\n assertHmacSecret(this.secret);\n for (const values of [this.events, this.models]) {\n if (\n !Array.isArray(values) ||\n values.length > 100 ||\n values.some((v) => typeof v !== 'string' || !v || v.length > 200)\n ) {\n throw new Error('Webhook filters must be lists of nonempty names.');\n }\n }\n return super.save();\n }\n}\nexport class WebhookSubscriptionCollection extends SmrtCollection<WebhookSubscription> {\n static readonly _itemClass = WebhookSubscription;\n}\n\n/** Durable outbox and safe outcome metadata; payload never appears in public projections. */\n@smrt({\n tableName: '_smrt_webhook_deliveries',\n api: false,\n cli: false,\n mcp: false,\n indexes: [\n {\n name: '_smrt_webhook_delivery_identity',\n columns: ['tenantId', 'subscriptionId', 'eventId'],\n unique: true,\n },\n ],\n})\n@TenantScoped({ mode: 'required' })\nexport class WebhookDelivery extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-webhooks\";\n @tenantId() tenantId: string = '';\n @foreignKey(WebhookSubscription, {\n onUpdate: 'RESTRICT',\n onDelete: 'RESTRICT',\n })\n subscriptionId: string = '';\n @field({ type: 'text', required: true }) eventId: string = '';\n @field({ type: 'text', required: true, sensitive: true }) payload: string =\n '';\n @field({ type: 'text', default: 'pending' }) status: string = 'pending';\n @field({ type: 'integer', default: 0 }) generation: number = 0;\n @field({ type: 'integer', default: 0 }) attempts: number = 0;\n @field({ type: 'integer', nullable: true }) responseStatus: number | null =\n null;\n @field({ type: 'text', nullable: true }) error: string | null = null;\n @field({ type: 'text', nullable: true, sensitive: true }) leaseToken:\n | string\n | null = null;\n @field({ type: 'datetime', nullable: true }) leaseUntil: Date | null = null;\n}\nexport class WebhookDeliveryCollection extends SmrtCollection<WebhookDelivery> {\n static readonly _itemClass = WebhookDelivery;\n}\n\n/** Jobs queue entry point; persisted arguments contain IDs only. */\n@smrt({\n tableName: '_smrt_webhook_delivery_tasks',\n api: false,\n cli: false,\n mcp: false,\n})\nexport class WebhookDeliveryTask extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-webhooks\";\n @backgroundEligible()\n async deliver(\n args: Record<string, unknown> = {},\n _context?: JobExecutionContext,\n ): Promise<void> {\n const { runWebhookDeliveryJob } = await import('./dispatcher.js');\n await runWebhookDeliveryJob(args);\n }\n}\n\n/** Tenant-bound durable cursor for replayable model change-feed publication. */\n@smrt({\n tableName: '_smrt_webhook_cursors',\n api: false,\n cli: false,\n mcp: false,\n indexes: [\n {\n name: '_smrt_webhook_cursor_identity',\n columns: ['tenantId', 'runtime'],\n unique: true,\n },\n ],\n})\n@TenantScoped({ mode: 'required' })\nexport class WebhookCursor extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-webhooks\";\n @tenantId() tenantId: string = '';\n @field({ type: 'text', required: true }) runtime: string = '';\n @field({ type: 'integer', default: 0 }) position: number = 0;\n}\n","import { requireTenant } from '@happyvertical/smrt-tenancy';\nimport { WebhookSubscriptionCollection } from './models.js';\nimport type { WebhookStore } from './store.js';\nimport type {\n WebhookDeliveryRecord,\n WebhookSubscriptionRecord,\n} from './types.js';\nexport interface WebhookAdminView {\n subscriptions: WebhookSubscriptionRecord[];\n deliveries: WebhookDeliveryRecord[];\n}\n/** Allowlisted projection; never spread persistence objects into a browser response. */\nexport function webhookAdminView(\n subscriptions: WebhookSubscriptionRecord[],\n deliveries: WebhookDeliveryRecord[],\n): WebhookAdminView {\n return {\n subscriptions: subscriptions.map((s) => ({\n id: s.id,\n tenantId: s.tenantId,\n url: s.url,\n models: s.models,\n events: s.events,\n enabled: s.enabled,\n })),\n deliveries: deliveries.map((d) => ({\n id: d.id,\n tenantId: d.tenantId,\n subscriptionId: d.subscriptionId,\n eventId: d.eventId,\n status: d.status,\n attempts: d.attempts,\n responseStatus: d.responseStatus,\n error: d.error,\n })),\n };\n}\n/** Host supplies its administrator permission check and authenticated tenant context. */\nexport class WebhookAdminService {\n constructor(\n private readonly store: WebhookStore,\n private readonly authorize: () => Promise<boolean>,\n ) {}\n private async guard() {\n requireTenant();\n if (!(await this.authorize()))\n throw new Error('Webhook administration forbidden.');\n }\n async view(): Promise<WebhookAdminView> {\n await this.guard();\n await this.store.reconcile();\n const subscriptions = await this.store.subscriptions();\n const deliveries = await this.store.deliveries();\n return webhookAdminView(\n subscriptions.map((s) => ({ ...s, id: s.id ?? '' })),\n deliveries.map((d) => ({ ...d, id: d.id ?? '' })),\n );\n }\n async create(input: {\n url: string;\n events: string[];\n models: string[];\n secret: string;\n }): Promise<void> {\n await this.guard();\n const collection = await WebhookSubscriptionCollection.create({\n db: this.store.db,\n });\n const sub = await collection.create({\n tenantId: requireTenant().tenantId,\n url: input.url,\n events: input.events,\n models: input.models,\n secret: input.secret,\n enabled: true,\n });\n await sub.save();\n }\n async toggle(id: string, enabled: boolean): Promise<void> {\n await this.guard();\n if (typeof enabled !== 'boolean') throw new Error('Invalid enabled value.');\n const sub = await this.store.subscription(id);\n if (!sub) throw new Error('Webhook subscription not found.');\n sub.enabled = enabled;\n await sub.save();\n }\n async replay(id: string): Promise<boolean> {\n await this.guard();\n return this.store.replay(id);\n }\n}\n"],"mappings":"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"}