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