@rool-dev/sdk 0.12.5 → 2.0.0-dev.36748aa

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 +436 -784
  2. package/dist/agents.d.ts +138 -0
  3. package/dist/agents.d.ts.map +1 -0
  4. package/dist/agents.js +563 -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 +323 -635
  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 +174 -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 +51 -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,364 @@
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
+ const MACHINE_ROUTE_CHANGED_HEADER = "Rool-Machine-Route-Changed";
7
+ const ROUTE_CHANGE_MAX_RETRIES = 6;
8
+ const ROUTE_CHANGE_RETRY_BASE_MS = 150;
9
+ const ROUTE_CHANGE_RETRY_MAX_MS = 5_000;
10
+ export const roolSdkVersion = packageJson.version;
11
+ export class RoolClient {
12
+ apiUrl;
13
+ fetchRequest;
14
+ usesDefaultFetch;
15
+ getTokens;
16
+ onAuthInvalidated;
17
+ identity;
18
+ machines = new Map();
19
+ events;
38
20
  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
- }
54
- }
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();
100
- }
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();
21
+ this.apiUrl = (config.apiUrl ?? DEFAULT_API_URL).replace(/\/+$/, "");
22
+ this.fetchRequest = config.fetch ?? globalThis.fetch.bind(globalThis);
23
+ this.usesDefaultFetch = config.fetch === undefined;
24
+ this.identity = config.client;
25
+ this.onAuthInvalidated = config.onAuthInvalidated;
26
+ if (config.getTokens && config.getAccessToken) {
27
+ throw new Error("getTokens and getAccessToken cannot be used together");
121
28
  }
122
- catch (error) {
123
- this.logger.warn('[RoolClient] user hydration deferred (offline?):', error);
29
+ if (config.getTokens) {
30
+ this.getTokens = async () => (await config.getTokens()) ?? undefined;
124
31
  }
125
- }
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);
162
- }
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;
177
- }
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();
32
+ else if (config.getAccessToken) {
33
+ this.getTokens = async () => {
34
+ const accessToken = await config.getAccessToken();
35
+ return accessToken ? { accessToken, roolToken: "" } : undefined;
36
+ };
189
37
  }
190
- return ok;
191
- }
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();
38
+ else {
39
+ this.getTokens = async () => undefined;
202
40
  }
203
- return result;
204
- }
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);
213
- }
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);
222
- }
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);
234
- }
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();
243
- }
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();
250
- }
251
- /**
252
- * Check if user is currently authenticated (validates token is usable).
253
- */
254
- async isAuthenticated() {
255
- return this.authManager.isAuthenticated();
256
- }
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 });
41
+ this.events = createRoolEvents({
42
+ poll: (syncToken, signal) => {
43
+ const query = new URLSearchParams({ syncToken });
44
+ return this.request(`/v2/events?${query}`, { headers: { Prefer: "wait=45" }, signal }, true);
45
+ },
46
+ getSession: () => this.getSession(),
47
+ });
273
48
  }
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();
49
+ getSession() {
50
+ return this.requestJson("/v2/session");
280
51
  }
281
- /**
282
- * Get the current Rool user (cached from initialize).
283
- * Available after successful authentication.
284
- */
285
- get currentUser() {
286
- return this._currentUser;
52
+ getAccount() {
53
+ return this.requestJson("/v2/me");
287
54
  }
288
- get serverInfo() {
289
- return this._serverInfo;
55
+ async deleteAccount() {
56
+ await this.request("/v2/me", { method: "DELETE" });
290
57
  }
291
- /**
292
- * List all spaces accessible to the user.
293
- */
294
- async listSpaces() {
295
- return this.graphqlClient.listSpaces();
58
+ getProfile() {
59
+ return this.requestJson("/v2/me/profile");
296
60
  }
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,
61
+ replaceProfile(profile) {
62
+ return this.requestJson("/v2/me/profile", {
63
+ method: "PUT",
64
+ headers: { "Content-Type": "application/json" },
65
+ body: JSON.stringify(profile),
323
66
  });
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
67
  }
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);
68
+ getUserAppData() {
69
+ return this.requestJson("/v2/me/app-data");
350
70
  }
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),
71
+ async setUserAppData(key, value) {
72
+ await this.request(`/v2/me/app-data/${encodeURIComponent(key)}`, {
73
+ method: "PUT",
74
+ headers: { "Content-Type": "application/json" },
75
+ body: JSON.stringify({ value }),
363
76
  });
364
77
  }
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);
78
+ async deleteUserAppData(key) {
79
+ await this.request(`/v2/me/app-data/${encodeURIComponent(key)}`, {
80
+ method: "DELETE",
81
+ });
431
82
  }
432
- /**
433
- * Redeem an invite link, joining the space it belongs to.
434
- */
435
- async redeemInvite(token) {
436
- return this.restClient.redeemInvite(token);
83
+ getGreeting(language) {
84
+ if (!language)
85
+ return this.requestJson("/v2/greeting");
86
+ const query = new URLSearchParams({ language });
87
+ return this.requestJson(`/v2/greeting?${query}`);
437
88
  }
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);
89
+ listMachines() {
90
+ return this.requestJson("/v2/machines");
444
91
  }
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();
455
- }
456
- catch (error) {
457
- this.logger.warn('[RoolClient] user refresh after gift claim deferred:', error);
458
- }
459
- return result;
460
- }
461
- /**
462
- * The current user's gifts, spent and unspent.
463
- */
464
- async listGifts() {
465
- return this.restClient.listGifts();
92
+ createMachine(settings) {
93
+ return this.requestJson("/v2/machines", {
94
+ method: "POST",
95
+ headers: { "Content-Type": "application/json" },
96
+ body: JSON.stringify(settings),
97
+ });
466
98
  }
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);
99
+ machine(machineId) {
100
+ let machine = this.machines.get(machineId);
101
+ if (machine)
102
+ return machine;
103
+ machine = new RoolMachine(machineId, {
104
+ send: (path, init, onUploadProgress) => this.send(path, init, { onUploadProgress }),
105
+ request: (path, init, allowHttpErrors) => this.request(path, init, allowHttpErrors),
106
+ requestJson: (path, init) => this.requestJson(path, init),
107
+ subscribeEvents: (listener) => this.events.subscribe(listener),
108
+ deleted: () => this.machines.delete(machineId),
109
+ });
110
+ this.machines.set(machineId, machine);
111
+ return machine;
473
112
  }
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);
113
+ getInvitePreview(token) {
114
+ return this.requestJson(`/v2/invites/${encodeURIComponent(token)}`);
480
115
  }
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;
116
+ redeemInvite(token) {
117
+ return this.requestJson(`/v2/invites/${encodeURIComponent(token)}/redemption`, { method: "POST" });
490
118
  }
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];
119
+ /** Look up a gift by its code without claiming it. */
120
+ previewGift(code) {
121
+ return this.requestJson(`/v2/gifts/${encodeURIComponent(code)}`);
498
122
  }
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];
509
- }
510
- else {
511
- this._storageCache[key] = value;
512
- }
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');
123
+ /** Claim a gift for the current account. */
124
+ claimGift(code) {
125
+ return this.requestJson(`/v2/gifts/${encodeURIComponent(code)}/claim`, {
126
+ method: "POST",
519
127
  });
520
128
  }
521
- /**
522
- * Get all user storage data (sync read from local cache).
523
- */
524
- getAllUserStorage() {
525
- return { ...this._storageCache };
129
+ /** List the current user's claimed and unclaimed gifts. */
130
+ listGifts() {
131
+ return this.requestJson("/v2/me/gifts");
526
132
  }
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);
133
+ /** Set or clear a gift's note, or change its archived state. */
134
+ updateGift(giftId, changes) {
135
+ return this.requestJson(`/v2/me/gifts/${encodeURIComponent(giftId)}`, {
136
+ method: "PATCH",
137
+ headers: { "Content-Type": "application/json" },
138
+ body: JSON.stringify(changes),
534
139
  });
535
140
  }
536
- get restClient() {
537
- return new RestClient({
538
- apiUrl: this.baseUrl,
539
- authManager: this.authManager,
540
- clientInfo: this.clientInfo,
141
+ /** Mint a new code for an unclaimed gift. */
142
+ rotateGiftCode(giftId) {
143
+ return this.requestJson(`/v2/me/gifts/${encodeURIComponent(giftId)}/code`, {
144
+ method: "POST",
541
145
  });
542
146
  }
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;
147
+ async requestJson(path, init) {
148
+ const response = await this.request(path, init);
149
+ return response.json();
150
+ }
151
+ async request(path, init, allowHttpErrors = false) {
152
+ const accept = new Headers(init?.headers).get("Accept") ?? "application/json";
153
+ const response = await this.send(path, init, { accept });
154
+ if (response.status === 401)
155
+ void this.onAuthInvalidated?.();
156
+ if (response.ok || allowHttpErrors)
157
+ return response;
158
+ return throwProblemResponse(response);
159
+ }
160
+ async send(path, init, options = {}) {
161
+ const signal = init?.signal;
162
+ signal?.throwIfAborted();
163
+ const headers = new Headers(init?.headers);
164
+ if (options.accept)
165
+ headers.set("Accept", options.accept);
166
+ headers.set("X-Rool-SDK-Name", packageJson.name);
167
+ headers.set("X-Rool-SDK-Version", packageJson.version);
168
+ if (this.identity?.appName) {
169
+ headers.set("X-Rool-App-Name", this.identity.appName);
582
170
  }
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;
171
+ if (this.identity?.appVersion) {
172
+ headers.set("X-Rool-App-Version", this.identity.appVersion);
173
+ }
174
+ if (this.identity?.osVersion) {
175
+ headers.set("X-Rool-OS-Version", this.identity.osVersion);
176
+ }
177
+ const tokens = await this.getTokens();
178
+ signal?.throwIfAborted();
179
+ if (tokens?.accessToken) {
180
+ headers.set("Authorization", `Bearer ${tokens.accessToken}`);
181
+ }
182
+ if (tokens?.roolToken)
183
+ headers.set("X-Rool-Token", tokens.roolToken);
184
+ const url = `${this.apiUrl}${path}`;
185
+ const configuredBody = init?.body;
186
+ const bodyFactory = typeof configuredBody === "function"
187
+ ? configuredBody
188
+ : () => configuredBody;
189
+ const bodyIsReplayable = typeof configuredBody === "function" || !isReadableStream(configuredBody);
190
+ for (let retry = 0;; retry++) {
191
+ signal?.throwIfAborted();
192
+ const body = bodyFactory();
193
+ const requestInit = {
194
+ ...init,
195
+ headers,
196
+ body,
197
+ };
198
+ if (isReadableStream(body))
199
+ requestInit.duplex = "half";
200
+ let response;
201
+ if (!options.onUploadProgress || body === undefined || body === null) {
202
+ response = await this.fetchRequest(url, requestInit);
614
203
  }
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);
204
+ else {
205
+ const useXmlHttpRequest = this.usesDefaultFetch &&
206
+ typeof XMLHttpRequest !== "undefined" &&
207
+ !isReadableStream(body);
208
+ if (useXmlHttpRequest) {
209
+ response = await sendXmlHttpRequest(url, requestInit, body, options.onUploadProgress);
637
210
  }
638
211
  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
- });
212
+ const progressRequest = requestWithUploadProgress(url, requestInit, body, options.onUploadProgress);
213
+ response = await this.fetchRequest(url, progressRequest);
650
214
  }
651
- break;
652
- case 'user_storage_changed':
653
- this.handleUserStorageChanged(event.key, event.value);
654
- break;
655
- }
656
- }
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
215
  }
669
- else {
670
- this._storageCache[key] = value;
216
+ const routeChanged = response.status === 421 &&
217
+ response.headers.get(MACHINE_ROUTE_CHANGED_HEADER) === "1";
218
+ if (!routeChanged ||
219
+ !bodyIsReplayable ||
220
+ retry >= ROUTE_CHANGE_MAX_RETRIES) {
221
+ return response;
671
222
  }
672
- this.emit('userStorageChanged', { key, value: value ?? null, source: 'remote' });
223
+ await response.body?.cancel();
224
+ await delay(routeChangeBackoffMs(retry), signal);
673
225
  }
674
226
  }
675
227
  }
228
+ function routeChangeBackoffMs(retry) {
229
+ const ceiling = Math.min(ROUTE_CHANGE_RETRY_BASE_MS * 2 ** retry, ROUTE_CHANGE_RETRY_MAX_MS);
230
+ return ceiling / 2 + Math.random() * (ceiling / 2);
231
+ }
232
+ function delay(milliseconds, signal) {
233
+ signal?.throwIfAborted();
234
+ return new Promise((resolve, reject) => {
235
+ const finish = () => {
236
+ signal?.removeEventListener("abort", abort);
237
+ resolve();
238
+ };
239
+ const abort = () => {
240
+ clearTimeout(timer);
241
+ reject(signal?.reason);
242
+ };
243
+ const timer = setTimeout(finish, milliseconds);
244
+ signal?.addEventListener("abort", abort, { once: true });
245
+ });
246
+ }
247
+ function isReadableStream(body) {
248
+ return (typeof ReadableStream !== "undefined" && body instanceof ReadableStream);
249
+ }
250
+ function requestWithUploadProgress(url, init, body, onUploadProgress) {
251
+ const preparedInit = { ...init };
252
+ if (isReadableStream(body))
253
+ preparedInit.duplex = "half";
254
+ const preparedRequest = new Request(url, preparedInit);
255
+ if (!preparedRequest.body)
256
+ throw new Error("Upload request is missing its body");
257
+ const totalBytes = uploadBodySize(body);
258
+ let transferredBytes = 0;
259
+ reportUploadProgress(onUploadProgress, transferredBytes, totalBytes);
260
+ const progressBody = preparedRequest.body.pipeThrough(new TransformStream({
261
+ transform(chunk, controller) {
262
+ controller.enqueue(chunk);
263
+ transferredBytes += chunk.byteLength;
264
+ reportUploadProgress(onUploadProgress, transferredBytes, totalBytes);
265
+ },
266
+ }));
267
+ return {
268
+ ...init,
269
+ headers: preparedRequest.headers,
270
+ body: progressBody,
271
+ duplex: "half",
272
+ };
273
+ }
274
+ function sendXmlHttpRequest(url, init, body, onUploadProgress) {
275
+ return new Promise((resolve, reject) => {
276
+ const request = new XMLHttpRequest();
277
+ const signal = init.signal;
278
+ const totalBytes = uploadBodySize(body);
279
+ let transferredBytes = 0;
280
+ const cleanup = () => signal?.removeEventListener("abort", abort);
281
+ const abort = () => request.abort();
282
+ request.upload.onprogress = (event) => {
283
+ transferredBytes = event.loaded;
284
+ const reportedTotal = event.lengthComputable ? event.total : totalBytes;
285
+ reportUploadProgress(onUploadProgress, transferredBytes, reportedTotal);
286
+ };
287
+ request.upload.onload = (event) => {
288
+ const loadedBytes = event.loaded;
289
+ if (loadedBytes === transferredBytes)
290
+ return;
291
+ transferredBytes = loadedBytes;
292
+ const reportedTotal = event.lengthComputable ? event.total : totalBytes;
293
+ reportUploadProgress(onUploadProgress, transferredBytes, reportedTotal);
294
+ };
295
+ request.onload = () => {
296
+ cleanup();
297
+ const hasResponseBody = request.response instanceof ArrayBuffer &&
298
+ request.response.byteLength > 0;
299
+ const responseBody = hasResponseBody ? request.response : null;
300
+ resolve(new Response(responseBody, {
301
+ status: request.status,
302
+ statusText: request.statusText,
303
+ headers: parseXmlHttpRequestHeaders(request.getAllResponseHeaders()),
304
+ }));
305
+ };
306
+ request.onerror = () => {
307
+ cleanup();
308
+ reject(new TypeError("Network request failed"));
309
+ };
310
+ request.onabort = () => {
311
+ cleanup();
312
+ reject(signal?.reason ??
313
+ new DOMException("This operation was aborted", "AbortError"));
314
+ };
315
+ if (signal?.aborted) {
316
+ reject(signal.reason ??
317
+ new DOMException("This operation was aborted", "AbortError"));
318
+ return;
319
+ }
320
+ request.open(init.method ?? "GET", url);
321
+ request.responseType = "arraybuffer";
322
+ request.withCredentials = init.credentials === "include";
323
+ new Headers(init.headers).forEach((value, name) => {
324
+ request.setRequestHeader(name, value);
325
+ });
326
+ signal?.addEventListener("abort", abort, { once: true });
327
+ reportUploadProgress(onUploadProgress, transferredBytes, totalBytes);
328
+ request.send(body);
329
+ });
330
+ }
331
+ function reportUploadProgress(listener, transferredBytes, totalBytes) {
332
+ const progress = { transferredBytes };
333
+ if (totalBytes !== undefined)
334
+ progress.totalBytes = totalBytes;
335
+ listener(progress);
336
+ }
337
+ function uploadBodySize(body) {
338
+ if (typeof body === "string")
339
+ return new TextEncoder().encode(body).byteLength;
340
+ if (typeof Blob !== "undefined" && body instanceof Blob)
341
+ return body.size;
342
+ if (body instanceof ArrayBuffer)
343
+ return body.byteLength;
344
+ if (ArrayBuffer.isView(body))
345
+ return body.byteLength;
346
+ if (typeof URLSearchParams !== "undefined" &&
347
+ body instanceof URLSearchParams) {
348
+ return new TextEncoder().encode(body.toString()).byteLength;
349
+ }
350
+ return undefined;
351
+ }
352
+ function parseXmlHttpRequestHeaders(rawHeaders) {
353
+ const headers = new Headers();
354
+ for (const line of rawHeaders.split(/\r?\n/)) {
355
+ if (!line)
356
+ continue;
357
+ const separator = line.indexOf(":");
358
+ if (separator === -1)
359
+ throw new Error(`Invalid response header: ${line}`);
360
+ headers.append(line.slice(0, separator), line.slice(separator + 1).trim());
361
+ }
362
+ return headers;
363
+ }
676
364
  //# sourceMappingURL=client.js.map