@rool-dev/sdk 0.12.5 → 2.0.0-dev.4adc2c2

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.
Files changed (108) hide show
  1. package/README.md +434 -786
  2. package/dist/agents.d.ts +130 -0
  3. package/dist/agents.d.ts.map +1 -0
  4. package/dist/agents.js +348 -0
  5. package/dist/agents.js.map +1 -0
  6. package/dist/auth-base.d.ts +43 -88
  7. package/dist/auth-base.d.ts.map +1 -1
  8. package/dist/auth-base.js +178 -302
  9. package/dist/auth-base.js.map +1 -1
  10. package/dist/auth-browser.d.ts +7 -25
  11. package/dist/auth-browser.d.ts.map +1 -1
  12. package/dist/auth-browser.js +45 -82
  13. package/dist/auth-browser.js.map +1 -1
  14. package/dist/auth-native.d.ts +9 -56
  15. package/dist/auth-native.d.ts.map +1 -1
  16. package/dist/auth-native.js +88 -177
  17. package/dist/auth-native.js.map +1 -1
  18. package/dist/auth-node.d.ts +32 -36
  19. package/dist/auth-node.d.ts.map +1 -1
  20. package/dist/auth-node.js +338 -265
  21. package/dist/auth-node.js.map +1 -1
  22. package/dist/auth-url.d.ts +6 -0
  23. package/dist/auth-url.d.ts.map +1 -0
  24. package/dist/auth-url.js +30 -0
  25. package/dist/auth-url.js.map +1 -0
  26. package/dist/client.d.ts +35 -290
  27. package/dist/client.d.ts.map +1 -1
  28. package/dist/client.js +275 -637
  29. package/dist/client.js.map +1 -1
  30. package/dist/events.d.ts +40 -0
  31. package/dist/events.d.ts.map +1 -0
  32. package/dist/events.js +175 -0
  33. package/dist/events.js.map +1 -0
  34. package/dist/files.d.ts +170 -0
  35. package/dist/files.d.ts.map +1 -0
  36. package/dist/files.js +846 -0
  37. package/dist/files.js.map +1 -0
  38. package/dist/index.d.ts +16 -19
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +7 -20
  41. package/dist/index.js.map +1 -1
  42. package/dist/machine.d.ts +49 -0
  43. package/dist/machine.d.ts.map +1 -0
  44. package/dist/machine.js +142 -0
  45. package/dist/machine.js.map +1 -0
  46. package/dist/problem.d.ts +11 -0
  47. package/dist/problem.d.ts.map +1 -0
  48. package/dist/problem.js +20 -0
  49. package/dist/problem.js.map +1 -0
  50. package/dist/structured.d.ts +84 -0
  51. package/dist/structured.d.ts.map +1 -0
  52. package/dist/structured.js +405 -0
  53. package/dist/structured.js.map +1 -0
  54. package/dist/types.d.ts +119 -520
  55. package/dist/types.d.ts.map +1 -1
  56. package/package.json +11 -16
  57. package/dist/auth.d.ts +0 -99
  58. package/dist/auth.d.ts.map +0 -1
  59. package/dist/auth.js +0 -217
  60. package/dist/auth.js.map +0 -1
  61. package/dist/client-info.d.ts +0 -18
  62. package/dist/client-info.d.ts.map +0 -1
  63. package/dist/client-info.js +0 -25
  64. package/dist/client-info.js.map +0 -1
  65. package/dist/event-emitter.d.ts +0 -45
  66. package/dist/event-emitter.d.ts.map +0 -1
  67. package/dist/event-emitter.js +0 -86
  68. package/dist/event-emitter.js.map +0 -1
  69. package/dist/graphql.d.ts +0 -90
  70. package/dist/graphql.d.ts.map +0 -1
  71. package/dist/graphql.js +0 -471
  72. package/dist/graphql.js.map +0 -1
  73. package/dist/logger.d.ts +0 -13
  74. package/dist/logger.d.ts.map +0 -1
  75. package/dist/logger.js +0 -17
  76. package/dist/logger.js.map +0 -1
  77. package/dist/path.d.ts +0 -6
  78. package/dist/path.d.ts.map +0 -1
  79. package/dist/path.js +0 -47
  80. package/dist/path.js.map +0 -1
  81. package/dist/reroute.d.ts +0 -22
  82. package/dist/reroute.d.ts.map +0 -1
  83. package/dist/reroute.js +0 -70
  84. package/dist/reroute.js.map +0 -1
  85. package/dist/rest.d.ts +0 -54
  86. package/dist/rest.d.ts.map +0 -1
  87. package/dist/rest.js +0 -171
  88. package/dist/rest.js.map +0 -1
  89. package/dist/router.d.ts +0 -20
  90. package/dist/router.d.ts.map +0 -1
  91. package/dist/router.js +0 -54
  92. package/dist/router.js.map +0 -1
  93. package/dist/space-session.d.ts +0 -30
  94. package/dist/space-session.d.ts.map +0 -1
  95. package/dist/space-session.js +0 -83
  96. package/dist/space-session.js.map +0 -1
  97. package/dist/space.d.ts +0 -161
  98. package/dist/space.d.ts.map +0 -1
  99. package/dist/space.js +0 -618
  100. package/dist/space.js.map +0 -1
  101. package/dist/subscription.d.ts +0 -36
  102. package/dist/subscription.d.ts.map +0 -1
  103. package/dist/subscription.js +0 -419
  104. package/dist/subscription.js.map +0 -1
  105. package/dist/webdav.d.ts +0 -171
  106. package/dist/webdav.d.ts.map +0 -1
  107. package/dist/webdav.js +0 -481
  108. package/dist/webdav.js.map +0 -1
package/dist/client.js CHANGED
@@ -1,676 +1,314 @@
1
- import { EventEmitter } from './event-emitter.js';
2
- import { AuthManager } from './auth.js';
3
- import { GraphQLClient } from './graphql.js';
4
- import { ClientSubscriptionManager } from './subscription.js';
5
- import { RestClient } from './rest.js';
6
- import { RoolSpace } from './space.js';
7
- import { SpaceRouter } from './router.js';
8
- import { generateEntityId } from './space-session.js';
9
- import { defaultLogger } from './logger.js';
10
- import { addClientInfoHeaders, resolveClientInfo } from './client-info.js';
11
- /**
12
- * Rool Client - Manages authentication, space lifecycle, and shared infrastructure.
13
- *
14
- * The client is lightweight - most operations happen on RoolSpace instances.
15
- *
16
- * Features:
17
- * - Authentication (login, logout, token management)
18
- * - Space lifecycle (list, create, delete, rename)
19
- * - Client-level subscription for lifecycle events
20
- * - User storage (cross-device key-value storage)
21
- */
22
- export class RoolClient extends EventEmitter {
23
- baseUrl;
24
- urls;
25
- authManager;
26
- graphqlClient;
27
- router;
28
- subscriptionManager = null;
29
- logger;
30
- clientInfo;
31
- _serverInfo = null;
32
- // Open spaces (cached for reuse)
33
- openSpaces = new Map();
34
- // User storage cache (in-memory; populated from server on initialize)
35
- _storageCache = {};
36
- // Current user (fetched during initialize)
37
- _currentUser = null;
1
+ import packageJson from "../package.json" with { type: "json" };
2
+ import { createRoolEvents } from "./events.js";
3
+ import { RoolMachine } from "./machine.js";
4
+ import { throwProblemResponse } from "./problem.js";
5
+ const DEFAULT_API_URL = "https://api.rool.dev";
6
+ export const roolSdkVersion = packageJson.version;
7
+ export class RoolClient {
8
+ apiUrl;
9
+ fetchRequest;
10
+ usesDefaultFetch;
11
+ getTokens;
12
+ onAuthInvalidated;
13
+ identity;
14
+ machines = new Map();
15
+ events;
38
16
  constructor(config = {}) {
39
- super();
40
- this.logger = config.logger ?? defaultLogger;
41
- this._emitterLogger = this.logger;
42
- this.clientInfo = resolveClientInfo(config.client);
43
- // Resolve API origin and auth URL.
44
- // Auth is derived by stripping the api. hostname prefix from the API URL.
45
- // For local dev (localhost etc.), set authUrl explicitly.
46
- const apiOrigin = ((config.apiUrl ?? config.baseUrl) ?? 'https://api.rool.dev').replace(/\/+$/, '');
47
- let authOrigin = apiOrigin;
48
- try {
49
- const parsed = new URL(apiOrigin);
50
- if (parsed.hostname.startsWith('api.')) {
51
- parsed.hostname = parsed.hostname.slice(4);
52
- authOrigin = parsed.origin;
53
- }
17
+ this.apiUrl = (config.apiUrl ?? DEFAULT_API_URL).replace(/\/+$/, "");
18
+ this.fetchRequest = config.fetch ?? globalThis.fetch.bind(globalThis);
19
+ this.usesDefaultFetch = config.fetch === undefined;
20
+ this.identity = config.client;
21
+ this.onAuthInvalidated = config.onAuthInvalidated;
22
+ if (config.getTokens && config.getAccessToken) {
23
+ throw new Error("getTokens and getAccessToken cannot be used together");
54
24
  }
55
- catch { /* keep apiOrigin as fallback */ }
56
- this.baseUrl = apiOrigin;
57
- this.urls = {
58
- graphql: config.graphqlUrl ?? `${apiOrigin}/graphql`,
59
- auth: config.authUrl ?? `${authOrigin}/auth`,
60
- webdav: apiOrigin,
61
- };
62
- this.authManager = new AuthManager({
63
- authUrl: this.urls.auth,
64
- authProvider: config.authProvider,
65
- logger: this.logger,
66
- onAuthStateChanged: (authenticated) => {
67
- // Covers every sign-out path (logout() and 401-driven token clearing).
68
- // Subscriptions must die with the session, or they would keep
69
- // reconnecting — and would pick up the next account's tokens.
70
- if (!authenticated) {
71
- this._storageCache = {};
72
- this.setCurrentUser(null);
73
- this.closeRealtimeResources();
74
- }
75
- this.emit('authStateChanged', authenticated);
76
- },
77
- });
78
- this.graphqlClient = new GraphQLClient({
79
- graphqlUrl: this.urls.graphql,
80
- authManager: this.authManager,
81
- clientInfo: this.clientInfo,
82
- });
83
- this.router = new SpaceRouter({
84
- apiUrl: this.baseUrl,
85
- authManager: this.authManager,
86
- clientInfo: this.clientInfo,
87
- });
88
- }
89
- /**
90
- * Initialize the client - should be called on app startup.
91
- * Processes any auth callback in the URL, sets up auto-refresh,
92
- * and starts real-time event subscription if authenticated.
93
- * @returns true if authenticated, false otherwise
94
- */
95
- async initialize() {
96
- this.authManager.initialize();
97
- const authenticated = await this.isAuthenticated();
98
- if (authenticated) {
99
- await this.hydrateAuthenticatedSession();
25
+ if (config.getTokens) {
26
+ this.getTokens = async () => (await config.getTokens()) ?? undefined;
100
27
  }
101
- return authenticated;
102
- }
103
- /**
104
- * Start the realtime subscription and load the current user/storage. Shared
105
- * by initialize() and verify() — any path that lands the user in an
106
- * authenticated state needs this hydration.
107
- *
108
- * Hydration is best-effort and never throws: a backend outage must not read
109
- * as a logout. The subscription owns reconnect/backoff and refetches the user
110
- * on (re)connect (see ensureSubscribed), so a session that boots while the
111
- * server is down stays authenticated and self-heals when it returns. Only a
112
- * hard auth failure ends the session: a refresh rejection (token layer) or
113
- * a 401 from the GraphQL transport, which calls authManager.logout().
114
- */
115
- async hydrateAuthenticatedSession() {
116
- this.ensureSubscribed().catch((error) => {
117
- this.logger.warn('[RoolClient] subscription start deferred:', error);
118
- });
119
- try {
120
- await this.fetchUserAndStorage();
28
+ else if (config.getAccessToken) {
29
+ this.getTokens = async () => {
30
+ const accessToken = await config.getAccessToken();
31
+ return accessToken ? { accessToken, roolToken: "" } : undefined;
32
+ };
121
33
  }
122
- catch (error) {
123
- this.logger.warn('[RoolClient] user hydration deferred (offline?):', error);
34
+ else {
35
+ this.getTokens = async () => undefined;
124
36
  }
37
+ this.events = createRoolEvents({
38
+ poll: (syncToken, signal) => {
39
+ const query = new URLSearchParams({ syncToken });
40
+ return this.request(`/v2/events?${query}`, { headers: { Prefer: "wait=45" }, signal }, true);
41
+ },
42
+ getSession: () => this.getSession(),
43
+ });
125
44
  }
126
- /**
127
- * Fetch the current user and populate the storage cache, then emit
128
- * currentUserChanged. Cache is set before the emit so listeners reading
129
- * getAllUserStorage() in response see fresh storage.
130
- */
131
- async fetchUserAndStorage() {
132
- const user = await this.graphqlClient.getCurrentUser();
133
- this._storageCache = user.storage ?? {};
134
- this.setCurrentUser(user);
135
- }
136
- setCurrentUser(user) {
137
- this._currentUser = user;
138
- this.emit('currentUserChanged', user);
139
- }
140
- /**
141
- * Clean up resources - call when destroying the client.
142
- */
143
- destroy() {
144
- this.authManager.destroy();
145
- this.closeRealtimeResources();
146
- this.removeAllListeners();
147
- }
148
- /**
149
- * Initiate login by redirecting to auth page.
150
- * @param appName - The name of the application requesting login (displayed on auth page)
151
- */
152
- async login(appName, params) {
153
- return this.authManager.login(appName, params);
154
- }
155
- /**
156
- * Initiate signup by redirecting to auth page.
157
- * @param appName - The name of the application requesting signup (displayed on auth page)
158
- * @param params - Optional additional query parameters to pass to the auth server
159
- */
160
- async signup(appName, params) {
161
- return this.authManager.signup(appName, params);
45
+ getSession() {
46
+ return this.requestJson("/v2/session");
162
47
  }
163
- /**
164
- * Complete an email verification flow using a token from the verification
165
- * email link. Exchanges the token for a live session and signs the user in
166
- * without a redirect. Intended to be called when the app detects a
167
- * `?verify=<token>` query parameter on load.
168
- *
169
- * Returns true if the user is now signed in as a result.
170
- */
171
- async verify(token) {
172
- const ok = await this.authManager.verify(token);
173
- if (ok) {
174
- await this.hydrateAuthenticatedSession();
175
- }
176
- return ok;
48
+ getAccount() {
49
+ return this.requestJson("/v2/me");
177
50
  }
178
- /**
179
- * Complete a native sign-in (PKCE) from a deep-link callback URL. Call this
180
- * from the app's platform deep-link handler (e.g. Capacitor's `appUrlOpen`)
181
- * with the full callback URL. Exchanges the code for a live session.
182
- *
183
- * Returns true if the user is now signed in as a result.
184
- */
185
- async handleAuthRedirect(url) {
186
- const ok = await this.authManager.handleRedirect(url);
187
- if (ok) {
188
- await this.hydrateAuthenticatedSession();
189
- }
190
- return ok;
51
+ async deleteAccount() {
52
+ await this.request("/v2/me", { method: "DELETE" });
191
53
  }
192
- /**
193
- * Sign in with email + password. Resolves to `{ status: 'signed_in' }` once
194
- * authenticated, or `{ status: 'verify_required' }` when the account's email
195
- * isn't verified yet (a magic link has been emailed). Rejects with a
196
- * human-readable Error on bad credentials or server failure.
197
- */
198
- async signInWithPassword(email, password) {
199
- const result = await this.authManager.signInWithPassword(email, password);
200
- if (result.status === 'signed_in') {
201
- await this.hydrateAuthenticatedSession();
202
- }
203
- return result;
54
+ getProfile() {
55
+ return this.requestJson("/v2/me/profile");
204
56
  }
205
- /**
206
- * Request a magic sign-in link by email. The server emails a link; the user
207
- * completes sign-in by following it, which is finished via `verify()` /
208
- * `handleAuthRedirect()` when it lands back in the app. Resolves once the
209
- * email is accepted; rejects with a human-readable Error on a bad address.
210
- */
211
- async requestMagicLink(email) {
212
- return this.authManager.requestMagicLink(email);
57
+ replaceProfile(profile) {
58
+ return this.requestJson("/v2/me/profile", {
59
+ method: "PUT",
60
+ headers: { "Content-Type": "application/json" },
61
+ body: JSON.stringify(profile),
62
+ });
213
63
  }
214
- /**
215
- * Set or change the current user's password. Other authenticated sessions are
216
- * revoked while this client receives replacement credentials.
217
- *
218
- * Throws an Error with a human-readable message on validation or server failure.
219
- */
220
- async setPassword(password) {
221
- return this.authManager.setPassword(password);
64
+ getUserAppData() {
65
+ return this.requestJson("/v2/me/app-data");
222
66
  }
223
- /**
224
- * Request an email address change for the current user. The server mails a
225
- * confirmation link to the new address; the change applies when that link
226
- * is clicked. After confirmation this session no longer belongs to the
227
- * account sign out and sign in with the new address.
228
- *
229
- * Rejects with EmailChangeError (code + user-facing message) on refusal,
230
- * e.g. 'email_in_use' or 'invalid_email'.
231
- */
232
- async requestEmailChange(newEmail) {
233
- return this.authManager.requestEmailChange(newEmail);
67
+ async setUserAppData(key, value) {
68
+ await this.request(`/v2/me/app-data/${encodeURIComponent(key)}`, {
69
+ method: "PUT",
70
+ headers: { "Content-Type": "application/json" },
71
+ body: JSON.stringify({ value }),
72
+ });
234
73
  }
235
- /**
236
- * Logout - clear all tokens and state.
237
- */
238
- logout() {
239
- this.authManager.logout();
240
- // Redundant when the provider notified (the auth-state handler already
241
- // tore down), but kept for providers without auth-state bridging.
242
- this.closeRealtimeResources();
74
+ async deleteUserAppData(key) {
75
+ await this.request(`/v2/me/app-data/${encodeURIComponent(key)}`, {
76
+ method: "DELETE",
77
+ });
243
78
  }
244
- /** Stop the client subscription and close all open spaces. Idempotent. */
245
- closeRealtimeResources() {
246
- this.unsubscribe();
247
- for (const space of this.openSpaces.values())
248
- space.close();
249
- this.openSpaces.clear();
79
+ getGreeting(language) {
80
+ if (!language)
81
+ return this.requestJson("/v2/greeting");
82
+ const query = new URLSearchParams({ language });
83
+ return this.requestJson(`/v2/greeting?${query}`);
250
84
  }
251
- /**
252
- * Check if user is currently authenticated (validates token is usable).
253
- */
254
- async isAuthenticated() {
255
- return this.authManager.isAuthenticated();
85
+ listMachines() {
86
+ return this.requestJson("/v2/machines");
256
87
  }
257
- /**
258
- * Make an authenticated fetch request to the Rool API.
259
- * @internal Not part of the public API — use typed methods instead.
260
- *
261
- * @param path - Path relative to the base URL (e.g., '/billing/usage')
262
- * @param init - Standard fetch RequestInit options. Authorization header is added automatically.
263
- */
264
- async _api(path, init) {
265
- const tokens = await this.authManager.getTokens();
266
- if (!tokens)
267
- throw new Error('Not authenticated');
268
- const headers = new Headers(init?.headers);
269
- headers.set('Authorization', `Bearer ${tokens.accessToken}`);
270
- headers.set('X-Rool-Token', tokens.roolToken);
271
- addClientInfoHeaders(headers, this.clientInfo);
272
- return fetch(`${this.baseUrl}${path}`, { ...init, headers });
88
+ createMachine(settings) {
89
+ return this.requestJson("/v2/machines", {
90
+ method: "POST",
91
+ headers: { "Content-Type": "application/json" },
92
+ body: JSON.stringify(settings),
93
+ });
273
94
  }
274
- /**
275
- * Get auth identity decoded from JWT token.
276
- * For the Rool user (with server-assigned id), use currentUser.
277
- */
278
- getAuthUser() {
279
- return this.authManager.getAuthUser();
95
+ machine(machineId) {
96
+ let machine = this.machines.get(machineId);
97
+ if (machine)
98
+ return machine;
99
+ machine = new RoolMachine(machineId, {
100
+ send: (path, init, onUploadProgress) => this.send(path, init, { onUploadProgress }),
101
+ request: (path, init, allowHttpErrors) => this.request(path, init, allowHttpErrors),
102
+ requestJson: (path, init) => this.requestJson(path, init),
103
+ deleted: () => this.machines.delete(machineId),
104
+ });
105
+ this.machines.set(machineId, machine);
106
+ return machine;
280
107
  }
281
- /**
282
- * Get the current Rool user (cached from initialize).
283
- * Available after successful authentication.
284
- */
285
- get currentUser() {
286
- return this._currentUser;
108
+ getInvitePreview(token) {
109
+ return this.requestJson(`/v2/invites/${encodeURIComponent(token)}`);
287
110
  }
288
- get serverInfo() {
289
- return this._serverInfo;
111
+ redeemInvite(token) {
112
+ return this.requestJson(`/v2/invites/${encodeURIComponent(token)}/redemption`, { method: "POST" });
290
113
  }
291
- /**
292
- * List all spaces accessible to the user.
293
- */
294
- async listSpaces() {
295
- return this.graphqlClient.listSpaces();
114
+ /** Look up a gift by its code without claiming it. */
115
+ previewGift(code) {
116
+ return this.requestJson(`/v2/gifts/${encodeURIComponent(code)}`);
296
117
  }
297
- /**
298
- * Open a space with a real-time subscription.
299
- * Returns a live RoolSpace handle with conversation and file events.
300
- * Reuses an existing handle if the space is already open.
301
- *
302
- * Call space.close() when done to stop the subscription.
303
- */
304
- async openSpace(spaceId) {
305
- // Reuse existing open space
306
- const existing = this.openSpaces.get(spaceId);
307
- if (existing)
308
- return existing;
309
- // Ensure client subscription is active (for lifecycle events)
310
- // .catch prevents a rejection here from crashing Node before the caller awaits it.
311
- this.ensureSubscribed().catch(() => { });
312
- const initialRoute = await this.router.resolve(spaceId);
313
- const scopedGraphqlUrl = `${initialRoute.server.replace(/\/+$/, '')}/graphql`;
314
- const scopedClient = new GraphQLClient({
315
- graphqlUrl: scopedGraphqlUrl,
316
- authManager: this.authManager,
317
- clientInfo: this.clientInfo,
318
- });
319
- const scopedRestClient = new RestClient({
320
- apiUrl: initialRoute.server,
321
- authManager: this.authManager,
322
- clientInfo: this.clientInfo,
118
+ /** Claim a gift for the current account. */
119
+ claimGift(code) {
120
+ return this.requestJson(`/v2/gifts/${encodeURIComponent(code)}/claim`, {
121
+ method: "POST",
323
122
  });
324
- const openSpaceResult = await scopedClient.openSpace(spaceId);
325
- const space = new RoolSpace({
326
- id: spaceId,
327
- openSpaceResult,
328
- graphqlClient: scopedClient,
329
- restClient: scopedRestClient,
330
- authManager: this.authManager,
331
- clientInfo: this.clientInfo,
332
- router: this.router,
333
- initialRoute,
334
- logger: this.logger,
335
- onClose: () => this.openSpaces.delete(spaceId),
336
- });
337
- this.openSpaces.set(spaceId, space);
338
- return space;
339
123
  }
340
- /**
341
- * Create a new space.
342
- * Returns a RoolSpace handle with a real-time subscription.
343
- * Use the returned RoolSpace to work with objects, conversations, and AI.
344
- */
345
- async createSpace(name) {
346
- // Prevents a rejection here from crashing Node before the caller awaits it.
347
- this.ensureSubscribed().catch(() => { });
348
- const { spaceId } = await this.graphqlClient.createSpace(name);
349
- return this.openSpace(spaceId);
124
+ /** List the current user's claimed and unclaimed gifts. */
125
+ listGifts() {
126
+ return this.requestJson("/v2/me/gifts");
350
127
  }
351
- /**
352
- * GraphQL client pinned to a space's owning node, rerouting on refusal.
353
- * Space-scoped mutations must not go to an arbitrary shard via the base URL.
354
- */
355
- async scopedGraphqlClient(spaceId) {
356
- const scopedUrl = (server) => `${server.replace(/\/+$/, '')}/graphql`;
357
- const route = await this.router.resolve(spaceId);
358
- return new GraphQLClient({
359
- graphqlUrl: scopedUrl(route.server),
360
- authManager: this.authManager,
361
- clientInfo: this.clientInfo,
362
- onRefused: async () => scopedUrl((await this.router.resolve(spaceId)).server),
128
+ /** Set or clear a gift's note, or change its archived state. */
129
+ updateGift(giftId, changes) {
130
+ return this.requestJson(`/v2/me/gifts/${encodeURIComponent(giftId)}`, {
131
+ method: "PATCH",
132
+ headers: { "Content-Type": "application/json" },
133
+ body: JSON.stringify(changes),
363
134
  });
364
135
  }
365
- /**
366
- * Delete a space.
367
- * Note: This closes any cached open RoolSpace handle.
368
- */
369
- async deleteSpace(spaceId) {
370
- const scoped = await this.scopedGraphqlClient(spaceId);
371
- await scoped.deleteSpace(spaceId);
372
- // Close and remove the cached space if open
373
- const space = this.openSpaces.get(spaceId);
374
- if (space) {
375
- space.close();
376
- this.openSpaces.delete(spaceId);
377
- }
378
- // Client-level event will be emitted via SSE subscription
379
- }
380
- /**
381
- * Duplicate an existing space. Returns a handle to the new space.
382
- */
383
- async duplicateSpace(sourceSpaceId, name) {
384
- this.ensureSubscribed().catch(() => { });
385
- const scoped = await this.scopedGraphqlClient(sourceSpaceId);
386
- const { spaceId } = await scoped.duplicateSpace(sourceSpaceId, name);
387
- return this.openSpace(spaceId);
388
- }
389
- /**
390
- * Mark the current user for deletion (7-day grace period).
391
- * Irrecoverable after the grace period elapses.
392
- */
393
- async deleteCurrentUser() {
394
- await this.graphqlClient.deleteCurrentUser();
395
- this.logout();
396
- }
397
- /**
398
- * Import a space from a zip archive.
399
- * Creates a new space with the given name and imports objects, relations, and files.
400
- * Returns a RoolSpace handle.
401
- */
402
- async importArchive(name, archive) {
403
- // .catch prevents a rejection here from crashing Node before the caller awaits it.
404
- this.ensureSubscribed().catch(() => { });
405
- // Import via REST endpoint (creates the space)
406
- const spaceId = await this.restClient.importArchive(name, archive);
407
- // Open the space to get its data
408
- return this.openSpace(spaceId);
409
- }
410
- /**
411
- * Get the current Rool user from the server.
412
- * Returns the user's server-assigned id, email, plan, and credits.
413
- */
414
- async getCurrentUser() {
415
- const user = await this.graphqlClient.getCurrentUser();
416
- // On first hydration, populate the storage cache before emitting so
417
- // listeners don't see a user with empty storage. Once hydrated, a polled
418
- // snapshot must not clobber optimistic setUserStorage writes.
419
- if (!this._currentUser) {
420
- this._storageCache = user.storage ?? {};
421
- }
422
- this.setCurrentUser(user);
423
- return user;
424
- }
425
- /**
426
- * Look up an invite link by its token, without redeeming it.
427
- * Does not require authentication.
428
- */
429
- async previewInvite(token) {
430
- return this.restClient.previewInvite(token);
136
+ /** Mint a new code for an unclaimed gift. */
137
+ rotateGiftCode(giftId) {
138
+ return this.requestJson(`/v2/me/gifts/${encodeURIComponent(giftId)}/code`, {
139
+ method: "POST",
140
+ });
431
141
  }
432
- /**
433
- * Redeem an invite link, joining the space it belongs to.
434
- */
435
- async redeemInvite(token) {
436
- return this.restClient.redeemInvite(token);
142
+ async requestJson(path, init) {
143
+ const response = await this.request(path, init);
144
+ return response.json();
437
145
  }
438
- /**
439
- * Look up a gift by its code, without claiming it.
440
- * Does not require authentication.
441
- */
442
- async previewGift(code) {
443
- return this.restClient.previewGift(code);
146
+ async request(path, init, allowHttpErrors = false) {
147
+ const accept = new Headers(init?.headers).get("Accept") ?? "application/json";
148
+ const response = await this.send(path, init, { accept });
149
+ if (response.status === 401)
150
+ void this.onAuthInvalidated?.();
151
+ if (response.ok || allowHttpErrors)
152
+ return response;
153
+ return throwProblemResponse(response);
444
154
  }
445
- /**
446
- * Claim a gift, granting what it holds to the current account.
447
- * Refetches the user, since any gift kind changes their standing.
448
- */
449
- async claimGift(code) {
450
- const result = await this.restClient.claimGift(code);
451
- // The claim is spent and irreversible by now, so a failed refresh must not
452
- // surface as a failed claim: the retry would report gift_claimed.
453
- try {
454
- await this.getCurrentUser();
155
+ async send(path, init, options = {}) {
156
+ const headers = new Headers(init?.headers);
157
+ if (options.accept)
158
+ headers.set("Accept", options.accept);
159
+ headers.set("X-Rool-SDK-Name", packageJson.name);
160
+ headers.set("X-Rool-SDK-Version", packageJson.version);
161
+ if (this.identity?.appName) {
162
+ headers.set("X-Rool-App-Name", this.identity.appName);
455
163
  }
456
- catch (error) {
457
- this.logger.warn('[RoolClient] user refresh after gift claim deferred:', error);
164
+ if (this.identity?.appVersion) {
165
+ headers.set("X-Rool-App-Version", this.identity.appVersion);
458
166
  }
459
- return result;
460
- }
461
- /**
462
- * The current user's gifts, spent and unspent.
463
- */
464
- async listGifts() {
465
- return this.restClient.listGifts();
466
- }
467
- /**
468
- * Update one of the current user's gifts: set or clear its note, or change
469
- * its archived state. Returns the updated gift.
470
- */
471
- async updateGift(giftId, changes) {
472
- return this.restClient.updateGift(giftId, changes);
473
- }
474
- /**
475
- * Mint a new code for one of the current user's gifts. The old code and
476
- * link stop working; the gift itself is untouched.
477
- */
478
- async rotateGiftCode(giftId) {
479
- return this.restClient.rotateGiftCode(giftId);
480
- }
481
- /**
482
- * Update the current user's profile.
483
- * - name: display name
484
- * - slug: used in app publishing URLs (3-32 chars, start with letter, lowercase alphanumeric/hyphens/underscores)
485
- */
486
- async updateCurrentUser(input) {
487
- const user = await this.graphqlClient.updateCurrentUser(input);
488
- this.setCurrentUser(user);
489
- return user;
490
- }
491
- /**
492
- * Get a value from user storage (sync read from in-memory cache).
493
- * Cache is populated from the server on initialize().
494
- * Returns undefined if key doesn't exist.
495
- */
496
- getUserStorage(key) {
497
- return this._storageCache[key];
498
- }
499
- /**
500
- * Set a value in user storage.
501
- * Updates in-memory cache immediately, then syncs to server.
502
- * Pass undefined/null to delete the key.
503
- * Storage is limited to 10MB total.
504
- */
505
- setUserStorage(key, value) {
506
- // Update local cache
507
- if (value === null || value === undefined) {
508
- delete this._storageCache[key];
167
+ if (this.identity?.osVersion) {
168
+ headers.set("X-Rool-OS-Version", this.identity.osVersion);
509
169
  }
510
- else {
511
- this._storageCache[key] = value;
170
+ const tokens = await this.getTokens();
171
+ if (tokens?.accessToken) {
172
+ headers.set("Authorization", `Bearer ${tokens.accessToken}`);
512
173
  }
513
- // Emit event (local source)
514
- this.emit('userStorageChanged', { key, value: value ?? null, source: 'local' });
515
- // Fire-and-forget server sync
516
- this.graphqlClient.setUserStorage(key, value).catch((error) => {
517
- this.logger.error('[RoolClient] Failed to sync user storage:', error);
518
- this.emit('error', error instanceof Error ? error : new Error(String(error)), 'userStorage');
519
- });
520
- }
521
- /**
522
- * Get all user storage data (sync read from local cache).
523
- */
524
- getAllUserStorage() {
525
- return { ...this._storageCache };
526
- }
527
- /**
528
- * Report an event to the server.
529
- * Fire-and-forget — errors are logged but not propagated.
530
- */
531
- reportEvent(event, url) {
532
- this.graphqlClient.reportEvent(event, url).catch((error) => {
533
- this.logger.error('[RoolClient] Failed to report event:', error);
534
- });
535
- }
536
- get restClient() {
537
- return new RestClient({
538
- apiUrl: this.baseUrl,
539
- authManager: this.authManager,
540
- clientInfo: this.clientInfo,
541
- });
542
- }
543
- /**
544
- * Ensure the client-level event subscription is active.
545
- * Called automatically when opening spaces.
546
- * @internal
547
- */
548
- async ensureSubscribed() {
549
- if (this.subscriptionManager)
550
- return;
551
- this.subscriptionManager = new ClientSubscriptionManager({
552
- graphqlUrl: this.urls.graphql,
553
- authManager: this.authManager,
554
- clientInfo: this.clientInfo,
555
- logger: this.logger,
556
- onEvent: (event) => this.handleClientEvent(event),
557
- onConnectionStateChanged: (state) => {
558
- this.emit('connectionStateChanged', state);
559
- // Finish a deferred hydration: if we booted while the server was down,
560
- // _currentUser is still null. Now that we're connected, fetch the user
561
- // and storage so the app can complete sign-in.
562
- if (state === 'connected' && !this._currentUser) {
563
- this.fetchUserAndStorage().catch((error) => {
564
- this.logger.warn('[RoolClient] deferred user hydration failed:', error);
565
- });
566
- }
567
- },
568
- onError: (error) => {
569
- this.emit('error', error, 'subscription');
570
- },
571
- });
572
- await this.subscriptionManager.subscribe();
573
- }
574
- /**
575
- * Disconnect from real-time events.
576
- * @internal
577
- */
578
- unsubscribe() {
579
- if (this.subscriptionManager) {
580
- this.subscriptionManager.destroy();
581
- this.subscriptionManager = null;
174
+ if (tokens?.roolToken)
175
+ headers.set("X-Rool-Token", tokens.roolToken);
176
+ const requestInit = {
177
+ ...init,
178
+ headers,
179
+ };
180
+ if (isReadableStream(init?.body))
181
+ requestInit.duplex = "half";
182
+ const url = `${this.apiUrl}${path}`;
183
+ const body = init?.body;
184
+ if (!options.onUploadProgress || body === undefined || body === null) {
185
+ return this.fetchRequest(url, requestInit);
582
186
  }
583
- }
584
- /**
585
- * Generate a unique 6-character alphanumeric ID.
586
- */
587
- static generateId() {
588
- return generateEntityId();
589
- }
590
- /**
591
- * Execute an arbitrary GraphQL query or mutation.
592
- * @internal Not part of the public API — use typed methods instead.
593
- */
594
- async _graphql(query, variables) {
595
- return this.graphqlClient.query(query, variables);
596
- }
597
- /**
598
- * Handle a client-level event from the subscription.
599
- * @internal
600
- */
601
- handleClientEvent(event) {
602
- switch (event.type) {
603
- case 'connected': {
604
- const info = {
605
- version: event.serverVersion,
606
- minimumSdkVersion: event.minimumSdkVersion,
607
- compatibility: event.compatibility ?? 'ok',
608
- };
609
- this._serverInfo = info;
610
- this.emit('serverInfoChanged', info);
611
- if (info.compatibility === 'unsupported')
612
- this.emit('unsupported', info);
613
- break;
614
- }
615
- case 'space_created':
616
- this.emit('spaceAdded', {
617
- id: event.spaceId,
618
- name: event.name,
619
- inboundEmailAddress: event.inboundEmailAddress ?? '',
620
- role: event.role ?? 'owner',
621
- ownerId: event.ownerId ?? '',
622
- size: event.size ?? 0,
623
- createdAt: event.createdAt ?? new Date().toISOString(),
624
- updatedAt: event.updatedAt ?? new Date().toISOString(),
625
- memberCount: 1, // Creator is the only member
626
- });
627
- break;
628
- case 'space_deleted':
629
- this.emit('spaceRemoved', event.spaceId);
630
- break;
631
- case 'space_renamed':
632
- this.emit('spaceRenamed', event.spaceId, event.name);
633
- break;
634
- case 'space_access_changed':
635
- if (event.role === 'none') {
636
- this.emit('spaceRemoved', event.spaceId);
637
- }
638
- else {
639
- this.emit('spaceAdded', {
640
- id: event.spaceId,
641
- name: event.name,
642
- inboundEmailAddress: event.inboundEmailAddress,
643
- role: event.role,
644
- ownerId: event.ownerId,
645
- size: event.size,
646
- createdAt: event.createdAt,
647
- updatedAt: event.updatedAt,
648
- memberCount: event.memberCount,
649
- });
650
- }
651
- break;
652
- case 'user_storage_changed':
653
- this.handleUserStorageChanged(event.key, event.value);
654
- break;
187
+ const useXmlHttpRequest = this.usesDefaultFetch &&
188
+ typeof XMLHttpRequest !== "undefined" &&
189
+ !isReadableStream(body);
190
+ if (useXmlHttpRequest) {
191
+ return sendXmlHttpRequest(url, requestInit, body, options.onUploadProgress);
655
192
  }
193
+ const progressRequest = requestWithUploadProgress(url, requestInit, body, options.onUploadProgress);
194
+ return this.fetchRequest(url, progressRequest);
656
195
  }
657
- /**
658
- * Handle a user storage change from SSE (remote update).
659
- * Updates cache and emits event if value actually changed.
660
- * @internal
661
- */
662
- handleUserStorageChanged(key, value) {
663
- const currentValue = this._storageCache[key];
664
- // Only update and emit if value actually changed
665
- if (JSON.stringify(currentValue) !== JSON.stringify(value)) {
666
- if (value === null || value === undefined) {
667
- delete this._storageCache[key];
668
- }
669
- else {
670
- this._storageCache[key] = value;
671
- }
672
- this.emit('userStorageChanged', { key, value: value ?? null, source: 'remote' });
196
+ }
197
+ function isReadableStream(body) {
198
+ return (typeof ReadableStream !== "undefined" && body instanceof ReadableStream);
199
+ }
200
+ function requestWithUploadProgress(url, init, body, onUploadProgress) {
201
+ const preparedInit = { ...init };
202
+ if (isReadableStream(body))
203
+ preparedInit.duplex = "half";
204
+ const preparedRequest = new Request(url, preparedInit);
205
+ if (!preparedRequest.body)
206
+ throw new Error("Upload request is missing its body");
207
+ const totalBytes = uploadBodySize(body);
208
+ let transferredBytes = 0;
209
+ reportUploadProgress(onUploadProgress, transferredBytes, totalBytes);
210
+ const progressBody = preparedRequest.body.pipeThrough(new TransformStream({
211
+ transform(chunk, controller) {
212
+ controller.enqueue(chunk);
213
+ transferredBytes += chunk.byteLength;
214
+ reportUploadProgress(onUploadProgress, transferredBytes, totalBytes);
215
+ },
216
+ }));
217
+ return {
218
+ ...init,
219
+ headers: preparedRequest.headers,
220
+ body: progressBody,
221
+ duplex: "half",
222
+ };
223
+ }
224
+ function sendXmlHttpRequest(url, init, body, onUploadProgress) {
225
+ return new Promise((resolve, reject) => {
226
+ const request = new XMLHttpRequest();
227
+ const signal = init.signal;
228
+ const totalBytes = uploadBodySize(body);
229
+ let transferredBytes = 0;
230
+ const cleanup = () => signal?.removeEventListener("abort", abort);
231
+ const abort = () => request.abort();
232
+ request.upload.onprogress = (event) => {
233
+ transferredBytes = event.loaded;
234
+ const reportedTotal = event.lengthComputable ? event.total : totalBytes;
235
+ reportUploadProgress(onUploadProgress, transferredBytes, reportedTotal);
236
+ };
237
+ request.upload.onload = (event) => {
238
+ const loadedBytes = event.loaded;
239
+ if (loadedBytes === transferredBytes)
240
+ return;
241
+ transferredBytes = loadedBytes;
242
+ const reportedTotal = event.lengthComputable ? event.total : totalBytes;
243
+ reportUploadProgress(onUploadProgress, transferredBytes, reportedTotal);
244
+ };
245
+ request.onload = () => {
246
+ cleanup();
247
+ const hasResponseBody = request.response instanceof ArrayBuffer &&
248
+ request.response.byteLength > 0;
249
+ const responseBody = hasResponseBody ? request.response : null;
250
+ resolve(new Response(responseBody, {
251
+ status: request.status,
252
+ statusText: request.statusText,
253
+ headers: parseXmlHttpRequestHeaders(request.getAllResponseHeaders()),
254
+ }));
255
+ };
256
+ request.onerror = () => {
257
+ cleanup();
258
+ reject(new TypeError("Network request failed"));
259
+ };
260
+ request.onabort = () => {
261
+ cleanup();
262
+ reject(signal?.reason ??
263
+ new DOMException("This operation was aborted", "AbortError"));
264
+ };
265
+ if (signal?.aborted) {
266
+ reject(signal.reason ??
267
+ new DOMException("This operation was aborted", "AbortError"));
268
+ return;
673
269
  }
674
- }
270
+ request.open(init.method ?? "GET", url);
271
+ request.responseType = "arraybuffer";
272
+ request.withCredentials = init.credentials === "include";
273
+ new Headers(init.headers).forEach((value, name) => {
274
+ request.setRequestHeader(name, value);
275
+ });
276
+ signal?.addEventListener("abort", abort, { once: true });
277
+ reportUploadProgress(onUploadProgress, transferredBytes, totalBytes);
278
+ request.send(body);
279
+ });
280
+ }
281
+ function reportUploadProgress(listener, transferredBytes, totalBytes) {
282
+ const progress = { transferredBytes };
283
+ if (totalBytes !== undefined)
284
+ progress.totalBytes = totalBytes;
285
+ listener(progress);
286
+ }
287
+ function uploadBodySize(body) {
288
+ if (typeof body === "string")
289
+ return new TextEncoder().encode(body).byteLength;
290
+ if (typeof Blob !== "undefined" && body instanceof Blob)
291
+ return body.size;
292
+ if (body instanceof ArrayBuffer)
293
+ return body.byteLength;
294
+ if (ArrayBuffer.isView(body))
295
+ return body.byteLength;
296
+ if (typeof URLSearchParams !== "undefined" &&
297
+ body instanceof URLSearchParams) {
298
+ return new TextEncoder().encode(body.toString()).byteLength;
299
+ }
300
+ return undefined;
301
+ }
302
+ function parseXmlHttpRequestHeaders(rawHeaders) {
303
+ const headers = new Headers();
304
+ for (const line of rawHeaders.split(/\r?\n/)) {
305
+ if (!line)
306
+ continue;
307
+ const separator = line.indexOf(":");
308
+ if (separator === -1)
309
+ throw new Error(`Invalid response header: ${line}`);
310
+ headers.append(line.slice(0, separator), line.slice(separator + 1).trim());
311
+ }
312
+ return headers;
675
313
  }
676
314
  //# sourceMappingURL=client.js.map