@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/index.js CHANGED
@@ -677,7 +677,12 @@ var Errors = {
677
677
  "Idempotency key was already used for a different request",
678
678
  { details }
679
679
  ),
680
- emailDeliveryFailed: (detail) => new CapxulError2("EMAIL_DELIVERY_FAILED", `Failed to send email: ${detail}`),
680
+ emailDeliveryFailed: (detail, details) => new CapxulError2("EMAIL_DELIVERY_FAILED", `Failed to send email: ${detail}`, {
681
+ details
682
+ }),
683
+ rateLimited: (details) => new CapxulError2("RATE_LIMITED", "Request was rate limited", {
684
+ details: { ...details }
685
+ }),
681
686
  internalError: (reason) => new CapxulError2("INTERNAL_ERROR", `Internal error: ${reason}`),
682
687
  /**
683
688
  * Verification gate. Surfaced when a request hits a verification
@@ -750,13 +755,13 @@ function createLifecycle(initial) {
750
755
  }
751
756
  function makeBuildTimeUrlsTransport(config) {
752
757
  if (!config.authBaseUrl || config.authBaseUrl.trim().length === 0) {
753
- throw Errors.invalidInput(
758
+ throw invalidConfigError(
754
759
  "authBaseUrl",
755
760
  "build-time-urls transport requires a non-empty authBaseUrl."
756
761
  );
757
762
  }
758
763
  if (!config.convexUrl || config.convexUrl.trim().length === 0) {
759
- throw Errors.invalidInput(
764
+ throw invalidConfigError(
760
765
  "convexUrl",
761
766
  "build-time-urls transport requires a non-empty convexUrl."
762
767
  );
@@ -770,6 +775,7 @@ function makeBuildTimeUrlsTransport(config) {
770
775
  return {
771
776
  authBaseUrl,
772
777
  convexUrl,
778
+ ensureRuntime: async () => runtime,
773
779
  fetch: (path, init) => fetchImpl(resolveUrl(authBaseUrl, path), init),
774
780
  getState: lifecycle.getState,
775
781
  subscribe: lifecycle.subscribe,
@@ -785,14 +791,15 @@ function makeBuildTimeUrlsTransport(config) {
785
791
  };
786
792
  }
787
793
  function makePublishableKeyTransport(config) {
788
- if (!config.publishableKey || config.publishableKey.trim().length === 0) {
789
- throw Errors.invalidInput(
794
+ const publishableKey = config.publishableKey?.trim();
795
+ if (!publishableKey) {
796
+ throw invalidConfigError(
790
797
  "publishableKey",
791
798
  "publishable-key transport requires a non-empty publishableKey."
792
799
  );
793
800
  }
794
801
  const fetchImpl = config.fetchImpl ?? globalThis.fetch;
795
- const bootstrapUrl = stripTrailingSlash(
802
+ const bootstrapUrl = normalizeBootstrapUrl(
796
803
  config.bootstrapUrl ?? `${CAPXUL_API_BASE_URL}/v1/client/bootstrap`
797
804
  );
798
805
  let authBaseUrl = "";
@@ -807,23 +814,20 @@ function makePublishableKeyTransport(config) {
807
814
  const response = await fetchImpl(bootstrapUrl, {
808
815
  method: "POST",
809
816
  headers: { "content-type": "application/json" },
810
- body: JSON.stringify({ publishableKey: config.publishableKey })
817
+ body: JSON.stringify({ publishableKey })
811
818
  });
812
819
  if (!response.ok) {
813
- throw Errors.invalidInput(
814
- "publishableKey",
815
- `${bootstrapUrl} failed with HTTP ${response.status}.`
816
- );
820
+ throw await bootstrapResponseError(response, bootstrapUrl);
817
821
  }
818
- const body = await response.json();
822
+ const body = await readBootstrapSuccessBody(response);
819
823
  if (typeof body.authBaseUrl !== "string" || body.authBaseUrl.trim().length === 0) {
820
- throw Errors.invalidInput(
824
+ throw bootstrapContractError(
821
825
  "authBaseUrl",
822
826
  "/v1/client/bootstrap returned no authBaseUrl."
823
827
  );
824
828
  }
825
829
  if (typeof body.convexUrl !== "string" || body.convexUrl.trim().length === 0) {
826
- throw Errors.invalidInput(
830
+ throw bootstrapContractError(
827
831
  "convexUrl",
828
832
  "/v1/client/bootstrap returned no convexUrl."
829
833
  );
@@ -835,16 +839,13 @@ function makePublishableKeyTransport(config) {
835
839
  return runtime;
836
840
  })();
837
841
  bootstrapPromise = attempt.catch((err) => {
842
+ const error = normalizeBootstrapThrownError(err);
838
843
  bootstrapPromise = null;
839
844
  lifecycle.setState({
840
845
  status: "error",
841
- error: err instanceof CapxulError ? err : new CapxulError({
842
- code: "UNKNOWN",
843
- message: "Bootstrap failed without a typed CapxulError.",
844
- cause: err
845
- })
846
+ error
846
847
  });
847
- throw err;
848
+ throw error;
848
849
  });
849
850
  return await bootstrapPromise;
850
851
  }
@@ -855,6 +856,7 @@ function makePublishableKeyTransport(config) {
855
856
  get convexUrl() {
856
857
  return convexUrl;
857
858
  },
859
+ ensureRuntime: ensureBootstrap,
858
860
  fetch: async (path, init) => {
859
861
  const resolved = await ensureBootstrap();
860
862
  return await fetchImpl(resolveUrl(resolved.authBaseUrl, path), init);
@@ -865,7 +867,7 @@ function makePublishableKeyTransport(config) {
865
867
  markAuthenticated: ({ dataClient: nextDataClient }) => {
866
868
  const current = lifecycle.getState();
867
869
  if (current.status !== "ready" && current.status !== "authenticated") {
868
- throw Errors.internalError(
870
+ throw internalTransportError(
869
871
  `markAuthenticated() called from status="${current.status}". Expected "ready" or "authenticated".`
870
872
  );
871
873
  }
@@ -887,6 +889,24 @@ function makePublishableKeyTransport(config) {
887
889
  function stripTrailingSlash(url) {
888
890
  return url.replace(/\/+$/, "");
889
891
  }
892
+ function normalizeBootstrapUrl(url) {
893
+ const normalized = stripTrailingSlash(url.trim());
894
+ if (!isAbsoluteHttpUrl(normalized)) {
895
+ throw invalidConfigError(
896
+ "bootstrapUrl",
897
+ "publishable-key transport requires an absolute http(s) bootstrapUrl."
898
+ );
899
+ }
900
+ return normalized;
901
+ }
902
+ function isAbsoluteHttpUrl(url) {
903
+ try {
904
+ const parsed = new URL(url);
905
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
906
+ } catch {
907
+ return false;
908
+ }
909
+ }
890
910
  function resolveUrl(authBaseUrl, path) {
891
911
  if (path.startsWith("http://") || path.startsWith("https://")) {
892
912
  return path;
@@ -894,10 +914,142 @@ function resolveUrl(authBaseUrl, path) {
894
914
  return `${authBaseUrl}${path}`;
895
915
  }
896
916
  function assertNever(value) {
897
- throw Errors.internalError(
917
+ throw internalTransportError(
898
918
  `Unhandled BrowserCapxulConfig.mode: ${String(value.mode)}.`
899
919
  );
900
920
  }
921
+ function invalidConfigError(field, reason) {
922
+ return new CapxulError({
923
+ code: "INVALID_INPUT",
924
+ message: `Invalid ${field}: ${reason}`,
925
+ details: { source: "sdk-config", field, reason }
926
+ });
927
+ }
928
+ function bootstrapContractError(field, message) {
929
+ return new CapxulError({
930
+ code: "INVALID_INPUT",
931
+ message,
932
+ details: {
933
+ source: "backend-bootstrap",
934
+ phase: "publishable-key-bootstrap",
935
+ field,
936
+ reason: message
937
+ }
938
+ });
939
+ }
940
+ function internalTransportError(reason) {
941
+ return new CapxulError({
942
+ code: "INTERNAL_ERROR",
943
+ message: `Internal error: ${reason}`,
944
+ details: { source: "sdk-transport", reason }
945
+ });
946
+ }
947
+ async function bootstrapResponseError(response, bootstrapUrl) {
948
+ const envelope = await readBootstrapErrorEnvelope(response);
949
+ const wireCode = readNonEmptyString(envelope?.error?.code);
950
+ const normalized = normalizeBootstrapErrorCode(wireCode);
951
+ const message = readNonEmptyString(envelope?.error?.message) ?? `${bootstrapUrl} failed with HTTP ${response.status}.`;
952
+ const backendDetails = readRecord(envelope?.error?.details);
953
+ return new CapxulError({
954
+ code: normalized.code,
955
+ message,
956
+ details: {
957
+ ...backendDetails,
958
+ source: "backend-bootstrap",
959
+ phase: "publishable-key-bootstrap",
960
+ httpStatus: response.status,
961
+ ...normalized.wireCode ? { wireCode: normalized.wireCode } : {}
962
+ },
963
+ operationId: readNonEmptyString(envelope?.error?.operationId),
964
+ correlationId: readNonEmptyString(envelope?.error?.correlationId),
965
+ retryable: typeof envelope?.error?.retryable === "boolean" ? envelope.error.retryable : void 0
966
+ });
967
+ }
968
+ async function readBootstrapErrorEnvelope(response) {
969
+ try {
970
+ const parsed = await response.json();
971
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
972
+ } catch {
973
+ return null;
974
+ }
975
+ }
976
+ async function readBootstrapSuccessBody(response) {
977
+ try {
978
+ const parsed = await response.json();
979
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
980
+ } catch {
981
+ throw bootstrapContractError(
982
+ "body",
983
+ "/v1/client/bootstrap returned invalid JSON."
984
+ );
985
+ }
986
+ }
987
+ function normalizeBootstrapThrownError(error) {
988
+ if (error instanceof CapxulError) return error;
989
+ return new CapxulError({
990
+ code: "NETWORK_ERROR",
991
+ message: "Publishable-key bootstrap network failure.",
992
+ cause: error,
993
+ details: {
994
+ source: "bootstrap-network",
995
+ phase: "publishable-key-bootstrap"
996
+ }
997
+ });
998
+ }
999
+ function normalizeBootstrapErrorCode(wireCode) {
1000
+ if (wireCode === "INTERNAL_SERVER_ERROR") {
1001
+ return { code: "INTERNAL_ERROR", wireCode };
1002
+ }
1003
+ if (wireCode && isCapxulErrorCode(wireCode)) {
1004
+ return { code: wireCode };
1005
+ }
1006
+ return wireCode ? { code: "UNKNOWN", wireCode } : { code: "UNKNOWN" };
1007
+ }
1008
+ var CAPXUL_ERROR_CODES = /* @__PURE__ */ new Set([
1009
+ "NOT_AUTHENTICATED",
1010
+ "EMAIL_DELIVERY_FAILED",
1011
+ "PROFILE_NOT_FOUND",
1012
+ "SMART_ACCOUNT_MISSING",
1013
+ "PLAYER_NOT_FOUND",
1014
+ "ACCOUNT_NOT_FOUND",
1015
+ "PROVIDER_ERROR",
1016
+ "INVALID_INPUT",
1017
+ "ENV_MISSING",
1018
+ "NOT_IMPLEMENTED",
1019
+ "VERIFICATION_REQUIRED",
1020
+ "INSUFFICIENT_BALANCE",
1021
+ "INVALID_RECIPIENT",
1022
+ "TRANSACTION_FAILED",
1023
+ "RATE_LIMITED",
1024
+ "NETWORK_ERROR",
1025
+ "UNKNOWN",
1026
+ "PERMISSION_DENIED",
1027
+ "API_KEY_INVALID",
1028
+ "API_KEY_EXPIRED",
1029
+ "IDEMPOTENCY_CONFLICT",
1030
+ "NOT_FOUND",
1031
+ "OPERATION_CANCELED",
1032
+ "OPERATION_TIMEOUT",
1033
+ "ACTION_REQUIRED",
1034
+ "KYC_REQUIRED",
1035
+ "POLICY_DENIED",
1036
+ "SAFE_NOT_READY",
1037
+ "PROVIDER_UNAVAILABLE",
1038
+ "PROVIDER_REJECTED",
1039
+ "RECONCILIATION_FAILED",
1040
+ "INTERNAL_ERROR",
1041
+ "QUOTE_EXPIRED",
1042
+ "QUOTE_NOT_FOUND"
1043
+ ]);
1044
+ function isCapxulErrorCode(value) {
1045
+ return CAPXUL_ERROR_CODES.has(value);
1046
+ }
1047
+ function readRecord(value) {
1048
+ return typeof value === "object" && value !== null ? value : null;
1049
+ }
1050
+ function readNonEmptyString(value) {
1051
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
1052
+ }
901
1053
 
902
1054
  // src/core/auth.ts
903
1055
  function createAuthClient(config = {}) {
@@ -952,13 +1104,16 @@ function createAuthClient(config = {}) {
952
1104
  email: signIn.user.email,
953
1105
  token: signIn.token,
954
1106
  convexJwt,
955
- expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString()
1107
+ expiresAt: new Date(
1108
+ Date.now() + 30 * 24 * 60 * 60 * 1e3
1109
+ ).toISOString()
956
1110
  };
957
1111
  sessionStore.set(session);
958
1112
  if (config.auth?.createDataClient) {
959
1113
  try {
960
1114
  dataClient = await config.auth.createDataClient(session);
961
1115
  mutableConfig(config).data = dataClient;
1116
+ transport.markAuthenticated({ dataClient });
962
1117
  } catch (cause) {
963
1118
  return [
964
1119
  new CapxulError({
@@ -977,6 +1132,8 @@ function createAuthClient(config = {}) {
977
1132
  sessionStore.clear();
978
1133
  dataClient = null;
979
1134
  mutableConfig(config).data = void 0;
1135
+ const transport = getTransport();
1136
+ transport?.clearAuth();
980
1137
  return [null, void 0];
981
1138
  },
982
1139
  serviceTokenMint: async () => stub("auth.serviceTokenMint"),
@@ -1030,16 +1187,19 @@ async function postBetterAuth(transport, path, body, code, signal) {
1030
1187
  body: JSON.stringify(body),
1031
1188
  signal
1032
1189
  });
1190
+ const text = await response.text();
1033
1191
  if (!response.ok) {
1192
+ const parsedError = parseBetterAuthError(text);
1034
1193
  return [
1035
1194
  new CapxulError({
1036
- code,
1037
- message: `BetterAuth ${path} failed with HTTP ${response.status}.`
1195
+ code: parsedError.code ?? code,
1196
+ message: parsedError.message ?? `BetterAuth ${path} failed with HTTP ${response.status}.`,
1197
+ details: parsedError.details,
1198
+ retryable: parsedError.retryable
1038
1199
  }),
1039
1200
  null
1040
1201
  ];
1041
1202
  }
1042
- const text = await response.text();
1043
1203
  return [null, text ? JSON.parse(text) : void 0];
1044
1204
  } catch (cause) {
1045
1205
  return [
@@ -1052,6 +1212,36 @@ async function postBetterAuth(transport, path, body, code, signal) {
1052
1212
  ];
1053
1213
  }
1054
1214
  }
1215
+ function parseBetterAuthError(text) {
1216
+ if (!text.trim()) {
1217
+ return {};
1218
+ }
1219
+ try {
1220
+ const body = JSON.parse(text);
1221
+ if (!body || typeof body !== "object") {
1222
+ return {};
1223
+ }
1224
+ const record = body;
1225
+ const nested = record.error && typeof record.error === "object" ? record.error : record;
1226
+ const code = typeof nested.code === "string" ? nested.code : void 0;
1227
+ const message = typeof nested.message === "string" ? nested.message : void 0;
1228
+ const details = nested.details && typeof nested.details === "object" ? nested.details : void 0;
1229
+ const correlationId = typeof nested.correlationId === "string" ? nested.correlationId : void 0;
1230
+ const retryable = typeof nested.retryable === "boolean" ? nested.retryable : void 0;
1231
+ return {
1232
+ code: isCapxulErrorCode2(code) ? code : void 0,
1233
+ message,
1234
+ details,
1235
+ correlationId,
1236
+ retryable
1237
+ };
1238
+ } catch {
1239
+ return {};
1240
+ }
1241
+ }
1242
+ function isCapxulErrorCode2(code) {
1243
+ 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";
1244
+ }
1055
1245
  async function exchangeConvexToken(transport, config, token, signal) {
1056
1246
  const path = config.auth?.convexTokenUrl ?? "/convex/token";
1057
1247
  try {