@resira/sdk 0.2.4 → 0.2.7

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
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable SDK and public API tracking updates are documented here.
4
4
 
5
+ ## 0.2.6
6
+
7
+ - Updated SDK routing to ignore ambient host-app env API overrides by default.
8
+ - Kept explicit `baseUrl` and `baseUrls` support for integrator-controlled routing.
9
+ - Added DOM libs to the SDK package tsconfig so standalone typechecking passes cleanly.
10
+
11
+ ## 0.2.5
12
+
13
+ - Added weighted-origin session stickiness so a browser session stays pinned to the same API origin instead of re-randomizing on every request.
14
+ - Added lightweight client-side routing telemetry helpers for monitoring origin assignments during rollout.
15
+
5
16
  ## 0.2.3
6
17
 
7
18
  - Current workspace release for the public reservation API package.
package/README.md CHANGED
@@ -1,18 +1,18 @@
1
- # @resira/sdk v0.2.0
1
+ # @resira/sdk v0.2.6
2
2
 
3
3
  TypeScript SDK for the Resira public reservation API.
4
4
 
5
- Version tracking lives in [CHANGELOG.md](./CHANGELOG.md).
5
+ Version history lives in [CHANGELOG.md](./CHANGELOG.md).
6
6
 
7
7
  ## Installation
8
8
 
9
9
  ```bash
10
- npm install @resira/sdk@0.2.0
10
+ npm install @resira/sdk@0.2.6
11
11
  ```
12
12
 
13
13
  ## Quick start
14
14
 
15
- ```typescript
15
+ ```ts
16
16
  import { Resira } from "@resira/sdk";
17
17
 
18
18
  const resira = new Resira({
@@ -20,57 +20,109 @@ const resira = new Resira({
20
20
  });
21
21
  ```
22
22
 
23
- ## API reference
23
+ ## Client config
24
24
 
25
- ### `new Resira(config)`
25
+ ```ts
26
+ const resira = new Resira({
27
+ apiKey: "resira_live_your_api_key_here",
28
+ maxRetries: 3,
29
+ retryBaseDelay: 500,
30
+ });
31
+ ```
26
32
 
27
- | Option | Type | Default | Description |
28
- | ---------------- | ---------- | --------------------------- | ------------------------------------------------- |
29
- | `apiKey` | `string` | **(required)** | API key (`resira_live_…`) |
30
- | `baseUrl` | `string` | `https://api.resira.io` | Base URL of the API |
31
- | `maxRetries` | `number` | `3` | Max retry attempts for 429 / 5xx |
32
- | `retryBaseDelay` | `number` | `500` | Base delay (ms) for exponential backoff |
33
- | `fetch` | `function` | `globalThis.fetch` | Custom fetch implementation |
33
+ | Option | Type | Default | Description |
34
+ | --- | --- | --- | --- |
35
+ | `apiKey` | `string` | required | Public Resira API key |
36
+ | `baseUrl` | `string` | unset | Pin all SDK requests to one origin |
37
+ | `baseUrls` | `Array<string \| { url: string; weight?: number }>` | unset | Explicit client-side routing override |
38
+ | `maxRetries` | `number` | `3` | Retry attempts for 429 and 5xx responses |
39
+ | `retryBaseDelay` | `number` | `500` | Base exponential backoff delay in ms |
40
+ | `fetch` | `typeof fetch` | `globalThis.fetch` | Custom fetch implementation |
34
41
 
35
- ---
42
+ ## Routing behavior
36
43
 
37
- ### `resira.getAvailability(resourceId, params?)`
44
+ - The SDK keeps a browser session sticky to the selected origin.
45
+ - Ambient host-app env vars such as `NEXT_PUBLIC_API_URL` are ignored.
46
+ - Only explicit SDK config via `baseUrl` or `baseUrls` overrides the default routing behavior.
47
+ - In development, the SDK defaults to `http://localhost:3001`.
48
+
49
+ ## Core methods
50
+
51
+ ### `resira.getConfig()`
38
52
 
39
- Query availability for a resource.
53
+ Fetch non-sensitive public property config such as Stripe publishable key, deposit percentage, currency, and branding.
54
+
55
+ ```ts
56
+ const config = await resira.getConfig();
57
+ console.log(config.stripePublishableKey);
58
+ ```
59
+
60
+ ### `resira.validatePromoCode(code)`
61
+
62
+ ```ts
63
+ const promo = await resira.validatePromoCode("SUMMER20");
64
+ if (promo.valid) {
65
+ console.log(promo.discountType, promo.discountValue);
66
+ }
67
+ ```
40
68
 
41
- **Property (date-based):**
42
- ```typescript
43
- const avail = await resira.getAvailability("prop-1", {
69
+ ### `resira.getAvailability(resourceId, params?)`
70
+
71
+ ```ts
72
+ const availability = await resira.getAvailability("prop-1", {
44
73
  startDate: "2026-07-01",
45
74
  endDate: "2026-07-07",
46
75
  });
47
-
48
- console.log(avail.dates?.available); // true
49
- console.log(avail.dates?.totalPrice); // 85000 (cents)
50
- console.log(avail.dates?.blockedDates); // ["2026-07-15", …]
51
76
  ```
52
77
 
53
- **Restaurant (time-slot):**
54
- ```typescript
55
- const avail = await resira.getAvailability("table-5", {
78
+ ```ts
79
+ const slots = await resira.getAvailability("table-5", {
56
80
  date: "2026-07-01",
57
81
  partySize: 4,
58
82
  });
83
+ ```
59
84
 
60
- for (const slot of avail.timeSlots ?? []) {
61
- console.log(`${slot.start} → ${slot.end}: ${slot.remaining} seats`);
62
- }
85
+ ### `resira.listResources()`
86
+
87
+ ```ts
88
+ const { resources } = await resira.listResources();
89
+ ```
90
+
91
+ ### `resira.listProducts()`
92
+
93
+ ```ts
94
+ const { products } = await resira.listProducts();
95
+ ```
96
+
97
+ ### `resira.createPaymentIntent(payload, options?)`
98
+
99
+ ```ts
100
+ const paymentIntent = await resira.createPaymentIntent({
101
+ productId: "prod-123",
102
+ resourceId: "res-456",
103
+ partySize: 2,
104
+ startDate: "2026-07-01",
105
+ startTime: "2026-07-01T10:00:00Z",
106
+ endTime: "2026-07-01T11:00:00Z",
107
+ guestName: "Jane Doe",
108
+ guestEmail: "jane@example.com",
109
+ });
63
110
  ```
64
111
 
65
- ---
112
+ ### `resira.confirmPayment(payload)`
113
+
114
+ ```ts
115
+ const confirmation = await resira.confirmPayment({
116
+ paymentIntentId: "pi_xxx",
117
+ reservationId: "res_123",
118
+ });
119
+ ```
66
120
 
67
121
  ### `resira.createReservation(payload, options?)`
68
122
 
69
- Create a reservation. The `resourceId` is included in the request body.
70
- An `Idempotency-Key` header is automatically generated to prevent
71
- duplicate bookings on retries.
123
+ Creates a reservation via the public booking flow. An idempotency key is automatically generated unless you pass one explicitly.
72
124
 
73
- ```typescript
125
+ ```ts
74
126
  const { reservation } = await resira.createReservation({
75
127
  resourceId: "prop-1",
76
128
  guestName: "Jane Doe",
@@ -79,107 +131,59 @@ const { reservation } = await resira.createReservation({
79
131
  endDate: "2026-07-07",
80
132
  partySize: 3,
81
133
  });
82
-
83
- console.log(reservation.id); // "uuid-…"
84
- console.log(reservation.status); // "pending"
85
- console.log(reservation.totalPrice);// 85000
86
- ```
87
-
88
- **Custom idempotency key:**
89
- ```typescript
90
- const { reservation } = await resira.createReservation(
91
- payload,
92
- { idempotencyKey: "form-submit-abc123" },
93
- );
94
134
  ```
95
135
 
96
- ---
97
-
98
136
  ### `resira.listReservations(resourceId, params?)`
99
137
 
100
- List reservations (paginated).
101
-
102
- ```typescript
138
+ ```ts
103
139
  const page = await resira.listReservations("prop-1", {
104
140
  status: "confirmed",
105
141
  page: 1,
106
142
  limit: 50,
107
143
  });
108
-
109
- console.log(page.data); // Reservation[]
110
- console.log(page.total); // 142
111
- console.log(page.totalPages); // 3
112
144
  ```
113
145
 
114
- ---
115
-
116
146
  ### `resira.getReservation(resourceId, reservationId)`
117
147
 
118
- Get a single reservation by ID.
119
-
120
- ```typescript
121
- const { reservation } = await resira.getReservation("prop-1", "res-uuid");
148
+ ```ts
149
+ const result = await resira.getReservation("prop-1", "res-uuid");
122
150
  ```
123
151
 
124
- ---
125
-
126
152
  ## Error handling
127
153
 
128
- All errors extend `ResiraError`. Use `instanceof` to discriminate:
154
+ All SDK errors extend `ResiraError`.
129
155
 
130
- ```typescript
156
+ ```ts
131
157
  import {
132
- Resira,
133
158
  ResiraApiError,
134
- ResiraRateLimitError,
135
159
  ResiraNetworkError,
160
+ ResiraRateLimitError,
136
161
  } from "@resira/sdk";
137
162
 
138
163
  try {
139
164
  await resira.createReservation(payload);
140
165
  } catch (error) {
141
166
  if (error instanceof ResiraRateLimitError) {
142
- // 429 — SDK already retried `maxRetries` times
143
- console.log(`Rate limited. Retry after ${error.retryAfter}s`);
167
+ console.log(error.retryAfter);
144
168
  } else if (error instanceof ResiraApiError) {
145
- // 4xx / 5xx
146
- console.log(`${error.status}: ${error.message}`);
147
- console.log(error.retryable); // true for 5xx
169
+ console.log(error.status, error.message);
148
170
  } else if (error instanceof ResiraNetworkError) {
149
- // DNS / TLS / offline
150
- console.log("Network error:", error.message);
171
+ console.log(error.message);
151
172
  }
152
173
  }
153
174
  ```
154
175
 
155
- | Error class | When | Retried automatically? |
156
- | ---------------------- | ----------------------------- | ---------------------- |
157
- | `ResiraRateLimitError` | 429 Too Many Requests | (respects Retry-After) |
158
- | `ResiraApiError` | Any non-2xx | for 5xx only |
159
- | `ResiraNetworkError` | fetch failed (DNS, offline…) | |
160
-
161
- ---
162
-
163
- ## Retry behaviour
164
-
165
- The SDK retries failed requests with **exponential backoff + jitter**:
166
-
167
- - **Attempt 1**: 0 – 500 ms
168
- - **Attempt 2**: 0 – 1 000 ms
169
- - **Attempt 3**: 0 – 2 000 ms
170
-
171
- For 429 responses, the `Retry-After` header value is used as a minimum delay.
172
-
173
- Idempotency keys ensure that retried POST requests don't create
174
- duplicate reservations.
175
-
176
- ---
176
+ | Error class | Meaning |
177
+ | --- | --- |
178
+ | `ResiraRateLimitError` | 429 response after retry handling |
179
+ | `ResiraApiError` | Non-2xx API response |
180
+ | `ResiraNetworkError` | Network/DNS/TLS failure |
177
181
 
178
182
  ## Requirements
179
183
 
180
- - Node.js 18 (uses native `fetch`)
184
+ - Node.js >= 18
181
185
  - No runtime dependencies
182
186
 
183
- ## Version tracking
187
+ ## Release notes
184
188
 
185
- Use [CHANGELOG.md](./CHANGELOG.md) to track SDK releases against public API changes. This is the canonical place to record version-to-version API surface updates.
189
+ See [CHANGELOG.md](./CHANGELOG.md) for release-by-release details.
package/dist/index.cjs CHANGED
@@ -40,8 +40,188 @@ var ResiraNetworkError = class extends ResiraError {
40
40
  }
41
41
  };
42
42
 
43
+ // src/routing.ts
44
+ var DEFAULT_LOCAL_BASE_URLS = [
45
+ { url: "http://localhost:3001", weight: 100 }
46
+ ];
47
+ var DEFAULT_PRODUCTION_BASE_URLS = [
48
+ { url: "https://resira-api-sips-production.up.railway.app", weight: 50 },
49
+ { url: "https://api.resira.app", weight: 50 }
50
+ ];
51
+ var STICKY_SELECTION_KEY = "resira_sdk_api_origin_sticky_v1";
52
+ var ROUTING_STATS_KEY = "resira_sdk_api_routing_stats_v1";
53
+ var stickySelectionCache = /* @__PURE__ */ new Map();
54
+ function readEnv(name) {
55
+ if (typeof process === "undefined") return void 0;
56
+ return process.env?.[name];
57
+ }
58
+ function isBrowser() {
59
+ return typeof window !== "undefined";
60
+ }
61
+ function getStorage(type) {
62
+ if (!isBrowser()) return null;
63
+ try {
64
+ return type === "session" ? window.sessionStorage : window.localStorage;
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+ function getBaseUrlSignature(baseUrls) {
70
+ return baseUrls.map((entry) => `${entry.url}|${entry.weight}`).join(",");
71
+ }
72
+ function pickWeightedBaseUrl(baseUrls) {
73
+ const totalWeight = baseUrls.reduce((sum, entry) => sum + entry.weight, 0);
74
+ if (totalWeight <= 0) {
75
+ return DEFAULT_PRODUCTION_BASE_URLS[0];
76
+ }
77
+ let cursor = Math.random() * totalWeight;
78
+ for (const entry of baseUrls) {
79
+ cursor -= entry.weight;
80
+ if (cursor < 0) {
81
+ return entry;
82
+ }
83
+ }
84
+ return baseUrls[baseUrls.length - 1] ?? DEFAULT_PRODUCTION_BASE_URLS[0];
85
+ }
86
+ function ensureAnalyticsQueue() {
87
+ if (!isBrowser()) return null;
88
+ const analyticsWindow = window;
89
+ if (!analyticsWindow.va) {
90
+ analyticsWindow.va = (...params) => {
91
+ analyticsWindow.vaq = analyticsWindow.vaq || [];
92
+ analyticsWindow.vaq.push(params);
93
+ };
94
+ }
95
+ return analyticsWindow;
96
+ }
97
+ function readStickySelection(signature) {
98
+ const cached = stickySelectionCache.get(signature);
99
+ if (cached) return cached;
100
+ const storage = getStorage("session");
101
+ const raw = storage?.getItem(STICKY_SELECTION_KEY);
102
+ if (!raw) return null;
103
+ try {
104
+ const parsed = JSON.parse(raw);
105
+ if (!parsed?.signature || !parsed?.url || parsed.signature !== signature) {
106
+ return null;
107
+ }
108
+ stickySelectionCache.set(signature, parsed);
109
+ return parsed;
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+ function persistStickySelection(signature, url) {
115
+ const selection = {
116
+ signature,
117
+ url,
118
+ assignedAt: (/* @__PURE__ */ new Date()).toISOString()
119
+ };
120
+ stickySelectionCache.set(signature, selection);
121
+ getStorage("session")?.setItem(STICKY_SELECTION_KEY, JSON.stringify(selection));
122
+ }
123
+ function recordRoutingStats(signature, entry) {
124
+ const storage = getStorage("local");
125
+ if (!storage) return;
126
+ let stats = {};
127
+ try {
128
+ stats = JSON.parse(storage.getItem(ROUTING_STATS_KEY) ?? "{}");
129
+ } catch {
130
+ stats = {};
131
+ }
132
+ const current = stats[signature] ?? {
133
+ lastAssignedAt: "",
134
+ lastOrigin: "",
135
+ selections: 0,
136
+ origins: {}
137
+ };
138
+ current.lastAssignedAt = (/* @__PURE__ */ new Date()).toISOString();
139
+ current.lastOrigin = entry.url;
140
+ current.selections += 1;
141
+ current.origins[entry.url] = (current.origins[entry.url] ?? 0) + 1;
142
+ stats[signature] = current;
143
+ storage.setItem(ROUTING_STATS_KEY, JSON.stringify(stats));
144
+ }
145
+ function emitRoutingSelection(entry, signature, totalWeight) {
146
+ if (!isBrowser()) return;
147
+ const detail = {
148
+ source: "sdk",
149
+ strategy: "session-sticky",
150
+ originHost: new URL(entry.url).host,
151
+ originUrl: entry.url,
152
+ originWeight: entry.weight,
153
+ totalWeight,
154
+ signature
155
+ };
156
+ ensureAnalyticsQueue()?.va?.("event", {
157
+ name: "api_origin_selected",
158
+ data: detail
159
+ });
160
+ window.dispatchEvent(
161
+ new CustomEvent("resira:api-origin-selected", {
162
+ detail
163
+ })
164
+ );
165
+ const debugEnabled = /^(1|true)$/i.test(
166
+ readEnv("NEXT_PUBLIC_RESIRA_API_ROUTING_DEBUG") ?? readEnv("NEXT_PUBLIC_API_ROUTING_DEBUG") ?? ""
167
+ );
168
+ if (debugEnabled) {
169
+ console.info("[resira-sdk] sticky origin selected", detail);
170
+ }
171
+ }
172
+ function normalizeUrl(url) {
173
+ return url.trim().replace(/\/+$/, "");
174
+ }
175
+ function isPositiveNumber(value) {
176
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
177
+ }
178
+ function toResolvedBaseUrl(value) {
179
+ if (typeof value === "string") {
180
+ const url2 = normalizeUrl(value);
181
+ return url2 ? { url: url2, weight: 1 } : null;
182
+ }
183
+ const url = normalizeUrl(value.url);
184
+ if (!url) return null;
185
+ return {
186
+ url,
187
+ weight: isPositiveNumber(value.weight) ? value.weight : 1
188
+ };
189
+ }
190
+ function resolveBaseUrls(config) {
191
+ if (config.baseUrl) {
192
+ return [{ url: normalizeUrl(config.baseUrl), weight: 100 }];
193
+ }
194
+ if (config.baseUrls?.length) {
195
+ const explicit = config.baseUrls.flatMap((entry) => {
196
+ const resolved = toResolvedBaseUrl(entry);
197
+ return resolved ? [resolved] : [];
198
+ });
199
+ if (explicit.length > 0) return explicit;
200
+ }
201
+ if (readEnv("NODE_ENV") === "development") {
202
+ return DEFAULT_LOCAL_BASE_URLS;
203
+ }
204
+ return DEFAULT_PRODUCTION_BASE_URLS;
205
+ }
206
+ function pickBaseUrl(baseUrls) {
207
+ const signature = getBaseUrlSignature(baseUrls);
208
+ const stickySelection = readStickySelection(signature);
209
+ const stickyEntry = stickySelection ? baseUrls.find((entry) => entry.url === stickySelection.url) : null;
210
+ if (stickyEntry) {
211
+ return stickyEntry.url;
212
+ }
213
+ const selectedEntry = pickWeightedBaseUrl(baseUrls);
214
+ persistStickySelection(signature, selectedEntry.url);
215
+ recordRoutingStats(signature, selectedEntry);
216
+ emitRoutingSelection(
217
+ selectedEntry,
218
+ signature,
219
+ baseUrls.reduce((sum, entry) => sum + entry.weight, 0)
220
+ );
221
+ return selectedEntry.url;
222
+ }
223
+
43
224
  // src/client.ts
44
- var DEFAULT_BASE_URL = "https://api-res-production.up.railway.app";
45
225
  var DEFAULT_MAX_RETRIES = 3;
46
226
  var DEFAULT_RETRY_BASE_DELAY = 500;
47
227
  var API_PREFIX = "/v1/public";
@@ -96,7 +276,7 @@ var Resira = class {
96
276
  throw new Error("Resira: apiKey is required");
97
277
  }
98
278
  this.apiKey = config.apiKey;
99
- this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
279
+ this.baseUrls = resolveBaseUrls(config);
100
280
  this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
101
281
  this.retryBaseDelay = config.retryBaseDelay ?? DEFAULT_RETRY_BASE_DELAY;
102
282
  this._fetch = config.fetch ?? globalThis.fetch.bind(globalThis);
@@ -249,7 +429,7 @@ var Resira = class {
249
429
  throw new Error("resourceId is required for createReservation");
250
430
  }
251
431
  const idempotencyKey = options?.idempotencyKey ?? uuid();
252
- const url = `${this.baseUrl}/v2/api/reservations`;
432
+ const url = `${this.getBaseUrl()}/v2/api/reservations`;
253
433
  const headers = {
254
434
  Authorization: `Bearer ${this.apiKey}`,
255
435
  Accept: "application/json",
@@ -467,7 +647,7 @@ var Resira = class {
467
647
  * - Parses error bodies and throws typed error classes.
468
648
  */
469
649
  async request(method, path, body, extraHeaders) {
470
- const url = `${this.baseUrl}${API_PREFIX}${path}`;
650
+ const url = `${this.getBaseUrl()}${API_PREFIX}${path}`;
471
651
  const headers = {
472
652
  Authorization: `Bearer ${this.apiKey}`,
473
653
  Accept: "application/json",
@@ -545,6 +725,9 @@ var Resira = class {
545
725
  }
546
726
  return jittered;
547
727
  }
728
+ getBaseUrl() {
729
+ return pickBaseUrl(this.baseUrls);
730
+ }
548
731
  };
549
732
 
550
733
  exports.Resira = Resira;