@owlmeans/client-auth 0.1.18-rc.8 → 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 +85 -23
- 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.d.ts.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/errors.d.ts.map +1 -1
- package/build/manager/errors.js.map +1 -1
- 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/wallet.d.ts.map +1 -1
- 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/service.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
package/src/login/service.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import { createLazyService } from '@owlmeans/context'
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { LoginOutcome } from './
|
|
7
|
-
import
|
|
1
|
+
import { createLazyService, type BasicConfig, type BasicContext } from '@owlmeans/context'
|
|
2
|
+
import type { CommonConfig } from '@owlmeans/config'
|
|
3
|
+
import type { ClientContext } from '@owlmeans/client'
|
|
4
|
+
import { flowLandingOf } from '@owlmeans/client-flow'
|
|
5
|
+
import { logger } from '@owlmeans/log'
|
|
6
|
+
import { DEFAULT_ALIAS, LOGIN_TARGET_TTL, LoginOutcome } from './consts.js'
|
|
7
|
+
import { UNSIGNED_OUTCOMES } from './consts.local.js'
|
|
8
|
+
import { loginEnvHelper } from './env.js'
|
|
9
|
+
import { loginTokenOf } from './adopt.js'
|
|
10
|
+
import { loginMethodsHelper } from './methods.js'
|
|
11
|
+
import type {
|
|
12
|
+
LoginContext, LoginLandingHook, LoginMethodSource, LoginNotifier, LoginPlugin, LoginPrecondition,
|
|
13
|
+
LoginRequest, LoginScreenComponent, LoginService, LoginServiceAppend, LoginStep
|
|
14
|
+
} from './types.js'
|
|
15
|
+
|
|
16
|
+
const log = logger('client-auth:login')
|
|
8
17
|
|
|
9
18
|
/**
|
|
10
19
|
* Login service = plugin host. It holds a registry of login plugins and, on every facade call,
|
|
@@ -19,15 +28,61 @@ import type { LoginContext, LoginPlugin, LoginService, LoginServiceAppend } from
|
|
|
19
28
|
* stage. `context.service()` throws for an uninitialized non-lazy service, which would make
|
|
20
29
|
* `ensureLoginService` fail exactly where apps are told to call it.
|
|
21
30
|
*
|
|
22
|
-
* `begin`
|
|
23
|
-
* microtask boundary, and a `window.open` that lands after the user gesture has
|
|
24
|
-
* handled is eaten by the popup blocker. The same rule binds every plugin's own
|
|
31
|
+
* `begin` and `logout` are deliberately NOT `async`. An async facade method defers the plugin's
|
|
32
|
+
* body past a microtask boundary, and a `window.open` that lands after the user gesture has
|
|
33
|
+
* finished being handled is eaten by the popup blocker. The same rule binds every plugin's own
|
|
34
|
+
* `begin` and `logout`, and it is why a precondition must be synchronous too.
|
|
25
35
|
*/
|
|
26
36
|
export const makeLoginService = (alias: string = DEFAULT_ALIAS): LoginService => {
|
|
27
37
|
const plugins: LoginPlugin[] = []
|
|
38
|
+
const preconditions: LoginPrecondition[] = []
|
|
39
|
+
const methodSources: LoginMethodSource[] = []
|
|
40
|
+
const steps: LoginStep[] = []
|
|
41
|
+
const hooks: LoginLandingHook[] = []
|
|
42
|
+
let screen: LoginScreenComponent | null = null
|
|
43
|
+
let notifier: LoginNotifier | null = null
|
|
28
44
|
|
|
29
45
|
const ctx = (): LoginContext => service.ctx as LoginContext
|
|
30
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Park `request.target` for the post-sign-in landing, and hand back the request whose continuation
|
|
49
|
+
* waits for that write.
|
|
50
|
+
*
|
|
51
|
+
* The write is STARTED here and never awaited on the way in: `begin` must reach the plugin's
|
|
52
|
+
* `window.open` inside the gesture. Only the continuation waits — the redirect plugin's in-app hop
|
|
53
|
+
* to the dispatcher and the surrogate's hop after the popup has handed its token back — so the
|
|
54
|
+
* dispatcher that resumes the landing always finds the record, whatever the storage behind it.
|
|
55
|
+
* A write that fails costs the landing, never the sign-in.
|
|
56
|
+
*/
|
|
57
|
+
const parkTarget = (
|
|
58
|
+
ctx: LoginContext, request: LoginRequest
|
|
59
|
+
): [LoginRequest, Promise<boolean> | null] => {
|
|
60
|
+
if (request.target == null || request.target === '') {
|
|
61
|
+
return [request, null]
|
|
62
|
+
}
|
|
63
|
+
const parked = flowLandingOf(ctx as unknown as ClientContext).suspendLanding(
|
|
64
|
+
{ entrypoint: request.target, query: {} },
|
|
65
|
+
{ expiresAt: Date.now() + LOGIN_TARGET_TTL }
|
|
66
|
+
).catch((error: unknown) => {
|
|
67
|
+
log.warn('Could not park the sign-in target; the landing falls back', {
|
|
68
|
+
target: request.target, error,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
return false
|
|
72
|
+
})
|
|
73
|
+
const navigate = request.navigate
|
|
74
|
+
|
|
75
|
+
return [
|
|
76
|
+
navigate == null ? request : {
|
|
77
|
+
...request, navigate: async () => {
|
|
78
|
+
await parked
|
|
79
|
+
await navigate()
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
parked,
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
|
|
31
86
|
const service: LoginService = createLazyService<LoginService>(alias, {
|
|
32
87
|
registerPlugin: (plugin: LoginPlugin) => {
|
|
33
88
|
const existing = plugins.findIndex(candidate => candidate.alias === plugin.alias)
|
|
@@ -38,8 +93,59 @@ export const makeLoginService = (alias: string = DEFAULT_ALIAS): LoginService =>
|
|
|
38
93
|
plugins.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
|
|
39
94
|
},
|
|
40
95
|
|
|
96
|
+
registerPrecondition: (precondition: LoginPrecondition) => {
|
|
97
|
+
const existing = preconditions.findIndex(candidate => candidate.alias === precondition.alias)
|
|
98
|
+
if (existing >= 0) {
|
|
99
|
+
preconditions.splice(existing, 1)
|
|
100
|
+
}
|
|
101
|
+
preconditions.push(precondition)
|
|
102
|
+
preconditions.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
registerMethodSource: (source: LoginMethodSource) => {
|
|
106
|
+
const existing = methodSources.findIndex(candidate => candidate.alias === source.alias)
|
|
107
|
+
if (existing >= 0) {
|
|
108
|
+
methodSources.splice(existing, 1)
|
|
109
|
+
}
|
|
110
|
+
methodSources.push(source)
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
methods: methodCtx => loginMethodsHelper.resolveLoginMethods(
|
|
114
|
+
methodCtx,
|
|
115
|
+
(methodCtx.context.cfg as CommonConfig).security?.auth?.login,
|
|
116
|
+
methodSources
|
|
117
|
+
),
|
|
118
|
+
|
|
119
|
+
registerScreen: value => { screen = value },
|
|
120
|
+
|
|
121
|
+
screen: () => screen,
|
|
122
|
+
|
|
123
|
+
registerNotifier: value => { notifier = value },
|
|
124
|
+
|
|
125
|
+
registerStep: (step: LoginStep) => {
|
|
126
|
+
const existing = steps.findIndex(candidate => candidate.alias === step.alias)
|
|
127
|
+
if (existing >= 0) {
|
|
128
|
+
steps.splice(existing, 1)
|
|
129
|
+
}
|
|
130
|
+
steps.push(step)
|
|
131
|
+
steps.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
steps: () => [...steps],
|
|
135
|
+
|
|
136
|
+
onLanded: (hook: LoginLandingHook) => {
|
|
137
|
+
const existing = hooks.findIndex(candidate => candidate.alias === hook.alias)
|
|
138
|
+
if (existing >= 0) {
|
|
139
|
+
hooks.splice(existing, 1)
|
|
140
|
+
}
|
|
141
|
+
hooks.push(hook)
|
|
142
|
+
hooks.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
landingHooks: () => [...hooks],
|
|
146
|
+
|
|
41
147
|
plugin: env => {
|
|
42
|
-
const environment = env ?? defaultLoginEnv()
|
|
148
|
+
const environment = env ?? loginEnvHelper.defaultLoginEnv()
|
|
43
149
|
const plugin = plugins.find(
|
|
44
150
|
candidate => candidate.match?.(environment, service.ctx as LoginContext | undefined) ?? true
|
|
45
151
|
)
|
|
@@ -49,32 +155,89 @@ export const makeLoginService = (alias: string = DEFAULT_ALIAS): LoginService =>
|
|
|
49
155
|
return plugin
|
|
50
156
|
},
|
|
51
157
|
|
|
52
|
-
env: () => defaultLoginEnv(),
|
|
158
|
+
env: () => loginEnvHelper.defaultLoginEnv(),
|
|
53
159
|
|
|
54
160
|
enter: () => {
|
|
55
|
-
const env = defaultLoginEnv()
|
|
161
|
+
const env = loginEnvHelper.defaultLoginEnv()
|
|
56
162
|
service.plugin(env).enter?.(ctx(), env)
|
|
57
163
|
},
|
|
58
164
|
|
|
59
165
|
begin: request => {
|
|
60
|
-
const env = defaultLoginEnv()
|
|
166
|
+
const env = loginEnvHelper.defaultLoginEnv()
|
|
167
|
+
// Synchronously, before anything can open a window: a precondition that refuses leaves the
|
|
168
|
+
// user exactly where `Gesture` describes — unable to proceed until they act again.
|
|
169
|
+
for (const precondition of preconditions) {
|
|
170
|
+
if (!precondition.check(ctx(), request, env)) {
|
|
171
|
+
return Promise.resolve(LoginOutcome.Gesture)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Parked only past the preconditions: a refused attempt leaves no landing behind for the
|
|
176
|
+
// next, unrelated sign-in to resume.
|
|
177
|
+
const [parkedRequest, parked] = parkTarget(ctx(), request)
|
|
178
|
+
|
|
179
|
+
// Notified here, in the facade, rather than by each caller: `useLogin` fires the flow and
|
|
180
|
+
// forgets the result, and this is the one place every caller — the header hook and the
|
|
181
|
+
// sign-in screen's method buttons alike — passes through on the way to a settled outcome.
|
|
182
|
+
return service.plugin(env).begin(ctx(), parkedRequest, env).then(outcome => {
|
|
183
|
+
if (parked != null && UNSIGNED_OUTCOMES.has(outcome)) {
|
|
184
|
+
void parked.then(async () => {
|
|
185
|
+
await flowLandingOf(ctx() as unknown as ClientContext).discardSuspendedLanding()
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
notifier?.(outcome, env)
|
|
61
189
|
|
|
62
|
-
|
|
190
|
+
return outcome
|
|
191
|
+
})
|
|
63
192
|
},
|
|
64
193
|
|
|
65
194
|
authorize: async url => {
|
|
66
|
-
const env = defaultLoginEnv()
|
|
195
|
+
const env = loginEnvHelper.defaultLoginEnv()
|
|
67
196
|
|
|
68
197
|
return await service.plugin(env).authorize(ctx(), url, env)
|
|
69
198
|
},
|
|
70
199
|
|
|
71
200
|
complete: async token => {
|
|
72
|
-
const env = defaultLoginEnv()
|
|
201
|
+
const env = loginEnvHelper.defaultLoginEnv()
|
|
73
202
|
|
|
74
203
|
return await service.plugin(env).complete(ctx(), token, env)
|
|
75
204
|
},
|
|
76
205
|
|
|
77
|
-
|
|
206
|
+
resume: async token => {
|
|
207
|
+
const env = loginEnvHelper.defaultLoginEnv()
|
|
208
|
+
// Absent means "keep it and carry on" — which is what an ordinary tab has always done, and
|
|
209
|
+
// is why the redirect plugin implements nothing here.
|
|
210
|
+
return await service.plugin(env).resume?.(ctx(), token, env) ?? LoginOutcome.Passed
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
logout: request => {
|
|
214
|
+
const env = loginEnvHelper.defaultLoginEnv()
|
|
215
|
+
const plugin = service.plugin(env)
|
|
216
|
+
if (plugin.logout == null) {
|
|
217
|
+
// A plugin with no logout mechanic still has to end the session it started.
|
|
218
|
+
return loginTokenOf(ctx()).revokeToken().then(async () => {
|
|
219
|
+
await request.navigate?.()
|
|
220
|
+
|
|
221
|
+
return LoginOutcome.Passed
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return plugin.logout(ctx(), request, env).then(outcome => {
|
|
226
|
+
notifier?.(outcome, env)
|
|
227
|
+
|
|
228
|
+
return outcome
|
|
229
|
+
})
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
logoutComplete: async () => {
|
|
233
|
+
const env = loginEnvHelper.defaultLoginEnv()
|
|
234
|
+
|
|
235
|
+
return await service.plugin(env).logoutComplete?.(ctx(), env) ?? LoginOutcome.Passed
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
adopt: async token => { await loginTokenOf(ctx()).adoptToken(token) },
|
|
239
|
+
|
|
240
|
+
revoke: async () => { await loginTokenOf(ctx()).revokeToken() },
|
|
78
241
|
})
|
|
79
242
|
|
|
80
243
|
return service
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { LoginOutcome } from '../consts.js'
|
|
2
|
+
|
|
3
|
+
/** What a "Log in" control hands `startLogin`: where the flow starts, where it lands, how to move. */
|
|
4
|
+
export interface LoginStart {
|
|
5
|
+
/** The resolved dispatcher path — where the login flow starts. */
|
|
6
|
+
url: string
|
|
7
|
+
/** Entrypoint alias to land on once signed in; absent means the ordinary landing. */
|
|
8
|
+
target?: string
|
|
9
|
+
/** The in-app navigation, supplied by a component because only a component may hold one. */
|
|
10
|
+
go: (alias: string) => void | Promise<void>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The decision behind a "Log in" control, bound to the application context. */
|
|
14
|
+
export interface LoginStartHelper {
|
|
15
|
+
/**
|
|
16
|
+
* What a "Log in" control does when clicked — the decision behind `useLogin`, kept free of React so
|
|
17
|
+
* it is the same rule wherever a control is rendered.
|
|
18
|
+
*
|
|
19
|
+
* - A target and a session already here: go straight to the target — there is nobody to sign in.
|
|
20
|
+
* - Otherwise: begin a sign-in whose continuation is ALWAYS the dispatcher, never the target. A
|
|
21
|
+
* target travels as `LoginRequest.target`, which the facade parks for the landing; the
|
|
22
|
+
* dispatcher's own post-sign-in landing (`landAfterLogin` — landing hooks, pending steps such as a
|
|
23
|
+
* consent screen, then the parked target) brings the person there signed in. Navigating to a
|
|
24
|
+
* guarded target instead is what used to land a person on it signed out.
|
|
25
|
+
*
|
|
26
|
+
* NOT async, and nothing is awaited before `begin`: the surrogate plugin opens its window inside
|
|
27
|
+
* this call, and a window opened after the gesture has been handled is eaten by the popup blocker.
|
|
28
|
+
*/
|
|
29
|
+
startLogin: (start: LoginStart) => Promise<LoginOutcome>
|
|
30
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { DISPATCHER } from '@owlmeans/auth'
|
|
2
|
+
import type { AuthService } from '@owlmeans/auth-common'
|
|
3
|
+
import { DEFAULT_ALIAS as AUTH_ALIAS } from '../consts.js'
|
|
4
|
+
import { LOGIN_SERVICE, LoginOutcome } from './consts.js'
|
|
5
|
+
import type { LoginContext, LoginService } from './types.js'
|
|
6
|
+
import { memoHelper } from '@owlmeans/context'
|
|
7
|
+
import type { LoginStartHelper } from './start/types.js'
|
|
8
|
+
|
|
9
|
+
export const makeLoginStartHelper = (ctx: LoginContext): LoginStartHelper => {
|
|
10
|
+
/**
|
|
11
|
+
* Whether this document already holds a session, answered WITHOUT awaiting anything.
|
|
12
|
+
*
|
|
13
|
+
* Read off the auth service's in-memory token, which every guard and `authenticated()` call fills.
|
|
14
|
+
* A session not yet read from storage answers `false` and costs nothing: the sign-in it starts finds
|
|
15
|
+
* the session at the dispatcher, which lands on the parked target all the same.
|
|
16
|
+
*/
|
|
17
|
+
const holdsSession = (): boolean => {
|
|
18
|
+
if (!ctx.hasService(AUTH_ALIAS)) {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
const token = ctx.service<AuthService>(AUTH_ALIAS).token
|
|
22
|
+
|
|
23
|
+
return token != null && token !== ''
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const startLogin: LoginStartHelper['startLogin'] = start => {
|
|
27
|
+
const { url, target, go } = start
|
|
28
|
+
if (target != null && target !== '' && holdsSession()) {
|
|
29
|
+
return Promise.resolve(go(target)).then(() => LoginOutcome.Handled)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return ctx.service<LoginService>(LOGIN_SERVICE).begin({
|
|
33
|
+
url,
|
|
34
|
+
target,
|
|
35
|
+
// Only a component may call `useNavigate`, so the in-app continuation is handed to the
|
|
36
|
+
// plugin rather than reinvented by it.
|
|
37
|
+
navigate: () => { void go(DISPATCHER) },
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { startLogin }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const loginStartOf = memoHelper.oncePer(makeLoginStartHelper)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LoginIntent } from '../consts.js'
|
|
2
|
+
|
|
3
|
+
export interface SurrogateTarget {
|
|
4
|
+
intent: LoginIntent
|
|
5
|
+
/** The address the surrogate should actually run, once it is one window up. */
|
|
6
|
+
next?: string
|
|
7
|
+
/** The method the user already chose in the opener. */
|
|
8
|
+
method?: string
|
|
9
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { DISPATCHER_SURROGATE } from '@owlmeans/auth'
|
|
2
|
+
import type { ClientEntrypoint } from '@owlmeans/client-entrypoint'
|
|
3
|
+
import { LOGIN_INTENT_QUERY, LOGIN_METHOD_QUERY, LOGIN_NEXT_QUERY } from './consts.js'
|
|
4
|
+
import type { LoginContext } from './types.js'
|
|
5
|
+
import type { SurrogateTarget } from './surrogate/types.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Where a surrogate window opens.
|
|
9
|
+
*
|
|
10
|
+
* Returns `null` when the application's entrypoint list predates the surrogate route — an app
|
|
11
|
+
* built against an older `@owlmeans/auth-common` has no such entrypoint, and the caller then falls
|
|
12
|
+
* back to opening the dispatcher directly, which is exactly what it used to do. That fallback is
|
|
13
|
+
* the whole compatibility story for already-deployed applications, so it must never be removed in
|
|
14
|
+
* favour of throwing.
|
|
15
|
+
*/
|
|
16
|
+
export const surrogatePath = (ctx: LoginContext, target: SurrogateTarget): string | null => {
|
|
17
|
+
if (!ctx.hasEntrypoint(DISPATCHER_SURROGATE)) {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
let path: string
|
|
21
|
+
try {
|
|
22
|
+
path = ctx.entrypoint<ClientEntrypoint>(DISPATCHER_SURROGATE).path()
|
|
23
|
+
} catch {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const query = new URLSearchParams()
|
|
28
|
+
query.set(LOGIN_INTENT_QUERY, target.intent)
|
|
29
|
+
if (target.next != null && target.next !== '') {
|
|
30
|
+
query.set(LOGIN_NEXT_QUERY, target.next)
|
|
31
|
+
}
|
|
32
|
+
if (target.method != null && target.method !== '') {
|
|
33
|
+
query.set(LOGIN_METHOD_QUERY, target.method)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return `${path}?${query.toString()}`
|
|
37
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { LoginTermsConfig } from '@owlmeans/config'
|
|
2
|
+
import type { LoginContext } from '../types.js'
|
|
3
|
+
|
|
4
|
+
/** One document (or notice) a sign-in screen links to. */
|
|
5
|
+
export interface ResolvedTermsDocument {
|
|
6
|
+
key: string
|
|
7
|
+
href: string
|
|
8
|
+
/** Published translations, selected at render/record time; `href` remains canonical. */
|
|
9
|
+
hrefMap?: Record<string, string>
|
|
10
|
+
/** A caller's own label, already resolved to one string — only ever set for a custom document. */
|
|
11
|
+
label?: string
|
|
12
|
+
/** A locale map for the label, when the config supplied one — resolved by the renderer, not here. */
|
|
13
|
+
labelMap?: Record<string, string>
|
|
14
|
+
/** Translation key under `login.terms.*`, when this document has one. */
|
|
15
|
+
i18nKey?: string
|
|
16
|
+
revisedAt?: string
|
|
17
|
+
/** Extra interpolation values for `i18nKey`'s translation (e.g. `{ product: 'Acme' }`). */
|
|
18
|
+
params?: Record<string, string>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ResolvedTerms {
|
|
22
|
+
required: boolean
|
|
23
|
+
terms: string
|
|
24
|
+
privacy: string
|
|
25
|
+
cookies?: string
|
|
26
|
+
version: string
|
|
27
|
+
/** What the checkbox agrees to — terms, then billing/product when configured, then custom. */
|
|
28
|
+
documents: ResolvedTermsDocument[]
|
|
29
|
+
/** What is merely DISCLOSED, never consented to — privacy, plus cookies per the rule below. */
|
|
30
|
+
notices: ResolvedTermsDocument[]
|
|
31
|
+
/** The latest `revisedAt` among {@link documents}. Only set when `showRevision` is true. */
|
|
32
|
+
revisedAt?: string
|
|
33
|
+
showRevision: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One document as `@owlmeans/marketing-consent`'s `TermsDocumentRef` shape — structurally, with
|
|
37
|
+
* no dependency on that package (client-auth is a layer below it). */
|
|
38
|
+
export interface TermsAcceptanceRef {
|
|
39
|
+
key: string
|
|
40
|
+
href: string
|
|
41
|
+
revisedAt?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One fragment of an interpolated terms sentence — plain text, or a link to a document. */
|
|
45
|
+
export interface TermsSentencePart {
|
|
46
|
+
text: string
|
|
47
|
+
href?: string
|
|
48
|
+
documentKey?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** What a sign-in-time terms acceptance sends the server. */
|
|
52
|
+
export interface LoginTermsAcceptance {
|
|
53
|
+
documents: TermsAcceptanceRef[]
|
|
54
|
+
notices: TermsAcceptanceRef[]
|
|
55
|
+
version: string
|
|
56
|
+
locale?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The documents a sign-in agrees to: their resolution, the local acceptance record and the sentence. */
|
|
60
|
+
export interface LoginTermsHelper {
|
|
61
|
+
/** The configured documents and notices, with the version acceptance is recorded against; null when terms are off. */
|
|
62
|
+
resolveTerms: (cfg?: LoginTermsConfig | false) => ResolvedTerms | null
|
|
63
|
+
/**
|
|
64
|
+
* Whether this browser has already agreed to exactly these documents.
|
|
65
|
+
*
|
|
66
|
+
* `localStorage` and not a cookie: the record is a UI convenience, it never travels to a server,
|
|
67
|
+
* and it must be readable by the surrogate login window — which is same-origin, so it sees the
|
|
68
|
+
* opener's acceptance and does not ask twice. A preview iframe on a different origin gets its own
|
|
69
|
+
* partition and therefore its own acceptance, which is correct rather than unfortunate.
|
|
70
|
+
*/
|
|
71
|
+
termsAccepted: (resolved: ResolvedTerms | null) => boolean
|
|
72
|
+
/** Record (or, with `accepted: false`, forget) this browser's agreement to these documents. */
|
|
73
|
+
acceptTerms: (resolved: ResolvedTerms | null, accepted: boolean) => void
|
|
74
|
+
/**
|
|
75
|
+
* A document's own label: a caller's literal `label`, else its `labelMap` for the current locale,
|
|
76
|
+
* else its `i18nKey` translated — each with `params` (e.g. `{ product: 'Acme' }`) interpolated
|
|
77
|
+
* afterwards, since the `translate` contract every renderer shares is a plain
|
|
78
|
+
* `(key, defaultValue) => string` with no interpolation option of its own.
|
|
79
|
+
*
|
|
80
|
+
* The one label resolver every renderer of a terms sentence uses — `FallbackLoginScreen` here, and
|
|
81
|
+
* `@owlmeans/web-panel`'s `LoginTerms` — so the English fallbacks and the resolution order live in
|
|
82
|
+
* exactly one place instead of two hand-kept copies.
|
|
83
|
+
*/
|
|
84
|
+
termsLabelResolver: (
|
|
85
|
+
translate: (key: string, defaultValue: string) => string, locale: string | undefined
|
|
86
|
+
) => (doc: ResolvedTermsDocument) => string
|
|
87
|
+
/** Explicit locale destination, then base language, then canonical href; never rewrites paths. */
|
|
88
|
+
termsHrefOf: (doc: ResolvedTermsDocument, locale?: string) => string
|
|
89
|
+
/**
|
|
90
|
+
* What a sign-in-time terms acceptance sends the server — structurally
|
|
91
|
+
* `@owlmeans/marketing-consent`'s `TermsAcceptance`, so `client.recordTerms(termsAcceptanceOf(...))`
|
|
92
|
+
* type-checks with no dependency in this direction. Shared by `termsRecorder` (the sign-in screen's
|
|
93
|
+
* own local acceptance, copied at landing) and a Terms-mode consent screen recording the box it
|
|
94
|
+
* just showed.
|
|
95
|
+
*/
|
|
96
|
+
termsAcceptanceOf: (
|
|
97
|
+
resolved: Pick<ResolvedTerms, 'documents' | 'notices' | 'version'>, locale?: string
|
|
98
|
+
) => LoginTermsAcceptance
|
|
99
|
+
/**
|
|
100
|
+
* Whether the Terms confirmation has been moved off the sign-in screen onto a registered step.
|
|
101
|
+
*
|
|
102
|
+
* True only when a step both DECLARES `confirmsTerms` and is BOUND (`ctx.hasEntrypoint`) — an app
|
|
103
|
+
* that registers the step (e.g. `appendMarketingConsent({ terms: 'step' })`) but never binds its
|
|
104
|
+
* screen (an older target, a partial import) keeps the sign-in checkbox, fail-closed: a person must
|
|
105
|
+
* never find the confirmation missing from both places at once.
|
|
106
|
+
*
|
|
107
|
+
* Reads the service directly (`ctx.hasService`), never `ensureLoginService` — that registers an
|
|
108
|
+
* empty host as a side effect, which a render-time check must not do.
|
|
109
|
+
*/
|
|
110
|
+
termsDeferred: (ctx: LoginContext) => boolean
|
|
111
|
+
/**
|
|
112
|
+
* Interpolate a translated sentence template around its document list(s).
|
|
113
|
+
*
|
|
114
|
+
* Supports `{{documents}}` (→ `resolved.documents`) and `{{notices}}` (→ `resolved.notices`), and
|
|
115
|
+
* — for backward compatibility with an older template string — the legacy `{{terms}}`, `{{privacy}}`
|
|
116
|
+
* and `{{cookies}}` placeholders, each resolved to its one matching document. Framework-agnostic on
|
|
117
|
+
* purpose: it returns plain data, never JSX, so both `@owlmeans/web-panel` and the plain fallback
|
|
118
|
+
* screen can turn the parts into their own markup.
|
|
119
|
+
*/
|
|
120
|
+
termsSentence: (
|
|
121
|
+
template: string,
|
|
122
|
+
resolved: Pick<ResolvedTerms, 'documents' | 'notices'>,
|
|
123
|
+
locale: string | undefined,
|
|
124
|
+
resolveLabel: (doc: ResolvedTermsDocument) => string
|
|
125
|
+
) => TermsSentencePart[]
|
|
126
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extra `LoginTermsConfig` fields, added by TypeScript module augmentation rather than by editing
|
|
3
|
+
* `@owlmeans/config` itself.
|
|
4
|
+
*
|
|
5
|
+
* `@owlmeans/config` has roughly 85 dependents, and every one of them sits in the same release
|
|
6
|
+
* closure — a config bump ripples through every package that pins a range against it, whether or
|
|
7
|
+
* not it touches sign-in. The fields below are needed by exactly one consumer family (the sign-in
|
|
8
|
+
* screen and its terms confirmation, all downstream of `@owlmeans/client-auth`), so they are
|
|
9
|
+
* declared here instead, merged onto the base interface by TypeScript's own declaration merging.
|
|
10
|
+
*
|
|
11
|
+
* This file must be part of whatever a consumer's own compilation actually loads, or the merge
|
|
12
|
+
* never happens — an ambient augmentation applies only once the compiler has read the file that
|
|
13
|
+
* declares it. `@owlmeans/client-auth/login`'s barrel (`./index.ts`) re-exports this module for
|
|
14
|
+
* exactly that reason, so ANY import from `@owlmeans/client-auth/login` — which every application
|
|
15
|
+
* wiring sign-in already has — pulls this augmentation into that program. An app that constructs a
|
|
16
|
+
* `LoginTermsConfig` literal in a file that imports NOTHING from `@owlmeans/client-auth/login`
|
|
17
|
+
* still benefits as long as some other file in the same `tsc` program does; if none does (an
|
|
18
|
+
* isolated leaf package that only ever imports `@owlmeans/config`), add
|
|
19
|
+
* `import type {} from '@owlmeans/client-auth/login'` to the file building the literal — an
|
|
20
|
+
* import with an empty type list, kept only to pull this file into that program's graph.
|
|
21
|
+
*
|
|
22
|
+
* `export {}` below is required, not decorative: without at least one top-level import/export this
|
|
23
|
+
* file is a SCRIPT to TypeScript, not a MODULE, and `declare module '@owlmeans/config'` inside a
|
|
24
|
+
* script augments the global scope instead of merging onto the named module's exports.
|
|
25
|
+
*/
|
|
26
|
+
declare module '@owlmeans/config' {
|
|
27
|
+
interface LoginTermsConfig {
|
|
28
|
+
/** `documents[].key` (or a custom document's own key) → the ISO date it was last revised. */
|
|
29
|
+
revisions?: Record<string, string>
|
|
30
|
+
/**
|
|
31
|
+
* Document/notice key → locale → published href. Opt-in; omitted locales use the canonical
|
|
32
|
+
* href. Renderers and acceptance evidence resolve the same current locale, while these
|
|
33
|
+
* translated destinations leave the canonical agreement version and revisions unchanged.
|
|
34
|
+
*/
|
|
35
|
+
localizedHrefs?: Record<string, Record<string, string>>
|
|
36
|
+
/** Off by default. A bare string is `{ href }`. */
|
|
37
|
+
billing?: string | { href: string, revisedAt?: string } | false
|
|
38
|
+
/** Off by default. `name` is interpolated into the `login.terms.product` translation. */
|
|
39
|
+
product?: { name: string, href: string, revisedAt?: string } | false
|
|
40
|
+
/**
|
|
41
|
+
* Custom documents beyond terms/billing/product, in the order they are agreed to.
|
|
42
|
+
*
|
|
43
|
+
* `label` may be a locale map (`{ en: '...', fr: '...' }`) — resolved at RENDER time, by
|
|
44
|
+
* whichever renderer has the current locale, never here: this file stays locale-free.
|
|
45
|
+
*/
|
|
46
|
+
documents?: Array<{
|
|
47
|
+
key: string, href: string, label?: string | Record<string, string>, i18nKey?: string,
|
|
48
|
+
revisedAt?: string,
|
|
49
|
+
}>
|
|
50
|
+
/** Show the latest revision date beneath the confirmation. Default false. */
|
|
51
|
+
showRevision?: boolean
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { }
|