@tiquo/dom-package 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,637 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ TiquoAuth: () => TiquoAuth,
24
+ TiquoAuthError: () => TiquoAuthError,
25
+ default: () => index_default,
26
+ useTiquoAuth: () => useTiquoAuth
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+ var TiquoAuthError = class extends Error {
30
+ constructor(message, code, statusCode) {
31
+ super(message);
32
+ this.code = code;
33
+ this.statusCode = statusCode;
34
+ this.name = "TiquoAuthError";
35
+ }
36
+ };
37
+ var TiquoAuth = class {
38
+ constructor(config) {
39
+ this.sessionToken = null;
40
+ this.session = null;
41
+ this.listeners = /* @__PURE__ */ new Set();
42
+ this.refreshTimer = null;
43
+ // Multi-tab sync
44
+ this.broadcastChannel = null;
45
+ this.isProcessingTabSync = false;
46
+ if (!config.publicKey) {
47
+ throw new TiquoAuthError("publicKey is required", "MISSING_PUBLIC_KEY");
48
+ }
49
+ if (!config.publicKey.startsWith("pk_dom_")) {
50
+ throw new TiquoAuthError(
51
+ "Invalid public key format. Expected pk_dom_xxx",
52
+ "INVALID_PUBLIC_KEY"
53
+ );
54
+ }
55
+ this.config = {
56
+ publicKey: config.publicKey,
57
+ apiEndpoint: config.apiEndpoint || "https://edge.tiquo.app",
58
+ storagePrefix: config.storagePrefix || "tiquo_auth_",
59
+ debug: config.debug || false,
60
+ enableTabSync: config.enableTabSync !== false
61
+ // Default true
62
+ };
63
+ this.tabId = this.generateTabId();
64
+ if (this.config.enableTabSync) {
65
+ this.initTabSync();
66
+ }
67
+ this.restoreSession();
68
+ }
69
+ // ============================================
70
+ // PUBLIC METHODS
71
+ // ============================================
72
+ /**
73
+ * Send an OTP verification code to the user's email
74
+ */
75
+ async sendOTP(email) {
76
+ this.log("Sending OTP to:", email);
77
+ const response = await this.request("/api/auth-dom/otp/send", {
78
+ method: "POST",
79
+ body: JSON.stringify({
80
+ publicKey: this.config.publicKey,
81
+ email
82
+ })
83
+ });
84
+ if (!response.ok) {
85
+ const error = await response.json().catch(() => ({ error: "Failed to send OTP" }));
86
+ throw new TiquoAuthError(error.error || "Failed to send OTP", "OTP_SEND_FAILED", response.status);
87
+ }
88
+ const result = await response.json();
89
+ return {
90
+ success: true,
91
+ message: result.message || "OTP sent"
92
+ };
93
+ }
94
+ /**
95
+ * Verify an OTP code and authenticate the user
96
+ */
97
+ async verifyOTP(email, otp) {
98
+ this.log("Verifying OTP for:", email);
99
+ const response = await this.request("/api/auth-dom/otp/verify", {
100
+ method: "POST",
101
+ body: JSON.stringify({
102
+ publicKey: this.config.publicKey,
103
+ email,
104
+ otp
105
+ })
106
+ });
107
+ if (!response.ok) {
108
+ const error = await response.json().catch(() => ({ error: "Invalid OTP" }));
109
+ throw new TiquoAuthError(error.error || "Invalid OTP", "OTP_VERIFY_FAILED", response.status);
110
+ }
111
+ const result = await response.json();
112
+ this.sessionToken = result.sessionToken;
113
+ this.saveSession();
114
+ await this.refreshSession();
115
+ this.broadcastTabSync("LOGIN");
116
+ return {
117
+ success: true,
118
+ sessionToken: result.sessionToken,
119
+ expiresAt: result.expiresAt,
120
+ isNewUser: result.isNewUser,
121
+ hasCustomer: result.hasCustomer
122
+ };
123
+ }
124
+ /**
125
+ * Get the current authenticated user and customer data
126
+ */
127
+ async getUser() {
128
+ if (!this.sessionToken) {
129
+ return null;
130
+ }
131
+ if (this.session && this.session.expiresAt > Date.now()) {
132
+ return this.session;
133
+ }
134
+ return this.refreshSession();
135
+ }
136
+ /**
137
+ * Check if user is currently authenticated
138
+ */
139
+ isAuthenticated() {
140
+ return !!this.sessionToken && (!this.session || this.session.expiresAt > Date.now());
141
+ }
142
+ /**
143
+ * Update the authenticated customer's profile
144
+ * Only allows updating the logged-in customer's own data
145
+ */
146
+ async updateProfile(updates) {
147
+ if (!this.sessionToken) {
148
+ throw new TiquoAuthError("Not authenticated", "NOT_AUTHENTICATED");
149
+ }
150
+ this.log("Updating customer profile:", updates);
151
+ const response = await this.request("/api/auth-dom/profile", {
152
+ method: "PATCH",
153
+ body: JSON.stringify({
154
+ publicKey: this.config.publicKey,
155
+ sessionToken: this.sessionToken,
156
+ updates
157
+ })
158
+ });
159
+ if (!response.ok) {
160
+ const error = await response.json().catch(() => ({ error: "Failed to update profile" }));
161
+ throw new TiquoAuthError(error.error || "Failed to update profile", "PROFILE_UPDATE_FAILED", response.status);
162
+ }
163
+ const result = await response.json();
164
+ if (this.session && result.customer) {
165
+ this.session = {
166
+ ...this.session,
167
+ customer: {
168
+ id: result.customer.id,
169
+ firstName: result.customer.firstName,
170
+ lastName: result.customer.lastName,
171
+ displayName: result.customer.displayName,
172
+ customerNumber: result.customer.customerNumber,
173
+ email: result.customer.email,
174
+ phone: result.customer.phone
175
+ }
176
+ };
177
+ this.notifyListeners();
178
+ this.broadcastTabSync("SESSION_UPDATE");
179
+ }
180
+ return result;
181
+ }
182
+ /**
183
+ * Log out the current user
184
+ */
185
+ async logout() {
186
+ this.log("Logging out");
187
+ if (this.sessionToken) {
188
+ try {
189
+ await this.request("/api/auth-dom/logout", {
190
+ method: "POST",
191
+ body: JSON.stringify({
192
+ publicKey: this.config.publicKey,
193
+ sessionToken: this.sessionToken
194
+ })
195
+ });
196
+ } catch (error) {
197
+ this.log("Logout API error:", error);
198
+ }
199
+ }
200
+ this.broadcastTabSync("LOGOUT");
201
+ this.clearSession();
202
+ }
203
+ /**
204
+ * Subscribe to authentication state changes
205
+ */
206
+ onAuthStateChange(callback) {
207
+ this.listeners.add(callback);
208
+ callback(this.session);
209
+ return () => {
210
+ this.listeners.delete(callback);
211
+ };
212
+ }
213
+ /**
214
+ * Get the authenticated customer's order history
215
+ * Only returns orders for the logged-in customer
216
+ */
217
+ async getOrders(options) {
218
+ if (!this.sessionToken) {
219
+ throw new TiquoAuthError("Not authenticated", "NOT_AUTHENTICATED");
220
+ }
221
+ this.log("Fetching customer orders:", options);
222
+ const url = new URL(`${this.config.apiEndpoint}/api/auth-dom/orders`);
223
+ url.searchParams.set("publicKey", this.config.publicKey);
224
+ if (options?.limit) {
225
+ url.searchParams.set("limit", options.limit.toString());
226
+ }
227
+ if (options?.cursor) {
228
+ url.searchParams.set("cursor", options.cursor);
229
+ }
230
+ if (options?.status) {
231
+ url.searchParams.set("status", options.status);
232
+ }
233
+ const response = await fetch(url.toString(), {
234
+ method: "GET",
235
+ headers: {
236
+ "Authorization": `Bearer ${this.sessionToken}`
237
+ },
238
+ credentials: "include"
239
+ });
240
+ if (!response.ok) {
241
+ const error = await response.json().catch(() => ({ error: "Failed to get orders" }));
242
+ throw new TiquoAuthError(error.error || "Failed to get orders", "GET_ORDERS_FAILED", response.status);
243
+ }
244
+ return response.json();
245
+ }
246
+ /**
247
+ * Get the authenticated customer's booking history
248
+ * Only returns bookings for the logged-in customer
249
+ */
250
+ async getBookings(options) {
251
+ if (!this.sessionToken) {
252
+ throw new TiquoAuthError("Not authenticated", "NOT_AUTHENTICATED");
253
+ }
254
+ this.log("Fetching customer bookings:", options);
255
+ const url = new URL(`${this.config.apiEndpoint}/api/auth-dom/bookings`);
256
+ url.searchParams.set("publicKey", this.config.publicKey);
257
+ if (options?.limit) {
258
+ url.searchParams.set("limit", options.limit.toString());
259
+ }
260
+ if (options?.cursor) {
261
+ url.searchParams.set("cursor", options.cursor);
262
+ }
263
+ if (options?.status) {
264
+ url.searchParams.set("status", options.status);
265
+ }
266
+ if (options?.upcoming) {
267
+ url.searchParams.set("upcoming", "true");
268
+ }
269
+ const response = await fetch(url.toString(), {
270
+ method: "GET",
271
+ headers: {
272
+ "Authorization": `Bearer ${this.sessionToken}`
273
+ },
274
+ credentials: "include"
275
+ });
276
+ if (!response.ok) {
277
+ const error = await response.json().catch(() => ({ error: "Failed to get bookings" }));
278
+ throw new TiquoAuthError(error.error || "Failed to get bookings", "GET_BOOKINGS_FAILED", response.status);
279
+ }
280
+ return response.json();
281
+ }
282
+ /**
283
+ * Get the authenticated customer's enquiry history
284
+ * Only returns enquiries for the logged-in customer
285
+ */
286
+ async getEnquiries(options) {
287
+ if (!this.sessionToken) {
288
+ throw new TiquoAuthError("Not authenticated", "NOT_AUTHENTICATED");
289
+ }
290
+ this.log("Fetching customer enquiries:", options);
291
+ const url = new URL(`${this.config.apiEndpoint}/api/auth-dom/enquiries`);
292
+ url.searchParams.set("publicKey", this.config.publicKey);
293
+ if (options?.limit) {
294
+ url.searchParams.set("limit", options.limit.toString());
295
+ }
296
+ if (options?.cursor) {
297
+ url.searchParams.set("cursor", options.cursor);
298
+ }
299
+ if (options?.status) {
300
+ url.searchParams.set("status", options.status);
301
+ }
302
+ const response = await fetch(url.toString(), {
303
+ method: "GET",
304
+ headers: {
305
+ "Authorization": `Bearer ${this.sessionToken}`
306
+ },
307
+ credentials: "include"
308
+ });
309
+ if (!response.ok) {
310
+ const error = await response.json().catch(() => ({ error: "Failed to get enquiries" }));
311
+ throw new TiquoAuthError(error.error || "Failed to get enquiries", "GET_ENQUIRIES_FAILED", response.status);
312
+ }
313
+ return response.json();
314
+ }
315
+ /**
316
+ * Generate a short-lived token for customer flow iframe authentication
317
+ */
318
+ async getIframeToken(customerFlowId) {
319
+ if (!this.sessionToken) {
320
+ throw new TiquoAuthError("Not authenticated", "NOT_AUTHENTICATED");
321
+ }
322
+ this.log("Generating iframe token");
323
+ const response = await this.request("/api/auth-dom/iframe-token", {
324
+ method: "POST",
325
+ body: JSON.stringify({
326
+ publicKey: this.config.publicKey,
327
+ sessionToken: this.sessionToken,
328
+ customerFlowId
329
+ })
330
+ });
331
+ if (!response.ok) {
332
+ const error = await response.json().catch(() => ({ error: "Failed to generate token" }));
333
+ throw new TiquoAuthError(error.error || "Failed to generate token", "IFRAME_TOKEN_FAILED", response.status);
334
+ }
335
+ return response.json();
336
+ }
337
+ /**
338
+ * Embed a customer flow with automatic authentication
339
+ *
340
+ * @param flowUrl - The URL of the customer flow (book.tiquo.app/...)
341
+ * @param container - Container element or selector
342
+ * @param options - Optional iframe configuration
343
+ */
344
+ async embedCustomerFlow(flowUrl, container, options) {
345
+ const containerEl = typeof container === "string" ? document.querySelector(container) : container;
346
+ if (!containerEl) {
347
+ throw new TiquoAuthError("Container element not found", "CONTAINER_NOT_FOUND");
348
+ }
349
+ let authToken;
350
+ if (this.sessionToken) {
351
+ try {
352
+ const { token } = await this.getIframeToken();
353
+ authToken = token;
354
+ } catch (error) {
355
+ this.log("Failed to get iframe token:", error);
356
+ }
357
+ }
358
+ const url = new URL(flowUrl);
359
+ if (authToken) {
360
+ url.searchParams.set("_auth_token", authToken);
361
+ }
362
+ const iframe = document.createElement("iframe");
363
+ iframe.src = url.toString();
364
+ iframe.style.width = options?.width || "100%";
365
+ iframe.style.height = options?.height || "600px";
366
+ iframe.style.border = "none";
367
+ iframe.setAttribute("allow", "payment");
368
+ iframe.setAttribute("loading", "lazy");
369
+ if (options?.onLoad) {
370
+ iframe.addEventListener("load", options.onLoad);
371
+ }
372
+ if (options?.onError) {
373
+ iframe.addEventListener("error", () => options.onError(new Error("Failed to load iframe")));
374
+ }
375
+ containerEl.innerHTML = "";
376
+ containerEl.appendChild(iframe);
377
+ return iframe;
378
+ }
379
+ /**
380
+ * Get the current session token (for advanced use cases)
381
+ */
382
+ getSessionToken() {
383
+ return this.sessionToken;
384
+ }
385
+ // ============================================
386
+ // PRIVATE METHODS
387
+ // ============================================
388
+ async request(path, options) {
389
+ const url = `${this.config.apiEndpoint}${path}`;
390
+ const headers = {
391
+ "Content-Type": "application/json",
392
+ ...options.headers
393
+ };
394
+ if (this.sessionToken && !(typeof options.body === "string" && options.body.includes("sessionToken"))) {
395
+ headers["Authorization"] = `Bearer ${this.sessionToken}`;
396
+ }
397
+ return fetch(url, {
398
+ ...options,
399
+ headers,
400
+ credentials: "include"
401
+ });
402
+ }
403
+ async refreshSession() {
404
+ if (!this.sessionToken) {
405
+ return null;
406
+ }
407
+ try {
408
+ const url = new URL(`${this.config.apiEndpoint}/api/auth-dom/session`);
409
+ url.searchParams.set("publicKey", this.config.publicKey);
410
+ const response = await fetch(url.toString(), {
411
+ method: "GET",
412
+ headers: {
413
+ "Authorization": `Bearer ${this.sessionToken}`
414
+ },
415
+ credentials: "include"
416
+ });
417
+ if (!response.ok) {
418
+ this.log("Session invalid, clearing");
419
+ this.clearSession();
420
+ return null;
421
+ }
422
+ const data = await response.json();
423
+ this.session = {
424
+ user: data.user,
425
+ customer: data.customer,
426
+ expiresAt: data.session.expiresAt
427
+ };
428
+ this.notifyListeners();
429
+ this.scheduleRefresh();
430
+ return this.session;
431
+ } catch (error) {
432
+ this.log("Session refresh error:", error);
433
+ return null;
434
+ }
435
+ }
436
+ saveSession() {
437
+ if (this.sessionToken) {
438
+ try {
439
+ localStorage.setItem(
440
+ `${this.config.storagePrefix}session`,
441
+ this.sessionToken
442
+ );
443
+ } catch (error) {
444
+ this.log("Failed to save session to storage:", error);
445
+ }
446
+ }
447
+ }
448
+ restoreSession() {
449
+ try {
450
+ const token = localStorage.getItem(`${this.config.storagePrefix}session`);
451
+ if (token) {
452
+ this.sessionToken = token;
453
+ this.refreshSession();
454
+ }
455
+ } catch (error) {
456
+ this.log("Failed to restore session from storage:", error);
457
+ }
458
+ }
459
+ clearSession() {
460
+ this.sessionToken = null;
461
+ this.session = null;
462
+ if (this.refreshTimer) {
463
+ clearTimeout(this.refreshTimer);
464
+ this.refreshTimer = null;
465
+ }
466
+ try {
467
+ localStorage.removeItem(`${this.config.storagePrefix}session`);
468
+ } catch (error) {
469
+ this.log("Failed to clear session from storage:", error);
470
+ }
471
+ this.notifyListeners();
472
+ }
473
+ notifyListeners() {
474
+ for (const listener of this.listeners) {
475
+ try {
476
+ listener(this.session);
477
+ } catch (error) {
478
+ this.log("Listener error:", error);
479
+ }
480
+ }
481
+ }
482
+ scheduleRefresh() {
483
+ if (this.refreshTimer) {
484
+ clearTimeout(this.refreshTimer);
485
+ }
486
+ if (!this.session) return;
487
+ const refreshIn = this.session.expiresAt - Date.now() - 5 * 60 * 1e3;
488
+ if (refreshIn > 0) {
489
+ this.refreshTimer = setTimeout(() => {
490
+ this.refreshSession();
491
+ }, refreshIn);
492
+ }
493
+ }
494
+ log(...args) {
495
+ if (this.config.debug) {
496
+ console.log("[TiquoAuth]", ...args);
497
+ }
498
+ }
499
+ // ============================================
500
+ // MULTI-TAB SYNC METHODS
501
+ // ============================================
502
+ generateTabId() {
503
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
504
+ }
505
+ initTabSync() {
506
+ if (typeof BroadcastChannel === "undefined") {
507
+ this.log("BroadcastChannel not supported, tab sync disabled");
508
+ return;
509
+ }
510
+ try {
511
+ const channelName = `tiquo_auth_${this.config.publicKey}`;
512
+ this.broadcastChannel = new BroadcastChannel(channelName);
513
+ this.broadcastChannel.onmessage = (event) => {
514
+ this.handleTabSyncMessage(event.data);
515
+ };
516
+ this.log("Tab sync initialized, channel:", channelName);
517
+ } catch (error) {
518
+ this.log("Failed to initialize tab sync:", error);
519
+ }
520
+ }
521
+ broadcastTabSync(type) {
522
+ if (!this.broadcastChannel) return;
523
+ const message = {
524
+ type,
525
+ tabId: this.tabId,
526
+ timestamp: Date.now(),
527
+ sessionToken: type === "LOGOUT" ? null : this.sessionToken,
528
+ session: type === "LOGOUT" ? null : this.session
529
+ };
530
+ try {
531
+ this.broadcastChannel.postMessage(message);
532
+ this.log("Broadcasted tab sync message:", type);
533
+ } catch (error) {
534
+ this.log("Failed to broadcast tab sync:", error);
535
+ }
536
+ }
537
+ handleTabSyncMessage(message) {
538
+ if (message.tabId === this.tabId) return;
539
+ if (this.isProcessingTabSync) return;
540
+ this.isProcessingTabSync = true;
541
+ this.log("Received tab sync message:", message.type, "from tab:", message.tabId);
542
+ try {
543
+ switch (message.type) {
544
+ case "LOGIN":
545
+ if (message.sessionToken && message.session) {
546
+ this.sessionToken = message.sessionToken;
547
+ this.session = message.session;
548
+ this.saveSession();
549
+ this.scheduleRefresh();
550
+ this.notifyListeners();
551
+ this.log("Session synced from another tab (login)");
552
+ }
553
+ break;
554
+ case "LOGOUT":
555
+ this.sessionToken = null;
556
+ this.session = null;
557
+ if (this.refreshTimer) {
558
+ clearTimeout(this.refreshTimer);
559
+ this.refreshTimer = null;
560
+ }
561
+ try {
562
+ localStorage.removeItem(`${this.config.storagePrefix}session`);
563
+ } catch {
564
+ }
565
+ this.notifyListeners();
566
+ this.log("Session cleared from another tab (logout)");
567
+ break;
568
+ case "SESSION_UPDATE":
569
+ if (message.session) {
570
+ this.session = message.session;
571
+ this.notifyListeners();
572
+ this.log("Session synced from another tab (update)");
573
+ }
574
+ break;
575
+ }
576
+ } finally {
577
+ this.isProcessingTabSync = false;
578
+ }
579
+ }
580
+ /**
581
+ * Clean up resources (call when destroying the auth instance)
582
+ */
583
+ destroy() {
584
+ this.log("Destroying TiquoAuth instance");
585
+ if (this.refreshTimer) {
586
+ clearTimeout(this.refreshTimer);
587
+ this.refreshTimer = null;
588
+ }
589
+ if (this.broadcastChannel) {
590
+ this.broadcastChannel.close();
591
+ this.broadcastChannel = null;
592
+ }
593
+ this.listeners.clear();
594
+ }
595
+ };
596
+ function useTiquoAuth(auth) {
597
+ let session = null;
598
+ let isLoading = true;
599
+ auth.getUser().then((s) => {
600
+ session = s;
601
+ isLoading = false;
602
+ });
603
+ return {
604
+ get user() {
605
+ return session?.user || null;
606
+ },
607
+ get customer() {
608
+ return session?.customer || null;
609
+ },
610
+ get session() {
611
+ return session;
612
+ },
613
+ get isLoading() {
614
+ return isLoading;
615
+ },
616
+ get isAuthenticated() {
617
+ return auth.isAuthenticated();
618
+ },
619
+ sendOTP: (email) => auth.sendOTP(email),
620
+ verifyOTP: (email, otp) => auth.verifyOTP(email, otp),
621
+ logout: () => auth.logout(),
622
+ updateProfile: (updates) => auth.updateProfile(updates),
623
+ getOrders: (options) => auth.getOrders(options),
624
+ getBookings: (options) => auth.getBookings(options),
625
+ getEnquiries: (options) => auth.getEnquiries(options),
626
+ getIframeToken: (flowId) => auth.getIframeToken(flowId),
627
+ embedCustomerFlow: auth.embedCustomerFlow.bind(auth),
628
+ onAuthStateChange: (cb) => auth.onAuthStateChange(cb)
629
+ };
630
+ }
631
+ var index_default = TiquoAuth;
632
+ // Annotate the CommonJS export names for ESM import in node:
633
+ 0 && (module.exports = {
634
+ TiquoAuth,
635
+ TiquoAuthError,
636
+ useTiquoAuth
637
+ });