@xtandard/webhooks 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 +315 -0
- package/bin/xtandard-webhooks.mjs +3 -0
- package/dist/basic-BIW3Rvuz.cjs +199 -0
- package/dist/basic-BIW3Rvuz.cjs.map +1 -0
- package/dist/basic-DKk0Xfuu.mjs +176 -0
- package/dist/basic-DKk0Xfuu.mjs.map +1 -0
- package/dist/chunk-D7D4PA-g.mjs +13 -0
- package/dist/cli.cjs +655 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +42 -0
- package/dist/cli.d.mts +42 -0
- package/dist/cli.mjs +653 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/contract-8h-Azxa5.d.cts +71 -0
- package/dist/contract-9XpcwcCn.mjs +22 -0
- package/dist/contract-9XpcwcCn.mjs.map +1 -0
- package/dist/contract-B2d5dNU3.cjs +33 -0
- package/dist/contract-B2d5dNU3.cjs.map +1 -0
- package/dist/contract-BEhDcd_5.mjs +28 -0
- package/dist/contract-BEhDcd_5.mjs.map +1 -0
- package/dist/contract-Bf1qguwt.cjs +57 -0
- package/dist/contract-Bf1qguwt.cjs.map +1 -0
- package/dist/contract-Bnb3fgRJ.d.cts +177 -0
- package/dist/contract-C2r2Xzwp.d.mts +46 -0
- package/dist/contract-CiPskNvS.d.cts +46 -0
- package/dist/contract-DhQ4JjGG.d.mts +71 -0
- package/dist/contract-T1kcZNdG.d.mts +177 -0
- package/dist/contract-lETlIuXo.d.cts +30 -0
- package/dist/contract-lETlIuXo.d.mts +30 -0
- package/dist/core-CMpnmI5Q.mjs +1605 -0
- package/dist/core-CMpnmI5Q.mjs.map +1 -0
- package/dist/core-DT4ppWh8.d.mts +502 -0
- package/dist/core-KJawHjFF.d.cts +502 -0
- package/dist/core-ZGhH6Vs2.cjs +1790 -0
- package/dist/core-ZGhH6Vs2.cjs.map +1 -0
- package/dist/core.cjs +8 -0
- package/dist/core.d.cts +2 -0
- package/dist/core.d.mts +2 -0
- package/dist/core.mjs +2 -0
- package/dist/create-fetch-handler-BIdk9P30.mjs +1724 -0
- package/dist/create-fetch-handler-BIdk9P30.mjs.map +1 -0
- package/dist/create-fetch-handler-CmooujQo.cjs +1771 -0
- package/dist/create-fetch-handler-CmooujQo.cjs.map +1 -0
- package/dist/create-fetch-handler-Dlkhustu.d.cts +162 -0
- package/dist/create-fetch-handler-jy3hy5nZ.d.mts +162 -0
- package/dist/dispatcher-B0xTEHt1.cjs +212 -0
- package/dist/dispatcher-B0xTEHt1.cjs.map +1 -0
- package/dist/dispatcher-Coubwrka.mjs +196 -0
- package/dist/dispatcher-Coubwrka.mjs.map +1 -0
- package/dist/entry-auth-basic.cjs +5 -0
- package/dist/entry-auth-basic.d.cts +83 -0
- package/dist/entry-auth-basic.d.mts +83 -0
- package/dist/entry-auth-basic.mjs +2 -0
- package/dist/entry-auth-delegated.cjs +28 -0
- package/dist/entry-auth-delegated.cjs.map +1 -0
- package/dist/entry-auth-delegated.d.cts +36 -0
- package/dist/entry-auth-delegated.d.mts +36 -0
- package/dist/entry-auth-delegated.mjs +27 -0
- package/dist/entry-auth-delegated.mjs.map +1 -0
- package/dist/entry-auth-none.cjs +4 -0
- package/dist/entry-auth-none.d.cts +25 -0
- package/dist/entry-auth-none.d.mts +25 -0
- package/dist/entry-auth-none.mjs +2 -0
- package/dist/entry-authorization-delegated.cjs +27 -0
- package/dist/entry-authorization-delegated.cjs.map +1 -0
- package/dist/entry-authorization-delegated.d.cts +31 -0
- package/dist/entry-authorization-delegated.d.mts +31 -0
- package/dist/entry-authorization-delegated.mjs +26 -0
- package/dist/entry-authorization-delegated.mjs.map +1 -0
- package/dist/entry-authorization-none.cjs +3 -0
- package/dist/entry-authorization-none.d.cts +18 -0
- package/dist/entry-authorization-none.d.mts +18 -0
- package/dist/entry-authorization-none.mjs +2 -0
- package/dist/entry-authorization-roles.cjs +6 -0
- package/dist/entry-authorization-roles.d.cts +65 -0
- package/dist/entry-authorization-roles.d.mts +65 -0
- package/dist/entry-authorization-roles.mjs +2 -0
- package/dist/entry-bun.cjs +24 -0
- package/dist/entry-bun.cjs.map +1 -0
- package/dist/entry-bun.d.cts +8 -0
- package/dist/entry-bun.d.mts +8 -0
- package/dist/entry-bun.mjs +23 -0
- package/dist/entry-bun.mjs.map +1 -0
- package/dist/entry-drizzle-mysql.cjs +20 -0
- package/dist/entry-drizzle-mysql.cjs.map +1 -0
- package/dist/entry-drizzle-mysql.d.cts +27 -0
- package/dist/entry-drizzle-mysql.d.mts +27 -0
- package/dist/entry-drizzle-mysql.mjs +19 -0
- package/dist/entry-drizzle-mysql.mjs.map +1 -0
- package/dist/entry-drizzle-pg.cjs +21 -0
- package/dist/entry-drizzle-pg.cjs.map +1 -0
- package/dist/entry-drizzle-pg.d.cts +26 -0
- package/dist/entry-drizzle-pg.d.mts +26 -0
- package/dist/entry-drizzle-pg.mjs +20 -0
- package/dist/entry-drizzle-pg.mjs.map +1 -0
- package/dist/entry-drizzle-sqlite.cjs +21 -0
- package/dist/entry-drizzle-sqlite.cjs.map +1 -0
- package/dist/entry-drizzle-sqlite.d.cts +23 -0
- package/dist/entry-drizzle-sqlite.d.mts +23 -0
- package/dist/entry-drizzle-sqlite.mjs +20 -0
- package/dist/entry-drizzle-sqlite.mjs.map +1 -0
- package/dist/entry-elysia.cjs +125 -0
- package/dist/entry-elysia.cjs.map +1 -0
- package/dist/entry-elysia.d.cts +1017 -0
- package/dist/entry-elysia.d.mts +1017 -0
- package/dist/entry-elysia.mjs +123 -0
- package/dist/entry-elysia.mjs.map +1 -0
- package/dist/entry-express.cjs +57 -0
- package/dist/entry-express.cjs.map +1 -0
- package/dist/entry-express.d.cts +15 -0
- package/dist/entry-express.d.mts +15 -0
- package/dist/entry-express.mjs +56 -0
- package/dist/entry-express.mjs.map +1 -0
- package/dist/entry-hono.cjs +35 -0
- package/dist/entry-hono.cjs.map +1 -0
- package/dist/entry-hono.d.cts +16 -0
- package/dist/entry-hono.d.mts +16 -0
- package/dist/entry-hono.mjs +34 -0
- package/dist/entry-hono.mjs.map +1 -0
- package/dist/entry-hooks-log.cjs +22 -0
- package/dist/entry-hooks-log.cjs.map +1 -0
- package/dist/entry-hooks-log.d.cts +23 -0
- package/dist/entry-hooks-log.d.mts +23 -0
- package/dist/entry-hooks-log.mjs +21 -0
- package/dist/entry-hooks-log.mjs.map +1 -0
- package/dist/entry-storage-cloudflare-kv.cjs +47 -0
- package/dist/entry-storage-cloudflare-kv.cjs.map +1 -0
- package/dist/entry-storage-cloudflare-kv.d.cts +42 -0
- package/dist/entry-storage-cloudflare-kv.d.mts +42 -0
- package/dist/entry-storage-cloudflare-kv.mjs +46 -0
- package/dist/entry-storage-cloudflare-kv.mjs.map +1 -0
- package/dist/entry-storage-drizzle.cjs +78 -0
- package/dist/entry-storage-drizzle.cjs.map +1 -0
- package/dist/entry-storage-drizzle.d.cts +30 -0
- package/dist/entry-storage-drizzle.d.mts +30 -0
- package/dist/entry-storage-drizzle.mjs +77 -0
- package/dist/entry-storage-drizzle.mjs.map +1 -0
- package/dist/entry-storage-file.cjs +4 -0
- package/dist/entry-storage-file.d.cts +30 -0
- package/dist/entry-storage-file.d.mts +30 -0
- package/dist/entry-storage-file.mjs +2 -0
- package/dist/entry-storage-libsql.cjs +3 -0
- package/dist/entry-storage-libsql.d.cts +48 -0
- package/dist/entry-storage-libsql.d.mts +48 -0
- package/dist/entry-storage-libsql.mjs +2 -0
- package/dist/entry-storage-memory.cjs +3 -0
- package/dist/entry-storage-memory.d.cts +2 -0
- package/dist/entry-storage-memory.d.mts +2 -0
- package/dist/entry-storage-memory.mjs +2 -0
- package/dist/entry-storage-mongodb.cjs +3 -0
- package/dist/entry-storage-mongodb.d.cts +55 -0
- package/dist/entry-storage-mongodb.d.mts +55 -0
- package/dist/entry-storage-mongodb.mjs +2 -0
- package/dist/entry-storage-postgres.cjs +3 -0
- package/dist/entry-storage-postgres.d.cts +62 -0
- package/dist/entry-storage-postgres.d.mts +62 -0
- package/dist/entry-storage-postgres.mjs +2 -0
- package/dist/entry-storage-redis.cjs +4 -0
- package/dist/entry-storage-redis.d.cts +77 -0
- package/dist/entry-storage-redis.d.mts +77 -0
- package/dist/entry-storage-redis.mjs +2 -0
- package/dist/entry-storage-sqlite.cjs +3 -0
- package/dist/entry-storage-sqlite.d.cts +36 -0
- package/dist/entry-storage-sqlite.d.mts +36 -0
- package/dist/entry-storage-sqlite.mjs +2 -0
- package/dist/entry-storage-unstorage.cjs +42 -0
- package/dist/entry-storage-unstorage.cjs.map +1 -0
- package/dist/entry-storage-unstorage.d.cts +29 -0
- package/dist/entry-storage-unstorage.d.mts +29 -0
- package/dist/entry-storage-unstorage.mjs +41 -0
- package/dist/entry-storage-unstorage.mjs.map +1 -0
- package/dist/file-COBYZA4Q.cjs +148 -0
- package/dist/file-COBYZA4Q.cjs.map +1 -0
- package/dist/file-fi02eFHk.mjs +131 -0
- package/dist/file-fi02eFHk.mjs.map +1 -0
- package/dist/index.cjs +123 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +368 -0
- package/dist/index.d.mts +366 -0
- package/dist/index.mjs +61 -0
- package/dist/index.mjs.map +1 -0
- package/dist/keys-Byyj4quQ.mjs +111 -0
- package/dist/keys-Byyj4quQ.mjs.map +1 -0
- package/dist/keys-FiKpaVHX.cjs +302 -0
- package/dist/keys-FiKpaVHX.cjs.map +1 -0
- package/dist/libsql-bpVi0bXN.mjs +113 -0
- package/dist/libsql-bpVi0bXN.mjs.map +1 -0
- package/dist/libsql-pPJEo1e4.cjs +124 -0
- package/dist/libsql-pPJEo1e4.cjs.map +1 -0
- package/dist/memory-8Ef-PL5a.cjs +137 -0
- package/dist/memory-8Ef-PL5a.cjs.map +1 -0
- package/dist/memory-BMsSSwqn.mjs +127 -0
- package/dist/memory-BMsSSwqn.mjs.map +1 -0
- package/dist/memory-FnMJWCmB.d.cts +28 -0
- package/dist/memory-qIvANEs_.d.mts +28 -0
- package/dist/mongodb-Cy8yo0uk.cjs +108 -0
- package/dist/mongodb-Cy8yo0uk.cjs.map +1 -0
- package/dist/mongodb-Ddaq9mml.mjs +97 -0
- package/dist/mongodb-Ddaq9mml.mjs.map +1 -0
- package/dist/none-BnZtaGNJ.mjs +23 -0
- package/dist/none-BnZtaGNJ.mjs.map +1 -0
- package/dist/none-CAsxCOWN.cjs +49 -0
- package/dist/none-CAsxCOWN.cjs.map +1 -0
- package/dist/none-CZVrfnmF.cjs +33 -0
- package/dist/none-CZVrfnmF.cjs.map +1 -0
- package/dist/none-GhVIoh_s.mjs +33 -0
- package/dist/none-GhVIoh_s.mjs.map +1 -0
- package/dist/postgres-C8WbchFa.cjs +134 -0
- package/dist/postgres-C8WbchFa.cjs.map +1 -0
- package/dist/postgres-c3pAhmhr.mjs +123 -0
- package/dist/postgres-c3pAhmhr.mjs.map +1 -0
- package/dist/react.css +1 -0
- package/dist/react.js +31465 -0
- package/dist/receiver.cjs +43 -0
- package/dist/receiver.cjs.map +1 -0
- package/dist/receiver.d.cts +36 -0
- package/dist/receiver.d.mts +36 -0
- package/dist/receiver.mjs +40 -0
- package/dist/receiver.mjs.map +1 -0
- package/dist/redis-CFJkuSgB.cjs +270 -0
- package/dist/redis-CFJkuSgB.cjs.map +1 -0
- package/dist/redis-CvLi0KF7.mjs +254 -0
- package/dist/redis-CvLi0KF7.mjs.map +1 -0
- package/dist/roles-D0G9XqBq.cjs +128 -0
- package/dist/roles-D0G9XqBq.cjs.map +1 -0
- package/dist/roles-vp361lTk.mjs +99 -0
- package/dist/roles-vp361lTk.mjs.map +1 -0
- package/dist/schema-mo__wv4P.d.cts +233 -0
- package/dist/schema-mo__wv4P.d.mts +233 -0
- package/dist/schema.cjs +13 -0
- package/dist/schema.cjs.map +1 -0
- package/dist/schema.d.cts +2 -0
- package/dist/schema.d.mts +2 -0
- package/dist/schema.mjs +11 -0
- package/dist/schema.mjs.map +1 -0
- package/dist/signing.cjs +162 -0
- package/dist/signing.cjs.map +1 -0
- package/dist/signing.d.cts +73 -0
- package/dist/signing.d.mts +73 -0
- package/dist/signing.mjs +156 -0
- package/dist/signing.mjs.map +1 -0
- package/dist/sqlite-Cmqnrjes.mjs +67 -0
- package/dist/sqlite-Cmqnrjes.mjs.map +1 -0
- package/dist/sqlite-Dcufk0x3.cjs +78 -0
- package/dist/sqlite-Dcufk0x3.cjs.map +1 -0
- package/dist/table-Ce3Tzwqs.d.cts +11 -0
- package/dist/table-Ce3Tzwqs.d.mts +11 -0
- package/dist/testing.cjs +134 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +80 -0
- package/dist/testing.d.mts +80 -0
- package/dist/testing.mjs +131 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types-react/react.d.ts +98 -0
- package/dist/types-react/schema.d.ts +229 -0
- package/dist/types-react/ui/App.d.ts +22 -0
- package/dist/types-react/ui/api.d.ts +97 -0
- package/dist/types-react/ui/components/JsonCodeEditor.d.ts +12 -0
- package/dist/types-react/ui/components/ThemeToggle.d.ts +2 -0
- package/dist/types-react/ui/components/Toast.d.ts +16 -0
- package/dist/types-react/ui/components/primitives.d.ts +50 -0
- package/dist/types-react/ui/components/ui-bits.d.ts +22 -0
- package/dist/types-react/ui/components/webhook-bits.d.ts +51 -0
- package/dist/types-react/ui/lib/format.d.ts +39 -0
- package/dist/types-react/ui/lib/nav-guard.d.ts +20 -0
- package/dist/types-react/ui/lib/utils.d.ts +3 -0
- package/dist/types-react/ui/theme.d.ts +12 -0
- package/dist/types-react/ui/types.d.ts +80 -0
- package/dist/types-react/ui/views/AuditView.d.ts +6 -0
- package/dist/types-react/ui/views/DeliveriesView.d.ts +12 -0
- package/dist/types-react/ui/views/EndpointsView.d.ts +11 -0
- package/dist/types-react/ui/views/EventTypesView.d.ts +11 -0
- package/dist/types-react/ui/views/MessagesView.d.ts +10 -0
- package/dist/types-react/ui/views/OverviewView.d.ts +12 -0
- package/dist/ui/assets/index-B0eoQX2U.css +1 -0
- package/dist/ui/assets/index-S5t_CLOe.js +209 -0
- package/dist/ui/index.html +14 -0
- package/package.json +487 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const require_keys = require("./keys-FiKpaVHX.cjs");
|
|
2
|
+
let bun_sqlite = require("bun:sqlite");
|
|
3
|
+
//#region src/storage/sqlite.ts
|
|
4
|
+
/**
|
|
5
|
+
* SQLite storage adapter built on `bun:sqlite` — **Bun only**, zero npm deps.
|
|
6
|
+
* Ideal for single-node deployments and local/dev persistence: durable, fast, and
|
|
7
|
+
* file-backed (or in-memory). For multi-node runtimes prefer Redis/Postgres.
|
|
8
|
+
*
|
|
9
|
+
* `bun:sqlite` is marked external at build time, so this module only resolves
|
|
10
|
+
* under the Bun runtime. Importing it under Node will throw — by design.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { createSqliteStorage } from "@xtandard/webhooks/storage/sqlite";
|
|
14
|
+
* const storage = createSqliteStorage({ path: "./webhooks.sqlite" });
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
var sqlite_exports = /* @__PURE__ */ require_keys.__exportAll({ createSqliteStorage: () => createSqliteStorage });
|
|
20
|
+
const escapeLike = (prefix) => prefix.replace(/[\\%_]/g, (c) => `\\${c}`);
|
|
21
|
+
/**
|
|
22
|
+
* Create a SQLite-backed storage. Requires the Bun runtime (`bun:sqlite`).
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { createSqliteStorage } from "@xtandard/webhooks/storage/sqlite";
|
|
27
|
+
*
|
|
28
|
+
* // File-backed (persists across restarts):
|
|
29
|
+
* const storage = createSqliteStorage({ path: "./webhooks.sqlite" });
|
|
30
|
+
*
|
|
31
|
+
* // In-memory (reset each run, useful for tests):
|
|
32
|
+
* // const storage = createSqliteStorage({ path: ":memory:" });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
function createSqliteStorage(options = {}) {
|
|
36
|
+
const table = options.table ?? "xtandard_webhooks";
|
|
37
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) throw new Error(`Invalid table name: ${JSON.stringify(table)}`);
|
|
38
|
+
const ownsDb = !options.database;
|
|
39
|
+
const db = options.database ?? new bun_sqlite.Database(options.path ?? ":memory:");
|
|
40
|
+
db.run(`CREATE TABLE IF NOT EXISTS ${table} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`);
|
|
41
|
+
const selectStmt = db.query(`SELECT value FROM ${table} WHERE key = ?`);
|
|
42
|
+
const upsertStmt = db.query(`INSERT INTO ${table} (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`);
|
|
43
|
+
const deleteStmt = db.query(`DELETE FROM ${table} WHERE key = ?`);
|
|
44
|
+
const keysStmt = db.query(`SELECT key FROM ${table} WHERE key LIKE ? ESCAPE '\\'`);
|
|
45
|
+
return {
|
|
46
|
+
async getItem(key) {
|
|
47
|
+
const row = selectStmt.get(key);
|
|
48
|
+
return row ? JSON.parse(row.value) : null;
|
|
49
|
+
},
|
|
50
|
+
async setItem(key, value) {
|
|
51
|
+
upsertStmt.run(key, JSON.stringify(value));
|
|
52
|
+
},
|
|
53
|
+
async removeItem(key) {
|
|
54
|
+
deleteStmt.run(key);
|
|
55
|
+
},
|
|
56
|
+
async getKeys(prefix) {
|
|
57
|
+
return keysStmt.all(`${escapeLike(prefix)}%`).map((r) => r.key);
|
|
58
|
+
},
|
|
59
|
+
close() {
|
|
60
|
+
if (ownsDb) db.close();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
Object.defineProperty(exports, "createSqliteStorage", {
|
|
66
|
+
enumerable: true,
|
|
67
|
+
get: function() {
|
|
68
|
+
return createSqliteStorage;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
Object.defineProperty(exports, "sqlite_exports", {
|
|
72
|
+
enumerable: true,
|
|
73
|
+
get: function() {
|
|
74
|
+
return sqlite_exports;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
//# sourceMappingURL=sqlite-Dcufk0x3.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-Dcufk0x3.cjs","names":["Database"],"sources":["../src/storage/sqlite.ts"],"sourcesContent":["/**\n * SQLite storage adapter built on `bun:sqlite` — **Bun only**, zero npm deps.\n * Ideal for single-node deployments and local/dev persistence: durable, fast, and\n * file-backed (or in-memory). For multi-node runtimes prefer Redis/Postgres.\n *\n * `bun:sqlite` is marked external at build time, so this module only resolves\n * under the Bun runtime. Importing it under Node will throw — by design.\n *\n * ```ts\n * import { createSqliteStorage } from \"@xtandard/webhooks/storage/sqlite\";\n * const storage = createSqliteStorage({ path: \"./webhooks.sqlite\" });\n * ```\n *\n * @module\n */\n\nimport { Database } from \"bun:sqlite\";\nimport type { WebhooksStorage } from \"./contract.ts\";\n\n/** Options for {@link createSqliteStorage}. */\nexport interface SqliteStorageOptions {\n /** File path for the database. Default `\":memory:\"`. Ignored when `database` is given. */\n path?: string;\n /** An existing `bun:sqlite` Database instance to use instead of opening one. */\n database?: Database;\n /** Table name (default `\"xtandard_webhooks\"`). Validated as a safe identifier. */\n table?: string;\n}\n\n/** A {@link WebhooksStorage} backed by SQLite, plus `close()`. */\nexport interface SqliteWebhooksStorage extends WebhooksStorage {\n /** Close the database if this adapter opened it; no-op for a borrowed instance. */\n close(): void;\n}\n\nconst escapeLike = (prefix: string): string => prefix.replace(/[\\\\%_]/g, (c) => `\\\\${c}`);\n\n/**\n * Create a SQLite-backed storage. Requires the Bun runtime (`bun:sqlite`).\n *\n * @example\n * ```ts\n * import { createSqliteStorage } from \"@xtandard/webhooks/storage/sqlite\";\n *\n * // File-backed (persists across restarts):\n * const storage = createSqliteStorage({ path: \"./webhooks.sqlite\" });\n *\n * // In-memory (reset each run, useful for tests):\n * // const storage = createSqliteStorage({ path: \":memory:\" });\n * ```\n */\nexport function createSqliteStorage(options: SqliteStorageOptions = {}): SqliteWebhooksStorage {\n const table = options.table ?? \"xtandard_webhooks\";\n if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) {\n throw new Error(`Invalid table name: ${JSON.stringify(table)}`);\n }\n\n const ownsDb = !options.database;\n const db = options.database ?? new Database(options.path ?? \":memory:\");\n db.run(`CREATE TABLE IF NOT EXISTS ${table} (key TEXT PRIMARY KEY, value TEXT NOT NULL)`);\n\n const selectStmt = db.query(`SELECT value FROM ${table} WHERE key = ?`);\n const upsertStmt = db.query(\n `INSERT INTO ${table} (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`,\n );\n const deleteStmt = db.query(`DELETE FROM ${table} WHERE key = ?`);\n const keysStmt = db.query(`SELECT key FROM ${table} WHERE key LIKE ? ESCAPE '\\\\'`);\n\n return {\n async getItem<T>(key: string): Promise<T | null> {\n const row = selectStmt.get(key) as { value: string } | null;\n return row ? (JSON.parse(row.value) as T) : null;\n },\n async setItem<T>(key: string, value: T): Promise<void> {\n upsertStmt.run(key, JSON.stringify(value));\n },\n async removeItem(key: string): Promise<void> {\n deleteStmt.run(key);\n },\n async getKeys(prefix: string): Promise<string[]> {\n const rows = keysStmt.all(`${escapeLike(prefix)}%`) as Array<{ key: string }>;\n return rows.map((r) => r.key);\n },\n close(): void {\n if (ownsDb) db.close();\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmCA,MAAM,cAAc,WAA2B,OAAO,QAAQ,YAAY,MAAM,KAAK,GAAG;;;;;;;;;;;;;;;AAgBxF,SAAgB,oBAAoB,UAAgC,CAAC,GAA0B;CAC7F,MAAM,QAAQ,QAAQ,SAAS;CAC/B,IAAI,CAAC,2BAA2B,KAAK,KAAK,GACxC,MAAM,IAAI,MAAM,uBAAuB,KAAK,UAAU,KAAK,GAAG;CAGhE,MAAM,SAAS,CAAC,QAAQ;CACxB,MAAM,KAAK,QAAQ,YAAY,IAAIA,WAAAA,SAAS,QAAQ,QAAQ,UAAU;CACtE,GAAG,IAAI,8BAA8B,MAAM,6CAA6C;CAExF,MAAM,aAAa,GAAG,MAAM,qBAAqB,MAAM,eAAe;CACtE,MAAM,aAAa,GAAG,MACpB,eAAe,MAAM,kFACvB;CACA,MAAM,aAAa,GAAG,MAAM,eAAe,MAAM,eAAe;CAChE,MAAM,WAAW,GAAG,MAAM,mBAAmB,MAAM,8BAA8B;CAEjF,OAAO;EACL,MAAM,QAAW,KAAgC;GAC/C,MAAM,MAAM,WAAW,IAAI,GAAG;GAC9B,OAAO,MAAO,KAAK,MAAM,IAAI,KAAK,IAAU;EAC9C;EACA,MAAM,QAAW,KAAa,OAAyB;GACrD,WAAW,IAAI,KAAK,KAAK,UAAU,KAAK,CAAC;EAC3C;EACA,MAAM,WAAW,KAA4B;GAC3C,WAAW,IAAI,GAAG;EACpB;EACA,MAAM,QAAQ,QAAmC;GAE/C,OADa,SAAS,IAAI,GAAG,WAAW,MAAM,EAAE,EACtC,EAAE,KAAK,MAAM,EAAE,GAAG;EAC9B;EACA,QAAc;GACZ,IAAI,QAAQ,GAAG,MAAM;EACvB;CACF;AACF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Column, Table } from "drizzle-orm";
|
|
2
|
+
|
|
3
|
+
//#region src/drizzle/table.d.ts
|
|
4
|
+
/** A table with the fixed `key`/`value` columns the webhooks KV store requires. */
|
|
5
|
+
type DrizzleKvTable = Table & {
|
|
6
|
+
key: Column;
|
|
7
|
+
value: Column;
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
10
|
+
export { DrizzleKvTable as t };
|
|
11
|
+
//# sourceMappingURL=table-Ce3Tzwqs.d.cts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Column, Table } from "drizzle-orm";
|
|
2
|
+
|
|
3
|
+
//#region src/drizzle/table.d.ts
|
|
4
|
+
/** A table with the fixed `key`/`value` columns the webhooks KV store requires. */
|
|
5
|
+
type DrizzleKvTable = Table & {
|
|
6
|
+
key: Column;
|
|
7
|
+
value: Column;
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
10
|
+
export { DrizzleKvTable as t };
|
|
11
|
+
//# sourceMappingURL=table-Ce3Tzwqs.d.mts.map
|
package/dist/testing.cjs
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_signing = require("./signing.cjs");
|
|
3
|
+
const require_core = require("./core-ZGhH6Vs2.cjs");
|
|
4
|
+
const require_dispatcher = require("./dispatcher-B0xTEHt1.cjs");
|
|
5
|
+
const require_memory = require("./memory-8Ef-PL5a.cjs");
|
|
6
|
+
let node_http = require("node:http");
|
|
7
|
+
//#region src/testing.ts
|
|
8
|
+
/**
|
|
9
|
+
* Testing utilities — `@xtandard/webhooks/testing`.
|
|
10
|
+
*
|
|
11
|
+
* Everything a host app (or this package's own suite) needs to test its
|
|
12
|
+
* webhook wiring without the network or timers:
|
|
13
|
+
*
|
|
14
|
+
* - {@link createTestWebhooks}: an in-memory core + a **not-started** dispatcher,
|
|
15
|
+
* driven manually via `dispatcher.tick()` / {@link drainDeliveries}.
|
|
16
|
+
* - {@link createTestReceiver}: a real local HTTP server that records (and,
|
|
17
|
+
* given the secret, verifies) every delivery it receives — including
|
|
18
|
+
* programmable failures to exercise the retry path.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* An in-memory core + dispatcher for tests. The dispatcher is **not started**
|
|
24
|
+
* (no timers) and defaults to an immediate three-attempt retry schedule so
|
|
25
|
+
* failure paths drain in a few ticks.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* import { createTestWebhooks, drainDeliveries } from "@xtandard/webhooks/testing";
|
|
30
|
+
*
|
|
31
|
+
* const { core, dispatcher } = createTestWebhooks();
|
|
32
|
+
* await core.createApplication({ key: "acme" });
|
|
33
|
+
* // … create an endpoint, publish, then:
|
|
34
|
+
* await drainDeliveries(dispatcher);
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
function createTestWebhooks(options = {}) {
|
|
38
|
+
const storage = require_memory.createMemoryStorage();
|
|
39
|
+
const { dispatcher: dispatcherOptions, ...coreOptions } = options;
|
|
40
|
+
const core = require_core.createWebhooksCore({
|
|
41
|
+
storage,
|
|
42
|
+
allowInsecureUrls: true,
|
|
43
|
+
...coreOptions,
|
|
44
|
+
dispatcher: {
|
|
45
|
+
retrySchedule: [
|
|
46
|
+
"0s",
|
|
47
|
+
"0s",
|
|
48
|
+
"0s"
|
|
49
|
+
],
|
|
50
|
+
...dispatcherOptions
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
core,
|
|
55
|
+
storage,
|
|
56
|
+
dispatcher: require_dispatcher.createDispatcher(core)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Start a real local HTTP server that plays the receiving side of a webhook.
|
|
61
|
+
* Works under Bun and Node.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* const receiver = await createTestReceiver({ secret, failFirst: 2 });
|
|
66
|
+
* await core.createEndpoint("acme", { url: receiver.url });
|
|
67
|
+
* // … publish + drain; receiver.received now holds the verified envelopes.
|
|
68
|
+
* await receiver.close();
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
async function createTestReceiver(options = {}) {
|
|
72
|
+
const received = [];
|
|
73
|
+
const requests = [];
|
|
74
|
+
let remainingFailures = options.failFirst ?? 0;
|
|
75
|
+
const server = (0, node_http.createServer)((req, res) => {
|
|
76
|
+
const chunks = [];
|
|
77
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
78
|
+
req.on("end", () => {
|
|
79
|
+
(async () => {
|
|
80
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
81
|
+
const headers = {};
|
|
82
|
+
for (const [name, value] of Object.entries(req.headers)) if (typeof value === "string") headers[name] = value;
|
|
83
|
+
requests.push({
|
|
84
|
+
headers,
|
|
85
|
+
body
|
|
86
|
+
});
|
|
87
|
+
if (remainingFailures > 0) {
|
|
88
|
+
remainingFailures--;
|
|
89
|
+
res.writeHead(options.status ?? 500, { "content-type": "text/plain" });
|
|
90
|
+
res.end("simulated failure");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (options.secret) try {
|
|
94
|
+
received.push(await require_signing.verify({
|
|
95
|
+
payload: body,
|
|
96
|
+
headers,
|
|
97
|
+
secret: options.secret
|
|
98
|
+
}));
|
|
99
|
+
} catch {
|
|
100
|
+
res.writeHead(401, { "content-type": "text/plain" });
|
|
101
|
+
res.end("invalid signature");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
res.writeHead(200, { "content-type": "text/plain" });
|
|
105
|
+
res.end("ok");
|
|
106
|
+
})();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
110
|
+
const address = server.address();
|
|
111
|
+
if (address === null || typeof address === "string") throw new Error("Test receiver failed to bind a port");
|
|
112
|
+
return {
|
|
113
|
+
url: `http://127.0.0.1:${address.port}/webhooks`,
|
|
114
|
+
received,
|
|
115
|
+
requests,
|
|
116
|
+
close: () => new Promise((resolve, reject) => {
|
|
117
|
+
server.close((err) => err ? reject(err) : resolve());
|
|
118
|
+
})
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Tick the dispatcher until a pass makes no attempts (or `maxTicks` is hit).
|
|
123
|
+
* Pair with an all-immediate retry schedule ({@link createTestWebhooks}'s
|
|
124
|
+
* default) so failed deliveries are due again on the very next tick.
|
|
125
|
+
*/
|
|
126
|
+
async function drainDeliveries(dispatcher, maxTicks = 25) {
|
|
127
|
+
for (let i = 0; i < maxTicks; i++) if (await dispatcher.tick() === 0) return;
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
exports.createTestReceiver = createTestReceiver;
|
|
131
|
+
exports.createTestWebhooks = createTestWebhooks;
|
|
132
|
+
exports.drainDeliveries = drainDeliveries;
|
|
133
|
+
|
|
134
|
+
//# sourceMappingURL=testing.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.cjs","names":["createMemoryStorage","createWebhooksCore","createDispatcher","verify"],"sources":["../src/testing.ts"],"sourcesContent":["/**\n * Testing utilities — `@xtandard/webhooks/testing`.\n *\n * Everything a host app (or this package's own suite) needs to test its\n * webhook wiring without the network or timers:\n *\n * - {@link createTestWebhooks}: an in-memory core + a **not-started** dispatcher,\n * driven manually via `dispatcher.tick()` / {@link drainDeliveries}.\n * - {@link createTestReceiver}: a real local HTTP server that records (and,\n * given the secret, verifies) every delivery it receives — including\n * programmable failures to exercise the retry path.\n *\n * @module\n */\n\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from \"node:http\";\nimport { createWebhooksCore, type WebhooksCore, type WebhooksCoreOptions } from \"./core.ts\";\nimport { createDispatcher, type Dispatcher, type DispatcherOptions } from \"./dispatcher.ts\";\nimport type { WebhookEnvelope } from \"./schema.ts\";\nimport { verify } from \"./signing.ts\";\nimport { createMemoryStorage, type MemoryWebhooksStorage } from \"./storage/memory.ts\";\n\n/** Options for {@link createTestWebhooks}. */\nexport interface TestWebhooksOptions extends Omit<WebhooksCoreOptions, \"storage\" | \"queueStorage\"> {\n /** Dispatcher config; a fast all-immediate retry schedule is the default. */\n dispatcher?: DispatcherOptions;\n}\n\n/** Result of {@link createTestWebhooks}. */\nexport interface TestWebhooks {\n core: WebhooksCore;\n storage: MemoryWebhooksStorage;\n /** NOT started — drive it with `tick()` or {@link drainDeliveries}. */\n dispatcher: Dispatcher;\n}\n\n/**\n * An in-memory core + dispatcher for tests. The dispatcher is **not started**\n * (no timers) and defaults to an immediate three-attempt retry schedule so\n * failure paths drain in a few ticks.\n *\n * @example\n * ```ts\n * import { createTestWebhooks, drainDeliveries } from \"@xtandard/webhooks/testing\";\n *\n * const { core, dispatcher } = createTestWebhooks();\n * await core.createApplication({ key: \"acme\" });\n * // … create an endpoint, publish, then:\n * await drainDeliveries(dispatcher);\n * ```\n */\nexport function createTestWebhooks(options: TestWebhooksOptions = {}): TestWebhooks {\n const storage = createMemoryStorage();\n const { dispatcher: dispatcherOptions, ...coreOptions } = options;\n const core = createWebhooksCore({\n storage,\n allowInsecureUrls: true, // test receivers are local http\n ...coreOptions,\n dispatcher: {\n retrySchedule: [\"0s\", \"0s\", \"0s\"],\n ...dispatcherOptions,\n },\n });\n const dispatcher = createDispatcher(core);\n return { core, storage, dispatcher };\n}\n\n/** Options for {@link createTestReceiver}. */\nexport interface TestReceiverOptions {\n /**\n * When set, each request is verified against this secret and its parsed\n * envelope lands in `received`; an invalid signature answers 401.\n */\n secret?: string;\n /** Fail (with `status`) the first N requests — exercises the retry path. */\n failFirst?: number;\n /** The failure status used while failing. Default `500`. */\n status?: number;\n}\n\n/** A running test receiver. */\nexport interface TestReceiver {\n /** The local URL to register as an endpoint. */\n url: string;\n /** Verified envelopes (only populated when `secret` was provided). */\n received: WebhookEnvelope[];\n /** Every request, verified or not, in arrival order. */\n requests: { headers: Record<string, string>; body: string }[];\n close(): Promise<void>;\n}\n\n/**\n * Start a real local HTTP server that plays the receiving side of a webhook.\n * Works under Bun and Node.\n *\n * @example\n * ```ts\n * const receiver = await createTestReceiver({ secret, failFirst: 2 });\n * await core.createEndpoint(\"acme\", { url: receiver.url });\n * // … publish + drain; receiver.received now holds the verified envelopes.\n * await receiver.close();\n * ```\n */\nexport async function createTestReceiver(options: TestReceiverOptions = {}): Promise<TestReceiver> {\n const received: WebhookEnvelope[] = [];\n const requests: { headers: Record<string, string>; body: string }[] = [];\n let remainingFailures = options.failFirst ?? 0;\n\n const server: Server = createServer((req: IncomingMessage, res: ServerResponse) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => {\n void (async () => {\n const body = Buffer.concat(chunks).toString(\"utf8\");\n const headers: Record<string, string> = {};\n for (const [name, value] of Object.entries(req.headers)) {\n if (typeof value === \"string\") headers[name] = value;\n }\n requests.push({ headers, body });\n\n if (remainingFailures > 0) {\n remainingFailures--;\n res.writeHead(options.status ?? 500, { \"content-type\": \"text/plain\" });\n res.end(\"simulated failure\");\n return;\n }\n\n if (options.secret) {\n try {\n received.push(await verify({ payload: body, headers, secret: options.secret }));\n } catch {\n res.writeHead(401, { \"content-type\": \"text/plain\" });\n res.end(\"invalid signature\");\n return;\n }\n }\n res.writeHead(200, { \"content-type\": \"text/plain\" });\n res.end(\"ok\");\n })();\n });\n });\n\n await new Promise<void>((resolve) => server.listen(0, \"127.0.0.1\", resolve));\n const address = server.address();\n if (address === null || typeof address === \"string\") {\n throw new Error(\"Test receiver failed to bind a port\");\n }\n\n return {\n url: `http://127.0.0.1:${address.port}/webhooks`,\n received,\n requests,\n close: () =>\n new Promise<void>((resolve, reject) => {\n server.close((err) => (err ? reject(err) : resolve()));\n }),\n };\n}\n\n/**\n * Tick the dispatcher until a pass makes no attempts (or `maxTicks` is hit).\n * Pair with an all-immediate retry schedule ({@link createTestWebhooks}'s\n * default) so failed deliveries are due again on the very next tick.\n */\nexport async function drainDeliveries(dispatcher: Dispatcher, maxTicks = 25): Promise<void> {\n for (let i = 0; i < maxTicks; i++) {\n if ((await dispatcher.tick()) === 0) return;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,SAAgB,mBAAmB,UAA+B,CAAC,GAAiB;CAClF,MAAM,UAAUA,eAAAA,oBAAoB;CACpC,MAAM,EAAE,YAAY,mBAAmB,GAAG,gBAAgB;CAC1D,MAAM,OAAOC,aAAAA,mBAAmB;EAC9B;EACA,mBAAmB;EACnB,GAAG;EACH,YAAY;GACV,eAAe;IAAC;IAAM;IAAM;GAAI;GAChC,GAAG;EACL;CACF,CAAC;CAED,OAAO;EAAE;EAAM;EAAS,YADLC,mBAAAA,iBAAiB,IACH;CAAE;AACrC;;;;;;;;;;;;;AAsCA,eAAsB,mBAAmB,UAA+B,CAAC,GAA0B;CACjG,MAAM,WAA8B,CAAC;CACrC,MAAM,WAAgE,CAAC;CACvE,IAAI,oBAAoB,QAAQ,aAAa;CAE7C,MAAM,UAAA,GAAA,UAAA,eAA+B,KAAsB,QAAwB;EACjF,MAAM,SAAmB,CAAC;EAC1B,IAAI,GAAG,SAAS,UAAkB,OAAO,KAAK,KAAK,CAAC;EACpD,IAAI,GAAG,aAAa;GAClB,CAAM,YAAY;IAChB,MAAM,OAAO,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM;IAClD,MAAM,UAAkC,CAAC;IACzC,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,IAAI,OAAO,GACpD,IAAI,OAAO,UAAU,UAAU,QAAQ,QAAQ;IAEjD,SAAS,KAAK;KAAE;KAAS;IAAK,CAAC;IAE/B,IAAI,oBAAoB,GAAG;KACzB;KACA,IAAI,UAAU,QAAQ,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;KACrE,IAAI,IAAI,mBAAmB;KAC3B;IACF;IAEA,IAAI,QAAQ,QACV,IAAI;KACF,SAAS,KAAK,MAAMC,gBAAAA,OAAO;MAAE,SAAS;MAAM;MAAS,QAAQ,QAAQ;KAAO,CAAC,CAAC;IAChF,QAAQ;KACN,IAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;KACnD,IAAI,IAAI,mBAAmB;KAC3B;IACF;IAEF,IAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;IACnD,IAAI,IAAI,IAAI;GACd,GAAG;EACL,CAAC;CACH,CAAC;CAED,MAAM,IAAI,SAAe,YAAY,OAAO,OAAO,GAAG,aAAa,OAAO,CAAC;CAC3E,MAAM,UAAU,OAAO,QAAQ;CAC/B,IAAI,YAAY,QAAQ,OAAO,YAAY,UACzC,MAAM,IAAI,MAAM,qCAAqC;CAGvD,OAAO;EACL,KAAK,oBAAoB,QAAQ,KAAK;EACtC;EACA;EACA,aACE,IAAI,SAAe,SAAS,WAAW;GACrC,OAAO,OAAO,QAAS,MAAM,OAAO,GAAG,IAAI,QAAQ,CAAE;EACvD,CAAC;CACL;AACF;;;;;;AAOA,eAAsB,gBAAgB,YAAwB,WAAW,IAAmB;CAC1F,KAAK,IAAI,IAAI,GAAG,IAAI,UAAU,KAC5B,IAAK,MAAM,WAAW,KAAK,MAAO,GAAG;AAEzC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { _ as WebhookEnvelope } from "./schema-mo__wv4P.cjs";
|
|
2
|
+
import { S as DispatcherOptions, _ as WebhooksCore, v as WebhooksCoreOptions, x as Dispatcher } from "./core-KJawHjFF.cjs";
|
|
3
|
+
import { n as MemoryWebhooksStorage } from "./memory-FnMJWCmB.cjs";
|
|
4
|
+
|
|
5
|
+
//#region src/testing.d.ts
|
|
6
|
+
/** Options for {@link createTestWebhooks}. */
|
|
7
|
+
interface TestWebhooksOptions extends Omit<WebhooksCoreOptions, "storage" | "queueStorage"> {
|
|
8
|
+
/** Dispatcher config; a fast all-immediate retry schedule is the default. */
|
|
9
|
+
dispatcher?: DispatcherOptions;
|
|
10
|
+
}
|
|
11
|
+
/** Result of {@link createTestWebhooks}. */
|
|
12
|
+
interface TestWebhooks {
|
|
13
|
+
core: WebhooksCore;
|
|
14
|
+
storage: MemoryWebhooksStorage;
|
|
15
|
+
/** NOT started — drive it with `tick()` or {@link drainDeliveries}. */
|
|
16
|
+
dispatcher: Dispatcher;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* An in-memory core + dispatcher for tests. The dispatcher is **not started**
|
|
20
|
+
* (no timers) and defaults to an immediate three-attempt retry schedule so
|
|
21
|
+
* failure paths drain in a few ticks.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { createTestWebhooks, drainDeliveries } from "@xtandard/webhooks/testing";
|
|
26
|
+
*
|
|
27
|
+
* const { core, dispatcher } = createTestWebhooks();
|
|
28
|
+
* await core.createApplication({ key: "acme" });
|
|
29
|
+
* // … create an endpoint, publish, then:
|
|
30
|
+
* await drainDeliveries(dispatcher);
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
declare function createTestWebhooks(options?: TestWebhooksOptions): TestWebhooks;
|
|
34
|
+
/** Options for {@link createTestReceiver}. */
|
|
35
|
+
interface TestReceiverOptions {
|
|
36
|
+
/**
|
|
37
|
+
* When set, each request is verified against this secret and its parsed
|
|
38
|
+
* envelope lands in `received`; an invalid signature answers 401.
|
|
39
|
+
*/
|
|
40
|
+
secret?: string;
|
|
41
|
+
/** Fail (with `status`) the first N requests — exercises the retry path. */
|
|
42
|
+
failFirst?: number;
|
|
43
|
+
/** The failure status used while failing. Default `500`. */
|
|
44
|
+
status?: number;
|
|
45
|
+
}
|
|
46
|
+
/** A running test receiver. */
|
|
47
|
+
interface TestReceiver {
|
|
48
|
+
/** The local URL to register as an endpoint. */
|
|
49
|
+
url: string;
|
|
50
|
+
/** Verified envelopes (only populated when `secret` was provided). */
|
|
51
|
+
received: WebhookEnvelope[];
|
|
52
|
+
/** Every request, verified or not, in arrival order. */
|
|
53
|
+
requests: {
|
|
54
|
+
headers: Record<string, string>;
|
|
55
|
+
body: string;
|
|
56
|
+
}[];
|
|
57
|
+
close(): Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Start a real local HTTP server that plays the receiving side of a webhook.
|
|
61
|
+
* Works under Bun and Node.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* const receiver = await createTestReceiver({ secret, failFirst: 2 });
|
|
66
|
+
* await core.createEndpoint("acme", { url: receiver.url });
|
|
67
|
+
* // … publish + drain; receiver.received now holds the verified envelopes.
|
|
68
|
+
* await receiver.close();
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
declare function createTestReceiver(options?: TestReceiverOptions): Promise<TestReceiver>;
|
|
72
|
+
/**
|
|
73
|
+
* Tick the dispatcher until a pass makes no attempts (or `maxTicks` is hit).
|
|
74
|
+
* Pair with an all-immediate retry schedule ({@link createTestWebhooks}'s
|
|
75
|
+
* default) so failed deliveries are due again on the very next tick.
|
|
76
|
+
*/
|
|
77
|
+
declare function drainDeliveries(dispatcher: Dispatcher, maxTicks?: number): Promise<void>;
|
|
78
|
+
//#endregion
|
|
79
|
+
export { TestReceiver, TestReceiverOptions, TestWebhooks, TestWebhooksOptions, createTestReceiver, createTestWebhooks, drainDeliveries };
|
|
80
|
+
//# sourceMappingURL=testing.d.cts.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { _ as WebhookEnvelope } from "./schema-mo__wv4P.mjs";
|
|
2
|
+
import { S as DispatcherOptions, _ as WebhooksCore, v as WebhooksCoreOptions, x as Dispatcher } from "./core-DT4ppWh8.mjs";
|
|
3
|
+
import { n as MemoryWebhooksStorage } from "./memory-qIvANEs_.mjs";
|
|
4
|
+
|
|
5
|
+
//#region src/testing.d.ts
|
|
6
|
+
/** Options for {@link createTestWebhooks}. */
|
|
7
|
+
interface TestWebhooksOptions extends Omit<WebhooksCoreOptions, "storage" | "queueStorage"> {
|
|
8
|
+
/** Dispatcher config; a fast all-immediate retry schedule is the default. */
|
|
9
|
+
dispatcher?: DispatcherOptions;
|
|
10
|
+
}
|
|
11
|
+
/** Result of {@link createTestWebhooks}. */
|
|
12
|
+
interface TestWebhooks {
|
|
13
|
+
core: WebhooksCore;
|
|
14
|
+
storage: MemoryWebhooksStorage;
|
|
15
|
+
/** NOT started — drive it with `tick()` or {@link drainDeliveries}. */
|
|
16
|
+
dispatcher: Dispatcher;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* An in-memory core + dispatcher for tests. The dispatcher is **not started**
|
|
20
|
+
* (no timers) and defaults to an immediate three-attempt retry schedule so
|
|
21
|
+
* failure paths drain in a few ticks.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* import { createTestWebhooks, drainDeliveries } from "@xtandard/webhooks/testing";
|
|
26
|
+
*
|
|
27
|
+
* const { core, dispatcher } = createTestWebhooks();
|
|
28
|
+
* await core.createApplication({ key: "acme" });
|
|
29
|
+
* // … create an endpoint, publish, then:
|
|
30
|
+
* await drainDeliveries(dispatcher);
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
declare function createTestWebhooks(options?: TestWebhooksOptions): TestWebhooks;
|
|
34
|
+
/** Options for {@link createTestReceiver}. */
|
|
35
|
+
interface TestReceiverOptions {
|
|
36
|
+
/**
|
|
37
|
+
* When set, each request is verified against this secret and its parsed
|
|
38
|
+
* envelope lands in `received`; an invalid signature answers 401.
|
|
39
|
+
*/
|
|
40
|
+
secret?: string;
|
|
41
|
+
/** Fail (with `status`) the first N requests — exercises the retry path. */
|
|
42
|
+
failFirst?: number;
|
|
43
|
+
/** The failure status used while failing. Default `500`. */
|
|
44
|
+
status?: number;
|
|
45
|
+
}
|
|
46
|
+
/** A running test receiver. */
|
|
47
|
+
interface TestReceiver {
|
|
48
|
+
/** The local URL to register as an endpoint. */
|
|
49
|
+
url: string;
|
|
50
|
+
/** Verified envelopes (only populated when `secret` was provided). */
|
|
51
|
+
received: WebhookEnvelope[];
|
|
52
|
+
/** Every request, verified or not, in arrival order. */
|
|
53
|
+
requests: {
|
|
54
|
+
headers: Record<string, string>;
|
|
55
|
+
body: string;
|
|
56
|
+
}[];
|
|
57
|
+
close(): Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Start a real local HTTP server that plays the receiving side of a webhook.
|
|
61
|
+
* Works under Bun and Node.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* const receiver = await createTestReceiver({ secret, failFirst: 2 });
|
|
66
|
+
* await core.createEndpoint("acme", { url: receiver.url });
|
|
67
|
+
* // … publish + drain; receiver.received now holds the verified envelopes.
|
|
68
|
+
* await receiver.close();
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
declare function createTestReceiver(options?: TestReceiverOptions): Promise<TestReceiver>;
|
|
72
|
+
/**
|
|
73
|
+
* Tick the dispatcher until a pass makes no attempts (or `maxTicks` is hit).
|
|
74
|
+
* Pair with an all-immediate retry schedule ({@link createTestWebhooks}'s
|
|
75
|
+
* default) so failed deliveries are due again on the very next tick.
|
|
76
|
+
*/
|
|
77
|
+
declare function drainDeliveries(dispatcher: Dispatcher, maxTicks?: number): Promise<void>;
|
|
78
|
+
//#endregion
|
|
79
|
+
export { TestReceiver, TestReceiverOptions, TestWebhooks, TestWebhooksOptions, createTestReceiver, createTestWebhooks, drainDeliveries };
|
|
80
|
+
//# sourceMappingURL=testing.d.mts.map
|
package/dist/testing.mjs
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { verify } from "./signing.mjs";
|
|
2
|
+
import { o as createWebhooksCore } from "./core-CMpnmI5Q.mjs";
|
|
3
|
+
import { n as createDispatcher } from "./dispatcher-Coubwrka.mjs";
|
|
4
|
+
import { t as createMemoryStorage } from "./memory-BMsSSwqn.mjs";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
//#region src/testing.ts
|
|
7
|
+
/**
|
|
8
|
+
* Testing utilities — `@xtandard/webhooks/testing`.
|
|
9
|
+
*
|
|
10
|
+
* Everything a host app (or this package's own suite) needs to test its
|
|
11
|
+
* webhook wiring without the network or timers:
|
|
12
|
+
*
|
|
13
|
+
* - {@link createTestWebhooks}: an in-memory core + a **not-started** dispatcher,
|
|
14
|
+
* driven manually via `dispatcher.tick()` / {@link drainDeliveries}.
|
|
15
|
+
* - {@link createTestReceiver}: a real local HTTP server that records (and,
|
|
16
|
+
* given the secret, verifies) every delivery it receives — including
|
|
17
|
+
* programmable failures to exercise the retry path.
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* An in-memory core + dispatcher for tests. The dispatcher is **not started**
|
|
23
|
+
* (no timers) and defaults to an immediate three-attempt retry schedule so
|
|
24
|
+
* failure paths drain in a few ticks.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* import { createTestWebhooks, drainDeliveries } from "@xtandard/webhooks/testing";
|
|
29
|
+
*
|
|
30
|
+
* const { core, dispatcher } = createTestWebhooks();
|
|
31
|
+
* await core.createApplication({ key: "acme" });
|
|
32
|
+
* // … create an endpoint, publish, then:
|
|
33
|
+
* await drainDeliveries(dispatcher);
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
function createTestWebhooks(options = {}) {
|
|
37
|
+
const storage = createMemoryStorage();
|
|
38
|
+
const { dispatcher: dispatcherOptions, ...coreOptions } = options;
|
|
39
|
+
const core = createWebhooksCore({
|
|
40
|
+
storage,
|
|
41
|
+
allowInsecureUrls: true,
|
|
42
|
+
...coreOptions,
|
|
43
|
+
dispatcher: {
|
|
44
|
+
retrySchedule: [
|
|
45
|
+
"0s",
|
|
46
|
+
"0s",
|
|
47
|
+
"0s"
|
|
48
|
+
],
|
|
49
|
+
...dispatcherOptions
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
core,
|
|
54
|
+
storage,
|
|
55
|
+
dispatcher: createDispatcher(core)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Start a real local HTTP server that plays the receiving side of a webhook.
|
|
60
|
+
* Works under Bun and Node.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* const receiver = await createTestReceiver({ secret, failFirst: 2 });
|
|
65
|
+
* await core.createEndpoint("acme", { url: receiver.url });
|
|
66
|
+
* // … publish + drain; receiver.received now holds the verified envelopes.
|
|
67
|
+
* await receiver.close();
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
async function createTestReceiver(options = {}) {
|
|
71
|
+
const received = [];
|
|
72
|
+
const requests = [];
|
|
73
|
+
let remainingFailures = options.failFirst ?? 0;
|
|
74
|
+
const server = createServer((req, res) => {
|
|
75
|
+
const chunks = [];
|
|
76
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
77
|
+
req.on("end", () => {
|
|
78
|
+
(async () => {
|
|
79
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
80
|
+
const headers = {};
|
|
81
|
+
for (const [name, value] of Object.entries(req.headers)) if (typeof value === "string") headers[name] = value;
|
|
82
|
+
requests.push({
|
|
83
|
+
headers,
|
|
84
|
+
body
|
|
85
|
+
});
|
|
86
|
+
if (remainingFailures > 0) {
|
|
87
|
+
remainingFailures--;
|
|
88
|
+
res.writeHead(options.status ?? 500, { "content-type": "text/plain" });
|
|
89
|
+
res.end("simulated failure");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (options.secret) try {
|
|
93
|
+
received.push(await verify({
|
|
94
|
+
payload: body,
|
|
95
|
+
headers,
|
|
96
|
+
secret: options.secret
|
|
97
|
+
}));
|
|
98
|
+
} catch {
|
|
99
|
+
res.writeHead(401, { "content-type": "text/plain" });
|
|
100
|
+
res.end("invalid signature");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
res.writeHead(200, { "content-type": "text/plain" });
|
|
104
|
+
res.end("ok");
|
|
105
|
+
})();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
109
|
+
const address = server.address();
|
|
110
|
+
if (address === null || typeof address === "string") throw new Error("Test receiver failed to bind a port");
|
|
111
|
+
return {
|
|
112
|
+
url: `http://127.0.0.1:${address.port}/webhooks`,
|
|
113
|
+
received,
|
|
114
|
+
requests,
|
|
115
|
+
close: () => new Promise((resolve, reject) => {
|
|
116
|
+
server.close((err) => err ? reject(err) : resolve());
|
|
117
|
+
})
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Tick the dispatcher until a pass makes no attempts (or `maxTicks` is hit).
|
|
122
|
+
* Pair with an all-immediate retry schedule ({@link createTestWebhooks}'s
|
|
123
|
+
* default) so failed deliveries are due again on the very next tick.
|
|
124
|
+
*/
|
|
125
|
+
async function drainDeliveries(dispatcher, maxTicks = 25) {
|
|
126
|
+
for (let i = 0; i < maxTicks; i++) if (await dispatcher.tick() === 0) return;
|
|
127
|
+
}
|
|
128
|
+
//#endregion
|
|
129
|
+
export { createTestReceiver, createTestWebhooks, drainDeliveries };
|
|
130
|
+
|
|
131
|
+
//# sourceMappingURL=testing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.mjs","names":[],"sources":["../src/testing.ts"],"sourcesContent":["/**\n * Testing utilities — `@xtandard/webhooks/testing`.\n *\n * Everything a host app (or this package's own suite) needs to test its\n * webhook wiring without the network or timers:\n *\n * - {@link createTestWebhooks}: an in-memory core + a **not-started** dispatcher,\n * driven manually via `dispatcher.tick()` / {@link drainDeliveries}.\n * - {@link createTestReceiver}: a real local HTTP server that records (and,\n * given the secret, verifies) every delivery it receives — including\n * programmable failures to exercise the retry path.\n *\n * @module\n */\n\nimport { createServer, type IncomingMessage, type Server, type ServerResponse } from \"node:http\";\nimport { createWebhooksCore, type WebhooksCore, type WebhooksCoreOptions } from \"./core.ts\";\nimport { createDispatcher, type Dispatcher, type DispatcherOptions } from \"./dispatcher.ts\";\nimport type { WebhookEnvelope } from \"./schema.ts\";\nimport { verify } from \"./signing.ts\";\nimport { createMemoryStorage, type MemoryWebhooksStorage } from \"./storage/memory.ts\";\n\n/** Options for {@link createTestWebhooks}. */\nexport interface TestWebhooksOptions extends Omit<WebhooksCoreOptions, \"storage\" | \"queueStorage\"> {\n /** Dispatcher config; a fast all-immediate retry schedule is the default. */\n dispatcher?: DispatcherOptions;\n}\n\n/** Result of {@link createTestWebhooks}. */\nexport interface TestWebhooks {\n core: WebhooksCore;\n storage: MemoryWebhooksStorage;\n /** NOT started — drive it with `tick()` or {@link drainDeliveries}. */\n dispatcher: Dispatcher;\n}\n\n/**\n * An in-memory core + dispatcher for tests. The dispatcher is **not started**\n * (no timers) and defaults to an immediate three-attempt retry schedule so\n * failure paths drain in a few ticks.\n *\n * @example\n * ```ts\n * import { createTestWebhooks, drainDeliveries } from \"@xtandard/webhooks/testing\";\n *\n * const { core, dispatcher } = createTestWebhooks();\n * await core.createApplication({ key: \"acme\" });\n * // … create an endpoint, publish, then:\n * await drainDeliveries(dispatcher);\n * ```\n */\nexport function createTestWebhooks(options: TestWebhooksOptions = {}): TestWebhooks {\n const storage = createMemoryStorage();\n const { dispatcher: dispatcherOptions, ...coreOptions } = options;\n const core = createWebhooksCore({\n storage,\n allowInsecureUrls: true, // test receivers are local http\n ...coreOptions,\n dispatcher: {\n retrySchedule: [\"0s\", \"0s\", \"0s\"],\n ...dispatcherOptions,\n },\n });\n const dispatcher = createDispatcher(core);\n return { core, storage, dispatcher };\n}\n\n/** Options for {@link createTestReceiver}. */\nexport interface TestReceiverOptions {\n /**\n * When set, each request is verified against this secret and its parsed\n * envelope lands in `received`; an invalid signature answers 401.\n */\n secret?: string;\n /** Fail (with `status`) the first N requests — exercises the retry path. */\n failFirst?: number;\n /** The failure status used while failing. Default `500`. */\n status?: number;\n}\n\n/** A running test receiver. */\nexport interface TestReceiver {\n /** The local URL to register as an endpoint. */\n url: string;\n /** Verified envelopes (only populated when `secret` was provided). */\n received: WebhookEnvelope[];\n /** Every request, verified or not, in arrival order. */\n requests: { headers: Record<string, string>; body: string }[];\n close(): Promise<void>;\n}\n\n/**\n * Start a real local HTTP server that plays the receiving side of a webhook.\n * Works under Bun and Node.\n *\n * @example\n * ```ts\n * const receiver = await createTestReceiver({ secret, failFirst: 2 });\n * await core.createEndpoint(\"acme\", { url: receiver.url });\n * // … publish + drain; receiver.received now holds the verified envelopes.\n * await receiver.close();\n * ```\n */\nexport async function createTestReceiver(options: TestReceiverOptions = {}): Promise<TestReceiver> {\n const received: WebhookEnvelope[] = [];\n const requests: { headers: Record<string, string>; body: string }[] = [];\n let remainingFailures = options.failFirst ?? 0;\n\n const server: Server = createServer((req: IncomingMessage, res: ServerResponse) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => {\n void (async () => {\n const body = Buffer.concat(chunks).toString(\"utf8\");\n const headers: Record<string, string> = {};\n for (const [name, value] of Object.entries(req.headers)) {\n if (typeof value === \"string\") headers[name] = value;\n }\n requests.push({ headers, body });\n\n if (remainingFailures > 0) {\n remainingFailures--;\n res.writeHead(options.status ?? 500, { \"content-type\": \"text/plain\" });\n res.end(\"simulated failure\");\n return;\n }\n\n if (options.secret) {\n try {\n received.push(await verify({ payload: body, headers, secret: options.secret }));\n } catch {\n res.writeHead(401, { \"content-type\": \"text/plain\" });\n res.end(\"invalid signature\");\n return;\n }\n }\n res.writeHead(200, { \"content-type\": \"text/plain\" });\n res.end(\"ok\");\n })();\n });\n });\n\n await new Promise<void>((resolve) => server.listen(0, \"127.0.0.1\", resolve));\n const address = server.address();\n if (address === null || typeof address === \"string\") {\n throw new Error(\"Test receiver failed to bind a port\");\n }\n\n return {\n url: `http://127.0.0.1:${address.port}/webhooks`,\n received,\n requests,\n close: () =>\n new Promise<void>((resolve, reject) => {\n server.close((err) => (err ? reject(err) : resolve()));\n }),\n };\n}\n\n/**\n * Tick the dispatcher until a pass makes no attempts (or `maxTicks` is hit).\n * Pair with an all-immediate retry schedule ({@link createTestWebhooks}'s\n * default) so failed deliveries are due again on the very next tick.\n */\nexport async function drainDeliveries(dispatcher: Dispatcher, maxTicks = 25): Promise<void> {\n for (let i = 0; i < maxTicks; i++) {\n if ((await dispatcher.tick()) === 0) return;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,SAAgB,mBAAmB,UAA+B,CAAC,GAAiB;CAClF,MAAM,UAAU,oBAAoB;CACpC,MAAM,EAAE,YAAY,mBAAmB,GAAG,gBAAgB;CAC1D,MAAM,OAAO,mBAAmB;EAC9B;EACA,mBAAmB;EACnB,GAAG;EACH,YAAY;GACV,eAAe;IAAC;IAAM;IAAM;GAAI;GAChC,GAAG;EACL;CACF,CAAC;CAED,OAAO;EAAE;EAAM;EAAS,YADL,iBAAiB,IACH;CAAE;AACrC;;;;;;;;;;;;;AAsCA,eAAsB,mBAAmB,UAA+B,CAAC,GAA0B;CACjG,MAAM,WAA8B,CAAC;CACrC,MAAM,WAAgE,CAAC;CACvE,IAAI,oBAAoB,QAAQ,aAAa;CAE7C,MAAM,SAAiB,cAAc,KAAsB,QAAwB;EACjF,MAAM,SAAmB,CAAC;EAC1B,IAAI,GAAG,SAAS,UAAkB,OAAO,KAAK,KAAK,CAAC;EACpD,IAAI,GAAG,aAAa;GAClB,CAAM,YAAY;IAChB,MAAM,OAAO,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM;IAClD,MAAM,UAAkC,CAAC;IACzC,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,IAAI,OAAO,GACpD,IAAI,OAAO,UAAU,UAAU,QAAQ,QAAQ;IAEjD,SAAS,KAAK;KAAE;KAAS;IAAK,CAAC;IAE/B,IAAI,oBAAoB,GAAG;KACzB;KACA,IAAI,UAAU,QAAQ,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;KACrE,IAAI,IAAI,mBAAmB;KAC3B;IACF;IAEA,IAAI,QAAQ,QACV,IAAI;KACF,SAAS,KAAK,MAAM,OAAO;MAAE,SAAS;MAAM;MAAS,QAAQ,QAAQ;KAAO,CAAC,CAAC;IAChF,QAAQ;KACN,IAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;KACnD,IAAI,IAAI,mBAAmB;KAC3B;IACF;IAEF,IAAI,UAAU,KAAK,EAAE,gBAAgB,aAAa,CAAC;IACnD,IAAI,IAAI,IAAI;GACd,GAAG;EACL,CAAC;CACH,CAAC;CAED,MAAM,IAAI,SAAe,YAAY,OAAO,OAAO,GAAG,aAAa,OAAO,CAAC;CAC3E,MAAM,UAAU,OAAO,QAAQ;CAC/B,IAAI,YAAY,QAAQ,OAAO,YAAY,UACzC,MAAM,IAAI,MAAM,qCAAqC;CAGvD,OAAO;EACL,KAAK,oBAAoB,QAAQ,KAAK;EACtC;EACA;EACA,aACE,IAAI,SAAe,SAAS,WAAW;GACrC,OAAO,OAAO,QAAS,MAAM,OAAO,GAAG,IAAI,QAAQ,CAAE;EACvD,CAAC;CACL;AACF;;;;;;AAOA,eAAsB,gBAAgB,YAAwB,WAAW,IAAmB;CAC1F,KAAK,IAAI,IAAI,GAAG,IAAI,UAAU,KAC5B,IAAK,MAAM,WAAW,KAAK,MAAO,GAAG;AAEzC"}
|