@owlmeans/client-flow 0.1.18-rc.52 → 0.1.18-rc.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,7 @@ Client-side flow orchestration service for managing multi-step workflows in brow
12
12
  ## Installation
13
13
 
14
14
  ```bash
15
- bun add @owlmeans/client-flow@^0.1.18-rc.52
15
+ bun add @owlmeans/client-flow@^0.1.18-rc.54
16
16
  ```
17
17
 
18
18
  ## Usage
@@ -75,7 +75,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
75
75
  your project's skill store (`.agents/skills/`):
76
76
 
77
77
  ```sh
78
- npx @owlmeans/agent-skills@^0.1.18-rc.47
78
+ npx @owlmeans/agent-skills@^0.1.18-rc.49
79
79
  ```
80
80
 
81
81
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/client-flow",
4
- "version": "0.1.18-rc.52",
5
- "generatedAt": "2026-10-03T20:05:07.581Z",
4
+ "version": "0.1.18-rc.54",
5
+ "generatedAt": "2026-10-05T21:03:06.361Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/client-flow
9
9
 
10
10
  **Layer:** Client
11
- **Install:** `"@owlmeans/client-flow": "^0.1.18-rc.52"` in `dependencies`
11
+ **Install:** `"@owlmeans/client-flow": "^0.1.18-rc.54"` in `dependencies`
12
12
 
13
13
  Two objects, with different lifetimes. The **service** lives on the context and owns the flow
14
14
  definitions and the one live `FlowModel`. The **client** is built per screen, wraps that model with
@@ -26,7 +26,7 @@ a `Navigator`, and is what a component actually calls.
26
26
  | `ResolvePair` | The `{ resolve, reject }` behind `service.supplied` |
27
27
  | `DEFAULT_ALIAS` (`flow`) | The service alias |
28
28
  | `FLOW_STATE` (`state:flow`) | Alias of the client resource the state is persisted in, and the record id inside it |
29
- | `suspendFlow(context, model, { expiresAt })` · `resumeSuspendedFlow(context)` · `RESUME_FLOW` (`resume-flow`) | The suspended landing: park where a flow was headed before sign-in, read it back once after |
29
+ | `flowLandingOf(context)` → `.suspendFlow(model, { expiresAt })` · `.suspendLanding({ entrypoint, query }, { expiresAt })` · `.resumeSuspendedFlow()` · `.discardSuspendedLanding()` · `RESUME_FLOW` (`resume-flow`) | The suspended landing: park where a flow was headed (or a known screen) before sign-in, read it back once after, or drop it unread |
30
30
  | `SuspendedLanding` `{ entrypoint, query }` · `SuspendedLandingRecord` | What resumes / what is stored |
31
31
  | `EXTRA_FLOW` (`extra-flow`) · `REHACK_MOD` (`__redirect`) | Id of the second, side-band state record kept in the same resource, and the alias of the entrypoint synthesized to address a target service |
32
32
 
@@ -127,27 +127,34 @@ record all belong to the OIDC sign-in machinery, so a flow that must leave for s
127
127
  side-band record under `RESUME_FLOW` in the same `FLOW_STATE` resource — the `EXTRA_FLOW` precedent —
128
128
  which is IndexedDB in a browser and so survives a full-page Google round trip.
129
129
 
130
- - `suspendFlow(context, model, { expiresAt })` asks the model's own `next()` where the current step
130
+ - `flowLandingOf(context).suspendFlow(model, { expiresAt })` asks the model's own `next()` where the current step
131
131
  leads and stores that destination step's **`module`** (an entrypoint alias), the model's `payload()`
132
132
  as `query`, and `expiresAt` (epoch ms). It answers `false` — and stores nothing — when `FLOW_STATE`
133
133
  is not registered, the step has no forward transition, or the destination has no `module`; the
134
134
  caller then lands on `HOME` as before. The record is not a serialized flow token: the destination
135
135
  is a screen the app can enter fresh, reading its own parameters from the query.
136
- - `resumeSuspendedFlow(context)` returns `{ entrypoint, query }` or `null`, and is **delete-on-read** —
136
+ - `.suspendLanding({ entrypoint, query }, { expiresAt })` writes the SAME record for a caller
137
+ whose destination is already an alias rather than a flow step — `@owlmeans/client-auth`'s login
138
+ facade parks `useLogin(target)`'s screen with it. One record, one reader: the later write replaces
139
+ the earlier, whichever helper wrote it. `false` when `FLOW_STATE` is not registered.
140
+ - `.discardSuspendedLanding()` deletes the record unread — for a sign-in that ended without
141
+ signing anyone in, so its landing cannot hijack the next, unrelated one. A no-op with nothing there.
142
+ - `.resumeSuspendedFlow()` returns `{ entrypoint, query }` or `null`, and is **delete-on-read** —
137
143
  a landing answers exactly one sign-in, so a stale tab's record never resurrects on someone else's
138
144
  later sign-in. `null` covers no resource, no record, a failed read and an expired record.
139
145
  - **Destinations are entrypoint aliases from a registered flow definition, never stored URLs**, so a
140
146
  landing cannot become an open redirect. Consumers navigate to the alias with the query.
141
147
 
142
- Two flows use it today: the OAuth consent screen (`oauthFlow`, a signed-out person on a consent link) and
143
- viable's intent-first landing (`intentFlow` from `@owlmeans/viable-common/intent`, a visitor who arrives
144
- from the public site with `?ref=` and no account) — both enter their flow FRESH on an `initial` screen
145
- and never carry `?flow=`.
148
+ Writers today: `useLogin(target)` (`@owlmeans/client-auth`, through the login facade's
149
+ `suspendLanding`), the OAuth consent screen (`oauthFlow`, a signed-out person on a consent link) and
150
+ viable's intent-first landing (`intentFlow` from `@owlmeans/viable-common/intent`, a visitor who
151
+ arrives from the public site with `?ref=` and no account) — the two flows enter FRESH on an `initial`
152
+ screen and never carry `?flow=`.
146
153
 
147
154
  Every sign-in completion asks `resumeSuspendedFlow` before it navigates home: `DispatcherHOC`'s HOME
148
155
  branch (`@owlmeans/client-auth`, so the `/dispatcher?token=` and resume paths are covered) and the
149
156
  supervisor and Google login plugins (`web-auth`, `web-oidc-rp`) — see [[login-plugins]].
150
- `bun test ./tests` covers suspend, resume, expiry and the empty cases.
157
+ `bun test ./tests` covers suspend (flow and known landing), resume, discard, expiry and the empty cases.
151
158
 
152
159
  ## Depends On
153
160
 
package/build/consts.d.ts CHANGED
@@ -2,4 +2,7 @@ export declare const DEFAULT_ALIAS = "flow";
2
2
  export declare const FLOW_STATE = "state:flow";
3
3
  export declare const EXTRA_FLOW = "extra-flow";
4
4
  export declare const REHACK_MOD = "__redirect";
5
+ /** The record id a suspended landing is stored under, in the SAME resource `EXTRA_FLOW` uses —
6
+ * a side-band slot, never the live `FlowService.flow` and never the `?flow=` query parameter. */
7
+ export declare const RESUME_FLOW = "resume-flow";
5
8
  //# sourceMappingURL=consts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,aAAa,SAAS,CAAA;AAEnC,eAAO,MAAM,UAAU,eAAe,CAAA;AAEtC,eAAO,MAAM,UAAU,eAAe,CAAA;AAEtC,eAAO,MAAM,UAAU,eAAe,CAAA"}
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,aAAa,SAAS,CAAA;AAEnC,eAAO,MAAM,UAAU,eAAe,CAAA;AAEtC,eAAO,MAAM,UAAU,eAAe,CAAA;AAEtC,eAAO,MAAM,UAAU,eAAe,CAAA;AAEtC;iGACiG;AACjG,eAAO,MAAM,WAAW,gBAAgB,CAAA"}
package/build/consts.js CHANGED
@@ -2,4 +2,7 @@ export const DEFAULT_ALIAS = 'flow';
2
2
  export const FLOW_STATE = 'state:flow';
3
3
  export const EXTRA_FLOW = 'extra-flow';
4
4
  export const REHACK_MOD = '__redirect';
5
+ /** The record id a suspended landing is stored under, in the SAME resource `EXTRA_FLOW` uses —
6
+ * a side-band slot, never the live `FlowService.flow` and never the `?flow=` query parameter. */
7
+ export const RESUME_FLOW = 'resume-flow';
5
8
  //# sourceMappingURL=consts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAA;AAEnC,MAAM,CAAC,MAAM,UAAU,GAAG,YAAY,CAAA;AAEtC,MAAM,CAAC,MAAM,UAAU,GAAG,YAAY,CAAA;AAEtC,MAAM,CAAC,MAAM,UAAU,GAAG,YAAY,CAAA"}
1
+ {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAA;AAEnC,MAAM,CAAC,MAAM,UAAU,GAAG,YAAY,CAAA;AAEtC,MAAM,CAAC,MAAM,UAAU,GAAG,YAAY,CAAA;AAEtC,MAAM,CAAC,MAAM,UAAU,GAAG,YAAY,CAAA;AAEtC;iGACiG;AACjG,MAAM,CAAC,MAAM,WAAW,GAAG,aAAa,CAAA"}
package/build/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export type * from './types.js';
2
+ export type * from './landing/types.js';
2
3
  export * from './consts.js';
3
4
  export * from './client.js';
4
5
  export * from './service.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,mBAAmB,YAAY,CAAA;AAC/B,mBAAmB,oBAAoB,CAAA;AACvC,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,cAAc,CAAA"}
@@ -0,0 +1,56 @@
1
+ import type { FlowModel } from '@owlmeans/flow';
2
+ import type { SuspendedLanding } from '../types.js';
3
+ /** A flow suspended across the platform's sign-in, and the landing it is resumed at. */
4
+ export interface FlowLandingHelper {
5
+ /**
6
+ * Suspend a flow that is about to leave for the platform's own sign-in dispatcher, so whichever
7
+ * sign-in method completes can send the person back to where they started instead of `HOME`.
8
+ *
9
+ * Driven by the flow model rather than a raw URL: `model.next()` is the flow's own answer to
10
+ * "where does this step lead", so a caller never re-derives a destination the flow already knows,
11
+ * and a flow whose current step offers no way forward (`next()` throws) suspends nothing rather
12
+ * than persisting a landing that can never be reached. The persisted record is deliberately NOT a
13
+ * serialized flow token — the destination step is always one this application can enter fresh (an
14
+ * `initial`-marked screen reading its own `ref`/`kind` from the query), so nothing needs to
15
+ * reconstruct the exact `FlowModel` instance on the other side of a sign-in redirect, and this
16
+ * helper stays usable by any flow, not only one particular package's.
17
+ *
18
+ * Returns `false` when there is nowhere to persist this (no `FLOW_STATE` resource registered) or
19
+ * nothing to suspend to (the current step has no forward transition, or its destination has no
20
+ * `module`) — the caller falls back to its own default landing (ordinarily `HOME`).
21
+ */
22
+ suspendFlow: (model: FlowModel, opts: {
23
+ expiresAt: number;
24
+ }) => Promise<boolean>;
25
+ /**
26
+ * Suspend a landing whose destination is already known — an entrypoint alias, not a flow step —
27
+ * into the same side-band record {@link suspendFlow} writes and {@link resumeSuspendedFlow} reads.
28
+ *
29
+ * For a caller that has no flow to derive a destination from: a sign-in control aimed at one
30
+ * screen (`useLogin(target)` in `@owlmeans/client-auth`) parks that screen here and sends the person
31
+ * to the dispatcher, whose ordinary post-sign-in landing then resumes on it. One record, one reader:
32
+ * a landing parked this way and one parked by a flow are indistinguishable on the other side, and
33
+ * the later write replaces the earlier one.
34
+ *
35
+ * Returns `false` when there is nowhere to persist this (no `FLOW_STATE` resource registered).
36
+ */
37
+ suspendLanding: (landing: SuspendedLanding, opts: {
38
+ expiresAt: number;
39
+ }) => Promise<boolean>;
40
+ /**
41
+ * Drop a suspended landing without acting on it — for a sign-in that ended before it signed anyone
42
+ * in (a refused popup, a window the person closed), so the landing it parked cannot hijack the next,
43
+ * unrelated sign-in. Safe to call when nothing is suspended or nowhere to keep it exists.
44
+ */
45
+ discardSuspendedLanding: () => Promise<void>;
46
+ /**
47
+ * Read back a suspended landing, once. Delete-on-read: the record answers exactly one sign-in,
48
+ * because a landing a stale browser tab left behind must never resurrect on somebody else's
49
+ * sign-in later in the same session.
50
+ *
51
+ * `null` covers every reason there is nothing to resume: no resource, no record, or a record
52
+ * whose window has closed — the caller's own default landing is exactly as safe an answer.
53
+ */
54
+ resumeSuspendedFlow: () => Promise<SuspendedLanding | null>;
55
+ }
56
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/landing/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAEnD,wFAAwF;AACxF,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;;;;;;;;;OAgBG;IACH,WAAW,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAChF;;;;;;;;;;;OAWG;IACH,cAAc,EAAE,CAAC,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IAC5F;;;;OAIG;IACH,uBAAuB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5C;;;;;;;OAOG;IACH,mBAAmB,EAAE,MAAM,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;CAC5D"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/landing/types.ts"],"names":[],"mappings":""}
@@ -1,48 +1,14 @@
1
1
  import type { ClientContext } from '@owlmeans/client';
2
2
  import type { ClientConfig } from '@owlmeans/client-context';
3
- import type { ResourceRecord } from '@owlmeans/resource';
4
- import type { FlowModel, FlowPayload } from '@owlmeans/flow';
5
- /** The record id a suspended landing is stored under, in the SAME resource `EXTRA_FLOW` uses —
6
- * a side-band slot, never the live `FlowService.flow` and never the `?flow=` query parameter. */
7
- export declare const RESUME_FLOW = "resume-flow";
8
- export interface SuspendedLandingRecord extends ResourceRecord {
9
- /** The entrypoint alias to navigate to once sign-in completes. */
10
- entrypoint: string;
11
- /** The flow's payload at the moment it was suspended — carried along as the destination's query. */
12
- query: FlowPayload;
13
- expiresAt: number;
14
- }
15
- export interface SuspendedLanding {
16
- entrypoint: string;
17
- query: FlowPayload;
18
- }
19
- /**
20
- * Suspend a flow that is about to leave for the platform's own sign-in dispatcher, so whichever
21
- * sign-in method completes can send the person back to where they started instead of `HOME`.
22
- *
23
- * Driven by the flow model rather than a raw URL: `model.next()` is the flow's own answer to
24
- * "where does this step lead", so a caller never re-derives a destination the flow already knows,
25
- * and a flow whose current step offers no way forward (`next()` throws) suspends nothing rather
26
- * than persisting a landing that can never be reached. The persisted record is deliberately NOT a
27
- * serialized flow token — the destination step is always one this application can enter fresh (an
28
- * `initial`-marked screen reading its own `ref`/`kind` from the query), so nothing needs to
29
- * reconstruct the exact `FlowModel` instance on the other side of a sign-in redirect, and this
30
- * helper stays usable by any flow, not only one particular package's.
31
- *
32
- * Returns `false` when there is nowhere to persist this (no `FLOW_STATE` resource registered) or
33
- * nothing to suspend to (the current step has no forward transition, or its destination has no
34
- * `module`) — the caller falls back to its own default landing (ordinarily `HOME`).
35
- */
3
+ import type { FlowModel } from '@owlmeans/flow';
4
+ import type { SuspendedLanding } from './types.js';
5
+ import type { FlowLandingHelper } from './landing/types.js';
6
+ export declare const makeFlowLandingHelper: <C extends ClientConfig, T extends ClientContext<C>>(context: T) => FlowLandingHelper;
7
+ export declare const flowLandingOf: <C extends ClientConfig, T extends ClientContext<C>>(target: T) => FlowLandingHelper;
8
+ /** @deprecated compat:factory-refactor — use `flowLandingOf(context).suspendFlow(…)` */
36
9
  export declare const suspendFlow: <C extends ClientConfig, T extends ClientContext<C>>(context: T, model: FlowModel, opts: {
37
10
  expiresAt: number;
38
11
  }) => Promise<boolean>;
39
- /**
40
- * Read back a suspended landing, once. Delete-on-read: the record answers exactly one sign-in,
41
- * because a landing a stale browser tab left behind must never resurrect on somebody else's
42
- * sign-in later in the same session.
43
- *
44
- * `null` covers every reason there is nothing to resume: no resource, no record, or a record
45
- * whose window has closed — the caller's own default landing is exactly as safe an answer.
46
- */
12
+ /** @deprecated compat:factory-refactor — use `flowLandingOf(context).resumeSuspendedFlow()` */
47
13
  export declare const resumeSuspendedFlow: <C extends ClientConfig, T extends ClientContext<C>>(context: T) => Promise<SuspendedLanding | null>;
48
14
  //# sourceMappingURL=landing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"landing.d.ts","sourceRoot":"","sources":["../src/landing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAG5D;iGACiG;AACjG,eAAO,MAAM,WAAW,gBAAgB,CAAA;AAExC,MAAM,WAAW,sBAAuB,SAAQ,cAAc;IAC5D,kEAAkE;IAClE,UAAU,EAAE,MAAM,CAAA;IAClB,oGAAoG;IACpG,KAAK,EAAE,WAAW,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,WAAW,CAAA;CACnB;AAOD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,WAAW,GAAU,CAAC,SAAS,YAAY,EAAE,CAAC,SAAS,aAAa,CAAC,CAAC,CAAC,WACzE,CAAC,SAAS,SAAS,QAAQ;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,KACxD,OAAO,CAAC,OAAO,CAkBjB,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,GAAU,CAAC,SAAS,YAAY,EAAE,CAAC,SAAS,aAAa,CAAC,CAAC,CAAC,WACjF,CAAC,KACT,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAgBjC,CAAA"}
1
+ {"version":3,"file":"landing.d.ts","sourceRoot":"","sources":["../src/landing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAG5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE/C,OAAO,KAAK,EAAE,gBAAgB,EAA0B,MAAM,YAAY,CAAA;AAC1E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAE3D,eAAO,MAAM,qBAAqB,GAAI,CAAC,SAAS,YAAY,EAAE,CAAC,SAAS,aAAa,CAAC,CAAC,CAAC,WAC7E,CAAC,KACT,iBAyDF,CAAA;AAED,eAAO,MAAM,aAAa,GA7DY,CAAC,SAAS,YAAY,EAAE,CAAC,SAAS,aAAa,CAAC,CAAC,CAAC,iCA6DlB,CAAA;AAEtE,wFAAwF;AACxF,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,YAAY,EAAE,CAAC,SAAS,aAAa,CAAC,CAAC,CAAC,WACnE,CAAC,SAAS,SAAS,QAAQ;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,KACxD,OAAO,CAAC,OAAO,CAAoD,CAAA;AAEtE,+FAA+F;AAC/F,eAAO,MAAM,mBAAmB,GAAI,CAAC,SAAS,YAAY,EAAE,CAAC,SAAS,aAAa,CAAC,CAAC,CAAC,WAC3E,CAAC,KACT,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAiD,CAAA"}
package/build/landing.js CHANGED
@@ -1,68 +1,61 @@
1
- import { FLOW_STATE } from './consts.js';
2
- /** The record id a suspended landing is stored under, in the SAME resource `EXTRA_FLOW` uses —
3
- * a side-band slot, never the live `FlowService.flow` and never the `?flow=` query parameter. */
4
- export const RESUME_FLOW = 'resume-flow';
5
- const landingResource = (context) => context.hasResource(FLOW_STATE) ? context.resource(FLOW_STATE) : null;
6
- /**
7
- * Suspend a flow that is about to leave for the platform's own sign-in dispatcher, so whichever
8
- * sign-in method completes can send the person back to where they started instead of `HOME`.
9
- *
10
- * Driven by the flow model rather than a raw URL: `model.next()` is the flow's own answer to
11
- * "where does this step lead", so a caller never re-derives a destination the flow already knows,
12
- * and a flow whose current step offers no way forward (`next()` throws) suspends nothing rather
13
- * than persisting a landing that can never be reached. The persisted record is deliberately NOT a
14
- * serialized flow token — the destination step is always one this application can enter fresh (an
15
- * `initial`-marked screen reading its own `ref`/`kind` from the query), so nothing needs to
16
- * reconstruct the exact `FlowModel` instance on the other side of a sign-in redirect, and this
17
- * helper stays usable by any flow, not only one particular package's.
18
- *
19
- * Returns `false` when there is nowhere to persist this (no `FLOW_STATE` resource registered) or
20
- * nothing to suspend to (the current step has no forward transition, or its destination has no
21
- * `module`) — the caller falls back to its own default landing (ordinarily `HOME`).
22
- */
23
- export const suspendFlow = async (context, model, opts) => {
24
- const resource = landingResource(context);
25
- if (resource == null)
26
- return false;
27
- let destinationModule;
28
- try {
29
- const transition = model.next();
30
- destinationModule = model.step(transition.step).module;
31
- }
32
- catch {
33
- return false;
34
- }
35
- if (destinationModule == null)
36
- return false;
37
- await resource.save({
38
- id: RESUME_FLOW, entrypoint: destinationModule, query: model.payload(), expiresAt: opts.expiresAt,
39
- });
40
- return true;
41
- };
42
- /**
43
- * Read back a suspended landing, once. Delete-on-read: the record answers exactly one sign-in,
44
- * because a landing a stale browser tab left behind must never resurrect on somebody else's
45
- * sign-in later in the same session.
46
- *
47
- * `null` covers every reason there is nothing to resume: no resource, no record, or a record
48
- * whose window has closed — the caller's own default landing is exactly as safe an answer.
49
- */
50
- export const resumeSuspendedFlow = async (context) => {
51
- const resource = landingResource(context);
52
- if (resource == null)
53
- return null;
54
- let record;
55
- try {
56
- record = await resource.load(RESUME_FLOW);
57
- }
58
- catch {
59
- return null;
60
- }
61
- if (record == null)
62
- return null;
63
- await resource.delete(RESUME_FLOW).catch(() => undefined);
64
- if (record.expiresAt < Date.now())
65
- return null;
66
- return { entrypoint: record.entrypoint, query: record.query };
1
+ import { memoHelper } from '@owlmeans/context';
2
+ import { FLOW_STATE, RESUME_FLOW } from './consts.js';
3
+ export const makeFlowLandingHelper = (context) => {
4
+ const landingResource = () => context.hasResource(FLOW_STATE) ? context.resource(FLOW_STATE) : null;
5
+ const suspendFlow = async (model, opts) => {
6
+ const resource = landingResource();
7
+ if (resource == null)
8
+ return false;
9
+ let destinationModule;
10
+ try {
11
+ const transition = model.next();
12
+ destinationModule = model.step(transition.step).module;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ if (destinationModule == null)
18
+ return false;
19
+ return await suspendLanding({ entrypoint: destinationModule, query: model.payload() }, opts);
20
+ };
21
+ const suspendLanding = async (landing, opts) => {
22
+ const resource = landingResource();
23
+ if (resource == null)
24
+ return false;
25
+ await resource.save({
26
+ id: RESUME_FLOW, entrypoint: landing.entrypoint, query: landing.query, expiresAt: opts.expiresAt,
27
+ });
28
+ return true;
29
+ };
30
+ const discardSuspendedLanding = async () => {
31
+ const resource = landingResource();
32
+ if (resource == null)
33
+ return;
34
+ await resource.delete(RESUME_FLOW).catch(() => undefined);
35
+ };
36
+ const resumeSuspendedFlow = async () => {
37
+ const resource = landingResource();
38
+ if (resource == null)
39
+ return null;
40
+ let record;
41
+ try {
42
+ record = await resource.load(RESUME_FLOW);
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ if (record == null)
48
+ return null;
49
+ await resource.delete(RESUME_FLOW).catch(() => undefined);
50
+ if (record.expiresAt < Date.now())
51
+ return null;
52
+ return { entrypoint: record.entrypoint, query: record.query };
53
+ };
54
+ return { suspendFlow, suspendLanding, discardSuspendedLanding, resumeSuspendedFlow };
67
55
  };
56
+ export const flowLandingOf = memoHelper.oncePer(makeFlowLandingHelper);
57
+ /** @deprecated compat:factory-refactor — use `flowLandingOf(context).suspendFlow(…)` */
58
+ export const suspendFlow = (context, model, opts) => flowLandingOf(context).suspendFlow(model, opts);
59
+ /** @deprecated compat:factory-refactor — use `flowLandingOf(context).resumeSuspendedFlow()` */
60
+ export const resumeSuspendedFlow = (context) => flowLandingOf(context).resumeSuspendedFlow();
68
61
  //# sourceMappingURL=landing.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"landing.js","sourceRoot":"","sources":["../src/landing.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC;iGACiG;AACjG,MAAM,CAAC,MAAM,WAAW,GAAG,aAAa,CAAA;AAexC,MAAM,eAAe,GAAG,CACtB,OAAU,EACqC,EAAE,CACjD,OAAO,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAyC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AAE/G;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,EAC9B,OAAU,EAAE,KAAgB,EAAE,IAA2B,EACvC,EAAE;IACpB,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,CAAC,CAAA;IACzC,IAAI,QAAQ,IAAI,IAAI;QAAE,OAAO,KAAK,CAAA;IAElC,IAAI,iBAAqC,CAAA;IACzC,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,EAAE,CAAA;QAC/B,iBAAiB,GAAG,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,MAAM,CAAA;IACxD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;IACD,IAAI,iBAAiB,IAAI,IAAI;QAAE,OAAO,KAAK,CAAA;IAE3C,MAAM,QAAQ,CAAC,IAAI,CAAC;QAClB,EAAE,EAAE,WAAW,EAAE,UAAU,EAAE,iBAAiB,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS;KAClG,CAAC,CAAA;IAEF,OAAO,IAAI,CAAA;AACb,CAAC,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,EACtC,OAAU,EACwB,EAAE;IACpC,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,CAAC,CAAA;IACzC,IAAI,QAAQ,IAAI,IAAI;QAAE,OAAO,IAAI,CAAA;IAEjC,IAAI,MAAqC,CAAA;IACzC,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;IACD,IAAI,MAAM,IAAI,IAAI;QAAE,OAAO,IAAI,CAAA;IAE/B,MAAM,QAAQ,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;IACzD,IAAI,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE;QAAE,OAAO,IAAI,CAAA;IAE9C,OAAO,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAA;AAC/D,CAAC,CAAA"}
1
+ {"version":3,"file":"landing.js","sourceRoot":"","sources":["../src/landing.ts"],"names":[],"mappings":"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"}
package/build/service.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { assertContext, createLazyService } from '@owlmeans/context';
2
2
  import { DEFAULT_ALIAS } from './consts.js';
3
- import { DEFAULT_ALIAS as CONFIG_RESOURCE, fromConfigRecord } from '@owlmeans/config';
3
+ import { DEFAULT_ALIAS as CONFIG_RESOURCE, configHelper } from '@owlmeans/config';
4
4
  import { FLOW_RECORD, FlowUnsupported, makeFlowModel, STD_AUTH_FLOW, UnknownFlow } from '@owlmeans/flow';
5
5
  export const makeBasicFlowService = (alias = DEFAULT_ALIAS) => {
6
6
  const location = `flow-service:${alias}`;
@@ -50,7 +50,7 @@ export const makeBasicFlowService = (alias = DEFAULT_ALIAS) => {
50
50
  // @TODO Put it into some configuration and make it more flexible
51
51
  const configRes = ctx.resource(CONFIG_RESOURCE);
52
52
  const flowConfigs = await configRes.list({ recordType: FLOW_RECORD });
53
- await Promise.all(flowConfigs.items.map(item => fromConfigRecord(item)).map(async (flow) => {
53
+ await Promise.all(flowConfigs.items.map(item => configHelper.fromConfigRecord(item)).map(async (flow) => {
54
54
  Object.values(flow.steps).forEach(step => {
55
55
  if (step.service.startsWith('$')) {
56
56
  const service = flowCfg.services?.[step.service.substring(1)];
@@ -1 +1 @@
1
- {"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
package/build/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { LazyService } from '@owlmeans/context';
2
- import type { FlowConfig, FlowModel, FlowProvider, FlowState, FlowTransition } from '@owlmeans/flow';
2
+ import type { FlowConfig, FlowModel, FlowProvider, FlowState, FlowTransition, FlowPayload } from '@owlmeans/flow';
3
3
  import type { AbstractRequest } from '@owlmeans/entrypoint';
4
4
  import type { ResourceRecord } from '@owlmeans/resource';
5
5
  import type { ResolvedServiceRoute } from '@owlmeans/route';
@@ -31,4 +31,15 @@ export interface StateRecord extends ResourceRecord, FlowState {
31
31
  }
32
32
  export interface StateResource extends ClientResource<StateRecord> {
33
33
  }
34
+ export interface SuspendedLandingRecord extends ResourceRecord {
35
+ /** The entrypoint alias to navigate to once sign-in completes. */
36
+ entrypoint: string;
37
+ /** The flow's payload at the moment it was suspended — carried along as the destination's query. */
38
+ query: FlowPayload;
39
+ expiresAt: number;
40
+ }
41
+ export interface SuspendedLanding {
42
+ entrypoint: string;
43
+ query: FlowPayload;
44
+ }
34
45
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACpG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAE/D,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC9C,QAAQ,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;IAE1B,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;IAEtB,KAAK,EAAE,MAAM,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAA;IAEtC,WAAW,EAAE,MAAM,WAAW,CAAA;IAE9B,MAAM,EAAE,MAAM,UAAU,CAAA;IAExB,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,CAAC,CAAA;IAE3D,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,CAAC,CAAA;IAE1C,WAAW,EAAE,YAAY,CAAA;IAEzB,OAAO,EAAE,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CAC/E;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAChC,MAAM,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAA;CAC7B;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAA;IACnE,KAAK,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,UAAU,CAAA;IACtC,IAAI,EAAE,MAAM,SAAS,CAAA;IACrB,OAAO,EAAE,MAAM,oBAAoB,CAAA;IACnC,OAAO,EAAE,CAAC,WAAW,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACvF,OAAO,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,WAAY,SAAQ,cAAc,EAAE,SAAS;CAAG;AAEjE,MAAM,WAAW,aAAc,SAAQ,cAAc,CAAC,WAAW,CAAC;CAAG"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACjH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAE/D,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC9C,QAAQ,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;IAE1B,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;IAEtB,KAAK,EAAE,MAAM,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAA;IAEtC,WAAW,EAAE,MAAM,WAAW,CAAA;IAE9B,MAAM,EAAE,MAAM,UAAU,CAAA;IAExB,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,CAAC,CAAA;IAE3D,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,CAAC,CAAA;IAE1C,WAAW,EAAE,YAAY,CAAA;IAEzB,OAAO,EAAE,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CAC/E;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAChC,MAAM,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAA;CAC7B;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAA;IACnE,KAAK,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,UAAU,CAAA;IACtC,IAAI,EAAE,MAAM,SAAS,CAAA;IACrB,OAAO,EAAE,MAAM,oBAAoB,CAAA;IACnC,OAAO,EAAE,CAAC,WAAW,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACvF,OAAO,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CAChC;AAED,MAAM,WAAW,WAAY,SAAQ,cAAc,EAAE,SAAS;CAAG;AAEjE,MAAM,WAAW,aAAc,SAAQ,cAAc,CAAC,WAAW,CAAC;CAAG;AAErE,MAAM,WAAW,sBAAuB,SAAQ,cAAc;IAC5D,kEAAkE;IAClE,UAAU,EAAE,MAAM,CAAA;IAClB,oGAAoG;IACpG,KAAK,EAAE,WAAW,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,WAAW,CAAA;CACnB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/client-flow",
3
- "version": "0.1.18-rc.52",
3
+ "version": "0.1.18-rc.54",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -22,26 +22,26 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "@owlmeans/auth-common": "^0.1.18-rc.45",
26
- "@owlmeans/client": "^0.1.18-rc.49",
27
- "@owlmeans/client-context": "^0.1.18-rc.46",
28
- "@owlmeans/client-entrypoint": "^0.1.18-rc.45",
29
- "@owlmeans/client-resource": "^0.1.18-rc.45",
30
- "@owlmeans/config": "^0.1.18-rc.42",
31
- "@owlmeans/context": "^0.1.18-rc.32",
32
- "@owlmeans/error": "^0.1.18-rc.36",
33
- "@owlmeans/flow": "^0.1.18-rc.43",
34
- "@owlmeans/entrypoint": "^0.1.18-rc.39",
35
- "@owlmeans/resource": "^0.1.18-rc.38",
36
- "@owlmeans/route": "^0.1.18-rc.33"
25
+ "@owlmeans/auth-common": "^0.1.18-rc.47",
26
+ "@owlmeans/client": "^0.1.18-rc.51",
27
+ "@owlmeans/client-context": "^0.1.18-rc.48",
28
+ "@owlmeans/client-entrypoint": "^0.1.18-rc.47",
29
+ "@owlmeans/client-resource": "^0.1.18-rc.47",
30
+ "@owlmeans/config": "^0.1.18-rc.44",
31
+ "@owlmeans/context": "^0.1.18-rc.34",
32
+ "@owlmeans/error": "^0.1.18-rc.38",
33
+ "@owlmeans/flow": "^0.1.18-rc.45",
34
+ "@owlmeans/entrypoint": "^0.1.18-rc.41",
35
+ "@owlmeans/resource": "^0.1.18-rc.40",
36
+ "@owlmeans/route": "^0.1.18-rc.35"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "react": "*"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@owlmeans/dep-config": "workspace:*",
43
- "@owlmeans/static-resource": "^0.1.18-rc.37",
44
- "@types/bun": "^1.4.0",
43
+ "@owlmeans/static-resource": "^0.1.18-rc.39",
44
+ "@types/bun": "^1.4.2",
45
45
  "nodemon": "^3.1.14",
46
46
  "typescript": "^7.0.2"
47
47
  },
package/src/consts.ts CHANGED
@@ -6,3 +6,7 @@ export const FLOW_STATE = 'state:flow'
6
6
  export const EXTRA_FLOW = 'extra-flow'
7
7
 
8
8
  export const REHACK_MOD = '__redirect'
9
+
10
+ /** The record id a suspended landing is stored under, in the SAME resource `EXTRA_FLOW` uses —
11
+ * a side-band slot, never the live `FlowService.flow` and never the `?flow=` query parameter. */
12
+ export const RESUME_FLOW = 'resume-flow'
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export type * from './types.js'
2
+ export type * from './landing/types.js'
2
3
  export * from './consts.js'
3
4
  export * from './client.js'
4
5
  export * from './service.js'
@@ -0,0 +1,52 @@
1
+ import type { FlowModel } from '@owlmeans/flow'
2
+ import type { SuspendedLanding } from '../types.js'
3
+
4
+ /** A flow suspended across the platform's sign-in, and the landing it is resumed at. */
5
+ export interface FlowLandingHelper {
6
+ /**
7
+ * Suspend a flow that is about to leave for the platform's own sign-in dispatcher, so whichever
8
+ * sign-in method completes can send the person back to where they started instead of `HOME`.
9
+ *
10
+ * Driven by the flow model rather than a raw URL: `model.next()` is the flow's own answer to
11
+ * "where does this step lead", so a caller never re-derives a destination the flow already knows,
12
+ * and a flow whose current step offers no way forward (`next()` throws) suspends nothing rather
13
+ * than persisting a landing that can never be reached. The persisted record is deliberately NOT a
14
+ * serialized flow token — the destination step is always one this application can enter fresh (an
15
+ * `initial`-marked screen reading its own `ref`/`kind` from the query), so nothing needs to
16
+ * reconstruct the exact `FlowModel` instance on the other side of a sign-in redirect, and this
17
+ * helper stays usable by any flow, not only one particular package's.
18
+ *
19
+ * Returns `false` when there is nowhere to persist this (no `FLOW_STATE` resource registered) or
20
+ * nothing to suspend to (the current step has no forward transition, or its destination has no
21
+ * `module`) — the caller falls back to its own default landing (ordinarily `HOME`).
22
+ */
23
+ suspendFlow: (model: FlowModel, opts: { expiresAt: number }) => Promise<boolean>
24
+ /**
25
+ * Suspend a landing whose destination is already known — an entrypoint alias, not a flow step —
26
+ * into the same side-band record {@link suspendFlow} writes and {@link resumeSuspendedFlow} reads.
27
+ *
28
+ * For a caller that has no flow to derive a destination from: a sign-in control aimed at one
29
+ * screen (`useLogin(target)` in `@owlmeans/client-auth`) parks that screen here and sends the person
30
+ * to the dispatcher, whose ordinary post-sign-in landing then resumes on it. One record, one reader:
31
+ * a landing parked this way and one parked by a flow are indistinguishable on the other side, and
32
+ * the later write replaces the earlier one.
33
+ *
34
+ * Returns `false` when there is nowhere to persist this (no `FLOW_STATE` resource registered).
35
+ */
36
+ suspendLanding: (landing: SuspendedLanding, opts: { expiresAt: number }) => Promise<boolean>
37
+ /**
38
+ * Drop a suspended landing without acting on it — for a sign-in that ended before it signed anyone
39
+ * in (a refused popup, a window the person closed), so the landing it parked cannot hijack the next,
40
+ * unrelated sign-in. Safe to call when nothing is suspended or nowhere to keep it exists.
41
+ */
42
+ discardSuspendedLanding: () => Promise<void>
43
+ /**
44
+ * Read back a suspended landing, once. Delete-on-read: the record answers exactly one sign-in,
45
+ * because a landing a stale browser tab left behind must never resurrect on somebody else's
46
+ * sign-in later in the same session.
47
+ *
48
+ * `null` covers every reason there is nothing to resume: no resource, no record, or a record
49
+ * whose window has closed — the caller's own default landing is exactly as safe an answer.
50
+ */
51
+ resumeSuspendedFlow: () => Promise<SuspendedLanding | null>
52
+ }
package/src/landing.ts CHANGED
@@ -1,95 +1,81 @@
1
1
  import type { ClientContext } from '@owlmeans/client'
2
2
  import type { ClientConfig } from '@owlmeans/client-context'
3
3
  import type { ClientResource } from '@owlmeans/client-resource'
4
- import type { ResourceRecord } from '@owlmeans/resource'
5
- import type { FlowModel, FlowPayload } from '@owlmeans/flow'
6
- import { FLOW_STATE } from './consts.js'
7
-
8
- /** The record id a suspended landing is stored under, in the SAME resource `EXTRA_FLOW` uses —
9
- * a side-band slot, never the live `FlowService.flow` and never the `?flow=` query parameter. */
10
- export const RESUME_FLOW = 'resume-flow'
11
-
12
- export interface SuspendedLandingRecord extends ResourceRecord {
13
- /** The entrypoint alias to navigate to once sign-in completes. */
14
- entrypoint: string
15
- /** The flow's payload at the moment it was suspended — carried along as the destination's query. */
16
- query: FlowPayload
17
- expiresAt: number
18
- }
19
-
20
- export interface SuspendedLanding {
21
- entrypoint: string
22
- query: FlowPayload
23
- }
4
+ import { memoHelper } from '@owlmeans/context'
5
+ import type { FlowModel } from '@owlmeans/flow'
6
+ import { FLOW_STATE, RESUME_FLOW } from './consts.js'
7
+ import type { SuspendedLanding, SuspendedLandingRecord } from './types.js'
8
+ import type { FlowLandingHelper } from './landing/types.js'
24
9
 
25
- const landingResource = <C extends ClientConfig, T extends ClientContext<C>>(
10
+ export const makeFlowLandingHelper = <C extends ClientConfig, T extends ClientContext<C>>(
26
11
  context: T
27
- ): ClientResource<SuspendedLandingRecord> | null =>
28
- context.hasResource(FLOW_STATE) ? context.resource<ClientResource<SuspendedLandingRecord>>(FLOW_STATE) : null
29
-
30
- /**
31
- * Suspend a flow that is about to leave for the platform's own sign-in dispatcher, so whichever
32
- * sign-in method completes can send the person back to where they started instead of `HOME`.
33
- *
34
- * Driven by the flow model rather than a raw URL: `model.next()` is the flow's own answer to
35
- * "where does this step lead", so a caller never re-derives a destination the flow already knows,
36
- * and a flow whose current step offers no way forward (`next()` throws) suspends nothing rather
37
- * than persisting a landing that can never be reached. The persisted record is deliberately NOT a
38
- * serialized flow token — the destination step is always one this application can enter fresh (an
39
- * `initial`-marked screen reading its own `ref`/`kind` from the query), so nothing needs to
40
- * reconstruct the exact `FlowModel` instance on the other side of a sign-in redirect, and this
41
- * helper stays usable by any flow, not only one particular package's.
42
- *
43
- * Returns `false` when there is nowhere to persist this (no `FLOW_STATE` resource registered) or
44
- * nothing to suspend to (the current step has no forward transition, or its destination has no
45
- * `module`) — the caller falls back to its own default landing (ordinarily `HOME`).
46
- */
47
- export const suspendFlow = async <C extends ClientConfig, T extends ClientContext<C>>(
48
- context: T, model: FlowModel, opts: { expiresAt: number }
49
- ): Promise<boolean> => {
50
- const resource = landingResource(context)
51
- if (resource == null) return false
52
-
53
- let destinationModule: string | undefined
54
- try {
55
- const transition = model.next()
56
- destinationModule = model.step(transition.step).module
57
- } catch {
58
- return false
12
+ ): FlowLandingHelper => {
13
+ const landingResource = (): ClientResource<SuspendedLandingRecord> | null =>
14
+ context.hasResource(FLOW_STATE) ? context.resource<ClientResource<SuspendedLandingRecord>>(FLOW_STATE) : null
15
+
16
+ const suspendFlow = async (model: FlowModel, opts: { expiresAt: number }): Promise<boolean> => {
17
+ const resource = landingResource()
18
+ if (resource == null) return false
19
+
20
+ let destinationModule: string | undefined
21
+ try {
22
+ const transition = model.next()
23
+ destinationModule = model.step(transition.step).module
24
+ } catch {
25
+ return false
26
+ }
27
+ if (destinationModule == null) return false
28
+
29
+ return await suspendLanding({ entrypoint: destinationModule, query: model.payload() }, opts)
59
30
  }
60
- if (destinationModule == null) return false
61
31
 
62
- await resource.save({
63
- id: RESUME_FLOW, entrypoint: destinationModule, query: model.payload(), expiresAt: opts.expiresAt,
64
- })
32
+ const suspendLanding = async (landing: SuspendedLanding, opts: { expiresAt: number }): Promise<boolean> => {
33
+ const resource = landingResource()
34
+ if (resource == null) return false
65
35
 
66
- return true
67
- }
36
+ await resource.save({
37
+ id: RESUME_FLOW, entrypoint: landing.entrypoint, query: landing.query, expiresAt: opts.expiresAt,
38
+ })
68
39
 
69
- /**
70
- * Read back a suspended landing, once. Delete-on-read: the record answers exactly one sign-in,
71
- * because a landing a stale browser tab left behind must never resurrect on somebody else's
72
- * sign-in later in the same session.
73
- *
74
- * `null` covers every reason there is nothing to resume: no resource, no record, or a record
75
- * whose window has closed — the caller's own default landing is exactly as safe an answer.
76
- */
77
- export const resumeSuspendedFlow = async <C extends ClientConfig, T extends ClientContext<C>>(
78
- context: T
79
- ): Promise<SuspendedLanding | null> => {
80
- const resource = landingResource(context)
81
- if (resource == null) return null
82
-
83
- let record: SuspendedLandingRecord | null
84
- try {
85
- record = await resource.load(RESUME_FLOW)
86
- } catch {
87
- return null
40
+ return true
88
41
  }
89
- if (record == null) return null
90
42
 
91
- await resource.delete(RESUME_FLOW).catch(() => undefined)
92
- if (record.expiresAt < Date.now()) return null
43
+ const discardSuspendedLanding = async (): Promise<void> => {
44
+ const resource = landingResource()
45
+ if (resource == null) return
46
+
47
+ await resource.delete(RESUME_FLOW).catch(() => undefined)
48
+ }
93
49
 
94
- return { entrypoint: record.entrypoint, query: record.query }
50
+ const resumeSuspendedFlow = async (): Promise<SuspendedLanding | null> => {
51
+ const resource = landingResource()
52
+ if (resource == null) return null
53
+
54
+ let record: SuspendedLandingRecord | null
55
+ try {
56
+ record = await resource.load(RESUME_FLOW)
57
+ } catch {
58
+ return null
59
+ }
60
+ if (record == null) return null
61
+
62
+ await resource.delete(RESUME_FLOW).catch(() => undefined)
63
+ if (record.expiresAt < Date.now()) return null
64
+
65
+ return { entrypoint: record.entrypoint, query: record.query }
66
+ }
67
+
68
+ return { suspendFlow, suspendLanding, discardSuspendedLanding, resumeSuspendedFlow }
95
69
  }
70
+
71
+ export const flowLandingOf = memoHelper.oncePer(makeFlowLandingHelper)
72
+
73
+ /** @deprecated compat:factory-refactor — use `flowLandingOf(context).suspendFlow(…)` */
74
+ export const suspendFlow = <C extends ClientConfig, T extends ClientContext<C>>(
75
+ context: T, model: FlowModel, opts: { expiresAt: number }
76
+ ): Promise<boolean> => flowLandingOf(context).suspendFlow(model, opts)
77
+
78
+ /** @deprecated compat:factory-refactor — use `flowLandingOf(context).resumeSuspendedFlow()` */
79
+ export const resumeSuspendedFlow = <C extends ClientConfig, T extends ClientContext<C>>(
80
+ context: T
81
+ ): Promise<SuspendedLanding | null> => flowLandingOf(context).resumeSuspendedFlow()
package/src/service.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { assertContext, createLazyService } from '@owlmeans/context'
2
2
  import type { ConfigRecord } from '@owlmeans/context'
3
3
  import { DEFAULT_ALIAS } from './consts.js'
4
- import { DEFAULT_ALIAS as CONFIG_RESOURCE, fromConfigRecord } from '@owlmeans/config'
4
+ import { DEFAULT_ALIAS as CONFIG_RESOURCE, configHelper } from '@owlmeans/config'
5
5
  import type { ConfigResource } from '@owlmeans/config'
6
6
  import type { FlowService, ResolvePair } from './types.js'
7
7
  import type { Flow, FlowConfigRecord, WithFlowConfig } from '@owlmeans/flow'
@@ -72,7 +72,7 @@ export const makeBasicFlowService = (alias: string = DEFAULT_ALIAS): FlowService
72
72
  const configRes = ctx.resource<ConfigResource>(CONFIG_RESOURCE)
73
73
  const flowConfigs = await configRes.list({ recordType: FLOW_RECORD })
74
74
  await Promise.all(flowConfigs.items.map(
75
- item => fromConfigRecord<ConfigRecord, FlowConfigRecord>(item)
75
+ item => configHelper.fromConfigRecord<ConfigRecord, FlowConfigRecord>(item)
76
76
  ).map(async flow => {
77
77
  Object.values(flow.steps).forEach(step => {
78
78
  if (step.service.startsWith('$')) {
package/src/types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  import type { LazyService } from '@owlmeans/context'
3
- import type { FlowConfig, FlowModel, FlowProvider, FlowState, FlowTransition } from '@owlmeans/flow'
3
+ import type { FlowConfig, FlowModel, FlowProvider, FlowState, FlowTransition, FlowPayload } from '@owlmeans/flow'
4
4
  import type { AbstractRequest } from '@owlmeans/entrypoint'
5
5
  import type { ResourceRecord } from '@owlmeans/resource'
6
6
  import type { ResolvedServiceRoute } from '@owlmeans/route'
@@ -43,3 +43,16 @@ export interface FlowClient {
43
43
  export interface StateRecord extends ResourceRecord, FlowState {}
44
44
 
45
45
  export interface StateResource extends ClientResource<StateRecord> {}
46
+
47
+ export interface SuspendedLandingRecord extends ResourceRecord {
48
+ /** The entrypoint alias to navigate to once sign-in completes. */
49
+ entrypoint: string
50
+ /** The flow's payload at the moment it was suspended — carried along as the destination's query. */
51
+ query: FlowPayload
52
+ expiresAt: number
53
+ }
54
+
55
+ export interface SuspendedLanding {
56
+ entrypoint: string
57
+ query: FlowPayload
58
+ }
package/tests/context.ts CHANGED
@@ -1,10 +1,9 @@
1
- import { AppType, makeBasicContext } from '@owlmeans/context'
2
- import type { BasicConfig, BasicContext } from '@owlmeans/context'
1
+ import { AppType, makeBasicContext, type BasicConfig, type BasicContext } from '@owlmeans/context'
3
2
  import { createStaticResource } from '@owlmeans/static-resource'
4
3
  import type { ClientContext } from '@owlmeans/client'
5
4
  import type { ClientConfig } from '@owlmeans/client-context'
6
5
  import { FLOW_STATE } from '../src/consts.js'
7
- import type { SuspendedLandingRecord } from '../src/landing.js'
6
+ import type { SuspendedLandingRecord } from '../src/types.js'
8
7
 
9
8
  /**
10
9
  * The smallest context `suspendFlow`/`resumeSuspendedFlow` need: a `FLOW_STATE` resource and
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import { makeFlowModel } from '@owlmeans/flow'
3
3
  import type { ShallowFlow } from '@owlmeans/flow'
4
- import { resumeSuspendedFlow, suspendFlow } from '../src/landing.js'
4
+ import { flowLandingOf } from '../src/landing.js'
5
5
  import { makeTestContext } from './context.js'
6
6
 
7
7
  const testFlow: ShallowFlow = {
@@ -29,27 +29,27 @@ describe('suspendFlow / resumeSuspendedFlow', () => {
29
29
  const model = await makeFlowModel(testFlow)
30
30
  model.updatePayload({ ref: 'abc-123' })
31
31
 
32
- expect(await suspendFlow(context, model, { expiresAt: Date.now() + 60_000 })).toBe(true)
32
+ expect(await flowLandingOf(context).suspendFlow(model, { expiresAt: Date.now() + 60_000 })).toBe(true)
33
33
 
34
- const landing = await resumeSuspendedFlow(context)
34
+ const landing = await flowLandingOf(context).resumeSuspendedFlow()
35
35
  expect(landing).toEqual({ entrypoint: 'target-entrypoint', query: { ref: 'abc-123' } })
36
36
  })
37
37
 
38
38
  test('resuming is single-use — a second call finds nothing', async () => {
39
39
  const context = makeTestContext()
40
40
  const model = await makeFlowModel(testFlow)
41
- await suspendFlow(context, model, { expiresAt: Date.now() + 60_000 })
41
+ await flowLandingOf(context).suspendFlow(model, { expiresAt: Date.now() + 60_000 })
42
42
 
43
- await resumeSuspendedFlow(context)
44
- expect(await resumeSuspendedFlow(context)).toBeNull()
43
+ await flowLandingOf(context).resumeSuspendedFlow()
44
+ expect(await flowLandingOf(context).resumeSuspendedFlow()).toBeNull()
45
45
  })
46
46
 
47
47
  test('an expired landing resumes to nothing', async () => {
48
48
  const context = makeTestContext()
49
49
  const model = await makeFlowModel(testFlow)
50
- await suspendFlow(context, model, { expiresAt: Date.now() - 1 })
50
+ await flowLandingOf(context).suspendFlow(model, { expiresAt: Date.now() - 1 })
51
51
 
52
- expect(await resumeSuspendedFlow(context)).toBeNull()
52
+ expect(await flowLandingOf(context).resumeSuspendedFlow()).toBeNull()
53
53
  })
54
54
 
55
55
  test('a step with no forward transition suspends nothing', async () => {
@@ -59,15 +59,50 @@ describe('suspendFlow / resumeSuspendedFlow', () => {
59
59
  // has nothing to offer and suspending must decline rather than persist a landing to nowhere.
60
60
  deadModel.setState({ ...deadModel.state(), step: 'dead' })
61
61
 
62
- expect(await suspendFlow(context, deadModel, { expiresAt: Date.now() + 60_000 })).toBe(false)
63
- expect(await resumeSuspendedFlow(context)).toBeNull()
62
+ expect(await flowLandingOf(context).suspendFlow(deadModel, { expiresAt: Date.now() + 60_000 })).toBe(false)
63
+ expect(await flowLandingOf(context).resumeSuspendedFlow()).toBeNull()
64
64
  })
65
65
 
66
66
  test('with no FLOW_STATE resource registered, both are safe no-ops', async () => {
67
67
  const context = makeTestContext(false)
68
68
  const model = await makeFlowModel(testFlow)
69
69
 
70
- expect(await suspendFlow(context, model, { expiresAt: Date.now() + 60_000 })).toBe(false)
71
- expect(await resumeSuspendedFlow(context)).toBeNull()
70
+ expect(await flowLandingOf(context).suspendFlow(model, { expiresAt: Date.now() + 60_000 })).toBe(false)
71
+ expect(await flowLandingOf(context).resumeSuspendedFlow()).toBeNull()
72
+ })
73
+ })
74
+
75
+ describe('suspendLanding / discardSuspendedLanding', () => {
76
+ test('parks a known destination in the record resumeSuspendedFlow reads, once', async () => {
77
+ const context = makeTestContext()
78
+
79
+ expect(await flowLandingOf(context).suspendLanding({ entrypoint: 'guarded-screen', query: {} }, { expiresAt: Date.now() + 60_000 })).toBe(true)
80
+
81
+ expect(await flowLandingOf(context).resumeSuspendedFlow()).toEqual({ entrypoint: 'guarded-screen', query: {} })
82
+ expect(await flowLandingOf(context).resumeSuspendedFlow()).toBeNull()
83
+ })
84
+
85
+ test('the later write replaces a landing a flow suspended earlier', async () => {
86
+ const context = makeTestContext()
87
+ await flowLandingOf(context).suspendFlow(await makeFlowModel(testFlow), { expiresAt: Date.now() + 60_000 })
88
+ await flowLandingOf(context).suspendLanding({ entrypoint: 'guarded-screen', query: {} }, { expiresAt: Date.now() + 60_000 })
89
+
90
+ expect(await flowLandingOf(context).resumeSuspendedFlow()).toEqual({ entrypoint: 'guarded-screen', query: {} })
91
+ })
92
+
93
+ test('a discarded landing resumes to nothing', async () => {
94
+ const context = makeTestContext()
95
+ await flowLandingOf(context).suspendLanding({ entrypoint: 'guarded-screen', query: {} }, { expiresAt: Date.now() + 60_000 })
96
+ await flowLandingOf(context).discardSuspendedLanding()
97
+
98
+ expect(await flowLandingOf(context).resumeSuspendedFlow()).toBeNull()
99
+ })
100
+
101
+ test('with no FLOW_STATE resource registered, both are safe no-ops', async () => {
102
+ const context = makeTestContext(false)
103
+
104
+ expect(await flowLandingOf(context).suspendLanding({ entrypoint: 'guarded-screen', query: {} }, { expiresAt: Date.now() + 60_000 })).toBe(false)
105
+ await flowLandingOf(context).discardSuspendedLanding()
106
+ expect(await flowLandingOf(context).resumeSuspendedFlow()).toBeNull()
72
107
  })
73
108
  })