@owlmeans/web-client 0.1.18-rc.4 → 0.1.18-rc.41

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 (110) hide show
  1. package/README.md +247 -43
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/web-client/SKILL.md +30 -48
  4. package/build/components/dispatcher/component.d.ts.map +1 -1
  5. package/build/components/dispatcher/component.js +54 -9
  6. package/build/components/dispatcher/component.js.map +1 -1
  7. package/build/components/dispatcher/types.d.ts +2 -2
  8. package/build/components/dispatcher/types.d.ts.map +1 -1
  9. package/build/context.d.ts.map +1 -1
  10. package/build/context.js +2 -1
  11. package/build/context.js.map +1 -1
  12. package/build/entrypoints.d.ts +16 -0
  13. package/build/entrypoints.d.ts.map +1 -0
  14. package/build/entrypoints.js +20 -0
  15. package/build/entrypoints.js.map +1 -0
  16. package/build/exports.d.ts +2 -1
  17. package/build/exports.d.ts.map +1 -1
  18. package/build/exports.js +1 -1
  19. package/build/exports.js.map +1 -1
  20. package/build/i18n/auth-be.json +32 -0
  21. package/build/i18n/auth-de.json +32 -0
  22. package/build/i18n/auth-en.json +32 -0
  23. package/build/i18n/auth-es.json +32 -0
  24. package/build/i18n/auth-pl.json +32 -0
  25. package/build/i18n/auth-ru.json +32 -0
  26. package/build/i18n/auth-uk.json +32 -0
  27. package/build/index.d.ts +2 -1
  28. package/build/index.d.ts.map +1 -1
  29. package/build/index.js +2 -1
  30. package/build/index.js.map +1 -1
  31. package/build/login/consts.d.ts +11 -0
  32. package/build/login/consts.d.ts.map +1 -0
  33. package/build/login/consts.js +11 -0
  34. package/build/login/consts.js.map +1 -0
  35. package/build/login/index.d.ts +8 -0
  36. package/build/login/index.d.ts.map +1 -0
  37. package/build/login/index.js +8 -0
  38. package/build/login/index.js.map +1 -0
  39. package/build/login/pump.d.ts +20 -0
  40. package/build/login/pump.d.ts.map +1 -0
  41. package/build/login/pump.js +46 -0
  42. package/build/login/pump.js.map +1 -0
  43. package/build/login/redirect.d.ts +9 -0
  44. package/build/login/redirect.d.ts.map +1 -0
  45. package/build/login/redirect.js +47 -0
  46. package/build/login/redirect.js.map +1 -0
  47. package/build/login/screen.d.ts +15 -0
  48. package/build/login/screen.d.ts.map +1 -0
  49. package/build/login/screen.js +97 -0
  50. package/build/login/screen.js.map +1 -0
  51. package/build/login/service.d.ts +10 -0
  52. package/build/login/service.d.ts.map +1 -0
  53. package/build/login/service.js +17 -0
  54. package/build/login/service.js.map +1 -0
  55. package/build/login/surrogate.d.ts +37 -0
  56. package/build/login/surrogate.d.ts.map +1 -0
  57. package/build/login/surrogate.js +173 -0
  58. package/build/login/surrogate.js.map +1 -0
  59. package/build/login/view.d.ts +26 -0
  60. package/build/login/view.d.ts.map +1 -0
  61. package/build/login/view.js +73 -0
  62. package/build/login/view.js.map +1 -0
  63. package/build/main.d.ts +9 -1
  64. package/build/main.d.ts.map +1 -1
  65. package/build/main.js +9 -1
  66. package/build/main.js.map +1 -1
  67. package/build/middleware.d.ts.map +1 -1
  68. package/build/middleware.js +7 -6
  69. package/build/middleware.js.map +1 -1
  70. package/build/service.js +1 -1
  71. package/build/service.js.map +1 -1
  72. package/build/types.d.ts +2 -1
  73. package/build/types.d.ts.map +1 -1
  74. package/package.json +17 -17
  75. package/src/components/dispatcher/component.tsx +72 -8
  76. package/src/components/dispatcher/types.ts +2 -2
  77. package/src/context.ts +2 -2
  78. package/src/entrypoints.ts +20 -0
  79. package/src/exports.ts +2 -1
  80. package/src/i18n/auth-be.json +32 -0
  81. package/src/i18n/auth-de.json +32 -0
  82. package/src/i18n/auth-en.json +32 -0
  83. package/src/i18n/auth-es.json +32 -0
  84. package/src/i18n/auth-pl.json +32 -0
  85. package/src/i18n/auth-ru.json +32 -0
  86. package/src/i18n/auth-uk.json +32 -0
  87. package/src/index.ts +2 -1
  88. package/src/login/consts.ts +12 -0
  89. package/src/login/index.ts +7 -0
  90. package/src/login/pump.ts +53 -0
  91. package/src/login/redirect.ts +59 -0
  92. package/src/login/screen.tsx +119 -0
  93. package/src/login/service.ts +19 -0
  94. package/src/login/surrogate.ts +211 -0
  95. package/src/login/view.tsx +94 -0
  96. package/src/main.tsx +11 -2
  97. package/src/middleware.ts +7 -6
  98. package/src/service.ts +1 -1
  99. package/src/types.ts +2 -1
  100. package/tests/surrogate.spec.d.ts +2 -0
  101. package/tests/surrogate.spec.d.ts.map +1 -0
  102. package/tests/surrogate.spec.js +43 -0
  103. package/tests/surrogate.spec.js.map +1 -0
  104. package/tests/surrogate.spec.ts +52 -0
  105. package/tsconfig.json +1 -1
  106. package/build/modules.d.ts +0 -2
  107. package/build/modules.d.ts.map +0 -1
  108. package/build/modules.js +0 -8
  109. package/build/modules.js.map +0 -1
  110. package/src/modules.ts +0 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/web-client",
3
- "version": "0.1.18-rc.4",
3
+ "version": "0.1.18-rc.41",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -21,22 +21,22 @@
21
21
  }
22
22
  },
23
23
  "dependencies": {
24
- "@owlmeans/auth": "^0.1.18-rc.4",
25
- "@owlmeans/auth-common": "^0.1.18-rc.4",
26
- "@owlmeans/client": "^0.1.18-rc.4",
27
- "@owlmeans/client-auth": "^0.1.18-rc.4",
28
- "@owlmeans/client-context": "^0.1.18-rc.4",
29
- "@owlmeans/client-i18n": "^0.1.18-rc.4",
30
- "@owlmeans/client-entrypoint": "^0.1.18-rc.4",
31
- "@owlmeans/client-resource": "^0.1.18-rc.4",
32
- "@owlmeans/client-route": "^0.1.18-rc.4",
33
- "@owlmeans/config": "^0.1.18-rc.4",
34
- "@owlmeans/context": "^0.1.18-rc.4",
35
- "@owlmeans/error": "^0.1.18-rc.4",
36
- "@owlmeans/i18n": "^0.1.18-rc.4",
37
- "@owlmeans/route": "^0.1.18-rc.4",
38
- "@owlmeans/web-db": "^0.1.18-rc.4",
39
- "@owlmeans/web-router": "^0.1.18-rc.4",
24
+ "@owlmeans/auth": "^0.1.18-rc.27",
25
+ "@owlmeans/auth-common": "^0.1.18-rc.29",
26
+ "@owlmeans/client": "^0.1.18-rc.32",
27
+ "@owlmeans/client-auth": "^0.1.18-rc.37",
28
+ "@owlmeans/client-context": "^0.1.18-rc.30",
29
+ "@owlmeans/client-i18n": "^0.1.18-rc.31",
30
+ "@owlmeans/client-entrypoint": "^0.1.18-rc.29",
31
+ "@owlmeans/client-resource": "^0.1.18-rc.29",
32
+ "@owlmeans/client-route": "^0.1.18-rc.30",
33
+ "@owlmeans/config": "^0.1.18-rc.29",
34
+ "@owlmeans/context": "^0.1.18-rc.25",
35
+ "@owlmeans/error": "^0.1.18-rc.27",
36
+ "@owlmeans/i18n": "^0.1.18-rc.28",
37
+ "@owlmeans/route": "^0.1.18-rc.26",
38
+ "@owlmeans/web-db": "^0.1.18-rc.30",
39
+ "@owlmeans/web-router": "^0.1.18-rc.30",
40
40
  "react-dropzone": "^14.3.5"
41
41
  },
42
42
  "peerDependencies": {
@@ -1,16 +1,33 @@
1
1
  import { DispatcherHOC } from '@owlmeans/client-auth'
2
- import { useEffect } from 'react'
2
+ import { useEffect, useState } from 'react'
3
3
  import { useI18nLib } from '@owlmeans/client-i18n'
4
4
  import { AUTH_QUERY } from '@owlmeans/auth'
5
+ import {
6
+ FallbackLoginScreen, LoginIntent, LoginOutcome, ResumeAction, resumeAction,
7
+ } from '@owlmeans/client-auth/login'
8
+ import { LoginSurrogateView, SurrogateStage } from '../../login/view.js'
5
9
  import { useContext } from '../../context.js'
6
10
 
7
- export const Dispatcher = DispatcherHOC(({ provideToken }) => {
11
+ export const Dispatcher = DispatcherHOC(({ provideToken, navigate }) => {
8
12
  const context = useContext()
9
13
  const [query] = context.router().useSearchParams()
10
14
 
11
15
  const t = useI18nLib('auth', 'dispatcher')
16
+ const tAuth = useI18nLib('auth')
17
+
18
+ // Read in the component body, not in the effect: `window.name` is set by `window.open` before
19
+ // this document loads and `sessionStorage` carries the marker across the provider round trip, so
20
+ // the answer is already correct at first paint — and a first paint that showed the application
21
+ // before an effect corrected it is the defect this exists to prevent.
22
+ const [env] = useState(() => context.login().env())
23
+ const [outcome, setOutcome] = useState<LoginOutcome | null>(null)
24
+ // Nothing to return from and nobody signed in: offer the choice rather than starting a flow.
25
+ const [choose, setChoose] = useState(false)
12
26
 
13
27
  useEffect(() => {
28
+ // First statement of the effect on purpose: everything below can navigate this window away.
29
+ context.login().enter()
30
+
14
31
  const token = query.get(AUTH_QUERY)
15
32
  if (token != null) {
16
33
  const params: Record<string, string> = {}
@@ -21,15 +38,62 @@ export const Dispatcher = DispatcherHOC(({ provideToken }) => {
21
38
  })
22
39
 
23
40
  provideToken({ token }, params)
24
- } else {
25
- context.auth().authenticated().then(authzToken => {
26
- if (authzToken == null) {
27
- provideToken({ token: '' }, undefined)
28
- }
29
- })
41
+
42
+ return
30
43
  }
44
+
45
+ void context.auth().authenticated().then(async authzToken => {
46
+ if (authzToken == null || authzToken === '') {
47
+ setChoose(true)
48
+
49
+ return
50
+ }
51
+ // A session already exists in THIS document. Whether it is useful here, or belongs to the
52
+ // window that opened this one, is the plugin's call — the dispatcher reads no environment.
53
+ const settled = await context.login().resume(authzToken)
54
+ switch (resumeAction(settled)) {
55
+ case ResumeAction.Stop:
56
+ return
57
+ case ResumeAction.Render:
58
+ setOutcome(settled)
59
+
60
+ return
61
+ default:
62
+ await navigate()
63
+ }
64
+ })
31
65
  }, [])
32
66
 
67
+ // A surrogate window never renders the application, whatever else is true. Checked ahead of
68
+ // every other return for exactly that reason.
69
+ //
70
+ // `choose` is the one state this must NOT mask, and it is not an exception to the rule: the
71
+ // login chooser is not the application. A login window with nothing to return from and nobody
72
+ // signed in has exactly one thing left that can move it forward, and that is asking which
73
+ // provider. The surrogate URL normally carries `?method=` so the question is already settled,
74
+ // but a window opened from a bare "Log in" — the framed application's own header button — has no
75
+ // method to send. Returning the "working" panel there left the popup on "Signing you in…"
76
+ // permanently, because nothing was ever going to start.
77
+ if (env.surrogate && !choose) {
78
+ return <LoginSurrogateView
79
+ intent={LoginIntent.Login}
80
+ stage={
81
+ outcome === LoginOutcome.Orphaned ? SurrogateStage.Orphaned
82
+ : outcome === LoginOutcome.Failed ? SurrogateStage.Failed
83
+ : outcome === LoginOutcome.Gesture ? SurrogateStage.Gesture
84
+ : SurrogateStage.Working
85
+ }
86
+ onAction={() => window.close()}
87
+ translate={(key, defaultValue) => tAuth(key, { defaultValue })}
88
+ />
89
+ }
90
+
91
+ if (choose) {
92
+ const Screen = context.login().screen() ?? FallbackLoginScreen
93
+
94
+ return <Screen translate={(key, defaultValue) => tAuth(key, { defaultValue })} />
95
+ }
96
+
33
97
  return query.has(AUTH_QUERY) ? <div style={{
34
98
  width: '100%', // Makes the div occupy the full width
35
99
  display: 'flex', // Enables flexbox layout
@@ -1,6 +1,6 @@
1
- import type { ModuleContextParams } from "@owlmeans/client"
1
+ import type { EntrypointContextParams } from "@owlmeans/client"
2
2
  import type { DispatcherProps } from "@owlmeans/client-auth"
3
3
  import type { PropsWithChildren } from "react"
4
4
 
5
- export interface ParametrisedProps extends PropsWithChildren<ModuleContextParams & DispatcherProps> {
5
+ export interface ParametrisedProps extends PropsWithChildren<EntrypointContextParams & DispatcherProps> {
6
6
  }
package/src/context.ts CHANGED
@@ -8,6 +8,7 @@ import { useContext as useCtx } from '@owlmeans/client'
8
8
  import { extractPrimaryHost } from './helper.js'
9
9
  import { appendWebAuthService } from './service.js'
10
10
  import { appendWebRouter } from '@owlmeans/web-router'
11
+ import { appendWebLogin } from './login/service.js'
11
12
 
12
13
  export const makeContext = <C extends AppConfig = AppConfig, T extends AppContext<C> = AppContext<C>>(
13
14
  cfg: C
@@ -19,8 +20,7 @@ export const makeContext = <C extends AppConfig = AppConfig, T extends AppContex
19
20
  appendWebDbService<C, T>(context)
20
21
  appendClientResource<C, T>(context, AUTH_RESOURCE)
21
22
  appendWebRouter<C, T>(context)
22
-
23
- context.makeContext = makeContext as typeof context.makeContext
23
+ appendWebLogin<C, T>(context)
24
24
 
25
25
  return context
26
26
  }
@@ -0,0 +1,20 @@
1
+ import { authProtocols } from '@owlmeans/auth-common'
2
+ import { entrypoints as authBindings } from '@owlmeans/client-auth'
3
+ import { bindScreen } from '@owlmeans/client-entrypoint'
4
+ import { handler } from '@owlmeans/client'
5
+ import { Dispatcher } from './components/dispatcher/component.js'
6
+ import { SurrogateScreen } from './login/screen.js'
7
+
8
+ /**
9
+ * Bound here rather than in a relying-party package on purpose.
10
+ *
11
+ * The surrogate mechanic is not OIDC-specific — it is "run the login route one window up and hand
12
+ * the result back" — and every web application already spreads this list. Putting the screen here
13
+ * means `web-panel`, `mui-panel` and every generated target gain a working login window with no
14
+ * edit of their own, which is the only way an application that is already deployed can get it.
15
+ */
16
+ export const entrypoints = [
17
+ ...authBindings,
18
+ bindScreen(authProtocols.dispatcher, handler(Dispatcher)),
19
+ bindScreen(authProtocols.surrogate, handler(SurrogateScreen)),
20
+ ]
package/src/exports.ts CHANGED
@@ -3,7 +3,8 @@ export { handler } from '@owlmeans/client'
3
3
  export { config } from '@owlmeans/client-context'
4
4
  export { service } from '@owlmeans/config'
5
5
  export { AppType, HOME, ROOT, BASE, GUEST } from '@owlmeans/context'
6
- export { entrypoint, elevate, provideRequest, stab } from '@owlmeans/client-entrypoint'
6
+ export { bind, bindAll, bindScreen, provideRequest, stab } from '@owlmeans/client-entrypoint'
7
7
  export { route, frontend } from '@owlmeans/route'
8
+ export type { ClientEntrypoint } from '@owlmeans/client-entrypoint'
8
9
  export type { ClientEntrypoint as Module } from '@owlmeans/client-entrypoint'
9
10
  export type { ClientRoute as Route } from '@owlmeans/client-route'
@@ -2,5 +2,37 @@
2
2
  "dispatcher": {
3
3
  "loading": "Загрузка...",
4
4
  "error": "Не ўдалося прайсці аўтэнтыфікацыю: {{error}}"
5
+ },
6
+ "surrogate": {
7
+ "login": {
8
+ "working": "Выконваем уваход…",
9
+ "gesture": {
10
+ "title": "Працягніце, каб увайсці.",
11
+ "action": "Увайсці"
12
+ },
13
+ "handing": "Гатова. Вяртаемся ў праграму…",
14
+ "orphaned": "Уваход выкананы. Можаце закрыць гэтае акно і вярнуцца ў праграму.",
15
+ "failed": "Не ўдалося завяршыць. Закрыйце гэтае акно і паспрабуйце зноў.",
16
+ "standalone": {
17
+ "title": "Гэтая старонка выкарыстоўваецца для ўваходу з іншага акна.",
18
+ "action": "Адкрыць праграму"
19
+ },
20
+ "close": "Закрыць"
21
+ },
22
+ "logout": {
23
+ "working": "Выконваем выхад…",
24
+ "gesture": {
25
+ "title": "Працягніце, каб выйсці.",
26
+ "action": "Выйсці"
27
+ },
28
+ "handing": "Выхад выкананы. Вяртаемся ў праграму…",
29
+ "orphaned": "Выхад выкананы. Можаце закрыць гэтае акно.",
30
+ "failed": "Выхад тут не завершаны. Можаце закрыць гэтае акно.",
31
+ "standalone": {
32
+ "title": "Гэтая старонка выкарыстоўваецца для выхаду з іншага акна.",
33
+ "action": "Адкрыць праграму"
34
+ },
35
+ "close": "Закрыць"
36
+ }
5
37
  }
6
38
  }
@@ -2,5 +2,37 @@
2
2
  "dispatcher": {
3
3
  "loading": "Wird geladen...",
4
4
  "error": "Authentifizierung fehlgeschlagen: {{error}}"
5
+ },
6
+ "surrogate": {
7
+ "login": {
8
+ "working": "Anmeldung läuft…",
9
+ "gesture": {
10
+ "title": "Fortfahren, um sich anzumelden.",
11
+ "action": "Anmelden"
12
+ },
13
+ "handing": "Fertig. Zurück zur Anwendung…",
14
+ "orphaned": "Angemeldet. Sie können dieses Fenster schließen und in der Anwendung fortfahren.",
15
+ "failed": "Das wurde nicht abgeschlossen. Schließen Sie dieses Fenster und versuchen Sie es erneut.",
16
+ "standalone": {
17
+ "title": "Diese Seite dient der Anmeldung aus einem anderen Fenster.",
18
+ "action": "Anwendung öffnen"
19
+ },
20
+ "close": "Schließen"
21
+ },
22
+ "logout": {
23
+ "working": "Abmeldung läuft…",
24
+ "gesture": {
25
+ "title": "Fortfahren, um sich abzumelden.",
26
+ "action": "Abmelden"
27
+ },
28
+ "handing": "Abgemeldet. Zurück zur Anwendung…",
29
+ "orphaned": "Abgemeldet. Sie können dieses Fenster schließen.",
30
+ "failed": "Die Abmeldung wurde hier nicht abgeschlossen. Sie können dieses Fenster schließen.",
31
+ "standalone": {
32
+ "title": "Diese Seite dient der Abmeldung aus einem anderen Fenster.",
33
+ "action": "Anwendung öffnen"
34
+ },
35
+ "close": "Schließen"
36
+ }
5
37
  }
6
38
  }
@@ -2,5 +2,37 @@
2
2
  "dispatcher": {
3
3
  "loading": "Loading...",
4
4
  "error": "Authentication failed: {{error}}"
5
+ },
6
+ "surrogate": {
7
+ "login": {
8
+ "working": "Signing you in…",
9
+ "gesture": {
10
+ "title": "Continue to sign in.",
11
+ "action": "Sign in"
12
+ },
13
+ "handing": "Done. Returning to the application…",
14
+ "orphaned": "Signed in. You can close this window and continue in the application.",
15
+ "failed": "That did not complete. You can close this window and try again.",
16
+ "standalone": {
17
+ "title": "This page is used to sign in from another window.",
18
+ "action": "Open the application"
19
+ },
20
+ "close": "Close"
21
+ },
22
+ "logout": {
23
+ "working": "Signing you out…",
24
+ "gesture": {
25
+ "title": "Continue to sign out.",
26
+ "action": "Sign out"
27
+ },
28
+ "handing": "Signed out. Returning to the application…",
29
+ "orphaned": "Signed out. You can close this window.",
30
+ "failed": "Sign-out did not complete here. You can close this window.",
31
+ "standalone": {
32
+ "title": "This page is used to sign out from another window.",
33
+ "action": "Open the application"
34
+ },
35
+ "close": "Close"
36
+ }
5
37
  }
6
38
  }
@@ -2,5 +2,37 @@
2
2
  "dispatcher": {
3
3
  "loading": "Cargando...",
4
4
  "error": "Error de autenticación: {{error}}"
5
+ },
6
+ "surrogate": {
7
+ "login": {
8
+ "working": "Iniciando sesión…",
9
+ "gesture": {
10
+ "title": "Continúa para iniciar sesión.",
11
+ "action": "Iniciar sesión"
12
+ },
13
+ "handing": "Listo. Volviendo a la aplicación…",
14
+ "orphaned": "Sesión iniciada. Puedes cerrar esta ventana y continuar en la aplicación.",
15
+ "failed": "No se completó. Cierra esta ventana e inténtalo de nuevo.",
16
+ "standalone": {
17
+ "title": "Esta página se usa para iniciar sesión desde otra ventana.",
18
+ "action": "Abrir la aplicación"
19
+ },
20
+ "close": "Cerrar"
21
+ },
22
+ "logout": {
23
+ "working": "Cerrando sesión…",
24
+ "gesture": {
25
+ "title": "Continúa para cerrar sesión.",
26
+ "action": "Cerrar sesión"
27
+ },
28
+ "handing": "Sesión cerrada. Volviendo a la aplicación…",
29
+ "orphaned": "Sesión cerrada. Puedes cerrar esta ventana.",
30
+ "failed": "El cierre de sesión no se completó aquí. Puedes cerrar esta ventana.",
31
+ "standalone": {
32
+ "title": "Esta página se usa para cerrar sesión desde otra ventana.",
33
+ "action": "Abrir la aplicación"
34
+ },
35
+ "close": "Cerrar"
36
+ }
5
37
  }
6
38
  }
@@ -2,5 +2,37 @@
2
2
  "dispatcher": {
3
3
  "loading": "Ładowanie...",
4
4
  "error": "Uwierzytelnianie nie powiodło się: {{error}}"
5
+ },
6
+ "surrogate": {
7
+ "login": {
8
+ "working": "Trwa logowanie…",
9
+ "gesture": {
10
+ "title": "Kontynuuj, aby się zalogować.",
11
+ "action": "Zaloguj się"
12
+ },
13
+ "handing": "Gotowe. Wracamy do aplikacji…",
14
+ "orphaned": "Zalogowano. Możesz zamknąć to okno i kontynuować w aplikacji.",
15
+ "failed": "Nie udało się ukończyć. Zamknij to okno i spróbuj ponownie.",
16
+ "standalone": {
17
+ "title": "Ta strona służy do logowania z innego okna.",
18
+ "action": "Otwórz aplikację"
19
+ },
20
+ "close": "Zamknij"
21
+ },
22
+ "logout": {
23
+ "working": "Trwa wylogowywanie…",
24
+ "gesture": {
25
+ "title": "Kontynuuj, aby się wylogować.",
26
+ "action": "Wyloguj się"
27
+ },
28
+ "handing": "Wylogowano. Wracamy do aplikacji…",
29
+ "orphaned": "Wylogowano. Możesz zamknąć to okno.",
30
+ "failed": "Wylogowanie nie zostało tu ukończone. Możesz zamknąć to okno.",
31
+ "standalone": {
32
+ "title": "Ta strona służy do wylogowania z innego okna.",
33
+ "action": "Otwórz aplikację"
34
+ },
35
+ "close": "Zamknij"
36
+ }
5
37
  }
6
38
  }
@@ -2,5 +2,37 @@
2
2
  "dispatcher": {
3
3
  "loading": "Загрузка...",
4
4
  "error": "Не удалось выполнить аутентификацию: {{error}}"
5
+ },
6
+ "surrogate": {
7
+ "login": {
8
+ "working": "Выполняем вход…",
9
+ "gesture": {
10
+ "title": "Продолжите, чтобы войти.",
11
+ "action": "Войти"
12
+ },
13
+ "handing": "Готово. Возвращаемся в приложение…",
14
+ "orphaned": "Вход выполнен. Можете закрыть это окно и вернуться в приложение.",
15
+ "failed": "Не удалось завершить. Закройте это окно и попробуйте снова.",
16
+ "standalone": {
17
+ "title": "Эта страница используется для входа из другого окна.",
18
+ "action": "Открыть приложение"
19
+ },
20
+ "close": "Закрыть"
21
+ },
22
+ "logout": {
23
+ "working": "Выполняем выход…",
24
+ "gesture": {
25
+ "title": "Продолжите, чтобы выйти.",
26
+ "action": "Выйти"
27
+ },
28
+ "handing": "Выход выполнен. Возвращаемся в приложение…",
29
+ "orphaned": "Выход выполнен. Можете закрыть это окно.",
30
+ "failed": "Выход здесь не завершён. Можете закрыть это окно.",
31
+ "standalone": {
32
+ "title": "Эта страница используется для выхода из другого окна.",
33
+ "action": "Открыть приложение"
34
+ },
35
+ "close": "Закрыть"
36
+ }
5
37
  }
6
38
  }
@@ -2,5 +2,37 @@
2
2
  "dispatcher": {
3
3
  "loading": "Завантаження...",
4
4
  "error": "Не вдалося пройти автентифікацію: {{error}}"
5
+ },
6
+ "surrogate": {
7
+ "login": {
8
+ "working": "Виконуємо вхід…",
9
+ "gesture": {
10
+ "title": "Продовжте, щоб увійти.",
11
+ "action": "Увійти"
12
+ },
13
+ "handing": "Готово. Повертаємось до застосунку…",
14
+ "orphaned": "Вхід виконано. Можете закрити це вікно й повернутися до застосунку.",
15
+ "failed": "Не вдалося завершити. Закрийте це вікно та спробуйте ще раз.",
16
+ "standalone": {
17
+ "title": "Ця сторінка використовується для входу з іншого вікна.",
18
+ "action": "Відкрити застосунок"
19
+ },
20
+ "close": "Закрити"
21
+ },
22
+ "logout": {
23
+ "working": "Виконуємо вихід…",
24
+ "gesture": {
25
+ "title": "Продовжте, щоб вийти.",
26
+ "action": "Вийти"
27
+ },
28
+ "handing": "Вихід виконано. Повертаємось до застосунку…",
29
+ "orphaned": "Вихід виконано. Можете закрити це вікно.",
30
+ "failed": "Вихід тут не завершено. Можете закрити це вікно.",
31
+ "standalone": {
32
+ "title": "Ця сторінка використовується для виходу з іншого вікна.",
33
+ "action": "Відкрити застосунок"
34
+ },
35
+ "close": "Закрити"
36
+ }
5
37
  }
6
38
  }
package/src/index.ts CHANGED
@@ -4,10 +4,11 @@ export * from './types.js'
4
4
  export * from './main.js'
5
5
  export * from './router.js'
6
6
  export * from './components/index.js'
7
- export * from './modules.js'
7
+ export * from './entrypoints.js'
8
8
  export * from './exports.js'
9
9
  export * from './context.js'
10
10
  export * from './helper.js'
11
11
  export * from './i18n.js'
12
12
  export * from './service.js'
13
13
  export * from './errors.js'
14
+ export * from './login/index.js'
@@ -0,0 +1,12 @@
1
+ /** The ordinary flow: leave for the provider and come back. */
2
+ export const REDIRECT_LOGIN = 'owlmeans-redirect-login'
3
+
4
+ /** The flow that runs one window up, for a document that cannot redirect where it is. */
5
+ export const SURROGATE_LOGIN = 'owlmeans-surrogate-login'
6
+
7
+ /**
8
+ * Registered above the default so it wins the cascade wherever it applies. It applies narrowly —
9
+ * only in a frame, or in the surrogate window itself — so the default still serves every
10
+ * ordinary tab.
11
+ */
12
+ export const SURROGATE_LOGIN_PRIORITY = 100
@@ -0,0 +1,7 @@
1
+ export * from './consts.js'
2
+ export * from './pump.js'
3
+ export * from './redirect.js'
4
+ export * from './surrogate.js'
5
+ export * from './view.js'
6
+ export * from './screen.js'
7
+ export * from './service.js'
@@ -0,0 +1,53 @@
1
+ import { LOGIN_WATCH_INTERVAL, LoginOutcome } from '@owlmeans/client-auth/login'
2
+
3
+ /**
4
+ * Wait on a surrogate window until it says something, or goes away.
5
+ *
6
+ * Extracted so login and logout share one implementation rather than two that agree until one of
7
+ * them is changed. Every guard here was a real failure at some point:
8
+ *
9
+ * - the **origin pin** is not decoration — the message carries a bearer token, and a listener that
10
+ * accepted any origin would take one from any page that could reach this window;
11
+ * - the **`settled` latch** stops a second message, or a close racing a message, resolving twice;
12
+ * - the **`closed` poll** is the only exit when the user simply closes the window, because a window
13
+ * the user closes announces nothing at all;
14
+ * - the listener and the interval are removed on every path, including the ones that resolve.
15
+ */
16
+ export const awaitSurrogate = async (
17
+ surrogate: Window,
18
+ messageType: string,
19
+ onMessage: (data: { type?: string, token?: string, ok?: boolean }) => Promise<LoginOutcome>
20
+ ): Promise<LoginOutcome> => await new Promise<LoginOutcome>(resolve => {
21
+ let settled = false
22
+ let watch: ReturnType<typeof setInterval> | undefined
23
+
24
+ const finish = (result: LoginOutcome): void => {
25
+ if (settled) {
26
+ return
27
+ }
28
+ settled = true
29
+ window.removeEventListener('message', handler)
30
+ if (watch != null) {
31
+ clearInterval(watch)
32
+ }
33
+ resolve(result)
34
+ }
35
+
36
+ function handler(event: MessageEvent): void {
37
+ if (event.origin !== window.location.origin) {
38
+ return
39
+ }
40
+ const data = event.data as { type?: string, token?: string, ok?: boolean } | null
41
+ if (data?.type !== messageType) {
42
+ return
43
+ }
44
+ void onMessage(data).then(finish).catch(() => finish(LoginOutcome.Failed))
45
+ }
46
+
47
+ window.addEventListener('message', handler)
48
+ watch = setInterval(() => {
49
+ if (surrogate.closed) {
50
+ finish(LoginOutcome.Failed)
51
+ }
52
+ }, LOGIN_WATCH_INTERVAL)
53
+ })
@@ -0,0 +1,59 @@
1
+ import { revokeToken, LoginOutcome } from '@owlmeans/client-auth/login'
2
+ import type { LoginContext, LoginPlugin } from '@owlmeans/client-auth/login'
3
+ import { REDIRECT_LOGIN } from './consts.js'
4
+
5
+ /**
6
+ * The ordinary login flow: this document leaves for the identity provider and comes back.
7
+ *
8
+ * The default plugin, matching everything, so any environment that does not need special handling
9
+ * behaves exactly as it always did.
10
+ */
11
+ export const makeRedirectLoginPlugin = (): LoginPlugin => ({
12
+ alias: REDIRECT_LOGIN,
13
+ priority: 0,
14
+ mode: 'redirect',
15
+ match: () => true,
16
+
17
+ begin: async (_ctx, request) => {
18
+ // Prefer the in-app continuation the caller handed over: a client-side navigation to the
19
+ // dispatcher keeps the app mounted, where a full page load would tear it down and rebuild it.
20
+ if (request.navigate != null) {
21
+ await request.navigate()
22
+
23
+ return LoginOutcome.Handled
24
+ }
25
+ window.location.href = request.url
26
+
27
+ return LoginOutcome.Redirected
28
+ },
29
+
30
+ authorize: async (_ctx, url) => {
31
+ window.location.href = url
32
+
33
+ return LoginOutcome.Redirected
34
+ },
35
+
36
+ // Nothing to hand anywhere — the token was issued in the document that will use it.
37
+ complete: async () => LoginOutcome.Passed,
38
+
39
+ // `resume` is deliberately NOT implemented. The host defaults to `Passed`, which is "keep the
40
+ // session you already have and carry on" — byte for byte what an ordinary tab has always done.
41
+ // Implementing it here would be the one way to regress every unframed application.
42
+
43
+ logout: (ctx, request) => revokeToken(ctx as LoginContext).then(async () => {
44
+ if (request.navigate != null) {
45
+ await request.navigate()
46
+
47
+ return LoginOutcome.Handled
48
+ }
49
+ // A reload rather than a client-side navigation: `useOwlAuth` and its kin read through
50
+ // cached values, so the honest way to make a whole application forget a session is to
51
+ // rebuild it. This is what `useLogout` did inline before the mechanic became a plugin.
52
+ window.location.reload()
53
+
54
+ return LoginOutcome.Handled
55
+ }),
56
+
57
+ // Nothing to tell anyone — the session was cleared in the document that owned it.
58
+ logoutComplete: async () => LoginOutcome.Passed,
59
+ })