@theelena/sdk 0.1.0 → 0.1.2

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/dist/index.js ADDED
@@ -0,0 +1,1766 @@
1
+ // src/errors.ts
2
+ var ElenaApiError = class extends Error {
3
+ status;
4
+ code;
5
+ details;
6
+ retryAfter;
7
+ constructor(message, details) {
8
+ super(message);
9
+ this.name = "ElenaApiError";
10
+ this.status = details.status;
11
+ this.code = details.code;
12
+ this.details = details.details;
13
+ this.retryAfter = details.retryAfter;
14
+ }
15
+ };
16
+ var ElenaNetworkError = class extends Error {
17
+ constructor(message = "Error de red desconocido") {
18
+ super(message);
19
+ this.name = "ElenaNetworkError";
20
+ }
21
+ };
22
+ var ElenaTimeoutError = class extends Error {
23
+ timeoutMs;
24
+ constructor(timeoutMs) {
25
+ super(`Elena no respondi\xF3 en ${timeoutMs} ms.`);
26
+ this.name = "ElenaTimeoutError";
27
+ this.timeoutMs = timeoutMs;
28
+ }
29
+ };
30
+ var ElenaRequestAbortedError = class extends Error {
31
+ constructor(message = "La solicitud a Elena fue cancelada.") {
32
+ super(message);
33
+ this.name = "ElenaRequestAbortedError";
34
+ }
35
+ };
36
+ var ElenaResponseError = class extends Error {
37
+ status;
38
+ bodyText;
39
+ constructor(message, details) {
40
+ super(message);
41
+ this.name = "ElenaResponseError";
42
+ this.status = details.status;
43
+ this.bodyText = details.bodyText;
44
+ }
45
+ };
46
+ var ElenaCheckoutValidationError = class extends Error {
47
+ code;
48
+ /** Selection path, e.g. selections[0].quantity or selections[0].selectedOptions. */
49
+ field;
50
+ constructor(message, details) {
51
+ super(message);
52
+ this.name = "ElenaCheckoutValidationError";
53
+ this.code = details.code;
54
+ this.field = details.field;
55
+ }
56
+ };
57
+
58
+ // src/resources/orders.ts
59
+ var AccessResource = class {
60
+ constructor(http, businessSlug) {
61
+ this.http = http;
62
+ this.businessSlug = businessSlug;
63
+ }
64
+ http;
65
+ businessSlug;
66
+ resolve(publicKey) {
67
+ return this.http.request(
68
+ `/api/v1/public/catalog/${encodeURIComponent(this.businessSlug)}/access/${encodeURIComponent(publicKey)}`
69
+ );
70
+ }
71
+ createOrder(publicKey, input, options = {}) {
72
+ const body = options.idempotencyKey && !input.idempotencyKey ? { ...input, idempotencyKey: options.idempotencyKey } : input;
73
+ return this.http.request(
74
+ `/api/v1/public/catalog/${encodeURIComponent(this.businessSlug)}/access/${encodeURIComponent(publicKey)}/orders`,
75
+ { method: "POST", body, signal: options.signal }
76
+ );
77
+ }
78
+ };
79
+ var OrdersResource = class {
80
+ constructor(http, businessSlug) {
81
+ this.http = http;
82
+ this.businessSlug = businessSlug;
83
+ }
84
+ http;
85
+ businessSlug;
86
+ create(input, options = {}) {
87
+ const body = options.idempotencyKey && !input.idempotencyKey ? { ...input, idempotencyKey: options.idempotencyKey } : input;
88
+ return this.http.request(
89
+ `/api/v1/public/catalog/${encodeURIComponent(this.businessSlug)}/orders`,
90
+ { method: "POST", body, signal: options.signal }
91
+ );
92
+ }
93
+ async getById(orderId) {
94
+ if (!this.http.visitorId) {
95
+ throw new ElenaApiError("Consultar un pedido requiere visitorId.", { status: 0 });
96
+ }
97
+ return this.http.request(
98
+ `/api/v1/public/catalog/${encodeURIComponent(this.businessSlug)}/orders/${encodeURIComponent(orderId)}`
99
+ );
100
+ }
101
+ getTracking(trackingToken) {
102
+ return this.http.request(
103
+ `/api/v1/public/catalog/${encodeURIComponent(this.businessSlug)}/tracking/${encodeURIComponent(trackingToken)}`
104
+ );
105
+ }
106
+ submitPaymentClaim(trackingToken, input) {
107
+ const form = new FormData();
108
+ form.set("referenceNumber", input.referenceNumber);
109
+ if (input.proofImage) form.set("proofImage", input.proofImage);
110
+ return this.http.request(
111
+ `/api/v1/public/catalog/${encodeURIComponent(this.businessSlug)}/tracking/${encodeURIComponent(trackingToken)}/payment-claims`,
112
+ { method: "POST", body: form }
113
+ );
114
+ }
115
+ };
116
+
117
+ // src/resources/catalog.ts
118
+ var CatalogResource = class {
119
+ constructor(http, businessSlug) {
120
+ this.http = http;
121
+ this.businessSlug = businessSlug;
122
+ }
123
+ http;
124
+ businessSlug;
125
+ getInfo() {
126
+ return this.http.request(
127
+ `/api/v1/public/catalog/${encodeURIComponent(this.businessSlug)}`
128
+ );
129
+ }
130
+ getMenu(query) {
131
+ return this.http.request(
132
+ `/api/v1/public/catalog/${encodeURIComponent(this.businessSlug)}/menu`,
133
+ { query: { search: query?.search } }
134
+ );
135
+ }
136
+ listHeroSlides() {
137
+ return this.http.request(
138
+ `/api/v1/public/catalog/${encodeURIComponent(this.businessSlug)}/hero-slides`
139
+ );
140
+ }
141
+ };
142
+ var TablesResource = class {
143
+ constructor(http, businessSlug) {
144
+ this.http = http;
145
+ this.businessSlug = businessSlug;
146
+ }
147
+ http;
148
+ businessSlug;
149
+ listAvailable() {
150
+ return this.http.request(
151
+ `/api/v1/public/catalog/${encodeURIComponent(this.businessSlug)}/tables/available`
152
+ );
153
+ }
154
+ get(tableId) {
155
+ return this.http.request(
156
+ `/api/v1/public/catalog/${encodeURIComponent(this.businessSlug)}/tables/${encodeURIComponent(tableId)}`
157
+ );
158
+ }
159
+ };
160
+
161
+ // src/resources/realtime.ts
162
+ var RealtimeResource = class {
163
+ constructor(http, businessSlug) {
164
+ this.http = http;
165
+ this.businessSlug = businessSlug;
166
+ }
167
+ http;
168
+ businessSlug;
169
+ /** SSE stream URL. EventSource-compatible: identity travels in query, not headers. */
170
+ getConnection(visitorId) {
171
+ const resolvedVisitorId = visitorId ?? this.http.visitorId;
172
+ if (!resolvedVisitorId) {
173
+ throw new ElenaApiError("El stream de eventos requiere visitorId.", { status: 0 });
174
+ }
175
+ return {
176
+ url: this.http.url("/api/v1/public/events/stream", {
177
+ visitorId: resolvedVisitorId,
178
+ restaurantSlug: this.businessSlug
179
+ })
180
+ };
181
+ }
182
+ };
183
+ var ChatResource = class {
184
+ constructor(http, businessSlug) {
185
+ this.http = http;
186
+ this.businessSlug = businessSlug;
187
+ }
188
+ http;
189
+ businessSlug;
190
+ /** Connection descriptor for the existing Tari chat runtime. No HTTP call. */
191
+ getConnection() {
192
+ return {
193
+ apiUrl: this.http.url("/api/v1"),
194
+ businessSlug: this.businessSlug,
195
+ ...this.http.visitorId ? { visitorId: this.http.visitorId } : {}
196
+ };
197
+ }
198
+ };
199
+ var ContinuationsResource = class {
200
+ constructor(http, businessSlug) {
201
+ this.http = http;
202
+ this.businessSlug = businessSlug;
203
+ }
204
+ http;
205
+ businessSlug;
206
+ exchange(token) {
207
+ return this.http.request(
208
+ `/api/v1/public/journeys/${encodeURIComponent(this.businessSlug)}/exchange`,
209
+ { method: "POST", body: { token } }
210
+ );
211
+ }
212
+ };
213
+ var JourneyConflictError = class extends ElenaApiError {
214
+ journey;
215
+ constructor(journey) {
216
+ super("La journey cambi\xF3 en el servidor; recarga el estado.", {
217
+ status: 409,
218
+ code: "journey_conflict",
219
+ details: { journey }
220
+ });
221
+ this.name = "JourneyConflictError";
222
+ this.journey = journey;
223
+ }
224
+ };
225
+ var JourneyResource = class _JourneyResource {
226
+ constructor(http, businessSlug) {
227
+ this.http = http;
228
+ this.businessSlug = businessSlug;
229
+ }
230
+ http;
231
+ businessSlug;
232
+ getState() {
233
+ return this.http.request(
234
+ `/api/v1/public/journeys/${encodeURIComponent(this.businessSlug)}/state`
235
+ );
236
+ }
237
+ async dispatch(input) {
238
+ try {
239
+ return await this.http.request(
240
+ `/api/v1/public/journeys/${encodeURIComponent(this.businessSlug)}/events`,
241
+ {
242
+ method: "POST",
243
+ body: {
244
+ eventId: input.eventId,
245
+ expectedVersion: input.expectedVersion,
246
+ event: input.event
247
+ }
248
+ }
249
+ );
250
+ } catch (error) {
251
+ throw _JourneyResource.withConflict(error);
252
+ }
253
+ }
254
+ async confirm(input) {
255
+ try {
256
+ return await this.http.request(`/api/v1/public/journeys/${encodeURIComponent(this.businessSlug)}/confirm`, {
257
+ method: "POST",
258
+ body: {
259
+ eventId: input.eventId,
260
+ expectedVersion: input.expectedVersion,
261
+ fingerprint: input.fingerprint
262
+ }
263
+ });
264
+ } catch (error) {
265
+ throw _JourneyResource.withConflict(error);
266
+ }
267
+ }
268
+ static withConflict(error) {
269
+ if (error instanceof ElenaApiError && error.status === 409 && typeof error.details === "object" && error.details !== null && "journey" in error.details) {
270
+ const journey = error.details.journey;
271
+ return new JourneyConflictError(journey);
272
+ }
273
+ return error;
274
+ }
275
+ };
276
+
277
+ // src/resources/receipts.ts
278
+ var ReceiptsResource = class {
279
+ constructor(http, businessSlug) {
280
+ this.http = http;
281
+ this.businessSlug = businessSlug;
282
+ }
283
+ http;
284
+ businessSlug;
285
+ async get(receiptToken) {
286
+ const receipt = await this.http.request(
287
+ `/api/v1/public/catalog/${encodeURIComponent(this.businessSlug)}/sales/${encodeURIComponent(receiptToken)}`
288
+ );
289
+ const url = receipt.receipt?.url;
290
+ if (!url || !url.startsWith("/")) return receipt;
291
+ if (!receipt.receipt) return receipt;
292
+ return { ...receipt, receipt: { ...receipt.receipt, url: this.http.url(url) } };
293
+ }
294
+ /** PDF endpoint answers 302; callers open/download the URL, never JSON-fetch it. */
295
+ getPdfUrl(receiptToken) {
296
+ return this.http.url(
297
+ `/api/v1/public/catalog/${encodeURIComponent(this.businessSlug)}/sales/${encodeURIComponent(receiptToken)}/pdf`
298
+ );
299
+ }
300
+ };
301
+ var PresenceResource = class {
302
+ constructor(http, businessSlug) {
303
+ this.http = http;
304
+ this.businessSlug = businessSlug;
305
+ }
306
+ http;
307
+ businessSlug;
308
+ report(input, options = {}) {
309
+ return this.http.request(
310
+ `/api/v1/public/catalog/${encodeURIComponent(this.businessSlug)}/live-session`,
311
+ { method: "POST", body: input, signal: options.signal }
312
+ );
313
+ }
314
+ /** Beacon payload for pagehide/unload, where fetch cannot be awaited. */
315
+ getBeaconRequest(input) {
316
+ return {
317
+ url: this.http.url(
318
+ `/api/v1/public/catalog/${encodeURIComponent(this.businessSlug)}/live-session`
319
+ ),
320
+ body: new Blob([JSON.stringify(input)], { type: "application/json" })
321
+ };
322
+ }
323
+ };
324
+
325
+ // src/resources/discovery.ts
326
+ var EventsResource = class {
327
+ constructor(http) {
328
+ this.http = http;
329
+ }
330
+ http;
331
+ get(businessSlug, eventSlug) {
332
+ return this.http.request("/api/v1/public/events/detail", {
333
+ query: { restaurantSlug: businessSlug, eventSlug }
334
+ });
335
+ }
336
+ };
337
+ var TicketsResource = class {
338
+ constructor(http) {
339
+ this.http = http;
340
+ }
341
+ http;
342
+ emit(input) {
343
+ return this.http.request("/api/v1/public/tickets/emissions", {
344
+ method: "POST",
345
+ body: input
346
+ });
347
+ }
348
+ getStatus(token) {
349
+ return this.http.request(`/api/v1/public/tickets/${encodeURIComponent(token)}`);
350
+ }
351
+ };
352
+ var AssetsResource = class {
353
+ constructor(http) {
354
+ this.http = http;
355
+ }
356
+ http;
357
+ /** Render endpoint serves binary content; callers use the URL, never JSON-fetch it. */
358
+ getRenderUrl(assetId) {
359
+ return this.http.url(`/api/v1/public/assets/${encodeURIComponent(assetId)}/render`);
360
+ }
361
+ };
362
+
363
+ // src/business.ts
364
+ var BusinessScope = class {
365
+ constructor(http, slug) {
366
+ this.http = http;
367
+ const normalized = slug.trim();
368
+ if (!normalized) {
369
+ throw new TypeError("BusinessScope necesita un slug.");
370
+ }
371
+ this.slug = normalized;
372
+ this.catalog = new CatalogResource(http, normalized);
373
+ this.tables = new TablesResource(http, normalized);
374
+ this.access = new AccessResource(http, normalized);
375
+ this.orders = new OrdersResource(http, normalized);
376
+ this.receipts = new ReceiptsResource(http, normalized);
377
+ this.presence = new PresenceResource(http, normalized);
378
+ this.continuations = new ContinuationsResource(http, normalized);
379
+ this.realtime = new RealtimeResource(http, normalized);
380
+ this.chat = new ChatResource(http, normalized);
381
+ this.eventsResource = new EventsResource(http);
382
+ this.ticketsResource = new TicketsResource(http);
383
+ }
384
+ http;
385
+ slug;
386
+ catalog;
387
+ tables;
388
+ access;
389
+ orders;
390
+ receipts;
391
+ presence;
392
+ continuations;
393
+ realtime;
394
+ chat;
395
+ eventsResource;
396
+ ticketsResource;
397
+ getInfo() {
398
+ return this.catalog.getInfo();
399
+ }
400
+ getEvent(eventSlug) {
401
+ return this.eventsResource.get(this.slug, eventSlug);
402
+ }
403
+ emitTickets(input) {
404
+ return this.ticketsResource.emit({ ...input, restaurantSlug: this.slug });
405
+ }
406
+ };
407
+
408
+ // src/http-client.ts
409
+ var DEFAULT_TIMEOUT_MS = 15e3;
410
+ function isFormDataBody(body) {
411
+ return typeof FormData !== "undefined" && body instanceof FormData;
412
+ }
413
+ function isPlainJsonBody(body) {
414
+ if (body === null || body === void 0) return false;
415
+ if (typeof body !== "object") return false;
416
+ if (isFormDataBody(body)) return false;
417
+ if (body instanceof Blob) return false;
418
+ if (body instanceof ArrayBuffer) return false;
419
+ if (ArrayBuffer.isView(body)) return false;
420
+ if (body instanceof URLSearchParams) return false;
421
+ return true;
422
+ }
423
+ var ElenaHttpClient = class {
424
+ baseUrl;
425
+ visitorId;
426
+ fetchImpl;
427
+ timeoutMs;
428
+ accessToken;
429
+ constructor(options, accessToken) {
430
+ const normalizedBaseUrl = options.baseUrl.trim().replace(/\/+$/, "");
431
+ if (!normalizedBaseUrl) {
432
+ throw new TypeError("Elena SDK necesita un baseUrl.");
433
+ }
434
+ try {
435
+ const base = new URL(normalizedBaseUrl);
436
+ if (!/^https?:\/\//i.test(normalizedBaseUrl) || /[?#\\\s]/.test(normalizedBaseUrl) || base.username || base.password) {
437
+ throw new TypeError();
438
+ }
439
+ } catch {
440
+ throw new TypeError(
441
+ "El baseUrl de Elena debe ser una URL HTTP(S) sin credenciales, query ni fragmento."
442
+ );
443
+ }
444
+ this.baseUrl = normalizedBaseUrl;
445
+ const visitorId = options.visitorId?.trim();
446
+ this.visitorId = visitorId ? visitorId : void 0;
447
+ const fetchImpl = options.fetch ?? globalThis.fetch;
448
+ if (typeof fetchImpl !== "function") {
449
+ throw new TypeError("Elena SDK necesita una implementaci\xF3n de fetch.");
450
+ }
451
+ this.fetchImpl = fetchImpl.bind(globalThis);
452
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
453
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
454
+ throw new TypeError("El timeout del SDK de Elena debe ser mayor que cero.");
455
+ }
456
+ this.timeoutMs = timeoutMs;
457
+ const token = accessToken?.trim();
458
+ this.accessToken = token ? token : void 0;
459
+ }
460
+ url(path, query) {
461
+ if (/^[a-z][a-z\d+.-]*:/i.test(path) || path.startsWith("//") || // eslint-disable-next-line no-control-regex -- Intentional: reject control chars and whitespace in paths.
462
+ /[\\\s\u0000-\u001f\u007f]/.test(path) || path.split(/[/?#]/).some((segment) => /^(?:\.|%2e){1,2}$/i.test(segment))) {
463
+ throw new TypeError(
464
+ "Las rutas del SDK de Elena deben ser relativas al baseUrl y no escapar de \xE9l."
465
+ );
466
+ }
467
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
468
+ const url = `${this.baseUrl}${normalizedPath}`;
469
+ if (!query) return url;
470
+ const params = new URLSearchParams();
471
+ for (const [key, value] of Object.entries(query)) {
472
+ if (value === void 0) continue;
473
+ params.set(key, String(value));
474
+ }
475
+ const qs = params.toString();
476
+ return qs ? `${url}?${qs}` : url;
477
+ }
478
+ async request(path, options = {}) {
479
+ if (options.signal?.aborted) {
480
+ throw new ElenaRequestAbortedError();
481
+ }
482
+ const headers = new Headers();
483
+ headers.set("Accept", "application/json");
484
+ let body;
485
+ if (options.body !== void 0) {
486
+ if (isPlainJsonBody(options.body)) {
487
+ headers.set("Content-Type", "application/json");
488
+ body = JSON.stringify(options.body);
489
+ } else {
490
+ body = options.body;
491
+ }
492
+ }
493
+ if (this.visitorId) headers.set("x-visitor-id", this.visitorId);
494
+ if (options.headers) {
495
+ new Headers(options.headers).forEach((value, key) => {
496
+ headers.set(key, value);
497
+ });
498
+ }
499
+ if (this.accessToken) headers.set("Authorization", `Bearer ${this.accessToken}`);
500
+ const controller = new AbortController();
501
+ let timedOut = false;
502
+ const timeout = setTimeout(() => {
503
+ timedOut = true;
504
+ controller.abort();
505
+ }, this.timeoutMs);
506
+ const onCallerAbort = () => {
507
+ controller.abort();
508
+ };
509
+ options.signal?.addEventListener("abort", onCallerAbort, { once: true });
510
+ if (options.signal?.aborted) onCallerAbort();
511
+ const bounded = (promise) => new Promise((resolve, reject) => {
512
+ const onAbort = () => {
513
+ cleanup();
514
+ reject(timedOut ? new ElenaTimeoutError(this.timeoutMs) : new ElenaRequestAbortedError());
515
+ };
516
+ const cleanup = () => controller.signal.removeEventListener("abort", onAbort);
517
+ controller.signal.addEventListener("abort", onAbort, { once: true });
518
+ promise.then(
519
+ (value) => {
520
+ cleanup();
521
+ resolve(value);
522
+ },
523
+ (error) => {
524
+ cleanup();
525
+ reject(error);
526
+ }
527
+ );
528
+ if (controller.signal.aborted) onAbort();
529
+ });
530
+ let response;
531
+ let text;
532
+ try {
533
+ response = await bounded(
534
+ Promise.resolve().then(() => {
535
+ if (controller.signal.aborted) throw new ElenaRequestAbortedError();
536
+ return this.fetchImpl(this.url(path, options.query), {
537
+ method: options.method ?? "GET",
538
+ headers,
539
+ body,
540
+ credentials: "omit",
541
+ redirect: "error",
542
+ referrerPolicy: "no-referrer",
543
+ signal: controller.signal,
544
+ ...options.keepalive ? { keepalive: true } : {}
545
+ });
546
+ })
547
+ );
548
+ if (response.redirected || response.status >= 300 && response.status < 400 || response.type === "opaqueredirect" || response.url && new URL(response.url).origin !== new URL(this.baseUrl).origin) {
549
+ throw new ElenaResponseError("El SDK de Elena no sigue redirecciones de la API.", {
550
+ status: response.status
551
+ });
552
+ }
553
+ text = await bounded(Promise.resolve().then(() => response.text()));
554
+ } catch (error) {
555
+ if (timedOut) throw new ElenaTimeoutError(this.timeoutMs);
556
+ if (options.signal?.aborted) throw new ElenaRequestAbortedError();
557
+ if (error instanceof ElenaResponseError) throw error;
558
+ if (error instanceof Error && error.name === "AbortError") {
559
+ throw new ElenaRequestAbortedError();
560
+ }
561
+ throw new ElenaNetworkError(error instanceof Error ? error.message : void 0);
562
+ } finally {
563
+ clearTimeout(timeout);
564
+ options.signal?.removeEventListener("abort", onCallerAbort);
565
+ }
566
+ const retryAfter = response.headers.get("retry-after") ?? void 0;
567
+ let payload = null;
568
+ if (text) {
569
+ try {
570
+ payload = JSON.parse(text);
571
+ } catch {
572
+ if (!response.ok) {
573
+ throw new ElenaApiError(`Elena respondi\xF3 ${response.status}: ${response.statusText}`, {
574
+ status: response.status,
575
+ details: text,
576
+ retryAfter
577
+ });
578
+ }
579
+ throw new ElenaResponseError("Respuesta inv\xE1lida del servidor de Elena.", {
580
+ status: response.status,
581
+ bodyText: text
582
+ });
583
+ }
584
+ }
585
+ if (!response.ok) {
586
+ let code;
587
+ let message;
588
+ if (typeof payload === "object" && payload !== null) {
589
+ if ("code" in payload && typeof payload.code === "string" && payload.code) {
590
+ code = payload.code;
591
+ } else if ("error" in payload && typeof payload.error === "string" && payload.error) {
592
+ code = payload.error;
593
+ }
594
+ if (!code && "details" in payload && typeof payload.details === "object" && payload.details !== null && "code" in payload.details && typeof payload.details.code === "string" && payload.details.code) {
595
+ code = payload.details.code;
596
+ }
597
+ if ("message" in payload && typeof payload.message === "string" && payload.message) {
598
+ message = payload.message;
599
+ }
600
+ }
601
+ throw new ElenaApiError(
602
+ message ?? `Elena respondi\xF3 ${response.status}: ${response.statusText}`,
603
+ { status: response.status, code, details: payload, retryAfter }
604
+ );
605
+ }
606
+ return payload;
607
+ }
608
+ };
609
+
610
+ // src/client.ts
611
+ function createElenaPublicClient(options) {
612
+ const http = new ElenaHttpClient(options);
613
+ return {
614
+ business: (slug) => new BusinessScope(http, slug),
615
+ tickets: new TicketsResource(http),
616
+ assets: new AssetsResource(http)
617
+ };
618
+ }
619
+ function createElenaJourneyClient(options) {
620
+ if (!options.accessToken?.trim()) {
621
+ throw new TypeError("El cliente journey de Elena necesita un accessToken.");
622
+ }
623
+ const businessSlug = options.businessSlug.trim();
624
+ if (!businessSlug) {
625
+ throw new TypeError("El cliente journey de Elena necesita un businessSlug.");
626
+ }
627
+ const http = new ElenaHttpClient(
628
+ {
629
+ baseUrl: options.baseUrl,
630
+ fetch: options.fetch,
631
+ timeoutMs: options.timeoutMs
632
+ },
633
+ options.accessToken
634
+ );
635
+ return { businessSlug, journeys: new JourneyResource(http, businessSlug) };
636
+ }
637
+
638
+ // src/checkout.ts
639
+ var catalogName = (name) => name.trim().toLocaleLowerCase();
640
+ function invalid(code, field, message) {
641
+ throw new ElenaCheckoutValidationError(message, { code, field });
642
+ }
643
+ function buildOrderItems(menu, selections) {
644
+ return selections.map((selection, itemIndex) => {
645
+ const field = `selections[${itemIndex}]`;
646
+ const products = menu.products.filter((product2) => product2.id === selection.productId);
647
+ const product = products[0];
648
+ if (!product) {
649
+ return invalid(
650
+ "PRODUCT_NOT_FOUND",
651
+ `${field}.productId`,
652
+ "El producto no existe en el cat\xE1logo."
653
+ );
654
+ }
655
+ if (products.length !== 1) {
656
+ return invalid("AMBIGUOUS_SELECTION", `${field}.productId`, "El ID del producto es ambiguo.");
657
+ }
658
+ if (!product.available) {
659
+ return invalid(
660
+ "PRODUCT_UNAVAILABLE",
661
+ `${field}.productId`,
662
+ "El producto no est\xE1 disponible."
663
+ );
664
+ }
665
+ if (!Number.isInteger(selection.quantity) || selection.quantity < 1 || selection.quantity > 100) {
666
+ return invalid(
667
+ "INVALID_QUANTITY",
668
+ `${field}.quantity`,
669
+ "La cantidad debe ser un entero entre 1 y 100."
670
+ );
671
+ }
672
+ const counts = /* @__PURE__ */ new Map();
673
+ const selected = /* @__PURE__ */ new Map();
674
+ const extras = (selection.selectedOptions ?? []).map((selectionOption, optionIndex) => {
675
+ const optionField = `${field}.selectedOptions[${optionIndex}]`;
676
+ const groups = product.customizationGroups.filter(
677
+ (group2) => group2.id === selectionOption.groupId
678
+ );
679
+ const group = groups[0];
680
+ if (!group) {
681
+ return invalid(
682
+ "UNKNOWN_GROUP",
683
+ `${optionField}.groupId`,
684
+ "El grupo no pertenece al producto."
685
+ );
686
+ }
687
+ if (groups.length !== 1 || !catalogName(group.name) || product.customizationGroups.filter(
688
+ (candidate) => catalogName(candidate.name) === catalogName(group.name)
689
+ ).length !== 1) {
690
+ return invalid(
691
+ "AMBIGUOUS_SELECTION",
692
+ `${optionField}.groupId`,
693
+ "El nombre o ID del grupo es ambiguo."
694
+ );
695
+ }
696
+ const options = group.options.filter((option2) => option2.id === selectionOption.optionId);
697
+ const option = options[0];
698
+ if (!option) {
699
+ return invalid(
700
+ "UNKNOWN_OPTION",
701
+ `${optionField}.optionId`,
702
+ "La opci\xF3n no pertenece al grupo."
703
+ );
704
+ }
705
+ if (options.length !== 1 || !catalogName(option.name) || group.options.filter(
706
+ (candidate) => catalogName(candidate.name) === catalogName(option.name)
707
+ ).length !== 1) {
708
+ return invalid(
709
+ "AMBIGUOUS_SELECTION",
710
+ `${optionField}.optionId`,
711
+ "El nombre o ID de la opci\xF3n es ambiguo."
712
+ );
713
+ }
714
+ const groupSelections = selected.get(group.id) ?? /* @__PURE__ */ new Set();
715
+ if (groupSelections.has(option.id)) {
716
+ return invalid(
717
+ "DUPLICATE_OPTION",
718
+ `${optionField}.optionId`,
719
+ "Una opci\xF3n no puede seleccionarse m\xE1s de una vez por art\xEDculo."
720
+ );
721
+ }
722
+ groupSelections.add(option.id);
723
+ selected.set(group.id, groupSelections);
724
+ counts.set(group.id, (counts.get(group.id) ?? 0) + 1);
725
+ return {
726
+ groupName: group.name,
727
+ optionName: option.name,
728
+ additionalPrice: option.additionalPrice
729
+ };
730
+ });
731
+ for (const group of product.customizationGroups) {
732
+ const count = counts.get(group.id) ?? 0;
733
+ if (group.required && count === 0) {
734
+ return invalid(
735
+ "REQUIRED_OPTIONS",
736
+ `${field}.selectedOptions`,
737
+ `El grupo "${group.name}" requiere una opci\xF3n.`
738
+ );
739
+ }
740
+ if (count < group.minSelections) {
741
+ return invalid(
742
+ "MIN_SELECTIONS",
743
+ `${field}.selectedOptions`,
744
+ `El grupo "${group.name}" requiere al menos ${group.minSelections} opci\xF3n(es).`
745
+ );
746
+ }
747
+ if (count > group.maxSelections) {
748
+ return invalid(
749
+ "MAX_SELECTIONS",
750
+ `${field}.selectedOptions`,
751
+ `El grupo "${group.name}" permite como m\xE1ximo ${group.maxSelections} opci\xF3n(es).`
752
+ );
753
+ }
754
+ }
755
+ return {
756
+ productId: product.id,
757
+ cantidad: selection.quantity,
758
+ unitPrice: product.basePrice,
759
+ ...selection.notes !== void 0 ? { specialNotes: selection.notes } : {},
760
+ ...extras.length ? { extras } : {}
761
+ };
762
+ });
763
+ }
764
+
765
+ // ../shared/src/agent-contracts/access.ts
766
+ import { z as z2 } from "zod";
767
+
768
+ // ../shared/src/agent-contracts/common.ts
769
+ import { z } from "zod";
770
+ var ELENA_AGENT_CONTRACT_VERSION = "1";
771
+ var elenaAgentVersionSchema = z.literal(ELENA_AGENT_CONTRACT_VERSION);
772
+ var elenaAgentIdSchema = z.uuid();
773
+ var elenaAgentRevisionSchema = z.number().int().min(1).max(Number.MAX_SAFE_INTEGER);
774
+ var elenaAgentTimestampSchema = z.iso.datetime();
775
+ var elenaAgentHashSchema = z.string().regex(/^[a-f0-9]{64}$/);
776
+ var elenaAgentRefSchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,99}$/);
777
+ var elenaAgentNameSchema = z.string().trim().min(1).max(160);
778
+ var elenaAgentTextSchema = z.string().trim().min(1).max(2e3);
779
+ var elenaAgentCurrencySchema = z.literal("PEN");
780
+ var elenaAgentNonnegativeMoneySchema = z.string().regex(/^(0|[1-9]\d{0,7})\.\d{2}$/);
781
+ var elenaAgentMoneySchema = elenaAgentNonnegativeMoneySchema.refine(
782
+ (value) => value !== "0.00",
783
+ "Product price must be greater than zero"
784
+ );
785
+ var ELENA_AGENT_LIMITS = Object.freeze({
786
+ categoriesPerImport: 50,
787
+ productsPerImport: 200,
788
+ changesPerOperation: 200,
789
+ publicationProducts: 200,
790
+ galleryImages: 8,
791
+ extraGroups: 10,
792
+ optionsPerGroup: 30,
793
+ jsonBytes: 1024 * 1024,
794
+ mediaBytes: 10 * 1024 * 1024,
795
+ stagedBytesPerRestaurant: 100 * 1024 * 1024,
796
+ stagedFilesPerRestaurant: 20,
797
+ openOperationsPerRestaurant: 20,
798
+ openPreviewsPerRestaurant: 20,
799
+ activeKeysPerFamily: 10,
800
+ keyDefaultTtlSeconds: 30 * 24 * 60 * 60,
801
+ keyMaxTtlSeconds: 90 * 24 * 60 * 60,
802
+ pairingTtlSeconds: 600,
803
+ pairingPollSeconds: 5,
804
+ proposalTtlSeconds: 24 * 60 * 60,
805
+ previewTtlSeconds: 24 * 60 * 60,
806
+ stagingTtlSeconds: 48 * 60 * 60,
807
+ applyWindowSeconds: 60 * 60,
808
+ workerLeaseSeconds: 30,
809
+ workerHeartbeatSeconds: 10,
810
+ maxWorkerAttempts: 5,
811
+ resultRetentionDays: 90,
812
+ pageSize: 100
813
+ });
814
+ var elenaAgentErrorCodeSchema = z.enum([
815
+ "INVALID_REQUEST",
816
+ "UNSUPPORTED_VERSION",
817
+ "CREDENTIAL_REQUIRED",
818
+ "CREDENTIAL_INVALID",
819
+ "CREDENTIAL_EXPIRED",
820
+ "CREDENTIAL_REVOKED",
821
+ "CREDENTIAL_MIXED",
822
+ "CREDENTIAL_WRONG_FAMILY",
823
+ "OWNER_REQUIRED",
824
+ "ORIGIN_REJECTED",
825
+ "APPROVAL_REQUIRED",
826
+ "APPROVAL_REJECTED",
827
+ "APPROVAL_EXPIRED",
828
+ "APPROVAL_MISMATCH",
829
+ "SCOPE_REQUIRED",
830
+ "MODE_READ_ONLY",
831
+ "API_ACCESS_DISABLED",
832
+ "RESTAURANT_NOT_APPROVED",
833
+ "NOT_FOUND",
834
+ "VERSION_CONFLICT",
835
+ "IDEMPOTENCY_CONFLICT",
836
+ "INVALID_PRICE",
837
+ "UNCONFIRMED_DATA",
838
+ "REFERENCE_CONFLICT",
839
+ "MEDIA_INVALID",
840
+ "MEDIA_EXPIRED",
841
+ "QUOTA_EXCEEDED",
842
+ "RATE_LIMITED",
843
+ "PAIRING_PENDING",
844
+ "PAIRING_DENIED",
845
+ "PAIRING_EXPIRED",
846
+ "PAIRING_CONSUMED",
847
+ "OPERATION_TERMINAL",
848
+ "APPLY_WINDOW_EXPIRED",
849
+ "DEPENDENCY_FAILED",
850
+ "TEMPORARY_UNAVAILABLE",
851
+ "INTERNAL_ERROR"
852
+ ]);
853
+ var elenaAgentErrorSchema = z.strictObject({
854
+ version: elenaAgentVersionSchema,
855
+ error: z.strictObject({
856
+ code: elenaAgentErrorCodeSchema,
857
+ message: z.string().max(500),
858
+ retryable: z.boolean(),
859
+ requestId: elenaAgentRefSchema,
860
+ retryAfterSeconds: z.number().int().min(1).max(3600).optional(),
861
+ issues: z.array(
862
+ z.strictObject({
863
+ path: z.string().max(200),
864
+ code: elenaAgentErrorCodeSchema,
865
+ message: z.string().max(200)
866
+ })
867
+ ).max(250).optional()
868
+ })
869
+ });
870
+ var elenaAgentPageQuerySchema = z.strictObject({
871
+ version: elenaAgentVersionSchema,
872
+ cursor: z.string().regex(/^[a-zA-Z0-9_-]{1,512}$/).optional(),
873
+ limit: z.number().int().min(1).max(ELENA_AGENT_LIMITS.pageSize).default(50)
874
+ });
875
+ var elenaAgentEmptyRequestSchema = z.strictObject({ version: elenaAgentVersionSchema });
876
+ var elenaAgentAckSchema = z.strictObject({
877
+ version: elenaAgentVersionSchema,
878
+ ok: z.literal(true)
879
+ });
880
+
881
+ // ../shared/src/agent-contracts/access.ts
882
+ var ELENA_AGENT_TRANSPORT = Object.freeze({
883
+ agentPrefix: "elena_sk_",
884
+ agentHeader: "x-elena-agent-key",
885
+ agentPath: "/api/v1/agent",
886
+ clientPrefix: "elena_pk_",
887
+ clientHeader: "x-elena-client-key",
888
+ clientPath: "/api/v1/public/sdk",
889
+ ownerPath: "/api/v1/agent-access"
890
+ });
891
+ var elenaAgentSecretSchema = z2.string().regex(/^elena_sk_[A-Za-z0-9_-]{43}$/);
892
+ var elenaManualClientKeySchema = z2.string().regex(/^elena_pk_[A-Za-z0-9_-]{43}$/);
893
+ var elenaManagedClientKeySchema = z2.string().max(160).regex(
894
+ /^elena_pk_m_[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.[1-9]\d{0,15}\.[A-Za-z0-9_-]{43}$/
895
+ );
896
+ var elenaClientKeySchema = z2.union([
897
+ elenaManualClientKeySchema,
898
+ elenaManagedClientKeySchema
899
+ ]);
900
+ var elenaAgentDeviceCodeSchema = z2.string().regex(/^[A-Za-z0-9_-]{43}$/);
901
+ var elenaAgentUserCodeSchema = z2.string().regex(/^[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}$/);
902
+ var elenaAgentModeSchema = z2.enum(["read", "mutate"]);
903
+ var elenaAgentScopeSchema = z2.enum([
904
+ "catalog:read",
905
+ "catalog:write",
906
+ "catalog:publish",
907
+ "assets:read",
908
+ "assets:write"
909
+ ]);
910
+ var elenaAgentScopesSchema = z2.array(elenaAgentScopeSchema).min(1).max(5).refine((scopes) => new Set(scopes).size === scopes.length, "Duplicate scope").refine((scopes) => scopes.includes("catalog:read"), "catalog:read is mandatory");
911
+ var elenaAgentActionSchema = z2.enum([
912
+ "context.read",
913
+ "catalog.read",
914
+ "assets.read",
915
+ "assets.stage",
916
+ "operations.read",
917
+ "operations.propose",
918
+ "operations.apply",
919
+ "catalog.publish"
920
+ ]);
921
+ var elenaAgentRestaurantSchema = z2.strictObject({
922
+ id: elenaAgentIdSchema,
923
+ name: elenaAgentNameSchema,
924
+ slug: z2.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).max(100),
925
+ currency: elenaAgentCurrencySchema
926
+ });
927
+ var keyTtl = z2.number().int().min(3600).max(ELENA_AGENT_LIMITS.keyMaxTtlSeconds).default(ELENA_AGENT_LIMITS.keyDefaultTtlSeconds);
928
+ var elenaAgentKeyCreateRequestSchema = z2.discriminatedUnion("kind", [
929
+ z2.strictObject({
930
+ version: elenaAgentVersionSchema,
931
+ kind: z2.literal("agent"),
932
+ label: elenaAgentNameSchema,
933
+ mode: elenaAgentModeSchema.default("read"),
934
+ scopes: elenaAgentScopesSchema.default(["catalog:read", "assets:read"]),
935
+ ttlSeconds: keyTtl
936
+ }),
937
+ z2.strictObject({
938
+ version: elenaAgentVersionSchema,
939
+ kind: z2.literal("client"),
940
+ label: elenaAgentNameSchema,
941
+ ttlSeconds: keyTtl
942
+ })
943
+ ]);
944
+ var elenaAgentKeyPolicyRequestSchema = z2.strictObject({
945
+ version: elenaAgentVersionSchema,
946
+ expectedRevision: elenaAgentRevisionSchema,
947
+ mode: elenaAgentModeSchema,
948
+ scopes: elenaAgentScopesSchema
949
+ });
950
+ var elenaAgentKeyRevokeRequestSchema = z2.strictObject({
951
+ version: elenaAgentVersionSchema,
952
+ expectedRevision: elenaAgentRevisionSchema
953
+ });
954
+ var elenaAgentKeyReplaceRequestSchema = z2.strictObject({
955
+ version: elenaAgentVersionSchema,
956
+ expectedRevision: elenaAgentRevisionSchema,
957
+ ttlSeconds: keyTtl
958
+ });
959
+ var keyMetadata = {
960
+ id: elenaAgentIdSchema,
961
+ label: elenaAgentNameSchema,
962
+ displaySuffix: z2.string().regex(/^[A-Za-z0-9_-]{6}$/),
963
+ revision: elenaAgentRevisionSchema,
964
+ status: z2.enum(["active", "revoked", "expired"]),
965
+ createdAt: elenaAgentTimestampSchema,
966
+ expiresAt: elenaAgentTimestampSchema,
967
+ revokedAt: elenaAgentTimestampSchema.nullable(),
968
+ lastUsedAt: elenaAgentTimestampSchema.nullable()
969
+ };
970
+ var elenaAgentKeyMetadataSchema = z2.discriminatedUnion("kind", [
971
+ z2.strictObject({
972
+ ...keyMetadata,
973
+ kind: z2.literal("agent"),
974
+ mode: elenaAgentModeSchema,
975
+ scopes: elenaAgentScopesSchema
976
+ }),
977
+ z2.strictObject({ ...keyMetadata, kind: z2.literal("client") })
978
+ ]);
979
+ var elenaAgentKeyListSchema = z2.strictObject({
980
+ version: elenaAgentVersionSchema,
981
+ keys: z2.array(elenaAgentKeyMetadataSchema).max(20)
982
+ });
983
+ var elenaAgentKeyReceiptSchema = z2.discriminatedUnion("kind", [
984
+ z2.strictObject({
985
+ version: elenaAgentVersionSchema,
986
+ kind: z2.literal("agent"),
987
+ key: elenaAgentKeyMetadataSchema.options[0],
988
+ secret: elenaAgentSecretSchema
989
+ }),
990
+ z2.strictObject({
991
+ version: elenaAgentVersionSchema,
992
+ kind: z2.literal("client"),
993
+ key: elenaAgentKeyMetadataSchema.options[1],
994
+ clientKey: elenaManualClientKeySchema
995
+ })
996
+ ]);
997
+ var elenaAgentKeyResponseSchema = z2.strictObject({
998
+ version: elenaAgentVersionSchema,
999
+ key: elenaAgentKeyMetadataSchema
1000
+ });
1001
+ var elenaAgentHttpsUrlSchema = z2.url({ protocol: /^https$/ }).max(2048).refine(
1002
+ (value) => /^https:\/\/[^/?#@\\\s]+(?:[/?#]|$)/.test(value),
1003
+ "URL must not contain userinfo"
1004
+ );
1005
+ var elenaAgentConnectionUrlSchema = z2.url({ protocol: /^https?$/ }).max(2048).refine(
1006
+ (value) => /^https?:\/\/[^/?#@\\\s]+(?:[/?#]|$)/.test(value),
1007
+ "URL must not contain userinfo"
1008
+ );
1009
+ var elenaAgentBaseUrlSchema = elenaAgentHttpsUrlSchema.refine(
1010
+ (value) => /^https:\/\/[^/?#@\\\s]+\/?$/.test(value),
1011
+ "Base URL must be an explicit origin"
1012
+ );
1013
+ var elenaAgentConnectionOriginSchema = elenaAgentConnectionUrlSchema.refine(
1014
+ (value) => /^https?:\/\/[^/?#@\\\s]+\/?$/.test(value),
1015
+ "Base URL must be an explicit origin"
1016
+ );
1017
+ var elenaAgentPairingStartRequestSchema = z2.strictObject({
1018
+ version: elenaAgentVersionSchema,
1019
+ label: elenaAgentNameSchema
1020
+ });
1021
+ var elenaAgentPairingStartResponseSchema = z2.strictObject({
1022
+ version: elenaAgentVersionSchema,
1023
+ pairingId: elenaAgentIdSchema,
1024
+ deviceCode: elenaAgentDeviceCodeSchema,
1025
+ userCode: elenaAgentUserCodeSchema,
1026
+ verificationUrl: elenaAgentConnectionUrlSchema,
1027
+ expiresAt: elenaAgentTimestampSchema,
1028
+ intervalSeconds: z2.literal(ELENA_AGENT_LIMITS.pairingPollSeconds)
1029
+ });
1030
+ var elenaAgentPairingPollRequestSchema = z2.strictObject({
1031
+ version: elenaAgentVersionSchema,
1032
+ deviceCode: elenaAgentDeviceCodeSchema
1033
+ });
1034
+ var elenaAgentPairingReviewSchema = z2.strictObject({
1035
+ version: elenaAgentVersionSchema,
1036
+ label: elenaAgentNameSchema,
1037
+ userCode: elenaAgentUserCodeSchema,
1038
+ state: z2.enum(["pending", "approved", "rejected", "expired", "consumed"]),
1039
+ expiresAt: elenaAgentTimestampSchema
1040
+ });
1041
+ var elenaAgentPairingDecisionRequestSchema = z2.discriminatedUnion("decision", [
1042
+ z2.strictObject({
1043
+ version: elenaAgentVersionSchema,
1044
+ userCode: elenaAgentUserCodeSchema,
1045
+ decision: z2.literal("approve"),
1046
+ mode: elenaAgentModeSchema.default("read"),
1047
+ scopes: elenaAgentScopesSchema.default(["catalog:read", "assets:read"]),
1048
+ ttlSeconds: keyTtl
1049
+ }),
1050
+ z2.strictObject({
1051
+ version: elenaAgentVersionSchema,
1052
+ userCode: elenaAgentUserCodeSchema,
1053
+ decision: z2.literal("reject")
1054
+ })
1055
+ ]);
1056
+ var elenaAgentPairingPollResponseSchema = z2.discriminatedUnion("status", [
1057
+ z2.strictObject({
1058
+ version: elenaAgentVersionSchema,
1059
+ status: z2.literal("pending"),
1060
+ expiresAt: elenaAgentTimestampSchema,
1061
+ intervalSeconds: z2.literal(ELENA_AGENT_LIMITS.pairingPollSeconds)
1062
+ }),
1063
+ z2.strictObject({
1064
+ version: elenaAgentVersionSchema,
1065
+ status: z2.literal("delivered"),
1066
+ restaurant: elenaAgentRestaurantSchema,
1067
+ credential: elenaAgentKeyReceiptSchema.options[0]
1068
+ })
1069
+ ]);
1070
+ var elenaAgentContextSchema = z2.strictObject({
1071
+ version: elenaAgentVersionSchema,
1072
+ restaurant: elenaAgentRestaurantSchema,
1073
+ principal: z2.strictObject({
1074
+ kind: z2.literal("agent"),
1075
+ credentialId: elenaAgentIdSchema,
1076
+ credentialRevision: elenaAgentRevisionSchema,
1077
+ mode: elenaAgentModeSchema,
1078
+ scopes: elenaAgentScopesSchema,
1079
+ expiresAt: elenaAgentTimestampSchema
1080
+ }),
1081
+ capabilities: z2.strictObject({
1082
+ apiAccess: z2.literal(true),
1083
+ menu: z2.literal(true),
1084
+ imports: z2.boolean(),
1085
+ media: z2.boolean(),
1086
+ publication: z2.boolean()
1087
+ }),
1088
+ actions: z2.array(elenaAgentActionSchema).min(1).max(8).refine((actions) => new Set(actions).size === actions.length, "Duplicate action")
1089
+ }).superRefine((value, ctx) => {
1090
+ const requiredScopes = {
1091
+ "catalog.read": "catalog:read",
1092
+ "assets.read": "assets:read",
1093
+ "assets.stage": "assets:write",
1094
+ "operations.propose": "catalog:write",
1095
+ "operations.apply": "catalog:write",
1096
+ "catalog.publish": "catalog:publish"
1097
+ };
1098
+ const writes = {
1099
+ "assets.stage": true,
1100
+ "operations.propose": true,
1101
+ "operations.apply": true,
1102
+ "catalog.publish": true
1103
+ };
1104
+ for (const action of value.actions) {
1105
+ if (value.principal.mode === "read" && writes[action]) {
1106
+ ctx.addIssue({
1107
+ code: "custom",
1108
+ path: ["actions"],
1109
+ message: "Read mode cannot expose mutation actions"
1110
+ });
1111
+ }
1112
+ const scope = requiredScopes[action];
1113
+ if (scope && !value.principal.scopes.includes(scope)) {
1114
+ ctx.addIssue({
1115
+ code: "custom",
1116
+ path: ["actions"],
1117
+ message: "Action exceeds granted scopes"
1118
+ });
1119
+ }
1120
+ }
1121
+ if (value.actions.includes("catalog.publish") && !value.principal.scopes.includes("catalog:write")) {
1122
+ ctx.addIssue({
1123
+ code: "custom",
1124
+ path: ["actions"],
1125
+ message: "Publication also requires catalog:write"
1126
+ });
1127
+ }
1128
+ const expectedCapabilities = {
1129
+ imports: value.actions.includes("operations.propose") && value.actions.includes("operations.apply"),
1130
+ media: value.actions.includes("assets.stage"),
1131
+ publication: value.actions.includes("catalog.publish")
1132
+ };
1133
+ for (const capability of ["imports", "media", "publication"]) {
1134
+ if (value.capabilities[capability] !== expectedCapabilities[capability]) {
1135
+ ctx.addIssue({
1136
+ code: "custom",
1137
+ path: ["capabilities", capability],
1138
+ message: "Capability must match effective actions"
1139
+ });
1140
+ }
1141
+ }
1142
+ });
1143
+ var elenaClientContextSchema = z2.strictObject({
1144
+ version: elenaAgentVersionSchema,
1145
+ restaurant: elenaAgentRestaurantSchema,
1146
+ capabilities: z2.strictObject({ menu: z2.literal(true), assets: z2.literal(true) })
1147
+ });
1148
+ var elenaAgentCliAuthStatusSchema = z2.strictObject({
1149
+ version: elenaAgentVersionSchema,
1150
+ authenticated: z2.boolean(),
1151
+ source: z2.enum(["store", "environment", "none"]),
1152
+ baseUrl: elenaAgentBaseUrlSchema.nullable(),
1153
+ credentialId: elenaAgentIdSchema.nullable(),
1154
+ restaurantId: elenaAgentIdSchema.nullable(),
1155
+ expiresAt: elenaAgentTimestampSchema.nullable()
1156
+ });
1157
+ var elenaAgentCliLogoutSchema = z2.strictObject({
1158
+ version: elenaAgentVersionSchema,
1159
+ loggedOut: z2.literal(true),
1160
+ remoteRevoked: z2.literal(false),
1161
+ environmentCredentialPresent: z2.boolean()
1162
+ });
1163
+
1164
+ // ../shared/src/agent-contracts/catalog.ts
1165
+ import { z as z3 } from "zod";
1166
+ var elenaAgentExistingTargetSchema = z3.strictObject({
1167
+ kind: z3.literal("id"),
1168
+ id: elenaAgentIdSchema,
1169
+ expectedVersion: elenaAgentRevisionSchema
1170
+ });
1171
+ var elenaAgentExternalTargetSchema = z3.strictObject({
1172
+ kind: z3.literal("external"),
1173
+ namespace: elenaAgentRefSchema,
1174
+ key: elenaAgentRefSchema,
1175
+ expectedVersion: elenaAgentRevisionSchema.nullable()
1176
+ });
1177
+ var elenaAgentTargetSchema = z3.discriminatedUnion("kind", [
1178
+ elenaAgentExistingTargetSchema,
1179
+ elenaAgentExternalTargetSchema
1180
+ ]);
1181
+ var elenaAgentCategoryReferenceSchema = z3.discriminatedUnion("kind", [
1182
+ elenaAgentExistingTargetSchema,
1183
+ z3.strictObject({ kind: z3.literal("ref"), ref: elenaAgentRefSchema })
1184
+ ]);
1185
+ var elenaAgentDescriptionSchema = z3.discriminatedUnion("status", [
1186
+ z3.strictObject({ status: z3.literal("transcribed"), text: elenaAgentTextSchema }),
1187
+ z3.strictObject({ status: z3.literal("proposed"), text: elenaAgentTextSchema })
1188
+ ]);
1189
+ var elenaAgentConfirmedPriceSchema = z3.strictObject({
1190
+ status: z3.literal("confirmed"),
1191
+ amount: elenaAgentMoneySchema,
1192
+ basis: z3.literal("operator")
1193
+ });
1194
+ var elenaAgentDraftPriceSchema = z3.discriminatedUnion("status", [
1195
+ elenaAgentConfirmedPriceSchema,
1196
+ z3.strictObject({
1197
+ status: z3.literal("unconfirmed"),
1198
+ reason: z3.enum(["missing", "illegible"]),
1199
+ rawText: z3.string().max(100).nullable()
1200
+ })
1201
+ ]);
1202
+ var elenaAgentMediaReferenceSchema = z3.discriminatedUnion("kind", [
1203
+ z3.strictObject({
1204
+ kind: z3.literal("existing"),
1205
+ assetId: elenaAgentIdSchema,
1206
+ sha256: elenaAgentHashSchema
1207
+ }),
1208
+ z3.strictObject({
1209
+ kind: z3.literal("staged"),
1210
+ stagingId: elenaAgentIdSchema,
1211
+ sha256: elenaAgentHashSchema
1212
+ })
1213
+ ]);
1214
+ var elenaAgentGallerySchema = z3.array(elenaAgentMediaReferenceSchema).max(ELENA_AGENT_LIMITS.galleryImages).refine(
1215
+ (images) => new Set(images.map((image) => image.kind === "existing" ? image.assetId : image.stagingId)).size === images.length,
1216
+ "Duplicate media reference"
1217
+ );
1218
+ var elenaAgentExtraOptionSchema = z3.strictObject({
1219
+ name: elenaAgentNameSchema,
1220
+ additionalPrice: elenaAgentNonnegativeMoneySchema,
1221
+ order: z3.number().int().min(0).max(1e3)
1222
+ });
1223
+ var elenaAgentExtraGroupSchema = z3.strictObject({
1224
+ name: elenaAgentNameSchema,
1225
+ required: z3.boolean(),
1226
+ minSelections: z3.number().int().min(0).max(ELENA_AGENT_LIMITS.optionsPerGroup),
1227
+ maxSelections: z3.number().int().min(1).max(ELENA_AGENT_LIMITS.optionsPerGroup),
1228
+ order: z3.number().int().min(0).max(1e3),
1229
+ options: z3.array(elenaAgentExtraOptionSchema).min(1).max(ELENA_AGENT_LIMITS.optionsPerGroup)
1230
+ }).superRefine((group, ctx) => {
1231
+ if (group.minSelections > group.maxSelections || group.maxSelections > group.options.length) {
1232
+ ctx.addIssue({
1233
+ code: "custom",
1234
+ path: ["maxSelections"],
1235
+ message: "Invalid selection bounds"
1236
+ });
1237
+ }
1238
+ if (group.required && group.minSelections < 1 || !group.required && group.minSelections !== 0) {
1239
+ ctx.addIssue({
1240
+ code: "custom",
1241
+ path: ["minSelections"],
1242
+ message: "Required must match minimum selection count"
1243
+ });
1244
+ }
1245
+ if (new Set(
1246
+ group.options.map((option) => option.name.normalize("NFKC").trim().toLocaleLowerCase("es"))
1247
+ ).size !== group.options.length) {
1248
+ ctx.addIssue({ code: "custom", path: ["options"], message: "Duplicate option name" });
1249
+ }
1250
+ });
1251
+ var elenaAgentExtrasSchema = z3.strictObject({
1252
+ confirmed: z3.literal(true),
1253
+ groups: z3.array(elenaAgentExtraGroupSchema).max(ELENA_AGENT_LIMITS.extraGroups)
1254
+ }).refine(
1255
+ (extras) => new Set(
1256
+ extras.groups.map((group) => group.name.normalize("NFKC").trim().toLocaleLowerCase("es"))
1257
+ ).size === extras.groups.length,
1258
+ "Duplicate group name"
1259
+ );
1260
+ var elenaAgentCategoryDataSchema = z3.strictObject({
1261
+ name: elenaAgentNameSchema,
1262
+ description: elenaAgentDescriptionSchema.nullable(),
1263
+ order: z3.number().int().min(0).max(1e4)
1264
+ });
1265
+ var productFields = {
1266
+ type: z3.literal("menu"),
1267
+ name: elenaAgentNameSchema,
1268
+ description: elenaAgentDescriptionSchema.nullable(),
1269
+ category: elenaAgentCategoryReferenceSchema,
1270
+ available: z3.boolean(),
1271
+ images: elenaAgentGallerySchema,
1272
+ extras: elenaAgentExtrasSchema
1273
+ };
1274
+ var elenaAgentProductDataSchema = z3.strictObject({
1275
+ ...productFields,
1276
+ price: elenaAgentConfirmedPriceSchema
1277
+ });
1278
+ var elenaAgentDraftProductDataSchema = z3.strictObject({
1279
+ ...productFields,
1280
+ price: elenaAgentDraftPriceSchema
1281
+ });
1282
+ var elenaAgentCategoryChangeSchema = z3.strictObject({
1283
+ kind: z3.literal("category.upsert"),
1284
+ ref: elenaAgentRefSchema,
1285
+ target: elenaAgentTargetSchema,
1286
+ data: elenaAgentCategoryDataSchema
1287
+ });
1288
+ var elenaAgentProductChangeSchema = z3.strictObject({
1289
+ kind: z3.literal("product.upsert"),
1290
+ ref: elenaAgentRefSchema,
1291
+ target: elenaAgentTargetSchema,
1292
+ data: elenaAgentProductDataSchema
1293
+ });
1294
+ var elenaAgentChangeSchema = z3.discriminatedUnion("kind", [
1295
+ elenaAgentCategoryChangeSchema,
1296
+ elenaAgentProductChangeSchema
1297
+ ]);
1298
+ var elenaAgentCategorySchema = z3.strictObject({
1299
+ id: elenaAgentIdSchema,
1300
+ revision: elenaAgentRevisionSchema,
1301
+ name: elenaAgentNameSchema,
1302
+ description: elenaAgentTextSchema.nullable(),
1303
+ order: z3.number().int().min(0).max(1e4)
1304
+ });
1305
+ var elenaAgentProductSchema = z3.strictObject({
1306
+ id: elenaAgentIdSchema,
1307
+ revision: elenaAgentRevisionSchema,
1308
+ type: z3.literal("menu"),
1309
+ name: elenaAgentNameSchema,
1310
+ basePrice: elenaAgentMoneySchema,
1311
+ description: elenaAgentTextSchema.nullable(),
1312
+ categoryId: elenaAgentIdSchema,
1313
+ available: z3.boolean(),
1314
+ publishedToMenu: z3.boolean(),
1315
+ isDraft: z3.boolean(),
1316
+ images: z3.array(z3.strictObject({ assetId: elenaAgentIdSchema, sha256: elenaAgentHashSchema })).max(ELENA_AGENT_LIMITS.galleryImages),
1317
+ /** Present on current native reads; optional only for retained v1 snapshots. */
1318
+ primaryImageId: elenaAgentIdSchema.nullable().optional(),
1319
+ extraGroups: z3.array(elenaAgentExtraGroupSchema).max(ELENA_AGENT_LIMITS.extraGroups)
1320
+ });
1321
+ var elenaAgentCategoryListSchema = z3.strictObject({
1322
+ version: elenaAgentVersionSchema,
1323
+ categories: z3.array(elenaAgentCategorySchema).max(ELENA_AGENT_LIMITS.pageSize),
1324
+ nextCursor: z3.string().regex(/^[a-zA-Z0-9_-]{1,512}$/).nullable()
1325
+ });
1326
+ var elenaAgentProductListSchema = z3.strictObject({
1327
+ version: elenaAgentVersionSchema,
1328
+ products: z3.array(elenaAgentProductSchema).max(ELENA_AGENT_LIMITS.pageSize),
1329
+ nextCursor: z3.string().regex(/^[a-zA-Z0-9_-]{1,512}$/).nullable()
1330
+ });
1331
+ var elenaAgentProductResponseSchema = z3.strictObject({
1332
+ version: elenaAgentVersionSchema,
1333
+ product: elenaAgentProductSchema,
1334
+ /** Authoritative editing metadata; never infer primary image from gallery order. */
1335
+ editable: z3.strictObject({
1336
+ restaurantId: elenaAgentIdSchema,
1337
+ productId: elenaAgentIdSchema,
1338
+ imageId: elenaAgentIdSchema.nullable(),
1339
+ categoryRevision: elenaAgentRevisionSchema,
1340
+ primaryImage: z3.strictObject({ assetId: elenaAgentIdSchema, sha256: elenaAgentHashSchema }).nullable().optional(),
1341
+ imageEditorUrl: elenaAgentHttpsUrlSchema.optional()
1342
+ }).optional()
1343
+ });
1344
+ var elenaAgentCategoryResponseSchema = z3.strictObject({
1345
+ version: elenaAgentVersionSchema,
1346
+ category: elenaAgentCategorySchema
1347
+ });
1348
+ var elenaAgentMediaStageRequestSchema = z3.strictObject({
1349
+ version: elenaAgentVersionSchema,
1350
+ idempotencyKey: elenaAgentRefSchema,
1351
+ fileName: z3.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,119}\.(?:jpg|jpeg|png|webp)$/),
1352
+ mimeType: z3.enum(["image/jpeg", "image/png", "image/webp"]),
1353
+ bytes: z3.number().int().min(1).max(ELENA_AGENT_LIMITS.mediaBytes),
1354
+ sha256: elenaAgentHashSchema
1355
+ });
1356
+ var elenaAgentMediaStagedSchema = z3.strictObject({
1357
+ version: elenaAgentVersionSchema,
1358
+ stagingId: elenaAgentIdSchema,
1359
+ sha256: elenaAgentHashSchema,
1360
+ mimeType: z3.enum(["image/jpeg", "image/png", "image/webp"]),
1361
+ bytes: z3.number().int().min(1).max(ELENA_AGENT_LIMITS.mediaBytes),
1362
+ expiresAt: elenaAgentTimestampSchema
1363
+ });
1364
+ var elenaAgentAssetSchema = z3.strictObject({
1365
+ version: elenaAgentVersionSchema,
1366
+ assetId: elenaAgentIdSchema,
1367
+ sha256: elenaAgentHashSchema,
1368
+ mimeType: z3.enum(["image/jpeg", "image/png", "image/webp"]),
1369
+ bytes: z3.number().int().min(1).max(ELENA_AGENT_LIMITS.mediaBytes)
1370
+ });
1371
+ var elenaClientAssetSchema = z3.strictObject({
1372
+ assetId: elenaAgentIdSchema,
1373
+ url: elenaAgentHttpsUrlSchema
1374
+ });
1375
+ var elenaClientCategorySchema = z3.strictObject({
1376
+ id: elenaAgentIdSchema,
1377
+ name: elenaAgentNameSchema,
1378
+ description: elenaAgentTextSchema.nullable(),
1379
+ order: z3.number().int().min(0).max(1e4)
1380
+ });
1381
+ var elenaClientProductSchema = z3.strictObject({
1382
+ id: elenaAgentIdSchema,
1383
+ type: z3.literal("menu"),
1384
+ name: elenaAgentNameSchema,
1385
+ basePrice: elenaAgentMoneySchema,
1386
+ description: elenaAgentTextSchema.nullable(),
1387
+ categoryId: elenaAgentIdSchema,
1388
+ available: z3.boolean(),
1389
+ images: z3.array(elenaClientAssetSchema).max(ELENA_AGENT_LIMITS.galleryImages),
1390
+ extraGroups: z3.array(elenaAgentExtraGroupSchema).max(ELENA_AGENT_LIMITS.extraGroups)
1391
+ });
1392
+ var elenaClientCatalogSchema = z3.strictObject({
1393
+ version: elenaAgentVersionSchema,
1394
+ categories: z3.array(elenaClientCategorySchema).max(ELENA_AGENT_LIMITS.pageSize),
1395
+ products: z3.array(elenaClientProductSchema).max(ELENA_AGENT_LIMITS.pageSize),
1396
+ nextCursor: z3.string().regex(/^[a-zA-Z0-9_-]{1,512}$/).nullable()
1397
+ });
1398
+ var elenaClientProductResponseSchema = z3.strictObject({
1399
+ version: elenaAgentVersionSchema,
1400
+ product: elenaClientProductSchema
1401
+ });
1402
+ var elenaClientAssetResponseSchema = z3.strictObject({
1403
+ version: elenaAgentVersionSchema,
1404
+ asset: elenaClientAssetSchema
1405
+ });
1406
+
1407
+ // src/key-transport.ts
1408
+ var ElenaAgentTransportError = class extends Error {
1409
+ constructor(code, status = 0, retryable = false, requestId = null, retryAfterSeconds) {
1410
+ super(`Elena request failed (${code}).`);
1411
+ this.code = code;
1412
+ this.status = status;
1413
+ this.retryable = retryable;
1414
+ this.requestId = requestId;
1415
+ this.retryAfterSeconds = retryAfterSeconds;
1416
+ }
1417
+ code;
1418
+ status;
1419
+ retryable;
1420
+ requestId;
1421
+ retryAfterSeconds;
1422
+ name = "ElenaAgentTransportError";
1423
+ };
1424
+ function parseWire(schema, value, response = false) {
1425
+ let unsupportedVersion = false;
1426
+ try {
1427
+ if (value !== null && typeof value === "object" && "version" in value) {
1428
+ unsupportedVersion = value.version !== void 0 && value.version !== "1";
1429
+ }
1430
+ const parsed = schema.safeParse(value);
1431
+ if (parsed.success) return parsed.data;
1432
+ } catch {
1433
+ }
1434
+ throw new ElenaAgentTransportError(
1435
+ unsupportedVersion ? "UNSUPPORTED_VERSION" : response ? "INVALID_RESPONSE" : "INVALID_REQUEST"
1436
+ );
1437
+ }
1438
+ function assertOptionKeys(value, keys) {
1439
+ if (!value || typeof value !== "object" || Object.keys(value).some((key) => !keys.includes(key))) {
1440
+ throw new ElenaAgentTransportError("INVALID_REQUEST");
1441
+ }
1442
+ }
1443
+ function explicitOrigin(value, allowTestHttp = false) {
1444
+ try {
1445
+ if (typeof value !== "string" || !/^https?:\/\/[^/?#@\\\s]+\/?$/.test(value)) throw 0;
1446
+ if (/elena_(?:sk|pk)_/i.test(value)) throw 0;
1447
+ const url = new URL(value);
1448
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
1449
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && (typeof process !== "undefined" && process.env.NODE_ENV === "development" || allowTestHttp && loopback)))
1450
+ throw 0;
1451
+ if (url.username || url.password || url.search || url.hash || url.pathname !== "/") throw 0;
1452
+ return url.origin;
1453
+ } catch {
1454
+ throw new ElenaAgentTransportError("ORIGIN_REJECTED");
1455
+ }
1456
+ }
1457
+ function resourceId(value) {
1458
+ return parseWire(elenaAgentIdSchema, value);
1459
+ }
1460
+ function pageQuery(value = { version: "1" }) {
1461
+ const query = parseWire(elenaAgentPageQuerySchema, value);
1462
+ const params = new URLSearchParams({ version: query.version, limit: String(query.limit) });
1463
+ if (query.cursor !== void 0) params.set("cursor", query.cursor);
1464
+ return `?${params}`;
1465
+ }
1466
+ function timeoutValue(value) {
1467
+ if (!Number.isInteger(value) || value < 1 || value > 3e5) {
1468
+ throw new ElenaAgentTransportError("INVALID_REQUEST");
1469
+ }
1470
+ return value;
1471
+ }
1472
+ var privateCredential = /elena_sk_[A-Za-z0-9_-]{43}/;
1473
+ var retryCodes = {
1474
+ RATE_LIMITED: true,
1475
+ TEMPORARY_UNAVAILABLE: true
1476
+ };
1477
+ function createKeyTransport(options, header, credential) {
1478
+ if (options.testing !== void 0) {
1479
+ assertOptionKeys(options.testing, ["allowHttp"]);
1480
+ if (options.testing.allowHttp !== true || !options.fetch) {
1481
+ throw new ElenaAgentTransportError("INVALID_REQUEST");
1482
+ }
1483
+ }
1484
+ const baseUrl = explicitOrigin(options.baseUrl, options.testing?.allowHttp === true);
1485
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1486
+ if (typeof fetchImpl !== "function") throw new ElenaAgentTransportError("INVALID_REQUEST");
1487
+ const defaultTimeout = timeoutValue(options.timeoutMs ?? 3e4);
1488
+ const retries = options.maxRetries ?? 2;
1489
+ if (!Number.isInteger(retries) || retries < 0 || retries > 2) {
1490
+ throw new ElenaAgentTransportError("INVALID_REQUEST");
1491
+ }
1492
+ async function request(path, schema, requestOptions = {}, body, stage = false, retryAmbiguous = true) {
1493
+ assertOptionKeys(requestOptions, ["signal", "timeoutMs"]);
1494
+ if (privateCredential.test(path) || credential && path.includes(credential) || typeof body === "string" && (privateCredential.test(body) || credential && body.includes(credential))) {
1495
+ throw new ElenaAgentTransportError("INVALID_REQUEST");
1496
+ }
1497
+ if (requestOptions.signal !== void 0 && !(requestOptions.signal instanceof AbortSignal)) {
1498
+ throw new ElenaAgentTransportError("INVALID_REQUEST");
1499
+ }
1500
+ const timeoutMs = timeoutValue(requestOptions.timeoutMs ?? (stage ? 12e4 : defaultTimeout));
1501
+ if (requestOptions.signal?.aborted) throw new ElenaAgentTransportError("ABORTED");
1502
+ const controller = new AbortController();
1503
+ let timedOut = false;
1504
+ const timer = setTimeout(() => {
1505
+ timedOut = true;
1506
+ controller.abort();
1507
+ }, timeoutMs);
1508
+ const onAbort = () => controller.abort();
1509
+ requestOptions.signal?.addEventListener("abort", onAbort, { once: true });
1510
+ const abortError = () => new ElenaAgentTransportError(timedOut ? "TIMEOUT" : "ABORTED");
1511
+ const deadline = Date.now() + timeoutMs;
1512
+ const bounded = (promise) => new Promise((resolve, reject) => {
1513
+ const abort = () => {
1514
+ cleanup();
1515
+ reject(abortError());
1516
+ };
1517
+ const cleanup = () => controller.signal.removeEventListener("abort", abort);
1518
+ if (controller.signal.aborted) {
1519
+ reject(abortError());
1520
+ return;
1521
+ }
1522
+ controller.signal.addEventListener("abort", abort, { once: true });
1523
+ promise.then(
1524
+ (value) => {
1525
+ cleanup();
1526
+ resolve(value);
1527
+ },
1528
+ (error) => {
1529
+ cleanup();
1530
+ reject(error);
1531
+ }
1532
+ );
1533
+ });
1534
+ const delay = (ms) => new Promise((resolve, reject) => {
1535
+ const abort = () => {
1536
+ clearTimeout(wait);
1537
+ controller.signal.removeEventListener("abort", abort);
1538
+ reject(abortError());
1539
+ };
1540
+ const wait = setTimeout(() => {
1541
+ controller.signal.removeEventListener("abort", abort);
1542
+ resolve();
1543
+ }, ms);
1544
+ controller.signal.addEventListener("abort", abort, { once: true });
1545
+ if (controller.signal.aborted) abort();
1546
+ });
1547
+ try {
1548
+ for (let attempt = 0; ; attempt++) {
1549
+ let failure;
1550
+ let retryDelay = 100 * 2 ** attempt;
1551
+ try {
1552
+ const headers = new Headers({ Accept: "application/json" });
1553
+ if (header) headers.set(header, credential);
1554
+ if (typeof body === "string") headers.set("Content-Type", "application/json");
1555
+ const response = await bounded(
1556
+ Promise.resolve().then(() => {
1557
+ if (controller.signal.aborted) throw abortError();
1558
+ return fetchImpl(`${baseUrl}${path}`, {
1559
+ method: body === void 0 ? "GET" : "POST",
1560
+ headers,
1561
+ body: typeof body === "function" ? body() : body,
1562
+ credentials: "omit",
1563
+ redirect: "error",
1564
+ referrerPolicy: "no-referrer",
1565
+ cache: "no-store",
1566
+ signal: controller.signal
1567
+ });
1568
+ }).catch(() => {
1569
+ throw new ElenaAgentTransportError("NETWORK_ERROR", 0, retryAmbiguous);
1570
+ })
1571
+ );
1572
+ if (response.redirected || response.status >= 300 && response.status < 400) {
1573
+ throw new ElenaAgentTransportError("REDIRECT_REJECTED", response.status);
1574
+ }
1575
+ if (response.url && new URL(response.url).origin !== baseUrl) {
1576
+ throw new ElenaAgentTransportError("ORIGIN_REJECTED");
1577
+ }
1578
+ const text = await bounded(
1579
+ Promise.resolve().then(() => response.text()).catch(() => {
1580
+ throw new ElenaAgentTransportError("NETWORK_ERROR", 0, retryAmbiguous);
1581
+ })
1582
+ );
1583
+ if (privateCredential.test(text) || credential && text.includes(credential)) {
1584
+ throw new ElenaAgentTransportError("INVALID_RESPONSE", response.status);
1585
+ }
1586
+ let payload;
1587
+ try {
1588
+ payload = JSON.parse(text);
1589
+ } catch {
1590
+ throw new ElenaAgentTransportError("INVALID_RESPONSE", response.status);
1591
+ }
1592
+ const decodedText = JSON.stringify(payload);
1593
+ if (privateCredential.test(decodedText) || credential && decodedText.includes(credential)) {
1594
+ throw new ElenaAgentTransportError("INVALID_RESPONSE", response.status);
1595
+ }
1596
+ if (response.ok) return parseWire(schema, payload, true);
1597
+ const wire = parseWire(elenaAgentErrorSchema, payload, true).error;
1598
+ const retryable = retryCodes[wire.code] === true && wire.retryable && (wire.code === "RATE_LIMITED" && response.status === 429 || wire.code === "TEMPORARY_UNAVAILABLE" && response.status === 503);
1599
+ failure = new ElenaAgentTransportError(
1600
+ wire.code,
1601
+ response.status,
1602
+ retryable,
1603
+ wire.requestId,
1604
+ wire.retryAfterSeconds
1605
+ );
1606
+ const retryAfter = response.headers.get("retry-after");
1607
+ let headerDelay = 0;
1608
+ if (retryAfter !== null) {
1609
+ const seconds = /^\d+$/.test(retryAfter) ? Number(retryAfter) : (Date.parse(retryAfter) - Date.now()) / 1e3;
1610
+ if (!Number.isFinite(seconds) || seconds < 0 || seconds > 3600) {
1611
+ throw new ElenaAgentTransportError(
1612
+ wire.code,
1613
+ response.status,
1614
+ false,
1615
+ wire.requestId,
1616
+ wire.retryAfterSeconds
1617
+ );
1618
+ }
1619
+ headerDelay = Math.ceil(seconds * 1e3);
1620
+ }
1621
+ retryDelay = Math.max(retryDelay, headerDelay, (wire.retryAfterSeconds ?? 0) * 1e3);
1622
+ } catch (error) {
1623
+ if (controller.signal.aborted) throw abortError();
1624
+ failure = error instanceof ElenaAgentTransportError ? error : new ElenaAgentTransportError("NETWORK_ERROR", 0, retryAmbiguous);
1625
+ }
1626
+ if (!failure.retryable || attempt >= retries || Date.now() + retryDelay >= deadline)
1627
+ throw failure;
1628
+ await delay(retryDelay);
1629
+ }
1630
+ } finally {
1631
+ clearTimeout(timer);
1632
+ requestOptions.signal?.removeEventListener("abort", onAbort);
1633
+ }
1634
+ }
1635
+ return { request };
1636
+ }
1637
+
1638
+ // src/client-key.ts
1639
+ function createElenaClientKeyClient(options) {
1640
+ assertOptionKeys(options, [
1641
+ "baseUrl",
1642
+ "clientKey",
1643
+ "fetch",
1644
+ "timeoutMs",
1645
+ "maxRetries",
1646
+ "testing"
1647
+ ]);
1648
+ if (!elenaClientKeySchema.safeParse(options.clientKey).success) {
1649
+ throw new ElenaAgentTransportError("CREDENTIAL_WRONG_FAMILY");
1650
+ }
1651
+ const { request } = createKeyTransport(options, "x-elena-client-key", options.clientKey);
1652
+ const root = "/api/v1/public/sdk";
1653
+ return Object.freeze({
1654
+ context: Object.freeze({
1655
+ get: (options2) => request(`${root}/context`, elenaClientContextSchema, options2)
1656
+ }),
1657
+ catalog: Object.freeze({
1658
+ list: (query, options2) => request(`${root}/catalog${pageQuery(query)}`, elenaClientCatalogSchema, options2),
1659
+ getProduct: async (id, options2) => {
1660
+ const expectedId = resourceId(id);
1661
+ const result = await request(
1662
+ `${root}/catalog/products/${expectedId}`,
1663
+ elenaClientProductResponseSchema,
1664
+ options2
1665
+ );
1666
+ if (result.product.id !== expectedId)
1667
+ throw new ElenaAgentTransportError("INVALID_RESPONSE");
1668
+ return result;
1669
+ }
1670
+ }),
1671
+ assets: Object.freeze({
1672
+ get: async (id, options2) => {
1673
+ const expectedId = resourceId(id);
1674
+ const result = await request(
1675
+ `${root}/assets/${expectedId}`,
1676
+ elenaClientAssetResponseSchema,
1677
+ options2
1678
+ );
1679
+ if (result.asset.assetId !== expectedId)
1680
+ throw new ElenaAgentTransportError("INVALID_RESPONSE");
1681
+ return result;
1682
+ }
1683
+ })
1684
+ });
1685
+ }
1686
+
1687
+ // ../shared/src/agent-contracts/public-connection.ts
1688
+ import { z as z4 } from "zod";
1689
+ var ELENA_PUBLIC_CONNECTION_DISCOVERY_ROOT = "/api/v1/public/catalog-connections";
1690
+ var elenaPublicConnectionSlugSchema = z4.string().min(1).max(120).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);
1691
+ var ttl = z4.number().int().min(3600).max(ELENA_AGENT_LIMITS.keyMaxTtlSeconds).default(ELENA_AGENT_LIMITS.keyDefaultTtlSeconds);
1692
+ var mutation = {
1693
+ version: elenaAgentVersionSchema,
1694
+ // Zero asserts that no connection has ever been created for this restaurant.
1695
+ expectedRevision: z4.number().int().min(0).max(Number.MAX_SAFE_INTEGER - 1)
1696
+ };
1697
+ var elenaPublicConnectionMutationSchema = z4.discriminatedUnion("action", [
1698
+ z4.strictObject({ ...mutation, action: z4.literal("enable"), ttlSeconds: ttl }),
1699
+ z4.strictObject({ ...mutation, action: z4.literal("renew"), ttlSeconds: ttl }),
1700
+ z4.strictObject({ ...mutation, action: z4.literal("disable") }),
1701
+ z4.strictObject({ ...mutation, action: z4.literal("revoke") })
1702
+ ]);
1703
+ var elenaPublicConnectionMetadataSchema = z4.strictObject({
1704
+ revision: elenaAgentRevisionSchema,
1705
+ status: z4.enum(["published", "disabled", "revoked"]),
1706
+ expiresAt: elenaAgentTimestampSchema
1707
+ });
1708
+ var elenaPublicConnectionManagementSchema = z4.strictObject({
1709
+ version: elenaAgentVersionSchema,
1710
+ publication: elenaPublicConnectionMetadataSchema.nullable()
1711
+ });
1712
+ var elenaPublicConnectionSchema = z4.strictObject({
1713
+ version: elenaAgentVersionSchema,
1714
+ apiOrigin: elenaAgentBaseUrlSchema,
1715
+ clientKey: elenaManagedClientKeySchema,
1716
+ publication: z4.strictObject({
1717
+ revision: elenaAgentRevisionSchema,
1718
+ expiresAt: elenaAgentTimestampSchema
1719
+ }),
1720
+ context: elenaClientContextSchema
1721
+ });
1722
+
1723
+ // src/public-connection.ts
1724
+ async function discoverElenaPublicConnection(options, slug, requestOptions) {
1725
+ assertOptionKeys(options, ["baseUrl", "fetch", "timeoutMs", "maxRetries", "testing"]);
1726
+ const identifier = parseWire(elenaPublicConnectionSlugSchema, slug);
1727
+ const result = await createKeyTransport(options, null, "").request(
1728
+ `${ELENA_PUBLIC_CONNECTION_DISCOVERY_ROOT}/${identifier}`,
1729
+ elenaPublicConnectionSchema,
1730
+ requestOptions
1731
+ );
1732
+ if (explicitOrigin(result.apiOrigin) !== explicitOrigin(options.baseUrl, options.testing?.allowHttp) || result.context.restaurant.slug !== identifier)
1733
+ throw new ElenaAgentTransportError("ORIGIN_REJECTED");
1734
+ return result;
1735
+ }
1736
+ export {
1737
+ AccessResource,
1738
+ AssetsResource,
1739
+ BusinessScope,
1740
+ CatalogResource,
1741
+ ChatResource,
1742
+ ContinuationsResource,
1743
+ ElenaAgentTransportError,
1744
+ ElenaApiError,
1745
+ ElenaCheckoutValidationError,
1746
+ ElenaHttpClient,
1747
+ ElenaNetworkError,
1748
+ ElenaRequestAbortedError,
1749
+ ElenaResponseError,
1750
+ ElenaTimeoutError,
1751
+ EventsResource,
1752
+ JourneyConflictError,
1753
+ JourneyResource,
1754
+ OrdersResource,
1755
+ PresenceResource,
1756
+ RealtimeResource,
1757
+ ReceiptsResource,
1758
+ TablesResource,
1759
+ TicketsResource,
1760
+ buildOrderItems,
1761
+ createElenaClientKeyClient,
1762
+ createElenaJourneyClient,
1763
+ createElenaPublicClient,
1764
+ discoverElenaPublicConnection
1765
+ };
1766
+ //# sourceMappingURL=index.js.map