@things-factory/auth-ui 10.1.250 → 10.1.254

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.
@@ -7,18 +7,13 @@ import type { VerificationOutcome } from './webauthn-verification.js';
7
7
  * rename it; this is only what the list shows until they do.
8
8
  */
9
9
  export declare function deviceLabel(userAgent: string): string;
10
- export type SigninAnswer = {
11
- outcome: 'signed-in';
12
- redirectURL: string;
13
- } | {
14
- outcome: 'refused';
15
- message: string;
16
- };
17
10
  /**
18
- * What the passkey sign-in answered. A refusal comes back as 401 with the refusal, already in the
19
- * person's language, as a plain-text body (`authenticate401Middleware`). Never throws.
11
+ * The passkey sign-in, posted as a form the way the password is (ADR-0156 decision 4): the browser's
12
+ * answer as JSON text and where to go after. The page that comes back is the password's - a refusal
13
+ * included, so a locked account lands on the activate page and every other refusal on this screen with
14
+ * its one line.
20
15
  */
21
- export declare function readSigninAnswer(response: Response): Promise<SigninAnswer>;
16
+ export declare function passkeySigninForm(document: Document, response: unknown, redirectTo: string): HTMLFormElement;
22
17
  /**
23
18
  * The browser said no: the person closed the prompt, or let it time out. Nothing to report - they
24
19
  * know. Other errors are worth a word.
@@ -38,25 +38,24 @@ export function deviceLabel(userAgent) {
38
38
  return [system, browser].filter(Boolean).join(' · ');
39
39
  }
40
40
  /**
41
- * What the passkey sign-in answered. A refusal comes back as 401 with the refusal, already in the
42
- * person's language, as a plain-text body (`authenticate401Middleware`). Never throws.
41
+ * The passkey sign-in, posted as a form the way the password is (ADR-0156 decision 4): the browser's
42
+ * answer as JSON text and where to go after. The page that comes back is the password's - a refusal
43
+ * included, so a locked account lands on the activate page and every other refusal on this screen with
44
+ * its one line.
43
45
  */
44
- export async function readSigninAnswer(response) {
45
- let body = '';
46
- try {
47
- body = await response.text();
46
+ export function passkeySigninForm(document, response, redirectTo) {
47
+ const form = document.createElement('form');
48
+ form.method = 'post';
49
+ form.action = '/auth/signin-webauthn';
50
+ form.hidden = true;
51
+ for (const [name, value] of Object.entries({ response: JSON.stringify(response), redirectTo })) {
52
+ const input = document.createElement('input');
53
+ input.type = 'hidden';
54
+ input.name = name;
55
+ input.value = value;
56
+ form.appendChild(input);
48
57
  }
49
- catch { }
50
- if (response.ok) {
51
- try {
52
- const parsed = JSON.parse(body);
53
- if (parsed?.verified === true && typeof parsed.redirectURL === 'string') {
54
- return { outcome: 'signed-in', redirectURL: parsed.redirectURL };
55
- }
56
- }
57
- catch { }
58
- }
59
- return { outcome: 'refused', message: body.trim() };
58
+ return form;
60
59
  }
61
60
  /**
62
61
  * The browser said no: the person closed the prompt, or let it time out. Nothing to report - they
@@ -1 +1 @@
1
- {"version":3,"file":"passkey.js","sourceRoot":"","sources":["../../client/utils/passkey.ts"],"names":[],"mappings":"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","sourcesContent":["import type { VerificationOutcome } from './webauthn-verification.js'\n\n/**\n * The rules the passkey screens follow (ADR-0156), apart from the screens so they are tested.\n */\n\n/**\n * A label for a passkey made here - the system and the browser, 「macOS · Chrome」. The person can\n * rename it; this is only what the list shows until they do.\n */\nexport function deviceLabel(userAgent: string): string {\n const system = /iPhone/.test(userAgent)\n ? 'iPhone'\n : /iPad/.test(userAgent)\n ? 'iPad'\n : /Android/.test(userAgent)\n ? 'Android'\n : /Windows/.test(userAgent)\n ? 'Windows'\n : /Mac OS X|Macintosh/.test(userAgent)\n ? 'macOS'\n : /CrOS/.test(userAgent)\n ? 'ChromeOS'\n : /Linux/.test(userAgent)\n ? 'Linux'\n : ''\n\n /* Order matters: Edge and Opera say Chrome too, Chrome says Safari too. */\n const browser = /Edg\\//.test(userAgent)\n ? 'Edge'\n : /OPR\\//.test(userAgent)\n ? 'Opera'\n : /SamsungBrowser\\//.test(userAgent)\n ? 'Samsung Internet'\n : /Firefox\\/|FxiOS\\//.test(userAgent)\n ? 'Firefox'\n : /Chrome\\/|CriOS\\//.test(userAgent)\n ? 'Chrome'\n : /Safari\\//.test(userAgent)\n ? 'Safari'\n : ''\n\n return [system, browser].filter(Boolean).join(' · ')\n}\n\nexport type SigninAnswer = { outcome: 'signed-in'; redirectURL: string } | { outcome: 'refused'; message: string }\n\n/**\n * What the passkey sign-in answered. A refusal comes back as 401 with the refusal, already in the\n * person's language, as a plain-text body (`authenticate401Middleware`). Never throws.\n */\nexport async function readSigninAnswer(response: Response): Promise<SigninAnswer> {\n let body = ''\n try {\n body = await response.text()\n } catch {}\n\n if (response.ok) {\n try {\n const parsed = JSON.parse(body)\n if (parsed?.verified === true && typeof parsed.redirectURL === 'string') {\n return { outcome: 'signed-in', redirectURL: parsed.redirectURL }\n }\n } catch {}\n }\n\n return { outcome: 'refused', message: body.trim() }\n}\n\n/**\n * The browser said no: the person closed the prompt, or let it time out. Nothing to report - they\n * know. Other errors are worth a word.\n */\nexport function wasCancelled(error: any): boolean {\n return error?.name === 'NotAllowedError' || error?.name === 'AbortError' || error?.code === 'ERROR_CEREMONY_ABORTED'\n}\n\n/** The authenticator already holds one of this person's passkeys for this site. */\nexport function alreadyRegistered(error: any): boolean {\n return error?.name === 'InvalidStateError' || error?.code === 'ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED'\n}\n\n/**\n * Where this browser keeps that the offer is settled for a person - they said 「do not ask again on\n * this device」, or a passkey of theirs is on this device now. Per browser, per person.\n */\nexport function offerSettledKey(username: string): string {\n return `passkey-offer-settled:${username}`\n}\n\n/**\n * Whether the offer screen has anything to offer. It needs an authenticator built into this device -\n * a person on someone else's computer is not asked to put a passkey on it - and the offer must not be\n * settled on this browser.\n */\nexport function shouldOffer({ platformAuthenticator, settled }: { platformAuthenticator: boolean; settled: boolean }): boolean {\n return platformAuthenticator && !settled\n}\n\n/**\n * Where the offer stands after the browser was asked to make a passkey. A passkey on this device -\n * made just now, or already there - settles the offer here, as a no does: asking again at the next\n * sign-in would only make the person register the same device once more. A closed prompt leaves the\n * offer open; a refusal by the server, or anything else, is worth a word.\n */\nexport function registrationEnd({\n verification,\n error\n}: {\n verification?: VerificationOutcome\n error?: any\n}): 'settled' | 'cancelled' | 'refused' | 'failed' {\n if (verification) return verification.outcome === 'registered' ? 'settled' : 'refused'\n if (wasCancelled(error)) return 'cancelled'\n if (alreadyRegistered(error)) return 'settled'\n return 'failed'\n}\n"]}
1
+ {"version":3,"file":"passkey.js","sourceRoot":"","sources":["../../client/utils/passkey.ts"],"names":[],"mappings":"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","sourcesContent":["import type { VerificationOutcome } from './webauthn-verification.js'\n\n/**\n * The rules the passkey screens follow (ADR-0156), apart from the screens so they are tested.\n */\n\n/**\n * A label for a passkey made here - the system and the browser, 「macOS · Chrome」. The person can\n * rename it; this is only what the list shows until they do.\n */\nexport function deviceLabel(userAgent: string): string {\n const system = /iPhone/.test(userAgent)\n ? 'iPhone'\n : /iPad/.test(userAgent)\n ? 'iPad'\n : /Android/.test(userAgent)\n ? 'Android'\n : /Windows/.test(userAgent)\n ? 'Windows'\n : /Mac OS X|Macintosh/.test(userAgent)\n ? 'macOS'\n : /CrOS/.test(userAgent)\n ? 'ChromeOS'\n : /Linux/.test(userAgent)\n ? 'Linux'\n : ''\n\n /* Order matters: Edge and Opera say Chrome too, Chrome says Safari too. */\n const browser = /Edg\\//.test(userAgent)\n ? 'Edge'\n : /OPR\\//.test(userAgent)\n ? 'Opera'\n : /SamsungBrowser\\//.test(userAgent)\n ? 'Samsung Internet'\n : /Firefox\\/|FxiOS\\//.test(userAgent)\n ? 'Firefox'\n : /Chrome\\/|CriOS\\//.test(userAgent)\n ? 'Chrome'\n : /Safari\\//.test(userAgent)\n ? 'Safari'\n : ''\n\n return [system, browser].filter(Boolean).join(' · ')\n}\n\n/**\n * The passkey sign-in, posted as a form the way the password is (ADR-0156 decision 4): the browser's\n * answer as JSON text and where to go after. The page that comes back is the password's - a refusal\n * included, so a locked account lands on the activate page and every other refusal on this screen with\n * its one line.\n */\nexport function passkeySigninForm(document: Document, response: unknown, redirectTo: string): HTMLFormElement {\n const form = document.createElement('form')\n form.method = 'post'\n form.action = '/auth/signin-webauthn'\n form.hidden = true\n\n for (const [name, value] of Object.entries({ response: JSON.stringify(response), redirectTo })) {\n const input = document.createElement('input')\n input.type = 'hidden'\n input.name = name\n input.value = value\n form.appendChild(input)\n }\n\n return form\n}\n\n/**\n * The browser said no: the person closed the prompt, or let it time out. Nothing to report - they\n * know. Other errors are worth a word.\n */\nexport function wasCancelled(error: any): boolean {\n return error?.name === 'NotAllowedError' || error?.name === 'AbortError' || error?.code === 'ERROR_CEREMONY_ABORTED'\n}\n\n/** The authenticator already holds one of this person's passkeys for this site. */\nexport function alreadyRegistered(error: any): boolean {\n return error?.name === 'InvalidStateError' || error?.code === 'ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED'\n}\n\n/**\n * Where this browser keeps that the offer is settled for a person - they said 「do not ask again on\n * this device」, or a passkey of theirs is on this device now. Per browser, per person.\n */\nexport function offerSettledKey(username: string): string {\n return `passkey-offer-settled:${username}`\n}\n\n/**\n * Whether the offer screen has anything to offer. It needs an authenticator built into this device -\n * a person on someone else's computer is not asked to put a passkey on it - and the offer must not be\n * settled on this browser.\n */\nexport function shouldOffer({ platformAuthenticator, settled }: { platformAuthenticator: boolean; settled: boolean }): boolean {\n return platformAuthenticator && !settled\n}\n\n/**\n * Where the offer stands after the browser was asked to make a passkey. A passkey on this device -\n * made just now, or already there - settles the offer here, as a no does: asking again at the next\n * sign-in would only make the person register the same device once more. A closed prompt leaves the\n * offer open; a refusal by the server, or anything else, is worth a word.\n */\nexport function registrationEnd({\n verification,\n error\n}: {\n verification?: VerificationOutcome\n error?: any\n}): 'settled' | 'cancelled' | 'refused' | 'failed' {\n if (verification) return verification.outcome === 'registered' ? 'settled' : 'refused'\n if (wasCancelled(error)) return 'cancelled'\n if (alreadyRegistered(error)) return 'settled'\n return 'failed'\n}\n"]}
@@ -1,16 +1,14 @@
1
- import { alreadyRegistered, deviceLabel, offerSettledKey, readSigninAnswer, registrationEnd, shouldOffer, wasCancelled } from './passkey';
2
- /** Just enough of `Response` for the reader. */
3
- function response({ ok = true, status = 200, body = '', unreadable = false } = {}) {
4
- return {
5
- ok,
6
- status,
7
- text: async () => {
8
- if (unreadable)
9
- throw new TypeError('body already consumed');
10
- return body;
1
+ import { alreadyRegistered, deviceLabel, offerSettledKey, passkeySigninForm, registrationEnd, shouldOffer, wasCancelled } from './passkey';
2
+ /** Just enough of `Document` for the form: elements are plain objects that keep their children. */
3
+ const fakeDocument = {
4
+ createElement: (tagName) => ({
5
+ tagName,
6
+ children: [],
7
+ appendChild(child) {
8
+ this.children.push(child);
11
9
  }
12
- };
13
- }
10
+ })
11
+ };
14
12
  describe('a label for a passkey made here', () => {
15
13
  it.each([
16
14
  ['Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36', 'macOS · Chrome'],
@@ -26,26 +24,16 @@ describe('a label for a passkey made here', () => {
26
24
  expect(deviceLabel('curl/8.0')).toBe('');
27
25
  });
28
26
  });
29
- describe('what the passkey sign-in answered', () => {
30
- it('signed in, with where to go next', async () => {
31
- expect(await readSigninAnswer(response({ body: '{"verified":true,"redirectURL":"/auth/checkin?redirect_to=%2F"}' }))).toEqual({
32
- outcome: 'signed-in',
33
- redirectURL: '/auth/checkin?redirect_to=%2F'
34
- });
27
+ describe('the passkey sign-in is posted as a form, like the password', () => {
28
+ const form = passkeySigninForm(fakeDocument, { id: 'cred-1', response: { signature: 'sig' } }, '/office');
29
+ it('goes to the sign-in route as a page request, so a refusal comes back as a page and never as a toast body', () => {
30
+ expect([form.method, form.action, form.hidden]).toEqual(['post', '/auth/signin-webauthn', true]);
35
31
  });
36
- it("a refusal carries the server's words, already in the person's language", async () => {
37
- expect(await readSigninAnswer(response({ ok: false, status: 401, body: '계정이 잠겨 있습니다.' }))).toEqual({
38
- outcome: 'refused',
39
- message: '계정이 잠겨 있습니다.'
40
- });
41
- });
42
- it('an answer that is ok but not a sign-in is not taken for one', async () => {
43
- expect((await readSigninAnswer(response({ body: '{"verified":false}' }))).outcome).toBe('refused');
44
- expect((await readSigninAnswer(response({ body: '<html>' }))).outcome).toBe('refused');
45
- expect((await readSigninAnswer(response({ ok: false, status: 401, body: '{"verified":true,"redirectURL":"/x"}' }))).outcome).toBe('refused');
46
- });
47
- it('an unreadable body is a refusal with nothing to say, never a throw', async () => {
48
- expect(await readSigninAnswer(response({ unreadable: true }))).toEqual({ outcome: 'refused', message: '' });
32
+ it("carries the browser's answer as JSON text and where to go after", () => {
33
+ const fields = Object.fromEntries(form.children.map((input) => [input.name, input.value]));
34
+ expect(form.children.every((input) => input.type === 'hidden')).toBe(true);
35
+ expect(JSON.parse(fields.response)).toEqual({ id: 'cred-1', response: { signature: 'sig' } });
36
+ expect(fields.redirectTo).toBe('/office');
49
37
  });
50
38
  });
51
39
  describe('what the browser said', () => {
@@ -1 +1 @@
1
- {"version":3,"file":"passkey.test.js","sourceRoot":"","sources":["../../client/utils/passkey.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { alreadyRegistered, deviceLabel, offerSettledKey, readSigninAnswer, registrationEnd, shouldOffer, wasCancelled } from './passkey'\n\n/** Just enough of `Response` for the reader. */\nfunction response({ ok = true, status = 200, body = '', unreadable = false } = {}): Response {\n return {\n ok,\n status,\n text: async () => {\n if (unreadable) throw new TypeError('body already consumed')\n return body\n }\n } as unknown as Response\n}\n\ndescribe('a label for a passkey made here', () => {\n it.each([\n ['Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36', 'macOS · Chrome'],\n ['Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15', 'macOS · Safari'],\n ['Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0', 'Windows · Edge'],\n ['Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1', 'iPhone · Safari'],\n ['Mozilla/5.0 (Linux; Android 14; SM-S918N) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/26.0 Chrome/122.0.0.0 Mobile Safari/537.36', 'Android · Samsung Internet'],\n ['Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0', 'Linux · Firefox']\n ])('%s', (userAgent, label) => {\n expect(deviceLabel(userAgent)).toBe(label)\n })\n\n it('says nothing it cannot tell', () => {\n expect(deviceLabel('curl/8.0')).toBe('')\n })\n})\n\ndescribe('what the passkey sign-in answered', () => {\n it('signed in, with where to go next', async () => {\n expect(await readSigninAnswer(response({ body: '{\"verified\":true,\"redirectURL\":\"/auth/checkin?redirect_to=%2F\"}' }))).toEqual({\n outcome: 'signed-in',\n redirectURL: '/auth/checkin?redirect_to=%2F'\n })\n })\n\n it(\"a refusal carries the server's words, already in the person's language\", async () => {\n expect(await readSigninAnswer(response({ ok: false, status: 401, body: '계정이 잠겨 있습니다.' }))).toEqual({\n outcome: 'refused',\n message: '계정이 잠겨 있습니다.'\n })\n })\n\n it('an answer that is ok but not a sign-in is not taken for one', async () => {\n expect((await readSigninAnswer(response({ body: '{\"verified\":false}' }))).outcome).toBe('refused')\n expect((await readSigninAnswer(response({ body: '<html>' }))).outcome).toBe('refused')\n expect((await readSigninAnswer(response({ ok: false, status: 401, body: '{\"verified\":true,\"redirectURL\":\"/x\"}' }))).outcome).toBe('refused')\n })\n\n it('an unreadable body is a refusal with nothing to say, never a throw', async () => {\n expect(await readSigninAnswer(response({ unreadable: true }))).toEqual({ outcome: 'refused', message: '' })\n })\n})\n\ndescribe('what the browser said', () => {\n it('a closed or cancelled prompt is not an error worth reporting', () => {\n expect(wasCancelled({ name: 'NotAllowedError' })).toBe(true)\n expect(wasCancelled({ name: 'AbortError' })).toBe(true)\n expect(wasCancelled({ code: 'ERROR_CEREMONY_ABORTED' })).toBe(true)\n expect(wasCancelled({ name: 'SecurityError' })).toBe(false)\n expect(wasCancelled(undefined)).toBe(false)\n })\n\n it('an authenticator that already holds this person’s passkey', () => {\n expect(alreadyRegistered({ name: 'InvalidStateError' })).toBe(true)\n expect(alreadyRegistered({ code: 'ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED' })).toBe(true)\n expect(alreadyRegistered({ name: 'NotAllowedError' })).toBe(false)\n })\n})\n\ndescribe('the offer after a password sign-in', () => {\n it('is made only on a device with its own authenticator, where it is not settled yet', () => {\n expect(shouldOffer({ platformAuthenticator: true, settled: false })).toBe(true)\n expect(shouldOffer({ platformAuthenticator: false, settled: false })).toBe(false)\n expect(shouldOffer({ platformAuthenticator: true, settled: true })).toBe(false)\n })\n\n it('is settled per person on this browser', () => {\n expect(offerSettledKey('kim')).not.toBe(offerSettledKey('lee'))\n })\n\n it('a passkey registered here settles it - the next sign-in on this browser is not offered one again', () => {\n expect(registrationEnd({ verification: { outcome: 'registered' } })).toBe('settled')\n })\n\n it('a passkey of this person already on this device settles it too', () => {\n expect(registrationEnd({ error: { name: 'InvalidStateError' } })).toBe('settled')\n })\n\n it('a closed prompt leaves it open; a refusal or a failure is reported and leaves it open', () => {\n expect(registrationEnd({ error: { name: 'NotAllowedError' } })).toBe('cancelled')\n expect(registrationEnd({ verification: { outcome: 'refused', reason: 'AUTHN_VERIFICATION_FAILED' } })).toBe('refused')\n expect(registrationEnd({ error: new TypeError('Failed to fetch') })).toBe('failed')\n })\n})\n"]}
1
+ {"version":3,"file":"passkey.test.js","sourceRoot":"","sources":["../../client/utils/passkey.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { alreadyRegistered, deviceLabel, offerSettledKey, passkeySigninForm, registrationEnd, shouldOffer, wasCancelled } from './passkey'\n\n/** Just enough of `Document` for the form: elements are plain objects that keep their children. */\nconst fakeDocument = {\n createElement: (tagName: string) => ({\n tagName,\n children: [] as any[],\n appendChild(child: any) {\n this.children.push(child)\n }\n })\n} as unknown as Document\n\ndescribe('a label for a passkey made here', () => {\n it.each([\n ['Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36', 'macOS · Chrome'],\n ['Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15', 'macOS · Safari'],\n ['Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0', 'Windows · Edge'],\n ['Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1', 'iPhone · Safari'],\n ['Mozilla/5.0 (Linux; Android 14; SM-S918N) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/26.0 Chrome/122.0.0.0 Mobile Safari/537.36', 'Android · Samsung Internet'],\n ['Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0', 'Linux · Firefox']\n ])('%s', (userAgent, label) => {\n expect(deviceLabel(userAgent)).toBe(label)\n })\n\n it('says nothing it cannot tell', () => {\n expect(deviceLabel('curl/8.0')).toBe('')\n })\n})\n\ndescribe('the passkey sign-in is posted as a form, like the password', () => {\n const form: any = passkeySigninForm(fakeDocument, { id: 'cred-1', response: { signature: 'sig' } }, '/office')\n\n it('goes to the sign-in route as a page request, so a refusal comes back as a page and never as a toast body', () => {\n expect([form.method, form.action, form.hidden]).toEqual(['post', '/auth/signin-webauthn', true])\n })\n\n it(\"carries the browser's answer as JSON text and where to go after\", () => {\n const fields = Object.fromEntries(form.children.map((input: any) => [input.name, input.value]))\n\n expect(form.children.every((input: any) => input.type === 'hidden')).toBe(true)\n expect(JSON.parse(fields.response)).toEqual({ id: 'cred-1', response: { signature: 'sig' } })\n expect(fields.redirectTo).toBe('/office')\n })\n})\n\ndescribe('what the browser said', () => {\n it('a closed or cancelled prompt is not an error worth reporting', () => {\n expect(wasCancelled({ name: 'NotAllowedError' })).toBe(true)\n expect(wasCancelled({ name: 'AbortError' })).toBe(true)\n expect(wasCancelled({ code: 'ERROR_CEREMONY_ABORTED' })).toBe(true)\n expect(wasCancelled({ name: 'SecurityError' })).toBe(false)\n expect(wasCancelled(undefined)).toBe(false)\n })\n\n it('an authenticator that already holds this person’s passkey', () => {\n expect(alreadyRegistered({ name: 'InvalidStateError' })).toBe(true)\n expect(alreadyRegistered({ code: 'ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED' })).toBe(true)\n expect(alreadyRegistered({ name: 'NotAllowedError' })).toBe(false)\n })\n})\n\ndescribe('the offer after a password sign-in', () => {\n it('is made only on a device with its own authenticator, where it is not settled yet', () => {\n expect(shouldOffer({ platformAuthenticator: true, settled: false })).toBe(true)\n expect(shouldOffer({ platformAuthenticator: false, settled: false })).toBe(false)\n expect(shouldOffer({ platformAuthenticator: true, settled: true })).toBe(false)\n })\n\n it('is settled per person on this browser', () => {\n expect(offerSettledKey('kim')).not.toBe(offerSettledKey('lee'))\n })\n\n it('a passkey registered here settles it - the next sign-in on this browser is not offered one again', () => {\n expect(registrationEnd({ verification: { outcome: 'registered' } })).toBe('settled')\n })\n\n it('a passkey of this person already on this device settles it too', () => {\n expect(registrationEnd({ error: { name: 'InvalidStateError' } })).toBe('settled')\n })\n\n it('a closed prompt leaves it open; a refusal or a failure is reported and leaves it open', () => {\n expect(registrationEnd({ error: { name: 'NotAllowedError' } })).toBe('cancelled')\n expect(registrationEnd({ verification: { outcome: 'refused', reason: 'AUTHN_VERIFICATION_FAILED' } })).toBe('refused')\n expect(registrationEnd({ error: new TypeError('Failed to fetch') })).toBe('failed')\n })\n})\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/auth-ui",
3
- "version": "10.1.250",
3
+ "version": "10.1.254",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -42,12 +42,12 @@
42
42
  "@operato/shell": "^10.29.39",
43
43
  "@operato/styles": "^10.0.0",
44
44
  "@operato/utils": "^10.29.39",
45
- "@things-factory/apptool-base": "^10.1.247",
46
- "@things-factory/auth-base": "^10.1.250",
47
- "@things-factory/component-ui": "^10.1.250",
48
- "@things-factory/more-base": "^10.1.247",
45
+ "@things-factory/apptool-base": "^10.1.254",
46
+ "@things-factory/auth-base": "^10.1.254",
47
+ "@things-factory/component-ui": "^10.1.254",
48
+ "@things-factory/more-base": "^10.1.254",
49
49
  "base64url": "^3.0.1",
50
50
  "clipboard": "^2.0.6"
51
51
  },
52
- "gitHead": "4fdb6d24ef24f43d9e158d4c6d6396ad3097bb9e"
52
+ "gitHead": "793e2fd617db5bb917ec892e64956fb690f7e7b0"
53
53
  }