@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.
Files changed (170) hide show
  1. package/README.md +6 -6
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/client-auth/SKILL.md +134 -21
  4. package/build/components/dispatcher/component.js +1 -1
  5. package/build/components/dispatcher/component.js.map +1 -1
  6. package/build/{modules.d.ts → entrypoints.d.ts} +2 -2
  7. package/build/entrypoints.d.ts.map +1 -0
  8. package/build/{modules.js → entrypoints.js} +3 -3
  9. package/build/entrypoints.js.map +1 -0
  10. package/build/helper.js +1 -1
  11. package/build/helper.js.map +1 -1
  12. package/build/index.d.ts +1 -1
  13. package/build/index.d.ts.map +1 -1
  14. package/build/index.js +1 -1
  15. package/build/index.js.map +1 -1
  16. package/build/login/adopt.d.ts +24 -0
  17. package/build/login/adopt.d.ts.map +1 -0
  18. package/build/login/adopt.js +28 -0
  19. package/build/login/adopt.js.map +1 -0
  20. package/build/login/consts.d.ts +57 -0
  21. package/build/login/consts.d.ts.map +1 -0
  22. package/build/login/consts.js +57 -0
  23. package/build/login/consts.js.map +1 -0
  24. package/build/login/credit.d.ts +24 -0
  25. package/build/login/credit.d.ts.map +1 -0
  26. package/build/login/credit.js +58 -0
  27. package/build/login/credit.js.map +1 -0
  28. package/build/login/env.d.ts +32 -0
  29. package/build/login/env.d.ts.map +1 -0
  30. package/build/login/env.js +84 -0
  31. package/build/login/env.js.map +1 -0
  32. package/build/login/flow.d.ts +20 -0
  33. package/build/login/flow.d.ts.map +1 -0
  34. package/build/login/flow.js +39 -0
  35. package/build/login/flow.js.map +1 -0
  36. package/build/login/hook.d.ts +37 -0
  37. package/build/login/hook.d.ts.map +1 -0
  38. package/build/login/hook.js +72 -0
  39. package/build/login/hook.js.map +1 -0
  40. package/build/login/i18n/be.json +37 -0
  41. package/build/login/i18n/de.json +37 -0
  42. package/build/login/i18n/en.json +37 -0
  43. package/build/login/i18n/es.json +37 -0
  44. package/build/login/i18n/pl.json +37 -0
  45. package/build/login/i18n/ru.json +37 -0
  46. package/build/login/i18n/uk.json +37 -0
  47. package/build/login/i18n.d.ts +2 -0
  48. package/build/login/i18n.d.ts.map +1 -0
  49. package/build/login/i18n.js +25 -0
  50. package/build/login/i18n.js.map +1 -0
  51. package/build/login/index.d.ts +15 -0
  52. package/build/login/index.d.ts.map +1 -0
  53. package/build/login/index.js +15 -0
  54. package/build/login/index.js.map +1 -0
  55. package/build/login/methods.d.ts +25 -0
  56. package/build/login/methods.d.ts.map +1 -0
  57. package/build/login/methods.js +97 -0
  58. package/build/login/methods.js.map +1 -0
  59. package/build/login/resume.d.ts +30 -0
  60. package/build/login/resume.d.ts.map +1 -0
  61. package/build/login/resume.js +55 -0
  62. package/build/login/resume.js.map +1 -0
  63. package/build/login/screen.d.ts +17 -0
  64. package/build/login/screen.d.ts.map +1 -0
  65. package/build/login/screen.js +76 -0
  66. package/build/login/screen.js.map +1 -0
  67. package/build/login/service.d.ts +31 -0
  68. package/build/login/service.d.ts.map +1 -0
  69. package/build/login/service.js +137 -0
  70. package/build/login/service.js.map +1 -0
  71. package/build/login/surrogate.d.ts +20 -0
  72. package/build/login/surrogate.d.ts.map +1 -0
  73. package/build/login/surrogate.js +33 -0
  74. package/build/login/surrogate.js.map +1 -0
  75. package/build/login/terms.d.ts +20 -0
  76. package/build/login/terms.d.ts.map +1 -0
  77. package/build/login/terms.js +0 -0
  78. package/build/login/terms.js.map +1 -0
  79. package/build/login/types.d.ts +236 -0
  80. package/build/login/types.d.ts.map +1 -0
  81. package/build/login/types.js +23 -0
  82. package/build/login/types.js.map +1 -0
  83. package/build/manager/components/authentication/control.js +3 -3
  84. package/build/manager/components/authentication/control.js.map +1 -1
  85. package/build/manager/components/authentication/types.d.ts +2 -2
  86. package/build/manager/components/authentication/types.d.ts.map +1 -1
  87. package/build/manager/components/tunnel-consumer.js +2 -2
  88. package/build/manager/components/tunnel-consumer.js.map +1 -1
  89. package/build/manager/entrypoints.d.ts +2 -0
  90. package/build/manager/entrypoints.d.ts.map +1 -0
  91. package/build/manager/{modules.js → entrypoints.js} +4 -4
  92. package/build/manager/entrypoints.js.map +1 -0
  93. package/build/manager/plugins/basic-ed25519.d.ts.map +1 -1
  94. package/build/manager/plugins/basic-ed25519.js +5 -0
  95. package/build/manager/plugins/basic-ed25519.js.map +1 -1
  96. package/build/manager/plugins/exports.d.ts +2 -0
  97. package/build/manager/plugins/exports.d.ts.map +1 -1
  98. package/build/manager/plugins/exports.js +2 -0
  99. package/build/manager/plugins/exports.js.map +1 -1
  100. package/build/manager/plugins/index.d.ts +1 -5
  101. package/build/manager/plugins/index.d.ts.map +1 -1
  102. package/build/manager/plugins/index.js +9 -1
  103. package/build/manager/plugins/index.js.map +1 -1
  104. package/build/manager/plugins/methods.d.ts +6 -0
  105. package/build/manager/plugins/methods.d.ts.map +1 -0
  106. package/build/manager/plugins/methods.js +65 -0
  107. package/build/manager/plugins/methods.js.map +1 -0
  108. package/build/manager/plugins/re-captcha.d.ts.map +1 -1
  109. package/build/manager/plugins/re-captcha.js +5 -0
  110. package/build/manager/plugins/re-captcha.js.map +1 -1
  111. package/build/manager/plugins/registry.d.ts +20 -0
  112. package/build/manager/plugins/registry.d.ts.map +1 -0
  113. package/build/manager/plugins/registry.js +19 -0
  114. package/build/manager/plugins/registry.js.map +1 -0
  115. package/build/manager/plugins/tunnel-consumer.d.ts.map +1 -1
  116. package/build/manager/plugins/tunnel-consumer.js +4 -0
  117. package/build/manager/plugins/tunnel-consumer.js.map +1 -1
  118. package/build/manager/plugins/types.d.ts +53 -0
  119. package/build/manager/plugins/types.d.ts.map +1 -1
  120. package/build/service.js +1 -1
  121. package/build/service.js.map +1 -1
  122. package/package.json +36 -27
  123. package/src/components/dispatcher/component.tsx +1 -1
  124. package/src/{modules.ts → entrypoints.ts} +2 -2
  125. package/src/helper.ts +1 -1
  126. package/src/index.ts +1 -1
  127. package/src/login/adopt.ts +31 -0
  128. package/src/login/consts.ts +70 -0
  129. package/src/login/credit.ts +75 -0
  130. package/src/login/env.ts +86 -0
  131. package/src/login/flow.ts +44 -0
  132. package/src/login/hook.ts +80 -0
  133. package/src/login/i18n/be.json +37 -0
  134. package/src/login/i18n/de.json +37 -0
  135. package/src/login/i18n/en.json +37 -0
  136. package/src/login/i18n/es.json +37 -0
  137. package/src/login/i18n/pl.json +37 -0
  138. package/src/login/i18n/ru.json +37 -0
  139. package/src/login/i18n/uk.json +37 -0
  140. package/src/login/i18n.ts +26 -0
  141. package/src/login/index.ts +15 -0
  142. package/src/login/methods.ts +113 -0
  143. package/src/login/resume.ts +56 -0
  144. package/src/login/screen.tsx +136 -0
  145. package/src/login/service.ts +181 -0
  146. package/src/login/surrogate.ts +45 -0
  147. package/src/login/terms.ts +0 -0
  148. package/src/login/types.ts +258 -0
  149. package/src/manager/README.md +1 -1
  150. package/src/manager/components/authentication/control.ts +3 -3
  151. package/src/manager/components/authentication/types.ts +2 -2
  152. package/src/manager/components/tunnel-consumer.tsx +2 -2
  153. package/src/manager/{modules.ts → entrypoints.ts} +3 -3
  154. package/src/manager/plugins/basic-ed25519.tsx +7 -0
  155. package/src/manager/plugins/exports.ts +2 -0
  156. package/src/manager/plugins/index.ts +10 -4
  157. package/src/manager/plugins/methods.ts +74 -0
  158. package/src/manager/plugins/re-captcha.tsx +7 -0
  159. package/src/manager/plugins/registry.ts +26 -0
  160. package/src/manager/plugins/tunnel-consumer.tsx +6 -0
  161. package/src/manager/plugins/types.ts +54 -0
  162. package/src/service.ts +1 -1
  163. package/tests/context.ts +1 -2
  164. package/tests/login.spec.ts +313 -0
  165. package/build/.gitkeep +0 -0
  166. package/build/manager/modules.d.ts +0 -2
  167. package/build/manager/modules.d.ts.map +0 -1
  168. package/build/manager/modules.js.map +0 -1
  169. package/build/modules.d.ts.map +0 -1
  170. 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 [allowance] = await module.call({ body: control.request })
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 [token, status] = await context.entrypoint<ClientEntrypoint<AuthToken>>(AUTHEN_AUTHEN)
93
- .call({ body: credentials })
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 { ModuleContextParams } from '@owlmeans/client'
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 ModuleContextParams {
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 { useModule } from '@owlmeans/client'
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 = useModule()
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 { modules as list } from '@owlmeans/auth-common'
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, { force: true })
16
+ elevate(list, DISPATCHER, stab)
17
17
 
18
- export const modules = list
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,4 +1,6 @@
1
1
  export type * from './types.js'
2
+ export * from './registry.js'
3
+ export * from './methods.js'
2
4
  export * from './tunnel/index.js'
3
5
  export * from './basic-ed25519.js'
4
6
  export * from './re-captcha.js'
@@ -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 const plugins: { [type: ClientAuthType]: AuthenticationPlugin } = {}
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 [authToken] = await context.entrypoint<ClientEntrypoint<AuthToken>>(DISPATCHER_AUTHEN)
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, Layer, makeBasicContext } from '@owlmeans/context'
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,2 +0,0 @@
1
- export declare const modules: import("@owlmeans/entrypoint").CommonEntrypoint[];
2
- //# sourceMappingURL=modules.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"modules.d.ts","sourceRoot":"","sources":["../../src/manager/modules.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,OAAO,mDAAO,CAAA"}