@resira/sdk 0.2.4 → 0.2.6

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,115 @@ 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
+ // Optional explicit overrides:
31
+ // baseUrl: "https://your-api.example.com",
32
+ // baseUrls: [
33
+ // { url: "https://origin-a.example.com", weight: 1 },
34
+ // { url: "https://origin-b.example.com", weight: 1 },
35
+ // ],
36
+ });
37
+ ```
26
38
 
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 |
39
+ | Option | Type | Default | Description |
40
+ | --- | --- | --- | --- |
41
+ | `apiKey` | `string` | required | Public Resira API key |
42
+ | `baseUrl` | `string` | unset | Pin all SDK requests to one origin |
43
+ | `baseUrls` | `Array<string \| { url: string; weight?: number }>` | unset | Explicit client-side routing override |
44
+ | `maxRetries` | `number` | `3` | Retry attempts for 429 and 5xx responses |
45
+ | `retryBaseDelay` | `number` | `500` | Base exponential backoff delay in ms |
46
+ | `fetch` | `typeof fetch` | `globalThis.fetch` | Custom fetch implementation |
34
47
 
35
- ---
48
+ ## Routing behavior
36
49
 
37
- ### `resira.getAvailability(resourceId, params?)`
50
+ - The SDK keeps a browser session sticky to the selected origin.
51
+ - Ambient host-app env vars such as `NEXT_PUBLIC_API_URL` are ignored.
52
+ - Only explicit SDK config via `baseUrl` or `baseUrls` overrides the built-in production defaults.
53
+ - In development, the SDK defaults to `http://localhost:3001`.
54
+
55
+ ## Core methods
56
+
57
+ ### `resira.getConfig()`
38
58
 
39
- Query availability for a resource.
59
+ Fetch non-sensitive public property config such as Stripe publishable key, deposit percentage, currency, and branding.
60
+
61
+ ```ts
62
+ const config = await resira.getConfig();
63
+ console.log(config.stripePublishableKey);
64
+ ```
65
+
66
+ ### `resira.validatePromoCode(code)`
67
+
68
+ ```ts
69
+ const promo = await resira.validatePromoCode("SUMMER20");
70
+ if (promo.valid) {
71
+ console.log(promo.discountType, promo.discountValue);
72
+ }
73
+ ```
40
74
 
41
- **Property (date-based):**
42
- ```typescript
43
- const avail = await resira.getAvailability("prop-1", {
75
+ ### `resira.getAvailability(resourceId, params?)`
76
+
77
+ ```ts
78
+ const availability = await resira.getAvailability("prop-1", {
44
79
  startDate: "2026-07-01",
45
80
  endDate: "2026-07-07",
46
81
  });
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
82
  ```
52
83
 
53
- **Restaurant (time-slot):**
54
- ```typescript
55
- const avail = await resira.getAvailability("table-5", {
84
+ ```ts
85
+ const slots = await resira.getAvailability("table-5", {
56
86
  date: "2026-07-01",
57
87
  partySize: 4,
58
88
  });
89
+ ```
59
90
 
60
- for (const slot of avail.timeSlots ?? []) {
61
- console.log(`${slot.start} → ${slot.end}: ${slot.remaining} seats`);
62
- }
91
+ ### `resira.listResources()`
92
+
93
+ ```ts
94
+ const { resources } = await resira.listResources();
95
+ ```
96
+
97
+ ### `resira.listProducts()`
98
+
99
+ ```ts
100
+ const { products } = await resira.listProducts();
101
+ ```
102
+
103
+ ### `resira.createPaymentIntent(payload, options?)`
104
+
105
+ ```ts
106
+ const paymentIntent = await resira.createPaymentIntent({
107
+ productId: "prod-123",
108
+ resourceId: "res-456",
109
+ partySize: 2,
110
+ startDate: "2026-07-01",
111
+ startTime: "2026-07-01T10:00:00Z",
112
+ endTime: "2026-07-01T11:00:00Z",
113
+ guestName: "Jane Doe",
114
+ guestEmail: "jane@example.com",
115
+ });
63
116
  ```
64
117
 
65
- ---
118
+ ### `resira.confirmPayment(payload)`
119
+
120
+ ```ts
121
+ const confirmation = await resira.confirmPayment({
122
+ paymentIntentId: "pi_xxx",
123
+ reservationId: "res_123",
124
+ });
125
+ ```
66
126
 
67
127
  ### `resira.createReservation(payload, options?)`
68
128
 
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.
129
+ Creates a reservation via the public booking flow. An idempotency key is automatically generated unless you pass one explicitly.
72
130
 
73
- ```typescript
131
+ ```ts
74
132
  const { reservation } = await resira.createReservation({
75
133
  resourceId: "prop-1",
76
134
  guestName: "Jane Doe",
@@ -79,107 +137,59 @@ const { reservation } = await resira.createReservation({
79
137
  endDate: "2026-07-07",
80
138
  partySize: 3,
81
139
  });
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
140
  ```
95
141
 
96
- ---
97
-
98
142
  ### `resira.listReservations(resourceId, params?)`
99
143
 
100
- List reservations (paginated).
101
-
102
- ```typescript
144
+ ```ts
103
145
  const page = await resira.listReservations("prop-1", {
104
146
  status: "confirmed",
105
147
  page: 1,
106
148
  limit: 50,
107
149
  });
108
-
109
- console.log(page.data); // Reservation[]
110
- console.log(page.total); // 142
111
- console.log(page.totalPages); // 3
112
150
  ```
113
151
 
114
- ---
115
-
116
152
  ### `resira.getReservation(resourceId, reservationId)`
117
153
 
118
- Get a single reservation by ID.
119
-
120
- ```typescript
121
- const { reservation } = await resira.getReservation("prop-1", "res-uuid");
154
+ ```ts
155
+ const result = await resira.getReservation("prop-1", "res-uuid");
122
156
  ```
123
157
 
124
- ---
125
-
126
158
  ## Error handling
127
159
 
128
- All errors extend `ResiraError`. Use `instanceof` to discriminate:
160
+ All SDK errors extend `ResiraError`.
129
161
 
130
- ```typescript
162
+ ```ts
131
163
  import {
132
- Resira,
133
164
  ResiraApiError,
134
- ResiraRateLimitError,
135
165
  ResiraNetworkError,
166
+ ResiraRateLimitError,
136
167
  } from "@resira/sdk";
137
168
 
138
169
  try {
139
170
  await resira.createReservation(payload);
140
171
  } catch (error) {
141
172
  if (error instanceof ResiraRateLimitError) {
142
- // 429 — SDK already retried `maxRetries` times
143
- console.log(`Rate limited. Retry after ${error.retryAfter}s`);
173
+ console.log(error.retryAfter);
144
174
  } else if (error instanceof ResiraApiError) {
145
- // 4xx / 5xx
146
- console.log(`${error.status}: ${error.message}`);
147
- console.log(error.retryable); // true for 5xx
175
+ console.log(error.status, error.message);
148
176
  } else if (error instanceof ResiraNetworkError) {
149
- // DNS / TLS / offline
150
- console.log("Network error:", error.message);
177
+ console.log(error.message);
151
178
  }
152
179
  }
153
180
  ```
154
181
 
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
- ---
182
+ | Error class | Meaning |
183
+ | --- | --- |
184
+ | `ResiraRateLimitError` | 429 response after retry handling |
185
+ | `ResiraApiError` | Non-2xx API response |
186
+ | `ResiraNetworkError` | Network/DNS/TLS failure |
177
187
 
178
188
  ## Requirements
179
189
 
180
- - Node.js 18 (uses native `fetch`)
190
+ - Node.js >= 18
181
191
  - No runtime dependencies
182
192
 
183
- ## Version tracking
193
+ ## Release notes
184
194
 
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.
195
+ 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;