@usethrottle/auth 0.1.0

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/CLAUDE.md ADDED
@@ -0,0 +1,52 @@
1
+ # @usethrottle/auth
2
+
3
+ Buyer authentication SDK: framework-free core (`src/client.ts`, `src/account.ts`,
4
+ `src/session.ts`, `src/storage.ts`, `src/request.ts`), a React provider + hooks
5
+ (`src/react/`), and five prebuilt forms (`src/react/forms/`). Three entry
6
+ points: `.` (core), `./react`, `./forms` — see `exports` in `package.json`.
7
+
8
+ ## Invariants an agent must not break
9
+
10
+ - **The access token never goes to storage.** It lives only in `SessionState`
11
+ (`src/session.ts`), in memory. Only the refresh token is persisted, via the
12
+ `AuthStorage` adapter. Do not add a code path that writes `accessToken`
13
+ anywhere durable.
14
+ - **Refresh is single-flight and cross-tab.** `client.ts`'s `refresh()` shares
15
+ one in-flight promise per client instance so N concurrent 401s produce one
16
+ `/auth/refresh` call, and `withLock()` additionally serialises refreshes
17
+ across browser tabs via the Web Locks API (falls back to no-op where
18
+ unsupported). The refresh token **rotates on every use**, and the server
19
+ revokes the whole session family on replay of a spent one — changing this
20
+ logic without preserving single-flight risks logging every buyer out the
21
+ next time two requests race.
22
+ - **`useSavedCards` is named that way on purpose**, not `usePaymentMethods`.
23
+ `@usethrottle/payment-methods` exports a hook with that name, and merchants
24
+ commonly import both packages in one file. `src/exports.test.ts` asserts
25
+ `usePaymentMethods` is absent from this package's `./react` entry — do not
26
+ add it back, and do not rename `useSavedCards` without checking that test.
27
+ - **The server API lives in `packages/platform-storefront`.** This package is
28
+ a client for `/v1/storefront/*` and must never outrun it — do not add a
29
+ method here for a route that package doesn't implement yet, and do not
30
+ change a request/response shape here without confirming the server already
31
+ matches it. Docs for the wire contract:
32
+ `apps/web/src/pages/docs/developers/storefront-auth/index.astro`.
33
+ - **The claim cutoff is a server behavior, not a client one.** This package
34
+ does not filter anything client-side for it — an unverified buyer simply
35
+ gets empty lists and 403s back from the server. Do not "fix" apparently
36
+ empty `useOrders`/`useAddresses` results by adding client-side workarounds;
37
+ see the README's "Email verification and the claim cutoff" section.
38
+
39
+ ## Conventions
40
+
41
+ - Every exported function's public surface is asserted in `src/exports.test.ts`
42
+ — a passing build with a broken barrel is the failure mode that test exists
43
+ to catch. Update it when you intentionally add/remove/rename a public export.
44
+ - React stays a peer dependency (`peerDependenciesMeta.react.optional: true`).
45
+ Do not add a runtime `import` of `react` to any file reachable from the `.`
46
+ (core) entry point.
47
+ - Test with `pnpm --filter @usethrottle/auth test`, not `pnpm vitest run
48
+ packages/auth-sdk/...` from the repo root — the latter resolves the root
49
+ vitest config, which lacks the jsdom environment this package's React tests
50
+ need. Tests are colocated (`src/*.test.ts(x)`); `vitest.config.ts`'s
51
+ `include` is scoped to `src/**` on purpose — a new test file outside `src/`
52
+ will not run.
package/README.md ADDED
@@ -0,0 +1,230 @@
1
+ # @usethrottle/auth
2
+
3
+ Buyer authentication for Throttle storefronts — a framework-free core, React
4
+ hooks, and five drop-in forms. Wraps the `/v1/storefront/*` buyer-auth API
5
+ (silent refresh, single-flight and cross-tab safe, retry-once-on-401) so a
6
+ storefront never hand-rolls that logic.
7
+
8
+ This is a different plane from your merchant `sk_*`/`pk_*` API key. See the
9
+ [Storefront Customer Auth docs](https://usethrottle.dev/docs/developers/storefront-auth)
10
+ for the full HTTP contract this package wraps.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install @usethrottle/auth
16
+ ```
17
+
18
+ React is a **peer dependency** and optional: the core client
19
+ (`createThrottleAuth`, `createAccount`) works with no React installed at all.
20
+ Only importing from `@usethrottle/auth/react` or `@usethrottle/auth/forms`
21
+ requires `react` + `react-dom` (`>=18`) to be present.
22
+
23
+ ## Prerequisites
24
+
25
+ Before any of this works, in the Throttle dashboard:
26
+
27
+ 1. Turn on customer accounts for the application (`auth.enabled: true`), per
28
+ environment.
29
+ 2. Add the storefront's origin to the application's allowed-origins list (the
30
+ same list `PUT /api/v1/embed-config` manages).
31
+ 3. Use a **publishable** (`pk_`) key — this package only ever talks to the
32
+ buyer plane, never the merchant one.
33
+
34
+ The two failures merchants hit first:
35
+
36
+ | Error | Cause |
37
+ | ----- | ----- |
38
+ | `404 auth_not_enabled` | Customer accounts are off for this application/environment. |
39
+ | `403 origin_not_allowed` | The storefront's `Origin` is not on the allow-list (a production environment with an empty list denies everything; sandbox allows everything). |
40
+
41
+ ## React quick start
42
+
43
+ ```tsx
44
+ import { ThrottleAuthProvider, useAuth } from '@usethrottle/auth/react';
45
+ import { SignInForm } from '@usethrottle/auth/forms';
46
+
47
+ function App() {
48
+ return (
49
+ <ThrottleAuthProvider publishableKey="pk_...">
50
+ <Account />
51
+ </ThrottleAuthProvider>
52
+ );
53
+ }
54
+
55
+ function Account() {
56
+ const { isAuthenticated, isLoading, customer } = useAuth();
57
+ if (isLoading) return null;
58
+ if (!isAuthenticated) return <SignInForm />;
59
+ return <p>Signed in as {customer!.email}</p>;
60
+ }
61
+ ```
62
+
63
+ `<ThrottleAuthProvider>` goes once, near the root — it owns the auth client
64
+ and calls `restore()` on mount to pick up an existing session from storage.
65
+
66
+ ## Headless quick start
67
+
68
+ No React required:
69
+
70
+ ```ts
71
+ import { createThrottleAuth, createAccount } from '@usethrottle/auth';
72
+
73
+ const auth = createThrottleAuth({ publishableKey: 'pk_...' });
74
+ const account = createAccount(auth);
75
+
76
+ await auth.signIn('buyer@example.com', 'hunter2000');
77
+ const profile = await account.getProfile();
78
+ ```
79
+
80
+ `auth.subscribe(fn)` + `auth.getSnapshot()` give you the same state a
81
+ framework adapter would use to re-render.
82
+
83
+ ## Account pages
84
+
85
+ Six hooks, each a `{ data, isLoading, error, refetch }` resource plus the
86
+ mutations that resource supports:
87
+
88
+ ```tsx
89
+ import { useCustomer, useAddresses, useSavedCards, useOrders, useInvoices, useSubscriptions } from '@usethrottle/auth/react';
90
+
91
+ function Orders() {
92
+ const { data: orders, isLoading, error } = useOrders();
93
+ if (isLoading) return <p>Loading…</p>;
94
+ if (error) return <p>{error.message}</p>;
95
+ return <ul>{orders.map((o) => <li key={o.id}>{o.orderNumber}</li>)}</ul>;
96
+ }
97
+ ```
98
+
99
+ - `useCustomer()` — `update(patch)` calls `PATCH /me`.
100
+ - `useAddresses()` — `create`, `update`, `remove`.
101
+ - `useSavedCards()` — `setDefault`, `remove`. **Not** `usePaymentMethods`:
102
+ that name belongs to `@usethrottle/payment-methods`, which merchants often
103
+ import alongside this package in the same file.
104
+ - `useOrders(page?)`, `useInvoices(page?)` — read-only, cursor-paginated
105
+ (`{ cursor, limit }`).
106
+ - `useSubscriptions(page?)` — `cancel(id, { atPeriodEnd })`, `pause(id)`,
107
+ `resume(id)`.
108
+
109
+ ## Email verification and the claim cutoff
110
+
111
+ **This is the single most surprising thing about this API — read it before
112
+ you file a bug.**
113
+
114
+ Registration issues a session immediately; a buyer never waits on an email
115
+ round trip to browse or check out. But until they click the verification
116
+ link, `emailVerified` is `false` on the session, and that has real
117
+ consequences:
118
+
119
+ - `PATCH /me` (`account.updateProfile` / `useCustomer().update`) returns
120
+ `403 verification_required`.
121
+ - Saved payment methods are refused outright (`useSavedCards`), not just
122
+ filtered.
123
+ - If the buyer's email already had **guest** orders, addresses, or a
124
+ subscription on this application, that pre-existing history stays hidden —
125
+ `useOrders`, `useInvoices`, `useSubscriptions`, and `useAddresses` all
126
+ return **empty** for anything created before registration — until
127
+ verification clears the cutoff.
128
+
129
+ None of this is a bug. It looks exactly like one (an account with real order
130
+ history rendering as empty), so show a persistent banner rather than treating
131
+ it as broken:
132
+
133
+ ```tsx
134
+ import { useAuth } from '@usethrottle/auth/react';
135
+ import { VerifyEmailPanel } from '@usethrottle/auth/forms';
136
+
137
+ function AccountShell({ children }: { children: React.ReactNode }) {
138
+ const { isAuthenticated, emailVerified } = useAuth();
139
+ return (
140
+ <>
141
+ {isAuthenticated && !emailVerified && <VerifyEmailPanel />}
142
+ {children}
143
+ </>
144
+ );
145
+ }
146
+ ```
147
+
148
+ `<VerifyEmailPanel token={token} />` (token from the emailed link's query
149
+ string) verifies on mount; call it with no `token` prop to render a
150
+ "resend verification email" prompt instead.
151
+
152
+ ## Step-up
153
+
154
+ A handful of destructive actions require the buyer to re-prove their current
155
+ password: changing the password itself, removing a saved card, and
156
+ cancelling/pausing/resuming a subscription. `ThrottleAuth` handles this for
157
+ you — on a `403 step_up_required`, it calls `onStepUpRequired()`, expects the
158
+ buyer's password back (or `null` to cancel), and retries the call once:
159
+
160
+ ```tsx
161
+ <ThrottleAuthProvider
162
+ publishableKey={pk}
163
+ onStepUpRequired={async () => window.prompt('Confirm your password to continue')}
164
+ >
165
+ ```
166
+
167
+ Prefer a real password dialog over `window.prompt` in production — this is
168
+ the minimum that satisfies the contract.
169
+
170
+ ## Errors
171
+
172
+ Every rejected call throws a `ThrottleAuthError`:
173
+
174
+ ```ts
175
+ class ThrottleAuthError extends Error {
176
+ code: string;
177
+ statusCode: number;
178
+ retryAfter?: number;
179
+ details?: unknown;
180
+ }
181
+ ```
182
+
183
+ `messageForError(err)` (from `@usethrottle/auth/forms`) maps every code to
184
+ buyer-safe copy — use it instead of showing `err.message` directly, since two
185
+ codes (`invalid_credentials`, and anything unrecognized) are deliberately
186
+ vague by design.
187
+
188
+ | Code | Meaning |
189
+ | ---- | ------- |
190
+ | `auth_not_enabled` | Customer accounts are off for this application/environment. |
191
+ | `origin_not_allowed` | Storefront origin isn't on the allow-list. |
192
+ | `invalid_api_key` | The publishable key is invalid, revoked, or expired. |
193
+ | `invalid_credentials` | Sign-in or step-up failed — any cause, deliberately vague. |
194
+ | `weak_password` | Fails the password policy; `message` carries the specific reason. |
195
+ | `too_many_attempts` | Rate limited; `retryAfter` (seconds) is set when the server sent one. |
196
+ | `verification_required` | The route needs a verified session (see claim cutoff above). |
197
+ | `step_up_required` | The route needs a fresh step-up proof. |
198
+ | `session_revoked` | The session was revoked, or its epoch is stale. |
199
+ | `token_expired` / `token_invalid` / `token_already_used` | An email-verification or reset link is dead. |
200
+ | `environment_mismatch` | Session and API key belong to different environments (sandbox vs. production). |
201
+ | `auth_unavailable` | Credential routes (login/register/etc.) fail closed when the rate limiter is unreachable. |
202
+ | `cart_already_claimed` | `claimCart` targeted a cart already bound to a different customer. |
203
+ | `not_found` | The address/card/session/order/invoice/subscription id doesn't belong to this buyer. |
204
+
205
+ ## Security notes
206
+
207
+ - The **access token lives in memory only** — this package never writes it to
208
+ storage, and there is no way to configure it to.
209
+ - The **refresh token** is the only thing persisted, through the storage
210
+ adapter you provide (`localStorageAdapter()` by default).
211
+ - Pass `storage: memoryStorage()` to `createThrottleAuth` /
212
+ `<ThrottleAuthProvider>` to opt out of persistence entirely — the buyer is
213
+ signed out on every page reload, useful for kiosk/shared-device contexts.
214
+ - Refresh is single-flight (concurrent 401s share one refresh call) and
215
+ cross-tab safe via the Web Locks API where available. Do not build your own
216
+ refresh loop around the same publishable key; use `auth.call()` /
217
+ `account.*` / the hooks, all of which already go through it.
218
+
219
+ ## What this package does NOT do
220
+
221
+ - **It does not add a saved card.** There is no buyer-plane "add payment
222
+ method" route — cards are added through
223
+ [`@usethrottle/payment-methods`](https://usethrottle.dev/docs/developers/payment-methods),
224
+ which needs a merchant-minted client token. `useSavedCards()` here only
225
+ lists, sets a default, and removes an already-saved card.
226
+ - It does not let a buyer change which card a subscription renews on directly
227
+ — that's done by setting a different card as the account default
228
+ (`useSavedCards().setDefault(id)`).
229
+ - It does not change a buyer's email address (`PATCH /me` covers name, phone,
230
+ company, marketing consent — not email).
@@ -0,0 +1,378 @@
1
+ // src/errors.ts
2
+ var ThrottleAuthError = class extends Error {
3
+ code;
4
+ statusCode;
5
+ retryAfter;
6
+ details;
7
+ constructor(args) {
8
+ super(args.message);
9
+ this.name = "ThrottleAuthError";
10
+ this.code = args.code;
11
+ this.statusCode = args.statusCode;
12
+ this.retryAfter = args.retryAfter;
13
+ this.details = args.details;
14
+ }
15
+ };
16
+
17
+ // src/storage.ts
18
+ function memoryStorage() {
19
+ const map = /* @__PURE__ */ new Map();
20
+ return {
21
+ get: (k) => map.get(k) ?? null,
22
+ set: (k, v) => void map.set(k, v),
23
+ remove: (k) => void map.delete(k)
24
+ };
25
+ }
26
+ function localStorageAdapter() {
27
+ const fallback = memoryStorage();
28
+ const ls = () => {
29
+ try {
30
+ return typeof localStorage !== "undefined" ? localStorage : null;
31
+ } catch {
32
+ return null;
33
+ }
34
+ };
35
+ return {
36
+ get(k) {
37
+ try {
38
+ const v = ls()?.getItem(k);
39
+ if (v != null) return v;
40
+ } catch {
41
+ }
42
+ return fallback.get(k);
43
+ },
44
+ set(k, v) {
45
+ fallback.set(k, v);
46
+ try {
47
+ ls()?.setItem(k, v);
48
+ } catch {
49
+ }
50
+ },
51
+ remove(k) {
52
+ fallback.remove(k);
53
+ try {
54
+ ls()?.removeItem(k);
55
+ } catch {
56
+ }
57
+ }
58
+ };
59
+ }
60
+
61
+ // src/request.ts
62
+ async function request(input) {
63
+ const headers = { "X-API-Key": input.publishableKey };
64
+ if (input.body !== void 0) headers["Content-Type"] = "application/json";
65
+ if (input.accessToken) headers.Authorization = `Bearer ${input.accessToken}`;
66
+ if (input.stepUpToken) headers["X-Step-Up"] = input.stepUpToken;
67
+ const res = await fetch(`${input.baseUrl}/v1/storefront${input.path}`, {
68
+ method: input.method,
69
+ headers,
70
+ ...input.body !== void 0 ? { body: JSON.stringify(input.body) } : {}
71
+ });
72
+ const text = await res.text();
73
+ let parsed = {};
74
+ try {
75
+ parsed = text ? JSON.parse(text) : {};
76
+ } catch {
77
+ }
78
+ if (!res.ok) {
79
+ const e = parsed?.error ?? {};
80
+ const ra = Number(res.headers.get("retry-after"));
81
+ throw new ThrottleAuthError({
82
+ code: e.code ?? "request_failed",
83
+ message: e.message ?? `Request failed with ${res.status}`,
84
+ statusCode: res.status,
85
+ retryAfter: Number.isFinite(ra) && ra >= 0 ? ra : void 0,
86
+ details: e.details
87
+ });
88
+ }
89
+ return parsed && typeof parsed === "object" && "data" in parsed ? parsed.data : parsed ?? {};
90
+ }
91
+
92
+ // src/session.ts
93
+ var SessionState = class {
94
+ #snapshot = { status: "loading", customer: null, emailVerified: false };
95
+ #accessToken = null;
96
+ #expiresAt = null;
97
+ #listeners = /* @__PURE__ */ new Set();
98
+ get() {
99
+ return this.#snapshot;
100
+ }
101
+ accessToken() {
102
+ return this.#accessToken;
103
+ }
104
+ expiresAt() {
105
+ return this.#expiresAt;
106
+ }
107
+ setSession(session) {
108
+ this.#accessToken = session.accessToken;
109
+ this.#expiresAt = Date.parse(session.expiresAt);
110
+ this.#publish({
111
+ status: "authenticated",
112
+ customer: session.customer,
113
+ emailVerified: Boolean(session.customer?.emailVerified)
114
+ });
115
+ }
116
+ setStatus(status) {
117
+ this.#publish({ ...this.#snapshot, status });
118
+ }
119
+ clear() {
120
+ this.#accessToken = null;
121
+ this.#expiresAt = null;
122
+ this.#publish({ status: "unauthenticated", customer: null, emailVerified: false });
123
+ }
124
+ subscribe(fn) {
125
+ this.#listeners.add(fn);
126
+ return () => {
127
+ this.#listeners.delete(fn);
128
+ };
129
+ }
130
+ #publish(next) {
131
+ const same = next.status === this.#snapshot.status && next.emailVerified === this.#snapshot.emailVerified && customerEquals(next.customer, this.#snapshot.customer);
132
+ if (same) return;
133
+ this.#snapshot = next;
134
+ for (const fn of [...this.#listeners]) {
135
+ try {
136
+ fn(next);
137
+ } catch {
138
+ }
139
+ }
140
+ }
141
+ };
142
+ function customerEquals(a, b) {
143
+ if (a === b) return true;
144
+ if (!a || !b) return false;
145
+ const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
146
+ for (const k of keys) if (a[k] !== b[k]) return false;
147
+ return true;
148
+ }
149
+
150
+ // src/client.ts
151
+ var DEFAULT_BASE_URL = "https://api.usethrottle.dev";
152
+ var REFRESH_SKEW_MS = 3e4;
153
+ function createThrottleAuth(config) {
154
+ const baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
155
+ const storage = config.storage ?? localStorageAdapter();
156
+ const storageKey = `throttle_auth_rt:${config.publishableKey}`;
157
+ const lockName = `throttle_auth_refresh:${config.publishableKey}`;
158
+ const state = new SessionState();
159
+ let refreshInFlight = null;
160
+ let stepUpToken = null;
161
+ let stepUpExpiresAt = 0;
162
+ const base = { baseUrl, publishableKey: config.publishableKey };
163
+ async function persist(session) {
164
+ state.setSession(session);
165
+ await storage.set(storageKey, session.refreshToken);
166
+ }
167
+ async function forget() {
168
+ stepUpToken = null;
169
+ state.clear();
170
+ await storage.remove(storageKey);
171
+ }
172
+ function withLock(fn) {
173
+ const locks = globalThis.navigator?.locks;
174
+ return locks?.request ? locks.request(lockName, fn) : fn();
175
+ }
176
+ async function doRefresh() {
177
+ await withLock(async () => {
178
+ const refreshToken = await storage.get(storageKey);
179
+ if (!refreshToken) {
180
+ await forget();
181
+ throw new ThrottleAuthError({ code: "session_revoked", message: "No session", statusCode: 401 });
182
+ }
183
+ try {
184
+ const data = await request({ ...base, method: "POST", path: "/auth/refresh", body: { refreshToken } });
185
+ await persist(data);
186
+ } catch (err) {
187
+ await forget();
188
+ throw err;
189
+ }
190
+ });
191
+ }
192
+ function refresh() {
193
+ if (!refreshInFlight) {
194
+ refreshInFlight = doRefresh().finally(() => {
195
+ refreshInFlight = null;
196
+ });
197
+ }
198
+ return refreshInFlight;
199
+ }
200
+ async function accessToken() {
201
+ const expires = state.expiresAt();
202
+ if (state.accessToken() && expires && expires - Date.now() > REFRESH_SKEW_MS) {
203
+ return state.accessToken();
204
+ }
205
+ if (await storage.get(storageKey)) {
206
+ await refresh();
207
+ return state.accessToken();
208
+ }
209
+ return state.accessToken();
210
+ }
211
+ async function ensureStepUp() {
212
+ if (stepUpToken && stepUpExpiresAt > Date.now()) return true;
213
+ if (!config.onStepUpRequired) return false;
214
+ const password = await config.onStepUpRequired();
215
+ if (!password) return false;
216
+ await stepUp(password);
217
+ return true;
218
+ }
219
+ async function stepUp(password) {
220
+ const data = await request({
221
+ ...base,
222
+ method: "POST",
223
+ path: "/auth/step-up",
224
+ accessToken: await accessToken() ?? void 0,
225
+ body: { password }
226
+ });
227
+ stepUpToken = data.stepUpToken;
228
+ stepUpExpiresAt = Date.now() + (Number(data.expiresIn) || 300) * 1e3 - 5e3;
229
+ }
230
+ async function call(method, path, body) {
231
+ let retriedAuth = false;
232
+ let retriedStepUp = false;
233
+ for (; ; ) {
234
+ const token = await accessToken();
235
+ try {
236
+ return await request({
237
+ ...base,
238
+ method,
239
+ path,
240
+ body,
241
+ accessToken: token ?? void 0,
242
+ stepUpToken: stepUpToken ?? void 0
243
+ });
244
+ } catch (err) {
245
+ const e = err;
246
+ const expired = e.statusCode === 401 && (e.code === "unauthorized" || e.code === "token_expired" || e.code === "session_revoked");
247
+ if (expired && !retriedAuth) {
248
+ retriedAuth = true;
249
+ await refresh();
250
+ continue;
251
+ }
252
+ if (e.code === "step_up_required" && !retriedStepUp) {
253
+ retriedStepUp = true;
254
+ stepUpToken = null;
255
+ if (await ensureStepUp()) continue;
256
+ }
257
+ throw err;
258
+ }
259
+ }
260
+ }
261
+ return {
262
+ async register(email, password) {
263
+ const data = await request({ ...base, method: "POST", path: "/auth/register", body: { email, password } });
264
+ if (data?.status === "pending") return { status: "pending" };
265
+ await persist(data);
266
+ return { status: "active" };
267
+ },
268
+ async signIn(email, password) {
269
+ const data = await request({ ...base, method: "POST", path: "/auth/login", body: { email, password } });
270
+ await persist(data);
271
+ },
272
+ async signOut() {
273
+ try {
274
+ await call("POST", "/auth/logout");
275
+ } catch {
276
+ }
277
+ await forget();
278
+ },
279
+ async signOutEverywhere() {
280
+ try {
281
+ await call("POST", "/auth/logout-all");
282
+ } catch {
283
+ }
284
+ await forget();
285
+ },
286
+ async restore() {
287
+ if (!await storage.get(storageKey)) {
288
+ state.setStatus("unauthenticated");
289
+ return;
290
+ }
291
+ try {
292
+ await refresh();
293
+ } catch {
294
+ }
295
+ },
296
+ getSnapshot: () => state.get(),
297
+ subscribe: (fn) => state.subscribe(fn),
298
+ stepUp,
299
+ call,
300
+ async verifyEmail(token) {
301
+ await request({ ...base, method: "POST", path: "/auth/verify-email", body: { token } });
302
+ if (await storage.get(storageKey)) {
303
+ try {
304
+ await refresh();
305
+ } catch {
306
+ }
307
+ }
308
+ },
309
+ async resendVerification() {
310
+ await call("POST", "/auth/verify-email/resend");
311
+ },
312
+ async forgotPassword(email) {
313
+ await request({ ...base, method: "POST", path: "/auth/forgot-password", body: { email } });
314
+ },
315
+ async resetPassword(token, password) {
316
+ await request({ ...base, method: "POST", path: "/auth/reset-password", body: { token, password } });
317
+ await forget();
318
+ },
319
+ async changePassword(password) {
320
+ await call("POST", "/me/change-password", { password });
321
+ await refresh();
322
+ },
323
+ async listSessions() {
324
+ return call("GET", "/me/sessions");
325
+ },
326
+ async revokeSession(id) {
327
+ const mine = await call("GET", "/me/sessions");
328
+ const current = mine.find((s) => s.current);
329
+ await call("DELETE", `/me/sessions/${encodeURIComponent(id)}`);
330
+ if (current?.id === id) await forget();
331
+ }
332
+ };
333
+ }
334
+
335
+ // src/account.ts
336
+ var seg = (v) => encodeURIComponent(v);
337
+ function query(page) {
338
+ const params = new URLSearchParams();
339
+ if (page?.cursor) params.set("cursor", page.cursor);
340
+ if (page?.limit != null) params.set("limit", String(page.limit));
341
+ const s = params.toString();
342
+ return s ? `?${s}` : "";
343
+ }
344
+ function createAccount(auth) {
345
+ const { call } = auth;
346
+ return {
347
+ getProfile: () => call("GET", "/me"),
348
+ updateProfile: (patch) => call("PATCH", "/me", patch),
349
+ listAddresses: () => call("GET", "/me/addresses"),
350
+ createAddress: (input) => call("POST", "/me/addresses", input),
351
+ updateAddress: (id, patch) => call("PATCH", `/me/addresses/${seg(id)}`, patch),
352
+ deleteAddress: (id) => call("DELETE", `/me/addresses/${seg(id)}`),
353
+ listPaymentMethods: () => call("GET", "/me/payment-methods"),
354
+ // The platform has no "change this subscription's card" capability, so
355
+ // this is how a buyer changes what they are billed on: move the default.
356
+ setDefaultPaymentMethod: (id) => call("PATCH", `/me/payment-methods/${seg(id)}`, { isDefault: true }),
357
+ deletePaymentMethod: (id) => call("DELETE", `/me/payment-methods/${seg(id)}`),
358
+ listOrders: (page) => call("GET", `/me/orders${query(page)}`),
359
+ getOrder: (id) => call("GET", `/me/orders/${seg(id)}`),
360
+ listInvoices: (page) => call("GET", `/me/invoices${query(page)}`),
361
+ getInvoice: (id) => call("GET", `/me/invoices/${seg(id)}`),
362
+ listSubscriptions: (page) => call("GET", `/me/subscriptions${query(page)}`),
363
+ getSubscription: (id) => call("GET", `/me/subscriptions/${seg(id)}`),
364
+ cancelSubscription: (id, opts) => call("POST", `/me/subscriptions/${seg(id)}/cancel`, opts),
365
+ pauseSubscription: (id) => call("POST", `/me/subscriptions/${seg(id)}/pause`),
366
+ resumeSubscription: (id) => call("POST", `/me/subscriptions/${seg(id)}/resume`),
367
+ claimCart: (cartId) => call("POST", `/me/carts/${seg(cartId)}/claim`)
368
+ };
369
+ }
370
+
371
+ export {
372
+ ThrottleAuthError,
373
+ memoryStorage,
374
+ localStorageAdapter,
375
+ createThrottleAuth,
376
+ createAccount
377
+ };
378
+ //# sourceMappingURL=chunk-NQU7HM23.js.map