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