@semiont/api-client 0.4.20 → 0.4.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,1044 +1,279 @@
1
1
  import ky, { HTTPError } from 'ky';
2
- import { BehaviorSubject, Observable, merge } from 'rxjs';
3
- import { map, distinctUntilChanged, filter, takeUntil } from 'rxjs/operators';
4
- import { annotationId, resourceId, searchQuery, email, googleCredential, refreshToken } from '@semiont/core';
5
- export { getFragmentSelector, getSvgSelector, getTextPositionSelector, validateSvgMarkup } from '@semiont/core';
2
+ import { Subject, BehaviorSubject } from 'rxjs';
3
+ import { RESOURCE_BROADCAST_TYPES, PERSISTED_EVENT_TYPES, BRIDGED_CHANNELS, busLog } from '@semiont/core';
4
+ import { getActiveTraceparent, recordBusEmit, withSpan, SpanKind, extractTraceparent, withTraceparent } from '@semiont/observability';
5
+ import { share, filter, map } from 'rxjs/operators';
6
6
 
7
- // src/client.ts
8
-
9
- // src/sse/stream.ts
10
- function createSSEStream(url, fetchOptions, config, logger) {
11
- let abortController = new AbortController();
12
- let closed = false;
7
+ // src/transport/http-transport.ts
8
+ var DEGRADED_THRESHOLD_MS = 3e3;
9
+ var ALLOWED_TRANSITIONS = {
10
+ initial: ["connecting", "closed"],
11
+ connecting: ["open", "reconnecting", "closed"],
12
+ open: ["reconnecting", "closed"],
13
+ reconnecting: ["connecting", "degraded", "closed"],
14
+ degraded: ["connecting", "closed"],
15
+ closed: []
16
+ };
17
+ function createActorVM(options) {
18
+ const { baseUrl, token: tokenOrGetter, channels: initialChannels, scope: initialScope, reconnectMs = 5e3 } = options;
19
+ const getToken = typeof tokenOrGetter === "function" ? tokenOrGetter : () => tokenOrGetter;
20
+ const g = globalThis;
21
+ g.__SEMIONT_ACTOR_INSTANCES__ = (g.__SEMIONT_ACTOR_INSTANCES__ ?? 0) + 1;
22
+ const actorSerial = g.__SEMIONT_ACTOR_INSTANCES__;
23
+ console.debug(`[diag] ActorVM #${actorSerial} constructed (baseUrl=${baseUrl})`);
24
+ const globalChannels = new Set(initialChannels);
25
+ const scopedChannels = /* @__PURE__ */ new Set();
26
+ let activeScope = initialScope;
27
+ const events$ = new Subject();
28
+ const state$ = new BehaviorSubject("initial");
29
+ let currentState = "initial";
30
+ let degradedTimer = null;
31
+ const transition = (next) => {
32
+ if (currentState === next) return;
33
+ const allowed = ALLOWED_TRANSITIONS[currentState];
34
+ if (!allowed.includes(next)) {
35
+ throw new Error(`Invalid connection state transition: ${currentState} \u2192 ${next}`);
36
+ }
37
+ const prev = currentState;
38
+ currentState = next;
39
+ if (next === "reconnecting" && prev !== "reconnecting") {
40
+ if (degradedTimer) clearTimeout(degradedTimer);
41
+ degradedTimer = setTimeout(() => {
42
+ if (currentState === "reconnecting") transition("degraded");
43
+ }, DEGRADED_THRESHOLD_MS);
44
+ }
45
+ if (prev === "reconnecting" && next !== "reconnecting") {
46
+ if (degradedTimer) {
47
+ clearTimeout(degradedTimer);
48
+ degradedTimer = null;
49
+ }
50
+ }
51
+ state$.next(next);
52
+ };
53
+ let running = false;
54
+ const inflightControllers = /* @__PURE__ */ new Set();
55
+ let reconnectTimer = null;
13
56
  let lastEventId = null;
14
- const RECONNECT_INITIAL_MS = 1e3;
15
- const RECONNECT_MAX_MS = 3e4;
16
- let reconnectDelayMs = RECONNECT_INITIAL_MS;
57
+ const shared$ = events$.pipe(share());
58
+ const disconnect = () => {
59
+ for (const c of inflightControllers) {
60
+ try {
61
+ c.abort();
62
+ } catch {
63
+ }
64
+ }
65
+ inflightControllers.clear();
66
+ if (reconnectTimer) {
67
+ clearTimeout(reconnectTimer);
68
+ reconnectTimer = null;
69
+ }
70
+ };
17
71
  const connect = async () => {
18
- try {
19
- logger?.debug("SSE Stream Request", {
20
- type: "sse_request",
21
- url,
22
- method: fetchOptions.method || "GET",
23
- timestamp: Date.now()
24
- });
25
- const headers = {
26
- ...fetchOptions.headers,
27
- "Accept": "text/event-stream"
28
- };
29
- if (lastEventId !== null) {
30
- headers["Last-Event-ID"] = lastEventId;
72
+ transition("connecting");
73
+ for (const c of inflightControllers) {
74
+ try {
75
+ c.abort();
76
+ } catch {
31
77
  }
32
- const response = await fetch(url, {
33
- ...fetchOptions,
34
- signal: abortController.signal,
35
- headers
36
- });
37
- if (!response.ok) {
38
- const errorData = await response.json().catch(() => ({}));
39
- const error = new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`);
40
- logger?.error("SSE Stream Error", {
41
- type: "sse_error",
42
- url,
43
- error: error.message,
44
- status: response.status,
45
- phase: "connect"
46
- });
47
- throw error;
78
+ }
79
+ inflightControllers.clear();
80
+ const params = new URLSearchParams();
81
+ for (const ch of globalChannels) {
82
+ params.append("channel", ch);
83
+ }
84
+ if (activeScope && scopedChannels.size > 0) {
85
+ params.append("scope", activeScope);
86
+ for (const ch of scopedChannels) {
87
+ params.append("scoped", ch);
48
88
  }
49
- if (!response.body) {
50
- const error = new Error("Response body is null - server did not return a stream");
51
- logger?.error("SSE Stream Error", {
52
- type: "sse_error",
53
- url,
54
- error: error.message,
55
- phase: "connect"
56
- });
57
- throw error;
89
+ }
90
+ const url = `${baseUrl}/bus/subscribe?${params.toString()}`;
91
+ const controller = new AbortController();
92
+ inflightControllers.add(controller);
93
+ try {
94
+ const headers = { Authorization: `Bearer ${getToken()}` };
95
+ if (lastEventId) headers["Last-Event-ID"] = lastEventId;
96
+ const response = await fetch(url, { headers, signal: controller.signal });
97
+ if (!response.ok || !response.body) {
98
+ throw new Error(`SSE connect failed: ${response.status}`);
58
99
  }
59
- logger?.info("SSE Stream Connected", {
60
- type: "sse_connected",
61
- url,
62
- status: response.status,
63
- contentType: response.headers.get("content-type") || "unknown"
64
- });
100
+ transition("open");
65
101
  const reader = response.body.getReader();
66
102
  const decoder = new TextDecoder();
67
103
  let buffer = "";
68
- let eventType = "";
69
- let eventData = "";
70
- let eventId = "";
71
- while (true) {
104
+ let currentEvent = "";
105
+ let currentData = "";
106
+ let currentId;
107
+ while (running && inflightControllers.has(controller)) {
72
108
  const { done, value } = await reader.read();
73
- if (done || closed) break;
74
- const chunk = decoder.decode(value, { stream: true });
75
- buffer += chunk;
109
+ if (done) break;
110
+ buffer += decoder.decode(value, { stream: true });
76
111
  const lines = buffer.split("\n");
77
- buffer = lines.pop() || "";
112
+ buffer = lines.pop() ?? "";
78
113
  for (const line of lines) {
79
- if (line.startsWith("event:")) {
80
- eventType = line.slice(6).trim();
81
- } else if (line.startsWith("data:")) {
82
- eventData = line.slice(5).trim();
83
- } else if (line.startsWith("id:")) {
84
- eventId = line.slice(3).trim();
114
+ if (line.startsWith("event: ")) {
115
+ currentEvent = line.slice(7);
116
+ } else if (line.startsWith("data: ")) {
117
+ currentData = line.slice(6);
118
+ } else if (line.startsWith("id: ")) {
119
+ currentId = line.slice(4);
85
120
  } else if (line === "") {
86
- if (eventData && !closed) {
87
- handleEvent(eventType, eventData, eventId);
88
- if (closed) break;
89
- eventType = "";
90
- eventData = "";
91
- eventId = "";
121
+ if (currentEvent === "bus-event" && currentData) {
122
+ if (currentId !== void 0) lastEventId = currentId;
123
+ const parsed = JSON.parse(currentData);
124
+ busLog("RECV", parsed.channel, parsed.payload, parsed.scope);
125
+ const carrier = extractTraceparent(
126
+ parsed.payload
127
+ );
128
+ await withTraceparent(
129
+ carrier,
130
+ () => withSpan(
131
+ `bus.recv:${parsed.channel}`,
132
+ () => {
133
+ events$.next(parsed);
134
+ },
135
+ {
136
+ kind: SpanKind.CONSUMER,
137
+ attrs: {
138
+ "bus.channel": parsed.channel,
139
+ ...parsed.scope ? { "bus.scope": parsed.scope } : {}
140
+ }
141
+ }
142
+ )
143
+ );
92
144
  }
145
+ currentEvent = "";
146
+ currentData = "";
147
+ currentId = void 0;
93
148
  }
94
149
  }
95
- if (closed) break;
96
- }
97
- logger?.info("SSE Stream Closed", {
98
- type: "sse_closed",
99
- url,
100
- reason: "complete"
101
- });
102
- } catch (error) {
103
- if (error instanceof Error && error.name !== "AbortError") {
104
- logger?.error("SSE Stream Error", {
105
- type: "sse_error",
106
- url,
107
- error: error.message,
108
- phase: "stream"
109
- });
110
- } else if (error instanceof Error && error.name === "AbortError") {
111
- logger?.info("SSE Stream Closed", {
112
- type: "sse_closed",
113
- url,
114
- reason: "abort"
115
- });
116
150
  }
151
+ } catch (err) {
152
+ if (err.name === "AbortError") return;
153
+ } finally {
154
+ inflightControllers.delete(controller);
117
155
  }
118
- };
119
- const handleEvent = (eventType, data, id) => {
120
- if (data.startsWith(":")) {
121
- return;
122
- }
123
- if (id) {
124
- lastEventId = id;
125
- }
126
- try {
127
- const parsed = JSON.parse(data);
128
- logger?.debug("SSE Event Received", {
129
- type: "sse_event",
130
- url,
131
- event: eventType || "message",
132
- hasData: !!data
133
- });
134
- if (typeof parsed === "object" && parsed !== null && "metadata" in parsed) {
135
- config.eventBus.get(eventType).next(parsed);
136
- return;
137
- }
138
- config.eventBus.get(eventType).next(parsed);
139
- if (config.completeEvent && eventType === config.completeEvent) {
140
- closed = true;
141
- abortController.abort();
142
- } else if (config.errorEvent && eventType === config.errorEvent) {
143
- closed = true;
144
- abortController.abort();
145
- }
146
- } catch (error) {
147
- logger?.error("SSE Failed to parse event data", { error, eventType, data });
156
+ if (running) {
157
+ transition("reconnecting");
158
+ reconnectTimer = setTimeout(() => {
159
+ if (running) connect();
160
+ }, reconnectMs);
148
161
  }
149
162
  };
150
- const runConnect = async () => {
151
- if (!config.reconnect) {
152
- return connect();
153
- }
154
- while (!closed) {
155
- abortController = new AbortController();
156
- try {
157
- await connect();
158
- if (closed) return;
159
- logger?.info("SSE Stream ended cleanly; reconnecting", { url });
160
- } catch (error) {
161
- if (closed) return;
162
- if (error instanceof Error && error.name === "AbortError") return;
163
- logger?.warn("SSE Stream errored; reconnecting", {
164
- url,
165
- error: error instanceof Error ? error.message : String(error),
166
- delayMs: reconnectDelayMs
167
- });
168
- }
169
- await new Promise((resolve) => setTimeout(resolve, reconnectDelayMs));
170
- reconnectDelayMs = Math.min(reconnectDelayMs * 2, RECONNECT_MAX_MS);
163
+ const reconnect = () => {
164
+ if (!running) return;
165
+ if (currentState === "open" || currentState === "connecting" || currentState === "degraded") {
166
+ transition("reconnecting");
171
167
  }
168
+ disconnect();
169
+ connect();
172
170
  };
173
- void runConnect();
174
- return {
175
- close() {
176
- closed = true;
177
- abortController.abort();
178
- }
171
+ let reconnectTimer2 = null;
172
+ const RECONNECT_DEBOUNCE_MS = 100;
173
+ const scheduleReconnect = () => {
174
+ if (reconnectTimer2) clearTimeout(reconnectTimer2);
175
+ reconnectTimer2 = setTimeout(() => {
176
+ reconnectTimer2 = null;
177
+ reconnect();
178
+ }, RECONNECT_DEBOUNCE_MS);
179
179
  };
180
- }
181
-
182
- // src/sse/index.ts
183
- var SSE_STREAM_CONNECTED = "stream-connected";
184
- var SSEClient = class {
185
- baseUrl;
186
- logger;
187
- constructor(config) {
188
- this.baseUrl = config.baseUrl.endsWith("/") ? config.baseUrl.slice(0, -1) : config.baseUrl;
189
- this.logger = config.logger;
190
- }
191
- /**
192
- * Get common headers for SSE requests
193
- */
194
- getHeaders(auth) {
195
- const headers = {
196
- "Content-Type": "application/json"
197
- };
198
- if (auth) {
199
- headers["Authorization"] = `Bearer ${auth}`;
200
- }
201
- return headers;
202
- }
203
- /**
204
- * Subscribe to resource events (long-lived stream)
205
- *
206
- * Opens a long-lived SSE connection to receive real-time events for a resource.
207
- * Used for collaborative editing - see events from other users as they happen.
208
- *
209
- * This stream does NOT have a complete event - it stays open until explicitly closed.
210
- *
211
- * @param resourceId - Resource URI or ID to subscribe to
212
- * @param options - Request options (auth token)
213
- * @returns SSE stream controller with event callback
214
- *
215
- * @example
216
- * ```typescript
217
- * const stream = sseClient.resourceEvents(
218
- * 'http://localhost:4000/resources/doc-123',
219
- * { auth: 'your-token' }
220
- * );
221
- *
222
- * stream.onProgress((event) => {
223
- * console.log(`Event: ${event.type}`);
224
- * console.log(`User: ${event.userId}`);
225
- * console.log(`Sequence: ${event.metadata.sequenceNumber}`);
226
- * console.log(`Payload:`, event.payload);
227
- * });
228
- *
229
- * stream.onError((error) => {
230
- * console.error('Stream error:', error.message);
231
- * });
232
- *
233
- * // Close when no longer needed (e.g., component unmount)
234
- * stream.close();
235
- * ```
236
- */
237
- resourceEvents(resourceId, options) {
238
- const url = `${this.baseUrl}/resources/${resourceId}/events/stream`;
239
- const stream = createSSEStream(
240
- url,
241
- {
242
- method: "GET",
243
- headers: this.getHeaders(options.auth)
244
- },
245
- {
246
- progressEvents: ["*"],
247
- // Accept all event types (long-lived stream)
248
- completeEvent: null,
249
- // Never completes (long-lived)
250
- errorEvent: null,
251
- // No error event (errors throw)
252
- eventBus: options.eventBus,
253
- reconnect: true
254
- },
255
- this.logger
256
- );
257
- if (options.onConnected) {
258
- const sub = options.eventBus.get(SSE_STREAM_CONNECTED).subscribe(() => {
259
- options.onConnected();
260
- sub.unsubscribe();
261
- });
262
- }
263
- return stream;
264
- }
265
- /**
266
- * Subscribe to global system events (long-lived stream)
267
- *
268
- * Opens a long-lived SSE connection to receive system-level domain events
269
- * (entity type additions, etc.) that are not scoped to a specific resource.
270
- *
271
- * @param options - Request options (auth token, eventBus)
272
- * @returns SSE stream controller
273
- *
274
- * @example
275
- * ```typescript
276
- * const stream = sseClient.globalEvents({ auth: 'your-token', eventBus });
277
- *
278
- * // Events auto-emit to EventBus typed channels — subscribe there
279
- * eventBus.get('mark:entity-type-added').subscribe((stored) => {
280
- * // Invalidate entity types query
281
- * });
282
- *
283
- * // Close when no longer needed
284
- * stream.close();
285
- * ```
286
- */
287
- globalEvents(options) {
288
- const url = `${this.baseUrl}/api/events/stream`;
289
- const stream = createSSEStream(
290
- url,
291
- {
292
- method: "GET",
293
- headers: this.getHeaders(options.auth)
294
- },
295
- {
296
- progressEvents: ["*"],
297
- completeEvent: null,
298
- errorEvent: null,
299
- eventBus: options.eventBus,
300
- reconnect: true
301
- },
302
- this.logger
303
- );
304
- if (options.onConnected) {
305
- const sub = options.eventBus.get(SSE_STREAM_CONNECTED).subscribe(() => {
306
- options.onConnected();
307
- sub.unsubscribe();
308
- });
309
- }
310
- return stream;
311
- }
312
- /**
313
- * Subscribe to participant attention stream (long-lived stream)
314
- *
315
- * Opens a participant-scoped SSE connection to receive cross-participant
316
- * beckon signals. Signals are delivered as 'beckon:focus' events routed
317
- * to the EventBus — the existing scroll/highlight machinery handles them.
318
- *
319
- * Signals are ephemeral — delivered if connected, dropped if not.
320
- *
321
- * @param options - Request options (auth token, eventBus)
322
- * @returns SSE stream controller
323
- */
324
- attentionStream(options) {
325
- const url = `${this.baseUrl}/api/participants/me/attention-stream`;
326
- const stream = createSSEStream(
327
- url,
328
- {
329
- method: "GET",
330
- headers: this.getHeaders(options.auth)
331
- },
332
- {
333
- progressEvents: ["*"],
334
- completeEvent: null,
335
- errorEvent: null,
336
- eventBus: options.eventBus,
337
- reconnect: true
338
- },
339
- this.logger
340
- );
341
- if (options.onConnected) {
342
- const sub = options.eventBus.get(SSE_STREAM_CONNECTED).subscribe(() => {
343
- options.onConnected();
344
- sub.unsubscribe();
345
- });
346
- }
347
- return stream;
348
- }
349
- };
350
- var BrowseNamespace = class {
351
- constructor(http, eventBus, getToken) {
352
- this.http = http;
353
- this.eventBus = eventBus;
354
- this.getToken = getToken;
355
- this.subscribeToEvents();
356
- }
357
- // ── Annotation list cache ───────────────────────────────────────────────
358
- annotationList$ = new BehaviorSubject(/* @__PURE__ */ new Map());
359
- fetchingAnnotationList = /* @__PURE__ */ new Set();
360
- annotationListObs$ = /* @__PURE__ */ new Map();
361
- // ── Annotation detail cache ─────────────────────────────────────────────
362
- annotationDetail$ = new BehaviorSubject(/* @__PURE__ */ new Map());
363
- fetchingAnnotationDetail = /* @__PURE__ */ new Set();
364
- annotationDetailObs$ = /* @__PURE__ */ new Map();
365
- // ── Resource detail cache ───────────────────────────────────────────────
366
- resourceDetail$ = new BehaviorSubject(/* @__PURE__ */ new Map());
367
- fetchingResourceDetail = /* @__PURE__ */ new Set();
368
- resourceDetailObs$ = /* @__PURE__ */ new Map();
369
- // ── Resource list cache ─────────────────────────────────────────────────
370
- resourceList$ = new BehaviorSubject(/* @__PURE__ */ new Map());
371
- fetchingResourceList = /* @__PURE__ */ new Set();
372
- resourceListObs$ = /* @__PURE__ */ new Map();
373
- // ── Entity types cache ──────────────────────────────────────────────────
374
- entityTypes$ = new BehaviorSubject(void 0);
375
- fetchingEntityTypes = false;
376
- // ── Referenced-by cache ─────────────────────────────────────────────────
377
- referencedBy$ = new BehaviorSubject(/* @__PURE__ */ new Map());
378
- fetchingReferencedBy = /* @__PURE__ */ new Set();
379
- referencedByObs$ = /* @__PURE__ */ new Map();
380
- getToken;
381
- // ── Live queries ────────────────────────────────────────────────────────
382
- resource(resourceId) {
383
- if (!this.resourceDetail$.value.has(resourceId) && !this.fetchingResourceDetail.has(resourceId)) {
384
- this.fetchResourceDetail(resourceId);
385
- }
386
- let obs = this.resourceDetailObs$.get(resourceId);
387
- if (!obs) {
388
- obs = this.resourceDetail$.pipe(map((m) => m.get(resourceId)), distinctUntilChanged());
389
- this.resourceDetailObs$.set(resourceId, obs);
390
- }
391
- return obs;
392
- }
393
- resources(filters) {
394
- const key = JSON.stringify(filters ?? {});
395
- if (!this.resourceList$.value.has(key) && !this.fetchingResourceList.has(key)) {
396
- this.fetchResourceList(key, filters);
397
- }
398
- let obs = this.resourceListObs$.get(key);
399
- if (!obs) {
400
- obs = this.resourceList$.pipe(map((m) => m.get(key)), distinctUntilChanged());
401
- this.resourceListObs$.set(key, obs);
402
- }
403
- return obs;
404
- }
405
- annotations(resourceId) {
406
- if (!this.annotationList$.value.has(resourceId) && !this.fetchingAnnotationList.has(resourceId)) {
407
- this.fetchAnnotationList(resourceId);
408
- }
409
- let obs = this.annotationListObs$.get(resourceId);
410
- if (!obs) {
411
- obs = this.annotationList$.pipe(
412
- map((m) => m.get(resourceId)?.annotations),
413
- distinctUntilChanged()
180
+ return {
181
+ on$(channel) {
182
+ return shared$.pipe(
183
+ filter((e) => e.channel === channel),
184
+ map((e) => e.payload)
414
185
  );
415
- this.annotationListObs$.set(resourceId, obs);
416
- }
417
- return obs;
418
- }
419
- annotation(resourceId, annotationId) {
420
- if (!this.annotationDetail$.value.has(annotationId) && !this.fetchingAnnotationDetail.has(annotationId)) {
421
- this.fetchAnnotationDetail(resourceId, annotationId);
422
- }
423
- let obs = this.annotationDetailObs$.get(annotationId);
424
- if (!obs) {
425
- obs = this.annotationDetail$.pipe(map((m) => m.get(annotationId)), distinctUntilChanged());
426
- this.annotationDetailObs$.set(annotationId, obs);
427
- }
428
- return obs;
429
- }
430
- entityTypes() {
431
- if (this.entityTypes$.value === void 0 && !this.fetchingEntityTypes) {
432
- this.fetchEntityTypes();
433
- }
434
- return this.entityTypes$.asObservable();
435
- }
436
- referencedBy(resourceId) {
437
- if (!this.referencedBy$.value.has(resourceId) && !this.fetchingReferencedBy.has(resourceId)) {
438
- this.fetchReferencedBy(resourceId);
439
- }
440
- let obs = this.referencedByObs$.get(resourceId);
441
- if (!obs) {
442
- obs = this.referencedBy$.pipe(map((m) => m.get(resourceId)), distinctUntilChanged());
443
- this.referencedByObs$.set(resourceId, obs);
444
- }
445
- return obs;
446
- }
447
- // ── One-shot reads ──────────────────────────────────────────────────────
448
- async resourceContent(resourceId) {
449
- const result = await this.http.getResourceRepresentation(resourceId, {
450
- accept: "text/plain",
451
- auth: this.getToken()
452
- });
453
- const decoder = new TextDecoder();
454
- return decoder.decode(result.data);
455
- }
456
- async resourceRepresentation(resourceId, options) {
457
- return this.http.getResourceRepresentation(resourceId, {
458
- accept: options?.accept,
459
- auth: this.getToken()
460
- });
461
- }
462
- async resourceRepresentationStream(resourceId, options) {
463
- return this.http.getResourceRepresentationStream(resourceId, {
464
- accept: options?.accept,
465
- auth: this.getToken()
466
- });
467
- }
468
- async resourceEvents(resourceId) {
469
- const result = await this.http.getResourceEvents(resourceId, { auth: this.getToken() });
470
- return result.events;
471
- }
472
- async annotationHistory(resourceId, annotationId) {
473
- return this.http.getAnnotationHistory(resourceId, annotationId, { auth: this.getToken() });
474
- }
475
- async connections(_resourceId) {
476
- throw new Error("Not implemented: connections endpoint does not exist yet");
477
- }
478
- async backlinks(_resourceId) {
479
- throw new Error("Not implemented: backlinks endpoint does not exist yet");
480
- }
481
- async resourcesByName(_query, _limit) {
482
- throw new Error("Not implemented: resourcesByName endpoint does not exist yet");
483
- }
484
- async files(dirPath, sort) {
485
- return this.http.browseFiles(dirPath, sort, { auth: this.getToken() });
486
- }
487
- // ── Invalidation (exposed for other namespaces) ─────────────────────────
488
- invalidateAnnotationList(resourceId) {
489
- const next = new Map(this.annotationList$.value);
490
- next.delete(resourceId);
491
- this.annotationList$.next(next);
492
- this.fetchAnnotationList(resourceId);
493
- }
494
- invalidateAnnotationDetail(annotationId) {
495
- const next = new Map(this.annotationDetail$.value);
496
- next.delete(annotationId);
497
- this.annotationDetail$.next(next);
498
- }
499
- invalidateResourceDetail(id) {
500
- const next = new Map(this.resourceDetail$.value);
501
- next.delete(id);
502
- this.resourceDetail$.next(next);
503
- this.fetchResourceDetail(id);
504
- }
505
- invalidateResourceLists() {
506
- this.resourceList$.next(/* @__PURE__ */ new Map());
507
- }
508
- invalidateEntityTypes() {
509
- this.entityTypes$.next(void 0);
510
- this.fetchEntityTypes();
511
- }
512
- invalidateReferencedBy(resourceId) {
513
- const next = new Map(this.referencedBy$.value);
514
- next.delete(resourceId);
515
- this.referencedBy$.next(next);
516
- this.fetchReferencedBy(resourceId);
517
- }
518
- updateAnnotationInPlace(resourceId, annotation) {
519
- const currentList = this.annotationList$.value.get(resourceId);
520
- if (!currentList) return;
521
- const existingIdx = currentList.annotations.findIndex((a) => a.id === annotation.id);
522
- const nextAnnotations = existingIdx >= 0 ? currentList.annotations.map((a, i) => i === existingIdx ? annotation : a) : [...currentList.annotations, annotation];
523
- const nextList = { ...currentList, annotations: nextAnnotations };
524
- const nextMap = new Map(this.annotationList$.value);
525
- nextMap.set(resourceId, nextList);
526
- this.annotationList$.next(nextMap);
527
- }
528
- // ── EventBus subscriptions ──────────────────────────────────────────────
529
- subscribeToEvents() {
530
- const bus = this.eventBus;
531
- bus.get("mark:delete-ok").subscribe((event) => {
532
- this.invalidateAnnotationDetail(annotationId(event.annotationId));
533
- });
534
- bus.get("mark:added").subscribe((stored) => {
535
- if (stored.resourceId) {
536
- this.invalidateAnnotationList(stored.resourceId);
186
+ },
187
+ emit: async (channel, payload, emitScope) => {
188
+ const body = { channel, payload };
189
+ if (emitScope) body.scope = emitScope;
190
+ const headers = {
191
+ "Content-Type": "application/json",
192
+ Authorization: `Bearer ${getToken()}`
193
+ };
194
+ const trace = getActiveTraceparent();
195
+ if (trace) {
196
+ headers["traceparent"] = trace.traceparent;
197
+ if (trace.tracestate) headers["tracestate"] = trace.tracestate;
537
198
  }
538
- });
539
- bus.get("mark:removed").subscribe((stored) => {
540
- if (stored.resourceId) {
541
- this.invalidateAnnotationList(stored.resourceId);
199
+ await fetch(`${baseUrl}/bus/emit`, {
200
+ method: "POST",
201
+ headers,
202
+ body: JSON.stringify(body)
203
+ });
204
+ },
205
+ state$: state$.asObservable(),
206
+ addChannels: (channels, scope) => {
207
+ let changed = false;
208
+ if (scope !== void 0) {
209
+ for (const ch of channels) {
210
+ if (!scopedChannels.has(ch)) {
211
+ scopedChannels.add(ch);
212
+ changed = true;
213
+ }
214
+ }
215
+ if (scope !== activeScope) {
216
+ activeScope = scope;
217
+ changed = true;
218
+ }
219
+ } else {
220
+ for (const ch of channels) {
221
+ if (!globalChannels.has(ch)) {
222
+ globalChannels.add(ch);
223
+ changed = true;
224
+ }
225
+ }
542
226
  }
543
- this.invalidateAnnotationDetail(annotationId(stored.payload.annotationId));
544
- });
545
- bus.get("mark:body-updated").subscribe((event) => {
546
- const enriched = event;
547
- if (!enriched.resourceId || !enriched.annotation) return;
548
- this.updateAnnotationInPlace(enriched.resourceId, enriched.annotation);
549
- this.invalidateAnnotationDetail(annotationId(enriched.annotation.id));
550
- });
551
- bus.get("mark:entity-tag-added").subscribe((stored) => {
552
- if (stored.resourceId) {
553
- this.invalidateAnnotationList(stored.resourceId);
554
- this.invalidateResourceDetail(stored.resourceId);
227
+ if (changed) scheduleReconnect();
228
+ },
229
+ removeChannels: (channels) => {
230
+ let changed = false;
231
+ for (const ch of channels) {
232
+ if (scopedChannels.delete(ch)) changed = true;
233
+ if (globalChannels.delete(ch)) changed = true;
555
234
  }
556
- });
557
- bus.get("mark:entity-tag-removed").subscribe((stored) => {
558
- if (stored.resourceId) {
559
- this.invalidateAnnotationList(stored.resourceId);
560
- this.invalidateResourceDetail(stored.resourceId);
235
+ if (scopedChannels.size === 0) activeScope = void 0;
236
+ if (changed) scheduleReconnect();
237
+ },
238
+ start: () => {
239
+ if (running) return;
240
+ running = true;
241
+ connect();
242
+ },
243
+ stop: () => {
244
+ running = false;
245
+ if (currentState !== "closed") transition("closed");
246
+ if (reconnectTimer2) {
247
+ clearTimeout(reconnectTimer2);
248
+ reconnectTimer2 = null;
561
249
  }
562
- });
563
- bus.get("replay-window-exceeded").subscribe((event) => {
564
- if (event.resourceId) {
565
- this.invalidateAnnotationList(event.resourceId);
250
+ if (degradedTimer) {
251
+ clearTimeout(degradedTimer);
252
+ degradedTimer = null;
566
253
  }
567
- });
568
- bus.get("yield:create-ok").subscribe((event) => {
569
- this.fetchResourceDetail(resourceId(event.resourceId));
570
- this.invalidateResourceLists();
571
- });
572
- bus.get("yield:update-ok").subscribe((event) => {
573
- this.invalidateResourceDetail(resourceId(event.resourceId));
574
- this.invalidateResourceLists();
575
- });
576
- bus.get("mark:archived").subscribe((stored) => {
577
- if (stored.resourceId) {
578
- this.invalidateResourceDetail(stored.resourceId);
579
- this.invalidateResourceLists();
254
+ disconnect();
255
+ },
256
+ dispose: () => {
257
+ running = false;
258
+ if (currentState !== "closed") transition("closed");
259
+ if (reconnectTimer2) {
260
+ clearTimeout(reconnectTimer2);
261
+ reconnectTimer2 = null;
580
262
  }
581
- });
582
- bus.get("mark:unarchived").subscribe((stored) => {
583
- if (stored.resourceId) {
584
- this.invalidateResourceDetail(stored.resourceId);
585
- this.invalidateResourceLists();
263
+ if (degradedTimer) {
264
+ clearTimeout(degradedTimer);
265
+ degradedTimer = null;
586
266
  }
587
- });
588
- bus.get("mark:entity-type-added").subscribe(() => {
589
- this.invalidateEntityTypes();
590
- });
591
- }
592
- // ── Fetch helpers ───────────────────────────────────────────────────────
593
- async fetchAnnotationList(resourceId) {
594
- if (this.fetchingAnnotationList.has(resourceId)) return;
595
- this.fetchingAnnotationList.add(resourceId);
596
- try {
597
- const result = await this.http.browseAnnotations(resourceId, void 0, { auth: this.getToken() });
598
- const next = new Map(this.annotationList$.value);
599
- next.set(resourceId, result);
600
- this.annotationList$.next(next);
601
- } catch {
602
- } finally {
603
- this.fetchingAnnotationList.delete(resourceId);
604
- }
605
- }
606
- async fetchAnnotationDetail(resourceId, annotationId) {
607
- if (this.fetchingAnnotationDetail.has(annotationId)) return;
608
- this.fetchingAnnotationDetail.add(annotationId);
609
- try {
610
- const result = await this.http.browseAnnotation(resourceId, annotationId, { auth: this.getToken() });
611
- const next = new Map(this.annotationDetail$.value);
612
- next.set(annotationId, result.annotation);
613
- this.annotationDetail$.next(next);
614
- } catch {
615
- } finally {
616
- this.fetchingAnnotationDetail.delete(annotationId);
617
- }
618
- }
619
- async fetchResourceDetail(id) {
620
- if (this.fetchingResourceDetail.has(id)) return;
621
- this.fetchingResourceDetail.add(id);
622
- try {
623
- const result = await this.http.browseResource(id, { auth: this.getToken() });
624
- const next = new Map(this.resourceDetail$.value);
625
- next.set(id, result.resource);
626
- this.resourceDetail$.next(next);
627
- } catch {
628
- } finally {
629
- this.fetchingResourceDetail.delete(id);
630
- }
631
- }
632
- async fetchResourceList(key, filters) {
633
- if (this.fetchingResourceList.has(key)) return;
634
- this.fetchingResourceList.add(key);
635
- try {
636
- const search = filters?.search ? searchQuery(filters.search) : void 0;
637
- const result = await this.http.browseResources(filters?.limit, filters?.archived, search, { auth: this.getToken() });
638
- const next = new Map(this.resourceList$.value);
639
- next.set(key, result.resources);
640
- this.resourceList$.next(next);
641
- } catch {
642
- } finally {
643
- this.fetchingResourceList.delete(key);
644
- }
645
- }
646
- async fetchEntityTypes() {
647
- if (this.fetchingEntityTypes) return;
648
- this.fetchingEntityTypes = true;
649
- try {
650
- const result = await this.http.listEntityTypes({ auth: this.getToken() });
651
- this.entityTypes$.next(result.entityTypes);
652
- } catch {
653
- } finally {
654
- this.fetchingEntityTypes = false;
655
- }
656
- }
657
- async fetchReferencedBy(resourceId) {
658
- if (this.fetchingReferencedBy.has(resourceId)) return;
659
- this.fetchingReferencedBy.add(resourceId);
660
- try {
661
- const result = await this.http.browseReferences(resourceId, { auth: this.getToken() });
662
- const next = new Map(this.referencedBy$.value);
663
- next.set(resourceId, result.referencedBy);
664
- this.referencedBy$.next(next);
665
- } catch {
666
- } finally {
667
- this.fetchingReferencedBy.delete(resourceId);
668
- }
669
- }
670
- };
671
- var MarkNamespace = class {
672
- constructor(http, eventBus, getToken) {
673
- this.http = http;
674
- this.eventBus = eventBus;
675
- this.getToken = getToken;
676
- }
677
- async annotation(resourceId, input) {
678
- return this.http.markAnnotation(resourceId, input, { auth: this.getToken() });
679
- }
680
- async delete(resourceId, annotationId) {
681
- return this.http.deleteAnnotation(resourceId, annotationId, { auth: this.getToken() });
682
- }
683
- async entityType(type) {
684
- return this.http.addEntityType(type, { auth: this.getToken() });
685
- }
686
- async entityTypes(types) {
687
- return this.http.addEntityTypesBulk(types, { auth: this.getToken() });
688
- }
689
- async updateResource(resourceId, data) {
690
- return this.http.updateResource(resourceId, data, { auth: this.getToken() });
691
- }
692
- async archive(resourceId) {
693
- return this.http.updateResource(resourceId, { archived: true }, { auth: this.getToken() });
694
- }
695
- async unarchive(resourceId) {
696
- return this.http.updateResource(resourceId, { archived: false }, { auth: this.getToken() });
697
- }
698
- assist(resourceId, motivation, options) {
699
- return new Observable((subscriber) => {
700
- const progress$ = this.eventBus.get("mark:progress").pipe(
701
- filter((e) => e.resourceId === resourceId)
702
- );
703
- const finished$ = this.eventBus.get("mark:assist-finished").pipe(
704
- filter((e) => e.resourceId === resourceId && e.motivation === motivation)
705
- );
706
- const failed$ = this.eventBus.get("mark:assist-failed").pipe(
707
- filter((e) => e.resourceId === resourceId)
708
- );
709
- const progressSub = progress$.pipe(takeUntil(merge(finished$, failed$))).subscribe((e) => subscriber.next(e));
710
- const finishedSub = finished$.subscribe((e) => {
711
- subscriber.next(e);
712
- subscriber.complete();
713
- });
714
- const failedSub = failed$.subscribe((e) => {
715
- subscriber.error(new Error(e.message));
716
- });
717
- const auth = this.getToken();
718
- const postPromise = this.dispatchAssist(resourceId, motivation, options, auth);
719
- postPromise.catch((error) => {
720
- subscriber.error(error);
721
- });
722
- return () => {
723
- progressSub.unsubscribe();
724
- finishedSub.unsubscribe();
725
- failedSub.unsubscribe();
726
- };
727
- });
728
- }
729
- async dispatchAssist(resourceId, motivation, options, auth) {
730
- if (motivation === "tagging") {
731
- const { schemaId, categories } = options;
732
- if (!schemaId || !categories?.length) throw new Error("Tag assist requires schemaId and categories");
733
- await this.http.annotateTags(resourceId, { schemaId, categories }, { auth });
734
- } else if (motivation === "linking") {
735
- const { entityTypes, includeDescriptiveReferences } = options;
736
- if (!entityTypes?.length) throw new Error("Reference assist requires entityTypes");
737
- await this.http.annotateReferences(resourceId, {
738
- entityTypes,
739
- includeDescriptiveReferences: includeDescriptiveReferences ?? false
740
- }, { auth });
741
- } else if (motivation === "highlighting") {
742
- await this.http.annotateHighlights(resourceId, {
743
- instructions: options.instructions,
744
- density: options.density
745
- }, { auth });
746
- } else if (motivation === "assessing") {
747
- await this.http.annotateAssessments(resourceId, {
748
- instructions: options.instructions,
749
- tone: options.tone,
750
- density: options.density,
751
- language: options.language
752
- }, { auth });
753
- } else if (motivation === "commenting") {
754
- await this.http.annotateComments(resourceId, {
755
- instructions: options.instructions,
756
- tone: options.tone,
757
- density: options.density,
758
- language: options.language
759
- }, { auth });
267
+ disconnect();
268
+ events$.complete();
269
+ state$.complete();
760
270
  }
761
- }
762
- };
763
-
764
- // src/namespaces/bind.ts
765
- var BindNamespace = class {
766
- constructor(http, getToken) {
767
- this.http = http;
768
- this.getToken = getToken;
769
- }
770
- async body(resourceId, annotationId, operations) {
771
- await this.http.bindAnnotation(resourceId, annotationId, { operations }, { auth: this.getToken() });
772
- }
773
- };
774
- var GatherNamespace = class {
775
- constructor(http, eventBus, getToken) {
776
- this.http = http;
777
- this.eventBus = eventBus;
778
- this.getToken = getToken;
779
- }
780
- annotation(annotationId$1, resourceId, options) {
781
- return new Observable((subscriber) => {
782
- const correlationId = crypto.randomUUID();
783
- const complete$ = this.eventBus.get("gather:complete").pipe(
784
- filter((e) => e.correlationId === correlationId)
785
- );
786
- const failed$ = this.eventBus.get("gather:failed").pipe(
787
- filter((e) => e.correlationId === correlationId)
788
- );
789
- const sub = merge(
790
- this.eventBus.get("gather:annotation-progress").pipe(
791
- // Progress events don't carry correlationId, so match by annotationId
792
- filter((e) => e.annotationId === annotationId$1),
793
- map((e) => e)
794
- ),
795
- complete$.pipe(map((e) => e))
796
- ).pipe(takeUntil(merge(complete$, failed$))).subscribe({
797
- next: (v) => subscriber.next(v),
798
- error: (e) => subscriber.error(e)
799
- });
800
- const completeSub = complete$.subscribe((e) => {
801
- subscriber.next(e);
802
- subscriber.complete();
803
- });
804
- const failedSub = failed$.subscribe((e) => {
805
- subscriber.error(new Error(e.message));
806
- });
807
- this.http.gatherAnnotationContext(
808
- resourceId,
809
- annotationId(annotationId$1),
810
- { correlationId, contextWindow: options?.contextWindow ?? 2e3 },
811
- { auth: this.getToken() }
812
- ).catch((error) => {
813
- subscriber.error(error);
814
- });
815
- return () => {
816
- sub.unsubscribe();
817
- completeSub.unsubscribe();
818
- failedSub.unsubscribe();
819
- };
820
- });
821
- }
822
- resource(_resourceId, _options) {
823
- throw new Error("Not implemented: gather.resource() \u2014 no backend route yet");
824
- }
825
- };
826
- var MatchNamespace = class {
827
- constructor(http, eventBus, getToken) {
828
- this.http = http;
829
- this.eventBus = eventBus;
830
- this.getToken = getToken;
831
- }
832
- search(resourceId, referenceId, context, options) {
833
- return new Observable((subscriber) => {
834
- const correlationId = crypto.randomUUID();
835
- const result$ = this.eventBus.get("match:search-results").pipe(
836
- filter((e) => e.correlationId === correlationId)
837
- );
838
- const failed$ = this.eventBus.get("match:search-failed").pipe(
839
- filter((e) => e.correlationId === correlationId)
840
- );
841
- const resultSub = result$.subscribe((e) => {
842
- subscriber.next(e);
843
- subscriber.complete();
844
- });
845
- const failedSub = failed$.subscribe((e) => {
846
- subscriber.error(new Error(e.error));
847
- });
848
- this.http.matchSearch(
849
- resourceId,
850
- {
851
- correlationId,
852
- referenceId,
853
- context,
854
- limit: options?.limit,
855
- useSemanticScoring: options?.useSemanticScoring
856
- },
857
- { auth: this.getToken() }
858
- ).catch((error) => {
859
- subscriber.error(error);
860
- });
861
- return () => {
862
- resultSub.unsubscribe();
863
- failedSub.unsubscribe();
864
- };
865
- });
866
- }
867
- };
868
- var YieldNamespace = class {
869
- constructor(http, eventBus, getToken) {
870
- this.http = http;
871
- this.eventBus = eventBus;
872
- this.getToken = getToken;
873
- }
874
- async resource(data) {
875
- return this.http.yieldResource(data, { auth: this.getToken() });
876
- }
877
- fromAnnotation(resourceId$1, annotationId$1, options) {
878
- return new Observable((subscriber) => {
879
- const progress$ = this.eventBus.get("yield:progress").pipe(
880
- filter((e) => e.referenceId === annotationId$1)
881
- );
882
- const finished$ = this.eventBus.get("yield:finished").pipe(
883
- filter((e) => e.referenceId === annotationId$1)
884
- );
885
- const failed$ = this.eventBus.get("yield:failed").pipe(
886
- filter((e) => e.referenceId === annotationId$1)
887
- );
888
- const progressSub = progress$.pipe(takeUntil(merge(finished$, failed$))).subscribe((e) => subscriber.next(e));
889
- const finishedSub = finished$.subscribe((event) => {
890
- subscriber.next(event);
891
- subscriber.complete();
892
- if (event.resourceId && event.referenceId && event.sourceResourceId) {
893
- this.eventBus.get("bind:update-body").next({
894
- correlationId: crypto.randomUUID(),
895
- annotationId: annotationId(event.referenceId),
896
- resourceId: resourceId(event.sourceResourceId),
897
- operations: [{ op: "add", item: { type: "SpecificResource", source: event.resourceId } }]
898
- });
899
- }
900
- });
901
- const failedSub = failed$.subscribe((e) => {
902
- subscriber.error(new Error(e.error ?? e.message ?? "Generation failed"));
903
- });
904
- this.http.yieldResourceFromAnnotation(
905
- resourceId$1,
906
- annotationId$1,
907
- options,
908
- { auth: this.getToken() }
909
- ).catch((error) => {
910
- subscriber.error(error);
911
- });
912
- return () => {
913
- progressSub.unsubscribe();
914
- finishedSub.unsubscribe();
915
- failedSub.unsubscribe();
916
- };
917
- });
918
- }
919
- async cloneToken(resourceId) {
920
- const result = await this.http.generateCloneToken(resourceId, { auth: this.getToken() });
921
- return result;
922
- }
923
- async fromToken(token) {
924
- const result = await this.http.getResourceByToken(token, { auth: this.getToken() });
925
- return result.sourceResource;
926
- }
927
- async createFromToken(options) {
928
- return this.http.createResourceFromToken(options, { auth: this.getToken() });
929
- }
930
- };
931
-
932
- // src/namespaces/beckon.ts
933
- var BeckonNamespace = class {
934
- constructor(http, getToken) {
935
- this.http = http;
936
- this.getToken = getToken;
937
- }
938
- attention(annotationId, resourceId) {
939
- this.http.beckonAttention(
940
- "me",
941
- // participantId — always 'me' for self-identification
942
- { annotationId, resourceId },
943
- { auth: this.getToken() }
944
- ).catch(() => {
945
- });
946
- }
947
- };
948
-
949
- // src/namespaces/job.ts
950
- var JobNamespace = class {
951
- constructor(http, getToken) {
952
- this.http = http;
953
- this.getToken = getToken;
954
- }
955
- async status(jobId) {
956
- return this.http.getJobStatus(jobId, { auth: this.getToken() });
957
- }
958
- async pollUntilComplete(jobId, options) {
959
- return this.http.pollJobUntilComplete(jobId, {
960
- interval: options?.interval,
961
- timeout: options?.timeout,
962
- onProgress: options?.onProgress,
963
- auth: this.getToken()
964
- });
965
- }
966
- async cancel(jobId, type) {
967
- throw new Error(`Not implemented: job.cancel(${jobId}, ${type}) \u2014 needs EventBus wiring`);
968
- }
969
- };
970
- var AuthNamespace = class {
971
- constructor(http, getToken) {
972
- this.http = http;
973
- this.getToken = getToken;
974
- }
975
- async password(emailStr, passwordStr) {
976
- return this.http.authenticatePassword(email(emailStr), passwordStr);
977
- }
978
- async google(credential) {
979
- return this.http.authenticateGoogle(googleCredential(credential));
980
- }
981
- async refresh(token) {
982
- return this.http.refreshToken(refreshToken(token));
983
- }
984
- async logout() {
985
- await this.http.logout({ auth: this.getToken() });
986
- }
987
- async me() {
988
- return this.http.getMe({ auth: this.getToken() });
989
- }
990
- async acceptTerms() {
991
- await this.http.acceptTerms({ auth: this.getToken() });
992
- }
993
- async mcpToken() {
994
- return this.http.generateMCPToken({ auth: this.getToken() });
995
- }
996
- async mediaToken(resourceId) {
997
- return this.http.getMediaToken(resourceId, { auth: this.getToken() });
998
- }
999
- };
1000
-
1001
- // src/namespaces/admin.ts
1002
- var AdminNamespace = class {
1003
- constructor(http, getToken) {
1004
- this.http = http;
1005
- this.getToken = getToken;
1006
- }
1007
- async users() {
1008
- const result = await this.http.listUsers({ auth: this.getToken() });
1009
- return result.users;
1010
- }
1011
- async userStats() {
1012
- return this.http.getUserStats({ auth: this.getToken() });
1013
- }
1014
- async updateUser(userId, data) {
1015
- const result = await this.http.updateUser(userId, data, { auth: this.getToken() });
1016
- return result.user;
1017
- }
1018
- async oauthConfig() {
1019
- return this.http.getOAuthConfig({ auth: this.getToken() });
1020
- }
1021
- async healthCheck() {
1022
- return this.http.healthCheck({ auth: this.getToken() });
1023
- }
1024
- async status() {
1025
- return this.http.getStatus({ auth: this.getToken() });
1026
- }
1027
- async backup() {
1028
- return this.http.backupKnowledgeBase({ auth: this.getToken() });
1029
- }
1030
- async restore(file, onProgress) {
1031
- return this.http.restoreKnowledgeBase(file, { auth: this.getToken(), onProgress });
1032
- }
1033
- async exportKnowledgeBase(params) {
1034
- return this.http.exportKnowledgeBase(params, { auth: this.getToken() });
1035
- }
1036
- async importKnowledgeBase(file, onProgress) {
1037
- return this.http.importKnowledgeBase(file, { auth: this.getToken(), onProgress });
1038
- }
1039
- };
1040
-
1041
- // src/client.ts
271
+ };
272
+ }
273
+ var RESOURCE_SCOPED_CHANNELS = [
274
+ ...PERSISTED_EVENT_TYPES.filter((t) => t !== "mark:entity-type-added"),
275
+ ...RESOURCE_BROADCAST_TYPES
276
+ ];
1042
277
  var APIError = class extends Error {
1043
278
  constructor(message, status, statusText, details) {
1044
279
  super(message);
@@ -1048,40 +283,22 @@ var APIError = class extends Error {
1048
283
  this.name = "APIError";
1049
284
  }
1050
285
  };
1051
- var SemiontApiClient = class {
1052
- http;
286
+ var HttpTransport = class {
1053
287
  baseUrl;
1054
- /** The workspace-scoped EventBus this client was constructed with. */
1055
- eventBus;
288
+ http;
289
+ token$;
1056
290
  logger;
1057
- /**
1058
- * Shared mutable token getter. All verb namespaces read from this.
1059
- * Updated via setTokenGetter() from the React auth layer.
1060
- */
1061
- _getToken = () => void 0;
1062
- /**
1063
- * SSE streaming client for real-time operations
1064
- *
1065
- * Separate from the main HTTP client to clearly mark streaming endpoints.
1066
- * Uses native fetch() instead of ky for SSE support.
1067
- */
1068
- sse;
1069
- // ── Verb-oriented namespace API ──────────────────────────────────────────
1070
- browse;
1071
- mark;
1072
- bind;
1073
- gather;
1074
- match;
1075
- yield;
1076
- beckon;
1077
- job;
1078
- auth;
1079
- admin;
291
+ _actor = null;
292
+ _actorStarted = false;
293
+ disposed = false;
294
+ activeResource = null;
295
+ /** Buses we've been asked to bridge wire events into. */
296
+ bridges = [];
1080
297
  constructor(config) {
1081
- const { baseUrl, eventBus, timeout = 3e4, retry = 2, logger, tokenRefresher } = config;
1082
- this.eventBus = eventBus;
1083
- this.logger = logger;
298
+ const { baseUrl, timeout = 3e4, retry = 2, logger, tokenRefresher } = config;
1084
299
  this.baseUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
300
+ this.token$ = config.token$ ?? new BehaviorSubject(null);
301
+ this.logger = logger;
1085
302
  const retryConfig = tokenRefresher ? {
1086
303
  limit: 1,
1087
304
  methods: ["get", "post", "put", "patch", "delete", "head", "options"],
@@ -1161,462 +378,234 @@ var SemiontApiClient = class {
1161
378
  ]
1162
379
  }
1163
380
  });
1164
- this.sse = new SSEClient({
1165
- baseUrl: this.baseUrl,
1166
- logger: this.logger
1167
- });
1168
- if (config.getToken) this._getToken = config.getToken;
1169
- const getToken = () => this._getToken();
1170
- this.browse = new BrowseNamespace(this, this.eventBus, getToken);
1171
- this.mark = new MarkNamespace(this, this.eventBus, getToken);
1172
- this.bind = new BindNamespace(this, getToken);
1173
- this.gather = new GatherNamespace(this, this.eventBus, getToken);
1174
- this.match = new MatchNamespace(this, this.eventBus, getToken);
1175
- this.yield = new YieldNamespace(this, this.eventBus, getToken);
1176
- this.beckon = new BeckonNamespace(this, getToken);
1177
- this.job = new JobNamespace(this, getToken);
1178
- this.auth = new AuthNamespace(this, getToken);
1179
- this.admin = new AdminNamespace(this, getToken);
1180
- }
1181
- /**
1182
- * Update the token getter for all verb namespaces.
1183
- * Called from the React auth layer when the token changes.
1184
- * All namespaces share this getter via closure — no per-namespace sync needed.
1185
- */
1186
- setTokenGetter(getter) {
1187
- this._getToken = getter;
1188
- }
1189
- authHeaders(options) {
1190
- return options?.auth ? { Authorization: `Bearer ${options.auth}` } : {};
1191
- }
1192
- // ============================================================================
1193
- // AUTHENTICATION
1194
- // ============================================================================
1195
- async authenticatePassword(email, password, options) {
1196
- return this.http.post(`${this.baseUrl}/api/tokens/password`, {
1197
- json: { email, password },
1198
- headers: this.authHeaders(options)
1199
- }).json();
1200
- }
1201
- async refreshToken(token, options) {
1202
- return this.http.post(`${this.baseUrl}/api/tokens/refresh`, {
1203
- json: { refreshToken: token },
1204
- headers: this.authHeaders(options)
1205
- }).json();
1206
- }
1207
- async authenticateGoogle(credential, options) {
1208
- return this.http.post(`${this.baseUrl}/api/tokens/google`, {
1209
- json: { credential },
1210
- headers: this.authHeaders(options)
1211
- }).json();
1212
- }
1213
- async generateMCPToken(options) {
1214
- return this.http.post(`${this.baseUrl}/api/tokens/mcp-generate`, {
1215
- headers: this.authHeaders(options)
1216
- }).json();
1217
- }
1218
- async getMediaToken(resourceId, options) {
1219
- return this.http.post(`${this.baseUrl}/api/tokens/media`, {
1220
- json: { resourceId },
1221
- headers: this.authHeaders(options)
1222
- }).json();
1223
- }
1224
- // ============================================================================
1225
- // USERS
1226
- // ============================================================================
1227
- async getMe(options) {
1228
- return this.http.get(`${this.baseUrl}/api/users/me`, {
1229
- headers: this.authHeaders(options)
1230
- }).json();
1231
- }
1232
- async acceptTerms(options) {
1233
- return this.http.post(`${this.baseUrl}/api/users/accept-terms`, {
1234
- headers: this.authHeaders(options)
1235
- }).json();
1236
- }
1237
- async logout(options) {
1238
- return this.http.post(`${this.baseUrl}/api/users/logout`, {
1239
- headers: this.authHeaders(options)
1240
- }).json();
1241
- }
1242
- // ============================================================================
1243
- // RESOURCES
1244
- // ============================================================================
1245
- /**
1246
- * Create a new resource with binary content support
1247
- *
1248
- * @param data - Resource creation data
1249
- * @param data.name - Resource name
1250
- * @param data.file - File object or Buffer with binary content
1251
- * @param data.format - MIME type (e.g., 'text/markdown', 'image/png')
1252
- * @param data.entityTypes - Optional array of entity types
1253
- * @param data.language - Optional ISO 639-1 language code
1254
- * @param data.creationMethod - Optional creation method
1255
- * @param data.sourceAnnotationId - Optional source annotation ID
1256
- * @param data.sourceResourceId - Optional source resource ID
1257
- * @param options - Request options including auth
1258
- */
1259
- async yieldResource(data, options) {
1260
- const formData = new FormData();
1261
- formData.append("name", data.name);
1262
- formData.append("format", data.format);
1263
- formData.append("storageUri", data.storageUri);
1264
- if (data.file instanceof File) {
1265
- formData.append("file", data.file);
1266
- } else if (Buffer.isBuffer(data.file)) {
1267
- const blob = new Blob([new Uint8Array(data.file)], { type: data.format });
1268
- formData.append("file", blob, data.name);
1269
- } else {
1270
- throw new Error("file must be a File or Buffer");
1271
- }
1272
- if (data.entityTypes && data.entityTypes.length > 0) {
1273
- formData.append("entityTypes", JSON.stringify(data.entityTypes));
1274
- }
1275
- if (data.language) {
1276
- formData.append("language", data.language);
1277
- }
1278
- if (data.creationMethod) {
1279
- formData.append("creationMethod", data.creationMethod);
1280
- }
1281
- if (data.sourceAnnotationId) {
1282
- formData.append("sourceAnnotationId", data.sourceAnnotationId);
1283
- }
1284
- if (data.sourceResourceId) {
1285
- formData.append("sourceResourceId", data.sourceResourceId);
1286
- }
1287
- return this.http.post(`${this.baseUrl}/resources`, {
1288
- body: formData,
1289
- headers: this.authHeaders(options)
1290
- }).json();
1291
- }
1292
- async browseResource(id, options) {
1293
- return this.http.get(`${this.baseUrl}/resources/${id}`, {
1294
- headers: this.authHeaders(options)
1295
- }).json();
1296
- }
1297
- /**
1298
- * Get resource representation using W3C content negotiation
1299
- * Returns raw binary content (images, PDFs, text, etc.) with content type
1300
- *
1301
- * @param resourceUri - Full resource URI
1302
- * @param options - Options including Accept header for content negotiation and auth
1303
- * @returns Object with data (ArrayBuffer) and contentType (string)
1304
- *
1305
- * @example
1306
- * ```typescript
1307
- * // Get markdown representation
1308
- * const { data, contentType } = await client.getResourceRepresentation(rUri, { accept: 'text/markdown', auth: token });
1309
- * const markdown = new TextDecoder().decode(data);
1310
- *
1311
- * // Get image representation
1312
- * const { data, contentType } = await client.getResourceRepresentation(rUri, { accept: 'image/png', auth: token });
1313
- * const blob = new Blob([data], { type: contentType });
1314
- *
1315
- * // Get PDF representation
1316
- * const { data, contentType } = await client.getResourceRepresentation(rUri, { accept: 'application/pdf', auth: token });
1317
- * ```
1318
- */
1319
- async getResourceRepresentation(id, options) {
1320
- const response = await this.http.get(`${this.baseUrl}/resources/${id}`, {
1321
- headers: {
1322
- Accept: options?.accept || "text/plain",
1323
- ...this.authHeaders(options)
1324
- }
1325
- });
1326
- const contentType = response.headers.get("content-type") || "application/octet-stream";
1327
- const data = await response.arrayBuffer();
1328
- return { data, contentType };
1329
- }
1330
- /**
1331
- * Get resource representation as a stream using W3C content negotiation
1332
- * Returns streaming binary content (for large files: videos, large PDFs, etc.)
1333
- *
1334
- * Use this for large files to avoid loading entire content into memory.
1335
- * The stream is consumed incrementally and the backend connection stays open
1336
- * until the stream is fully consumed or closed.
1337
- *
1338
- * @param resourceUri - Full resource URI
1339
- * @param options - Options including Accept header for content negotiation and auth
1340
- * @returns Object with stream (ReadableStream) and contentType (string)
1341
- *
1342
- * @example
1343
- * ```typescript
1344
- * // Stream large file
1345
- * const { stream, contentType } = await client.getResourceRepresentationStream(rUri, {
1346
- * accept: 'video/mp4',
1347
- * auth: token
1348
- * });
1349
- *
1350
- * // Consume stream chunk by chunk (never loads entire file into memory)
1351
- * for await (const chunk of stream) {
1352
- * // Process chunk
1353
- * console.log(`Received ${chunk.length} bytes`);
1354
- * }
1355
- *
1356
- * // Or pipe to a file in Node.js
1357
- * const fileStream = fs.createWriteStream('output.mp4');
1358
- * const reader = stream.getReader();
1359
- * while (true) {
1360
- * const { done, value } = await reader.read();
1361
- * if (done) break;
1362
- * fileStream.write(value);
1363
- * }
1364
- * ```
1365
- */
1366
- async getResourceRepresentationStream(id, options) {
1367
- const response = await this.http.get(`${this.baseUrl}/resources/${id}`, {
1368
- headers: {
1369
- Accept: options?.accept || "text/plain",
1370
- ...this.authHeaders(options)
381
+ this.token$.subscribe((token) => {
382
+ if (token && !this._actorStarted && !this.disposed) {
383
+ this._actorStarted = true;
384
+ this.actor.start();
1371
385
  }
1372
386
  });
1373
- const contentType = response.headers.get("content-type") || "application/octet-stream";
1374
- if (!response.body) {
1375
- throw new Error("Response body is null - cannot create stream");
1376
- }
1377
- return { stream: response.body, contentType };
1378
- }
1379
- async browseResources(limit, archived, query, options) {
1380
- const searchParams = new URLSearchParams();
1381
- if (limit) searchParams.append("limit", limit.toString());
1382
- if (archived !== void 0) searchParams.append("archived", archived.toString());
1383
- if (query) searchParams.append("q", query);
1384
- return this.http.get(`${this.baseUrl}/resources`, {
1385
- searchParams,
1386
- headers: this.authHeaders(options)
1387
- }).json();
1388
- }
1389
- async updateResource(id, data, options) {
1390
- await this.http.patch(`${this.baseUrl}/resources/${id}`, {
1391
- json: data,
1392
- headers: this.authHeaders(options)
1393
- }).text();
1394
- }
1395
- async getResourceEvents(id, options) {
1396
- return this.http.get(`${this.baseUrl}/resources/${id}/events`, {
1397
- headers: this.authHeaders(options)
1398
- }).json();
1399
- }
1400
- async browseReferences(id, options) {
1401
- return this.http.get(`${this.baseUrl}/resources/${id}/referenced-by`, {
1402
- headers: this.authHeaders(options)
1403
- }).json();
1404
- }
1405
- async generateCloneToken(id, options) {
1406
- return this.http.post(`${this.baseUrl}/resources/${id}/clone-with-token`, {
1407
- headers: this.authHeaders(options)
1408
- }).json();
1409
- }
1410
- async getResourceByToken(token, options) {
1411
- return this.http.get(`${this.baseUrl}/api/clone-tokens/${token}`, {
1412
- headers: this.authHeaders(options)
1413
- }).json();
1414
- }
1415
- async createResourceFromToken(data, options) {
1416
- return this.http.post(`${this.baseUrl}/api/clone-tokens/create-resource`, {
1417
- json: data,
1418
- headers: this.authHeaders(options)
1419
- }).json();
1420
- }
1421
- // ============================================================================
1422
- // ANNOTATIONS
1423
- // ============================================================================
1424
- async markAnnotation(id, data, options) {
1425
- return this.http.post(`${this.baseUrl}/resources/${id}/annotations`, {
1426
- json: data,
1427
- headers: this.authHeaders(options)
1428
- }).json();
1429
- }
1430
- async getAnnotation(id, options) {
1431
- return this.http.get(`${this.baseUrl}/annotations/${id}`, {
1432
- headers: this.authHeaders(options)
1433
- }).json();
1434
387
  }
1435
- async browseAnnotation(resourceId, annotationId, options) {
1436
- return this.http.get(`${this.baseUrl}/resources/${resourceId}/annotations/${annotationId}`, {
1437
- headers: this.authHeaders(options)
1438
- }).json();
388
+ // ── Lazy actor construction + per-channel fan-in to bridges ───────────
389
+ //
390
+ // `actor` is exposed so the legacy `SemiontClient` can keep `.actor`
391
+ // pointing at the same ActorVM during the transport-abstraction
392
+ // migration. Once SemiontClient is removed, this should be made
393
+ // private again — external callers should use emit/on/stream/state$.
394
+ get actor() {
395
+ if (!this._actor) {
396
+ this._actor = createActorVM({
397
+ baseUrl: this.baseUrl,
398
+ token: () => this.token$.getValue() ?? "",
399
+ channels: [...BRIDGED_CHANNELS]
400
+ });
401
+ for (const channel of BRIDGED_CHANNELS) {
402
+ this._actor.on$(channel).subscribe((payload) => {
403
+ for (const bus of this.bridges) {
404
+ bus.get(channel).next(payload);
405
+ }
406
+ });
407
+ }
408
+ }
409
+ return this._actor;
410
+ }
411
+ // ── ITransport — bus primitives ───────────────────────────────────────
412
+ async emit(channel, payload, resourceScope) {
413
+ busLog("EMIT", channel, payload, resourceScope);
414
+ recordBusEmit(channel, resourceScope);
415
+ await withSpan(
416
+ `bus.emit:${channel}`,
417
+ async () => {
418
+ if (resourceScope !== void 0) {
419
+ await this.actor.emit(
420
+ channel,
421
+ payload,
422
+ resourceScope
423
+ );
424
+ } else {
425
+ await this.actor.emit(
426
+ channel,
427
+ payload
428
+ );
429
+ }
430
+ },
431
+ {
432
+ kind: SpanKind.PRODUCER,
433
+ attrs: {
434
+ "bus.channel": channel,
435
+ ...resourceScope ? { "bus.scope": resourceScope } : {}
436
+ }
437
+ }
438
+ );
1439
439
  }
1440
- async browseAnnotations(id, motivation, options) {
1441
- const searchParams = new URLSearchParams();
1442
- if (motivation) searchParams.append("motivation", motivation);
1443
- return this.http.get(`${this.baseUrl}/resources/${id}/annotations`, {
1444
- searchParams,
1445
- headers: this.authHeaders(options)
1446
- }).json();
440
+ on(channel, handler) {
441
+ const sub = this.actor.on$(channel).subscribe(handler);
442
+ return () => sub.unsubscribe();
1447
443
  }
1448
- async deleteAnnotation(resourceId, annotationId, options) {
1449
- await this.http.delete(`${this.baseUrl}/resources/${resourceId}/annotations/${annotationId}`, {
1450
- headers: this.authHeaders(options)
1451
- });
444
+ stream(channel) {
445
+ return this.actor.on$(channel);
1452
446
  }
1453
- async bindAnnotation(resourceId, annotationId, data, options) {
1454
- return this.http.post(`${this.baseUrl}/resources/${resourceId}/annotations/${annotationId}/bind`, {
1455
- json: { resourceId, ...data },
1456
- headers: this.authHeaders(options)
1457
- }).json();
447
+ /**
448
+ * Wire this transport's SSE fan-in into the given bus. Every channel
449
+ * in `BRIDGED_CHANNELS` (and subsequently per-resource scoped channels
450
+ * opened by `subscribeToResource`) is published on the bus. Safe to
451
+ * call multiple times — each bus is added to the fan-out list.
452
+ */
453
+ bridgeInto(bus) {
454
+ this.bridges.push(bus);
455
+ }
456
+ subscribeToResource(resourceId) {
457
+ if (this.activeResource) {
458
+ if (this.activeResource.resourceId !== resourceId) {
459
+ throw new Error(
460
+ `HttpTransport already subscribed to resource ${this.activeResource.resourceId}; call the unsubscribe returned from the previous subscribeToResource before subscribing to ${resourceId}.`
461
+ );
462
+ }
463
+ this.activeResource.refCount++;
464
+ return this.makeUnsubscriber();
465
+ }
466
+ this.actor.addChannels([...RESOURCE_SCOPED_CHANNELS], resourceId);
467
+ const bridgeSubs = [];
468
+ for (const channel of RESOURCE_SCOPED_CHANNELS) {
469
+ bridgeSubs.push(
470
+ this.actor.on$(channel).subscribe((payload) => {
471
+ for (const bus of this.bridges) {
472
+ bus.get(channel).next(payload);
473
+ }
474
+ })
475
+ );
476
+ }
477
+ this.activeResource = { resourceId, refCount: 1, bridgeSubs };
478
+ return this.makeUnsubscriber();
479
+ }
480
+ makeUnsubscriber() {
481
+ let called = false;
482
+ return () => {
483
+ if (called) return;
484
+ called = true;
485
+ if (!this.activeResource) return;
486
+ this.activeResource.refCount--;
487
+ if (this.activeResource.refCount > 0) return;
488
+ for (const sub of this.activeResource.bridgeSubs) sub.unsubscribe();
489
+ this.actor.removeChannels([...RESOURCE_SCOPED_CHANNELS]);
490
+ this.activeResource = null;
491
+ };
1458
492
  }
1459
- async getAnnotationHistory(resourceId, annotationId, options) {
1460
- return this.http.get(`${this.baseUrl}/resources/${resourceId}/annotations/${annotationId}/history`, {
1461
- headers: this.authHeaders(options)
1462
- }).json();
493
+ get state$() {
494
+ return this.actor.state$;
1463
495
  }
1464
- async annotateReferences(resourceId, data, options) {
1465
- return this.http.post(`${this.baseUrl}/resources/${resourceId}/annotate-references`, {
1466
- json: data,
1467
- headers: this.authHeaders(options)
1468
- }).json();
496
+ dispose() {
497
+ if (this.disposed) return;
498
+ this.disposed = true;
499
+ if (this.activeResource) {
500
+ for (const sub of this.activeResource.bridgeSubs) sub.unsubscribe();
501
+ this.activeResource = null;
502
+ }
503
+ if (this._actor) {
504
+ this._actor.dispose();
505
+ this._actor = null;
506
+ }
1469
507
  }
1470
- async annotateHighlights(resourceId, data, options) {
1471
- return this.http.post(`${this.baseUrl}/resources/${resourceId}/annotate-highlights`, {
1472
- json: data,
1473
- headers: this.authHeaders(options)
1474
- }).json();
508
+ // ── Auth ──────────────────────────────────────────────────────────────
509
+ authHeaders() {
510
+ const token = this.token$.getValue() ?? void 0;
511
+ return token ? { Authorization: `Bearer ${token}` } : {};
1475
512
  }
1476
- async annotateAssessments(resourceId, data, options) {
1477
- return this.http.post(`${this.baseUrl}/resources/${resourceId}/annotate-assessments`, {
1478
- json: data,
1479
- headers: this.authHeaders(options)
513
+ async authenticatePassword(email, password) {
514
+ return this.http.post(`${this.baseUrl}/api/tokens/password`, {
515
+ json: { email, password },
516
+ headers: this.authHeaders()
1480
517
  }).json();
1481
518
  }
1482
- async annotateComments(resourceId, data, options) {
1483
- return this.http.post(`${this.baseUrl}/resources/${resourceId}/annotate-comments`, {
1484
- json: data,
1485
- headers: this.authHeaders(options)
519
+ async authenticateGoogle(credential) {
520
+ return this.http.post(`${this.baseUrl}/api/tokens/google`, {
521
+ json: { credential },
522
+ headers: this.authHeaders()
1486
523
  }).json();
1487
524
  }
1488
- async annotateTags(resourceId, data, options) {
1489
- return this.http.post(`${this.baseUrl}/resources/${resourceId}/annotate-tags`, {
1490
- json: data,
1491
- headers: this.authHeaders(options)
525
+ async refreshAccessToken(token) {
526
+ return this.http.post(`${this.baseUrl}/api/tokens/refresh`, {
527
+ json: { refreshToken: token },
528
+ headers: this.authHeaders()
1492
529
  }).json();
1493
530
  }
1494
- async yieldResourceFromAnnotation(resourceId, annotationId, data, options) {
1495
- return this.http.post(`${this.baseUrl}/resources/${resourceId}/annotations/${annotationId}/yield-resource`, {
1496
- json: data,
1497
- headers: this.authHeaders(options)
531
+ async logout() {
532
+ await this.http.post(`${this.baseUrl}/api/users/logout`, {
533
+ headers: this.authHeaders()
1498
534
  }).json();
1499
535
  }
1500
- async gatherAnnotationContext(resourceId, annotationId, data, options) {
1501
- return this.http.post(`${this.baseUrl}/resources/${resourceId}/annotations/${annotationId}/gather`, {
1502
- json: data,
1503
- headers: this.authHeaders(options)
536
+ async acceptTerms() {
537
+ await this.http.post(`${this.baseUrl}/api/users/accept-terms`, {
538
+ headers: this.authHeaders()
1504
539
  }).json();
1505
540
  }
1506
- async matchSearch(resourceId, data, options) {
1507
- return this.http.post(`${this.baseUrl}/resources/${resourceId}/match-search`, {
1508
- json: { resourceId, ...data },
1509
- headers: this.authHeaders(options)
541
+ async getCurrentUser() {
542
+ return this.http.get(`${this.baseUrl}/api/users/me`, {
543
+ headers: this.authHeaders()
1510
544
  }).json();
1511
545
  }
1512
- // ============================================================================
1513
- // ENTITY TYPES
1514
- // ============================================================================
1515
- async addEntityType(type, options) {
1516
- await this.http.post(`${this.baseUrl}/api/entity-types`, {
1517
- json: { tag: type },
1518
- headers: this.authHeaders(options)
1519
- });
1520
- }
1521
- async addEntityTypesBulk(types, options) {
1522
- await this.http.post(`${this.baseUrl}/api/entity-types/bulk`, {
1523
- json: { tags: types },
1524
- headers: this.authHeaders(options)
1525
- });
1526
- }
1527
- async listEntityTypes(options) {
1528
- return this.http.get(`${this.baseUrl}/api/entity-types`, {
1529
- headers: this.authHeaders(options)
546
+ async generateMcpToken() {
547
+ return this.http.post(`${this.baseUrl}/api/tokens/mcp-generate`, {
548
+ headers: this.authHeaders()
1530
549
  }).json();
1531
550
  }
1532
- // ============================================================================
1533
- // PARTICIPANTS
1534
- // ============================================================================
1535
- async beckonAttention(participantId, data, options) {
1536
- return this.http.post(`${this.baseUrl}/api/participants/${participantId}/attention`, {
1537
- json: data,
1538
- headers: this.authHeaders(options)
551
+ async getMediaToken(resourceId) {
552
+ return this.http.post(`${this.baseUrl}/api/tokens/media`, {
553
+ json: { resourceId },
554
+ headers: this.authHeaders()
1539
555
  }).json();
1540
556
  }
1541
- // ============================================================================
1542
- // ADMIN
1543
- // ============================================================================
1544
- async listUsers(options) {
557
+ // ── Admin ─────────────────────────────────────────────────────────────
558
+ async listUsers() {
1545
559
  return this.http.get(`${this.baseUrl}/api/admin/users`, {
1546
- headers: this.authHeaders(options)
560
+ headers: this.authHeaders()
1547
561
  }).json();
1548
562
  }
1549
- async getUserStats(options) {
563
+ async getUserStats() {
1550
564
  return this.http.get(`${this.baseUrl}/api/admin/users/stats`, {
1551
- headers: this.authHeaders(options)
565
+ headers: this.authHeaders()
1552
566
  }).json();
1553
567
  }
1554
- /**
1555
- * Update a user by ID
1556
- * Note: Users use DID identifiers (did:web:domain:users:id), not HTTP URIs.
1557
- */
1558
- async updateUser(id, data, options) {
568
+ async updateUser(id, data) {
1559
569
  return this.http.patch(`${this.baseUrl}/api/admin/users/${id}`, {
1560
570
  json: data,
1561
- headers: this.authHeaders(options)
571
+ headers: this.authHeaders()
1562
572
  }).json();
1563
573
  }
1564
- async getOAuthConfig(options) {
574
+ async getOAuthConfig() {
1565
575
  return this.http.get(`${this.baseUrl}/api/admin/oauth/config`, {
1566
- headers: this.authHeaders(options)
576
+ headers: this.authHeaders()
1567
577
  }).json();
1568
578
  }
1569
- // ============================================================================
1570
- // ADMIN — EXCHANGE (Backup/Restore)
1571
- // ============================================================================
1572
- /**
1573
- * Create a backup of the knowledge base. Returns raw Response for streaming download.
1574
- * Caller should use response.blob() to trigger a file download.
1575
- */
1576
- async backupKnowledgeBase(options) {
579
+ // ── Exchange (backup/restore/export/import) ───────────────────────────
580
+ async backupKnowledgeBase() {
1577
581
  return this.http.post(`${this.baseUrl}/api/admin/exchange/backup`, {
1578
- headers: this.authHeaders(options)
582
+ headers: this.authHeaders()
1579
583
  });
1580
584
  }
1581
- /**
1582
- * Restore knowledge base from a backup file. Parses SSE progress events and calls onProgress.
1583
- * Returns the final SSE event (phase: 'complete' or 'error').
1584
- */
1585
- async restoreKnowledgeBase(file, options) {
585
+ async restoreKnowledgeBase(file, onProgress) {
1586
586
  const formData = new FormData();
1587
587
  formData.append("file", file);
1588
588
  const response = await this.http.post(`${this.baseUrl}/api/admin/exchange/restore`, {
1589
589
  body: formData,
1590
- headers: this.authHeaders(options)
590
+ headers: this.authHeaders()
1591
591
  });
1592
- return this.parseSSEStream(response, options?.onProgress);
592
+ return this.parseSSEStream(response, onProgress);
1593
593
  }
1594
- // ============================================================================
1595
- // ADMIN — EXCHANGE (Linked Data Export/Import)
1596
- // ============================================================================
1597
- /**
1598
- * Export the knowledge base as a JSON-LD Linked Data archive. Returns raw Response for streaming download.
1599
- * Caller should use response.blob() to trigger a file download.
1600
- */
1601
- async exportKnowledgeBase(params, options) {
594
+ async exportKnowledgeBase(params) {
1602
595
  const searchParams = params?.includeArchived ? new URLSearchParams({ includeArchived: "true" }) : void 0;
1603
596
  return this.http.post(`${this.baseUrl}/api/moderate/exchange/export`, {
1604
- headers: this.authHeaders(options),
597
+ headers: this.authHeaders(),
1605
598
  ...searchParams ? { searchParams } : {}
1606
599
  });
1607
600
  }
1608
- /**
1609
- * Import a JSON-LD Linked Data archive into the knowledge base. Parses SSE progress events and calls onProgress.
1610
- * Returns the final SSE event (phase: 'complete' or 'error').
1611
- */
1612
- async importKnowledgeBase(file, options) {
601
+ async importKnowledgeBase(file, onProgress) {
1613
602
  const formData = new FormData();
1614
603
  formData.append("file", file);
1615
604
  const response = await this.http.post(`${this.baseUrl}/api/moderate/exchange/import`, {
1616
605
  body: formData,
1617
- headers: this.authHeaders(options)
606
+ headers: this.authHeaders()
1618
607
  });
1619
- return this.parseSSEStream(response, options?.onProgress);
608
+ return this.parseSSEStream(response, onProgress);
1620
609
  }
1621
610
  async parseSSEStream(response, onProgress) {
1622
611
  const reader = response.body.getReader();
@@ -1633,737 +622,153 @@ var SemiontApiClient = class {
1633
622
  if (line.startsWith("data: ")) {
1634
623
  const event = JSON.parse(line.slice(6));
1635
624
  onProgress?.(event);
1636
- finalResult = event;
625
+ if (event.phase === "complete" || event.phase === "error" || event.phase === "failed") {
626
+ finalResult = event;
627
+ }
1637
628
  }
1638
629
  }
1639
630
  }
1640
631
  return finalResult;
1641
632
  }
1642
- // ============================================================================
1643
- // JOB STATUS
1644
- // ============================================================================
1645
- async getJobStatus(id, options) {
1646
- return this.http.get(`${this.baseUrl}/api/jobs/${id}`, {
1647
- headers: this.authHeaders(options)
1648
- }).json();
1649
- }
1650
- /**
1651
- * Poll a job until it completes or fails
1652
- * @param id - The job ID to poll
1653
- * @param options - Polling options
1654
- * @returns The final job status
1655
- */
1656
- async pollJobUntilComplete(id, options) {
1657
- const interval = options?.interval ?? 1e3;
1658
- const timeout = options?.timeout ?? 6e4;
1659
- const startTime = Date.now();
1660
- while (true) {
1661
- const status = await this.getJobStatus(id, { auth: options?.auth });
1662
- if (options?.onProgress) {
1663
- options.onProgress(status);
1664
- }
1665
- if (status.status === "complete" || status.status === "failed" || status.status === "cancelled") {
1666
- return status;
1667
- }
1668
- if (Date.now() - startTime > timeout) {
1669
- throw new Error(`Job polling timeout after ${timeout}ms`);
1670
- }
1671
- await new Promise((resolve) => setTimeout(resolve, interval));
1672
- }
1673
- }
1674
- // ============================================================================
1675
- // SYSTEM STATUS
1676
- // ============================================================================
1677
- async healthCheck(options) {
633
+ // ── System status ─────────────────────────────────────────────────────
634
+ async healthCheck() {
1678
635
  return this.http.get(`${this.baseUrl}/api/health`, {
1679
- headers: this.authHeaders(options)
636
+ headers: this.authHeaders()
1680
637
  }).json();
1681
638
  }
1682
- async getStatus(options) {
639
+ async getStatus() {
1683
640
  return this.http.get(`${this.baseUrl}/api/status`, {
1684
- headers: this.authHeaders(options)
641
+ headers: this.authHeaders()
1685
642
  }).json();
1686
643
  }
1687
- async browseFiles(dirPath, sort, options) {
1688
- const searchParams = new URLSearchParams();
1689
- if (dirPath) searchParams.append("path", dirPath);
1690
- if (sort) searchParams.append("sort", sort);
1691
- return this.http.get(`${this.baseUrl}/api/browse/files`, {
1692
- searchParams,
1693
- headers: this.authHeaders(options)
1694
- }).json();
644
+ // ── Internal: ky accessor for legacy passthroughs (temporary) ─────────
645
+ /**
646
+ * Temporary escape hatch for the ongoing transport migration: namespaces
647
+ * that still need to issue ad-hoc HTTP calls (e.g. legacy browse/mark
648
+ * HTTP fallbacks) can borrow the configured `ky` instance here. Will be
649
+ * deleted once all namespaces route through bus channels or through
650
+ * typed methods on this transport.
651
+ */
652
+ get rawHttp() {
653
+ return this.http;
654
+ }
655
+ /**
656
+ * Current access token (synchronously read from the BehaviorSubject).
657
+ * Used by content-transport and legacy namespace HTTP fallbacks that
658
+ * need to pass `auth: token` through some code paths.
659
+ */
660
+ getToken() {
661
+ return this.token$.getValue() ?? void 0;
1695
662
  }
1696
663
  };
1697
- function getBodySource(body) {
1698
- if (Array.isArray(body)) {
1699
- for (const item of body) {
1700
- if (typeof item === "object" && item !== null && "type" in item && "source" in item) {
1701
- const itemType = item.type;
1702
- const itemSource = item.source;
1703
- if (itemType === "SpecificResource" && typeof itemSource === "string") {
1704
- return itemSource;
664
+ var HttpContentTransport = class {
665
+ constructor(transport) {
666
+ this.transport = transport;
667
+ }
668
+ async putBinary(request, options) {
669
+ const sizeBytes = request.file instanceof File ? request.file.size : request.file.length;
670
+ busLog("PUT", "content", {
671
+ name: request.name,
672
+ format: request.format,
673
+ storageUri: request.storageUri,
674
+ sizeBytes
675
+ });
676
+ return withSpan(
677
+ "content.put",
678
+ async () => {
679
+ const formData = new FormData();
680
+ formData.append("name", request.name);
681
+ formData.append("format", request.format);
682
+ formData.append("storageUri", request.storageUri);
683
+ if (request.file instanceof File) {
684
+ formData.append("file", request.file);
685
+ } else if (Buffer.isBuffer(request.file)) {
686
+ const blob = new Blob([new Uint8Array(request.file)], { type: request.format });
687
+ formData.append("file", blob, request.name);
688
+ } else {
689
+ throw new Error("file must be a File or Buffer");
690
+ }
691
+ if (request.entityTypes && request.entityTypes.length > 0) {
692
+ formData.append("entityTypes", JSON.stringify(request.entityTypes));
693
+ }
694
+ if (request.language) formData.append("language", request.language);
695
+ if (request.creationMethod) formData.append("creationMethod", String(request.creationMethod));
696
+ if (request.sourceAnnotationId) formData.append("sourceAnnotationId", String(request.sourceAnnotationId));
697
+ if (request.sourceResourceId) formData.append("sourceResourceId", String(request.sourceResourceId));
698
+ if (request.generationPrompt) formData.append("generationPrompt", request.generationPrompt);
699
+ if (request.generator) formData.append("generator", JSON.stringify(request.generator));
700
+ if (request.isDraft !== void 0) formData.append("isDraft", String(request.isDraft));
701
+ const result = await this.transport.rawHttp.post(`${this.transport.baseUrl}/resources`, {
702
+ body: formData,
703
+ headers: this.requestHeaders(options?.auth)
704
+ }).json();
705
+ return { resourceId: result.resourceId };
706
+ },
707
+ {
708
+ kind: SpanKind.CLIENT,
709
+ attrs: {
710
+ "content.format": request.format,
711
+ "content.size_bytes": sizeBytes
1705
712
  }
1706
713
  }
1707
- }
1708
- return null;
1709
- }
1710
- if (typeof body === "object" && body !== null && "type" in body && "source" in body) {
1711
- const bodyType = body.type;
1712
- const bodySource = body.source;
1713
- if (bodyType === "SpecificResource" && typeof bodySource === "string") {
1714
- return bodySource;
1715
- }
1716
- }
1717
- return null;
1718
- }
1719
- function getBodyType(body) {
1720
- if (Array.isArray(body)) {
1721
- if (body.length === 0) {
1722
- return null;
1723
- }
1724
- if (typeof body[0] === "object" && body[0] !== null && "type" in body[0]) {
1725
- const firstType = body[0].type;
1726
- if (firstType === "TextualBody" || firstType === "SpecificResource") {
1727
- return firstType;
1728
- }
1729
- }
1730
- return null;
1731
- }
1732
- if (typeof body === "object" && body !== null && "type" in body) {
1733
- const bodyType = body.type;
1734
- if (bodyType === "TextualBody" || bodyType === "SpecificResource") {
1735
- return bodyType;
1736
- }
1737
- }
1738
- return null;
1739
- }
1740
- function isBodyResolved(body) {
1741
- return getBodySource(body) !== null;
1742
- }
1743
- function getTargetSource(target) {
1744
- if (typeof target === "string") {
1745
- return target;
1746
- }
1747
- return target.source;
1748
- }
1749
- function getTargetSelector(target) {
1750
- if (typeof target === "string") {
1751
- return void 0;
1752
- }
1753
- return target.selector;
1754
- }
1755
- function hasTargetSelector(target) {
1756
- return typeof target !== "string" && target.selector !== void 0;
1757
- }
1758
- function isHighlight(annotation) {
1759
- return annotation.motivation === "highlighting";
1760
- }
1761
- function isReference(annotation) {
1762
- return annotation.motivation === "linking";
1763
- }
1764
- function isAssessment(annotation) {
1765
- return annotation.motivation === "assessing";
1766
- }
1767
- function isComment(annotation) {
1768
- return annotation.motivation === "commenting";
1769
- }
1770
- function isTag(annotation) {
1771
- return annotation.motivation === "tagging";
1772
- }
1773
- function getCommentText(annotation) {
1774
- if (!isComment(annotation)) return void 0;
1775
- const body = Array.isArray(annotation.body) ? annotation.body[0] : annotation.body;
1776
- if (body && "value" in body) {
1777
- return body.value;
1778
- }
1779
- return void 0;
1780
- }
1781
- function isStubReference(annotation) {
1782
- return isReference(annotation) && !isBodyResolved(annotation.body);
1783
- }
1784
- function isResolvedReference(annotation) {
1785
- return isReference(annotation) && isBodyResolved(annotation.body);
1786
- }
1787
- function getExactText(selector) {
1788
- if (!selector) {
1789
- return "";
1790
- }
1791
- const selectors = Array.isArray(selector) ? selector : [selector];
1792
- const quoteSelector = selectors.find((s) => s.type === "TextQuoteSelector");
1793
- if (quoteSelector) {
1794
- return quoteSelector.exact;
1795
- }
1796
- return "";
1797
- }
1798
- function getAnnotationExactText(annotation) {
1799
- const selector = getTargetSelector(annotation.target);
1800
- return getExactText(selector);
1801
- }
1802
- function getPrimarySelector(selector) {
1803
- if (Array.isArray(selector)) {
1804
- if (selector.length === 0) {
1805
- throw new Error("Empty selector array");
1806
- }
1807
- const first = selector[0];
1808
- if (!first) {
1809
- throw new Error("Invalid selector array");
1810
- }
1811
- return first;
1812
- }
1813
- return selector;
1814
- }
1815
- function getTextQuoteSelector(selector) {
1816
- const selectors = Array.isArray(selector) ? selector : [selector];
1817
- const found = selectors.find((s) => s.type === "TextQuoteSelector");
1818
- if (!found) return null;
1819
- return found.type === "TextQuoteSelector" ? found : null;
1820
- }
1821
- function extractBoundingBox(svg) {
1822
- const viewBoxMatch = svg.match(/<svg[^>]*viewBox="([^"]+)"/);
1823
- if (viewBoxMatch) {
1824
- const values = viewBoxMatch[1].split(/\s+/).map(parseFloat);
1825
- if (values.length === 4 && values.every((v) => !isNaN(v))) {
1826
- return {
1827
- x: values[0],
1828
- y: values[1],
1829
- width: values[2],
1830
- height: values[3]
1831
- };
1832
- }
1833
- }
1834
- const svgTagMatch = svg.match(/<svg[^>]*>/);
1835
- if (svgTagMatch) {
1836
- const svgTag = svgTagMatch[0];
1837
- const widthMatch = svgTag.match(/width="([^"]+)"/);
1838
- const heightMatch = svgTag.match(/height="([^"]+)"/);
1839
- if (widthMatch && heightMatch) {
1840
- const width = parseFloat(widthMatch[1]);
1841
- const height = parseFloat(heightMatch[1]);
1842
- if (!isNaN(width) && !isNaN(height)) {
1843
- return { x: 0, y: 0, width, height };
1844
- }
1845
- }
1846
- }
1847
- return null;
1848
- }
1849
-
1850
- // src/utils/fuzzy-anchor.ts
1851
- function normalizeText(text) {
1852
- return text.replace(/\s+/g, " ").replace(/[\u2018\u2019]/g, "'").replace(/[\u201C\u201D]/g, '"').replace(/\u2014/g, "--").replace(/\u2013/g, "-").trim();
1853
- }
1854
- function levenshteinDistance(str1, str2) {
1855
- const len1 = str1.length;
1856
- const len2 = str2.length;
1857
- const matrix = [];
1858
- for (let i = 0; i <= len1; i++) {
1859
- matrix[i] = [i];
1860
- }
1861
- for (let j = 0; j <= len2; j++) {
1862
- matrix[0][j] = j;
1863
- }
1864
- for (let i = 1; i <= len1; i++) {
1865
- for (let j = 1; j <= len2; j++) {
1866
- const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
1867
- const deletion = matrix[i - 1][j] + 1;
1868
- const insertion = matrix[i][j - 1] + 1;
1869
- const substitution = matrix[i - 1][j - 1] + cost;
1870
- matrix[i][j] = Math.min(deletion, insertion, substitution);
1871
- }
1872
- }
1873
- return matrix[len1][len2];
1874
- }
1875
- function buildContentCache(content) {
1876
- return {
1877
- normalizedContent: normalizeText(content),
1878
- lowerContent: content.toLowerCase()
1879
- };
1880
- }
1881
- function findBestTextMatch(content, searchText, positionHint, cache) {
1882
- const maxFuzzyDistance = Math.max(5, Math.floor(searchText.length * 0.05));
1883
- const exactIndex = content.indexOf(searchText);
1884
- if (exactIndex !== -1) {
1885
- return {
1886
- start: exactIndex,
1887
- end: exactIndex + searchText.length,
1888
- matchQuality: "exact"
1889
- };
1890
- }
1891
- const normalizedSearch = normalizeText(searchText);
1892
- const normalizedIndex = cache.normalizedContent.indexOf(normalizedSearch);
1893
- if (normalizedIndex !== -1) {
1894
- let actualPos = 0;
1895
- let normalizedPos = 0;
1896
- while (normalizedPos < normalizedIndex && actualPos < content.length) {
1897
- const char = content[actualPos];
1898
- const normalizedChar = normalizeText(char);
1899
- if (normalizedChar) {
1900
- normalizedPos += normalizedChar.length;
1901
- }
1902
- actualPos++;
1903
- }
1904
- return {
1905
- start: actualPos,
1906
- end: actualPos + searchText.length,
1907
- matchQuality: "normalized"
1908
- };
1909
- }
1910
- const lowerSearch = searchText.toLowerCase();
1911
- const caseInsensitiveIndex = cache.lowerContent.indexOf(lowerSearch);
1912
- if (caseInsensitiveIndex !== -1) {
1913
- return {
1914
- start: caseInsensitiveIndex,
1915
- end: caseInsensitiveIndex + searchText.length,
1916
- matchQuality: "case-insensitive"
1917
- };
1918
- }
1919
- const windowSize = searchText.length;
1920
- const searchRadius = Math.min(500, content.length);
1921
- const searchStart = positionHint !== void 0 ? Math.max(0, positionHint - searchRadius) : 0;
1922
- const searchEnd = positionHint !== void 0 ? Math.min(content.length, positionHint + searchRadius) : content.length;
1923
- let bestMatch = null;
1924
- for (let i = searchStart; i <= searchEnd - windowSize; i++) {
1925
- const candidate = content.substring(i, i + windowSize);
1926
- const distance = levenshteinDistance(searchText, candidate);
1927
- if (distance <= maxFuzzyDistance) {
1928
- if (!bestMatch || distance < bestMatch.distance) {
1929
- bestMatch = { start: i, distance };
1930
- }
1931
- }
1932
- }
1933
- if (bestMatch) {
1934
- return {
1935
- start: bestMatch.start,
1936
- end: bestMatch.start + windowSize,
1937
- matchQuality: "fuzzy"
1938
- };
1939
- }
1940
- return null;
1941
- }
1942
- function findTextWithContext(content, exact, prefix, suffix, positionHint, cache) {
1943
- if (!exact) return null;
1944
- if (positionHint !== void 0 && positionHint >= 0 && positionHint + exact.length <= content.length) {
1945
- if (content.substring(positionHint, positionHint + exact.length) === exact) {
1946
- return { start: positionHint, end: positionHint + exact.length };
1947
- }
1948
- }
1949
- const occurrences = [];
1950
- let index = content.indexOf(exact);
1951
- while (index !== -1) {
1952
- occurrences.push(index);
1953
- index = content.indexOf(exact, index + 1);
1954
- }
1955
- if (occurrences.length === 0) {
1956
- const fuzzyMatch = findBestTextMatch(content, exact, positionHint, cache);
1957
- if (fuzzyMatch) {
1958
- return { start: fuzzyMatch.start, end: fuzzyMatch.end };
1959
- }
1960
- return null;
1961
- }
1962
- if (occurrences.length === 1) {
1963
- const pos2 = occurrences[0];
1964
- return { start: pos2, end: pos2 + exact.length };
1965
- }
1966
- if (prefix || suffix) {
1967
- for (const pos2 of occurrences) {
1968
- const actualPrefixStart = Math.max(0, pos2 - (prefix?.length || 0));
1969
- const actualPrefix = content.substring(actualPrefixStart, pos2);
1970
- const actualSuffixEnd = Math.min(content.length, pos2 + exact.length + (suffix?.length || 0));
1971
- const actualSuffix = content.substring(pos2 + exact.length, actualSuffixEnd);
1972
- const prefixMatch = !prefix || actualPrefix.endsWith(prefix);
1973
- const suffixMatch = !suffix || actualSuffix.startsWith(suffix);
1974
- if (prefixMatch && suffixMatch) {
1975
- return { start: pos2, end: pos2 + exact.length };
1976
- }
1977
- }
1978
- for (const pos2 of occurrences) {
1979
- const actualPrefix = content.substring(Math.max(0, pos2 - (prefix?.length || 0)), pos2);
1980
- const actualSuffix = content.substring(pos2 + exact.length, pos2 + exact.length + (suffix?.length || 0));
1981
- const fuzzyPrefixMatch = !prefix || actualPrefix.includes(prefix.trim());
1982
- const fuzzySuffixMatch = !suffix || actualSuffix.includes(suffix.trim());
1983
- if (fuzzyPrefixMatch && fuzzySuffixMatch) {
1984
- return { start: pos2, end: pos2 + exact.length };
1985
- }
1986
- }
1987
- }
1988
- const pos = occurrences[0];
1989
- return { start: pos, end: pos + exact.length };
1990
- }
1991
- function verifyPosition(content, position, expectedExact) {
1992
- const actualText = content.substring(position.start, position.end);
1993
- return actualText === expectedExact;
1994
- }
1995
-
1996
- // src/utils/locales.ts
1997
- var LOCALES = [
1998
- { code: "ar", nativeName: "\u0627\u0644\u0639\u0631\u0628\u064A\u0629", englishName: "Arabic" },
1999
- { code: "bn", nativeName: "\u09AC\u09BE\u0982\u09B2\u09BE", englishName: "Bengali" },
2000
- { code: "cs", nativeName: "\u010Ce\u0161tina", englishName: "Czech" },
2001
- { code: "da", nativeName: "Dansk", englishName: "Danish" },
2002
- { code: "de", nativeName: "Deutsch", englishName: "German" },
2003
- { code: "el", nativeName: "\u0395\u03BB\u03BB\u03B7\u03BD\u03B9\u03BA\u03AC", englishName: "Greek" },
2004
- { code: "en", nativeName: "English", englishName: "English" },
2005
- { code: "es", nativeName: "Espa\xF1ol", englishName: "Spanish" },
2006
- { code: "fa", nativeName: "\u0641\u0627\u0631\u0633\u06CC", englishName: "Persian" },
2007
- { code: "fi", nativeName: "Suomi", englishName: "Finnish" },
2008
- { code: "fr", nativeName: "Fran\xE7ais", englishName: "French" },
2009
- { code: "he", nativeName: "\u05E2\u05D1\u05E8\u05D9\u05EA", englishName: "Hebrew" },
2010
- { code: "hi", nativeName: "\u0939\u093F\u0928\u094D\u0926\u0940", englishName: "Hindi" },
2011
- { code: "id", nativeName: "Bahasa Indonesia", englishName: "Indonesian" },
2012
- { code: "it", nativeName: "Italiano", englishName: "Italian" },
2013
- { code: "ja", nativeName: "\u65E5\u672C\u8A9E", englishName: "Japanese" },
2014
- { code: "ko", nativeName: "\uD55C\uAD6D\uC5B4", englishName: "Korean" },
2015
- { code: "ms", nativeName: "Bahasa Melayu", englishName: "Malay" },
2016
- { code: "nl", nativeName: "Nederlands", englishName: "Dutch" },
2017
- { code: "no", nativeName: "Norsk", englishName: "Norwegian" },
2018
- { code: "pl", nativeName: "Polski", englishName: "Polish" },
2019
- { code: "pt", nativeName: "Portugu\xEAs", englishName: "Portuguese" },
2020
- { code: "ro", nativeName: "Rom\xE2n\u0103", englishName: "Romanian" },
2021
- { code: "sv", nativeName: "Svenska", englishName: "Swedish" },
2022
- { code: "th", nativeName: "\u0E44\u0E17\u0E22", englishName: "Thai" },
2023
- { code: "tr", nativeName: "T\xFCrk\xE7e", englishName: "Turkish" },
2024
- { code: "uk", nativeName: "\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430", englishName: "Ukrainian" },
2025
- { code: "vi", nativeName: "Ti\u1EBFng Vi\u1EC7t", englishName: "Vietnamese" },
2026
- { code: "zh", nativeName: "\u4E2D\u6587", englishName: "Chinese" }
2027
- ];
2028
- var localeByCode = new Map(
2029
- LOCALES.map((locale) => [locale.code.toLowerCase(), locale])
2030
- );
2031
- function getLocaleInfo(code) {
2032
- if (!code) return void 0;
2033
- return localeByCode.get(code.toLowerCase());
2034
- }
2035
- function getLocaleNativeName(code) {
2036
- return getLocaleInfo(code)?.nativeName;
2037
- }
2038
- function getLocaleEnglishName(code) {
2039
- return getLocaleInfo(code)?.englishName;
2040
- }
2041
- function formatLocaleDisplay(code) {
2042
- if (!code) return void 0;
2043
- const info = getLocaleInfo(code);
2044
- if (!info) return code;
2045
- return `${info.nativeName} (${code.toLowerCase()})`;
2046
- }
2047
- function getAllLocaleCodes() {
2048
- return LOCALES.map((l) => l.code);
2049
- }
2050
-
2051
- // src/utils/resources.ts
2052
- function getResourceId(resource) {
2053
- if (!resource) return void 0;
2054
- return resource["@id"] || void 0;
2055
- }
2056
- function getPrimaryRepresentation(resource) {
2057
- if (!resource?.representations) return void 0;
2058
- const reps = Array.isArray(resource.representations) ? resource.representations : [resource.representations];
2059
- return reps[0];
2060
- }
2061
- function getPrimaryMediaType(resource) {
2062
- return getPrimaryRepresentation(resource)?.mediaType;
2063
- }
2064
- function getChecksum(resource) {
2065
- return getPrimaryRepresentation(resource)?.checksum;
2066
- }
2067
- function getLanguage(resource) {
2068
- return getPrimaryRepresentation(resource)?.language;
2069
- }
2070
- function getStorageUri(resource) {
2071
- return getPrimaryRepresentation(resource)?.storageUri;
2072
- }
2073
- function getCreator(resource) {
2074
- if (!resource?.wasAttributedTo) return void 0;
2075
- return Array.isArray(resource.wasAttributedTo) ? resource.wasAttributedTo[0] : resource.wasAttributedTo;
2076
- }
2077
- function getDerivedFrom(resource) {
2078
- if (!resource?.wasDerivedFrom) return void 0;
2079
- return Array.isArray(resource.wasDerivedFrom) ? resource.wasDerivedFrom[0] : resource.wasDerivedFrom;
2080
- }
2081
- function isArchived(resource) {
2082
- return resource?.archived === true;
2083
- }
2084
- function getResourceEntityTypes(resource) {
2085
- return resource?.entityTypes || [];
2086
- }
2087
- function isDraft(resource) {
2088
- return resource?.isDraft === true;
2089
- }
2090
- function getNodeEncoding(charset) {
2091
- const normalized = charset.toLowerCase().replace(/[-_]/g, "");
2092
- const charsetMap = {
2093
- "utf8": "utf8",
2094
- "iso88591": "latin1",
2095
- "latin1": "latin1",
2096
- "ascii": "ascii",
2097
- "usascii": "ascii",
2098
- "utf16le": "utf16le",
2099
- "ucs2": "ucs2",
2100
- "binary": "binary",
2101
- "windows1252": "latin1",
2102
- // Windows-1252 is a superset of Latin-1
2103
- "cp1252": "latin1"
2104
- };
2105
- return charsetMap[normalized] || "utf8";
2106
- }
2107
- function decodeRepresentation(buffer, mediaType) {
2108
- const charsetMatch = mediaType.match(/charset=([^\s;]+)/i);
2109
- const charset = (charsetMatch?.[1] || "utf-8").toLowerCase();
2110
- const encoding = getNodeEncoding(charset);
2111
- return buffer.toString(encoding);
2112
- }
2113
-
2114
- // src/utils/svg-utils.ts
2115
- function createRectangleSvg(start, end) {
2116
- const x = Math.min(start.x, end.x);
2117
- const y = Math.min(start.y, end.y);
2118
- const width = Math.abs(end.x - start.x);
2119
- const height = Math.abs(end.y - start.y);
2120
- return `<svg xmlns="http://www.w3.org/2000/svg"><rect x="${x}" y="${y}" width="${width}" height="${height}"/></svg>`;
2121
- }
2122
- function createPolygonSvg(points) {
2123
- if (points.length < 3) {
2124
- throw new Error("Polygon requires at least 3 points");
2125
- }
2126
- const pointsStr = points.map((p) => `${p.x},${p.y}`).join(" ");
2127
- return `<svg xmlns="http://www.w3.org/2000/svg"><polygon points="${pointsStr}"/></svg>`;
2128
- }
2129
- function createCircleSvg(center, radius) {
2130
- if (radius <= 0) {
2131
- throw new Error("Circle radius must be positive");
2132
- }
2133
- return `<svg xmlns="http://www.w3.org/2000/svg"><circle cx="${center.x}" cy="${center.y}" r="${radius}"/></svg>`;
2134
- }
2135
- function parseSvgSelector(svg) {
2136
- const rectMatch = svg.match(/<rect\s+([^>]+)\/>/);
2137
- if (rectMatch && rectMatch[1]) {
2138
- const attrs = rectMatch[1];
2139
- const x = parseFloat(attrs.match(/x="([^"]+)"/)?.[1] || "0");
2140
- const y = parseFloat(attrs.match(/y="([^"]+)"/)?.[1] || "0");
2141
- const width = parseFloat(attrs.match(/width="([^"]+)"/)?.[1] || "0");
2142
- const height = parseFloat(attrs.match(/height="([^"]+)"/)?.[1] || "0");
2143
- return {
2144
- type: "rect",
2145
- data: { x, y, width, height }
2146
- };
2147
- }
2148
- const polygonMatch = svg.match(/<polygon\s+points="([^"]+)"/);
2149
- if (polygonMatch && polygonMatch[1]) {
2150
- const pointsStr = polygonMatch[1];
2151
- const points = pointsStr.split(/\s+/).map((pair) => {
2152
- const [x, y] = pair.split(",").map(parseFloat);
2153
- return { x, y };
2154
- });
2155
- return {
2156
- type: "polygon",
2157
- data: { points }
2158
- };
2159
- }
2160
- const circleMatch = svg.match(/<circle\s+([^>]+)\/>/);
2161
- if (circleMatch && circleMatch[1]) {
2162
- const attrs = circleMatch[1];
2163
- const cx = parseFloat(attrs.match(/cx="([^"]+)"/)?.[1] || "0");
2164
- const cy = parseFloat(attrs.match(/cy="([^"]+)"/)?.[1] || "0");
2165
- const r = parseFloat(attrs.match(/r="([^"]+)"/)?.[1] || "0");
2166
- return {
2167
- type: "circle",
2168
- data: { cx, cy, r }
2169
- };
2170
- }
2171
- return null;
2172
- }
2173
- function normalizeCoordinates(point, displayWidth, displayHeight, imageWidth, imageHeight) {
2174
- return {
2175
- x: point.x / displayWidth * imageWidth,
2176
- y: point.y / displayHeight * imageHeight
2177
- };
2178
- }
2179
- function scaleSvgToNative(svg, displayWidth, displayHeight, imageWidth, imageHeight) {
2180
- const parsed = parseSvgSelector(svg);
2181
- if (!parsed) return svg;
2182
- const scaleX = imageWidth / displayWidth;
2183
- const scaleY = imageHeight / displayHeight;
2184
- switch (parsed.type) {
2185
- case "rect": {
2186
- const { x, y, width, height } = parsed.data;
2187
- return createRectangleSvg(
2188
- { x: x * scaleX, y: y * scaleY },
2189
- { x: (x + width) * scaleX, y: (y + height) * scaleY }
2190
- );
2191
- }
2192
- case "circle": {
2193
- const { cx, cy, r } = parsed.data;
2194
- return createCircleSvg(
2195
- { x: cx * scaleX, y: cy * scaleY },
2196
- r * Math.min(scaleX, scaleY)
2197
- );
2198
- }
2199
- case "polygon": {
2200
- const points = parsed.data.points.map((p) => ({
2201
- x: p.x * scaleX,
2202
- y: p.y * scaleY
2203
- }));
2204
- return createPolygonSvg(points);
2205
- }
714
+ );
2206
715
  }
2207
- return svg;
2208
- }
2209
-
2210
- // src/utils/text-context.ts
2211
- function extractContext(content, start, end) {
2212
- const CONTEXT_LENGTH = 64;
2213
- const MAX_EXTENSION = 32;
2214
- let prefix;
2215
- if (start > 0) {
2216
- let prefixStart = Math.max(0, start - CONTEXT_LENGTH);
2217
- let extensionCount = 0;
2218
- while (prefixStart > 0 && extensionCount < MAX_EXTENSION) {
2219
- const char = content[prefixStart - 1];
2220
- if (!char || /[\s.,;:!?'"()\[\]{}<>\/\\]/.test(char)) {
2221
- break;
2222
- }
2223
- prefixStart--;
2224
- extensionCount++;
2225
- }
2226
- prefix = content.substring(prefixStart, start);
716
+ async getBinary(resourceId, options) {
717
+ busLog("GET", "content", { resourceId, accept: options?.accept });
718
+ return withSpan(
719
+ "content.get",
720
+ async () => {
721
+ const response = await this.transport.rawHttp.get(`${this.transport.baseUrl}/resources/${resourceId}`, {
722
+ headers: {
723
+ Accept: options?.accept ?? "text/plain",
724
+ ...this.requestHeaders(options?.auth)
725
+ }
726
+ });
727
+ const contentType = response.headers.get("content-type") || "application/octet-stream";
728
+ const data = await response.arrayBuffer();
729
+ return { data, contentType };
730
+ },
731
+ { kind: SpanKind.CLIENT, attrs: { "resource.id": resourceId } }
732
+ );
2227
733
  }
2228
- let suffix;
2229
- if (end < content.length) {
2230
- let suffixEnd = Math.min(content.length, end + CONTEXT_LENGTH);
2231
- let extensionCount = 0;
2232
- while (suffixEnd < content.length && extensionCount < MAX_EXTENSION) {
2233
- const char = content[suffixEnd];
2234
- if (!char || /[\s.,;:!?'"()\[\]{}<>\/\\]/.test(char)) {
2235
- break;
734
+ async getBinaryStream(resourceId, options) {
735
+ busLog("GET", "content", { resourceId, accept: options?.accept, stream: true });
736
+ return withSpan(
737
+ "content.get",
738
+ async () => {
739
+ const response = await this.transport.rawHttp.get(`${this.transport.baseUrl}/resources/${resourceId}`, {
740
+ headers: {
741
+ Accept: options?.accept ?? "text/plain",
742
+ ...this.requestHeaders(options?.auth)
743
+ }
744
+ });
745
+ const contentType = response.headers.get("content-type") || "application/octet-stream";
746
+ if (!response.body) {
747
+ throw new Error("Response body is null - cannot create stream");
748
+ }
749
+ return { stream: response.body, contentType };
750
+ },
751
+ {
752
+ kind: SpanKind.CLIENT,
753
+ attrs: { "resource.id": resourceId, "content.stream": true }
2236
754
  }
2237
- suffixEnd++;
2238
- extensionCount++;
2239
- }
2240
- suffix = content.substring(end, suffixEnd);
2241
- }
2242
- return { prefix, suffix };
2243
- }
2244
- function validateAndCorrectOffsets(content, aiStart, aiEnd, exact) {
2245
- const textAtOffset = content.substring(aiStart, aiEnd);
2246
- if (textAtOffset === exact) {
2247
- const context2 = extractContext(content, aiStart, aiEnd);
2248
- return {
2249
- start: aiStart,
2250
- end: aiEnd,
2251
- exact,
2252
- prefix: context2.prefix,
2253
- suffix: context2.suffix,
2254
- corrected: false,
2255
- matchQuality: "exact"
2256
- };
2257
- }
2258
- const cache = buildContentCache(content);
2259
- const match = findBestTextMatch(content, exact, aiStart, cache);
2260
- if (!match) {
2261
- throw new Error(
2262
- "Cannot find acceptable match for text in content. All search strategies failed. Text may be hallucinated."
2263
755
  );
2264
756
  }
2265
- const actualText = content.substring(match.start, match.end);
2266
- const context = extractContext(content, match.start, match.end);
2267
- return {
2268
- start: match.start,
2269
- end: match.end,
2270
- exact: actualText,
2271
- // Use actual text from document, not AI's version
2272
- prefix: context.prefix,
2273
- suffix: context.suffix,
2274
- corrected: true,
2275
- fuzzyMatched: match.matchQuality !== "exact",
2276
- matchQuality: match.matchQuality
2277
- };
2278
- }
2279
-
2280
- // src/utils/text-encoding.ts
2281
- function extractCharset(mediaType) {
2282
- const charsetMatch = mediaType.match(/charset=([^\s;]+)/i);
2283
- return (charsetMatch?.[1] || "utf-8").toLowerCase();
2284
- }
2285
- function decodeWithCharset(buffer, mediaType) {
2286
- const charset = extractCharset(mediaType);
2287
- const decoder = new TextDecoder(charset);
2288
- return decoder.decode(buffer);
2289
- }
2290
-
2291
- // src/utils/validation.ts
2292
- var JWTTokenSchema = {
2293
- parse(token) {
2294
- if (typeof token !== "string") {
2295
- throw new Error("Token must be a string");
2296
- }
2297
- if (!token || token.length === 0) {
2298
- throw new Error("Token is required");
2299
- }
2300
- const jwtRegex = /^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]*$/;
2301
- if (!jwtRegex.test(token)) {
2302
- throw new Error("Invalid JWT token format");
2303
- }
2304
- return token;
2305
- },
2306
- safeParse(token) {
2307
- try {
2308
- const validated = this.parse(token);
2309
- return { success: true, data: validated };
2310
- } catch (error) {
2311
- return {
2312
- success: false,
2313
- error: error instanceof Error ? error.message : "Invalid JWT token"
2314
- };
757
+ dispose() {
758
+ }
759
+ /** Auth header + W3C trace propagation for the active span. */
760
+ requestHeaders(override) {
761
+ const token = override ?? this.transport.getToken();
762
+ const headers = token ? { Authorization: `Bearer ${token}` } : {};
763
+ const trace = getActiveTraceparent();
764
+ if (trace) {
765
+ headers["traceparent"] = trace.traceparent;
766
+ if (trace.tracestate) headers["tracestate"] = trace.tracestate;
2315
767
  }
768
+ return headers;
2316
769
  }
2317
770
  };
2318
- function validateData(schema, data) {
2319
- try {
2320
- const validated = schema.parse(data);
2321
- return { success: true, data: validated };
2322
- } catch (error) {
2323
- return {
2324
- success: false,
2325
- error: error instanceof Error ? error.message : "Validation failed"
2326
- };
2327
- }
2328
- }
2329
- function isValidEmail(email) {
2330
- if (email.length < 1 || email.length > 255) {
2331
- return false;
2332
- }
2333
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2334
- return emailRegex.test(email);
2335
- }
2336
-
2337
- // src/mime-utils.ts
2338
- function getExtensionForMimeType(mimeType) {
2339
- const map3 = {
2340
- "text/plain": "txt",
2341
- "text/markdown": "md",
2342
- "image/png": "png",
2343
- "image/jpeg": "jpg",
2344
- "application/pdf": "pdf"
2345
- };
2346
- return map3[mimeType] || "dat";
2347
- }
2348
- function isImageMimeType(mimeType) {
2349
- return mimeType === "image/png" || mimeType === "image/jpeg";
2350
- }
2351
- function isTextMimeType(mimeType) {
2352
- return mimeType === "text/plain" || mimeType === "text/markdown";
2353
- }
2354
- function isPdfMimeType(mimeType) {
2355
- return mimeType === "application/pdf";
2356
- }
2357
- function getMimeCategory(mimeType) {
2358
- if (isTextMimeType(mimeType)) {
2359
- return "text";
2360
- }
2361
- if (isImageMimeType(mimeType) || isPdfMimeType(mimeType)) {
2362
- return "image";
2363
- }
2364
- return "unsupported";
2365
- }
2366
771
 
2367
- export { APIError, AdminNamespace, AuthNamespace, BeckonNamespace, BindNamespace, BrowseNamespace, GatherNamespace, JWTTokenSchema, JobNamespace, LOCALES, MarkNamespace, MatchNamespace, SSEClient, SSE_STREAM_CONNECTED, SemiontApiClient, YieldNamespace, buildContentCache, createCircleSvg, createPolygonSvg, createRectangleSvg, decodeRepresentation, decodeWithCharset, extractBoundingBox, extractCharset, extractContext, findBestTextMatch, findTextWithContext, formatLocaleDisplay, getAllLocaleCodes, getAnnotationExactText, getBodySource, getBodyType, getChecksum, getCommentText, getCreator, getDerivedFrom, getExactText, getExtensionForMimeType, getLanguage, getLocaleEnglishName, getLocaleInfo, getLocaleNativeName, getMimeCategory, getNodeEncoding, getPrimaryMediaType, getPrimaryRepresentation, getPrimarySelector, getResourceEntityTypes, getResourceId, getStorageUri, getTargetSelector, getTargetSource, getTextQuoteSelector, hasTargetSelector, isArchived, isAssessment, isBodyResolved, isComment, isDraft, isHighlight, isImageMimeType, isPdfMimeType, isReference, isResolvedReference, isStubReference, isTag, isTextMimeType, isValidEmail, normalizeCoordinates, normalizeText, parseSvgSelector, scaleSvgToNative, validateAndCorrectOffsets, validateData, verifyPosition };
772
+ export { APIError, DEGRADED_THRESHOLD_MS, HttpContentTransport, HttpTransport, createActorVM };
2368
773
  //# sourceMappingURL=index.js.map
2369
774
  //# sourceMappingURL=index.js.map