@bongos/core 1.21.135 → 1.21.137
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/.bongos-core.json +63 -28
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +12 -0
- package/clients/bongos-client/index.cjs +12 -0
- package/clients/bongos-client/index.d.ts +20 -0
- package/clients/bongos-client/index.mjs +12 -0
- package/docs/api/openapi.json +349 -3
- package/docs/api-reference.md +8 -2
- package/docs/architecture.md +10 -0
- package/docs/copy-inventory.md +67 -51
- package/docs/copy-registry.json +199 -55
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +1 -0
- package/docs/page-readings.json +154 -149
- package/migrations/core_277_passkey_foundation.sql +53 -0
- package/modules/hall-ui/public/settings-passkeys.js +113 -0
- package/modules/hall-ui/public/settings.html +20 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/run-unit-tests.js +3 -0
- package/src/bongos/passkeys.js +153 -0
- package/src/bongos/routes/my-passkeys.js +249 -0
- package/src/bongos/routes.js +2 -0
- package/src/module-api.js +1 -1
- package/tests/my_passkeys.mjs +108 -0
- package/tests/own_scope_guard.mjs +5 -0
- package/tests/passkey_foundation_db.mjs +90 -0
- package/tests/passkey_pure.mjs +24 -0
package/src/bongos/routes.js
CHANGED
|
@@ -28,6 +28,7 @@ const buildMeRouter = require('./routes/me');
|
|
|
28
28
|
// task 1003341). Its own router rather than three more routes on the /me
|
|
29
29
|
// aggregator or the admin pair in routes/auth.js — see that file's header.
|
|
30
30
|
const buildMySessionsRouter = require('./routes/my-sessions');
|
|
31
|
+
const buildMyPasskeysRouter = require('./routes/my-passkeys');
|
|
31
32
|
const buildBuildersRouter = require('./routes/builders');
|
|
32
33
|
// The LIFECYCLE routes (tasks · claims · versions · done-when · goals ·
|
|
33
34
|
// dependencies · gate-approvals · analytics) carved into modules/lifecycle/
|
|
@@ -161,6 +162,7 @@ function buildGdsRouter() {
|
|
|
161
162
|
router.use(buildGithubRouter());
|
|
162
163
|
router.use(buildMeRouter());
|
|
163
164
|
router.use(buildMySessionsRouter());
|
|
165
|
+
router.use(buildMyPasskeysRouter());
|
|
164
166
|
router.use(buildBuildersRouter());
|
|
165
167
|
// tasks · claims · versions · done-when · goals · dependencies · analytics now
|
|
166
168
|
// mount via moduleLoader.mountModuleRoutes below (lifecycle module, BV1.R86) —
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.137'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// tests/my_passkeys.mjs — own-scoped card registration routes (task 1004746, ADR 0382).
|
|
2
|
+
// DB-free: a fake store and fake WebAuthn library behind the real router, over real HTTP.
|
|
3
|
+
// Pins: list/rename/remove are keyed on the CALLER's id; register/verify hands the
|
|
4
|
+
// library a challenge check that consumes via the store, bound to purpose 'register'
|
|
5
|
+
// and the caller; a failed verification stores nothing.
|
|
6
|
+
// Run: node tests/my_passkeys.mjs
|
|
7
|
+
import { strict as assert } from 'node:assert';
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import http from 'node:http';
|
|
10
|
+
delete process.env.PASSKEY_RP_ID; delete process.env.PASSKEY_ORIGIN;
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const express = require('express');
|
|
13
|
+
const build = require('../src/bongos/routes/my-passkeys');
|
|
14
|
+
|
|
15
|
+
const creds = [];
|
|
16
|
+
const consumed = [];
|
|
17
|
+
const store = {
|
|
18
|
+
listCredentials: async (b) => creds.filter((c) => c.builder_id === b),
|
|
19
|
+
getOrCreateUserHandle: async () => 'aGFuZGxl',
|
|
20
|
+
createChallenge: async () => ({ challenge: 'Y2hhbGw' }),
|
|
21
|
+
consumeChallenge: async (c, o) => { consumed.push([c, o]); return { purpose: 'register', builderId: o.expectBuilderId }; },
|
|
22
|
+
getCredentialById: async (id) => creds.find((c) => c.credential_id === id) || null,
|
|
23
|
+
addCredential: async (c) => { creds.push({ credential_id: c.credentialId, builder_id: c.builderId, nickname: c.nickname, transports: c.transports, created_at: 'x' }); },
|
|
24
|
+
renameCredential: async (b, id, n) => { const c = creds.find((x) => x.builder_id === b && x.credential_id === id); if (c) c.nickname = n; return !!c; },
|
|
25
|
+
removeCredential: async (b, id) => { const i = creds.findIndex((x) => x.builder_id === b && x.credential_id === id); if (i < 0) return false; creds.splice(i, 1); return true; },
|
|
26
|
+
};
|
|
27
|
+
let verifyOk = true;
|
|
28
|
+
const webauthn = {
|
|
29
|
+
generateRegistrationOptions: async (o) => ({ rp: { id: o.rpID }, excludeCredentials: o.excludeCredentials }),
|
|
30
|
+
verifyRegistrationResponse: async (o) => {
|
|
31
|
+
assert.equal(o.requireUserVerification, true);
|
|
32
|
+
if (!(await o.expectedChallenge('Y2hhbGw'))) throw new Error('challenge');
|
|
33
|
+
if (!verifyOk) return { verified: false };
|
|
34
|
+
return { verified: true, registrationInfo: { aaguid: 'a', credential: { id: 'cred1', publicKey: new Uint8Array([1]), counter: 0, transports: ['nfc'] } } };
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
const app = express();
|
|
38
|
+
app.use(express.json());
|
|
39
|
+
app.use((req, res, next) => { res.fail = (code, { status = 400, message } = {}) => res.status(status).json({ error: code, message }); next(); });
|
|
40
|
+
app.use((req, res, next) => { req.builder = { id: '7', github_login: 'me' }; next(); });
|
|
41
|
+
app.use('/', build({ store, webauthn, requireBuilder: (q, r, n) => n() }));
|
|
42
|
+
const server = http.createServer(app).listen(0);
|
|
43
|
+
const port = server.address().port;
|
|
44
|
+
const call = (method, path, body) => new Promise((resolve) => {
|
|
45
|
+
const r = http.request({ port, path, method, headers: { 'content-type': 'application/json', host: 'localhost' } }, (res) => {
|
|
46
|
+
let d = ''; res.on('data', (c) => (d += c)); res.on('end', () => { let b = null; try { b = JSON.parse(d); } catch { /* not json */ } resolve({ status: res.statusCode, body: b }); });
|
|
47
|
+
});
|
|
48
|
+
if (body) r.write(JSON.stringify(body));
|
|
49
|
+
r.end();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
let n = 0;
|
|
53
|
+
const ok = (name) => { n++; console.log(`ok - ${name}`); };
|
|
54
|
+
|
|
55
|
+
let r = await call('POST', '/me/passkeys/register/options', {});
|
|
56
|
+
assert.equal(r.status, 200); assert.equal(r.body.options.rp.id, 'localhost'); ok('options issues for caller');
|
|
57
|
+
|
|
58
|
+
verifyOk = false;
|
|
59
|
+
r = await call('POST', '/me/passkeys/register/verify', { response: { id: 'x' } });
|
|
60
|
+
assert.equal(r.status, 400); assert.equal(creds.length, 0); ok('failed verification stores nothing');
|
|
61
|
+
|
|
62
|
+
verifyOk = true;
|
|
63
|
+
r = await call('POST', '/me/passkeys/register/verify', { response: { id: 'x' }, nickname: ' My card ' });
|
|
64
|
+
assert.equal(r.status, 200); assert.equal(creds[0].nickname, 'My card'); assert.equal(creds[0].builder_id, '7');
|
|
65
|
+
assert.deepEqual(consumed.at(-1)[1], { expectPurpose: 'register', expectBuilderId: '7' }); ok('verify stores; challenge bound to caller and purpose');
|
|
66
|
+
|
|
67
|
+
r = await call('POST', '/me/passkeys/register/verify', { response: { id: 'x' } });
|
|
68
|
+
assert.equal(r.status, 409); ok('duplicate card refused');
|
|
69
|
+
|
|
70
|
+
creds.push({ credential_id: 'other', builder_id: '9', nickname: 'theirs' });
|
|
71
|
+
r = await call('POST', '/me/passkeys/remove', { credential_id: 'other' });
|
|
72
|
+
assert.equal(r.status, 404); assert.equal(creds.length, 2); ok("cannot remove another builder's card");
|
|
73
|
+
r = await call('POST', '/me/passkeys/rename', { credential_id: 'other', nickname: 'mine now' });
|
|
74
|
+
assert.equal(r.status, 404); assert.equal(creds[1].nickname, 'theirs'); ok("cannot rename another builder's card");
|
|
75
|
+
|
|
76
|
+
r = await call('GET', '/me/passkeys');
|
|
77
|
+
assert.equal(r.body.passkeys.length, 1); assert.ok(!('public_key' in r.body.passkeys[0])); ok('list is own-only, no key material');
|
|
78
|
+
|
|
79
|
+
r = await call('POST', '/me/passkeys/rename', { credential_id: 'cred1', nickname: 'x'.repeat(61) });
|
|
80
|
+
assert.equal(r.status, 400); ok('long nickname refused');
|
|
81
|
+
r = await call('POST', '/me/passkeys/rename', { credential_id: 'cred1', nickname: 'Desk' });
|
|
82
|
+
assert.equal(r.status, 200); assert.equal(creds[0].nickname, 'Desk'); ok('rename');
|
|
83
|
+
r = await call('POST', '/me/passkeys/remove', { credential_id: 'cred1' });
|
|
84
|
+
assert.equal(r.status, 200); ok('remove');
|
|
85
|
+
|
|
86
|
+
r = await call('GET', '/me/passkeys/browser.js');
|
|
87
|
+
assert.equal(r.status, 200); ok('browser bundle is served');
|
|
88
|
+
|
|
89
|
+
r = await call('POST', '/me/passkeys/rename', { credential_id: 'x' });
|
|
90
|
+
assert.equal(r.status, 400); ok('rename without a nickname is refused');
|
|
91
|
+
|
|
92
|
+
// Past the cap: options and verify both refuse.
|
|
93
|
+
creds.length = 0;
|
|
94
|
+
for (let i = 0; i < 10; i++) creds.push({ credential_id: 'c' + i, builder_id: '7' });
|
|
95
|
+
r = await call('POST', '/me/passkeys/register/options', {});
|
|
96
|
+
assert.equal(r.status, 409);
|
|
97
|
+
r = await call('POST', '/me/passkeys/register/verify', { response: { id: 'x' } });
|
|
98
|
+
assert.equal(r.status, 409); assert.equal(creds.length, 10); ok('card cap enforced on options and verify');
|
|
99
|
+
|
|
100
|
+
// A non-local host with no configured RP is refused, not guessed.
|
|
101
|
+
r = await new Promise((resolve) => {
|
|
102
|
+
const q = http.request({ port, path: '/me/passkeys/register/options', method: 'POST', headers: { 'content-type': 'application/json', host: 'evil.example', 'x-forwarded-host': 'evil.example' } }, (res) => { res.resume(); resolve({ status: res.statusCode }); });
|
|
103
|
+
q.write('{}'); q.end();
|
|
104
|
+
});
|
|
105
|
+
assert.equal(r.status, 503); ok('unconfigured non-local host refused');
|
|
106
|
+
|
|
107
|
+
server.close();
|
|
108
|
+
console.log(`${n} passed`);
|
|
@@ -84,6 +84,11 @@ const OWN_SCOPED_SURFACES = [
|
|
|
84
84
|
why: "the caller's OWN auth sessions — list, revoke one, sign out everywhere (task 1003341, ADR 0206). Every statement carries builder_id = req.builder.id, so a session id belonging to somebody else matches no row. Kept OUT of routes/auth.js precisely so this whole file can be held to the own-scope rule: the admin cross-builder pair next door takes its target from the body, which is what makes it admin.",
|
|
85
85
|
crossOwner: [],
|
|
86
86
|
},
|
|
87
|
+
{
|
|
88
|
+
file: 'src/bongos/routes/my-passkeys.js',
|
|
89
|
+
why: "the caller's OWN hardware security cards — list, register, rename, remove (task 1004746, ADR 0382). Every store call is keyed on req.builder.id; a credential id belonging to somebody else matches no row. The one ungated route serves a vendored static library file.",
|
|
90
|
+
crossOwner: [],
|
|
91
|
+
},
|
|
87
92
|
{
|
|
88
93
|
file: 'modules/specialities/routes/specialities.js',
|
|
89
94
|
why: "role sub-specialities (task 1003855, ADR 0296). The own-scoped route here is GET /me/specialities — the CALLER's own adoptions, taken from req.builder.id with nothing read off the request. The rest of the router is /specialities/* rather than /me/*, and those are ownership-checked per row against req.builder.id after re-reading the speciality server-side, never from a client-supplied owner id.",
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// tests/passkey_foundation_db.mjs — task 1004750 (ADR 0382 D1, D6). Real DB.
|
|
2
|
+
// The passkey tables built from the real migration in a throwaway schema, and the
|
|
3
|
+
// store's rules: single-use challenges, TTL, sweep, unique credential ids, sign-count.
|
|
4
|
+
// Self-skips without a reachable Postgres.
|
|
5
|
+
// Run: DATABASE_URL=... node tests/passkey_foundation_db.mjs
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { strict as assert } from 'node:assert';
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const { Pool } = require('pg');
|
|
13
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
|
|
15
|
+
const pgPool = new Pool({ connectionString: process.env.DATABASE_URL || undefined, max: 1 });
|
|
16
|
+
let client;
|
|
17
|
+
try { client = await pgPool.connect(); } catch (e) {
|
|
18
|
+
console.log(`passkey_foundation_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
|
|
19
|
+
await pgPool.end().catch(() => {});
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
const SCHEMA = `t1004750_${process.pid}`;
|
|
23
|
+
let pass = 0;
|
|
24
|
+
const ok = (c, m) => { assert.ok(c, m); pass++; console.log(` ok - ${m}`); };
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
await client.query(`CREATE SCHEMA ${SCHEMA}`);
|
|
28
|
+
await client.query(`SET search_path TO ${SCHEMA}`);
|
|
29
|
+
await client.query('CREATE TABLE builders (id bigserial PRIMARY KEY)');
|
|
30
|
+
await client.query('CREATE TABLE schema_migrations (version text PRIMARY KEY)');
|
|
31
|
+
await client.query(readFileSync(join(here, '..', 'migrations', 'core_277_passkey_foundation.sql'), 'utf8'));
|
|
32
|
+
await client.query('INSERT INTO builders DEFAULT VALUES');
|
|
33
|
+
await client.query('INSERT INTO builders DEFAULT VALUES');
|
|
34
|
+
|
|
35
|
+
const pk = require('../src/bongos/passkeys.js');
|
|
36
|
+
const o = { pool: client };
|
|
37
|
+
|
|
38
|
+
const c = await pk.createChallenge({ purpose: 'register', builderId: 1, ...o });
|
|
39
|
+
ok(typeof c.challenge === 'string' && c.challenge.length >= 40, 'challenge is long random base64url');
|
|
40
|
+
const got = await pk.consumeChallenge(c.challenge, o);
|
|
41
|
+
ok(got && got.purpose === 'register' && got.builderId === '1', 'a live challenge is consumed once');
|
|
42
|
+
ok((await pk.consumeChallenge(c.challenge, o)) === null, 'a second consume (replay) finds nothing');
|
|
43
|
+
|
|
44
|
+
const expire = (ch) => client.query("UPDATE passkey_challenges SET expires_at = now() - interval '1 second' WHERE challenge = $1", [ch]);
|
|
45
|
+
const old = await pk.createChallenge({ purpose: 'authenticate', ...o });
|
|
46
|
+
await expire(old.challenge);
|
|
47
|
+
ok((await pk.consumeChallenge(old.challenge, o)) === null, 'an expired challenge is refused');
|
|
48
|
+
const stale = await pk.createChallenge({ purpose: 'authenticate', ...o });
|
|
49
|
+
await expire(stale.challenge);
|
|
50
|
+
const live = await pk.createChallenge({ purpose: 'authenticate', ...o });
|
|
51
|
+
ok((await client.query('SELECT 1 FROM passkey_challenges WHERE challenge = $1', [stale.challenge])).rowCount === 0, 'issuing a challenge sweeps expired rows');
|
|
52
|
+
const stale2 = await pk.createChallenge({ purpose: 'authenticate', ...o });
|
|
53
|
+
await expire(stale2.challenge);
|
|
54
|
+
ok((await pk.sweepExpiredChallenges(o)) === 1, 'sweep removes only expired challenges');
|
|
55
|
+
ok((await pk.consumeChallenge(live.challenge, o)) !== null, 'sweep left the live one');
|
|
56
|
+
await assert.rejects(pk.createChallenge({ purpose: 'nope', ...o }));
|
|
57
|
+
pass++; console.log(' ok - unknown purpose rejected');
|
|
58
|
+
await assert.rejects(client.query("INSERT INTO passkey_challenges (challenge, purpose, expires_at) VALUES ('x','bogus', now())"));
|
|
59
|
+
pass++; console.log(' ok - the table itself refuses an unknown purpose');
|
|
60
|
+
|
|
61
|
+
const wrong = await pk.createChallenge({ purpose: 'register', builderId: 1, ...o });
|
|
62
|
+
ok((await pk.consumeChallenge(wrong.challenge, { ...o, expectPurpose: 'authenticate' })) === null, 'a challenge for another purpose is refused');
|
|
63
|
+
ok((await pk.consumeChallenge(wrong.challenge, o)) === null, 'and it is spent by the wrong presentation');
|
|
64
|
+
const b1 = await pk.createChallenge({ purpose: 'register', builderId: 1, ...o });
|
|
65
|
+
ok((await pk.consumeChallenge(b1.challenge, { ...o, expectBuilderId: 2 })) === null, 'a challenge for another builder is refused');
|
|
66
|
+
const b2 = await pk.createChallenge({ purpose: 'register', builderId: 1, ...o });
|
|
67
|
+
ok((await pk.consumeChallenge(b2.challenge, { ...o, expectPurpose: 'register', expectBuilderId: 1 })) !== null, 'matching purpose and builder pass');
|
|
68
|
+
|
|
69
|
+
const h1 = await pk.getOrCreateUserHandle(1, o);
|
|
70
|
+
ok(h1 === await pk.getOrCreateUserHandle(1, o) && h1 !== await pk.getOrCreateUserHandle(2, o), 'user handle is stable per builder and distinct');
|
|
71
|
+
|
|
72
|
+
await pk.addCredential({ builderId: 1, credentialId: 'cred-a', publicKey: Buffer.from([1, 2, 3]), transports: ['usb'], nickname: 'card' }, o);
|
|
73
|
+
await assert.rejects(pk.addCredential({ builderId: 2, credentialId: 'cred-a', publicKey: Buffer.from([9]) }, o));
|
|
74
|
+
pass++; console.log(' ok - a credential id belongs to one builder');
|
|
75
|
+
ok((await pk.listCredentials(1, o)).length === 1 && (await pk.listCredentials(2, o)).length === 0, 'credentials listed per builder');
|
|
76
|
+
|
|
77
|
+
ok((await pk.recordUse('cred-a', 5, o)).ok, 'sign count advances');
|
|
78
|
+
ok((await pk.recordUse('cred-a', 5, o)).reason === 'sign_count_regressed', 'a non-advancing counter is refused');
|
|
79
|
+
ok((await pk.recordUse('cred-a', 3, o)).reason === 'sign_count_regressed' && Number((await pk.getCredentialById('cred-a', o)).sign_count) === 5, 'a lower counter never overwrites the stored one');
|
|
80
|
+
ok((await pk.recordUse('nope', 1, o)).reason === 'unknown_credential', 'unknown credential reported');
|
|
81
|
+
ok((await pk.getCredentialById('cred-a', o)).last_used_at != null, 'last_used_at is stamped');
|
|
82
|
+
ok(!(await pk.removeCredential(2, 'cred-a', o)), 'another builder cannot remove it');
|
|
83
|
+
ok(await pk.removeCredential(1, 'cred-a', o), 'owner removes it');
|
|
84
|
+
console.log(`passkey_foundation_db.mjs: ${pass} passed`);
|
|
85
|
+
} finally {
|
|
86
|
+
await client.query('SET search_path TO public').catch(() => {});
|
|
87
|
+
await client.query(`DROP SCHEMA IF EXISTS ${SCHEMA} CASCADE`).catch(() => {});
|
|
88
|
+
client.release();
|
|
89
|
+
await pgPool.end();
|
|
90
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// tests/passkey_pure.mjs — task 1004750 (ADR 0382 D6). DB-free: the pure rules of
|
|
2
|
+
// src/bongos/passkeys.js that the required unit lane can run. The real-SQL half is
|
|
3
|
+
// tests/passkey_foundation_db.mjs.
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { strict as assert } from 'node:assert';
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const pk = require('../src/bongos/passkeys.js');
|
|
8
|
+
|
|
9
|
+
let pass = 0;
|
|
10
|
+
const ok = (c, m) => { assert.ok(c, m); pass++; console.log(` ok - ${m}`); };
|
|
11
|
+
|
|
12
|
+
ok(pk.PURPOSES.includes('register') && pk.PURPOSES.includes('authenticate') && pk.PURPOSES.length === 2, 'two ceremony purposes');
|
|
13
|
+
ok(pk.clampTtlSeconds(60) === 60, 'a sane TTL passes through');
|
|
14
|
+
ok(pk.clampTtlSeconds(-5) === pk.CHALLENGE_TTL_SECONDS, 'a negative TTL falls back to the default');
|
|
15
|
+
ok(pk.clampTtlSeconds('abc') === pk.CHALLENGE_TTL_SECONDS, 'a non-number falls back to the default');
|
|
16
|
+
ok(pk.clampTtlSeconds(86400) === pk.MAX_CHALLENGE_TTL_SECONDS, 'a huge TTL is capped');
|
|
17
|
+
ok(pk.counterAdvances(0, 0), 'a card with no counter is accepted');
|
|
18
|
+
ok(pk.counterAdvances(5, 6), 'an advancing counter is accepted');
|
|
19
|
+
ok(!pk.counterAdvances(5, 5), 'a repeated counter is refused');
|
|
20
|
+
ok(!pk.counterAdvances(5, 3), 'a lower counter is refused');
|
|
21
|
+
ok(!pk.counterAdvances(5, 0), 'a counter reset to zero is refused');
|
|
22
|
+
await assert.rejects(pk.createChallenge({ purpose: 'bogus', pool: { query: async () => { throw new Error('should not query'); } } }));
|
|
23
|
+
pass++; console.log(' ok - an unknown purpose is rejected before any query');
|
|
24
|
+
console.log(`passkey_pure.mjs: ${pass} passed`);
|