@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.
package/README.md CHANGED
@@ -107,6 +107,40 @@ try {
107
107
  }
108
108
  ```
109
109
 
110
+ ## Is the contract still the right one?
111
+
112
+ Pinning a version fixes *which* contract you compiled against. It does not tell you whether the
113
+ node-shop you actually call still speaks it — and nothing else will: your types are green, your
114
+ build is green, and the first thing that fails is a real request in production.
115
+
116
+ `diagnoseShop()` closes that gap by asking the deployment itself:
117
+
118
+ ```ts
119
+ import { diagnoseShop } from "@cronvello/shop-sdk";
120
+
121
+ const report = await diagnoseShop({ client: shop }); // or { baseUrl }
122
+
123
+ report.ok; // true only when the match was VERIFIED
124
+ report.contract.status; // "match" | "drift" | "unknown"
125
+ report.contract.sdk; // hash this package was built against
126
+ report.contract.live; // hash the deployment reports
127
+ report.sdkVersion;
128
+ report.problems; // plain sentences, empty when ok
129
+ ```
130
+
131
+ `unknown` (offline, or a node-shop too old to serve `GET /health/contract`) is deliberately not
132
+ `ok`. A green light that means "could not check" is the exact failure this exists to remove.
133
+
134
+ It never throws for an operational reason — an unreachable shop is a result, not an exception — so
135
+ it is safe to put behind your own health endpoint and let a monitor pull it:
136
+
137
+ ```ts
138
+ export async function GET() {
139
+ const report = await diagnoseShop({ client: shop });
140
+ return Response.json(report, { status: report.ok ? 200 : 503 });
141
+ }
142
+ ```
143
+
110
144
  ## Updating the contract
111
145
 
112
146
  node-shop remains the producer. From this repository, after regenerating node-shop's contract:
package/dist/browser.cjs CHANGED
@@ -5272,7 +5272,7 @@ function createShopClient(options = {}) {
5272
5272
  const defaultTimeoutMs = options.timeoutMs ?? 3e4;
5273
5273
  const defaultRetries = options.retries ?? 2;
5274
5274
  const retryDelayMs = options.retryDelayMs ?? 250;
5275
- async function resolveBaseUrl() {
5275
+ async function resolveBaseUrl2() {
5276
5276
  const value = typeof baseUrl === "function" ? await baseUrl() : baseUrl;
5277
5277
  return normalizeBaseUrl(value);
5278
5278
  }
@@ -5294,7 +5294,7 @@ function createShopClient(options = {}) {
5294
5294
  const body = hasBody ? JSON.stringify(values.body) : void 0;
5295
5295
  const retries = Math.max(0, requestOptions.retries ?? defaultRetries);
5296
5296
  const canRetry = SAFE_METHODS.has(route.method) || requestOptions.retryUnsafe === true;
5297
- const resolvedBaseUrl = await resolveBaseUrl();
5297
+ const resolvedBaseUrl = await resolveBaseUrl2();
5298
5298
  for (let attempt = 0; ; attempt += 1) {
5299
5299
  const timeout = createRequestSignal(requestOptions.signal, requestOptions.timeoutMs ?? defaultTimeoutMs);
5300
5300
  try {
@@ -5371,6 +5371,102 @@ function createShopClient(options = {}) {
5371
5371
  return { baseUrl, request, requestText, requestRaw };
5372
5372
  }
5373
5373
 
5374
+ // src/contract-hash.ts
5375
+ var CONTRACT_SHA256 = "335eed391a2786a4f7f010b056d2a6a5b49d9e1244960a15516a5d77b93693b5";
5376
+
5377
+ // src/client/diagnose.ts
5378
+ var SDK_VERSION = "0.2.0" ;
5379
+ async function resolveBaseUrl(options) {
5380
+ const provider = options.client?.baseUrl ?? options.baseUrl ?? SHOP_API_URL;
5381
+ const value = typeof provider === "function" ? await provider() : provider;
5382
+ return normalizeBaseUrl(value);
5383
+ }
5384
+ async function diagnoseShop(options = {}) {
5385
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
5386
+ const baseUrl = await resolveBaseUrl(options);
5387
+ const doFetch = options.fetch ?? globalThis.fetch;
5388
+ const timeoutMs = options.timeoutMs ?? 5e3;
5389
+ const base = {
5390
+ ok: false,
5391
+ baseUrl,
5392
+ sdkVersion: SDK_VERSION,
5393
+ reachable: false,
5394
+ contract: { status: "unknown", sdk: CONTRACT_SHA256, live: null, files: null },
5395
+ problems: [],
5396
+ checkedAt
5397
+ };
5398
+ if (typeof doFetch !== "function") {
5399
+ return { ...base, problems: ["No fetch implementation available; pass `fetch` explicitly."] };
5400
+ }
5401
+ const controller = new AbortController();
5402
+ const onAbort = () => controller.abort(options.signal?.reason);
5403
+ options.signal?.addEventListener("abort", onAbort, { once: true });
5404
+ if (options.signal?.aborted) controller.abort(options.signal.reason);
5405
+ const timer = setTimeout(() => controller.abort(new Error(`Contract probe timed out after ${timeoutMs}ms`)), timeoutMs);
5406
+ let response;
5407
+ try {
5408
+ response = await doFetch(`${baseUrl}/health/contract`, {
5409
+ method: "GET",
5410
+ headers: { accept: "application/json" },
5411
+ signal: controller.signal
5412
+ });
5413
+ } catch (error) {
5414
+ const detail = error instanceof Error ? error.message : String(error);
5415
+ return { ...base, problems: [`node-shop at ${baseUrl} could not be reached: ${detail}`] };
5416
+ } finally {
5417
+ clearTimeout(timer);
5418
+ options.signal?.removeEventListener("abort", onAbort);
5419
+ }
5420
+ if (response.status === 404) {
5421
+ return {
5422
+ ...base,
5423
+ reachable: true,
5424
+ problems: [
5425
+ `node-shop at ${baseUrl} does not serve GET /health/contract, so the contract cannot be verified. Deploy a node-shop that includes it.`
5426
+ ]
5427
+ };
5428
+ }
5429
+ if (!response.ok) {
5430
+ return {
5431
+ ...base,
5432
+ reachable: true,
5433
+ problems: [`node-shop at ${baseUrl} answered ${response.status} for GET /health/contract; contract not verified.`]
5434
+ };
5435
+ }
5436
+ let body;
5437
+ try {
5438
+ body = await response.json();
5439
+ } catch {
5440
+ return { ...base, reachable: true, problems: [`node-shop at ${baseUrl} returned a non-JSON contract fingerprint.`] };
5441
+ }
5442
+ const live = typeof body.contractSha256 === "string" && body.contractSha256 ? body.contractSha256 : null;
5443
+ const files = typeof body.files === "number" ? body.files : null;
5444
+ if (!live) {
5445
+ return {
5446
+ ...base,
5447
+ reachable: true,
5448
+ contract: { ...base.contract, files },
5449
+ problems: [`node-shop at ${baseUrl} could not compute its own contract fingerprint; contract not verified.`]
5450
+ };
5451
+ }
5452
+ if (live !== CONTRACT_SHA256) {
5453
+ return {
5454
+ ...base,
5455
+ reachable: true,
5456
+ contract: { status: "drift", sdk: CONTRACT_SHA256, live, files },
5457
+ problems: [
5458
+ `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.`
5459
+ ]
5460
+ };
5461
+ }
5462
+ return {
5463
+ ...base,
5464
+ ok: true,
5465
+ reachable: true,
5466
+ contract: { status: "match", sdk: CONTRACT_SHA256, live, files }
5467
+ };
5468
+ }
5469
+
5374
5470
  // src/browser.ts
5375
5471
  function createShopBrowserClient(options = {}) {
5376
5472
  const configured = options.headers;
@@ -5387,9 +5483,11 @@ function createShopBrowserClient(options = {}) {
5387
5483
  });
5388
5484
  }
5389
5485
 
5486
+ exports.CONTRACT_SHA256 = CONTRACT_SHA256;
5390
5487
  exports.SHOP_API_URL = SHOP_API_URL;
5391
5488
  exports.ShopApiError = ShopApiError;
5392
5489
  exports.ShopConfigurationError = ShopConfigurationError;
5393
5490
  exports.createShopBrowserClient = createShopBrowserClient;
5491
+ exports.diagnoseShop = diagnoseShop;
5394
5492
  //# sourceMappingURL=browser.cjs.map
5395
5493
  //# sourceMappingURL=browser.cjs.map