@owlmeans/client-auth 0.1.18-rc.2 → 0.1.18-rc.21
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 +6 -6
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/client-auth/SKILL.md +134 -21
- package/build/components/dispatcher/component.js +1 -1
- package/build/components/dispatcher/component.js.map +1 -1
- package/build/{modules.d.ts → entrypoints.d.ts} +2 -2
- package/build/entrypoints.d.ts.map +1 -0
- package/build/{modules.js → entrypoints.js} +3 -3
- package/build/entrypoints.js.map +1 -0
- package/build/helper.js +1 -1
- 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.d.ts +24 -0
- package/build/login/adopt.d.ts.map +1 -0
- package/build/login/adopt.js +28 -0
- package/build/login/adopt.js.map +1 -0
- package/build/login/consts.d.ts +57 -0
- package/build/login/consts.d.ts.map +1 -0
- package/build/login/consts.js +57 -0
- package/build/login/consts.js.map +1 -0
- package/build/login/credit.d.ts +24 -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.d.ts +32 -0
- package/build/login/env.d.ts.map +1 -0
- package/build/login/env.js +84 -0
- package/build/login/env.js.map +1 -0
- 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 +37 -0
- package/build/login/hook.d.ts.map +1 -0
- package/build/login/hook.js +72 -0
- package/build/login/hook.js.map +1 -0
- package/build/login/i18n/be.json +37 -0
- package/build/login/i18n/de.json +37 -0
- package/build/login/i18n/en.json +37 -0
- package/build/login/i18n/es.json +37 -0
- package/build/login/i18n/pl.json +37 -0
- package/build/login/i18n/ru.json +37 -0
- package/build/login/i18n/uk.json +37 -0
- package/build/login/i18n.d.ts +2 -0
- package/build/login/i18n.d.ts.map +1 -0
- package/build/login/i18n.js +25 -0
- package/build/login/i18n.js.map +1 -0
- package/build/login/index.d.ts +15 -0
- package/build/login/index.d.ts.map +1 -0
- package/build/login/index.js +15 -0
- package/build/login/index.js.map +1 -0
- package/build/login/methods.d.ts +25 -0
- package/build/login/methods.d.ts.map +1 -0
- package/build/login/methods.js +97 -0
- package/build/login/methods.js.map +1 -0
- package/build/login/resume.d.ts +30 -0
- package/build/login/resume.d.ts.map +1 -0
- package/build/login/resume.js +55 -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 +76 -0
- package/build/login/screen.js.map +1 -0
- package/build/login/service.d.ts +31 -0
- package/build/login/service.d.ts.map +1 -0
- package/build/login/service.js +137 -0
- package/build/login/service.js.map +1 -0
- package/build/login/surrogate.d.ts +20 -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.d.ts +20 -0
- package/build/login/terms.d.ts.map +1 -0
- package/build/login/terms.js +0 -0
- package/build/login/terms.js.map +1 -0
- package/build/login/types.d.ts +236 -0
- package/build/login/types.d.ts.map +1 -0
- package/build/login/types.js +23 -0
- package/build/login/types.js.map +1 -0
- 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 +2 -0
- package/build/manager/entrypoints.d.ts.map +1 -0
- package/build/manager/{modules.js → entrypoints.js} +4 -4
- 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 +2 -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 +1 -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.d.ts +20 -0
- package/build/manager/plugins/registry.d.ts.map +1 -0
- package/build/manager/plugins/registry.js +19 -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 +4 -0
- 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.js +1 -1
- package/build/service.js.map +1 -1
- package/package.json +36 -27
- package/src/components/dispatcher/component.tsx +1 -1
- package/src/{modules.ts → entrypoints.ts} +2 -2
- package/src/helper.ts +1 -1
- package/src/index.ts +1 -1
- package/src/login/adopt.ts +31 -0
- package/src/login/consts.ts +70 -0
- package/src/login/credit.ts +75 -0
- package/src/login/env.ts +86 -0
- package/src/login/flow.ts +44 -0
- package/src/login/hook.ts +80 -0
- package/src/login/i18n/be.json +37 -0
- package/src/login/i18n/de.json +37 -0
- package/src/login/i18n/en.json +37 -0
- package/src/login/i18n/es.json +37 -0
- package/src/login/i18n/pl.json +37 -0
- package/src/login/i18n/ru.json +37 -0
- package/src/login/i18n/uk.json +37 -0
- package/src/login/i18n.ts +26 -0
- package/src/login/index.ts +15 -0
- package/src/login/methods.ts +113 -0
- package/src/login/resume.ts +56 -0
- package/src/login/screen.tsx +136 -0
- package/src/login/service.ts +181 -0
- package/src/login/surrogate.ts +45 -0
- package/src/login/terms.ts +0 -0
- package/src/login/types.ts +258 -0
- package/src/manager/README.md +1 -1
- 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/{modules.ts → entrypoints.ts} +3 -3
- package/src/manager/plugins/basic-ed25519.tsx +7 -0
- package/src/manager/plugins/exports.ts +2 -0
- package/src/manager/plugins/index.ts +10 -4
- package/src/manager/plugins/methods.ts +74 -0
- package/src/manager/plugins/re-captcha.tsx +7 -0
- package/src/manager/plugins/registry.ts +26 -0
- package/src/manager/plugins/tunnel-consumer.tsx +6 -0
- package/src/manager/plugins/types.ts +54 -0
- package/src/service.ts +1 -1
- package/tests/context.ts +1 -2
- package/tests/login.spec.ts +313 -0
- package/build/.gitkeep +0 -0
- package/build/manager/modules.d.ts +0 -2
- package/build/manager/modules.d.ts.map +0 -1
- package/build/manager/modules.js.map +0 -1
- package/build/modules.d.ts.map +0 -1
- package/build/modules.js.map +0 -1
|
@@ -45,7 +45,7 @@ export const makeControl = (
|
|
|
45
45
|
control.afterAuthenticate = plugins[control.type].afterAuthenticate
|
|
46
46
|
|
|
47
47
|
const module = context.entrypoint<ClientEntrypoint<AllowanceResponse>>(AUTHEN_INIT)
|
|
48
|
-
const
|
|
48
|
+
const allowance = await module.call({ body: control.request })
|
|
49
49
|
|
|
50
50
|
control.allowance = allowance
|
|
51
51
|
|
|
@@ -89,8 +89,8 @@ export const makeControl = (
|
|
|
89
89
|
// We return back unwrapped challenge
|
|
90
90
|
credentials.challenge = control.allowance?.challenge
|
|
91
91
|
|
|
92
|
-
const
|
|
93
|
-
.
|
|
92
|
+
const { value: token, outcome: status } = await context.entrypoint<ClientEntrypoint<AuthToken>>(AUTHEN_AUTHEN)
|
|
93
|
+
.invoke({ body: credentials })
|
|
94
94
|
|
|
95
95
|
if (status === EntrypointOutcome.Ok && token.token != null
|
|
96
96
|
&& token.token !== '' && control.afterAuthenticate != null) {
|
|
@@ -2,7 +2,7 @@ import type {
|
|
|
2
2
|
AllowanceRequest, AllowanceResponse, AuthCredentials, AuthenticationStage, AuthenticationType,
|
|
3
3
|
AuthToken
|
|
4
4
|
} from '@owlmeans/auth'
|
|
5
|
-
import type {
|
|
5
|
+
import type { EntrypointContextParams } from '@owlmeans/client'
|
|
6
6
|
import type { FC } from 'react'
|
|
7
7
|
import type { ClientContext } from '@owlmeans/client'
|
|
8
8
|
import type { FlowService } from '@owlmeans/client-flow'
|
|
@@ -10,7 +10,7 @@ import type { ResilientError } from '@owlmeans/error'
|
|
|
10
10
|
|
|
11
11
|
export type ClientAuthType = AuthenticationType | string
|
|
12
12
|
|
|
13
|
-
export interface AuthenticationProps extends
|
|
13
|
+
export interface AuthenticationProps extends EntrypointContextParams {
|
|
14
14
|
type?: ClientAuthType
|
|
15
15
|
callback?: AuthenticationCallback
|
|
16
16
|
source?: string
|
|
@@ -2,11 +2,11 @@ import { AuthenticationType } from '@owlmeans/auth'
|
|
|
2
2
|
import { AuthenticationHOC } from './authentication/component.js'
|
|
3
3
|
import type { FC } from 'react'
|
|
4
4
|
import { useMemo } from 'react'
|
|
5
|
-
import {
|
|
5
|
+
import { useEntrypoint } from '@owlmeans/client'
|
|
6
6
|
import type { TunnelAuthenticationProps } from './types.js'
|
|
7
7
|
|
|
8
8
|
export const TunnelConsumer: FC<Partial<TunnelAuthenticationProps>> = ({ ...props }) => {
|
|
9
|
-
const mod =
|
|
9
|
+
const mod = useEntrypoint()
|
|
10
10
|
const Implementation = useMemo(
|
|
11
11
|
() => AuthenticationHOC(undefined, AuthenticationType.WalletConsumer), []
|
|
12
12
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import { AUTHEN, AUTHEN_AUTHEN, AUTHEN_INIT, AUTHEN_RELY, CAUTHEN, CAUTHEN_AUTHEN, CAUTHEN_AUTHEN_DEFAULT, CAUTHEN_AUTHEN_TYPED, DISPATCHER } from '@owlmeans/auth'
|
|
3
|
-
import {
|
|
3
|
+
import { entrypoints as list } from '@owlmeans/auth-common'
|
|
4
4
|
import { handler } from '@owlmeans/client'
|
|
5
5
|
import { elevate, stab } from '@owlmeans/client-entrypoint'
|
|
6
6
|
import { AuthenticationHOC } from './components/authentication/component.js'
|
|
@@ -13,6 +13,6 @@ elevate(list, CAUTHEN)
|
|
|
13
13
|
elevate(list, CAUTHEN_AUTHEN)
|
|
14
14
|
elevate(list, CAUTHEN_AUTHEN_DEFAULT, handler(AuthenticationHOC()))
|
|
15
15
|
elevate(list, CAUTHEN_AUTHEN_TYPED, handler(AuthenticationHOC()))
|
|
16
|
-
elevate(list, DISPATCHER, stab
|
|
16
|
+
elevate(list, DISPATCHER, stab)
|
|
17
17
|
|
|
18
|
-
export const
|
|
18
|
+
export const entrypoints = list
|
|
@@ -7,6 +7,13 @@ import { AuthenCredError } from '../errors.js'
|
|
|
7
7
|
export const ed25519BasicUIPlugin: AuthenticationPlugin = {
|
|
8
8
|
type: AuthenticationType.BasicEd25519,
|
|
9
9
|
|
|
10
|
+
method: { order: 300, icon: 'key', emphasis: 'secondary' },
|
|
11
|
+
|
|
12
|
+
// This package ships no UI family, so the form below is a `Renderer` a panel package assigns
|
|
13
|
+
// (`@owlmeans/web-panel/auth/plugins`, `@owlmeans/mui-panel/auth/plugins`). Until one does, the
|
|
14
|
+
// Implementation throws on mount — so the sign-in screen must not offer the method.
|
|
15
|
+
requiresRenderer: true,
|
|
16
|
+
|
|
10
17
|
Implementation: Renderer => ({ type, stage, control, params }) => {
|
|
11
18
|
|
|
12
19
|
type = type ?? AuthenticationType.BasicEd25519
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import type { ClientAuthType } from '../components/index.js'
|
|
2
|
-
|
|
3
1
|
import { AuthenticationType } from '@owlmeans/auth'
|
|
4
2
|
import { ed25519BasicUIPlugin } from './basic-ed25519.js'
|
|
5
|
-
import { AuthenticationPlugin } from './types.js'
|
|
6
3
|
import { reCaptchaPlugin } from './re-captcha.js'
|
|
7
4
|
import { tunnelConsumerUIPlugin } from './tunnel-consumer.js'
|
|
5
|
+
import { pluginMethodSource } from './methods.js'
|
|
6
|
+
import { registerMethodSource } from '../../login/methods.js'
|
|
7
|
+
import { plugins } from './registry.js'
|
|
8
8
|
|
|
9
|
-
export
|
|
9
|
+
export { plugins, registerAuthPlugin, getAuthPlugin, listAuthPlugins } from './registry.js'
|
|
10
10
|
|
|
11
11
|
plugins[AuthenticationType.BasicEd25519] = ed25519BasicUIPlugin
|
|
12
12
|
plugins[AuthenticationType.ReCaptcha] = reCaptchaPlugin
|
|
13
13
|
plugins[AuthenticationType.WalletConsumer] = tunnelConsumerUIPlugin
|
|
14
|
+
|
|
15
|
+
// Registered from here, not from `login/`, so that importing `@owlmeans/client-auth/login` never
|
|
16
|
+
// drags this module's React plugin implementations into a bundle that only wanted the login host.
|
|
17
|
+
// A generated application imports the login subpath through `client-iam`; it must not gain three
|
|
18
|
+
// authentication methods it never registered as a side effect of that.
|
|
19
|
+
registerMethodSource(pluginMethodSource)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { CAUTHEN_AUTHEN_TYPED } from '@owlmeans/auth'
|
|
2
|
+
import type { ClientEntrypoint } from '@owlmeans/client-entrypoint'
|
|
3
|
+
import { DEFAULT_METHOD_ORDER, LOGIN_SERVICE } from '../../login/consts.js'
|
|
4
|
+
import type { LoginMethod, LoginMethodContext, LoginMethodSource, LoginService } from '../../login/types.js'
|
|
5
|
+
import type { AuthenticationPlugin } from './types.js'
|
|
6
|
+
import { listAuthPlugins } from './registry.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Whether a registered plugin may be put on the sign-in screen of THIS application.
|
|
10
|
+
*
|
|
11
|
+
* The registry is global and is filled by side-effect imports, so "registered" means only that
|
|
12
|
+
* some module was pulled into the bundle — three separate reasons why that is not enough:
|
|
13
|
+
*
|
|
14
|
+
* 1. No `method`: `re-captcha` is a STEP inside another flow, not a way to sign in.
|
|
15
|
+
* 2. No renderer: a plugin that declares `requiresRenderer` throws the moment its screen mounts
|
|
16
|
+
* unless a UI package (`web-panel`, `mui-panel`) assigned one. An app that imported the plugin
|
|
17
|
+
* host without that package would be offering buttons whose only outcome is a broken page.
|
|
18
|
+
* 3. Not wired: a plugin whose flow needs a service its context never appended fails on the
|
|
19
|
+
* lookup instead. Packages register their plugins in groups, so an app that wanted one of them
|
|
20
|
+
* gets the rest too, and the rest have to be able to say they do not apply here.
|
|
21
|
+
*/
|
|
22
|
+
const offerable = (plugin: AuthenticationPlugin, ctx: LoginMethodContext): boolean => {
|
|
23
|
+
if (plugin.method == null || plugin.method.hidden === true) {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
if (plugin.requiresRenderer === true && plugin.Renderer == null) {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return plugin.method.available?.(ctx) !== false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Every registered authentication plugin that declares itself offerable, as a sign-in method.
|
|
35
|
+
*/
|
|
36
|
+
export const pluginMethodSource: LoginMethodSource = {
|
|
37
|
+
alias: 'authentication-plugins',
|
|
38
|
+
|
|
39
|
+
list: ctx => listAuthPlugins()
|
|
40
|
+
.filter(plugin => offerable(plugin, ctx))
|
|
41
|
+
.map((plugin): LoginMethod => {
|
|
42
|
+
const meta = plugin.method!
|
|
43
|
+
const type = plugin.type
|
|
44
|
+
const id = meta.id ?? type
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
id, type,
|
|
48
|
+
...(meta.label != null ? { label: meta.label } : {}),
|
|
49
|
+
i18nKey: meta.i18nKey ?? id,
|
|
50
|
+
...(meta.icon != null ? { icon: meta.icon } : {}),
|
|
51
|
+
order: meta.order ?? DEFAULT_METHOD_ORDER,
|
|
52
|
+
...(meta.emphasis != null ? { emphasis: meta.emphasis } : {}),
|
|
53
|
+
...(meta.restricted === true ? { restricted: true } : {}),
|
|
54
|
+
params: { type },
|
|
55
|
+
|
|
56
|
+
// Non-async through the whole chain: `login.begin` opens the surrogate window in its first
|
|
57
|
+
// statement, and a window opened after the gesture has been handled is blocked.
|
|
58
|
+
start: methodCtx => {
|
|
59
|
+
const login = methodCtx.context.service<LoginService>(LOGIN_SERVICE)
|
|
60
|
+
const entrypoint = methodCtx.context.entrypoint<ClientEntrypoint>(CAUTHEN_AUTHEN_TYPED)
|
|
61
|
+
const url = entrypoint.path().replace(':type', encodeURIComponent(type))
|
|
62
|
+
|
|
63
|
+
return login.begin({
|
|
64
|
+
url,
|
|
65
|
+
// In an ordinary tab this keeps the application mounted, exactly as the pre-chooser
|
|
66
|
+
// dispatcher did; the surrogate plugin ignores it and opens a window instead.
|
|
67
|
+
...(methodCtx.navigate != null
|
|
68
|
+
? { navigate: () => methodCtx.navigate?.(CAUTHEN_AUTHEN_TYPED, { type }) }
|
|
69
|
+
: {}),
|
|
70
|
+
})
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
}),
|
|
74
|
+
}
|
|
@@ -5,6 +5,13 @@ import { useEffect } from 'react'
|
|
|
5
5
|
export const reCaptchaPlugin: AuthenticationPlugin = {
|
|
6
6
|
type: AuthenticationType.ReCaptcha,
|
|
7
7
|
|
|
8
|
+
// A STEP inside another flow, never a way to sign in — so it is registered and never offered.
|
|
9
|
+
method: { hidden: true },
|
|
10
|
+
|
|
11
|
+
// Its widget is a `Renderer` a panel package assigns. `hidden` already keeps it off the sign-in
|
|
12
|
+
// screen; this is what the flow that DOES drive it can check before handing it a stage.
|
|
13
|
+
requiresRenderer: true,
|
|
14
|
+
|
|
8
15
|
Implementation: Renderer => ({ type, stage, control, params }) => {
|
|
9
16
|
Renderer = Renderer ?? reCaptchaPlugin.Renderer
|
|
10
17
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ClientAuthType } from '../components/authentication/types.js'
|
|
2
|
+
import type { AuthenticationPlugin } from './types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The registry every authentication plugin lands in.
|
|
6
|
+
*
|
|
7
|
+
* It stays a plain exported object, and every registration stays a plain assignment, because four
|
|
8
|
+
* packages already write into it from side-effect imports (`web-oidc-rp`, `mui-oidc-rp`,
|
|
9
|
+
* `web-auth`, and this package's own `index`). Hiding it behind an API would break all of them for
|
|
10
|
+
* no gain — the functions below simply read it.
|
|
11
|
+
*
|
|
12
|
+
* It lives in its own module rather than in `index.ts` so that the method source can read it
|
|
13
|
+
* without the two importing each other.
|
|
14
|
+
*/
|
|
15
|
+
export const plugins: { [type: ClientAuthType]: AuthenticationPlugin } = {}
|
|
16
|
+
|
|
17
|
+
export const registerAuthPlugin = (plugin: AuthenticationPlugin): AuthenticationPlugin => {
|
|
18
|
+
plugins[plugin.type] = plugin
|
|
19
|
+
|
|
20
|
+
return plugin
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const getAuthPlugin = (type: ClientAuthType): AuthenticationPlugin | undefined =>
|
|
24
|
+
plugins[type]
|
|
25
|
+
|
|
26
|
+
export const listAuthPlugins = (): AuthenticationPlugin[] => Object.values(plugins)
|
|
@@ -13,6 +13,12 @@ import { createWalletFacade } from './tunnel/wallet.js'
|
|
|
13
13
|
export const tunnelConsumerUIPlugin: AuthenticationPlugin = {
|
|
14
14
|
type: AuthenticationType.WalletConsumer,
|
|
15
15
|
|
|
16
|
+
method: { order: 400, icon: 'wallet' },
|
|
17
|
+
|
|
18
|
+
// The PIN form is a `Renderer` a panel package assigns; without one the Implementation throws on
|
|
19
|
+
// mount, so the method must not be offered.
|
|
20
|
+
requiresRenderer: true,
|
|
21
|
+
|
|
16
22
|
Implementation: renderer => ({ type, stage, control, params }) => {
|
|
17
23
|
type = type ?? AuthenticationType.WalletConsumer
|
|
18
24
|
const Renderer: TunnelAuthenticationRenderer | undefined = renderer
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { FC } from 'react'
|
|
2
|
+
import type { LoginMethodEmphasis } from '@owlmeans/config'
|
|
3
|
+
import type { LoginMethodContext } from '../../login/types.js'
|
|
2
4
|
import type { AuthenticationControl, AuthenticationRenderer, AuthenticationRendererProps, } from '../components/authentication/types.js'
|
|
3
5
|
|
|
4
6
|
export interface AuthenticationPlugin extends Pick<
|
|
@@ -7,6 +9,58 @@ export interface AuthenticationPlugin extends Pick<
|
|
|
7
9
|
type: string
|
|
8
10
|
Implementation: PluginImplemnetation
|
|
9
11
|
Renderer?: AuthenticationRenderer
|
|
12
|
+
/**
|
|
13
|
+
* This plugin's `Implementation` cannot render without a `Renderer` a UI package supplies.
|
|
14
|
+
*
|
|
15
|
+
* It is declared rather than inferred because there is nothing to infer it from: a plugin that
|
|
16
|
+
* renders its own form and one that throws `Renderer is not defined` are the same object until
|
|
17
|
+
* the screen mounts. Declaring it is what lets the sign-in screen leave out a method whose
|
|
18
|
+
* renderer nobody registered, instead of offering a button that crashes the page it opens.
|
|
19
|
+
*/
|
|
20
|
+
requiresRenderer?: boolean
|
|
21
|
+
/**
|
|
22
|
+
* How this method presents itself on the sign-in screen.
|
|
23
|
+
*
|
|
24
|
+
* Absent means the plugin is never OFFERED — it is still reachable by type, which is what a
|
|
25
|
+
* step-in-a-flow (re-captcha) and a plugin an app deep-links to both need. Every field is
|
|
26
|
+
* optional so an existing plugin object keeps type-checking without being touched.
|
|
27
|
+
*/
|
|
28
|
+
method?: AuthMethodMeta
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AuthMethodMeta {
|
|
32
|
+
/** Defaults to the plugin's `type`. */
|
|
33
|
+
id?: string
|
|
34
|
+
/** Literal fallback used when no translation resolves. */
|
|
35
|
+
label?: string
|
|
36
|
+
/** Key under the `auth` library's `login.method.*`; defaults to the id. */
|
|
37
|
+
i18nKey?: string
|
|
38
|
+
/** Icon registry NAME, never markup — this package must stay free of an icon library. */
|
|
39
|
+
icon?: string
|
|
40
|
+
/** Ascending; default 100. */
|
|
41
|
+
order?: number
|
|
42
|
+
emphasis?: LoginMethodEmphasis
|
|
43
|
+
/**
|
|
44
|
+
* Never offered unless the configuration explicitly enables it.
|
|
45
|
+
*
|
|
46
|
+
* This is the secret-key gate: an operator login is registered like any other method and must
|
|
47
|
+
* not become offerable merely by being registered.
|
|
48
|
+
*/
|
|
49
|
+
restricted?: boolean
|
|
50
|
+
/** Registered, but never a choice. */
|
|
51
|
+
hidden?: boolean
|
|
52
|
+
/**
|
|
53
|
+
* Whether the application this method is about to be offered in actually wired it.
|
|
54
|
+
*
|
|
55
|
+
* The plugin registry is global and is written to by side-effect imports, so being registered
|
|
56
|
+
* says only that some bundle pulled the module in — and modules arrive in groups. An app that
|
|
57
|
+
* wanted one method out of a package gets every method that package registers, including ones
|
|
58
|
+
* whose first act is to look up a service its context never appended. This is where a plugin
|
|
59
|
+
* says so, per context, at render time; `false` leaves it off the screen.
|
|
60
|
+
*
|
|
61
|
+
* Absent means "always", which is right for a plugin that needs nothing but itself.
|
|
62
|
+
*/
|
|
63
|
+
available?: (ctx: LoginMethodContext) => boolean
|
|
10
64
|
}
|
|
11
65
|
|
|
12
66
|
export interface PluginImplemnetation {
|
package/src/service.ts
CHANGED
|
@@ -21,7 +21,7 @@ export const makeAuthService = (alias: string = DEFAULT_ALIAS): AuthService => {
|
|
|
21
21
|
authenticate: async token => {
|
|
22
22
|
const context = assertContext(service.ctx, location)
|
|
23
23
|
|
|
24
|
-
const
|
|
24
|
+
const authToken = await context.entrypoint<ClientEntrypoint<AuthToken>>(DISPATCHER_AUTHEN)
|
|
25
25
|
.call({ body: token })
|
|
26
26
|
|
|
27
27
|
await service.update(authToken.token)
|
package/tests/context.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AppType,
|
|
1
|
+
import { AppType, makeBasicContext } from '@owlmeans/context'
|
|
2
2
|
import type { BasicConfig, BasicContext } from '@owlmeans/context'
|
|
3
3
|
import { createStaticResource } from '@owlmeans/static-resource'
|
|
4
4
|
import { AUTH_RESOURCE, DEFAULT_ALIAS } from '../src/consts.js'
|
|
@@ -10,7 +10,6 @@ export const makeTestContext = () => {
|
|
|
10
10
|
const cfg: BasicConfig = {
|
|
11
11
|
ready: false,
|
|
12
12
|
service: 'client-auth-tests',
|
|
13
|
-
layer: Layer.Application,
|
|
14
13
|
type: AppType.Frontend,
|
|
15
14
|
services: {},
|
|
16
15
|
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { makeTestContext } from './context.js'
|
|
3
|
+
import { appendLogin, makeLoginService } from '../src/login/service.js'
|
|
4
|
+
import { resolveLoginMethods, primaryLoginMethod, registerMethodSource } from '../src/login/methods.js'
|
|
5
|
+
import { resolveCredit } from '../src/login/credit.js'
|
|
6
|
+
import { pluginMethodSource } from '../src/manager/plugins/methods.js'
|
|
7
|
+
import { registerAuthPlugin } from '../src/manager/plugins/registry.js'
|
|
8
|
+
import type { AuthenticationPlugin } from '../src/manager/plugins/types.js'
|
|
9
|
+
import { resumeAction, ResumeAction, loginAttemptError } from '../src/login/resume.js'
|
|
10
|
+
import { LoginOutcome } from '../src/login/types.js'
|
|
11
|
+
import type {
|
|
12
|
+
LoginEnv, LoginMethod, LoginMethodContext, LoginPlugin, LoginService,
|
|
13
|
+
} from '../src/login/types.js'
|
|
14
|
+
import type { BasicContext, BasicConfig } from '@owlmeans/context'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A hand-written environment descriptor.
|
|
18
|
+
*
|
|
19
|
+
* `LoginEnv` exists precisely so a plugin's decisions can be exercised without a DOM — a stubbed
|
|
20
|
+
* `window` would prove that the stub behaves, not that the cascade does.
|
|
21
|
+
*/
|
|
22
|
+
const env = (over?: Partial<LoginEnv>): LoginEnv => ({
|
|
23
|
+
hasWindow: true, embedded: false, surrogate: false, hasOpener: false, ...over,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const stubPlugin = (over: Partial<LoginPlugin> & { alias: string }): LoginPlugin => ({
|
|
27
|
+
begin: async () => LoginOutcome.Passed,
|
|
28
|
+
authorize: async () => LoginOutcome.Passed,
|
|
29
|
+
complete: async () => LoginOutcome.Passed,
|
|
30
|
+
...over,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const bootstrap = (): [BasicContext<BasicConfig>, LoginService] => {
|
|
34
|
+
const context = makeTestContext()
|
|
35
|
+
const contextual = appendLogin(context)
|
|
36
|
+
|
|
37
|
+
return [context, contextual.login()]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const methodCtx = (context: BasicContext<BasicConfig>, over?: Partial<LoginEnv>): LoginMethodContext =>
|
|
41
|
+
({ context, env: env(over) })
|
|
42
|
+
|
|
43
|
+
const method = (over: Partial<LoginMethod> & { id: string }): LoginMethod => ({
|
|
44
|
+
start: async () => LoginOutcome.Passed, ...over,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('login plugin host', () => {
|
|
48
|
+
test('the cascade picks the highest-priority plugin whose match holds', () => {
|
|
49
|
+
const service = makeLoginService()
|
|
50
|
+
service.registerPlugin(stubPlugin({ alias: 'default', priority: 0, match: () => true }))
|
|
51
|
+
service.registerPlugin(stubPlugin({
|
|
52
|
+
alias: 'framed', priority: 100, match: candidate => candidate.embedded,
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
expect(service.plugin(env()).alias).toBe('default')
|
|
56
|
+
expect(service.plugin(env({ embedded: true })).alias).toBe('framed')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('a plugin with no resume yields Passed, so an ordinary tab is untouched', async () => {
|
|
60
|
+
const [, service] = bootstrap()
|
|
61
|
+
service.registerPlugin(stubPlugin({ alias: 'plain', match: () => true }))
|
|
62
|
+
|
|
63
|
+
expect(await service.resume('a-token')).toBe(LoginOutcome.Passed)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('a plugin with a resume is asked instead', async () => {
|
|
67
|
+
const [, service] = bootstrap()
|
|
68
|
+
service.registerPlugin(stubPlugin({
|
|
69
|
+
alias: 'framed', match: () => true, resume: async () => LoginOutcome.Handled,
|
|
70
|
+
}))
|
|
71
|
+
|
|
72
|
+
expect(await service.resume('a-token')).toBe(LoginOutcome.Handled)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('a refusing precondition stops the flow before any plugin runs', async () => {
|
|
76
|
+
const [, service] = bootstrap()
|
|
77
|
+
let started = false
|
|
78
|
+
service.registerPlugin(stubPlugin({
|
|
79
|
+
alias: 'plain', match: () => true,
|
|
80
|
+
begin: async () => { started = true; return LoginOutcome.Handled },
|
|
81
|
+
}))
|
|
82
|
+
service.registerPrecondition({ alias: 'refuse', check: () => false })
|
|
83
|
+
|
|
84
|
+
expect(await service.begin({ url: '/dispatcher' })).toBe(LoginOutcome.Gesture)
|
|
85
|
+
expect(started).toBe(false)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('a passing precondition lets the plugin run', async () => {
|
|
89
|
+
const [, service] = bootstrap()
|
|
90
|
+
service.registerPlugin(stubPlugin({
|
|
91
|
+
alias: 'plain', match: () => true, begin: async () => LoginOutcome.Handled,
|
|
92
|
+
}))
|
|
93
|
+
service.registerPrecondition({ alias: 'allow', check: () => true })
|
|
94
|
+
|
|
95
|
+
expect(await service.begin({ url: '/dispatcher' })).toBe(LoginOutcome.Handled)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('a plugin with no logout still ends the session it was asked to end', async () => {
|
|
99
|
+
const [context, service] = bootstrap()
|
|
100
|
+
context.configure()
|
|
101
|
+
await context.init()
|
|
102
|
+
service.registerPlugin(stubPlugin({ alias: 'plain', match: () => true }))
|
|
103
|
+
const auth = context.service<{ token?: string, update: (t?: string) => Promise<void> }>('auth')
|
|
104
|
+
|
|
105
|
+
expect(await service.logout({ url: '' })).toBe(LoginOutcome.Passed)
|
|
106
|
+
expect(auth.token == null || auth.token === '').toBe(true)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('login method resolution', () => {
|
|
111
|
+
test('methods sort by declared order, ties by id', () => {
|
|
112
|
+
const context = makeTestContext()
|
|
113
|
+
const source = {
|
|
114
|
+
alias: 'test', list: () => [
|
|
115
|
+
method({ id: 'b', order: 10 }), method({ id: 'a', order: 10 }), method({ id: 'z', order: 1 }),
|
|
116
|
+
],
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const resolved = resolveLoginMethods(methodCtx(context), undefined, [source])
|
|
120
|
+
|
|
121
|
+
expect(resolved.map(item => item.id)).toEqual(['z', 'a', 'b'])
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('a restricted method is not offered until the configuration asks for it', () => {
|
|
125
|
+
const context = makeTestContext()
|
|
126
|
+
const source = {
|
|
127
|
+
alias: 'test', list: () => [method({ id: 'ordinary' }), method({ id: 'operator', restricted: true })],
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
expect(resolveLoginMethods(methodCtx(context), undefined, [source]).map(m => m.id))
|
|
131
|
+
.toEqual(['ordinary'])
|
|
132
|
+
expect(resolveLoginMethods(
|
|
133
|
+
methodCtx(context), { overrides: { operator: { enabled: true } } }, [source]
|
|
134
|
+
).map(m => m.id).sort()).toEqual(['operator', 'ordinary'])
|
|
135
|
+
expect(resolveLoginMethods(
|
|
136
|
+
methodCtx(context), { methods: ['operator'] }, [source]
|
|
137
|
+
).map(m => m.id)).toEqual(['operator'])
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('an explicit list is both the filter and the order', () => {
|
|
141
|
+
const context = makeTestContext()
|
|
142
|
+
const source = {
|
|
143
|
+
alias: 'test', list: () => [method({ id: 'a' }), method({ id: 'b' }), method({ id: 'c' })],
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
expect(resolveLoginMethods(methodCtx(context), { methods: ['c', 'a'] }, [source]).map(m => m.id))
|
|
147
|
+
.toEqual(['c', 'a'])
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('a source that throws is skipped rather than taking the screen down', () => {
|
|
151
|
+
const context = makeTestContext()
|
|
152
|
+
const broken = { alias: 'broken', list: () => { throw new Error('misconfigured') } }
|
|
153
|
+
const working = { alias: 'working', list: () => [method({ id: 'ok' })] }
|
|
154
|
+
|
|
155
|
+
expect(resolveLoginMethods(methodCtx(context), undefined, [broken, working]).map(m => m.id))
|
|
156
|
+
.toEqual(['ok'])
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
test('the primary method is the first emphasised one, else the first', () => {
|
|
160
|
+
expect(primaryLoginMethod([
|
|
161
|
+
method({ id: 'a' }), method({ id: 'b', emphasis: 'primary' }),
|
|
162
|
+
])?.id).toBe('b')
|
|
163
|
+
expect(primaryLoginMethod([method({ id: 'a' })])?.id).toBe('a')
|
|
164
|
+
expect(primaryLoginMethod([])).toBeNull()
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test('a globally registered source reaches the resolver', () => {
|
|
168
|
+
const context = makeTestContext()
|
|
169
|
+
registerMethodSource({ alias: 'global-test', list: () => [method({ id: 'global' })] })
|
|
170
|
+
|
|
171
|
+
expect(resolveLoginMethods(methodCtx(context)).map(m => m.id)).toContain('global')
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('the credit line', () => {
|
|
176
|
+
const year = new Date().getFullYear()
|
|
177
|
+
|
|
178
|
+
test('is a copyright NOTICE — the mark and the year, not a bare name', () => {
|
|
179
|
+
expect(resolveCredit({ product: 'OwlMeans', organization: 'OwlMeans' }).line)
|
|
180
|
+
.toBe(`© ${year} OwlMeans`)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('pairs the product with the notice when the product is not the holder', () => {
|
|
184
|
+
expect(resolveCredit({ product: 'Taskly', organization: 'Acme' }).line)
|
|
185
|
+
.toBe(`Taskly — © ${year} Acme`)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('falls back to the entity slug when the organization has no name', () => {
|
|
189
|
+
expect(resolveCredit({ product: 'Taskly', entity: 'acme-7f2' }).line)
|
|
190
|
+
.toBe(`Taskly — © ${year} acme-7f2`)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('a first year in the past makes the notice a range', () => {
|
|
194
|
+
expect(resolveCredit({ organization: 'Acme', since: 2019 }).line).toBe(`© 2019–${year} Acme`)
|
|
195
|
+
// The current year is not a range, and neither is one a skewed clock puts ahead of it.
|
|
196
|
+
expect(resolveCredit({ organization: 'Acme', since: year }).line).toBe(`© ${year} Acme`)
|
|
197
|
+
expect(resolveCredit({ organization: 'Acme', since: year + 5 }).line).toBe(`© ${year} Acme`)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('an explicit holder is who the notice names', () => {
|
|
201
|
+
expect(resolveCredit({ product: 'Taskly', organization: 'Taskly', holder: 'Acme Inc.' }).line)
|
|
202
|
+
.toBe(`Taskly — © ${year} Acme Inc.`)
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
test('an owner\'s own wording is used verbatim', () => {
|
|
206
|
+
expect(resolveCredit({ organization: 'Acme', copyright: '© Acme. All rights reserved.' }).line)
|
|
207
|
+
.toBe('© Acme. All rights reserved.')
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('switching the notice off leaves the bare pairing it used to be', () => {
|
|
211
|
+
expect(resolveCredit({ product: 'Taskly', organization: 'Acme', copyright: false }).line)
|
|
212
|
+
.toBe('Taskly — Acme')
|
|
213
|
+
expect(resolveCredit({ product: 'Taskly', copyright: false }).line).toBe('Taskly')
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test('a literal line replaces the composition entirely', () => {
|
|
217
|
+
expect(resolveCredit({ product: 'Taskly', organization: 'Acme', line: '© Acme' }).line)
|
|
218
|
+
.toBe('© Acme')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test('nobody to name is nothing to render', () => {
|
|
222
|
+
expect(resolveCredit().line).toBeNull()
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('an EMPTY value is unset, not a value', () => {
|
|
226
|
+
// Exactly what a platform delivers for branding it does not have: every key present, the
|
|
227
|
+
// undelivered ones empty. `??` does not fall through an empty string, so the organization
|
|
228
|
+
// used to win the chain and compose "pisteva — " with nothing after the dash.
|
|
229
|
+
expect(resolveCredit({}, { name: 'pisteva', organization: '', entity: '' }).line)
|
|
230
|
+
.toBe(`© ${year} pisteva`)
|
|
231
|
+
expect(resolveCredit({ line: '', product: '', organization: '' }, { name: 'Taskly' }).line)
|
|
232
|
+
.toBe(`© ${year} Taskly`)
|
|
233
|
+
expect(resolveCredit({ copyright: ' ' }, { name: 'Taskly' }).line).toBe(`© ${year} Taskly`)
|
|
234
|
+
expect(resolveCredit({}, {}, '').line).toBeNull()
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test('the platform credit is on unless it is switched off', () => {
|
|
238
|
+
expect(resolveCredit().poweredBy).toBe(true)
|
|
239
|
+
expect(resolveCredit({ poweredBy: false }).poweredBy).toBe(false)
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
describe('the authentication-plugin method source', () => {
|
|
244
|
+
const plugin = (
|
|
245
|
+
over: Partial<AuthenticationPlugin> & { type: string }
|
|
246
|
+
): AuthenticationPlugin => ({ Implementation: () => () => null, ...over })
|
|
247
|
+
|
|
248
|
+
const offered = (context: BasicContext<BasicConfig>): string[] =>
|
|
249
|
+
pluginMethodSource.list(methodCtx(context)).map(item => item.id)
|
|
250
|
+
|
|
251
|
+
test('a plugin that declares no method is not a way to sign in', () => {
|
|
252
|
+
const context = makeTestContext()
|
|
253
|
+
registerAuthPlugin(plugin({ type: 'step-in-a-flow' }))
|
|
254
|
+
registerAuthPlugin(plugin({ type: 'offerable', method: {} }))
|
|
255
|
+
|
|
256
|
+
expect(offered(context)).toContain('offerable')
|
|
257
|
+
expect(offered(context)).not.toContain('step-in-a-flow')
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
test('a plugin that needs a renderer nobody assigned is left off the screen', () => {
|
|
261
|
+
const context = makeTestContext()
|
|
262
|
+
// Exactly the state an app is in when it imported the plugin host but not a panel package:
|
|
263
|
+
// the plugin is registered, and mounting its screen throws.
|
|
264
|
+
registerAuthPlugin(plugin({ type: 'needs-ui', method: {}, requiresRenderer: true }))
|
|
265
|
+
|
|
266
|
+
expect(offered(context)).not.toContain('needs-ui')
|
|
267
|
+
|
|
268
|
+
registerAuthPlugin(plugin({
|
|
269
|
+
type: 'needs-ui', method: {}, requiresRenderer: true, Renderer: () => null,
|
|
270
|
+
}))
|
|
271
|
+
|
|
272
|
+
expect(offered(context)).toContain('needs-ui')
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
test('a plugin whose own wiring is absent says so and is left off', () => {
|
|
276
|
+
const context = makeTestContext()
|
|
277
|
+
registerAuthPlugin(plugin({
|
|
278
|
+
type: 'needs-wiring', method: { available: ctx => ctx.context.hasService('never-appended') },
|
|
279
|
+
}))
|
|
280
|
+
|
|
281
|
+
expect(offered(context)).not.toContain('needs-wiring')
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
describe('a finished sign-in attempt', () => {
|
|
286
|
+
test('reports every outcome that leaves the user on the screen', () => {
|
|
287
|
+
// The regression: a method that could not build an authorization URL returned `Passed`, the
|
|
288
|
+
// screen rendered nothing, and the button read as broken. `Passed` is a valid answer TO A
|
|
289
|
+
// DISPATCHER, which has a continuation; a screen does not.
|
|
290
|
+
expect(loginAttemptError(LoginOutcome.Passed)).toBe('login.error.failed')
|
|
291
|
+
expect(loginAttemptError(LoginOutcome.Failed)).toBe('login.error.failed')
|
|
292
|
+
expect(loginAttemptError(LoginOutcome.Gesture)).toBe('login.error.blocked')
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test('says nothing when the attempt actually went somewhere', () => {
|
|
296
|
+
expect(loginAttemptError(LoginOutcome.Handled)).toBeNull()
|
|
297
|
+
expect(loginAttemptError(LoginOutcome.Redirected)).toBeNull()
|
|
298
|
+
expect(loginAttemptError(LoginOutcome.Orphaned)).toBeNull()
|
|
299
|
+
expect(loginAttemptError(null)).toBeNull()
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
describe('resume outcomes', () => {
|
|
304
|
+
test('map to exactly one action each', () => {
|
|
305
|
+
expect(resumeAction(LoginOutcome.Handled)).toBe(ResumeAction.Stop)
|
|
306
|
+
expect(resumeAction(LoginOutcome.Redirected)).toBe(ResumeAction.Stop)
|
|
307
|
+
expect(resumeAction(LoginOutcome.Orphaned)).toBe(ResumeAction.Render)
|
|
308
|
+
expect(resumeAction(LoginOutcome.Failed)).toBe(ResumeAction.Render)
|
|
309
|
+
expect(resumeAction(LoginOutcome.Gesture)).toBe(ResumeAction.Render)
|
|
310
|
+
// The ordinary tab: keep the session and carry on. This is the one that must never change.
|
|
311
|
+
expect(resumeAction(LoginOutcome.Passed)).toBe(ResumeAction.Navigate)
|
|
312
|
+
})
|
|
313
|
+
})
|
package/build/.gitkeep
DELETED
|
File without changes
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"modules.d.ts","sourceRoot":"","sources":["../../src/manager/modules.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,OAAO,mDAAO,CAAA"}
|