@things-factory/auth-ui 10.1.250 → 10.1.256
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/dist-client/components/abstract-sign.js +4 -11
- package/dist-client/components/abstract-sign.js.map +1 -1
- package/dist-client/components/user-detail.js +14 -3
- package/dist-client/components/user-detail.js.map +1 -1
- package/dist-client/components/user-list-phone.test.d.ts +1 -0
- package/dist-client/components/user-list-phone.test.js +20 -0
- package/dist-client/components/user-list-phone.test.js.map +1 -0
- package/dist-client/pages/user/user-list-page.js +6 -2
- package/dist-client/pages/user/user-list-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/passkey.d.ts +5 -10
- package/dist-client/utils/passkey.js +16 -17
- package/dist-client/utils/passkey.js.map +1 -1
- package/dist-client/utils/passkey.test.js +19 -31
- package/dist-client/utils/passkey.test.js.map +1 -1
- package/package.json +6 -6
|
@@ -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
|
-
*
|
|
19
|
-
*
|
|
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
|
|
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
|
-
*
|
|
42
|
-
*
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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":"AAEA;;GAEG;AAEH;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,SAAiB;IAC3C,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC;QACrC,CAAC,CAAC,QAAQ;QACV,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC;YACtB,CAAC,CAAC,MAAM;YACR,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC;gBACzB,CAAC,CAAC,SAAS;gBACX,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC;oBACzB,CAAC,CAAC,SAAS;oBACX,CAAC,CAAC,oBAAoB,CAAC,IAAI,CAAC,SAAS,CAAC;wBACpC,CAAC,CAAC,OAAO;wBACT,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC;4BACtB,CAAC,CAAC,UAAU;4BACZ,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC;gCACvB,CAAC,CAAC,OAAO;gCACT,CAAC,CAAC,EAAE,CAAA;IAElB,2EAA2E;IAC3E,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC;QACrC,CAAC,CAAC,MAAM;QACR,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC;YACvB,CAAC,CAAC,OAAO;YACT,CAAC,CAAC,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC;gBAClC,CAAC,CAAC,kBAAkB;gBACpB,CAAC,CAAC,mBAAmB,CAAC,IAAI,CAAC,SAAS,CAAC;oBACnC,CAAC,CAAC,SAAS;oBACX,CAAC,CAAC,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC;wBAClC,CAAC,CAAC,QAAQ;wBACV,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC;4BAC1B,CAAC,CAAC,QAAQ;4BACV,CAAC,CAAC,EAAE,CAAA;IAEhB,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;AACtD,CAAC;
|
|
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,
|
|
2
|
-
/** Just enough of `
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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('
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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("
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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":"AAAA,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,eAAe,EAAE,
|
|
1
|
+
{"version":3,"file":"passkey.test.js","sourceRoot":"","sources":["../../client/utils/passkey.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,eAAe,EAAE,iBAAiB,EAAE,eAAe,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAE1I,mGAAmG;AACnG,MAAM,YAAY,GAAG;IACnB,aAAa,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,CAAC;QACnC,OAAO;QACP,QAAQ,EAAE,EAAW;QACrB,WAAW,CAAC,KAAU;YACpB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC3B,CAAC;KACF,CAAC;CACoB,CAAA;AAExB,QAAQ,CAAC,iCAAiC,EAAE,GAAG,EAAE;IAC/C,EAAE,CAAC,IAAI,CAAC;QACN,CAAC,uHAAuH,EAAE,gBAAgB,CAAC;QAC3I,CAAC,uHAAuH,EAAE,gBAAgB,CAAC;QAC3I,CAAC,+HAA+H,EAAE,gBAAgB,CAAC;QACnJ,CAAC,yIAAyI,EAAE,iBAAiB,CAAC;QAC9J,CAAC,4IAA4I,EAAE,4BAA4B,CAAC;QAC5K,CAAC,wEAAwE,EAAE,iBAAiB,CAAC;KAC9F,CAAC,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE;QAC5B,MAAM,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC5C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAC1C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,4DAA4D,EAAE,GAAG,EAAE;IAC1E,MAAM,IAAI,GAAQ,iBAAiB,CAAC,YAAY,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAE,SAAS,CAAC,CAAA;IAE9G,EAAE,CAAC,0GAA0G,EAAE,GAAG,EAAE;QAClH,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,uBAAuB,EAAE,IAAI,CAAC,CAAC,CAAA;IAClG,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;QACzE,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAU,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;QAE/F,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,KAAU,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC/E,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,CAAC,CAAA;QAC7F,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IAC3C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,EAAE,CAAC,8DAA8D,EAAE,GAAG,EAAE;QACtE,MAAM,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC5D,MAAM,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACvD,MAAM,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,wBAAwB,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACnE,MAAM,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC3D,MAAM,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC7C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,CAAC,iBAAiB,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACnE,MAAM,CAAC,iBAAiB,CAAC,EAAE,IAAI,EAAE,2CAA2C,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC3F,MAAM,CAAC,iBAAiB,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACpE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,oCAAoC,EAAE,GAAG,EAAE;IAClD,EAAE,CAAC,kFAAkF,EAAE,GAAG,EAAE;QAC1F,MAAM,CAAC,WAAW,CAAC,EAAE,qBAAqB,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC/E,MAAM,CAAC,WAAW,CAAC,EAAE,qBAAqB,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACjF,MAAM,CAAC,WAAW,CAAC,EAAE,qBAAqB,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACjF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;QAC/C,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAA;IACjE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kGAAkG,EAAE,GAAG,EAAE;QAC1G,MAAM,CAAC,eAAe,CAAC,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IACtF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,MAAM,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IACnF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,uFAAuF,EAAE,GAAG,EAAE;QAC/F,MAAM,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;QACjF,MAAM,CAAC,eAAe,CAAC,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,2BAA2B,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACtH,MAAM,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,IAAI,SAAS,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACrF,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","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.
|
|
3
|
+
"version": "10.1.256",
|
|
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.
|
|
46
|
-
"@things-factory/auth-base": "^10.1.
|
|
47
|
-
"@things-factory/component-ui": "^10.1.
|
|
48
|
-
"@things-factory/more-base": "^10.1.
|
|
45
|
+
"@things-factory/apptool-base": "^10.1.254",
|
|
46
|
+
"@things-factory/auth-base": "^10.1.256",
|
|
47
|
+
"@things-factory/component-ui": "^10.1.256",
|
|
48
|
+
"@things-factory/more-base": "^10.1.254",
|
|
49
49
|
"base64url": "^3.0.1",
|
|
50
50
|
"clipboard": "^2.0.6"
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "2fd641697190ce6e87bcf454a60b2321553e8110"
|
|
53
53
|
}
|