@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,176 @@
|
|
|
1
|
+
import { t as __exportAll } from "./chunk-D7D4PA-g.mjs";
|
|
2
|
+
import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
|
|
3
|
+
//#region src/auth/basic.ts
|
|
4
|
+
/**
|
|
5
|
+
* HTTP Basic authentication {@link AuthProvider}.
|
|
6
|
+
*
|
|
7
|
+
* Parses the `Authorization: Basic <base64>` header, looks the username up in a
|
|
8
|
+
* configured user list, and verifies the supplied password using one of three
|
|
9
|
+
* credential modes (in order of preference):
|
|
10
|
+
*
|
|
11
|
+
* 1. A custom `passwordVerifier(username, password)` callback — for delegating
|
|
12
|
+
* to your own user store.
|
|
13
|
+
* 2. A `passwordHash` produced by {@link hashPassword} — scrypt with a random
|
|
14
|
+
* salt, verified in constant time. **Recommended for production.**
|
|
15
|
+
* 3. A plain `password` field — **development only**. Never ship real
|
|
16
|
+
* credentials as plaintext; they are compared in constant time but stored as
|
|
17
|
+
* cleartext in your config.
|
|
18
|
+
*
|
|
19
|
+
* On success the matched user becomes a {@link Principal}; on any failure
|
|
20
|
+
* (missing/malformed header, unknown user, bad password) `authenticate` returns
|
|
21
|
+
* `null`. {@link AuthProvider.challenge} emits a `401` with a
|
|
22
|
+
* `WWW-Authenticate: Basic realm="…"` header so browsers prompt for credentials.
|
|
23
|
+
*
|
|
24
|
+
* Password hashing uses Node's `node:crypto` `scrypt`, which is available in
|
|
25
|
+
* both Node and Bun — no Bun-only APIs and no extra dependencies.
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
*/
|
|
29
|
+
var basic_exports = /* @__PURE__ */ __exportAll({
|
|
30
|
+
basicAuth: () => basicAuth,
|
|
31
|
+
hashPassword: () => hashPassword,
|
|
32
|
+
verifyPassword: () => verifyPassword
|
|
33
|
+
});
|
|
34
|
+
/** scrypt key length (bytes) used by {@link hashPassword}. */
|
|
35
|
+
const KEY_LENGTH = 64;
|
|
36
|
+
/** Salt length (bytes) used by {@link hashPassword}. */
|
|
37
|
+
const SALT_LENGTH = 16;
|
|
38
|
+
/** Prefix identifying a {@link hashPassword} digest. */
|
|
39
|
+
const SCRYPT_PREFIX = "scrypt";
|
|
40
|
+
const scryptAsync = (password, salt, keylen) => new Promise((resolve, reject) => {
|
|
41
|
+
scrypt(password, salt, keylen, (err, derivedKey) => {
|
|
42
|
+
if (err) reject(err);
|
|
43
|
+
else resolve(derivedKey);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
/** Lowercase hex encoding of a buffer. */
|
|
47
|
+
const toHex = (buf) => buf.toString("hex");
|
|
48
|
+
/**
|
|
49
|
+
* Hash a password with scrypt and a fresh random salt.
|
|
50
|
+
*
|
|
51
|
+
* The returned string is self-describing and safe to store in config or a
|
|
52
|
+
* database: `scrypt$<saltHex>$<hashHex>`. Pass it back to
|
|
53
|
+
* {@link verifyPassword} (or supply it as a user's `passwordHash`) to check a
|
|
54
|
+
* candidate password.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* const stored = await hashPassword("correct horse battery staple");
|
|
59
|
+
* // → "scrypt$<32 hex chars>$<128 hex chars>"
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
async function hashPassword(password) {
|
|
63
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
64
|
+
const derived = await scryptAsync(password, salt, KEY_LENGTH);
|
|
65
|
+
return `${SCRYPT_PREFIX}$${toHex(salt)}$${toHex(derived)}`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Verify a candidate `password` against a digest produced by
|
|
69
|
+
* {@link hashPassword}.
|
|
70
|
+
*
|
|
71
|
+
* Re-derives the scrypt hash using the stored salt and compares it to the stored
|
|
72
|
+
* hash with `crypto.timingSafeEqual` (constant time). Returns `false` — rather
|
|
73
|
+
* than throwing — for malformed or unrecognized digests.
|
|
74
|
+
*/
|
|
75
|
+
async function verifyPassword(password, stored) {
|
|
76
|
+
const parts = stored.split("$");
|
|
77
|
+
if (parts.length !== 3 || parts[0] !== SCRYPT_PREFIX) return false;
|
|
78
|
+
const saltHex = parts[1];
|
|
79
|
+
const hashHex = parts[2];
|
|
80
|
+
if (!saltHex || !hashHex) return false;
|
|
81
|
+
let salt;
|
|
82
|
+
let expected;
|
|
83
|
+
try {
|
|
84
|
+
salt = Buffer.from(saltHex, "hex");
|
|
85
|
+
expected = Buffer.from(hashHex, "hex");
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (salt.length === 0 || expected.length === 0) return false;
|
|
90
|
+
const derived = await scryptAsync(password, salt, expected.length);
|
|
91
|
+
if (derived.length !== expected.length) return false;
|
|
92
|
+
return timingSafeEqual(derived, expected);
|
|
93
|
+
}
|
|
94
|
+
/** Constant-time string comparison that does not leak length via early exit. */
|
|
95
|
+
function constantTimeEquals(a, b) {
|
|
96
|
+
const bufA = Buffer.from(a, "utf8");
|
|
97
|
+
const bufB = Buffer.from(b, "utf8");
|
|
98
|
+
if (bufA.length !== bufB.length) {
|
|
99
|
+
timingSafeEqual(bufA, bufA);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return timingSafeEqual(bufA, bufB);
|
|
103
|
+
}
|
|
104
|
+
/** Decode the `Authorization: Basic <base64>` header, or `null` if absent/malformed. */
|
|
105
|
+
function parseBasicHeader(request) {
|
|
106
|
+
const header = request.headers.get("authorization");
|
|
107
|
+
if (!header) return null;
|
|
108
|
+
const [scheme, encoded] = header.split(" ", 2);
|
|
109
|
+
if (!scheme || scheme.toLowerCase() !== "basic" || !encoded) return null;
|
|
110
|
+
let decoded;
|
|
111
|
+
try {
|
|
112
|
+
decoded = Buffer.from(encoded, "base64").toString("utf8");
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const sep = decoded.indexOf(":");
|
|
117
|
+
if (sep < 0) return null;
|
|
118
|
+
return {
|
|
119
|
+
username: decoded.slice(0, sep),
|
|
120
|
+
password: decoded.slice(sep + 1)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** Build the {@link Principal} for a successfully authenticated user. */
|
|
124
|
+
function principalFor(user) {
|
|
125
|
+
return {
|
|
126
|
+
id: user.id ?? user.username,
|
|
127
|
+
name: user.username,
|
|
128
|
+
...user.email !== void 0 ? { email: user.email } : {},
|
|
129
|
+
...user.roles !== void 0 ? { roles: user.roles } : {}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Create an HTTP Basic {@link AuthProvider}.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```ts
|
|
137
|
+
* import { basicAuth, hashPassword } from "@xtandard/webhooks/auth/basic";
|
|
138
|
+
*
|
|
139
|
+
* const auth = basicAuth({
|
|
140
|
+
* realm: "Webhooks Admin",
|
|
141
|
+
* users: [
|
|
142
|
+
* { username: "admin", passwordHash: await hashPassword("s3cret"), roles: ["admin"] },
|
|
143
|
+
* ],
|
|
144
|
+
* });
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
function basicAuth(options) {
|
|
148
|
+
const realm = options.realm ?? "xtandard-webhooks";
|
|
149
|
+
const usersByName = /* @__PURE__ */ new Map();
|
|
150
|
+
for (const user of options.users) usersByName.set(user.username, user);
|
|
151
|
+
return {
|
|
152
|
+
async authenticate(request) {
|
|
153
|
+
const creds = parseBasicHeader(request);
|
|
154
|
+
if (!creds) return null;
|
|
155
|
+
const user = usersByName.get(creds.username);
|
|
156
|
+
if (!user) {
|
|
157
|
+
if (options.passwordVerifier) await options.passwordVerifier(creds.username, creds.password);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
if (options.passwordVerifier) return await options.passwordVerifier(creds.username, creds.password) ? principalFor(user) : null;
|
|
161
|
+
if (user.passwordHash !== void 0) return await verifyPassword(creds.password, user.passwordHash) ? principalFor(user) : null;
|
|
162
|
+
if (user.password !== void 0) return constantTimeEquals(creds.password, user.password) ? principalFor(user) : null;
|
|
163
|
+
return null;
|
|
164
|
+
},
|
|
165
|
+
challenge(_request) {
|
|
166
|
+
return new Response("Unauthorized", {
|
|
167
|
+
status: 401,
|
|
168
|
+
headers: { "WWW-Authenticate": `Basic realm="${realm}"` }
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
//#endregion
|
|
174
|
+
export { verifyPassword as i, basic_exports as n, hashPassword as r, basicAuth as t };
|
|
175
|
+
|
|
176
|
+
//# sourceMappingURL=basic-DKk0Xfuu.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"basic-DKk0Xfuu.mjs","names":[],"sources":["../src/auth/basic.ts"],"sourcesContent":["/**\n * HTTP Basic authentication {@link AuthProvider}.\n *\n * Parses the `Authorization: Basic <base64>` header, looks the username up in a\n * configured user list, and verifies the supplied password using one of three\n * credential modes (in order of preference):\n *\n * 1. A custom `passwordVerifier(username, password)` callback — for delegating\n * to your own user store.\n * 2. A `passwordHash` produced by {@link hashPassword} — scrypt with a random\n * salt, verified in constant time. **Recommended for production.**\n * 3. A plain `password` field — **development only**. Never ship real\n * credentials as plaintext; they are compared in constant time but stored as\n * cleartext in your config.\n *\n * On success the matched user becomes a {@link Principal}; on any failure\n * (missing/malformed header, unknown user, bad password) `authenticate` returns\n * `null`. {@link AuthProvider.challenge} emits a `401` with a\n * `WWW-Authenticate: Basic realm=\"…\"` header so browsers prompt for credentials.\n *\n * Password hashing uses Node's `node:crypto` `scrypt`, which is available in\n * both Node and Bun — no Bun-only APIs and no extra dependencies.\n *\n * @module\n */\n\nimport { randomBytes, scrypt as scryptCb, timingSafeEqual } from \"node:crypto\";\nimport type { AuthProvider, Principal } from \"./contract.ts\";\n\n/** scrypt key length (bytes) used by {@link hashPassword}. */\nconst KEY_LENGTH = 64;\n/** Salt length (bytes) used by {@link hashPassword}. */\nconst SALT_LENGTH = 16;\n/** Prefix identifying a {@link hashPassword} digest. */\nconst SCRYPT_PREFIX = \"scrypt\";\n\nconst scryptAsync = (password: string, salt: Buffer, keylen: number): Promise<Buffer> =>\n new Promise((resolve, reject) => {\n scryptCb(password, salt, keylen, (err, derivedKey) => {\n if (err) reject(err);\n else resolve(derivedKey);\n });\n });\n\n/** Lowercase hex encoding of a buffer. */\nconst toHex = (buf: Buffer): string => buf.toString(\"hex\");\n\n/**\n * Hash a password with scrypt and a fresh random salt.\n *\n * The returned string is self-describing and safe to store in config or a\n * database: `scrypt$<saltHex>$<hashHex>`. Pass it back to\n * {@link verifyPassword} (or supply it as a user's `passwordHash`) to check a\n * candidate password.\n *\n * @example\n * ```ts\n * const stored = await hashPassword(\"correct horse battery staple\");\n * // → \"scrypt$<32 hex chars>$<128 hex chars>\"\n * ```\n */\nexport async function hashPassword(password: string): Promise<string> {\n const salt = randomBytes(SALT_LENGTH);\n const derived = await scryptAsync(password, salt, KEY_LENGTH);\n return `${SCRYPT_PREFIX}$${toHex(salt)}$${toHex(derived)}`;\n}\n\n/**\n * Verify a candidate `password` against a digest produced by\n * {@link hashPassword}.\n *\n * Re-derives the scrypt hash using the stored salt and compares it to the stored\n * hash with `crypto.timingSafeEqual` (constant time). Returns `false` — rather\n * than throwing — for malformed or unrecognized digests.\n */\nexport async function verifyPassword(password: string, stored: string): Promise<boolean> {\n const parts = stored.split(\"$\");\n if (parts.length !== 3 || parts[0] !== SCRYPT_PREFIX) return false;\n const saltHex = parts[1];\n const hashHex = parts[2];\n if (!saltHex || !hashHex) return false;\n\n let salt: Buffer;\n let expected: Buffer;\n try {\n salt = Buffer.from(saltHex, \"hex\");\n expected = Buffer.from(hashHex, \"hex\");\n } catch {\n return false;\n }\n if (salt.length === 0 || expected.length === 0) return false;\n\n const derived = await scryptAsync(password, salt, expected.length);\n if (derived.length !== expected.length) return false;\n return timingSafeEqual(derived, expected);\n}\n\n/** A configured user for {@link basicAuth}. */\nexport interface BasicAuthUser {\n /** The login name, matched against the Basic credentials. */\n username: string;\n /**\n * A digest produced by {@link hashPassword} (`scrypt$<salt>$<hash>`).\n * Preferred for production.\n */\n passwordHash?: string;\n /**\n * A plaintext password. **Development only** — prefer `passwordHash`. Compared\n * in constant time but stored as cleartext in your config.\n */\n password?: string;\n /** Roles attached to the resulting {@link Principal}. */\n roles?: string[];\n /** Email attached to the resulting {@link Principal}. */\n email?: string;\n /** Principal id. Defaults to {@link BasicAuthUser.username} when omitted. */\n id?: string;\n}\n\n/** Options for {@link basicAuth}. */\nexport interface BasicAuthOptions {\n /** The known users. */\n users: BasicAuthUser[];\n /**\n * Realm advertised in the `WWW-Authenticate` header on challenge.\n * @default \"xtandard-webhooks\"\n */\n realm?: string;\n /**\n * Custom verifier. When supplied it takes precedence over `passwordHash` and\n * `password` for matched users — delegate to your own credential store and\n * return `true` to accept.\n */\n passwordVerifier?: (username: string, password: string) => Promise<boolean> | boolean;\n}\n\n/** Constant-time string comparison that does not leak length via early exit. */\nfunction constantTimeEquals(a: string, b: string): boolean {\n const bufA = Buffer.from(a, \"utf8\");\n const bufB = Buffer.from(b, \"utf8\");\n // timingSafeEqual requires equal lengths; hash to a fixed width first so the\n // comparison time does not depend on the inputs.\n if (bufA.length !== bufB.length) {\n // Still perform a comparison to keep timing uniform, then fail.\n timingSafeEqual(bufA, bufA);\n return false;\n }\n return timingSafeEqual(bufA, bufB);\n}\n\n/** Result of parsing an `Authorization: Basic` header. */\ninterface BasicCredentials {\n username: string;\n password: string;\n}\n\n/** Decode the `Authorization: Basic <base64>` header, or `null` if absent/malformed. */\nfunction parseBasicHeader(request: Request): BasicCredentials | null {\n const header = request.headers.get(\"authorization\");\n if (!header) return null;\n const [scheme, encoded] = header.split(\" \", 2);\n if (!scheme || scheme.toLowerCase() !== \"basic\" || !encoded) return null;\n\n let decoded: string;\n try {\n decoded = Buffer.from(encoded, \"base64\").toString(\"utf8\");\n } catch {\n return null;\n }\n const sep = decoded.indexOf(\":\");\n if (sep < 0) return null;\n return { username: decoded.slice(0, sep), password: decoded.slice(sep + 1) };\n}\n\n/** Build the {@link Principal} for a successfully authenticated user. */\nfunction principalFor(user: BasicAuthUser): Principal {\n return {\n id: user.id ?? user.username,\n name: user.username,\n ...(user.email !== undefined ? { email: user.email } : {}),\n ...(user.roles !== undefined ? { roles: user.roles } : {}),\n };\n}\n\n/**\n * Create an HTTP Basic {@link AuthProvider}.\n *\n * @example\n * ```ts\n * import { basicAuth, hashPassword } from \"@xtandard/webhooks/auth/basic\";\n *\n * const auth = basicAuth({\n * realm: \"Webhooks Admin\",\n * users: [\n * { username: \"admin\", passwordHash: await hashPassword(\"s3cret\"), roles: [\"admin\"] },\n * ],\n * });\n * ```\n */\nexport function basicAuth(options: BasicAuthOptions): AuthProvider {\n const realm = options.realm ?? \"xtandard-webhooks\";\n const usersByName = new Map<string, BasicAuthUser>();\n for (const user of options.users) usersByName.set(user.username, user);\n\n return {\n async authenticate(request: Request): Promise<Principal | null> {\n const creds = parseBasicHeader(request);\n if (!creds) return null;\n\n const user = usersByName.get(creds.username);\n if (!user) {\n // Run a dummy verification to keep timing roughly uniform for unknown\n // users versus known users.\n if (options.passwordVerifier) {\n await options.passwordVerifier(creds.username, creds.password);\n }\n return null;\n }\n\n // (a) custom verifier wins.\n if (options.passwordVerifier) {\n const ok = await options.passwordVerifier(creds.username, creds.password);\n return ok ? principalFor(user) : null;\n }\n\n // (b) scrypt hash.\n if (user.passwordHash !== undefined) {\n const ok = await verifyPassword(creds.password, user.passwordHash);\n return ok ? principalFor(user) : null;\n }\n\n // (c) dev-only plaintext.\n if (user.password !== undefined) {\n const ok = constantTimeEquals(creds.password, user.password);\n return ok ? principalFor(user) : null;\n }\n\n return null;\n },\n\n challenge(_request: Request): Response {\n return new Response(\"Unauthorized\", {\n status: 401,\n headers: { \"WWW-Authenticate\": `Basic realm=\"${realm}\"` },\n });\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAM,aAAa;;AAEnB,MAAM,cAAc;;AAEpB,MAAM,gBAAgB;AAEtB,MAAM,eAAe,UAAkB,MAAc,WACnD,IAAI,SAAS,SAAS,WAAW;CAC/B,OAAS,UAAU,MAAM,SAAS,KAAK,eAAe;EACpD,IAAI,KAAK,OAAO,GAAG;OACd,QAAQ,UAAU;CACzB,CAAC;AACH,CAAC;;AAGH,MAAM,SAAS,QAAwB,IAAI,SAAS,KAAK;;;;;;;;;;;;;;;AAgBzD,eAAsB,aAAa,UAAmC;CACpE,MAAM,OAAO,YAAY,WAAW;CACpC,MAAM,UAAU,MAAM,YAAY,UAAU,MAAM,UAAU;CAC5D,OAAO,GAAG,cAAc,GAAG,MAAM,IAAI,EAAE,GAAG,MAAM,OAAO;AACzD;;;;;;;;;AAUA,eAAsB,eAAe,UAAkB,QAAkC;CACvF,MAAM,QAAQ,OAAO,MAAM,GAAG;CAC9B,IAAI,MAAM,WAAW,KAAK,MAAM,OAAO,eAAe,OAAO;CAC7D,MAAM,UAAU,MAAM;CACtB,MAAM,UAAU,MAAM;CACtB,IAAI,CAAC,WAAW,CAAC,SAAS,OAAO;CAEjC,IAAI;CACJ,IAAI;CACJ,IAAI;EACF,OAAO,OAAO,KAAK,SAAS,KAAK;EACjC,WAAW,OAAO,KAAK,SAAS,KAAK;CACvC,QAAQ;EACN,OAAO;CACT;CACA,IAAI,KAAK,WAAW,KAAK,SAAS,WAAW,GAAG,OAAO;CAEvD,MAAM,UAAU,MAAM,YAAY,UAAU,MAAM,SAAS,MAAM;CACjE,IAAI,QAAQ,WAAW,SAAS,QAAQ,OAAO;CAC/C,OAAO,gBAAgB,SAAS,QAAQ;AAC1C;;AA0CA,SAAS,mBAAmB,GAAW,GAAoB;CACzD,MAAM,OAAO,OAAO,KAAK,GAAG,MAAM;CAClC,MAAM,OAAO,OAAO,KAAK,GAAG,MAAM;CAGlC,IAAI,KAAK,WAAW,KAAK,QAAQ;EAE/B,gBAAgB,MAAM,IAAI;EAC1B,OAAO;CACT;CACA,OAAO,gBAAgB,MAAM,IAAI;AACnC;;AASA,SAAS,iBAAiB,SAA2C;CACnE,MAAM,SAAS,QAAQ,QAAQ,IAAI,eAAe;CAClD,IAAI,CAAC,QAAQ,OAAO;CACpB,MAAM,CAAC,QAAQ,WAAW,OAAO,MAAM,KAAK,CAAC;CAC7C,IAAI,CAAC,UAAU,OAAO,YAAY,MAAM,WAAW,CAAC,SAAS,OAAO;CAEpE,IAAI;CACJ,IAAI;EACF,UAAU,OAAO,KAAK,SAAS,QAAQ,EAAE,SAAS,MAAM;CAC1D,QAAQ;EACN,OAAO;CACT;CACA,MAAM,MAAM,QAAQ,QAAQ,GAAG;CAC/B,IAAI,MAAM,GAAG,OAAO;CACpB,OAAO;EAAE,UAAU,QAAQ,MAAM,GAAG,GAAG;EAAG,UAAU,QAAQ,MAAM,MAAM,CAAC;CAAE;AAC7E;;AAGA,SAAS,aAAa,MAAgC;CACpD,OAAO;EACL,IAAI,KAAK,MAAM,KAAK;EACpB,MAAM,KAAK;EACX,GAAI,KAAK,UAAU,KAAA,IAAY,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;EACxD,GAAI,KAAK,UAAU,KAAA,IAAY,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;CAC1D;AACF;;;;;;;;;;;;;;;;AAiBA,SAAgB,UAAU,SAAyC;CACjE,MAAM,QAAQ,QAAQ,SAAS;CAC/B,MAAM,8BAAc,IAAI,IAA2B;CACnD,KAAK,MAAM,QAAQ,QAAQ,OAAO,YAAY,IAAI,KAAK,UAAU,IAAI;CAErE,OAAO;EACL,MAAM,aAAa,SAA6C;GAC9D,MAAM,QAAQ,iBAAiB,OAAO;GACtC,IAAI,CAAC,OAAO,OAAO;GAEnB,MAAM,OAAO,YAAY,IAAI,MAAM,QAAQ;GAC3C,IAAI,CAAC,MAAM;IAGT,IAAI,QAAQ,kBACV,MAAM,QAAQ,iBAAiB,MAAM,UAAU,MAAM,QAAQ;IAE/D,OAAO;GACT;GAGA,IAAI,QAAQ,kBAEV,OAAO,MADU,QAAQ,iBAAiB,MAAM,UAAU,MAAM,QAAQ,IAC5D,aAAa,IAAI,IAAI;GAInC,IAAI,KAAK,iBAAiB,KAAA,GAExB,OAAO,MADU,eAAe,MAAM,UAAU,KAAK,YAAY,IACrD,aAAa,IAAI,IAAI;GAInC,IAAI,KAAK,aAAa,KAAA,GAEpB,OADW,mBAAmB,MAAM,UAAU,KAAK,QAC3C,IAAI,aAAa,IAAI,IAAI;GAGnC,OAAO;EACT;EAEA,UAAU,UAA6B;GACrC,OAAO,IAAI,SAAS,gBAAgB;IAClC,QAAQ;IACR,SAAS,EAAE,oBAAoB,gBAAgB,MAAM,GAAG;GAC1D,CAAC;EACH;CACF;AACF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { __exportAll as t };
|