@things-factory/auth-ui 10.1.212 → 10.1.215
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/auth-style-sign.js +2 -1
- package/dist-client/auth-style-sign.js.map +1 -1
- package/dist-client/components/user-detail.d.ts +9 -0
- package/dist-client/components/user-detail.js +36 -1
- package/dist-client/components/user-detail.js.map +1 -1
- package/dist-client/entries/auth/passkey-offer.d.ts +4 -3
- package/dist-client/entries/auth/passkey-offer.js +26 -23
- package/dist-client/entries/auth/passkey-offer.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/passkey.d.ts +20 -6
- package/dist-client/utils/passkey.js +25 -7
- package/dist-client/utils/passkey.js.map +1 -1
- package/dist-client/utils/passkey.test.js +18 -7
- package/dist-client/utils/passkey.test.js.map +1 -1
- package/package.json +4 -4
- package/tests/auth-text-button-colour.test.ts +27 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { VerificationOutcome } from './webauthn-verification.js';
|
|
1
2
|
/**
|
|
2
3
|
* The rules the passkey screens follow (ADR-0156), apart from the screens so they are tested.
|
|
3
4
|
*/
|
|
@@ -25,14 +26,27 @@ export declare function readSigninAnswer(response: Response): Promise<SigninAnsw
|
|
|
25
26
|
export declare function wasCancelled(error: any): boolean;
|
|
26
27
|
/** The authenticator already holds one of this person's passkeys for this site. */
|
|
27
28
|
export declare function alreadyRegistered(error: any): boolean;
|
|
28
|
-
/**
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Where this browser keeps that the offer is settled for a person - they said 「do not ask again on
|
|
31
|
+
* this device」, or a passkey of theirs is on this device now. Per browser, per person.
|
|
32
|
+
*/
|
|
33
|
+
export declare function offerSettledKey(username: string): string;
|
|
30
34
|
/**
|
|
31
35
|
* Whether the offer screen has anything to offer. It needs an authenticator built into this device -
|
|
32
|
-
* a person on someone else's computer is not asked to put a passkey on it - and the
|
|
33
|
-
*
|
|
36
|
+
* a person on someone else's computer is not asked to put a passkey on it - and the offer must not be
|
|
37
|
+
* settled on this browser.
|
|
34
38
|
*/
|
|
35
|
-
export declare function shouldOffer({ platformAuthenticator,
|
|
39
|
+
export declare function shouldOffer({ platformAuthenticator, settled }: {
|
|
36
40
|
platformAuthenticator: boolean;
|
|
37
|
-
|
|
41
|
+
settled: boolean;
|
|
38
42
|
}): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Where the offer stands after the browser was asked to make a passkey. A passkey on this device -
|
|
45
|
+
* made just now, or already there - settles the offer here, as a no does: asking again at the next
|
|
46
|
+
* sign-in would only make the person register the same device once more. A closed prompt leaves the
|
|
47
|
+
* offer open; a refusal by the server, or anything else, is worth a word.
|
|
48
|
+
*/
|
|
49
|
+
export declare function registrationEnd({ verification, error }: {
|
|
50
|
+
verification?: VerificationOutcome;
|
|
51
|
+
error?: any;
|
|
52
|
+
}): 'settled' | 'cancelled' | 'refused' | 'failed';
|
|
@@ -69,16 +69,34 @@ export function wasCancelled(error) {
|
|
|
69
69
|
export function alreadyRegistered(error) {
|
|
70
70
|
return error?.name === 'InvalidStateError' || error?.code === 'ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED';
|
|
71
71
|
}
|
|
72
|
-
/**
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Where this browser keeps that the offer is settled for a person - they said 「do not ask again on
|
|
74
|
+
* this device」, or a passkey of theirs is on this device now. Per browser, per person.
|
|
75
|
+
*/
|
|
76
|
+
export function offerSettledKey(username) {
|
|
77
|
+
return `passkey-offer-settled:${username}`;
|
|
75
78
|
}
|
|
76
79
|
/**
|
|
77
80
|
* Whether the offer screen has anything to offer. It needs an authenticator built into this device -
|
|
78
|
-
* a person on someone else's computer is not asked to put a passkey on it - and the
|
|
79
|
-
*
|
|
81
|
+
* a person on someone else's computer is not asked to put a passkey on it - and the offer must not be
|
|
82
|
+
* settled on this browser.
|
|
83
|
+
*/
|
|
84
|
+
export function shouldOffer({ platformAuthenticator, settled }) {
|
|
85
|
+
return platformAuthenticator && !settled;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Where the offer stands after the browser was asked to make a passkey. A passkey on this device -
|
|
89
|
+
* made just now, or already there - settles the offer here, as a no does: asking again at the next
|
|
90
|
+
* sign-in would only make the person register the same device once more. A closed prompt leaves the
|
|
91
|
+
* offer open; a refusal by the server, or anything else, is worth a word.
|
|
80
92
|
*/
|
|
81
|
-
export function
|
|
82
|
-
|
|
93
|
+
export function registrationEnd({ verification, error }) {
|
|
94
|
+
if (verification)
|
|
95
|
+
return verification.outcome === 'registered' ? 'settled' : 'refused';
|
|
96
|
+
if (wasCancelled(error))
|
|
97
|
+
return 'cancelled';
|
|
98
|
+
if (alreadyRegistered(error))
|
|
99
|
+
return 'settled';
|
|
100
|
+
return 'failed';
|
|
83
101
|
}
|
|
84
102
|
//# sourceMappingURL=passkey.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"passkey.js","sourceRoot":"","sources":["../../client/utils/passkey.ts"],"names":[],"mappings":"
|
|
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,4 +1,4 @@
|
|
|
1
|
-
import { alreadyRegistered, deviceLabel,
|
|
1
|
+
import { alreadyRegistered, deviceLabel, offerSettledKey, readSigninAnswer, registrationEnd, shouldOffer, wasCancelled } from './passkey';
|
|
2
2
|
/** Just enough of `Response` for the reader. */
|
|
3
3
|
function response({ ok = true, status = 200, body = '', unreadable = false } = {}) {
|
|
4
4
|
return {
|
|
@@ -63,13 +63,24 @@ describe('what the browser said', () => {
|
|
|
63
63
|
});
|
|
64
64
|
});
|
|
65
65
|
describe('the offer after a password sign-in', () => {
|
|
66
|
-
it('is made only on a device with its own authenticator,
|
|
67
|
-
expect(shouldOffer({ platformAuthenticator: true,
|
|
68
|
-
expect(shouldOffer({ platformAuthenticator: false,
|
|
69
|
-
expect(shouldOffer({ platformAuthenticator: true,
|
|
66
|
+
it('is made only on a device with its own authenticator, where it is not settled yet', () => {
|
|
67
|
+
expect(shouldOffer({ platformAuthenticator: true, settled: false })).toBe(true);
|
|
68
|
+
expect(shouldOffer({ platformAuthenticator: false, settled: false })).toBe(false);
|
|
69
|
+
expect(shouldOffer({ platformAuthenticator: true, settled: true })).toBe(false);
|
|
70
70
|
});
|
|
71
|
-
it('
|
|
72
|
-
expect(
|
|
71
|
+
it('is settled per person on this browser', () => {
|
|
72
|
+
expect(offerSettledKey('kim')).not.toBe(offerSettledKey('lee'));
|
|
73
|
+
});
|
|
74
|
+
it('a passkey registered here settles it - the next sign-in on this browser is not offered one again', () => {
|
|
75
|
+
expect(registrationEnd({ verification: { outcome: 'registered' } })).toBe('settled');
|
|
76
|
+
});
|
|
77
|
+
it('a passkey of this person already on this device settles it too', () => {
|
|
78
|
+
expect(registrationEnd({ error: { name: 'InvalidStateError' } })).toBe('settled');
|
|
79
|
+
});
|
|
80
|
+
it('a closed prompt leaves it open; a refusal or a failure is reported and leaves it open', () => {
|
|
81
|
+
expect(registrationEnd({ error: { name: 'NotAllowedError' } })).toBe('cancelled');
|
|
82
|
+
expect(registrationEnd({ verification: { outcome: 'refused', reason: 'AUTHN_VERIFICATION_FAILED' } })).toBe('refused');
|
|
83
|
+
expect(registrationEnd({ error: new TypeError('Failed to fetch') })).toBe('failed');
|
|
73
84
|
});
|
|
74
85
|
});
|
|
75
86
|
//# sourceMappingURL=passkey.test.js.map
|
|
@@ -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,
|
|
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"]}
|
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.215",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"browser": "dist-client/index.js",
|
|
6
6
|
"things-factory": true,
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
"@operato/shell": "^10.29.39",
|
|
41
41
|
"@operato/styles": "^10.0.0",
|
|
42
42
|
"@operato/utils": "^10.29.39",
|
|
43
|
-
"@things-factory/auth-base": "^10.1.
|
|
44
|
-
"@things-factory/component-ui": "^10.1.
|
|
43
|
+
"@things-factory/auth-base": "^10.1.215",
|
|
44
|
+
"@things-factory/component-ui": "^10.1.215",
|
|
45
45
|
"@things-factory/more-base": "^10.1.208",
|
|
46
46
|
"base64url": "^3.0.1",
|
|
47
47
|
"clipboard": "^2.0.6"
|
|
48
48
|
},
|
|
49
|
-
"gitHead": "
|
|
49
|
+
"gitHead": "7de5e80eb1f4e2e268746aee39f820e7563f454d"
|
|
50
50
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readFileSync } from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The sign-in family draws on the primary colour, and Material's text button labels itself in that
|
|
6
|
+
* same primary colour. A text button the sign-in style does not recolour is invisible. The passkey
|
|
7
|
+
* offer's 「later」 and 「do not ask again on this device」 were - only 「register」 could be seen, so
|
|
8
|
+
* the offer came back at every sign-in (ADR-0156 decision 5).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const STYLE = path.resolve(__dirname, '../client/auth-style-sign.ts')
|
|
12
|
+
|
|
13
|
+
const withoutComments = (text: string) => text.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
14
|
+
|
|
15
|
+
/** The selectors of each block that sets the text button label colour to on-primary. */
|
|
16
|
+
function onPrimaryTextButtonSelectors(): string[] {
|
|
17
|
+
const css = withoutComments(readFileSync(STYLE, 'utf-8'))
|
|
18
|
+
return [...css.matchAll(/([^{}]+)\{([^}]*)\}/g)]
|
|
19
|
+
.filter(([, , body]) => /--md-text-button-label-text-color:\s*var\(--md-sys-color-on-primary\)/.test(body))
|
|
20
|
+
.flatMap(([, selectors]) => selectors.split(',').map(selector => selector.trim()))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('text buttons on the sign-in surface', () => {
|
|
24
|
+
it('every one inside the page is labelled in on-primary, not only the links', () => {
|
|
25
|
+
expect(onPrimaryTextButtonSelectors()).toContain('.wrap md-text-button')
|
|
26
|
+
})
|
|
27
|
+
})
|