@owlmeans/client-auth 0.1.18-rc.9 → 0.1.18
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/README.md +283 -37
- package/agent-meta/manifest.json +50 -4
- package/agent-meta/skills/client-auth/SKILL.md +84 -55
- package/agent-meta/skills/client-auth/parts/login.md +77 -0
- package/agent-meta/skills/client-auth/parts/manager.md +37 -0
- package/build/components/dispatcher/component.d.ts.map +1 -1
- package/build/components/dispatcher/component.js +26 -6
- package/build/components/dispatcher/component.js.map +1 -1
- package/build/components/dispatcher/types.local.d.ts +7 -0
- package/build/components/dispatcher/types.local.d.ts.map +1 -0
- package/build/components/dispatcher/types.local.js +2 -0
- package/build/components/dispatcher/types.local.js.map +1 -0
- package/build/entrypoints.d.ts +7 -0
- package/build/entrypoints.d.ts.map +1 -0
- package/build/entrypoints.js +7 -0
- package/build/entrypoints.js.map +1 -0
- package/build/helper.d.ts +2 -1
- package/build/helper.d.ts.map +1 -1
- package/build/helper.js +28 -5
- package/build/helper.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/login/adopt/types.d.ts +26 -0
- package/build/login/adopt/types.d.ts.map +1 -0
- package/build/login/adopt/types.js +2 -0
- package/build/login/adopt/types.js.map +1 -0
- package/build/login/adopt.d.ts +7 -12
- package/build/login/adopt.d.ts.map +1 -1
- package/build/login/adopt.js +15 -14
- package/build/login/adopt.js.map +1 -1
- package/build/login/consts.d.ts +89 -4
- package/build/login/consts.d.ts.map +1 -1
- package/build/login/consts.js +93 -5
- package/build/login/consts.js.map +1 -1
- package/build/login/consts.local.d.ts +16 -0
- package/build/login/consts.local.d.ts.map +1 -0
- package/build/login/consts.local.js +30 -0
- package/build/login/consts.local.js.map +1 -0
- package/build/login/credit/types.d.ts +7 -0
- package/build/login/credit/types.d.ts.map +1 -0
- package/build/login/credit/types.js +2 -0
- package/build/login/credit/types.js.map +1 -0
- package/build/login/credit.d.ts +19 -0
- package/build/login/credit.d.ts.map +1 -0
- package/build/login/credit.js +58 -0
- package/build/login/credit.js.map +1 -0
- package/build/login/env/types.d.ts +35 -0
- package/build/login/env/types.d.ts.map +1 -0
- package/build/login/env/types.js +2 -0
- package/build/login/env/types.js.map +1 -0
- package/build/login/env.d.ts +5 -29
- package/build/login/env.d.ts.map +1 -1
- package/build/login/env.js +63 -80
- package/build/login/env.js.map +1 -1
- package/build/login/flow.d.ts +20 -0
- package/build/login/flow.d.ts.map +1 -0
- package/build/login/flow.js +39 -0
- package/build/login/flow.js.map +1 -0
- package/build/login/hook.d.ts +30 -4
- package/build/login/hook.d.ts.map +1 -1
- package/build/login/hook.js +53 -18
- package/build/login/hook.js.map +1 -1
- package/build/login/i18n/be.json +41 -0
- package/build/login/i18n/de.json +41 -0
- package/build/login/i18n/en.json +41 -0
- package/build/login/i18n/es.json +41 -0
- package/build/login/i18n/fr.json +41 -0
- package/build/login/i18n/pl.json +41 -0
- package/build/login/i18n/ru.json +41 -0
- package/build/login/i18n/uk.json +41 -0
- package/build/login/i18n.d.ts +2 -0
- package/build/login/i18n.d.ts.map +1 -0
- package/build/login/i18n.js +27 -0
- package/build/login/i18n.js.map +1 -0
- package/build/login/index.d.ts +20 -0
- package/build/login/index.d.ts.map +1 -1
- package/build/login/index.js +11 -0
- package/build/login/index.js.map +1 -1
- package/build/login/land/types.d.ts +52 -0
- package/build/login/land/types.d.ts.map +1 -0
- package/build/login/land/types.js +2 -0
- package/build/login/land/types.js.map +1 -0
- package/build/login/land.d.ts +6 -0
- package/build/login/land.d.ts.map +1 -0
- package/build/login/land.js +122 -0
- package/build/login/land.js.map +1 -0
- package/build/login/methods/types.d.ts +30 -0
- package/build/login/methods/types.d.ts.map +1 -0
- package/build/login/methods/types.js +2 -0
- package/build/login/methods/types.js.map +1 -0
- package/build/login/methods.d.ts +7 -0
- package/build/login/methods.d.ts.map +1 -0
- package/build/login/methods.js +87 -0
- package/build/login/methods.js.map +1 -0
- package/build/login/resume/types.d.ts +24 -0
- package/build/login/resume/types.d.ts.map +1 -0
- package/build/login/resume/types.js +2 -0
- package/build/login/resume/types.js.map +1 -0
- package/build/login/resume.d.ts +9 -0
- package/build/login/resume.d.ts.map +1 -0
- package/build/login/resume.js +38 -0
- package/build/login/resume.js.map +1 -0
- package/build/login/screen.d.ts +17 -0
- package/build/login/screen.d.ts.map +1 -0
- package/build/login/screen.js +66 -0
- package/build/login/screen.js.map +1 -0
- package/build/login/service.d.ts +6 -5
- package/build/login/service.d.ts.map +1 -1
- package/build/login/service.js +141 -15
- package/build/login/service.js.map +1 -1
- package/build/login/start/types.d.ts +29 -0
- package/build/login/start/types.d.ts.map +1 -0
- package/build/login/start/types.js +2 -0
- package/build/login/start/types.js.map +1 -0
- package/build/login/start.d.ts +5 -0
- package/build/login/start.d.ts.map +1 -0
- package/build/login/start.js +36 -0
- package/build/login/start.js.map +1 -0
- package/build/login/surrogate/types.d.ts +9 -0
- package/build/login/surrogate/types.d.ts.map +1 -0
- package/build/login/surrogate/types.js +2 -0
- package/build/login/surrogate/types.js.map +1 -0
- package/build/login/surrogate.d.ts +13 -0
- package/build/login/surrogate.d.ts.map +1 -0
- package/build/login/surrogate.js +33 -0
- package/build/login/surrogate.js.map +1 -0
- package/build/login/terms/types.d.ts +112 -0
- package/build/login/terms/types.d.ts.map +1 -0
- package/build/login/terms/types.js +2 -0
- package/build/login/terms/types.js.map +1 -0
- package/build/login/terms-config.d.ts +65 -0
- package/build/login/terms-config.d.ts.map +1 -0
- package/build/login/terms-config.js +2 -0
- package/build/login/terms-config.js.map +1 -0
- package/build/login/terms.d.ts +21 -0
- package/build/login/terms.d.ts.map +1 -0
- package/build/login/terms.js +235 -0
- package/build/login/terms.js.map +1 -0
- package/build/login/types.d.ts +223 -16
- package/build/login/types.d.ts.map +1 -1
- package/build/login/types.js +1 -16
- package/build/login/types.js.map +1 -1
- package/build/manager/components/authentication/control.js +3 -3
- package/build/manager/components/authentication/control.js.map +1 -1
- package/build/manager/components/authentication/types.d.ts +2 -2
- package/build/manager/components/authentication/types.d.ts.map +1 -1
- package/build/manager/components/tunnel-consumer.js +2 -2
- package/build/manager/components/tunnel-consumer.js.map +1 -1
- package/build/manager/entrypoints.d.ts +11 -0
- package/build/manager/entrypoints.d.ts.map +1 -0
- package/build/manager/entrypoints.js +17 -0
- package/build/manager/entrypoints.js.map +1 -0
- package/build/manager/plugins/basic-ed25519.d.ts.map +1 -1
- package/build/manager/plugins/basic-ed25519.js +5 -0
- package/build/manager/plugins/basic-ed25519.js.map +1 -1
- package/build/manager/plugins/exports.d.ts +3 -0
- package/build/manager/plugins/exports.d.ts.map +1 -1
- package/build/manager/plugins/exports.js +2 -0
- package/build/manager/plugins/exports.js.map +1 -1
- package/build/manager/plugins/index.d.ts +2 -5
- package/build/manager/plugins/index.d.ts.map +1 -1
- package/build/manager/plugins/index.js +9 -1
- package/build/manager/plugins/index.js.map +1 -1
- package/build/manager/plugins/methods.d.ts +6 -0
- package/build/manager/plugins/methods.d.ts.map +1 -0
- package/build/manager/plugins/methods.js +65 -0
- package/build/manager/plugins/methods.js.map +1 -0
- package/build/manager/plugins/re-captcha.d.ts.map +1 -1
- package/build/manager/plugins/re-captcha.js +5 -0
- package/build/manager/plugins/re-captcha.js.map +1 -1
- package/build/manager/plugins/registry/types.d.ts +10 -0
- package/build/manager/plugins/registry/types.d.ts.map +1 -0
- package/build/manager/plugins/registry/types.js +2 -0
- package/build/manager/plugins/registry/types.js.map +1 -0
- package/build/manager/plugins/registry.d.ts +20 -0
- package/build/manager/plugins/registry.d.ts.map +1 -0
- package/build/manager/plugins/registry.js +23 -0
- package/build/manager/plugins/registry.js.map +1 -0
- package/build/manager/plugins/tunnel-consumer.d.ts.map +1 -1
- package/build/manager/plugins/tunnel-consumer.js +10 -3
- package/build/manager/plugins/tunnel-consumer.js.map +1 -1
- package/build/manager/plugins/types.d.ts +53 -0
- package/build/manager/plugins/types.d.ts.map +1 -1
- package/build/modules.d.ts.map +1 -1
- package/build/service.js +1 -1
- package/build/service.js.map +1 -1
- package/package.json +33 -30
- package/src/components/dispatcher/component.tsx +37 -14
- package/src/components/dispatcher/types.local.ts +7 -0
- package/src/entrypoints.ts +10 -0
- package/src/helper.ts +31 -5
- package/src/index.ts +1 -1
- package/src/login/adopt/types.ts +25 -0
- package/src/login/adopt.ts +21 -14
- package/src/login/consts.local.ts +34 -0
- package/src/login/consts.ts +105 -5
- package/src/login/credit/types.ts +6 -0
- package/src/login/credit.ts +69 -0
- package/src/login/env/types.ts +35 -0
- package/src/login/env.ts +60 -72
- package/src/login/flow.ts +44 -0
- package/src/login/hook.ts +58 -22
- package/src/login/i18n/be.json +41 -0
- package/src/login/i18n/de.json +41 -0
- package/src/login/i18n/en.json +41 -0
- package/src/login/i18n/es.json +41 -0
- package/src/login/i18n/fr.json +41 -0
- package/src/login/i18n/pl.json +41 -0
- package/src/login/i18n/ru.json +41 -0
- package/src/login/i18n/uk.json +41 -0
- package/src/login/i18n.ts +29 -0
- package/src/login/index.ts +21 -0
- package/src/login/land/types.ts +54 -0
- package/src/login/land.ts +149 -0
- package/src/login/methods/types.ts +32 -0
- package/src/login/methods.ts +109 -0
- package/src/login/resume/types.ts +24 -0
- package/src/login/resume.ts +45 -0
- package/src/login/screen.tsx +140 -0
- package/src/login/service.ts +181 -18
- package/src/login/start/types.ts +30 -0
- package/src/login/start.ts +44 -0
- package/src/login/surrogate/types.ts +9 -0
- package/src/login/surrogate.ts +37 -0
- package/src/login/terms/types.ts +126 -0
- package/src/login/terms-config.ts +55 -0
- package/src/login/terms.ts +315 -0
- package/src/login/types.ts +233 -17
- package/src/manager/README.md +37 -80
- package/src/manager/components/authentication/control.ts +3 -3
- package/src/manager/components/authentication/types.ts +2 -2
- package/src/manager/components/tunnel-consumer.tsx +2 -2
- package/src/manager/entrypoints.ts +18 -0
- package/src/manager/plugins/basic-ed25519.tsx +7 -0
- package/src/manager/plugins/exports.ts +3 -0
- package/src/manager/plugins/index.ts +11 -4
- package/src/manager/plugins/methods.ts +74 -0
- package/src/manager/plugins/re-captcha.tsx +7 -0
- package/src/manager/plugins/registry/types.ts +10 -0
- package/src/manager/plugins/registry.ts +33 -0
- package/src/manager/plugins/tunnel-consumer.tsx +12 -3
- package/src/manager/plugins/types.ts +54 -0
- package/src/service.ts +1 -1
- package/tests/context.ts +1 -2
- package/tests/i18n.spec.ts +68 -0
- package/tests/land.spec.ts +292 -0
- package/tests/login.spec.ts +332 -0
- package/tests/target.spec.ts +210 -0
- package/tests/terms-deferred.spec.ts +65 -0
- package/tests/terms.spec.ts +314 -0
- package/build/.gitkeep +0 -0
- package/src/manager/modules.ts +0 -18
- package/src/modules.ts +0 -13
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { OWLMEANS_COOKIES_URL, OWLMEANS_PRIVACY_URL, OWLMEANS_TERMS_URL, type LoginTermsConfig } from '@owlmeans/config'
|
|
2
|
+
import { LOGIN_SERVICE, LOGIN_TERMS_STORAGE } from './consts.js'
|
|
3
|
+
import type { LoginContext, LoginService } from './types.js'
|
|
4
|
+
import type {
|
|
5
|
+
LoginTermsAcceptance, LoginTermsHelper, ResolvedTerms, ResolvedTermsDocument, TermsSentencePart
|
|
6
|
+
} from './terms/types.js'
|
|
7
|
+
import './terms-config.js'
|
|
8
|
+
import { DEFAULT_LABEL } from './consts.local.js'
|
|
9
|
+
|
|
10
|
+
export const createLoginTermsHelper = (): LoginTermsHelper => {
|
|
11
|
+
/**
|
|
12
|
+
* A stable identity for one set of documents.
|
|
13
|
+
*
|
|
14
|
+
* Acceptance is recorded against it, so changing a URL — or a revision date, or adding a document
|
|
15
|
+
* — re-asks. Cheap and order-independent rather than cryptographic; it only has to change when the
|
|
16
|
+
* content pointer does.
|
|
17
|
+
*
|
|
18
|
+
* With no billing/product/custom documents and no revisions configured, this MUST produce the same
|
|
19
|
+
* digest as before this file grew those fields: `digest([terms, privacy, cookies])`. `terms.spec.ts`
|
|
20
|
+
* pins that byte-identity so no existing user is silently asked to re-accept.
|
|
21
|
+
*/
|
|
22
|
+
const digest = (parts: string[]): string => {
|
|
23
|
+
let hash = 0
|
|
24
|
+
for (const char of parts.join(' ')) {
|
|
25
|
+
hash = ((hash << 5) - hash + char.charCodeAt(0)) | 0
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (hash >>> 0).toString(36)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const withRevisedAt = (
|
|
32
|
+
doc: Omit<ResolvedTermsDocument, 'revisedAt'>, revisedAt: string | undefined
|
|
33
|
+
): ResolvedTermsDocument => revisedAt != null ? { ...doc, revisedAt } : doc
|
|
34
|
+
|
|
35
|
+
const resolveTerms = (cfg?: LoginTermsConfig | false): ResolvedTerms | null => {
|
|
36
|
+
if (cfg === false) {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
const terms = cfg?.terms ?? OWLMEANS_TERMS_URL
|
|
40
|
+
const privacy = cfg?.privacy ?? OWLMEANS_PRIVACY_URL
|
|
41
|
+
const cookies = cfg?.cookies ?? OWLMEANS_COOKIES_URL
|
|
42
|
+
|
|
43
|
+
const documents: ResolvedTermsDocument[] = [
|
|
44
|
+
withRevisedAt(
|
|
45
|
+
{ key: 'terms', href: terms, i18nKey: 'login.terms.terms' }, cfg?.revisions?.terms
|
|
46
|
+
),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
if (cfg?.billing) {
|
|
50
|
+
const billing = typeof cfg.billing === 'string' ? { href: cfg.billing } : cfg.billing
|
|
51
|
+
documents.push(withRevisedAt(
|
|
52
|
+
{ key: 'billing', href: billing.href, i18nKey: 'login.terms.billing' },
|
|
53
|
+
cfg?.revisions?.billing ?? billing.revisedAt
|
|
54
|
+
))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (cfg?.product) {
|
|
58
|
+
documents.push(withRevisedAt(
|
|
59
|
+
{
|
|
60
|
+
key: 'product', href: cfg.product.href, i18nKey: 'login.terms.product',
|
|
61
|
+
params: { product: cfg.product.name },
|
|
62
|
+
},
|
|
63
|
+
cfg.product.revisedAt ?? cfg?.revisions?.product
|
|
64
|
+
))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const custom of cfg?.documents ?? []) {
|
|
68
|
+
documents.push(withRevisedAt(
|
|
69
|
+
{
|
|
70
|
+
key: custom.key, href: custom.href,
|
|
71
|
+
...(typeof custom.label === 'string' ? { label: custom.label } : {}),
|
|
72
|
+
...(custom.label != null && typeof custom.label !== 'string' ? { labelMap: custom.label } : {}),
|
|
73
|
+
...(custom.i18nKey != null ? { i18nKey: custom.i18nKey } : {}),
|
|
74
|
+
},
|
|
75
|
+
custom.revisedAt ?? cfg?.revisions?.[custom.key]
|
|
76
|
+
))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Cookies is disclosed as ITS OWN notice only when the app named a cookie policy explicitly, or
|
|
80
|
+
// when terms/privacy/cookies are ALL still the OwlMeans defaults — never as a silent extra on an
|
|
81
|
+
// app that customized terms and privacy but left cookies unset.
|
|
82
|
+
const allDefault = terms === OWLMEANS_TERMS_URL && privacy === OWLMEANS_PRIVACY_URL
|
|
83
|
+
&& cookies === OWLMEANS_COOKIES_URL
|
|
84
|
+
const includeCookies = cfg?.cookies != null || allDefault
|
|
85
|
+
|
|
86
|
+
const notices: ResolvedTermsDocument[] = [
|
|
87
|
+
withRevisedAt({ key: 'privacy', href: privacy, i18nKey: 'login.terms.privacy' }, cfg?.revisions?.privacy),
|
|
88
|
+
...(includeCookies
|
|
89
|
+
? [withRevisedAt({ key: 'cookies', href: cookies, i18nKey: 'login.terms.cookies' }, cfg?.revisions?.cookies)]
|
|
90
|
+
: []),
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
// The digest input for a plain config (no billing/product/custom/revisions) is BYTE-IDENTICAL to
|
|
94
|
+
// the pre-existing `[terms, privacy, cookies]` — see `terms.spec.ts`.
|
|
95
|
+
const digestInput = [terms, privacy, cookies]
|
|
96
|
+
for (const doc of documents) {
|
|
97
|
+
if (doc.key !== 'terms') {
|
|
98
|
+
digestInput.push(`${doc.key}=${doc.href}`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const doc of [...documents, ...notices]) {
|
|
102
|
+
if (doc.revisedAt != null) {
|
|
103
|
+
digestInput.push(`${doc.key}@${doc.revisedAt}`)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const showRevision = cfg?.showRevision === true
|
|
108
|
+
const revisedAtValues = documents
|
|
109
|
+
.map(doc => doc.revisedAt)
|
|
110
|
+
.filter((value): value is string => value != null)
|
|
111
|
+
const revisedAt = showRevision && revisedAtValues.length > 0
|
|
112
|
+
? revisedAtValues.reduce((max, value) => value > max ? value : max)
|
|
113
|
+
: undefined
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
required: cfg?.required ?? true,
|
|
117
|
+
terms, privacy, cookies,
|
|
118
|
+
version: cfg?.version ?? digest(digestInput),
|
|
119
|
+
// Translations are alternate destinations for this canonical revision, not a new agreement.
|
|
120
|
+
documents: documents.map(doc => withLocalizedHrefs(doc, cfg)),
|
|
121
|
+
notices: notices.map(doc => withLocalizedHrefs(doc, cfg)),
|
|
122
|
+
showRevision,
|
|
123
|
+
...(revisedAt != null ? { revisedAt } : {}),
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const withLocalizedHrefs = (doc: ResolvedTermsDocument, cfg?: LoginTermsConfig): ResolvedTermsDocument => {
|
|
128
|
+
const hrefMap = cfg?.localizedHrefs?.[doc.key]
|
|
129
|
+
|
|
130
|
+
return hrefMap != null ? { ...doc, hrefMap } : doc
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const termsHrefOf = (doc: ResolvedTermsDocument, locale?: string): string => {
|
|
134
|
+
const localized = locale != null
|
|
135
|
+
? doc.hrefMap?.[locale] ?? doc.hrefMap?.[locale.toLowerCase()]
|
|
136
|
+
?? doc.hrefMap?.[locale.split('-')[0].toLowerCase()]
|
|
137
|
+
: undefined
|
|
138
|
+
|
|
139
|
+
return typeof localized === 'string' && localized.trim() !== '' ? localized : doc.href
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const termsAccepted = (resolved: ResolvedTerms | null): boolean => {
|
|
143
|
+
if (resolved == null || !resolved.required) {
|
|
144
|
+
return true
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
return window.localStorage.getItem(LOGIN_TERMS_STORAGE) === resolved.version
|
|
148
|
+
} catch {
|
|
149
|
+
// Storage can be unavailable (private modes, blocked cookies). Refusing to remember is not
|
|
150
|
+
// refusing to proceed: the user is asked again, every time, which is the safe direction.
|
|
151
|
+
return false
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const acceptTerms = (resolved: ResolvedTerms | null, accepted: boolean): void => {
|
|
156
|
+
if (resolved == null) {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
if (accepted) {
|
|
161
|
+
window.localStorage.setItem(LOGIN_TERMS_STORAGE, resolved.version)
|
|
162
|
+
} else {
|
|
163
|
+
window.localStorage.removeItem(LOGIN_TERMS_STORAGE)
|
|
164
|
+
}
|
|
165
|
+
} catch { /* nothing to remember if storage was never available */ }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const termsLabelResolver = (
|
|
169
|
+
translate: (key: string, defaultValue: string) => string, locale: string | undefined
|
|
170
|
+
) => (doc: ResolvedTermsDocument): string => {
|
|
171
|
+
const fromMap = doc.labelMap != null
|
|
172
|
+
? (locale != null ? doc.labelMap[locale] : undefined) ?? Object.values(doc.labelMap)[0]
|
|
173
|
+
: undefined
|
|
174
|
+
let label = doc.label ?? fromMap
|
|
175
|
+
?? (doc.i18nKey != null ? translate(doc.i18nKey, DEFAULT_LABEL[doc.key] ?? doc.key) : doc.key)
|
|
176
|
+
|
|
177
|
+
if (doc.params != null) {
|
|
178
|
+
for (const [key, value] of Object.entries(doc.params)) {
|
|
179
|
+
label = label.split(`{{${key}}}`).join(value)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return label
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const termsAcceptanceOf = (
|
|
187
|
+
resolved: Pick<ResolvedTerms, 'documents' | 'notices' | 'version'>, locale?: string
|
|
188
|
+
): LoginTermsAcceptance => ({
|
|
189
|
+
documents: resolved.documents.map(doc => ({ key: doc.key, href: termsHrefOf(doc, locale), revisedAt: doc.revisedAt })),
|
|
190
|
+
notices: resolved.notices.map(doc => ({ key: doc.key, href: termsHrefOf(doc, locale), revisedAt: doc.revisedAt })),
|
|
191
|
+
version: resolved.version,
|
|
192
|
+
...(locale != null ? { locale } : {}),
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const termsDeferred = (ctx: LoginContext): boolean =>
|
|
196
|
+
ctx.hasService(LOGIN_SERVICE)
|
|
197
|
+
&& ctx.service<LoginService>(LOGIN_SERVICE).steps()
|
|
198
|
+
.some(step => step.confirmsTerms === true && ctx.hasEntrypoint(step.entrypoint))
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Format a list of documents into sentence parts, via `Intl.ListFormat` when it exists ("A, B and
|
|
202
|
+
* C" in the caller's own locale and conjunction word), falling back to a plain ", "/" and " join
|
|
203
|
+
* when it does not (an old runtime with no `Intl.ListFormat`).
|
|
204
|
+
*/
|
|
205
|
+
const formatDocumentList = (
|
|
206
|
+
docs: ResolvedTermsDocument[], locale: string | undefined, resolveLabel: (doc: ResolvedTermsDocument) => string
|
|
207
|
+
): TermsSentencePart[] => {
|
|
208
|
+
if (docs.length < 1) {
|
|
209
|
+
return []
|
|
210
|
+
}
|
|
211
|
+
const labels = docs.map(resolveLabel)
|
|
212
|
+
|
|
213
|
+
if (typeof Intl !== 'undefined' && typeof Intl.ListFormat === 'function') {
|
|
214
|
+
let cursor = 0
|
|
215
|
+
const formatter = new Intl.ListFormat(locale, { style: 'long', type: 'conjunction' })
|
|
216
|
+
|
|
217
|
+
return formatter.formatToParts(labels).map(part => {
|
|
218
|
+
if (part.type === 'element') {
|
|
219
|
+
const doc = docs[cursor++]
|
|
220
|
+
|
|
221
|
+
return { text: part.value, href: termsHrefOf(doc, locale), documentKey: doc.key }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { text: part.value }
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const parts: TermsSentencePart[] = []
|
|
229
|
+
docs.forEach((doc, index) => {
|
|
230
|
+
if (index > 0) {
|
|
231
|
+
parts.push({ text: index === docs.length - 1 ? ' and ' : ', ' })
|
|
232
|
+
}
|
|
233
|
+
parts.push({ text: labels[index], href: termsHrefOf(doc, locale), documentKey: doc.key })
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
return parts
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const termsSentence = (
|
|
240
|
+
template: string,
|
|
241
|
+
resolved: Pick<ResolvedTerms, 'documents' | 'notices'>,
|
|
242
|
+
locale: string | undefined,
|
|
243
|
+
resolveLabel: (doc: ResolvedTermsDocument) => string
|
|
244
|
+
): TermsSentencePart[] => {
|
|
245
|
+
const byKey = (key: string): ResolvedTermsDocument | undefined =>
|
|
246
|
+
resolved.documents.find(doc => doc.key === key) ?? resolved.notices.find(doc => doc.key === key)
|
|
247
|
+
|
|
248
|
+
const placeholder = /\{\{(documents|notices|terms|privacy|cookies)\}\}/g
|
|
249
|
+
const parts: TermsSentencePart[] = []
|
|
250
|
+
let lastIndex = 0
|
|
251
|
+
let match: RegExpExecArray | null
|
|
252
|
+
|
|
253
|
+
while ((match = placeholder.exec(template)) != null) {
|
|
254
|
+
if (match.index > lastIndex) {
|
|
255
|
+
parts.push({ text: template.slice(lastIndex, match.index) })
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const token = match[1]
|
|
259
|
+
if (token === 'documents') {
|
|
260
|
+
parts.push(...formatDocumentList(resolved.documents, locale, resolveLabel))
|
|
261
|
+
} else if (token === 'notices') {
|
|
262
|
+
parts.push(...formatDocumentList(resolved.notices, locale, resolveLabel))
|
|
263
|
+
} else {
|
|
264
|
+
const doc = byKey(token)
|
|
265
|
+
if (doc != null) {
|
|
266
|
+
parts.push({ text: resolveLabel(doc), href: termsHrefOf(doc, locale), documentKey: doc.key })
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
lastIndex = placeholder.lastIndex
|
|
271
|
+
}
|
|
272
|
+
if (lastIndex < template.length) {
|
|
273
|
+
parts.push({ text: template.slice(lastIndex) })
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return parts
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
resolveTerms, termsAccepted, acceptTerms, termsLabelResolver, termsHrefOf, termsAcceptanceOf, termsDeferred, termsSentence,
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export const loginTermsHelper = createLoginTermsHelper()
|
|
285
|
+
|
|
286
|
+
/** @deprecated compat:factory-refactor — use `loginTermsHelper.resolveTerms(…)` */
|
|
287
|
+
export const resolveTerms = (cfg?: LoginTermsConfig | false): ResolvedTerms | null => loginTermsHelper.resolveTerms(cfg)
|
|
288
|
+
|
|
289
|
+
/** @deprecated compat:factory-refactor — use `loginTermsHelper.termsAccepted(…)` */
|
|
290
|
+
export const termsAccepted = (resolved: ResolvedTerms | null): boolean => loginTermsHelper.termsAccepted(resolved)
|
|
291
|
+
|
|
292
|
+
/** @deprecated compat:factory-refactor — use `loginTermsHelper.acceptTerms(…)` */
|
|
293
|
+
export const acceptTerms = (resolved: ResolvedTerms | null, accepted: boolean): void =>
|
|
294
|
+
loginTermsHelper.acceptTerms(resolved, accepted)
|
|
295
|
+
|
|
296
|
+
/** @deprecated compat:factory-refactor — use `loginTermsHelper.termsLabelResolver(…)` */
|
|
297
|
+
export const termsLabelResolver = (
|
|
298
|
+
translate: (key: string, defaultValue: string) => string, locale: string | undefined
|
|
299
|
+
): ((doc: ResolvedTermsDocument) => string) => loginTermsHelper.termsLabelResolver(translate, locale)
|
|
300
|
+
|
|
301
|
+
/** @deprecated compat:factory-refactor — use `loginTermsHelper.termsAcceptanceOf(…)` */
|
|
302
|
+
export const termsAcceptanceOf = (
|
|
303
|
+
resolved: Pick<ResolvedTerms, 'documents' | 'notices' | 'version'>, locale?: string
|
|
304
|
+
): LoginTermsAcceptance => loginTermsHelper.termsAcceptanceOf(resolved, locale)
|
|
305
|
+
|
|
306
|
+
/** @deprecated compat:factory-refactor — use `loginTermsHelper.termsDeferred(…)` */
|
|
307
|
+
export const termsDeferred = (ctx: LoginContext): boolean => loginTermsHelper.termsDeferred(ctx)
|
|
308
|
+
|
|
309
|
+
/** @deprecated compat:factory-refactor — use `loginTermsHelper.termsSentence(…)` */
|
|
310
|
+
export const termsSentence = (
|
|
311
|
+
template: string,
|
|
312
|
+
resolved: Pick<ResolvedTerms, 'documents' | 'notices'>,
|
|
313
|
+
locale: string | undefined,
|
|
314
|
+
resolveLabel: (doc: ResolvedTermsDocument) => string
|
|
315
|
+
): TermsSentencePart[] => loginTermsHelper.termsSentence(template, resolved, locale, resolveLabel)
|
package/src/login/types.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import type { ComponentType, CSSProperties, ReactNode } from 'react'
|
|
1
2
|
import type { LazyService, BasicContext } from '@owlmeans/context'
|
|
3
|
+
import type { LoginMethodEmphasis, LoginScreenConfig, LoginTermsConfig } from '@owlmeans/config'
|
|
4
|
+
import { LoginOutcome } from './consts.js'
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* The context a login plugin is handed.
|
|
@@ -27,22 +30,6 @@ export interface LoginEnv {
|
|
|
27
30
|
hasOpener: boolean
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
/** What a stage did, and therefore what the caller must do next. */
|
|
31
|
-
export enum LoginOutcome {
|
|
32
|
-
/** The plugin took the flow over and it is finished. The caller does nothing more. */
|
|
33
|
-
Handled = 'handled',
|
|
34
|
-
/** The plugin did nothing — the caller carries on with its ordinary continuation. */
|
|
35
|
-
Passed = 'passed',
|
|
36
|
-
/** The browser is leaving this document. The caller must not navigate or render. */
|
|
37
|
-
Redirected = 'redirected',
|
|
38
|
-
/** Cannot proceed without a fresh user gesture — the caller renders a sign-in control. */
|
|
39
|
-
Gesture = 'gesture',
|
|
40
|
-
/** Authenticated, but with no channel back to the window that started the flow. */
|
|
41
|
-
Orphaned = 'orphaned',
|
|
42
|
-
/** The attempt ended with no token (blocked window, user closed it, provider refused). */
|
|
43
|
-
Failed = 'failed',
|
|
44
|
-
}
|
|
45
|
-
|
|
46
33
|
export interface LoginRequest {
|
|
47
34
|
/** Where the login flow starts — a resolved dispatcher path, or the current address. */
|
|
48
35
|
url: string
|
|
@@ -52,10 +39,28 @@ export interface LoginRequest {
|
|
|
52
39
|
* fallback is a full page load of {@link url}.
|
|
53
40
|
*/
|
|
54
41
|
navigate?: () => void | Promise<void>
|
|
55
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Entrypoint alias to land on once sign-in completes.
|
|
44
|
+
*
|
|
45
|
+
* The facade parks it (`suspendLanding` from `@owlmeans/client-flow`, the record
|
|
46
|
+
* `resumeSuspendedFlow` reads) once the preconditions have passed, and holds {@link navigate}
|
|
47
|
+
* until the write has landed — so the continuation still goes to the dispatcher, whose ordinary
|
|
48
|
+
* post-sign-in landing (`landAfterLogin`: landing hooks, pending steps such as a consent screen,
|
|
49
|
+
* then the suspended landing) ends on this screen. It is never a navigation target of its own:
|
|
50
|
+
* a guarded screen reached before sign-in renders signed out. An attempt that ends without a
|
|
51
|
+
* sign-in (`Blocked`, `Failed`, `Gesture`) discards the parked screen again. A caller with no
|
|
52
|
+
* {@link navigate} leaves the document at once, so its parked screen is best-effort only.
|
|
53
|
+
*/
|
|
56
54
|
target?: string
|
|
57
55
|
}
|
|
58
56
|
|
|
57
|
+
export interface LogoutRequest {
|
|
58
|
+
/** Where a surrogate logout runs — a resolved surrogate path. */
|
|
59
|
+
url: string
|
|
60
|
+
/** The in-app continuation once the local session is gone. */
|
|
61
|
+
navigate?: () => void | Promise<void>
|
|
62
|
+
}
|
|
63
|
+
|
|
59
64
|
/**
|
|
60
65
|
* A pluggable login mechanic — WHERE the authorization round trip runs.
|
|
61
66
|
*
|
|
@@ -95,14 +100,161 @@ export interface LoginPlugin {
|
|
|
95
100
|
|
|
96
101
|
/** A bearer token was issued in this document — decide where it goes. */
|
|
97
102
|
complete: (ctx: LoginContext, token: string, env: LoginEnv) => Promise<LoginOutcome>
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* This document ALREADY holds a session — decide whether it is useful here.
|
|
106
|
+
*
|
|
107
|
+
* Absent means `Passed`: keep it and carry on, which is what an ordinary tab has always done.
|
|
108
|
+
* A surrogate hands it back to its opener instead, which is the whole point: a popup that
|
|
109
|
+
* discovers an existing session must sign the framed application in, not display the
|
|
110
|
+
* application to itself.
|
|
111
|
+
*
|
|
112
|
+
* Deliberately not `complete`: "a token was just issued here" and "a token was already here"
|
|
113
|
+
* are different facts, and a silent-refresh plugin will need to tell them apart.
|
|
114
|
+
*/
|
|
115
|
+
resume?: (ctx: LoginContext, token: string, env: LoginEnv) => Promise<LoginOutcome>
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Start logout from a user gesture.
|
|
119
|
+
*
|
|
120
|
+
* MUST open any window synchronously, for exactly the reason `begin` must — logging out of a
|
|
121
|
+
* framed application opens a window too. Implement as a NON-async function.
|
|
122
|
+
*/
|
|
123
|
+
logout?: (ctx: LoginContext, request: LogoutRequest, env: LoginEnv) => Promise<LoginOutcome>
|
|
124
|
+
|
|
125
|
+
/** The local session is gone in this document — decide what to tell whom. */
|
|
126
|
+
logoutComplete?: (ctx: LoginContext, env: LoginEnv) => Promise<LoginOutcome>
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Something that must be true before a login flow may start.
|
|
131
|
+
*
|
|
132
|
+
* SYNCHRONOUS on purpose. `begin` must not cross a microtask boundary before a plugin's
|
|
133
|
+
* `window.open`, or the popup blocker eats the window. A precondition that has to ask a server
|
|
134
|
+
* belongs somewhere else entirely.
|
|
135
|
+
*
|
|
136
|
+
* Returning false stops the flow and resolves `begin` as {@link LoginOutcome.Gesture} — which
|
|
137
|
+
* already means "cannot proceed without a fresh user gesture; render a control", and is exactly
|
|
138
|
+
* the state a user is in after a blocking dialog has opened over the page.
|
|
139
|
+
*/
|
|
140
|
+
export interface LoginPrecondition {
|
|
141
|
+
alias: string
|
|
142
|
+
/** Higher runs first. Defaults to 0. */
|
|
143
|
+
priority?: number
|
|
144
|
+
check: (ctx: LoginContext, request: LoginRequest, env: LoginEnv) => boolean
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** What a source needs in order to describe the methods it offers. */
|
|
148
|
+
export interface LoginMethodContext {
|
|
149
|
+
context: LoginContext
|
|
150
|
+
env: LoginEnv
|
|
151
|
+
/** In-app navigation, when a component supplied it. */
|
|
152
|
+
navigate?: (alias: string, params?: Record<string, string>) => void | Promise<void>
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* One way to sign in, as the screen renders it.
|
|
157
|
+
*
|
|
158
|
+
* `start` is what a button calls. It MUST be callable synchronously from a click — branch on
|
|
159
|
+
* `env.embedded && !env.surrogate` first and open any window before the first `await`.
|
|
160
|
+
*/
|
|
161
|
+
export interface LoginMethod {
|
|
162
|
+
id: string
|
|
163
|
+
/** Set when an `AuthenticationPlugin` drives this method. */
|
|
164
|
+
type?: string
|
|
165
|
+
label?: string
|
|
166
|
+
i18nKey?: string
|
|
167
|
+
icon?: string
|
|
168
|
+
order?: number
|
|
169
|
+
emphasis?: LoginMethodEmphasis
|
|
170
|
+
restricted?: boolean
|
|
171
|
+
params?: Record<string, string>
|
|
172
|
+
start: (ctx: LoginMethodContext) => Promise<LoginOutcome>
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface LoginMethodSource {
|
|
176
|
+
alias: string
|
|
177
|
+
list: (ctx: LoginMethodContext) => LoginMethod[]
|
|
98
178
|
}
|
|
99
179
|
|
|
180
|
+
/** What every rendering of the sign-in screen accepts, whatever its UI family. */
|
|
181
|
+
export interface LoginScreenProps {
|
|
182
|
+
/** The one thing a consuming application is expected to supply. */
|
|
183
|
+
Logo?: ComponentType<{ className?: string }> | ReactNode
|
|
184
|
+
title?: ReactNode
|
|
185
|
+
subtitle?: ReactNode
|
|
186
|
+
/**
|
|
187
|
+
* `(key, defaultValue) => string`. A prop, never an implicit context read: a component that
|
|
188
|
+
* reaches for an i18n provider crashes the whole render in an app mounted without one.
|
|
189
|
+
*/
|
|
190
|
+
translate?: (key: string, defaultValue: string) => string
|
|
191
|
+
/**
|
|
192
|
+
* The current language, for `Intl.ListFormat`, locale-keyed document labels and explicit hrefs.
|
|
193
|
+
*
|
|
194
|
+
* A prop for the same reason `translate` is: a component that reaches for an i18n context
|
|
195
|
+
* directly crashes the whole render in an app mounted without one.
|
|
196
|
+
*/
|
|
197
|
+
locale?: string
|
|
198
|
+
/** Replace or reorder what the resolver produced. */
|
|
199
|
+
methods?: LoginMethod[] | ((methods: LoginMethod[]) => LoginMethod[])
|
|
200
|
+
terms?: LoginTermsConfig | false
|
|
201
|
+
config?: LoginScreenConfig
|
|
202
|
+
/** Replaces the composed credit line entirely. */
|
|
203
|
+
footer?: ReactNode
|
|
204
|
+
className?: string
|
|
205
|
+
containerClassName?: string
|
|
206
|
+
/**
|
|
207
|
+
* Inline overrides for the screen's outer box.
|
|
208
|
+
*
|
|
209
|
+
* It exists because the outer box carries its viewport height inline rather than as a utility
|
|
210
|
+
* class, and an escape hatch that a class can no longer provide has to be provided some other
|
|
211
|
+
* way. See the note on the screen itself.
|
|
212
|
+
*/
|
|
213
|
+
style?: CSSProperties
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export type LoginScreenComponent = ComponentType<LoginScreenProps>
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* A way to surface a `begin`/`logout` outcome that has no inline screen to render it on.
|
|
220
|
+
*
|
|
221
|
+
* A header "Log in"/"Log out" control (`useLogin`/`useLogout`) fires the facade and forgets the
|
|
222
|
+
* result — it renders nothing of its own, unlike the sign-in screen, which already shows
|
|
223
|
+
* `loginAttemptError` inline. Registering one is how a UI package (`web-panel`, `mui-panel`) gives
|
|
224
|
+
* that control a way to speak — e.g. a toast when {@link LoginOutcome.Blocked} fires. Unregistered,
|
|
225
|
+
* it is silence, which is what a non-DOM host and a screen-mounted flow both already have.
|
|
226
|
+
*/
|
|
227
|
+
export interface LoginNotifier { (outcome: LoginOutcome, env: LoginEnv): void }
|
|
228
|
+
|
|
100
229
|
export interface LoginService extends LazyService {
|
|
101
230
|
registerPlugin: (plugin: LoginPlugin) => void
|
|
102
231
|
/** Select the active plugin for the given (or default) environment. */
|
|
103
232
|
plugin: (env?: LoginEnv) => LoginPlugin
|
|
104
233
|
/** The environment the cascade is currently selecting on. */
|
|
105
234
|
env: () => LoginEnv
|
|
235
|
+
/** Something that must hold before any flow starts. Checked synchronously, in `begin`. */
|
|
236
|
+
registerPrecondition: (precondition: LoginPrecondition) => void
|
|
237
|
+
/** A source of offerable sign-in methods, scoped to this context. */
|
|
238
|
+
registerMethodSource: (source: LoginMethodSource) => void
|
|
239
|
+
methods: (ctx: LoginMethodContext) => LoginMethod[]
|
|
240
|
+
/**
|
|
241
|
+
* The screen a dispatcher renders when it cannot proceed.
|
|
242
|
+
*
|
|
243
|
+
* A slot rather than an import, because a relying party (`web-oidc-rp`) must never depend on a
|
|
244
|
+
* UI family (`web-panel` / `mui-panel`) — that edge would force every relying party to pick one.
|
|
245
|
+
*/
|
|
246
|
+
registerScreen: (screen: LoginScreenComponent) => void
|
|
247
|
+
screen: () => LoginScreenComponent | null
|
|
248
|
+
/** See {@link LoginNotifier}. Replaces any previously registered notifier. */
|
|
249
|
+
registerNotifier: (notifier: LoginNotifier) => void
|
|
250
|
+
/** Register a post-login step. Replace-by-alias, priority-sorted (higher first). */
|
|
251
|
+
registerStep: (step: LoginStep) => void
|
|
252
|
+
/** Every registered step, priority-sorted (higher first). */
|
|
253
|
+
steps: () => LoginStep[]
|
|
254
|
+
/** Register a hook that observes a freshly authenticated token. Replace-by-alias, priority-sorted. */
|
|
255
|
+
onLanded: (hook: LoginLandingHook) => void
|
|
256
|
+
/** Every registered landing hook, priority-sorted (higher first). */
|
|
257
|
+
landingHooks: () => LoginLandingHook[]
|
|
106
258
|
// Facade — every method re-selects the plugin and delegates.
|
|
107
259
|
// NOTE: these stay plain writable instance properties, never getters, so that alternative
|
|
108
260
|
// implementations (e.g. a native login service) can monkey-patch them directly.
|
|
@@ -110,8 +262,72 @@ export interface LoginService extends LazyService {
|
|
|
110
262
|
begin: (request: LoginRequest) => Promise<LoginOutcome>
|
|
111
263
|
authorize: (url: string) => Promise<LoginOutcome>
|
|
112
264
|
complete: (token: string) => Promise<LoginOutcome>
|
|
265
|
+
resume: (token: string) => Promise<LoginOutcome>
|
|
266
|
+
logout: (request: LogoutRequest) => Promise<LoginOutcome>
|
|
267
|
+
logoutComplete: () => Promise<LoginOutcome>
|
|
113
268
|
/** Adopt an issued bearer token as this context's authentication. */
|
|
114
269
|
adopt: (token: string) => Promise<void>
|
|
270
|
+
/** Drop this document's authentication. The single de-adoption path. */
|
|
271
|
+
revoke: () => Promise<void>
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The value shape a landing's `params`/`query` carries.
|
|
276
|
+
*
|
|
277
|
+
* Matches `FlowPayload` (`@owlmeans/flow`) and the `AbstractRequest` params/query fields
|
|
278
|
+
* (`@owlmeans/entrypoint`) structurally, rather than the narrower `Record<string, string>`, so a
|
|
279
|
+
* `LoginLanding` can be built directly from either a `SuspendedLandingRecord`'s query or a
|
|
280
|
+
* dispatcher's own forwarded `params`/`query` without a lossy re-cast at every call site.
|
|
281
|
+
*/
|
|
282
|
+
export type LoginLandingParams = Record<string, string | number | boolean | undefined | null>
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* A step to run after sign-in completes, before the ordinary landing.
|
|
286
|
+
*
|
|
287
|
+
* Registered by a package that must collect something from a freshly authenticated user before
|
|
288
|
+
* the application resumes wherever it was headed — the marketing-consent screen is the first
|
|
289
|
+
* consumer. `landAfterLogin`/`continueLogin` (./land.js) walk the registered steps in priority
|
|
290
|
+
* order and land on the first one whose `pending` resolves `true`.
|
|
291
|
+
*/
|
|
292
|
+
export interface LoginStep {
|
|
293
|
+
alias: string
|
|
294
|
+
/** Higher runs first. Defaults to 0. */
|
|
295
|
+
priority?: number
|
|
296
|
+
/** Entrypoint alias to navigate to while this step is pending. */
|
|
297
|
+
entrypoint: string
|
|
298
|
+
/** Extra query parameters for the landing, alongside the step's own alias. */
|
|
299
|
+
query?: (ctx: LoginContext) => LoginLandingParams | undefined
|
|
300
|
+
/** Whether this step still has something to collect from the signed-in user. */
|
|
301
|
+
pending: (ctx: LoginContext) => Promise<boolean>
|
|
302
|
+
/**
|
|
303
|
+
* This step is where the Terms confirmation lives instead of on the sign-in screen.
|
|
304
|
+
*
|
|
305
|
+
* `termsDeferred(ctx)` (`./terms.js`) is true only while a step with this flag is both
|
|
306
|
+
* registered AND bound (`ctx.hasEntrypoint(step.entrypoint)`) — an app that registers the step
|
|
307
|
+
* but never binds its screen keeps the sign-in checkbox, fail-closed.
|
|
308
|
+
*/
|
|
309
|
+
confirmsTerms?: boolean
|
|
310
|
+
/**
|
|
311
|
+
* A `pending` that throws or times out counts as PENDING rather than "not pending".
|
|
312
|
+
*
|
|
313
|
+
* The default (`undefined`/`false`) keeps every existing step fail-open, exactly as before —
|
|
314
|
+
* this flag is for a step whose whole point is to gate the landing on something the person must
|
|
315
|
+
* do (Terms confirmation), where a broken read must show the step rather than let it through.
|
|
316
|
+
*/
|
|
317
|
+
required?: boolean
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Observes that a freshly authenticated token has landed — unlike {@link LoginStep}, it never
|
|
322
|
+
* blocks the landing, it only reacts to it, once per distinct token.
|
|
323
|
+
*
|
|
324
|
+
* MUST never throw uncaught: the caller (`landAfterLogin`) bounds and swallows every hook's
|
|
325
|
+
* failure, on a timeout as well as a rejection, so a broken hook can never block sign-in.
|
|
326
|
+
*/
|
|
327
|
+
export interface LoginLandingHook {
|
|
328
|
+
alias: string
|
|
329
|
+
priority?: number
|
|
330
|
+
landed: (ctx: LoginContext) => Promise<void>
|
|
115
331
|
}
|
|
116
332
|
|
|
117
333
|
export interface LoginServiceAppend {
|