@spacelr/sdk 0.1.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,1438 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // libs/sdk/src/core/errors.ts
9
+ var SpacelrError = class extends Error {
10
+ constructor(message, code, statusCode, details) {
11
+ super(message);
12
+ this.name = "SpacelrError";
13
+ this.code = code;
14
+ this.statusCode = statusCode;
15
+ this.details = details;
16
+ }
17
+ };
18
+ var SpacelrAuthError = class extends SpacelrError {
19
+ constructor(message, statusCode = 401, details) {
20
+ super(message, "AUTH_ERROR", statusCode, details);
21
+ this.name = "SpacelrAuthError";
22
+ }
23
+ };
24
+ var SpacelrNetworkError = class extends SpacelrError {
25
+ constructor(message, details) {
26
+ super(message, "NETWORK_ERROR", void 0, details);
27
+ this.name = "SpacelrNetworkError";
28
+ }
29
+ };
30
+ var SpacelrTimeoutError = class extends SpacelrError {
31
+ constructor(timeoutMs) {
32
+ super(
33
+ `Request timed out after ${timeoutMs}ms`,
34
+ "TIMEOUT_ERROR",
35
+ void 0,
36
+ { timeoutMs }
37
+ );
38
+ this.name = "SpacelrTimeoutError";
39
+ }
40
+ };
41
+ var SpacelrTwoFactorRequiredError = class extends SpacelrError {
42
+ constructor(twoFactorToken, details) {
43
+ super(
44
+ "Two-factor authentication required",
45
+ "TWO_FACTOR_REQUIRED",
46
+ 200,
47
+ details
48
+ );
49
+ this.name = "SpacelrTwoFactorRequiredError";
50
+ this.twoFactorToken = twoFactorToken;
51
+ }
52
+ };
53
+ var SpacelrEmailVerificationRequiredError = class extends SpacelrError {
54
+ constructor(emailSent, details) {
55
+ super(
56
+ emailSent ? "Email verification required. A new verification email has been sent." : "Email verification required. Please check your inbox for the verification email.",
57
+ "EMAIL_VERIFICATION_REQUIRED",
58
+ 200,
59
+ details
60
+ );
61
+ this.name = "SpacelrEmailVerificationRequiredError";
62
+ this.emailSent = emailSent;
63
+ }
64
+ };
65
+
66
+ // libs/sdk/src/core/http-client.ts
67
+ var HttpClient = class {
68
+ constructor(config, tokenManager) {
69
+ this.config = config;
70
+ this.tokenManager = tokenManager;
71
+ }
72
+ async request(options) {
73
+ const url = this.buildUrl(options.path, options.query);
74
+ const headers = await this.buildHeaders(options);
75
+ const timeout = this.config.timeout ?? 3e4;
76
+ const includeCredentials = options.withCredentials ?? options.path.startsWith("/auth/");
77
+ const controller = new AbortController();
78
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
79
+ try {
80
+ const response = await fetch(url, {
81
+ method: options.method,
82
+ headers,
83
+ body: options.body ? JSON.stringify(options.body) : void 0,
84
+ signal: controller.signal,
85
+ ...includeCredentials && { credentials: "include" }
86
+ });
87
+ const responseBody = await this.parseResponse(response);
88
+ if (!response.ok) {
89
+ this.throwHttpError(response.status, responseBody);
90
+ }
91
+ return this.extractData(responseBody);
92
+ } catch (error) {
93
+ if (error instanceof SpacelrError) throw error;
94
+ if (error instanceof DOMException && error.name === "AbortError") {
95
+ throw new SpacelrTimeoutError(timeout);
96
+ }
97
+ throw new SpacelrNetworkError(
98
+ error instanceof Error ? error.message : "Network request failed"
99
+ );
100
+ } finally {
101
+ clearTimeout(timeoutId);
102
+ }
103
+ }
104
+ async uploadForm(path, formData, onProgress) {
105
+ const url = this.buildUrl(path);
106
+ const headers = await this.buildFormHeaders();
107
+ const timeoutMs = this.config.timeout ?? 3e4;
108
+ if (onProgress) {
109
+ return this.uploadFormWithProgress(url, headers, formData, onProgress, timeoutMs);
110
+ }
111
+ const controller = new AbortController();
112
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
113
+ try {
114
+ const response = await fetch(url, {
115
+ method: "POST",
116
+ headers,
117
+ body: formData,
118
+ signal: controller.signal
119
+ });
120
+ const responseBody = await this.parseResponse(response);
121
+ if (!response.ok) {
122
+ this.throwHttpError(response.status, responseBody);
123
+ }
124
+ return this.extractData(responseBody);
125
+ } catch (error) {
126
+ if (error instanceof SpacelrError) throw error;
127
+ if (error instanceof DOMException && error.name === "AbortError") {
128
+ throw new SpacelrTimeoutError(timeoutMs);
129
+ }
130
+ throw new SpacelrNetworkError(
131
+ error instanceof Error ? error.message : "Network request failed"
132
+ );
133
+ } finally {
134
+ clearTimeout(timeoutId);
135
+ }
136
+ }
137
+ uploadFormWithProgress(url, headers, formData, onProgress, timeoutMs) {
138
+ return new Promise((resolve, reject) => {
139
+ const xhr = new XMLHttpRequest();
140
+ xhr.open("POST", url);
141
+ xhr.timeout = timeoutMs;
142
+ for (const [key, value] of Object.entries(headers)) {
143
+ xhr.setRequestHeader(key, value);
144
+ }
145
+ xhr.upload.addEventListener("progress", (e) => {
146
+ if (e.lengthComputable) {
147
+ onProgress({ loaded: e.loaded, total: e.total });
148
+ }
149
+ });
150
+ xhr.addEventListener("load", () => {
151
+ try {
152
+ const contentType = xhr.getResponseHeader("content-type") ?? "";
153
+ if (!contentType.includes("application/json")) {
154
+ if (xhr.status >= 200 && xhr.status < 300) {
155
+ resolve({ success: true, data: xhr.responseText });
156
+ } else {
157
+ reject(new SpacelrNetworkError(`HTTP ${xhr.status}: ${xhr.responseText.slice(0, 200)}`));
158
+ }
159
+ return;
160
+ }
161
+ const body = JSON.parse(xhr.responseText);
162
+ if (xhr.status >= 200 && xhr.status < 300) {
163
+ resolve(this.extractData(body));
164
+ } else {
165
+ this.throwHttpError(xhr.status, body);
166
+ }
167
+ } catch (error) {
168
+ if (error instanceof SpacelrError) {
169
+ reject(error);
170
+ } else {
171
+ reject(new SpacelrNetworkError("Failed to parse response"));
172
+ }
173
+ }
174
+ });
175
+ xhr.addEventListener("error", () => {
176
+ reject(new SpacelrNetworkError("Network request failed"));
177
+ });
178
+ xhr.addEventListener("timeout", () => {
179
+ xhr.abort();
180
+ reject(new SpacelrTimeoutError(timeoutMs));
181
+ });
182
+ xhr.send(formData);
183
+ });
184
+ }
185
+ buildUrl(path, query) {
186
+ const cleanPath = path.startsWith("/") ? path : `/${path}`;
187
+ const base = cleanPath.startsWith("/.well-known") ? new URL(this.config.apiUrl).origin : this.config.apiUrl.replace(/\/+$/, "");
188
+ const url = new URL(`${base}${cleanPath}`);
189
+ if (query) {
190
+ for (const [key, value] of Object.entries(query)) {
191
+ if (value !== void 0) {
192
+ url.searchParams.set(key, String(value));
193
+ }
194
+ }
195
+ }
196
+ return url.toString();
197
+ }
198
+ async buildHeaders(options) {
199
+ const headers = {
200
+ "Content-Type": "application/json",
201
+ "x-client-id": this.config.clientId,
202
+ "x-project-id": this.config.projectId,
203
+ ...options.headers
204
+ };
205
+ if (options.authenticated) {
206
+ const token = await this.tokenManager.getAccessToken();
207
+ if (token) {
208
+ headers["Authorization"] = `Bearer ${token}`;
209
+ }
210
+ }
211
+ return headers;
212
+ }
213
+ async buildFormHeaders() {
214
+ const headers = {
215
+ "x-client-id": this.config.clientId,
216
+ "x-project-id": this.config.projectId
217
+ };
218
+ const token = await this.tokenManager.getAccessToken();
219
+ if (token) {
220
+ headers["Authorization"] = `Bearer ${token}`;
221
+ }
222
+ return headers;
223
+ }
224
+ async parseResponse(response) {
225
+ const contentType = response.headers.get("content-type") ?? "";
226
+ if (contentType.includes("application/json")) {
227
+ return response.json();
228
+ }
229
+ const text = await response.text();
230
+ return { success: response.ok, data: text };
231
+ }
232
+ throwHttpError(statusCode, body) {
233
+ const apiBody = body;
234
+ const message = apiBody.error?.message ?? `HTTP ${statusCode}`;
235
+ const code = apiBody.error?.code ?? `HTTP_${statusCode}`;
236
+ const details = apiBody.error?.details;
237
+ if (statusCode === 401 || statusCode === 403) {
238
+ throw new SpacelrAuthError(message, statusCode, details);
239
+ }
240
+ throw new SpacelrError(message, code, statusCode, details);
241
+ }
242
+ extractData(body) {
243
+ const apiBody = body;
244
+ if ("success" in apiBody && apiBody.data !== void 0) {
245
+ const data = apiBody.data;
246
+ if (data["emailVerificationRequired"] === true) {
247
+ throw new SpacelrEmailVerificationRequiredError(data["emailSent"] === true);
248
+ }
249
+ if (data["twoFactorRequired"] === true && typeof data["twoFactorToken"] === "string") {
250
+ throw new SpacelrTwoFactorRequiredError(data["twoFactorToken"]);
251
+ }
252
+ return apiBody.data;
253
+ }
254
+ const rawBody = body;
255
+ if (rawBody["emailVerificationRequired"] === true) {
256
+ throw new SpacelrEmailVerificationRequiredError(rawBody["emailSent"] === true);
257
+ }
258
+ if (rawBody["twoFactorRequired"] === true && typeof rawBody["twoFactorToken"] === "string") {
259
+ throw new SpacelrTwoFactorRequiredError(rawBody["twoFactorToken"]);
260
+ }
261
+ return body;
262
+ }
263
+ };
264
+
265
+ // libs/sdk/src/core/token-storage.ts
266
+ var MemoryTokenStorage = class {
267
+ constructor() {
268
+ this.tokens = null;
269
+ }
270
+ async getTokens() {
271
+ return this.tokens;
272
+ }
273
+ async setTokens(tokens) {
274
+ this.tokens = tokens;
275
+ }
276
+ async clearTokens() {
277
+ this.tokens = null;
278
+ }
279
+ };
280
+ var BrowserTokenStorage = class {
281
+ constructor(storageKey = "spacelr_tokens") {
282
+ this.storageKey = storageKey;
283
+ }
284
+ async getTokens() {
285
+ try {
286
+ const raw = localStorage.getItem(this.storageKey);
287
+ if (!raw) return null;
288
+ return JSON.parse(raw);
289
+ } catch {
290
+ return null;
291
+ }
292
+ }
293
+ async setTokens(tokens) {
294
+ try {
295
+ localStorage.setItem(this.storageKey, JSON.stringify(tokens));
296
+ } catch {
297
+ }
298
+ }
299
+ async clearTokens() {
300
+ try {
301
+ localStorage.removeItem(this.storageKey);
302
+ } catch {
303
+ }
304
+ }
305
+ };
306
+
307
+ // libs/sdk/src/core/token-manager.ts
308
+ var TokenManager = class {
309
+ constructor(storage, refreshBufferSeconds = 60) {
310
+ this.refreshCallback = null;
311
+ this.refreshPromise = null;
312
+ this.storage = storage ?? new MemoryTokenStorage();
313
+ this.refreshBufferSeconds = refreshBufferSeconds;
314
+ }
315
+ setRefreshCallback(callback) {
316
+ this.refreshCallback = callback;
317
+ }
318
+ async getAccessToken() {
319
+ const tokens = await this.storage.getTokens();
320
+ if (!tokens) return null;
321
+ if (this.isTokenExpired(tokens)) {
322
+ const refreshed = await this.tryRefresh(tokens);
323
+ return refreshed?.accessToken ?? null;
324
+ }
325
+ if (this.shouldRefresh(tokens)) {
326
+ this.tryRefresh(tokens).catch(() => {
327
+ });
328
+ }
329
+ return tokens.accessToken;
330
+ }
331
+ async setTokens(tokens) {
332
+ await this.storage.setTokens(tokens);
333
+ }
334
+ async clearTokens() {
335
+ await this.storage.clearTokens();
336
+ this.refreshPromise = null;
337
+ }
338
+ async getStoredTokens() {
339
+ return this.storage.getTokens();
340
+ }
341
+ isTokenExpired(tokens) {
342
+ if (!tokens.expiresAt) return false;
343
+ return Date.now() >= tokens.expiresAt * 1e3;
344
+ }
345
+ shouldRefresh(tokens) {
346
+ if (!tokens.expiresAt || !tokens.refreshToken) return false;
347
+ const bufferMs = this.refreshBufferSeconds * 1e3;
348
+ return Date.now() >= tokens.expiresAt * 1e3 - bufferMs;
349
+ }
350
+ async tryRefresh(tokens) {
351
+ if (!tokens.refreshToken || !this.refreshCallback) return null;
352
+ if (this.refreshPromise) {
353
+ return this.refreshPromise;
354
+ }
355
+ this.refreshPromise = this.executeRefresh(tokens.refreshToken);
356
+ try {
357
+ const result = await this.refreshPromise;
358
+ return result;
359
+ } finally {
360
+ this.refreshPromise = null;
361
+ }
362
+ }
363
+ async executeRefresh(refreshToken) {
364
+ const callback = this.refreshCallback;
365
+ const newTokens = await callback(refreshToken);
366
+ await this.storage.setTokens(newTokens);
367
+ return newTokens;
368
+ }
369
+ };
370
+
371
+ // libs/sdk/src/types/common.ts
372
+ var FileVisibility = /* @__PURE__ */ ((FileVisibility2) => {
373
+ FileVisibility2["PRIVATE"] = "private";
374
+ FileVisibility2["SHARED"] = "shared";
375
+ FileVisibility2["PUBLIC"] = "public";
376
+ return FileVisibility2;
377
+ })(FileVisibility || {});
378
+ var GrantType = /* @__PURE__ */ ((GrantType2) => {
379
+ GrantType2["AUTHORIZATION_CODE"] = "authorization_code";
380
+ GrantType2["CLIENT_CREDENTIALS"] = "client_credentials";
381
+ GrantType2["REFRESH_TOKEN"] = "refresh_token";
382
+ return GrantType2;
383
+ })(GrantType || {});
384
+ var CodeChallengeMethod = /* @__PURE__ */ ((CodeChallengeMethod2) => {
385
+ CodeChallengeMethod2["PLAIN"] = "plain";
386
+ CodeChallengeMethod2["S256"] = "S256";
387
+ return CodeChallengeMethod2;
388
+ })(CodeChallengeMethod || {});
389
+ var SharePermission = /* @__PURE__ */ ((SharePermission2) => {
390
+ SharePermission2["READ"] = "read";
391
+ SharePermission2["WRITE"] = "write";
392
+ return SharePermission2;
393
+ })(SharePermission || {});
394
+
395
+ // libs/sdk/src/core/pkce.ts
396
+ function generateRandomBytes(length) {
397
+ if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.getRandomValues) {
398
+ const bytes = new Uint8Array(length);
399
+ globalThis.crypto.getRandomValues(bytes);
400
+ return bytes;
401
+ }
402
+ const nodeCrypto = __require("crypto");
403
+ return new Uint8Array(nodeCrypto.randomBytes(length));
404
+ }
405
+ function base64UrlEncode(buffer) {
406
+ let binary = "";
407
+ for (let i = 0; i < buffer.length; i++) {
408
+ binary += String.fromCharCode(buffer[i]);
409
+ }
410
+ const base64 = btoa(binary);
411
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
412
+ }
413
+ async function sha256(input) {
414
+ if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.subtle) {
415
+ const encoder = new TextEncoder();
416
+ const data = encoder.encode(input);
417
+ const hash2 = await globalThis.crypto.subtle.digest("SHA-256", data);
418
+ return new Uint8Array(hash2);
419
+ }
420
+ const nodeCrypto = __require("crypto");
421
+ const hash = nodeCrypto.createHash("sha256").update(input).digest();
422
+ return new Uint8Array(hash);
423
+ }
424
+ async function generatePKCEChallenge() {
425
+ const randomBytes = generateRandomBytes(32);
426
+ const codeVerifier = base64UrlEncode(randomBytes);
427
+ const hashBytes = await sha256(codeVerifier);
428
+ const codeChallenge = base64UrlEncode(hashBytes);
429
+ return {
430
+ codeVerifier,
431
+ codeChallenge,
432
+ codeChallengeMethod: "S256" /* S256 */
433
+ };
434
+ }
435
+
436
+ // libs/sdk/src/core/realtime.ts
437
+ import { io } from "socket.io-client";
438
+ var RealtimeClient = class {
439
+ constructor(config) {
440
+ this.socket = null;
441
+ this.subscriptions = /* @__PURE__ */ new Map();
442
+ this.connecting = null;
443
+ // Store original where objects per room for reconnect resubscription
444
+ this.roomWhereMap = /* @__PURE__ */ new Map();
445
+ this.config = config;
446
+ }
447
+ async subscribe(projectId, collectionName, callback, onError, where) {
448
+ await this.ensureConnected();
449
+ const room = this.buildRoomKey(projectId, collectionName, where);
450
+ if (!this.subscriptions.has(room)) {
451
+ this.subscriptions.set(room, /* @__PURE__ */ new Set());
452
+ }
453
+ const callbacks = this.subscriptions.get(room);
454
+ callbacks?.add(callback);
455
+ if (callbacks?.size === 1) {
456
+ if (where && Object.keys(where).length > 0) {
457
+ this.roomWhereMap.set(room, where);
458
+ }
459
+ const payload = { projectId, collectionName };
460
+ if (where && Object.keys(where).length > 0) {
461
+ payload["where"] = where;
462
+ }
463
+ this.socket?.emit(
464
+ "subscribe",
465
+ payload,
466
+ (response) => {
467
+ if (response.error && onError) {
468
+ onError(new Error(response.error));
469
+ }
470
+ }
471
+ );
472
+ }
473
+ return () => {
474
+ const callbacks2 = this.subscriptions.get(room);
475
+ if (callbacks2) {
476
+ callbacks2.delete(callback);
477
+ if (callbacks2.size === 0) {
478
+ this.subscriptions.delete(room);
479
+ this.roomWhereMap.delete(room);
480
+ const payload = { projectId, collectionName };
481
+ if (where && Object.keys(where).length > 0) {
482
+ payload["where"] = where;
483
+ }
484
+ this.socket?.emit("unsubscribe", payload);
485
+ }
486
+ }
487
+ };
488
+ }
489
+ disconnect() {
490
+ if (this.socket) {
491
+ this.socket.disconnect();
492
+ this.socket = null;
493
+ }
494
+ this.subscriptions.clear();
495
+ this.roomWhereMap.clear();
496
+ this.connecting = null;
497
+ }
498
+ buildRoomKey(projectId, collectionName, where) {
499
+ const base = `db:${projectId}:${collectionName}`;
500
+ if (!where || Object.keys(where).length === 0) {
501
+ return base;
502
+ }
503
+ const filterStr = Object.keys(where).sort().map((k) => `${k}=${String(where[k])}`).join("&");
504
+ return `${base}?${filterStr}`;
505
+ }
506
+ async ensureConnected() {
507
+ if (this.socket?.connected) return;
508
+ if (this.connecting) {
509
+ return this.connecting;
510
+ }
511
+ this.connecting = this.connect();
512
+ try {
513
+ await this.connecting;
514
+ } finally {
515
+ this.connecting = null;
516
+ }
517
+ }
518
+ async connect() {
519
+ const token = await this.config.getToken();
520
+ if (!token) {
521
+ throw new Error("No authentication token available");
522
+ }
523
+ const wsUrl = this.config.baseUrl.replace(/\/api\/v\d+\/?$/, "");
524
+ return new Promise((resolve, reject) => {
525
+ let initialConnect = true;
526
+ this.socket = io(`${wsUrl}/database`, {
527
+ auth: async (cb) => {
528
+ try {
529
+ const freshToken = await this.config.getToken();
530
+ cb({ token: freshToken });
531
+ } catch {
532
+ cb({ token: null });
533
+ }
534
+ },
535
+ transports: ["websocket"],
536
+ reconnection: false
537
+ // Disabled until first successful connect
538
+ });
539
+ this.socket.on("authenticated", () => {
540
+ if (initialConnect) {
541
+ initialConnect = false;
542
+ if (this.socket) {
543
+ this.socket.io.opts.reconnection = true;
544
+ this.socket.io.opts.reconnectionDelay = 1e3;
545
+ this.socket.io.opts.reconnectionDelayMax = 5e3;
546
+ this.socket.io.opts.reconnectionAttempts = 50;
547
+ }
548
+ resolve();
549
+ }
550
+ });
551
+ this.socket.on("connect", () => {
552
+ if (!initialConnect) {
553
+ this.resubscribeAll();
554
+ }
555
+ });
556
+ this.socket.on("connect_error", (err) => {
557
+ if (initialConnect) {
558
+ reject(new Error(`WebSocket connection failed: ${err.message}`));
559
+ }
560
+ });
561
+ this.socket.on("db:event", (event) => {
562
+ const base = `db:${event.projectId}:${event.collectionName}`;
563
+ const baseCallbacks = this.subscriptions.get(base);
564
+ if (baseCallbacks) {
565
+ for (const cb of baseCallbacks) {
566
+ try {
567
+ cb(event);
568
+ } catch {
569
+ }
570
+ }
571
+ }
572
+ for (const [room, callbacks] of this.subscriptions) {
573
+ if (room.startsWith(`${base}?`) && this.eventMatchesRoom(event, room)) {
574
+ for (const cb of callbacks) {
575
+ try {
576
+ cb(event);
577
+ } catch {
578
+ }
579
+ }
580
+ }
581
+ }
582
+ });
583
+ this.socket.on("disconnect", () => {
584
+ });
585
+ });
586
+ }
587
+ eventMatchesRoom(event, room) {
588
+ const base = `db:${event.projectId}:${event.collectionName}`;
589
+ if (room === base) return true;
590
+ if (!room.startsWith(`${base}?`)) return false;
591
+ const where = this.roomWhereMap.get(room);
592
+ if (!where) return true;
593
+ if (!event.document) return false;
594
+ for (const [key, value] of Object.entries(where)) {
595
+ const docValue = event.document[key];
596
+ if (Array.isArray(docValue)) {
597
+ if (!docValue.some((item) => String(item) === String(value))) {
598
+ return false;
599
+ }
600
+ } else if (String(docValue) !== String(value)) {
601
+ return false;
602
+ }
603
+ }
604
+ return true;
605
+ }
606
+ resubscribeAll() {
607
+ for (const [room] of this.subscriptions) {
608
+ const queryIdx = room.indexOf("?");
609
+ const basePart = queryIdx >= 0 ? room.substring(0, queryIdx) : room;
610
+ const parts = basePart.split(":");
611
+ if (parts.length >= 3) {
612
+ const projectId = parts[1];
613
+ const collectionName = parts.slice(2).join(":");
614
+ const payload = { projectId, collectionName };
615
+ const where = this.roomWhereMap.get(room);
616
+ if (where) {
617
+ payload["where"] = where;
618
+ }
619
+ this.socket?.emit(
620
+ "subscribe",
621
+ payload,
622
+ (response) => {
623
+ if (response?.error) {
624
+ this.subscriptions.delete(room);
625
+ this.roomWhereMap.delete(room);
626
+ }
627
+ }
628
+ );
629
+ }
630
+ }
631
+ }
632
+ };
633
+
634
+ // libs/sdk/src/modules/auth.module.ts
635
+ var AuthModule = class {
636
+ constructor(http, tokenManager, config) {
637
+ this.http = http;
638
+ this.tokenManager = tokenManager;
639
+ this.config = config;
640
+ this.tokenManager.setRefreshCallback(async (refreshToken) => {
641
+ const result = await this.refresh(refreshToken);
642
+ const expiresAt = result.expires_in ? Math.floor(Date.now() / 1e3) + result.expires_in : void 0;
643
+ return {
644
+ accessToken: result.access_token,
645
+ refreshToken: result.refresh_token,
646
+ expiresAt
647
+ };
648
+ });
649
+ }
650
+ async login(params) {
651
+ const response = await this.http.request({
652
+ method: "POST",
653
+ path: "/auth/login",
654
+ body: params
655
+ });
656
+ await this.storeTokensFromLogin(response);
657
+ return response;
658
+ }
659
+ async register(params) {
660
+ const response = await this.http.request({
661
+ method: "POST",
662
+ path: "/auth/register",
663
+ body: params
664
+ });
665
+ if (response.access_token) {
666
+ await this.storeTokensFromRegister(response);
667
+ }
668
+ return response;
669
+ }
670
+ async refresh(refreshToken) {
671
+ return this.http.request({
672
+ method: "POST",
673
+ path: "/auth/refresh",
674
+ body: { refreshToken }
675
+ });
676
+ }
677
+ async getProfile() {
678
+ return this.http.request({
679
+ method: "GET",
680
+ path: "/auth/me",
681
+ authenticated: true
682
+ });
683
+ }
684
+ async logout() {
685
+ await this.http.request({
686
+ method: "POST",
687
+ path: "/auth/logout",
688
+ authenticated: true
689
+ });
690
+ await this.tokenManager.clearTokens();
691
+ }
692
+ async verifyEmail(token) {
693
+ return this.http.request({
694
+ method: "GET",
695
+ path: "/auth/verify-email",
696
+ query: { token }
697
+ });
698
+ }
699
+ async resendVerification(email) {
700
+ return this.http.request({
701
+ method: "POST",
702
+ path: "/auth/resend-verification",
703
+ body: { email }
704
+ });
705
+ }
706
+ async getUserInfo() {
707
+ return this.http.request({
708
+ method: "GET",
709
+ path: this.config.userInfoEndpoint ?? "/auth/userinfo",
710
+ authenticated: true
711
+ });
712
+ }
713
+ getAuthorizationUrl(params) {
714
+ const baseUrl = this.config.apiUrl.replace(/\/+$/, "");
715
+ const endpoint = this.config.authorizationEndpoint ?? "/auth/authorize";
716
+ const url = new URL(`${baseUrl}${endpoint}`);
717
+ url.searchParams.set("client_id", this.config.clientId);
718
+ url.searchParams.set("redirect_uri", params.redirectUri);
719
+ url.searchParams.set("response_type", params.responseType ?? "code");
720
+ const scope = params.scope ?? this.config.scopes?.join(" ") ?? "openid";
721
+ url.searchParams.set("scope", scope);
722
+ if (params.state) {
723
+ url.searchParams.set("state", params.state);
724
+ }
725
+ if (params.codeChallenge) {
726
+ url.searchParams.set("code_challenge", params.codeChallenge);
727
+ }
728
+ if (params.codeChallengeMethod) {
729
+ url.searchParams.set(
730
+ "code_challenge_method",
731
+ params.codeChallengeMethod
732
+ );
733
+ }
734
+ return url.toString();
735
+ }
736
+ async exchangeCode(params) {
737
+ const body = {
738
+ grant_type: params.grantType ?? "authorization_code" /* AUTHORIZATION_CODE */,
739
+ code: params.code,
740
+ redirect_uri: params.redirectUri,
741
+ client_id: this.config.clientId
742
+ };
743
+ if (params.clientSecret) {
744
+ body["client_secret"] = params.clientSecret;
745
+ }
746
+ if (params.codeVerifier) {
747
+ body["code_verifier"] = params.codeVerifier;
748
+ }
749
+ const tokenEndpoint = this.config.tokenEndpoint ?? "/auth/token";
750
+ const response = await this.http.request({
751
+ method: "POST",
752
+ path: tokenEndpoint,
753
+ body
754
+ });
755
+ const expiresAt = response.expires_in ? Math.floor(Date.now() / 1e3) + response.expires_in : void 0;
756
+ await this.tokenManager.setTokens({
757
+ accessToken: response.access_token,
758
+ refreshToken: response.refresh_token,
759
+ expiresAt
760
+ });
761
+ return response;
762
+ }
763
+ async generatePKCE() {
764
+ return generatePKCEChallenge();
765
+ }
766
+ async getOpenIDConfiguration() {
767
+ return this.http.request({
768
+ method: "GET",
769
+ path: "/.well-known/openid-configuration"
770
+ });
771
+ }
772
+ async getJWKS() {
773
+ return this.http.request({
774
+ method: "GET",
775
+ path: "/.well-known/jwks.json"
776
+ });
777
+ }
778
+ /**
779
+ * Request a password reset email.
780
+ * Always returns a generic message regardless of whether the email exists.
781
+ */
782
+ async requestPasswordReset(email) {
783
+ return this.http.request({
784
+ method: "POST",
785
+ path: "/auth/request-password-reset",
786
+ body: { email }
787
+ });
788
+ }
789
+ /**
790
+ * Reset password using a token received via email.
791
+ */
792
+ async resetPassword(token, password) {
793
+ return this.http.request({
794
+ method: "POST",
795
+ path: "/auth/reset-password",
796
+ body: { token, password }
797
+ });
798
+ }
799
+ /**
800
+ * Exchange a one-time verification code for tokens.
801
+ * Use this after email verification redirects the user with a ?loginCode= parameter.
802
+ */
803
+ async exchangeVerificationCode(code) {
804
+ const response = await this.http.request({
805
+ method: "POST",
806
+ path: "/auth/exchange-code",
807
+ body: { code }
808
+ });
809
+ await this.storeTokensFromLogin(response);
810
+ return response;
811
+ }
812
+ /**
813
+ * Resend a two-factor authentication code email.
814
+ * Call this when the user hasn't received the code or it expired.
815
+ */
816
+ async resendTwoFactorCode(token) {
817
+ return this.http.request({
818
+ method: "POST",
819
+ path: "/auth/resend-two-factor-code",
820
+ body: { token }
821
+ });
822
+ }
823
+ /**
824
+ * Verify a two-factor authentication code.
825
+ * Call this after catching SpacelrTwoFactorRequiredError from login().
826
+ */
827
+ async verifyTwoFactor(params) {
828
+ const response = await this.http.request({
829
+ method: "POST",
830
+ path: "/auth/verify-two-factor",
831
+ body: { token: params.token, code: params.code }
832
+ });
833
+ await this.storeTokensFromLogin(response);
834
+ return response;
835
+ }
836
+ async storeTokensFromLogin(response) {
837
+ const expiresAt = response.expires_in ? Math.floor(Date.now() / 1e3) + response.expires_in : void 0;
838
+ await this.tokenManager.setTokens({
839
+ accessToken: response.access_token,
840
+ refreshToken: response.refresh_token,
841
+ expiresAt
842
+ });
843
+ }
844
+ async storeTokensFromRegister(response) {
845
+ if (!response.access_token) return;
846
+ await this.tokenManager.setTokens({
847
+ accessToken: response.access_token,
848
+ refreshToken: response.refresh_token
849
+ });
850
+ }
851
+ };
852
+
853
+ // libs/sdk/src/modules/storage.module.ts
854
+ var StorageModule = class {
855
+ constructor(http, tokenManager, config) {
856
+ this.http = http;
857
+ this.tokenManager = tokenManager;
858
+ this.config = config;
859
+ }
860
+ /**
861
+ * Upload a file through the gateway (no direct MinIO access).
862
+ * Accepts a Blob/File (browser) or ArrayBuffer/Uint8Array (Node).
863
+ */
864
+ async uploadFile(file, params, onProgress) {
865
+ const formData = new FormData();
866
+ const blob = file instanceof Blob ? file : new Blob([file], { type: params.mimeType });
867
+ formData.append("file", blob, params.filename);
868
+ if (params.visibility) formData.append("visibility", params.visibility);
869
+ if (params.description) formData.append("description", params.description);
870
+ const progressHandler = onProgress ? (e) => {
871
+ onProgress({
872
+ loaded: e.loaded,
873
+ total: e.total,
874
+ percentage: e.total > 0 ? Math.round(e.loaded / e.total * 100) : 0
875
+ });
876
+ } : void 0;
877
+ return this.http.uploadForm("/storage/files", formData, progressHandler);
878
+ }
879
+ /**
880
+ * Upload a large file using multipart upload through the gateway.
881
+ * Splits into parts, uploads concurrently, and completes.
882
+ */
883
+ async uploadLargeFile(file, params, onProgress) {
884
+ const fileSize = file instanceof Blob ? file.size : file.byteLength;
885
+ const concurrency = params.concurrency ?? 3;
886
+ const init = await this.initMultipartUpload({
887
+ filename: params.filename,
888
+ mimeType: params.mimeType,
889
+ totalSizeBytes: fileSize,
890
+ visibility: params.visibility,
891
+ description: params.description
892
+ });
893
+ const { partSize, totalParts, fileId } = init;
894
+ const completedParts = [];
895
+ let completedBytes = 0;
896
+ const partProgressMap = /* @__PURE__ */ new Map();
897
+ try {
898
+ const allPartNumbers = Array.from(
899
+ { length: totalParts },
900
+ (_, i) => i + 1
901
+ );
902
+ for (let i = 0; i < allPartNumbers.length; i += concurrency) {
903
+ const batch = allPartNumbers.slice(i, i + concurrency);
904
+ const uploads = batch.map(async (partNumber) => {
905
+ const start = (partNumber - 1) * partSize;
906
+ const end = Math.min(start + partSize, fileSize);
907
+ const chunkSize = end - start;
908
+ const chunk = file instanceof Blob ? file.slice(start, end) : new Blob([file.slice(start, end)]);
909
+ const formData = new FormData();
910
+ formData.append("file", chunk, `part-${partNumber}`);
911
+ formData.append("partNumber", String(partNumber));
912
+ const partProgress = onProgress && typeof XMLHttpRequest !== "undefined" ? (e) => {
913
+ const ratio = e.total > 0 ? e.loaded / e.total : 0;
914
+ partProgressMap.set(partNumber, Math.min(ratio * chunkSize, chunkSize));
915
+ const inFlightBytes = Array.from(partProgressMap.values()).reduce((sum, v) => sum + v, 0);
916
+ const totalLoaded = Math.min(completedBytes + inFlightBytes, fileSize);
917
+ onProgress({
918
+ loaded: totalLoaded,
919
+ total: fileSize,
920
+ percentage: fileSize > 0 ? Math.min(100, Math.round(totalLoaded / fileSize * 100)) : 0
921
+ });
922
+ } : void 0;
923
+ const result = await this.http.uploadForm(
924
+ `/storage/files/${fileId}/multipart/upload-part`,
925
+ formData,
926
+ partProgress
927
+ );
928
+ partProgressMap.delete(partNumber);
929
+ completedBytes += chunkSize;
930
+ if (onProgress) {
931
+ onProgress({
932
+ loaded: Math.min(completedBytes, fileSize),
933
+ total: fileSize,
934
+ percentage: fileSize > 0 ? Math.min(100, Math.round(completedBytes / fileSize * 100)) : 0
935
+ });
936
+ }
937
+ completedParts.push({
938
+ partNumber,
939
+ etag: result.etag
940
+ });
941
+ });
942
+ await Promise.all(uploads);
943
+ partProgressMap.clear();
944
+ }
945
+ completedParts.sort((a, b) => a.partNumber - b.partNumber);
946
+ await this.completeMultipartUpload(fileId, completedParts);
947
+ return this.getFileInfo(fileId);
948
+ } catch (error) {
949
+ try {
950
+ await this.abortMultipartUpload(fileId);
951
+ } catch {
952
+ }
953
+ throw error;
954
+ }
955
+ }
956
+ async initMultipartUpload(params) {
957
+ return this.http.request({
958
+ method: "POST",
959
+ path: "/storage/files/multipart/init",
960
+ body: params,
961
+ authenticated: true
962
+ });
963
+ }
964
+ async completeMultipartUpload(fileId, parts) {
965
+ return this.http.request({
966
+ method: "POST",
967
+ path: `/storage/files/${fileId}/multipart/complete`,
968
+ body: { parts },
969
+ authenticated: true
970
+ });
971
+ }
972
+ async abortMultipartUpload(fileId) {
973
+ await this.http.request({
974
+ method: "POST",
975
+ path: `/storage/files/${fileId}/multipart/abort`,
976
+ authenticated: true
977
+ });
978
+ }
979
+ async listFiles(params) {
980
+ return this.http.request({
981
+ method: "GET",
982
+ path: "/storage/files",
983
+ query: params,
984
+ authenticated: true
985
+ });
986
+ }
987
+ async listSharedFiles(params) {
988
+ return this.http.request({
989
+ method: "GET",
990
+ path: "/storage/shared",
991
+ query: params,
992
+ authenticated: true
993
+ });
994
+ }
995
+ async getFileInfo(fileId) {
996
+ return this.http.request({
997
+ method: "GET",
998
+ path: `/storage/files/${fileId}`,
999
+ authenticated: true
1000
+ });
1001
+ }
1002
+ async downloadFile(fileId) {
1003
+ const baseUrl = this.config.apiUrl.replace(/\/+$/, "");
1004
+ const url = `${baseUrl}/storage/files/${fileId}/download`;
1005
+ const headers = {
1006
+ "x-client-id": this.config.clientId,
1007
+ "x-project-id": this.config.projectId
1008
+ };
1009
+ const token = await this.tokenManager.getAccessToken();
1010
+ if (token) {
1011
+ headers["Authorization"] = `Bearer ${token}`;
1012
+ }
1013
+ const response = await fetch(url, { headers });
1014
+ if (!response.ok) {
1015
+ throw new Error(`Download failed: ${response.status}`);
1016
+ }
1017
+ return response.blob();
1018
+ }
1019
+ async getDownloadUrl(fileId) {
1020
+ return this.http.request({
1021
+ method: "GET",
1022
+ path: `/storage/files/${fileId}/download-url`,
1023
+ authenticated: true
1024
+ });
1025
+ }
1026
+ async deleteFile(fileId) {
1027
+ await this.http.request({
1028
+ method: "DELETE",
1029
+ path: `/storage/files/${fileId}`,
1030
+ authenticated: true
1031
+ });
1032
+ }
1033
+ async shareFile(fileId, params) {
1034
+ await this.http.request({
1035
+ method: "POST",
1036
+ path: `/storage/files/${fileId}/share`,
1037
+ body: params,
1038
+ authenticated: true
1039
+ });
1040
+ }
1041
+ async unshareFile(fileId, params) {
1042
+ await this.http.request({
1043
+ method: "POST",
1044
+ path: `/storage/files/${fileId}/unshare`,
1045
+ body: params,
1046
+ authenticated: true
1047
+ });
1048
+ }
1049
+ async updateVisibility(fileId, visibility) {
1050
+ return this.http.request({
1051
+ method: "PATCH",
1052
+ path: `/storage/files/${fileId}/visibility`,
1053
+ body: { visibility },
1054
+ authenticated: true
1055
+ });
1056
+ }
1057
+ async getQuota() {
1058
+ return this.http.request({
1059
+ method: "GET",
1060
+ path: "/storage/quota",
1061
+ authenticated: true
1062
+ });
1063
+ }
1064
+ async getPublicFileUrl(fileId, projectId) {
1065
+ const baseUrl = this.config.apiUrl.replace(/\/+$/, "");
1066
+ const resolvedProjectId = projectId ?? this.config.projectId;
1067
+ return `${baseUrl}/public/files/${resolvedProjectId}/${fileId}`;
1068
+ }
1069
+ };
1070
+
1071
+ // libs/sdk/src/modules/database.module.ts
1072
+ var QueryBuilder = class {
1073
+ constructor(http, basePath, filter) {
1074
+ this.http = http;
1075
+ this.basePath = basePath;
1076
+ this._populate = [];
1077
+ this._filter = filter;
1078
+ }
1079
+ sort(sort) {
1080
+ this._sort = sort;
1081
+ return this;
1082
+ }
1083
+ limit(limit) {
1084
+ this._limit = limit;
1085
+ return this;
1086
+ }
1087
+ offset(offset) {
1088
+ this._offset = offset;
1089
+ return this;
1090
+ }
1091
+ select(fields) {
1092
+ this._fields = fields;
1093
+ return this;
1094
+ }
1095
+ populate(field, collection, foreignField) {
1096
+ this._populate.push({ field, collection, foreignField });
1097
+ return this;
1098
+ }
1099
+ async execute() {
1100
+ const query = {};
1101
+ if (this._filter) query["filter"] = JSON.stringify(this._filter);
1102
+ if (this._sort) query["sort"] = JSON.stringify(this._sort);
1103
+ if (this._limit !== void 0) query["limit"] = this._limit;
1104
+ if (this._offset !== void 0) query["offset"] = this._offset;
1105
+ if (this._fields) query["fields"] = this._fields.join(",");
1106
+ if (this._populate.length) {
1107
+ query["populate"] = this._populate.map(
1108
+ (p) => p.foreignField ? `${p.field}:${p.collection}:${p.foreignField}` : `${p.field}:${p.collection}`
1109
+ ).join(",");
1110
+ }
1111
+ return this.http.request({
1112
+ method: "GET",
1113
+ path: this.basePath,
1114
+ query,
1115
+ authenticated: true
1116
+ });
1117
+ }
1118
+ };
1119
+ var CollectionRef = class {
1120
+ constructor(http, realtime, projectId, collectionName) {
1121
+ this.http = http;
1122
+ this.realtime = realtime;
1123
+ this.projectId = projectId;
1124
+ this.collectionName = collectionName;
1125
+ this.basePath = `/db/${collectionName}`;
1126
+ }
1127
+ async insert(document) {
1128
+ return this.http.request({
1129
+ method: "POST",
1130
+ path: this.basePath,
1131
+ body: { documents: [document] },
1132
+ authenticated: true
1133
+ });
1134
+ }
1135
+ async insertMany(documents) {
1136
+ return this.http.request({
1137
+ method: "POST",
1138
+ path: this.basePath,
1139
+ body: { documents },
1140
+ authenticated: true
1141
+ });
1142
+ }
1143
+ find(filter) {
1144
+ return new QueryBuilder(this.http, this.basePath, filter);
1145
+ }
1146
+ async findById(id, options) {
1147
+ const query = {};
1148
+ if (options?.populate?.length) {
1149
+ query["populate"] = options.populate.map(
1150
+ (p) => p.foreignField ? `${p.field}:${p.collection}:${p.foreignField}` : `${p.field}:${p.collection}`
1151
+ ).join(",");
1152
+ }
1153
+ return this.http.request({
1154
+ method: "GET",
1155
+ path: `${this.basePath}/${id}`,
1156
+ query,
1157
+ authenticated: true
1158
+ });
1159
+ }
1160
+ async update(id, update) {
1161
+ return this.http.request({
1162
+ method: "PATCH",
1163
+ path: `${this.basePath}/${id}`,
1164
+ body: { update },
1165
+ authenticated: true
1166
+ });
1167
+ }
1168
+ async delete(id) {
1169
+ return this.http.request({
1170
+ method: "DELETE",
1171
+ path: `${this.basePath}/${id}`,
1172
+ authenticated: true
1173
+ });
1174
+ }
1175
+ async count(filter) {
1176
+ const result = await this.http.request({
1177
+ method: "POST",
1178
+ path: `${this.basePath}/count`,
1179
+ body: { filter },
1180
+ authenticated: true
1181
+ });
1182
+ return result.count;
1183
+ }
1184
+ subscribe(handlers) {
1185
+ if (!this.realtime) {
1186
+ throw new Error("Realtime not available: no RealtimeClient configured");
1187
+ }
1188
+ let unsubscribeFn = null;
1189
+ let pendingUnsub = false;
1190
+ const callback = (event) => {
1191
+ switch (event.type) {
1192
+ case "insert":
1193
+ if (handlers.onInsert && event.document) {
1194
+ handlers.onInsert(event.document);
1195
+ }
1196
+ break;
1197
+ case "update":
1198
+ if (handlers.onUpdate && event.document) {
1199
+ handlers.onUpdate(event.document);
1200
+ }
1201
+ break;
1202
+ case "delete":
1203
+ if (handlers.onDelete) {
1204
+ handlers.onDelete(event.documentId);
1205
+ }
1206
+ break;
1207
+ }
1208
+ };
1209
+ this.realtime.subscribe(this.projectId, this.collectionName, callback, handlers.onError, handlers.where).then((unsub) => {
1210
+ if (pendingUnsub) {
1211
+ unsub();
1212
+ } else {
1213
+ unsubscribeFn = unsub;
1214
+ }
1215
+ }).catch((err) => {
1216
+ if (handlers.onError) {
1217
+ handlers.onError(err instanceof Error ? err : new Error(String(err)));
1218
+ }
1219
+ });
1220
+ return () => {
1221
+ if (unsubscribeFn) {
1222
+ unsubscribeFn();
1223
+ } else {
1224
+ pendingUnsub = true;
1225
+ }
1226
+ };
1227
+ }
1228
+ };
1229
+ var DatabaseModule = class {
1230
+ constructor(http, projectId, realtime) {
1231
+ this.http = http;
1232
+ this.projectId = projectId;
1233
+ this.realtime = realtime ?? null;
1234
+ }
1235
+ collection(name) {
1236
+ return new CollectionRef(this.http, this.realtime, this.projectId, name);
1237
+ }
1238
+ };
1239
+
1240
+ // libs/sdk/src/modules/notifications.module.ts
1241
+ var DEVICE_ID_KEY = "spacelr_device_id";
1242
+ var NotificationsModule = class {
1243
+ constructor(http) {
1244
+ this.http = http;
1245
+ this.customDeviceId = null;
1246
+ this.customDeviceName = null;
1247
+ }
1248
+ /** Set a custom device ID (e.g. from Capacitor Preferences for persistence beyond localStorage) */
1249
+ setDeviceId(id) {
1250
+ this.customDeviceId = id;
1251
+ }
1252
+ /** Set a custom device name (e.g. "iOS App", "macOS - Chrome") */
1253
+ setDeviceName(name) {
1254
+ this.customDeviceName = name;
1255
+ }
1256
+ /** Get or generate a stable device identifier. Custom ID takes priority over localStorage. */
1257
+ getDeviceId() {
1258
+ if (this.customDeviceId) return this.customDeviceId;
1259
+ if (typeof localStorage === "undefined") return void 0;
1260
+ let id = localStorage.getItem(DEVICE_ID_KEY);
1261
+ if (!id) {
1262
+ id = crypto.randomUUID();
1263
+ localStorage.setItem(DEVICE_ID_KEY, id);
1264
+ }
1265
+ return id;
1266
+ }
1267
+ /** Get device name: custom name > auto-detected from user agent > undefined */
1268
+ getDeviceName() {
1269
+ if (this.customDeviceName) return this.customDeviceName;
1270
+ return this.detectDeviceName();
1271
+ }
1272
+ /** Auto-detect a short device label from navigator.userAgent (e.g. "macOS - Chrome") */
1273
+ detectDeviceName() {
1274
+ if (typeof navigator === "undefined" || !navigator.userAgent) return void 0;
1275
+ const ua = navigator.userAgent;
1276
+ let os = "Unknown";
1277
+ if (/iPad|iPhone|iPod/.test(ua)) os = "iOS";
1278
+ else if (/Android/.test(ua)) os = "Android";
1279
+ else if (/Mac OS X/.test(ua)) os = "macOS";
1280
+ else if (/Windows/.test(ua)) os = "Windows";
1281
+ else if (/Linux/.test(ua)) os = "Linux";
1282
+ let browser = "Unknown";
1283
+ if (/Edg\//.test(ua)) browser = "Edge";
1284
+ else if (/OPR\/|Opera/.test(ua)) browser = "Opera";
1285
+ else if (/Chrome\//.test(ua)) browser = "Chrome";
1286
+ else if (/Safari\//.test(ua) && !/Chrome/.test(ua)) browser = "Safari";
1287
+ else if (/Firefox\//.test(ua)) browser = "Firefox";
1288
+ return `${os} - ${browser}`;
1289
+ }
1290
+ /** Get the VAPID public key for Web Push setup */
1291
+ async getVapidPublicKey() {
1292
+ return this.http.request({
1293
+ method: "GET",
1294
+ path: "/notifications/vapid-key",
1295
+ authenticated: true
1296
+ });
1297
+ }
1298
+ /** Register a push subscription (web, android, or ios) */
1299
+ async subscribe(subscription, deviceName) {
1300
+ return this.http.request({
1301
+ method: "POST",
1302
+ path: "/notifications/subscribe",
1303
+ body: {
1304
+ ...subscription,
1305
+ deviceName: deviceName ?? this.getDeviceName(),
1306
+ deviceId: this.getDeviceId()
1307
+ },
1308
+ authenticated: true
1309
+ });
1310
+ }
1311
+ /** Unregister a push subscription */
1312
+ async unsubscribe(platform, identifier) {
1313
+ const body = { platform };
1314
+ if (platform === "web") {
1315
+ body["endpoint"] = identifier;
1316
+ } else {
1317
+ body["deviceToken"] = identifier;
1318
+ }
1319
+ return this.http.request({
1320
+ method: "DELETE",
1321
+ path: "/notifications/subscribe",
1322
+ body,
1323
+ authenticated: true
1324
+ });
1325
+ }
1326
+ /** Get all subscriptions for the current user */
1327
+ async getSubscriptions() {
1328
+ return this.http.request({
1329
+ method: "GET",
1330
+ path: "/notifications/subscriptions",
1331
+ authenticated: true
1332
+ });
1333
+ }
1334
+ /**
1335
+ * Helper: Register browser Web Push subscription.
1336
+ * Requests notification permission, subscribes via Push API,
1337
+ * and registers the subscription with the server.
1338
+ */
1339
+ async registerBrowserPush(serviceWorkerRegistration, deviceName) {
1340
+ const { publicKey } = await this.getVapidPublicKey();
1341
+ if (!publicKey) {
1342
+ throw new Error(
1343
+ "VAPID public key not configured on the server"
1344
+ );
1345
+ }
1346
+ const permission = await Notification.requestPermission();
1347
+ if (permission !== "granted") {
1348
+ throw new Error(
1349
+ `Notification permission denied: ${permission}`
1350
+ );
1351
+ }
1352
+ const applicationServerKey = this.urlBase64ToUint8Array(publicKey);
1353
+ const pushSubscription = await serviceWorkerRegistration.pushManager.subscribe({
1354
+ userVisibleOnly: true,
1355
+ applicationServerKey
1356
+ });
1357
+ const subJson = pushSubscription.toJSON();
1358
+ const p256dh = subJson.keys?.["p256dh"];
1359
+ const auth = subJson.keys?.["auth"];
1360
+ if (!subJson.endpoint || !p256dh || !auth) {
1361
+ throw new Error("Invalid push subscription: missing endpoint or keys");
1362
+ }
1363
+ return this.subscribe(
1364
+ {
1365
+ platform: "web",
1366
+ endpoint: subJson.endpoint,
1367
+ keys: { p256dh, auth }
1368
+ },
1369
+ deviceName
1370
+ );
1371
+ }
1372
+ /**
1373
+ * Helper: Register a mobile device push token (FCM or APNs).
1374
+ */
1375
+ async registerDevicePush(deviceToken, platform, deviceName) {
1376
+ return this.subscribe(
1377
+ {
1378
+ platform,
1379
+ deviceToken
1380
+ },
1381
+ deviceName
1382
+ );
1383
+ }
1384
+ urlBase64ToUint8Array(base64String) {
1385
+ const padding = "=".repeat((4 - base64String.length % 4) % 4);
1386
+ const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
1387
+ const rawData = atob(base64);
1388
+ const outputArray = new Uint8Array(rawData.length);
1389
+ for (let i = 0; i < rawData.length; ++i) {
1390
+ outputArray[i] = rawData.charCodeAt(i);
1391
+ }
1392
+ return outputArray.buffer;
1393
+ }
1394
+ };
1395
+
1396
+ // libs/sdk/src/client.ts
1397
+ function createClient(config) {
1398
+ const tokenStorage = config.tokenStorage ?? (typeof window !== "undefined" && typeof window.localStorage !== "undefined" ? new BrowserTokenStorage() : new MemoryTokenStorage());
1399
+ const tokenManager = new TokenManager(
1400
+ tokenStorage,
1401
+ config.refreshBufferSeconds ?? 60
1402
+ );
1403
+ const httpClient = new HttpClient(config, tokenManager);
1404
+ const realtime = new RealtimeClient({
1405
+ baseUrl: config.apiUrl,
1406
+ getToken: () => tokenManager.getAccessToken()
1407
+ });
1408
+ const auth = new AuthModule(httpClient, tokenManager, config);
1409
+ const storage = new StorageModule(httpClient, tokenManager, config);
1410
+ const db = new DatabaseModule(httpClient, config.projectId, realtime);
1411
+ const notifications = new NotificationsModule(httpClient);
1412
+ return {
1413
+ auth,
1414
+ storage,
1415
+ db,
1416
+ notifications,
1417
+ disconnect() {
1418
+ realtime.disconnect();
1419
+ }
1420
+ };
1421
+ }
1422
+ export {
1423
+ BrowserTokenStorage,
1424
+ CodeChallengeMethod,
1425
+ FileVisibility,
1426
+ GrantType,
1427
+ MemoryTokenStorage,
1428
+ SharePermission,
1429
+ SpacelrAuthError,
1430
+ SpacelrEmailVerificationRequiredError,
1431
+ SpacelrError,
1432
+ SpacelrNetworkError,
1433
+ SpacelrTimeoutError,
1434
+ SpacelrTwoFactorRequiredError,
1435
+ createClient,
1436
+ generatePKCEChallenge
1437
+ };
1438
+ //# sourceMappingURL=index.mjs.map