uidex 0.6.0 → 0.7.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.
Files changed (39) hide show
  1. package/README.md +3 -3
  2. package/dist/cli/cli.cjs +1510 -1244
  3. package/dist/cli/cli.cjs.map +1 -1
  4. package/dist/cloud/index.cjs +385 -175
  5. package/dist/cloud/index.cjs.map +1 -1
  6. package/dist/cloud/index.d.cts +192 -4
  7. package/dist/cloud/index.d.ts +192 -4
  8. package/dist/cloud/index.js +377 -177
  9. package/dist/cloud/index.js.map +1 -1
  10. package/dist/headless/index.cjs +82 -255
  11. package/dist/headless/index.cjs.map +1 -1
  12. package/dist/headless/index.d.cts +5 -11
  13. package/dist/headless/index.d.ts +5 -11
  14. package/dist/headless/index.js +82 -257
  15. package/dist/headless/index.js.map +1 -1
  16. package/dist/index.cjs +721 -1053
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +149 -160
  19. package/dist/index.d.ts +149 -160
  20. package/dist/index.js +741 -1068
  21. package/dist/index.js.map +1 -1
  22. package/dist/react/index.cjs +729 -1000
  23. package/dist/react/index.cjs.map +1 -1
  24. package/dist/react/index.d.cts +99 -86
  25. package/dist/react/index.d.ts +99 -86
  26. package/dist/react/index.js +745 -1015
  27. package/dist/react/index.js.map +1 -1
  28. package/dist/scan/index.cjs +1518 -1237
  29. package/dist/scan/index.cjs.map +1 -1
  30. package/dist/scan/index.d.cts +209 -12
  31. package/dist/scan/index.d.ts +209 -12
  32. package/dist/scan/index.js +1515 -1236
  33. package/dist/scan/index.js.map +1 -1
  34. package/package.json +22 -21
  35. package/templates/claude/SKILL.md +71 -0
  36. package/templates/claude/references/audit.md +43 -0
  37. package/templates/claude/{rules.md → references/conventions.md} +25 -28
  38. package/templates/claude/audit.md +0 -43
  39. /package/templates/claude/{api.md → references/api.md} +0 -0
@@ -22,96 +22,61 @@ var cloud_exports = {};
22
22
  __export(cloud_exports, {
23
23
  CloudError: () => CloudError,
24
24
  DEFAULT_CLOUD_ENDPOINT: () => DEFAULT_CLOUD_ENDPOINT,
25
+ DEFAULT_RPC_TIMEOUT_MS: () => DEFAULT_RPC_TIMEOUT_MS,
26
+ RPC_METHODS: () => RPC_METHODS,
27
+ SCREENSHOT_CHUNK_BYTES: () => SCREENSHOT_CHUNK_BYTES,
28
+ SCREENSHOT_INLINE_MAX_BYTES: () => SCREENSHOT_INLINE_MAX_BYTES,
25
29
  cloud: () => cloud,
26
- createRealtimeChannel: () => createRealtimeChannel
30
+ createRealtimeChannel: () => createRealtimeChannel,
31
+ createRpcClient: () => createRpcClient,
32
+ isRpcMethod: () => isRpcMethod,
33
+ parseRpcResponseFrame: () => parseRpcResponseFrame
27
34
  });
28
35
  module.exports = __toCommonJS(cloud_exports);
29
36
 
30
- // src/cloud/client.ts
31
- var import_client_fetch2 = require("@hey-api/client-fetch");
32
-
33
- // ../api-client/src/client.gen.ts
34
- var import_client_fetch = require("@hey-api/client-fetch");
35
- var client = (0, import_client_fetch.createClient)(
36
- (0, import_client_fetch.createConfig)({
37
- baseUrl: "https://app.uidex.dev"
38
- })
39
- );
40
-
41
- // ../api-client/src/sdk.gen.ts
42
- var submitReport = (options) => {
43
- return (options.client ?? client).post({
44
- security: [
45
- {
46
- scheme: "bearer",
47
- type: "http"
48
- }
49
- ],
50
- url: "/api/ingest",
51
- ...options,
52
- headers: {
53
- "Content-Type": "application/json",
54
- ...options?.headers
55
- }
56
- });
57
- };
58
- var getIngestConfig = (options) => {
59
- return (options?.client ?? client).get({
60
- security: [
61
- {
62
- scheme: "bearer",
63
- type: "http"
64
- }
65
- ],
66
- url: "/api/ingest/config",
67
- ...options
68
- });
69
- };
70
- var listIngestReports = (options) => {
71
- return (options?.client ?? client).get({
72
- security: [
73
- {
74
- scheme: "bearer",
75
- type: "http"
76
- }
77
- ],
78
- url: "/api/ingest/reports",
79
- ...options
80
- });
81
- };
82
- var listPins = (options) => {
83
- return (options?.client ?? client).get({
84
- security: [
85
- {
86
- scheme: "bearer",
87
- type: "http"
88
- }
89
- ],
90
- url: "/api/ingest/pins",
91
- ...options
92
- });
93
- };
94
- var archivePin = (options) => {
95
- return (options.client ?? client).post({
96
- security: [
97
- {
98
- scheme: "bearer",
99
- type: "http"
100
- }
101
- ],
102
- url: "/api/ingest/pins/archive",
103
- ...options,
104
- headers: {
105
- "Content-Type": "application/json",
106
- ...options?.headers
107
- }
108
- });
109
- };
37
+ // src/cloud/protocol.ts
38
+ var RPC_METHODS = [
39
+ "reports.submit",
40
+ "reports.list",
41
+ "config.get",
42
+ "pins.list",
43
+ "pins.screenshot",
44
+ "pins.archive",
45
+ "screenshot.chunk"
46
+ ];
47
+ var SCREENSHOT_INLINE_MAX_BYTES = 20 * 1024 * 1024;
48
+ var SCREENSHOT_CHUNK_BYTES = 8 * 1024 * 1024;
49
+ function isRpcMethod(value) {
50
+ return typeof value === "string" && RPC_METHODS.includes(value);
51
+ }
52
+ function parseRpcResponseFrame(msg) {
53
+ if (!msg || typeof msg !== "object") return null;
54
+ const m = msg;
55
+ if (m.type !== "rpc:res" || typeof m.id !== "string") return null;
56
+ if (m.ok === true) {
57
+ return { type: "rpc:res", id: m.id, ok: true, data: m.data };
58
+ }
59
+ if (m.ok === false) {
60
+ if (typeof m.status !== "number" || typeof m.error !== "string") return null;
61
+ return {
62
+ type: "rpc:res",
63
+ id: m.id,
64
+ ok: false,
65
+ status: m.status,
66
+ error: m.error,
67
+ retryAfter: typeof m.retryAfter === "number" ? m.retryAfter : void 0,
68
+ details: m.details
69
+ };
70
+ }
71
+ return null;
72
+ }
110
73
 
111
74
  // src/cloud/realtime.ts
112
75
  var RECONNECT_INITIAL_MS = 1e3;
113
76
  var RECONNECT_MAX_MS = 3e4;
77
+ var MAX_RECONNECT_ATTEMPTS = 10;
114
78
  var CLOSE_CODE_AUTH_FAILED = 4001;
79
+ var CLOSE_CODE_SYNTHETIC = 1006;
115
80
  function emit(listeners, value) {
116
81
  for (const cb of listeners) {
117
82
  try {
@@ -130,14 +95,28 @@ function resolveWebSocket(override) {
130
95
  );
131
96
  }
132
97
  function createRealtimeChannel(options) {
133
- const WS = resolveWebSocket(options.WebSocketImpl);
98
+ let WS = options.WebSocketImpl ?? null;
134
99
  const presenceListeners = /* @__PURE__ */ new Set();
135
100
  const pinListeners = /* @__PURE__ */ new Set();
101
+ const pinArchivedListeners = /* @__PURE__ */ new Set();
102
+ const openListeners = /* @__PURE__ */ new Set();
103
+ const closeListeners = /* @__PURE__ */ new Set();
104
+ const rpcListeners = /* @__PURE__ */ new Set();
136
105
  let ws = null;
137
106
  let state = "disconnected";
138
107
  let disposed = false;
139
108
  let reconnectAttempts = 0;
140
109
  let reconnectTimer = null;
110
+ let identity = null;
111
+ let route = null;
112
+ function emitClose(code, willReconnect) {
113
+ for (const cb of closeListeners) {
114
+ try {
115
+ cb(code, willReconnect);
116
+ } catch {
117
+ }
118
+ }
119
+ }
141
120
  function clearReconnectTimer() {
142
121
  if (reconnectTimer !== null) {
143
122
  clearTimeout(reconnectTimer);
@@ -168,6 +147,11 @@ function createRealtimeChannel(options) {
168
147
  }
169
148
  if (!parsed || typeof parsed !== "object") return;
170
149
  const msg = parsed;
150
+ if (msg.type === "rpc:res") {
151
+ const frame = parseRpcResponseFrame(parsed);
152
+ if (frame) emit(rpcListeners, frame);
153
+ return;
154
+ }
171
155
  if (msg.type === "presence") {
172
156
  const p = msg;
173
157
  if (!Array.isArray(p.users)) return;
@@ -185,15 +169,21 @@ function createRealtimeChannel(options) {
185
169
  emit(presenceListeners, users);
186
170
  return;
187
171
  }
172
+ if (msg.type === "pin:archived") {
173
+ const p = msg;
174
+ if (typeof p.reportId !== "string") return;
175
+ emit(pinArchivedListeners, p.reportId);
176
+ return;
177
+ }
188
178
  if (msg.type === "pin") {
189
179
  const p = msg;
190
- if (typeof p.feedbackId !== "string" || !p.elementRef || typeof p.elementRef !== "object" || typeof p.elementRef.kind !== "string" || typeof p.elementRef.id !== "string" || !p.author || typeof p.author !== "object" || typeof p.body !== "string" || typeof p.reportType !== "string" || typeof p.reportSeverity !== "string" || typeof p.createdAt !== "string") {
180
+ if (typeof p.reportId !== "string" || !p.elementRef || typeof p.elementRef !== "object" || typeof p.elementRef.kind !== "string" || typeof p.elementRef.id !== "string" || !p.author || typeof p.author !== "object" || typeof p.body !== "string" || typeof p.reportType !== "string" || typeof p.reportSeverity !== "string" || typeof p.createdAt !== "string") {
191
181
  return;
192
182
  }
193
183
  const author = p.author;
194
184
  const elRef = p.elementRef;
195
185
  const pin = {
196
- id: p.feedbackId,
186
+ id: p.reportId,
197
187
  entity: `${elRef.kind}:${elRef.id}`,
198
188
  reporter: {
199
189
  name: typeof author.name === "string" ? author.name : void 0,
@@ -210,20 +200,45 @@ function createRealtimeChannel(options) {
210
200
  return;
211
201
  }
212
202
  }
203
+ function sendRaw(frame) {
204
+ if (!ws || !WS || ws.readyState !== WS.OPEN) return false;
205
+ try {
206
+ ws.send(JSON.stringify(frame));
207
+ return true;
208
+ } catch {
209
+ return false;
210
+ }
211
+ }
213
212
  function openSocket() {
214
213
  if (disposed) return;
215
214
  state = "connecting";
215
+ if (!WS) {
216
+ try {
217
+ WS = resolveWebSocket(options.WebSocketImpl);
218
+ } catch {
219
+ state = "disconnected";
220
+ emitClose(CLOSE_CODE_SYNTHETIC, false);
221
+ return;
222
+ }
223
+ }
216
224
  let url;
217
225
  try {
218
226
  url = options.buildUrl();
219
227
  } catch {
220
228
  state = "disconnected";
229
+ emitClose(CLOSE_CODE_SYNTHETIC, false);
221
230
  return;
222
231
  }
223
232
  let socket;
224
233
  try {
225
234
  socket = new WS(url);
226
235
  } catch {
236
+ if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
237
+ state = "disconnected";
238
+ emitClose(CLOSE_CODE_SYNTHETIC, false);
239
+ return;
240
+ }
241
+ emitClose(CLOSE_CODE_SYNTHETIC, true);
227
242
  scheduleReconnect();
228
243
  return;
229
244
  }
@@ -232,6 +247,18 @@ function createRealtimeChannel(options) {
232
247
  if (ws !== socket) return;
233
248
  state = "connected";
234
249
  reconnectAttempts = 0;
250
+ if (identity) {
251
+ sendRaw({
252
+ type: "identify",
253
+ userId: identity.id,
254
+ ...identity.name ? { name: identity.name } : {},
255
+ ...identity.avatar ? { avatar: identity.avatar } : {}
256
+ });
257
+ }
258
+ if (route !== null) {
259
+ sendRaw({ type: "join", route });
260
+ }
261
+ emit(openListeners, void 0);
235
262
  });
236
263
  socket.addEventListener("message", (event) => {
237
264
  if (ws !== socket) return;
@@ -241,10 +268,12 @@ function createRealtimeChannel(options) {
241
268
  if (ws !== socket) return;
242
269
  ws = null;
243
270
  const code = event.code;
244
- if (disposed || code === CLOSE_CODE_AUTH_FAILED) {
271
+ if (disposed || code === CLOSE_CODE_AUTH_FAILED || reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
245
272
  state = "disconnected";
273
+ emitClose(code, false);
246
274
  return;
247
275
  }
276
+ emitClose(code, true);
248
277
  scheduleReconnect();
249
278
  });
250
279
  socket.addEventListener("error", () => {
@@ -263,22 +292,35 @@ function createRealtimeChannel(options) {
263
292
  disposed = true;
264
293
  clearReconnectTimer();
265
294
  state = "disconnected";
295
+ identity = null;
296
+ route = null;
266
297
  if (ws) {
298
+ const socket = ws;
299
+ ws = null;
267
300
  try {
268
- ws.close(1e3);
301
+ socket.close(1e3);
269
302
  } catch {
270
303
  }
271
- ws = null;
272
304
  }
305
+ emitClose(CLOSE_CODE_SYNTHETIC, false);
273
306
  }
274
- function joinRoute(route) {
307
+ function clearSession() {
308
+ identity = null;
309
+ route = null;
310
+ }
311
+ function identify(user) {
312
+ identity = user;
313
+ sendRaw({
314
+ type: "identify",
315
+ userId: user.id,
316
+ ...user.name ? { name: user.name } : {},
317
+ ...user.avatar ? { avatar: user.avatar } : {}
318
+ });
319
+ }
320
+ function joinRoute(nextRoute) {
321
+ route = nextRoute;
275
322
  emit(presenceListeners, []);
276
- if (ws && ws.readyState === WS.OPEN) {
277
- try {
278
- ws.send(JSON.stringify({ type: "join", route }));
279
- } catch {
280
- }
281
- }
323
+ sendRaw({ type: "join", route: nextRoute });
282
324
  }
283
325
  function onPresence(cb) {
284
326
  presenceListeners.add(cb);
@@ -292,15 +334,46 @@ function createRealtimeChannel(options) {
292
334
  pinListeners.delete(cb);
293
335
  };
294
336
  }
337
+ function onPinArchived(cb) {
338
+ pinArchivedListeners.add(cb);
339
+ return () => {
340
+ pinArchivedListeners.delete(cb);
341
+ };
342
+ }
343
+ function onOpen(cb) {
344
+ openListeners.add(cb);
345
+ return () => {
346
+ openListeners.delete(cb);
347
+ };
348
+ }
349
+ function onClose(cb) {
350
+ closeListeners.add(cb);
351
+ return () => {
352
+ closeListeners.delete(cb);
353
+ };
354
+ }
355
+ function onRpcResponse(cb) {
356
+ rpcListeners.add(cb);
357
+ return () => {
358
+ rpcListeners.delete(cb);
359
+ };
360
+ }
295
361
  return {
296
362
  get state() {
297
363
  return state;
298
364
  },
299
365
  connect,
300
366
  disconnect,
367
+ identify,
368
+ clearSession,
301
369
  joinRoute,
370
+ sendFrame: sendRaw,
302
371
  onPresence,
303
- onPin
372
+ onPin,
373
+ onPinArchived,
374
+ onOpen,
375
+ onClose,
376
+ onRpcResponse
304
377
  };
305
378
  }
306
379
 
@@ -319,29 +392,111 @@ var CloudError = class extends Error {
319
392
  }
320
393
  };
321
394
 
395
+ // src/cloud/rpc.ts
396
+ var DEFAULT_RPC_TIMEOUT_MS = 3e4;
397
+ var SUBMIT_RPC_TIMEOUT_MS = 12e4;
398
+ function authFailedError() {
399
+ return new CloudError("WebSocket authentication failed", { status: 401 });
400
+ }
401
+ function createRpcClient(channel, options) {
402
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_RPC_TIMEOUT_MS;
403
+ const idSuffix = Math.random().toString(36).slice(2, 8);
404
+ let idCounter = 0;
405
+ const pending = /* @__PURE__ */ new Map();
406
+ const queue = [];
407
+ let terminalError = null;
408
+ function settlePending(err) {
409
+ for (const entry of pending.values()) {
410
+ clearTimeout(entry.timer);
411
+ entry.reject(err);
412
+ }
413
+ pending.clear();
414
+ }
415
+ channel.onOpen(() => {
416
+ while (queue.length > 0) {
417
+ const next = queue[0];
418
+ if (!channel.sendFrame(next.frame)) return;
419
+ queue.shift();
420
+ pending.set(next.frame.id, next.entry);
421
+ }
422
+ });
423
+ function rejectQueue(err) {
424
+ for (const queued of queue) {
425
+ clearTimeout(queued.entry.timer);
426
+ queued.entry.reject(err);
427
+ }
428
+ queue.length = 0;
429
+ }
430
+ channel.onClose((code, willReconnect) => {
431
+ if (code === CLOSE_CODE_AUTH_FAILED) {
432
+ const err = authFailedError();
433
+ terminalError = err;
434
+ settlePending(err);
435
+ rejectQueue(err);
436
+ return;
437
+ }
438
+ settlePending(new CloudError("Connection lost", { status: 0 }));
439
+ if (!willReconnect) {
440
+ rejectQueue(new CloudError("Connection lost", { status: 0 }));
441
+ }
442
+ });
443
+ channel.onRpcResponse((frame) => {
444
+ const entry = pending.get(frame.id);
445
+ if (!entry) return;
446
+ pending.delete(frame.id);
447
+ clearTimeout(entry.timer);
448
+ if (frame.ok) {
449
+ entry.resolve(frame.data);
450
+ } else {
451
+ entry.reject(
452
+ new CloudError(frame.error, {
453
+ status: frame.status,
454
+ retryAfter: frame.retryAfter,
455
+ details: frame.details
456
+ })
457
+ );
458
+ }
459
+ });
460
+ function call(method, params, opts) {
461
+ if (terminalError) return Promise.reject(terminalError);
462
+ const callTimeoutMs = opts?.timeoutMs ?? timeoutMs;
463
+ return new Promise((resolve, reject) => {
464
+ idCounter += 1;
465
+ const id = `${idCounter.toString(36)}-${idSuffix}`;
466
+ const frame = {
467
+ type: "rpc",
468
+ id,
469
+ method,
470
+ ...params !== void 0 ? { params } : {}
471
+ };
472
+ const timer = setTimeout(() => {
473
+ pending.delete(id);
474
+ const queuedIndex = queue.findIndex((q) => q.frame.id === id);
475
+ if (queuedIndex !== -1) queue.splice(queuedIndex, 1);
476
+ reject(new CloudError("Request timed out", { status: 0 }));
477
+ }, callTimeoutMs);
478
+ const entry = {
479
+ resolve,
480
+ reject,
481
+ timer
482
+ };
483
+ if (channel.sendFrame(frame)) {
484
+ pending.set(id, entry);
485
+ } else {
486
+ queue.push({ frame, entry });
487
+ channel.connect();
488
+ }
489
+ });
490
+ }
491
+ return { call };
492
+ }
493
+
322
494
  // src/cloud/client.ts
323
495
  function trimEndpoint(endpoint) {
324
496
  return endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
325
497
  }
326
- function unwrap(result) {
327
- if (result.error !== void 0 || !result.data) {
328
- const status = result.response.status;
329
- const retryAfter = status === 429 ? parseRetryAfter(result.response.headers.get("Retry-After")) : void 0;
330
- const msg = result.error && typeof result.error === "object" && "error" in result.error && typeof result.error.error === "string" ? result.error.error : `Request failed (${status})`;
331
- throw new CloudError(msg, { status, retryAfter, details: result.error });
332
- }
333
- return result.data;
334
- }
335
- function parseRetryAfter(header) {
336
- if (!header) return void 0;
337
- const seconds = Number(header);
338
- if (Number.isFinite(seconds) && seconds >= 0) return seconds;
339
- const date = Date.parse(header);
340
- if (Number.isFinite(date)) {
341
- const delta = Math.ceil((date - Date.now()) / 1e3);
342
- return delta > 0 ? delta : 0;
343
- }
344
- return void 0;
498
+ function nextUploadId() {
499
+ return `up-${crypto.randomUUID()}`;
345
500
  }
346
501
  function cloud(options) {
347
502
  let cachedConfig = null;
@@ -352,13 +507,18 @@ function cloud(options) {
352
507
  }
353
508
  const endpoint = trimEndpoint(options.endpoint ?? DEFAULT_CLOUD_ENDPOINT);
354
509
  const git = options.git;
355
- const apiClient = (0, import_client_fetch2.createClient)(
356
- (0, import_client_fetch2.createConfig)({
357
- baseUrl: endpoint,
358
- ...options.fetch ? { fetch: options.fetch } : {},
359
- headers: { Authorization: `Bearer ${projectKey}` }
360
- })
361
- );
510
+ function buildWsUrl() {
511
+ const httpToWs = endpoint.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
512
+ const params = new URLSearchParams();
513
+ params.set("key", projectKey);
514
+ return `${httpToWs}/ws?${params.toString()}`;
515
+ }
516
+ const channel = createRealtimeChannel({
517
+ buildUrl: buildWsUrl,
518
+ ...options.WebSocketImpl ? { WebSocketImpl: options.WebSocketImpl } : {}
519
+ });
520
+ const rpc = createRpcClient(channel);
521
+ const eagerConnect = options.WebSocketImpl !== void 0 || typeof window !== "undefined" && typeof WebSocket === "function";
362
522
  async function submit(payload) {
363
523
  const enriched = git?.branch || git?.commit ? {
364
524
  ...payload,
@@ -370,103 +530,153 @@ function cloud(options) {
370
530
  }
371
531
  }
372
532
  } : payload;
373
- const result = await submitReport({
374
- client: apiClient,
375
- body: enriched
533
+ const screenshot = enriched.screenshot;
534
+ if (typeof screenshot === "string" && screenshot.length > SCREENSHOT_INLINE_MAX_BYTES) {
535
+ const uploadId = nextUploadId();
536
+ const total = Math.ceil(screenshot.length / SCREENSHOT_CHUNK_BYTES);
537
+ for (let seq = 0; seq < total; seq++) {
538
+ const data = screenshot.slice(
539
+ seq * SCREENSHOT_CHUNK_BYTES,
540
+ (seq + 1) * SCREENSHOT_CHUNK_BYTES
541
+ );
542
+ await rpc.call(
543
+ "screenshot.chunk",
544
+ { uploadId, seq, total, data },
545
+ { timeoutMs: SUBMIT_RPC_TIMEOUT_MS }
546
+ );
547
+ }
548
+ return rpc.call(
549
+ "reports.submit",
550
+ { ...enriched, screenshot: void 0, screenshotUploadId: uploadId },
551
+ { timeoutMs: SUBMIT_RPC_TIMEOUT_MS }
552
+ );
553
+ }
554
+ return rpc.call("reports.submit", enriched, {
555
+ timeoutMs: SUBMIT_RPC_TIMEOUT_MS
376
556
  });
377
- return unwrap(result);
378
- }
379
- async function fetchConfig() {
380
- const result = await getIngestConfig({ client: apiClient });
381
- return unwrap(result);
382
557
  }
383
558
  function startFetch() {
384
- const promise = fetchConfig();
559
+ const promise = rpc.call("config.get", void 0);
560
+ cachedConfig = promise;
385
561
  promise.then(
386
562
  (config) => {
387
563
  resolvedConfig = config;
388
564
  },
389
565
  () => {
566
+ if (cachedConfig === promise) cachedConfig = null;
390
567
  }
391
568
  );
392
569
  return promise;
393
570
  }
394
- cachedConfig = startFetch();
571
+ if (eagerConnect) {
572
+ channel.connect();
573
+ startFetch();
574
+ }
395
575
  function getConfig() {
396
- return cachedConfig ?? (cachedConfig = startFetch());
576
+ return cachedConfig ?? startFetch();
397
577
  }
398
578
  function getCachedConfig() {
399
579
  return resolvedConfig;
400
580
  }
401
- function realtimeUrl(route, user) {
402
- const httpToWs = endpoint.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
403
- const params = new URLSearchParams();
404
- params.set("key", projectKey);
405
- params.set("route", route);
406
- params.set("userId", user.id);
407
- if (user.name) params.set("name", user.name);
408
- if (user.avatar) params.set("avatar", user.avatar);
409
- return `${httpToWs}/ws?${params.toString()}`;
410
- }
411
581
  function connectRealtime(opts) {
412
582
  if (!opts || !opts.user || typeof opts.user.id !== "string") {
413
583
  throw new TypeError("uidex/cloud: realtime.connect requires `user.id`");
414
584
  }
415
585
  let currentRoute = opts.route;
416
- const channel = createRealtimeChannel({
417
- buildUrl: () => realtimeUrl(currentRoute, opts.user)
418
- });
419
- const originalJoinRoute = channel.joinRoute;
420
- const wrapped = {
586
+ let detached = false;
587
+ const registrations = /* @__PURE__ */ new Set();
588
+ function register(attach) {
589
+ const reg = { attach, detach: detached ? null : attach() };
590
+ registrations.add(reg);
591
+ return () => {
592
+ registrations.delete(reg);
593
+ reg.detach?.();
594
+ reg.detach = null;
595
+ };
596
+ }
597
+ function arm() {
598
+ channel.identify(opts.user);
599
+ channel.joinRoute(currentRoute);
600
+ channel.connect();
601
+ }
602
+ arm();
603
+ return {
421
604
  get state() {
422
- return channel.state;
605
+ return detached ? "disconnected" : channel.state;
606
+ },
607
+ // Re-arms identity/route and re-attaches this facade's listeners so an
608
+ // explicit disconnect() -> connect() restores presence and pin events.
609
+ connect: () => {
610
+ if (detached) {
611
+ detached = false;
612
+ for (const reg of registrations) reg.detach ??= reg.attach();
613
+ }
614
+ arm();
615
+ },
616
+ // Soft leave: drop presence and the stored identity (so a later
617
+ // reopen/revival stays anonymous) but keep the adapter's shared socket
618
+ // alive — in-flight and future RPCs (reports.submit, pins.*) must
619
+ // survive a surface unmount. Socket teardown is reserved for
620
+ // CloudAdapter.dispose().
621
+ disconnect: () => {
622
+ detached = true;
623
+ channel.sendFrame({ type: "leave" });
624
+ channel.clearSession();
625
+ for (const reg of registrations) {
626
+ reg.detach?.();
627
+ reg.detach = null;
628
+ }
423
629
  },
424
- connect: () => channel.connect(),
425
- disconnect: () => channel.disconnect(),
426
630
  joinRoute: (route) => {
427
631
  currentRoute = route;
428
- originalJoinRoute(route);
632
+ channel.joinRoute(route);
429
633
  },
430
- onPresence: (cb) => channel.onPresence(cb),
431
- onPin: (cb) => channel.onPin(cb)
634
+ onPresence: (cb) => register(() => channel.onPresence(cb)),
635
+ onPin: (cb) => register(() => channel.onPin(cb)),
636
+ onPinArchived: (cb) => register(() => channel.onPinArchived(cb))
432
637
  };
433
- channel.connect();
434
- return wrapped;
435
638
  }
436
- async function listPins2(params) {
437
- const result = await listPins({
438
- client: apiClient,
439
- query: params
639
+ async function listPins(params) {
640
+ const entities = params.entities ? params.entities.split(",").filter(Boolean) : void 0;
641
+ const result = await rpc.call("pins.list", {
642
+ ...params.route !== void 0 ? { route: params.route } : {},
643
+ ...entities ? { entities } : {}
440
644
  });
441
- const data = unwrap(result);
442
- return data.pins;
645
+ return result.pins;
443
646
  }
444
- async function archivePin2(reportId, reason) {
445
- const result = await archivePin({
446
- client: apiClient,
447
- body: { reportId, ...reason ? { reason } : {} }
448
- });
449
- unwrap(result);
647
+ async function getPinScreenshot(reportId) {
648
+ const result = await rpc.call("pins.screenshot", { reportId });
649
+ return result.screenshot;
450
650
  }
451
- async function listReports(opts) {
452
- const result = await listIngestReports({
453
- client: apiClient,
454
- query: opts
455
- });
456
- return unwrap(result);
651
+ async function closePin(reportId, reason) {
652
+ await rpc.call("pins.archive", { reportId, ...reason ? { reason } : {} });
653
+ }
654
+ function listReports(opts) {
655
+ return rpc.call("reports.list", opts ?? {});
656
+ }
657
+ function dispose() {
658
+ channel.disconnect();
457
659
  }
458
660
  return {
459
661
  reports: { submit, list: listReports },
460
662
  integrations: { getConfig, getCachedConfig },
461
663
  realtime: { connect: connectRealtime },
462
- pins: { list: listPins2, archive: archivePin2 }
664
+ pins: { list: listPins, screenshot: getPinScreenshot, close: closePin },
665
+ dispose
463
666
  };
464
667
  }
465
668
  // Annotate the CommonJS export names for ESM import in node:
466
669
  0 && (module.exports = {
467
670
  CloudError,
468
671
  DEFAULT_CLOUD_ENDPOINT,
672
+ DEFAULT_RPC_TIMEOUT_MS,
673
+ RPC_METHODS,
674
+ SCREENSHOT_CHUNK_BYTES,
675
+ SCREENSHOT_INLINE_MAX_BYTES,
469
676
  cloud,
470
- createRealtimeChannel
677
+ createRealtimeChannel,
678
+ createRpcClient,
679
+ isRpcMethod,
680
+ parseRpcResponseFrame
471
681
  });
472
682
  //# sourceMappingURL=index.cjs.map