@saas-support/react 0.2.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,524 @@
1
+ class b extends Error {
2
+ constructor(t, e, s = "unknown") {
3
+ super(e), this.name = "SaaSError", this.code = t, this.domain = s;
4
+ }
5
+ get isNotFound() {
6
+ return this.code === 404;
7
+ }
8
+ get isUnauthorized() {
9
+ return this.code === 401;
10
+ }
11
+ get isForbidden() {
12
+ return this.code === 403;
13
+ }
14
+ get isConflict() {
15
+ return this.code === 409;
16
+ }
17
+ get isRateLimited() {
18
+ return this.code === 429;
19
+ }
20
+ }
21
+ class m {
22
+ constructor(t, e) {
23
+ this.baseUrl = t, this.authMode = e;
24
+ }
25
+ async request(t, e, s, r) {
26
+ const i = {
27
+ "Content-Type": "application/json",
28
+ ...this.getAuthHeaders(),
29
+ ...r
30
+ }, a = await (await fetch(`${this.baseUrl}${e}`, {
31
+ method: t,
32
+ headers: i,
33
+ body: s ? JSON.stringify(s) : void 0
34
+ })).json();
35
+ if (a.code && a.code >= 400) {
36
+ const d = this.inferDomain(e);
37
+ throw new b(a.code, a.message || "Request failed", d);
38
+ }
39
+ return a.data;
40
+ }
41
+ async get(t, e) {
42
+ return this.request("GET", t, void 0, e);
43
+ }
44
+ async post(t, e, s) {
45
+ return this.request("POST", t, e, s);
46
+ }
47
+ async patch(t, e, s) {
48
+ return this.request("PATCH", t, e, s);
49
+ }
50
+ async del(t, e) {
51
+ return this.request("DELETE", t, void 0, e);
52
+ }
53
+ getAuthHeaders() {
54
+ switch (this.authMode.type) {
55
+ case "publishableKey":
56
+ case "apiKey":
57
+ return { "X-API-Key": this.authMode.key };
58
+ case "portalToken":
59
+ case "embedToken":
60
+ return { Authorization: `Bearer ${this.authMode.token}` };
61
+ }
62
+ }
63
+ inferDomain(t) {
64
+ return t.startsWith("/auth") ? "auth" : t.startsWith("/billing") ? "billing" : t.startsWith("/report") ? "report" : "unknown";
65
+ }
66
+ }
67
+ class w {
68
+ constructor(t) {
69
+ this.accessToken = null, this.refreshToken = null, this.refreshTimer = null, this.onRefreshNeeded = null, this.storageKey = `ss_rt_${t.slice(0, 12)}`, this.refreshToken = this.loadRefreshToken();
70
+ }
71
+ setRefreshCallback(t) {
72
+ this.onRefreshNeeded = t;
73
+ }
74
+ getAccessToken() {
75
+ return this.accessToken;
76
+ }
77
+ getRefreshToken() {
78
+ return this.refreshToken;
79
+ }
80
+ hasRefreshToken() {
81
+ return this.refreshToken !== null;
82
+ }
83
+ setTokens(t, e) {
84
+ this.accessToken = t, this.refreshToken = e, this.saveRefreshToken(e), this.scheduleRefresh(t);
85
+ }
86
+ clearTokens() {
87
+ this.accessToken = null, this.refreshToken = null, this.removeRefreshToken(), this.refreshTimer && (clearTimeout(this.refreshTimer), this.refreshTimer = null);
88
+ }
89
+ destroy() {
90
+ this.refreshTimer && (clearTimeout(this.refreshTimer), this.refreshTimer = null);
91
+ }
92
+ scheduleRefresh(t) {
93
+ var r;
94
+ this.refreshTimer && clearTimeout(this.refreshTimer);
95
+ const e = this.getTokenExpiry(t);
96
+ if (!e) return;
97
+ const s = e * 1e3 - Date.now() - 6e4;
98
+ if (s <= 0) {
99
+ (r = this.onRefreshNeeded) == null || r.call(this);
100
+ return;
101
+ }
102
+ this.refreshTimer = setTimeout(() => {
103
+ var i;
104
+ (i = this.onRefreshNeeded) == null || i.call(this);
105
+ }, s);
106
+ }
107
+ getTokenExpiry(t) {
108
+ try {
109
+ const e = t.split(".")[1];
110
+ return JSON.parse(atob(e)).exp ?? null;
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+ loadRefreshToken() {
116
+ try {
117
+ return localStorage.getItem(this.storageKey);
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+ saveRefreshToken(t) {
123
+ try {
124
+ localStorage.setItem(this.storageKey, t);
125
+ } catch {
126
+ }
127
+ }
128
+ removeRefreshToken() {
129
+ try {
130
+ localStorage.removeItem(this.storageKey);
131
+ } catch {
132
+ }
133
+ }
134
+ }
135
+ class S {
136
+ constructor() {
137
+ this.listeners = /* @__PURE__ */ new Map();
138
+ }
139
+ on(t, e) {
140
+ return this.listeners.has(t) || this.listeners.set(t, /* @__PURE__ */ new Set()), this.listeners.get(t).add(e), () => {
141
+ var s;
142
+ (s = this.listeners.get(t)) == null || s.delete(e);
143
+ };
144
+ }
145
+ emit(t, e) {
146
+ var s;
147
+ (s = this.listeners.get(t)) == null || s.forEach((r) => r(e));
148
+ }
149
+ removeAll() {
150
+ this.listeners.clear();
151
+ }
152
+ }
153
+ const k = 500, T = 600, $ = 5 * 60 * 1e3;
154
+ class R {
155
+ constructor(t, e, s, r) {
156
+ this.cachedUser = null, this.cachedSettings = null, this.loaded = !1, this.transport = t, this.tokenManager = e, this.emitter = s, this.baseUrl = r;
157
+ }
158
+ // ---------------------------------------------------------------------------
159
+ // Lifecycle
160
+ // ---------------------------------------------------------------------------
161
+ async load() {
162
+ var t, e;
163
+ if (!this.loaded) {
164
+ try {
165
+ this.cachedSettings = await this.transport.get("/auth/settings");
166
+ } catch {
167
+ }
168
+ if ((t = this.tokenManager) != null && t.hasRefreshToken())
169
+ try {
170
+ await this.performRefresh();
171
+ } catch {
172
+ (e = this.tokenManager) == null || e.clearTokens();
173
+ }
174
+ this.loaded = !0;
175
+ }
176
+ }
177
+ // ---------------------------------------------------------------------------
178
+ // Core auth operations
179
+ // ---------------------------------------------------------------------------
180
+ async signIn(t, e) {
181
+ const s = await this.transport.post("/auth/login", { email: t, password: e });
182
+ if ("mfaRequired" in s && s.mfaRequired)
183
+ return s;
184
+ const r = s;
185
+ return this.setSession(r), r;
186
+ }
187
+ async signUp(t, e) {
188
+ const s = await this.transport.post("/auth/register", { email: t, password: e });
189
+ return this.setSession(s), s;
190
+ }
191
+ async signOut() {
192
+ var e;
193
+ const t = (e = this.tokenManager) == null ? void 0 : e.getRefreshToken();
194
+ if (t)
195
+ try {
196
+ await this.transport.post("/auth/logout", { refreshToken: t });
197
+ } catch {
198
+ }
199
+ this.clearSession();
200
+ }
201
+ async signInWithOAuth(t) {
202
+ const e = `${this.baseUrl}/auth/oauth/${t}/popup-callback`, { authUrl: s, state: r } = await this.transport.get(
203
+ `/auth/oauth/${t}?redirect_uri=${encodeURIComponent(e)}`
204
+ ), i = window.screenX + (window.innerWidth - k) / 2, g = window.screenY + (window.innerHeight - T) / 2, a = window.open(
205
+ s,
206
+ "saas-support-oauth",
207
+ `width=${k},height=${T},left=${i},top=${g},toolbar=no,menubar=no`
208
+ );
209
+ return new Promise((d, c) => {
210
+ let o = !1;
211
+ const u = async (h) => {
212
+ var f;
213
+ if (((f = h.data) == null ? void 0 : f.type) === "saas-support:oauth-callback" && !o) {
214
+ if (o = !0, window.removeEventListener("message", u), clearTimeout(y), clearInterval(p), a == null || a.close(), h.data.error) {
215
+ c(new Error(`OAuth error: ${h.data.error}`));
216
+ return;
217
+ }
218
+ try {
219
+ const l = await this.transport.post(
220
+ `/auth/oauth/${t}/callback`,
221
+ { code: h.data.code, state: h.data.state || r }
222
+ );
223
+ this.setSession(l), d(l);
224
+ } catch (l) {
225
+ c(l);
226
+ }
227
+ }
228
+ };
229
+ window.addEventListener("message", u);
230
+ const y = setTimeout(() => {
231
+ o || (o = !0, window.removeEventListener("message", u), clearInterval(p), a == null || a.close(), c(new Error("OAuth popup timed out")));
232
+ }, $), p = setInterval(() => {
233
+ a != null && a.closed && !o && (o = !0, clearInterval(p), clearTimeout(y), window.removeEventListener("message", u), c(new Error("OAuth popup was closed")));
234
+ }, 500);
235
+ });
236
+ }
237
+ async submitMfaCode(t, e) {
238
+ const s = await this.transport.post("/auth/login/mfa", { mfaToken: t, code: e });
239
+ return this.setSession(s), s;
240
+ }
241
+ // ---------------------------------------------------------------------------
242
+ // Magic link & password reset
243
+ // ---------------------------------------------------------------------------
244
+ async sendMagicLink(t, e) {
245
+ await this.transport.post("/auth/magic-link/send", { email: t, redirectUrl: e });
246
+ }
247
+ async verifyMagicLink(t) {
248
+ const e = await this.transport.post("/auth/magic-link/verify", { token: t });
249
+ return this.setSession(e), e;
250
+ }
251
+ async sendPasswordReset(t, e) {
252
+ await this.transport.post("/auth/password-reset/send", { email: t, redirectUrl: e });
253
+ }
254
+ async resetPassword(t, e) {
255
+ await this.transport.post("/auth/password-reset/verify", { token: t, newPassword: e });
256
+ }
257
+ // ---------------------------------------------------------------------------
258
+ // MFA management
259
+ // ---------------------------------------------------------------------------
260
+ async setupMfa() {
261
+ return this.transport.post("/auth/mfa/setup", void 0, this.authHeaders());
262
+ }
263
+ async verifyMfa(t) {
264
+ return this.transport.post("/auth/mfa/verify", { code: t }, this.authHeaders());
265
+ }
266
+ async disableMfa(t) {
267
+ await this.transport.post("/auth/mfa/disable", { code: t }, this.authHeaders());
268
+ }
269
+ // ---------------------------------------------------------------------------
270
+ // Token & user access
271
+ // ---------------------------------------------------------------------------
272
+ async getToken() {
273
+ var e, s, r;
274
+ const t = ((e = this.tokenManager) == null ? void 0 : e.getAccessToken()) ?? null;
275
+ if (t) return t;
276
+ if ((s = this.tokenManager) != null && s.hasRefreshToken())
277
+ try {
278
+ return await this.performRefresh(), ((r = this.tokenManager) == null ? void 0 : r.getAccessToken()) ?? null;
279
+ } catch {
280
+ return this.clearSession(), null;
281
+ }
282
+ return null;
283
+ }
284
+ async getUser() {
285
+ if (this.cachedUser) return this.cachedUser;
286
+ const t = await this.getToken();
287
+ if (!t) return null;
288
+ try {
289
+ return this.cachedUser = await this.transport.get("/auth/me", { Authorization: `Bearer ${t}` }), this.cachedUser;
290
+ } catch {
291
+ return null;
292
+ }
293
+ }
294
+ getUserSync() {
295
+ return this.cachedUser;
296
+ }
297
+ isLoaded() {
298
+ return this.loaded;
299
+ }
300
+ async getSettings() {
301
+ if (this.cachedSettings) return this.cachedSettings;
302
+ try {
303
+ return this.cachedSettings = await this.transport.get("/auth/settings"), this.cachedSettings;
304
+ } catch {
305
+ return null;
306
+ }
307
+ }
308
+ onAuthStateChange(t) {
309
+ return this.emitter.on("authStateChange", t);
310
+ }
311
+ // ---------------------------------------------------------------------------
312
+ // Profile
313
+ // ---------------------------------------------------------------------------
314
+ async updateProfile(t) {
315
+ const e = await this.transport.patch("/auth/me", { metadata: t }, this.authHeaders());
316
+ return this.cachedUser = e, this.emitter.emit("authStateChange", e), e;
317
+ }
318
+ // ---------------------------------------------------------------------------
319
+ // Organizations
320
+ // ---------------------------------------------------------------------------
321
+ async listOrgs() {
322
+ return this.transport.get("/auth/orgs", this.authHeaders());
323
+ }
324
+ async createOrg(t, e) {
325
+ return this.transport.post("/auth/orgs", { name: t, slug: e }, this.authHeaders());
326
+ }
327
+ async getOrg(t) {
328
+ return this.transport.get(`/auth/orgs/${t}`, this.authHeaders());
329
+ }
330
+ async updateOrg(t, e) {
331
+ return this.transport.patch(`/auth/orgs/${t}`, e, this.authHeaders());
332
+ }
333
+ async deleteOrg(t) {
334
+ await this.transport.del(`/auth/orgs/${t}`, this.authHeaders());
335
+ }
336
+ // ---------------------------------------------------------------------------
337
+ // Members & Invites
338
+ // ---------------------------------------------------------------------------
339
+ async listMembers(t) {
340
+ return this.transport.get(`/auth/orgs/${t}/members`, this.authHeaders());
341
+ }
342
+ async sendInvite(t, e, s) {
343
+ return this.transport.post(`/auth/orgs/${t}/invites`, { email: e, role: s }, this.authHeaders());
344
+ }
345
+ async updateMemberRole(t, e, s) {
346
+ await this.transport.patch(`/auth/orgs/${t}/members/${e}`, { role: s }, this.authHeaders());
347
+ }
348
+ async removeMember(t, e) {
349
+ await this.transport.del(`/auth/orgs/${t}/members/${e}`, this.authHeaders());
350
+ }
351
+ async acceptInvite(t) {
352
+ return this.transport.post(`/auth/invites/${t}/accept`, void 0, this.authHeaders());
353
+ }
354
+ // ---------------------------------------------------------------------------
355
+ // Internal
356
+ // ---------------------------------------------------------------------------
357
+ /** @internal */
358
+ async performRefresh() {
359
+ var s;
360
+ const t = (s = this.tokenManager) == null ? void 0 : s.getRefreshToken();
361
+ if (!t) throw new Error("No refresh token");
362
+ const e = await this.transport.post(
363
+ "/auth/refresh",
364
+ { refreshToken: t }
365
+ );
366
+ if (this.tokenManager.setTokens(e.accessToken, e.refreshToken), !this.cachedUser)
367
+ try {
368
+ this.cachedUser = await this.transport.get("/auth/me", { Authorization: `Bearer ${e.accessToken}` }), this.emitter.emit("authStateChange", this.cachedUser);
369
+ } catch {
370
+ }
371
+ }
372
+ setSession(t) {
373
+ var e;
374
+ (e = this.tokenManager) == null || e.setTokens(t.accessToken, t.refreshToken), this.cachedUser = t.user, this.emitter.emit("authStateChange", t.user);
375
+ }
376
+ clearSession() {
377
+ var t;
378
+ (t = this.tokenManager) == null || t.clearTokens(), this.cachedUser = null, this.emitter.emit("authStateChange", null);
379
+ }
380
+ authHeaders() {
381
+ var e;
382
+ const t = (e = this.tokenManager) == null ? void 0 : e.getAccessToken();
383
+ return t ? { Authorization: `Bearer ${t}` } : {};
384
+ }
385
+ }
386
+ class U {
387
+ constructor(t) {
388
+ this.transport = t;
389
+ }
390
+ // --- Customer ---
391
+ async createCustomer(t) {
392
+ return this.transport.post("/billing/customers", t);
393
+ }
394
+ async getCustomer(t) {
395
+ return this.transport.get(`/billing/customers/${t}`);
396
+ }
397
+ async updateCustomer(t, e) {
398
+ return this.transport.patch(`/billing/customers/${t}`, e);
399
+ }
400
+ // --- Subscription ---
401
+ async subscribe(t, e) {
402
+ return this.transport.post(`/billing/customers/${t}/subscribe`, { planId: e });
403
+ }
404
+ async changePlan(t, e) {
405
+ return this.transport.patch(`/billing/customers/${t}/subscription`, { planId: e });
406
+ }
407
+ async cancelSubscription(t) {
408
+ return this.transport.del(`/billing/customers/${t}/subscription`);
409
+ }
410
+ // --- Invoices ---
411
+ async getInvoices(t) {
412
+ return this.transport.get(`/billing/customers/${t}/invoices`);
413
+ }
414
+ // --- Usage ---
415
+ async ingestUsageEvent(t) {
416
+ return this.transport.post("/billing/events", t);
417
+ }
418
+ async getCurrentUsage(t) {
419
+ return this.transport.get(`/billing/customers/${t}/usage`);
420
+ }
421
+ // --- Portal ---
422
+ async createPortalToken(t, e) {
423
+ return this.transport.post("/billing/portal-tokens", { customerId: t, expiresIn: e });
424
+ }
425
+ // --- Coupon ---
426
+ async applyCoupon(t, e) {
427
+ return this.transport.post(`/billing/customers/${t}/coupon`, { code: e });
428
+ }
429
+ }
430
+ class M {
431
+ constructor(t) {
432
+ this.transport = t;
433
+ }
434
+ // --- Query ---
435
+ async executeQuery(t) {
436
+ return this.transport.post("/report/query", t);
437
+ }
438
+ // --- Saved Queries ---
439
+ async listQueries(t) {
440
+ const e = t ? this.toQueryString(t) : "";
441
+ return this.transport.get(`/report/queries${e}`);
442
+ }
443
+ async saveQuery(t) {
444
+ return this.transport.post("/report/queries", t);
445
+ }
446
+ async updateQuery(t, e) {
447
+ return this.transport.patch(`/report/queries/${t}`, e);
448
+ }
449
+ async deleteQuery(t) {
450
+ await this.transport.del(`/report/queries/${t}`);
451
+ }
452
+ // --- Dashboards ---
453
+ async listDashboards(t) {
454
+ const e = t ? this.toQueryString(t) : "";
455
+ return this.transport.get(`/report/dashboards${e}`);
456
+ }
457
+ async createDashboard(t) {
458
+ return this.transport.post("/report/dashboards", t);
459
+ }
460
+ async getDashboard(t) {
461
+ return this.transport.get(`/report/dashboards/${t}`);
462
+ }
463
+ async updateDashboard(t, e) {
464
+ return this.transport.patch(`/report/dashboards/${t}`, e);
465
+ }
466
+ async deleteDashboard(t) {
467
+ await this.transport.del(`/report/dashboards/${t}`);
468
+ }
469
+ // --- Embed Tokens ---
470
+ async createEmbedToken(t) {
471
+ return this.transport.post("/report/embed-tokens", t);
472
+ }
473
+ async listEmbedTokens() {
474
+ return this.transport.get("/report/embed-tokens");
475
+ }
476
+ async revokeEmbedToken(t) {
477
+ await this.transport.del(`/report/embed-tokens/${t}`);
478
+ }
479
+ toQueryString(t) {
480
+ const e = Object.entries(t).filter(([, s]) => s != null && s !== "");
481
+ return e.length === 0 ? "" : "?" + e.map(([s, r]) => `${s}=${encodeURIComponent(String(r))}`).join("&");
482
+ }
483
+ }
484
+ const v = "https://api.saas-support.com/v1";
485
+ class E {
486
+ constructor(t) {
487
+ if (this.tokenManager = null, this.loaded = !1, !t.publishableKey && !t.apiKey)
488
+ throw new Error("SaaSSupport: either publishableKey or apiKey is required");
489
+ const e = t.baseUrl ?? v;
490
+ this.emitter = new S();
491
+ const s = t.publishableKey ? new m(e, { type: "publishableKey", key: t.publishableKey }) : null, r = t.apiKey ? new m(e, { type: "apiKey", key: t.apiKey }) : null;
492
+ t.publishableKey && (this.tokenManager = new w(t.publishableKey)), this.auth = new R(
493
+ s ?? r,
494
+ this.tokenManager,
495
+ this.emitter,
496
+ e
497
+ ), this.billing = new U(r ?? s), this.report = new M(r ?? s), this.tokenManager && this.tokenManager.setRefreshCallback(() => this.auth.performRefresh());
498
+ }
499
+ async load() {
500
+ this.loaded || (await this.auth.load(), this.loaded = !0);
501
+ }
502
+ isLoaded() {
503
+ return this.loaded;
504
+ }
505
+ onError(t) {
506
+ return this.emitter.on("error", t);
507
+ }
508
+ destroy() {
509
+ var t;
510
+ (t = this.tokenManager) == null || t.destroy(), this.emitter.removeAll();
511
+ }
512
+ }
513
+ function H(n) {
514
+ return "mfaRequired" in n && n.mfaRequired === !0;
515
+ }
516
+ export {
517
+ R as AuthClient,
518
+ U as BillingClient,
519
+ M as ReportClient,
520
+ b as SaaSError,
521
+ E as SaaSSupport,
522
+ m as Transport,
523
+ H as isMfaRequired
524
+ };