@capxul/sdk 0.1.0-alpha.6 → 0.1.0-alpha.8

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/dist/client.js CHANGED
@@ -577,7 +577,12 @@ var Errors = {
577
577
  "Idempotency key was already used for a different request",
578
578
  { details }
579
579
  ),
580
- emailDeliveryFailed: (detail) => new CapxulError2("EMAIL_DELIVERY_FAILED", `Failed to send email: ${detail}`),
580
+ emailDeliveryFailed: (detail, details) => new CapxulError2("EMAIL_DELIVERY_FAILED", `Failed to send email: ${detail}`, {
581
+ details
582
+ }),
583
+ rateLimited: (details) => new CapxulError2("RATE_LIMITED", "Request was rate limited", {
584
+ details: { ...details }
585
+ }),
581
586
  internalError: (reason) => new CapxulError2("INTERNAL_ERROR", `Internal error: ${reason}`),
582
587
  /**
583
588
  * Verification gate. Surfaced when a request hits a verification
@@ -650,13 +655,13 @@ function createLifecycle(initial) {
650
655
  }
651
656
  function makeBuildTimeUrlsTransport(config) {
652
657
  if (!config.authBaseUrl || config.authBaseUrl.trim().length === 0) {
653
- throw Errors.invalidInput(
658
+ throw invalidConfigError(
654
659
  "authBaseUrl",
655
660
  "build-time-urls transport requires a non-empty authBaseUrl."
656
661
  );
657
662
  }
658
663
  if (!config.convexUrl || config.convexUrl.trim().length === 0) {
659
- throw Errors.invalidInput(
664
+ throw invalidConfigError(
660
665
  "convexUrl",
661
666
  "build-time-urls transport requires a non-empty convexUrl."
662
667
  );
@@ -670,6 +675,7 @@ function makeBuildTimeUrlsTransport(config) {
670
675
  return {
671
676
  authBaseUrl,
672
677
  convexUrl,
678
+ ensureRuntime: async () => runtime,
673
679
  fetch: (path, init) => fetchImpl(resolveUrl(authBaseUrl, path), init),
674
680
  getState: lifecycle.getState,
675
681
  subscribe: lifecycle.subscribe,
@@ -685,14 +691,15 @@ function makeBuildTimeUrlsTransport(config) {
685
691
  };
686
692
  }
687
693
  function makePublishableKeyTransport(config) {
688
- if (!config.publishableKey || config.publishableKey.trim().length === 0) {
689
- throw Errors.invalidInput(
694
+ const publishableKey = config.publishableKey?.trim();
695
+ if (!publishableKey) {
696
+ throw invalidConfigError(
690
697
  "publishableKey",
691
698
  "publishable-key transport requires a non-empty publishableKey."
692
699
  );
693
700
  }
694
701
  const fetchImpl = config.fetchImpl ?? globalThis.fetch;
695
- const bootstrapUrl = stripTrailingSlash(
702
+ const bootstrapUrl = normalizeBootstrapUrl(
696
703
  config.bootstrapUrl ?? `${CAPXUL_API_BASE_URL}/v1/client/bootstrap`
697
704
  );
698
705
  let authBaseUrl = "";
@@ -707,23 +714,20 @@ function makePublishableKeyTransport(config) {
707
714
  const response = await fetchImpl(bootstrapUrl, {
708
715
  method: "POST",
709
716
  headers: { "content-type": "application/json" },
710
- body: JSON.stringify({ publishableKey: config.publishableKey })
717
+ body: JSON.stringify({ publishableKey })
711
718
  });
712
719
  if (!response.ok) {
713
- throw Errors.invalidInput(
714
- "publishableKey",
715
- `${bootstrapUrl} failed with HTTP ${response.status}.`
716
- );
720
+ throw await bootstrapResponseError(response, bootstrapUrl);
717
721
  }
718
- const body = await response.json();
722
+ const body = await readBootstrapSuccessBody(response);
719
723
  if (typeof body.authBaseUrl !== "string" || body.authBaseUrl.trim().length === 0) {
720
- throw Errors.invalidInput(
724
+ throw bootstrapContractError(
721
725
  "authBaseUrl",
722
726
  "/v1/client/bootstrap returned no authBaseUrl."
723
727
  );
724
728
  }
725
729
  if (typeof body.convexUrl !== "string" || body.convexUrl.trim().length === 0) {
726
- throw Errors.invalidInput(
730
+ throw bootstrapContractError(
727
731
  "convexUrl",
728
732
  "/v1/client/bootstrap returned no convexUrl."
729
733
  );
@@ -735,16 +739,13 @@ function makePublishableKeyTransport(config) {
735
739
  return runtime;
736
740
  })();
737
741
  bootstrapPromise = attempt.catch((err) => {
742
+ const error = normalizeBootstrapThrownError(err);
738
743
  bootstrapPromise = null;
739
744
  lifecycle.setState({
740
745
  status: "error",
741
- error: err instanceof CapxulError ? err : new CapxulError({
742
- code: "UNKNOWN",
743
- message: "Bootstrap failed without a typed CapxulError.",
744
- cause: err
745
- })
746
+ error
746
747
  });
747
- throw err;
748
+ throw error;
748
749
  });
749
750
  return await bootstrapPromise;
750
751
  }
@@ -755,6 +756,7 @@ function makePublishableKeyTransport(config) {
755
756
  get convexUrl() {
756
757
  return convexUrl;
757
758
  },
759
+ ensureRuntime: ensureBootstrap,
758
760
  fetch: async (path, init) => {
759
761
  const resolved = await ensureBootstrap();
760
762
  return await fetchImpl(resolveUrl(resolved.authBaseUrl, path), init);
@@ -765,7 +767,7 @@ function makePublishableKeyTransport(config) {
765
767
  markAuthenticated: ({ dataClient: nextDataClient }) => {
766
768
  const current = lifecycle.getState();
767
769
  if (current.status !== "ready" && current.status !== "authenticated") {
768
- throw Errors.internalError(
770
+ throw internalTransportError(
769
771
  `markAuthenticated() called from status="${current.status}". Expected "ready" or "authenticated".`
770
772
  );
771
773
  }
@@ -787,6 +789,24 @@ function makePublishableKeyTransport(config) {
787
789
  function stripTrailingSlash(url) {
788
790
  return url.replace(/\/+$/, "");
789
791
  }
792
+ function normalizeBootstrapUrl(url) {
793
+ const normalized = stripTrailingSlash(url.trim());
794
+ if (!isAbsoluteHttpUrl(normalized)) {
795
+ throw invalidConfigError(
796
+ "bootstrapUrl",
797
+ "publishable-key transport requires an absolute http(s) bootstrapUrl."
798
+ );
799
+ }
800
+ return normalized;
801
+ }
802
+ function isAbsoluteHttpUrl(url) {
803
+ try {
804
+ const parsed = new URL(url);
805
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
806
+ } catch {
807
+ return false;
808
+ }
809
+ }
790
810
  function resolveUrl(authBaseUrl, path) {
791
811
  if (path.startsWith("http://") || path.startsWith("https://")) {
792
812
  return path;
@@ -794,10 +814,142 @@ function resolveUrl(authBaseUrl, path) {
794
814
  return `${authBaseUrl}${path}`;
795
815
  }
796
816
  function assertNever(value) {
797
- throw Errors.internalError(
817
+ throw internalTransportError(
798
818
  `Unhandled BrowserCapxulConfig.mode: ${String(value.mode)}.`
799
819
  );
800
820
  }
821
+ function invalidConfigError(field, reason) {
822
+ return new CapxulError({
823
+ code: "INVALID_INPUT",
824
+ message: `Invalid ${field}: ${reason}`,
825
+ details: { source: "sdk-config", field, reason }
826
+ });
827
+ }
828
+ function bootstrapContractError(field, message) {
829
+ return new CapxulError({
830
+ code: "INVALID_INPUT",
831
+ message,
832
+ details: {
833
+ source: "backend-bootstrap",
834
+ phase: "publishable-key-bootstrap",
835
+ field,
836
+ reason: message
837
+ }
838
+ });
839
+ }
840
+ function internalTransportError(reason) {
841
+ return new CapxulError({
842
+ code: "INTERNAL_ERROR",
843
+ message: `Internal error: ${reason}`,
844
+ details: { source: "sdk-transport", reason }
845
+ });
846
+ }
847
+ async function bootstrapResponseError(response, bootstrapUrl) {
848
+ const envelope = await readBootstrapErrorEnvelope(response);
849
+ const wireCode = readNonEmptyString(envelope?.error?.code);
850
+ const normalized = normalizeBootstrapErrorCode(wireCode);
851
+ const message = readNonEmptyString(envelope?.error?.message) ?? `${bootstrapUrl} failed with HTTP ${response.status}.`;
852
+ const backendDetails = readRecord(envelope?.error?.details);
853
+ return new CapxulError({
854
+ code: normalized.code,
855
+ message,
856
+ details: {
857
+ ...backendDetails,
858
+ source: "backend-bootstrap",
859
+ phase: "publishable-key-bootstrap",
860
+ httpStatus: response.status,
861
+ ...normalized.wireCode ? { wireCode: normalized.wireCode } : {}
862
+ },
863
+ operationId: readNonEmptyString(envelope?.error?.operationId),
864
+ correlationId: readNonEmptyString(envelope?.error?.correlationId),
865
+ retryable: typeof envelope?.error?.retryable === "boolean" ? envelope.error.retryable : void 0
866
+ });
867
+ }
868
+ async function readBootstrapErrorEnvelope(response) {
869
+ try {
870
+ const parsed = await response.json();
871
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
872
+ } catch {
873
+ return null;
874
+ }
875
+ }
876
+ async function readBootstrapSuccessBody(response) {
877
+ try {
878
+ const parsed = await response.json();
879
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
880
+ } catch {
881
+ throw bootstrapContractError(
882
+ "body",
883
+ "/v1/client/bootstrap returned invalid JSON."
884
+ );
885
+ }
886
+ }
887
+ function normalizeBootstrapThrownError(error) {
888
+ if (error instanceof CapxulError) return error;
889
+ return new CapxulError({
890
+ code: "NETWORK_ERROR",
891
+ message: "Publishable-key bootstrap network failure.",
892
+ cause: error,
893
+ details: {
894
+ source: "bootstrap-network",
895
+ phase: "publishable-key-bootstrap"
896
+ }
897
+ });
898
+ }
899
+ function normalizeBootstrapErrorCode(wireCode) {
900
+ if (wireCode === "INTERNAL_SERVER_ERROR") {
901
+ return { code: "INTERNAL_ERROR", wireCode };
902
+ }
903
+ if (wireCode && isCapxulErrorCode(wireCode)) {
904
+ return { code: wireCode };
905
+ }
906
+ return wireCode ? { code: "UNKNOWN", wireCode } : { code: "UNKNOWN" };
907
+ }
908
+ var CAPXUL_ERROR_CODES = /* @__PURE__ */ new Set([
909
+ "NOT_AUTHENTICATED",
910
+ "EMAIL_DELIVERY_FAILED",
911
+ "PROFILE_NOT_FOUND",
912
+ "SMART_ACCOUNT_MISSING",
913
+ "PLAYER_NOT_FOUND",
914
+ "ACCOUNT_NOT_FOUND",
915
+ "PROVIDER_ERROR",
916
+ "INVALID_INPUT",
917
+ "ENV_MISSING",
918
+ "NOT_IMPLEMENTED",
919
+ "VERIFICATION_REQUIRED",
920
+ "INSUFFICIENT_BALANCE",
921
+ "INVALID_RECIPIENT",
922
+ "TRANSACTION_FAILED",
923
+ "RATE_LIMITED",
924
+ "NETWORK_ERROR",
925
+ "UNKNOWN",
926
+ "PERMISSION_DENIED",
927
+ "API_KEY_INVALID",
928
+ "API_KEY_EXPIRED",
929
+ "IDEMPOTENCY_CONFLICT",
930
+ "NOT_FOUND",
931
+ "OPERATION_CANCELED",
932
+ "OPERATION_TIMEOUT",
933
+ "ACTION_REQUIRED",
934
+ "KYC_REQUIRED",
935
+ "POLICY_DENIED",
936
+ "SAFE_NOT_READY",
937
+ "PROVIDER_UNAVAILABLE",
938
+ "PROVIDER_REJECTED",
939
+ "RECONCILIATION_FAILED",
940
+ "INTERNAL_ERROR",
941
+ "QUOTE_EXPIRED",
942
+ "QUOTE_NOT_FOUND"
943
+ ]);
944
+ function isCapxulErrorCode(value) {
945
+ return CAPXUL_ERROR_CODES.has(value);
946
+ }
947
+ function readRecord(value) {
948
+ return typeof value === "object" && value !== null ? value : null;
949
+ }
950
+ function readNonEmptyString(value) {
951
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
952
+ }
801
953
 
802
954
  // src/core/auth.ts
803
955
  function createAuthClient(config = {}) {
@@ -852,13 +1004,16 @@ function createAuthClient(config = {}) {
852
1004
  email: signIn.user.email,
853
1005
  token: signIn.token,
854
1006
  convexJwt,
855
- expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString()
1007
+ expiresAt: new Date(
1008
+ Date.now() + 30 * 24 * 60 * 60 * 1e3
1009
+ ).toISOString()
856
1010
  };
857
1011
  sessionStore.set(session);
858
1012
  if (config.auth?.createDataClient) {
859
1013
  try {
860
1014
  dataClient = await config.auth.createDataClient(session);
861
1015
  mutableConfig(config).data = dataClient;
1016
+ transport.markAuthenticated({ dataClient });
862
1017
  } catch (cause) {
863
1018
  return [
864
1019
  new CapxulError({
@@ -877,6 +1032,8 @@ function createAuthClient(config = {}) {
877
1032
  sessionStore.clear();
878
1033
  dataClient = null;
879
1034
  mutableConfig(config).data = void 0;
1035
+ const transport = getTransport();
1036
+ transport?.clearAuth();
880
1037
  return [null, void 0];
881
1038
  },
882
1039
  serviceTokenMint: async () => stub("auth.serviceTokenMint"),
@@ -930,16 +1087,19 @@ async function postBetterAuth(transport, path, body, code, signal) {
930
1087
  body: JSON.stringify(body),
931
1088
  signal
932
1089
  });
1090
+ const text = await response.text();
933
1091
  if (!response.ok) {
1092
+ const parsedError = parseBetterAuthError(text);
934
1093
  return [
935
1094
  new CapxulError({
936
- code,
937
- message: `BetterAuth ${path} failed with HTTP ${response.status}.`
1095
+ code: parsedError.code ?? code,
1096
+ message: parsedError.message ?? `BetterAuth ${path} failed with HTTP ${response.status}.`,
1097
+ details: parsedError.details,
1098
+ retryable: parsedError.retryable
938
1099
  }),
939
1100
  null
940
1101
  ];
941
1102
  }
942
- const text = await response.text();
943
1103
  return [null, text ? JSON.parse(text) : void 0];
944
1104
  } catch (cause) {
945
1105
  return [
@@ -952,6 +1112,36 @@ async function postBetterAuth(transport, path, body, code, signal) {
952
1112
  ];
953
1113
  }
954
1114
  }
1115
+ function parseBetterAuthError(text) {
1116
+ if (!text.trim()) {
1117
+ return {};
1118
+ }
1119
+ try {
1120
+ const body = JSON.parse(text);
1121
+ if (!body || typeof body !== "object") {
1122
+ return {};
1123
+ }
1124
+ const record = body;
1125
+ const nested = record.error && typeof record.error === "object" ? record.error : record;
1126
+ const code = typeof nested.code === "string" ? nested.code : void 0;
1127
+ const message = typeof nested.message === "string" ? nested.message : void 0;
1128
+ const details = nested.details && typeof nested.details === "object" ? nested.details : void 0;
1129
+ const correlationId = typeof nested.correlationId === "string" ? nested.correlationId : void 0;
1130
+ const retryable = typeof nested.retryable === "boolean" ? nested.retryable : void 0;
1131
+ return {
1132
+ code: isCapxulErrorCode2(code) ? code : void 0,
1133
+ message,
1134
+ details,
1135
+ correlationId,
1136
+ retryable
1137
+ };
1138
+ } catch {
1139
+ return {};
1140
+ }
1141
+ }
1142
+ function isCapxulErrorCode2(code) {
1143
+ return code === "NOT_AUTHENTICATED" || code === "EMAIL_DELIVERY_FAILED" || code === "INVALID_INPUT" || code === "RATE_LIMITED" || code === "NETWORK_ERROR" || code === "API_KEY_INVALID" || code === "API_KEY_EXPIRED";
1144
+ }
955
1145
  async function exchangeConvexToken(transport, config, token, signal) {
956
1146
  const path = config.auth?.convexTokenUrl ?? "/convex/token";
957
1147
  try {