@cronvello/shop-sdk 0.1.0 → 0.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.
@@ -53,6 +53,59 @@ interface ShopClient {
53
53
  /** Stable production endpoint used when no override or resolver is supplied. */
54
54
  declare const SHOP_API_URL = "https://node-shop-production-86b7.up.railway.app";
55
55
 
56
+ /**
57
+ * `match` — the deployment reports exactly the contract compiled into this SDK.
58
+ * `drift` — it reports a different one. The types here are a lie about that
59
+ * deployment; requests can fail on shapes the compiler called fine.
60
+ * `unknown` — nobody could be asked (offline, 503, or a node-shop old enough not
61
+ * to serve the endpoint yet). Explicitly NOT the same as `match`.
62
+ */
63
+ type ShopContractStatus = "match" | "drift" | "unknown";
64
+ type ShopDiagnosis = {
65
+ /**
66
+ * True only when the contract was actually VERIFIED to match. `unknown` is
67
+ * false on purpose: a green light that means "could not check" is exactly the
68
+ * failure this whole mechanism exists to remove.
69
+ */
70
+ ok: boolean;
71
+ baseUrl: string | null;
72
+ sdkVersion: string;
73
+ reachable: boolean;
74
+ contract: {
75
+ status: ShopContractStatus;
76
+ sdk: string;
77
+ live: string | null;
78
+ files: number | null;
79
+ };
80
+ /** Plain sentences, safe to log or show. Empty when `ok` is true. */
81
+ problems: string[];
82
+ checkedAt: string;
83
+ };
84
+ type ShopDiagnoseOptions = {
85
+ /** Preferred: reuses the client's own base URL, including a dynamic resolver. */
86
+ client?: Pick<ShopClient, "baseUrl">;
87
+ baseUrl?: BaseUrlProvider;
88
+ fetch?: typeof globalThis.fetch;
89
+ timeoutMs?: number;
90
+ signal?: AbortSignal;
91
+ };
92
+ /**
93
+ * Ask a running node-shop which contract it speaks and compare it to the one
94
+ * this SDK was built against.
95
+ *
96
+ * Read-only, unauthenticated, and cheap — meant to sit behind a consumer's own
97
+ * health endpoint so the answer is a fact anyone can pull, rather than something
98
+ * a person has to remember to check after a release.
99
+ *
100
+ * Never throws for an operational reason: an unreachable shop is a RESULT
101
+ * (`reachable: false`), not an exception, so a health route cannot accidentally
102
+ * turn a diagnosis into a 500. Only a genuinely unusable base URL throws
103
+ * (`ShopConfigurationError`), because that is a wiring mistake, not a state.
104
+ */
105
+ declare function diagnoseShop(options?: ShopDiagnoseOptions): Promise<ShopDiagnosis>;
106
+
107
+ declare const CONTRACT_SHA256 = "335eed391a2786a4f7f010b056d2a6a5b49d9e1244960a15516a5d77b93693b5";
108
+
56
109
  type ShopErrorDetails = {
57
110
  status: number;
58
111
  method: string;
@@ -75,4 +128,4 @@ declare class ShopConfigurationError extends Error {
75
128
  constructor(message: string);
76
129
  }
77
130
 
78
- export { type BaseUrlProvider as B, type HeaderProvider as H, type MaybePromise as M, type ShopClientOptions as S, type ShopClient as a, SHOP_API_URL as b, ShopApiError as c, type ShopAuthType as d, ShopConfigurationError as e, type ShopRequestContext as f, type ShopRequestOptions as g, type ShopRoute as h, type ShopRouteAuth as i, type ShopRouteBody as j, type ShopRouteInput as k, type ShopRouteKey as l, type ShopRouteMethod as m, type ShopRouteParams as n, type ShopRouteQuery as o, type ShopRouteResponse as p, type ShopRouteResponseData as q };
131
+ export { type BaseUrlProvider as B, CONTRACT_SHA256 as C, type HeaderProvider as H, type MaybePromise as M, type ShopClientOptions as S, type ShopClient as a, SHOP_API_URL as b, ShopApiError as c, type ShopAuthType as d, ShopConfigurationError as e, type ShopContractStatus as f, type ShopDiagnoseOptions as g, type ShopDiagnosis as h, type ShopRequestContext as i, type ShopRequestOptions as j, type ShopRoute as k, type ShopRouteAuth as l, type ShopRouteBody as m, type ShopRouteInput as n, type ShopRouteKey as o, type ShopRouteMethod as p, type ShopRouteParams as q, type ShopRouteQuery as r, type ShopRouteResponse as s, type ShopRouteResponseData as t, diagnoseShop as u };
@@ -53,6 +53,59 @@ interface ShopClient {
53
53
  /** Stable production endpoint used when no override or resolver is supplied. */
54
54
  declare const SHOP_API_URL = "https://node-shop-production-86b7.up.railway.app";
55
55
 
56
+ /**
57
+ * `match` — the deployment reports exactly the contract compiled into this SDK.
58
+ * `drift` — it reports a different one. The types here are a lie about that
59
+ * deployment; requests can fail on shapes the compiler called fine.
60
+ * `unknown` — nobody could be asked (offline, 503, or a node-shop old enough not
61
+ * to serve the endpoint yet). Explicitly NOT the same as `match`.
62
+ */
63
+ type ShopContractStatus = "match" | "drift" | "unknown";
64
+ type ShopDiagnosis = {
65
+ /**
66
+ * True only when the contract was actually VERIFIED to match. `unknown` is
67
+ * false on purpose: a green light that means "could not check" is exactly the
68
+ * failure this whole mechanism exists to remove.
69
+ */
70
+ ok: boolean;
71
+ baseUrl: string | null;
72
+ sdkVersion: string;
73
+ reachable: boolean;
74
+ contract: {
75
+ status: ShopContractStatus;
76
+ sdk: string;
77
+ live: string | null;
78
+ files: number | null;
79
+ };
80
+ /** Plain sentences, safe to log or show. Empty when `ok` is true. */
81
+ problems: string[];
82
+ checkedAt: string;
83
+ };
84
+ type ShopDiagnoseOptions = {
85
+ /** Preferred: reuses the client's own base URL, including a dynamic resolver. */
86
+ client?: Pick<ShopClient, "baseUrl">;
87
+ baseUrl?: BaseUrlProvider;
88
+ fetch?: typeof globalThis.fetch;
89
+ timeoutMs?: number;
90
+ signal?: AbortSignal;
91
+ };
92
+ /**
93
+ * Ask a running node-shop which contract it speaks and compare it to the one
94
+ * this SDK was built against.
95
+ *
96
+ * Read-only, unauthenticated, and cheap — meant to sit behind a consumer's own
97
+ * health endpoint so the answer is a fact anyone can pull, rather than something
98
+ * a person has to remember to check after a release.
99
+ *
100
+ * Never throws for an operational reason: an unreachable shop is a RESULT
101
+ * (`reachable: false`), not an exception, so a health route cannot accidentally
102
+ * turn a diagnosis into a 500. Only a genuinely unusable base URL throws
103
+ * (`ShopConfigurationError`), because that is a wiring mistake, not a state.
104
+ */
105
+ declare function diagnoseShop(options?: ShopDiagnoseOptions): Promise<ShopDiagnosis>;
106
+
107
+ declare const CONTRACT_SHA256 = "335eed391a2786a4f7f010b056d2a6a5b49d9e1244960a15516a5d77b93693b5";
108
+
56
109
  type ShopErrorDetails = {
57
110
  status: number;
58
111
  method: string;
@@ -75,4 +128,4 @@ declare class ShopConfigurationError extends Error {
75
128
  constructor(message: string);
76
129
  }
77
130
 
78
- export { type BaseUrlProvider as B, type HeaderProvider as H, type MaybePromise as M, type ShopClientOptions as S, type ShopClient as a, SHOP_API_URL as b, ShopApiError as c, type ShopAuthType as d, ShopConfigurationError as e, type ShopRequestContext as f, type ShopRequestOptions as g, type ShopRoute as h, type ShopRouteAuth as i, type ShopRouteBody as j, type ShopRouteInput as k, type ShopRouteKey as l, type ShopRouteMethod as m, type ShopRouteParams as n, type ShopRouteQuery as o, type ShopRouteResponse as p, type ShopRouteResponseData as q };
131
+ export { type BaseUrlProvider as B, CONTRACT_SHA256 as C, type HeaderProvider as H, type MaybePromise as M, type ShopClientOptions as S, type ShopClient as a, SHOP_API_URL as b, ShopApiError as c, type ShopAuthType as d, ShopConfigurationError as e, type ShopContractStatus as f, type ShopDiagnoseOptions as g, type ShopDiagnosis as h, type ShopRequestContext as i, type ShopRequestOptions as j, type ShopRoute as k, type ShopRouteAuth as l, type ShopRouteBody as m, type ShopRouteInput as n, type ShopRouteKey as o, type ShopRouteMethod as p, type ShopRouteParams as q, type ShopRouteQuery as r, type ShopRouteResponse as s, type ShopRouteResponseData as t, diagnoseShop as u };
package/dist/index.cjs CHANGED
@@ -5621,7 +5621,7 @@ function createShopClient(options = {}) {
5621
5621
  const defaultTimeoutMs = options.timeoutMs ?? 3e4;
5622
5622
  const defaultRetries = options.retries ?? 2;
5623
5623
  const retryDelayMs = options.retryDelayMs ?? 250;
5624
- async function resolveBaseUrl() {
5624
+ async function resolveBaseUrl2() {
5625
5625
  const value = typeof baseUrl === "function" ? await baseUrl() : baseUrl;
5626
5626
  return normalizeBaseUrl(value);
5627
5627
  }
@@ -5643,7 +5643,7 @@ function createShopClient(options = {}) {
5643
5643
  const body = hasBody ? JSON.stringify(values.body) : void 0;
5644
5644
  const retries = Math.max(0, requestOptions.retries ?? defaultRetries);
5645
5645
  const canRetry = SAFE_METHODS.has(route.method) || requestOptions.retryUnsafe === true;
5646
- const resolvedBaseUrl = await resolveBaseUrl();
5646
+ const resolvedBaseUrl = await resolveBaseUrl2();
5647
5647
  for (let attempt = 0; ; attempt += 1) {
5648
5648
  const timeout = createRequestSignal(requestOptions.signal, requestOptions.timeoutMs ?? defaultTimeoutMs);
5649
5649
  try {
@@ -5720,6 +5720,103 @@ function createShopClient(options = {}) {
5720
5720
  return { baseUrl, request, requestText, requestRaw };
5721
5721
  }
5722
5722
 
5723
+ // src/contract-hash.ts
5724
+ var CONTRACT_SHA256 = "335eed391a2786a4f7f010b056d2a6a5b49d9e1244960a15516a5d77b93693b5";
5725
+
5726
+ // src/client/diagnose.ts
5727
+ var SDK_VERSION = "0.2.0" ;
5728
+ async function resolveBaseUrl(options) {
5729
+ const provider = options.client?.baseUrl ?? options.baseUrl ?? SHOP_API_URL;
5730
+ const value = typeof provider === "function" ? await provider() : provider;
5731
+ return normalizeBaseUrl(value);
5732
+ }
5733
+ async function diagnoseShop(options = {}) {
5734
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
5735
+ const baseUrl = await resolveBaseUrl(options);
5736
+ const doFetch = options.fetch ?? globalThis.fetch;
5737
+ const timeoutMs = options.timeoutMs ?? 5e3;
5738
+ const base = {
5739
+ ok: false,
5740
+ baseUrl,
5741
+ sdkVersion: SDK_VERSION,
5742
+ reachable: false,
5743
+ contract: { status: "unknown", sdk: CONTRACT_SHA256, live: null, files: null },
5744
+ problems: [],
5745
+ checkedAt
5746
+ };
5747
+ if (typeof doFetch !== "function") {
5748
+ return { ...base, problems: ["No fetch implementation available; pass `fetch` explicitly."] };
5749
+ }
5750
+ const controller = new AbortController();
5751
+ const onAbort = () => controller.abort(options.signal?.reason);
5752
+ options.signal?.addEventListener("abort", onAbort, { once: true });
5753
+ if (options.signal?.aborted) controller.abort(options.signal.reason);
5754
+ const timer = setTimeout(() => controller.abort(new Error(`Contract probe timed out after ${timeoutMs}ms`)), timeoutMs);
5755
+ let response;
5756
+ try {
5757
+ response = await doFetch(`${baseUrl}/health/contract`, {
5758
+ method: "GET",
5759
+ headers: { accept: "application/json" },
5760
+ signal: controller.signal
5761
+ });
5762
+ } catch (error) {
5763
+ const detail = error instanceof Error ? error.message : String(error);
5764
+ return { ...base, problems: [`node-shop at ${baseUrl} could not be reached: ${detail}`] };
5765
+ } finally {
5766
+ clearTimeout(timer);
5767
+ options.signal?.removeEventListener("abort", onAbort);
5768
+ }
5769
+ if (response.status === 404) {
5770
+ return {
5771
+ ...base,
5772
+ reachable: true,
5773
+ problems: [
5774
+ `node-shop at ${baseUrl} does not serve GET /health/contract, so the contract cannot be verified. Deploy a node-shop that includes it.`
5775
+ ]
5776
+ };
5777
+ }
5778
+ if (!response.ok) {
5779
+ return {
5780
+ ...base,
5781
+ reachable: true,
5782
+ problems: [`node-shop at ${baseUrl} answered ${response.status} for GET /health/contract; contract not verified.`]
5783
+ };
5784
+ }
5785
+ let body;
5786
+ try {
5787
+ body = await response.json();
5788
+ } catch {
5789
+ return { ...base, reachable: true, problems: [`node-shop at ${baseUrl} returned a non-JSON contract fingerprint.`] };
5790
+ }
5791
+ const live = typeof body.contractSha256 === "string" && body.contractSha256 ? body.contractSha256 : null;
5792
+ const files = typeof body.files === "number" ? body.files : null;
5793
+ if (!live) {
5794
+ return {
5795
+ ...base,
5796
+ reachable: true,
5797
+ contract: { ...base.contract, files },
5798
+ problems: [`node-shop at ${baseUrl} could not compute its own contract fingerprint; contract not verified.`]
5799
+ };
5800
+ }
5801
+ if (live !== CONTRACT_SHA256) {
5802
+ return {
5803
+ ...base,
5804
+ reachable: true,
5805
+ contract: { status: "drift", sdk: CONTRACT_SHA256, live, files },
5806
+ problems: [
5807
+ `Contract drift: node-shop at ${baseUrl} speaks ${live.slice(0, 12)}, @cronvello/shop-sdk ${SDK_VERSION} was built against ${CONTRACT_SHA256.slice(0, 12)}. Pin a newer SDK version, or deploy the node-shop the SDK was built from.`
5808
+ ]
5809
+ };
5810
+ }
5811
+ return {
5812
+ ...base,
5813
+ ok: true,
5814
+ reachable: true,
5815
+ contract: { status: "match", sdk: CONTRACT_SHA256, live, files }
5816
+ };
5817
+ }
5818
+
5819
+ exports.CONTRACT_SHA256 = CONTRACT_SHA256;
5723
5820
  exports.SHOP_API_URL = SHOP_API_URL;
5724
5821
  exports.ShopApiError = ShopApiError;
5725
5822
  exports.ShopConfigurationError = ShopConfigurationError;
@@ -5774,5 +5871,6 @@ exports.apiRoutes_users = apiRoutes_users;
5774
5871
  exports.apiRoutes_variant_entitlements = apiRoutes_variant_entitlements;
5775
5872
  exports.apiRoutes_webhooks = apiRoutes_webhooks;
5776
5873
  exports.createShopClient = createShopClient;
5874
+ exports.diagnoseShop = diagnoseShop;
5777
5875
  //# sourceMappingURL=index.cjs.map
5778
5876
  //# sourceMappingURL=index.cjs.map