@fanvue/builder-sdk 0.3.0 → 0.5.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.
Files changed (39) hide show
  1. package/README.md +361 -1
  2. package/dist/bridge/index.d.ts +2 -0
  3. package/dist/bridge/index.js +3 -0
  4. package/dist/bridge-QnF7P4Co.js +336 -0
  5. package/dist/bridge-QnF7P4Co.js.map +1 -0
  6. package/dist/core/index.d.ts +2 -2
  7. package/dist/core/index.js +3 -2
  8. package/dist/core-BhKiA55a.js +2962 -0
  9. package/dist/core-BhKiA55a.js.map +1 -0
  10. package/dist/index-C3CXrRiw.d.ts +1412 -0
  11. package/dist/index-C3CXrRiw.d.ts.map +1 -0
  12. package/dist/index-CEFYZtlb.d.ts +223 -0
  13. package/dist/index-CEFYZtlb.d.ts.map +1 -0
  14. package/dist/{index-pS9wR5yg.d.ts → index-cf3ZMnLJ.d.ts} +524 -901
  15. package/dist/index-cf3ZMnLJ.d.ts.map +1 -0
  16. package/dist/index-v_6Z4Q0u.d.ts +84 -0
  17. package/dist/index-v_6Z4Q0u.d.ts.map +1 -0
  18. package/dist/{core-CvVOMyqr.js → index.cjs-teGsk6HB.js} +1506 -2826
  19. package/dist/index.cjs-teGsk6HB.js.map +1 -0
  20. package/dist/nextjs/embedded-app/index.d.ts +2 -2
  21. package/dist/nextjs/embedded-app/index.js +3 -2
  22. package/dist/nextjs/embedded-app/index.js.map +1 -1
  23. package/dist/nextjs/off-platform/index.d.ts +3 -3
  24. package/dist/nextjs/off-platform/index.d.ts.map +1 -1
  25. package/dist/nextjs/off-platform/index.js +4 -3
  26. package/dist/nextjs/off-platform/index.js.map +1 -1
  27. package/dist/nextjs-CvVhtMdb.js +144 -0
  28. package/dist/nextjs-CvVhtMdb.js.map +1 -0
  29. package/dist/react/index.d.ts +37 -2
  30. package/dist/react/index.d.ts.map +1 -1
  31. package/dist/react/index.js +61 -2
  32. package/dist/react/index.js.map +1 -1
  33. package/package.json +20 -16
  34. package/dist/core-CvVOMyqr.js.map +0 -1
  35. package/dist/index-CweVyIKX.d.ts +0 -48
  36. package/dist/index-CweVyIKX.d.ts.map +0 -1
  37. package/dist/index-pS9wR5yg.d.ts.map +0 -1
  38. package/dist/nextjs-CESI_EiU.js +0 -80
  39. package/dist/nextjs-CESI_EiU.js.map +0 -1
@@ -0,0 +1,2962 @@
1
+ import { a as discriminatedUnion, c as number, f as string, h as url, i as boolean, l as object, n as _enum, o as literal, p as union, r as array, s as looseObject, t as require_index_cjs, u as preprocess } from "./index.cjs-teGsk6HB.js";
2
+ //#region src/core/constants.ts
3
+ /** The `Bearer ` token prefix (includes trailing space). */
4
+ const BEARER_PREFIX = "Bearer ";
5
+ /** Response header carrying a refreshed session JWT back to the client. */
6
+ const HEADER_UPDATED_SESSION = "X-Updated-Session";
7
+ //#endregion
8
+ //#region src/core/contracts/access-mode.ts
9
+ /**
10
+ * Experience access modes and the entitlement reasons the platform reports
11
+ * alongside them.
12
+ *
13
+ * The platform's own identifier for this enum is `AccessMode`;
14
+ * {@link FanvueAccessMode} is the SDK alias, named so it does not collide with
15
+ * an app's own access-mode types.
16
+ */
17
+ /**
18
+ * Access modes an experience can propose or carry on Fanvue.
19
+ *
20
+ * - `FREE` — visible and playable by anyone.
21
+ * - `SUBSCRIPTION` — requires an active subscription to the creator.
22
+ * - `PAID` — requires a one-off purchase.
23
+ * - `HIDDEN` — reachable only through a share grant, never listed.
24
+ */
25
+ const FANVUE_ACCESS_MODES = [
26
+ "FREE",
27
+ "SUBSCRIPTION",
28
+ "PAID",
29
+ "HIDDEN"
30
+ ];
31
+ /** Zod schema for {@link FanvueAccessMode}. */
32
+ const FanvueAccessModeSchema = _enum(FANVUE_ACCESS_MODES);
33
+ /**
34
+ * Every reason the platform reports when a fan **is** entitled to an
35
+ * experience, as emitted by fanguard's `resolveExperienceAccess`.
36
+ *
37
+ * Documented for completeness and for exhaustive logging/analytics; apps should
38
+ * branch on `entitlement.isEntitled` rather than on a specific reason, because
39
+ * this list grows without an API version bump.
40
+ *
41
+ * - `owner` — the requester is the creator who owns the experience.
42
+ * - `entitled_via_free`, `free` — the experience is `FREE`.
43
+ * - `entitled_via_subscription`, `subscribed` — active creator subscription.
44
+ * - `entitled_via_share` — reached through a share grant.
45
+ * - `purchased` — one-off purchase of the experience.
46
+ * - `app_subscribed` — subscription to the app itself.
47
+ * - `purchased_before_mode_change`, `app_subscribed_before_mode_change` —
48
+ * entitlement grandfathered from before the creator changed the access mode.
49
+ * - `hidden_token_exchange` — the documented exemption to the fail-closed rule:
50
+ * a `HIDDEN` experience exchanges its token without a prior grant.
51
+ */
52
+ const EXPERIENCE_ENTITLED_REASONS = [
53
+ "owner",
54
+ "entitled_via_free",
55
+ "entitled_via_share",
56
+ "entitled_via_subscription",
57
+ "free",
58
+ "subscribed",
59
+ "purchased",
60
+ "app_subscribed",
61
+ "purchased_before_mode_change",
62
+ "app_subscribed_before_mode_change",
63
+ "hidden_token_exchange"
64
+ ];
65
+ /**
66
+ * Every reason the platform reports when a fan is **denied** an experience.
67
+ *
68
+ * These are the only three values fanguard emits today; the type stays open for
69
+ * forward compatibility.
70
+ */
71
+ const EXPERIENCE_DENIAL_REASONS = [
72
+ "not_subscribed",
73
+ "not_purchased",
74
+ "no_share_grant"
75
+ ];
76
+ /**
77
+ * Recovers the access mode a denied experience must be in, from the denial
78
+ * reason.
79
+ *
80
+ * The token exchange fails closed: an unentitled fan gets a 403 carrying only a
81
+ * denial `reason`, and the experience payload — with it the access mode — is
82
+ * withheld. Mapping the reason back is the only way a locked screen can show
83
+ * the right copy ("subscribe" vs "buy" vs "ask for the link"). An unrecognised
84
+ * reason maps to `null`, meaning "show the generic locked copy".
85
+ *
86
+ * @param reason - The `reason` field from the platform's 403 body.
87
+ * @returns The implied access mode, or `null` when the reason is unrecognised.
88
+ * @example
89
+ * accessModeFromDenialReason('not_subscribed'); // 'SUBSCRIPTION'
90
+ * accessModeFromDenialReason('not_purchased'); // 'PAID'
91
+ * accessModeFromDenialReason('no_share_grant'); // 'HIDDEN'
92
+ * accessModeFromDenialReason('something_new'); // null
93
+ */
94
+ function accessModeFromDenialReason(reason) {
95
+ switch (reason) {
96
+ case "not_subscribed": return "SUBSCRIPTION";
97
+ case "not_purchased": return "PAID";
98
+ case "no_share_grant": return "HIDDEN";
99
+ default: return null;
100
+ }
101
+ }
102
+ //#endregion
103
+ //#region src/core/defaults.ts
104
+ /** Default OAuth scopes requested during authorization. */
105
+ const DEFAULT_SCOPES = "openid offline_access offline";
106
+ /** Default OAuth issuer URL for Fanvue authentication. */
107
+ const DEFAULT_ISSUER_URL = "https://auth.fanvue.com";
108
+ /** Default base URL for the Fanvue API. */
109
+ const DEFAULT_API_BASE_URL = "https://api.fanvue.com";
110
+ /** Default base URL for the Fanvue platform (embedded authorize-on-behalf). */
111
+ const DEFAULT_PLATFORM_URL = "https://www.fanvue.com";
112
+ /** The Fanvue API version header value sent with every request. */
113
+ const API_VERSION = "2025-06-26";
114
+ /**
115
+ * Validates that a URL belongs to the `fanvue.com` domain.
116
+ *
117
+ * @param url - The URL string to validate.
118
+ * @throws If the URL is malformed or its hostname is not `fanvue.com`
119
+ * (or a subdomain of it).
120
+ */
121
+ function assertFanvueDomain(url) {
122
+ let hostname;
123
+ try {
124
+ hostname = new URL(url).hostname;
125
+ } catch {
126
+ throw new Error(`Invalid apiBaseUrl: "${url}" is not a valid URL.`);
127
+ }
128
+ if (hostname !== "fanvue.com" && !hostname.endsWith(".fanvue.com")) throw new Error(`Invalid apiBaseUrl: "${url}" must be a fanvue.com domain (e.g. https://api.fanvue.com).`);
129
+ }
130
+ //#endregion
131
+ //#region src/core/contracts/env.ts
132
+ /**
133
+ * The canonical `FANVUE_*` environment contract.
134
+ *
135
+ * Every app that embeds on Fanvue reads the same nine variables and, before
136
+ * this module, validated them with nine slightly different schemas — one repo
137
+ * had two incompatible ones in the same process, each with its own cache, so
138
+ * whichever accessor ran first froze its own view of `process.env`.
139
+ *
140
+ * Two deliberate properties, both inherited from the app that got them right:
141
+ *
142
+ * - **Lazily parsed and cached.** A build must succeed without secrets, so
143
+ * nothing is read at import time; a bad value surfaces as a runtime failure on
144
+ * first use rather than at boot.
145
+ * - **Credentials are schema-optional.** `FANVUE_APP_UUID`, `FANVUE_CLIENT_ID`,
146
+ * `FANVUE_CLIENT_SECRET` and `FANVUE_OAUTH_REDIRECT_URI` default to `""`
147
+ * instead of being required, so a preview deploy or a local checkout boots
148
+ * without them. {@link isFanvueConfigured} is the separate, explicit check
149
+ * callers run before touching an OAuth path.
150
+ */
151
+ /**
152
+ * Treats a blank string as unset.
153
+ *
154
+ * `.env` templates ship lines like `FANVUE_API_BASE_URL=`, and a platform
155
+ * dashboard saves an emptied field as `""` rather than deleting it. For a
156
+ * variable with a real default, blank must mean "use the default" — a literal
157
+ * empty base URL would turn every request into a relative fetch.
158
+ */
159
+ function blankToUndefined(value) {
160
+ return typeof value === "string" && value.trim() === "" ? void 0 : value;
161
+ }
162
+ /**
163
+ * Zod schema for the `FANVUE_*` environment contract.
164
+ *
165
+ * - `FANVUE_APP_UUID` — the app's UUID on Fanvue. Checked against the `appUuid`
166
+ * an exchanged experience token carries, so a token minted for another app is
167
+ * rejected. Defaults to `""`.
168
+ * - `FANVUE_CLIENT_ID` / `FANVUE_CLIENT_SECRET` / `FANVUE_OAUTH_REDIRECT_URI` —
169
+ * OAuth client credentials. Default to `""`; see {@link isFanvueConfigured}.
170
+ * - `FANVUE_API_BASE_URL` — bare origin of the Fanvue API (no `/v0` suffix).
171
+ * Blank falls back to the default.
172
+ * - `FANVUE_AUTH_BASE_URL` — origin of the Fanvue authorization server. Blank
173
+ * falls back to the default.
174
+ * - `FANVUE_WEB_ORIGIN` — origin of the Fanvue web shell. The only variable
175
+ * with real URL validation, because it is concatenated into hrefs a creator
176
+ * or fan clicks: a schemeless value ships a dead link. Blank falls back to
177
+ * the default. Note this does not constrain the scheme.
178
+ * - `FANVUE_API_VERSION` — value sent as `X-Fanvue-API-Version`. Blank falls
179
+ * back to the default.
180
+ * - `FANVUE_EXPERIENCE_URL_TEMPLATE` — optional operator override for
181
+ * per-experience share links. Empty means "derive the URL".
182
+ */
183
+ const FanvueEnvSchema = object({
184
+ FANVUE_APP_UUID: string().default(""),
185
+ FANVUE_CLIENT_ID: string().default(""),
186
+ FANVUE_CLIENT_SECRET: string().default(""),
187
+ FANVUE_OAUTH_REDIRECT_URI: string().default(""),
188
+ FANVUE_API_BASE_URL: preprocess(blankToUndefined, string().default(DEFAULT_API_BASE_URL)),
189
+ FANVUE_AUTH_BASE_URL: preprocess(blankToUndefined, string().default(DEFAULT_ISSUER_URL)),
190
+ FANVUE_WEB_ORIGIN: preprocess(blankToUndefined, url().default(DEFAULT_PLATFORM_URL)),
191
+ FANVUE_API_VERSION: preprocess(blankToUndefined, string().default(API_VERSION)),
192
+ FANVUE_EXPERIENCE_URL_TEMPLATE: string().default("")
193
+ });
194
+ /**
195
+ * Validates an environment bag against {@link FanvueEnvSchema}.
196
+ *
197
+ * Pure and uncached — takes the environment as a parameter so it is unit
198
+ * testable. Prefer {@link fanvueEnv} in application code.
199
+ *
200
+ * @param source - The variables to validate.
201
+ * @returns The validated environment.
202
+ * @throws If a present value fails validation (today: a non-URL
203
+ * `FANVUE_WEB_ORIGIN`).
204
+ */
205
+ function parseFanvueEnv(source) {
206
+ return FanvueEnvSchema.parse(source);
207
+ }
208
+ let cachedEnv = null;
209
+ const warnedFlags = /* @__PURE__ */ new Set();
210
+ /**
211
+ * Returns the validated `FANVUE_*` environment, parsing `process.env` on first
212
+ * use and caching the result.
213
+ *
214
+ * @returns The validated environment.
215
+ * @throws If a present value fails validation.
216
+ * @example
217
+ * const baseUrl = fanvueEnv().FANVUE_API_BASE_URL;
218
+ */
219
+ function fanvueEnv() {
220
+ if (cachedEnv === null) cachedEnv = parseFanvueEnv(process.env);
221
+ return cachedEnv;
222
+ }
223
+ /**
224
+ * Clears every piece of memoised state in this module: the {@link fanvueEnv}
225
+ * cache and {@link flagEnabled}'s warned-flag set.
226
+ *
227
+ * Intended for tests, which mutate `process.env` between cases and would
228
+ * otherwise see whichever value the first case happened to freeze. The warn-once
229
+ * set is cleared for the same reason — left in place, whether a case sees the
230
+ * warning depends on which case ran first.
231
+ */
232
+ function resetFanvueEnvCache() {
233
+ cachedEnv = null;
234
+ warnedFlags.clear();
235
+ }
236
+ /**
237
+ * Whether the four Fanvue credential variables are all set.
238
+ *
239
+ * Covers exactly the values OAuth configuration requires, because config
240
+ * creation throws on a missing one and an uncaught throw in a route surfaces as
241
+ * a 500. Call this first and return a "not configured" response instead.
242
+ *
243
+ * @param env - The environment to check. Defaults to the cached
244
+ * {@link fanvueEnv}.
245
+ * @returns `true` when app UUID, client id, client secret and redirect URI are
246
+ * all non-blank. Whitespace-only counts as unset, matching how the schema
247
+ * treats blank values everywhere else in this module.
248
+ * @example
249
+ * if (!isFanvueConfigured()) {
250
+ * return Response.json({ error: { code: 'fanvue_not_configured', message: '…' } }, { status: 503 });
251
+ * }
252
+ */
253
+ function isFanvueConfigured(env = fanvueEnv()) {
254
+ return env.FANVUE_APP_UUID.trim() !== "" && env.FANVUE_CLIENT_ID.trim() !== "" && env.FANVUE_CLIENT_SECRET.trim() !== "" && env.FANVUE_OAUTH_REDIRECT_URI.trim() !== "";
255
+ }
256
+ /**
257
+ * Reads a boolean environment flag.
258
+ *
259
+ * A strict `=== "true"` is how one app's `MCP_ENABLED="TRUE"` switched a whole
260
+ * subsystem off in staging: every route answered 404 and the value looked
261
+ * correct in the dashboard. A flag that fails closed on a casing typo and says
262
+ * nothing costs more than one that is simply off, because there is no signal to
263
+ * find. So surrounding whitespace and case do not matter, and a value that
264
+ * still cannot be read as a boolean is warned about **once per flag name** and
265
+ * treated as off — off is the safe answer, but not a silent one.
266
+ *
267
+ * @param name - The environment variable name.
268
+ * @param source - The variables to read from. Defaults to `process.env`.
269
+ * @returns `true` only for `"true"` (after trimming and lowercasing).
270
+ * @see {@link resetFanvueEnvCache} to clear the warned-flag set between tests.
271
+ * @example
272
+ * flagEnabled('MY_APP_DEV_MODE'); // reads process.env.MY_APP_DEV_MODE
273
+ * flagEnabled('MY_APP_DEV_MODE', { MY_APP_DEV_MODE: ' TRUE ' }); // true
274
+ */
275
+ function flagEnabled(name, source = process.env) {
276
+ const raw = source[name];
277
+ const value = (raw ?? "").trim().toLowerCase();
278
+ if (value === "true") return true;
279
+ if (value !== "false" && value !== "" && !warnedFlags.has(name)) {
280
+ warnedFlags.add(name);
281
+ console.warn(`${name} is set to ${JSON.stringify(raw)}, which is not true or false. Treating it as off.`);
282
+ }
283
+ return false;
284
+ }
285
+ //#endregion
286
+ //#region src/core/contracts/errors.ts
287
+ /**
288
+ * Error contracts: the shapes the Fanvue API puts on the wire, the envelope an
289
+ * app puts on its own responses, and the codes that travel between them.
290
+ *
291
+ * The platform has **no single error envelope**. Four shapes are in production
292
+ * use, and a machine-readable `reason` rides alongside the human text on the
293
+ * experience-exchange 403 — losing it collapses "this fan is not entitled" into
294
+ * "this token belongs to another app". {@link parseFanvueErrorBody} normalises
295
+ * all four into one struct without discarding `reason`.
296
+ */
297
+ /**
298
+ * `{ message }` — 404s, contactability rejections, invalid creator UUIDs, and
299
+ * the experience-exchange 403 (which adds `reason`). `code` appears on
300
+ * bulk-media per-item errors.
301
+ */
302
+ const FanvueMessageErrorBodySchema = looseObject({
303
+ message: string(),
304
+ reason: string().nullish().catch(null),
305
+ code: string().nullish().catch(null)
306
+ });
307
+ /**
308
+ * `{ error }` — 401/403/500/503 and upstream tRPC failures, where `error` is the
309
+ * human text; and `{ error, message }` — version and age-verification failures,
310
+ * where `error` is a machine-readable code and `message` the human text.
311
+ */
312
+ const FanvueErrorFieldBodySchema = looseObject({
313
+ error: string(),
314
+ message: string().nullish().catch(null),
315
+ reason: string().nullish().catch(null)
316
+ });
317
+ /** `{ errors: [{ code, message }] }` — Zod request-validation failures. */
318
+ const FanvueIssueListErrorBodySchema = looseObject({ errors: array(looseObject({
319
+ code: string().nullish().catch(null),
320
+ message: string()
321
+ })).min(1) });
322
+ /** `{ errors: ["Invalid page: too small"] }` — the pagination middleware. */
323
+ const FanvueStringListErrorBodySchema = looseObject({ errors: array(string()).min(1) });
324
+ /**
325
+ * Any of the four documented Fanvue API error bodies.
326
+ *
327
+ * Member order matters: the `errors` array shapes are tried first because they
328
+ * carry no `message`/`error` key of their own, then `{ error }` (which may also
329
+ * carry a `message`), then `{ message }` alone. Exported for callers that want
330
+ * to validate a body in one step; {@link parseFanvueErrorBody} tries the members
331
+ * individually so that one unreadable field cannot cost the whole body.
332
+ */
333
+ const FanvueErrorBodySchema = union([
334
+ FanvueStringListErrorBodySchema,
335
+ FanvueIssueListErrorBodySchema,
336
+ FanvueErrorFieldBodySchema,
337
+ FanvueMessageErrorBodySchema
338
+ ]);
339
+ /**
340
+ * Normalises any Fanvue API error body into {@link NormalisedFanvueError}.
341
+ *
342
+ * Never throws and never inspects the HTTP status: a body it cannot read yields
343
+ * an empty `message`, which the transport replaces with its own status-derived
344
+ * text. Multiple validation issues are joined with `; ` so a single log line
345
+ * carries all of them.
346
+ *
347
+ * The `{ error }` shape is disambiguated by whether a `message` sits next to it:
348
+ * `{ error: 'age_verification_required', message: 'Verify…' }` puts the code in
349
+ * `code` and the text in `message`, while a bare `{ error: 'Unauthorized' }` is
350
+ * treated as text only.
351
+ *
352
+ * @param body - A parsed JSON body (or anything at all).
353
+ * @returns The normalised error fields.
354
+ * @example
355
+ * parseFanvueErrorBody({ message: 'Fan is not entitled', reason: 'not_subscribed' });
356
+ * // { message: 'Fan is not entitled', reason: 'not_subscribed', code: null }
357
+ * parseFanvueErrorBody({ errors: [{ code: 'invalid_type', message: 'Expected string' }] });
358
+ * // { message: 'Expected string', reason: null, code: 'invalid_type' }
359
+ * parseFanvueErrorBody('<html>502</html>');
360
+ * // { message: '', reason: null, code: null }
361
+ */
362
+ function parseFanvueErrorBody(body) {
363
+ const stringList = FanvueStringListErrorBodySchema.safeParse(body);
364
+ if (stringList.success) return {
365
+ message: stringList.data.errors.join("; "),
366
+ reason: null,
367
+ code: null
368
+ };
369
+ const issueList = FanvueIssueListErrorBodySchema.safeParse(body);
370
+ if (issueList.success) {
371
+ const issues = issueList.data.errors;
372
+ return {
373
+ message: issues.map((issue) => issue.message).join("; "),
374
+ reason: null,
375
+ code: issues.find((issue) => typeof issue.code === "string")?.code ?? null
376
+ };
377
+ }
378
+ const errorField = FanvueErrorFieldBodySchema.safeParse(body);
379
+ if (errorField.success) {
380
+ const { error, message, reason } = errorField.data;
381
+ return {
382
+ message: message ?? error,
383
+ reason: reason ?? null,
384
+ code: typeof message === "string" ? error : null
385
+ };
386
+ }
387
+ const messageBody = FanvueMessageErrorBodySchema.safeParse(body);
388
+ if (messageBody.success) return {
389
+ message: messageBody.data.message,
390
+ reason: messageBody.data.reason ?? null,
391
+ code: messageBody.data.code ?? null
392
+ };
393
+ return {
394
+ message: "",
395
+ reason: null,
396
+ code: null
397
+ };
398
+ }
399
+ /**
400
+ * Zod schema for an app's own error envelope: `{ error: { code, message } }`.
401
+ *
402
+ * Every Fanvue app helper produces this shape and every client parses it, so it
403
+ * is a contract rather than a convention. The `message` is what an end user
404
+ * reads — upstream and internal text belongs in the log, never in this body.
405
+ */
406
+ const AppErrorEnvelopeSchema = object({ error: object({
407
+ code: string().min(1),
408
+ message: string()
409
+ }) });
410
+ /**
411
+ * Canonical app-facing error codes for Fanvue-grant failures.
412
+ *
413
+ * - `fanvue_reconnect_required` (401) — the stored Fanvue grant is missing or
414
+ * dead. The creator must reopen the app from Fanvue.
415
+ * - `fanvue_permission_denied` (403) — the grant is alive but lacks the scope
416
+ * for this capability. Reconnecting re-consents the new scope.
417
+ * - `fanvue_api_error` (502) — the Fanvue API failed for any other reason.
418
+ */
419
+ const FANVUE_APP_ERROR_CODES = {
420
+ reconnectRequired: "fanvue_reconnect_required",
421
+ permissionDenied: "fanvue_permission_denied",
422
+ apiError: "fanvue_api_error"
423
+ };
424
+ /**
425
+ * The two 401 codes that mean the **Fanvue grant** is gone while the **app
426
+ * session** is still valid.
427
+ *
428
+ * A client that treats every 401 as session expiry signs the creator out of a
429
+ * working session and offers "Reload" — the one action that cannot restore a
430
+ * missing grant. Check membership of this set before running expiry handling,
431
+ * and route these to a reconnect prompt instead.
432
+ *
433
+ * @example
434
+ * if (status === 401 && !NON_SESSION_401_CODES.has(code)) {
435
+ * onSessionExpired();
436
+ * }
437
+ */
438
+ const NON_SESSION_401_CODES = new Set([FANVUE_APP_ERROR_CODES.reconnectRequired, FANVUE_APP_ERROR_CODES.permissionDenied]);
439
+ /**
440
+ * Zod schema for an OAuth 2.0 error response body, normalised to camelCase.
441
+ *
442
+ * Only `error` and `error_description` are kept. Hydra also returns
443
+ * `error_hint` and `error_debug`, which can echo request details back and have
444
+ * no place in an app's own error path.
445
+ */
446
+ const OAuthErrorBodySchema = looseObject({
447
+ error: string().min(1),
448
+ error_description: string().nullish()
449
+ }).transform((raw) => ({
450
+ error: raw.error,
451
+ errorDescription: raw.error_description ?? null
452
+ }));
453
+ //#endregion
454
+ //#region src/core/contracts/experience-protocol.ts
455
+ /**
456
+ * The `fanvue:experience:*` postMessage protocol between an embedded app and
457
+ * the Fanvue web shell (Eden).
458
+ *
459
+ * Publishing is Fanvue-mediated: the app's backend mints an opaque request
460
+ * token from the Fanvue API (`POST /v0/experiences/request-token`, scope
461
+ * `write:experience`), the iframe posts `{ type, token }` to the parent
462
+ * window, Fanvue verifies the token server-side and shows its native
463
+ * "Configure Experience" modal, then replies with a result message. An app
464
+ * never mutates Fanvue experience state directly.
465
+ *
466
+ * This module is the wire contract only — schemas, type guards and origin
467
+ * validation. The browser bridge that drives the handshake (timeouts, escalation
468
+ * to `window.top`, listener cleanup) lives outside this module.
469
+ */
470
+ /** Message type an app posts to request the publish modal. */
471
+ const PUBLISH_REQUEST_MESSAGE = "fanvue:experience:publish-request";
472
+ /** Message type Fanvue posts back with the outcome of a publish request. */
473
+ const PUBLISH_RESULT_MESSAGE = "fanvue:experience:publish-result";
474
+ /** Message type an app posts to request the unpublish confirmation. */
475
+ const UNPUBLISH_REQUEST_MESSAGE = "fanvue:experience:unpublish-request";
476
+ /** Message type Fanvue posts back with the outcome of an unpublish request. */
477
+ const UNPUBLISH_RESULT_MESSAGE = "fanvue:experience:unpublish-result";
478
+ /**
479
+ * Every message type in the protocol, in request/result pairs.
480
+ *
481
+ * Useful for filtering an incoming `MessageEvent` before schema validation.
482
+ */
483
+ const EXPERIENCE_MESSAGE_TYPES = [
484
+ PUBLISH_REQUEST_MESSAGE,
485
+ PUBLISH_RESULT_MESSAGE,
486
+ UNPUBLISH_REQUEST_MESSAGE,
487
+ UNPUBLISH_RESULT_MESSAGE
488
+ ];
489
+ /**
490
+ * Zod schema for the app → Fanvue publish request.
491
+ *
492
+ * `token` is the opaque request token minted by
493
+ * `POST /v0/experiences/request-token` with `action: "publish"`.
494
+ */
495
+ const PublishRequestMessageSchema = object({
496
+ type: literal(PUBLISH_REQUEST_MESSAGE),
497
+ token: string().min(1)
498
+ });
499
+ /**
500
+ * Zod schema for the app → Fanvue unpublish request.
501
+ *
502
+ * `token` is the opaque request token minted by
503
+ * `POST /v0/experiences/request-token` with `action: "unpublish"`.
504
+ */
505
+ const UnpublishRequestMessageSchema = object({
506
+ type: literal(UNPUBLISH_REQUEST_MESSAGE),
507
+ token: string().min(1)
508
+ });
509
+ /**
510
+ * Zod schema for the Fanvue → app publish result.
511
+ *
512
+ * `experienceId` is optional rather than nullable because it mirrors the wire
513
+ * shape: Fanvue omits the key entirely when the creator cancels. `cancelled`
514
+ * covers both a dismissed modal and a server-side rejection of the request
515
+ * token — the two are not distinguishable from this side.
516
+ *
517
+ * Any string is accepted, including `""`. A receiver must not reject an
518
+ * otherwise well-formed result over a useless id: from the app's side a rejected
519
+ * message is indistinguishable from no reply at all, so a publish that did
520
+ * happen would hang until the bridge timed out. Treat a falsy `experienceId` the
521
+ * way an absent one is treated.
522
+ */
523
+ const PublishResultMessageSchema = object({
524
+ type: literal(PUBLISH_RESULT_MESSAGE),
525
+ status: _enum(["published", "cancelled"]),
526
+ experienceId: string().optional()
527
+ });
528
+ /** Zod schema for the Fanvue → app unpublish result. */
529
+ const UnpublishResultMessageSchema = object({
530
+ type: literal(UNPUBLISH_RESULT_MESSAGE),
531
+ status: _enum(["unpublished", "cancelled"])
532
+ });
533
+ /**
534
+ * Zod schema for any message in the protocol, discriminated on `type`.
535
+ *
536
+ * @example
537
+ * const parsed = ExperienceMessageSchema.safeParse(event.data);
538
+ * if (parsed.success && parsed.data.type === PUBLISH_RESULT_MESSAGE) {
539
+ * // parsed.data is a PublishResultMessage
540
+ * }
541
+ */
542
+ const ExperienceMessageSchema = discriminatedUnion("type", [
543
+ PublishRequestMessageSchema,
544
+ PublishResultMessageSchema,
545
+ UnpublishRequestMessageSchema,
546
+ UnpublishResultMessageSchema
547
+ ]);
548
+ /**
549
+ * Narrows unknown `MessageEvent` data to a {@link PublishResultMessage}.
550
+ *
551
+ * @param value - The raw `event.data` from a `message` event.
552
+ * @returns `true` when the value validates against the publish-result schema.
553
+ * @example
554
+ * if (isPublishResultMessage(event.data) && isFanvueOrigin(event.origin)) {
555
+ * settle(event.data);
556
+ * }
557
+ */
558
+ function isPublishResultMessage(value) {
559
+ return PublishResultMessageSchema.safeParse(value).success;
560
+ }
561
+ /**
562
+ * Narrows unknown `MessageEvent` data to an {@link UnpublishResultMessage}.
563
+ *
564
+ * @param value - The raw `event.data` from a `message` event.
565
+ * @returns `true` when the value validates against the unpublish-result schema.
566
+ */
567
+ function isUnpublishResultMessage(value) {
568
+ return UnpublishResultMessageSchema.safeParse(value).success;
569
+ }
570
+ /**
571
+ * Whether an origin belongs to the Fanvue web shell.
572
+ *
573
+ * Mirrors the `frame-ancestors 'self' https://fanvue.com https://*.fanvue.com`
574
+ * CSP allowlist every embedded app ships. Eden posts its replies with
575
+ * `targetOrigin: "*"`, so validating `event.origin` on receipt is the only
576
+ * app-side control against a sibling frame forging a publish success. Requires
577
+ * `https:` — a plaintext origin is never Fanvue.
578
+ *
579
+ * @param origin - The `event.origin` of an incoming message.
580
+ * @returns `true` for `https://fanvue.com` and any `https://*.fanvue.com` host.
581
+ * @example
582
+ * isFanvueOrigin('https://www.fanvue.com'); // true
583
+ * isFanvueOrigin('http://fanvue.com'); // false (not https)
584
+ * isFanvueOrigin('https://fanvue.com.evil.test'); // false
585
+ */
586
+ function isFanvueOrigin(origin) {
587
+ let protocol;
588
+ let hostname;
589
+ try {
590
+ ({protocol, hostname} = new URL(origin));
591
+ } catch {
592
+ return false;
593
+ }
594
+ return protocol === "https:" && (hostname === "fanvue.com" || hostname.endsWith(".fanvue.com"));
595
+ }
596
+ //#endregion
597
+ //#region src/core/contracts/pagination.ts
598
+ /**
599
+ * Shared pagination fragments for the Fanvue API.
600
+ *
601
+ * The platform ships three conventions and every resource module has to pick
602
+ * one, so the fragments live here rather than being re-declared per resource:
603
+ *
604
+ * 1. **Offset** (`v0`) — `?page=&size=`, response `{ data, pagination }`.
605
+ * 2. **Cursor** (`v1` keyset and some `v0` routes) — response
606
+ * `{ data, nextCursor, total }`, where `total` is `null` on large keyset
607
+ * lists and absent entirely on the `v0` cursor routes.
608
+ * 3. **Hybrid** (`GET /v0/subscribers`) — offset plus an optional `nextCursor`,
609
+ * which is `null` when sorting by name.
610
+ *
611
+ * This module is schemas and clamping only. The async-iterator helper that
612
+ * walks pages while respecting rate-limit headers is a separate concern and
613
+ * ships with the transport.
614
+ */
615
+ /**
616
+ * Largest `size` the platform accepts on an offset-paginated route.
617
+ *
618
+ * The API responds `400` above this, so callers clamp client-side (see
619
+ * {@link clampPageSize}) rather than turning an over-large request into a
620
+ * failure the caller cannot act on.
621
+ */
622
+ const MAX_PAGE_SIZE = 50;
623
+ /** The platform's default `size` when the query parameter is omitted. */
624
+ const DEFAULT_PAGE_SIZE = 15;
625
+ /**
626
+ * Clamps a requested page size into the platform's accepted range.
627
+ *
628
+ * Non-finite and non-integer inputs fall back to {@link DEFAULT_PAGE_SIZE}
629
+ * rather than being rounded, because a fractional `size` is a caller bug and
630
+ * the platform would reject it.
631
+ *
632
+ * @param requested - The page size a caller asked for.
633
+ * @returns An integer in `[1, MAX_PAGE_SIZE]`.
634
+ * @example
635
+ * clampPageSize(200); // 50
636
+ * clampPageSize(0); // 1
637
+ * clampPageSize(NaN); // 15
638
+ */
639
+ function clampPageSize(requested) {
640
+ if (!Number.isInteger(requested)) return 15;
641
+ if (requested < 1) return 1;
642
+ return Math.min(requested, 50);
643
+ }
644
+ /**
645
+ * Zod schema for the `pagination` object on offset-paginated `v0` responses.
646
+ *
647
+ * `size` is the number of items actually returned, not the requested page size
648
+ * (styx builds it as `data.length`), and `hasMore` is the only reliable
649
+ * end-of-list signal — the platform does not send a total on most routes.
650
+ *
651
+ * `size` therefore allows `0`: paging past the end returns `{ data: [], size: 0 }`,
652
+ * which is a well-formed response and must not fail validation. The `1..50`
653
+ * bound belongs to the *request* parameter — see {@link clampPageSize}.
654
+ */
655
+ const OffsetPaginationSchema = object({
656
+ page: number().int().min(1),
657
+ size: number().int().min(0),
658
+ hasMore: boolean()
659
+ });
660
+ /**
661
+ * Zod schema for `GET /v0/subscribers`' hybrid pagination.
662
+ *
663
+ * Offset fields plus a keyset cursor that is `null` when the caller sorts by
664
+ * name (that ordering cannot be resumed from a cursor).
665
+ */
666
+ const HybridPaginationSchema = OffsetPaginationSchema.extend({ nextCursor: string().nullable() });
667
+ /**
668
+ * Builds the response schema for an offset-paginated list of `item`.
669
+ *
670
+ * @param item - Schema for a single row of `data`.
671
+ * @returns A schema for `{ data, pagination }`.
672
+ * @example
673
+ * const SubscribersPageSchema = offsetPageSchema(SubscriberSchema);
674
+ * const page = SubscribersPageSchema.parse(body);
675
+ * page.pagination.hasMore; // boolean
676
+ */
677
+ function offsetPageSchema(item) {
678
+ return object({
679
+ data: array(item),
680
+ pagination: OffsetPaginationSchema
681
+ });
682
+ }
683
+ /**
684
+ * Builds the response schema for a cursor-paginated list of `item`.
685
+ *
686
+ * `nextCursor` is `null` on the last page. Cursors are opaque and pin the
687
+ * sort, filters and creator they were minted for, so they cannot be replayed
688
+ * against a different query.
689
+ *
690
+ * `total` is the third field of the platform's `v1` keyset envelope
691
+ * (`{ data, nextCursor, total }`): the platform sends a number only where a
692
+ * count is free or cheap, `null` on large keyset lists (the count is the
693
+ * expensive query keyset avoids), and the older `v0` cursor routes omit the
694
+ * key entirely. All three normalise to `total: null` or a number here, so a
695
+ * caller never branches on "absent vs null".
696
+ *
697
+ * @param item - Schema for a single row of `data`.
698
+ * @returns A schema for `{ data, nextCursor, total }`.
699
+ * @example
700
+ * const PaymentsPageSchema = cursorPageSchema(CheckoutPaymentSchema);
701
+ * const page = PaymentsPageSchema.parse(body);
702
+ * const done = page.nextCursor === null;
703
+ */
704
+ function cursorPageSchema(item) {
705
+ return object({
706
+ data: array(item),
707
+ nextCursor: string().nullable(),
708
+ total: number().nullable().default(null)
709
+ });
710
+ }
711
+ if (typeof navigator === "undefined" || !navigator.userAgent?.startsWith?.("Mozilla/5.0 "));
712
+ const ERR_INVALID_ARG_VALUE = "ERR_INVALID_ARG_VALUE";
713
+ const ERR_INVALID_ARG_TYPE = "ERR_INVALID_ARG_TYPE";
714
+ function CodedTypeError(message, code, cause) {
715
+ const err = new TypeError(message, { cause });
716
+ Object.assign(err, { code });
717
+ return err;
718
+ }
719
+ const encoder$1 = new TextEncoder();
720
+ const decoder$1 = new TextDecoder();
721
+ function buf(input) {
722
+ if (typeof input === "string") return encoder$1.encode(input);
723
+ return decoder$1.decode(input);
724
+ }
725
+ let encodeBase64Url;
726
+ if (Uint8Array.prototype.toBase64) encodeBase64Url = (input) => {
727
+ if (input instanceof ArrayBuffer) input = new Uint8Array(input);
728
+ return input.toBase64({
729
+ alphabet: "base64url",
730
+ omitPadding: true
731
+ });
732
+ };
733
+ else {
734
+ const CHUNK_SIZE = 32768;
735
+ encodeBase64Url = (input) => {
736
+ if (input instanceof ArrayBuffer) input = new Uint8Array(input);
737
+ const arr = [];
738
+ for (let i = 0; i < input.byteLength; i += CHUNK_SIZE) arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE)));
739
+ return btoa(arr.join("")).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
740
+ };
741
+ }
742
+ let decodeBase64Url;
743
+ if (Uint8Array.fromBase64) decodeBase64Url = (input) => {
744
+ try {
745
+ return Uint8Array.fromBase64(input, { alphabet: "base64url" });
746
+ } catch (cause) {
747
+ throw CodedTypeError("The input to be decoded is not correctly encoded.", ERR_INVALID_ARG_VALUE, cause);
748
+ }
749
+ };
750
+ else decodeBase64Url = (input) => {
751
+ try {
752
+ const binary = atob(input.replace(/-/g, "+").replace(/_/g, "/").replace(/\s/g, ""));
753
+ const bytes = new Uint8Array(binary.length);
754
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
755
+ return bytes;
756
+ } catch (cause) {
757
+ throw CodedTypeError("The input to be decoded is not correctly encoded.", ERR_INVALID_ARG_VALUE, cause);
758
+ }
759
+ };
760
+ function b64u(input) {
761
+ if (typeof input === "string") return decodeBase64Url(input);
762
+ return encodeBase64Url(input);
763
+ }
764
+ var OperationProcessingError = class extends Error {
765
+ code;
766
+ constructor(message, options) {
767
+ super(message, options);
768
+ this.name = this.constructor.name;
769
+ if (options?.code) this.code = options?.code;
770
+ Error.captureStackTrace?.(this, this.constructor);
771
+ }
772
+ };
773
+ function OPE(message, code, cause) {
774
+ return new OperationProcessingError(message, {
775
+ code,
776
+ cause
777
+ });
778
+ }
779
+ function assertString(input, it, code, cause) {
780
+ try {
781
+ if (typeof input !== "string") throw CodedTypeError(`${it} must be a string`, ERR_INVALID_ARG_TYPE, cause);
782
+ if (input.length === 0) throw CodedTypeError(`${it} must not be empty`, ERR_INVALID_ARG_VALUE, cause);
783
+ } catch (err) {
784
+ if (code) throw OPE(err.message, code, cause);
785
+ throw err;
786
+ }
787
+ }
788
+ function randomBytes() {
789
+ return b64u(crypto.getRandomValues(new Uint8Array(32)));
790
+ }
791
+ function generateRandomCodeVerifier() {
792
+ return randomBytes();
793
+ }
794
+ function generateRandomState() {
795
+ return randomBytes();
796
+ }
797
+ async function calculatePKCECodeChallenge(codeVerifier) {
798
+ assertString(codeVerifier, "codeVerifier");
799
+ return b64u(await crypto.subtle.digest("SHA-256", buf(codeVerifier)));
800
+ }
801
+ URL.parse;
802
+ const tokenMatch = "[a-zA-Z0-9!#$%&\\'\\*\\+\\-\\.\\^_`\\|~]+";
803
+ const token68Match = "[a-zA-Z0-9\\-\\._\\~\\+\\/]+={0,2}";
804
+ const quotedParamMatcher = "(" + tokenMatch + ")\\s*=\\s*\"((?:[^\"\\\\]|\\\\[\\s\\S])*)\"";
805
+ const paramMatcher = "(" + tokenMatch + ")\\s*=\\s*([a-zA-Z0-9!#$%&\\'\\*\\+\\-\\.\\^_`\\|~]+)";
806
+ new RegExp("^[,\\s]*(" + tokenMatch + ")");
807
+ new RegExp("^[,\\s]*" + quotedParamMatcher + "[,\\s]*(.*)");
808
+ new RegExp("^[,\\s]*" + paramMatcher + "[,\\s]*(.*)");
809
+ new RegExp("^(" + token68Match + ")(?:$|[,\\s])(.*)");
810
+ //#endregion
811
+ //#region src/core/json.ts
812
+ var import_index_cjs = require_index_cjs();
813
+ const ERROR_RAW_TEXT_MAX_LENGTH = 500;
814
+ /**
815
+ * Safely parses a JSON string, returning a `Result` instead of throwing.
816
+ *
817
+ * @param text - The raw text to parse as JSON.
818
+ * @returns `ok(parsed)` on success, or `err({ code, rawText, message })` on failure.
819
+ */
820
+ function safeJsonParse(text) {
821
+ try {
822
+ return (0, import_index_cjs.ok)(JSON.parse(text));
823
+ } catch (e) {
824
+ return (0, import_index_cjs.err)({
825
+ code: "JSON_PARSE_ERROR",
826
+ rawText: text.length > ERROR_RAW_TEXT_MAX_LENGTH ? text.slice(0, ERROR_RAW_TEXT_MAX_LENGTH) + "..." : text,
827
+ message: e instanceof Error ? e.message : "Failed to parse JSON"
828
+ });
829
+ }
830
+ }
831
+ //#endregion
832
+ //#region src/core/schemas.ts
833
+ /** Zod schema for the raw token response from the OAuth token endpoint. */
834
+ const TokenResponseSchema = object({
835
+ access_token: string(),
836
+ refresh_token: string().nullable().optional().default(null),
837
+ expires_in: number(),
838
+ token_type: string(),
839
+ scope: string().nullable().optional().default(null),
840
+ id_token: string().nullable().optional().default(null)
841
+ });
842
+ /** Zod schema for a Fanvue user profile. */
843
+ const FanvueUserSchema = object({
844
+ uuid: string(),
845
+ email: string(),
846
+ handle: string(),
847
+ displayName: string(),
848
+ isCreator: boolean(),
849
+ avatarUrl: string().nullable(),
850
+ bannerUrl: string().nullable(),
851
+ createdAt: string(),
852
+ updatedAt: string().nullable()
853
+ });
854
+ /** Zod schema for the authorize-on-behalf response from the Fanvue platform. */
855
+ const AuthorizeOnBehalfResponseSchema = object({
856
+ code: string(),
857
+ state: string()
858
+ });
859
+ /** Zod schema for the session JWT payload. */
860
+ const SessionPayloadSchema = object({
861
+ accessToken: string(),
862
+ refreshToken: string().nullable(),
863
+ expiresAt: number(),
864
+ tokenType: string().nullable(),
865
+ scope: string().nullable(),
866
+ idToken: string().nullable(),
867
+ userUuid: string(),
868
+ handle: string(),
869
+ displayName: string(),
870
+ isCreator: boolean(),
871
+ avatarUrl: string().nullable()
872
+ }).passthrough();
873
+ //#endregion
874
+ //#region src/core/oauth.ts
875
+ const CONTENT_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded";
876
+ const OAUTH_TOKEN_ENDPOINT = "/oauth2/token";
877
+ /**
878
+ * Reads the authorization server's error body off a failed token response.
879
+ *
880
+ * The status line alone cannot separate a retired refresh token from bad client
881
+ * credentials — both are `400` — so the body is worth keeping. Any failure to
882
+ * read or validate it yields `null`: a missing diagnostic must never replace the
883
+ * error it was attached to. Only `error` and `error_description` survive; the
884
+ * provider's hint/debug fields can echo request details back.
885
+ */
886
+ async function readOAuthErrorBody(response) {
887
+ let text;
888
+ try {
889
+ text = await response.text();
890
+ } catch {
891
+ return null;
892
+ }
893
+ const parsed = safeJsonParse(text);
894
+ if (parsed.isErr()) return null;
895
+ const validated = OAuthErrorBodySchema.safeParse(parsed.value);
896
+ return validated.success ? validated.data : null;
897
+ }
898
+ /**
899
+ * Builds an OAuth 2.0 authorization URL with PKCE parameters.
900
+ *
901
+ * @param config - The OAuth configuration.
902
+ * @param opts - Optional overrides. Pass `state` to use a deterministic state value.
903
+ * @returns The authorization URL, the PKCE code verifier, and the state parameter.
904
+ */
905
+ async function createAuthorizationUrl(config, opts = null) {
906
+ const codeVerifier = generateRandomCodeVerifier();
907
+ const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
908
+ const state = opts?.state ?? generateRandomState();
909
+ const issuerUrl = config.issuerUrl ?? "https://auth.fanvue.com";
910
+ const scope = config.scopes !== null ? `${DEFAULT_SCOPES} ${config.scopes}`.trim() : DEFAULT_SCOPES;
911
+ const url = new URL(`${issuerUrl}/oauth2/auth`);
912
+ url.searchParams.set("response_type", "code");
913
+ url.searchParams.set("client_id", config.clientId);
914
+ url.searchParams.set("redirect_uri", config.redirectUri);
915
+ url.searchParams.set("scope", scope);
916
+ url.searchParams.set("state", state);
917
+ url.searchParams.set("code_challenge", codeChallenge);
918
+ url.searchParams.set("code_challenge_method", "S256");
919
+ if (config.responseMode !== null) url.searchParams.set("response_mode", config.responseMode);
920
+ if (config.prompt !== null) url.searchParams.set("prompt", config.prompt);
921
+ return {
922
+ url,
923
+ codeVerifier,
924
+ state
925
+ };
926
+ }
927
+ /**
928
+ * Exchanges an authorization code for tokens using the OAuth token endpoint.
929
+ *
930
+ * @param config - The OAuth configuration.
931
+ * @param opts - The authorization code, PKCE code verifier, and optional redirect URI override.
932
+ * @returns A `Result` containing `TokenResponse` on success or `OAuthError` on
933
+ * failure. A `TOKEN_EXCHANGE_FAILED` error carries the authorization server's
934
+ * own `oauthError` body when it sent a readable one.
935
+ */
936
+ async function exchangeCodeForToken(config, opts) {
937
+ const issuerUrl = config.issuerUrl ?? "https://auth.fanvue.com";
938
+ const body = new URLSearchParams({
939
+ grant_type: "authorization_code",
940
+ code: opts.code,
941
+ redirect_uri: opts.redirectUri ?? config.redirectUri,
942
+ client_id: config.clientId,
943
+ code_verifier: opts.codeVerifier
944
+ });
945
+ let response;
946
+ try {
947
+ response = await fetch(`${issuerUrl}${OAUTH_TOKEN_ENDPOINT}`, {
948
+ method: "POST",
949
+ headers: {
950
+ "Content-Type": CONTENT_TYPE_FORM_URLENCODED,
951
+ Authorization: `Basic ${btoa(`${config.clientId}:${config.clientSecret}`)}`
952
+ },
953
+ body
954
+ });
955
+ } catch (error) {
956
+ return (0, import_index_cjs.err)({
957
+ code: "TOKEN_EXCHANGE_FAILED",
958
+ statusCode: 0,
959
+ message: `Network error during token exchange: ${error instanceof Error ? error.message : String(error)}`,
960
+ oauthError: null
961
+ });
962
+ }
963
+ if (!response.ok) return (0, import_index_cjs.err)({
964
+ code: "TOKEN_EXCHANGE_FAILED",
965
+ statusCode: response.status,
966
+ message: `Token exchange failed: ${response.status} ${response.statusText}`,
967
+ oauthError: await readOAuthErrorBody(response)
968
+ });
969
+ const parseResult = safeJsonParse(await response.text());
970
+ if (parseResult.isErr()) {
971
+ const { rawText, message } = parseResult.error;
972
+ return (0, import_index_cjs.err)({
973
+ code: "OAUTH_JSON_PARSE_ERROR",
974
+ rawText,
975
+ message
976
+ });
977
+ }
978
+ const validated = TokenResponseSchema.safeParse(parseResult.value);
979
+ if (!validated.success) return (0, import_index_cjs.err)({
980
+ code: "OAUTH_VALIDATION_ERROR",
981
+ message: validated.error.message
982
+ });
983
+ return (0, import_index_cjs.ok)(validated.data);
984
+ }
985
+ /**
986
+ * Refreshes an access token using a refresh token.
987
+ *
988
+ * @param config - The OAuth configuration.
989
+ * @param refreshToken - The refresh token to use.
990
+ * @returns A `Result` containing `TokenResponse` on success or `OAuthError` on
991
+ * failure. A `TOKEN_REFRESH_FAILED` error carries the authorization server's
992
+ * own `oauthError` body when it sent a readable one — `invalid_grant` there
993
+ * means the stored refresh token is retired and the creator must reconnect.
994
+ */
995
+ async function refreshAccessToken(config, refreshToken) {
996
+ const issuerUrl = config.issuerUrl ?? "https://auth.fanvue.com";
997
+ const body = new URLSearchParams({
998
+ grant_type: "refresh_token",
999
+ refresh_token: refreshToken,
1000
+ client_id: config.clientId
1001
+ });
1002
+ let response;
1003
+ try {
1004
+ response = await fetch(`${issuerUrl}${OAUTH_TOKEN_ENDPOINT}`, {
1005
+ method: "POST",
1006
+ headers: {
1007
+ "Content-Type": CONTENT_TYPE_FORM_URLENCODED,
1008
+ Authorization: `Basic ${btoa(`${config.clientId}:${config.clientSecret}`)}`
1009
+ },
1010
+ body
1011
+ });
1012
+ } catch (error) {
1013
+ return (0, import_index_cjs.err)({
1014
+ code: "TOKEN_REFRESH_FAILED",
1015
+ statusCode: 0,
1016
+ message: `Network error during token refresh: ${error instanceof Error ? error.message : String(error)}`,
1017
+ oauthError: null
1018
+ });
1019
+ }
1020
+ if (!response.ok) return (0, import_index_cjs.err)({
1021
+ code: "TOKEN_REFRESH_FAILED",
1022
+ statusCode: response.status,
1023
+ message: `Token refresh failed: ${response.status} ${response.statusText}`,
1024
+ oauthError: await readOAuthErrorBody(response)
1025
+ });
1026
+ const parseResult = safeJsonParse(await response.text());
1027
+ if (parseResult.isErr()) {
1028
+ const { rawText, message } = parseResult.error;
1029
+ return (0, import_index_cjs.err)({
1030
+ code: "OAUTH_JSON_PARSE_ERROR",
1031
+ rawText,
1032
+ message
1033
+ });
1034
+ }
1035
+ const validated = TokenResponseSchema.safeParse(parseResult.value);
1036
+ if (!validated.success) return (0, import_index_cjs.err)({
1037
+ code: "OAUTH_VALIDATION_ERROR",
1038
+ message: validated.error.message
1039
+ });
1040
+ return (0, import_index_cjs.ok)(validated.data);
1041
+ }
1042
+ //#endregion
1043
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/lib/buffer_utils.js
1044
+ const encoder = new TextEncoder();
1045
+ const decoder = new TextDecoder();
1046
+ function concat(...buffers) {
1047
+ const size = buffers.reduce((acc, { length }) => acc + length, 0);
1048
+ const buf = new Uint8Array(size);
1049
+ let i = 0;
1050
+ for (const buffer of buffers) {
1051
+ buf.set(buffer, i);
1052
+ i += buffer.length;
1053
+ }
1054
+ return buf;
1055
+ }
1056
+ function encode$1(string) {
1057
+ const bytes = new Uint8Array(string.length);
1058
+ for (let i = 0; i < string.length; i++) {
1059
+ const code = string.charCodeAt(i);
1060
+ if (code > 127) throw new TypeError("non-ASCII string encountered in encode()");
1061
+ bytes[i] = code;
1062
+ }
1063
+ return bytes;
1064
+ }
1065
+ //#endregion
1066
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/lib/base64.js
1067
+ function encodeBase64(input) {
1068
+ if (Uint8Array.prototype.toBase64) return input.toBase64();
1069
+ const CHUNK_SIZE = 32768;
1070
+ const arr = [];
1071
+ for (let i = 0; i < input.length; i += CHUNK_SIZE) arr.push(String.fromCharCode.apply(null, input.subarray(i, i + CHUNK_SIZE)));
1072
+ return btoa(arr.join(""));
1073
+ }
1074
+ function decodeBase64(encoded) {
1075
+ if (Uint8Array.fromBase64) return Uint8Array.fromBase64(encoded);
1076
+ const binary = atob(encoded);
1077
+ const bytes = new Uint8Array(binary.length);
1078
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
1079
+ return bytes;
1080
+ }
1081
+ //#endregion
1082
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/util/base64url.js
1083
+ function decode(input) {
1084
+ if (Uint8Array.fromBase64) return Uint8Array.fromBase64(typeof input === "string" ? input : decoder.decode(input), { alphabet: "base64url" });
1085
+ let encoded = input;
1086
+ if (encoded instanceof Uint8Array) encoded = decoder.decode(encoded);
1087
+ encoded = encoded.replace(/-/g, "+").replace(/_/g, "/");
1088
+ try {
1089
+ return decodeBase64(encoded);
1090
+ } catch {
1091
+ throw new TypeError("The input to be decoded is not correctly encoded.");
1092
+ }
1093
+ }
1094
+ function encode(input) {
1095
+ let unencoded = input;
1096
+ if (typeof unencoded === "string") unencoded = encoder.encode(unencoded);
1097
+ if (Uint8Array.prototype.toBase64) return unencoded.toBase64({
1098
+ alphabet: "base64url",
1099
+ omitPadding: true
1100
+ });
1101
+ return encodeBase64(unencoded).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
1102
+ }
1103
+ //#endregion
1104
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/lib/crypto_key.js
1105
+ const unusable = (name, prop = "algorithm.name") => /* @__PURE__ */ new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`);
1106
+ const isAlgorithm = (algorithm, name) => algorithm.name === name;
1107
+ function getHashLength(hash) {
1108
+ return parseInt(hash.name.slice(4), 10);
1109
+ }
1110
+ function checkHashLength(algorithm, expected) {
1111
+ if (getHashLength(algorithm.hash) !== expected) throw unusable(`SHA-${expected}`, "algorithm.hash");
1112
+ }
1113
+ function getNamedCurve(alg) {
1114
+ switch (alg) {
1115
+ case "ES256": return "P-256";
1116
+ case "ES384": return "P-384";
1117
+ case "ES512": return "P-521";
1118
+ default: throw new Error("unreachable");
1119
+ }
1120
+ }
1121
+ function checkUsage(key, usage) {
1122
+ if (usage && !key.usages.includes(usage)) throw new TypeError(`CryptoKey does not support this operation, its usages must include ${usage}.`);
1123
+ }
1124
+ function checkSigCryptoKey(key, alg, usage) {
1125
+ switch (alg) {
1126
+ case "HS256":
1127
+ case "HS384":
1128
+ case "HS512":
1129
+ if (!isAlgorithm(key.algorithm, "HMAC")) throw unusable("HMAC");
1130
+ checkHashLength(key.algorithm, parseInt(alg.slice(2), 10));
1131
+ break;
1132
+ case "RS256":
1133
+ case "RS384":
1134
+ case "RS512":
1135
+ if (!isAlgorithm(key.algorithm, "RSASSA-PKCS1-v1_5")) throw unusable("RSASSA-PKCS1-v1_5");
1136
+ checkHashLength(key.algorithm, parseInt(alg.slice(2), 10));
1137
+ break;
1138
+ case "PS256":
1139
+ case "PS384":
1140
+ case "PS512":
1141
+ if (!isAlgorithm(key.algorithm, "RSA-PSS")) throw unusable("RSA-PSS");
1142
+ checkHashLength(key.algorithm, parseInt(alg.slice(2), 10));
1143
+ break;
1144
+ case "Ed25519":
1145
+ case "EdDSA":
1146
+ if (!isAlgorithm(key.algorithm, "Ed25519")) throw unusable("Ed25519");
1147
+ break;
1148
+ case "ML-DSA-44":
1149
+ case "ML-DSA-65":
1150
+ case "ML-DSA-87":
1151
+ if (!isAlgorithm(key.algorithm, alg)) throw unusable(alg);
1152
+ break;
1153
+ case "ES256":
1154
+ case "ES384":
1155
+ case "ES512": {
1156
+ if (!isAlgorithm(key.algorithm, "ECDSA")) throw unusable("ECDSA");
1157
+ const expected = getNamedCurve(alg);
1158
+ if (key.algorithm.namedCurve !== expected) throw unusable(expected, "algorithm.namedCurve");
1159
+ break;
1160
+ }
1161
+ default: throw new TypeError("CryptoKey does not support this operation");
1162
+ }
1163
+ checkUsage(key, usage);
1164
+ }
1165
+ //#endregion
1166
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/lib/invalid_key_input.js
1167
+ function message(msg, actual, ...types) {
1168
+ types = types.filter(Boolean);
1169
+ if (types.length > 2) {
1170
+ const last = types.pop();
1171
+ msg += `one of type ${types.join(", ")}, or ${last}.`;
1172
+ } else if (types.length === 2) msg += `one of type ${types[0]} or ${types[1]}.`;
1173
+ else msg += `of type ${types[0]}.`;
1174
+ if (actual == null) msg += ` Received ${actual}`;
1175
+ else if (typeof actual === "function" && actual.name) msg += ` Received function ${actual.name}`;
1176
+ else if (typeof actual === "object" && actual != null) {
1177
+ if (actual.constructor?.name) msg += ` Received an instance of ${actual.constructor.name}`;
1178
+ }
1179
+ return msg;
1180
+ }
1181
+ const invalidKeyInput = (actual, ...types) => message("Key must be ", actual, ...types);
1182
+ const withAlg = (alg, actual, ...types) => message(`Key for the ${alg} algorithm must be `, actual, ...types);
1183
+ //#endregion
1184
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/util/errors.js
1185
+ var JOSEError = class extends Error {
1186
+ static code = "ERR_JOSE_GENERIC";
1187
+ code = "ERR_JOSE_GENERIC";
1188
+ constructor(message, options) {
1189
+ super(message, options);
1190
+ this.name = this.constructor.name;
1191
+ Error.captureStackTrace?.(this, this.constructor);
1192
+ }
1193
+ };
1194
+ var JWTClaimValidationFailed = class extends JOSEError {
1195
+ static code = "ERR_JWT_CLAIM_VALIDATION_FAILED";
1196
+ code = "ERR_JWT_CLAIM_VALIDATION_FAILED";
1197
+ claim;
1198
+ reason;
1199
+ payload;
1200
+ constructor(message, payload, claim = "unspecified", reason = "unspecified") {
1201
+ super(message, { cause: {
1202
+ claim,
1203
+ reason,
1204
+ payload
1205
+ } });
1206
+ this.claim = claim;
1207
+ this.reason = reason;
1208
+ this.payload = payload;
1209
+ }
1210
+ };
1211
+ var JWTExpired = class extends JOSEError {
1212
+ static code = "ERR_JWT_EXPIRED";
1213
+ code = "ERR_JWT_EXPIRED";
1214
+ claim;
1215
+ reason;
1216
+ payload;
1217
+ constructor(message, payload, claim = "unspecified", reason = "unspecified") {
1218
+ super(message, { cause: {
1219
+ claim,
1220
+ reason,
1221
+ payload
1222
+ } });
1223
+ this.claim = claim;
1224
+ this.reason = reason;
1225
+ this.payload = payload;
1226
+ }
1227
+ };
1228
+ var JOSEAlgNotAllowed = class extends JOSEError {
1229
+ static code = "ERR_JOSE_ALG_NOT_ALLOWED";
1230
+ code = "ERR_JOSE_ALG_NOT_ALLOWED";
1231
+ };
1232
+ var JOSENotSupported = class extends JOSEError {
1233
+ static code = "ERR_JOSE_NOT_SUPPORTED";
1234
+ code = "ERR_JOSE_NOT_SUPPORTED";
1235
+ };
1236
+ var JWSInvalid = class extends JOSEError {
1237
+ static code = "ERR_JWS_INVALID";
1238
+ code = "ERR_JWS_INVALID";
1239
+ };
1240
+ var JWTInvalid = class extends JOSEError {
1241
+ static code = "ERR_JWT_INVALID";
1242
+ code = "ERR_JWT_INVALID";
1243
+ };
1244
+ Symbol.asyncIterator;
1245
+ var JWSSignatureVerificationFailed = class extends JOSEError {
1246
+ static code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED";
1247
+ code = "ERR_JWS_SIGNATURE_VERIFICATION_FAILED";
1248
+ constructor(message = "signature verification failed", options) {
1249
+ super(message, options);
1250
+ }
1251
+ };
1252
+ //#endregion
1253
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/lib/is_key_like.js
1254
+ const isCryptoKey = (key) => {
1255
+ if (key?.[Symbol.toStringTag] === "CryptoKey") return true;
1256
+ try {
1257
+ return key instanceof CryptoKey;
1258
+ } catch {
1259
+ return false;
1260
+ }
1261
+ };
1262
+ const isKeyObject = (key) => key?.[Symbol.toStringTag] === "KeyObject";
1263
+ const isKeyLike = (key) => isCryptoKey(key) || isKeyObject(key);
1264
+ //#endregion
1265
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/lib/helpers.js
1266
+ function assertNotSet(value, name) {
1267
+ if (value) throw new TypeError(`${name} can only be called once`);
1268
+ }
1269
+ function decodeBase64url(value, label, ErrorClass) {
1270
+ try {
1271
+ return decode(value);
1272
+ } catch {
1273
+ throw new ErrorClass(`Failed to base64url decode the ${label}`);
1274
+ }
1275
+ }
1276
+ //#endregion
1277
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/lib/type_checks.js
1278
+ const isObjectLike = (value) => typeof value === "object" && value !== null;
1279
+ function isObject(input) {
1280
+ if (!isObjectLike(input) || Object.prototype.toString.call(input) !== "[object Object]") return false;
1281
+ if (Object.getPrototypeOf(input) === null) return true;
1282
+ let proto = input;
1283
+ while (Object.getPrototypeOf(proto) !== null) proto = Object.getPrototypeOf(proto);
1284
+ return Object.getPrototypeOf(input) === proto;
1285
+ }
1286
+ function isDisjoint(...headers) {
1287
+ const sources = headers.filter(Boolean);
1288
+ if (sources.length === 0 || sources.length === 1) return true;
1289
+ let acc;
1290
+ for (const header of sources) {
1291
+ const parameters = Object.keys(header);
1292
+ if (!acc || acc.size === 0) {
1293
+ acc = new Set(parameters);
1294
+ continue;
1295
+ }
1296
+ for (const parameter of parameters) {
1297
+ if (acc.has(parameter)) return false;
1298
+ acc.add(parameter);
1299
+ }
1300
+ }
1301
+ return true;
1302
+ }
1303
+ const isJWK = (key) => isObject(key) && typeof key.kty === "string";
1304
+ const isPrivateJWK = (key) => key.kty !== "oct" && (key.kty === "AKP" && typeof key.priv === "string" || typeof key.d === "string");
1305
+ const isPublicJWK = (key) => key.kty !== "oct" && key.d === void 0 && key.priv === void 0;
1306
+ const isSecretJWK = (key) => key.kty === "oct" && typeof key.k === "string";
1307
+ //#endregion
1308
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/lib/signing.js
1309
+ function checkKeyLength(alg, key) {
1310
+ if (alg.startsWith("RS") || alg.startsWith("PS")) {
1311
+ const { modulusLength } = key.algorithm;
1312
+ if (typeof modulusLength !== "number" || modulusLength < 2048) throw new TypeError(`${alg} requires key modulusLength to be 2048 bits or larger`);
1313
+ }
1314
+ }
1315
+ function subtleAlgorithm(alg, algorithm) {
1316
+ const hash = `SHA-${alg.slice(-3)}`;
1317
+ switch (alg) {
1318
+ case "HS256":
1319
+ case "HS384":
1320
+ case "HS512": return {
1321
+ hash,
1322
+ name: "HMAC"
1323
+ };
1324
+ case "PS256":
1325
+ case "PS384":
1326
+ case "PS512": return {
1327
+ hash,
1328
+ name: "RSA-PSS",
1329
+ saltLength: parseInt(alg.slice(-3), 10) >> 3
1330
+ };
1331
+ case "RS256":
1332
+ case "RS384":
1333
+ case "RS512": return {
1334
+ hash,
1335
+ name: "RSASSA-PKCS1-v1_5"
1336
+ };
1337
+ case "ES256":
1338
+ case "ES384":
1339
+ case "ES512": return {
1340
+ hash,
1341
+ name: "ECDSA",
1342
+ namedCurve: algorithm.namedCurve
1343
+ };
1344
+ case "Ed25519":
1345
+ case "EdDSA": return { name: "Ed25519" };
1346
+ case "ML-DSA-44":
1347
+ case "ML-DSA-65":
1348
+ case "ML-DSA-87": return { name: alg };
1349
+ default: throw new JOSENotSupported(`alg ${alg} is not supported either by JOSE or your javascript runtime`);
1350
+ }
1351
+ }
1352
+ async function getSigKey(alg, key, usage) {
1353
+ if (key instanceof Uint8Array) {
1354
+ if (!alg.startsWith("HS")) throw new TypeError(invalidKeyInput(key, "CryptoKey", "KeyObject", "JSON Web Key"));
1355
+ return crypto.subtle.importKey("raw", key, {
1356
+ hash: `SHA-${alg.slice(-3)}`,
1357
+ name: "HMAC"
1358
+ }, false, [usage]);
1359
+ }
1360
+ checkSigCryptoKey(key, alg, usage);
1361
+ return key;
1362
+ }
1363
+ async function sign(alg, key, data) {
1364
+ const cryptoKey = await getSigKey(alg, key, "sign");
1365
+ checkKeyLength(alg, cryptoKey);
1366
+ const signature = await crypto.subtle.sign(subtleAlgorithm(alg, cryptoKey.algorithm), cryptoKey, data);
1367
+ return new Uint8Array(signature);
1368
+ }
1369
+ async function verify(alg, key, signature, data) {
1370
+ const cryptoKey = await getSigKey(alg, key, "verify");
1371
+ checkKeyLength(alg, cryptoKey);
1372
+ const algorithm = subtleAlgorithm(alg, cryptoKey.algorithm);
1373
+ try {
1374
+ return await crypto.subtle.verify(algorithm, cryptoKey, signature, data);
1375
+ } catch {
1376
+ return false;
1377
+ }
1378
+ }
1379
+ //#endregion
1380
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/lib/jwk_to_key.js
1381
+ const unsupportedAlg = "Invalid or unsupported JWK \"alg\" (Algorithm) Parameter value";
1382
+ function subtleMapping(jwk) {
1383
+ let algorithm;
1384
+ let keyUsages;
1385
+ switch (jwk.kty) {
1386
+ case "AKP":
1387
+ switch (jwk.alg) {
1388
+ case "ML-DSA-44":
1389
+ case "ML-DSA-65":
1390
+ case "ML-DSA-87":
1391
+ algorithm = { name: jwk.alg };
1392
+ keyUsages = jwk.priv ? ["sign"] : ["verify"];
1393
+ break;
1394
+ default: throw new JOSENotSupported(unsupportedAlg);
1395
+ }
1396
+ break;
1397
+ case "RSA":
1398
+ switch (jwk.alg) {
1399
+ case "PS256":
1400
+ case "PS384":
1401
+ case "PS512":
1402
+ algorithm = {
1403
+ name: "RSA-PSS",
1404
+ hash: `SHA-${jwk.alg.slice(-3)}`
1405
+ };
1406
+ keyUsages = jwk.d ? ["sign"] : ["verify"];
1407
+ break;
1408
+ case "RS256":
1409
+ case "RS384":
1410
+ case "RS512":
1411
+ algorithm = {
1412
+ name: "RSASSA-PKCS1-v1_5",
1413
+ hash: `SHA-${jwk.alg.slice(-3)}`
1414
+ };
1415
+ keyUsages = jwk.d ? ["sign"] : ["verify"];
1416
+ break;
1417
+ case "RSA-OAEP":
1418
+ case "RSA-OAEP-256":
1419
+ case "RSA-OAEP-384":
1420
+ case "RSA-OAEP-512":
1421
+ algorithm = {
1422
+ name: "RSA-OAEP",
1423
+ hash: `SHA-${parseInt(jwk.alg.slice(-3), 10) || 1}`
1424
+ };
1425
+ keyUsages = jwk.d ? ["decrypt", "unwrapKey"] : ["encrypt", "wrapKey"];
1426
+ break;
1427
+ default: throw new JOSENotSupported(unsupportedAlg);
1428
+ }
1429
+ break;
1430
+ case "EC":
1431
+ switch (jwk.alg) {
1432
+ case "ES256":
1433
+ case "ES384":
1434
+ case "ES512":
1435
+ algorithm = {
1436
+ name: "ECDSA",
1437
+ namedCurve: {
1438
+ ES256: "P-256",
1439
+ ES384: "P-384",
1440
+ ES512: "P-521"
1441
+ }[jwk.alg]
1442
+ };
1443
+ keyUsages = jwk.d ? ["sign"] : ["verify"];
1444
+ break;
1445
+ case "ECDH-ES":
1446
+ case "ECDH-ES+A128KW":
1447
+ case "ECDH-ES+A192KW":
1448
+ case "ECDH-ES+A256KW":
1449
+ algorithm = {
1450
+ name: "ECDH",
1451
+ namedCurve: jwk.crv
1452
+ };
1453
+ keyUsages = jwk.d ? ["deriveBits"] : [];
1454
+ break;
1455
+ default: throw new JOSENotSupported(unsupportedAlg);
1456
+ }
1457
+ break;
1458
+ case "OKP":
1459
+ switch (jwk.alg) {
1460
+ case "Ed25519":
1461
+ case "EdDSA":
1462
+ algorithm = { name: "Ed25519" };
1463
+ keyUsages = jwk.d ? ["sign"] : ["verify"];
1464
+ break;
1465
+ case "ECDH-ES":
1466
+ case "ECDH-ES+A128KW":
1467
+ case "ECDH-ES+A192KW":
1468
+ case "ECDH-ES+A256KW":
1469
+ algorithm = { name: jwk.crv };
1470
+ keyUsages = jwk.d ? ["deriveBits"] : [];
1471
+ break;
1472
+ default: throw new JOSENotSupported(unsupportedAlg);
1473
+ }
1474
+ break;
1475
+ default: throw new JOSENotSupported("Invalid or unsupported JWK \"kty\" (Key Type) Parameter value");
1476
+ }
1477
+ return {
1478
+ algorithm,
1479
+ keyUsages
1480
+ };
1481
+ }
1482
+ async function jwkToKey(jwk) {
1483
+ if (!jwk.alg) throw new TypeError("\"alg\" argument is required when \"jwk.alg\" is not present");
1484
+ const { algorithm, keyUsages } = subtleMapping(jwk);
1485
+ const keyData = { ...jwk };
1486
+ if (keyData.kty !== "AKP") delete keyData.alg;
1487
+ delete keyData.use;
1488
+ return crypto.subtle.importKey("jwk", keyData, algorithm, jwk.ext ?? (jwk.d || jwk.priv ? false : true), jwk.key_ops ?? keyUsages);
1489
+ }
1490
+ //#endregion
1491
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/lib/normalize_key.js
1492
+ const unusableForAlg = "given KeyObject instance cannot be used for this algorithm";
1493
+ let cache;
1494
+ const handleJWK = async (key, jwk, alg, freeze = false) => {
1495
+ cache ||= /* @__PURE__ */ new WeakMap();
1496
+ let cached = cache.get(key);
1497
+ if (cached?.[alg]) return cached[alg];
1498
+ const cryptoKey = await jwkToKey({
1499
+ ...jwk,
1500
+ alg
1501
+ });
1502
+ if (freeze) Object.freeze(key);
1503
+ if (!cached) cache.set(key, { [alg]: cryptoKey });
1504
+ else cached[alg] = cryptoKey;
1505
+ return cryptoKey;
1506
+ };
1507
+ const handleKeyObject = (keyObject, alg) => {
1508
+ cache ||= /* @__PURE__ */ new WeakMap();
1509
+ let cached = cache.get(keyObject);
1510
+ if (cached?.[alg]) return cached[alg];
1511
+ const isPublic = keyObject.type === "public";
1512
+ const extractable = isPublic ? true : false;
1513
+ let cryptoKey;
1514
+ if (keyObject.asymmetricKeyType === "x25519") {
1515
+ switch (alg) {
1516
+ case "ECDH-ES":
1517
+ case "ECDH-ES+A128KW":
1518
+ case "ECDH-ES+A192KW":
1519
+ case "ECDH-ES+A256KW": break;
1520
+ default: throw new TypeError(unusableForAlg);
1521
+ }
1522
+ cryptoKey = keyObject.toCryptoKey(keyObject.asymmetricKeyType, extractable, isPublic ? [] : ["deriveBits"]);
1523
+ }
1524
+ if (keyObject.asymmetricKeyType === "ed25519") {
1525
+ if (alg !== "EdDSA" && alg !== "Ed25519") throw new TypeError(unusableForAlg);
1526
+ cryptoKey = keyObject.toCryptoKey(keyObject.asymmetricKeyType, extractable, [isPublic ? "verify" : "sign"]);
1527
+ }
1528
+ switch (keyObject.asymmetricKeyType) {
1529
+ case "ml-dsa-44":
1530
+ case "ml-dsa-65":
1531
+ case "ml-dsa-87":
1532
+ if (alg !== keyObject.asymmetricKeyType.toUpperCase()) throw new TypeError(unusableForAlg);
1533
+ cryptoKey = keyObject.toCryptoKey(keyObject.asymmetricKeyType, extractable, [isPublic ? "verify" : "sign"]);
1534
+ }
1535
+ if (keyObject.asymmetricKeyType === "rsa") {
1536
+ let hash;
1537
+ switch (alg) {
1538
+ case "RSA-OAEP":
1539
+ hash = "SHA-1";
1540
+ break;
1541
+ case "RS256":
1542
+ case "PS256":
1543
+ case "RSA-OAEP-256":
1544
+ hash = "SHA-256";
1545
+ break;
1546
+ case "RS384":
1547
+ case "PS384":
1548
+ case "RSA-OAEP-384":
1549
+ hash = "SHA-384";
1550
+ break;
1551
+ case "RS512":
1552
+ case "PS512":
1553
+ case "RSA-OAEP-512":
1554
+ hash = "SHA-512";
1555
+ break;
1556
+ default: throw new TypeError(unusableForAlg);
1557
+ }
1558
+ if (alg.startsWith("RSA-OAEP")) return keyObject.toCryptoKey({
1559
+ name: "RSA-OAEP",
1560
+ hash
1561
+ }, extractable, isPublic ? ["encrypt"] : ["decrypt"]);
1562
+ cryptoKey = keyObject.toCryptoKey({
1563
+ name: alg.startsWith("PS") ? "RSA-PSS" : "RSASSA-PKCS1-v1_5",
1564
+ hash
1565
+ }, extractable, [isPublic ? "verify" : "sign"]);
1566
+ }
1567
+ if (keyObject.asymmetricKeyType === "ec") {
1568
+ const namedCurve = new Map([
1569
+ ["prime256v1", "P-256"],
1570
+ ["secp384r1", "P-384"],
1571
+ ["secp521r1", "P-521"]
1572
+ ]).get(keyObject.asymmetricKeyDetails?.namedCurve);
1573
+ if (!namedCurve) throw new TypeError(unusableForAlg);
1574
+ const expectedCurve = {
1575
+ ES256: "P-256",
1576
+ ES384: "P-384",
1577
+ ES512: "P-521"
1578
+ };
1579
+ if (expectedCurve[alg] && namedCurve === expectedCurve[alg]) cryptoKey = keyObject.toCryptoKey({
1580
+ name: "ECDSA",
1581
+ namedCurve
1582
+ }, extractable, [isPublic ? "verify" : "sign"]);
1583
+ if (alg.startsWith("ECDH-ES")) cryptoKey = keyObject.toCryptoKey({
1584
+ name: "ECDH",
1585
+ namedCurve
1586
+ }, extractable, isPublic ? [] : ["deriveBits"]);
1587
+ }
1588
+ if (!cryptoKey) throw new TypeError(unusableForAlg);
1589
+ if (!cached) cache.set(keyObject, { [alg]: cryptoKey });
1590
+ else cached[alg] = cryptoKey;
1591
+ return cryptoKey;
1592
+ };
1593
+ async function normalizeKey(key, alg) {
1594
+ if (key instanceof Uint8Array) return key;
1595
+ if (isCryptoKey(key)) return key;
1596
+ if (isKeyObject(key)) {
1597
+ if (key.type === "secret") return key.export();
1598
+ if ("toCryptoKey" in key && typeof key.toCryptoKey === "function") try {
1599
+ return handleKeyObject(key, alg);
1600
+ } catch (err) {
1601
+ if (err instanceof TypeError) throw err;
1602
+ }
1603
+ return handleJWK(key, key.export({ format: "jwk" }), alg);
1604
+ }
1605
+ if (isJWK(key)) {
1606
+ if (key.k) return decode(key.k);
1607
+ return handleJWK(key, key, alg, true);
1608
+ }
1609
+ throw new Error("unreachable");
1610
+ }
1611
+ //#endregion
1612
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/lib/validate_crit.js
1613
+ function validateCrit(Err, recognizedDefault, recognizedOption, protectedHeader, joseHeader) {
1614
+ if (joseHeader.crit !== void 0 && protectedHeader?.crit === void 0) throw new Err("\"crit\" (Critical) Header Parameter MUST be integrity protected");
1615
+ if (!protectedHeader || protectedHeader.crit === void 0) return /* @__PURE__ */ new Set();
1616
+ if (!Array.isArray(protectedHeader.crit) || protectedHeader.crit.length === 0 || protectedHeader.crit.some((input) => typeof input !== "string" || input.length === 0)) throw new Err("\"crit\" (Critical) Header Parameter MUST be an array of non-empty strings when present");
1617
+ let recognized;
1618
+ if (recognizedOption !== void 0) recognized = new Map([...Object.entries(recognizedOption), ...recognizedDefault.entries()]);
1619
+ else recognized = recognizedDefault;
1620
+ for (const parameter of protectedHeader.crit) {
1621
+ if (!recognized.has(parameter)) throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`);
1622
+ if (joseHeader[parameter] === void 0) throw new Err(`Extension Header Parameter "${parameter}" is missing`);
1623
+ if (recognized.get(parameter) && protectedHeader[parameter] === void 0) throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`);
1624
+ }
1625
+ return new Set(protectedHeader.crit);
1626
+ }
1627
+ //#endregion
1628
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/lib/validate_algorithms.js
1629
+ function validateAlgorithms(option, algorithms) {
1630
+ if (algorithms !== void 0 && (!Array.isArray(algorithms) || algorithms.some((s) => typeof s !== "string"))) throw new TypeError(`"${option}" option must be an array of strings`);
1631
+ if (!algorithms) return;
1632
+ return new Set(algorithms);
1633
+ }
1634
+ //#endregion
1635
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/lib/check_key_type.js
1636
+ const tag = (key) => key?.[Symbol.toStringTag];
1637
+ const jwkMatchesOp = (alg, key, usage) => {
1638
+ if (key.use !== void 0) {
1639
+ let expected;
1640
+ switch (usage) {
1641
+ case "sign":
1642
+ case "verify":
1643
+ expected = "sig";
1644
+ break;
1645
+ case "encrypt":
1646
+ case "decrypt":
1647
+ expected = "enc";
1648
+ break;
1649
+ }
1650
+ if (key.use !== expected) throw new TypeError(`Invalid key for this operation, its "use" must be "${expected}" when present`);
1651
+ }
1652
+ if (key.alg !== void 0 && key.alg !== alg) throw new TypeError(`Invalid key for this operation, its "alg" must be "${alg}" when present`);
1653
+ if (Array.isArray(key.key_ops)) {
1654
+ let expectedKeyOp;
1655
+ switch (true) {
1656
+ case usage === "sign" || usage === "verify":
1657
+ case alg === "dir":
1658
+ case alg.includes("CBC-HS"):
1659
+ expectedKeyOp = usage;
1660
+ break;
1661
+ case alg.startsWith("PBES2"):
1662
+ expectedKeyOp = "deriveBits";
1663
+ break;
1664
+ case /^A\d{3}(?:GCM)?(?:KW)?$/.test(alg):
1665
+ if (!alg.includes("GCM") && alg.endsWith("KW")) expectedKeyOp = usage === "encrypt" ? "wrapKey" : "unwrapKey";
1666
+ else expectedKeyOp = usage;
1667
+ break;
1668
+ case usage === "encrypt" && alg.startsWith("RSA"):
1669
+ expectedKeyOp = "wrapKey";
1670
+ break;
1671
+ case usage === "decrypt":
1672
+ expectedKeyOp = alg.startsWith("RSA") ? "unwrapKey" : "deriveBits";
1673
+ break;
1674
+ }
1675
+ if (expectedKeyOp && key.key_ops?.includes?.(expectedKeyOp) === false) throw new TypeError(`Invalid key for this operation, its "key_ops" must include "${expectedKeyOp}" when present`);
1676
+ }
1677
+ return true;
1678
+ };
1679
+ const symmetricTypeCheck = (alg, key, usage) => {
1680
+ if (key instanceof Uint8Array) return;
1681
+ if (isJWK(key)) {
1682
+ if (isSecretJWK(key) && jwkMatchesOp(alg, key, usage)) return;
1683
+ throw new TypeError(`JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present`);
1684
+ }
1685
+ if (!isKeyLike(key)) throw new TypeError(withAlg(alg, key, "CryptoKey", "KeyObject", "JSON Web Key", "Uint8Array"));
1686
+ if (key.type !== "secret") throw new TypeError(`${tag(key)} instances for symmetric algorithms must be of type "secret"`);
1687
+ };
1688
+ const asymmetricTypeCheck = (alg, key, usage) => {
1689
+ if (isJWK(key)) switch (usage) {
1690
+ case "decrypt":
1691
+ case "sign":
1692
+ if (isPrivateJWK(key) && jwkMatchesOp(alg, key, usage)) return;
1693
+ throw new TypeError(`JSON Web Key for this operation must be a private JWK`);
1694
+ case "encrypt":
1695
+ case "verify":
1696
+ if (isPublicJWK(key) && jwkMatchesOp(alg, key, usage)) return;
1697
+ throw new TypeError(`JSON Web Key for this operation must be a public JWK`);
1698
+ }
1699
+ if (!isKeyLike(key)) throw new TypeError(withAlg(alg, key, "CryptoKey", "KeyObject", "JSON Web Key"));
1700
+ if (key.type === "secret") throw new TypeError(`${tag(key)} instances for asymmetric algorithms must not be of type "secret"`);
1701
+ if (key.type === "public") switch (usage) {
1702
+ case "sign": throw new TypeError(`${tag(key)} instances for asymmetric algorithm signing must be of type "private"`);
1703
+ case "decrypt": throw new TypeError(`${tag(key)} instances for asymmetric algorithm decryption must be of type "private"`);
1704
+ }
1705
+ if (key.type === "private") switch (usage) {
1706
+ case "verify": throw new TypeError(`${tag(key)} instances for asymmetric algorithm verifying must be of type "public"`);
1707
+ case "encrypt": throw new TypeError(`${tag(key)} instances for asymmetric algorithm encryption must be of type "public"`);
1708
+ }
1709
+ };
1710
+ function checkKeyType(alg, key, usage) {
1711
+ switch (alg.substring(0, 2)) {
1712
+ case "A1":
1713
+ case "A2":
1714
+ case "di":
1715
+ case "HS":
1716
+ case "PB":
1717
+ symmetricTypeCheck(alg, key, usage);
1718
+ break;
1719
+ default: asymmetricTypeCheck(alg, key, usage);
1720
+ }
1721
+ }
1722
+ //#endregion
1723
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/jws/flattened/verify.js
1724
+ async function flattenedVerify(jws, key, options) {
1725
+ if (!isObject(jws)) throw new JWSInvalid("Flattened JWS must be an object");
1726
+ if (jws.protected === void 0 && jws.header === void 0) throw new JWSInvalid("Flattened JWS must have either of the \"protected\" or \"header\" members");
1727
+ if (jws.protected !== void 0 && typeof jws.protected !== "string") throw new JWSInvalid("JWS Protected Header incorrect type");
1728
+ if (jws.payload === void 0) throw new JWSInvalid("JWS Payload missing");
1729
+ if (typeof jws.signature !== "string") throw new JWSInvalid("JWS Signature missing or incorrect type");
1730
+ if (jws.header !== void 0 && !isObject(jws.header)) throw new JWSInvalid("JWS Unprotected Header incorrect type");
1731
+ let parsedProt = {};
1732
+ if (jws.protected) try {
1733
+ const protectedHeader = decode(jws.protected);
1734
+ parsedProt = JSON.parse(decoder.decode(protectedHeader));
1735
+ } catch {
1736
+ throw new JWSInvalid("JWS Protected Header is invalid");
1737
+ }
1738
+ if (!isDisjoint(parsedProt, jws.header)) throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint");
1739
+ const joseHeader = {
1740
+ ...parsedProt,
1741
+ ...jws.header
1742
+ };
1743
+ const extensions = validateCrit(JWSInvalid, new Map([["b64", true]]), options?.crit, parsedProt, joseHeader);
1744
+ let b64 = true;
1745
+ if (extensions.has("b64")) {
1746
+ b64 = parsedProt.b64;
1747
+ if (typeof b64 !== "boolean") throw new JWSInvalid("The \"b64\" (base64url-encode payload) Header Parameter must be a boolean");
1748
+ }
1749
+ const { alg } = joseHeader;
1750
+ if (typeof alg !== "string" || !alg) throw new JWSInvalid("JWS \"alg\" (Algorithm) Header Parameter missing or invalid");
1751
+ const algorithms = options && validateAlgorithms("algorithms", options.algorithms);
1752
+ if (algorithms && !algorithms.has(alg)) throw new JOSEAlgNotAllowed("\"alg\" (Algorithm) Header Parameter value not allowed");
1753
+ if (b64) {
1754
+ if (typeof jws.payload !== "string") throw new JWSInvalid("JWS Payload must be a string");
1755
+ } else if (typeof jws.payload !== "string" && !(jws.payload instanceof Uint8Array)) throw new JWSInvalid("JWS Payload must be a string or an Uint8Array instance");
1756
+ let resolvedKey = false;
1757
+ if (typeof key === "function") {
1758
+ key = await key(parsedProt, jws);
1759
+ resolvedKey = true;
1760
+ }
1761
+ checkKeyType(alg, key, "verify");
1762
+ const data = concat(jws.protected !== void 0 ? encode$1(jws.protected) : new Uint8Array(), encode$1("."), typeof jws.payload === "string" ? b64 ? encode$1(jws.payload) : encoder.encode(jws.payload) : jws.payload);
1763
+ const signature = decodeBase64url(jws.signature, "signature", JWSInvalid);
1764
+ const k = await normalizeKey(key, alg);
1765
+ if (!await verify(alg, k, signature, data)) throw new JWSSignatureVerificationFailed();
1766
+ let payload;
1767
+ if (b64) payload = decodeBase64url(jws.payload, "payload", JWSInvalid);
1768
+ else if (typeof jws.payload === "string") payload = encoder.encode(jws.payload);
1769
+ else payload = jws.payload;
1770
+ const result = { payload };
1771
+ if (jws.protected !== void 0) result.protectedHeader = parsedProt;
1772
+ if (jws.header !== void 0) result.unprotectedHeader = jws.header;
1773
+ if (resolvedKey) return {
1774
+ ...result,
1775
+ key: k
1776
+ };
1777
+ return result;
1778
+ }
1779
+ //#endregion
1780
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/jws/compact/verify.js
1781
+ async function compactVerify(jws, key, options) {
1782
+ if (jws instanceof Uint8Array) jws = decoder.decode(jws);
1783
+ if (typeof jws !== "string") throw new JWSInvalid("Compact JWS must be a string or Uint8Array");
1784
+ const { 0: protectedHeader, 1: payload, 2: signature, length } = jws.split(".");
1785
+ if (length !== 3) throw new JWSInvalid("Invalid Compact JWS");
1786
+ const verified = await flattenedVerify({
1787
+ payload,
1788
+ protected: protectedHeader,
1789
+ signature
1790
+ }, key, options);
1791
+ const result = {
1792
+ payload: verified.payload,
1793
+ protectedHeader: verified.protectedHeader
1794
+ };
1795
+ if (typeof key === "function") return {
1796
+ ...result,
1797
+ key: verified.key
1798
+ };
1799
+ return result;
1800
+ }
1801
+ //#endregion
1802
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/lib/jwt_claims_set.js
1803
+ const epoch = (date) => Math.floor(date.getTime() / 1e3);
1804
+ const minute = 60;
1805
+ const hour = minute * 60;
1806
+ const day = hour * 24;
1807
+ const week = day * 7;
1808
+ const year = day * 365.25;
1809
+ const REGEX = /^(\+|\-)? ?(\d+|\d+\.\d+) ?(seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)(?: (ago|from now))?$/i;
1810
+ function secs(str) {
1811
+ const matched = REGEX.exec(str);
1812
+ if (!matched || matched[4] && matched[1]) throw new TypeError("Invalid time period format");
1813
+ const value = parseFloat(matched[2]);
1814
+ const unit = matched[3].toLowerCase();
1815
+ let numericDate;
1816
+ switch (unit) {
1817
+ case "sec":
1818
+ case "secs":
1819
+ case "second":
1820
+ case "seconds":
1821
+ case "s":
1822
+ numericDate = Math.round(value);
1823
+ break;
1824
+ case "minute":
1825
+ case "minutes":
1826
+ case "min":
1827
+ case "mins":
1828
+ case "m":
1829
+ numericDate = Math.round(value * minute);
1830
+ break;
1831
+ case "hour":
1832
+ case "hours":
1833
+ case "hr":
1834
+ case "hrs":
1835
+ case "h":
1836
+ numericDate = Math.round(value * hour);
1837
+ break;
1838
+ case "day":
1839
+ case "days":
1840
+ case "d":
1841
+ numericDate = Math.round(value * day);
1842
+ break;
1843
+ case "week":
1844
+ case "weeks":
1845
+ case "w":
1846
+ numericDate = Math.round(value * week);
1847
+ break;
1848
+ default:
1849
+ numericDate = Math.round(value * year);
1850
+ break;
1851
+ }
1852
+ if (matched[1] === "-" || matched[4] === "ago") return -numericDate;
1853
+ return numericDate;
1854
+ }
1855
+ function validateInput(label, input) {
1856
+ if (!Number.isFinite(input)) throw new TypeError(`Invalid ${label} input`);
1857
+ return input;
1858
+ }
1859
+ const normalizeTyp = (value) => {
1860
+ if (value.includes("/")) return value.toLowerCase();
1861
+ return `application/${value.toLowerCase()}`;
1862
+ };
1863
+ const checkAudiencePresence = (audPayload, audOption) => {
1864
+ if (typeof audPayload === "string") return audOption.includes(audPayload);
1865
+ if (Array.isArray(audPayload)) return audOption.some(Set.prototype.has.bind(new Set(audPayload)));
1866
+ return false;
1867
+ };
1868
+ function validateClaimsSet(protectedHeader, encodedPayload, options = {}) {
1869
+ let payload;
1870
+ try {
1871
+ payload = JSON.parse(decoder.decode(encodedPayload));
1872
+ } catch {}
1873
+ if (!isObject(payload)) throw new JWTInvalid("JWT Claims Set must be a top-level JSON object");
1874
+ const { typ } = options;
1875
+ if (typ && (typeof protectedHeader.typ !== "string" || normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ))) throw new JWTClaimValidationFailed("unexpected \"typ\" JWT header value", payload, "typ", "check_failed");
1876
+ const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options;
1877
+ const presenceCheck = [...requiredClaims];
1878
+ if (maxTokenAge !== void 0) presenceCheck.push("iat");
1879
+ if (audience !== void 0) presenceCheck.push("aud");
1880
+ if (subject !== void 0) presenceCheck.push("sub");
1881
+ if (issuer !== void 0) presenceCheck.push("iss");
1882
+ for (const claim of new Set(presenceCheck.reverse())) if (!(claim in payload)) throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, payload, claim, "missing");
1883
+ if (issuer && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss)) throw new JWTClaimValidationFailed("unexpected \"iss\" claim value", payload, "iss", "check_failed");
1884
+ if (subject && payload.sub !== subject) throw new JWTClaimValidationFailed("unexpected \"sub\" claim value", payload, "sub", "check_failed");
1885
+ if (audience && !checkAudiencePresence(payload.aud, typeof audience === "string" ? [audience] : audience)) throw new JWTClaimValidationFailed("unexpected \"aud\" claim value", payload, "aud", "check_failed");
1886
+ let tolerance;
1887
+ switch (typeof options.clockTolerance) {
1888
+ case "string":
1889
+ tolerance = secs(options.clockTolerance);
1890
+ break;
1891
+ case "number":
1892
+ tolerance = options.clockTolerance;
1893
+ break;
1894
+ case "undefined":
1895
+ tolerance = 0;
1896
+ break;
1897
+ default: throw new TypeError("Invalid clockTolerance option type");
1898
+ }
1899
+ const { currentDate } = options;
1900
+ const now = epoch(currentDate || /* @__PURE__ */ new Date());
1901
+ if ((payload.iat !== void 0 || maxTokenAge) && typeof payload.iat !== "number") throw new JWTClaimValidationFailed("\"iat\" claim must be a number", payload, "iat", "invalid");
1902
+ if (payload.nbf !== void 0) {
1903
+ if (typeof payload.nbf !== "number") throw new JWTClaimValidationFailed("\"nbf\" claim must be a number", payload, "nbf", "invalid");
1904
+ if (payload.nbf > now + tolerance) throw new JWTClaimValidationFailed("\"nbf\" claim timestamp check failed", payload, "nbf", "check_failed");
1905
+ }
1906
+ if (payload.exp !== void 0) {
1907
+ if (typeof payload.exp !== "number") throw new JWTClaimValidationFailed("\"exp\" claim must be a number", payload, "exp", "invalid");
1908
+ if (payload.exp <= now - tolerance) throw new JWTExpired("\"exp\" claim timestamp check failed", payload, "exp", "check_failed");
1909
+ }
1910
+ if (maxTokenAge) {
1911
+ const age = now - payload.iat;
1912
+ const max = typeof maxTokenAge === "number" ? maxTokenAge : secs(maxTokenAge);
1913
+ if (age - tolerance > max) throw new JWTExpired("\"iat\" claim timestamp check failed (too far in the past)", payload, "iat", "check_failed");
1914
+ if (age < 0 - tolerance) throw new JWTClaimValidationFailed("\"iat\" claim timestamp check failed (it should be in the past)", payload, "iat", "check_failed");
1915
+ }
1916
+ return payload;
1917
+ }
1918
+ var JWTClaimsBuilder = class {
1919
+ #payload;
1920
+ constructor(payload) {
1921
+ if (!isObject(payload)) throw new TypeError("JWT Claims Set MUST be an object");
1922
+ this.#payload = structuredClone(payload);
1923
+ }
1924
+ data() {
1925
+ return encoder.encode(JSON.stringify(this.#payload));
1926
+ }
1927
+ get iss() {
1928
+ return this.#payload.iss;
1929
+ }
1930
+ set iss(value) {
1931
+ this.#payload.iss = value;
1932
+ }
1933
+ get sub() {
1934
+ return this.#payload.sub;
1935
+ }
1936
+ set sub(value) {
1937
+ this.#payload.sub = value;
1938
+ }
1939
+ get aud() {
1940
+ return this.#payload.aud;
1941
+ }
1942
+ set aud(value) {
1943
+ this.#payload.aud = value;
1944
+ }
1945
+ set jti(value) {
1946
+ this.#payload.jti = value;
1947
+ }
1948
+ set nbf(value) {
1949
+ if (typeof value === "number") this.#payload.nbf = validateInput("setNotBefore", value);
1950
+ else if (value instanceof Date) this.#payload.nbf = validateInput("setNotBefore", epoch(value));
1951
+ else this.#payload.nbf = epoch(/* @__PURE__ */ new Date()) + secs(value);
1952
+ }
1953
+ set exp(value) {
1954
+ if (typeof value === "number") this.#payload.exp = validateInput("setExpirationTime", value);
1955
+ else if (value instanceof Date) this.#payload.exp = validateInput("setExpirationTime", epoch(value));
1956
+ else this.#payload.exp = epoch(/* @__PURE__ */ new Date()) + secs(value);
1957
+ }
1958
+ set iat(value) {
1959
+ if (value === void 0) this.#payload.iat = epoch(/* @__PURE__ */ new Date());
1960
+ else if (value instanceof Date) this.#payload.iat = validateInput("setIssuedAt", epoch(value));
1961
+ else if (typeof value === "string") this.#payload.iat = validateInput("setIssuedAt", epoch(/* @__PURE__ */ new Date()) + secs(value));
1962
+ else this.#payload.iat = validateInput("setIssuedAt", value);
1963
+ }
1964
+ };
1965
+ //#endregion
1966
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/jwt/verify.js
1967
+ async function jwtVerify(jwt, key, options) {
1968
+ const verified = await compactVerify(jwt, key, options);
1969
+ if (verified.protectedHeader.crit?.includes("b64") && verified.protectedHeader.b64 === false) throw new JWTInvalid("JWTs MUST NOT use unencoded payload");
1970
+ const result = {
1971
+ payload: validateClaimsSet(verified.protectedHeader, verified.payload, options),
1972
+ protectedHeader: verified.protectedHeader
1973
+ };
1974
+ if (typeof key === "function") return {
1975
+ ...result,
1976
+ key: verified.key
1977
+ };
1978
+ return result;
1979
+ }
1980
+ //#endregion
1981
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/jws/flattened/sign.js
1982
+ var FlattenedSign = class {
1983
+ #payload;
1984
+ #protectedHeader;
1985
+ #unprotectedHeader;
1986
+ constructor(payload) {
1987
+ if (!(payload instanceof Uint8Array)) throw new TypeError("payload must be an instance of Uint8Array");
1988
+ this.#payload = payload;
1989
+ }
1990
+ setProtectedHeader(protectedHeader) {
1991
+ assertNotSet(this.#protectedHeader, "setProtectedHeader");
1992
+ this.#protectedHeader = protectedHeader;
1993
+ return this;
1994
+ }
1995
+ setUnprotectedHeader(unprotectedHeader) {
1996
+ assertNotSet(this.#unprotectedHeader, "setUnprotectedHeader");
1997
+ this.#unprotectedHeader = unprotectedHeader;
1998
+ return this;
1999
+ }
2000
+ async sign(key, options) {
2001
+ if (!this.#protectedHeader && !this.#unprotectedHeader) throw new JWSInvalid("either setProtectedHeader or setUnprotectedHeader must be called before #sign()");
2002
+ if (!isDisjoint(this.#protectedHeader, this.#unprotectedHeader)) throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint");
2003
+ const joseHeader = {
2004
+ ...this.#protectedHeader,
2005
+ ...this.#unprotectedHeader
2006
+ };
2007
+ const extensions = validateCrit(JWSInvalid, new Map([["b64", true]]), options?.crit, this.#protectedHeader, joseHeader);
2008
+ let b64 = true;
2009
+ if (extensions.has("b64")) {
2010
+ b64 = this.#protectedHeader.b64;
2011
+ if (typeof b64 !== "boolean") throw new JWSInvalid("The \"b64\" (base64url-encode payload) Header Parameter must be a boolean");
2012
+ }
2013
+ const { alg } = joseHeader;
2014
+ if (typeof alg !== "string" || !alg) throw new JWSInvalid("JWS \"alg\" (Algorithm) Header Parameter missing or invalid");
2015
+ checkKeyType(alg, key, "sign");
2016
+ let payloadS;
2017
+ let payloadB;
2018
+ if (b64) {
2019
+ payloadS = encode(this.#payload);
2020
+ payloadB = encode$1(payloadS);
2021
+ } else {
2022
+ payloadB = this.#payload;
2023
+ payloadS = "";
2024
+ }
2025
+ let protectedHeaderString;
2026
+ let protectedHeaderBytes;
2027
+ if (this.#protectedHeader) {
2028
+ protectedHeaderString = encode(JSON.stringify(this.#protectedHeader));
2029
+ protectedHeaderBytes = encode$1(protectedHeaderString);
2030
+ } else {
2031
+ protectedHeaderString = "";
2032
+ protectedHeaderBytes = new Uint8Array();
2033
+ }
2034
+ const data = concat(protectedHeaderBytes, encode$1("."), payloadB);
2035
+ const jws = {
2036
+ signature: encode(await sign(alg, await normalizeKey(key, alg), data)),
2037
+ payload: payloadS
2038
+ };
2039
+ if (this.#unprotectedHeader) jws.header = this.#unprotectedHeader;
2040
+ if (this.#protectedHeader) jws.protected = protectedHeaderString;
2041
+ return jws;
2042
+ }
2043
+ };
2044
+ //#endregion
2045
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/jws/compact/sign.js
2046
+ var CompactSign = class {
2047
+ #flattened;
2048
+ constructor(payload) {
2049
+ this.#flattened = new FlattenedSign(payload);
2050
+ }
2051
+ setProtectedHeader(protectedHeader) {
2052
+ this.#flattened.setProtectedHeader(protectedHeader);
2053
+ return this;
2054
+ }
2055
+ async sign(key, options) {
2056
+ const jws = await this.#flattened.sign(key, options);
2057
+ if (jws.payload === void 0) throw new TypeError("use the flattened module for creating JWS with b64: false");
2058
+ return `${jws.protected}.${jws.payload}.${jws.signature}`;
2059
+ }
2060
+ };
2061
+ //#endregion
2062
+ //#region node_modules/.pnpm/jose@6.2.1/node_modules/jose/dist/webapi/jwt/sign.js
2063
+ var SignJWT = class {
2064
+ #protectedHeader;
2065
+ #jwt;
2066
+ constructor(payload = {}) {
2067
+ this.#jwt = new JWTClaimsBuilder(payload);
2068
+ }
2069
+ setIssuer(issuer) {
2070
+ this.#jwt.iss = issuer;
2071
+ return this;
2072
+ }
2073
+ setSubject(subject) {
2074
+ this.#jwt.sub = subject;
2075
+ return this;
2076
+ }
2077
+ setAudience(audience) {
2078
+ this.#jwt.aud = audience;
2079
+ return this;
2080
+ }
2081
+ setJti(jwtId) {
2082
+ this.#jwt.jti = jwtId;
2083
+ return this;
2084
+ }
2085
+ setNotBefore(input) {
2086
+ this.#jwt.nbf = input;
2087
+ return this;
2088
+ }
2089
+ setExpirationTime(input) {
2090
+ this.#jwt.exp = input;
2091
+ return this;
2092
+ }
2093
+ setIssuedAt(input) {
2094
+ this.#jwt.iat = input;
2095
+ return this;
2096
+ }
2097
+ setProtectedHeader(protectedHeader) {
2098
+ this.#protectedHeader = protectedHeader;
2099
+ return this;
2100
+ }
2101
+ async sign(key, options) {
2102
+ const sig = new CompactSign(this.#jwt.data());
2103
+ sig.setProtectedHeader(this.#protectedHeader);
2104
+ if (Array.isArray(this.#protectedHeader?.crit) && this.#protectedHeader.crit.includes("b64") && this.#protectedHeader.b64 === false) throw new JWTInvalid("JWTs MUST NOT use unencoded payload");
2105
+ return sig.sign(key, options);
2106
+ }
2107
+ };
2108
+ //#endregion
2109
+ //#region src/core/session.ts
2110
+ /**
2111
+ * Creates a signed JWT containing the given session payload.
2112
+ *
2113
+ * @param secret - The secret key used to sign the JWT.
2114
+ * @param payload - The session data to embed in the token.
2115
+ * @param expiresIn - How long until the token expires (e.g., `"30d"`), or `null` for the default (`"30d"`).
2116
+ * @returns The signed JWT string.
2117
+ */
2118
+ async function createSessionJwt(secret, payload, expiresIn = null) {
2119
+ const key = new TextEncoder().encode(secret);
2120
+ return new SignJWT({ ...payload }).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(expiresIn ?? "30d").sign(key);
2121
+ }
2122
+ /**
2123
+ * Verifies a session JWT and returns the validated session payload.
2124
+ *
2125
+ * @param secret - The secret key used to verify the JWT signature.
2126
+ * @param token - The JWT string to verify.
2127
+ * @returns A {@link Result} containing the validated {@link SessionPayload}, or a {@link SessionVerifyError}.
2128
+ */
2129
+ async function verifySessionJwt(secret, token) {
2130
+ const key = new TextEncoder().encode(secret);
2131
+ let payload;
2132
+ try {
2133
+ payload = (await jwtVerify(token, key)).payload;
2134
+ } catch (e) {
2135
+ return (0, import_index_cjs.err)({
2136
+ code: "JWT_VERIFY_FAILED",
2137
+ message: e instanceof Error ? e.message : "JWT verification failed"
2138
+ });
2139
+ }
2140
+ const parsed = SessionPayloadSchema.safeParse(payload);
2141
+ if (!parsed.success) return (0, import_index_cjs.err)({
2142
+ code: "SESSION_VALIDATION_ERROR",
2143
+ message: parsed.error.message
2144
+ });
2145
+ return (0, import_index_cjs.ok)(parsed.data);
2146
+ }
2147
+ //#endregion
2148
+ //#region src/core/embedded.ts
2149
+ const AUTHORIZE_ON_BEHALF_PATH = "/api/v1/app-integrations/authorize-on-behalf";
2150
+ const SESSION_TOKEN_QUERY_PARAM = "token";
2151
+ const THEME_QUERY_PARAM = "theme";
2152
+ /**
2153
+ * Extracts the embedded session token from a URL.
2154
+ *
2155
+ * When Fanvue opens an embedded app, it loads the app's embed URL with a
2156
+ * short-lived session token appended as a `?token=` query parameter.
2157
+ *
2158
+ * @param url - The URL to read the token from (e.g. `window.location.href`).
2159
+ * @returns The session token, or `null` when absent or the URL is malformed.
2160
+ */
2161
+ function getSessionTokenFromUrl(url) {
2162
+ let parsed;
2163
+ try {
2164
+ parsed = new URL(url);
2165
+ } catch {
2166
+ return null;
2167
+ }
2168
+ const token = parsed.searchParams.get(SESSION_TOKEN_QUERY_PARAM);
2169
+ return token !== null && token.length > 0 ? token : null;
2170
+ }
2171
+ /**
2172
+ * Extracts the creator's active colour scheme from a URL.
2173
+ *
2174
+ * When Fanvue opens an embedded app, it appends the creator's resolved colour
2175
+ * scheme as a `?theme=` query parameter (`light` or `dark`), letting the app
2176
+ * theme-match Fanvue.
2177
+ *
2178
+ * @param url - The URL to read the theme from (e.g. `window.location.href`).
2179
+ * @returns The {@link FanvueTheme}, or `null` when absent, unrecognised, or the
2180
+ * URL is malformed (e.g. the app was opened outside Fanvue).
2181
+ */
2182
+ function getThemeFromUrl(url) {
2183
+ let parsed;
2184
+ try {
2185
+ parsed = new URL(url);
2186
+ } catch {
2187
+ return null;
2188
+ }
2189
+ const theme = parsed.searchParams.get(THEME_QUERY_PARAM);
2190
+ return theme === "light" || theme === "dark" ? theme : null;
2191
+ }
2192
+ /**
2193
+ * Requests an authorization code from the Fanvue platform on behalf of the
2194
+ * creator currently using the embedded app.
2195
+ *
2196
+ * This is the "authorize" half of the delegated flow: the platform verifies
2197
+ * the session token, runs the OAuth authorize as the creator, and returns the
2198
+ * resulting authorization code. The PKCE verifier and client secret never
2199
+ * leave the caller — only the challenge is sent.
2200
+ *
2201
+ * Most apps should use {@link exchangeSessionToken}, which composes this with
2202
+ * the code exchange.
2203
+ *
2204
+ * @param config - The embedded auth configuration.
2205
+ * @param sessionToken - The short-lived session token received in the iframe.
2206
+ * @param opts - The PKCE code challenge and state to bind the code to.
2207
+ * @returns A `Result` containing the authorization code on success or
2208
+ * {@link EmbeddedAuthError} on failure.
2209
+ */
2210
+ async function requestAuthorizationCodeOnBehalf(config, sessionToken, opts) {
2211
+ const platformUrl = config.platformUrl ?? "https://www.fanvue.com";
2212
+ let response;
2213
+ try {
2214
+ response = await fetch(`${platformUrl}${AUTHORIZE_ON_BEHALF_PATH}`, {
2215
+ method: "POST",
2216
+ headers: {
2217
+ "Content-Type": "application/json",
2218
+ Authorization: `Bearer ${sessionToken}`
2219
+ },
2220
+ body: JSON.stringify({
2221
+ code_challenge: opts.codeChallenge,
2222
+ code_challenge_method: "S256",
2223
+ state: opts.state
2224
+ })
2225
+ });
2226
+ } catch (error) {
2227
+ return (0, import_index_cjs.err)({
2228
+ code: "AUTHORIZE_ON_BEHALF_FAILED",
2229
+ statusCode: 0,
2230
+ message: `Network error during authorize-on-behalf: ${error instanceof Error ? error.message : String(error)}`
2231
+ });
2232
+ }
2233
+ if (response.status === 401) return (0, import_index_cjs.err)({
2234
+ code: "SESSION_TOKEN_REJECTED",
2235
+ statusCode: response.status,
2236
+ message: "The platform rejected the session token. Session tokens expire after ~60 seconds — reopen the embedded surface to receive a fresh one."
2237
+ });
2238
+ if (response.status === 403) return (0, import_index_cjs.err)({
2239
+ code: "CONSENT_REQUIRED",
2240
+ statusCode: response.status,
2241
+ message: "The creator has not approved (or has revoked) consent for this app. Consent is granted on the Fanvue consent screen when the surface is opened."
2242
+ });
2243
+ if (!response.ok) return (0, import_index_cjs.err)({
2244
+ code: "AUTHORIZE_ON_BEHALF_FAILED",
2245
+ statusCode: response.status,
2246
+ message: `authorize-on-behalf failed: ${response.status} ${response.statusText}`
2247
+ });
2248
+ const parseResult = safeJsonParse(await response.text());
2249
+ if (parseResult.isErr()) {
2250
+ const { rawText, message } = parseResult.error;
2251
+ return (0, import_index_cjs.err)({
2252
+ code: "EMBEDDED_JSON_PARSE_ERROR",
2253
+ rawText,
2254
+ message
2255
+ });
2256
+ }
2257
+ const validated = AuthorizeOnBehalfResponseSchema.safeParse(parseResult.value);
2258
+ if (!validated.success) return (0, import_index_cjs.err)({
2259
+ code: "EMBEDDED_VALIDATION_ERROR",
2260
+ message: validated.error.message
2261
+ });
2262
+ if (validated.data.state !== opts.state) return (0, import_index_cjs.err)({
2263
+ code: "AUTHORIZE_STATE_MISMATCH",
2264
+ message: "authorize-on-behalf returned a different state than was sent."
2265
+ });
2266
+ return (0, import_index_cjs.ok)({ code: validated.data.code });
2267
+ }
2268
+ /**
2269
+ * Exchanges an embedded session token for OAuth access and refresh tokens.
2270
+ *
2271
+ * Runs the complete delegated authorize-on-behalf flow:
2272
+ *
2273
+ * 1. Generates a PKCE verifier/challenge pair and a random `state`.
2274
+ * 2. Asks the Fanvue platform to authorize as the creator
2275
+ * ({@link requestAuthorizationCodeOnBehalf}).
2276
+ * 3. Exchanges the returned code at the token endpoint using the client
2277
+ * secret and PKCE verifier.
2278
+ *
2279
+ * Must run server-side: it uses the client secret. The session token should
2280
+ * be used promptly — it expires after ~60 seconds.
2281
+ *
2282
+ * @param config - The embedded auth configuration. `redirectUri` must be a
2283
+ * redirect URI registered on the OAuth client (it is never visited; it only
2284
+ * binds the authorization code).
2285
+ * @param sessionToken - The short-lived session token received in the iframe.
2286
+ * @returns A `Result` containing {@link TokenResponse} on success or an
2287
+ * {@link EmbeddedAuthError} / {@link OAuthError} on failure.
2288
+ */
2289
+ async function exchangeSessionToken(config, sessionToken) {
2290
+ const codeVerifier = generateRandomCodeVerifier();
2291
+ const codeResult = await requestAuthorizationCodeOnBehalf(config, sessionToken, {
2292
+ codeChallenge: await calculatePKCECodeChallenge(codeVerifier),
2293
+ state: generateRandomState()
2294
+ });
2295
+ if (codeResult.isErr()) return (0, import_index_cjs.err)(codeResult.error);
2296
+ return exchangeCodeForToken(config, {
2297
+ code: codeResult.value.code,
2298
+ codeVerifier,
2299
+ redirectUri: null
2300
+ });
2301
+ }
2302
+ //#endregion
2303
+ //#region src/core/client.ts
2304
+ /**
2305
+ * Creates a new {@link FanvueClient} for making authenticated requests to the Fanvue API.
2306
+ *
2307
+ * @param accessToken - The OAuth access token to authenticate requests.
2308
+ * @param apiBaseUrl - The base URL for the Fanvue API, or `null` to use the default.
2309
+ * @returns A {@link FanvueClient} instance.
2310
+ */
2311
+ function createFanvueClient(accessToken, apiBaseUrl) {
2312
+ if (apiBaseUrl != null) assertFanvueDomain(apiBaseUrl);
2313
+ const baseUrl = apiBaseUrl ?? "https://api.fanvue.com";
2314
+ async function fetchApi(path) {
2315
+ let response;
2316
+ try {
2317
+ response = await fetch(`${baseUrl}${path}`, { headers: {
2318
+ Authorization: `Bearer ${accessToken}`,
2319
+ "X-Fanvue-API-Version": API_VERSION
2320
+ } });
2321
+ } catch (error) {
2322
+ return (0, import_index_cjs.err)({
2323
+ code: "API_REQUEST_FAILED",
2324
+ statusCode: 0,
2325
+ message: `Network error: ${error instanceof Error ? error.message : String(error)}`
2326
+ });
2327
+ }
2328
+ if (!response.ok) return (0, import_index_cjs.err)({
2329
+ code: "API_REQUEST_FAILED",
2330
+ statusCode: response.status,
2331
+ message: `API request failed: ${response.status} ${response.statusText}`
2332
+ });
2333
+ const parseResult = safeJsonParse(await response.text());
2334
+ if (parseResult.isErr()) {
2335
+ const { rawText, message } = parseResult.error;
2336
+ return (0, import_index_cjs.err)({
2337
+ code: "API_JSON_PARSE_ERROR",
2338
+ rawText,
2339
+ message
2340
+ });
2341
+ }
2342
+ return (0, import_index_cjs.ok)(parseResult.value);
2343
+ }
2344
+ async function getCurrentUser() {
2345
+ const result = await fetchApi("/users/me");
2346
+ if (result.isErr()) return (0, import_index_cjs.err)(result.error);
2347
+ const validated = FanvueUserSchema.safeParse(result.value);
2348
+ if (!validated.success) return (0, import_index_cjs.err)({
2349
+ code: "API_VALIDATION_ERROR",
2350
+ message: validated.error.message
2351
+ });
2352
+ return (0, import_index_cjs.ok)(validated.data);
2353
+ }
2354
+ return { getCurrentUser };
2355
+ }
2356
+ //#endregion
2357
+ //#region src/core/machine-auth/machine-auth.ts
2358
+ /**
2359
+ * Shortest bearer secret the SDK will compare against.
2360
+ *
2361
+ * A cron endpoint is reachable by anyone on the internet and its secret is
2362
+ * never rotated by a user, so a guessable one is a standing invitation. Below
2363
+ * this length the secret is treated as absent — the endpoint reports
2364
+ * `not_configured` instead of quietly accepting a weak credential.
2365
+ */
2366
+ const MINIMUM_BEARER_SECRET_LENGTH = 32;
2367
+ /**
2368
+ * The `Bearer` auth scheme and its separating whitespace.
2369
+ *
2370
+ * Case-insensitive because RFC 7235 defines auth scheme names as
2371
+ * case-insensitive — `bearer` is as valid on the wire as `Bearer`. Non-global,
2372
+ * so the shared instance carries no `lastIndex` between calls.
2373
+ */
2374
+ const BEARER_SCHEME_PATTERN = /^bearer[ \t]+/i;
2375
+ const textEncoder = new TextEncoder();
2376
+ /**
2377
+ * Compares two strings in time independent of their content.
2378
+ *
2379
+ * Hand-rolled rather than `node:crypto`'s `timingSafeEqual` because
2380
+ * `src/core` must stay runtime-agnostic — it is also imported by browser and
2381
+ * edge bundles, which a `node:crypto` import would break. This is a byte
2382
+ * comparison, not a cryptographic primitive: every byte is XOR-accumulated with
2383
+ * no early exit, and the length check happens before any comparison (an
2384
+ * unavoidable leak of length, matching `timingSafeEqual`'s own precondition).
2385
+ */
2386
+ function constantTimeTextEqual(left, right) {
2387
+ const leftBytes = textEncoder.encode(left);
2388
+ const rightBytes = textEncoder.encode(right);
2389
+ if (leftBytes.byteLength !== rightBytes.byteLength) return false;
2390
+ let difference = 0;
2391
+ for (let index = 0; index < leftBytes.byteLength; index += 1) difference |= (leftBytes[index] ?? 0) ^ (rightBytes[index] ?? 0);
2392
+ return difference === 0;
2393
+ }
2394
+ /**
2395
+ * Reads the bearer credential from an `Authorization` header.
2396
+ *
2397
+ * The scheme is required: a bare secret in the header is rejected, so a client
2398
+ * that forgets the scheme fails loudly rather than working by accident against
2399
+ * one deployment and not another. The scheme name itself is matched
2400
+ * case-insensitively, per RFC 7235 — rejecting `bearer` would be a lie about
2401
+ * what the header means.
2402
+ */
2403
+ function bearerToken(request) {
2404
+ const header = request.headers.get("authorization");
2405
+ if (header === null) return null;
2406
+ const scheme = BEARER_SCHEME_PATTERN.exec(header);
2407
+ if (scheme === null) return null;
2408
+ return header.slice(scheme[0].length);
2409
+ }
2410
+ async function verifiesSignature(request, verifier, rawBody) {
2411
+ try {
2412
+ return await verifier.verify(request, rawBody ?? new Uint8Array(0));
2413
+ } catch {
2414
+ return false;
2415
+ }
2416
+ }
2417
+ /**
2418
+ * Authenticates a machine-invoked route (cron tick, queue drain, sweeper).
2419
+ *
2420
+ * Two strategies, tried in order, either of which is sufficient:
2421
+ *
2422
+ * 1. a shared bearer secret compared in constant time, and
2423
+ * 2. an app-supplied {@link SignedRequestVerifier}.
2424
+ *
2425
+ * Deny by default. When neither strategy is usable — no verifier and a missing
2426
+ * or too-short `bearerSecret` — the result is `not_configured`, which the
2427
+ * caller should surface as 503; `unauthorized` (401) means a strategy was
2428
+ * available and the request failed it.
2429
+ *
2430
+ * Web-standard `Request` only, so this works unchanged in a Next.js route
2431
+ * handler, a Node server and an edge function.
2432
+ *
2433
+ * @param request - The incoming machine request.
2434
+ * @param options - Available credentials and strategies.
2435
+ * @returns The discriminated {@link MachineAuthResult}.
2436
+ * @example
2437
+ * // Bearer-only cron route.
2438
+ * const auth = await requireMachineAuth(request, {
2439
+ * bearerSecret: process.env.CRON_SECRET ?? null,
2440
+ * signedRequestVerifier: null,
2441
+ * rawBody: null,
2442
+ * });
2443
+ * if (auth.status === 'not_configured') return new Response(null, { status: 503 });
2444
+ * if (auth.status === 'unauthorized') return new Response(null, { status: 401 });
2445
+ * @example
2446
+ * // Queue drain that also accepts QStash-signed deliveries. `@upstash/qstash`
2447
+ * // is intentionally NOT a dependency of this package — the app owns it:
2448
+ * //
2449
+ * // import { Receiver } from '@upstash/qstash';
2450
+ * //
2451
+ * // const qstashVerifier: SignedRequestVerifier = {
2452
+ * // verify: async (request, rawBody) => {
2453
+ * // const signature = request.headers.get('upstash-signature');
2454
+ * // const currentSigningKey = process.env.QSTASH_CURRENT_SIGNING_KEY;
2455
+ * // const nextSigningKey = process.env.QSTASH_NEXT_SIGNING_KEY;
2456
+ * // if (!signature || !currentSigningKey || !nextSigningKey) return false;
2457
+ * // return new Receiver({ currentSigningKey, nextSigningKey }).verify({
2458
+ * // signature,
2459
+ * // body: new TextDecoder().decode(rawBody),
2460
+ * // url: request.url,
2461
+ * // upstashRegion: request.headers.get('upstash-region') ?? undefined,
2462
+ * // });
2463
+ * // },
2464
+ * // };
2465
+ * //
2466
+ * // Pass the raw bytes, never a reparsed body:
2467
+ * const rawBody = new Uint8Array(await request.arrayBuffer());
2468
+ * const auth = await requireMachineAuth(request, {
2469
+ * bearerSecret: process.env.CRON_SECRET ?? null,
2470
+ * signedRequestVerifier: qstashVerifier,
2471
+ * rawBody,
2472
+ * });
2473
+ */
2474
+ async function requireMachineAuth(request, options) {
2475
+ const { bearerSecret, signedRequestVerifier, rawBody } = options;
2476
+ const usableSecret = bearerSecret !== null && bearerSecret.length >= 32 ? bearerSecret : null;
2477
+ if (usableSecret === null && signedRequestVerifier === null) return { status: "not_configured" };
2478
+ if (usableSecret !== null) {
2479
+ const provided = bearerToken(request);
2480
+ if (provided !== null && constantTimeTextEqual(provided, usableSecret)) return { status: "authorized" };
2481
+ }
2482
+ if (signedRequestVerifier !== null) {
2483
+ if (await verifiesSignature(request, signedRequestVerifier, rawBody)) return { status: "authorized" };
2484
+ }
2485
+ return { status: "unauthorized" };
2486
+ }
2487
+ /**
2488
+ * Clamps a caller-supplied batch size to a safe positive bound.
2489
+ *
2490
+ * Machine routes take `?limit=` to let an operator drain faster, which is also
2491
+ * an unauthenticated-shaped lever on database load, so the value is bounded
2492
+ * rather than trusted. Anything unparseable, non-integer, zero or negative
2493
+ * falls back to the default; the default itself is capped by `max`, so a
2494
+ * misconfigured default cannot exceed the ceiling either.
2495
+ *
2496
+ * The result is always a positive safe integer, even when `defaultSize` or
2497
+ * `max` is itself invalid (non-integer, zero or negative): a query builder
2498
+ * handed a negative limit can interpret it as "no limit" — the exact fail-open
2499
+ * this function exists to prevent — so a misconfigured constant degrades to a
2500
+ * batch size of 1, never to an unbounded one.
2501
+ *
2502
+ * @param requested - Raw query value, e.g. `searchParams.get('limit')`.
2503
+ * @param defaultSize - Size to use when `requested` is absent or invalid.
2504
+ * @param max - Hard ceiling.
2505
+ * @returns A positive safe integer batch size.
2506
+ * @example
2507
+ * boundedBatchSize(null, 20, 100); // 20
2508
+ * boundedBatchSize('0', 20, 100); // 20
2509
+ * boundedBatchSize('1000', 20, 100); // 100
2510
+ */
2511
+ function boundedBatchSize(requested, defaultSize, max) {
2512
+ const ceiling = Number.isSafeInteger(max) && max >= 1 ? max : 1;
2513
+ const fallback = Number.isSafeInteger(defaultSize) && defaultSize >= 1 ? Math.min(defaultSize, ceiling) : 1;
2514
+ if (requested === null) return fallback;
2515
+ const parsed = Number(requested);
2516
+ if (!Number.isSafeInteger(parsed) || parsed < 1) return fallback;
2517
+ return Math.min(parsed, ceiling);
2518
+ }
2519
+ //#endregion
2520
+ //#region src/core/observability/email.ts
2521
+ /**
2522
+ * An email address, as a pattern source.
2523
+ *
2524
+ * Deliberately loose rather than RFC 5322: redaction must fail closed, so
2525
+ * anything shaped like `local@domain.tld` is treated as an address. The TLD is
2526
+ * required to be alphabetic so version-shaped strings (`jose@6.0.11`) and
2527
+ * `host:port` fragments are not mistaken for addresses.
2528
+ */
2529
+ const EMAIL_SOURCE = "[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}";
2530
+ /**
2531
+ * Matches an email address anywhere in a string.
2532
+ *
2533
+ * Non-global, so `.test()` is stateless and safe to share.
2534
+ *
2535
+ * @internal
2536
+ */
2537
+ const EMAIL_PATTERN = new RegExp(EMAIL_SOURCE, "i");
2538
+ /**
2539
+ * The global variant of {@link EMAIL_PATTERN}, for replacing every address
2540
+ * inside a string. Only ever used with `String.prototype.replace`, which
2541
+ * resets `lastIndex`, so the shared instance cannot carry state between calls.
2542
+ *
2543
+ * @internal
2544
+ */
2545
+ const EMAIL_PATTERN_GLOBAL = new RegExp(EMAIL_SOURCE, "gi");
2546
+ /**
2547
+ * The placeholder substituted for a redacted email address.
2548
+ *
2549
+ * @internal
2550
+ */
2551
+ const REDACTED_EMAIL = "[redacted-email]";
2552
+ //#endregion
2553
+ //#region src/core/observability/uuid.ts
2554
+ /** The one pattern source both exported regexes are built from. */
2555
+ const UUID_SOURCE = `(^|[^0-9a-f])[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(?![0-9a-f])`;
2556
+ /**
2557
+ * Matches a canonically-formatted UUID anywhere in a string.
2558
+ *
2559
+ * Non-global, so `.test()` is stateless and safe to share.
2560
+ *
2561
+ * @internal
2562
+ */
2563
+ const UUID_PATTERN = new RegExp(UUID_SOURCE, "i");
2564
+ /**
2565
+ * The global variant of {@link UUID_PATTERN}, for replacing every UUID inside a
2566
+ * string rather than testing for one.
2567
+ *
2568
+ * Group 1 is whatever preceded the UUID and must be re-emitted by the
2569
+ * replacement, hence {@link REDACTED_UUID_REPLACEMENT}. Only ever used with
2570
+ * `String.prototype.replace`, which resets `lastIndex`, so the shared instance
2571
+ * cannot carry state between calls.
2572
+ *
2573
+ * @internal
2574
+ */
2575
+ const UUID_PATTERN_GLOBAL = new RegExp(UUID_SOURCE, "gi");
2576
+ /**
2577
+ * The placeholder substituted for a redacted UUID.
2578
+ *
2579
+ * @internal
2580
+ */
2581
+ const REDACTED_UUID = "[redacted-uuid]";
2582
+ /**
2583
+ * The replacement string to pair with {@link UUID_PATTERN_GLOBAL}: re-emits the
2584
+ * captured preceding character, then the placeholder.
2585
+ *
2586
+ * @internal
2587
+ */
2588
+ const REDACTED_UUID_REPLACEMENT = `$1${REDACTED_UUID}`;
2589
+ //#endregion
2590
+ //#region src/core/observability/log-fields.ts
2591
+ /**
2592
+ * The generic Fanvue log keys every app shares.
2593
+ *
2594
+ * Anything app-specific (a wheel id, a bounty id, a claim status) is added per
2595
+ * app via `createSafeLogFields(['wheelId', ...])`.
2596
+ */
2597
+ const BASE_ALLOWED_LOG_KEYS = [
2598
+ "cause",
2599
+ "code",
2600
+ "creatorId",
2601
+ "errorName",
2602
+ "httpStatus",
2603
+ "reason",
2604
+ "retryCount",
2605
+ "status"
2606
+ ];
2607
+ /**
2608
+ * Builds an allowlisting log-field filter.
2609
+ *
2610
+ * The returned function is the only sanctioned way to get caller-supplied
2611
+ * fields into a log line. It fails closed in three independent ways:
2612
+ *
2613
+ * 1. A key that is not allowlisted is dropped entirely (so a new field cannot
2614
+ * reach the logs until someone adds it to the list on purpose).
2615
+ * 2. A value that is not `string | number | boolean | null` is dropped — an
2616
+ * `Error`, a response body or a token bag is never serialised, even under an
2617
+ * allowlisted key.
2618
+ * 3. An allowlisted string value containing a UUID becomes `'[redacted-uuid]'`
2619
+ * (platform UUIDs identify real people; any version matches, including v7 —
2620
+ * see `UUID_PATTERN`), and one containing an email address becomes
2621
+ * `'[redacted-email]'`. The whole value is replaced, not just the match,
2622
+ * because the surrounding text is usually the same identifier restated.
2623
+ *
2624
+ * `undefined` values are dropped rather than logged as `undefined`.
2625
+ *
2626
+ * @param extraAllowedKeys - App-specific keys to allow in addition to
2627
+ * {@link BASE_ALLOWED_LOG_KEYS}.
2628
+ * @returns A `safeLogFields(fields)` filter.
2629
+ * @example
2630
+ * const safeLogFields = createSafeLogFields(['wheelId']);
2631
+ * safeLogFields({ wheelId: 'wheel_1', retryCount: 2, accessToken: 'secret' });
2632
+ * // => { wheelId: 'wheel_1', retryCount: 2 }
2633
+ */
2634
+ function createSafeLogFields(extraAllowedKeys = []) {
2635
+ const allowed = new Set([...BASE_ALLOWED_LOG_KEYS, ...extraAllowedKeys]);
2636
+ return function safeLogFields(fields = {}) {
2637
+ const safe = {};
2638
+ for (const [key, value] of Object.entries(fields)) {
2639
+ if (!allowed.has(key) || value === void 0) continue;
2640
+ if (typeof value === "string") {
2641
+ if (UUID_PATTERN.test(value)) safe[key] = REDACTED_UUID;
2642
+ else if (EMAIL_PATTERN.test(value)) safe[key] = REDACTED_EMAIL;
2643
+ else safe[key] = value;
2644
+ continue;
2645
+ }
2646
+ if (value === null || typeof value === "number" || typeof value === "boolean") safe[key] = value;
2647
+ }
2648
+ return safe;
2649
+ };
2650
+ }
2651
+ /**
2652
+ * Reads a loggable name out of an unknown thrown value.
2653
+ *
2654
+ * Only the class name is returned — never `error.message`, which routinely
2655
+ * carries the very data the allowlist exists to keep out of the logs (upstream
2656
+ * response bodies, signed URLs, secrets embedded in connection strings).
2657
+ *
2658
+ * @param error - Any caught value.
2659
+ * @returns The `Error` subclass name, or the `typeof` of a non-Error throw.
2660
+ * @example
2661
+ * errorName(new TypeError('secret=abc')); // 'TypeError'
2662
+ * errorName('boom'); // 'string'
2663
+ */
2664
+ function errorName(error) {
2665
+ return error instanceof Error ? error.name : typeof error;
2666
+ }
2667
+ /**
2668
+ * Builds a structured event logger over an allowlist.
2669
+ *
2670
+ * The emitted shape is `prefix` followed by `{ event, ...safeFields }`, matching
2671
+ * the convention the apps already log in, so existing log-based dashboards keep
2672
+ * working after the lift.
2673
+ *
2674
+ * @param options - Prefix, allowlist and sink overrides. Every field is
2675
+ * optional; the defaults are `'[fanvue]'`, `createSafeLogFields()` and the
2676
+ * global `console`.
2677
+ * @returns A `logEvent(level, event, fields)` function.
2678
+ * @example
2679
+ * const logEvent = createLogEvent({ prefix: '[spinwheel]' });
2680
+ * logEvent('warn', 'exchange.retry', { httpStatus: 503, retryCount: 1 });
2681
+ * // [spinwheel] { event: 'exchange.retry', httpStatus: 503, retryCount: 1 }
2682
+ */
2683
+ function createLogEvent(options) {
2684
+ const prefix = options?.prefix ?? "[fanvue]";
2685
+ const safeLogFields = options?.safeLogFields ?? createSafeLogFields();
2686
+ const sink = options?.sink ?? console;
2687
+ return function logEvent(level, event, fields = {}) {
2688
+ const payload = {
2689
+ event,
2690
+ ...safeLogFields(fields)
2691
+ };
2692
+ if (level === "error") {
2693
+ sink.error(prefix, payload);
2694
+ return;
2695
+ }
2696
+ if (level === "warn") {
2697
+ sink.warn(prefix, payload);
2698
+ return;
2699
+ }
2700
+ sink.info(prefix, payload);
2701
+ };
2702
+ }
2703
+ /**
2704
+ * A ready-made {@link LogEvent} over the base allowlist, prefixed `[fanvue]`.
2705
+ *
2706
+ * Use {@link createLogEvent} instead when the app has its own prefix or extra
2707
+ * allowlisted keys.
2708
+ *
2709
+ * @example
2710
+ * logEvent('error', 'session.exchange_failed', { errorName: errorName(err) });
2711
+ */
2712
+ const logEvent = createLogEvent();
2713
+ //#endregion
2714
+ //#region src/core/observability/readiness.ts
2715
+ /**
2716
+ * The four OAuth credential variables an embedded Fanvue app needs before it
2717
+ * can complete a creator connection.
2718
+ */
2719
+ const FANVUE_OAUTH_VARS = [
2720
+ "FANVUE_APP_UUID",
2721
+ "FANVUE_CLIENT_ID",
2722
+ "FANVUE_CLIENT_SECRET",
2723
+ "FANVUE_OAUTH_REDIRECT_URI"
2724
+ ];
2725
+ /**
2726
+ * The app's own public base URL, checked for an https origin.
2727
+ *
2728
+ * `FANVUE_APP_BASE_URL` is the SDK-prefixed name; `APP_BASE_URL` is accepted
2729
+ * because the apps this was lifted from already set it.
2730
+ */
2731
+ const APP_BASE_URL_VARS = ["FANVUE_APP_BASE_URL", "APP_BASE_URL"];
2732
+ function firstSetValue(env, names) {
2733
+ for (const name of names) {
2734
+ const value = env[name];
2735
+ if (value !== void 0 && value.length > 0) return value;
2736
+ }
2737
+ return null;
2738
+ }
2739
+ function isHttpsUrl(value) {
2740
+ if (value === null) return false;
2741
+ try {
2742
+ return new URL(value).protocol === "https:";
2743
+ } catch {
2744
+ return false;
2745
+ }
2746
+ }
2747
+ /**
2748
+ * Reports whether the generic Fanvue configuration an embedded app depends on
2749
+ * is present, as a list of named checks suitable for a `/api/readyz` body.
2750
+ *
2751
+ * Pure by construction — the environment is a parameter, so a deployment's
2752
+ * readiness can be asserted in tests without mutating `process.env`. Two
2753
+ * checks:
2754
+ *
2755
+ * - `app_url` — the app's public base URL is set and https. Fanvue refuses to
2756
+ * embed a non-https origin, so an http (or unset) value means creators would
2757
+ * see an empty frame rather than an error.
2758
+ * - `fanvue_oauth` — all four OAuth credential variables are present. The
2759
+ * detail names the missing variables, never their values.
2760
+ *
2761
+ * Provider-specific checks are deliberately out of scope: webhook signature
2762
+ * readiness belongs to the webhooks module (which owns the contract), and
2763
+ * queue/cron credentials belong to whichever machine-auth strategy the app
2764
+ * plugs in.
2765
+ *
2766
+ * @param env - Environment to read. Defaults to `process.env`; on a runtime
2767
+ * with no `process` global (browser, some edge workers) it defaults to an
2768
+ * empty environment, so the probe reports not-ready instead of throwing a
2769
+ * `ReferenceError`.
2770
+ * @returns One {@link ReadinessCheck} per generic concern, in a stable order.
2771
+ * @example
2772
+ * const checks = configurationReadiness();
2773
+ * const ready = checks.every((check) => check.ready);
2774
+ * return Response.json({ ready, checks }, { status: ready ? 200 : 503 });
2775
+ */
2776
+ function configurationReadiness(env = typeof process === "undefined" ? {} : process.env) {
2777
+ const appUrlReady = isHttpsUrl(firstSetValue(env, APP_BASE_URL_VARS));
2778
+ const missingOauthVars = FANVUE_OAUTH_VARS.filter((name) => {
2779
+ const value = env[name];
2780
+ return value === void 0 || value.length === 0;
2781
+ });
2782
+ return [{
2783
+ name: "app_url",
2784
+ ready: appUrlReady,
2785
+ detail: appUrlReady ? null : `set ${APP_BASE_URL_VARS[0]} to an https:// origin`
2786
+ }, {
2787
+ name: "fanvue_oauth",
2788
+ ready: missingOauthVars.length === 0,
2789
+ detail: missingOauthVars.length === 0 ? null : `missing ${missingOauthVars.join(", ")}`
2790
+ }];
2791
+ }
2792
+ //#endregion
2793
+ //#region src/core/observability/sentry-scrubber.ts
2794
+ /**
2795
+ * Key-name fragments whose values are never allowed to reach an error tracker.
2796
+ *
2797
+ * Matched as a substring, case-insensitively, so `Authorization`,
2798
+ * `authorizationHeader`, `x-authorization`, `x-api-key` and `apiKey` all hit.
2799
+ * Substring matching over-redacts on purpose (`emailSent`, `iphoneModel`) —
2800
+ * losing a benign field fails closed, letting a credential or a fan's contact
2801
+ * detail through does not. Anything app-specific (`prizedetail`, `fanhmac`,
2802
+ * `seed`, …) is added per app via the `extraSensitiveKeyPattern` argument.
2803
+ */
2804
+ const BASE_SENSITIVE_KEY_PATTERN = /(authorization|cookie|token|secret|password|signedurl|api[-_]?key|credential|email|phone|ip[-_]?address)/i;
2805
+ /** The placeholder substituted for a value under a sensitive key. */
2806
+ const REDACTED = "[redacted]";
2807
+ /**
2808
+ * The placeholder substituted for an object already visited anywhere in this
2809
+ * event.
2810
+ *
2811
+ * The guard is per-event rather than per-branch, so a value referenced twice
2812
+ * shows as `[circular]` the second time even when there is no cycle. That is
2813
+ * deliberate: it bounds the work to one visit per node, which a branch-scoped
2814
+ * guard would not (a shallow object graph that shares children can be walked an
2815
+ * exponential number of times), and it matches how Sentry's own `normalize`
2816
+ * memoises. Same reasoning, same trade-off.
2817
+ */
2818
+ const CIRCULAR = "[circular]";
2819
+ /** The placeholder substituted for a property whose getter threw. */
2820
+ const UNREADABLE = "[unreadable]";
2821
+ /** The placeholder substituted for a node nested deeper than {@link MAX_DEPTH}. */
2822
+ const TOO_DEEP = "[max-depth]";
2823
+ /**
2824
+ * How far into an event the scrubber will walk.
2825
+ *
2826
+ * Recursion has to be bounded: an event can carry an arbitrarily nested object
2827
+ * (a linked list, a parsed AST, a deeply chained `cause`), and blowing the call
2828
+ * stack inside `beforeSend` turns a scrub into a thrown `RangeError` — the event
2829
+ * is lost and the throw surfaces wherever Sentry was invoked. Anything deeper is
2830
+ * replaced rather than traversed, which fails closed: an untraversed subtree
2831
+ * cannot leak. Generous next to Sentry's own default normalisation depth of 3.
2832
+ */
2833
+ const MAX_DEPTH = 32;
2834
+ /**
2835
+ * Merges the base sensitive-key pattern with an app-supplied one.
2836
+ *
2837
+ * The result is rebuilt from `source` with only the `i` flag so a caller
2838
+ * passing a `/g` pattern cannot make matching stateful (a shared `lastIndex`
2839
+ * would make redaction depend on call order — i.e. fail open intermittently).
2840
+ */
2841
+ function mergeSensitiveKeyPattern(extra) {
2842
+ if (extra === null) return BASE_SENSITIVE_KEY_PATTERN;
2843
+ return new RegExp(`(?:${BASE_SENSITIVE_KEY_PATTERN.source})|(?:${extra.source})`, "i");
2844
+ }
2845
+ /**
2846
+ * Removes the credential-bearing parts of anything that parses as a URL.
2847
+ *
2848
+ * The whole query string goes, not named parameters — signed media URLs carry
2849
+ * their credential in parameters whose names differ per provider, so an
2850
+ * allowlist there would fail open. The fragment and the userinfo go for the same
2851
+ * reason: an OAuth redirect puts `access_token` in the fragment, and a
2852
+ * connection string puts the password in `user:pass@`, so keying redaction on
2853
+ * the query alone would leak both.
2854
+ *
2855
+ * A string that parses as a URL but carries none of those parts is returned
2856
+ * unchanged, so an ordinary log line that happens to look like a non-special URI
2857
+ * (`'note: something happened'`) is not mangled.
2858
+ */
2859
+ function stripUrlCredentials(value) {
2860
+ let url;
2861
+ try {
2862
+ url = new URL(value);
2863
+ } catch {
2864
+ return value;
2865
+ }
2866
+ if (url.search === "" && url.hash === "" && url.username === "" && url.password === "") return value;
2867
+ url.search = "";
2868
+ url.hash = "";
2869
+ url.username = "";
2870
+ url.password = "";
2871
+ return `${url.toString()}?${REDACTED}`;
2872
+ }
2873
+ /**
2874
+ * Scrubs a string value: strips every UUID, then removes the credential-bearing
2875
+ * parts of anything that parses as a URL, then strips every email address.
2876
+ *
2877
+ * Emails go last so an address that survives URL stripping — a `mailto:` path,
2878
+ * a verification link's path segment — is still caught. A fan's email is PII in
2879
+ * its own right, and error messages routinely embed one (`user x@y.com not
2880
+ * found`).
2881
+ */
2882
+ function scrubString(value) {
2883
+ return stripUrlCredentials(value.replace(UUID_PATTERN_GLOBAL, REDACTED_UUID_REPLACEMENT)).replace(EMAIL_PATTERN_GLOBAL, REDACTED_EMAIL);
2884
+ }
2885
+ /**
2886
+ * Reads one property without letting a throwing getter abort the whole scrub.
2887
+ *
2888
+ * Sentry events routinely carry app objects, and a getter that throws (a lazy
2889
+ * field, a revoked proxy, a Vue/Mobx observable read outside its scope) would
2890
+ * otherwise propagate out of `beforeSend`.
2891
+ */
2892
+ function readProperty(source, key) {
2893
+ try {
2894
+ return Reflect.get(source, key);
2895
+ } catch {
2896
+ return UNREADABLE;
2897
+ }
2898
+ }
2899
+ function scrubValue(value, key, sensitiveKeyPattern, seen, depth) {
2900
+ if (sensitiveKeyPattern.test(key)) return REDACTED;
2901
+ if (typeof value === "string") return scrubString(value);
2902
+ if (value === null || typeof value !== "object") return value;
2903
+ if (seen.has(value)) return CIRCULAR;
2904
+ if (depth >= MAX_DEPTH) return TOO_DEEP;
2905
+ seen.add(value);
2906
+ if (Array.isArray(value)) {
2907
+ const items = [];
2908
+ for (let index = 0; index < value.length; index += 1) {
2909
+ const item = readProperty(value, index);
2910
+ items.push(scrubValue(item, key, sensitiveKeyPattern, seen, depth + 1));
2911
+ }
2912
+ return items;
2913
+ }
2914
+ const scrubbed = {};
2915
+ for (const childKey of Object.keys(value)) scrubbed[childKey] = scrubValue(readProperty(value, childKey), childKey, sensitiveKeyPattern, seen, depth + 1);
2916
+ return scrubbed;
2917
+ }
2918
+ /**
2919
+ * Builds a recursive `beforeSend` scrubber for Sentry (or any error tracker
2920
+ * with the same hook shape).
2921
+ *
2922
+ * Four redaction rules, applied to every node of the event:
2923
+ *
2924
+ * - a key matching {@link BASE_SENSITIVE_KEY_PATTERN} (or the app's extra
2925
+ * pattern) has its value replaced with `'[redacted]'`, whatever its type;
2926
+ * - every UUID inside a string becomes `'[redacted-uuid]'` — any version,
2927
+ * including v7;
2928
+ * - a string that parses as a URL keeps its origin and path but loses its query
2929
+ * string, fragment and userinfo;
2930
+ * - every email address inside a string becomes `'[redacted-email]'`.
2931
+ *
2932
+ * Bounded and total by construction, because a `beforeSend` that throws loses
2933
+ * the event and raises inside the caller: repeat visits yield `'[circular]'`
2934
+ * (`WeakSet`), nodes past a depth of 32 yield `'[max-depth]'`, and a property
2935
+ * whose getter throws yields `'[unreadable]'`. Each of those fails closed — an
2936
+ * untraversed value cannot leak.
2937
+ *
2938
+ * Deliberately dependency-free: the event is typed structurally, so the privacy
2939
+ * behaviour is unit-testable without a Sentry client and this package takes no
2940
+ * dependency on `@sentry/*`.
2941
+ *
2942
+ * @param extraSensitiveKeyPattern - Extra key fragments to redact, merged with
2943
+ * the base pattern. Its flags are ignored; matching is always
2944
+ * case-insensitive and stateless.
2945
+ * @returns A function suitable for `Sentry.init({ beforeSend })`.
2946
+ * @example
2947
+ * Sentry.init({
2948
+ * dsn,
2949
+ * enabled: Boolean(dsn),
2950
+ * beforeSend: createSentryScrubber(/(prizedetail|fanhmac|seed)/i),
2951
+ * });
2952
+ */
2953
+ function createSentryScrubber(extraSensitiveKeyPattern = null) {
2954
+ const sensitiveKeyPattern = mergeSensitiveKeyPattern(extraSensitiveKeyPattern);
2955
+ return function scrubSentryEvent(event) {
2956
+ return scrubValue(event, "", sensitiveKeyPattern, /* @__PURE__ */ new WeakSet(), 0);
2957
+ };
2958
+ }
2959
+ //#endregion
2960
+ export { FanvueStringListErrorBodySchema as $, OffsetPaginationSchema as A, UNPUBLISH_REQUEST_MESSAGE as B, FanvueUserSchema as C, DEFAULT_PAGE_SIZE as D, safeJsonParse as E, ExperienceMessageSchema as F, isPublishResultMessage as G, UnpublishRequestMessageSchema as H, PUBLISH_REQUEST_MESSAGE as I, FANVUE_APP_ERROR_CODES as J, isUnpublishResultMessage as K, PUBLISH_RESULT_MESSAGE as L, cursorPageSchema as M, offsetPageSchema as N, HybridPaginationSchema as O, EXPERIENCE_MESSAGE_TYPES as P, FanvueMessageErrorBodySchema as Q, PublishRequestMessageSchema as R, AuthorizeOnBehalfResponseSchema as S, TokenResponseSchema as T, UnpublishResultMessageSchema as U, UNPUBLISH_RESULT_MESSAGE as V, isFanvueOrigin as W, FanvueErrorFieldBodySchema as X, FanvueErrorBodySchema as Y, FanvueIssueListErrorBodySchema as Z, createSessionJwt as _, FANVUE_ACCESS_MODES as _t, createLogEvent as a, flagEnabled as at, exchangeCodeForToken as b, BEARER_PREFIX as bt, logEvent as c, resetFanvueEnvCache as ct, requireMachineAuth as d, DEFAULT_ISSUER_URL as dt, NON_SESSION_401_CODES as et, createFanvueClient as f, DEFAULT_PLATFORM_URL as ft, requestAuthorizationCodeOnBehalf as g, EXPERIENCE_ENTITLED_REASONS as gt, getThemeFromUrl as h, EXPERIENCE_DENIAL_REASONS as ht, BASE_ALLOWED_LOG_KEYS as i, fanvueEnv as it, clampPageSize as j, MAX_PAGE_SIZE as k, MINIMUM_BEARER_SECRET_LENGTH as l, API_VERSION as lt, getSessionTokenFromUrl as m, assertFanvueDomain as mt, createSentryScrubber as n, parseFanvueErrorBody as nt, createSafeLogFields as o, isFanvueConfigured as ot, exchangeSessionToken as p, DEFAULT_SCOPES as pt, AppErrorEnvelopeSchema as q, configurationReadiness as r, FanvueEnvSchema as rt, errorName as s, parseFanvueEnv as st, BASE_SENSITIVE_KEY_PATTERN as t, OAuthErrorBodySchema as tt, boundedBatchSize as u, DEFAULT_API_BASE_URL as ut, verifySessionJwt as v, FanvueAccessModeSchema as vt, SessionPayloadSchema as w, refreshAccessToken as x, HEADER_UPDATED_SESSION as xt, createAuthorizationUrl as y, accessModeFromDenialReason as yt, PublishResultMessageSchema as z };
2961
+
2962
+ //# sourceMappingURL=core-BhKiA55a.js.map