@owlmeans/web-client 0.1.18-rc.1 → 0.1.18-rc.10

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 (43) hide show
  1. package/agent-meta/manifest.json +2 -2
  2. package/agent-meta/skills/web-client/SKILL.md +45 -10
  3. package/build/context.d.ts.map +1 -1
  4. package/build/context.js +2 -0
  5. package/build/context.js.map +1 -1
  6. package/build/exports.d.ts +1 -0
  7. package/build/exports.d.ts.map +1 -1
  8. package/build/index.d.ts +1 -0
  9. package/build/index.d.ts.map +1 -1
  10. package/build/index.js +1 -0
  11. package/build/index.js.map +1 -1
  12. package/build/login/consts.d.ts +11 -0
  13. package/build/login/consts.d.ts.map +1 -0
  14. package/build/login/consts.js +11 -0
  15. package/build/login/consts.js.map +1 -0
  16. package/build/login/index.d.ts +5 -0
  17. package/build/login/index.d.ts.map +1 -0
  18. package/build/login/index.js +5 -0
  19. package/build/login/index.js.map +1 -0
  20. package/build/login/redirect.d.ts +9 -0
  21. package/build/login/redirect.d.ts.map +1 -0
  22. package/build/login/redirect.js +31 -0
  23. package/build/login/redirect.js.map +1 -0
  24. package/build/login/service.d.ts +10 -0
  25. package/build/login/service.d.ts.map +1 -0
  26. package/build/login/service.js +17 -0
  27. package/build/login/service.js.map +1 -0
  28. package/build/login/surrogate.d.ts +21 -0
  29. package/build/login/surrogate.d.ts.map +1 -0
  30. package/build/login/surrogate.js +114 -0
  31. package/build/login/surrogate.js.map +1 -0
  32. package/build/types.d.ts +2 -1
  33. package/build/types.d.ts.map +1 -1
  34. package/package.json +17 -17
  35. package/src/context.ts +2 -0
  36. package/src/exports.ts +1 -0
  37. package/src/index.ts +1 -0
  38. package/src/login/consts.ts +12 -0
  39. package/src/login/index.ts +4 -0
  40. package/src/login/redirect.ts +38 -0
  41. package/src/login/service.ts +19 -0
  42. package/src/login/surrogate.ts +132 -0
  43. package/src/types.ts +2 -1
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/web-client",
4
- "version": "0.1.18-rc.0",
5
- "generatedAt": "2026-08-16T22:20:50.508Z",
4
+ "version": "0.1.18-rc.9",
5
+ "generatedAt": "2026-08-24T21:14:00.125Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,19 +8,21 @@ user-invocable: false
8
8
  # @owlmeans/web-client
9
9
 
10
10
  **Layer:** Web (React)
11
- **Install:** `"@owlmeans/web-client": "^0.1.18-rc.0"` in `dependencies`
11
+ **Install:** `"@owlmeans/web-client": "^0.1.18-rc.9"` in `dependencies`
12
12
 
13
13
  ## Key Exports
14
14
 
15
15
  | Export | Description |
16
16
  |--------|-------------|
17
17
  | `renderApp<C, T>(context)` | Mount the React app (routing via the active router plugin — OwlMeans by default) |
18
- | `elevate(modules, alias, handler)` | Attach a React component (or `handler(Component)`) to an entrypoint |
18
+ | `elevate(entrypoints, alias, handler)` | Attach a React component (or `handler(Component)`) to an entrypoint |
19
19
  | `handler(Component)` | Wrap a React component as an entrypoint handler |
20
- | `context.registerEntrypoints(modules)` | Register the full entrypoint list on the context |
20
+ | `context.registerEntrypoints(entrypoints)` | Register the full entrypoint list on the context |
21
21
  | `context.serviceRoute(alias, isDefault?)` | Mark a service's routing root |
22
+ | `makeContext(cfg)` | Build the browser context — auth, web db, client resource, router and login are all appended here |
23
+ | `appendWebLogin(context)` | Register the login host plus the redirect and surrogate-window plugins |
22
24
  | `router`, `components`, `service`, `i18n`, `helpers`, `errors` | Web-specific helpers |
23
- | Constants | Default aliases (BASE, HOME) |
25
+ | Constants | Default aliases (BASE, HOME), `REDIRECT_LOGIN`, `SURROGATE_LOGIN` |
24
26
 
25
27
  ## Usage
26
28
 
@@ -29,31 +31,64 @@ user-invocable: false
29
31
  // index.tsx
30
32
  import { renderApp } from '@owlmeans/web-client'
31
33
  import { makeContext } from './context.js'
32
- import { appModules } from './modules.js'
34
+ import { appEntrypoints } from './entrypoints.js'
33
35
  import { MANAGER, MANAGER_API } from 'my-common'
34
36
  import config from './config.js'
35
37
 
36
38
  const context = makeContext(config)
37
- context.registerEntrypoints(appModules)
39
+ context.registerEntrypoints(appEntrypoints)
38
40
  context.serviceRoute(MANAGER, true)
39
41
  context.serviceRoute(MANAGER_API, true)
40
42
  renderApp<Config, Context>(context)
41
43
  ```
42
44
 
43
- ### Module elevation with React components
45
+ ### Entrypoint elevation with React components
44
46
  ```typescript
45
- import { elevate, route, frontend, handler, entrypoint, guard } from '@owlmeans/web-client'
47
+ import { elevate, route, frontend, handler, entrypoint } from '@owlmeans/web-client'
46
48
  import { HomeScreen, ProjectDashboardScreen } from './screens/index.js'
47
49
 
48
- elevate(modules, manager.front.project.dashboard, handler(ProjectDashboardScreen))
50
+ elevate(entrypoints, manager.front.project.dashboard, handler(ProjectDashboardScreen))
49
51
 
50
- modules.push(
52
+ entrypoints.push(
51
53
  entrypoint(route(HOME, '/', frontend({ default: true, parent: BASE })), handler(HomeScreen))
52
54
  )
53
55
  ```
54
56
 
57
+ A **backend** alias the client calls needs a bare `elevate(entrypoints, alias)` — no handler. That
58
+ makes `context.entrypoint<ClientEntrypoint<T>>(alias).call({ params, body, query })` work; the auth
59
+ header is attached automatically for guarded entrypoints.
60
+
61
+ ### Login comes with the context
62
+
63
+ `makeContext` calls `appendWebLogin`, which registers the login host (`@owlmeans/client-auth/login`)
64
+ and both browser plugins on it — redirect (`REDIRECT_LOGIN`, priority 0, applies always) and
65
+ surrogate window (`SURROGATE_LOGIN`, priority 100, applies in a frame or in the surrogate itself).
66
+ So every web app — and everything built on `web-panel` or `mui-panel` — has working sign-in,
67
+ including from an embedded app, with nothing registered by hand:
68
+
69
+ ```typescript
70
+ import { useLogin, useLogout } from '@owlmeans/client-auth/login'
71
+
72
+ const [, onLogIn] = useLogin() // one control, one handler, no environment checks
73
+ ```
74
+
75
+ An app that needs a different mechanic registers its own plugin at a higher priority after building
76
+ the base context (`context.login().registerPlugin(...)`); it never replaces these. The contract, the
77
+ cascade and the invariants are the `login-plugins` skill.
78
+
79
+ ### Navigation
80
+ ```typescript
81
+ import { useNavigate } from '@owlmeans/client' // not re-exported by web-client
82
+ import { useStoreList, useStoreModel } from '@owlmeans/client' // client state — also not re-exported
83
+
84
+ const nav = useNavigate()
85
+ <Button onClick={nav.press(manager.front.project.dashboard, { params: { projectId } })}>Open</Button>
86
+ await nav.go(alias, { params }) // programmatic; nav.back() to go back
87
+ ```
88
+
55
89
  ## Depends On
56
90
 
57
91
  - `@owlmeans/client`, `@owlmeans/web-router` (default OwlMeans browser routing), `@owlmeans/web-panel` (typically), `@owlmeans/client-i18n`
92
+ - `@owlmeans/client-auth` — `AUTH_RESOURCE` and the `./login` host the browser plugins register on
58
93
  - `@owlmeans/entrypoint`, `@owlmeans/route`
59
94
  - `react`, `react-dom` (peer). No longer depends on `react-router` — opt into it with `@owlmeans/web-router-react-router` (`appendReactRouter`). The former `provide` export is a deprecated `undefined`.
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AASlD,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,SAAS,GAAG,SAAS,EAAE,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,OAC7F,CAAC,KACL,CAYF,CAAA;AAED,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,SAAS,GAAG,SAAS,EAAC,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,QACnF,CAAA"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAUlD,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,SAAS,GAAG,SAAS,EAAE,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,OAC7F,CAAC,KACL,CAaF,CAAA;AAED,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,SAAS,GAAG,SAAS,EAAC,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,QACnF,CAAA"}
package/build/context.js CHANGED
@@ -6,6 +6,7 @@ import { useContext as useCtx } from '@owlmeans/client';
6
6
  import { extractPrimaryHost } from './helper.js';
7
7
  import { appendWebAuthService } from './service.js';
8
8
  import { appendWebRouter } from '@owlmeans/web-router';
9
+ import { appendWebLogin } from './login/service.js';
9
10
  export const makeContext = (cfg) => {
10
11
  const context = makeClientContext(cfg);
11
12
  extractPrimaryHost(context);
@@ -13,6 +14,7 @@ export const makeContext = (cfg) => {
13
14
  appendWebDbService(context);
14
15
  appendClientResource(context, AUTH_RESOURCE);
15
16
  appendWebRouter(context);
17
+ appendWebLogin(context);
16
18
  context.makeContext = makeContext;
17
19
  return context;
18
20
  };
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAEpD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,EAAE,UAAU,IAAI,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAEtD,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,GAAM,EACH,EAAE;IACL,MAAM,OAAO,GAAG,iBAAiB,CAAC,GAAG,CAAM,CAAA;IAC3C,kBAAkB,CAAO,OAAO,CAAC,CAAA;IAEjC,oBAAoB,CAAO,OAAO,CAAC,CAAA;IACnC,kBAAkB,CAAO,OAAO,CAAC,CAAA;IACjC,oBAAoB,CAAO,OAAO,EAAE,aAAa,CAAC,CAAA;IAClD,eAAe,CAAO,OAAO,CAAC,CAAA;IAE9B,OAAO,CAAC,WAAW,GAAG,WAAyC,CAAA;IAE/D,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,UAAU,GAAG,GAA4E,EAAE,CACtG,MAAM,EAAO,CAAA"}
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAEpD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,EAAE,UAAU,IAAI,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAEnD,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,GAAM,EACH,EAAE;IACL,MAAM,OAAO,GAAG,iBAAiB,CAAC,GAAG,CAAM,CAAA;IAC3C,kBAAkB,CAAO,OAAO,CAAC,CAAA;IAEjC,oBAAoB,CAAO,OAAO,CAAC,CAAA;IACnC,kBAAkB,CAAO,OAAO,CAAC,CAAA;IACjC,oBAAoB,CAAO,OAAO,EAAE,aAAa,CAAC,CAAA;IAClD,eAAe,CAAO,OAAO,CAAC,CAAA;IAC9B,cAAc,CAAO,OAAO,CAAC,CAAA;IAE7B,OAAO,CAAC,WAAW,GAAG,WAAyC,CAAA;IAE/D,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,UAAU,GAAG,GAA4E,EAAE,CACtG,MAAM,EAAO,CAAA"}
@@ -4,6 +4,7 @@ export { service } from '@owlmeans/config';
4
4
  export { AppType, HOME, ROOT, BASE, GUEST } from '@owlmeans/context';
5
5
  export { entrypoint, elevate, provideRequest, stab } from '@owlmeans/client-entrypoint';
6
6
  export { route, frontend } from '@owlmeans/route';
7
+ export type { ClientEntrypoint } from '@owlmeans/client-entrypoint';
7
8
  export type { ClientEntrypoint as Module } from '@owlmeans/client-entrypoint';
8
9
  export type { ClientRoute as Route } from '@owlmeans/client-route';
9
10
  //# sourceMappingURL=exports.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"exports.d.ts","sourceRoot":"","sources":["../src/exports.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAA;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAC1C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AACpE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAA;AACvF,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AACjD,YAAY,EAAE,gBAAgB,IAAI,MAAM,EAAE,MAAM,6BAA6B,CAAA;AAC7E,YAAY,EAAE,WAAW,IAAI,KAAK,EAAE,MAAM,wBAAwB,CAAA"}
1
+ {"version":3,"file":"exports.d.ts","sourceRoot":"","sources":["../src/exports.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAA;AACjD,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAC1C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AACpE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAA;AACvF,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AACjD,YAAY,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AACnE,YAAY,EAAE,gBAAgB,IAAI,MAAM,EAAE,MAAM,6BAA6B,CAAA;AAC7E,YAAY,EAAE,WAAW,IAAI,KAAK,EAAE,MAAM,wBAAwB,CAAA"}
package/build/index.d.ts CHANGED
@@ -10,4 +10,5 @@ export * from './helper.js';
10
10
  export * from './i18n.js';
11
11
  export * from './service.js';
12
12
  export * from './errors.js';
13
+ export * from './login/index.js';
13
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,kBAAkB,CAAA"}
package/build/index.js CHANGED
@@ -10,4 +10,5 @@ export * from './helper.js';
10
10
  export * from './i18n.js';
11
11
  export * from './service.js';
12
12
  export * from './errors.js';
13
+ export * from './login/index.js';
13
14
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,kBAAkB,CAAA"}
@@ -0,0 +1,11 @@
1
+ /** The ordinary flow: leave for the provider and come back. */
2
+ export declare const REDIRECT_LOGIN = "owlmeans-redirect-login";
3
+ /** The flow that runs one window up, for a document that cannot redirect where it is. */
4
+ export declare const SURROGATE_LOGIN = "owlmeans-surrogate-login";
5
+ /**
6
+ * Registered above the default so it wins the cascade wherever it applies. It applies narrowly —
7
+ * only in a frame, or in the surrogate window itself — so the default still serves every
8
+ * ordinary tab.
9
+ */
10
+ export declare const SURROGATE_LOGIN_PRIORITY = 100;
11
+ //# sourceMappingURL=consts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../../src/login/consts.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,eAAO,MAAM,cAAc,4BAA4B,CAAA;AAEvD,yFAAyF;AACzF,eAAO,MAAM,eAAe,6BAA6B,CAAA;AAEzD;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,MAAM,CAAA"}
@@ -0,0 +1,11 @@
1
+ /** The ordinary flow: leave for the provider and come back. */
2
+ export const REDIRECT_LOGIN = 'owlmeans-redirect-login';
3
+ /** The flow that runs one window up, for a document that cannot redirect where it is. */
4
+ export const SURROGATE_LOGIN = 'owlmeans-surrogate-login';
5
+ /**
6
+ * Registered above the default so it wins the cascade wherever it applies. It applies narrowly —
7
+ * only in a frame, or in the surrogate window itself — so the default still serves every
8
+ * ordinary tab.
9
+ */
10
+ export const SURROGATE_LOGIN_PRIORITY = 100;
11
+ //# sourceMappingURL=consts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consts.js","sourceRoot":"","sources":["../../src/login/consts.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,MAAM,CAAC,MAAM,cAAc,GAAG,yBAAyB,CAAA;AAEvD,yFAAyF;AACzF,MAAM,CAAC,MAAM,eAAe,GAAG,0BAA0B,CAAA;AAEzD;;;;GAIG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,CAAA"}
@@ -0,0 +1,5 @@
1
+ export * from './consts.js';
2
+ export * from './redirect.js';
3
+ export * from './surrogate.js';
4
+ export * from './service.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/login/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,cAAc,CAAA"}
@@ -0,0 +1,5 @@
1
+ export * from './consts.js';
2
+ export * from './redirect.js';
3
+ export * from './surrogate.js';
4
+ export * from './service.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/login/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,cAAc,CAAA"}
@@ -0,0 +1,9 @@
1
+ import type { LoginPlugin } from '@owlmeans/client-auth/login';
2
+ /**
3
+ * The ordinary login flow: this document leaves for the identity provider and comes back.
4
+ *
5
+ * The default plugin, matching everything, so any environment that does not need special handling
6
+ * behaves exactly as it always did.
7
+ */
8
+ export declare const makeRedirectLoginPlugin: () => LoginPlugin;
9
+ //# sourceMappingURL=redirect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redirect.d.ts","sourceRoot":"","sources":["../../src/login/redirect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAA;AAG9D;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAAO,WA2BzC,CAAA"}
@@ -0,0 +1,31 @@
1
+ import { LoginOutcome } from '@owlmeans/client-auth/login';
2
+ import { REDIRECT_LOGIN } from './consts.js';
3
+ /**
4
+ * The ordinary login flow: this document leaves for the identity provider and comes back.
5
+ *
6
+ * The default plugin, matching everything, so any environment that does not need special handling
7
+ * behaves exactly as it always did.
8
+ */
9
+ export const makeRedirectLoginPlugin = () => ({
10
+ alias: REDIRECT_LOGIN,
11
+ priority: 0,
12
+ mode: 'redirect',
13
+ match: () => true,
14
+ begin: async (_ctx, request) => {
15
+ // Prefer the in-app continuation the caller handed over: a client-side navigation to the
16
+ // dispatcher keeps the app mounted, where a full page load would tear it down and rebuild it.
17
+ if (request.navigate != null) {
18
+ await request.navigate();
19
+ return LoginOutcome.Handled;
20
+ }
21
+ window.location.href = request.url;
22
+ return LoginOutcome.Redirected;
23
+ },
24
+ authorize: async (_ctx, url) => {
25
+ window.location.href = url;
26
+ return LoginOutcome.Redirected;
27
+ },
28
+ // Nothing to hand anywhere — the token was issued in the document that will use it.
29
+ complete: async () => LoginOutcome.Passed,
30
+ });
31
+ //# sourceMappingURL=redirect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redirect.js","sourceRoot":"","sources":["../../src/login/redirect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAE1D,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAE5C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAgB,EAAE,CAAC,CAAC;IACzD,KAAK,EAAE,cAAc;IACrB,QAAQ,EAAE,CAAC;IACX,IAAI,EAAE,UAAU;IAChB,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI;IAEjB,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;QAC7B,yFAAyF;QACzF,8FAA8F;QAC9F,IAAI,OAAO,CAAC,QAAQ,IAAI,IAAI,EAAE,CAAC;YAC7B,MAAM,OAAO,CAAC,QAAQ,EAAE,CAAA;YAExB,OAAO,YAAY,CAAC,OAAO,CAAA;QAC7B,CAAC;QACD,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,OAAO,CAAC,GAAG,CAAA;QAElC,OAAO,YAAY,CAAC,UAAU,CAAA;IAChC,CAAC;IAED,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE;QAC7B,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,GAAG,CAAA;QAE1B,OAAO,YAAY,CAAC,UAAU,CAAA;IAChC,CAAC;IAED,oFAAoF;IACpF,QAAQ,EAAE,KAAK,IAAI,EAAE,CAAC,YAAY,CAAC,MAAM;CAC1C,CAAC,CAAA"}
@@ -0,0 +1,10 @@
1
+ import type { BasicConfig, BasicContext } from '@owlmeans/context';
2
+ /**
3
+ * Register the login host and the two browser flows on a context.
4
+ *
5
+ * Called by `makeContext`, so every web app has working login — including a framed one — without
6
+ * registering anything. An app that needs a different mechanic registers its own plugin at a
7
+ * higher priority; it does not replace this.
8
+ */
9
+ export declare const appendWebLogin: <C extends BasicConfig, T extends BasicContext<C>>(ctx: T) => T;
10
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/login/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAIlE;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,CAAC,SAAS,WAAW,EAAE,CAAC,SAAS,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,KAAG,CAMzF,CAAA"}
@@ -0,0 +1,17 @@
1
+ import { appendLogin } from '@owlmeans/client-auth/login';
2
+ import { makeRedirectLoginPlugin } from './redirect.js';
3
+ import { makeSurrogateLoginPlugin } from './surrogate.js';
4
+ /**
5
+ * Register the login host and the two browser flows on a context.
6
+ *
7
+ * Called by `makeContext`, so every web app has working login — including a framed one — without
8
+ * registering anything. An app that needs a different mechanic registers its own plugin at a
9
+ * higher priority; it does not replace this.
10
+ */
11
+ export const appendWebLogin = (ctx) => {
12
+ const service = appendLogin(ctx).login();
13
+ service.registerPlugin(makeRedirectLoginPlugin());
14
+ service.registerPlugin(makeSurrogateLoginPlugin());
15
+ return ctx;
16
+ };
17
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/login/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAA;AAEzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAA;AACvD,OAAO,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAA;AAEzD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAmD,GAAM,EAAK,EAAE;IAC5F,MAAM,OAAO,GAAG,WAAW,CAAO,GAAG,CAAC,CAAC,KAAK,EAAE,CAAA;IAC9C,OAAO,CAAC,cAAc,CAAC,uBAAuB,EAAE,CAAC,CAAA;IACjD,OAAO,CAAC,cAAc,CAAC,wBAAwB,EAAE,CAAC,CAAA;IAElD,OAAO,GAAG,CAAA;AACZ,CAAC,CAAA"}
@@ -0,0 +1,21 @@
1
+ import type { LoginPlugin } from '@owlmeans/client-auth/login';
2
+ /**
3
+ * Login for a document that cannot complete the flow where it is.
4
+ *
5
+ * An app embedded in a frame cannot redirect to an identity provider: the provider refuses to be
6
+ * embedded (`frame-ancestors`), and its session cookies are third-party there. So the flow runs
7
+ * one window up, in a surrogate window that is top-level and first-party on this application's own
8
+ * origin — where the very same flow completes normally — and the token is passed back to the
9
+ * document that asked for it.
10
+ *
11
+ * This plugin is deliberately not OIDC-specific. The mechanic is "run the login route one window
12
+ * up and hand the token back", and the adoption is the plain auth service, so any relying party
13
+ * that renders a login route gets it by registering nothing.
14
+ *
15
+ * `match` covers BOTH halves of the lifecycle — the framed opener and the surrogate itself, where
16
+ * `embedded` is false. Inside the surrogate the redirect plugin must not win: that window IS
17
+ * top-level, so it authorizes by an ordinary redirect and differs only in what it does with the
18
+ * token at the end.
19
+ */
20
+ export declare const makeSurrogateLoginPlugin: () => LoginPlugin;
21
+ //# sourceMappingURL=surrogate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"surrogate.d.ts","sourceRoot":"","sources":["../../src/login/surrogate.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAgB,WAAW,EAAE,MAAM,6BAA6B,CAAA;AAG5E;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,wBAAwB,QAAO,WA0G1C,CAAA"}
@@ -0,0 +1,114 @@
1
+ import { adoptToken, clearSurrogate, markSurrogate, LoginOutcome, LOGIN_SURROGATE_FEATURES, LOGIN_SURROGATE_NAME, LOGIN_TOKEN_MESSAGE, LOGIN_WATCH_INTERVAL, } from '@owlmeans/client-auth/login';
2
+ import { SURROGATE_LOGIN, SURROGATE_LOGIN_PRIORITY } from './consts.js';
3
+ /**
4
+ * Login for a document that cannot complete the flow where it is.
5
+ *
6
+ * An app embedded in a frame cannot redirect to an identity provider: the provider refuses to be
7
+ * embedded (`frame-ancestors`), and its session cookies are third-party there. So the flow runs
8
+ * one window up, in a surrogate window that is top-level and first-party on this application's own
9
+ * origin — where the very same flow completes normally — and the token is passed back to the
10
+ * document that asked for it.
11
+ *
12
+ * This plugin is deliberately not OIDC-specific. The mechanic is "run the login route one window
13
+ * up and hand the token back", and the adoption is the plain auth service, so any relying party
14
+ * that renders a login route gets it by registering nothing.
15
+ *
16
+ * `match` covers BOTH halves of the lifecycle — the framed opener and the surrogate itself, where
17
+ * `embedded` is false. Inside the surrogate the redirect plugin must not win: that window IS
18
+ * top-level, so it authorizes by an ordinary redirect and differs only in what it does with the
19
+ * token at the end.
20
+ */
21
+ export const makeSurrogateLoginPlugin = () => ({
22
+ alias: SURROGATE_LOGIN,
23
+ priority: SURROGATE_LOGIN_PRIORITY,
24
+ mode: 'surrogate',
25
+ match: env => env.hasWindow && (env.embedded || env.surrogate),
26
+ enter: () => { markSurrogate(); },
27
+ begin: (ctx, request, env) => {
28
+ // Already one window up — nothing to open, just run the flow.
29
+ if (env.surrogate) {
30
+ window.location.href = request.url;
31
+ return Promise.resolve(LoginOutcome.Redirected);
32
+ }
33
+ // Opened FIRST and synchronously: `window.open` escapes the popup blocker only while the user
34
+ // gesture is still being handled, so nothing may be awaited before this line. That is why this
35
+ // function is not `async`.
36
+ const surrogate = window.open(request.url, LOGIN_SURROGATE_NAME, LOGIN_SURROGATE_FEATURES);
37
+ if (surrogate == null) {
38
+ return Promise.resolve(LoginOutcome.Failed);
39
+ }
40
+ return new Promise(resolve => {
41
+ let settled = false;
42
+ let watch;
43
+ const finish = (result) => {
44
+ if (settled) {
45
+ return;
46
+ }
47
+ settled = true;
48
+ window.removeEventListener('message', onMessage);
49
+ if (watch != null) {
50
+ clearInterval(watch);
51
+ }
52
+ resolve(result);
53
+ };
54
+ function onMessage(event) {
55
+ if (event.origin !== window.location.origin) {
56
+ return;
57
+ }
58
+ const data = event.data;
59
+ if (data?.type !== LOGIN_TOKEN_MESSAGE || data.token == null || data.token === '') {
60
+ return;
61
+ }
62
+ void adoptToken(ctx, data.token)
63
+ .then(async () => {
64
+ // Adopting the token is not the end of the flow — running the caller's continuation
65
+ // is, exactly as the redirect plugin does before it reports `Handled`. Without it the
66
+ // token lands in the auth service and nothing else happens: the framed app keeps
67
+ // rendering its signed-out tree, and the user watches the login window close over a
68
+ // preview that never changes. `Handled` promises the caller there is nothing left to
69
+ // do, so it has to be true before it is returned.
70
+ await request.navigate?.();
71
+ finish(LoginOutcome.Handled);
72
+ })
73
+ .catch(() => finish(LoginOutcome.Failed));
74
+ }
75
+ window.addEventListener('message', onMessage);
76
+ // A window the user simply closes announces nothing, so the only way to stop waiting on it
77
+ // is to watch for it going away.
78
+ watch = setInterval(() => {
79
+ if (surrogate.closed) {
80
+ finish(LoginOutcome.Failed);
81
+ }
82
+ }, LOGIN_WATCH_INTERVAL);
83
+ });
84
+ },
85
+ authorize: async (_ctx, url, env) => {
86
+ // The surrogate is top-level, so it redirects like any ordinary tab.
87
+ if (env.surrogate) {
88
+ window.location.href = url;
89
+ return LoginOutcome.Redirected;
90
+ }
91
+ // Framed, and not yet one window up. A redirect from here would land in the frame and be
92
+ // refused, and the window that would fix it cannot be opened without a user gesture — so the
93
+ // caller is told to render a control rather than to navigate.
94
+ return LoginOutcome.Gesture;
95
+ },
96
+ complete: async (_ctx, token, env) => {
97
+ if (!env.surrogate) {
98
+ // This document started the flow and finished it in place — it already has what it needs.
99
+ return LoginOutcome.Passed;
100
+ }
101
+ if (!env.hasOpener) {
102
+ // Authenticated with nowhere to send it. `Cross-Origin-Opener-Policy: same-origin` from the
103
+ // provider severs `window.opener` permanently, so this is not a timing problem to retry.
104
+ return LoginOutcome.Orphaned;
105
+ }
106
+ clearSurrogate();
107
+ // Both windows are the same origin (the surrogate is this application's own login route), so
108
+ // the origin is pinned rather than passed as `*` — the message carries a bearer token.
109
+ window.opener.postMessage({ type: LOGIN_TOKEN_MESSAGE, token }, window.location.origin);
110
+ window.close();
111
+ return LoginOutcome.Handled;
112
+ },
113
+ });
114
+ //# sourceMappingURL=surrogate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"surrogate.js","sourceRoot":"","sources":["../../src/login/surrogate.ts"],"names":[],"mappings":"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"}
package/build/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { AuthServiceAppend } from '@owlmeans/client-auth';
2
+ import type { LoginServiceAppend } from '@owlmeans/client-auth/login';
2
3
  import type { ClientConfig } from '@owlmeans/client-context';
3
4
  import type { ClientContext } from '@owlmeans/client';
4
5
  export interface RenderOptions {
@@ -9,6 +10,6 @@ export interface RenderOptions {
9
10
  }
10
11
  export interface AppConfig extends ClientConfig {
11
12
  }
12
- export interface AppContext<C extends AppConfig = AppConfig> extends ClientContext<C>, AuthServiceAppend {
13
+ export interface AppContext<C extends AppConfig = AppConfig> extends ClientContext<C>, AuthServiceAppend, LoginServiceAppend {
13
14
  }
14
15
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAC9D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAErD,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,MAAM,WAAW,SAAU,SAAQ,YAAY;CAC9C;AAED,MAAM,WAAW,UAAU,CAAC,CAAC,SAAS,SAAS,GAAG,SAAS,CAAE,SAAQ,aAAa,CAAC,CAAC,CAAC,EACnF,iBAAiB;CAClB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAC9D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAA;AACrE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAErD,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB;AAED,MAAM,WAAW,SAAU,SAAQ,YAAY;CAC9C;AAED,MAAM,WAAW,UAAU,CAAC,CAAC,SAAS,SAAS,GAAG,SAAS,CAAE,SAAQ,aAAa,CAAC,CAAC,CAAC,EACnF,iBAAiB,EAAE,kBAAkB;CACtC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/web-client",
3
- "version": "0.1.18-rc.1",
3
+ "version": "0.1.18-rc.10",
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.1",
25
- "@owlmeans/auth-common": "^0.1.18-rc.1",
26
- "@owlmeans/client": "^0.1.18-rc.1",
27
- "@owlmeans/client-auth": "^0.1.18-rc.1",
28
- "@owlmeans/client-context": "^0.1.18-rc.1",
29
- "@owlmeans/client-i18n": "^0.1.18-rc.1",
30
- "@owlmeans/client-entrypoint": "^0.1.18-rc.1",
31
- "@owlmeans/client-resource": "^0.1.18-rc.1",
32
- "@owlmeans/client-route": "^0.1.18-rc.1",
33
- "@owlmeans/config": "^0.1.18-rc.1",
34
- "@owlmeans/context": "^0.1.18-rc.1",
35
- "@owlmeans/error": "^0.1.18-rc.1",
36
- "@owlmeans/i18n": "^0.1.18-rc.1",
37
- "@owlmeans/route": "^0.1.18-rc.1",
38
- "@owlmeans/web-db": "^0.1.18-rc.1",
39
- "@owlmeans/web-router": "^0.1.18-rc.1",
24
+ "@owlmeans/auth": "^0.1.18-rc.6",
25
+ "@owlmeans/auth-common": "^0.1.18-rc.7",
26
+ "@owlmeans/client": "^0.1.18-rc.8",
27
+ "@owlmeans/client-auth": "^0.1.18-rc.9",
28
+ "@owlmeans/client-context": "^0.1.18-rc.7",
29
+ "@owlmeans/client-i18n": "^0.1.18-rc.7",
30
+ "@owlmeans/client-entrypoint": "^0.1.18-rc.7",
31
+ "@owlmeans/client-resource": "^0.1.18-rc.7",
32
+ "@owlmeans/client-route": "^0.1.18-rc.7",
33
+ "@owlmeans/config": "^0.1.18-rc.7",
34
+ "@owlmeans/context": "^0.1.18-rc.6",
35
+ "@owlmeans/error": "^0.1.18-rc.6",
36
+ "@owlmeans/i18n": "^0.1.18-rc.6",
37
+ "@owlmeans/route": "^0.1.18-rc.6",
38
+ "@owlmeans/web-db": "^0.1.18-rc.7",
39
+ "@owlmeans/web-router": "^0.1.18-rc.7",
40
40
  "react-dropzone": "^14.3.5"
41
41
  },
42
42
  "peerDependencies": {
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,6 +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)
23
+ appendWebLogin<C, T>(context)
22
24
 
23
25
  context.makeContext = makeContext as typeof context.makeContext
24
26
 
package/src/exports.ts CHANGED
@@ -5,5 +5,6 @@ export { service } from '@owlmeans/config'
5
5
  export { AppType, HOME, ROOT, BASE, GUEST } from '@owlmeans/context'
6
6
  export { entrypoint, elevate, 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'
package/src/index.ts CHANGED
@@ -11,3 +11,4 @@ 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,4 @@
1
+ export * from './consts.js'
2
+ export * from './redirect.js'
3
+ export * from './surrogate.js'
4
+ export * from './service.js'
@@ -0,0 +1,38 @@
1
+ import { LoginOutcome } from '@owlmeans/client-auth/login'
2
+ import type { 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
+ })
@@ -0,0 +1,19 @@
1
+ import { appendLogin } from '@owlmeans/client-auth/login'
2
+ import type { BasicConfig, BasicContext } from '@owlmeans/context'
3
+ import { makeRedirectLoginPlugin } from './redirect.js'
4
+ import { makeSurrogateLoginPlugin } from './surrogate.js'
5
+
6
+ /**
7
+ * Register the login host and the two browser flows on a context.
8
+ *
9
+ * Called by `makeContext`, so every web app has working login — including a framed one — without
10
+ * registering anything. An app that needs a different mechanic registers its own plugin at a
11
+ * higher priority; it does not replace this.
12
+ */
13
+ export const appendWebLogin = <C extends BasicConfig, T extends BasicContext<C>>(ctx: T): T => {
14
+ const service = appendLogin<C, T>(ctx).login()
15
+ service.registerPlugin(makeRedirectLoginPlugin())
16
+ service.registerPlugin(makeSurrogateLoginPlugin())
17
+
18
+ return ctx
19
+ }
@@ -0,0 +1,132 @@
1
+ import {
2
+ adoptToken, clearSurrogate, markSurrogate, LoginOutcome,
3
+ LOGIN_SURROGATE_FEATURES, LOGIN_SURROGATE_NAME, LOGIN_TOKEN_MESSAGE, LOGIN_WATCH_INTERVAL,
4
+ } from '@owlmeans/client-auth/login'
5
+ import type { LoginContext, LoginPlugin } from '@owlmeans/client-auth/login'
6
+ import { SURROGATE_LOGIN, SURROGATE_LOGIN_PRIORITY } from './consts.js'
7
+
8
+ /**
9
+ * Login for a document that cannot complete the flow where it is.
10
+ *
11
+ * An app embedded in a frame cannot redirect to an identity provider: the provider refuses to be
12
+ * embedded (`frame-ancestors`), and its session cookies are third-party there. So the flow runs
13
+ * one window up, in a surrogate window that is top-level and first-party on this application's own
14
+ * origin — where the very same flow completes normally — and the token is passed back to the
15
+ * document that asked for it.
16
+ *
17
+ * This plugin is deliberately not OIDC-specific. The mechanic is "run the login route one window
18
+ * up and hand the token back", and the adoption is the plain auth service, so any relying party
19
+ * that renders a login route gets it by registering nothing.
20
+ *
21
+ * `match` covers BOTH halves of the lifecycle — the framed opener and the surrogate itself, where
22
+ * `embedded` is false. Inside the surrogate the redirect plugin must not win: that window IS
23
+ * top-level, so it authorizes by an ordinary redirect and differs only in what it does with the
24
+ * token at the end.
25
+ */
26
+ export const makeSurrogateLoginPlugin = (): LoginPlugin => ({
27
+ alias: SURROGATE_LOGIN,
28
+ priority: SURROGATE_LOGIN_PRIORITY,
29
+ mode: 'surrogate',
30
+ match: env => env.hasWindow && (env.embedded || env.surrogate),
31
+
32
+ enter: () => { markSurrogate() },
33
+
34
+ begin: (ctx, request, env) => {
35
+ // Already one window up — nothing to open, just run the flow.
36
+ if (env.surrogate) {
37
+ window.location.href = request.url
38
+
39
+ return Promise.resolve(LoginOutcome.Redirected)
40
+ }
41
+
42
+ // Opened FIRST and synchronously: `window.open` escapes the popup blocker only while the user
43
+ // gesture is still being handled, so nothing may be awaited before this line. That is why this
44
+ // function is not `async`.
45
+ const surrogate = window.open(request.url, LOGIN_SURROGATE_NAME, LOGIN_SURROGATE_FEATURES)
46
+ if (surrogate == null) {
47
+ return Promise.resolve(LoginOutcome.Failed)
48
+ }
49
+
50
+ return new Promise<LoginOutcome>(resolve => {
51
+ let settled = false
52
+ let watch: ReturnType<typeof setInterval> | undefined
53
+
54
+ const finish = (result: LoginOutcome): void => {
55
+ if (settled) {
56
+ return
57
+ }
58
+ settled = true
59
+ window.removeEventListener('message', onMessage)
60
+ if (watch != null) {
61
+ clearInterval(watch)
62
+ }
63
+ resolve(result)
64
+ }
65
+
66
+ function onMessage(event: MessageEvent): void {
67
+ if (event.origin !== window.location.origin) {
68
+ return
69
+ }
70
+ const data = event.data as { type?: string, token?: string } | null
71
+ if (data?.type !== LOGIN_TOKEN_MESSAGE || data.token == null || data.token === '') {
72
+ return
73
+ }
74
+ void adoptToken(ctx as LoginContext, data.token)
75
+ .then(async () => {
76
+ // Adopting the token is not the end of the flow — running the caller's continuation
77
+ // is, exactly as the redirect plugin does before it reports `Handled`. Without it the
78
+ // token lands in the auth service and nothing else happens: the framed app keeps
79
+ // rendering its signed-out tree, and the user watches the login window close over a
80
+ // preview that never changes. `Handled` promises the caller there is nothing left to
81
+ // do, so it has to be true before it is returned.
82
+ await request.navigate?.()
83
+ finish(LoginOutcome.Handled)
84
+ })
85
+ .catch(() => finish(LoginOutcome.Failed))
86
+ }
87
+
88
+ window.addEventListener('message', onMessage)
89
+ // A window the user simply closes announces nothing, so the only way to stop waiting on it
90
+ // is to watch for it going away.
91
+ watch = setInterval(() => {
92
+ if (surrogate.closed) {
93
+ finish(LoginOutcome.Failed)
94
+ }
95
+ }, LOGIN_WATCH_INTERVAL)
96
+ })
97
+ },
98
+
99
+ authorize: async (_ctx, url, env) => {
100
+ // The surrogate is top-level, so it redirects like any ordinary tab.
101
+ if (env.surrogate) {
102
+ window.location.href = url
103
+
104
+ return LoginOutcome.Redirected
105
+ }
106
+
107
+ // Framed, and not yet one window up. A redirect from here would land in the frame and be
108
+ // refused, and the window that would fix it cannot be opened without a user gesture — so the
109
+ // caller is told to render a control rather than to navigate.
110
+ return LoginOutcome.Gesture
111
+ },
112
+
113
+ complete: async (_ctx, token, env) => {
114
+ if (!env.surrogate) {
115
+ // This document started the flow and finished it in place — it already has what it needs.
116
+ return LoginOutcome.Passed
117
+ }
118
+ if (!env.hasOpener) {
119
+ // Authenticated with nowhere to send it. `Cross-Origin-Opener-Policy: same-origin` from the
120
+ // provider severs `window.opener` permanently, so this is not a timing problem to retry.
121
+ return LoginOutcome.Orphaned
122
+ }
123
+
124
+ clearSurrogate()
125
+ // Both windows are the same origin (the surrogate is this application's own login route), so
126
+ // the origin is pinned rather than passed as `*` — the message carries a bearer token.
127
+ window.opener.postMessage({ type: LOGIN_TOKEN_MESSAGE, token }, window.location.origin)
128
+ window.close()
129
+
130
+ return LoginOutcome.Handled
131
+ },
132
+ })
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { AuthServiceAppend } from '@owlmeans/client-auth'
2
+ import type { LoginServiceAppend } from '@owlmeans/client-auth/login'
2
3
  import type { ClientConfig } from '@owlmeans/client-context'
3
4
  import type { ClientContext } from '@owlmeans/client'
4
5
 
@@ -13,5 +14,5 @@ export interface AppConfig extends ClientConfig {
13
14
  }
14
15
 
15
16
  export interface AppContext<C extends AppConfig = AppConfig> extends ClientContext<C>,
16
- AuthServiceAppend {
17
+ AuthServiceAppend, LoginServiceAppend {
17
18
  }