@resira/sdk 0.2.3 → 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 ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable SDK and public API tracking updates are documented here.
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
+
16
+ ## 0.2.3
17
+
18
+ - Current workspace release for the public reservation API package.
19
+ - Tracks the typed payment intent, payment confirmation, promo-code validation, product catalog, and product-availability flows exposed by the SDK.
20
+ - Keeps the SDK aligned with the current `v1/public` endpoints plus reservation creation via `v2/api/reservations`.
21
+
22
+ ## 0.2.1
23
+
24
+ - Published package update after the initial TypeScript SDK rollout.
25
+ - Stabilized the package metadata for the public reservation API client.
26
+
27
+ ## 0.2.0
28
+
29
+ - Introduced the first public TypeScript SDK for Resira reservations.
30
+ - Added typed availability, reservation creation, reservation lookup, and retry-aware error handling.
31
+
32
+ ## 0.1.1
33
+
34
+ - Internal follow-up release used to align the early SDK package with the UI package work.
35
+
36
+ ## 0.1.0
37
+
38
+ - Initial SDK package scaffold.
package/README.md CHANGED
@@ -1,16 +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 history lives in [CHANGELOG.md](./CHANGELOG.md).
6
+
5
7
  ## Installation
6
8
 
7
9
  ```bash
8
- npm install @resira/sdk@0.2.0
10
+ npm install @resira/sdk@0.2.6
9
11
  ```
10
12
 
11
13
  ## Quick start
12
14
 
13
- ```typescript
15
+ ```ts
14
16
  import { Resira } from "@resira/sdk";
15
17
 
16
18
  const resira = new Resira({
@@ -18,57 +20,115 @@ const resira = new Resira({
18
20
  });
19
21
  ```
20
22
 
21
- ## API reference
23
+ ## Client config
22
24
 
23
- ### `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
+ ```
24
38
 
25
- | Option | Type | Default | Description |
26
- | ---------------- | ---------- | --------------------------- | ------------------------------------------------- |
27
- | `apiKey` | `string` | **(required)** | API key (`resira_live_…`) |
28
- | `baseUrl` | `string` | `https://api.resira.io` | Base URL of the API |
29
- | `maxRetries` | `number` | `3` | Max retry attempts for 429 / 5xx |
30
- | `retryBaseDelay` | `number` | `500` | Base delay (ms) for exponential backoff |
31
- | `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 |
32
47
 
33
- ---
48
+ ## Routing behavior
34
49
 
35
- ### `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
36
56
 
37
- Query availability for a resource.
57
+ ### `resira.getConfig()`
58
+
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
+ ```
38
74
 
39
- **Property (date-based):**
40
- ```typescript
41
- const avail = await resira.getAvailability("prop-1", {
75
+ ### `resira.getAvailability(resourceId, params?)`
76
+
77
+ ```ts
78
+ const availability = await resira.getAvailability("prop-1", {
42
79
  startDate: "2026-07-01",
43
80
  endDate: "2026-07-07",
44
81
  });
45
-
46
- console.log(avail.dates?.available); // true
47
- console.log(avail.dates?.totalPrice); // 85000 (cents)
48
- console.log(avail.dates?.blockedDates); // ["2026-07-15", …]
49
82
  ```
50
83
 
51
- **Restaurant (time-slot):**
52
- ```typescript
53
- const avail = await resira.getAvailability("table-5", {
84
+ ```ts
85
+ const slots = await resira.getAvailability("table-5", {
54
86
  date: "2026-07-01",
55
87
  partySize: 4,
56
88
  });
89
+ ```
57
90
 
58
- for (const slot of avail.timeSlots ?? []) {
59
- console.log(`${slot.start} → ${slot.end}: ${slot.remaining} seats`);
60
- }
91
+ ### `resira.listResources()`
92
+
93
+ ```ts
94
+ const { resources } = await resira.listResources();
61
95
  ```
62
96
 
63
- ---
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
+ });
116
+ ```
117
+
118
+ ### `resira.confirmPayment(payload)`
119
+
120
+ ```ts
121
+ const confirmation = await resira.confirmPayment({
122
+ paymentIntentId: "pi_xxx",
123
+ reservationId: "res_123",
124
+ });
125
+ ```
64
126
 
65
127
  ### `resira.createReservation(payload, options?)`
66
128
 
67
- Create a reservation. The `resourceId` is included in the request body.
68
- An `Idempotency-Key` header is automatically generated to prevent
69
- duplicate bookings on retries.
129
+ Creates a reservation via the public booking flow. An idempotency key is automatically generated unless you pass one explicitly.
70
130
 
71
- ```typescript
131
+ ```ts
72
132
  const { reservation } = await resira.createReservation({
73
133
  resourceId: "prop-1",
74
134
  guestName: "Jane Doe",
@@ -77,103 +137,59 @@ const { reservation } = await resira.createReservation({
77
137
  endDate: "2026-07-07",
78
138
  partySize: 3,
79
139
  });
80
-
81
- console.log(reservation.id); // "uuid-…"
82
- console.log(reservation.status); // "pending"
83
- console.log(reservation.totalPrice);// 85000
84
140
  ```
85
141
 
86
- **Custom idempotency key:**
87
- ```typescript
88
- const { reservation } = await resira.createReservation(
89
- payload,
90
- { idempotencyKey: "form-submit-abc123" },
91
- );
92
- ```
93
-
94
- ---
95
-
96
142
  ### `resira.listReservations(resourceId, params?)`
97
143
 
98
- List reservations (paginated).
99
-
100
- ```typescript
144
+ ```ts
101
145
  const page = await resira.listReservations("prop-1", {
102
146
  status: "confirmed",
103
147
  page: 1,
104
148
  limit: 50,
105
149
  });
106
-
107
- console.log(page.data); // Reservation[]
108
- console.log(page.total); // 142
109
- console.log(page.totalPages); // 3
110
150
  ```
111
151
 
112
- ---
113
-
114
152
  ### `resira.getReservation(resourceId, reservationId)`
115
153
 
116
- Get a single reservation by ID.
117
-
118
- ```typescript
119
- const { reservation } = await resira.getReservation("prop-1", "res-uuid");
154
+ ```ts
155
+ const result = await resira.getReservation("prop-1", "res-uuid");
120
156
  ```
121
157
 
122
- ---
123
-
124
158
  ## Error handling
125
159
 
126
- All errors extend `ResiraError`. Use `instanceof` to discriminate:
160
+ All SDK errors extend `ResiraError`.
127
161
 
128
- ```typescript
162
+ ```ts
129
163
  import {
130
- Resira,
131
164
  ResiraApiError,
132
- ResiraRateLimitError,
133
165
  ResiraNetworkError,
166
+ ResiraRateLimitError,
134
167
  } from "@resira/sdk";
135
168
 
136
169
  try {
137
170
  await resira.createReservation(payload);
138
171
  } catch (error) {
139
172
  if (error instanceof ResiraRateLimitError) {
140
- // 429 — SDK already retried `maxRetries` times
141
- console.log(`Rate limited. Retry after ${error.retryAfter}s`);
173
+ console.log(error.retryAfter);
142
174
  } else if (error instanceof ResiraApiError) {
143
- // 4xx / 5xx
144
- console.log(`${error.status}: ${error.message}`);
145
- console.log(error.retryable); // true for 5xx
175
+ console.log(error.status, error.message);
146
176
  } else if (error instanceof ResiraNetworkError) {
147
- // DNS / TLS / offline
148
- console.log("Network error:", error.message);
177
+ console.log(error.message);
149
178
  }
150
179
  }
151
180
  ```
152
181
 
153
- | Error class | When | Retried automatically? |
154
- | ---------------------- | ----------------------------- | ---------------------- |
155
- | `ResiraRateLimitError` | 429 Too Many Requests | (respects Retry-After) |
156
- | `ResiraApiError` | Any non-2xx | for 5xx only |
157
- | `ResiraNetworkError` | fetch failed (DNS, offline…) | |
158
-
159
- ---
160
-
161
- ## Retry behaviour
162
-
163
- The SDK retries failed requests with **exponential backoff + jitter**:
164
-
165
- - **Attempt 1**: 0 – 500 ms
166
- - **Attempt 2**: 0 – 1 000 ms
167
- - **Attempt 3**: 0 – 2 000 ms
168
-
169
- For 429 responses, the `Retry-After` header value is used as a minimum delay.
170
-
171
- Idempotency keys ensure that retried POST requests don't create
172
- duplicate reservations.
173
-
174
- ---
182
+ | Error class | Meaning |
183
+ | --- | --- |
184
+ | `ResiraRateLimitError` | 429 response after retry handling |
185
+ | `ResiraApiError` | Non-2xx API response |
186
+ | `ResiraNetworkError` | Network/DNS/TLS failure |
175
187
 
176
188
  ## Requirements
177
189
 
178
- - Node.js 18 (uses native `fetch`)
190
+ - Node.js >= 18
179
191
  - No runtime dependencies
192
+
193
+ ## Release notes
194
+
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;