@fytkit/sdk 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@ All notable changes to `@fytkit/sdk` are documented in this file.
4
4
  Format: [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/).
5
5
  Versioning: [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 4.2.0
8
+
9
+ To be published by hand with sdk-publish.yml (the manifest records 4.2.0). Create the tag and GitHub release `@fytkit/sdk@4.2.0` when it is published.
10
+
11
+ ### Features
12
+
13
+ * `nutritionFollows`: the meal plan the trainee follows, one at a time. `list()` resolves to the active follow with its plan (`{ id, planId, status, startDate, startedAt, stoppedAt, plan: { id, title, description, accessLevel, canAccess, status, durationWeeks, headerImageUrl } }`), or to `[]`. `start({ planId, startDate? })` follows one of the studio's published plans from `startDate` ("YYYY-MM-DD", day 1 of week 1 in the trainee's calendar; their local today when omitted) and stops the trainee's current follow of another plan in the same write, naming it in `stoppedFollowId`; following the plan already followed changes nothing and returns that follow. `stop({ id })` stops a follow. A premium plan without `premium_nutrition` rejects as `nutrition.get` does (`payment_required` or `forbidden`); a draft, another studio's or an unknown plan, and another trainee's follow, reject with `not_found`; stopping a follow already stopped rejects with `conflict`. Writes are refused while a fytkit admin is signed in as the trainee. Additive ([#594](https://github.com/cbarlow1993/fytkit-web/issues/594))
14
+ * `auth.signInWithIdToken({ provider, idToken, nonce, name? })`: Sign in with Apple or Google in Fytkit Coach. Signs in, signs up, or joins this studio with the provider's ID token, and resolves like `signIn`: `{ kind: "signed_in", session, memberCreated }` (the session is cached and `signed_in` emitted; `memberCreated` is true when this call made the person a member of this studio, for onboarding) or `{ kind: "two_factor_required", challenge }`, completed with the existing `verify*` methods. `nonce` is the raw nonce the app generated (pass its SHA-256 hex to Apple, the raw nonce to Google); `name` is the name Apple gives the app on the first authorisation only. A credential call: a 401 never refreshes or signs out. Studio-owned (custom) apps are refused. New types `IdTokenProvider` and `IdTokenSignInResult` ([#544](https://github.com/cbarlow1993/fytkit-web/issues/544))
15
+ * `FytkitErrorCode` gains `social_sign_in_unavailable` and `social_email_has_password`, `social_email_has_other_provider`, `two_factor_join_unsupported` (on `forbidden`, 403) and `invalid_id_token`, `social_email_unverified` (on `server`, 400), the refusals of `signInWithIdToken`; `first_party_account` is sent by it too ([#544](https://github.com/cbarlow1993/fytkit-web/issues/544))
16
+ * `auth.closeAccount` accepts a fresh Apple or Google ID token instead of a password, so a member whose account has no password (it signs in with Apple or Google only) can close it in the app: `closeAccount({ confirmEmail, identityToken: { provider, idToken, nonce, authorizationCode? } })`. Run the provider's sign-in again with a new nonce; the token must be at most 10 minutes old and belong to the Apple or Google account the member signs in with. An account with a password still confirms with `closeAccount({ confirmEmail, password })`. The input is the new exported union `CloseAccountInput`, so sending both proofs, or neither, does not compile; the existing password call is unchanged. New type `IdTokenReauthentication`. On success the session is cleared as before ([#544](https://github.com/cbarlow1993/fytkit-web/issues/544))
17
+ * `auth.signInWithIdToken` takes an optional `authorizationCode` (Apple only; a validation error with Google): Apple's authorization code from the same sign-in, which the server exchanges for a refresh token so the app's Apple tokens can be revoked when the account is closed. `closeAccount`'s `identityToken.authorizationCode` does the same for a member whose code was never sent. A failed exchange never fails sign-in. Additive ([#544](https://github.com/cbarlow1993/fytkit-web/issues/544))
18
+ * `FytkitErrorCode` gains `password_not_set` (on `server`, 400: `closeAccount` was sent a password but the account has none; send `identityToken`), `password_required` (on `server`, 400: `closeAccount` was sent `identityToken` but the account has a password) and `reauth_identity_mismatch` (on `forbidden`, 403: the ID token is for another Apple or Google account than the member's). `invalid_id_token` and `social_sign_in_unavailable` can now come from `closeAccount` too. Every `closeAccount` refusal keeps the session ([#544](https://github.com/cbarlow1993/fytkit-web/issues/544))
19
+
20
+ ### Changed (server)
21
+
22
+ * `nutrition.list` and `recipes.list` now return premium plans and recipes the caller cannot open, with `canAccess: false`, where before they left them out. The gate is unchanged on `nutrition.get` and `recipes.get` (`payment_required`). This is a server change, so every SDK version sees it ([#597](https://github.com/cbarlow1993/fytkit-web/issues/597))
23
+
7
24
  ## 4.1.0
8
25
 
9
26
  To be published by hand with sdk-publish.yml (the manifest records 4.1.0). Create the tag and GitHub release `@fytkit/sdk@4.1.0` when it is published.
package/README.md CHANGED
@@ -293,6 +293,7 @@ await fytkit.preferences.updateNotifications({ streakAlerts: false }); // partia
293
293
 
294
294
  - `resolveJoinCode` (a studio's join code to its client id, name, logo and app theme, with no client) and `app.bootstrap` (the launch read, revalidated with its ETag), with `resolvePaymentMode` ("Subscribe" link-out or "What your plan includes", per storefront)
295
295
  - `auth.signUp` / `signIn` / `signOut` / `getSession` / `onAuthStateChange`
296
+ - `auth.signInWithIdToken` (Sign in with Apple or Google, Fytkit Coach only: pass the raw nonce; Apple gets its SHA-256 hex, Google the raw nonce)
296
297
  - `auth.requestPasswordReset` / `confirmPasswordReset` / `confirmEmail` / `resendVerification`
297
298
  - Two-factor: `auth.verifyTotp` / `verifyBackupCode` / `requestEmailOtp` / `verifyEmailOtp` (sign-in), `enableTotp` / `verifyTotpSetup` / `disableTwoFactor` / `regenerateBackupCodes` / `getSecurityStatus` (management)
298
299
  - Account security: `auth.changePassword` / `changeEmail` / `confirmEmailChange` / `listSessions` / `revokeSession` / `revokeOtherSessions` / `closeAccount`
package/dist/index.d.ts CHANGED
@@ -156,6 +156,22 @@ declare type ClientEntitlementCode = "premium_workouts" | "premium_programs" | "
156
156
 
157
157
  declare type ClientEntitlements = readonly ClientEntitlementCode[] | "all";
158
158
 
159
+ /**
160
+ * The input to `auth.closeAccount()`: the member's email as confirmation
161
+ * plus exactly one proof. `password` for an account with a password;
162
+ * `identityToken` for an account that signs in with Apple or Google only
163
+ * and has no password. Sending both, or neither, does not compile.
164
+ */
165
+ export declare type CloseAccountInput = {
166
+ confirmEmail: string;
167
+ password: string;
168
+ identityToken?: never;
169
+ } | {
170
+ confirmEmail: string;
171
+ identityToken: IdTokenReauthentication;
172
+ password?: never;
173
+ };
174
+
159
175
  /**
160
176
  * How long a staff member stays in {@link CoachTypingState}'s `names` after
161
177
  * the last sign that they are writing. The dashboard repeats the sign every
@@ -218,6 +234,23 @@ export declare interface FytkitClient {
218
234
  email: string;
219
235
  password: string;
220
236
  }) => Promise<SignInResult>;
237
+ /** Signs in, signs up or joins this studio with an Apple or Google ID
238
+ * token (Fytkit Coach only). `nonce` is the raw nonce the app generated:
239
+ * pass its SHA-256 hex to Apple, or the raw nonce to Google. `name` is the
240
+ * name Apple hands the app on the first authorisation (it is never in the
241
+ * token). `authorizationCode` is Apple's authorization code from the
242
+ * same sign-in (Apple only; a validation error with Google): the server
243
+ * exchanges it for a refresh token so it can revoke the app's Apple
244
+ * tokens when the account is closed. On `signed_in` the session is cached
245
+ * (emits signed_in); on `two_factor_required` continue with the `verify*`
246
+ * methods. Refusals carry a `code` (see `FytkitErrorCode`). */
247
+ signInWithIdToken: (input: {
248
+ provider: IdTokenProvider;
249
+ idToken: string;
250
+ nonce: string;
251
+ name?: string;
252
+ authorizationCode?: string;
253
+ }) => Promise<IdTokenSignInResult>;
221
254
  signOut: () => Promise<void>;
222
255
  /** Synchronously read the cached session, if any. */
223
256
  getSession: () => Session | null;
@@ -312,14 +345,18 @@ export declare interface FytkitClient {
312
345
  * untouched; `changePassword` with `revokeOtherSessions: true` ends those. */
313
346
  revokeOtherSessions: () => Promise<void>;
314
347
  /** Permanently closes the member's account (cancels their subscriptions,
315
- * anonymises their data, deletes the login) after re-verifying the email
316
- * and password. On success the SDK clears its session and emits
317
- * `signed_out`. A wrong password rejects with `{ kind: "server", status:
318
- * 400, code: "invalid_password" }` and leaves the session signed in. */
319
- closeAccount: (input: {
320
- confirmEmail: string;
321
- password: string;
322
- }) => Promise<void>;
348
+ * anonymises their data, deletes the login, revokes the app's Sign in
349
+ * with Apple tokens) after re-verifying the email and one proof: the
350
+ * `password`, or, for an account without a password, a fresh Apple or
351
+ * Google `identityToken` (see `CloseAccountInput`). On success the SDK
352
+ * clears its session and emits `signed_out`. Every refusal leaves the
353
+ * session signed in: a wrong password rejects with `{ kind: "server",
354
+ * status: 400, code: "invalid_password" }`; the wrong proof for the
355
+ * account with `password_not_set` or `password_required` (`server`, 400);
356
+ * an ID token for another Apple or Google account with
357
+ * `reauth_identity_mismatch` (`forbidden`, 403); an unverifiable, reused
358
+ * or stale ID token with `invalid_id_token` (`server`, 400). */
359
+ closeAccount: (input: CloseAccountInput) => Promise<void>;
323
360
  };
324
361
  readonly workouts: PublicApi["workout"];
325
362
  readonly exercises: PublicApi["exercise"];
@@ -328,6 +365,15 @@ export declare interface FytkitClient {
328
365
  readonly equipment: PublicApi["equipment"];
329
366
  readonly recipes: PublicApi["recipe"];
330
367
  readonly nutrition: PublicApi["nutrition"];
368
+ /**
369
+ * The meal plan the trainee follows, one at a time: `list` (the active
370
+ * follow with its plan, or nothing), `start({ planId, startDate? })`, which
371
+ * stops the trainee's current follow of another plan, and `stop({ id })`.
372
+ * `startDate` is day 1 of week 1 in the trainee's own calendar, for
373
+ * "week 2 of 4, day 3". Writes are refused while a fytkit admin is signed
374
+ * in as the trainee.
375
+ */
376
+ readonly nutritionFollows: PublicApi["nutritionFollow"];
331
377
  readonly articles: PublicApi["article"];
332
378
  readonly enrollments: PublicApi["enrollment"];
333
379
  readonly favourites: PublicApi["favourite"];
@@ -469,13 +515,53 @@ export declare type FytkitError = {
469
515
  * - `"first_party_account"` (`forbidden`, 403) — the account also runs or
470
516
  * works at a trainer on Fytkit, so an app may not sign it in, sign it up,
471
517
  * reset its password or handle its two-factor steps (`signIn`, `signUp`,
472
- * the `verify*` and TOTP methods), and an existing session can't change
518
+ * `signInWithIdToken`, the `verify*` and TOTP methods), and an existing session can't change
473
519
  * its password or email or close it (`changePassword`, `changeEmail`,
474
520
  * `closeAccount`). The credentials were right; tell the
475
521
  * person to sign in with Fytkit itself. `requestPasswordReset` never sends
476
- * this: it answers `ok` and sends no email.
522
+ * this: it answers `ok` and sends no email. `signInWithIdToken` also sends
523
+ * it for a Fytkit platform administrator.
524
+ *
525
+ * `closeAccount` re-authentication refusals (see `CloseAccountInput`):
526
+ *
527
+ * - `"password_not_set"` (`server`, 400) — `password` was sent, but the
528
+ * account has no password (it signs in with Apple or Google). Re-run the
529
+ * provider's sign-in and send `identityToken` instead.
530
+ * - `"password_required"` (`server`, 400) — `identityToken` was sent, but the
531
+ * account has a password. Ask for the password; an account with a password
532
+ * always confirms with it.
533
+ * - `"reauth_identity_mismatch"` (`forbidden`, 403) — the ID token is valid
534
+ * but is not for the Apple or Google account the member signs in with.
535
+ * Ask them to use that account. The session is untouched.
536
+ *
537
+ * `closeAccount` with `identityToken` can also send `invalid_id_token` (the
538
+ * token could not be verified, was already used, or is older than 10
539
+ * minutes) and `social_sign_in_unavailable`, with the meanings below.
540
+ *
541
+ * `signInWithIdToken` refusals (the ID token is checked first, except for
542
+ * `social_sign_in_unavailable`); the first two are also `closeAccount`
543
+ * refusals:
544
+ *
545
+ * - `"social_sign_in_unavailable"` (`forbidden`, 403) — this app can't use
546
+ * Sign in with Apple or Google: the provider is not configured for it, or
547
+ * the app is a studio's own (custom) app. Hide the button.
548
+ * - `"invalid_id_token"` (`server`, 400) — the ID token could not be
549
+ * verified (expired, wrong audience, nonce mismatch, already used, or no
550
+ * email on a first sign-in). Start the provider's sign-in again with a new
551
+ * nonce.
552
+ * - `"social_email_unverified"` (`server`, 400) — the provider has not
553
+ * verified the account's email address.
554
+ * - `"social_email_has_password"` (`forbidden`, 403) — the email already
555
+ * belongs to an account with a password. Ask the person to sign in with
556
+ * their password; accounts are never linked implicitly.
557
+ * - `"social_email_has_other_provider"` (`forbidden`, 403) — the email
558
+ * already belongs to an account that signs in with the other provider.
559
+ * Ask the person to use that one.
560
+ * - `"two_factor_join_unsupported"` (`forbidden`, 403) — the account uses
561
+ * two-step verification and is not yet a member of this studio; it can't
562
+ * join a new studio from the app.
477
563
  */
478
- export declare type FytkitErrorCode = "reauth_required" | "invalid_password" | "invalid_code" | "first_party_account" | (string & {});
564
+ export declare type FytkitErrorCode = "reauth_required" | "invalid_password" | "invalid_code" | "first_party_account" | "social_sign_in_unavailable" | "invalid_id_token" | "social_email_unverified" | "social_email_has_password" | "social_email_has_other_provider" | "two_factor_join_unsupported" | "password_not_set" | "password_required" | "reauth_identity_mismatch" | (string & {});
479
565
 
480
566
  /**
481
567
  * The server's structured refusal on a `FytkitError`: why it said no and
@@ -692,6 +778,43 @@ export declare interface FytkitRefusalRetryAt {
692
778
  readonly retryAfterMs?: number;
693
779
  }
694
780
 
781
+ /** The identity providers whose ID tokens `auth.signInWithIdToken` accepts. */
782
+ export declare type IdTokenProvider = "apple" | "google";
783
+
784
+ /**
785
+ * A fresh Apple or Google ID token that proves the signed-in member is
786
+ * present, for `auth.closeAccount` on an account without a password. Run
787
+ * the provider's sign-in again with a new nonce, as for `signInWithIdToken`:
788
+ * `nonce` is the raw nonce the app generated (pass its SHA-256 hex to Apple,
789
+ * the raw nonce to Google). The token must be at most 10 minutes old and
790
+ * belong to the Apple or Google account the member signs in with.
791
+ * `authorizationCode` is Apple's authorization code from that same sign-in
792
+ * (Apple only); send it so the server can revoke the app's Apple tokens.
793
+ */
794
+ export declare interface IdTokenReauthentication {
795
+ provider: IdTokenProvider;
796
+ idToken: string;
797
+ nonce: string;
798
+ authorizationCode?: string;
799
+ }
800
+
801
+ /**
802
+ * The discriminated return value from `auth.signInWithIdToken()`. As
803
+ * `SignInResult`, plus `memberCreated` on `"signed_in"`: true when this call
804
+ * made the person a member of this studio (a new account, or an existing
805
+ * account joining this studio for the first time), so the app can show its
806
+ * onboarding. `"two_factor_required"` continues with the same `verify*`
807
+ * methods as `signIn`'s challenge.
808
+ */
809
+ export declare type IdTokenSignInResult = {
810
+ readonly kind: "signed_in";
811
+ readonly session: Session;
812
+ readonly memberCreated: boolean;
813
+ } | {
814
+ readonly kind: "two_factor_required";
815
+ readonly challenge: TwoFactorChallenge;
816
+ };
817
+
695
818
  /** A coach a join code's studio names publicly. */
696
819
  export declare interface JoinCodeCoach {
697
820
  /** The coach's public name. */
@@ -935,6 +1058,46 @@ declare const publicRouter: {
935
1058
  }>>;
936
1059
  }, $strip>], "kind">;
937
1060
  }, $strip>, Record<never, never>, Record<never, never>>;
1061
+ signInWithIdToken: DecoratedProcedure<MergedInitialContext<DeveloperAppRequestContext & Record<never, never>, DeveloperAppRequestContext & Record<never, never>, DeveloperAppRequestContext>, MergedCurrentContext<DeveloperAppRequestContext, {
1062
+ trainerId: string;
1063
+ appId: ActiveDeveloperAppId;
1064
+ clientId: string | undefined;
1065
+ hasActiveSubscription: boolean;
1066
+ clientEntitlements: ClientEntitlements;
1067
+ authMode: "dev-app";
1068
+ developerApp: CallingDeveloperApp;
1069
+ impersonatedBy: string | null;
1070
+ }>, ZodObject< {
1071
+ provider: ZodEnum< {
1072
+ apple: "apple";
1073
+ google: "google";
1074
+ }>;
1075
+ idToken: ZodString;
1076
+ nonce: ZodString;
1077
+ name: ZodOptional<ZodString>;
1078
+ authorizationCode: ZodOptional<ZodString>;
1079
+ }, $strip>, ZodObject< {
1080
+ data: ZodDiscriminatedUnion<[ZodObject< {
1081
+ kind: ZodLiteral<"signed_in">;
1082
+ user: ZodObject< {
1083
+ id: ZodString;
1084
+ email: ZodString;
1085
+ }, $strip>;
1086
+ session: ZodObject< {
1087
+ accessToken: ZodString;
1088
+ expiresAt: ZodString;
1089
+ }, $strip>;
1090
+ memberCreated: ZodBoolean;
1091
+ }, $strip>, ZodObject< {
1092
+ kind: ZodLiteral<"two_factor_required">;
1093
+ twoFactorToken: ZodString;
1094
+ availableMethods: ZodArray<ZodEnum< {
1095
+ totp: "totp";
1096
+ backup_code: "backup_code";
1097
+ email_otp: "email_otp";
1098
+ }>>;
1099
+ }, $strip>], "kind">;
1100
+ }, $strip>, Record<never, never>, Record<never, never>>;
938
1101
  signOut: DecoratedProcedure<MergedInitialContext<DeveloperAppRequestContext & Record<never, never>, DeveloperAppRequestContext & Record<never, never>, DeveloperAppRequestContext>, MergedCurrentContext<DeveloperAppRequestContext, {
939
1102
  trainerId: string;
940
1103
  appId: ActiveDeveloperAppId;
@@ -1275,7 +1438,16 @@ declare const publicRouter: {
1275
1438
  impersonatedBy: string | null;
1276
1439
  }>, ZodObject< {
1277
1440
  confirmEmail: ZodString;
1278
- password: ZodString;
1441
+ password: ZodOptional<ZodString>;
1442
+ identityToken: ZodOptional<ZodObject< {
1443
+ provider: ZodEnum< {
1444
+ apple: "apple";
1445
+ google: "google";
1446
+ }>;
1447
+ idToken: ZodString;
1448
+ nonce: ZodString;
1449
+ authorizationCode: ZodOptional<ZodString>;
1450
+ }, $strip>>;
1279
1451
  }, $strip>, ZodObject< {
1280
1452
  data: ZodObject< {
1281
1453
  success: ZodLiteral<true>;
@@ -1726,6 +1898,63 @@ declare const publicRouter: {
1726
1898
  }, $strip>;
1727
1899
  }, $strip>, Record<never, never>, Record<never, never>>;
1728
1900
  };
1901
+ nutritionFollow: {
1902
+ list: DecoratedProcedure<MergedInitialContext<DeveloperAppRequestContext & Record<never, never>, DeveloperAppRequestContext & Record<never, never>, DeveloperAppRequestContext>, MergedCurrentContext<DeveloperAppRequestContext, ApiKeyContext>, Schema<unknown, unknown>, ZodObject< {
1903
+ data: ZodArray<ZodObject< {
1904
+ id: ZodString;
1905
+ planId: ZodString;
1906
+ status: ZodEnum< {
1907
+ active: "active";
1908
+ stopped: "stopped";
1909
+ }>;
1910
+ startDate: ZodISODate;
1911
+ startedAt: ZodString;
1912
+ stoppedAt: ZodNullable<ZodString>;
1913
+ plan: ZodObject< {
1914
+ id: ZodString;
1915
+ title: ZodString;
1916
+ description: ZodString;
1917
+ accessLevel: ZodEnum< {
1918
+ free: "free";
1919
+ premium: "premium";
1920
+ }>;
1921
+ canAccess: ZodBoolean;
1922
+ status: ZodEnum< {
1923
+ draft: "draft";
1924
+ published: "published";
1925
+ archived: "archived";
1926
+ }>;
1927
+ durationWeeks: ZodNumber;
1928
+ headerImageUrl: ZodNullable<ZodString>;
1929
+ }, $strip>;
1930
+ }, $strip>>;
1931
+ }, $strip>, Record<never, never>, Record<never, never>>;
1932
+ start: DecoratedProcedure<MergedInitialContext<DeveloperAppRequestContext & Record<never, never>, DeveloperAppRequestContext & Record<never, never>, DeveloperAppRequestContext>, MergedCurrentContext<DeveloperAppRequestContext, ApiKeyContext>, ZodObject< {
1933
+ planId: ZodString;
1934
+ startDate: ZodOptional<ZodISODate>;
1935
+ }, $strip>, ZodObject< {
1936
+ data: ZodObject< {
1937
+ id: ZodString;
1938
+ planId: ZodString;
1939
+ status: ZodEnum< {
1940
+ active: "active";
1941
+ stopped: "stopped";
1942
+ }>;
1943
+ startDate: ZodISODate;
1944
+ startedAt: ZodString;
1945
+ stoppedAt: ZodNullable<ZodString>;
1946
+ stoppedFollowId: ZodNullable<ZodString>;
1947
+ }, $strip>;
1948
+ }, $strip>, Record<never, never>, Record<never, never>>;
1949
+ stop: DecoratedProcedure<MergedInitialContext<DeveloperAppRequestContext & Record<never, never>, DeveloperAppRequestContext & Record<never, never>, DeveloperAppRequestContext>, MergedCurrentContext<DeveloperAppRequestContext, ApiKeyContext>, ZodObject< {
1950
+ id: ZodString;
1951
+ }, $strip>, ZodObject< {
1952
+ data: ZodObject< {
1953
+ id: ZodString;
1954
+ status: ZodLiteral<"stopped">;
1955
+ }, $strip>;
1956
+ }, $strip>, Record<never, never>, Record<never, never>>;
1957
+ };
1729
1958
  nutritionLog: {
1730
1959
  getDay: DecoratedProcedure<MergedInitialContext<DeveloperAppRequestContext & Record<never, never>, DeveloperAppRequestContext & Record<never, never>, DeveloperAppRequestContext>, MergedCurrentContext<DeveloperAppRequestContext, ApiKeyContext>, ZodObject< {
1731
1960
  date: ZodString;
@@ -5479,12 +5708,12 @@ declare const publicRouter: {
5479
5708
  name: ZodString;
5480
5709
  description: ZodString;
5481
5710
  icon: ZodNullable<ZodEnum< {
5711
+ Apple: "Apple";
5482
5712
  Footprints: "Footprints";
5483
5713
  Moon: "Moon";
5484
5714
  GlassWater: "GlassWater";
5485
5715
  Beef: "Beef";
5486
5716
  StretchHorizontal: "StretchHorizontal";
5487
- Apple: "Apple";
5488
5717
  Brain: "Brain";
5489
5718
  Sun: "Sun";
5490
5719
  }>>;
@@ -5636,12 +5865,12 @@ declare const publicRouter: {
5636
5865
  name: ZodString;
5637
5866
  description: ZodString;
5638
5867
  icon: ZodNullable<ZodEnum< {
5868
+ Apple: "Apple";
5639
5869
  Footprints: "Footprints";
5640
5870
  Moon: "Moon";
5641
5871
  GlassWater: "GlassWater";
5642
5872
  Beef: "Beef";
5643
5873
  StretchHorizontal: "StretchHorizontal";
5644
- Apple: "Apple";
5645
5874
  Brain: "Brain";
5646
5875
  Sun: "Sun";
5647
5876
  }>>;
package/dist/index.js CHANGED
@@ -238,6 +238,34 @@ function createAuthNamespace(opts) {
238
238
  const _exhaustive = result.data;
239
239
  throw _exhaustive;
240
240
  };
241
+ const signInWithIdToken = async (input) => {
242
+ const result = await call(
243
+ () => opts.transport.client.auth.signInWithIdToken({
244
+ provider: input.provider,
245
+ idToken: input.idToken,
246
+ nonce: input.nonce,
247
+ name: input.name,
248
+ authorizationCode: input.authorizationCode
249
+ })
250
+ );
251
+ if (result.data.kind === "two_factor_required") {
252
+ const token = result.data.twoFactorToken;
253
+ return {
254
+ kind: "two_factor_required",
255
+ challenge: { token, availableMethods: result.data.availableMethods }
256
+ };
257
+ }
258
+ if (result.data.kind === "signed_in") {
259
+ const { user, session, memberCreated } = result.data;
260
+ const cached2 = applySession(
261
+ { accessToken: session.accessToken, expiresAt: session.expiresAt, user },
262
+ "signed_in"
263
+ );
264
+ return { kind: "signed_in", session: cached2, memberCreated };
265
+ }
266
+ const _exhaustive = result.data;
267
+ throw _exhaustive;
268
+ };
241
269
  const signOut = async () => {
242
270
  const current = cached;
243
271
  if (!current) return;
@@ -350,12 +378,16 @@ function createAuthNamespace(opts) {
350
378
  await call(() => opts.transport.client.auth.revokeOtherSessions());
351
379
  };
352
380
  const closeAccount = async (input) => {
353
- await call(
354
- () => opts.transport.client.auth.closeAccount({
355
- confirmEmail: input.confirmEmail,
356
- password: input.password
357
- })
358
- );
381
+ const body = input.identityToken !== void 0 ? {
382
+ confirmEmail: input.confirmEmail,
383
+ identityToken: {
384
+ provider: input.identityToken.provider,
385
+ idToken: input.identityToken.idToken,
386
+ nonce: input.identityToken.nonce,
387
+ authorizationCode: input.identityToken.authorizationCode
388
+ }
389
+ } : { confirmEmail: input.confirmEmail, password: input.password };
390
+ await call(() => opts.transport.client.auth.closeAccount(body));
359
391
  clearSession();
360
392
  };
361
393
  const _refresh = async () => {
@@ -389,6 +421,7 @@ function createAuthNamespace(opts) {
389
421
  return {
390
422
  signUp,
391
423
  signIn,
424
+ signInWithIdToken,
392
425
  signOut,
393
426
  getSession,
394
427
  onAuthStateChange,
@@ -452,6 +485,7 @@ function createDataNamespaces(transport) {
452
485
  equipment: wrapSlice(c.equipment),
453
486
  recipes: wrapSlice(c.recipe),
454
487
  nutrition: wrapSlice(c.nutrition),
488
+ nutritionFollows: wrapSlice(c.nutritionFollow),
455
489
  articles: wrapSlice(c.article),
456
490
  enrollments: wrapSlice(c.enrollment),
457
491
  favourites: wrapSlice(c.favourite),
@@ -614,6 +648,7 @@ function createTransport(options) {
614
648
  var REFRESH_HEADER = "X-Fytkit-Refresh";
615
649
  var CREDENTIAL_PROCEDURES = [
616
650
  "auth/signIn",
651
+ "auth/signInWithIdToken",
617
652
  "auth/signUp",
618
653
  "auth/requestPasswordReset",
619
654
  "auth/confirmPasswordReset",
@@ -1088,6 +1123,7 @@ function createFytkitClient(options) {
1088
1123
  auth: {
1089
1124
  signUp: authInstance.signUp,
1090
1125
  signIn: authInstance.signIn,
1126
+ signInWithIdToken: authInstance.signInWithIdToken,
1091
1127
  signOut: authInstance.signOut,
1092
1128
  getSession: authInstance.getSession,
1093
1129
  onAuthStateChange: authInstance.onAuthStateChange,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/is-record.ts","../src/refusal.ts","../src/error.ts","../src/app.ts","../src/events.ts","../src/auth.ts","../src/base-url.ts","../src/data.ts","../src/storage.ts","../src/retry-after.ts","../src/transport.ts","../src/join.ts","../src/payment-policy.ts","../src/realtime.ts","../src/index.ts"],"names":["cached"],"mappings":";;;;;;AAIO,SAAS,SAAS,KAAA,EAA4D;AACnF,EAAA,OAAO,OAAO,UAAU,QAAA,IAAY,KAAA,KAAU,QAAQ,CAAC,KAAA,CAAM,QAAQ,KAAK,CAAA;AAC5E;;;ACiOO,SAAS,WAAA,CACd,OACA,MAAA,EACqC;AACrC,EAAA,IAAI,CAAC,SAAS,KAAK,CAAA,IAAK,OAAO,KAAA,CAAM,IAAA,KAAS,UAAU,OAAO,IAAA;AAC/D,EAAA,MAAM,UAAU,KAAA,CAAM,OAAA;AACtB,EAAA,IAAI,CAAC,QAAA,CAAS,OAAO,CAAA,IAAK,OAAA,CAAQ,MAAA,KAAW,MAAA,IAAU,CAAC,QAAA,CAAS,OAAA,CAAQ,IAAI,CAAA,EAAG,OAAO,IAAA;AAEvF,EAAA,OAAO,OAAA,CAAQ,IAAA;AACjB;AASA,IAAM,gBAAA,GAAmB,aAAA;AAOlB,SAAS,YAAY,IAAA,EAAqD;AAC/E,EAAA,IAAI,CAAC,QAAA,CAAS,IAAI,CAAA,SAAU,EAAC;AAC7B,EAAA,MAAM,EAAE,MAAA,EAAQ,GAAG,IAAA,EAAK,GAAI,IAAA;AAC5B,EAAA,IAAI,OAAO,MAAA,KAAW,QAAA,IAAY,MAAA,CAAO,MAAA,GAAS,CAAA,EAAG,OAAO,EAAE,OAAA,EAAS,EAAE,MAAA,EAAQ,IAAA,EAAM,MAAK,EAAE;AAC9F,EAAA,IAAI,KAAK,SAAA,KAAc,gBAAA;AACrB,IAAA,OAAO,EAAE,SAAS,EAAE,MAAA,EAAQ,kBAAkB,IAAA,EAAM,IAAG,EAAE;AAC3D,EAAA,OAAO,EAAC;AACV;;;AC5PA,SAAS,WAAW,IAAA,EAA0C;AAC5D,EAAA,IAAI,CAAC,IAAA,IAAQ,OAAO,IAAA,KAAS,UAAU,OAAO,IAAA;AAC9C,EAAA,MAAM,SAAU,IAAA,CAAyB,MAAA;AACzC,EAAA,OAAO,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,GAAI,MAAA,GAAS,IAAA;AAC1C;AAEA,SAAS,eAAe,IAAA,EAAuB;AAC7C,EAAA,IAAI,CAAC,IAAA,IAAQ,OAAO,IAAA,KAAS,UAAU,OAAO,CAAA;AAC9C,EAAA,MAAM,MAAO,IAAA,CAA6B,YAAA;AAC1C,EAAA,OAAO,OAAO,GAAA,KAAQ,QAAA,IAAY,GAAA,IAAO,IAAI,GAAA,GAAM,CAAA;AACrD;AASA,SAAS,eAAA,CACP,MACA,GAAA,EACuB;AACvB,EAAA,IAAI,CAAC,IAAA,IAAQ,OAAO,IAAA,KAAS,QAAA,SAAiB,EAAC;AAC/C,EAAA,MAAM,GAAA,GAAO,KAAqC,GAAG,CAAA;AACrD,EAAA,OAAO,OAAO,GAAA,KAAQ,QAAA,IAAY,GAAA,CAAI,MAAA,GAAS,CAAA,GAAK,EAAE,CAAC,GAAG,GAAG,GAAA,EAAI,GAA8B,EAAC;AAClG;AAWO,SAAS,eAAe,GAAA,EAA2B;AAExD,EAAA,IAAI,GAAA,YAAe,SAAA,EAAW,OAAO,EAAE,MAAM,SAAA,EAAU;AAEvD,EAAA,IAAI,eAAe,SAAA,EAAW;AAC5B,IAAA,MAAM,OAAO,GAAA,CAAI,IAAA;AACjB,IAAA,MAAM,SAAS,GAAA,CAAI,MAAA;AACnB,IAAA,MAAM,OAAgB,GAAA,CAAI,IAAA;AAC1B,IAAA,MAAM,UAAU,GAAA,CAAI,OAAA;AAEpB,IAAA,QAAQ,IAAA;AAAM,MACZ,KAAK,cAAA;AAKH,QAAA,OAAO,EAAE,IAAA,EAAM,eAAA,EAAiB,OAAA,EAAQ;AAAA,MAC1C,KAAK,WAAA;AACH,QAAA,OAAO;AAAA,UACL,IAAA,EAAM,WAAA;AAAA,UACN,QAAQ,OAAA,IAAW,WAAA;AAAA,UACnB,GAAG,eAAA,CAAgB,IAAA,EAAM,MAAM,CAAA;AAAA,UAC/B,GAAG,YAAY,IAAI;AAAA,SACrB;AAAA,MACF,KAAK,kBAAA;AACH,QAAA,OAAO,EAAE,IAAA,EAAM,kBAAA,EAAoB,OAAA,EAAS,WAAW,kBAAA,EAAmB;AAAA,MAC5E,KAAK,UAAA;AACH,QAAA,OAAO;AAAA,UACL,IAAA,EAAM,UAAA;AAAA,UACN,SAAS,OAAA,IAAW,UAAA;AAAA,UACpB,GAAG,eAAA,CAAgB,IAAA,EAAM,cAAc,CAAA;AAAA,UACvC,GAAG,YAAY,IAAI;AAAA,SACrB;AAAA,MACF,KAAK,WAAA;AACH,QAAA,OAAO,EAAE,IAAA,EAAM,WAAA,EAAa,GAAG,WAAA,CAAY,IAAI,CAAA,EAAE;AAAA,MACnD,KAAK,mBAAA;AACH,QAAA,OAAO,EAAE,IAAA,EAAM,cAAA,EAAgB,UAAA,EAAY,cAAA,CAAe,IAAI,CAAA,EAAG,GAAG,WAAA,CAAY,IAAI,CAAA,EAAE;AAAA,MACxF,KAAK,aAAA;AAAA,MACL,KAAK,uBAAA,EAAyB;AAC5B,QAAA,MAAM,MAAA,GAAS,WAAW,IAAI,CAAA;AAC9B,QAAA,IAAI,MAAA,SAAe,EAAE,IAAA,EAAM,cAAc,MAAA,EAAQ,GAAG,WAAA,CAAY,IAAI,CAAA,EAAE;AAItE,QAAA,OAAO;AAAA,UACL,IAAA,EAAM,QAAA;AAAA,UACN,MAAA;AAAA,UACA,OAAA;AAAA,UACA,GAAG,eAAA,CAAgB,IAAA,EAAM,MAAM,CAAA;AAAA,UAC/B,GAAG,YAAY,IAAI;AAAA,SACrB;AAAA,MACF;AAAA,MACA;AACE,QAAA,OAAO;AAAA,UACL,IAAA,EAAM,QAAA;AAAA,UACN,MAAA;AAAA,UACA,OAAA;AAAA,UACA,GAAG,eAAA,CAAgB,IAAA,EAAM,MAAM,CAAA;AAAA,UAC/B,GAAG,YAAY,IAAI;AAAA,SACrB;AAAA;AACJ,EACF;AAEA,EAAA,OAAO,EAAE,IAAA,EAAM,QAAA,EAAU,MAAA,EAAQ,CAAA,EAAE;AACrC;;;ACvCA,IAAM,uBAAA,GAAuC,EAAE,IAAA,EAAM,QAAA,EAAU,QAAQ,GAAA,EAAI;AAEpE,SAAS,mBAAmB,SAAA,EAAoC;AACrE,EAAA,MAAM,SAAA,GAAuC,OAAO,OAAA,GAAU,EAAC,KAAM;AACnE,IAAA,MAAM,IAAA,GAAO,QAAQ,IAAA,IAAQ,IAAA;AAE7B,IAAA,IAAI,MAAA;AACJ,IAAA,IAAI;AACF,MAAA,MAAA,GAAS,MAAM,SAAA,CAAU,MAAA,CAAO,GAAA,CAAI,UAAU,KAAA,CAAA,EAAW;AAAA,QACvD,SAAS,IAAA,KAAS,IAAA,GAAO,EAAC,GAAI,EAAE,aAAa,IAAA;AAAK,OACnD,CAAA;AAAA,IACH,SAAS,KAAA,EAAO;AACd,MAAA,MAAM,eAAe,KAAK,CAAA;AAAA,IAC5B;AACA,IAAA,IAAI,MAAA,KAAW,MAAA,IAAa,MAAA,KAAW,IAAA,EAAM;AAC3C,MAAA,IAAI,IAAA,KAAS,MAAM,MAAM,uBAAA;AACzB,MAAA,OAAO,EAAE,MAAA,EAAQ,WAAA,EAAa,IAAA,EAAK;AAAA,IACrC;AACA,IAAA,OAAO,EAAE,QAAQ,IAAA,EAAM,IAAA,EAAM,OAAO,IAAA,EAAM,IAAA,EAAM,MAAA,CAAO,IAAA,CAAK,IAAA,EAAK;AAAA,EACnE,CAAA;AACA,EAAA,OAAO,EAAE,SAAA,EAAU;AACrB;;;ACzFO,SAAS,cAAA,GAAiC;AAC/C,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAwB;AAC9C,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,CAAC,QAAA,KAAa;AAChB,MAAA,SAAA,CAAU,IAAI,QAAQ,CAAA;AACtB,MAAA,OAAO,MAAM;AACX,QAAA,SAAA,CAAU,OAAO,QAAQ,CAAA;AAAA,MAC3B,CAAA;AAAA,IACF,CAAA;AAAA,IACA,IAAA,EAAM,CAAC,KAAA,KAAU;AACf,MAAA,KAAA,MAAW,YAAY,SAAA,EAAW;AAChC,QAAA,IAAI;AACF,UAAA,QAAA,CAAS,KAAK,CAAA;AAAA,QAChB,SAAS,GAAA,EAAK;AAEZ,UAAA,OAAA,CAAQ,KAAA,CAAM,qCAAqC,GAAG,CAAA;AAAA,QACxD;AAAA,MACF;AAAA,IACF;AAAA,GACF;AACF;;;ACwDA,IAAM,yBAAA,GAA4B,GAAA;AASlC,IAAM,qBAAA,GAAwB,GAAA;AAC9B,IAAM,uBAAuB,CAAA,GAAI,GAAA;AACjC,IAAM,mBAAA,GAAsB,CAAA;AAQ5B,SAAS,kBAAkB,GAAA,EAA2B;AACpD,EAAA,OAAO,GAAA,CAAI,IAAA,KAAS,eAAA,IAAmB,GAAA,CAAI,IAAA,KAAS,WAAA;AACtD;AAEA,SAAS,iBAAA,CAAkB,SAAiB,GAAA,EAA0B;AACpE,EAAA,MAAM,UAAU,IAAA,CAAK,GAAA,CAAI,oBAAA,EAAsB,qBAAA,GAAwB,KAAK,OAAO,CAAA;AAEnF,EAAA,OAAO,GAAA,CAAI,SAAS,cAAA,GAAiB,IAAA,CAAK,IAAI,OAAA,EAAS,GAAA,CAAI,UAAU,CAAA,GAAI,OAAA;AAC3E;AAQO,SAAS,oBAAoB,IAAA,EAA2C;AAC7E,EAAA,MAAM,MAAM,cAAA,EAA0B;AACtC,EAAA,IAAI,MAAA,GAAyB,IAAA;AAC7B,EAAA,IAAI,KAAA,GAA8C,IAAA;AAGlD,EAAA,IAAI,YAAoC,MAAM;AAC5C,IAAA,MAAM,IAAI,MAAM,wCAAwC,CAAA;AAAA,EAC1D,CAAA;AAEA,EAAA,SAAS,UAAA,GAAmB;AAC1B,IAAA,IAAI,UAAU,IAAA,EAAM;AAClB,MAAA,YAAA,CAAa,KAAK,CAAA;AAClB,MAAA,KAAA,GAAQ,IAAA;AAAA,IACV;AAAA,EACF;AAOA,EAAA,MAAM,cAAA,GAAiB,UAAA;AAIvB,EAAA,SAAS,eAAA,CAAgB,OAAe,OAAA,EAAuB;AAC7D,IAAA,UAAA,EAAW;AACX,IAAA,KAAA,GAAQ,WAAW,MAAM;AACvB,MAAA,KAAA,GAAQ,IAAA;AACR,MAAA,KAAK,SAAA,EAAU,CAAE,KAAA,CAAM,CAAC,GAAA,KAAqB;AAI3C,QAAA,IAAI,MAAA,KAAW,IAAA,IAAQ,KAAA,KAAU,IAAA,IAAQ,WAAW,mBAAA,EAAqB;AACzE,QAAA,eAAA,CAAgB,iBAAA,CAAkB,OAAA,EAAS,GAAG,CAAA,EAAG,UAAU,CAAC,CAAA;AAAA,MAC9D,CAAC,CAAA;AAAA,IACH,GAAG,KAAK,CAAA;AAAA,EACV;AAEA,EAAA,SAAS,gBAAgB,OAAA,EAAwB;AAC/C,IAAA,UAAA,EAAW;AACX,IAAA,MAAM,YAAY,IAAI,IAAA,CAAK,OAAA,CAAQ,SAAS,EAAE,OAAA,EAAQ;AACtD,IAAA,IAAI,MAAA,CAAO,KAAA,CAAM,SAAS,CAAA,EAAG;AAC7B,IAAA,MAAM,QAAA,GAAW,SAAA,GAAY,IAAA,CAAK,GAAA,EAAI,GAAI,yBAAA;AAC1C,IAAA,eAAA,CAAgB,IAAA,CAAK,IAAI,cAAA,EAAgB,IAAA,CAAK,IAAI,CAAA,EAAG,QAAQ,CAAC,CAAA,EAAG,CAAC,CAAA;AAAA,EACpE;AAEA,EAAA,SAAS,YAAA,CAAa,SAAkB,KAAA,EAAmD;AACzF,IAAA,MAAA,GAAS,OAAA;AACT,IAAA,KAAK,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,OAAO,CAAA;AAC7B,IAAA,GAAA,CAAI,IAAA,CAAK,EAAE,IAAA,EAAM,KAAA,EAAO,SAAS,CAAA;AACjC,IAAA,eAAA,CAAgB,OAAO,CAAA;AACvB,IAAA,OAAO,OAAA;AAAA,EACT;AAEA,EAAA,SAAS,YAAA,CAAa,OAAO,IAAA,EAAY;AACvC,IAAA,MAAA,GAAS,IAAA;AACT,IAAA,UAAA,EAAW;AACX,IAAA,KAAK,IAAA,CAAK,QAAQ,MAAA,EAAO;AACzB,IAAA,IAAI,MAAM,GAAA,CAAI,IAAA,CAAK,EAAE,IAAA,EAAM,cAAc,CAAA;AACzC,IAAA,IAAA,CAAK,WAAA,IAAc;AAAA,EACrB;AAEA,EAAA,eAAe,KAAQ,KAAA,EAAqC;AAC1D,IAAA,IAAI;AACF,MAAA,OAAO,MAAM,KAAA,EAAM;AAAA,IACrB,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,eAAe,GAAG,CAAA;AAAA,IAC1B;AAAA,EACF;AAIA,EAAA,KAAK,OAAA,CAAQ,QAAQ,IAAA,CAAK,OAAA,CAAQ,KAAK,CAAA,CAAE,IAAA,CAAK,CAAC,CAAA,KAAM;AACnD,IAAA,IAAI,CAAA,EAAG;AACL,MAAA,MAAA,GAAS,CAAA;AACT,MAAA,eAAA,CAAgB,CAAC,CAAA;AAAA,IACnB;AAAA,EACF,CAAC,CAAA;AAED,EAAA,MAAM,MAAA,GAAkC,OAAO,KAAA,KAAU;AACvD,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,MAAM,IAAA,CAAK,UAAU,MAAA,CAAO,IAAA,CAAK,MAAA,CAAO,KAAK,CAAC,CAAA;AACxE,IAAA,MAAM,EAAE,IAAA,EAAM,OAAA,EAAQ,GAAI,MAAA,CAAO,IAAA;AACjC,IAAA,OAAO,YAAA;AAAA,MACL,EAAE,WAAA,EAAa,OAAA,CAAQ,aAAa,SAAA,EAAW,OAAA,CAAQ,WAAW,IAAA,EAAK;AAAA,MACvE;AAAA,KACF;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,MAAA,GAAkC,OAAO,KAAA,KAAU;AACvD,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,MAAM,IAAA,CAAK,UAAU,MAAA,CAAO,IAAA,CAAK,MAAA,CAAO,KAAK,CAAC,CAAA;AACxE,IAAA,IAAI,MAAA,CAAO,IAAA,CAAK,IAAA,KAAS,qBAAA,EAAuB;AAE9C,MAAA,MAAM,KAAA,GAAQ,OAAO,IAAA,CAAK,cAAA;AAC1B,MAAA,OAAO;AAAA,QACL,IAAA,EAAM,qBAAA;AAAA,QACN,WAAW,EAAE,KAAA,EAAO,gBAAA,EAAkB,MAAA,CAAO,KAAK,gBAAA;AAAiB,OACrE;AAAA,IACF;AACA,IAAA,IAAI,MAAA,CAAO,IAAA,CAAK,IAAA,KAAS,WAAA,EAAa;AACpC,MAAA,MAAM,EAAE,IAAA,EAAM,OAAA,EAAQ,GAAI,MAAA,CAAO,IAAA;AACjC,MAAA,MAAMA,OAAAA,GAAS,YAAA;AAAA,QACb,EAAE,WAAA,EAAa,OAAA,CAAQ,aAAa,SAAA,EAAW,OAAA,CAAQ,WAAW,IAAA,EAAK;AAAA,QACvE;AAAA,OACF;AACA,MAAA,OAAO,EAAE,IAAA,EAAM,WAAA,EAAa,OAAA,EAASA,OAAAA,EAAO;AAAA,IAC9C;AAEA,IAAA,MAAM,cAAqB,MAAA,CAAO,IAAA;AAClC,IAAA,MAAM,WAAA;AAAA,EACR,CAAA;AAEA,EAAA,MAAM,UAAoC,YAAY;AACpD,IAAA,MAAM,OAAA,GAAU,MAAA;AAChB,IAAA,IAAI,CAAC,OAAA,EAAS;AACd,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,CAAK,UAAU,MAAA,CAAO,IAAA,CAAK,QAAQ,EAAE,KAAA,EAAO,OAAA,CAAQ,WAAA,EAAa,CAAA;AAAA,IACzE,SAAS,GAAA,EAAK;AAGP,IACP;AACA,IAAA,YAAA,EAAa;AAAA,EACf,CAAA;AAEA,EAAA,MAAM,aAA0C,MAAM,MAAA;AACtD,EAAA,MAAM,iBAAA,GAAwD,CAAC,QAAA,KAAa,GAAA,CAAI,GAAG,QAAQ,CAAA;AAE3F,EAAA,MAAM,oBAAA,GAA8D,OAAO,KAAA,KAAU;AACnF,IAAA,MAAM,IAAA,CAAK,MAAM,IAAA,CAAK,SAAA,CAAU,OAAO,IAAA,CAAK,oBAAA,CAAqB,KAAK,CAAC,CAAA;AAAA,EACzE,CAAA;AACA,EAAA,MAAM,oBAAA,GAA8D,OAAO,KAAA,KAAU;AACnF,IAAA,MAAM,IAAA,CAAK,MAAM,IAAA,CAAK,SAAA,CAAU,OAAO,IAAA,CAAK,oBAAA,CAAqB,KAAK,CAAC,CAAA;AAAA,EACzE,CAAA;AACA,EAAA,MAAM,YAAA,GAA8C,OAAO,KAAA,KAAU;AACnE,IAAA,MAAM,IAAA,CAAK,MAAM,IAAA,CAAK,SAAA,CAAU,OAAO,IAAA,CAAK,YAAA,CAAa,KAAK,CAAC,CAAA;AAAA,EACjE,CAAA;AAEA,EAAA,MAAM,qBAA0D,YAAY;AAC1E,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,MAAM,IAAA,CAAK,SAAA,CAAU,MAAA,CAAO,IAAA,CAAK,kBAAA,CAAmB,EAAE,CAAC,CAAA;AACjF,IAAA,OAAO,MAAA,CAAO,IAAA;AAAA,EAChB,CAAA;AAOA,EAAA,MAAM,UAAA,GAA0C,OAAO,KAAA,KAAU;AAC/D,IAAA,MAAM,SAAS,MAAM,IAAA;AAAA,MAAK,MACxB,IAAA,CAAK,SAAA,CAAU,MAAA,CAAO,KAAK,UAAA,CAAW,EAAE,KAAA,EAAO,KAAA,CAAM,SAAA,CAAU,KAAA,EAAO,IAAA,EAAM,KAAA,CAAM,MAAM;AAAA,KAC1F;AACA,IAAA,MAAM,EAAE,IAAA,EAAM,OAAA,EAAQ,GAAI,MAAA,CAAO,IAAA;AACjC,IAAA,OAAO,YAAA;AAAA,MACL,EAAE,WAAA,EAAa,OAAA,CAAQ,aAAa,SAAA,EAAW,OAAA,CAAQ,WAAW,IAAA,EAAK;AAAA,MACvE;AAAA,KACF;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,gBAAA,GAAsD,OAAO,KAAA,KAAU;AAC3E,IAAA,MAAM,SAAS,MAAM,IAAA;AAAA,MAAK,MACxB,IAAA,CAAK,SAAA,CAAU,MAAA,CAAO,KAAK,gBAAA,CAAiB;AAAA,QAC1C,KAAA,EAAO,MAAM,SAAA,CAAU,KAAA;AAAA,QACvB,MAAM,KAAA,CAAM;AAAA,OACb;AAAA,KACH;AACA,IAAA,MAAM,EAAE,IAAA,EAAM,OAAA,EAAQ,GAAI,MAAA,CAAO,IAAA;AACjC,IAAA,OAAO,YAAA;AAAA,MACL,EAAE,WAAA,EAAa,OAAA,CAAQ,aAAa,SAAA,EAAW,OAAA,CAAQ,WAAW,IAAA,EAAK;AAAA,MACvE;AAAA,KACF;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,eAAA,GAAoD,OAAO,KAAA,KAAU;AACzE,IAAA,MAAM,IAAA,CAAK,MAAM,IAAA,CAAK,SAAA,CAAU,MAAA,CAAO,IAAA,CAAK,eAAA,CAAgB,EAAE,KAAA,EAAO,KAAA,CAAM,SAAA,CAAU,KAAA,EAAO,CAAC,CAAA;AAAA,EAC/F,CAAA;AAEA,EAAA,MAAM,cAAA,GAAkD,OAAO,KAAA,KAAU;AACvE,IAAA,MAAM,SAAS,MAAM,IAAA;AAAA,MAAK,MACxB,IAAA,CAAK,SAAA,CAAU,MAAA,CAAO,KAAK,cAAA,CAAe,EAAE,KAAA,EAAO,KAAA,CAAM,SAAA,CAAU,KAAA,EAAO,IAAA,EAAM,KAAA,CAAM,MAAM;AAAA,KAC9F;AACA,IAAA,MAAM,EAAE,IAAA,EAAM,OAAA,EAAQ,GAAI,MAAA,CAAO,IAAA;AACjC,IAAA,OAAO,YAAA;AAAA,MACL,EAAE,WAAA,EAAa,OAAA,CAAQ,aAAa,SAAA,EAAW,OAAA,CAAQ,WAAW,IAAA,EAAK;AAAA,MACvE;AAAA,KACF;AAAA,EACF,CAAA;AAUA,EAAA,SAAS,kBAAkB,WAAA,EAA2B;AACpD,IAAA,MAAM,OAAA,GAAU,MAAA;AAChB,IAAA,IAAI,CAAC,OAAA,IAAW,CAAC,WAAA,IAAe,WAAA,KAAgB,QAAQ,WAAA,EAAa;AACrE,IAAA,YAAA,CAAa,EAAE,GAAG,OAAA,EAAS,WAAA,IAAe,mBAAmB,CAAA;AAAA,EAC/D;AAEA,EAAA,MAAM,oBAAwD,YAAY;AACxE,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,MAAM,KAAK,SAAA,CAAU,MAAA,CAAO,IAAA,CAAK,iBAAA,EAAmB,CAAA;AAC9E,IAAA,OAAO,MAAA,CAAO,IAAA;AAAA,EAChB,CAAA;AAEA,EAAA,MAAM,UAAA,GAA0C,OAAO,KAAA,KAAU;AAC/D,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,MAAM,IAAA,CAAK,UAAU,MAAA,CAAO,IAAA,CAAK,UAAA,CAAW,KAAK,CAAC,CAAA;AAC5E,IAAA,OAAO,MAAA,CAAO,IAAA;AAAA,EAChB,CAAA;AAEA,EAAA,MAAM,eAAA,GAAoD,OAAO,KAAA,KAAU;AACzE,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,MAAM,IAAA,CAAK,UAAU,MAAA,CAAO,IAAA,CAAK,eAAA,CAAgB,KAAK,CAAC,CAAA;AACjF,IAAA,iBAAA,CAAkB,MAAA,CAAO,KAAK,WAAW,CAAA;AAAA,EAC3C,CAAA;AAEA,EAAA,MAAM,gBAAA,GAAsD,OAAO,KAAA,KAAU;AAC3E,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,MAAM,IAAA,CAAK,UAAU,MAAA,CAAO,IAAA,CAAK,gBAAA,CAAiB,KAAK,CAAC,CAAA;AAClF,IAAA,iBAAA,CAAkB,MAAA,CAAO,KAAK,WAAW,CAAA;AAAA,EAC3C,CAAA;AAEA,EAAA,MAAM,qBAAA,GAAgE,OAAO,KAAA,KAAU;AACrF,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,MAAM,IAAA,CAAK,UAAU,MAAA,CAAO,IAAA,CAAK,qBAAA,CAAsB,KAAK,CAAC,CAAA;AACvF,IAAA,OAAO,OAAO,IAAA,CAAK,WAAA;AAAA,EACrB,CAAA;AAIA,EAAA,MAAM,cAAA,GAAkD,OAAO,KAAA,KAAU;AACvE,IAAA,MAAM,SAAS,MAAM,IAAA;AAAA,MAAK,MACxB,IAAA,CAAK,SAAA,CAAU,MAAA,CAAO,KAAK,cAAA,CAAe;AAAA,QACxC,iBAAiB,KAAA,CAAM,eAAA;AAAA,QACvB,aAAa,KAAA,CAAM,WAAA;AAAA,QACnB,qBAAqB,KAAA,CAAM;AAAA,OAC5B;AAAA,KACH;AACA,IAAA,iBAAA,CAAkB,MAAA,CAAO,KAAK,WAAW,CAAA;AAAA,EAC3C,CAAA;AAEA,EAAA,MAAM,WAAA,GAA4C,OAAO,KAAA,KAAU;AACjE,IAAA,MAAM,IAAA,CAAK,MAAM,IAAA,CAAK,SAAA,CAAU,OAAO,IAAA,CAAK,WAAA,CAAY,KAAK,CAAC,CAAA;AAAA,EAChE,CAAA;AAEA,EAAA,MAAM,kBAAA,GAA0D,OAAO,KAAA,KAAU;AAC/E,IAAA,MAAM,IAAA,CAAK,MAAM,IAAA,CAAK,SAAA,CAAU,OAAO,IAAA,CAAK,kBAAA,CAAmB,KAAK,CAAC,CAAA;AAAA,EACvE,CAAA;AAEA,EAAA,MAAM,eAA8C,YAAY;AAC9D,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,MAAM,KAAK,SAAA,CAAU,MAAA,CAAO,IAAA,CAAK,YAAA,EAAc,CAAA;AACzE,IAAA,OAAO,MAAA,CAAO,IAAA;AAAA,EAChB,CAAA;AAEA,EAAA,MAAM,aAAA,GAAgD,OAAO,KAAA,KAAU;AACrE,IAAA,MAAM,IAAA,CAAK,MAAM,IAAA,CAAK,SAAA,CAAU,OAAO,IAAA,CAAK,aAAA,CAAc,KAAK,CAAC,CAAA;AAAA,EAClE,CAAA;AAEA,EAAA,MAAM,sBAA4D,YAAY;AAC5E,IAAA,MAAM,KAAK,MAAM,IAAA,CAAK,UAAU,MAAA,CAAO,IAAA,CAAK,qBAAqB,CAAA;AAAA,EACnE,CAAA;AAKA,EAAA,MAAM,YAAA,GAA8C,OAAO,KAAA,KAAU;AACnE,IAAA,MAAM,IAAA;AAAA,MAAK,MACT,IAAA,CAAK,SAAA,CAAU,MAAA,CAAO,KAAK,YAAA,CAAa;AAAA,QACtC,cAAc,KAAA,CAAM,YAAA;AAAA,QACpB,UAAU,KAAA,CAAM;AAAA,OACjB;AAAA,KACH;AACA,IAAA,YAAA,EAAa;AAAA,EACf,CAAA;AAEA,EAAA,MAAM,WAAsC,YAAY;AAGtD,IAAA,IAAI,OAAA,GAAU,MAAA;AACd,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA,MAAM,cAAc,MAAM,OAAA,CAAQ,QAAQ,IAAA,CAAK,OAAA,CAAQ,KAAK,CAAA;AAC5D,MAAA,IAAI,WAAA,EAAa;AACf,QAAA,MAAA,GAAS,WAAA;AACT,QAAA,OAAA,GAAU,WAAA;AAAA,MACZ;AAAA,IACF;AACA,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA,MAAM,GAAA,GAAmB,EAAE,IAAA,EAAM,eAAA,EAAgB;AACjD,MAAA,MAAM,GAAA;AAAA,IACR;AACA,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,SAAA,CAAU,MAAA,CAAO,IAAA,CAAK,OAAA,CAAQ,EAAE,KAAA,EAAO,OAAA,CAAQ,WAAA,EAAa,CAAA;AACtF,MAAA,MAAM,EAAE,IAAA,EAAM,OAAA,EAAQ,GAAI,MAAA,CAAO,IAAA;AACjC,MAAA,OAAO,YAAA;AAAA,QACL,EAAE,WAAA,EAAa,OAAA,CAAQ,aAAa,SAAA,EAAW,OAAA,CAAQ,WAAW,IAAA,EAAK;AAAA,QACvE;AAAA,OACF;AAAA,IACF,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,UAAA,GAAa,eAAe,GAAG,CAAA;AAGrC,MAAA,IAAI,iBAAA,CAAkB,UAAU,CAAA,EAAG,YAAA,EAAa;AAChD,MAAA,MAAM,UAAA;AAAA,IACR;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,aAAA,GAAgD,MAAM,YAAA,EAAa;AAEzE,EAAA,SAAA,GAAY,QAAA;AAEZ,EAAA,OAAO;AAAA,IACL,MAAA;AAAA,IACA,MAAA;AAAA,IACA,OAAA;AAAA,IACA,UAAA;AAAA,IACA,iBAAA;AAAA,IACA,oBAAA;AAAA,IACA,oBAAA;AAAA,IACA,YAAA;AAAA,IACA,kBAAA;AAAA,IACA,UAAA;AAAA,IACA,gBAAA;AAAA,IACA,eAAA;AAAA,IACA,cAAA;AAAA,IACA,iBAAA;AAAA,IACA,UAAA;AAAA,IACA,eAAA;AAAA,IACA,gBAAA;AAAA,IACA,qBAAA;AAAA,IACA,cAAA;AAAA,IACA,WAAA;AAAA,IACA,kBAAA;AAAA,IACA,YAAA;AAAA,IACA,aAAA;AAAA,IACA,mBAAA;AAAA,IACA,YAAA;AAAA,IACA,QAAA;AAAA,IACA;AAAA,GACF;AACF;;;ACpcO,IAAM,gBAAA,GAAmB,wBAAA;;;ACWhC,SAAS,UAA4B,KAAA,EAAa;AAChD,EAAA,MAAM,KAAA,uBAAY,GAAA,EAA8B;AAChD,EAAA,OAAO,IAAI,MAAM,KAAA,EAAO;AAAA,IACtB,GAAA,CAAI,MAAA,EAAQ,IAAA,EAAM,QAAA,EAAU;AAC1B,MAAA,IAAI,MAAM,GAAA,CAAI,IAAI,GAAG,OAAO,KAAA,CAAM,IAAI,IAAI,CAAA;AAC1C,MAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,GAAA,CAAI,MAAA,EAAQ,MAAM,QAAQ,CAAA;AAChD,MAAA,IAAI,OAAO,UAAU,UAAA,EAAY;AAG/B,QAAA,OAAO,KAAA;AAAA,MACT;AACA,MAAA,MAAM,OAAA,GAAU,UAAU,IAAA,KAAoB;AAC5C,QAAA,IAAI;AACF,UAAA,OAAO,MAAO,KAAA,CAAgD,GAAG,IAAI,CAAA;AAAA,QACvE,SAAS,GAAA,EAAK;AACZ,UAAA,MAAM,eAAe,GAAG,CAAA;AAAA,QAC1B;AAAA,MACF,CAAA;AACA,MAAA,KAAA,CAAM,GAAA,CAAI,MAAM,OAAO,CAAA;AACvB,MAAA,OAAO,OAAA;AAAA,IACT;AAAA,GACD,CAAA;AACH;AA+BO,SAAS,qBAAqB,SAAA,EAAsC;AACzE,EAAA,MAAM,IAAI,SAAA,CAAU,MAAA;AACpB,EAAA,OAAO;AAAA,IACL,QAAA,EAAU,SAAA,CAAU,CAAA,CAAE,OAAO,CAAA;AAAA,IAC7B,SAAA,EAAW,SAAA,CAAU,CAAA,CAAE,QAAQ,CAAA;AAAA,IAC/B,QAAA,EAAU,SAAA,CAAU,CAAA,CAAE,OAAO,CAAA;AAAA,IAC7B,KAAA,EAAO,SAAA,CAAU,CAAA,CAAE,IAAI,CAAA;AAAA,IACvB,SAAA,EAAW,SAAA,CAAU,CAAA,CAAE,SAAS,CAAA;AAAA,IAChC,OAAA,EAAS,SAAA,CAAU,CAAA,CAAE,MAAM,CAAA;AAAA,IAC3B,SAAA,EAAW,SAAA,CAAU,CAAA,CAAE,SAAS,CAAA;AAAA,IAChC,QAAA,EAAU,SAAA,CAAU,CAAA,CAAE,OAAO,CAAA;AAAA,IAC7B,WAAA,EAAa,SAAA,CAAU,CAAA,CAAE,UAAU,CAAA;AAAA,IACnC,UAAA,EAAY,SAAA,CAAU,CAAA,CAAE,SAAS,CAAA;AAAA,IACjC,WAAA,EAAa,SAAA,CAAU,CAAA,CAAE,UAAU,CAAA;AAAA,IACnC,WAAA,EAAa,SAAA,CAAU,CAAA,CAAE,WAAW,CAAA;AAAA,IACpC,WAAA,EAAa,SAAA,CAAU,CAAA,CAAE,UAAU,CAAA;AAAA,IACnC,gBAAA,EAAkB,SAAA,CAAU,CAAA,CAAE,eAAe,CAAA;AAAA,IAC7C,cAAA,EAAgB,SAAA,CAAU,CAAA,CAAE,aAAa,CAAA;AAAA,IACzC,QAAA,EAAU,SAAA,CAAU,CAAA,CAAE,OAAO,CAAA;AAAA,IAC7B,QAAA,EAAU,SAAA,CAAU,CAAA,CAAE,OAAO,CAAA;AAAA,IAC7B,QAAA,EAAU,SAAA,CAAU,CAAA,CAAE,QAAQ,CAAA;AAAA,IAC9B,MAAA,EAAQ,SAAA,CAAU,CAAA,CAAE,MAAM,CAAA;AAAA,IAC1B,MAAA,EAAQ,SAAA,CAAU,CAAA,CAAE,KAAK,CAAA;AAAA,IACzB,QAAA,EAAU,SAAA,CAAU,CAAA,CAAE,QAAQ,CAAA;AAAA,IAC9B,KAAA,EAAO,SAAA,CAAU,CAAA,CAAE,KAAK,CAAA;AAAA,IACxB,YAAA,EAAc,SAAA,CAAU,CAAA,CAAE,YAAY,CAAA;AAAA,IACtC,OAAA,EAAS,SAAA,CAAU,CAAA,CAAE,OAAO,CAAA;AAAA,IAC5B,YAAA,EAAc,SAAA,CAAU,CAAA,CAAE,YAAY,CAAA;AAAA,IACtC,WAAA,EAAa,SAAA,CAAU,CAAA,CAAE,WAAW;AAAA,GACtC;AACF;;;AC/FA,IAAM,kBAAA,GAAqB,SAAA;AAC3B,IAAM,kBAAA,GAAqB,SAAA;AAE3B,SAAS,OAAO,QAAA,EAA0B;AACxC,EAAA,OAAO,CAAA,EAAG,kBAAkB,CAAA,EAAG,QAAQ,GAAG,kBAAkB,CAAA,CAAA;AAC9D;AAEA,SAAS,eAAe,KAAA,EAAkC;AACxD,EAAA,IAAI,CAAC,KAAA,IAAS,OAAO,KAAA,KAAU,UAAU,OAAO,KAAA;AAChD,EAAA,MAAM,CAAA,GAAI,KAAA;AACV,EAAA,OACE,OAAO,EAAE,WAAA,KAAgB,QAAA,IACzB,OAAO,CAAA,CAAE,SAAA,KAAc,QAAA,IACvB,OAAO,CAAA,CAAE,IAAA,KAAS,YAClB,CAAA,CAAE,IAAA,KAAS,IAAA,IACX,OAAQ,CAAA,CAAE,IAAA,CAA0B,OAAO,QAAA,IAC3C,OAAQ,CAAA,CAAE,IAAA,CAA6B,KAAA,KAAU,QAAA;AAErD;AAMO,SAAS,0BAA0B,QAAA,EAAgC;AACxE,EAAA,MAAM,GAAA,GAAM,OAAO,QAAQ,CAAA;AAC3B,EAAA,OAAO;AAAA,IACL,KAAK,MAAM;AACT,MAAA,IAAI;AACF,QAAA,MAAM,GAAA,GAAM,UAAA,CAAW,YAAA,EAAc,OAAA,CAAQ,GAAG,CAAA;AAChD,QAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AACjB,QAAA,MAAM,MAAA,GAAkB,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AACtC,QAAA,OAAO,cAAA,CAAe,MAAM,CAAA,GAAI,MAAA,GAAS,IAAA;AAAA,MAC3C,CAAA,CAAA,MAAQ;AACN,QAAA,OAAO,IAAA;AAAA,MACT;AAAA,IACF,CAAA;AAAA,IACA,GAAA,EAAK,CAAC,OAAA,KAAqB;AACzB,MAAA,IAAI;AACF,QAAA,UAAA,CAAW,cAAc,OAAA,CAAQ,GAAA,EAAK,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC,CAAA;AAAA,MAC/D,CAAA,CAAA,MAAQ;AAAA,MAGR;AAAA,IACF,CAAA;AAAA,IACA,QAAQ,MAAM;AACZ,MAAA,IAAI;AACF,QAAA,UAAA,CAAW,YAAA,EAAc,WAAW,GAAG,CAAA;AAAA,MACzC,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AAAA,GACF;AACF;AAGO,SAAS,0BAAA,GAA2C;AACzD,EAAA,IAAI,OAAA,GAA0B,IAAA;AAC9B,EAAA,OAAO;AAAA,IACL,KAAK,MAAM,OAAA;AAAA,IACX,GAAA,EAAK,CAAC,OAAA,KAAqB;AACzB,MAAA,OAAA,GAAU,OAAA;AAAA,IACZ,CAAA;AAAA,IACA,QAAQ,MAAM;AACZ,MAAA,OAAA,GAAU,IAAA;AAAA,IACZ;AAAA,GACF;AACF;;;ACzDA,IAAM,aAAA,GAAgB,OAAA;AAOf,SAAS,kBAAA,CAAmB,OAAsB,GAAA,EAA4B;AACnF,EAAA,IAAI,KAAA,KAAU,MAAM,OAAO,IAAA;AAC3B,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,EAAK;AAC3B,EAAA,IAAI,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG,OAAO,IAAA;AACjC,EAAA,IAAI,cAAc,IAAA,CAAK,OAAO,GAAG,OAAO,MAAA,CAAO,OAAO,CAAA,GAAI,GAAA;AAE1D,EAAA,IAAI,CAAC,QAAA,CAAS,IAAA,CAAK,OAAO,GAAG,OAAO,IAAA;AACpC,EAAA,MAAM,EAAA,GAAK,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA;AAC7B,EAAA,OAAO,MAAA,CAAO,MAAM,EAAE,CAAA,GAAI,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,EAAA,GAAK,GAAG,CAAA;AACvD;AAEA,SAAS,UAAU,IAAA,EAAuB;AACxC,EAAA,IAAI;AACF,IAAA,OAAO,IAAA,CAAK,MAAM,IAAI,CAAA;AAAA,EACxB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,MAAA;AAAA,EACT;AACF;AAQA,SAAS,UAAA,CAAW,MAAe,MAAA,EAA0D;AAC3F,EAAA,IAAI,QAAA,CAAS,IAAI,CAAA,IAAK,QAAA,CAAS,IAAA,CAAK,IAAI,CAAA,IAAK,OAAO,IAAA,CAAK,IAAA,CAAK,IAAA,KAAS,QAAA,EAAU;AAC/E,IAAA,MAAM,QAAQ,IAAA,CAAK,IAAA;AACnB,IAAA,MAAM,OAAO,QAAA,CAAS,KAAA,CAAM,IAAI,CAAA,GAAI,KAAA,CAAM,OAAO,EAAC;AAClD,IAAA,IAAI,OAAO,IAAA,CAAK,YAAA,KAAiB,QAAA,EAAU,OAAO,IAAA;AAClD,IAAA,OAAO,EAAE,GAAG,IAAA,EAAM,IAAA,EAAM,EAAE,GAAG,KAAA,EAAO,IAAA,EAAM,EAAE,GAAG,IAAA,EAAM,YAAA,EAAc,MAAA,IAAS,EAAE;AAAA,EAChF;AACA,EAAA,OAAO;AAAA,IACL,IAAA,EAAM;AAAA,MACJ,OAAA,EAAS,KAAA;AAAA,MACT,IAAA,EAAM,mBAAA;AAAA,MACN,MAAA,EAAQ,GAAA;AAAA,MACR,OAAA,EAAS,mBAAA;AAAA,MACT,IAAA,EAAM,EAAE,YAAA,EAAc,MAAA;AAAO;AAC/B,GACF;AACF;AAMA,eAAsB,kBAAA,CAAmB,UAAoB,GAAA,EAAgC;AAC3F,EAAA,IAAI,QAAA,CAAS,MAAA,KAAW,GAAA,EAAK,OAAO,QAAA;AACpC,EAAA,MAAM,SAAS,kBAAA,CAAmB,QAAA,CAAS,QAAQ,GAAA,CAAI,aAAa,GAAG,GAAG,CAAA;AAC1E,EAAA,IAAI,MAAA,KAAW,MAAM,OAAO,QAAA;AAC5B,EAAA,MAAM,MAAA,GAAS,UAAA,CAAW,SAAA,CAAU,MAAM,QAAA,CAAS,OAAM,CAAE,IAAA,EAAM,CAAA,EAAG,MAAM,CAAA;AAC1E,EAAA,IAAI,MAAA,KAAW,MAAM,OAAO,QAAA;AAC5B,EAAA,MAAM,OAAA,GAAU,IAAI,OAAA,CAAQ,QAAA,CAAS,OAAO,CAAA;AAC5C,EAAA,OAAA,CAAQ,GAAA,CAAI,gBAAgB,kBAAkB,CAAA;AAC9C,EAAA,OAAA,CAAQ,OAAO,gBAAgB,CAAA;AAC/B,EAAA,OAAO,IAAI,QAAA,CAAS,IAAA,CAAK,SAAA,CAAU,MAAM,CAAA,EAAG;AAAA,IAC1C,QAAQ,QAAA,CAAS,MAAA;AAAA,IACjB,YAAY,QAAA,CAAS,UAAA;AAAA,IACrB;AAAA,GACD,CAAA;AACH;;;AC3CO,SAAS,gBAAgB,OAAA,EAA4C;AAC1E,EAAA,IAAI,WAAA,GAAuC,IAAA;AAC3C,EAAA,MAAM,SAAA,GAA0B,OAAA,CAAQ,KAAA,IAAS,KAAA,CAAM,KAAK,UAAU,CAAA;AAMtE,EAAA,MAAM,YAAA,GAA6B,OAAO,KAAA,EAAO,IAAA,KAAS;AACxD,IAAA,MAAM,UAAU,KAAA,YAAiB,OAAA,GAAU,QAAQ,IAAI,OAAA,CAAQ,OAAO,IAAI,CAAA;AAC1E,IAAA,MAAM,IAAA,GAAO,CAAC,GAAA,KAAiB,SAAA,CAAU,GAAG,CAAA;AAC5C,IAAA,MAAM,QAAA,GAAW,cAAc,MAAM,WAAA,CAAY,SAAS,IAAI,CAAA,GAAI,MAAM,IAAA,CAAK,OAAO,CAAA;AACpF,IAAA,OAAO,kBAAA,CAAmB,QAAA,EAAU,IAAA,CAAK,GAAA,EAAK,CAAA;AAAA,EAChD,CAAA;AAEA,EAAA,MAAM,IAAA,GAAO,IAAI,OAAA,CAA0B;AAAA,IACzC,GAAA,EAAK,MAAM,CAAA,EAAG,OAAA,CAAQ,OAAO,CAAA,IAAA,CAAA;AAAA,IAC7B,OAAA,EAAS,CAAC,EAAE,OAAA,EAAQ,KAAM;AACxB,MAAA,MAAM,OAAA,GAAU,QAAQ,UAAA,EAAW;AACnC,MAAA,MAAM,OAAA,GAAkC;AAAA,QACtC,sBAAsB,OAAA,CAAQ;AAAA,OAChC;AACA,MAAA,IAAI,OAAA,EAAS;AACX,QAAA,OAAA,CAAQ,aAAA,GAAgB,CAAA,OAAA,EAAU,OAAA,CAAQ,WAAW,CAAA,CAAA;AAAA,MACvD;AACA,MAAA,IAAI,OAAA,CAAQ,gBAAgB,MAAA,EAAW;AACrC,QAAA,OAAA,CAAQ,eAAe,IAAI,OAAA,CAAQ,WAAA;AAAA,MACrC;AACA,MAAA,OAAO,OAAA;AAAA,IACT,CAAA;AAAA,IACA,KAAA,EAAO;AAAA,GACR,CAAA;AAED,EAAA,MAAM,MAAA,GAA0B,iBAAiB,IAAI,CAAA;AACrD,EAAA,OAAO;AAAA,IACL,MAAA;AAAA,IACA,oBAAA,EAAsB,CAAC,CAAA,KAAM;AAC3B,MAAA,WAAA,GAAc,CAAA;AAAA,IAChB;AAAA,GACF;AACF;AAGA,IAAM,cAAA,GAAiB,kBAAA;AAUvB,IAAM,qBAAA,GAA2C;AAAA,EAC/C,aAAA;AAAA,EACA,aAAA;AAAA,EACA,2BAAA;AAAA,EACA,2BAAA;AAAA,EACA,mBAAA;AAAA,EACA,yBAAA;AAAA,EACA,iBAAA;AAAA,EACA,uBAAA;AAAA,EACA,sBAAA;AAAA,EACA;AACF,CAAA;AAQA,SAAS,gBAAgB,OAAA,EAA0B;AACjD,EAAA,IAAI;AACF,IAAA,OAAO,IAAI,GAAA,CAAI,OAAA,CAAQ,GAAG,CAAA,CAAE,QAAA;AAAA,EAC9B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,EAAA;AAAA,EACT;AACF;AAGA,SAAS,iBAAiB,OAAA,EAA2B;AACnD,EAAA,IAAI,QAAQ,OAAA,CAAQ,GAAA,CAAI,cAAc,CAAA,KAAM,KAAK,OAAO,IAAA;AACxD,EAAA,OAAO,eAAA,CAAgB,OAAO,CAAA,CAAE,QAAA,CAAS,eAAe,CAAA;AAC1D;AAGA,SAAS,oBAAoB,OAAA,EAA2B;AACtD,EAAA,MAAM,IAAA,GAAO,gBAAgB,OAAO,CAAA;AACpC,EAAA,OAAO,qBAAA,CAAsB,KAAK,CAAC,SAAA,KAAc,KAAK,QAAA,CAAS,CAAA,CAAA,EAAI,SAAS,CAAA,CAAE,CAAC,CAAA;AACjF;AAsBO,SAAS,yBAAyB,IAAA,EAA6C;AACpF,EAAA,IAAI,QAAA,GAAoC,IAAA;AAExC,EAAA,MAAM,cAAc,MAAwB;AAC1C,IAAA,IAAI,CAAC,QAAA,EAAU;AACb,MAAA,QAAA,GAAW,IAAA,CAAK,IAAA,CAAK,QAAA,EAAS,CAAE,QAAQ,MAAM;AAC5C,QAAA,QAAA,GAAW,IAAA;AAAA,MACb,CAAC,CAAA;AAAA,IACH;AACA,IAAA,OAAO,QAAA;AAAA,EACT,CAAA;AAEA,EAAA,IAAA,CAAK,SAAA,CAAU,oBAAA,CAAqB,OAAO,OAAA,EAAS,IAAA,KAAS;AAG3D,IAAA,IAAI,gBAAA,CAAiB,OAAO,CAAA,IAAK,mBAAA,CAAoB,OAAO,CAAA,EAAG;AAC7D,MAAA,OAAO,KAAK,OAAO,CAAA;AAAA,IACrB;AAIA,IAAA,MAAM,QAAA,GAAW,MAAM,OAAA,CAAQ,KAAA,GAAQ,WAAA,EAAY;AAEnD,IAAA,MAAM,aAAA,GAAgB,MAAM,IAAA,CAAK,OAAO,CAAA;AACxC,IAAA,IAAI,aAAA,CAAc,MAAA,KAAW,GAAA,EAAK,OAAO,aAAA;AAEzC,IAAA,IAAI;AACF,MAAA,MAAM,WAAA,EAAY;AAAA,IACpB,CAAA,CAAA,MAAQ;AAGN,MAAA,OAAO,aAAA;AAAA,IACT;AAKA,IAAA,MAAM,eAAA,GAAkB,IAAI,OAAA,CAAQ,OAAA,CAAQ,OAAO,CAAA;AACnD,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,IAAA,CAAK,UAAA,EAAW;AACrC,IAAA,IAAI,OAAA,EAAS;AACX,MAAA,eAAA,CAAgB,GAAA,CAAI,eAAA,EAAiB,CAAA,OAAA,EAAU,OAAA,CAAQ,WAAW,CAAA,CAAE,CAAA;AAAA,IACtE,CAAA,MAAO;AACL,MAAA,eAAA,CAAgB,OAAO,eAAe,CAAA;AAAA,IACxC;AACA,IAAA,MAAM,QAAA,GAAW,IAAI,OAAA,CAAQ,OAAA,CAAQ,GAAA,EAAK;AAAA,MACxC,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,OAAA,EAAS,eAAA;AAAA,MACT,IAAA,EAAM,QAAA,CAAS,UAAA,GAAa,CAAA,GAAI,QAAA,GAAW,MAAA;AAAA,MAC3C,aAAa,OAAA,CAAQ,WAAA;AAAA,MACrB,MAAM,OAAA,CAAQ,IAAA;AAAA,MACd,OAAO,OAAA,CAAQ,KAAA;AAAA,MACf,UAAU,OAAA,CAAQ,QAAA;AAAA,MAClB,UAAU,OAAA,CAAQ,QAAA;AAAA,MAClB,WAAW,OAAA,CAAQ;AAAA,KACpB,CAAA;AAED,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,QAAQ,CAAA;AACnC,IAAA,IAAI,OAAA,CAAQ,WAAW,GAAA,EAAK;AAG1B,MAAA,IAAA,CAAK,KAAK,aAAA,EAAc;AAAA,IAC1B;AACA,IAAA,OAAO,OAAA;AAAA,EACT,CAAC,CAAA;AACH;AC/GA,IAAM,QAAA,GAAwB;AAAA,EAC5B,IAAA,EAAM,WAAA;AAAA,EACN,SAAS,EAAE,MAAA,EAAQ,oBAAA,EAAsB,IAAA,EAAM,EAAC;AAClD,CAAA;AAOA,IAAM,kBAAA,GAAqB,4BAAA;AAO3B,IAAM,cAAA,GAAiB,qBAAA;AAGvB,SAAS,iBAAiB,KAAA,EAA8B;AACtD,EAAA,MAAM,SAAA,GAAY,KAAA,CAAM,OAAA,CAAQ,kBAAA,EAAoB,EAAE,CAAA;AACtD,EAAA,OAAO,eAAe,IAAA,CAAK,SAAS,CAAA,GAAI,SAAA,CAAU,aAAY,GAAI,IAAA;AACpE;AAEA,eAAe,SAAS,QAAA,EAAsC;AAC5D,EAAA,IAAI;AACF,IAAA,OAAO,MAAM,SAAS,IAAA,EAAK;AAAA,EAC7B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,MAAA;AAAA,EACT;AACF;AAGA,SAAS,SAAA,CAAU,UAAoB,IAAA,EAA4B;AACjE,EAAA,MAAM,IAAA,GAAO,MAAM,kBAAA,CAAmB,QAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,aAAa,CAAA,EAAG,IAAA,CAAK,GAAA,EAAK,CAAA,IAAK,CAAA;AAI1F,EAAA,IAAI,eAAA,CAAgB,IAAI,CAAA,EAAG;AACzB,IAAA,MAAM,KAAA,GAAQ,cAAA,CAAe,uBAAA,CAAwB,IAAI,CAAC,CAAA;AAE1D,IAAA,IAAI,KAAA,CAAM,IAAA,KAAS,cAAA,IAAkB,KAAA,CAAM,eAAe,CAAA,EAAG;AAC3D,MAAA,OAAO,EAAE,GAAG,KAAA,EAAO,UAAA,EAAY,MAAK,EAAE;AAAA,IACxC;AACA,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,IAAI,QAAA,CAAS,WAAW,GAAA,EAAK,OAAO,EAAE,IAAA,EAAM,cAAA,EAAgB,UAAA,EAAY,IAAA,EAAK,EAAE;AAC/E,EAAA,OAAO,EAAE,IAAA,EAAM,QAAA,EAAU,MAAA,EAAQ,SAAS,MAAA,EAAO;AACnD;AA4BA,eAAsB,eAAA,CACpB,IAAA,EACA,OAAA,GAAkC,EAAC,EACN;AAI7B,EAAA,MAAM,SAAA,GAAY,iBAAiB,IAAI,CAAA;AACvC,EAAA,IAAI,SAAA,KAAc,MAAM,MAAM,QAAA;AAE9B,EAAA,MAAM,OAAA,GAAU,QAAQ,OAAA,IAAW,gBAAA;AACnC,EAAA,MAAM,OAAA,GAAwB,OAAA,CAAQ,KAAA,IAAS,KAAA,CAAM,KAAK,UAAU,CAAA;AACpE,EAAA,IAAI,QAAA;AACJ,EAAA,IAAI;AACF,IAAA,QAAA,GAAW,MAAM,OAAA,CAAQ,CAAA,EAAG,OAAO,CAAA,aAAA,EAAgB,SAAS,CAAA,CAAA,EAAI;AAAA,MAC9D,MAAA,EAAQ,KAAA;AAAA,MACR,OAAA,EAAS,EAAE,MAAA,EAAQ,kBAAA;AAAmB,KACvC,CAAA;AAAA,EACH,SAAS,KAAA,EAAO;AACd,IAAA,MAAM,eAAe,KAAK,CAAA;AAAA,EAC5B;AAEA,EAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,QAAQ,CAAA;AACpC,EAAA,IAAI,SAAS,EAAA,EAAI;AACf,IAAA,IAAI,SAAS,IAAI,CAAA,IAAK,QAAA,CAAS,IAAA,CAAK,IAAI,CAAA,EAAG;AAEzC,MAAA,OAAO,IAAA,CAAK,IAAA;AAAA,IACd;AACA,IAAA,MAAM,EAAE,IAAA,EAAM,QAAA,EAAU,MAAA,EAAQ,SAAS,MAAA,EAAO;AAAA,EAClD;AACA,EAAA,MAAM,SAAA,CAAU,UAAU,IAAI,CAAA;AAChC;;;ACnKA,IAAM,SAAA,GAAyB,EAAE,IAAA,EAAM,WAAA,EAAY;AAiB5C,SAAS,kBAAA,CACd,QACA,OAAA,EACa;AAGb,EAAA,IAAI,CAAC,UAAU,CAAC,KAAA,CAAM,QAAQ,MAAA,CAAO,OAAO,GAAG,OAAO,SAAA;AACtD,EAAA,MAAM,aAAa,OAAA,CAAQ,UAAA,EAAY,IAAA,EAAK,CAAE,aAAY,IAAK,EAAA;AAC/D,EAAA,IAAI,UAAA,KAAe,IAAI,OAAO,SAAA;AAC9B,EAAA,MAAM,IAAA,GAAO,OAAO,OAAA,CAAQ,IAAA;AAAA,IAC1B,CAAC,KAAA,KACC,KAAA,EAAO,QAAA,KAAa,OAAA,CAAQ,QAAA,IAC5B,OAAO,KAAA,CAAM,UAAA,KAAe,QAAA,IAC5B,KAAA,CAAM,UAAA,CAAW,aAAY,KAAM;AAAA,GACvC;AACA,EAAA,IAAI,CAAC,QAAQ,CAAC,KAAA,CAAM,QAAQ,IAAA,CAAK,UAAU,GAAG,OAAO,SAAA;AACrD,EAAA,MAAM,MAAM,IAAI,GAAA,CAAI,OAAA,CAAQ,aAAA,IAAiB,EAAE,CAAA;AAC/C,EAAA,IAAI,CAAC,IAAA,CAAK,UAAA,CAAW,KAAA,CAAM,CAAC,SAAA,KAAc,GAAA,CAAI,GAAA,CAAI,SAAS,CAAC,CAAA,EAAG,OAAO,SAAA;AACtE,EAAA,OAAO,EAAE,IAAA,EAAM,UAAA,EAAY,UAAA,EAAY,KAAK,UAAA,EAAW;AACzD;;;AC6FO,IAAM,sBAAA,GAAyB;AA6B/B,IAAM,yBAAA,GAA4B;AAAA,EACvC,YAAA,EAAc,GAAA;AAAA,EACd,WAAA,EAAa;AACf;AAGA,IAAM,oBAAA,GAAuB,gBAAA;AAE7B,IAAM,kBAAA,GAAqB,cAAA;AAE3B,IAAM,mBAAA,GAAsB,SAAA;AAE5B,IAAM,qBAAA,GAAwB,GAAA;AAE9B,IAAM,kBAAA,GAAqB,GAAA;AAE3B,IAAM,QAAA,GAAW,GAAA;AAMjB,IAAM,gBAAA,GAAmB,CAAC,GAAA,EAAO,IAAA,EAAQ,GAAM,CAAA;AAG/C,IAAM,iBAAA,uBAA6C,GAAA,CAAI;AAAA,EACrD,YAAA;AAAA,EACA;AACF,CAAC,CAAA;AAGD,IAAM,iBAAA,uBAA6C,GAAA,CAAI;AAAA,EACrD,eAAA;AAAA,EACA,WAAA;AAAA,EACA,WAAA;AAAA,EACA;AACF,CAAC,CAAA;AAED,SAAS,aAAa,KAAA,EAAyB;AAC7C,EAAA,IAAI,OAAO,UAAU,QAAA,IAAY,KAAA,KAAU,QAAQ,EAAE,MAAA,IAAU,QAAQ,OAAO,KAAA;AAC9E,EAAA,MAAM,OAAgB,KAAA,CAAM,IAAA;AAC5B,EAAA,OAAO,OAAO,IAAA,KAAS,QAAA,IAAY,iBAAA,CAAkB,IAAI,IAAI,CAAA;AAC/D;AAEA,SAAS,cAAc,KAAA,EAAiC;AACtD,EAAA,OACE,OAAO,UAAU,QAAA,IACjB,KAAA,CAAM,WAAW,mBAAmB,CAAA,IACpC,KAAA,CAAM,MAAA,GAAS,mBAAA,CAAoB,MAAA;AAEvC;AAcA,SAAS,gBAAgB,OAAA,EAA0C;AACjE,EAAA,IAAI,OAAO,YAAY,QAAA,IAAY,OAAA,KAAY,QAAQ,EAAE,SAAA,IAAa,UAAU,OAAO,IAAA;AACvF,EAAA,MAAM,UAAmB,OAAA,CAAQ,OAAA;AACjC,EAAA,IAAI,OAAO,OAAA,KAAY,QAAA,IAAY,OAAA,KAAY,MAAM,OAAO,IAAA;AAC5D,EAAA,IAAI,EAAE,OAAA,IAAW,OAAA,CAAA,IAAY,EAAE,MAAA,IAAU,UAAU,OAAO,IAAA;AAC1D,EAAA,MAAM,EAAE,KAAA,EAAO,IAAA,EAAK,GAAI,OAAA;AACxB,EAAA,IAAI,KAAA,KAAU,QAAA,IAAY,KAAA,KAAU,SAAA,EAAW,OAAO,IAAA;AACtD,EAAA,IAAI,OAAO,IAAA,KAAS,QAAA,IAAY,IAAA,CAAK,MAAA,KAAW,GAAG,OAAO,IAAA;AAC1D,EAAA,MAAM,GAAA,GAAM,SAAS,OAAA,IAAW,OAAO,QAAQ,GAAA,KAAQ,QAAA,GAAW,QAAQ,GAAA,GAAM,EAAA;AAChF,EAAA,OAAO,EAAE,KAAA,EAAO,IAAA,EAAM,GAAA,EAAK,GAAA,CAAI,MAAA,GAAS,CAAA,GAAI,CAAA,IAAA,EAAO,GAAG,CAAA,CAAA,GAAK,CAAA,KAAA,EAAQ,IAAI,CAAA,CAAA,EAAG;AAC5E;AAEA,SAAS,aAAa,KAAA,EAA6C;AACjE,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,KAAU,MAAM,OAAO,KAAA;AACxD,EAAA,IAAI,EAAE,KAAA,IAAS,KAAA,CAAA,IAAU,EAAE,SAAA,IAAa,QAAQ,OAAO,KAAA;AACvD,EAAA,OACE,OAAO,KAAA,CAAM,GAAA,KAAQ,QAAA,IACrB,MAAM,GAAA,CAAI,MAAA,GAAS,CAAA,IACnB,OAAO,KAAA,CAAM,OAAA,KAAY,QAAA,IACzB,KAAA,CAAM,QAAQ,MAAA,GAAS,CAAA;AAE3B;AAEA,SAAS,YAAA,CAAa,WAAmB,GAAA,EAAqB;AAC5D,EAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,SAAS,CAAA;AACnC,EAAA,IAAI,MAAA,CAAO,KAAA,CAAM,MAAM,CAAA,EAAG,OAAO,QAAA;AACjC,EAAA,OAAO,IAAA,CAAK,GAAA,CAAI,kBAAA,EAAoB,MAAA,GAAS,wBAAwB,GAAG,CAAA;AAC1E;AAGA,SAAS,QAAA,CAAS,WAAmB,GAAA,EAAsB;AACzD,EAAA,OAAO,IAAA,CAAK,KAAA,CAAM,SAAS,CAAA,GAAI,GAAA,GAAM,kBAAA;AACvC;AAGA,SAAS,YAAY,QAAA,EAA0B;AAC7C,EAAA,MAAM,IAAA,GAAO,iBAAiB,MAAA,GAAS,CAAA;AACvC,EAAA,OAAO,gBAAA,CAAiB,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,GAAA,CAAI,WAAW,CAAA,EAAG,CAAC,CAAA,EAAG,IAAI,CAAC,CAAA;AACnE;AAEA,SAAS,MAAA,GAAe;AAExB;AAOA,SAAS,SAAA,CAAU,QAA4B,KAAA,EAA8B;AAC3E,EAAA,OAAO,IAAI,OAAA,CAAc,CAAC,OAAA,KAAY;AACpC,IAAA,OAAA,CAAQ,MAAA,CAAO,QAAA,CAAS,OAAA,CAAQ,KAAK,CAAC,CAAA;AAAA,EACxC,CAAC,CAAA,CAAE,IAAA,CAAK,MAAA,EAAQ,MAAM,CAAA;AACxB;AA0CO,SAAS,mBAAA,CACd,QACA,OAAA,EACY;AACZ,EAAA,IAAI,MAAA,GAAS,KAAA;AACb,EAAA,IAAI,UAAA,GAA4C,IAAA;AAEhD,EAAA,IAAI,KAAA,GAA8C,IAAA;AAClD,EAAA,IAAI,QAAA,GAAW,KAAA;AACf,EAAA,IAAI,UAAA,GAAwC,IAAA;AAG5C,EAAA,IAAI,MAAA,GAAoC,IAAA;AACxC,EAAA,IAAI,IAAA,GAAqC,IAAA;AACzC,EAAA,IAAI,OAAA,GAAsC,IAAA;AAG1C,EAAA,IAAI,WAAA,GAAoD,IAAA;AACxD,EAAA,IAAI,QAAA,GAAW,CAAA;AAEf,EAAA,IAAI,gBAAA,GAAwC,IAAA;AAE5C,EAAA,MAAM,OAAA,uBAAc,GAAA,EAGlB;AAGF,EAAA,IAAI,gBAAmC,EAAC;AAExC,EAAA,MAAM,eAAe,MAAY;AAC/B,IAAA,IAAI,MAAA,EAAQ;AAEZ,IAAA,MAAM,QAAQ,CAAC,GAAG,IAAI,GAAA,CAAI,CAAC,GAAG,OAAA,CAAQ,MAAA,EAAQ,EAAE,GAAA,CAAI,CAAC,WAAW,MAAA,CAAO,IAAI,CAAC,CAAC,CAAA;AAC7E,IAAA,IACE,KAAA,CAAM,MAAA,KAAW,aAAA,CAAc,MAAA,IAC/B,KAAA,CAAM,KAAA,CAAM,CAAC,IAAA,EAAM,CAAA,KAAM,IAAA,KAAS,aAAA,CAAc,CAAC,CAAC,CAAA,EAClD;AACA,MAAA;AAAA,IACF;AACA,IAAA,aAAA,GAAgB,KAAA;AAChB,IAAA,OAAA,CAAQ,aAAA,GAAgB,KAAA,CAAM,MAAA,KAAW,CAAA,GAAI,EAAE,IAAA,EAAM,MAAA,EAAO,GAAI,EAAE,IAAA,EAAM,QAAA,EAAU,KAAA,EAAO,CAAA;AAAA,EAC3F,CAAA;AAGA,EAAA,MAAM,cAAc,MAAY;AAC9B,IAAA,IAAI,OAAA,CAAQ,SAAS,CAAA,EAAG;AACxB,IAAA,KAAA,MAAW,UAAU,OAAA,CAAQ,MAAA,EAAO,EAAG,YAAA,CAAa,OAAO,MAAM,CAAA;AACjE,IAAA,OAAA,CAAQ,KAAA,EAAM;AACd,IAAA,YAAA,EAAa;AAAA,EACf,CAAA;AAEA,EAAA,MAAM,YAAA,GAAe,CAAC,IAAA,KAAgC;AACpD,IAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,GAAA,CAAI,IAAA,CAAK,GAAG,CAAA;AAClC,IAAA,IAAI,KAAA,KAAU,MAAA,EAAW,YAAA,CAAa,KAAA,CAAM,MAAM,CAAA;AAClD,IAAA,IAAI,IAAA,CAAK,UAAU,SAAA,EAAW;AAC5B,MAAA,IAAI,UAAU,MAAA,EAAW;AACzB,MAAA,OAAA,CAAQ,MAAA,CAAO,KAAK,GAAG,CAAA;AACvB,MAAA,YAAA,EAAa;AACb,MAAA;AAAA,IACF;AACA,IAAA,OAAA,CAAQ,GAAA,CAAI,KAAK,GAAA,EAAK;AAAA,MACpB,MAAM,IAAA,CAAK,IAAA;AAAA,MACX,MAAA,EAAQ,WAAW,MAAM;AACvB,QAAA,OAAA,CAAQ,MAAA,CAAO,KAAK,GAAG,CAAA;AACvB,QAAA,YAAA,EAAa;AAAA,MACf,GAAG,sBAAsB;AAAA,KAC1B,CAAA;AACD,IAAA,YAAA,EAAa;AAAA,EACf,CAAA;AAEA,EAAA,MAAM,MAAA,GAAS,CAAC,MAAA,KAAyC;AACvD,IAAA,IAAI,MAAA,IAAU,WAAW,UAAA,EAAY;AACrC,IAAA,UAAA,GAAa,MAAA;AAEb,IAAA,IAAI,MAAA,KAAW,QAAQ,WAAA,EAAY;AACnC,IAAA,OAAA,CAAQ,WAAW,MAAM,CAAA;AAAA,EAC3B,CAAA;AAEA,EAAA,MAAM,aAAa,MAAY;AAC7B,IAAA,IAAI,UAAU,IAAA,EAAM;AACpB,IAAA,YAAA,CAAa,KAAK,CAAA;AAClB,IAAA,KAAA,GAAQ,IAAA;AAAA,EACV,CAAA;AAEA,EAAA,MAAM,KAAA,GAAQ,CAAC,OAAA,EAAiB,GAAA,KAAmC;AACjE,IAAA,UAAA,EAAW;AACX,IAAA,IAAI,MAAA,EAAQ;AACZ,IAAA,KAAA,GAAQ,WAAW,MAAM;AACvB,MAAA,KAAA,GAAQ,IAAA;AACR,MAAA,KAAK,GAAA,EAAI;AAAA,IACX,GAAG,OAAO,CAAA;AAAA,EACZ,CAAA;AAEA,EAAA,MAAM,eAAe,MAAY;AAC/B,IAAA,IAAI,WAAA,KAAgB,IAAA,EAAM,YAAA,CAAa,WAAW,CAAA;AAClD,IAAA,WAAA,GAAc,IAAA;AAAA,EAChB,CAAA;AAGA,EAAA,MAAM,OAAO,MAAY;AACvB,IAAA,MAAM,IAAA,GAAO,MAAA;AACb,IAAA,MAAM,MAAA,GAAS,OAAA;AACf,IAAA,MAAA,GAAS,IAAA;AACT,IAAA,IAAA,GAAO,IAAA;AACP,IAAA,OAAA,GAAU,IAAA;AACV,IAAA,YAAA,EAAa;AACb,IAAA,QAAA,GAAW,CAAA;AACX,IAAA,WAAA,EAAY;AACZ,IAAA,IAAI,SAAS,IAAA,EAAM;AACnB,IAAA,IAAI,MAAA,KAAW,IAAA,EAAM,OAAA,CAAQ,OAAA,CAAQ,IAAA,CAAK,aAAA,CAAc,MAAM,CAAC,CAAA,CAAE,IAAA,CAAK,MAAA,EAAQ,MAAM,CAAA;AACpF,IAAA,IAAA,CAAK,SAAS,UAAA,EAAW;AAAA,EAC3B,CAAA;AAEA,EAAA,MAAM,QAAQ,MAAY;AACxB,IAAA,IAAI,MAAA,EAAQ;AACZ,IAAA,MAAA,GAAS,IAAA;AACT,IAAA,UAAA,EAAW;AACX,IAAA,IAAA,EAAK;AACL,IAAA,MAAM,IAAA,GAAO,gBAAA;AACb,IAAA,gBAAA,GAAmB,IAAA;AACnB,IAAA,IAAA,IAAO;AAAA,EACT,CAAA;AAMA,EAAA,MAAM,MAAA,GAAS,CAAC,MAAA,KAAyC;AACvD,IAAA,IAAI,MAAA,EAAQ;AACZ,IAAA,MAAM,UAAU,MAAA,KAAW,UAAA;AAC3B,IAAA,MAAM,SAAA,GAAY,QAAQ,IAAA,GAAO,CAAA;AACjC,IAAA,UAAA,GAAa,MAAA;AACb,IAAA,KAAA,EAAM;AACN,IAAA,IAAI,WAAW,OAAA,CAAQ,aAAA,GAAgB,EAAE,IAAA,EAAM,QAAQ,CAAA;AACvD,IAAA,IAAI,OAAA,EAAS,OAAA,CAAQ,QAAA,GAAW,MAAM,CAAA;AAAA,EACxC,CAAA;AAEA,EAAA,MAAM,IAAA,GAAO,CAAC,SAAA,EAAmB,IAAA,KAAmC;AAGlE,IAAA,IAAI,MAAA,IAAU,MAAA,KAAW,IAAA,IAAQ,OAAA,KAAY,IAAA,EAAM;AACnD,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,OAAA,CAAQ,SAAA,EAAW,EAAE,QAAQ,EAAE,OAAA,EAAS,IAAA,EAAK,EAAG,CAAA;AACpE,IAAA,OAAA,GAAU,MAAA;AAGV,IAAA,MAAM,OAAA,GAAU,MAAe,CAAC,MAAA,IAAU,OAAA,KAAY,MAAA;AACtD,IAAA,MAAA,CACG,GAAG,WAAA,EAAa,EAAE,KAAA,EAAO,oBAAA,IAAwB,MAAM;AACtD,MAAA,IAAI,OAAA,EAAQ,EAAG,OAAA,CAAQ,QAAA,EAAS;AAAA,IAClC,CAAC,EACA,EAAA,CAAG,WAAA,EAAa,EAAE,KAAA,EAAO,kBAAA,EAAmB,EAAG,CAAC,OAAA,KAAY;AAC3D,MAAA,MAAM,IAAA,GAAO,gBAAgB,OAAO,CAAA;AACpC,MAAA,IAAI,IAAA,KAAS,IAAA,IAAQ,OAAA,EAAQ,eAAgB,IAAI,CAAA;AAAA,IACnD,CAAC,CAAA,CACA,SAAA,CAAU,CAAC,MAAA,KAAW;AACrB,MAAA,IAAI,CAAC,SAAQ,EAAG;AAChB,MAAA,IAAI,WAAW,YAAA,EAAc;AAC3B,QAAA,QAAA,GAAW,CAAA;AACX,QAAA,MAAA,CAAO,MAAM,CAAA;AAEb,QAAA,OAAA,CAAQ,QAAA,EAAS;AACjB,QAAA;AAAA,MACF;AACA,MAAA,MAAA,CAAO,UAAU,CAAA;AAKjB,MAAA,IAAI,WAAW,QAAA,EAAU;AACvB,QAAA,OAAA,GAAU,IAAA;AACV,QAAA,cAAA,EAAe;AAAA,MACjB;AAAA,IACF,CAAC,CAAA;AAAA,EACL,CAAA;AAOA,EAAA,MAAM,gBAAA,GAAmB,CAAC,KAAA,EAA8B,IAAA,KAAmC;AACzF,IAAA,KAAK,SAAA,CAAU,IAAA,EAAM,KAAA,CAAM,KAAK,CAAA,CAAE,IAAA,CAAK,MAAM,IAAA,CAAK,KAAA,CAAM,KAAA,EAAO,IAAI,CAAC,CAAA;AAAA,EACtE,CAAA;AAGA,EAAA,MAAM,SAAS,MAAY;AACzB,IAAA,MAAM,IAAA,GAAO,MAAA;AACb,IAAA,MAAM,KAAA,GAAQ,IAAA;AACd,IAAA,IAAI,UAAU,IAAA,KAAS,IAAA,IAAQ,KAAA,KAAU,IAAA,IAAQ,YAAY,IAAA,EAAM;AAEnE,IAAA,IAAI,CAAC,QAAA,CAAS,KAAA,CAAM,WAAW,IAAA,CAAK,GAAA,EAAK,CAAA,EAAG;AAC1C,MAAA,KAAK,KAAA,EAAM;AACX,MAAA;AAAA,IACF;AACA,IAAA,gBAAA,CAAiB,OAAO,IAAI,CAAA;AAAA,EAC9B,CAAA;AAEA,EAAA,MAAM,iBAAiB,MAAY;AACjC,IAAA,IAAI,MAAA,IAAU,gBAAgB,IAAA,EAAM;AACpC,IAAA,QAAA,IAAY,CAAA;AACZ,IAAA,WAAA,GAAc,WAAW,MAAM;AAC7B,MAAA,WAAA,GAAc,IAAA;AACd,MAAA,MAAA,EAAO;AAAA,IACT,CAAA,EAAG,WAAA,CAAY,QAAQ,CAAC,CAAA;AAAA,EAC1B,CAAA;AAGA,EAAA,MAAM,OAAA,GAAU,CAAC,KAAA,EAA8B,QAAA,KAAuC;AACpF,IAAA,IAAI,MAAA;AACJ,IAAA,IAAI;AACF,MAAA,MAAA,GAAS,OAAA,CAAQ,SAAS,QAAQ,CAAA;AAAA,IACpC,CAAA,CAAA,MAAQ;AAGN,MAAA,MAAA,CAAO,UAAU,CAAA;AACjB,MAAA;AAAA,IACF;AACA,IAAA,MAAA,GAAS,MAAA;AACT,IAAA,IAAA,GAAO,KAAA;AACP,IAAA,KAAA,CAAM,aAAa,KAAA,CAAM,SAAA,EAAW,KAAK,GAAA,EAAK,GAAG,KAAK,CAAA;AACtD,IAAA,gBAAA,CAAiB,OAAO,MAAM,CAAA;AAAA,EAChC,CAAA;AAGA,EAAA,MAAM,SAAA,GAAY,OAAO,KAAA,KAAgD;AAEvE,IAAA,IAAI,CAAC,aAAA,CAAc,KAAA,CAAM,KAAK,CAAA,EAAG;AAC/B,MAAA,MAAA,CAAO,UAAU,CAAA;AACjB,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,eAAe,IAAA,EAAM;AACvB,MAAA,IAAI,QAAA;AACJ,MAAA,IAAI;AACF,QAAA,MAAM,SAAA,GAAY,MAAM,MAAA,CAAO,GAAA,CAAI,SAAA,EAAU;AAC7C,QAAA,QAAA,GAAW,SAAA,CAAU,MAAA,KAAW,IAAA,GAAO,SAAA,CAAU,KAAK,QAAA,GAAW,KAAA,CAAA;AAAA,MACnE,SAAS,KAAA,EAAO;AACd,QAAA,IAAI,MAAA,EAAQ;AACZ,QAAA,IAAI,YAAA,CAAa,KAAK,CAAA,EAAG;AACvB,UAAA,MAAA,CAAO,UAAU,CAAA;AACjB,UAAA;AAAA,QACF;AACA,QAAA,MAAA,CAAO,UAAU,CAAA;AACjB,QAAA,KAAA,CAAM,UAAU,KAAK,CAAA;AACrB,QAAA;AAAA,MACF;AACA,MAAA,IAAI,MAAA,EAAQ;AAEZ,MAAA,IAAI,CAAC,YAAA,CAAa,QAAQ,CAAA,EAAG;AAC3B,QAAA,MAAA,CAAO,UAAU,CAAA;AACjB,QAAA;AAAA,MACF;AACA,MAAA,UAAA,GAAa,QAAA;AAAA,IACf;AAEA,IAAA,OAAA,CAAQ,OAAO,UAAU,CAAA;AAAA,EAC3B,CAAA;AAEA,EAAA,MAAM,QAAQ,YAA2B;AAEvC,IAAA,IAAI,QAAA,EAAU;AACd,IAAA,QAAA,GAAW,IAAA;AACX,IAAA,IAAI,KAAA;AACJ,IAAA,IAAI;AACF,MAAA,KAAA,GAAA,CAAS,MAAM,MAAA,CAAO,QAAA,CAAS,aAAA,CAAc,EAAE,CAAA,EAAG,IAAA;AAAA,IACpD,SAAS,KAAA,EAAO;AACd,MAAA,IAAI,MAAA,EAAQ;AAEZ,MAAA,IAAI,YAAA,CAAa,KAAK,CAAA,EAAG,MAAA,CAAO,SAAS,CAAA;AAAA,WACpC,KAAA,CAAM,UAAU,KAAK,CAAA;AAC1B,MAAA;AAAA,IACF,CAAA,SAAE;AACA,MAAA,QAAA,GAAW,KAAA;AAAA,IACb;AACA,IAAA,MAAM,OAAA,GAAU,MAAA;AAChB,IAAA,IAAI,MAAA,IAAU,OAAA,KAAY,IAAA,IAAQ,IAAA,KAAS,IAAA,EAAM;AACjD,IAAA,IAAI,KAAA,CAAM,KAAA,KAAU,IAAA,CAAK,KAAA,EAAO;AAI9B,MAAA,IAAA,EAAK;AACL,MAAA,MAAA,CAAO,YAAY,CAAA;AACnB,MAAA,MAAM,UAAU,KAAK,CAAA;AACrB,MAAA;AAAA,IACF;AACA,IAAA,IAAA,GAAO,KAAA;AACP,IAAA,KAAA,CAAM,aAAa,KAAA,CAAM,SAAA,EAAW,KAAK,GAAA,EAAK,GAAG,KAAK,CAAA;AACtD,IAAA,IAAI,YAAY,IAAA,EAAM;AAEpB,MAAA,KAAK,SAAA,CAAU,OAAA,EAAS,KAAA,CAAM,KAAK,CAAA;AACnC,MAAA;AAAA,IACF;AAEA,IAAA,YAAA,EAAa;AACb,IAAA,gBAAA,CAAiB,OAAO,OAAO,CAAA;AAAA,EACjC,CAAA;AAEA,EAAA,MAAM,QAAQ,YAA2B;AACvC,IAAA,IAAI,KAAA;AACJ,IAAA,IAAI;AACF,MAAA,KAAA,GAAA,CAAS,MAAM,MAAA,CAAO,QAAA,CAAS,aAAA,CAAc,EAAE,CAAA,EAAG,IAAA;AAAA,IACpD,SAAS,KAAA,EAAO;AACd,MAAA,IAAI,MAAA,EAAQ;AACZ,MAAA,IAAI,YAAA,CAAa,KAAK,CAAA,EAAG;AACvB,QAAA,MAAA,CAAO,SAAS,CAAA;AAChB,QAAA;AAAA,MACF;AACA,MAAA,MAAA,CAAO,UAAU,CAAA;AACjB,MAAA,KAAA,CAAM,UAAU,KAAK,CAAA;AACrB,MAAA;AAAA,IACF;AACA,IAAA,IAAI,MAAA,EAAQ;AACZ,IAAA,MAAM,UAAU,KAAK,CAAA;AAAA,EACvB,CAAA;AAEA,EAAA,MAAA,CAAO,YAAY,CAAA;AAGnB,EAAA,gBAAA,GACE,MAAA,CAAO,IAAA,EAAM,iBAAA,CAAkB,CAAC,KAAA,KAAU;AACxC,IAAA,IAAI,kBAAkB,GAAA,CAAI,KAAA,CAAM,IAAI,CAAA,SAAU,SAAS,CAAA;AAAA,EACzD,CAAC,CAAA,IAAK,IAAA;AACR,EAAA,KAAK,KAAA,EAAM;AACX,EAAA,OAAO,KAAA;AACT;;;ACzqBO,SAAS,mBAAmB,OAAA,EAA4C;AAC7E,EAAA,IAAI,CAAC,WAAW,OAAO,OAAA,CAAQ,aAAa,QAAA,IAAY,OAAA,CAAQ,QAAA,CAAS,MAAA,KAAW,CAAA,EAAG;AACrF,IAAA,MAAM,IAAI,MAAM,4CAA4C,CAAA;AAAA,EAC9D;AAEA,EAAA,MAAM,OAAA,GAAU,QAAQ,OAAA,IAAW,gBAAA;AACnC,EAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,IAAW,yBAAA,CAA0B,QAAQ,QAAQ,CAAA;AAO7E,EAAA,IAAI,IAAA,GAAsD,IAAA;AAE1D,EAAA,MAAM,YAAY,eAAA,CAAgB;AAAA,IAChC,OAAA;AAAA,IACA,UAAU,OAAA,CAAQ,QAAA;AAAA,IAClB,UAAA,EAAY,MAAM,IAAA,EAAM,UAAA,EAAW,IAAK,IAAA;AAAA,IACxC,OAAO,OAAA,CAAQ;AAAA,GAChB,CAAA;AAED,EAAA,MAAM,YAAA,GAAe,mBAAA,CAAoB,EAAE,SAAA,EAAW,SAAS,CAAA;AAC/D,EAAA,IAAA,GAAO,YAAA;AACP,EAAA,wBAAA,CAAyB,EAAE,SAAA,EAAW,IAAA,EAAM,YAAA,EAAc,CAAA;AAE1D,EAAA,MAAM,IAAA,GAAO,qBAAqB,SAAS,CAAA;AAE3C,EAAA,OAAO;AAAA,IACL,UAAU,OAAA,CAAQ,QAAA;AAAA,IAClB,IAAA,EAAM;AAAA,MACJ,QAAQ,YAAA,CAAa,MAAA;AAAA,MACrB,QAAQ,YAAA,CAAa,MAAA;AAAA,MACrB,SAAS,YAAA,CAAa,OAAA;AAAA,MACtB,YAAY,YAAA,CAAa,UAAA;AAAA,MACzB,mBAAmB,YAAA,CAAa,iBAAA;AAAA,MAChC,sBAAsB,YAAA,CAAa,oBAAA;AAAA,MACnC,sBAAsB,YAAA,CAAa,oBAAA;AAAA,MACnC,cAAc,YAAA,CAAa,YAAA;AAAA,MAC3B,oBAAoB,YAAA,CAAa,kBAAA;AAAA,MACjC,YAAY,YAAA,CAAa,UAAA;AAAA,MACzB,kBAAkB,YAAA,CAAa,gBAAA;AAAA,MAC/B,iBAAiB,YAAA,CAAa,eAAA;AAAA,MAC9B,gBAAgB,YAAA,CAAa,cAAA;AAAA,MAC7B,mBAAmB,YAAA,CAAa,iBAAA;AAAA,MAChC,YAAY,YAAA,CAAa,UAAA;AAAA,MACzB,iBAAiB,YAAA,CAAa,eAAA;AAAA,MAC9B,kBAAkB,YAAA,CAAa,gBAAA;AAAA,MAC/B,uBAAuB,YAAA,CAAa,qBAAA;AAAA,MACpC,gBAAgB,YAAA,CAAa,cAAA;AAAA,MAC7B,aAAa,YAAA,CAAa,WAAA;AAAA,MAC1B,oBAAoB,YAAA,CAAa,kBAAA;AAAA,MACjC,cAAc,YAAA,CAAa,YAAA;AAAA,MAC3B,eAAe,YAAA,CAAa,aAAA;AAAA,MAC5B,qBAAqB,YAAA,CAAa,mBAAA;AAAA,MAClC,cAAc,YAAA,CAAa;AAAA,KAC7B;AAAA,IACA,GAAG,IAAA;AAAA,IACH,GAAA,EAAK,mBAAmB,SAAS;AAAA,GACnC;AACF","file":"index.js","sourcesContent":["// A guard shared by the SDK's readers of loosely typed server bodies\n// (`refusal.ts`, `retry-after.ts`). Internal: not exported from the package.\n\n/** A plain object (not null, not an array) whose keys can be read. */\nexport function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n","import { isRecord } from \"./is-record\";\n\n/**\n * Machine-readable reasons the server attaches to a refusal, as\n * `refusal.reason` on a `FytkitError`. Branch on these rather than on message\n * text. The union is open: the server may add reasons without a breaking SDK\n * release, so always keep a fallback branch.\n *\n * Check-ins (`checkIns.*`):\n * - `\"entitlement_required\"` (`forbidden`, 403): the member's plan lacks\n * `coaching_checkins`. On `checkIns.current` the data also says whether the\n * member is scheduled and has sent check-ins before (`member`).\n * - `\"already_submitted\"` (`conflict`, 409): this period already has a\n * check-in, `checkinId`; open or edit it instead.\n * - `\"reviewed\"` (`conflict`, 409): the coach reviewed it, so it is locked.\n * - `\"questions_changed\"` (`conflict`, 409): the coach changed the questions;\n * read `checkIns.current` again.\n * - `\"not_scheduled\"`, `\"not_open\"` (with `opensOn`), `\"not_current_period\"`\n * (with `currentPeriodStart`), `\"invalid_photos\"` (with `uploadIds`)\n * (`server`, 400).\n * - `\"invalid_answers\"` (`validation`, 400): `issues` by question.\n *\n * Messages (`messages.*`):\n * - `\"entitlement_required\"` (`forbidden`, 403): the member may not send now;\n * `eligibility` says whether they need an upgrade (`\"upgrade_required\"`) or\n * should wait for a coach to reply (`\"wait_for_coach\"`).\n * - `\"invalid_attachments\"` (`server`, 400): `uploadIds` that can't be sent.\n * - `\"per_minute_limit\"`, `\"daily_limit\"` (`rate_limited`, 429): with `limit`\n * and `retryAt`, when sending is allowed again.\n *\n * Photo uploads (`checkIns.requestPhotoUpload`,\n * `messages.requestAttachmentUpload`):\n * - `\"too_many_pending_uploads\"` (`rate_limited`, 429): with `limit` and\n * `retryAt`.\n *\n * Subscription checkout (`subscription.createCheckout`):\n * - `\"already_subscribed\"`, `\"payment_issue\"` (`conflict`, 409).\n * - `\"max_clients\"` (`forbidden`, 403): the studio is full and takes no new\n * paying members. The server names this one by its quota (`quotaCode`)\n * and sends nothing else with it; the SDK reports it as a reason like the\n * rest, with no data.\n *\n * Billing portal (`subscription.createPortal`):\n * - `\"no_billing_account\"` (`not_found`, 404): the member never started a\n * paid plan here; hide \"Manage billing\".\n * - `\"no_connected_account\"`, `\"portal_unavailable\"` (`server`, 400): the\n * studio's billing isn't set up; send the member to their coach.\n *\n * Habits (`habits.*`):\n * - `\"entitlement_required\"` (`forbidden`, 403): the member's plan lacks\n * `coaching_habits`. On `habits.today` the data also says whether their\n * coach gave them a habit that hasn't ended (`member.assigned`): show an\n * upgrade prompt when it is true.\n *\n * Weigh-ins and measurements (`bodyMetrics.update`, `bodyMetrics.delete`,\n * `bodyMeasurements.delete`):\n * - `\"staff_entered\"` (`conflict`, 409): their coach entered it; it is the\n * coach's to change or remove.\n * - `\"checkin_sourced\"` (`conflict`, 409, `bodyMetrics.update`): a check-in\n * recorded it; change it by editing the check-in.\n *\n * Progress photos (`progressPhotos.*`):\n * - `\"consent_required\"` (`conflict`, 409): progress photos are off.\n * - `\"photo_limit_reached\"` (`conflict`, 409): the timeline holds `limit`\n * photos.\n * - `\"invalid_uploads\"` (`server`, 400): `uploadIds` that can't be added.\n * - `\"poses_differ\"` (`server`, 400): compare two photos of the same pose.\n *\n * Health (`health.submitIntake`, `health.reportInjury`):\n * - `\"consent_required\"` (`conflict`, 409, `reportInjury`): no consent on\n * file at `currentVersion`; send the report again with consent.\n * - `\"consent_outdated\"` (`conflict`, 409): the consent wording changed to\n * `currentVersion`; show it again.\n * - `\"too_many_pending\"` (`conflict`, 409, `reportInjury`): `limit` reports\n * already wait for the coach.\n * - `\"questions_changed\"` (`conflict`, 409), `\"invalid_answers\"`\n * (`validation`, 400) on `submitIntake`, as on check-ins.\n *\n * Join codes (`resolveJoinCode`):\n * - `\"join_code_inactive\"` (`not_found`, 404): no studio has the code, or\n * the studio replaced it; ask the member to get a new one from their coach.\n */\nexport type FytkitRefusalReason =\n | \"entitlement_required\"\n | \"already_submitted\"\n | \"reviewed\"\n | \"questions_changed\"\n | \"not_scheduled\"\n | \"not_open\"\n | \"not_current_period\"\n | \"invalid_answers\"\n | \"invalid_photos\"\n | \"invalid_attachments\"\n | \"per_minute_limit\"\n | \"daily_limit\"\n | \"too_many_pending_uploads\"\n | \"already_subscribed\"\n | \"payment_issue\"\n | \"max_clients\"\n | \"no_billing_account\"\n | \"no_connected_account\"\n | \"portal_unavailable\"\n | \"staff_entered\"\n | \"checkin_sourced\"\n | \"consent_required\"\n | \"consent_outdated\"\n | \"too_many_pending\"\n | \"photo_limit_reached\"\n | \"invalid_uploads\"\n | \"poses_differ\"\n | \"join_code_inactive\"\n // Keeps the union open (and autocompletion for the known reasons).\n | (string & {});\n\n/**\n * The server's structured refusal on a `FytkitError`: why it said no and\n * what it sent with that. Present only when the server named a reason.\n */\nexport interface FytkitRefusal {\n /** The machine-readable reason (see `FytkitRefusalReason`). */\n readonly reason: FytkitRefusalReason;\n /** The rest of the error's data, without `reason`. */\n readonly data: Readonly<Record<string, unknown>>;\n}\n\n/** When a limited action is allowed again, as the 429 refusals send it. */\nexport interface FytkitRefusalRetryAt {\n /** The limit that was reached. */\n readonly limit: number;\n /** ISO 8601 instant when the limit lifts. */\n readonly retryAt: string;\n /** Milliseconds from the refusal until `retryAt`; also the error's `retryAfter`. */\n readonly retryAfterMs?: number;\n}\n\n/**\n * What each known reason's `refusal.data` holds, for `refusalData`. The\n * server documents these shapes; the SDK does not check them at runtime, as\n * it trusts every other response shape. An empty record is a refusal that\n * sends nothing beyond its reason.\n */\nexport interface FytkitRefusalDataByReason {\n /** A coaching feature the member's plan lacks. */\n readonly entitlement_required:\n | {\n readonly entitlement: \"coaching_checkins\";\n /** On `checkIns.current` only: whether to offer check-ins at all. */\n readonly member?: { readonly scheduled: boolean; readonly hasCheckins: boolean };\n }\n | {\n readonly entitlement: \"coaching_messages\";\n readonly eligibility: \"upgrade_required\" | \"wait_for_coach\";\n }\n | {\n readonly entitlement: \"coaching_habits\";\n /** On `habits.today` only: whether their coach gave them a habit that hasn't ended. */\n readonly member?: { readonly assigned: boolean };\n };\n /** The check-in already sent for this period. */\n readonly already_submitted: { readonly checkinId: string; readonly submittedAt: string };\n /** The check-in the coach reviewed, so it is locked. */\n readonly reviewed: { readonly checkinId: string };\n /** The coach changed the questions; read `checkIns.current` again. */\n readonly questions_changed: Readonly<Record<string, never>>;\n /** The member has no check-in schedule. */\n readonly not_scheduled: Readonly<Record<string, never>>;\n /** `opensOn` is a calendar date, `YYYY-MM-DD`. */\n readonly not_open: { readonly opensOn: string };\n /** `currentPeriodStart` is a calendar date, `YYYY-MM-DD`. */\n readonly not_current_period: { readonly currentPeriodStart: string };\n /** Answers that don't fit their questions, by question (null: the whole check-in). */\n readonly invalid_answers: {\n readonly issues: readonly { readonly questionId: string | null; readonly message: string }[];\n };\n /** Photos that can't be added to the check-in; upload them again. */\n readonly invalid_photos: { readonly uploadIds: readonly string[] };\n /** Photos that can't be sent with the message; upload them again. */\n readonly invalid_attachments: { readonly uploadIds: readonly string[] };\n /** 20 messages a minute. */\n readonly per_minute_limit: FytkitRefusalRetryAt;\n /** 300 messages a day, until midnight in the studio's time zone. */\n readonly daily_limit: FytkitRefusalRetryAt;\n /** 20 photos requested but not yet sent. */\n readonly too_many_pending_uploads: FytkitRefusalRetryAt;\n /** The member already has an active subscription. */\n readonly already_subscribed: Readonly<Record<string, never>>;\n /** The member's last payment failed and the old subscription is still live. */\n readonly payment_issue: Readonly<Record<string, never>>;\n /** The studio is full. Nothing beyond the reason: the refusal carries no figures. */\n readonly max_clients: Readonly<Record<string, never>>;\n /** The member has no billing account at this studio. */\n readonly no_billing_account: Readonly<Record<string, never>>;\n /** The studio has no connected payments account. */\n readonly no_connected_account: Readonly<Record<string, never>>;\n /** The studio hasn't set up its billing portal. */\n readonly portal_unavailable: Readonly<Record<string, never>>;\n /** Their coach entered this weigh-in or measurement. */\n readonly staff_entered: Readonly<Record<string, never>>;\n /** A check-in recorded this weigh-in; edit the check-in instead. */\n readonly checkin_sourced: Readonly<Record<string, never>>;\n /**\n * Health: no consent on file at `currentVersion` (`health.reportInjury`).\n * Progress photos: they are off, and nothing else is sent.\n */\n readonly consent_required: { readonly currentVersion?: string };\n /** The health-data consent wording changed; `currentVersion` is the current one. */\n readonly consent_outdated: { readonly currentVersion: string };\n /** `limit` injury reports already wait for the coach. */\n readonly too_many_pending: { readonly limit: number };\n /** The progress-photo timeline holds `limit` photos. */\n readonly photo_limit_reached: { readonly limit: number };\n /** Photo uploads that can't be added; upload them again. */\n readonly invalid_uploads: { readonly uploadIds: readonly string[] };\n /** Two photos of different poses can't be compared. */\n readonly poses_differ: Readonly<Record<string, never>>;\n /** No studio has the join code, or the studio replaced it. */\n readonly join_code_inactive: Readonly<Record<string, never>>;\n}\n\n/**\n * The data of `error`'s refusal when its reason is `reason`, typed for that\n * reason; null when `error` is not a `FytkitError`, carries no refusal, or\n * was refused for another reason.\n *\n * ```ts\n * catch (err) {\n * const sent = refusalData(err, \"already_submitted\");\n * if (sent) return openCheckIn(sent.checkinId);\n * }\n * ```\n */\nexport function refusalData<R extends keyof FytkitRefusalDataByReason>(\n error: unknown,\n reason: R,\n): FytkitRefusalDataByReason[R] | null {\n if (!isRecord(error) || typeof error.kind !== \"string\") return null;\n const refusal = error.refusal;\n if (!isRecord(refusal) || refusal.reason !== reason || !isRecord(refusal.data)) return null;\n // The server's documented shape for this reason (see FytkitRefusalDataByReason).\n return refusal.data as FytkitRefusalDataByReason[R];\n}\n\n/**\n * The member cap is the one refusal the server names by its quota\n * (`quotaCode`) rather than a `reason`: a studio that takes no new members\n * refuses a member's checkout with it. The refusal carries no figures, only\n * `quotaCode`, and the SDK passes the reason alone: should anything else ever\n * arrive beside it, it is dropped rather than handed to the app.\n */\nconst MEMBER_CAP_QUOTA = \"max_clients\";\n\n/**\n * The refusal in an error's `data`, as a spread-able object so a kind only\n * gains the key when the server named a reason (existing `toEqual` shapes\n * stay intact): the reason, and the rest of the data without it.\n */\nexport function pickRefusal(data: unknown): { readonly refusal?: FytkitRefusal } {\n if (!isRecord(data)) return {};\n const { reason, ...rest } = data;\n if (typeof reason === \"string\" && reason.length > 0) return { refusal: { reason, data: rest } };\n if (data.quotaCode === MEMBER_CAP_QUOTA)\n return { refusal: { reason: MEMBER_CAP_QUOTA, data: {} } };\n return {};\n}\n","import { ORPCError } from \"@orpc/client\";\nimport { pickRefusal } from \"./refusal\";\nimport type { FytkitError } from \"./types\";\n\ninterface MaybeIssuesData {\n issues?: readonly unknown[];\n}\ninterface MaybeRetryAfterData {\n retryAfterMs?: number;\n}\n\nfunction pickIssues(data: unknown): readonly unknown[] | null {\n if (!data || typeof data !== \"object\") return null;\n const issues = (data as MaybeIssuesData).issues;\n return Array.isArray(issues) ? issues : null;\n}\n\nfunction pickRetryAfter(data: unknown): number {\n if (!data || typeof data !== \"object\") return 0;\n const raw = (data as MaybeRetryAfterData).retryAfterMs;\n return typeof raw === \"number\" && raw >= 0 ? raw : 0;\n}\n\n/**\n * A non-empty string field of the error's `data`, when the server sent one:\n * `code` (the machine-readable reason, e.g. \"invalid_password\") or\n * `enrollmentId` (the existing enrolment on \"Already enrolled\"). Returned as\n * a spread-able object so kinds only gain the key when there is a value —\n * existing `toEqual` shapes stay intact.\n */\nfunction pickStringField<K extends \"code\" | \"enrollmentId\">(\n data: unknown,\n key: K,\n): { [P in K]?: string } {\n if (!data || typeof data !== \"object\") return {};\n const raw = (data as Partial<Record<K, unknown>>)[key];\n return typeof raw === \"string\" && raw.length > 0 ? ({ [key]: raw } as { [P in K]?: string }) : {};\n}\n\n/**\n * Map any error thrown by `@orpc/client` (or the underlying fetch) into the\n * SDK's `FytkitError` discriminated union. The SDK never re-throws raw orpc\n * or network errors at the public surface.\n *\n * A refusal the server named (`data.reason`, see `FytkitRefusal`) rides along\n * as `refusal` on the kinds that can carry one, beside the fields each kind\n * already had: `forbidden.reason` stays the message text.\n */\nexport function normalizeError(err: unknown): FytkitError {\n // Network failures from fetch surface as TypeError.\n if (err instanceof TypeError) return { kind: \"network\" };\n\n if (err instanceof ORPCError) {\n const code = err.code;\n const status = err.status;\n const data: unknown = err.data;\n const message = err.message;\n\n switch (code) {\n case \"UNAUTHORIZED\":\n // Carry the server's reason (e.g. \"Invalid credentials\" on a failed\n // sign-in) so the consumer can show it instead of a generic\n // 'session expired' copy. The synthetic no-session case in auth.ts\n // is built directly and stays message-less.\n return { kind: \"auth_required\", message };\n case \"FORBIDDEN\":\n return {\n kind: \"forbidden\",\n reason: message || \"Forbidden\",\n ...pickStringField(data, \"code\"),\n ...pickRefusal(data),\n };\n case \"PAYMENT_REQUIRED\":\n return { kind: \"payment_required\", message: message || \"Payment Required\" };\n case \"CONFLICT\":\n return {\n kind: \"conflict\",\n message: message || \"Conflict\",\n ...pickStringField(data, \"enrollmentId\"),\n ...pickRefusal(data),\n };\n case \"NOT_FOUND\":\n return { kind: \"not_found\", ...pickRefusal(data) };\n case \"TOO_MANY_REQUESTS\":\n return { kind: \"rate_limited\", retryAfter: pickRetryAfter(data), ...pickRefusal(data) };\n case \"BAD_REQUEST\":\n case \"UNPROCESSABLE_CONTENT\": {\n const issues = pickIssues(data);\n if (issues) return { kind: \"validation\", issues, ...pickRefusal(data) };\n // No structured issues — still carry the server's explanation (and\n // its code, e.g. \"invalid_password\") so it isn't dropped into a bare\n // status code.\n return {\n kind: \"server\",\n status,\n message,\n ...pickStringField(data, \"code\"),\n ...pickRefusal(data),\n };\n }\n default:\n return {\n kind: \"server\",\n status,\n message,\n ...pickStringField(data, \"code\"),\n ...pickRefusal(data),\n };\n }\n }\n\n return { kind: \"server\", status: 0 };\n}\n","// `app.bootstrap` (#535): Fytkit Coach's one launch read. Everything the app\n// paints and gates at launch, for the studio the client id names: its name\n// and logo, the app theme, feature overrides, the minimum app version, the\n// legal links, the Realtime settings and the payment policy.\n//\n// The payload is a pure function of the studio's settings and carries a\n// strong ETag. A call with the ETag the app stored last time is answered 304\n// with no body when nothing changed. The typed oRPC client resolves a 304's\n// empty body to `undefined` (its type says it never does), so this wraps the\n// raw procedure in a discriminated union instead of handing that through.\n\nimport type { PublicApi } from \"@fytkit/types\";\nimport { normalizeError } from \"./error\";\nimport type { Transport } from \"./transport\";\nimport type { FytkitError } from \"./types\";\n\n/**\n * Everything the app reads at launch for the studio (`app.bootstrap`'s\n * `data`): `studio` (display name and logo URL), `theme` (the effective\n * template and theme with its palette, derived variants and the oldest app\n * release that can paint it), `themeMinAppVersions`, `home.layout`,\n * `featureOverrides`, `minAppVersion`, `links` (terms, privacy, help),\n * `realtime` (the public Supabase Realtime settings) and `paymentPolicy` (the\n * storefronts where the app may link out to subscribe; read it with\n * `resolvePaymentMode`).\n */\nexport type AppBootstrapData = Awaited<ReturnType<PublicApi[\"app\"][\"bootstrap\"]>>[\"data\"];\n\n/** Options for `app.bootstrap`. */\nexport interface AppBootstrapOptions {\n /**\n * The `etag` of the last bootstrap the app kept. When the studio's\n * settings are unchanged the server answers 304 and the call resolves to\n * `{ status: \"unchanged\" }`, so keep using the data stored with it. Omit it\n * (or pass `null`) on a first launch, or when no data is stored.\n */\n readonly etag?: string | null;\n}\n\n/**\n * What `app.bootstrap` resolves to:\n * - `ok`: the current settings, and the `etag` to store beside them and send\n * next time;\n * - `unchanged`: the `etag` you sent is still current (a 304 with no body):\n * the data you stored with it is up to date.\n */\nexport type AppBootstrapResult =\n | { readonly status: \"ok\"; readonly data: AppBootstrapData; readonly etag: string }\n | { readonly status: \"unchanged\"; readonly etag: string };\n\n/** `client.app`: the app's launch read. */\nexport interface AppNamespace {\n /**\n * Reads the studio's app launch settings. Needs the client id alone; a\n * signed-in member's session changes nothing in the payload. Pass the\n * `etag` of the data you stored last time to revalidate it: an unchanged\n * studio answers `{ status: \"unchanged\" }` without a body.\n *\n * Rejects with a `FytkitError`, as every SDK method does (e.g.\n * `{ kind: \"not_found\" }` for a studio that no longer exists,\n * `{ kind: \"network\" }` offline): keep painting the stored data then.\n *\n * ```ts\n * const stored = await loadBootstrap(); // { data, etag } | null\n * const result = await fytkit.app.bootstrap({ etag: stored?.etag });\n * const data = result.status === \"ok\" ? result.data : stored.data;\n * if (result.status === \"ok\") await saveBootstrap(result);\n * ```\n */\n readonly bootstrap: (options?: AppBootstrapOptions) => Promise<AppBootstrapResult>;\n}\n\n/** A bodiless answer (a 304) to a call that sent no `If-None-Match`. */\nconst UNEXPECTED_NOT_MODIFIED: FytkitError = { kind: \"server\", status: 304 };\n\nexport function createAppNamespace(transport: Transport): AppNamespace {\n const bootstrap: AppNamespace[\"bootstrap\"] = async (options = {}) => {\n const etag = options.etag ?? null;\n // A 304 resolves to `undefined` at runtime, whatever the client's type says.\n let output: Awaited<ReturnType<PublicApi[\"app\"][\"bootstrap\"]>> | undefined;\n try {\n output = await transport.client.app.bootstrap(undefined, {\n context: etag === null ? {} : { ifNoneMatch: etag },\n });\n } catch (error) {\n throw normalizeError(error);\n }\n if (output === undefined || output === null) {\n if (etag === null) throw UNEXPECTED_NOT_MODIFIED;\n return { status: \"unchanged\", etag };\n }\n return { status: \"ok\", data: output.data, etag: output.meta.etag };\n };\n return { bootstrap };\n}\n","export interface EventBus<T> {\n readonly on: (listener: (value: T) => void) => () => void;\n readonly emit: (value: T) => void;\n}\n\nexport function createEventBus<T>(): EventBus<T> {\n const listeners = new Set<(value: T) => void>();\n return {\n on: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n emit: (value) => {\n for (const listener of listeners) {\n try {\n listener(value);\n } catch (err) {\n // A buggy listener must not break siblings or the calling flow.\n console.error(\"[fytkit-sdk] event listener threw\", err);\n }\n }\n },\n };\n}\n","import { normalizeError } from \"./error\";\nimport { createEventBus } from \"./events\";\nimport type { Transport } from \"./transport\";\nimport type {\n AuthState,\n FytkitError,\n ResendVerificationResult,\n SecurityStatus,\n Session,\n SessionInfo,\n SignInResult,\n TokenStorage,\n TotpEnrollment,\n TwoFactorChallenge,\n TwoFactorToken,\n} from \"./types\";\n\nexport interface AuthNamespaceOptions {\n readonly transport: Transport;\n readonly storage: TokenStorage;\n /** Called after the SDK signs out for any reason (manual, refresh failure). */\n readonly onSignedOut?: () => void;\n}\n\nexport interface AuthNamespace {\n readonly signUp: (input: { email: string; password: string; name: string }) => Promise<Session>;\n readonly signIn: (input: { email: string; password: string }) => Promise<SignInResult>;\n readonly signOut: () => Promise<void>;\n readonly getSession: () => Session | null;\n readonly onAuthStateChange: (listener: (state: AuthState) => void) => () => void;\n readonly requestPasswordReset: (input: { email: string }) => Promise<void>;\n readonly confirmPasswordReset: (input: { token: string; password: string }) => Promise<void>;\n readonly confirmEmail: (input: { token: string }) => Promise<void>;\n /**\n * Mail a new verification link to the signed-in member's own address. A\n * bearer call: a 401 refreshes or signs out as usual.\n */\n readonly resendVerification: () => Promise<ResendVerificationResult>;\n readonly verifyTotp: (input: { challenge: TwoFactorChallenge; code: string }) => Promise<Session>;\n readonly verifyBackupCode: (input: {\n challenge: TwoFactorChallenge;\n code: string;\n }) => Promise<Session>;\n readonly requestEmailOtp: (input: { challenge: TwoFactorChallenge }) => Promise<void>;\n readonly verifyEmailOtp: (input: {\n challenge: TwoFactorChallenge;\n code: string;\n }) => Promise<Session>;\n readonly getSecurityStatus: () => Promise<SecurityStatus>;\n readonly enableTotp: (input: { password: string }) => Promise<TotpEnrollment>;\n readonly verifyTotpSetup: (input: { code: string }) => Promise<void>;\n readonly disableTwoFactor: (input: { password: string }) => Promise<void>;\n readonly regenerateBackupCodes: (input: { password: string }) => Promise<readonly string[]>;\n readonly changePassword: (input: {\n currentPassword: string;\n newPassword: string;\n revokeOtherSessions?: boolean;\n }) => Promise<void>;\n readonly changeEmail: (input: { newEmail: string }) => Promise<void>;\n /**\n * Confirm an email change with the token from the confirmation link, for an\n * app that opens that link itself (Fytkit Coach's app link). No session\n * needed: the emailed token is the proof.\n */\n readonly confirmEmailChange: (input: { token: string }) => Promise<void>;\n readonly listSessions: () => Promise<readonly SessionInfo[]>;\n readonly revokeSession: (input: { revokeId: string }) => Promise<void>;\n readonly revokeOtherSessions: () => Promise<void>;\n readonly closeAccount: (input: { confirmEmail: string; password: string }) => Promise<void>;\n /** Internal: force a refresh now. Wired by transport's 401 retry path. */\n readonly _refresh: () => Promise<Session>;\n /** Internal: clear cached session + emit signed_out. */\n readonly _signOutLocal: () => void;\n}\n\n/**\n * Schedule a proactive refresh this many ms before `expiresAt`. The token\n * is a server session (7 days, extended on use), and a 60-second lead time\n * refreshes it transparently in long-running tabs without refreshing\n * needlessly.\n */\nconst PROACTIVE_REFRESH_LEAD_MS = 60_000;\n\n/**\n * When a proactive refresh fails for a reason other than the server rejecting\n * the token (offline, 5xx, rate limit), the session is kept and the timer\n * retries with exponential backoff: 5 s, 10 s, 20 s … capped at 5 min, for\n * at most this many attempts. After that the SDK stops polling; the next\n * request's reactive 401 path takes over if the token has actually lapsed.\n */\nconst REFRESH_RETRY_BASE_MS = 5_000;\nconst REFRESH_RETRY_MAX_MS = 5 * 60_000;\nconst REFRESH_MAX_RETRIES = 6;\n\n/**\n * Only an explicit server rejection of the token (401/403) ends the session.\n * Network failures, 5xx and rate limits say nothing about the token, so\n * signing the member out on them would be wrong — and on a phone that drops\n * signal it signs members out constantly.\n */\nfunction isSessionRejected(err: FytkitError): boolean {\n return err.kind === \"auth_required\" || err.kind === \"forbidden\";\n}\n\nfunction refreshRetryDelay(attempt: number, err: FytkitError): number {\n const backoff = Math.min(REFRESH_RETRY_MAX_MS, REFRESH_RETRY_BASE_MS * 2 ** attempt);\n // Honour the server's rate-limit window when it is longer than our backoff.\n return err.kind === \"rate_limited\" ? Math.max(backoff, err.retryAfter) : backoff;\n}\n\n/**\n * The auth namespace owns the cached session. It is the single writer to\n * `storage`; everything else reads via `getSession`. Calls that mutate\n * the session also emit on the event bus and (re)schedule the proactive\n * refresh timer against the new expiry.\n */\nexport function createAuthNamespace(opts: AuthNamespaceOptions): AuthNamespace {\n const bus = createEventBus<AuthState>();\n let cached: Session | null = null;\n let timer: ReturnType<typeof setTimeout> | null = null;\n // Forward declaration so scheduleRefresh can call the same _refresh body\n // the public surface exposes. Assigned at the bottom of the factory.\n let refreshFn: () => Promise<Session> = () => {\n throw new Error(\"refreshFn called before initialization\");\n };\n\n function clearTimer(): void {\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n }\n\n // `setTimeout` clamps any delay > 2^31-1 ms (~24.8 days) to 1 ms and\n // prints a Node deprecation warning. Sessions with an exotic far-future\n // expiry (test fixtures, mis-configured server) would otherwise fire the\n // timer instantly; cap the delay to the safe upper bound so the timer\n // simply re-arms on its next firing.\n const MAX_TIMEOUT_MS = 2_147_483_647;\n\n // `attempt` counts consecutive failed proactive refreshes; 0 for the\n // expiry-driven firing, 1+ for backoff retries.\n function armRefreshTimer(delay: number, attempt: number): void {\n clearTimer();\n timer = setTimeout(() => {\n timer = null;\n void refreshFn().catch((err: FytkitError) => {\n // A rejected token already cleared the session inside _refresh.\n // Otherwise keep it and retry — unless something re-armed the timer\n // meanwhile (a sign-in, a rotated token) or we are out of attempts.\n if (cached === null || timer !== null || attempt >= REFRESH_MAX_RETRIES) return;\n armRefreshTimer(refreshRetryDelay(attempt, err), attempt + 1);\n });\n }, delay);\n }\n\n function scheduleRefresh(session: Session): void {\n clearTimer();\n const expiresMs = new Date(session.expiresAt).getTime();\n if (Number.isNaN(expiresMs)) return; // bad date — bail; reactive 401 path will handle it\n const rawDelay = expiresMs - Date.now() - PROACTIVE_REFRESH_LEAD_MS;\n armRefreshTimer(Math.min(MAX_TIMEOUT_MS, Math.max(0, rawDelay)), 0);\n }\n\n function applySession(session: Session, event: \"signed_in\" | \"session_refreshed\"): Session {\n cached = session;\n void opts.storage.set(session);\n bus.emit({ type: event, session });\n scheduleRefresh(session);\n return session;\n }\n\n function clearSession(emit = true): void {\n cached = null;\n clearTimer();\n void opts.storage.remove();\n if (emit) bus.emit({ type: \"signed_out\" });\n opts.onSignedOut?.();\n }\n\n async function call<T>(thunk: () => Promise<T>): Promise<T> {\n try {\n return await thunk();\n } catch (err) {\n throw normalizeError(err);\n }\n }\n\n // Best-effort hydrate at construction. If the storage already holds a\n // session (warm reload, second tab), restore it and arm the timer.\n void Promise.resolve(opts.storage.get()).then((s) => {\n if (s) {\n cached = s;\n scheduleRefresh(s);\n }\n });\n\n const signUp: AuthNamespace[\"signUp\"] = async (input) => {\n const result = await call(() => opts.transport.client.auth.signUp(input));\n const { user, session } = result.data;\n return applySession(\n { accessToken: session.accessToken, expiresAt: session.expiresAt, user },\n \"signed_in\",\n );\n };\n\n const signIn: AuthNamespace[\"signIn\"] = async (input) => {\n const result = await call(() => opts.transport.client.auth.signIn(input));\n if (result.data.kind === \"two_factor_required\") {\n // Mint the branded token exactly once at the network boundary.\n const token = result.data.twoFactorToken as TwoFactorToken;\n return {\n kind: \"two_factor_required\",\n challenge: { token, availableMethods: result.data.availableMethods },\n };\n }\n if (result.data.kind === \"signed_in\") {\n const { user, session } = result.data;\n const cached = applySession(\n { accessToken: session.accessToken, expiresAt: session.expiresAt, user },\n \"signed_in\",\n );\n return { kind: \"signed_in\", session: cached };\n }\n // Exhaustiveness guard — a future server-side variant must not silently fall through.\n const _exhaustive: never = result.data;\n throw _exhaustive;\n };\n\n const signOut: AuthNamespace[\"signOut\"] = async () => {\n const current = cached;\n if (!current) return;\n try {\n await opts.transport.client.auth.signOut({ token: current.accessToken });\n } catch (err) {\n // Even if the server rejects (e.g. token already revoked), clear\n // the local cache so the user is signed out client-side.\n void err;\n }\n clearSession();\n };\n\n const getSession: AuthNamespace[\"getSession\"] = () => cached;\n const onAuthStateChange: AuthNamespace[\"onAuthStateChange\"] = (listener) => bus.on(listener);\n\n const requestPasswordReset: AuthNamespace[\"requestPasswordReset\"] = async (input) => {\n await call(() => opts.transport.client.auth.requestPasswordReset(input));\n };\n const confirmPasswordReset: AuthNamespace[\"confirmPasswordReset\"] = async (input) => {\n await call(() => opts.transport.client.auth.confirmPasswordReset(input));\n };\n const confirmEmail: AuthNamespace[\"confirmEmail\"] = async (input) => {\n await call(() => opts.transport.client.auth.confirmEmail(input));\n };\n\n const resendVerification: AuthNamespace[\"resendVerification\"] = async () => {\n const result = await call(() => opts.transport.client.auth.resendVerification({}));\n return result.data;\n };\n\n // --- Two-factor sign-in completion -----------------------------------------\n //\n // Each consumes the challenge minted by `signIn` and, on success, caches the\n // issued session exactly like the signed_in branch of `signIn`.\n\n const verifyTotp: AuthNamespace[\"verifyTotp\"] = async (input) => {\n const result = await call(() =>\n opts.transport.client.auth.verifyTotp({ token: input.challenge.token, code: input.code }),\n );\n const { user, session } = result.data;\n return applySession(\n { accessToken: session.accessToken, expiresAt: session.expiresAt, user },\n \"signed_in\",\n );\n };\n\n const verifyBackupCode: AuthNamespace[\"verifyBackupCode\"] = async (input) => {\n const result = await call(() =>\n opts.transport.client.auth.verifyBackupCode({\n token: input.challenge.token,\n code: input.code,\n }),\n );\n const { user, session } = result.data;\n return applySession(\n { accessToken: session.accessToken, expiresAt: session.expiresAt, user },\n \"signed_in\",\n );\n };\n\n const requestEmailOtp: AuthNamespace[\"requestEmailOtp\"] = async (input) => {\n await call(() => opts.transport.client.auth.requestEmailOtp({ token: input.challenge.token }));\n };\n\n const verifyEmailOtp: AuthNamespace[\"verifyEmailOtp\"] = async (input) => {\n const result = await call(() =>\n opts.transport.client.auth.verifyEmailOtp({ token: input.challenge.token, code: input.code }),\n );\n const { user, session } = result.data;\n return applySession(\n { accessToken: session.accessToken, expiresAt: session.expiresAt, user },\n \"signed_in\",\n );\n };\n\n /**\n * The server rotated the calling session and returned the replacement token\n * (TOTP-setup verification and password change with revokeOtherSessions do\n * this). Swap the cached token immediately — the old bearer is already dead,\n * so any request between now and the swap would 401. `expiresAt` is carried\n * over from the old session; it is at worst *earlier* than the new session's\n * real expiry, so the proactive refresh timer fires early and corrects it.\n */\n function adoptRotatedToken(accessToken: string): void {\n const current = cached;\n if (!current || !accessToken || accessToken === current.accessToken) return;\n applySession({ ...current, accessToken }, \"session_refreshed\");\n }\n\n const getSecurityStatus: AuthNamespace[\"getSecurityStatus\"] = async () => {\n const result = await call(() => opts.transport.client.auth.getSecurityStatus());\n return result.data;\n };\n\n const enableTotp: AuthNamespace[\"enableTotp\"] = async (input) => {\n const result = await call(() => opts.transport.client.auth.enableTotp(input));\n return result.data;\n };\n\n const verifyTotpSetup: AuthNamespace[\"verifyTotpSetup\"] = async (input) => {\n const result = await call(() => opts.transport.client.auth.verifyTotpSetup(input));\n adoptRotatedToken(result.data.accessToken);\n };\n\n const disableTwoFactor: AuthNamespace[\"disableTwoFactor\"] = async (input) => {\n const result = await call(() => opts.transport.client.auth.disableTwoFactor(input));\n adoptRotatedToken(result.data.accessToken);\n };\n\n const regenerateBackupCodes: AuthNamespace[\"regenerateBackupCodes\"] = async (input) => {\n const result = await call(() => opts.transport.client.auth.regenerateBackupCodes(input));\n return result.data.backupCodes;\n };\n\n // --- Account security -------------------------------------------------------\n\n const changePassword: AuthNamespace[\"changePassword\"] = async (input) => {\n const result = await call(() =>\n opts.transport.client.auth.changePassword({\n currentPassword: input.currentPassword,\n newPassword: input.newPassword,\n revokeOtherSessions: input.revokeOtherSessions,\n }),\n );\n adoptRotatedToken(result.data.accessToken);\n };\n\n const changeEmail: AuthNamespace[\"changeEmail\"] = async (input) => {\n await call(() => opts.transport.client.auth.changeEmail(input));\n };\n\n const confirmEmailChange: AuthNamespace[\"confirmEmailChange\"] = async (input) => {\n await call(() => opts.transport.client.auth.confirmEmailChange(input));\n };\n\n const listSessions: AuthNamespace[\"listSessions\"] = async () => {\n const result = await call(() => opts.transport.client.auth.listSessions());\n return result.data;\n };\n\n const revokeSession: AuthNamespace[\"revokeSession\"] = async (input) => {\n await call(() => opts.transport.client.auth.revokeSession(input));\n };\n\n const revokeOtherSessions: AuthNamespace[\"revokeOtherSessions\"] = async () => {\n await call(() => opts.transport.client.auth.revokeOtherSessions());\n };\n\n // The server deletes the login, so the cached bearer is dead the moment\n // this resolves — drop it locally (emits signed_out). On failure (wrong\n // password, network) the session is untouched and the error propagates.\n const closeAccount: AuthNamespace[\"closeAccount\"] = async (input) => {\n await call(() =>\n opts.transport.client.auth.closeAccount({\n confirmEmail: input.confirmEmail,\n password: input.password,\n }),\n );\n clearSession();\n };\n\n const _refresh: AuthNamespace[\"_refresh\"] = async () => {\n // Re-read storage as a fallback so a session written after\n // construction (e.g. on tab restore or in tests) is honoured.\n let current = cached;\n if (!current) {\n const fromStorage = await Promise.resolve(opts.storage.get());\n if (fromStorage) {\n cached = fromStorage;\n current = fromStorage;\n }\n }\n if (!current) {\n const err: FytkitError = { kind: \"auth_required\" };\n throw err;\n }\n try {\n const result = await opts.transport.client.auth.refresh({ token: current.accessToken });\n const { user, session } = result.data;\n return applySession(\n { accessToken: session.accessToken, expiresAt: session.expiresAt, user },\n \"session_refreshed\",\n );\n } catch (err) {\n const normalized = normalizeError(err);\n // Sign out only when the server actually rejected the token. A network\n // or server failure leaves the session in place for a later retry.\n if (isSessionRejected(normalized)) clearSession();\n throw normalized;\n }\n };\n\n const _signOutLocal: AuthNamespace[\"_signOutLocal\"] = () => clearSession();\n\n refreshFn = _refresh;\n\n return {\n signUp,\n signIn,\n signOut,\n getSession,\n onAuthStateChange,\n requestPasswordReset,\n confirmPasswordReset,\n confirmEmail,\n resendVerification,\n verifyTotp,\n verifyBackupCode,\n requestEmailOtp,\n verifyEmailOtp,\n getSecurityStatus,\n enableTotp,\n verifyTotpSetup,\n disableTwoFactor,\n regenerateBackupCodes,\n changePassword,\n changeEmail,\n confirmEmailChange,\n listSessions,\n revokeSession,\n revokeOtherSessions,\n closeAccount,\n _refresh,\n _signOutLocal,\n };\n}\n","// Internal: not exported from the package.\n\n/** Where the SDK calls fytkit unless `baseUrl` says otherwise. */\nexport const DEFAULT_BASE_URL = \"https://api.fytkit.com\";\n","import { normalizeError } from \"./error\";\nimport type { Transport } from \"./transport\";\n\n/**\n * Wrap every method on a public-router slice so thrown ORPCErrors become\n * normalised FytkitErrors at the SDK boundary.\n *\n * The slice we receive is an `@orpc/client` Proxy whose own keys are not\n * enumerable — every property lookup is intercepted lazily. We mirror that\n * shape by returning a Proxy of our own that wraps each function on first\n * access and caches the result. This way the SDK never needs to know the\n * method names ahead of time, and the typed shape from the server router\n * flows through unchanged.\n */\nfunction wrapSlice<T extends object>(slice: T): T {\n const cache = new Map<string | symbol, unknown>();\n return new Proxy(slice, {\n get(target, prop, receiver) {\n if (cache.has(prop)) return cache.get(prop);\n const value = Reflect.get(target, prop, receiver);\n if (typeof value !== \"function\") {\n // Allow nested namespaces to flow through unchanged (the orpc\n // proxy returns another proxy here, not a plain object).\n return value;\n }\n const wrapped = async (...args: unknown[]) => {\n try {\n return await (value as (...a: unknown[]) => Promise<unknown>)(...args);\n } catch (err) {\n throw normalizeError(err);\n }\n };\n cache.set(prop, wrapped);\n return wrapped;\n },\n }) as T;\n}\n\nexport interface DataNamespaces {\n readonly workouts: Transport[\"client\"][\"workout\"];\n readonly exercises: Transport[\"client\"][\"exercise\"];\n readonly programs: Transport[\"client\"][\"program\"];\n readonly goals: Transport[\"client\"][\"goal\"];\n readonly equipment: Transport[\"client\"][\"equipment\"];\n readonly recipes: Transport[\"client\"][\"recipe\"];\n readonly nutrition: Transport[\"client\"][\"nutrition\"];\n readonly articles: Transport[\"client\"][\"article\"];\n readonly enrollments: Transport[\"client\"][\"enrollment\"];\n readonly favourites: Transport[\"client\"][\"favourite\"];\n readonly sessionLogs: Transport[\"client\"][\"sessionLog\"];\n readonly progression: Transport[\"client\"][\"progression\"];\n readonly bodyMetrics: Transport[\"client\"][\"bodyMetric\"];\n readonly bodyMeasurements: Transport[\"client\"][\"bodyMeasurement\"];\n readonly progressPhotos: Transport[\"client\"][\"progressPhoto\"];\n readonly checkIns: Transport[\"client\"][\"checkin\"];\n readonly messages: Transport[\"client\"][\"message\"];\n readonly timeline: Transport[\"client\"][\"timeline\"];\n readonly health: Transport[\"client\"][\"health\"];\n readonly habits: Transport[\"client\"][\"habit\"];\n readonly progress: Transport[\"client\"][\"progress\"];\n readonly media: Transport[\"client\"][\"media\"];\n readonly subscription: Transport[\"client\"][\"subscription\"];\n readonly profile: Transport[\"client\"][\"profile\"];\n readonly nutritionLog: Transport[\"client\"][\"nutritionLog\"];\n readonly preferences: Transport[\"client\"][\"preferences\"];\n}\n\nexport function createDataNamespaces(transport: Transport): DataNamespaces {\n const c = transport.client;\n return {\n workouts: wrapSlice(c.workout),\n exercises: wrapSlice(c.exercise),\n programs: wrapSlice(c.program),\n goals: wrapSlice(c.goal),\n equipment: wrapSlice(c.equipment),\n recipes: wrapSlice(c.recipe),\n nutrition: wrapSlice(c.nutrition),\n articles: wrapSlice(c.article),\n enrollments: wrapSlice(c.enrollment),\n favourites: wrapSlice(c.favourite),\n sessionLogs: wrapSlice(c.sessionLog),\n progression: wrapSlice(c.progression),\n bodyMetrics: wrapSlice(c.bodyMetric),\n bodyMeasurements: wrapSlice(c.bodyMeasurement),\n progressPhotos: wrapSlice(c.progressPhoto),\n checkIns: wrapSlice(c.checkin),\n messages: wrapSlice(c.message),\n timeline: wrapSlice(c.timeline),\n health: wrapSlice(c.health),\n habits: wrapSlice(c.habit),\n progress: wrapSlice(c.progress),\n media: wrapSlice(c.media),\n subscription: wrapSlice(c.subscription),\n profile: wrapSlice(c.profile),\n nutritionLog: wrapSlice(c.nutritionLog),\n preferences: wrapSlice(c.preferences),\n };\n}\n","import type { Session, TokenStorage } from \"./types\";\n\nconst STORAGE_KEY_PREFIX = \"fytkit:\";\nconst STORAGE_KEY_SUFFIX = \":tokens\";\n\nfunction keyFor(clientId: string): string {\n return `${STORAGE_KEY_PREFIX}${clientId}${STORAGE_KEY_SUFFIX}`;\n}\n\nfunction isValidSession(value: unknown): value is Session {\n if (!value || typeof value !== \"object\") return false;\n const v = value as Record<string, unknown>;\n return (\n typeof v.accessToken === \"string\" &&\n typeof v.expiresAt === \"string\" &&\n typeof v.user === \"object\" &&\n v.user !== null &&\n typeof (v.user as { id?: unknown }).id === \"string\" &&\n typeof (v.user as { email?: unknown }).email === \"string\"\n );\n}\n\n/**\n * Default web storage. Reads/writes JSON under `fytkit:{clientId}:tokens`.\n * Silently no-ops when localStorage throws (Safari private mode, SSR).\n */\nexport function createLocalStorageAdapter(clientId: string): TokenStorage {\n const key = keyFor(clientId);\n return {\n get: () => {\n try {\n const raw = globalThis.localStorage?.getItem(key);\n if (!raw) return null;\n const parsed: unknown = JSON.parse(raw);\n return isValidSession(parsed) ? parsed : null;\n } catch {\n return null;\n }\n },\n set: (session: Session) => {\n try {\n globalThis.localStorage?.setItem(key, JSON.stringify(session));\n } catch {\n // Quota / private-mode / SSR — swallow and let the caller treat the\n // session as in-memory-only for this tab.\n }\n },\n remove: () => {\n try {\n globalThis.localStorage?.removeItem(key);\n } catch {\n // see set() rationale\n }\n },\n };\n}\n\n/** In-memory storage. Useful for tests and SSR. */\nexport function createMemoryStorageAdapter(): TokenStorage {\n let current: Session | null = null;\n return {\n get: () => current,\n set: (session: Session) => {\n current = session;\n },\n remove: () => {\n current = null;\n },\n };\n}\n","// The `Retry-After` fallback for 429s (SDK1, #461).\n//\n// The SDK reads a 429's wait from its error data (`data.retryAfterMs`), which\n// every fytkit 429 sends. A 429 that doesn't carry it (an older server's\n// plain body, a proxy or CDN in front of the API) used to reach an app as\n// `retryAfter: 0`, so a retry loop retried at once. When such a 429 has a\n// `Retry-After` header, the transport writes the header's wait into the\n// error data before oRPC reads the response, so `normalizeError` finds it\n// where it always looks. A hint the data already carries always wins.\n\nimport { isRecord } from \"./is-record\";\n\nconst DELAY_SECONDS = /^\\d+$/;\n\n/**\n * A `Retry-After` value's wait in milliseconds from `now`: delay-seconds\n * (`\"120\"`) or an HTTP date (zero once it has passed). Null when there is no\n * value or it is neither.\n */\nexport function retryAfterHeaderMs(value: string | null, now: number): number | null {\n if (value === null) return null;\n const trimmed = value.trim();\n if (trimmed.length === 0) return null;\n if (DELAY_SECONDS.test(trimmed)) return Number(trimmed) * 1000;\n // Only a date in letters: Date.parse also accepts bare numbers like \"1.5\".\n if (!/[a-z]/i.test(trimmed)) return null;\n const at = Date.parse(trimmed);\n return Number.isNaN(at) ? null : Math.max(0, at - now);\n}\n\nfunction parseJson(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return undefined;\n }\n}\n\n/**\n * The 429's body with `waitMs` as its error data's `retryAfterMs`, as oRPC's\n * RPC protocol frames it (`{ json: <error> }`); null when the data already\n * carries a hint. A body that isn't an oRPC error becomes the error oRPC\n * would have sent.\n */\nfunction hintedBody(body: unknown, waitMs: number): Readonly<Record<string, unknown>> | null {\n if (isRecord(body) && isRecord(body.json) && typeof body.json.code === \"string\") {\n const error = body.json;\n const data = isRecord(error.data) ? error.data : {};\n if (typeof data.retryAfterMs === \"number\") return null;\n return { ...body, json: { ...error, data: { ...data, retryAfterMs: waitMs } } };\n }\n return {\n json: {\n defined: false,\n code: \"TOO_MANY_REQUESTS\",\n status: 429,\n message: \"Too Many Requests\",\n data: { retryAfterMs: waitMs },\n },\n };\n}\n\n/**\n * `response` with the `Retry-After` header's wait written into its error data\n * when it is a 429 whose data has no `retryAfterMs`; unchanged otherwise.\n */\nexport async function withRetryAfterHint(response: Response, now: number): Promise<Response> {\n if (response.status !== 429) return response;\n const waitMs = retryAfterHeaderMs(response.headers.get(\"retry-after\"), now);\n if (waitMs === null) return response;\n const hinted = hintedBody(parseJson(await response.clone().text()), waitMs);\n if (hinted === null) return response;\n const headers = new Headers(response.headers);\n headers.set(\"content-type\", \"application/json\");\n headers.delete(\"content-length\");\n return new Response(JSON.stringify(hinted), {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n}\n","import type { PublicRouter } from \"@fytkit/types\";\nimport { createORPCClient } from \"@orpc/client\";\nimport { RPCLink } from \"@orpc/client/fetch\";\nimport type { RouterClient } from \"@orpc/server\";\nimport { withRetryAfterHint } from \"./retry-after\";\nimport type { Session } from \"./types\";\n\nexport interface CreateTransportOptions {\n readonly baseUrl: string;\n readonly clientId: string;\n readonly getSession: () => Session | null;\n readonly fetch?: typeof fetch;\n}\n\n/**\n * Per-call options the SDK's own wrappers pass to the link. `ifNoneMatch`\n * becomes the request's `If-None-Match` header (`app.bootstrap`'s ETag\n * revalidation, #535); every other call sends none.\n */\nexport interface TransportContext {\n readonly ifNoneMatch?: string;\n}\n\n/** The public router's client, taking {@link TransportContext} per call. */\nexport type TransportClient = RouterClient<PublicRouter, TransportContext>;\n\nexport interface Transport {\n readonly client: TransportClient;\n /** Internal: hook for attachRefreshInterceptor to install a fetch wrapper. */\n readonly _setFetchInterceptor: (interceptor: FetchInterceptor) => void;\n}\n\ntype FetchInterceptor = (\n request: Request,\n next: (req: Request) => Promise<Response>,\n) => Promise<Response>;\n\nexport function createTransport(options: CreateTransportOptions): Transport {\n let interceptor: FetchInterceptor | null = null;\n const baseFetch: typeof fetch = options.fetch ?? fetch.bind(globalThis);\n\n // The fetch handed to RPCLink resolves the interceptor lazily so callers\n // can install one after construction (see attachRefreshInterceptor). A 429\n // whose error data lacks a retry hint gets the `Retry-After` header's wait\n // written in (retry-after.ts), so `rate_limited.retryAfter` reads it.\n const wrappedFetch: typeof fetch = async (input, init) => {\n const request = input instanceof Request ? input : new Request(input, init);\n const next = (req: Request) => baseFetch(req);\n const response = interceptor ? await interceptor(request, next) : await next(request);\n return withRetryAfterHint(response, Date.now());\n };\n\n const link = new RPCLink<TransportContext>({\n url: () => `${options.baseUrl}/rpc`,\n headers: ({ context }) => {\n const session = options.getSession();\n const headers: Record<string, string> = {\n \"X-Fytkit-Client-Id\": options.clientId,\n };\n if (session) {\n headers.Authorization = `Bearer ${session.accessToken}`;\n }\n if (context.ifNoneMatch !== undefined) {\n headers[\"If-None-Match\"] = context.ifNoneMatch;\n }\n return headers;\n },\n fetch: wrappedFetch,\n });\n\n const client: TransportClient = createORPCClient(link);\n return {\n client,\n _setFetchInterceptor: (i) => {\n interceptor = i;\n },\n };\n}\n\n/** Internal marker header — set on the refresh call so we never recurse. */\nconst REFRESH_HEADER = \"X-Fytkit-Refresh\";\n\n/**\n * Procedures that authenticate with credentials in their input (a password,\n * an emailed token, a one-time code or a sign-in challenge), never with the\n * bearer. A 401 from one of them means those credentials were rejected, not\n * that the session is dead, so it passes straight through: no refresh, no\n * replay, no local sign-out. Otherwise a member who mistypes their password\n * while signing in again (after `reauth_required`) would be signed out.\n */\nconst CREDENTIAL_PROCEDURES: readonly string[] = [\n \"auth/signIn\",\n \"auth/signUp\",\n \"auth/requestPasswordReset\",\n \"auth/confirmPasswordReset\",\n \"auth/confirmEmail\",\n \"auth/confirmEmailChange\",\n \"auth/verifyTotp\",\n \"auth/verifyBackupCode\",\n \"auth/requestEmailOtp\",\n \"auth/verifyEmailOtp\",\n];\n\n/**\n * The RPCLink routes per-procedure calls under a URL whose pathname ends\n * with the procedure path (joined as `/segment/segment`), so procedures are\n * detected by suffix. endsWith keeps trailing query strings or link-level\n * prefixes from breaking detection.\n */\nfunction requestPathname(request: Request): string {\n try {\n return new URL(request.url).pathname;\n } catch {\n return \"\";\n }\n}\n\n/** Detect whether a request targets the refresh procedure. */\nfunction isRefreshRequest(request: Request): boolean {\n if (request.headers.get(REFRESH_HEADER) === \"1\") return true;\n return requestPathname(request).endsWith(\"/auth/refresh\");\n}\n\n/** Detect whether a request targets one of the `CREDENTIAL_PROCEDURES`. */\nfunction isCredentialRequest(request: Request): boolean {\n const path = requestPathname(request);\n return CREDENTIAL_PROCEDURES.some((procedure) => path.endsWith(`/${procedure}`));\n}\n\nexport interface AttachRefreshInterceptorOptions {\n readonly transport: Transport;\n readonly auth: {\n readonly _refresh: () => Promise<Session>;\n readonly _signOutLocal: () => void;\n readonly getSession: () => Session | null;\n };\n}\n\n/**\n * Install a fetch interceptor that, on a 401 response, calls auth._refresh\n * once and retries the original request. Concurrent 401s de-duplicate\n * onto a single in-flight refresh promise. A 401/403 from refresh itself or\n * a 401 from the retry triggers a local sign-out and propagates the original\n * 401 to the caller (after which orpc throws ORPCError(\"UNAUTHORIZED\")). A\n * refresh that fails for any other reason (offline, 5xx) also propagates the\n * original 401, but keeps the session so a later call can succeed. A 401 from\n * a credential procedure (`CREDENTIAL_PROCEDURES`) is never refreshed or\n * replayed: it rejects the credentials, not the session.\n */\nexport function attachRefreshInterceptor(opts: AttachRefreshInterceptorOptions): void {\n let inflight: Promise<Session> | null = null;\n\n const refreshOnce = (): Promise<Session> => {\n if (!inflight) {\n inflight = opts.auth._refresh().finally(() => {\n inflight = null;\n });\n }\n return inflight;\n };\n\n opts.transport._setFetchInterceptor(async (request, next) => {\n // Refresh calls must never trigger a recursive refresh, and a 401 from a\n // credential procedure is a wrong password or code, not a dead session.\n if (isRefreshRequest(request) || isCredentialRequest(request)) {\n return next(request);\n }\n\n // Capture the request body now, before we pass `request` to `next` —\n // once fetch consumes a Request body it can no longer be cloned.\n const buffered = await request.clone().arrayBuffer();\n\n const firstResponse = await next(request);\n if (firstResponse.status !== 401) return firstResponse;\n\n try {\n await refreshOnce();\n } catch {\n // _refresh decides whether the session survives: it signs out only\n // when the server rejected the token, not on a network/server failure.\n return firstResponse;\n }\n\n // Rebuild headers so the new Authorization header replaces the stale\n // one — the RPCLink only re-evaluates its own `headers()` once per call,\n // so we patch the bearer ourselves here.\n const replayedHeaders = new Headers(request.headers);\n const session = opts.auth.getSession();\n if (session) {\n replayedHeaders.set(\"Authorization\", `Bearer ${session.accessToken}`);\n } else {\n replayedHeaders.delete(\"Authorization\");\n }\n const replayed = new Request(request.url, {\n method: request.method,\n headers: replayedHeaders,\n body: buffered.byteLength > 0 ? buffered : undefined,\n credentials: request.credentials,\n mode: request.mode,\n cache: request.cache,\n redirect: request.redirect,\n referrer: request.referrer,\n integrity: request.integrity,\n });\n\n const retried = await next(replayed);\n if (retried.status === 401) {\n // Refresh succeeded server-side but the server still rejects — the\n // session is dead. Sign out locally and let orpc surface the 401.\n opts.auth._signOutLocal();\n }\n return retried;\n });\n}\n","// Resolving a studio's join code (#529): the one call an app makes before\n// it has a client id. The code on a studio's Fytkit Coach QR code or join\n// link names the studio; this turns it into the client id to pass to\n// `createFytkitClient`, with what a welcome screen needs.\n//\n// A plain `GET /api/v1/join/{code}`: it takes no credentials and needs no\n// client, so it is a function of its own rather than a client method.\n\nimport { createORPCErrorFromJson, isORPCErrorJson } from \"@orpc/client\";\nimport { DEFAULT_BASE_URL } from \"./base-url\";\nimport { normalizeError } from \"./error\";\nimport { isRecord } from \"./is-record\";\nimport { retryAfterHeaderMs } from \"./retry-after\";\nimport type { FytkitError } from \"./types\";\n\n/** Options for {@link resolveJoinCode}. */\nexport interface ResolveJoinCodeOptions {\n /** The API's origin, as for `createFytkitClient`. Defaults to `https://api.fytkit.com`. */\n readonly baseUrl?: string;\n /** A `fetch` to use instead of the global one. */\n readonly fetch?: typeof fetch;\n}\n\n/**\n * The design the app paints for the studio: its Fytkit Coach theme (an\n * app-only one if the studio picked one, otherwise its published site design),\n * the theme's palette and the shades derived from it. Every colour is a\n * `#rrggbb` hex string.\n */\nexport interface JoinCodeTheme {\n /** The site template the design comes from (the app calls it the theme). */\n readonly templateId: string;\n /** The template's colour theme (the app calls it the palette). */\n readonly themeId: string;\n /** The theme's curated colours. */\n readonly palette: {\n readonly primary: string;\n readonly secondary: string;\n readonly accent: string;\n readonly background: string;\n readonly text: string;\n };\n /** Shades derived from the palette, for states and text on fills. */\n readonly variants: {\n /** Hover/press shade of `primary`. */\n readonly primaryDeep: string;\n /** Tinted background of `primary`. */\n readonly primarySoft: string;\n /** Text that reads on `primary`. */\n readonly onPrimary: string;\n /** Hover/press shade of `accent`. */\n readonly accentDeep: string;\n /** Tinted background of `accent`. */\n readonly accentSoft: string;\n /** Text that reads on `accent`. */\n readonly onAccent: string;\n /** The highlight fill, legible under dark ink text. */\n readonly highlight: string;\n /** Hover/press shade of `highlight`. */\n readonly highlightDeep: string;\n /** Tinted background of `highlight`. */\n readonly highlightSoft: string;\n };\n}\n\n/** A coach a join code's studio names publicly. */\nexport interface JoinCodeCoach {\n /** The coach's public name. */\n readonly name: string;\n /** The coach's public profile image, a stable URL; null when there is none. */\n readonly avatarUrl: string | null;\n}\n\n/** What a join code resolves to. */\nexport interface JoinCodeResolution {\n /** The code in canonical form: upper case, no spaces or hyphens. */\n readonly code: string;\n /** The studio's Fytkit Coach client id: pass it to `createFytkitClient`. */\n readonly clientId: string;\n /** What the welcome screen names and shows. */\n readonly studio: {\n /** The name members know the studio by. */\n readonly name: string;\n /** The studio's logo, a stable public URL; null when it has none. */\n readonly logoUrl: string | null;\n /**\n * Who coaches there, as the studio's public site shows them: today one\n * entry, the studio's own coach profile (never staff). At most 3.\n */\n readonly coaches: readonly JoinCodeCoach[];\n /** Published programmes, any access level; drafts and archived ones never count. */\n readonly programCount: number;\n };\n /** The design the app paints for the studio. */\n readonly theme: JoinCodeTheme;\n}\n\n/**\n * What the server answers for an unknown or revoked code, and what this\n * answers itself, without a request, for input that can't safely go in the\n * URL path.\n */\nconst INACTIVE: FytkitError = {\n kind: \"not_found\",\n refusal: { reason: \"join_code_inactive\", data: {} },\n};\n\n/**\n * Whitespace, the hyphen-minus and the dashes phones substitute for it: the\n * separators the server ignores too (src/lib/join-code/code.ts in\n * fytkit-web, which a test there checks this against).\n */\nconst IGNORED_SEPARATORS = /[\\s\\-\\u2010-\\u2015\\u2212]/g;\n\n/**\n * Only what keeps the URL path safe: letters and digits, at most as many as\n * the server reads. The code's alphabet and length are the server's to\n * decide, so a format change never needs a new app build.\n */\nconst PATH_SAFE_CODE = /^[A-Za-z0-9]{1,64}$/;\n\n/** The code to put in the path, upper-cased; null when it isn't path-safe. */\nfunction pathSafeJoinCode(input: string): string | null {\n const candidate = input.replace(IGNORED_SEPARATORS, \"\");\n return PATH_SAFE_CODE.test(candidate) ? candidate.toUpperCase() : null;\n}\n\nasync function readJson(response: Response): Promise<unknown> {\n try {\n return await response.json();\n } catch {\n return undefined;\n }\n}\n\n/** The refusal in a non-2xx response, as every SDK method reports one. */\nfunction refusalOf(response: Response, body: unknown): FytkitError {\n const wait = () => retryAfterHeaderMs(response.headers.get(\"retry-after\"), Date.now()) ?? 0;\n // A 404 that isn't the route's own oRPC answer comes from a wrong\n // `baseUrl` or a proxy, not from the code: it stays a plain server error\n // (below), so a member is never told their code is dead because of it.\n if (isORPCErrorJson(body)) {\n const error = normalizeError(createORPCErrorFromJson(body));\n // A 429 without the hint in its data (a proxy's): read the header instead.\n if (error.kind === \"rate_limited\" && error.retryAfter === 0) {\n return { ...error, retryAfter: wait() };\n }\n return error;\n }\n if (response.status === 429) return { kind: \"rate_limited\", retryAfter: wait() };\n return { kind: \"server\", status: response.status };\n}\n\n/**\n * Resolves a studio's join code, as its QR code or join link carries it or a\n * member types it, to the studio's Fytkit Coach client id, name, logo, app\n * theme, public coach profile and published programme count. Needs no client\n * id or session. Codes are case-insensitive and spaces and hyphens are\n * ignored, so pass what the member typed as it is.\n *\n * Rejects with a `FytkitError`, as every SDK method does:\n * - `{ kind: \"not_found\", refusal: { reason: \"join_code_inactive\" } }`: no\n * studio has that code, the studio replaced it with a new one, or the\n * input has something other than letters, digits, spaces and hyphens, or\n * more than 64 letters and digits (separators are stripped first; then\n * without a request). Show\n * \"This code is no longer active. Ask your coach for a new one.\" The\n * cases are deliberately indistinguishable.\n * - `{ kind: \"rate_limited\", retryAfter }`: 60 lookups a minute per IP\n * address; wait `retryAfter` milliseconds.\n * - `{ kind: \"network\" }` when the request never reached the server.\n * - `{ kind: \"server\", status }` for anything else, e.g. a 404 that isn't\n * the API's own answer (a wrong `baseUrl`, or a proxy in front of it).\n *\n * ```ts\n * const studio = await resolveJoinCode(scannedCode);\n * const fytkit = createFytkitClient({ clientId: studio.clientId });\n * ```\n */\nexport async function resolveJoinCode(\n code: string,\n options: ResolveJoinCodeOptions = {},\n): Promise<JoinCodeResolution> {\n // Anything that isn't letters and digits once separators are gone (`/`,\n // `.`, `..`, `?`, `%`) is refused here, so it never reaches the path.\n // Whether the rest is a code at all is the server's call.\n const canonical = pathSafeJoinCode(code);\n if (canonical === null) throw INACTIVE;\n\n const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;\n const doFetch: typeof fetch = options.fetch ?? fetch.bind(globalThis);\n let response: Response;\n try {\n response = await doFetch(`${baseUrl}/api/v1/join/${canonical}`, {\n method: \"GET\",\n headers: { Accept: \"application/json\" },\n });\n } catch (error) {\n throw normalizeError(error);\n }\n\n const body = await readJson(response);\n if (response.ok) {\n if (isRecord(body) && isRecord(body.data)) {\n // The server's documented shape, trusted as every other response is.\n return body.data as unknown as JoinCodeResolution;\n }\n throw { kind: \"server\", status: response.status } satisfies FytkitError;\n }\n throw refusalOf(response, body);\n}\n","// \"Subscribe\" or \"What your plan includes\" (#538). Fytkit Coach takes no\n// in-app purchase: where the member's store allows it, the app links out to\n// the studio's Stripe checkout; everywhere else it shows what the plan\n// includes. `app.bootstrap` carries the list of storefronts where linking out\n// is allowed (`data.paymentPolicy`); this reads it for the storefront the\n// device's store reports. It fails closed: an unknown storefront, a missing\n// code or an unmet condition is plan info.\n\nimport type { AppBootstrapData } from \"./app\";\n\n/** `app.bootstrap`'s `data.paymentPolicy`. */\nexport type PaymentPolicy = AppBootstrapData[\"paymentPolicy\"];\n\n/** The store the running app was installed from. */\nexport type PaymentPlatform = PaymentPolicy[\"linkOut\"][number][\"platform\"];\n\n/** Options for {@link resolvePaymentMode}. */\nexport interface ResolvePaymentModeOptions {\n /** The store the running app was installed from: `\"ios\"` or `\"android\"`. */\n readonly platform: PaymentPlatform;\n /**\n * The storefront the store reports, case-insensitive: on iOS StoreKit's\n * `Storefront.current?.countryCode` (ISO 3166-1 alpha-3, e.g. \"USA\"); on\n * Android Play Billing's `BillingConfig.getCountryCode()` (alpha-2, e.g.\n * \"US\"). Pass `null` when the store reports none: that is plan info.\n */\n readonly storefront: string | null | undefined;\n /**\n * The conditions the app meets in this storefront (see `conditions` in\n * `paymentPolicy.linkOut`), e.g. `\"apple_external_purchase_link_entitlement\"`\n * once `ExternalPurchaseLink.canOpen` is true. A storefront whose\n * conditions aren't all met, or names one the app doesn't know, is plan info.\n */\n readonly conditionsMet?: readonly string[];\n}\n\n/**\n * What to show:\n * - `link_out`: \"Subscribe\", opening the studio's checkout outside the app,\n * in the way each of `conditions` requires (none: a plain link to the\n * system browser);\n * - `plan_info`: \"What your plan includes\", with no purchase and no link to one.\n */\nexport type PaymentMode =\n | { readonly mode: \"link_out\"; readonly conditions: readonly string[] }\n | { readonly mode: \"plan_info\" };\n\nconst PLAN_INFO: PaymentMode = { mode: \"plan_info\" };\n\n/**\n * Decides, for the storefront the device's store reports, whether the app\n * shows \"Subscribe\" (a link out) or \"What your plan includes\". Pure: it reads\n * the policy from a stored `app.bootstrap`, so it works offline. A missing or\n * malformed policy (e.g. a snapshot stored before it existed) is plan info.\n *\n * ```ts\n * const { mode } = resolvePaymentMode(data.paymentPolicy, {\n * platform: \"ios\",\n * storefront: storefrontCountryCode, // from StoreKit, e.g. \"USA\"\n * });\n * if (mode === \"link_out\") showSubscribe();\n * else showWhatYourPlanIncludes();\n * ```\n */\nexport function resolvePaymentMode(\n policy: PaymentPolicy | null | undefined,\n options: ResolvePaymentModeOptions,\n): PaymentMode {\n // A snapshot stored before the policy existed, or a malformed one, has no\n // list to read: plan info, as for a storefront it doesn't name.\n if (!policy || !Array.isArray(policy.linkOut)) return PLAN_INFO;\n const storefront = options.storefront?.trim().toUpperCase() ?? \"\";\n if (storefront === \"\") return PLAN_INFO;\n const rule = policy.linkOut.find(\n (entry) =>\n entry?.platform === options.platform &&\n typeof entry.storefront === \"string\" &&\n entry.storefront.toUpperCase() === storefront,\n );\n if (!rule || !Array.isArray(rule.conditions)) return PLAN_INFO;\n const met = new Set(options.conditionsMet ?? []);\n if (!rule.conditions.every((condition) => met.has(condition))) return PLAN_INFO;\n return { mode: \"link_out\", conditions: rule.conditions };\n}\n","// Live updates for a trainee's conversation (SDK1, #461): the third-party\n// counterpart of the member portal's own channel. `messages.realtimeToken`\n// hands out a 15-minute token and the one private Supabase Realtime topic it\n// may join (`member:<client id>`); `app.bootstrap` names the project's public\n// URL and anon key. The database broadcasts `thread_changed` on that topic\n// whenever the conversation changes, with no content: a cue to read\n// `messages.list` and `messages.unreadCount` again, never the data itself.\n// The same topic carries `coach_typing` while a staff member writes to the\n// trainee (W16), surfaced as `onCoachTyping`; only the server sends it.\n//\n// The SDK has no runtime dependency on supabase-js: the app passes a factory\n// that creates a Supabase client, and this module only uses the few members\n// described by `RealtimeClientLike`. Self-contained on purpose (no imports),\n// so the shapes can be checked against supabase-js's own types. The token is\n// set on the socket by hand (`realtime.setAuth(token)`), which supabase-js\n// keeps only from 2.87.1: earlier 2.x clients put the anon key back on the\n// next heartbeat, and the private channel is lost.\n\n/** fytkit's public Supabase Realtime settings, from `app.bootstrap` (`data.realtime`). */\nexport interface RealtimeConnection {\n /** The Supabase project URL. */\n readonly url: string;\n /** The project's public anon key. Safe to ship in an app. */\n readonly anonKey: string;\n}\n\n/** `messages.realtimeToken`'s `data`: a token and the private topic it admits. */\nexport interface MessagesRealtimeGrant {\n /** A Supabase Realtime token for the signed-in trainee, valid until `expiresAt`. */\n readonly token: string;\n /** When the token expires (ISO 8601), 15 minutes after it was issued. */\n readonly expiresAt: string;\n /** The only private topic the token may join: `member:<client id>`. */\n readonly topic: string;\n}\n\n/** An auth-state event, as `auth.onAuthStateChange` reports it; only `type` is read. */\nexport interface MessagesRealtimeAuthEvent {\n /** `signed_in`, `signed_out`, `session_refreshed` or `signed_out_due_to_token_reuse`. */\n readonly type: string;\n}\n\n/** The calls the subscription needs. A `FytkitClient` is one. */\nexport interface MessagesRealtimeSource {\n /**\n * `app.bootstrap`, for the Realtime connection settings. Called without an\n * ETag, so it always answers `ok` with the data.\n */\n readonly app: {\n bootstrap(): PromiseLike<\n | { readonly status: \"ok\"; readonly data: { readonly realtime: RealtimeConnection } }\n | { readonly status: \"unchanged\" }\n >;\n };\n /** `messages.realtimeToken`, for the token and its topic. */\n readonly messages: {\n realtimeToken(\n input: Record<string, never>,\n ): PromiseLike<{ readonly data: MessagesRealtimeGrant }>;\n };\n /**\n * `auth.onAuthStateChange`, optional. When present, a sign-out on the\n * client (`signed_out`, `signed_out_due_to_token_reuse`) ends the\n * subscription at once with `refused`, rather than at its next renewal:\n * the trainee's channel never outlives their session on this client.\n */\n readonly auth?: {\n onAuthStateChange(listener: (event: MessagesRealtimeAuthEvent) => void): () => void;\n };\n}\n\n/**\n * The part of a supabase-js `RealtimeChannel` the subscription uses. A\n * channel that reports `CLOSED` is finished: supabase-js removes it from its\n * socket and never joins it again.\n */\nexport interface RealtimeChannelLike {\n /** Listens for one Broadcast event on the channel. */\n on(\n type: \"broadcast\",\n filter: { readonly event: string },\n callback: (message: unknown) => void,\n ): RealtimeChannelLike;\n /** Joins the channel; `status` is `SUBSCRIBED`, `CHANNEL_ERROR`, `TIMED_OUT` or `CLOSED`. */\n subscribe(callback: (status: string, error?: Error) => void): RealtimeChannelLike;\n}\n\n/**\n * The part of a supabase-js `SupabaseClient` the subscription uses.\n * `@supabase/supabase-js` 2.87.1 or later: earlier clients swap the token the\n * subscription sets for the anon key on the next heartbeat.\n */\nexport interface RealtimeClientLike {\n /** Creates a channel on `name`; the subscription always asks for a private one. */\n channel(\n name: string,\n options: { readonly config: { readonly private: true } },\n ): RealtimeChannelLike;\n /** Leaves and forgets a channel. */\n removeChannel(channel: RealtimeChannelLike): unknown;\n /** The client's Realtime socket: `setAuth(token)` hands it the token. */\n readonly realtime: {\n setAuth(token: string): Promise<void>;\n disconnect(): void;\n };\n}\n\n/**\n * Creates a new Supabase client for the subscription, with\n * `@supabase/supabase-js` 2.87.1 or later:\n *\n * ```ts\n * ({ url, anonKey }) =>\n * createClient(url, anonKey, {\n * auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false },\n * })\n * ```\n *\n * The client carries the trainee's Realtime token and nothing else, so its\n * auth stays off: no stored session, no refresh, and no sign-in read from\n * your page's URL that could replace the token. Return a new client on every\n * call: the subscription sets its own token on the client's socket and\n * disconnects it when it ends, so a client the app uses for anything else\n * would lose both. A factory that throws ends the subscription with\n * `fallback`.\n */\nexport type RealtimeClientFactory = (connection: RealtimeConnection) => RealtimeClientLike;\n\n/**\n * How the conversation is kept current:\n * - `connecting`: before the first answer either way, and while it moves to\n * another trainee's channel. Poll meanwhile.\n * - `live`: the channel is joined; changes arrive as `onChange`. Stop polling.\n * - `fallback`: Realtime is unavailable. Poll at\n * {@link MESSAGES_FALLBACK_POLL_MS}. For now, and `live` may follow, when\n * the token or the connection settings couldn't be fetched (retried every\n * minute), or the channel errored or timed out (supabase-js joins it\n * again by itself), or Realtime closed it (a Realtime restart, a token\n * that expired before its renewal landed: the subscription joins a fresh\n * channel after 5 seconds, then 15, then every minute, with a new token\n * first if the one it holds has expired). For good, and polling carries\n * on, when the server has no live updates to offer (a token without\n * `topic`, no connection settings, or `app.bootstrap` refused) or the\n * factory threw.\n * - `refused`: the token request was refused (`auth_required`,\n * `forbidden`, `not_found` or `validation`), asking again won't change\n * that, and nearly always because there is no trainee to listen for\n * (signed out, account closed, a trainer previewing the portal); or the\n * `FytkitClient` passed in signed out. Final: the subscription has\n * stopped and asks nothing more; the conversation reads are refused the\n * same way.\n */\nexport type MessagesRealtimeStatus = \"connecting\" | \"live\" | \"fallback\" | \"refused\";\n\n/**\n * Whether the trainee's coaching team is writing to them right now, for a\n * \"Sam is writing…\" line under the conversation:\n * - `idle`: nobody is.\n * - `typing`: the staff members in `names` are, by the display name the\n * conversation shows on their messages, each name once, in the order\n * they started. Show the first, or \"Sam and Alex are writing…\".\n *\n * A hint, live only: it is heard only while the status is `live`, and goes\n * back to `idle` when the channel is lost. A name that hears nothing more\n * for 8 seconds ({@link COACH_TYPING_EXPIRY_MS}) drops out by itself, so a\n * lost \"stopped\" never leaves it showing. While polling it stays `idle`.\n */\nexport type CoachTypingState =\n | { readonly kind: \"idle\" }\n | { readonly kind: \"typing\"; readonly names: readonly string[] };\n\n/**\n * How long a staff member stays in {@link CoachTypingState}'s `names` after\n * the last sign that they are writing. The dashboard repeats the sign every\n * 3 seconds while they write.\n */\nexport const COACH_TYPING_EXPIRY_MS = 8_000;\n\n/** What {@link subscribeToMessages} needs besides the client. */\nexport interface SubscribeToMessagesOptions {\n /** Creates the Supabase client the subscription owns. See {@link RealtimeClientFactory}. */\n readonly realtime: RealtimeClientFactory;\n /**\n * The conversation may have changed: read `messages.list` and\n * `messages.unreadCount` again. Also called on every (re)join, since\n * changes while the channel was down were not heard.\n */\n readonly onChange: () => void;\n /** Each change of {@link MessagesRealtimeStatus}, starting with `connecting`. */\n readonly onStatus?: (status: MessagesRealtimeStatus) => void;\n /**\n * Each change of {@link CoachTypingState}: someone on the trainee's\n * coaching team started or stopped writing to them. Optional; never\n * called before a `typing`. A subscription that ends by itself\n * (`refused`, or `fallback` for good) reports `idle` first if anyone was\n * writing; after `unsubscribe()`, nothing is reported.\n */\n readonly onCoachTyping?: (typing: CoachTypingState) => void;\n}\n\n/**\n * How often to poll while the status is `connecting` or `fallback`: the open\n * conversation every 10 seconds, the unread count every 60. Polling spends\n * the app's shared per-minute request budget, so stop while `live`.\n */\nexport const MESSAGES_FALLBACK_POLL_MS = {\n conversation: 10_000,\n unreadCount: 60_000,\n} as const satisfies Readonly<Record<\"conversation\" | \"unreadCount\", number>>;\n\n/** The event the database broadcasts on a trainee's topic. */\nconst THREAD_CHANGED_EVENT = \"thread_changed\";\n/** The event a staff member's typing goes out as, on the same topic. */\nconst COACH_TYPING_EVENT = \"coach_typing\";\n/** The prefix of the only topic a trainee's token admits. */\nconst MEMBER_TOPIC_PREFIX = \"member:\";\n/** Ask for a new token this long before the current one expires. */\nconst TOKEN_RENEW_BEFORE_MS = 60_000;\n/** Never ask again sooner than this, however close the expiry. */\nconst TOKEN_MIN_DELAY_MS = 5_000;\n/** After a request that failed for now, try again this often. */\nconst RETRY_MS = 60_000;\n/**\n * The waits before each fresh join after Realtime closes the channel, from\n * the first closure on; the last repeats until a join subscribes. The same\n * as the member portal's own channel.\n */\nconst REJOIN_DELAYS_MS = [5_000, 15_000, 60_000] as const;\n\n/** Auth events after which there is no trainee on the client to listen for. */\nconst SIGNED_OUT_EVENTS: ReadonlySet<string> = new Set([\n \"signed_out\",\n \"signed_out_due_to_token_reuse\",\n]);\n\n/** Error kinds that asking again won't change: no trainee here, or a bad request. */\nconst FINAL_ERROR_KINDS: ReadonlySet<string> = new Set([\n \"auth_required\",\n \"forbidden\",\n \"not_found\",\n \"validation\",\n]);\n\nfunction isFinalError(error: unknown): boolean {\n if (typeof error !== \"object\" || error === null || !(\"kind\" in error)) return false;\n const kind: unknown = error.kind;\n return typeof kind === \"string\" && FINAL_ERROR_KINDS.has(kind);\n}\n\nfunction isMemberTopic(topic: unknown): topic is string {\n return (\n typeof topic === \"string\" &&\n topic.startsWith(MEMBER_TOPIC_PREFIX) &&\n topic.length > MEMBER_TOPIC_PREFIX.length\n );\n}\n\n/**\n * A `coach_typing` broadcast's payload: `{ state: \"typing\" | \"stopped\", name, key }`.\n * `key` tells apart two staff members of one name; a payload without one is\n * keyed by its name.\n */\ninterface CoachTypingSign {\n readonly state: \"typing\" | \"stopped\";\n readonly name: string;\n readonly key: string;\n}\n\n/** Reads a `coach_typing` broadcast, or null when it is not one the server sends. */\nfunction coachTypingSign(message: unknown): CoachTypingSign | null {\n if (typeof message !== \"object\" || message === null || !(\"payload\" in message)) return null;\n const payload: unknown = message.payload;\n if (typeof payload !== \"object\" || payload === null) return null;\n if (!(\"state\" in payload) || !(\"name\" in payload)) return null;\n const { state, name } = payload;\n if (state !== \"typing\" && state !== \"stopped\") return null;\n if (typeof name !== \"string\" || name.length === 0) return null;\n const key = \"key\" in payload && typeof payload.key === \"string\" ? payload.key : \"\";\n return { state, name, key: key.length > 0 ? `key:${key}` : `name:${name}` };\n}\n\nfunction isConnection(value: unknown): value is RealtimeConnection {\n if (typeof value !== \"object\" || value === null) return false;\n if (!(\"url\" in value) || !(\"anonKey\" in value)) return false;\n return (\n typeof value.url === \"string\" &&\n value.url.length > 0 &&\n typeof value.anonKey === \"string\" &&\n value.anonKey.length > 0\n );\n}\n\nfunction renewalDelay(expiresAt: string, now: number): number {\n const expiry = Date.parse(expiresAt);\n if (Number.isNaN(expiry)) return RETRY_MS;\n return Math.max(TOKEN_MIN_DELAY_MS, expiry - TOKEN_RENEW_BEFORE_MS - now);\n}\n\n/** Whether a token still has long enough to run for a join to be worth sending with it. */\nfunction isUsable(expiresAt: string, now: number): boolean {\n return Date.parse(expiresAt) - now > TOKEN_MIN_DELAY_MS;\n}\n\n/** The wait before the fresh join after `closures` closures with no subscription between. */\nfunction rejoinDelay(closures: number): number {\n const last = REJOIN_DELAYS_MS.length - 1;\n return REJOIN_DELAYS_MS[Math.min(Math.max(closures - 1, 0), last)];\n}\n\nfunction ignore(): void {\n // A promise whose outcome nothing waits for.\n}\n\n/**\n * Hands the socket a token. Settles once the socket has it, or once it\n * failed, and never throws: the join follows either way, and a channel\n * without the token reports its own failure.\n */\nfunction authorise(client: RealtimeClientLike, token: string): Promise<void> {\n return new Promise<void>((resolve) => {\n resolve(client.realtime.setAuth(token));\n }).then(ignore, ignore);\n}\n\n/**\n * Subscribes to changes in the signed-in trainee's conversation over\n * Supabase Realtime, and returns a function that ends the subscription.\n *\n * It fetches a token (`messages.realtimeToken`) and the connection settings\n * (`app.bootstrap`), creates a client with `options.realtime`, hands the\n * socket the token, and joins the private topic the token names. The token\n * is renewed a minute before it expires, on the open channel; a renewed\n * token for another topic (another trainee signed in on the same client)\n * leaves that channel and its client and joins the new topic on a new one.\n * A channel Realtime closes is joined afresh on the same client after a\n * wait (5 seconds, 15, then every minute), with the token handed to the\n * socket again first, or a new one if it has expired. Requests that fail\n * for now are retried every minute; a refused token (`auth_required`,\n * `forbidden`, `not_found`, `validation`) ends the subscription with\n * `refused`.\n *\n * `onChange` carries no data: read the conversation and the unread count\n * again. `onCoachTyping`, when given, says who on the coaching team is\n * writing to the trainee while the channel is live\n * ({@link CoachTypingState}). Poll while the status is `connecting` or\n * `fallback`. Needs\n * `@supabase/supabase-js` 2.87.1 or later (see {@link RealtimeClientFactory}).\n *\n * @example\n * ```ts\n * import { createClient } from \"@supabase/supabase-js\";\n * import { MESSAGES_FALLBACK_POLL_MS, subscribeToMessages } from \"@fytkit/sdk\";\n *\n * const unsubscribe = subscribeToMessages(fytkit, {\n * // A client for the subscription alone, carrying the trainee's token only.\n * realtime: ({ url, anonKey }) =>\n * createClient(url, anonKey, {\n * auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false },\n * }),\n * onChange: () => refreshConversation(),\n * onStatus: (status) => setPolling(status === \"connecting\" || status === \"fallback\"),\n * });\n * ```\n */\nexport function subscribeToMessages(\n client: MessagesRealtimeSource,\n options: SubscribeToMessagesOptions,\n): () => void {\n let closed = false;\n let lastStatus: MessagesRealtimeStatus | null = null;\n // The renewal timer: the next token request, or a retry of one.\n let timer: ReturnType<typeof setTimeout> | null = null;\n let renewing = false;\n let connection: RealtimeConnection | null = null;\n // The client the subscription owns, the grant whose token its socket\n // holds, and the channel once joined: one trainee's, replaced together.\n let socket: RealtimeClientLike | null = null;\n let held: MessagesRealtimeGrant | null = null;\n let channel: RealtimeChannelLike | null = null;\n // A fresh join waiting after Realtime closed the channel, and how many\n // closures came one after another without a subscription between.\n let rejoinTimer: ReturnType<typeof setTimeout> | null = null;\n let closures = 0;\n // Stops listening for a sign-out on the client, once the subscription ends.\n let stopAuthListener: (() => void) | null = null;\n // Who is writing, by their key, oldest first, each with the timer that drops them.\n const writing = new Map<\n string,\n { readonly name: string; readonly expiry: ReturnType<typeof setTimeout> }\n >();\n\n // The names last reported, so a change that shows the same names reports nothing.\n let reportedNames: readonly string[] = [];\n\n const reportTyping = (): void => {\n if (closed) return;\n // Two staff members of one name show it once.\n const names = [...new Set([...writing.values()].map((writer) => writer.name))];\n if (\n names.length === reportedNames.length &&\n names.every((name, i) => name === reportedNames[i])\n ) {\n return;\n }\n reportedNames = names;\n options.onCoachTyping?.(names.length === 0 ? { kind: \"idle\" } : { kind: \"typing\", names });\n };\n\n /** Forgets everyone writing (the channel is lost or replaced), and says so unless it ended. */\n const clearTyping = (): void => {\n if (writing.size === 0) return;\n for (const writer of writing.values()) clearTimeout(writer.expiry);\n writing.clear();\n reportTyping();\n };\n\n const onTypingSign = (sign: CoachTypingSign): void => {\n const known = writing.get(sign.key);\n if (known !== undefined) clearTimeout(known.expiry);\n if (sign.state === \"stopped\") {\n if (known === undefined) return;\n writing.delete(sign.key);\n reportTyping();\n return;\n }\n writing.set(sign.key, {\n name: sign.name,\n expiry: setTimeout(() => {\n writing.delete(sign.key);\n reportTyping();\n }, COACH_TYPING_EXPIRY_MS),\n });\n reportTyping();\n };\n\n const report = (status: MessagesRealtimeStatus): void => {\n if (closed || status === lastStatus) return;\n lastStatus = status;\n // Typing is heard only while live: off it, nobody can say they stopped.\n if (status !== \"live\") clearTyping();\n options.onStatus?.(status);\n };\n\n const clearTimer = (): void => {\n if (timer === null) return;\n clearTimeout(timer);\n timer = null;\n };\n\n const after = (delayMs: number, run: () => Promise<void>): void => {\n clearTimer();\n if (closed) return;\n timer = setTimeout(() => {\n timer = null;\n void run();\n }, delayMs);\n };\n\n const cancelRejoin = (): void => {\n if (rejoinTimer !== null) clearTimeout(rejoinTimer);\n rejoinTimer = null;\n };\n\n /** Leaves the channel and drops the socket; nothing they report counts any more. */\n const drop = (): void => {\n const open = socket;\n const joined = channel;\n socket = null;\n held = null;\n channel = null;\n cancelRejoin();\n closures = 0;\n clearTyping();\n if (open === null) return;\n if (joined !== null) Promise.resolve(open.removeChannel(joined)).then(ignore, ignore);\n open.realtime.disconnect();\n };\n\n const close = (): void => {\n if (closed) return;\n closed = true;\n clearTimer();\n drop();\n const stop = stopAuthListener;\n stopAuthListener = null;\n stop?.();\n };\n\n /**\n * Stops, then reports a last status: nothing is asked or reported again.\n * Stopping first means an `onStatus` that throws can't keep the channel.\n */\n const finish = (status: MessagesRealtimeStatus): void => {\n if (closed) return;\n const changed = status !== lastStatus;\n const wasTyping = writing.size > 0;\n lastStatus = status;\n close();\n if (wasTyping) options.onCoachTyping?.({ kind: \"idle\" });\n if (changed) options.onStatus?.(status);\n };\n\n const join = (joinTopic: string, open: RealtimeClientLike): void => {\n // Unsubscribed, the socket was replaced while it took the token, or a\n // join on it has been made since.\n if (closed || socket !== open || channel !== null) return;\n const joined = open.channel(joinTopic, { config: { private: true } });\n channel = joined;\n // A channel the subscription has left (a new trainee's replaced it,\n // Realtime closed it, or it ended) reports nothing.\n const current = (): boolean => !closed && channel === joined;\n joined\n .on(\"broadcast\", { event: THREAD_CHANGED_EVENT }, () => {\n if (current()) options.onChange();\n })\n .on(\"broadcast\", { event: COACH_TYPING_EVENT }, (message) => {\n const sign = coachTypingSign(message);\n if (sign !== null && current()) onTypingSign(sign);\n })\n .subscribe((status) => {\n if (!current()) return;\n if (status === \"SUBSCRIBED\") {\n closures = 0;\n report(\"live\");\n // Changes between the last read and the join were not heard.\n options.onChange();\n return;\n }\n report(\"fallback\");\n // CHANNEL_ERROR or TIMED_OUT: realtime-js joins this channel again\n // by itself. CLOSED (a Realtime restart, a token that expired before\n // its renewal landed): realtime-js has let it go for good, so join a\n // fresh one after a wait.\n if (status === \"CLOSED\") {\n channel = null;\n scheduleRejoin();\n }\n });\n };\n\n /**\n * Hands `open` the grant's token, then joins its topic: a join sent\n * before the socket holds the token carries the anon key alone, which the\n * private topic refuses.\n */\n const authoriseAndJoin = (grant: MessagesRealtimeGrant, open: RealtimeClientLike): void => {\n void authorise(open, grant.token).then(() => join(grant.topic, open));\n };\n\n /** Joins afresh after Realtime closed the channel, with a token it will take. */\n const rejoin = (): void => {\n const open = socket;\n const grant = held;\n if (closed || open === null || grant === null || channel !== null) return;\n // An expired token would be refused: the renewal joins with a new one.\n if (!isUsable(grant.expiresAt, Date.now())) {\n void renew();\n return;\n }\n authoriseAndJoin(grant, open);\n };\n\n const scheduleRejoin = (): void => {\n if (closed || rejoinTimer !== null) return;\n closures += 1;\n rejoinTimer = setTimeout(() => {\n rejoinTimer = null;\n rejoin();\n }, rejoinDelay(closures));\n };\n\n /** Opens a client on the settings and joins the grant's topic once its socket holds the token. */\n const connect = (grant: MessagesRealtimeGrant, settings: RealtimeConnection): void => {\n let opened: RealtimeClientLike;\n try {\n opened = options.realtime(settings);\n } catch {\n // The app's factory can't make a client (a malformed URL, say):\n // asking again won't change that.\n finish(\"fallback\");\n return;\n }\n socket = opened;\n held = grant;\n after(renewalDelay(grant.expiresAt, Date.now()), renew);\n authoriseAndJoin(grant, opened);\n };\n\n /** Opens the subscription on a grant: its topic, the settings (fetched once), a client. */\n const openGrant = async (grant: MessagesRealtimeGrant): Promise<void> => {\n // A server from before `topic` was added: no topic to join.\n if (!isMemberTopic(grant.topic)) {\n finish(\"fallback\");\n return;\n }\n\n if (connection === null) {\n let realtime: unknown;\n try {\n const bootstrap = await client.app.bootstrap();\n realtime = bootstrap.status === \"ok\" ? bootstrap.data.realtime : undefined;\n } catch (error) {\n if (closed) return;\n if (isFinalError(error)) {\n finish(\"fallback\");\n return;\n }\n report(\"fallback\");\n after(RETRY_MS, start);\n return;\n }\n if (closed) return;\n // A server from before the settings were published.\n if (!isConnection(realtime)) {\n finish(\"fallback\");\n return;\n }\n connection = realtime;\n }\n\n connect(grant, connection);\n };\n\n const renew = async (): Promise<void> => {\n // One request at a time: the one in flight joins when it lands.\n if (renewing) return;\n renewing = true;\n let grant: MessagesRealtimeGrant;\n try {\n grant = (await client.messages.realtimeToken({})).data;\n } catch (error) {\n if (closed) return;\n // A refusal stands: the token from before is not used again.\n if (isFinalError(error)) finish(\"refused\");\n else after(RETRY_MS, renew);\n return;\n } finally {\n renewing = false;\n }\n const current = socket;\n if (closed || current === null || held === null) return;\n if (grant.topic !== held.topic) {\n // Another trainee signed in on the same client: the open channel is\n // the previous trainee's. Leave it and open the new trainee's, as the\n // member portal does when its member changes.\n drop();\n report(\"connecting\");\n await openGrant(grant);\n return;\n }\n held = grant;\n after(renewalDelay(grant.expiresAt, Date.now()), renew);\n if (channel !== null) {\n // The open channel carries on with the new token; no rejoin.\n void authorise(current, grant.token);\n return;\n }\n // Realtime closed the channel: join afresh now, with the new token.\n cancelRejoin();\n authoriseAndJoin(grant, current);\n };\n\n const start = async (): Promise<void> => {\n let grant: MessagesRealtimeGrant;\n try {\n grant = (await client.messages.realtimeToken({})).data;\n } catch (error) {\n if (closed) return;\n if (isFinalError(error)) {\n finish(\"refused\");\n return;\n }\n report(\"fallback\");\n after(RETRY_MS, start);\n return;\n }\n if (closed) return;\n await openGrant(grant);\n };\n\n report(\"connecting\");\n // A sign-out leaves no trainee to listen for: stop now, not at the next\n // renewal, so the previous trainee's channel stays open no longer.\n stopAuthListener =\n client.auth?.onAuthStateChange((event) => {\n if (SIGNED_OUT_EVENTS.has(event.type)) finish(\"refused\");\n }) ?? null;\n void start();\n return close;\n}\n","import { createAppNamespace } from \"./app\";\nimport { createAuthNamespace } from \"./auth\";\nimport { DEFAULT_BASE_URL } from \"./base-url\";\nimport { createDataNamespaces } from \"./data\";\nimport { createLocalStorageAdapter, createMemoryStorageAdapter } from \"./storage\";\nimport { attachRefreshInterceptor, createTransport } from \"./transport\";\nimport type { FytkitClient, FytkitClientOptions } from \"./types\";\n\nexport function createFytkitClient(options: FytkitClientOptions): FytkitClient {\n if (!options || typeof options.clientId !== \"string\" || options.clientId.length === 0) {\n throw new Error(\"createFytkitClient: `clientId` is required\");\n }\n\n const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;\n const storage = options.storage ?? createLocalStorageAdapter(options.clientId);\n\n // `auth` is declared after `transport` because `createAuthNamespace`\n // needs the transport to make its RPC calls. The transport in turn needs\n // a `getSession` that reads the auth cache — we resolve the cycle with a\n // late-bound reference: the closure below reads `auth` lazily, which is\n // safe because no request can fire before construction finishes.\n let auth: ReturnType<typeof createAuthNamespace> | null = null;\n\n const transport = createTransport({\n baseUrl,\n clientId: options.clientId,\n getSession: () => auth?.getSession() ?? null,\n fetch: options.fetch,\n });\n\n const authInstance = createAuthNamespace({ transport, storage });\n auth = authInstance;\n attachRefreshInterceptor({ transport, auth: authInstance });\n\n const data = createDataNamespaces(transport);\n\n return {\n clientId: options.clientId,\n auth: {\n signUp: authInstance.signUp,\n signIn: authInstance.signIn,\n signOut: authInstance.signOut,\n getSession: authInstance.getSession,\n onAuthStateChange: authInstance.onAuthStateChange,\n requestPasswordReset: authInstance.requestPasswordReset,\n confirmPasswordReset: authInstance.confirmPasswordReset,\n confirmEmail: authInstance.confirmEmail,\n resendVerification: authInstance.resendVerification,\n verifyTotp: authInstance.verifyTotp,\n verifyBackupCode: authInstance.verifyBackupCode,\n requestEmailOtp: authInstance.requestEmailOtp,\n verifyEmailOtp: authInstance.verifyEmailOtp,\n getSecurityStatus: authInstance.getSecurityStatus,\n enableTotp: authInstance.enableTotp,\n verifyTotpSetup: authInstance.verifyTotpSetup,\n disableTwoFactor: authInstance.disableTwoFactor,\n regenerateBackupCodes: authInstance.regenerateBackupCodes,\n changePassword: authInstance.changePassword,\n changeEmail: authInstance.changeEmail,\n confirmEmailChange: authInstance.confirmEmailChange,\n listSessions: authInstance.listSessions,\n revokeSession: authInstance.revokeSession,\n revokeOtherSessions: authInstance.revokeOtherSessions,\n closeAccount: authInstance.closeAccount,\n },\n ...data,\n app: createAppNamespace(transport),\n };\n}\n\n// Public re-exports.\nexport { createLocalStorageAdapter, createMemoryStorageAdapter };\n// `PublicApi` is the inferred router type that backs every namespace\n// on `FytkitClient` (e.g. `client.workouts` is `PublicApi[\"workout\"]`).\n// Re-exported so consumers can write generics over the router shape —\n// previously this surfaced as an `ae-forgotten-export` warning on\n// `FytkitClient` in the api-extractor report. The api-extractor will\n// still warn about `PublicRouter`/`publicRouter` being unexported —\n// that is intentional, those are implementation-detail names from the\n// generated snapshot that should not be part of the consumer surface.\nexport type { PublicApi } from \"@fytkit/types\";\nexport type {\n AppBootstrapData,\n AppBootstrapOptions,\n AppBootstrapResult,\n AppNamespace,\n} from \"./app\";\nexport type {\n JoinCodeCoach,\n JoinCodeResolution,\n JoinCodeTheme,\n ResolveJoinCodeOptions,\n} from \"./join\";\nexport { resolveJoinCode } from \"./join\";\nexport type {\n PaymentMode,\n PaymentPlatform,\n PaymentPolicy,\n ResolvePaymentModeOptions,\n} from \"./payment-policy\";\nexport { resolvePaymentMode } from \"./payment-policy\";\nexport type {\n CoachTypingState,\n MessagesRealtimeAuthEvent,\n MessagesRealtimeGrant,\n MessagesRealtimeSource,\n MessagesRealtimeStatus,\n RealtimeChannelLike,\n RealtimeClientFactory,\n RealtimeClientLike,\n RealtimeConnection,\n SubscribeToMessagesOptions,\n} from \"./realtime\";\nexport {\n COACH_TYPING_EXPIRY_MS,\n MESSAGES_FALLBACK_POLL_MS,\n subscribeToMessages,\n} from \"./realtime\";\nexport type {\n FytkitRefusal,\n FytkitRefusalDataByReason,\n FytkitRefusalReason,\n FytkitRefusalRetryAt,\n} from \"./refusal\";\nexport { refusalData } from \"./refusal\";\nexport type {\n AuthState,\n FytkitClient,\n FytkitClientOptions,\n FytkitError,\n FytkitErrorCode,\n ResendVerificationResult,\n SecurityStatus,\n Session,\n SessionInfo,\n SignInResult,\n TokenStorage,\n TotpEnrollment,\n TwoFactorChallenge,\n TwoFactorMethod,\n TwoFactorToken,\n} from \"./types\";\n"]}
1
+ {"version":3,"sources":["../src/is-record.ts","../src/refusal.ts","../src/error.ts","../src/app.ts","../src/events.ts","../src/auth.ts","../src/base-url.ts","../src/data.ts","../src/storage.ts","../src/retry-after.ts","../src/transport.ts","../src/join.ts","../src/payment-policy.ts","../src/realtime.ts","../src/index.ts"],"names":["cached"],"mappings":"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","file":"index.js","sourcesContent":["// A guard shared by the SDK's readers of loosely typed server bodies\n// (`refusal.ts`, `retry-after.ts`). Internal: not exported from the package.\n\n/** A plain object (not null, not an array) whose keys can be read. */\nexport function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n","import { isRecord } from \"./is-record\";\n\n/**\n * Machine-readable reasons the server attaches to a refusal, as\n * `refusal.reason` on a `FytkitError`. Branch on these rather than on message\n * text. The union is open: the server may add reasons without a breaking SDK\n * release, so always keep a fallback branch.\n *\n * Check-ins (`checkIns.*`):\n * - `\"entitlement_required\"` (`forbidden`, 403): the member's plan lacks\n * `coaching_checkins`. On `checkIns.current` the data also says whether the\n * member is scheduled and has sent check-ins before (`member`).\n * - `\"already_submitted\"` (`conflict`, 409): this period already has a\n * check-in, `checkinId`; open or edit it instead.\n * - `\"reviewed\"` (`conflict`, 409): the coach reviewed it, so it is locked.\n * - `\"questions_changed\"` (`conflict`, 409): the coach changed the questions;\n * read `checkIns.current` again.\n * - `\"not_scheduled\"`, `\"not_open\"` (with `opensOn`), `\"not_current_period\"`\n * (with `currentPeriodStart`), `\"invalid_photos\"` (with `uploadIds`)\n * (`server`, 400).\n * - `\"invalid_answers\"` (`validation`, 400): `issues` by question.\n *\n * Messages (`messages.*`):\n * - `\"entitlement_required\"` (`forbidden`, 403): the member may not send now;\n * `eligibility` says whether they need an upgrade (`\"upgrade_required\"`) or\n * should wait for a coach to reply (`\"wait_for_coach\"`).\n * - `\"invalid_attachments\"` (`server`, 400): `uploadIds` that can't be sent.\n * - `\"per_minute_limit\"`, `\"daily_limit\"` (`rate_limited`, 429): with `limit`\n * and `retryAt`, when sending is allowed again.\n *\n * Photo uploads (`checkIns.requestPhotoUpload`,\n * `messages.requestAttachmentUpload`):\n * - `\"too_many_pending_uploads\"` (`rate_limited`, 429): with `limit` and\n * `retryAt`.\n *\n * Subscription checkout (`subscription.createCheckout`):\n * - `\"already_subscribed\"`, `\"payment_issue\"` (`conflict`, 409).\n * - `\"max_clients\"` (`forbidden`, 403): the studio is full and takes no new\n * paying members. The server names this one by its quota (`quotaCode`)\n * and sends nothing else with it; the SDK reports it as a reason like the\n * rest, with no data.\n *\n * Billing portal (`subscription.createPortal`):\n * - `\"no_billing_account\"` (`not_found`, 404): the member never started a\n * paid plan here; hide \"Manage billing\".\n * - `\"no_connected_account\"`, `\"portal_unavailable\"` (`server`, 400): the\n * studio's billing isn't set up; send the member to their coach.\n *\n * Habits (`habits.*`):\n * - `\"entitlement_required\"` (`forbidden`, 403): the member's plan lacks\n * `coaching_habits`. On `habits.today` the data also says whether their\n * coach gave them a habit that hasn't ended (`member.assigned`): show an\n * upgrade prompt when it is true.\n *\n * Weigh-ins and measurements (`bodyMetrics.update`, `bodyMetrics.delete`,\n * `bodyMeasurements.delete`):\n * - `\"staff_entered\"` (`conflict`, 409): their coach entered it; it is the\n * coach's to change or remove.\n * - `\"checkin_sourced\"` (`conflict`, 409, `bodyMetrics.update`): a check-in\n * recorded it; change it by editing the check-in.\n *\n * Progress photos (`progressPhotos.*`):\n * - `\"consent_required\"` (`conflict`, 409): progress photos are off.\n * - `\"photo_limit_reached\"` (`conflict`, 409): the timeline holds `limit`\n * photos.\n * - `\"invalid_uploads\"` (`server`, 400): `uploadIds` that can't be added.\n * - `\"poses_differ\"` (`server`, 400): compare two photos of the same pose.\n *\n * Health (`health.submitIntake`, `health.reportInjury`):\n * - `\"consent_required\"` (`conflict`, 409, `reportInjury`): no consent on\n * file at `currentVersion`; send the report again with consent.\n * - `\"consent_outdated\"` (`conflict`, 409): the consent wording changed to\n * `currentVersion`; show it again.\n * - `\"too_many_pending\"` (`conflict`, 409, `reportInjury`): `limit` reports\n * already wait for the coach.\n * - `\"questions_changed\"` (`conflict`, 409), `\"invalid_answers\"`\n * (`validation`, 400) on `submitIntake`, as on check-ins.\n *\n * Join codes (`resolveJoinCode`):\n * - `\"join_code_inactive\"` (`not_found`, 404): no studio has the code, or\n * the studio replaced it; ask the member to get a new one from their coach.\n */\nexport type FytkitRefusalReason =\n | \"entitlement_required\"\n | \"already_submitted\"\n | \"reviewed\"\n | \"questions_changed\"\n | \"not_scheduled\"\n | \"not_open\"\n | \"not_current_period\"\n | \"invalid_answers\"\n | \"invalid_photos\"\n | \"invalid_attachments\"\n | \"per_minute_limit\"\n | \"daily_limit\"\n | \"too_many_pending_uploads\"\n | \"already_subscribed\"\n | \"payment_issue\"\n | \"max_clients\"\n | \"no_billing_account\"\n | \"no_connected_account\"\n | \"portal_unavailable\"\n | \"staff_entered\"\n | \"checkin_sourced\"\n | \"consent_required\"\n | \"consent_outdated\"\n | \"too_many_pending\"\n | \"photo_limit_reached\"\n | \"invalid_uploads\"\n | \"poses_differ\"\n | \"join_code_inactive\"\n // Keeps the union open (and autocompletion for the known reasons).\n | (string & {});\n\n/**\n * The server's structured refusal on a `FytkitError`: why it said no and\n * what it sent with that. Present only when the server named a reason.\n */\nexport interface FytkitRefusal {\n /** The machine-readable reason (see `FytkitRefusalReason`). */\n readonly reason: FytkitRefusalReason;\n /** The rest of the error's data, without `reason`. */\n readonly data: Readonly<Record<string, unknown>>;\n}\n\n/** When a limited action is allowed again, as the 429 refusals send it. */\nexport interface FytkitRefusalRetryAt {\n /** The limit that was reached. */\n readonly limit: number;\n /** ISO 8601 instant when the limit lifts. */\n readonly retryAt: string;\n /** Milliseconds from the refusal until `retryAt`; also the error's `retryAfter`. */\n readonly retryAfterMs?: number;\n}\n\n/**\n * What each known reason's `refusal.data` holds, for `refusalData`. The\n * server documents these shapes; the SDK does not check them at runtime, as\n * it trusts every other response shape. An empty record is a refusal that\n * sends nothing beyond its reason.\n */\nexport interface FytkitRefusalDataByReason {\n /** A coaching feature the member's plan lacks. */\n readonly entitlement_required:\n | {\n readonly entitlement: \"coaching_checkins\";\n /** On `checkIns.current` only: whether to offer check-ins at all. */\n readonly member?: { readonly scheduled: boolean; readonly hasCheckins: boolean };\n }\n | {\n readonly entitlement: \"coaching_messages\";\n readonly eligibility: \"upgrade_required\" | \"wait_for_coach\";\n }\n | {\n readonly entitlement: \"coaching_habits\";\n /** On `habits.today` only: whether their coach gave them a habit that hasn't ended. */\n readonly member?: { readonly assigned: boolean };\n };\n /** The check-in already sent for this period. */\n readonly already_submitted: { readonly checkinId: string; readonly submittedAt: string };\n /** The check-in the coach reviewed, so it is locked. */\n readonly reviewed: { readonly checkinId: string };\n /** The coach changed the questions; read `checkIns.current` again. */\n readonly questions_changed: Readonly<Record<string, never>>;\n /** The member has no check-in schedule. */\n readonly not_scheduled: Readonly<Record<string, never>>;\n /** `opensOn` is a calendar date, `YYYY-MM-DD`. */\n readonly not_open: { readonly opensOn: string };\n /** `currentPeriodStart` is a calendar date, `YYYY-MM-DD`. */\n readonly not_current_period: { readonly currentPeriodStart: string };\n /** Answers that don't fit their questions, by question (null: the whole check-in). */\n readonly invalid_answers: {\n readonly issues: readonly { readonly questionId: string | null; readonly message: string }[];\n };\n /** Photos that can't be added to the check-in; upload them again. */\n readonly invalid_photos: { readonly uploadIds: readonly string[] };\n /** Photos that can't be sent with the message; upload them again. */\n readonly invalid_attachments: { readonly uploadIds: readonly string[] };\n /** 20 messages a minute. */\n readonly per_minute_limit: FytkitRefusalRetryAt;\n /** 300 messages a day, until midnight in the studio's time zone. */\n readonly daily_limit: FytkitRefusalRetryAt;\n /** 20 photos requested but not yet sent. */\n readonly too_many_pending_uploads: FytkitRefusalRetryAt;\n /** The member already has an active subscription. */\n readonly already_subscribed: Readonly<Record<string, never>>;\n /** The member's last payment failed and the old subscription is still live. */\n readonly payment_issue: Readonly<Record<string, never>>;\n /** The studio is full. Nothing beyond the reason: the refusal carries no figures. */\n readonly max_clients: Readonly<Record<string, never>>;\n /** The member has no billing account at this studio. */\n readonly no_billing_account: Readonly<Record<string, never>>;\n /** The studio has no connected payments account. */\n readonly no_connected_account: Readonly<Record<string, never>>;\n /** The studio hasn't set up its billing portal. */\n readonly portal_unavailable: Readonly<Record<string, never>>;\n /** Their coach entered this weigh-in or measurement. */\n readonly staff_entered: Readonly<Record<string, never>>;\n /** A check-in recorded this weigh-in; edit the check-in instead. */\n readonly checkin_sourced: Readonly<Record<string, never>>;\n /**\n * Health: no consent on file at `currentVersion` (`health.reportInjury`).\n * Progress photos: they are off, and nothing else is sent.\n */\n readonly consent_required: { readonly currentVersion?: string };\n /** The health-data consent wording changed; `currentVersion` is the current one. */\n readonly consent_outdated: { readonly currentVersion: string };\n /** `limit` injury reports already wait for the coach. */\n readonly too_many_pending: { readonly limit: number };\n /** The progress-photo timeline holds `limit` photos. */\n readonly photo_limit_reached: { readonly limit: number };\n /** Photo uploads that can't be added; upload them again. */\n readonly invalid_uploads: { readonly uploadIds: readonly string[] };\n /** Two photos of different poses can't be compared. */\n readonly poses_differ: Readonly<Record<string, never>>;\n /** No studio has the join code, or the studio replaced it. */\n readonly join_code_inactive: Readonly<Record<string, never>>;\n}\n\n/**\n * The data of `error`'s refusal when its reason is `reason`, typed for that\n * reason; null when `error` is not a `FytkitError`, carries no refusal, or\n * was refused for another reason.\n *\n * ```ts\n * catch (err) {\n * const sent = refusalData(err, \"already_submitted\");\n * if (sent) return openCheckIn(sent.checkinId);\n * }\n * ```\n */\nexport function refusalData<R extends keyof FytkitRefusalDataByReason>(\n error: unknown,\n reason: R,\n): FytkitRefusalDataByReason[R] | null {\n if (!isRecord(error) || typeof error.kind !== \"string\") return null;\n const refusal = error.refusal;\n if (!isRecord(refusal) || refusal.reason !== reason || !isRecord(refusal.data)) return null;\n // The server's documented shape for this reason (see FytkitRefusalDataByReason).\n return refusal.data as FytkitRefusalDataByReason[R];\n}\n\n/**\n * The member cap is the one refusal the server names by its quota\n * (`quotaCode`) rather than a `reason`: a studio that takes no new members\n * refuses a member's checkout with it. The refusal carries no figures, only\n * `quotaCode`, and the SDK passes the reason alone: should anything else ever\n * arrive beside it, it is dropped rather than handed to the app.\n */\nconst MEMBER_CAP_QUOTA = \"max_clients\";\n\n/**\n * The refusal in an error's `data`, as a spread-able object so a kind only\n * gains the key when the server named a reason (existing `toEqual` shapes\n * stay intact): the reason, and the rest of the data without it.\n */\nexport function pickRefusal(data: unknown): { readonly refusal?: FytkitRefusal } {\n if (!isRecord(data)) return {};\n const { reason, ...rest } = data;\n if (typeof reason === \"string\" && reason.length > 0) return { refusal: { reason, data: rest } };\n if (data.quotaCode === MEMBER_CAP_QUOTA)\n return { refusal: { reason: MEMBER_CAP_QUOTA, data: {} } };\n return {};\n}\n","import { ORPCError } from \"@orpc/client\";\nimport { pickRefusal } from \"./refusal\";\nimport type { FytkitError } from \"./types\";\n\ninterface MaybeIssuesData {\n issues?: readonly unknown[];\n}\ninterface MaybeRetryAfterData {\n retryAfterMs?: number;\n}\n\nfunction pickIssues(data: unknown): readonly unknown[] | null {\n if (!data || typeof data !== \"object\") return null;\n const issues = (data as MaybeIssuesData).issues;\n return Array.isArray(issues) ? issues : null;\n}\n\nfunction pickRetryAfter(data: unknown): number {\n if (!data || typeof data !== \"object\") return 0;\n const raw = (data as MaybeRetryAfterData).retryAfterMs;\n return typeof raw === \"number\" && raw >= 0 ? raw : 0;\n}\n\n/**\n * A non-empty string field of the error's `data`, when the server sent one:\n * `code` (the machine-readable reason, e.g. \"invalid_password\") or\n * `enrollmentId` (the existing enrolment on \"Already enrolled\"). Returned as\n * a spread-able object so kinds only gain the key when there is a value —\n * existing `toEqual` shapes stay intact.\n */\nfunction pickStringField<K extends \"code\" | \"enrollmentId\">(\n data: unknown,\n key: K,\n): { [P in K]?: string } {\n if (!data || typeof data !== \"object\") return {};\n const raw = (data as Partial<Record<K, unknown>>)[key];\n return typeof raw === \"string\" && raw.length > 0 ? ({ [key]: raw } as { [P in K]?: string }) : {};\n}\n\n/**\n * Map any error thrown by `@orpc/client` (or the underlying fetch) into the\n * SDK's `FytkitError` discriminated union. The SDK never re-throws raw orpc\n * or network errors at the public surface.\n *\n * A refusal the server named (`data.reason`, see `FytkitRefusal`) rides along\n * as `refusal` on the kinds that can carry one, beside the fields each kind\n * already had: `forbidden.reason` stays the message text.\n */\nexport function normalizeError(err: unknown): FytkitError {\n // Network failures from fetch surface as TypeError.\n if (err instanceof TypeError) return { kind: \"network\" };\n\n if (err instanceof ORPCError) {\n const code = err.code;\n const status = err.status;\n const data: unknown = err.data;\n const message = err.message;\n\n switch (code) {\n case \"UNAUTHORIZED\":\n // Carry the server's reason (e.g. \"Invalid credentials\" on a failed\n // sign-in) so the consumer can show it instead of a generic\n // 'session expired' copy. The synthetic no-session case in auth.ts\n // is built directly and stays message-less.\n return { kind: \"auth_required\", message };\n case \"FORBIDDEN\":\n return {\n kind: \"forbidden\",\n reason: message || \"Forbidden\",\n ...pickStringField(data, \"code\"),\n ...pickRefusal(data),\n };\n case \"PAYMENT_REQUIRED\":\n return { kind: \"payment_required\", message: message || \"Payment Required\" };\n case \"CONFLICT\":\n return {\n kind: \"conflict\",\n message: message || \"Conflict\",\n ...pickStringField(data, \"enrollmentId\"),\n ...pickRefusal(data),\n };\n case \"NOT_FOUND\":\n return { kind: \"not_found\", ...pickRefusal(data) };\n case \"TOO_MANY_REQUESTS\":\n return { kind: \"rate_limited\", retryAfter: pickRetryAfter(data), ...pickRefusal(data) };\n case \"BAD_REQUEST\":\n case \"UNPROCESSABLE_CONTENT\": {\n const issues = pickIssues(data);\n if (issues) return { kind: \"validation\", issues, ...pickRefusal(data) };\n // No structured issues — still carry the server's explanation (and\n // its code, e.g. \"invalid_password\") so it isn't dropped into a bare\n // status code.\n return {\n kind: \"server\",\n status,\n message,\n ...pickStringField(data, \"code\"),\n ...pickRefusal(data),\n };\n }\n default:\n return {\n kind: \"server\",\n status,\n message,\n ...pickStringField(data, \"code\"),\n ...pickRefusal(data),\n };\n }\n }\n\n return { kind: \"server\", status: 0 };\n}\n","// `app.bootstrap` (#535): Fytkit Coach's one launch read. Everything the app\n// paints and gates at launch, for the studio the client id names: its name\n// and logo, the app theme, feature overrides, the minimum app version, the\n// legal links, the Realtime settings and the payment policy.\n//\n// The payload is a pure function of the studio's settings and carries a\n// strong ETag. A call with the ETag the app stored last time is answered 304\n// with no body when nothing changed. The typed oRPC client resolves a 304's\n// empty body to `undefined` (its type says it never does), so this wraps the\n// raw procedure in a discriminated union instead of handing that through.\n\nimport type { PublicApi } from \"@fytkit/types\";\nimport { normalizeError } from \"./error\";\nimport type { Transport } from \"./transport\";\nimport type { FytkitError } from \"./types\";\n\n/**\n * Everything the app reads at launch for the studio (`app.bootstrap`'s\n * `data`): `studio` (display name and logo URL), `theme` (the effective\n * template and theme with its palette, derived variants and the oldest app\n * release that can paint it), `themeMinAppVersions`, `home.layout`,\n * `featureOverrides`, `minAppVersion`, `links` (terms, privacy, help),\n * `realtime` (the public Supabase Realtime settings) and `paymentPolicy` (the\n * storefronts where the app may link out to subscribe; read it with\n * `resolvePaymentMode`).\n */\nexport type AppBootstrapData = Awaited<ReturnType<PublicApi[\"app\"][\"bootstrap\"]>>[\"data\"];\n\n/** Options for `app.bootstrap`. */\nexport interface AppBootstrapOptions {\n /**\n * The `etag` of the last bootstrap the app kept. When the studio's\n * settings are unchanged the server answers 304 and the call resolves to\n * `{ status: \"unchanged\" }`, so keep using the data stored with it. Omit it\n * (or pass `null`) on a first launch, or when no data is stored.\n */\n readonly etag?: string | null;\n}\n\n/**\n * What `app.bootstrap` resolves to:\n * - `ok`: the current settings, and the `etag` to store beside them and send\n * next time;\n * - `unchanged`: the `etag` you sent is still current (a 304 with no body):\n * the data you stored with it is up to date.\n */\nexport type AppBootstrapResult =\n | { readonly status: \"ok\"; readonly data: AppBootstrapData; readonly etag: string }\n | { readonly status: \"unchanged\"; readonly etag: string };\n\n/** `client.app`: the app's launch read. */\nexport interface AppNamespace {\n /**\n * Reads the studio's app launch settings. Needs the client id alone; a\n * signed-in member's session changes nothing in the payload. Pass the\n * `etag` of the data you stored last time to revalidate it: an unchanged\n * studio answers `{ status: \"unchanged\" }` without a body.\n *\n * Rejects with a `FytkitError`, as every SDK method does (e.g.\n * `{ kind: \"not_found\" }` for a studio that no longer exists,\n * `{ kind: \"network\" }` offline): keep painting the stored data then.\n *\n * ```ts\n * const stored = await loadBootstrap(); // { data, etag } | null\n * const result = await fytkit.app.bootstrap({ etag: stored?.etag });\n * const data = result.status === \"ok\" ? result.data : stored.data;\n * if (result.status === \"ok\") await saveBootstrap(result);\n * ```\n */\n readonly bootstrap: (options?: AppBootstrapOptions) => Promise<AppBootstrapResult>;\n}\n\n/** A bodiless answer (a 304) to a call that sent no `If-None-Match`. */\nconst UNEXPECTED_NOT_MODIFIED: FytkitError = { kind: \"server\", status: 304 };\n\nexport function createAppNamespace(transport: Transport): AppNamespace {\n const bootstrap: AppNamespace[\"bootstrap\"] = async (options = {}) => {\n const etag = options.etag ?? null;\n // A 304 resolves to `undefined` at runtime, whatever the client's type says.\n let output: Awaited<ReturnType<PublicApi[\"app\"][\"bootstrap\"]>> | undefined;\n try {\n output = await transport.client.app.bootstrap(undefined, {\n context: etag === null ? {} : { ifNoneMatch: etag },\n });\n } catch (error) {\n throw normalizeError(error);\n }\n if (output === undefined || output === null) {\n if (etag === null) throw UNEXPECTED_NOT_MODIFIED;\n return { status: \"unchanged\", etag };\n }\n return { status: \"ok\", data: output.data, etag: output.meta.etag };\n };\n return { bootstrap };\n}\n","export interface EventBus<T> {\n readonly on: (listener: (value: T) => void) => () => void;\n readonly emit: (value: T) => void;\n}\n\nexport function createEventBus<T>(): EventBus<T> {\n const listeners = new Set<(value: T) => void>();\n return {\n on: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n emit: (value) => {\n for (const listener of listeners) {\n try {\n listener(value);\n } catch (err) {\n // A buggy listener must not break siblings or the calling flow.\n console.error(\"[fytkit-sdk] event listener threw\", err);\n }\n }\n },\n };\n}\n","import { normalizeError } from \"./error\";\nimport { createEventBus } from \"./events\";\nimport type { Transport } from \"./transport\";\nimport type {\n AuthState,\n CloseAccountInput,\n FytkitError,\n IdTokenProvider,\n IdTokenSignInResult,\n ResendVerificationResult,\n SecurityStatus,\n Session,\n SessionInfo,\n SignInResult,\n TokenStorage,\n TotpEnrollment,\n TwoFactorChallenge,\n TwoFactorToken,\n} from \"./types\";\n\nexport interface AuthNamespaceOptions {\n readonly transport: Transport;\n readonly storage: TokenStorage;\n /** Called after the SDK signs out for any reason (manual, refresh failure). */\n readonly onSignedOut?: () => void;\n}\n\nexport interface AuthNamespace {\n readonly signUp: (input: { email: string; password: string; name: string }) => Promise<Session>;\n readonly signIn: (input: { email: string; password: string }) => Promise<SignInResult>;\n /**\n * Sign in, sign up or join this studio with an Apple or Google ID token.\n * Like `signIn`, a credential call: a 401 never refreshes or signs out.\n */\n readonly signInWithIdToken: (input: {\n provider: IdTokenProvider;\n idToken: string;\n nonce: string;\n name?: string;\n authorizationCode?: string;\n }) => Promise<IdTokenSignInResult>;\n readonly signOut: () => Promise<void>;\n readonly getSession: () => Session | null;\n readonly onAuthStateChange: (listener: (state: AuthState) => void) => () => void;\n readonly requestPasswordReset: (input: { email: string }) => Promise<void>;\n readonly confirmPasswordReset: (input: { token: string; password: string }) => Promise<void>;\n readonly confirmEmail: (input: { token: string }) => Promise<void>;\n /**\n * Mail a new verification link to the signed-in member's own address. A\n * bearer call: a 401 refreshes or signs out as usual.\n */\n readonly resendVerification: () => Promise<ResendVerificationResult>;\n readonly verifyTotp: (input: { challenge: TwoFactorChallenge; code: string }) => Promise<Session>;\n readonly verifyBackupCode: (input: {\n challenge: TwoFactorChallenge;\n code: string;\n }) => Promise<Session>;\n readonly requestEmailOtp: (input: { challenge: TwoFactorChallenge }) => Promise<void>;\n readonly verifyEmailOtp: (input: {\n challenge: TwoFactorChallenge;\n code: string;\n }) => Promise<Session>;\n readonly getSecurityStatus: () => Promise<SecurityStatus>;\n readonly enableTotp: (input: { password: string }) => Promise<TotpEnrollment>;\n readonly verifyTotpSetup: (input: { code: string }) => Promise<void>;\n readonly disableTwoFactor: (input: { password: string }) => Promise<void>;\n readonly regenerateBackupCodes: (input: { password: string }) => Promise<readonly string[]>;\n readonly changePassword: (input: {\n currentPassword: string;\n newPassword: string;\n revokeOtherSessions?: boolean;\n }) => Promise<void>;\n readonly changeEmail: (input: { newEmail: string }) => Promise<void>;\n /**\n * Confirm an email change with the token from the confirmation link, for an\n * app that opens that link itself (Fytkit Coach's app link). No session\n * needed: the emailed token is the proof.\n */\n readonly confirmEmailChange: (input: { token: string }) => Promise<void>;\n readonly listSessions: () => Promise<readonly SessionInfo[]>;\n readonly revokeSession: (input: { revokeId: string }) => Promise<void>;\n readonly revokeOtherSessions: () => Promise<void>;\n /**\n * Close the account after re-verifying the email and exactly one proof:\n * the password, or a fresh Apple/Google ID token for an account without a\n * password. Clears the session on success; every refusal keeps it.\n */\n readonly closeAccount: (input: CloseAccountInput) => Promise<void>;\n /** Internal: force a refresh now. Wired by transport's 401 retry path. */\n readonly _refresh: () => Promise<Session>;\n /** Internal: clear cached session + emit signed_out. */\n readonly _signOutLocal: () => void;\n}\n\n/**\n * Schedule a proactive refresh this many ms before `expiresAt`. The token\n * is a server session (7 days, extended on use), and a 60-second lead time\n * refreshes it transparently in long-running tabs without refreshing\n * needlessly.\n */\nconst PROACTIVE_REFRESH_LEAD_MS = 60_000;\n\n/**\n * When a proactive refresh fails for a reason other than the server rejecting\n * the token (offline, 5xx, rate limit), the session is kept and the timer\n * retries with exponential backoff: 5 s, 10 s, 20 s … capped at 5 min, for\n * at most this many attempts. After that the SDK stops polling; the next\n * request's reactive 401 path takes over if the token has actually lapsed.\n */\nconst REFRESH_RETRY_BASE_MS = 5_000;\nconst REFRESH_RETRY_MAX_MS = 5 * 60_000;\nconst REFRESH_MAX_RETRIES = 6;\n\n/**\n * Only an explicit server rejection of the token (401/403) ends the session.\n * Network failures, 5xx and rate limits say nothing about the token, so\n * signing the member out on them would be wrong — and on a phone that drops\n * signal it signs members out constantly.\n */\nfunction isSessionRejected(err: FytkitError): boolean {\n return err.kind === \"auth_required\" || err.kind === \"forbidden\";\n}\n\nfunction refreshRetryDelay(attempt: number, err: FytkitError): number {\n const backoff = Math.min(REFRESH_RETRY_MAX_MS, REFRESH_RETRY_BASE_MS * 2 ** attempt);\n // Honour the server's rate-limit window when it is longer than our backoff.\n return err.kind === \"rate_limited\" ? Math.max(backoff, err.retryAfter) : backoff;\n}\n\n/**\n * The auth namespace owns the cached session. It is the single writer to\n * `storage`; everything else reads via `getSession`. Calls that mutate\n * the session also emit on the event bus and (re)schedule the proactive\n * refresh timer against the new expiry.\n */\nexport function createAuthNamespace(opts: AuthNamespaceOptions): AuthNamespace {\n const bus = createEventBus<AuthState>();\n let cached: Session | null = null;\n let timer: ReturnType<typeof setTimeout> | null = null;\n // Forward declaration so scheduleRefresh can call the same _refresh body\n // the public surface exposes. Assigned at the bottom of the factory.\n let refreshFn: () => Promise<Session> = () => {\n throw new Error(\"refreshFn called before initialization\");\n };\n\n function clearTimer(): void {\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n }\n\n // `setTimeout` clamps any delay > 2^31-1 ms (~24.8 days) to 1 ms and\n // prints a Node deprecation warning. Sessions with an exotic far-future\n // expiry (test fixtures, mis-configured server) would otherwise fire the\n // timer instantly; cap the delay to the safe upper bound so the timer\n // simply re-arms on its next firing.\n const MAX_TIMEOUT_MS = 2_147_483_647;\n\n // `attempt` counts consecutive failed proactive refreshes; 0 for the\n // expiry-driven firing, 1+ for backoff retries.\n function armRefreshTimer(delay: number, attempt: number): void {\n clearTimer();\n timer = setTimeout(() => {\n timer = null;\n void refreshFn().catch((err: FytkitError) => {\n // A rejected token already cleared the session inside _refresh.\n // Otherwise keep it and retry — unless something re-armed the timer\n // meanwhile (a sign-in, a rotated token) or we are out of attempts.\n if (cached === null || timer !== null || attempt >= REFRESH_MAX_RETRIES) return;\n armRefreshTimer(refreshRetryDelay(attempt, err), attempt + 1);\n });\n }, delay);\n }\n\n function scheduleRefresh(session: Session): void {\n clearTimer();\n const expiresMs = new Date(session.expiresAt).getTime();\n if (Number.isNaN(expiresMs)) return; // bad date — bail; reactive 401 path will handle it\n const rawDelay = expiresMs - Date.now() - PROACTIVE_REFRESH_LEAD_MS;\n armRefreshTimer(Math.min(MAX_TIMEOUT_MS, Math.max(0, rawDelay)), 0);\n }\n\n function applySession(session: Session, event: \"signed_in\" | \"session_refreshed\"): Session {\n cached = session;\n void opts.storage.set(session);\n bus.emit({ type: event, session });\n scheduleRefresh(session);\n return session;\n }\n\n function clearSession(emit = true): void {\n cached = null;\n clearTimer();\n void opts.storage.remove();\n if (emit) bus.emit({ type: \"signed_out\" });\n opts.onSignedOut?.();\n }\n\n async function call<T>(thunk: () => Promise<T>): Promise<T> {\n try {\n return await thunk();\n } catch (err) {\n throw normalizeError(err);\n }\n }\n\n // Best-effort hydrate at construction. If the storage already holds a\n // session (warm reload, second tab), restore it and arm the timer.\n void Promise.resolve(opts.storage.get()).then((s) => {\n if (s) {\n cached = s;\n scheduleRefresh(s);\n }\n });\n\n const signUp: AuthNamespace[\"signUp\"] = async (input) => {\n const result = await call(() => opts.transport.client.auth.signUp(input));\n const { user, session } = result.data;\n return applySession(\n { accessToken: session.accessToken, expiresAt: session.expiresAt, user },\n \"signed_in\",\n );\n };\n\n const signIn: AuthNamespace[\"signIn\"] = async (input) => {\n const result = await call(() => opts.transport.client.auth.signIn(input));\n if (result.data.kind === \"two_factor_required\") {\n // Mint the branded token exactly once at the network boundary.\n const token = result.data.twoFactorToken as TwoFactorToken;\n return {\n kind: \"two_factor_required\",\n challenge: { token, availableMethods: result.data.availableMethods },\n };\n }\n if (result.data.kind === \"signed_in\") {\n const { user, session } = result.data;\n const cached = applySession(\n { accessToken: session.accessToken, expiresAt: session.expiresAt, user },\n \"signed_in\",\n );\n return { kind: \"signed_in\", session: cached };\n }\n // Exhaustiveness guard — a future server-side variant must not silently fall through.\n const _exhaustive: never = result.data;\n throw _exhaustive;\n };\n\n const signInWithIdToken: AuthNamespace[\"signInWithIdToken\"] = async (input) => {\n const result = await call(() =>\n opts.transport.client.auth.signInWithIdToken({\n provider: input.provider,\n idToken: input.idToken,\n nonce: input.nonce,\n name: input.name,\n authorizationCode: input.authorizationCode,\n }),\n );\n if (result.data.kind === \"two_factor_required\") {\n // Mint the branded token exactly once at the network boundary.\n const token = result.data.twoFactorToken as TwoFactorToken;\n return {\n kind: \"two_factor_required\",\n challenge: { token, availableMethods: result.data.availableMethods },\n };\n }\n if (result.data.kind === \"signed_in\") {\n const { user, session, memberCreated } = result.data;\n const cached = applySession(\n { accessToken: session.accessToken, expiresAt: session.expiresAt, user },\n \"signed_in\",\n );\n return { kind: \"signed_in\", session: cached, memberCreated };\n }\n // Exhaustiveness guard — a future server-side variant must not silently fall through.\n const _exhaustive: never = result.data;\n throw _exhaustive;\n };\n\n const signOut: AuthNamespace[\"signOut\"] = async () => {\n const current = cached;\n if (!current) return;\n try {\n await opts.transport.client.auth.signOut({ token: current.accessToken });\n } catch (err) {\n // Even if the server rejects (e.g. token already revoked), clear\n // the local cache so the user is signed out client-side.\n void err;\n }\n clearSession();\n };\n\n const getSession: AuthNamespace[\"getSession\"] = () => cached;\n const onAuthStateChange: AuthNamespace[\"onAuthStateChange\"] = (listener) => bus.on(listener);\n\n const requestPasswordReset: AuthNamespace[\"requestPasswordReset\"] = async (input) => {\n await call(() => opts.transport.client.auth.requestPasswordReset(input));\n };\n const confirmPasswordReset: AuthNamespace[\"confirmPasswordReset\"] = async (input) => {\n await call(() => opts.transport.client.auth.confirmPasswordReset(input));\n };\n const confirmEmail: AuthNamespace[\"confirmEmail\"] = async (input) => {\n await call(() => opts.transport.client.auth.confirmEmail(input));\n };\n\n const resendVerification: AuthNamespace[\"resendVerification\"] = async () => {\n const result = await call(() => opts.transport.client.auth.resendVerification({}));\n return result.data;\n };\n\n // --- Two-factor sign-in completion -----------------------------------------\n //\n // Each consumes the challenge minted by `signIn` and, on success, caches the\n // issued session exactly like the signed_in branch of `signIn`.\n\n const verifyTotp: AuthNamespace[\"verifyTotp\"] = async (input) => {\n const result = await call(() =>\n opts.transport.client.auth.verifyTotp({ token: input.challenge.token, code: input.code }),\n );\n const { user, session } = result.data;\n return applySession(\n { accessToken: session.accessToken, expiresAt: session.expiresAt, user },\n \"signed_in\",\n );\n };\n\n const verifyBackupCode: AuthNamespace[\"verifyBackupCode\"] = async (input) => {\n const result = await call(() =>\n opts.transport.client.auth.verifyBackupCode({\n token: input.challenge.token,\n code: input.code,\n }),\n );\n const { user, session } = result.data;\n return applySession(\n { accessToken: session.accessToken, expiresAt: session.expiresAt, user },\n \"signed_in\",\n );\n };\n\n const requestEmailOtp: AuthNamespace[\"requestEmailOtp\"] = async (input) => {\n await call(() => opts.transport.client.auth.requestEmailOtp({ token: input.challenge.token }));\n };\n\n const verifyEmailOtp: AuthNamespace[\"verifyEmailOtp\"] = async (input) => {\n const result = await call(() =>\n opts.transport.client.auth.verifyEmailOtp({ token: input.challenge.token, code: input.code }),\n );\n const { user, session } = result.data;\n return applySession(\n { accessToken: session.accessToken, expiresAt: session.expiresAt, user },\n \"signed_in\",\n );\n };\n\n /**\n * The server rotated the calling session and returned the replacement token\n * (TOTP-setup verification and password change with revokeOtherSessions do\n * this). Swap the cached token immediately — the old bearer is already dead,\n * so any request between now and the swap would 401. `expiresAt` is carried\n * over from the old session; it is at worst *earlier* than the new session's\n * real expiry, so the proactive refresh timer fires early and corrects it.\n */\n function adoptRotatedToken(accessToken: string): void {\n const current = cached;\n if (!current || !accessToken || accessToken === current.accessToken) return;\n applySession({ ...current, accessToken }, \"session_refreshed\");\n }\n\n const getSecurityStatus: AuthNamespace[\"getSecurityStatus\"] = async () => {\n const result = await call(() => opts.transport.client.auth.getSecurityStatus());\n return result.data;\n };\n\n const enableTotp: AuthNamespace[\"enableTotp\"] = async (input) => {\n const result = await call(() => opts.transport.client.auth.enableTotp(input));\n return result.data;\n };\n\n const verifyTotpSetup: AuthNamespace[\"verifyTotpSetup\"] = async (input) => {\n const result = await call(() => opts.transport.client.auth.verifyTotpSetup(input));\n adoptRotatedToken(result.data.accessToken);\n };\n\n const disableTwoFactor: AuthNamespace[\"disableTwoFactor\"] = async (input) => {\n const result = await call(() => opts.transport.client.auth.disableTwoFactor(input));\n adoptRotatedToken(result.data.accessToken);\n };\n\n const regenerateBackupCodes: AuthNamespace[\"regenerateBackupCodes\"] = async (input) => {\n const result = await call(() => opts.transport.client.auth.regenerateBackupCodes(input));\n return result.data.backupCodes;\n };\n\n // --- Account security -------------------------------------------------------\n\n const changePassword: AuthNamespace[\"changePassword\"] = async (input) => {\n const result = await call(() =>\n opts.transport.client.auth.changePassword({\n currentPassword: input.currentPassword,\n newPassword: input.newPassword,\n revokeOtherSessions: input.revokeOtherSessions,\n }),\n );\n adoptRotatedToken(result.data.accessToken);\n };\n\n const changeEmail: AuthNamespace[\"changeEmail\"] = async (input) => {\n await call(() => opts.transport.client.auth.changeEmail(input));\n };\n\n const confirmEmailChange: AuthNamespace[\"confirmEmailChange\"] = async (input) => {\n await call(() => opts.transport.client.auth.confirmEmailChange(input));\n };\n\n const listSessions: AuthNamespace[\"listSessions\"] = async () => {\n const result = await call(() => opts.transport.client.auth.listSessions());\n return result.data;\n };\n\n const revokeSession: AuthNamespace[\"revokeSession\"] = async (input) => {\n await call(() => opts.transport.client.auth.revokeSession(input));\n };\n\n const revokeOtherSessions: AuthNamespace[\"revokeOtherSessions\"] = async () => {\n await call(() => opts.transport.client.auth.revokeOtherSessions());\n };\n\n // The server deletes the login, so the cached bearer is dead the moment\n // this resolves — drop it locally (emits signed_out). On failure (wrong\n // password, wrong identity, network) the session is untouched and the\n // error propagates. Exactly one proof is forwarded, so a caller that\n // bypasses the types still never sends both.\n const closeAccount: AuthNamespace[\"closeAccount\"] = async (input) => {\n const body =\n input.identityToken !== undefined\n ? {\n confirmEmail: input.confirmEmail,\n identityToken: {\n provider: input.identityToken.provider,\n idToken: input.identityToken.idToken,\n nonce: input.identityToken.nonce,\n authorizationCode: input.identityToken.authorizationCode,\n },\n }\n : { confirmEmail: input.confirmEmail, password: input.password };\n await call(() => opts.transport.client.auth.closeAccount(body));\n clearSession();\n };\n\n const _refresh: AuthNamespace[\"_refresh\"] = async () => {\n // Re-read storage as a fallback so a session written after\n // construction (e.g. on tab restore or in tests) is honoured.\n let current = cached;\n if (!current) {\n const fromStorage = await Promise.resolve(opts.storage.get());\n if (fromStorage) {\n cached = fromStorage;\n current = fromStorage;\n }\n }\n if (!current) {\n const err: FytkitError = { kind: \"auth_required\" };\n throw err;\n }\n try {\n const result = await opts.transport.client.auth.refresh({ token: current.accessToken });\n const { user, session } = result.data;\n return applySession(\n { accessToken: session.accessToken, expiresAt: session.expiresAt, user },\n \"session_refreshed\",\n );\n } catch (err) {\n const normalized = normalizeError(err);\n // Sign out only when the server actually rejected the token. A network\n // or server failure leaves the session in place for a later retry.\n if (isSessionRejected(normalized)) clearSession();\n throw normalized;\n }\n };\n\n const _signOutLocal: AuthNamespace[\"_signOutLocal\"] = () => clearSession();\n\n refreshFn = _refresh;\n\n return {\n signUp,\n signIn,\n signInWithIdToken,\n signOut,\n getSession,\n onAuthStateChange,\n requestPasswordReset,\n confirmPasswordReset,\n confirmEmail,\n resendVerification,\n verifyTotp,\n verifyBackupCode,\n requestEmailOtp,\n verifyEmailOtp,\n getSecurityStatus,\n enableTotp,\n verifyTotpSetup,\n disableTwoFactor,\n regenerateBackupCodes,\n changePassword,\n changeEmail,\n confirmEmailChange,\n listSessions,\n revokeSession,\n revokeOtherSessions,\n closeAccount,\n _refresh,\n _signOutLocal,\n };\n}\n","// Internal: not exported from the package.\n\n/** Where the SDK calls fytkit unless `baseUrl` says otherwise. */\nexport const DEFAULT_BASE_URL = \"https://api.fytkit.com\";\n","import { normalizeError } from \"./error\";\nimport type { Transport } from \"./transport\";\n\n/**\n * Wrap every method on a public-router slice so thrown ORPCErrors become\n * normalised FytkitErrors at the SDK boundary.\n *\n * The slice we receive is an `@orpc/client` Proxy whose own keys are not\n * enumerable — every property lookup is intercepted lazily. We mirror that\n * shape by returning a Proxy of our own that wraps each function on first\n * access and caches the result. This way the SDK never needs to know the\n * method names ahead of time, and the typed shape from the server router\n * flows through unchanged.\n */\nfunction wrapSlice<T extends object>(slice: T): T {\n const cache = new Map<string | symbol, unknown>();\n return new Proxy(slice, {\n get(target, prop, receiver) {\n if (cache.has(prop)) return cache.get(prop);\n const value = Reflect.get(target, prop, receiver);\n if (typeof value !== \"function\") {\n // Allow nested namespaces to flow through unchanged (the orpc\n // proxy returns another proxy here, not a plain object).\n return value;\n }\n const wrapped = async (...args: unknown[]) => {\n try {\n return await (value as (...a: unknown[]) => Promise<unknown>)(...args);\n } catch (err) {\n throw normalizeError(err);\n }\n };\n cache.set(prop, wrapped);\n return wrapped;\n },\n }) as T;\n}\n\nexport interface DataNamespaces {\n readonly workouts: Transport[\"client\"][\"workout\"];\n readonly exercises: Transport[\"client\"][\"exercise\"];\n readonly programs: Transport[\"client\"][\"program\"];\n readonly goals: Transport[\"client\"][\"goal\"];\n readonly equipment: Transport[\"client\"][\"equipment\"];\n readonly recipes: Transport[\"client\"][\"recipe\"];\n readonly nutrition: Transport[\"client\"][\"nutrition\"];\n readonly nutritionFollows: Transport[\"client\"][\"nutritionFollow\"];\n readonly articles: Transport[\"client\"][\"article\"];\n readonly enrollments: Transport[\"client\"][\"enrollment\"];\n readonly favourites: Transport[\"client\"][\"favourite\"];\n readonly sessionLogs: Transport[\"client\"][\"sessionLog\"];\n readonly progression: Transport[\"client\"][\"progression\"];\n readonly bodyMetrics: Transport[\"client\"][\"bodyMetric\"];\n readonly bodyMeasurements: Transport[\"client\"][\"bodyMeasurement\"];\n readonly progressPhotos: Transport[\"client\"][\"progressPhoto\"];\n readonly checkIns: Transport[\"client\"][\"checkin\"];\n readonly messages: Transport[\"client\"][\"message\"];\n readonly timeline: Transport[\"client\"][\"timeline\"];\n readonly health: Transport[\"client\"][\"health\"];\n readonly habits: Transport[\"client\"][\"habit\"];\n readonly progress: Transport[\"client\"][\"progress\"];\n readonly media: Transport[\"client\"][\"media\"];\n readonly subscription: Transport[\"client\"][\"subscription\"];\n readonly profile: Transport[\"client\"][\"profile\"];\n readonly nutritionLog: Transport[\"client\"][\"nutritionLog\"];\n readonly preferences: Transport[\"client\"][\"preferences\"];\n}\n\nexport function createDataNamespaces(transport: Transport): DataNamespaces {\n const c = transport.client;\n return {\n workouts: wrapSlice(c.workout),\n exercises: wrapSlice(c.exercise),\n programs: wrapSlice(c.program),\n goals: wrapSlice(c.goal),\n equipment: wrapSlice(c.equipment),\n recipes: wrapSlice(c.recipe),\n nutrition: wrapSlice(c.nutrition),\n nutritionFollows: wrapSlice(c.nutritionFollow),\n articles: wrapSlice(c.article),\n enrollments: wrapSlice(c.enrollment),\n favourites: wrapSlice(c.favourite),\n sessionLogs: wrapSlice(c.sessionLog),\n progression: wrapSlice(c.progression),\n bodyMetrics: wrapSlice(c.bodyMetric),\n bodyMeasurements: wrapSlice(c.bodyMeasurement),\n progressPhotos: wrapSlice(c.progressPhoto),\n checkIns: wrapSlice(c.checkin),\n messages: wrapSlice(c.message),\n timeline: wrapSlice(c.timeline),\n health: wrapSlice(c.health),\n habits: wrapSlice(c.habit),\n progress: wrapSlice(c.progress),\n media: wrapSlice(c.media),\n subscription: wrapSlice(c.subscription),\n profile: wrapSlice(c.profile),\n nutritionLog: wrapSlice(c.nutritionLog),\n preferences: wrapSlice(c.preferences),\n };\n}\n","import type { Session, TokenStorage } from \"./types\";\n\nconst STORAGE_KEY_PREFIX = \"fytkit:\";\nconst STORAGE_KEY_SUFFIX = \":tokens\";\n\nfunction keyFor(clientId: string): string {\n return `${STORAGE_KEY_PREFIX}${clientId}${STORAGE_KEY_SUFFIX}`;\n}\n\nfunction isValidSession(value: unknown): value is Session {\n if (!value || typeof value !== \"object\") return false;\n const v = value as Record<string, unknown>;\n return (\n typeof v.accessToken === \"string\" &&\n typeof v.expiresAt === \"string\" &&\n typeof v.user === \"object\" &&\n v.user !== null &&\n typeof (v.user as { id?: unknown }).id === \"string\" &&\n typeof (v.user as { email?: unknown }).email === \"string\"\n );\n}\n\n/**\n * Default web storage. Reads/writes JSON under `fytkit:{clientId}:tokens`.\n * Silently no-ops when localStorage throws (Safari private mode, SSR).\n */\nexport function createLocalStorageAdapter(clientId: string): TokenStorage {\n const key = keyFor(clientId);\n return {\n get: () => {\n try {\n const raw = globalThis.localStorage?.getItem(key);\n if (!raw) return null;\n const parsed: unknown = JSON.parse(raw);\n return isValidSession(parsed) ? parsed : null;\n } catch {\n return null;\n }\n },\n set: (session: Session) => {\n try {\n globalThis.localStorage?.setItem(key, JSON.stringify(session));\n } catch {\n // Quota / private-mode / SSR — swallow and let the caller treat the\n // session as in-memory-only for this tab.\n }\n },\n remove: () => {\n try {\n globalThis.localStorage?.removeItem(key);\n } catch {\n // see set() rationale\n }\n },\n };\n}\n\n/** In-memory storage. Useful for tests and SSR. */\nexport function createMemoryStorageAdapter(): TokenStorage {\n let current: Session | null = null;\n return {\n get: () => current,\n set: (session: Session) => {\n current = session;\n },\n remove: () => {\n current = null;\n },\n };\n}\n","// The `Retry-After` fallback for 429s (SDK1, #461).\n//\n// The SDK reads a 429's wait from its error data (`data.retryAfterMs`), which\n// every fytkit 429 sends. A 429 that doesn't carry it (an older server's\n// plain body, a proxy or CDN in front of the API) used to reach an app as\n// `retryAfter: 0`, so a retry loop retried at once. When such a 429 has a\n// `Retry-After` header, the transport writes the header's wait into the\n// error data before oRPC reads the response, so `normalizeError` finds it\n// where it always looks. A hint the data already carries always wins.\n\nimport { isRecord } from \"./is-record\";\n\nconst DELAY_SECONDS = /^\\d+$/;\n\n/**\n * A `Retry-After` value's wait in milliseconds from `now`: delay-seconds\n * (`\"120\"`) or an HTTP date (zero once it has passed). Null when there is no\n * value or it is neither.\n */\nexport function retryAfterHeaderMs(value: string | null, now: number): number | null {\n if (value === null) return null;\n const trimmed = value.trim();\n if (trimmed.length === 0) return null;\n if (DELAY_SECONDS.test(trimmed)) return Number(trimmed) * 1000;\n // Only a date in letters: Date.parse also accepts bare numbers like \"1.5\".\n if (!/[a-z]/i.test(trimmed)) return null;\n const at = Date.parse(trimmed);\n return Number.isNaN(at) ? null : Math.max(0, at - now);\n}\n\nfunction parseJson(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return undefined;\n }\n}\n\n/**\n * The 429's body with `waitMs` as its error data's `retryAfterMs`, as oRPC's\n * RPC protocol frames it (`{ json: <error> }`); null when the data already\n * carries a hint. A body that isn't an oRPC error becomes the error oRPC\n * would have sent.\n */\nfunction hintedBody(body: unknown, waitMs: number): Readonly<Record<string, unknown>> | null {\n if (isRecord(body) && isRecord(body.json) && typeof body.json.code === \"string\") {\n const error = body.json;\n const data = isRecord(error.data) ? error.data : {};\n if (typeof data.retryAfterMs === \"number\") return null;\n return { ...body, json: { ...error, data: { ...data, retryAfterMs: waitMs } } };\n }\n return {\n json: {\n defined: false,\n code: \"TOO_MANY_REQUESTS\",\n status: 429,\n message: \"Too Many Requests\",\n data: { retryAfterMs: waitMs },\n },\n };\n}\n\n/**\n * `response` with the `Retry-After` header's wait written into its error data\n * when it is a 429 whose data has no `retryAfterMs`; unchanged otherwise.\n */\nexport async function withRetryAfterHint(response: Response, now: number): Promise<Response> {\n if (response.status !== 429) return response;\n const waitMs = retryAfterHeaderMs(response.headers.get(\"retry-after\"), now);\n if (waitMs === null) return response;\n const hinted = hintedBody(parseJson(await response.clone().text()), waitMs);\n if (hinted === null) return response;\n const headers = new Headers(response.headers);\n headers.set(\"content-type\", \"application/json\");\n headers.delete(\"content-length\");\n return new Response(JSON.stringify(hinted), {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n}\n","import type { PublicRouter } from \"@fytkit/types\";\nimport { createORPCClient } from \"@orpc/client\";\nimport { RPCLink } from \"@orpc/client/fetch\";\nimport type { RouterClient } from \"@orpc/server\";\nimport { withRetryAfterHint } from \"./retry-after\";\nimport type { Session } from \"./types\";\n\nexport interface CreateTransportOptions {\n readonly baseUrl: string;\n readonly clientId: string;\n readonly getSession: () => Session | null;\n readonly fetch?: typeof fetch;\n}\n\n/**\n * Per-call options the SDK's own wrappers pass to the link. `ifNoneMatch`\n * becomes the request's `If-None-Match` header (`app.bootstrap`'s ETag\n * revalidation, #535); every other call sends none.\n */\nexport interface TransportContext {\n readonly ifNoneMatch?: string;\n}\n\n/** The public router's client, taking {@link TransportContext} per call. */\nexport type TransportClient = RouterClient<PublicRouter, TransportContext>;\n\nexport interface Transport {\n readonly client: TransportClient;\n /** Internal: hook for attachRefreshInterceptor to install a fetch wrapper. */\n readonly _setFetchInterceptor: (interceptor: FetchInterceptor) => void;\n}\n\ntype FetchInterceptor = (\n request: Request,\n next: (req: Request) => Promise<Response>,\n) => Promise<Response>;\n\nexport function createTransport(options: CreateTransportOptions): Transport {\n let interceptor: FetchInterceptor | null = null;\n const baseFetch: typeof fetch = options.fetch ?? fetch.bind(globalThis);\n\n // The fetch handed to RPCLink resolves the interceptor lazily so callers\n // can install one after construction (see attachRefreshInterceptor). A 429\n // whose error data lacks a retry hint gets the `Retry-After` header's wait\n // written in (retry-after.ts), so `rate_limited.retryAfter` reads it.\n const wrappedFetch: typeof fetch = async (input, init) => {\n const request = input instanceof Request ? input : new Request(input, init);\n const next = (req: Request) => baseFetch(req);\n const response = interceptor ? await interceptor(request, next) : await next(request);\n return withRetryAfterHint(response, Date.now());\n };\n\n const link = new RPCLink<TransportContext>({\n url: () => `${options.baseUrl}/rpc`,\n headers: ({ context }) => {\n const session = options.getSession();\n const headers: Record<string, string> = {\n \"X-Fytkit-Client-Id\": options.clientId,\n };\n if (session) {\n headers.Authorization = `Bearer ${session.accessToken}`;\n }\n if (context.ifNoneMatch !== undefined) {\n headers[\"If-None-Match\"] = context.ifNoneMatch;\n }\n return headers;\n },\n fetch: wrappedFetch,\n });\n\n const client: TransportClient = createORPCClient(link);\n return {\n client,\n _setFetchInterceptor: (i) => {\n interceptor = i;\n },\n };\n}\n\n/** Internal marker header — set on the refresh call so we never recurse. */\nconst REFRESH_HEADER = \"X-Fytkit-Refresh\";\n\n/**\n * Procedures that authenticate with credentials in their input (a password,\n * an emailed token, a one-time code or a sign-in challenge), never with the\n * bearer. A 401 from one of them means those credentials were rejected, not\n * that the session is dead, so it passes straight through: no refresh, no\n * replay, no local sign-out. Otherwise a member who mistypes their password\n * while signing in again (after `reauth_required`) would be signed out.\n */\nconst CREDENTIAL_PROCEDURES: readonly string[] = [\n \"auth/signIn\",\n \"auth/signInWithIdToken\",\n \"auth/signUp\",\n \"auth/requestPasswordReset\",\n \"auth/confirmPasswordReset\",\n \"auth/confirmEmail\",\n \"auth/confirmEmailChange\",\n \"auth/verifyTotp\",\n \"auth/verifyBackupCode\",\n \"auth/requestEmailOtp\",\n \"auth/verifyEmailOtp\",\n];\n\n/**\n * The RPCLink routes per-procedure calls under a URL whose pathname ends\n * with the procedure path (joined as `/segment/segment`), so procedures are\n * detected by suffix. endsWith keeps trailing query strings or link-level\n * prefixes from breaking detection.\n */\nfunction requestPathname(request: Request): string {\n try {\n return new URL(request.url).pathname;\n } catch {\n return \"\";\n }\n}\n\n/** Detect whether a request targets the refresh procedure. */\nfunction isRefreshRequest(request: Request): boolean {\n if (request.headers.get(REFRESH_HEADER) === \"1\") return true;\n return requestPathname(request).endsWith(\"/auth/refresh\");\n}\n\n/** Detect whether a request targets one of the `CREDENTIAL_PROCEDURES`. */\nfunction isCredentialRequest(request: Request): boolean {\n const path = requestPathname(request);\n return CREDENTIAL_PROCEDURES.some((procedure) => path.endsWith(`/${procedure}`));\n}\n\nexport interface AttachRefreshInterceptorOptions {\n readonly transport: Transport;\n readonly auth: {\n readonly _refresh: () => Promise<Session>;\n readonly _signOutLocal: () => void;\n readonly getSession: () => Session | null;\n };\n}\n\n/**\n * Install a fetch interceptor that, on a 401 response, calls auth._refresh\n * once and retries the original request. Concurrent 401s de-duplicate\n * onto a single in-flight refresh promise. A 401/403 from refresh itself or\n * a 401 from the retry triggers a local sign-out and propagates the original\n * 401 to the caller (after which orpc throws ORPCError(\"UNAUTHORIZED\")). A\n * refresh that fails for any other reason (offline, 5xx) also propagates the\n * original 401, but keeps the session so a later call can succeed. A 401 from\n * a credential procedure (`CREDENTIAL_PROCEDURES`) is never refreshed or\n * replayed: it rejects the credentials, not the session.\n */\nexport function attachRefreshInterceptor(opts: AttachRefreshInterceptorOptions): void {\n let inflight: Promise<Session> | null = null;\n\n const refreshOnce = (): Promise<Session> => {\n if (!inflight) {\n inflight = opts.auth._refresh().finally(() => {\n inflight = null;\n });\n }\n return inflight;\n };\n\n opts.transport._setFetchInterceptor(async (request, next) => {\n // Refresh calls must never trigger a recursive refresh, and a 401 from a\n // credential procedure is a wrong password or code, not a dead session.\n if (isRefreshRequest(request) || isCredentialRequest(request)) {\n return next(request);\n }\n\n // Capture the request body now, before we pass `request` to `next` —\n // once fetch consumes a Request body it can no longer be cloned.\n const buffered = await request.clone().arrayBuffer();\n\n const firstResponse = await next(request);\n if (firstResponse.status !== 401) return firstResponse;\n\n try {\n await refreshOnce();\n } catch {\n // _refresh decides whether the session survives: it signs out only\n // when the server rejected the token, not on a network/server failure.\n return firstResponse;\n }\n\n // Rebuild headers so the new Authorization header replaces the stale\n // one — the RPCLink only re-evaluates its own `headers()` once per call,\n // so we patch the bearer ourselves here.\n const replayedHeaders = new Headers(request.headers);\n const session = opts.auth.getSession();\n if (session) {\n replayedHeaders.set(\"Authorization\", `Bearer ${session.accessToken}`);\n } else {\n replayedHeaders.delete(\"Authorization\");\n }\n const replayed = new Request(request.url, {\n method: request.method,\n headers: replayedHeaders,\n body: buffered.byteLength > 0 ? buffered : undefined,\n credentials: request.credentials,\n mode: request.mode,\n cache: request.cache,\n redirect: request.redirect,\n referrer: request.referrer,\n integrity: request.integrity,\n });\n\n const retried = await next(replayed);\n if (retried.status === 401) {\n // Refresh succeeded server-side but the server still rejects — the\n // session is dead. Sign out locally and let orpc surface the 401.\n opts.auth._signOutLocal();\n }\n return retried;\n });\n}\n","// Resolving a studio's join code (#529): the one call an app makes before\n// it has a client id. The code on a studio's Fytkit Coach QR code or join\n// link names the studio; this turns it into the client id to pass to\n// `createFytkitClient`, with what a welcome screen needs.\n//\n// A plain `GET /api/v1/join/{code}`: it takes no credentials and needs no\n// client, so it is a function of its own rather than a client method.\n\nimport { createORPCErrorFromJson, isORPCErrorJson } from \"@orpc/client\";\nimport { DEFAULT_BASE_URL } from \"./base-url\";\nimport { normalizeError } from \"./error\";\nimport { isRecord } from \"./is-record\";\nimport { retryAfterHeaderMs } from \"./retry-after\";\nimport type { FytkitError } from \"./types\";\n\n/** Options for {@link resolveJoinCode}. */\nexport interface ResolveJoinCodeOptions {\n /** The API's origin, as for `createFytkitClient`. Defaults to `https://api.fytkit.com`. */\n readonly baseUrl?: string;\n /** A `fetch` to use instead of the global one. */\n readonly fetch?: typeof fetch;\n}\n\n/**\n * The design the app paints for the studio: its Fytkit Coach theme (an\n * app-only one if the studio picked one, otherwise its published site design),\n * the theme's palette and the shades derived from it. Every colour is a\n * `#rrggbb` hex string.\n */\nexport interface JoinCodeTheme {\n /** The site template the design comes from (the app calls it the theme). */\n readonly templateId: string;\n /** The template's colour theme (the app calls it the palette). */\n readonly themeId: string;\n /** The theme's curated colours. */\n readonly palette: {\n readonly primary: string;\n readonly secondary: string;\n readonly accent: string;\n readonly background: string;\n readonly text: string;\n };\n /** Shades derived from the palette, for states and text on fills. */\n readonly variants: {\n /** Hover/press shade of `primary`. */\n readonly primaryDeep: string;\n /** Tinted background of `primary`. */\n readonly primarySoft: string;\n /** Text that reads on `primary`. */\n readonly onPrimary: string;\n /** Hover/press shade of `accent`. */\n readonly accentDeep: string;\n /** Tinted background of `accent`. */\n readonly accentSoft: string;\n /** Text that reads on `accent`. */\n readonly onAccent: string;\n /** The highlight fill, legible under dark ink text. */\n readonly highlight: string;\n /** Hover/press shade of `highlight`. */\n readonly highlightDeep: string;\n /** Tinted background of `highlight`. */\n readonly highlightSoft: string;\n };\n}\n\n/** A coach a join code's studio names publicly. */\nexport interface JoinCodeCoach {\n /** The coach's public name. */\n readonly name: string;\n /** The coach's public profile image, a stable URL; null when there is none. */\n readonly avatarUrl: string | null;\n}\n\n/** What a join code resolves to. */\nexport interface JoinCodeResolution {\n /** The code in canonical form: upper case, no spaces or hyphens. */\n readonly code: string;\n /** The studio's Fytkit Coach client id: pass it to `createFytkitClient`. */\n readonly clientId: string;\n /** What the welcome screen names and shows. */\n readonly studio: {\n /** The name members know the studio by. */\n readonly name: string;\n /** The studio's logo, a stable public URL; null when it has none. */\n readonly logoUrl: string | null;\n /**\n * Who coaches there, as the studio's public site shows them: today one\n * entry, the studio's own coach profile (never staff). At most 3.\n */\n readonly coaches: readonly JoinCodeCoach[];\n /** Published programmes, any access level; drafts and archived ones never count. */\n readonly programCount: number;\n };\n /** The design the app paints for the studio. */\n readonly theme: JoinCodeTheme;\n}\n\n/**\n * What the server answers for an unknown or revoked code, and what this\n * answers itself, without a request, for input that can't safely go in the\n * URL path.\n */\nconst INACTIVE: FytkitError = {\n kind: \"not_found\",\n refusal: { reason: \"join_code_inactive\", data: {} },\n};\n\n/**\n * Whitespace, the hyphen-minus and the dashes phones substitute for it: the\n * separators the server ignores too (src/lib/join-code/code.ts in\n * fytkit-web, which a test there checks this against).\n */\nconst IGNORED_SEPARATORS = /[\\s\\-\\u2010-\\u2015\\u2212]/g;\n\n/**\n * Only what keeps the URL path safe: letters and digits, at most as many as\n * the server reads. The code's alphabet and length are the server's to\n * decide, so a format change never needs a new app build.\n */\nconst PATH_SAFE_CODE = /^[A-Za-z0-9]{1,64}$/;\n\n/** The code to put in the path, upper-cased; null when it isn't path-safe. */\nfunction pathSafeJoinCode(input: string): string | null {\n const candidate = input.replace(IGNORED_SEPARATORS, \"\");\n return PATH_SAFE_CODE.test(candidate) ? candidate.toUpperCase() : null;\n}\n\nasync function readJson(response: Response): Promise<unknown> {\n try {\n return await response.json();\n } catch {\n return undefined;\n }\n}\n\n/** The refusal in a non-2xx response, as every SDK method reports one. */\nfunction refusalOf(response: Response, body: unknown): FytkitError {\n const wait = () => retryAfterHeaderMs(response.headers.get(\"retry-after\"), Date.now()) ?? 0;\n // A 404 that isn't the route's own oRPC answer comes from a wrong\n // `baseUrl` or a proxy, not from the code: it stays a plain server error\n // (below), so a member is never told their code is dead because of it.\n if (isORPCErrorJson(body)) {\n const error = normalizeError(createORPCErrorFromJson(body));\n // A 429 without the hint in its data (a proxy's): read the header instead.\n if (error.kind === \"rate_limited\" && error.retryAfter === 0) {\n return { ...error, retryAfter: wait() };\n }\n return error;\n }\n if (response.status === 429) return { kind: \"rate_limited\", retryAfter: wait() };\n return { kind: \"server\", status: response.status };\n}\n\n/**\n * Resolves a studio's join code, as its QR code or join link carries it or a\n * member types it, to the studio's Fytkit Coach client id, name, logo, app\n * theme, public coach profile and published programme count. Needs no client\n * id or session. Codes are case-insensitive and spaces and hyphens are\n * ignored, so pass what the member typed as it is.\n *\n * Rejects with a `FytkitError`, as every SDK method does:\n * - `{ kind: \"not_found\", refusal: { reason: \"join_code_inactive\" } }`: no\n * studio has that code, the studio replaced it with a new one, or the\n * input has something other than letters, digits, spaces and hyphens, or\n * more than 64 letters and digits (separators are stripped first; then\n * without a request). Show\n * \"This code is no longer active. Ask your coach for a new one.\" The\n * cases are deliberately indistinguishable.\n * - `{ kind: \"rate_limited\", retryAfter }`: 60 lookups a minute per IP\n * address; wait `retryAfter` milliseconds.\n * - `{ kind: \"network\" }` when the request never reached the server.\n * - `{ kind: \"server\", status }` for anything else, e.g. a 404 that isn't\n * the API's own answer (a wrong `baseUrl`, or a proxy in front of it).\n *\n * ```ts\n * const studio = await resolveJoinCode(scannedCode);\n * const fytkit = createFytkitClient({ clientId: studio.clientId });\n * ```\n */\nexport async function resolveJoinCode(\n code: string,\n options: ResolveJoinCodeOptions = {},\n): Promise<JoinCodeResolution> {\n // Anything that isn't letters and digits once separators are gone (`/`,\n // `.`, `..`, `?`, `%`) is refused here, so it never reaches the path.\n // Whether the rest is a code at all is the server's call.\n const canonical = pathSafeJoinCode(code);\n if (canonical === null) throw INACTIVE;\n\n const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;\n const doFetch: typeof fetch = options.fetch ?? fetch.bind(globalThis);\n let response: Response;\n try {\n response = await doFetch(`${baseUrl}/api/v1/join/${canonical}`, {\n method: \"GET\",\n headers: { Accept: \"application/json\" },\n });\n } catch (error) {\n throw normalizeError(error);\n }\n\n const body = await readJson(response);\n if (response.ok) {\n if (isRecord(body) && isRecord(body.data)) {\n // The server's documented shape, trusted as every other response is.\n return body.data as unknown as JoinCodeResolution;\n }\n throw { kind: \"server\", status: response.status } satisfies FytkitError;\n }\n throw refusalOf(response, body);\n}\n","// \"Subscribe\" or \"What your plan includes\" (#538). Fytkit Coach takes no\n// in-app purchase: where the member's store allows it, the app links out to\n// the studio's Stripe checkout; everywhere else it shows what the plan\n// includes. `app.bootstrap` carries the list of storefronts where linking out\n// is allowed (`data.paymentPolicy`); this reads it for the storefront the\n// device's store reports. It fails closed: an unknown storefront, a missing\n// code or an unmet condition is plan info.\n\nimport type { AppBootstrapData } from \"./app\";\n\n/** `app.bootstrap`'s `data.paymentPolicy`. */\nexport type PaymentPolicy = AppBootstrapData[\"paymentPolicy\"];\n\n/** The store the running app was installed from. */\nexport type PaymentPlatform = PaymentPolicy[\"linkOut\"][number][\"platform\"];\n\n/** Options for {@link resolvePaymentMode}. */\nexport interface ResolvePaymentModeOptions {\n /** The store the running app was installed from: `\"ios\"` or `\"android\"`. */\n readonly platform: PaymentPlatform;\n /**\n * The storefront the store reports, case-insensitive: on iOS StoreKit's\n * `Storefront.current?.countryCode` (ISO 3166-1 alpha-3, e.g. \"USA\"); on\n * Android Play Billing's `BillingConfig.getCountryCode()` (alpha-2, e.g.\n * \"US\"). Pass `null` when the store reports none: that is plan info.\n */\n readonly storefront: string | null | undefined;\n /**\n * The conditions the app meets in this storefront (see `conditions` in\n * `paymentPolicy.linkOut`), e.g. `\"apple_external_purchase_link_entitlement\"`\n * once `ExternalPurchaseLink.canOpen` is true. A storefront whose\n * conditions aren't all met, or names one the app doesn't know, is plan info.\n */\n readonly conditionsMet?: readonly string[];\n}\n\n/**\n * What to show:\n * - `link_out`: \"Subscribe\", opening the studio's checkout outside the app,\n * in the way each of `conditions` requires (none: a plain link to the\n * system browser);\n * - `plan_info`: \"What your plan includes\", with no purchase and no link to one.\n */\nexport type PaymentMode =\n | { readonly mode: \"link_out\"; readonly conditions: readonly string[] }\n | { readonly mode: \"plan_info\" };\n\nconst PLAN_INFO: PaymentMode = { mode: \"plan_info\" };\n\n/**\n * Decides, for the storefront the device's store reports, whether the app\n * shows \"Subscribe\" (a link out) or \"What your plan includes\". Pure: it reads\n * the policy from a stored `app.bootstrap`, so it works offline. A missing or\n * malformed policy (e.g. a snapshot stored before it existed) is plan info.\n *\n * ```ts\n * const { mode } = resolvePaymentMode(data.paymentPolicy, {\n * platform: \"ios\",\n * storefront: storefrontCountryCode, // from StoreKit, e.g. \"USA\"\n * });\n * if (mode === \"link_out\") showSubscribe();\n * else showWhatYourPlanIncludes();\n * ```\n */\nexport function resolvePaymentMode(\n policy: PaymentPolicy | null | undefined,\n options: ResolvePaymentModeOptions,\n): PaymentMode {\n // A snapshot stored before the policy existed, or a malformed one, has no\n // list to read: plan info, as for a storefront it doesn't name.\n if (!policy || !Array.isArray(policy.linkOut)) return PLAN_INFO;\n const storefront = options.storefront?.trim().toUpperCase() ?? \"\";\n if (storefront === \"\") return PLAN_INFO;\n const rule = policy.linkOut.find(\n (entry) =>\n entry?.platform === options.platform &&\n typeof entry.storefront === \"string\" &&\n entry.storefront.toUpperCase() === storefront,\n );\n if (!rule || !Array.isArray(rule.conditions)) return PLAN_INFO;\n const met = new Set(options.conditionsMet ?? []);\n if (!rule.conditions.every((condition) => met.has(condition))) return PLAN_INFO;\n return { mode: \"link_out\", conditions: rule.conditions };\n}\n","// Live updates for a trainee's conversation (SDK1, #461): the third-party\n// counterpart of the member portal's own channel. `messages.realtimeToken`\n// hands out a 15-minute token and the one private Supabase Realtime topic it\n// may join (`member:<client id>`); `app.bootstrap` names the project's public\n// URL and anon key. The database broadcasts `thread_changed` on that topic\n// whenever the conversation changes, with no content: a cue to read\n// `messages.list` and `messages.unreadCount` again, never the data itself.\n// The same topic carries `coach_typing` while a staff member writes to the\n// trainee (W16), surfaced as `onCoachTyping`; only the server sends it.\n//\n// The SDK has no runtime dependency on supabase-js: the app passes a factory\n// that creates a Supabase client, and this module only uses the few members\n// described by `RealtimeClientLike`. Self-contained on purpose (no imports),\n// so the shapes can be checked against supabase-js's own types. The token is\n// set on the socket by hand (`realtime.setAuth(token)`), which supabase-js\n// keeps only from 2.87.1: earlier 2.x clients put the anon key back on the\n// next heartbeat, and the private channel is lost.\n\n/** fytkit's public Supabase Realtime settings, from `app.bootstrap` (`data.realtime`). */\nexport interface RealtimeConnection {\n /** The Supabase project URL. */\n readonly url: string;\n /** The project's public anon key. Safe to ship in an app. */\n readonly anonKey: string;\n}\n\n/** `messages.realtimeToken`'s `data`: a token and the private topic it admits. */\nexport interface MessagesRealtimeGrant {\n /** A Supabase Realtime token for the signed-in trainee, valid until `expiresAt`. */\n readonly token: string;\n /** When the token expires (ISO 8601), 15 minutes after it was issued. */\n readonly expiresAt: string;\n /** The only private topic the token may join: `member:<client id>`. */\n readonly topic: string;\n}\n\n/** An auth-state event, as `auth.onAuthStateChange` reports it; only `type` is read. */\nexport interface MessagesRealtimeAuthEvent {\n /** `signed_in`, `signed_out`, `session_refreshed` or `signed_out_due_to_token_reuse`. */\n readonly type: string;\n}\n\n/** The calls the subscription needs. A `FytkitClient` is one. */\nexport interface MessagesRealtimeSource {\n /**\n * `app.bootstrap`, for the Realtime connection settings. Called without an\n * ETag, so it always answers `ok` with the data.\n */\n readonly app: {\n bootstrap(): PromiseLike<\n | { readonly status: \"ok\"; readonly data: { readonly realtime: RealtimeConnection } }\n | { readonly status: \"unchanged\" }\n >;\n };\n /** `messages.realtimeToken`, for the token and its topic. */\n readonly messages: {\n realtimeToken(\n input: Record<string, never>,\n ): PromiseLike<{ readonly data: MessagesRealtimeGrant }>;\n };\n /**\n * `auth.onAuthStateChange`, optional. When present, a sign-out on the\n * client (`signed_out`, `signed_out_due_to_token_reuse`) ends the\n * subscription at once with `refused`, rather than at its next renewal:\n * the trainee's channel never outlives their session on this client.\n */\n readonly auth?: {\n onAuthStateChange(listener: (event: MessagesRealtimeAuthEvent) => void): () => void;\n };\n}\n\n/**\n * The part of a supabase-js `RealtimeChannel` the subscription uses. A\n * channel that reports `CLOSED` is finished: supabase-js removes it from its\n * socket and never joins it again.\n */\nexport interface RealtimeChannelLike {\n /** Listens for one Broadcast event on the channel. */\n on(\n type: \"broadcast\",\n filter: { readonly event: string },\n callback: (message: unknown) => void,\n ): RealtimeChannelLike;\n /** Joins the channel; `status` is `SUBSCRIBED`, `CHANNEL_ERROR`, `TIMED_OUT` or `CLOSED`. */\n subscribe(callback: (status: string, error?: Error) => void): RealtimeChannelLike;\n}\n\n/**\n * The part of a supabase-js `SupabaseClient` the subscription uses.\n * `@supabase/supabase-js` 2.87.1 or later: earlier clients swap the token the\n * subscription sets for the anon key on the next heartbeat.\n */\nexport interface RealtimeClientLike {\n /** Creates a channel on `name`; the subscription always asks for a private one. */\n channel(\n name: string,\n options: { readonly config: { readonly private: true } },\n ): RealtimeChannelLike;\n /** Leaves and forgets a channel. */\n removeChannel(channel: RealtimeChannelLike): unknown;\n /** The client's Realtime socket: `setAuth(token)` hands it the token. */\n readonly realtime: {\n setAuth(token: string): Promise<void>;\n disconnect(): void;\n };\n}\n\n/**\n * Creates a new Supabase client for the subscription, with\n * `@supabase/supabase-js` 2.87.1 or later:\n *\n * ```ts\n * ({ url, anonKey }) =>\n * createClient(url, anonKey, {\n * auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false },\n * })\n * ```\n *\n * The client carries the trainee's Realtime token and nothing else, so its\n * auth stays off: no stored session, no refresh, and no sign-in read from\n * your page's URL that could replace the token. Return a new client on every\n * call: the subscription sets its own token on the client's socket and\n * disconnects it when it ends, so a client the app uses for anything else\n * would lose both. A factory that throws ends the subscription with\n * `fallback`.\n */\nexport type RealtimeClientFactory = (connection: RealtimeConnection) => RealtimeClientLike;\n\n/**\n * How the conversation is kept current:\n * - `connecting`: before the first answer either way, and while it moves to\n * another trainee's channel. Poll meanwhile.\n * - `live`: the channel is joined; changes arrive as `onChange`. Stop polling.\n * - `fallback`: Realtime is unavailable. Poll at\n * {@link MESSAGES_FALLBACK_POLL_MS}. For now, and `live` may follow, when\n * the token or the connection settings couldn't be fetched (retried every\n * minute), or the channel errored or timed out (supabase-js joins it\n * again by itself), or Realtime closed it (a Realtime restart, a token\n * that expired before its renewal landed: the subscription joins a fresh\n * channel after 5 seconds, then 15, then every minute, with a new token\n * first if the one it holds has expired). For good, and polling carries\n * on, when the server has no live updates to offer (a token without\n * `topic`, no connection settings, or `app.bootstrap` refused) or the\n * factory threw.\n * - `refused`: the token request was refused (`auth_required`,\n * `forbidden`, `not_found` or `validation`), asking again won't change\n * that, and nearly always because there is no trainee to listen for\n * (signed out, account closed, a trainer previewing the portal); or the\n * `FytkitClient` passed in signed out. Final: the subscription has\n * stopped and asks nothing more; the conversation reads are refused the\n * same way.\n */\nexport type MessagesRealtimeStatus = \"connecting\" | \"live\" | \"fallback\" | \"refused\";\n\n/**\n * Whether the trainee's coaching team is writing to them right now, for a\n * \"Sam is writing…\" line under the conversation:\n * - `idle`: nobody is.\n * - `typing`: the staff members in `names` are, by the display name the\n * conversation shows on their messages, each name once, in the order\n * they started. Show the first, or \"Sam and Alex are writing…\".\n *\n * A hint, live only: it is heard only while the status is `live`, and goes\n * back to `idle` when the channel is lost. A name that hears nothing more\n * for 8 seconds ({@link COACH_TYPING_EXPIRY_MS}) drops out by itself, so a\n * lost \"stopped\" never leaves it showing. While polling it stays `idle`.\n */\nexport type CoachTypingState =\n | { readonly kind: \"idle\" }\n | { readonly kind: \"typing\"; readonly names: readonly string[] };\n\n/**\n * How long a staff member stays in {@link CoachTypingState}'s `names` after\n * the last sign that they are writing. The dashboard repeats the sign every\n * 3 seconds while they write.\n */\nexport const COACH_TYPING_EXPIRY_MS = 8_000;\n\n/** What {@link subscribeToMessages} needs besides the client. */\nexport interface SubscribeToMessagesOptions {\n /** Creates the Supabase client the subscription owns. See {@link RealtimeClientFactory}. */\n readonly realtime: RealtimeClientFactory;\n /**\n * The conversation may have changed: read `messages.list` and\n * `messages.unreadCount` again. Also called on every (re)join, since\n * changes while the channel was down were not heard.\n */\n readonly onChange: () => void;\n /** Each change of {@link MessagesRealtimeStatus}, starting with `connecting`. */\n readonly onStatus?: (status: MessagesRealtimeStatus) => void;\n /**\n * Each change of {@link CoachTypingState}: someone on the trainee's\n * coaching team started or stopped writing to them. Optional; never\n * called before a `typing`. A subscription that ends by itself\n * (`refused`, or `fallback` for good) reports `idle` first if anyone was\n * writing; after `unsubscribe()`, nothing is reported.\n */\n readonly onCoachTyping?: (typing: CoachTypingState) => void;\n}\n\n/**\n * How often to poll while the status is `connecting` or `fallback`: the open\n * conversation every 10 seconds, the unread count every 60. Polling spends\n * the app's shared per-minute request budget, so stop while `live`.\n */\nexport const MESSAGES_FALLBACK_POLL_MS = {\n conversation: 10_000,\n unreadCount: 60_000,\n} as const satisfies Readonly<Record<\"conversation\" | \"unreadCount\", number>>;\n\n/** The event the database broadcasts on a trainee's topic. */\nconst THREAD_CHANGED_EVENT = \"thread_changed\";\n/** The event a staff member's typing goes out as, on the same topic. */\nconst COACH_TYPING_EVENT = \"coach_typing\";\n/** The prefix of the only topic a trainee's token admits. */\nconst MEMBER_TOPIC_PREFIX = \"member:\";\n/** Ask for a new token this long before the current one expires. */\nconst TOKEN_RENEW_BEFORE_MS = 60_000;\n/** Never ask again sooner than this, however close the expiry. */\nconst TOKEN_MIN_DELAY_MS = 5_000;\n/** After a request that failed for now, try again this often. */\nconst RETRY_MS = 60_000;\n/**\n * The waits before each fresh join after Realtime closes the channel, from\n * the first closure on; the last repeats until a join subscribes. The same\n * as the member portal's own channel.\n */\nconst REJOIN_DELAYS_MS = [5_000, 15_000, 60_000] as const;\n\n/** Auth events after which there is no trainee on the client to listen for. */\nconst SIGNED_OUT_EVENTS: ReadonlySet<string> = new Set([\n \"signed_out\",\n \"signed_out_due_to_token_reuse\",\n]);\n\n/** Error kinds that asking again won't change: no trainee here, or a bad request. */\nconst FINAL_ERROR_KINDS: ReadonlySet<string> = new Set([\n \"auth_required\",\n \"forbidden\",\n \"not_found\",\n \"validation\",\n]);\n\nfunction isFinalError(error: unknown): boolean {\n if (typeof error !== \"object\" || error === null || !(\"kind\" in error)) return false;\n const kind: unknown = error.kind;\n return typeof kind === \"string\" && FINAL_ERROR_KINDS.has(kind);\n}\n\nfunction isMemberTopic(topic: unknown): topic is string {\n return (\n typeof topic === \"string\" &&\n topic.startsWith(MEMBER_TOPIC_PREFIX) &&\n topic.length > MEMBER_TOPIC_PREFIX.length\n );\n}\n\n/**\n * A `coach_typing` broadcast's payload: `{ state: \"typing\" | \"stopped\", name, key }`.\n * `key` tells apart two staff members of one name; a payload without one is\n * keyed by its name.\n */\ninterface CoachTypingSign {\n readonly state: \"typing\" | \"stopped\";\n readonly name: string;\n readonly key: string;\n}\n\n/** Reads a `coach_typing` broadcast, or null when it is not one the server sends. */\nfunction coachTypingSign(message: unknown): CoachTypingSign | null {\n if (typeof message !== \"object\" || message === null || !(\"payload\" in message)) return null;\n const payload: unknown = message.payload;\n if (typeof payload !== \"object\" || payload === null) return null;\n if (!(\"state\" in payload) || !(\"name\" in payload)) return null;\n const { state, name } = payload;\n if (state !== \"typing\" && state !== \"stopped\") return null;\n if (typeof name !== \"string\" || name.length === 0) return null;\n const key = \"key\" in payload && typeof payload.key === \"string\" ? payload.key : \"\";\n return { state, name, key: key.length > 0 ? `key:${key}` : `name:${name}` };\n}\n\nfunction isConnection(value: unknown): value is RealtimeConnection {\n if (typeof value !== \"object\" || value === null) return false;\n if (!(\"url\" in value) || !(\"anonKey\" in value)) return false;\n return (\n typeof value.url === \"string\" &&\n value.url.length > 0 &&\n typeof value.anonKey === \"string\" &&\n value.anonKey.length > 0\n );\n}\n\nfunction renewalDelay(expiresAt: string, now: number): number {\n const expiry = Date.parse(expiresAt);\n if (Number.isNaN(expiry)) return RETRY_MS;\n return Math.max(TOKEN_MIN_DELAY_MS, expiry - TOKEN_RENEW_BEFORE_MS - now);\n}\n\n/** Whether a token still has long enough to run for a join to be worth sending with it. */\nfunction isUsable(expiresAt: string, now: number): boolean {\n return Date.parse(expiresAt) - now > TOKEN_MIN_DELAY_MS;\n}\n\n/** The wait before the fresh join after `closures` closures with no subscription between. */\nfunction rejoinDelay(closures: number): number {\n const last = REJOIN_DELAYS_MS.length - 1;\n return REJOIN_DELAYS_MS[Math.min(Math.max(closures - 1, 0), last)];\n}\n\nfunction ignore(): void {\n // A promise whose outcome nothing waits for.\n}\n\n/**\n * Hands the socket a token. Settles once the socket has it, or once it\n * failed, and never throws: the join follows either way, and a channel\n * without the token reports its own failure.\n */\nfunction authorise(client: RealtimeClientLike, token: string): Promise<void> {\n return new Promise<void>((resolve) => {\n resolve(client.realtime.setAuth(token));\n }).then(ignore, ignore);\n}\n\n/**\n * Subscribes to changes in the signed-in trainee's conversation over\n * Supabase Realtime, and returns a function that ends the subscription.\n *\n * It fetches a token (`messages.realtimeToken`) and the connection settings\n * (`app.bootstrap`), creates a client with `options.realtime`, hands the\n * socket the token, and joins the private topic the token names. The token\n * is renewed a minute before it expires, on the open channel; a renewed\n * token for another topic (another trainee signed in on the same client)\n * leaves that channel and its client and joins the new topic on a new one.\n * A channel Realtime closes is joined afresh on the same client after a\n * wait (5 seconds, 15, then every minute), with the token handed to the\n * socket again first, or a new one if it has expired. Requests that fail\n * for now are retried every minute; a refused token (`auth_required`,\n * `forbidden`, `not_found`, `validation`) ends the subscription with\n * `refused`.\n *\n * `onChange` carries no data: read the conversation and the unread count\n * again. `onCoachTyping`, when given, says who on the coaching team is\n * writing to the trainee while the channel is live\n * ({@link CoachTypingState}). Poll while the status is `connecting` or\n * `fallback`. Needs\n * `@supabase/supabase-js` 2.87.1 or later (see {@link RealtimeClientFactory}).\n *\n * @example\n * ```ts\n * import { createClient } from \"@supabase/supabase-js\";\n * import { MESSAGES_FALLBACK_POLL_MS, subscribeToMessages } from \"@fytkit/sdk\";\n *\n * const unsubscribe = subscribeToMessages(fytkit, {\n * // A client for the subscription alone, carrying the trainee's token only.\n * realtime: ({ url, anonKey }) =>\n * createClient(url, anonKey, {\n * auth: { persistSession: false, autoRefreshToken: false, detectSessionInUrl: false },\n * }),\n * onChange: () => refreshConversation(),\n * onStatus: (status) => setPolling(status === \"connecting\" || status === \"fallback\"),\n * });\n * ```\n */\nexport function subscribeToMessages(\n client: MessagesRealtimeSource,\n options: SubscribeToMessagesOptions,\n): () => void {\n let closed = false;\n let lastStatus: MessagesRealtimeStatus | null = null;\n // The renewal timer: the next token request, or a retry of one.\n let timer: ReturnType<typeof setTimeout> | null = null;\n let renewing = false;\n let connection: RealtimeConnection | null = null;\n // The client the subscription owns, the grant whose token its socket\n // holds, and the channel once joined: one trainee's, replaced together.\n let socket: RealtimeClientLike | null = null;\n let held: MessagesRealtimeGrant | null = null;\n let channel: RealtimeChannelLike | null = null;\n // A fresh join waiting after Realtime closed the channel, and how many\n // closures came one after another without a subscription between.\n let rejoinTimer: ReturnType<typeof setTimeout> | null = null;\n let closures = 0;\n // Stops listening for a sign-out on the client, once the subscription ends.\n let stopAuthListener: (() => void) | null = null;\n // Who is writing, by their key, oldest first, each with the timer that drops them.\n const writing = new Map<\n string,\n { readonly name: string; readonly expiry: ReturnType<typeof setTimeout> }\n >();\n\n // The names last reported, so a change that shows the same names reports nothing.\n let reportedNames: readonly string[] = [];\n\n const reportTyping = (): void => {\n if (closed) return;\n // Two staff members of one name show it once.\n const names = [...new Set([...writing.values()].map((writer) => writer.name))];\n if (\n names.length === reportedNames.length &&\n names.every((name, i) => name === reportedNames[i])\n ) {\n return;\n }\n reportedNames = names;\n options.onCoachTyping?.(names.length === 0 ? { kind: \"idle\" } : { kind: \"typing\", names });\n };\n\n /** Forgets everyone writing (the channel is lost or replaced), and says so unless it ended. */\n const clearTyping = (): void => {\n if (writing.size === 0) return;\n for (const writer of writing.values()) clearTimeout(writer.expiry);\n writing.clear();\n reportTyping();\n };\n\n const onTypingSign = (sign: CoachTypingSign): void => {\n const known = writing.get(sign.key);\n if (known !== undefined) clearTimeout(known.expiry);\n if (sign.state === \"stopped\") {\n if (known === undefined) return;\n writing.delete(sign.key);\n reportTyping();\n return;\n }\n writing.set(sign.key, {\n name: sign.name,\n expiry: setTimeout(() => {\n writing.delete(sign.key);\n reportTyping();\n }, COACH_TYPING_EXPIRY_MS),\n });\n reportTyping();\n };\n\n const report = (status: MessagesRealtimeStatus): void => {\n if (closed || status === lastStatus) return;\n lastStatus = status;\n // Typing is heard only while live: off it, nobody can say they stopped.\n if (status !== \"live\") clearTyping();\n options.onStatus?.(status);\n };\n\n const clearTimer = (): void => {\n if (timer === null) return;\n clearTimeout(timer);\n timer = null;\n };\n\n const after = (delayMs: number, run: () => Promise<void>): void => {\n clearTimer();\n if (closed) return;\n timer = setTimeout(() => {\n timer = null;\n void run();\n }, delayMs);\n };\n\n const cancelRejoin = (): void => {\n if (rejoinTimer !== null) clearTimeout(rejoinTimer);\n rejoinTimer = null;\n };\n\n /** Leaves the channel and drops the socket; nothing they report counts any more. */\n const drop = (): void => {\n const open = socket;\n const joined = channel;\n socket = null;\n held = null;\n channel = null;\n cancelRejoin();\n closures = 0;\n clearTyping();\n if (open === null) return;\n if (joined !== null) Promise.resolve(open.removeChannel(joined)).then(ignore, ignore);\n open.realtime.disconnect();\n };\n\n const close = (): void => {\n if (closed) return;\n closed = true;\n clearTimer();\n drop();\n const stop = stopAuthListener;\n stopAuthListener = null;\n stop?.();\n };\n\n /**\n * Stops, then reports a last status: nothing is asked or reported again.\n * Stopping first means an `onStatus` that throws can't keep the channel.\n */\n const finish = (status: MessagesRealtimeStatus): void => {\n if (closed) return;\n const changed = status !== lastStatus;\n const wasTyping = writing.size > 0;\n lastStatus = status;\n close();\n if (wasTyping) options.onCoachTyping?.({ kind: \"idle\" });\n if (changed) options.onStatus?.(status);\n };\n\n const join = (joinTopic: string, open: RealtimeClientLike): void => {\n // Unsubscribed, the socket was replaced while it took the token, or a\n // join on it has been made since.\n if (closed || socket !== open || channel !== null) return;\n const joined = open.channel(joinTopic, { config: { private: true } });\n channel = joined;\n // A channel the subscription has left (a new trainee's replaced it,\n // Realtime closed it, or it ended) reports nothing.\n const current = (): boolean => !closed && channel === joined;\n joined\n .on(\"broadcast\", { event: THREAD_CHANGED_EVENT }, () => {\n if (current()) options.onChange();\n })\n .on(\"broadcast\", { event: COACH_TYPING_EVENT }, (message) => {\n const sign = coachTypingSign(message);\n if (sign !== null && current()) onTypingSign(sign);\n })\n .subscribe((status) => {\n if (!current()) return;\n if (status === \"SUBSCRIBED\") {\n closures = 0;\n report(\"live\");\n // Changes between the last read and the join were not heard.\n options.onChange();\n return;\n }\n report(\"fallback\");\n // CHANNEL_ERROR or TIMED_OUT: realtime-js joins this channel again\n // by itself. CLOSED (a Realtime restart, a token that expired before\n // its renewal landed): realtime-js has let it go for good, so join a\n // fresh one after a wait.\n if (status === \"CLOSED\") {\n channel = null;\n scheduleRejoin();\n }\n });\n };\n\n /**\n * Hands `open` the grant's token, then joins its topic: a join sent\n * before the socket holds the token carries the anon key alone, which the\n * private topic refuses.\n */\n const authoriseAndJoin = (grant: MessagesRealtimeGrant, open: RealtimeClientLike): void => {\n void authorise(open, grant.token).then(() => join(grant.topic, open));\n };\n\n /** Joins afresh after Realtime closed the channel, with a token it will take. */\n const rejoin = (): void => {\n const open = socket;\n const grant = held;\n if (closed || open === null || grant === null || channel !== null) return;\n // An expired token would be refused: the renewal joins with a new one.\n if (!isUsable(grant.expiresAt, Date.now())) {\n void renew();\n return;\n }\n authoriseAndJoin(grant, open);\n };\n\n const scheduleRejoin = (): void => {\n if (closed || rejoinTimer !== null) return;\n closures += 1;\n rejoinTimer = setTimeout(() => {\n rejoinTimer = null;\n rejoin();\n }, rejoinDelay(closures));\n };\n\n /** Opens a client on the settings and joins the grant's topic once its socket holds the token. */\n const connect = (grant: MessagesRealtimeGrant, settings: RealtimeConnection): void => {\n let opened: RealtimeClientLike;\n try {\n opened = options.realtime(settings);\n } catch {\n // The app's factory can't make a client (a malformed URL, say):\n // asking again won't change that.\n finish(\"fallback\");\n return;\n }\n socket = opened;\n held = grant;\n after(renewalDelay(grant.expiresAt, Date.now()), renew);\n authoriseAndJoin(grant, opened);\n };\n\n /** Opens the subscription on a grant: its topic, the settings (fetched once), a client. */\n const openGrant = async (grant: MessagesRealtimeGrant): Promise<void> => {\n // A server from before `topic` was added: no topic to join.\n if (!isMemberTopic(grant.topic)) {\n finish(\"fallback\");\n return;\n }\n\n if (connection === null) {\n let realtime: unknown;\n try {\n const bootstrap = await client.app.bootstrap();\n realtime = bootstrap.status === \"ok\" ? bootstrap.data.realtime : undefined;\n } catch (error) {\n if (closed) return;\n if (isFinalError(error)) {\n finish(\"fallback\");\n return;\n }\n report(\"fallback\");\n after(RETRY_MS, start);\n return;\n }\n if (closed) return;\n // A server from before the settings were published.\n if (!isConnection(realtime)) {\n finish(\"fallback\");\n return;\n }\n connection = realtime;\n }\n\n connect(grant, connection);\n };\n\n const renew = async (): Promise<void> => {\n // One request at a time: the one in flight joins when it lands.\n if (renewing) return;\n renewing = true;\n let grant: MessagesRealtimeGrant;\n try {\n grant = (await client.messages.realtimeToken({})).data;\n } catch (error) {\n if (closed) return;\n // A refusal stands: the token from before is not used again.\n if (isFinalError(error)) finish(\"refused\");\n else after(RETRY_MS, renew);\n return;\n } finally {\n renewing = false;\n }\n const current = socket;\n if (closed || current === null || held === null) return;\n if (grant.topic !== held.topic) {\n // Another trainee signed in on the same client: the open channel is\n // the previous trainee's. Leave it and open the new trainee's, as the\n // member portal does when its member changes.\n drop();\n report(\"connecting\");\n await openGrant(grant);\n return;\n }\n held = grant;\n after(renewalDelay(grant.expiresAt, Date.now()), renew);\n if (channel !== null) {\n // The open channel carries on with the new token; no rejoin.\n void authorise(current, grant.token);\n return;\n }\n // Realtime closed the channel: join afresh now, with the new token.\n cancelRejoin();\n authoriseAndJoin(grant, current);\n };\n\n const start = async (): Promise<void> => {\n let grant: MessagesRealtimeGrant;\n try {\n grant = (await client.messages.realtimeToken({})).data;\n } catch (error) {\n if (closed) return;\n if (isFinalError(error)) {\n finish(\"refused\");\n return;\n }\n report(\"fallback\");\n after(RETRY_MS, start);\n return;\n }\n if (closed) return;\n await openGrant(grant);\n };\n\n report(\"connecting\");\n // A sign-out leaves no trainee to listen for: stop now, not at the next\n // renewal, so the previous trainee's channel stays open no longer.\n stopAuthListener =\n client.auth?.onAuthStateChange((event) => {\n if (SIGNED_OUT_EVENTS.has(event.type)) finish(\"refused\");\n }) ?? null;\n void start();\n return close;\n}\n","import { createAppNamespace } from \"./app\";\nimport { createAuthNamespace } from \"./auth\";\nimport { DEFAULT_BASE_URL } from \"./base-url\";\nimport { createDataNamespaces } from \"./data\";\nimport { createLocalStorageAdapter, createMemoryStorageAdapter } from \"./storage\";\nimport { attachRefreshInterceptor, createTransport } from \"./transport\";\nimport type { FytkitClient, FytkitClientOptions } from \"./types\";\n\nexport function createFytkitClient(options: FytkitClientOptions): FytkitClient {\n if (!options || typeof options.clientId !== \"string\" || options.clientId.length === 0) {\n throw new Error(\"createFytkitClient: `clientId` is required\");\n }\n\n const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;\n const storage = options.storage ?? createLocalStorageAdapter(options.clientId);\n\n // `auth` is declared after `transport` because `createAuthNamespace`\n // needs the transport to make its RPC calls. The transport in turn needs\n // a `getSession` that reads the auth cache — we resolve the cycle with a\n // late-bound reference: the closure below reads `auth` lazily, which is\n // safe because no request can fire before construction finishes.\n let auth: ReturnType<typeof createAuthNamespace> | null = null;\n\n const transport = createTransport({\n baseUrl,\n clientId: options.clientId,\n getSession: () => auth?.getSession() ?? null,\n fetch: options.fetch,\n });\n\n const authInstance = createAuthNamespace({ transport, storage });\n auth = authInstance;\n attachRefreshInterceptor({ transport, auth: authInstance });\n\n const data = createDataNamespaces(transport);\n\n return {\n clientId: options.clientId,\n auth: {\n signUp: authInstance.signUp,\n signIn: authInstance.signIn,\n signInWithIdToken: authInstance.signInWithIdToken,\n signOut: authInstance.signOut,\n getSession: authInstance.getSession,\n onAuthStateChange: authInstance.onAuthStateChange,\n requestPasswordReset: authInstance.requestPasswordReset,\n confirmPasswordReset: authInstance.confirmPasswordReset,\n confirmEmail: authInstance.confirmEmail,\n resendVerification: authInstance.resendVerification,\n verifyTotp: authInstance.verifyTotp,\n verifyBackupCode: authInstance.verifyBackupCode,\n requestEmailOtp: authInstance.requestEmailOtp,\n verifyEmailOtp: authInstance.verifyEmailOtp,\n getSecurityStatus: authInstance.getSecurityStatus,\n enableTotp: authInstance.enableTotp,\n verifyTotpSetup: authInstance.verifyTotpSetup,\n disableTwoFactor: authInstance.disableTwoFactor,\n regenerateBackupCodes: authInstance.regenerateBackupCodes,\n changePassword: authInstance.changePassword,\n changeEmail: authInstance.changeEmail,\n confirmEmailChange: authInstance.confirmEmailChange,\n listSessions: authInstance.listSessions,\n revokeSession: authInstance.revokeSession,\n revokeOtherSessions: authInstance.revokeOtherSessions,\n closeAccount: authInstance.closeAccount,\n },\n ...data,\n app: createAppNamespace(transport),\n };\n}\n\n// Public re-exports.\nexport { createLocalStorageAdapter, createMemoryStorageAdapter };\n// `PublicApi` is the inferred router type that backs every namespace\n// on `FytkitClient` (e.g. `client.workouts` is `PublicApi[\"workout\"]`).\n// Re-exported so consumers can write generics over the router shape —\n// previously this surfaced as an `ae-forgotten-export` warning on\n// `FytkitClient` in the api-extractor report. The api-extractor will\n// still warn about `PublicRouter`/`publicRouter` being unexported —\n// that is intentional, those are implementation-detail names from the\n// generated snapshot that should not be part of the consumer surface.\nexport type { PublicApi } from \"@fytkit/types\";\nexport type {\n AppBootstrapData,\n AppBootstrapOptions,\n AppBootstrapResult,\n AppNamespace,\n} from \"./app\";\nexport type {\n JoinCodeCoach,\n JoinCodeResolution,\n JoinCodeTheme,\n ResolveJoinCodeOptions,\n} from \"./join\";\nexport { resolveJoinCode } from \"./join\";\nexport type {\n PaymentMode,\n PaymentPlatform,\n PaymentPolicy,\n ResolvePaymentModeOptions,\n} from \"./payment-policy\";\nexport { resolvePaymentMode } from \"./payment-policy\";\nexport type {\n CoachTypingState,\n MessagesRealtimeAuthEvent,\n MessagesRealtimeGrant,\n MessagesRealtimeSource,\n MessagesRealtimeStatus,\n RealtimeChannelLike,\n RealtimeClientFactory,\n RealtimeClientLike,\n RealtimeConnection,\n SubscribeToMessagesOptions,\n} from \"./realtime\";\nexport {\n COACH_TYPING_EXPIRY_MS,\n MESSAGES_FALLBACK_POLL_MS,\n subscribeToMessages,\n} from \"./realtime\";\nexport type {\n FytkitRefusal,\n FytkitRefusalDataByReason,\n FytkitRefusalReason,\n FytkitRefusalRetryAt,\n} from \"./refusal\";\nexport { refusalData } from \"./refusal\";\nexport type {\n AuthState,\n CloseAccountInput,\n FytkitClient,\n FytkitClientOptions,\n FytkitError,\n FytkitErrorCode,\n IdTokenProvider,\n IdTokenReauthentication,\n IdTokenSignInResult,\n ResendVerificationResult,\n SecurityStatus,\n Session,\n SessionInfo,\n SignInResult,\n TokenStorage,\n TotpEnrollment,\n TwoFactorChallenge,\n TwoFactorMethod,\n TwoFactorToken,\n} from \"./types\";\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fytkit/sdk",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "description": "Vanilla TypeScript SDK for fytkit's public API",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",