@bravely-studios/account-web 0.3.4

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 (59) hide show
  1. package/LICENSE +10 -0
  2. package/README.md +262 -0
  3. package/dist/ActivationStateMachine.d.ts +50 -0
  4. package/dist/ActivationStateMachine.d.ts.map +1 -0
  5. package/dist/ActivationStateMachine.js +141 -0
  6. package/dist/ActivationStateMachine.js.map +1 -0
  7. package/dist/BravelyAccountManager.d.ts +156 -0
  8. package/dist/BravelyAccountManager.d.ts.map +1 -0
  9. package/dist/BravelyAccountManager.js +621 -0
  10. package/dist/BravelyAccountManager.js.map +1 -0
  11. package/dist/EntitlementCache.d.ts +50 -0
  12. package/dist/EntitlementCache.d.ts.map +1 -0
  13. package/dist/EntitlementCache.js +116 -0
  14. package/dist/EntitlementCache.js.map +1 -0
  15. package/dist/components/ActivationLadder.d.ts +78 -0
  16. package/dist/components/ActivationLadder.d.ts.map +1 -0
  17. package/dist/components/ActivationLadder.js +145 -0
  18. package/dist/components/ActivationLadder.js.map +1 -0
  19. package/dist/components/CrossAppCard.d.ts +48 -0
  20. package/dist/components/CrossAppCard.d.ts.map +1 -0
  21. package/dist/components/CrossAppCard.js +45 -0
  22. package/dist/components/CrossAppCard.js.map +1 -0
  23. package/dist/deprecation.d.ts +19 -0
  24. package/dist/deprecation.d.ts.map +1 -0
  25. package/dist/deprecation.js +83 -0
  26. package/dist/deprecation.js.map +1 -0
  27. package/dist/displayName.d.ts +15 -0
  28. package/dist/displayName.d.ts.map +1 -0
  29. package/dist/displayName.js +41 -0
  30. package/dist/displayName.js.map +1 -0
  31. package/dist/dpop.d.ts +30 -0
  32. package/dist/dpop.d.ts.map +1 -0
  33. package/dist/dpop.js +87 -0
  34. package/dist/dpop.js.map +1 -0
  35. package/dist/hooks/useActivationLaneFromUrl.d.ts +54 -0
  36. package/dist/hooks/useActivationLaneFromUrl.d.ts.map +1 -0
  37. package/dist/hooks/useActivationLaneFromUrl.js +105 -0
  38. package/dist/hooks/useActivationLaneFromUrl.js.map +1 -0
  39. package/dist/hooks/useFreshLaunchRestoration.d.ts +62 -0
  40. package/dist/hooks/useFreshLaunchRestoration.d.ts.map +1 -0
  41. package/dist/hooks/useFreshLaunchRestoration.js +135 -0
  42. package/dist/hooks/useFreshLaunchRestoration.js.map +1 -0
  43. package/dist/index.d.ts +16 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +15 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/oauth.d.ts +50 -0
  48. package/dist/oauth.d.ts.map +1 -0
  49. package/dist/oauth.js +107 -0
  50. package/dist/oauth.js.map +1 -0
  51. package/dist/storage.d.ts +48 -0
  52. package/dist/storage.d.ts.map +1 -0
  53. package/dist/storage.js +153 -0
  54. package/dist/storage.js.map +1 -0
  55. package/dist/types.d.ts +172 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/types.js +7 -0
  58. package/dist/types.js.map +1 -0
  59. package/package.json +61 -0
package/LICENSE ADDED
@@ -0,0 +1,10 @@
1
+ Copyright (c) 2026 Bravely Studios LLC. All rights reserved.
2
+
3
+ This software is the proprietary work of Bravely Studios LLC. No part of this
4
+ package may be reproduced, distributed, transmitted, modified, or otherwise
5
+ exploited in any form or by any means without the prior written permission of
6
+ Bravely Studios LLC.
7
+
8
+ Unauthorized use, copying, or distribution is strictly prohibited.
9
+
10
+ For licensing inquiries, contact jeff@bravely.dev.
package/README.md ADDED
@@ -0,0 +1,262 @@
1
+ # @bravely-studios/account-web
2
+
3
+ Thin TypeScript facade over the Bravely identity API for browser-based
4
+ Bravely Studios apps. Used internally across the Bravely web app family.
5
+
6
+ ## What it does
7
+
8
+ - **OAuth 2.1 + PKCE** sign-in via `auth.bravely.dev` — RFC 7636 S256.
9
+ - **BAS lifecycle** — exchange / refresh against `identity.bravely.dev`.
10
+ - **Entitlement cache** — 72h offline fallback.
11
+ - **Activation state machine** — checkout-to-active flow.
12
+ - **Paddle account actions** — BAS-authed checkout session and
13
+ customer-portal session helpers through `identity.bravely.dev`.
14
+ - **`Bravely-Deprecation` handling** — soft warnings + hard
15
+ `BravelyClientKilledError` on HTTP 426 kill-switch.
16
+ - **DPoP-ready** — RFC 9449 proof generation.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install @bravely-studios/account-web
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ```ts
27
+ import { BravelyAccountManager } from "@bravely-studios/account-web";
28
+
29
+ const manager = new BravelyAccountManager({
30
+ authority: "https://auth.bravely.dev",
31
+ appSlug: "diskaroo",
32
+ clientVersion: "1.3.1",
33
+ });
34
+
35
+ // On page load
36
+ await manager.restore();
37
+ manager.onStateChange((state) => {
38
+ if (state.kind === "signed_in") renderApp(state);
39
+ });
40
+
41
+ // On a sign-in button click
42
+ await manager.signIn();
43
+
44
+ // React 19? Subscribe via useSyncExternalStore — `getState` returns a stable
45
+ // reference between updates (0.2.1+), so no `_cachedState` workaround needed.
46
+ //
47
+ // const state = useSyncExternalStore(
48
+ // manager.onStateChange.bind(manager),
49
+ // manager.getState.bind(manager),
50
+ // );
51
+
52
+ // Entitlement gate
53
+ if (await manager.hasEntitlement("diskaroo_pro")) {
54
+ showProFeatures();
55
+ }
56
+
57
+ // Paid upgrade
58
+ await manager.openCheckout("annual");
59
+
60
+ // Manage subscription
61
+ const portal = await manager.createPaddlePortalSession();
62
+ window.open(portal.url, "_blank");
63
+ ```
64
+
65
+ ## Module map
66
+
67
+ | File | Responsibility |
68
+ |-----------------------------------------|-----------------------------------------------------------------------------------|
69
+ | `BravelyAccountManager.ts` | Public facade. Sign-in, sign-out, entitlements, checkout, portal, activation. |
70
+ | `EntitlementCache.ts` | 72h offline cache with TTL + invalidation. |
71
+ | `ActivationStateMachine.ts` | Port of the canonical activation machine. |
72
+ | `oauth.ts` | PKCE S256 helpers + authorize URL builder. |
73
+ | `storage.ts` | sessionStorage / IndexedDB / memory adapters. |
74
+ | `dpop.ts` | WebCrypto ES256 keypair + RFC 9449 proofs (Gate 2-ready). |
75
+ | `deprecation.ts` | `Bravely-Deprecation` header parser + error classes. |
76
+ | `types.ts` | TS types mirroring the OpenAPI 3.1 schemas. |
77
+ | `displayName.ts` | Slug → display-name lookup (`diskaroo` → `Diskaroo`). |
78
+ | `components/ActivationLadder.tsx` | **M3** — post-checkout 4-phase ladder (`Activating <App> Pro…`). |
79
+ | `components/CrossAppCard.tsx` | **M4** — third-quadrant card (`You own N Bravely Pro apps on this account.`). |
80
+ | `hooks/useActivationLaneFromUrl.ts` | **M3** — detect `?upgraded` / `?checkout` / `?subscription` return params. |
81
+ | `hooks/useFreshLaunchRestoration.ts` | **M2** — silent rehydrate + brief `Synced N items from your <device>.` toast. |
82
+
83
+ ## C-ux M2/M3/M4 exports (0.2.0)
84
+
85
+ Wave A of the C-ux M2-M4 rollout (`cux-m2-m4-rollout-plan.md`). New exports
86
+ let the four D-Web variants — `prodjectly`, `scry-web`, `printscreenly-web`,
87
+ `todoingly-web` — mount the foundational surfaces in Wave B-D.
88
+
89
+ ### `<ActivationLadder>` (M3)
90
+
91
+ ```tsx
92
+ import { ActivationLadder } from "@bravely-studios/account-web";
93
+
94
+ <ActivationLadder
95
+ state={manager.getActivationState()}
96
+ appSlug="diskaroo"
97
+ orderId={paddleOrderId ?? null}
98
+ onRetry={() => manager.pollForActivation()}
99
+ onContactSupport={() => window.open("mailto:jeff@bravely.dev")}
100
+ />
101
+ ```
102
+
103
+ Renders nothing unless the manager is in `post_checkout_activation`.
104
+ Auto-ticks elapsed every second; rolls through the 4 locked phases at
105
+ 0/15s/60s/120s. Phase copy is byte-identical to
106
+ `bravely-commerce-router/docs/activation-state-machine.json` — the
107
+ drift-test in `__tests__/ActivationLadder.test.tsx` enforces it.
108
+
109
+ ### `<CrossAppCard>` (M4)
110
+
111
+ ```tsx
112
+ import { CrossAppCard } from "@bravely-studios/account-web";
113
+
114
+ <CrossAppCard
115
+ entitlements={state.entitlements}
116
+ currentAppSlug="diskaroo"
117
+ variant="card" // or "footer-chip"
118
+ dismissible={false} // journey-doc default = persistent
119
+ />
120
+ ```
121
+
122
+ Renders nothing when the user has zero cross-app entitlements. Excludes
123
+ the current app's own `<slug>_pro` from the count; treats
124
+ `bravely_premium` as a single bundle token.
125
+
126
+ ### `useActivationLaneFromUrl()` (M3)
127
+
128
+ ```tsx
129
+ const { inActivationLane, source, clearUrlParam } = useActivationLaneFromUrl({
130
+ manager,
131
+ autoStartPolling: true,
132
+ });
133
+
134
+ useEffect(() => {
135
+ if (inActivationLane) clearUrlParam();
136
+ }, [inActivationLane]);
137
+ ```
138
+
139
+ Detects the three observed post-checkout return URL patterns:
140
+ `?upgraded=true` (prodjectly), `?checkout=complete` (scry-web),
141
+ `?subscription=success` (todoingly-web). Auto-calls
142
+ `manager.notifyCheckoutCompleted()` and, if `autoStartPolling`, kicks
143
+ off `manager.pollForActivation()`.
144
+
145
+ ### `useFreshLaunchRestoration()` (M2)
146
+
147
+ ```tsx
148
+ const fresh = useFreshLaunchRestoration({
149
+ manager,
150
+ itemsLabel: "tasks",
151
+ resolveOtherDeviceName: () => null, // Gate 1 fallback
152
+ });
153
+
154
+ useEffect(() => {
155
+ fresh.setSyncedCount(myCollection.length);
156
+ }, [myCollection.length]);
157
+
158
+ return fresh.shouldShowToast ? <Toast>{fresh.toastText}</Toast> : null;
159
+ ```
160
+
161
+ First-launch detector. UI is silent for 3 s after sign-in; then `Synced
162
+ N items from your <device>.` shows for the host page to dismiss. The
163
+ banned phrase family (`Welcome back. Restoring your Pro features`) is
164
+ absent by design. Gate 1 device-name resolver is null; the hook drops
165
+ the `from your <device>` anchor automatically.
166
+
167
+ ### Manager additions
168
+
169
+ ```ts
170
+ // Cross-app filter — excludes <currentApp>_pro, includes bravely_premium.
171
+ const others = manager.crossAppEntitlements();
172
+
173
+ // Post-checkout polling runner — 30 retries × 1s..8s capped backoff.
174
+ const result = await manager.pollForActivation();
175
+ // result.outcome: "active" | "exhausted" | "timeout" | "not_signed_in"
176
+
177
+ // Paddle customer-portal session — callers decide how to open the URL.
178
+ const portal = await manager.createPaddlePortalSession();
179
+ // portal.url is the hosted customer-portal URL.
180
+ ```
181
+
182
+ ## Storage adapters
183
+
184
+ - **`sessionStorage`** — BAS, PKCE verifier, state. Wipes on tab close.
185
+ - **`IndexedDB`** — refresh_token, entitlement cache, DPoP key handle.
186
+ Survives reload; key bytes never leave the browser (extractable=false).
187
+ - **In-memory** — SSR / test fallback.
188
+
189
+ Host pages can swap in their own ServiceWorker-backed adapter by passing
190
+ `storage` into the manager config.
191
+
192
+ ## Activation state machine
193
+
194
+ `getActivationState()` returns the current state from the canonical machine.
195
+ Host pages render UI off the `name` (`restoring_session`, `verifying_entitlement`,
196
+ `entitlement_cached_valid`, `post_checkout_activation`, etc.) and the
197
+ `busy` flag (whether to show a spinner). CLAUDE.md hard rule
198
+ `feedback_no_etas`: never render a predicted ETA — always elapsed time.
199
+
200
+ ## DPoP gate transition
201
+
202
+ - **Gate 1 (today):** BAS-authed manager requests keep
203
+ `Authorization: Bearer <bas>` for router compatibility and also attach a
204
+ valid `DPoP` proof header with `ath`. The server runs in `off` mode and
205
+ accepts the Bearer scheme without verifying the proof.
206
+ - **Gate 2 (next):** server enforcement can start from real client traffic
207
+ because `getAppDataToken()`, `openCheckout()`,
208
+ `createPaddlePortalSession()`, entitlement refreshes, and activation polls
209
+ already carry proof headers.
210
+
211
+ ## Changelog
212
+
213
+ ### 0.3.4 — 2026-05-13
214
+
215
+ - **Infra:** package now publishes to `registry.npmjs.org` (public scope).
216
+ Previously hosted on GitHub Packages. No code changes; behavior is
217
+ identical to `0.3.3`.
218
+
219
+ ### 0.3.3 — 2026-05-13
220
+
221
+ - **Feature:** `BravelyAccountManager` enters `fresh_launch_restoration`
222
+ when a BAS exists but the entitlement cache is missing, smoothing the
223
+ cold-start UX before the first entitlement read completes.
224
+
225
+ ### 0.3.2 — 2026-05-13
226
+
227
+ - **Fix:** BAS-authenticated manager requests now attach an RFC 9449 `DPoP`
228
+ proof header, including `ath`, while preserving the Gate 1
229
+ `Authorization: Bearer <bas>` scheme required by current router endpoints.
230
+ - **Fix:** fresh entitlement cache writes carry the generated DPoP JKT
231
+ thumbprint so Gate 2 cache binding can inspect the local key identity.
232
+
233
+ ### 0.3.0 — 2026-05-13
234
+
235
+ - **Feature:** added `createPaddlePortalSession()`, a BAS-authed manager
236
+ primitive for `POST /api/paddle-portal`. It returns the hosted Paddle
237
+ customer-portal URL DTO and leaves checkout activation behavior unchanged.
238
+ - **Types:** exported `PaddlePortalSession` and aligned the API baseline note
239
+ to `bravely-commerce-router` OpenAPI `1.2.1`.
240
+
241
+ ### 0.2.1 — 2026-05-12
242
+
243
+ - **Fix:** `getState()` now returns a **stable reference** between writes.
244
+ Previously it cloned on every read, which broke React 19's
245
+ `useSyncExternalStore` (snapshot identity changed every render → React
246
+ error #185 / "Maximum update depth exceeded"). The clone now happens
247
+ once, inside `setState()`, before listeners fire. Listener payloads and
248
+ the next `getState()` call return the same object reference. Three
249
+ consumers (`prodjectly`, `printscreenly-web`, `todoingly-web`) carry
250
+ module-level `_cachedState` workarounds that become redundant with this
251
+ release — they can be dropped in a follow-up sweep.
252
+ - **Fix:** internal `libVersion` default aligned to package version
253
+ (was hard-coded to `"0.2.0"`).
254
+ - **Docs:** install snippet uses the correct `@bravely-studios` scope.
255
+
256
+ ### 0.2.0 — 2026-05-11
257
+
258
+ - Initial C-ux M2/M3/M4 facade shipped.
259
+
260
+ ## License
261
+
262
+ Proprietary. (c) 2026 Bravely Studios LLC.
@@ -0,0 +1,50 @@
1
+ import type { ActivationState, ActivationStateName } from "./types.js";
2
+ /** Canonical event names accepted by `transition()`. */
3
+ export type ActivationEvent = "app_launched" | "app_launched_no_session" | "bas_found" | "bas_missing" | "bas_invalid" | "entitlement_check_success_pro" | "entitlement_check_success_none" | "entitlement_check_failure" | "checkout_completed" | "activation_polled_active" | "activation_polled_inactive" | "activation_timed_out" | "user_signed_in" | "user_skipped" | "user_clicked_retry";
4
+ interface TransitionContext {
5
+ bas_present?: boolean;
6
+ entitlement_cache_present?: boolean;
7
+ }
8
+ type ConditionalTarget = {
9
+ when: TransitionContext;
10
+ to: ActivationStateName;
11
+ else?: ActivationStateName;
12
+ };
13
+ type TransitionTarget = ActivationStateName | ConditionalTarget;
14
+ /**
15
+ * The retry budget for `post_checkout_activation` — matches the JSON spec.
16
+ * Host pages use this to schedule entitlement polling.
17
+ */
18
+ export declare const POST_CHECKOUT_RETRY: {
19
+ readonly maxRetries: 30;
20
+ readonly initialBackoffMs: 1000;
21
+ readonly maxBackoffMs: 8000;
22
+ readonly backoffMultiplier: 1.5;
23
+ };
24
+ /** Auto-advance timeouts (ms). Matches the JSON `auto_advance_after_ms`. */
25
+ export declare const AUTO_ADVANCE_MS: Partial<Record<ActivationStateName, number>>;
26
+ /** The state name a given state auto-advances to on timeout. */
27
+ export declare const AUTO_ADVANCE_TO: Partial<Record<ActivationStateName, ActivationStateName>>;
28
+ export declare class ActivationStateMachine {
29
+ private state;
30
+ private subscribers;
31
+ constructor(initial?: ActivationStateName);
32
+ current(): ActivationState;
33
+ /**
34
+ * Attempt a transition. Returns the new state (or the unchanged state if
35
+ * the event is not a valid transition from the current state).
36
+ */
37
+ transition(event: ActivationEvent, ctx?: TransitionContext): ActivationState;
38
+ /** Force the machine into a specific state (used by activation pollers). */
39
+ force(name: ActivationStateName): ActivationState;
40
+ /** Subscribe to state changes; returns an unsubscribe function. */
41
+ onStateChange(cb: (s: ActivationState) => void): () => void;
42
+ /** Elapsed ms since the current state was entered. */
43
+ elapsedMs(): number;
44
+ /** Lookup table introspection. */
45
+ static transitions: Record<ActivationStateName, Partial<Record<ActivationEvent, TransitionTarget>>>;
46
+ static busyStates: Set<ActivationStateName>;
47
+ private enter;
48
+ }
49
+ export {};
50
+ //# sourceMappingURL=ActivationStateMachine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ActivationStateMachine.d.ts","sourceRoot":"","sources":["../src/ActivationStateMachine.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEvE,wDAAwD;AACxD,MAAM,MAAM,eAAe,GACvB,cAAc,GACd,yBAAyB,GACzB,WAAW,GACX,aAAa,GACb,aAAa,GACb,+BAA+B,GAC/B,gCAAgC,GAChC,2BAA2B,GAC3B,oBAAoB,GACpB,0BAA0B,GAC1B,4BAA4B,GAC5B,sBAAsB,GACtB,gBAAgB,GAChB,cAAc,GACd,oBAAoB,CAAC;AAEzB,UAAU,iBAAiB;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,yBAAyB,CAAC,EAAE,OAAO,CAAC;CACrC;AAED,KAAK,iBAAiB,GAAG;IACvB,IAAI,EAAE,iBAAiB,CAAC;IACxB,EAAE,EAAE,mBAAmB,CAAC;IACxB,IAAI,CAAC,EAAE,mBAAmB,CAAC;CAC5B,CAAC;AAEF,KAAK,gBAAgB,GAAG,mBAAmB,GAAG,iBAAiB,CAAC;AAwDhE;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;CAKtB,CAAC;AAEX,4EAA4E;AAC5E,eAAO,MAAM,eAAe,EAAE,OAAO,CAAC,MAAM,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAKxE,CAAC;AAEF,gEAAgE;AAChE,eAAO,MAAM,eAAe,EAAE,OAAO,CAAC,MAAM,CAAC,mBAAmB,EAAE,mBAAmB,CAAC,CAKrF,CAAC;AAEF,qBAAa,sBAAsB;IACjC,OAAO,CAAC,KAAK,CAAkB;IAC/B,OAAO,CAAC,WAAW,CAA2C;gBAElD,OAAO,GAAE,mBAA4B;IAIjD,OAAO,IAAI,eAAe;IAI1B;;;OAGG;IACH,UAAU,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,GAAE,iBAAsB,GAAG,eAAe;IAShF,4EAA4E;IAC5E,KAAK,CAAC,IAAI,EAAE,mBAAmB,GAAG,eAAe;IAIjD,mEAAmE;IACnE,aAAa,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,eAAe,KAAK,IAAI,GAAG,MAAM,IAAI;IAK3D,sDAAsD;IACtD,SAAS,IAAI,MAAM;IAInB,kCAAkC;IAClC,MAAM,CAAC,WAAW,kFAAe;IACjC,MAAM,CAAC,UAAU,2BAAe;IAEhC,OAAO,CAAC,KAAK;CAKd"}
@@ -0,0 +1,141 @@
1
+ // Port of bravely-commerce-router/docs/activation-state-machine.json (v1.0.0).
2
+ //
3
+ // Keep aligned by hand. The canonical doc is the source of truth; this port
4
+ // is a thin TS surface so host pages can render UI off `getActivationState()`.
5
+ //
6
+ // CLAUDE.md feedback_no_etas: every state surfaces ELAPSED time, never a
7
+ // predicted ETA. Anti-patterns from M5 enforced in the banned-phrases CI
8
+ // check (in bravely-commerce-router); the strings here MUST match the JSON.
9
+ const TRANSITIONS = {
10
+ idle: {
11
+ app_launched: { when: { bas_present: true }, to: "restoring_session" },
12
+ app_launched_no_session: { when: { bas_present: false }, to: "pending_user_action" },
13
+ },
14
+ restoring_session: {
15
+ bas_found: "verifying_entitlement",
16
+ bas_missing: "fresh_launch_restoration",
17
+ bas_invalid: "pending_user_action",
18
+ },
19
+ verifying_entitlement: {
20
+ entitlement_check_success_pro: "entitlement_fresh_valid",
21
+ entitlement_check_success_none: "entitlement_none",
22
+ entitlement_check_failure: {
23
+ when: { entitlement_cache_present: true },
24
+ to: "entitlement_cached_valid",
25
+ else: "failed",
26
+ },
27
+ },
28
+ entitlement_cached_valid: {
29
+ entitlement_check_success_pro: "entitlement_fresh_valid",
30
+ entitlement_check_success_none: "entitlement_none",
31
+ entitlement_check_failure: "entitlement_cached_valid",
32
+ },
33
+ entitlement_fresh_valid: {},
34
+ entitlement_none: {
35
+ checkout_completed: "post_checkout_activation",
36
+ },
37
+ post_checkout_activation: {
38
+ activation_polled_active: "entitlement_fresh_valid",
39
+ activation_polled_inactive: "post_checkout_activation",
40
+ activation_timed_out: "failed",
41
+ },
42
+ fresh_launch_restoration: {
43
+ entitlement_check_success_pro: "entitlement_fresh_valid",
44
+ entitlement_check_success_none: "entitlement_none",
45
+ entitlement_check_failure: "failed",
46
+ },
47
+ pending_user_action: {
48
+ user_signed_in: "restoring_session",
49
+ user_skipped: "entitlement_none",
50
+ },
51
+ failed: {
52
+ user_clicked_retry: "pending_user_action",
53
+ },
54
+ };
55
+ const BUSY_STATES = new Set([
56
+ "restoring_session",
57
+ "verifying_entitlement",
58
+ "post_checkout_activation",
59
+ "fresh_launch_restoration",
60
+ ]);
61
+ /**
62
+ * The retry budget for `post_checkout_activation` — matches the JSON spec.
63
+ * Host pages use this to schedule entitlement polling.
64
+ */
65
+ export const POST_CHECKOUT_RETRY = {
66
+ maxRetries: 30,
67
+ initialBackoffMs: 1000,
68
+ maxBackoffMs: 8000,
69
+ backoffMultiplier: 1.5,
70
+ };
71
+ /** Auto-advance timeouts (ms). Matches the JSON `auto_advance_after_ms`. */
72
+ export const AUTO_ADVANCE_MS = {
73
+ restoring_session: 8000,
74
+ verifying_entitlement: 8000,
75
+ post_checkout_activation: 120_000,
76
+ fresh_launch_restoration: 10_000,
77
+ };
78
+ /** The state name a given state auto-advances to on timeout. */
79
+ export const AUTO_ADVANCE_TO = {
80
+ restoring_session: "failed",
81
+ verifying_entitlement: "failed",
82
+ post_checkout_activation: "failed",
83
+ fresh_launch_restoration: "failed",
84
+ };
85
+ export class ActivationStateMachine {
86
+ state;
87
+ subscribers = new Set();
88
+ constructor(initial = "idle") {
89
+ this.state = { name: initial, enteredAt: new Date(), busy: BUSY_STATES.has(initial) };
90
+ }
91
+ current() {
92
+ return { ...this.state };
93
+ }
94
+ /**
95
+ * Attempt a transition. Returns the new state (or the unchanged state if
96
+ * the event is not a valid transition from the current state).
97
+ */
98
+ transition(event, ctx = {}) {
99
+ const fromTable = TRANSITIONS[this.state.name];
100
+ const target = fromTable[event];
101
+ if (!target)
102
+ return this.current(); // no-op
103
+ const next = resolveTarget(target, ctx);
104
+ if (!next)
105
+ return this.current();
106
+ return this.enter(next);
107
+ }
108
+ /** Force the machine into a specific state (used by activation pollers). */
109
+ force(name) {
110
+ return this.enter(name);
111
+ }
112
+ /** Subscribe to state changes; returns an unsubscribe function. */
113
+ onStateChange(cb) {
114
+ this.subscribers.add(cb);
115
+ return () => this.subscribers.delete(cb);
116
+ }
117
+ /** Elapsed ms since the current state was entered. */
118
+ elapsedMs() {
119
+ return Date.now() - this.state.enteredAt.getTime();
120
+ }
121
+ /** Lookup table introspection. */
122
+ static transitions = TRANSITIONS;
123
+ static busyStates = BUSY_STATES;
124
+ enter(name) {
125
+ this.state = { name, enteredAt: new Date(), busy: BUSY_STATES.has(name) };
126
+ for (const cb of this.subscribers)
127
+ cb({ ...this.state });
128
+ return this.current();
129
+ }
130
+ }
131
+ function resolveTarget(target, ctx) {
132
+ if (typeof target === "string")
133
+ return target;
134
+ for (const [k, v] of Object.entries(target.when)) {
135
+ if (ctx[k] !== v) {
136
+ return target.else ?? null;
137
+ }
138
+ }
139
+ return target.to;
140
+ }
141
+ //# sourceMappingURL=ActivationStateMachine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ActivationStateMachine.js","sourceRoot":"","sources":["../src/ActivationStateMachine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,156 @@
1
+ import type { BravelyAccountState, ManagerConfig, Entitlement, CheckoutPlan, PaddlePortalSession, ActivationState, LibVersionPolicy } from "./types.js";
2
+ import { BravelyClientKilledError, OAuthError } from "./deprecation.js";
3
+ /** Result of `pollForActivation()` — the manager's M3 activation runner. */
4
+ export type ActivationResult = {
5
+ outcome: "active";
6
+ entitlements: Entitlement[];
7
+ attempts: number;
8
+ } | {
9
+ outcome: "timeout";
10
+ attempts: number;
11
+ } | {
12
+ outcome: "exhausted";
13
+ attempts: number;
14
+ } | {
15
+ outcome: "not_signed_in";
16
+ };
17
+ type StateListener = (state: BravelyAccountState) => void;
18
+ export declare class BravelyAccountManager {
19
+ private readonly cfg;
20
+ private readonly storage;
21
+ private readonly cache;
22
+ private readonly activation;
23
+ private state;
24
+ private listeners;
25
+ private libVersionPolicy;
26
+ private dpopKeypairPromise;
27
+ private dpopJktThumbprint;
28
+ constructor(config: ManagerConfig);
29
+ /**
30
+ * Return the current state. The returned reference is **stable across
31
+ * calls** until `setState()` is invoked — i.e. while the state has not
32
+ * changed, two consecutive `getState()` calls produce values for which
33
+ * `Object.is(a, b)` is true.
34
+ *
35
+ * This is the contract React 19's `useSyncExternalStore` requires: the
36
+ * `getSnapshot` callback must return `===` identical values until the
37
+ * `subscribe` callback fires. Returning a fresh clone on every read (which
38
+ * an earlier version of this facade did) triggers React error #185 —
39
+ * "Maximum update depth exceeded" — because the snapshot keeps "changing"
40
+ * even when nothing has happened.
41
+ *
42
+ * The clone happens once, at write time, inside `setState()`. Listeners
43
+ * receive the same reference this method returns until the next
44
+ * `setState()` swaps in a new canonical clone.
45
+ */
46
+ getState(): BravelyAccountState;
47
+ onStateChange(cb: StateListener): () => void;
48
+ getActivationState(): ActivationState;
49
+ getLibVersionPolicy(): LibVersionPolicy | null;
50
+ /**
51
+ * Hydrate from persistent storage and (if a BAS is present) verify it
52
+ * against the identity API. Call once on page load before rendering UI
53
+ * that depends on `getState()`.
54
+ */
55
+ restore(): Promise<BravelyAccountState>;
56
+ /**
57
+ * Begin the OAuth 2.1 + PKCE sign-in dance. Resolves to the new state. If
58
+ * the browser is at the start of the dance, this triggers a redirect
59
+ * (and the promise effectively never resolves before unload). If the
60
+ * browser is on the callback URL with `?code=...&state=...`, this finishes
61
+ * the exchange.
62
+ */
63
+ signIn(opts?: {
64
+ loginHint?: string;
65
+ }): Promise<BravelyAccountState>;
66
+ /** Sign out: drop local state. Server-side revoke is best-effort. */
67
+ signOut(): Promise<void>;
68
+ /**
69
+ * Read entitlements, serving the cached row if fresh and refreshing
70
+ * opportunistically. Always returns an array (possibly empty).
71
+ */
72
+ getEntitlements(): Promise<Entitlement[]>;
73
+ /** Convenience: check a single entitlement (e.g. `diskaroo_pro`). */
74
+ hasEntitlement(lookupKey: string): Promise<boolean>;
75
+ /** Mint an app-data-token for apps that need direct Firebase SDK access. */
76
+ getAppDataToken(): Promise<string>;
77
+ /**
78
+ * Open Paddle hosted-checkout for the given plan. Hits
79
+ * `/api/checkout/sessions` (BAS-authed), opens the returned URL via the
80
+ * system browser (`window.open(url, '_blank')`). The promise resolves once
81
+ * the request returns; the caller is responsible for polling
82
+ * `getEntitlements()` (or watching `getActivationState()`) for the
83
+ * post-checkout activation.
84
+ */
85
+ openCheckout(plan: CheckoutPlan): Promise<void>;
86
+ /**
87
+ * Mint a Paddle customer-portal session for the signed-in account.
88
+ * Consumers decide how to open `session.url`; checkout activation state is
89
+ * intentionally unchanged.
90
+ */
91
+ createPaddlePortalSession(): Promise<PaddlePortalSession>;
92
+ /** Inform the manager that checkout completion was observed. */
93
+ notifyCheckoutCompleted(): void;
94
+ /**
95
+ * Return the active entitlements that count as "cross-app" for the M4
96
+ * `<CrossAppCard>`. Excludes the current app's own `<slug>_pro` token.
97
+ * `bravely_premium` (the bundle entitlement) is included — it's the
98
+ * unifying signal that drives the M4 copy ("You own N Bravely Pro
99
+ * apps").
100
+ *
101
+ * Reads from the in-memory signed-in state (which mirrors the
102
+ * `EntitlementCache`); does not hit the network. Host pages that need a
103
+ * fresh fetch should call `getEntitlements()` first.
104
+ */
105
+ crossAppEntitlements(): Entitlement[];
106
+ /**
107
+ * Run the canonical M3 post-checkout activation polling loop against
108
+ * `/api/entitlements?app=<slug>`. Uses the retry budget locked in
109
+ * `POST_CHECKOUT_RETRY` (30 retries, 1s..8s capped exponential
110
+ * backoff). Fires the right activation state events as it runs.
111
+ *
112
+ * Returns:
113
+ * - `{ outcome: "active" }` as soon as an entitlement is observed
114
+ * (typically `<currentApp>_pro` or `bravely_premium`).
115
+ * - `{ outcome: "exhausted" }` after `maxRetries` attempts without an
116
+ * active entitlement.
117
+ * - `{ outcome: "timeout" }` if total elapsed time hits the auto-
118
+ * advance ceiling for `post_checkout_activation` (120s).
119
+ * - `{ outcome: "not_signed_in" }` if no BAS is present.
120
+ *
121
+ * The promise resolves regardless of network failures. Each failure
122
+ * is treated as an inactive poll (the user's purchase is still safe;
123
+ * the activation state machine surfaces this via the ladder copy).
124
+ */
125
+ pollForActivation(): Promise<ActivationResult>;
126
+ private completeAuthorization;
127
+ private persistToken;
128
+ private refreshEntitlements;
129
+ /**
130
+ * Gate 1 compatibility path: keep `Authorization: Bearer <bas>` because
131
+ * the current router accepts Bearer on shared BAS endpoints, but attach a
132
+ * real RFC 9449 proof in the `DPoP` header so Gate 2 traffic is already
133
+ * exercising proof generation.
134
+ */
135
+ private fetchWithBas;
136
+ private dpopProof;
137
+ private getDpopKeypair;
138
+ /** Fetch wrapper that processes the `Bravely-Deprecation` header on every response. */
139
+ private fetchWithDeprecation;
140
+ private requireBas;
141
+ /**
142
+ * Replace the canonical in-memory state.
143
+ *
144
+ * Clones `next` once and stores the clone as `this.state` BEFORE notifying
145
+ * listeners. Listeners receive the same reference subsequent `getState()`
146
+ * calls will return — required for `useSyncExternalStore` consumers (see
147
+ * `getState()` for the React #185 background).
148
+ *
149
+ * Cloning at write time also seals off any external references the caller
150
+ * still holds to `next`: later mutations of `next` (or its nested
151
+ * `entitlements` array) cannot leak into `this.state`.
152
+ */
153
+ private setState;
154
+ }
155
+ export { BravelyClientKilledError, OAuthError };
156
+ //# sourceMappingURL=BravelyAccountManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BravelyAccountManager.d.ts","sourceRoot":"","sources":["../src/BravelyAccountManager.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,mBAAmB,EACnB,aAAa,EACb,WAAW,EAEX,YAAY,EACZ,mBAAmB,EACnB,eAAe,EACf,gBAAgB,EAGjB,MAAM,YAAY,CAAC;AAMpB,OAAO,EAEL,wBAAwB,EACxB,UAAU,EACX,MAAM,kBAAkB,CAAC;AAE1B,4EAA4E;AAC5E,MAAM,MAAM,gBAAgB,GACxB;IAAE,OAAO,EAAE,QAAQ,CAAC;IAAC,YAAY,EAAE,WAAW,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACpE;IAAE,OAAO,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACxC;IAAE,OAAO,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,OAAO,EAAE,eAAe,CAAA;CAAE,CAAC;AAgBjC,KAAK,aAAa,GAAG,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAC;AAE1D,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,GAAG,CACmF;IACvG,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAClC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmB;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAyB;IACpD,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,SAAS,CAA4B;IAC7C,OAAO,CAAC,gBAAgB,CAAiC;IACzD,OAAO,CAAC,kBAAkB,CAA4C;IACtE,OAAO,CAAC,iBAAiB,CAAuB;gBAEpC,MAAM,EAAE,aAAa;IA0BjC;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,IAAI,mBAAmB;IAI/B,aAAa,CAAC,EAAE,EAAE,aAAa,GAAG,MAAM,IAAI;IAK5C,kBAAkB,IAAI,eAAe;IAIrC,mBAAmB,IAAI,gBAAgB,GAAG,IAAI;IAI9C;;;;OAIG;IACG,OAAO,IAAI,OAAO,CAAC,mBAAmB,CAAC;IA4B7C;;;;;;OAMG;IACG,MAAM,CAAC,IAAI,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,mBAAmB,CAAC;IA6B7E,qEAAqE;IAC/D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA0B9B;;;OAGG;IACG,eAAe,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAU/C,qEAAqE;IAC/D,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKzD,4EAA4E;IACtE,eAAe,IAAI,OAAO,CAAC,MAAM,CAAC;IAYxC;;;;;;;OAOG;IACG,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBrD;;;;OAIG;IACG,yBAAyB,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAW/D,gEAAgE;IAChE,uBAAuB,IAAI,IAAI;IAI/B;;;;;;;;;;OAUG;IACH,oBAAoB,IAAI,WAAW,EAAE;IAMrC;;;;;;;;;;;;;;;;;;OAkBG;IACG,iBAAiB,IAAI,OAAO,CAAC,gBAAgB,CAAC;YAuEtC,qBAAqB;YAoCrB,YAAY;YA4BZ,mBAAmB;IA+CjC;;;;;OAKG;YACW,YAAY;YASZ,SAAS;YAgBT,cAAc;IAmB5B,uFAAuF;YACzE,oBAAoB;YAkCpB,UAAU;IAMxB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,QAAQ;CAMjB;AA4CD,OAAO,EAAE,wBAAwB,EAAE,UAAU,EAAE,CAAC"}