Merge pull request 'feat(passkeys): schema, dependency and per-environment Relying Party (#37)' (#330) from feature/37-passkeys-groundwork into main
Reviewed-on: #330
This commit was merged in pull request #330.
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
exports.up = (pgm) => {
|
||||
pgm.sql(`
|
||||
-- A registered passkey (#37). Groundwork only: nothing reads these yet.
|
||||
--
|
||||
-- Bound to the customer and deleted with them. Account deletion already
|
||||
-- removes the personal data this sits beside, and a credential that
|
||||
-- outlived its owner could authenticate as a customer who no longer exists.
|
||||
-- Disabling an account (#33) is a different question and is deliberately not
|
||||
-- a schema concern: a disabled customer keeps their credentials and is
|
||||
-- refused at the authentication ceremony instead, so re-enabling them does
|
||||
-- not mean re-registering every device.
|
||||
CREATE TABLE IF NOT EXISTS customer_credentials (
|
||||
id SERIAL PRIMARY KEY,
|
||||
customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
|
||||
|
||||
-- The credential ID as base64url text rather than bytea. It arrives from
|
||||
-- the browser in that form, is compared as an opaque string, and is never
|
||||
-- interpreted here — storing bytes would mean encoding on write and
|
||||
-- decoding on every read for no gain.
|
||||
--
|
||||
-- Unique across the table, not merely per customer: a credential ID
|
||||
-- identifies an authenticator, and the same one appearing under two
|
||||
-- accounts means something has gone wrong rather than that two people
|
||||
-- share a key.
|
||||
credential_id TEXT NOT NULL UNIQUE,
|
||||
|
||||
-- The COSE public key, base64url. Verified against, never parsed here.
|
||||
public_key TEXT NOT NULL,
|
||||
|
||||
-- BIGINT because the spec allows a 32-bit unsigned value, which overflows
|
||||
-- a signed INTEGER at half its range.
|
||||
--
|
||||
-- What to do when this fails to increase is NOT decided here. Many synced
|
||||
-- passkeys report 0 forever, so "a regression means cloning" is wrong for
|
||||
-- them and right for hardware keys. That policy belongs with the
|
||||
-- authentication ceremony that enforces it (#39); this column only has to
|
||||
-- be able to hold the value.
|
||||
signature_counter BIGINT NOT NULL DEFAULT 0,
|
||||
|
||||
-- How the authenticator can be reached: usb, nfc, ble, internal, hybrid.
|
||||
-- A JSON array as text, because it is passed back to the browser verbatim
|
||||
-- and never queried on.
|
||||
transports TEXT,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
|
||||
-- Null until first used. Shown on the account page (#40) so a customer can
|
||||
-- recognise which device a credential belongs to, which is the only way
|
||||
-- they can tell two entries apart.
|
||||
last_used_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- Listing a customer's credentials is the common read, and revocation (#40)
|
||||
-- has to scope by owner.
|
||||
CREATE INDEX IF NOT EXISTS customer_credentials_customer_id_idx
|
||||
ON customer_credentials (customer_id);
|
||||
|
||||
-- An in-flight WebAuthn challenge (#37).
|
||||
--
|
||||
-- A separate table rather than customer_tokens with a new kind, and the
|
||||
-- reason is structural rather than tidiness: customer_tokens.customer_id is
|
||||
-- NOT NULL, and an *authentication* challenge is issued before anyone is
|
||||
-- identified. A discoverable-credential sign-in has no customer to attach
|
||||
-- to at the moment the challenge is created, so it could not be stored
|
||||
-- there without making that column nullable for every other kind of token.
|
||||
CREATE TABLE IF NOT EXISTS webauthn_challenges (
|
||||
-- The challenge itself, base64url, as issued. Primary key because it is
|
||||
-- the thing looked up, and unique by construction.
|
||||
challenge TEXT PRIMARY KEY,
|
||||
|
||||
-- Null for authentication, set for registration. Registration requires an
|
||||
-- authenticated session — it is not a sign-up path — so that half always
|
||||
-- knows whose it is.
|
||||
customer_id INTEGER REFERENCES customers(id) ON DELETE CASCADE,
|
||||
|
||||
-- 'registration' or 'authentication'. Not a CHECK constraint: the values
|
||||
-- come from this codebase rather than from a request, and the project has
|
||||
-- no enum types elsewhere.
|
||||
kind TEXT NOT NULL,
|
||||
|
||||
expires_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
-- Expiry is swept by time, so the sweep reads this rather than the whole
|
||||
-- table. Single use is enforced by deleting the row on consumption, which
|
||||
-- needs no index beyond the primary key.
|
||||
CREATE INDEX IF NOT EXISTS webauthn_challenges_expires_at_idx
|
||||
ON webauthn_challenges (expires_at);
|
||||
`);
|
||||
};
|
||||
|
||||
exports.down = (pgm) => {
|
||||
pgm.sql(`
|
||||
DROP TABLE IF EXISTS webauthn_challenges;
|
||||
DROP TABLE IF EXISTS customer_credentials;
|
||||
`);
|
||||
};
|
||||
Generated
+338
-2
@@ -9,6 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.122.0",
|
||||
"@simplewebauthn/server": "^14.0.1",
|
||||
"@types/markdown-it": "^14.2.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cookie-parser": "^1.4.6",
|
||||
@@ -102,6 +103,7 @@
|
||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
@@ -1304,6 +1306,12 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@hexagon/base64": {
|
||||
"version": "1.1.28",
|
||||
"resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz",
|
||||
"integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
||||
@@ -2257,6 +2265,12 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@levischuck/tiny-cbor": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz",
|
||||
"integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
@@ -2280,6 +2294,260 @@
|
||||
"@noble/hashes": "^1.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-android": {
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.9.4.tgz",
|
||||
"integrity": "sha512-SYHm4SoWSI0nRCoos6jpGusIqhPH9bbGBqv7ohlZ+H6BunrDzzQPk2ePgDuEUzV82OdvbLgtW4twUDwhU9P3YQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-asym-key": {
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-asym-key/-/asn1-asym-key-2.9.4.tgz",
|
||||
"integrity": "sha512-s7SJfjcXlR3MYMngDTeMLCy3VjGaIxeEy905CQ0j09pDbeYhNBvBGA7r7cAvCZYkVFo9kVg9RAki0K2iUz7SGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-pkcs8": "^2.9.4",
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"@peculiar/asn1-x509": "^2.9.4",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-cms": {
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.9.4.tgz",
|
||||
"integrity": "sha512-cben7oxmQsUGZqotus7yt0srYdncOT6RNWcTQ77T2RFOXejYVYkXadrfePdRcrVpO9K95IRLKKglG2k38jKXuw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"@peculiar/asn1-x509": "^2.9.4",
|
||||
"@peculiar/asn1-x509-attr": "^2.9.4",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-csr": {
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.9.4.tgz",
|
||||
"integrity": "sha512-xd4YN4vpRjkDAQWVfZZkeu12IEND7DOpkqaHSIHxZl1uggUNa9Ju0QxY2jHvDAS9pP0zhRBytg8ifsnGo3V0jw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"@peculiar/asn1-x509": "^2.9.4",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-ecc": {
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.9.4.tgz",
|
||||
"integrity": "sha512-JJXefFshRAuVAjWQo/39bkg1ywc1VaiO44S8RRC+Ykvf/u2KDmYffoDb0ZBPCR5uJy4AGKQhl8mX+Q8ShcWaXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"@peculiar/asn1-x509": "^2.9.4",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-pfx": {
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.9.4.tgz",
|
||||
"integrity": "sha512-khuGzHTzNzk4GDlIBEILyIs6Lce0yn0ZBdoI9v93kmNncfZRhD+AQ5ODFqdhvoE8cMJF/JMTQ8yA+t1D14kqCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-cms": "^2.9.4",
|
||||
"@peculiar/asn1-pkcs8": "^2.9.4",
|
||||
"@peculiar/asn1-rsa": "^2.9.4",
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-pkcs8": {
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.9.4.tgz",
|
||||
"integrity": "sha512-duRdotlUx9eDZe6QrQpQKl61RbWykCHBCkKayP8V8XdEFwlKHZ8qGGDMyS6Pye7OX7nLFttTTpRkJeet78ckwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"@peculiar/asn1-x509": "^2.9.4",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-pkcs9": {
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.9.4.tgz",
|
||||
"integrity": "sha512-kaL4cNxBpdQE2dKlyZBqz4ygCrwffO+8wfoxTEqM1Z8RadvCeELBRzcv0dzM8aY9azHMwODO5nxU65zXmhToOQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-cms": "^2.9.4",
|
||||
"@peculiar/asn1-pfx": "^2.9.4",
|
||||
"@peculiar/asn1-pkcs8": "^2.9.4",
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"@peculiar/asn1-x509": "^2.9.4",
|
||||
"@peculiar/asn1-x509-attr": "^2.9.4",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-rsa": {
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.9.4.tgz",
|
||||
"integrity": "sha512-pZ96eD1PptovcWQ/GSmuNFXd/7EQJNlKfDaNCyE2rx3W0v6QFelkzquVqRSRyyDXXCYD69ZXJDzZ8GhIiQzKoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"@peculiar/asn1-x509": "^2.9.4",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-schema": {
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.9.4.tgz",
|
||||
"integrity": "sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/utils": "^2.0.2",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-x509": {
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.9.4.tgz",
|
||||
"integrity": "sha512-CxhBo/RdEbMMob7T31ZdQjGuoyRFLVwrDzTn25bihzBasRg9kRm/0IxIPvhgQtcK/9dNcO1XQL2fuPugwELL0Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"@peculiar/utils": "^2.0.2",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-x509-attr": {
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.9.4.tgz",
|
||||
"integrity": "sha512-ehQXbpQaQYycgu8OrvigwSPTFfVRcu0ECNYCWw+yzBp02Lw5paRqzzhUpfOgO2K38+WfFZuEz/0RPtam5g0OMg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"@peculiar/asn1-x509": "^2.9.4",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-x509-post-quantum": {
|
||||
"version": "2.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-post-quantum/-/asn1-x509-post-quantum-2.9.4.tgz",
|
||||
"integrity": "sha512-GD0k7BY3dsEIeai7C7bVRPddj/PzZu+sTawRPRxdHKbdy1f9b58WQyk0eFJdr28GShyLVTU8poyi4+bTAZfEtQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-asym-key": "^2.9.4",
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"@peculiar/asn1-x509": "^2.9.4",
|
||||
"asn1js": "^3.0.10",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/utils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz",
|
||||
"integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/x509": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-2.1.0.tgz",
|
||||
"integrity": "sha512-IYbg1R03CSQGWwl24kGyqrdVtixNSbRaDvBg1r5wyYjTP+VwPQkka1BzTgU5+vxiuwqg04OxdvdJ1xYYFIdUSA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@peculiar/asn1-cms": "^2.9.4",
|
||||
"@peculiar/asn1-csr": "^2.9.4",
|
||||
"@peculiar/asn1-ecc": "^2.9.4",
|
||||
"@peculiar/asn1-pkcs9": "^2.9.4",
|
||||
"@peculiar/asn1-rsa": "^2.9.4",
|
||||
"@peculiar/asn1-schema": "^2.9.4",
|
||||
"@peculiar/asn1-x509": "^2.9.4",
|
||||
"@peculiar/asn1-x509-post-quantum": "^2.9.4",
|
||||
"pvtsutils": "^1.3.6",
|
||||
"tslib": "^2.8.1",
|
||||
"tsyringe": "^4.10.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@simplewebauthn/server": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-14.0.1.tgz",
|
||||
"integrity": "sha512-kjWgcm9NdSv+Tobo4Swxd1WnEawsBogpYFMjg9pKsVLPN3FdCmR6BipkL+DcPDgmQ7UKbbBZqRqn3E3YaA7NBw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@hexagon/base64": "^1.1.27",
|
||||
"@levischuck/tiny-cbor": "^0.2.2",
|
||||
"@peculiar/asn1-android": "^2.6.0",
|
||||
"@peculiar/asn1-ecc": "^2.6.1",
|
||||
"@peculiar/asn1-rsa": "^2.6.1",
|
||||
"@peculiar/asn1-schema": "^2.6.0",
|
||||
"@peculiar/asn1-x509": "^2.6.1",
|
||||
"@peculiar/asn1-x509-post-quantum": "^2.9.4",
|
||||
"@peculiar/x509": "^2.1.0",
|
||||
"reflect-metadata": "^0.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.27.12",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz",
|
||||
@@ -2415,6 +2683,7 @@
|
||||
"integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^4.17.33",
|
||||
@@ -2575,6 +2844,7 @@
|
||||
"integrity": "sha512-AYdtudzabjLZgVgRZmAnU8bAnVUXzuJX2IYHeSIiIHm68olD+LgQYCGWdtcNYnP0uq9c4S4NibVG3Ni7VbKW7Q==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"pg-protocol": "*",
|
||||
@@ -2721,6 +2991,7 @@
|
||||
"integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.67.0",
|
||||
"@typescript-eslint/types": "8.67.0",
|
||||
@@ -3090,6 +3361,7 @@
|
||||
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3207,6 +3479,20 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asn1js": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz",
|
||||
"integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"pvtsutils": "^1.3.6",
|
||||
"pvutils": "^1.1.5",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@@ -3424,6 +3710,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.11.12",
|
||||
"caniuse-lite": "^1.0.30001809",
|
||||
@@ -4245,6 +4532,7 @@
|
||||
"integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -4635,6 +4923,7 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
|
||||
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@@ -5556,6 +5845,7 @@
|
||||
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "^29.7.0",
|
||||
"@jest/types": "^29.6.3",
|
||||
@@ -6268,6 +6558,7 @@
|
||||
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.17.tgz",
|
||||
"integrity": "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
@@ -7102,6 +7393,7 @@
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz",
|
||||
"integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.14.0",
|
||||
"pg-pool": "^3.14.0",
|
||||
@@ -7385,6 +7677,24 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pvtsutils": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
|
||||
"integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pvutils": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.2.0.tgz",
|
||||
"integrity": "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
|
||||
@@ -7466,6 +7776,12 @@
|
||||
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect-metadata": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/regexp-ast-analysis": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.7.1.tgz",
|
||||
@@ -8154,6 +8470,7 @@
|
||||
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -8291,8 +8608,7 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.23.12",
|
||||
@@ -8313,6 +8629,24 @@
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tsyringe": {
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz",
|
||||
"integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tsyringe/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@@ -8374,6 +8708,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -8696,6 +9031,7 @@
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.5.4.tgz",
|
||||
"integrity": "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.122.0",
|
||||
"@simplewebauthn/server": "^14.0.1",
|
||||
"@types/markdown-it": "^14.2.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cookie-parser": "^1.4.6",
|
||||
|
||||
@@ -9,6 +9,8 @@ export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
|
||||
? ColumnType<S, I | undefined, U>
|
||||
: ColumnType<T, T | undefined, T>;
|
||||
|
||||
export type Int8 = ColumnType<string, bigint | number | string, bigint | number | string>;
|
||||
|
||||
export type Json = JsonValue;
|
||||
|
||||
export type JsonArray = JsonValue[];
|
||||
@@ -71,6 +73,17 @@ export interface Checkouts {
|
||||
status: Generated<string>;
|
||||
}
|
||||
|
||||
export interface CustomerCredentials {
|
||||
created_at: Generated<Timestamp>;
|
||||
credential_id: string;
|
||||
customer_id: number;
|
||||
id: Generated<number>;
|
||||
last_used_at: Timestamp | null;
|
||||
public_key: string;
|
||||
signature_counter: Generated<Int8>;
|
||||
transports: string | null;
|
||||
}
|
||||
|
||||
export interface Customers {
|
||||
analytics_consent: Generated<boolean>;
|
||||
analytics_consent_at: Timestamp | null;
|
||||
@@ -212,6 +225,13 @@ export interface UploadLinks {
|
||||
token_hash: string;
|
||||
}
|
||||
|
||||
export interface WebauthnChallenges {
|
||||
challenge: string;
|
||||
customer_id: number | null;
|
||||
expires_at: Timestamp;
|
||||
kind: string;
|
||||
}
|
||||
|
||||
export interface DB {
|
||||
admin_settings: AdminSettings;
|
||||
cart_items: CartItems;
|
||||
@@ -219,6 +239,7 @@ export interface DB {
|
||||
categories: Categories;
|
||||
checkout_items: CheckoutItems;
|
||||
checkouts: Checkouts;
|
||||
customer_credentials: CustomerCredentials;
|
||||
customer_sessions: CustomerSessions;
|
||||
customer_tokens: CustomerTokens;
|
||||
customers: Customers;
|
||||
@@ -231,4 +252,5 @@ export interface DB {
|
||||
shipping_addresses: ShippingAddresses;
|
||||
tags: Tags;
|
||||
upload_links: UploadLinks;
|
||||
webauthn_challenges: WebauthnChallenges;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Who this application is, as far as WebAuthn is concerned (#37).
|
||||
*
|
||||
* ## Why this is derived rather than written down
|
||||
*
|
||||
* The Relying Party ID is a domain, and **a credential is bound to it
|
||||
* permanently**. A passkey registered against one RP ID cannot be used against
|
||||
* another — there is no migration, no re-signing, and no way to carry one over.
|
||||
* So the RP ID is the one piece of configuration that must never be wrong, and
|
||||
* must never be a value someone remembered to change.
|
||||
*
|
||||
* It comes from `PUBLIC_URL`, which is the same value every customer-facing
|
||||
* link is already built from. That makes the RP ID correct by construction in
|
||||
* any environment where mail works, and wrong only in environments where the
|
||||
* links were already wrong.
|
||||
*
|
||||
* ## The consequence worth stating plainly
|
||||
*
|
||||
* Each environment is a different Relying Party:
|
||||
*
|
||||
* | Environment | RP ID | Effect |
|
||||
* | --- | --- | --- |
|
||||
* | Local | `localhost` | A secure context by exception, so passkeys work |
|
||||
* | QA | the QA hostname | Registered here, usable only here |
|
||||
* | Production | the production hostname | Different credentials again |
|
||||
*
|
||||
* **QA can prove the flow and can never prove the credentials.** A passkey
|
||||
* registered in QA will not sign in to production, and that is correct rather
|
||||
* than a bug to work around.
|
||||
*
|
||||
* It also means **#313 destroys every passkey registered before it**. Moving to
|
||||
* `redefined-designs.com` changes the RP ID, so credentials bound to
|
||||
* `*.bermudalamb.synology.me` stop working at the cutover with no way back.
|
||||
* This code needs no change when that happens — it follows `PUBLIC_URL` — but
|
||||
* anyone who registered a passkey beforehand has to register it again. That is
|
||||
* free today, because production is not live and no real customer holds one,
|
||||
* and it stops being free the moment the shop opens.
|
||||
*/
|
||||
|
||||
/** Everything the ceremonies need to identify this Relying Party. */
|
||||
export interface RelyingParty {
|
||||
/** The RP ID: a bare domain, no scheme and no port. */
|
||||
id: string;
|
||||
/** Shown by the authenticator when it asks the customer to confirm. */
|
||||
name: string;
|
||||
/**
|
||||
* Origins a ceremony may legitimately come from.
|
||||
*
|
||||
* A list rather than one string because local development serves the app from
|
||||
* two: Vite on 5173 during `npm run dev`, and the backend on 3000 when the
|
||||
* built frontend is served by Express. Both are `localhost`, so both are the
|
||||
* same Relying Party — only the port differs, and the port is not part of the
|
||||
* RP ID. Deployed environments have exactly one.
|
||||
*/
|
||||
origins: string[];
|
||||
}
|
||||
|
||||
export const RELYING_PARTY_NAME = 'Redefined Designs';
|
||||
|
||||
/**
|
||||
* Local development, where `PUBLIC_URL` is legitimately unset.
|
||||
*
|
||||
* `envValidation` requires `PUBLIC_URL` only when SMTP is configured, so a local
|
||||
* setup that cannot send mail does not have it — and refusing to start there
|
||||
* would break every such setup to prevent nothing. `localhost` is a secure
|
||||
* context by exception in every browser that implements WebAuthn, so this works
|
||||
* without TLS.
|
||||
*/
|
||||
const LOCAL_ORIGINS = ['http://localhost:5173', 'http://localhost:3000'];
|
||||
|
||||
/**
|
||||
* The Relying Party for this environment.
|
||||
*
|
||||
* Takes the environment as an argument so it can be tested without touching
|
||||
* `process.env`, and reads it on each call rather than at import time: the
|
||||
* module would otherwise capture whatever was set when it was first required,
|
||||
* which in tests is whatever the previous suite happened to leave behind.
|
||||
*
|
||||
* Throws on a `PUBLIC_URL` that is set but unparseable. That is a deployment
|
||||
* that will also produce broken links in every email, so failing here is not
|
||||
* the first thing to go wrong — it is the first thing to *say so*.
|
||||
*/
|
||||
export function relyingParty(env: NodeJS.ProcessEnv = process.env): RelyingParty {
|
||||
const publicUrl = (env.PUBLIC_URL ?? '').trim();
|
||||
|
||||
if (publicUrl === '') {
|
||||
return { id: 'localhost', name: RELYING_PARTY_NAME, origins: LOCAL_ORIGINS };
|
||||
}
|
||||
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(publicUrl);
|
||||
} catch {
|
||||
throw new Error(
|
||||
`PUBLIC_URL is not a URL (${publicUrl}), so the WebAuthn Relying Party ID cannot be ` +
|
||||
'derived from it. Every passkey is bound permanently to that ID, so this is refused ' +
|
||||
'rather than guessed at.'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
// `hostname` rather than `host`: the RP ID is a domain and must not carry a
|
||||
// port. `host` includes one when the URL has it, and an RP ID of
|
||||
// "example.com:8443" matches nothing.
|
||||
id: parsed.hostname,
|
||||
name: RELYING_PARTY_NAME,
|
||||
// `origin` normalises away any path, trailing slash or default port, which
|
||||
// is exactly the string the browser will report.
|
||||
origins: [parsed.origin]
|
||||
};
|
||||
}
|
||||
@@ -42,7 +42,7 @@ describe('when the database loses its schema', () => {
|
||||
|
||||
// The count and a few names, not the whole list: the point is that a reader
|
||||
// can tell at a glance this is a missing schema rather than a logic bug.
|
||||
await expect(assertSchemaPresent('in a test')).rejects.toThrow(/Missing 18 of 18 tables/);
|
||||
await expect(assertSchemaPresent('in a test')).rejects.toThrow(/Missing 20 of 20 tables/);
|
||||
await expect(assertSchemaPresent('in a test')).rejects.toThrow(/items/);
|
||||
|
||||
await migrate();
|
||||
|
||||
@@ -37,6 +37,7 @@ const REQUIRED_TABLES = [
|
||||
'categories',
|
||||
'checkout_items',
|
||||
'checkouts',
|
||||
'customer_credentials',
|
||||
'customer_sessions',
|
||||
'customer_tokens',
|
||||
'customers',
|
||||
@@ -48,7 +49,8 @@ const REQUIRED_TABLES = [
|
||||
'orders',
|
||||
'shipping_addresses',
|
||||
'tags',
|
||||
'upload_links'
|
||||
'upload_links',
|
||||
'webauthn_challenges'
|
||||
] as const;
|
||||
|
||||
/** Which of them the database does not currently have. */
|
||||
@@ -151,6 +153,7 @@ export async function resetDb(): Promise<void> {
|
||||
await testPool.query(`
|
||||
TRUNCATE TABLE item_drafts, upload_links, orders, checkout_items, checkouts,
|
||||
shipping_addresses, cart_items, carts, customer_tokens, customer_sessions, favorites,
|
||||
webauthn_challenges, customer_credentials,
|
||||
customers, item_tags, item_images, items, tags, categories
|
||||
RESTART IDENTITY CASCADE
|
||||
`);
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { relyingParty, RELYING_PARTY_NAME } from '../../src/passkeys/relyingParty';
|
||||
|
||||
/**
|
||||
* The RP ID is the one value in this feature that cannot be corrected later: a
|
||||
* passkey is bound to it permanently, and a wrong one is only discovered when a
|
||||
* customer cannot sign in with a credential that no longer matches anything.
|
||||
*
|
||||
* So these assert the derivation itself rather than that the function returns
|
||||
* something. Most of them are about what must *not* end up in the ID.
|
||||
*/
|
||||
describe('relyingParty', () => {
|
||||
it('takes the RP ID from PUBLIC_URL', () => {
|
||||
const rp = relyingParty({ PUBLIC_URL: 'https://redefined-designs.com' });
|
||||
|
||||
expect(rp.id).toBe('redefined-designs.com');
|
||||
expect(rp.origins).toEqual(['https://redefined-designs.com']);
|
||||
expect(rp.name).toBe(RELYING_PARTY_NAME);
|
||||
});
|
||||
|
||||
it('keeps a subdomain, which is a different Relying Party', () => {
|
||||
// QA and production are separate RPs on purpose. A credential registered
|
||||
// against one does not work against the other, and collapsing them to the
|
||||
// registrable domain would silently make QA able to sign into production.
|
||||
const qa = relyingParty({ PUBLIC_URL: 'https://qa-redefined-designs.bermudalamb.synology.me' });
|
||||
const prod = relyingParty({ PUBLIC_URL: 'https://redefined-designs.bermudalamb.synology.me' });
|
||||
|
||||
expect(qa.id).toBe('qa-redefined-designs.bermudalamb.synology.me');
|
||||
expect(prod.id).toBe('redefined-designs.bermudalamb.synology.me');
|
||||
expect(qa.id).not.toBe(prod.id);
|
||||
});
|
||||
|
||||
it('strips the port, which is not part of an RP ID', () => {
|
||||
// An RP ID of "example.com:8443" matches nothing, and the failure is a
|
||||
// credential that cannot be used rather than an error at registration.
|
||||
const rp = relyingParty({ PUBLIC_URL: 'https://example.com:8443' });
|
||||
|
||||
expect(rp.id).toBe('example.com');
|
||||
expect(rp.origins).toEqual(['https://example.com:8443']);
|
||||
});
|
||||
|
||||
it('strips a path and a trailing slash from the origin', () => {
|
||||
const rp = relyingParty({ PUBLIC_URL: 'https://example.com/shop/' });
|
||||
|
||||
expect(rp.id).toBe('example.com');
|
||||
// The browser reports an origin, never a path, so a stored path would never
|
||||
// match what arrives.
|
||||
expect(rp.origins).toEqual(['https://example.com']);
|
||||
});
|
||||
|
||||
it.each(['', ' ', undefined])('falls back to localhost when PUBLIC_URL is %p', (value) => {
|
||||
// envValidation requires PUBLIC_URL only when SMTP is configured, so a local
|
||||
// setup that cannot send mail legitimately has none.
|
||||
const rp = relyingParty(value === undefined ? {} : { PUBLIC_URL: value });
|
||||
|
||||
expect(rp.id).toBe('localhost');
|
||||
// Both local origins, because the app is served from Vite during dev and
|
||||
// from Express once built, and they differ only by port.
|
||||
expect(rp.origins).toEqual(['http://localhost:5173', 'http://localhost:3000']);
|
||||
});
|
||||
|
||||
it('refuses a PUBLIC_URL that is not a URL rather than guessing', () => {
|
||||
// Guessing here would bind every passkey to something nobody chose, and the
|
||||
// binding is permanent.
|
||||
expect(() => relyingParty({ PUBLIC_URL: 'redefined-designs.com' })).toThrow(
|
||||
/Relying Party ID cannot be derived/
|
||||
);
|
||||
});
|
||||
|
||||
it('reads the environment on each call rather than at import', () => {
|
||||
// Captured at import, this would hold whatever the first suite to require
|
||||
// the module happened to have set.
|
||||
expect(relyingParty({ PUBLIC_URL: 'https://one.example' }).id).toBe('one.example');
|
||||
expect(relyingParty({ PUBLIC_URL: 'https://two.example' }).id).toBe('two.example');
|
||||
});
|
||||
});
|
||||
Generated
+25
-1
@@ -9,6 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.4.0",
|
||||
"@simplewebauthn/browser": "^14.0.0",
|
||||
"@uiw/react-md-editor": "^4.0.4",
|
||||
"antd": "^5.20.6",
|
||||
"react": "^18.3.1",
|
||||
@@ -37,6 +38,9 @@
|
||||
"vite": "^5.4.0",
|
||||
"vite-plugin-istanbul": "^6.0.2",
|
||||
"vitest": "^2.1.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/colors": {
|
||||
@@ -157,6 +161,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
|
||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
@@ -1678,6 +1683,12 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@simplewebauthn/browser": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-14.0.0.tgz",
|
||||
"integrity": "sha512-1odWVqeEBTl7lJ9zMKLEsmTlnyrDO5iRcTvfMKKk1WThUnp/i8JJdffdj2icP+tty159s4PgwE3BiMoEW9NFow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -1804,6 +1815,7 @@
|
||||
"version": "18.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
|
||||
"integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
@@ -1868,6 +1880,7 @@
|
||||
"integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.67.0",
|
||||
"@typescript-eslint/types": "8.67.0",
|
||||
@@ -2341,6 +2354,7 @@
|
||||
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -2751,6 +2765,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.11.12",
|
||||
"caniuse-lite": "^1.0.30001809",
|
||||
@@ -3199,7 +3214,8 @@
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.21",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz",
|
||||
"integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="
|
||||
"integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
@@ -3628,6 +3644,7 @@
|
||||
"integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -7133,6 +7150,7 @@
|
||||
"integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.14.0",
|
||||
"pg-pool": "^3.14.0",
|
||||
@@ -7236,6 +7254,7 @@
|
||||
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -8041,6 +8060,7 @@
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -8052,6 +8072,7 @@
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -9396,6 +9417,7 @@
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -9629,6 +9651,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
@@ -10185,6 +10208,7 @@
|
||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.4.0",
|
||||
"@simplewebauthn/browser": "^14.0.0",
|
||||
"@uiw/react-md-editor": "^4.0.4",
|
||||
"antd": "^5.20.6",
|
||||
"react": "^18.3.1",
|
||||
|
||||
Reference in New Issue
Block a user