@semiont/api-client 0.4.21 → 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,18 +1,10 @@
1
1
  import ky, { HTTPError } from 'ky';
2
- import { RESOURCE_BROADCAST_TYPES, PERSISTED_EVENT_TYPES, annotationId, resourceId, email, googleCredential, refreshToken, EventBus, accessToken, baseUrl, userDID, searchQuery } from '@semiont/core';
3
- export { getFragmentSelector, getSvgSelector, getTextPositionSelector, validateSvgMarkup } from '@semiont/core';
4
- import { Subject, BehaviorSubject, merge, firstValueFrom, map as map$1, distinctUntilChanged, Observable, Subscription, of, filter as filter$1, take as take$1, timeout as timeout$1 } from 'rxjs';
5
- import { share, filter, map, take, timeout, takeUntil, startWith, debounceTime, distinctUntilChanged as distinctUntilChanged$1, switchMap } from 'rxjs/operators';
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
- function busLog(direction, channel, payload, scope) {
9
- if (typeof globalThis === "undefined") return;
10
- const g = globalThis;
11
- if (!g.__SEMIONT_BUS_LOG__) return;
12
- const cid = payload?.correlationId;
13
- const tag = `[bus ${direction}] ${channel}` + (scope ? ` scope=${scope}` : "") + (cid ? ` cid=${String(cid).slice(0, 8)}` : "");
14
- console.debug(tag, payload);
15
- }
7
+ // src/transport/http-transport.ts
16
8
  var DEGRADED_THRESHOLD_MS = 3e3;
17
9
  var ALLOWED_TRANSITIONS = {
18
10
  initial: ["connecting", "closed"],
@@ -23,12 +15,12 @@ var ALLOWED_TRANSITIONS = {
23
15
  closed: []
24
16
  };
25
17
  function createActorVM(options) {
26
- const { baseUrl: baseUrl3, token: tokenOrGetter, channels: initialChannels, scope: initialScope, reconnectMs = 5e3 } = options;
18
+ const { baseUrl, token: tokenOrGetter, channels: initialChannels, scope: initialScope, reconnectMs = 5e3 } = options;
27
19
  const getToken = typeof tokenOrGetter === "function" ? tokenOrGetter : () => tokenOrGetter;
28
20
  const g = globalThis;
29
21
  g.__SEMIONT_ACTOR_INSTANCES__ = (g.__SEMIONT_ACTOR_INSTANCES__ ?? 0) + 1;
30
22
  const actorSerial = g.__SEMIONT_ACTOR_INSTANCES__;
31
- console.debug(`[diag] ActorVM #${actorSerial} constructed (baseUrl=${baseUrl3})`);
23
+ console.debug(`[diag] ActorVM #${actorSerial} constructed (baseUrl=${baseUrl})`);
32
24
  const globalChannels = new Set(initialChannels);
33
25
  const scopedChannels = /* @__PURE__ */ new Set();
34
26
  let activeScope = initialScope;
@@ -95,7 +87,7 @@ function createActorVM(options) {
95
87
  params.append("scoped", ch);
96
88
  }
97
89
  }
98
- const url = `${baseUrl3}/bus/subscribe?${params.toString()}`;
90
+ const url = `${baseUrl}/bus/subscribe?${params.toString()}`;
99
91
  const controller = new AbortController();
100
92
  inflightControllers.add(controller);
101
93
  try {
@@ -130,7 +122,25 @@ function createActorVM(options) {
130
122
  if (currentId !== void 0) lastEventId = currentId;
131
123
  const parsed = JSON.parse(currentData);
132
124
  busLog("RECV", parsed.channel, parsed.payload, parsed.scope);
133
- events$.next(parsed);
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
+ );
134
144
  }
135
145
  currentEvent = "";
136
146
  currentData = "";
@@ -175,15 +185,20 @@ function createActorVM(options) {
175
185
  );
176
186
  },
177
187
  emit: async (channel, payload, emitScope) => {
178
- busLog("EMIT", channel, payload, emitScope);
179
188
  const body = { channel, payload };
180
189
  if (emitScope) body.scope = emitScope;
181
- await fetch(`${baseUrl3}/bus/emit`, {
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;
198
+ }
199
+ await fetch(`${baseUrl}/bus/emit`, {
182
200
  method: "POST",
183
- headers: {
184
- "Content-Type": "application/json",
185
- Authorization: `Bearer ${getToken()}`
186
- },
201
+ headers,
187
202
  body: JSON.stringify(body)
188
203
  });
189
204
  },
@@ -255,4316 +270,505 @@ function createActorVM(options) {
255
270
  }
256
271
  };
257
272
  }
258
- var BusRequestError = class extends Error {
259
- constructor(message) {
273
+ var RESOURCE_SCOPED_CHANNELS = [
274
+ ...PERSISTED_EVENT_TYPES.filter((t) => t !== "mark:entity-type-added"),
275
+ ...RESOURCE_BROADCAST_TYPES
276
+ ];
277
+ var APIError = class extends Error {
278
+ constructor(message, status, statusText, details) {
260
279
  super(message);
261
- this.name = "BusRequestError";
280
+ this.status = status;
281
+ this.statusText = statusText;
282
+ this.details = details;
283
+ this.name = "APIError";
262
284
  }
263
285
  };
264
- async function busRequest(actor, emitChannel, payload, resultChannel, failureChannel, timeoutMs = 3e4) {
265
- const correlationId = crypto.randomUUID();
266
- const fullPayload = { ...payload, correlationId };
267
- const result$ = merge(
268
- actor.on$(resultChannel).pipe(
269
- filter((e) => e.correlationId === correlationId),
270
- map((e) => ({ ok: true, response: e.response }))
271
- ),
272
- actor.on$(failureChannel).pipe(
273
- filter((e) => e.correlationId === correlationId),
274
- map((e) => ({ ok: false, error: new BusRequestError(e.message) }))
275
- )
276
- ).pipe(take(1), timeout(timeoutMs));
277
- const resultPromise = firstValueFrom(result$);
278
- await actor.emit(emitChannel, fullPayload);
279
- const result = await resultPromise;
280
- if (!result.ok) {
281
- throw result.error;
282
- }
283
- return result.response;
284
- }
285
- function createCache(fetchFn) {
286
- const store$ = new BehaviorSubject(/* @__PURE__ */ new Map());
287
- const inflight = /* @__PURE__ */ new Set();
288
- const obsCache = /* @__PURE__ */ new Map();
289
- const doFetch = async (key) => {
290
- if (inflight.has(key)) return;
291
- inflight.add(key);
292
- try {
293
- const value = await fetchFn(key);
294
- const next = new Map(store$.value);
295
- next.set(key, value);
296
- store$.next(next);
297
- } catch {
298
- } finally {
299
- inflight.delete(key);
300
- }
301
- };
302
- return {
303
- observe(key) {
304
- if (!store$.value.has(key) && !inflight.has(key)) {
305
- void doFetch(key);
306
- }
307
- let obs = obsCache.get(key);
308
- if (!obs) {
309
- obs = store$.pipe(
310
- map$1((m) => m.get(key)),
311
- distinctUntilChanged()
312
- );
313
- obsCache.set(key, obs);
314
- }
315
- return obs;
316
- },
317
- get(key) {
318
- return store$.value.get(key);
319
- },
320
- keys() {
321
- return [...store$.value.keys()];
322
- },
323
- invalidate(key) {
324
- inflight.delete(key);
325
- void doFetch(key);
326
- },
327
- remove(key) {
328
- const next = new Map(store$.value);
329
- next.delete(key);
330
- store$.next(next);
331
- inflight.delete(key);
332
- },
333
- set(key, value) {
334
- const next = new Map(store$.value);
335
- next.set(key, value);
336
- store$.next(next);
337
- },
338
- invalidateAll() {
339
- for (const key of store$.value.keys()) {
340
- inflight.delete(key);
341
- void doFetch(key);
286
+ var HttpTransport = class {
287
+ baseUrl;
288
+ http;
289
+ token$;
290
+ logger;
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 = [];
297
+ constructor(config) {
298
+ const { baseUrl, timeout = 3e4, retry = 2, logger, tokenRefresher } = config;
299
+ this.baseUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
300
+ this.token$ = config.token$ ?? new BehaviorSubject(null);
301
+ this.logger = logger;
302
+ const retryConfig = tokenRefresher ? {
303
+ limit: 1,
304
+ methods: ["get", "post", "put", "patch", "delete", "head", "options"],
305
+ statusCodes: [401, 408, 413, 429, 500, 502, 503, 504]
306
+ } : retry;
307
+ this.http = ky.create({
308
+ timeout,
309
+ retry: retryConfig,
310
+ credentials: "include",
311
+ hooks: {
312
+ beforeRequest: [
313
+ (request) => {
314
+ if (this.logger) {
315
+ this.logger.debug("HTTP Request", {
316
+ type: "http_request",
317
+ url: request.url,
318
+ method: request.method,
319
+ timestamp: Date.now(),
320
+ hasAuth: request.headers.has("Authorization")
321
+ });
322
+ }
323
+ }
324
+ ],
325
+ beforeRetry: tokenRefresher ? [
326
+ async ({ request, error }) => {
327
+ if (!(error instanceof HTTPError) || error.response.status !== 401) {
328
+ return void 0;
329
+ }
330
+ try {
331
+ const newToken = await tokenRefresher();
332
+ if (!newToken) return ky.stop;
333
+ request.headers.set("Authorization", `Bearer ${newToken}`);
334
+ return void 0;
335
+ } catch {
336
+ return ky.stop;
337
+ }
338
+ }
339
+ ] : [],
340
+ afterResponse: [
341
+ (request, _options, response) => {
342
+ if (this.logger) {
343
+ this.logger.debug("HTTP Response", {
344
+ type: "http_response",
345
+ url: request.url,
346
+ method: request.method,
347
+ status: response.status,
348
+ statusText: response.statusText
349
+ });
350
+ }
351
+ return response;
352
+ }
353
+ ],
354
+ beforeError: [
355
+ async (error) => {
356
+ const { response, request } = error;
357
+ if (response) {
358
+ const body = await response.json().catch(() => ({}));
359
+ if (this.logger) {
360
+ this.logger.error("HTTP Request Failed", {
361
+ type: "http_error",
362
+ url: request.url,
363
+ method: request.method,
364
+ status: response.status,
365
+ statusText: response.statusText,
366
+ error: body.message || `HTTP ${response.status}: ${response.statusText}`
367
+ });
368
+ }
369
+ throw new APIError(
370
+ body.message || `HTTP ${response.status}: ${response.statusText}`,
371
+ response.status,
372
+ response.statusText,
373
+ body
374
+ );
375
+ }
376
+ return error;
377
+ }
378
+ ]
342
379
  }
343
- },
344
- dispose() {
345
- store$.complete();
346
- obsCache.clear();
347
- inflight.clear();
348
- }
349
- };
350
- }
351
-
352
- // src/namespaces/browse.ts
353
- var ENTITY_TYPES_KEY = "_";
354
- var BrowseNamespace = class {
355
- constructor(http, eventBus, getToken, actor) {
356
- this.http = http;
357
- this.eventBus = eventBus;
358
- this.getToken = getToken;
359
- this.actor = actor;
360
- const g = globalThis;
361
- g.__SEMIONT_BROWSE_INSTANCES__ = (g.__SEMIONT_BROWSE_INSTANCES__ ?? 0) + 1;
362
- const browseSerial = g.__SEMIONT_BROWSE_INSTANCES__;
363
- this.__serial__ = browseSerial;
364
- console.debug(`[diag] BrowseNamespace #${browseSerial} constructed`);
365
- this.resourceCache = createCache(async (id) => {
366
- const result = await busRequest(
367
- this.actor,
368
- "browse:resource-requested",
369
- { resourceId: id },
370
- "browse:resource-result",
371
- "browse:resource-failed"
372
- );
373
- return result.resource;
374
- });
375
- this.resourceListCache = createCache(async (key) => {
376
- const filters = this.resourceListFilters.get(key) ?? {};
377
- const search = filters.search ? searchQuery(filters.search) : void 0;
378
- const result = await busRequest(
379
- this.actor,
380
- "browse:resources-requested",
381
- { search, archived: filters.archived, limit: filters.limit ?? 100, offset: 0 },
382
- "browse:resources-result",
383
- "browse:resources-failed"
384
- );
385
- return result.resources;
386
- });
387
- this.annotationListCache = createCache(async (resourceId) => {
388
- return busRequest(
389
- this.actor,
390
- "browse:annotations-requested",
391
- { resourceId },
392
- "browse:annotations-result",
393
- "browse:annotations-failed"
394
- );
395
380
  });
396
- this.annotationDetailCache = createCache(async (annotationId) => {
397
- const resourceId = this.annotationResources.get(annotationId);
398
- if (!resourceId) {
399
- throw new Error(`Cannot fetch annotation ${annotationId}: no resourceId known`);
381
+ this.token$.subscribe((token) => {
382
+ if (token && !this._actorStarted && !this.disposed) {
383
+ this._actorStarted = true;
384
+ this.actor.start();
400
385
  }
401
- const result = await busRequest(
402
- this.actor,
403
- "browse:annotation-requested",
404
- { resourceId, annotationId },
405
- "browse:annotation-result",
406
- "browse:annotation-failed"
407
- );
408
- return result.annotation;
409
- });
410
- this.entityTypesCache = createCache(async () => {
411
- const serial = this.__serial__;
412
- console.debug(`[diag] BrowseNamespace#${serial} entityTypes fetchFn START`);
413
- const result = await busRequest(
414
- this.actor,
415
- "browse:entity-types-requested",
416
- {},
417
- "browse:entity-types-result",
418
- "browse:entity-types-failed"
419
- );
420
- console.debug(`[diag] BrowseNamespace#${serial} entityTypes fetchFn RESOLVE`, JSON.stringify(result.entityTypes).slice(0, 200));
421
- return result.entityTypes;
422
- });
423
- this.referencedByCache = createCache(async (resourceId) => {
424
- const result = await busRequest(
425
- this.actor,
426
- "browse:referenced-by-requested",
427
- { resourceId },
428
- "browse:referenced-by-result",
429
- "browse:referenced-by-failed"
430
- );
431
- return result.referencedBy;
432
- });
433
- this.resourceEventsCache = createCache(async (resourceId) => {
434
- const result = await busRequest(
435
- this.actor,
436
- "browse:events-requested",
437
- { resourceId },
438
- "browse:events-result",
439
- "browse:events-failed"
440
- );
441
- return result.events;
442
386
  });
443
- this.subscribeToEvents();
444
387
  }
445
- // ── Caches, backed by the RxJS-native `Cache<K, V>` primitive ───────────
446
- //
447
- // Each cache encapsulates the BehaviorSubject store, in-flight guard,
448
- // and per-key observable memoization that was previously open-coded
449
- // here. Behavioral contract: `packages/api-client/docs/CACHE-SEMANTICS.md`.
388
+ // ── Lazy actor construction + per-channel fan-in to bridges ───────────
450
389
  //
451
- // Public surface (`resource()`, `annotations()`, etc.) is unchanged;
452
- // the caches are an implementation detail of this namespace.
453
- resourceCache;
454
- resourceListCache;
455
- annotationListCache;
456
- /**
457
- * Annotation-detail cache keyed by `annotationId` only — the resourceId
458
- * is a routing hint for the backend fetch, not an identity component.
459
- * We track the most recent resourceId per annotationId in a side-map
460
- * so `mark:delete-ok` (which carries only `annotationId`) can reach
461
- * the right cache entry. Aligns with the pre-refactor semantics.
462
- */
463
- annotationDetailCache;
464
- annotationResources = /* @__PURE__ */ new Map();
465
- entityTypesCache;
466
- referencedByCache;
467
- resourceEventsCache;
468
- /** Filter-blob memory so `invalidateResourceLists` can replay per-key. */
469
- resourceListFilters = /* @__PURE__ */ new Map();
470
- /**
471
- * Per-key memo for `annotations()` observables. The cache stores the
472
- * full `AnnotationsListResponse`; the public shape is just the inner
473
- * `Annotation[]`. Without this memo, every call to `annotations(rId)`
474
- * would produce a fresh `.pipe(map(...))` observable, violating B4
475
- * (per-key observable stability). Consumers that compare observable
476
- * identity — React hooks depending on the observable reference,
477
- * `distinctUntilChanged` at a higher level — would misbehave.
478
- */
479
- annotationListObs = /* @__PURE__ */ new Map();
480
- getToken;
481
- actor;
482
- // ── Live queries ────────────────────────────────────────────────────────
483
- resource(resourceId) {
484
- return this.resourceCache.observe(resourceId);
485
- }
486
- resources(filters) {
487
- const key = JSON.stringify(filters ?? {});
488
- this.resourceListFilters.set(key, filters ?? {});
489
- return this.resourceListCache.observe(key);
490
- }
491
- annotations(resourceId) {
492
- let obs = this.annotationListObs.get(resourceId);
493
- if (!obs) {
494
- obs = this.annotationListCache.observe(resourceId).pipe(map$1((r) => r?.annotations));
495
- this.annotationListObs.set(resourceId, obs);
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
+ }
496
408
  }
497
- return obs;
409
+ return this._actor;
498
410
  }
499
- annotation(resourceId, annotationId) {
500
- this.annotationResources.set(annotationId, resourceId);
501
- return this.annotationDetailCache.observe(annotationId);
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
+ );
502
439
  }
503
- entityTypes() {
504
- const serial = this.__serial__;
505
- console.debug(`[diag] BrowseNamespace#${serial} entityTypes() called`);
506
- const self = this;
507
- if (!self.__entityTypesDiag__) {
508
- self.__entityTypesDiag__ = this.entityTypesCache.observe(ENTITY_TYPES_KEY).pipe(map$1((v) => {
509
- console.debug(`[diag] BrowseNamespace#${serial} entityTypes$ EMIT`, v === void 0 ? "undefined" : JSON.stringify(v).slice(0, 200));
510
- return v;
511
- }));
512
- }
513
- return self.__entityTypesDiag__;
440
+ on(channel, handler) {
441
+ const sub = this.actor.on$(channel).subscribe(handler);
442
+ return () => sub.unsubscribe();
514
443
  }
515
- referencedBy(resourceId) {
516
- return this.referencedByCache.observe(resourceId);
444
+ stream(channel) {
445
+ return this.actor.on$(channel);
517
446
  }
518
- events(resourceId) {
519
- return this.resourceEventsCache.observe(resourceId);
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);
520
455
  }
521
- // ── One-shot reads ──────────────────────────────────────────────────────
522
- async resourceContent(resourceId) {
523
- const result = await this.http.getResourceRepresentation(resourceId, {
524
- accept: "text/plain",
525
- auth: this.getToken()
526
- });
527
- const decoder = new TextDecoder();
528
- return decoder.decode(result.data);
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();
529
479
  }
530
- async resourceRepresentation(resourceId, options) {
531
- return this.http.getResourceRepresentation(resourceId, {
532
- accept: options?.accept,
533
- auth: this.getToken()
534
- });
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
+ };
535
492
  }
536
- async resourceRepresentationStream(resourceId, options) {
537
- return this.http.getResourceRepresentationStream(resourceId, {
538
- accept: options?.accept,
539
- auth: this.getToken()
540
- });
493
+ get state$() {
494
+ return this.actor.state$;
541
495
  }
542
- async resourceEvents(resourceId) {
543
- const result = await busRequest(
544
- this.actor,
545
- "browse:events-requested",
546
- { resourceId },
547
- "browse:events-result",
548
- "browse:events-failed"
549
- );
550
- return result.events;
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
+ }
551
507
  }
552
- async annotationHistory(resourceId, annotationId) {
553
- return busRequest(
554
- this.actor,
555
- "browse:annotation-history-requested",
556
- { resourceId, annotationId },
557
- "browse:annotation-history-result",
558
- "browse:annotation-history-failed"
559
- );
508
+ // ── Auth ──────────────────────────────────────────────────────────────
509
+ authHeaders() {
510
+ const token = this.token$.getValue() ?? void 0;
511
+ return token ? { Authorization: `Bearer ${token}` } : {};
560
512
  }
561
- async connections(_resourceId) {
562
- throw new Error("Not implemented: connections endpoint does not exist yet");
513
+ async authenticatePassword(email, password) {
514
+ return this.http.post(`${this.baseUrl}/api/tokens/password`, {
515
+ json: { email, password },
516
+ headers: this.authHeaders()
517
+ }).json();
563
518
  }
564
- async backlinks(_resourceId) {
565
- throw new Error("Not implemented: backlinks endpoint does not exist yet");
519
+ async authenticateGoogle(credential) {
520
+ return this.http.post(`${this.baseUrl}/api/tokens/google`, {
521
+ json: { credential },
522
+ headers: this.authHeaders()
523
+ }).json();
566
524
  }
567
- async resourcesByName(_query, _limit) {
568
- throw new Error("Not implemented: resourcesByName endpoint does not exist yet");
525
+ async refreshAccessToken(token) {
526
+ return this.http.post(`${this.baseUrl}/api/tokens/refresh`, {
527
+ json: { refreshToken: token },
528
+ headers: this.authHeaders()
529
+ }).json();
569
530
  }
570
- async files(dirPath, sort) {
571
- return busRequest(
572
- this.actor,
573
- "browse:directory-requested",
574
- { path: dirPath ?? ".", sort: sort ?? "name" },
575
- "browse:directory-result",
576
- "browse:directory-failed"
577
- );
531
+ async logout() {
532
+ await this.http.post(`${this.baseUrl}/api/users/logout`, {
533
+ headers: this.authHeaders()
534
+ }).json();
578
535
  }
579
- // ── Cache-mutation API (used by the bus-event subscribers below and by
580
- // other namespaces that know about specific updates) ─────────────────
581
- //
582
- // - `invalidate*` — SWR refetch (B7). Keeps prior value visible.
583
- // - `removeAnnotationDetail` — drops the entry (B13a: entity gone).
584
- // - `updateAnnotationInPlace` — write-through (B13b: new value known).
585
- invalidateAnnotationList(resourceId) {
586
- this.annotationListCache.invalidate(resourceId);
536
+ async acceptTerms() {
537
+ await this.http.post(`${this.baseUrl}/api/users/accept-terms`, {
538
+ headers: this.authHeaders()
539
+ }).json();
587
540
  }
588
- removeAnnotationDetail(annotationId) {
589
- this.annotationDetailCache.remove(annotationId);
590
- this.annotationResources.delete(annotationId);
541
+ async getCurrentUser() {
542
+ return this.http.get(`${this.baseUrl}/api/users/me`, {
543
+ headers: this.authHeaders()
544
+ }).json();
591
545
  }
592
- invalidateResourceDetail(id) {
593
- this.resourceCache.invalidate(id);
546
+ async generateMcpToken() {
547
+ return this.http.post(`${this.baseUrl}/api/tokens/mcp-generate`, {
548
+ headers: this.authHeaders()
549
+ }).json();
594
550
  }
595
- invalidateResourceLists() {
596
- this.resourceListCache.invalidateAll();
551
+ async getMediaToken(resourceId) {
552
+ return this.http.post(`${this.baseUrl}/api/tokens/media`, {
553
+ json: { resourceId },
554
+ headers: this.authHeaders()
555
+ }).json();
597
556
  }
598
- invalidateEntityTypes() {
599
- this.entityTypesCache.invalidate(ENTITY_TYPES_KEY);
557
+ // ── Admin ─────────────────────────────────────────────────────────────
558
+ async listUsers() {
559
+ return this.http.get(`${this.baseUrl}/api/admin/users`, {
560
+ headers: this.authHeaders()
561
+ }).json();
600
562
  }
601
- invalidateReferencedBy(resourceId) {
602
- this.referencedByCache.invalidate(resourceId);
563
+ async getUserStats() {
564
+ return this.http.get(`${this.baseUrl}/api/admin/users/stats`, {
565
+ headers: this.authHeaders()
566
+ }).json();
603
567
  }
604
- invalidateResourceEvents(resourceId) {
605
- this.resourceEventsCache.invalidate(resourceId);
568
+ async updateUser(id, data) {
569
+ return this.http.patch(`${this.baseUrl}/api/admin/users/${id}`, {
570
+ json: data,
571
+ headers: this.authHeaders()
572
+ }).json();
606
573
  }
607
- updateAnnotationInPlace(resourceId, annotation) {
608
- const currentList = this.annotationListCache.get(resourceId);
609
- if (currentList) {
610
- const idx = currentList.annotations.findIndex((a) => a.id === annotation.id);
611
- const nextAnnotations = idx >= 0 ? currentList.annotations.map((a, i) => i === idx ? annotation : a) : [...currentList.annotations, annotation];
612
- this.annotationListCache.set(resourceId, { ...currentList, annotations: nextAnnotations });
613
- }
614
- const aId = annotationId(annotation.id);
615
- this.annotationResources.set(aId, resourceId);
616
- this.annotationDetailCache.set(aId, annotation);
574
+ async getOAuthConfig() {
575
+ return this.http.get(`${this.baseUrl}/api/admin/oauth/config`, {
576
+ headers: this.authHeaders()
577
+ }).json();
617
578
  }
618
- // ── EventBus subscriptions ──────────────────────────────────────────────
619
- /**
620
- * Typed shorthand for `eventBus.get(channel).subscribe(handler)`.
621
- * Preserves per-channel payload typing so handlers read
622
- * `EventMap[K]` without any casts.
623
- */
624
- on(channel, handler) {
625
- this.eventBus.get(channel).subscribe(handler);
626
- }
627
- /**
628
- * Handler shared by `mark:entity-tag-added` and `mark:entity-tag-removed`.
629
- * Both events carry the same effect: the annotation list, the
630
- * resource descriptor, and the event log for that resource all may
631
- * now reflect different entity tagging, so invalidate all three.
632
- */
633
- onEntityTagChanged = (stored) => {
634
- if (!stored.resourceId) return;
635
- this.invalidateAnnotationList(stored.resourceId);
636
- this.invalidateResourceDetail(stored.resourceId);
637
- this.invalidateResourceEvents(stored.resourceId);
638
- };
639
- /**
640
- * Handler shared by `mark:archived` and `mark:unarchived`. Both
641
- * change a resource's archived flag, which is stored on the resource
642
- * descriptor and affects the resource-list filter.
643
- */
644
- onArchiveToggled = (stored) => {
645
- if (!stored.resourceId) return;
646
- this.invalidateResourceDetail(stored.resourceId);
647
- this.invalidateResourceLists();
648
- };
649
- /**
650
- * Handler shared by `yield:create-ok` and `yield:update-ok`. Both
651
- * report a resource mutation with the resourceId as a string (not
652
- * yet branded), so we brand and apply the same effect as
653
- * `onArchiveToggled`.
654
- */
655
- onYieldResourceMutated = (event) => {
656
- const rId = resourceId(event.resourceId);
657
- this.invalidateResourceDetail(rId);
658
- this.invalidateResourceLists();
659
- };
660
- subscribeToEvents() {
661
- this.on("bus:resume-gap", (event) => {
662
- const gapScope = event.scope;
663
- if (gapScope) {
664
- const rId = gapScope;
665
- this.invalidateAnnotationList(rId);
666
- this.invalidateResourceDetail(rId);
667
- this.invalidateResourceEvents(rId);
668
- this.invalidateReferencedBy(rId);
669
- } else {
670
- this.invalidateResourceLists();
671
- for (const rId of this.annotationListCache.keys()) this.invalidateAnnotationList(rId);
672
- for (const rId of this.resourceCache.keys()) this.invalidateResourceDetail(rId);
673
- for (const rId of this.resourceEventsCache.keys()) this.invalidateResourceEvents(rId);
674
- for (const rId of this.referencedByCache.keys()) this.invalidateReferencedBy(rId);
675
- }
676
- this.invalidateEntityTypes();
677
- });
678
- this.on("mark:delete-ok", (event) => {
679
- this.removeAnnotationDetail(annotationId(event.annotationId));
680
- });
681
- this.on("mark:added", (stored) => {
682
- if (stored.resourceId) {
683
- this.invalidateAnnotationList(stored.resourceId);
684
- this.invalidateResourceEvents(stored.resourceId);
685
- }
686
- });
687
- this.on("mark:removed", (stored) => {
688
- if (stored.resourceId) {
689
- this.invalidateAnnotationList(stored.resourceId);
690
- this.invalidateResourceEvents(stored.resourceId);
691
- }
692
- this.removeAnnotationDetail(annotationId(stored.payload.annotationId));
693
- });
694
- this.on("mark:body-updated", (event) => {
695
- const enriched = event;
696
- if (!enriched.resourceId || !enriched.annotation) return;
697
- this.updateAnnotationInPlace(enriched.resourceId, enriched.annotation);
698
- this.invalidateResourceEvents(enriched.resourceId);
699
- });
700
- this.on("mark:entity-tag-added", this.onEntityTagChanged);
701
- this.on("mark:entity-tag-removed", this.onEntityTagChanged);
702
- this.on("replay-window-exceeded", (event) => {
703
- if (event.resourceId) {
704
- this.invalidateAnnotationList(event.resourceId);
705
- }
579
+ // ── Exchange (backup/restore/export/import) ───────────────────────────
580
+ async backupKnowledgeBase() {
581
+ return this.http.post(`${this.baseUrl}/api/admin/exchange/backup`, {
582
+ headers: this.authHeaders()
706
583
  });
707
- this.on("yield:create-ok", this.onYieldResourceMutated);
708
- this.on("yield:update-ok", this.onYieldResourceMutated);
709
- this.on("mark:archived", this.onArchiveToggled);
710
- this.on("mark:unarchived", this.onArchiveToggled);
711
- this.on("mark:entity-type-added", () => this.invalidateEntityTypes());
712
- }
713
- };
714
- var MarkNamespace = class {
715
- constructor(http, eventBus, getToken, actor) {
716
- this.http = http;
717
- this.eventBus = eventBus;
718
- this.getToken = getToken;
719
- this.actor = actor;
720
- }
721
- async annotation(resourceId, input) {
722
- return busRequest(
723
- this.actor,
724
- "mark:create-request",
725
- { resourceId, request: input },
726
- "mark:create-ok",
727
- "mark:create-failed"
728
- );
729
- }
730
- async delete(resourceId, annotationId) {
731
- await this.actor.emit("mark:delete", { annotationId, resourceId });
732
- }
733
- async entityType(type) {
734
- await this.actor.emit("mark:add-entity-type", { tag: type });
735
- }
736
- async entityTypes(types) {
737
- for (const tag of types) {
738
- await this.actor.emit("mark:add-entity-type", { tag });
739
- }
740
- }
741
- async updateResource(resourceId, data) {
742
- return this.http.updateResource(resourceId, data, { auth: this.getToken() });
743
- }
744
- async archive(resourceId) {
745
- await this.actor.emit("mark:archive", { resourceId });
746
- }
747
- async unarchive(resourceId) {
748
- await this.actor.emit("mark:unarchive", { resourceId });
749
584
  }
750
- assist(resourceId, motivation, options) {
751
- return new Observable((subscriber) => {
752
- let done = false;
753
- let pollTimer = null;
754
- let pollInterval = null;
755
- const cleanup = () => {
756
- done = true;
757
- if (pollTimer) {
758
- clearTimeout(pollTimer);
759
- pollTimer = null;
760
- }
761
- if (pollInterval) {
762
- clearInterval(pollInterval);
763
- pollInterval = null;
764
- }
765
- };
766
- const resetPollTimer = (jobId) => {
767
- if (pollTimer) clearTimeout(pollTimer);
768
- if (pollInterval) {
769
- clearInterval(pollInterval);
770
- pollInterval = null;
771
- }
772
- pollTimer = setTimeout(() => {
773
- if (done) return;
774
- pollInterval = setInterval(() => {
775
- if (done) return;
776
- busRequest(
777
- this.actor,
778
- "job:status-requested",
779
- { jobId },
780
- "job:status-result",
781
- "job:status-failed"
782
- ).then((status) => {
783
- if (done) return;
784
- if (status.status === "complete") {
785
- cleanup();
786
- subscriber.next({ motivation, resourceId, progress: status.result });
787
- subscriber.complete();
788
- } else if (status.status === "failed") {
789
- cleanup();
790
- subscriber.error(new Error(status.error ?? "Job failed"));
791
- }
792
- }).catch(() => {
793
- });
794
- }, 5e3);
795
- }, 1e4);
796
- };
797
- let activeJobId = null;
798
- const progress$ = this.eventBus.get("job:report-progress").pipe(
799
- filter((e) => e.jobId === activeJobId)
800
- );
801
- const complete$ = this.eventBus.get("job:complete").pipe(
802
- filter((e) => e.jobId === activeJobId)
803
- );
804
- const fail$ = this.eventBus.get("job:fail").pipe(
805
- filter((e) => e.jobId === activeJobId)
806
- );
807
- const progressSub = progress$.pipe(takeUntil(merge(complete$, fail$))).subscribe((e) => {
808
- subscriber.next(e.progress);
809
- if (activeJobId) resetPollTimer(activeJobId);
810
- });
811
- const completeSub = complete$.subscribe(() => {
812
- cleanup();
813
- subscriber.complete();
814
- });
815
- const failSub = fail$.subscribe((e) => {
816
- cleanup();
817
- subscriber.error(new Error(e.error));
818
- });
819
- const auth = this.getToken();
820
- this.dispatchAssist(resourceId, motivation, options, auth).then(({ jobId }) => {
821
- if (jobId && !done) {
822
- activeJobId = jobId;
823
- resetPollTimer(jobId);
824
- }
825
- }).catch((error) => {
826
- cleanup();
827
- subscriber.error(error);
828
- });
829
- return () => {
830
- cleanup();
831
- progressSub.unsubscribe();
832
- completeSub.unsubscribe();
833
- failSub.unsubscribe();
834
- };
585
+ async restoreKnowledgeBase(file, onProgress) {
586
+ const formData = new FormData();
587
+ formData.append("file", file);
588
+ const response = await this.http.post(`${this.baseUrl}/api/admin/exchange/restore`, {
589
+ body: formData,
590
+ headers: this.authHeaders()
835
591
  });
592
+ return this.parseSSEStream(response, onProgress);
836
593
  }
837
- async dispatchAssist(resourceId, motivation, options, _auth) {
838
- const jobTypeMap = {
839
- tagging: "tag-annotation",
840
- linking: "reference-annotation",
841
- highlighting: "highlight-annotation",
842
- assessing: "assessment-annotation",
843
- commenting: "comment-annotation"
844
- };
845
- const jobType = jobTypeMap[motivation];
846
- if (!jobType) throw new Error(`Unsupported motivation: ${motivation}`);
847
- if (motivation === "tagging") {
848
- if (!options.schemaId || !options.categories?.length) throw new Error("Tag assist requires schemaId and categories");
849
- } else if (motivation === "linking") {
850
- if (!options.entityTypes?.length) throw new Error("Reference assist requires entityTypes");
851
- }
852
- const params = {};
853
- if (options.entityTypes) params.entityTypes = options.entityTypes;
854
- if (options.includeDescriptiveReferences !== void 0) params.includeDescriptiveReferences = options.includeDescriptiveReferences;
855
- if (options.instructions !== void 0) params.instructions = options.instructions;
856
- if (options.density !== void 0) params.density = options.density;
857
- if (options.tone !== void 0) params.tone = options.tone;
858
- if (options.language !== void 0) params.language = options.language;
859
- if (options.schemaId !== void 0) params.schemaId = options.schemaId;
860
- if (options.categories !== void 0) params.categories = options.categories;
861
- return busRequest(
862
- this.actor,
863
- "job:create",
864
- { jobType, resourceId, params },
865
- "job:created",
866
- "job:create-failed"
867
- );
868
- }
869
- };
870
-
871
- // src/namespaces/bind.ts
872
- var BindNamespace = class {
873
- constructor(actor) {
874
- this.actor = actor;
875
- }
876
- async body(resourceId, annotationId, operations) {
877
- await this.actor.emit("bind:update-body", {
878
- correlationId: crypto.randomUUID(),
879
- annotationId,
880
- resourceId,
881
- operations
594
+ async exportKnowledgeBase(params) {
595
+ const searchParams = params?.includeArchived ? new URLSearchParams({ includeArchived: "true" }) : void 0;
596
+ return this.http.post(`${this.baseUrl}/api/moderate/exchange/export`, {
597
+ headers: this.authHeaders(),
598
+ ...searchParams ? { searchParams } : {}
882
599
  });
883
600
  }
884
- };
885
- var GatherNamespace = class {
886
- constructor(eventBus, actor) {
887
- this.eventBus = eventBus;
888
- this.actor = actor;
889
- }
890
- annotation(annotationId, resourceId, options) {
891
- return new Observable((subscriber) => {
892
- const correlationId = crypto.randomUUID();
893
- const complete$ = this.eventBus.get("gather:complete").pipe(
894
- filter((e) => e.correlationId === correlationId)
895
- );
896
- const failed$ = this.eventBus.get("gather:failed").pipe(
897
- filter((e) => e.correlationId === correlationId)
898
- );
899
- const sub = merge(
900
- this.eventBus.get("gather:annotation-progress").pipe(
901
- filter((e) => e.annotationId === annotationId),
902
- map((e) => e)
903
- ),
904
- complete$.pipe(map((e) => e))
905
- ).pipe(takeUntil(merge(complete$, failed$))).subscribe({
906
- next: (v) => subscriber.next(v),
907
- error: (e) => subscriber.error(e)
908
- });
909
- const completeSub = complete$.subscribe((e) => {
910
- subscriber.next(e);
911
- subscriber.complete();
912
- });
913
- const failedSub = failed$.subscribe((e) => {
914
- subscriber.error(new Error(e.message));
915
- });
916
- this.actor.emit("gather:requested", {
917
- correlationId,
918
- annotationId,
919
- resourceId,
920
- contextWindow: options?.contextWindow ?? 2e3
921
- }).catch((error) => {
922
- subscriber.error(error);
923
- });
924
- return () => {
925
- sub.unsubscribe();
926
- completeSub.unsubscribe();
927
- failedSub.unsubscribe();
928
- };
601
+ async importKnowledgeBase(file, onProgress) {
602
+ const formData = new FormData();
603
+ formData.append("file", file);
604
+ const response = await this.http.post(`${this.baseUrl}/api/moderate/exchange/import`, {
605
+ body: formData,
606
+ headers: this.authHeaders()
929
607
  });
608
+ return this.parseSSEStream(response, onProgress);
930
609
  }
931
- resource(_resourceId, _options) {
932
- throw new Error("Not implemented: gather.resource() \u2014 no backend route yet");
610
+ async parseSSEStream(response, onProgress) {
611
+ const reader = response.body.getReader();
612
+ const decoder = new TextDecoder();
613
+ let buffer = "";
614
+ let finalResult = { phase: "unknown" };
615
+ while (true) {
616
+ const { done, value } = await reader.read();
617
+ if (done) break;
618
+ buffer += decoder.decode(value, { stream: true });
619
+ const lines = buffer.split("\n");
620
+ buffer = lines.pop();
621
+ for (const line of lines) {
622
+ if (line.startsWith("data: ")) {
623
+ const event = JSON.parse(line.slice(6));
624
+ onProgress?.(event);
625
+ if (event.phase === "complete" || event.phase === "error" || event.phase === "failed") {
626
+ finalResult = event;
627
+ }
628
+ }
629
+ }
630
+ }
631
+ return finalResult;
933
632
  }
934
- };
935
- var MatchNamespace = class {
936
- constructor(eventBus, actor) {
937
- this.eventBus = eventBus;
938
- this.actor = actor;
633
+ // ── System status ─────────────────────────────────────────────────────
634
+ async healthCheck() {
635
+ return this.http.get(`${this.baseUrl}/api/health`, {
636
+ headers: this.authHeaders()
637
+ }).json();
939
638
  }
940
- search(resourceId, referenceId, context, options) {
941
- return new Observable((subscriber) => {
942
- const correlationId = crypto.randomUUID();
943
- const result$ = this.eventBus.get("match:search-results").pipe(
944
- filter((e) => e.correlationId === correlationId)
945
- );
946
- const failed$ = this.eventBus.get("match:search-failed").pipe(
947
- filter((e) => e.correlationId === correlationId)
948
- );
949
- const resultSub = result$.subscribe((e) => {
950
- subscriber.next(e);
951
- subscriber.complete();
952
- });
953
- const failedSub = failed$.subscribe((e) => {
954
- subscriber.error(new Error(e.error));
955
- });
956
- this.actor.emit("match:search-requested", {
957
- correlationId,
958
- resourceId,
959
- referenceId,
960
- context,
961
- limit: options?.limit ?? 10,
962
- useSemanticScoring: options?.useSemanticScoring ?? true
963
- }).catch((error) => {
964
- subscriber.error(error);
965
- });
966
- return () => {
967
- resultSub.unsubscribe();
968
- failedSub.unsubscribe();
969
- };
970
- });
639
+ async getStatus() {
640
+ return this.http.get(`${this.baseUrl}/api/status`, {
641
+ headers: this.authHeaders()
642
+ }).json();
971
643
  }
972
- };
973
- var YieldNamespace = class {
974
- constructor(http, eventBus, getToken, actor) {
975
- this.http = http;
976
- this.eventBus = eventBus;
977
- this.getToken = getToken;
978
- this.actor = actor;
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;
979
654
  }
980
- async resource(data) {
981
- return this.http.yieldResource(data, { auth: this.getToken() });
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;
982
662
  }
983
- fromAnnotation(resourceId, annotationId, options) {
984
- return new Observable((subscriber) => {
985
- let done = false;
986
- let pollTimer = null;
987
- let pollInterval = null;
988
- const cleanup = () => {
989
- done = true;
990
- if (pollTimer) {
991
- clearTimeout(pollTimer);
992
- pollTimer = null;
993
- }
994
- if (pollInterval) {
995
- clearInterval(pollInterval);
996
- pollInterval = null;
663
+ };
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");
997
690
  }
998
- };
999
- const resetPollTimer = (jid) => {
1000
- if (pollTimer) clearTimeout(pollTimer);
1001
- if (pollInterval) {
1002
- clearInterval(pollInterval);
1003
- pollInterval = null;
691
+ if (request.entityTypes && request.entityTypes.length > 0) {
692
+ formData.append("entityTypes", JSON.stringify(request.entityTypes));
1004
693
  }
1005
- pollTimer = setTimeout(() => {
1006
- if (done) return;
1007
- pollInterval = setInterval(() => {
1008
- if (done) return;
1009
- busRequest(
1010
- this.actor,
1011
- "job:status-requested",
1012
- { jobId: jid },
1013
- "job:status-result",
1014
- "job:status-failed"
1015
- ).then((status) => {
1016
- if (done) return;
1017
- if (status.status === "complete") {
1018
- cleanup();
1019
- subscriber.next({ stage: "complete", percentage: 100, message: "Generation complete" });
1020
- subscriber.complete();
1021
- } else if (status.status === "failed") {
1022
- cleanup();
1023
- subscriber.error(new Error(status.error ?? "Generation failed"));
1024
- }
1025
- }).catch(() => {
1026
- });
1027
- }, 5e3);
1028
- }, 1e4);
1029
- };
1030
- let activeJobId = null;
1031
- const progress$ = this.eventBus.get("job:report-progress").pipe(
1032
- filter((e) => e.jobId === activeJobId)
1033
- );
1034
- const complete$ = this.eventBus.get("job:complete").pipe(
1035
- filter((e) => e.jobId === activeJobId)
1036
- );
1037
- const fail$ = this.eventBus.get("job:fail").pipe(
1038
- filter((e) => e.jobId === activeJobId)
1039
- );
1040
- const progressSub = progress$.pipe(takeUntil(merge(complete$, fail$))).subscribe((e) => {
1041
- subscriber.next(e.progress);
1042
- if (activeJobId) resetPollTimer(activeJobId);
1043
- });
1044
- const completeSub = complete$.subscribe(() => {
1045
- cleanup();
1046
- subscriber.complete();
1047
- });
1048
- const failSub = fail$.subscribe((e) => {
1049
- cleanup();
1050
- subscriber.error(new Error(e.error));
1051
- });
1052
- busRequest(
1053
- this.actor,
1054
- "job:create",
1055
- {
1056
- jobType: "generation",
1057
- resourceId,
1058
- params: {
1059
- referenceId: annotationId,
1060
- title: options.title,
1061
- prompt: options.prompt,
1062
- language: options.language,
1063
- temperature: options.temperature,
1064
- maxTokens: options.maxTokens,
1065
- storageUri: options.storageUri,
1066
- context: options.context
1067
- }
1068
- },
1069
- "job:created",
1070
- "job:create-failed"
1071
- ).then(({ jobId }) => {
1072
- if (jobId && !done) {
1073
- activeJobId = jobId;
1074
- resetPollTimer(jobId);
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
1075
712
  }
1076
- }).catch((error) => {
1077
- cleanup();
1078
- subscriber.error(error);
1079
- });
1080
- return () => {
1081
- cleanup();
1082
- progressSub.unsubscribe();
1083
- completeSub.unsubscribe();
1084
- failSub.unsubscribe();
1085
- };
1086
- });
1087
- }
1088
- async cloneToken(resourceId) {
1089
- return busRequest(
1090
- this.actor,
1091
- "yield:clone-token-requested",
1092
- { resourceId },
1093
- "yield:clone-token-generated",
1094
- "yield:clone-token-failed"
713
+ }
1095
714
  );
1096
715
  }
1097
- async fromToken(token) {
1098
- const result = await busRequest(
1099
- this.actor,
1100
- "yield:clone-resource-requested",
1101
- { token },
1102
- "yield:clone-resource-result",
1103
- "yield:clone-resource-failed"
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 } }
1104
732
  );
1105
- return result.sourceResource;
1106
733
  }
1107
- async createFromToken(options) {
1108
- return busRequest(
1109
- this.actor,
1110
- "yield:clone-create",
1111
- options,
1112
- "yield:clone-created",
1113
- "yield:clone-create-failed"
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 }
754
+ }
1114
755
  );
1115
756
  }
1116
- };
1117
-
1118
- // src/namespaces/beckon.ts
1119
- var BeckonNamespace = class {
1120
- constructor(actor) {
1121
- this.actor = actor;
1122
- }
1123
- attention(annotationId, resourceId) {
1124
- this.actor.emit("beckon:focus", { annotationId, resourceId }).catch(() => {
1125
- });
1126
- }
1127
- };
1128
-
1129
- // src/namespaces/job.ts
1130
- var JobNamespace = class {
1131
- constructor(actor) {
1132
- this.actor = actor;
1133
- }
1134
- async status(jobId) {
1135
- return busRequest(
1136
- this.actor,
1137
- "job:status-requested",
1138
- { jobId },
1139
- "job:status-result",
1140
- "job:status-failed"
1141
- );
757
+ dispose() {
1142
758
  }
1143
- async pollUntilComplete(jobId, options) {
1144
- const interval = options?.interval ?? 1e3;
1145
- const timeout7 = options?.timeout ?? 6e4;
1146
- const startTime = Date.now();
1147
- while (true) {
1148
- const status = await this.status(jobId);
1149
- if (options?.onProgress) options.onProgress(status);
1150
- if (status.status === "complete" || status.status === "failed" || status.status === "cancelled") {
1151
- return status;
1152
- }
1153
- if (Date.now() - startTime > timeout7) {
1154
- throw new Error(`Job polling timeout after ${timeout7}ms`);
1155
- }
1156
- await new Promise((resolve) => setTimeout(resolve, interval));
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;
1157
767
  }
1158
- }
1159
- async cancel(jobId, type) {
1160
- await this.actor.emit("job:cancel-requested", { jobId, type });
1161
- }
1162
- };
1163
- var AuthNamespace = class {
1164
- constructor(http, getToken) {
1165
- this.http = http;
1166
- this.getToken = getToken;
1167
- }
1168
- async password(emailStr, passwordStr) {
1169
- return this.http.authenticatePassword(email(emailStr), passwordStr);
1170
- }
1171
- async google(credential) {
1172
- return this.http.authenticateGoogle(googleCredential(credential));
1173
- }
1174
- async refresh(token) {
1175
- return this.http.refreshToken(refreshToken(token));
1176
- }
1177
- async logout() {
1178
- await this.http.logout({ auth: this.getToken() });
1179
- }
1180
- async me() {
1181
- return this.http.getMe({ auth: this.getToken() });
1182
- }
1183
- async acceptTerms() {
1184
- await this.http.acceptTerms({ auth: this.getToken() });
1185
- }
1186
- async mcpToken() {
1187
- return this.http.generateMCPToken({ auth: this.getToken() });
1188
- }
1189
- async mediaToken(resourceId) {
1190
- return this.http.getMediaToken(resourceId, { auth: this.getToken() });
1191
- }
1192
- };
1193
-
1194
- // src/namespaces/admin.ts
1195
- var AdminNamespace = class {
1196
- constructor(http, getToken) {
1197
- this.http = http;
1198
- this.getToken = getToken;
1199
- }
1200
- async users() {
1201
- const result = await this.http.listUsers({ auth: this.getToken() });
1202
- return result.users;
1203
- }
1204
- async userStats() {
1205
- return this.http.getUserStats({ auth: this.getToken() });
1206
- }
1207
- async updateUser(userId, data) {
1208
- const result = await this.http.updateUser(userId, data, { auth: this.getToken() });
1209
- return result.user;
1210
- }
1211
- async oauthConfig() {
1212
- return this.http.getOAuthConfig({ auth: this.getToken() });
1213
- }
1214
- async healthCheck() {
1215
- return this.http.healthCheck({ auth: this.getToken() });
1216
- }
1217
- async status() {
1218
- return this.http.getStatus({ auth: this.getToken() });
1219
- }
1220
- async backup() {
1221
- return this.http.backupKnowledgeBase({ auth: this.getToken() });
1222
- }
1223
- async restore(file, onProgress) {
1224
- return this.http.restoreKnowledgeBase(file, { auth: this.getToken(), onProgress });
1225
- }
1226
- async exportKnowledgeBase(params) {
1227
- return this.http.exportKnowledgeBase(params, { auth: this.getToken() });
1228
- }
1229
- async importKnowledgeBase(file, onProgress) {
1230
- return this.http.importKnowledgeBase(file, { auth: this.getToken(), onProgress });
1231
- }
1232
- };
1233
- var RESOURCE_SCOPED_CHANNELS = [
1234
- ...PERSISTED_EVENT_TYPES.filter((t) => t !== "mark:entity-type-added"),
1235
- ...RESOURCE_BROADCAST_TYPES
1236
- ];
1237
- var BUS_RESULT_CHANNELS = [
1238
- "browse:resources-result",
1239
- "browse:resources-failed",
1240
- "browse:resource-result",
1241
- "browse:resource-failed",
1242
- "browse:annotations-result",
1243
- "browse:annotations-failed",
1244
- "browse:annotation-result",
1245
- "browse:annotation-failed",
1246
- "browse:annotation-history-result",
1247
- "browse:annotation-history-failed",
1248
- "browse:events-result",
1249
- "browse:events-failed",
1250
- "browse:referenced-by-result",
1251
- "browse:referenced-by-failed",
1252
- "browse:entity-types-result",
1253
- "browse:entity-types-failed",
1254
- "browse:directory-result",
1255
- "browse:directory-failed",
1256
- "browse:annotation-context-result",
1257
- "browse:annotation-context-failed",
1258
- "mark:delete-ok",
1259
- "mark:delete-failed",
1260
- "mark:create-ok",
1261
- "mark:create-failed",
1262
- "match:search-results",
1263
- "match:search-failed",
1264
- "gather:complete",
1265
- "gather:failed",
1266
- "gather:annotation-progress",
1267
- "gather:annotation-finished",
1268
- "gather:summary-result",
1269
- "gather:summary-failed",
1270
- "bind:body-updated",
1271
- "bind:body-update-failed",
1272
- "job:report-progress",
1273
- "job:complete",
1274
- "job:fail",
1275
- "job:status-result",
1276
- "job:status-failed",
1277
- "job:created",
1278
- "job:create-failed",
1279
- "job:claimed",
1280
- "job:claim-failed",
1281
- "yield:clone-token-generated",
1282
- "yield:clone-token-failed",
1283
- "yield:clone-resource-result",
1284
- "yield:clone-resource-failed",
1285
- "yield:clone-created",
1286
- "yield:clone-create-failed",
1287
- "mark:entity-type-added",
1288
- "beckon:focus",
1289
- "beckon:sparkle",
1290
- "bus:resume-gap"
1291
- ];
1292
- var ACTOR_TO_LOCAL_BRIDGES = BUS_RESULT_CHANNELS;
1293
- var APIError = class extends Error {
1294
- constructor(message, status, statusText, details) {
1295
- super(message);
1296
- this.status = status;
1297
- this.statusText = statusText;
1298
- this.details = details;
1299
- this.name = "APIError";
768
+ return headers;
1300
769
  }
1301
770
  };
1302
- var SemiontApiClient = class {
1303
- http;
1304
- baseUrl;
1305
- /**
1306
- * Workspace-scoped EventBus — owned by the client, constructed
1307
- * internally, never accepted from config. Private: all bus access
1308
- * goes through `client.emit` / `client.on` / `client.stream`.
1309
- */
1310
- eventBus;
1311
- logger;
1312
- /**
1313
- * Observable token source. All auth reads from this.
1314
- */
1315
- token$;
1316
- _actor = null;
1317
- // ── Verb-oriented namespace API ──────────────────────────────────────────
1318
- browse;
1319
- mark;
1320
- bind;
1321
- gather;
1322
- match;
1323
- yield;
1324
- beckon;
1325
- job;
1326
- auth;
1327
- admin;
1328
- constructor(config) {
1329
- const { baseUrl: baseUrl3, timeout: timeout7 = 3e4, retry = 2, logger, tokenRefresher } = config;
1330
- this.eventBus = new EventBus();
1331
- this.logger = logger;
1332
- this.baseUrl = baseUrl3.endsWith("/") ? baseUrl3.slice(0, -1) : baseUrl3;
1333
- const retryConfig = tokenRefresher ? {
1334
- limit: 1,
1335
- methods: ["get", "post", "put", "patch", "delete", "head", "options"],
1336
- statusCodes: [401, 408, 413, 429, 500, 502, 503, 504]
1337
- } : retry;
1338
- this.http = ky.create({
1339
- timeout: timeout7,
1340
- retry: retryConfig,
1341
- credentials: "include",
1342
- hooks: {
1343
- beforeRequest: [
1344
- (request) => {
1345
- if (this.logger) {
1346
- this.logger.debug("HTTP Request", {
1347
- type: "http_request",
1348
- url: request.url,
1349
- method: request.method,
1350
- timestamp: Date.now(),
1351
- hasAuth: request.headers.has("Authorization")
1352
- });
1353
- }
1354
- }
1355
- ],
1356
- beforeRetry: tokenRefresher ? [
1357
- async ({ request, error }) => {
1358
- if (!(error instanceof HTTPError) || error.response.status !== 401) {
1359
- return void 0;
1360
- }
1361
- try {
1362
- const newToken = await tokenRefresher();
1363
- if (!newToken) return ky.stop;
1364
- request.headers.set("Authorization", `Bearer ${newToken}`);
1365
- return void 0;
1366
- } catch {
1367
- return ky.stop;
1368
- }
1369
- }
1370
- ] : [],
1371
- afterResponse: [
1372
- (request, _options, response) => {
1373
- if (this.logger) {
1374
- this.logger.debug("HTTP Response", {
1375
- type: "http_response",
1376
- url: request.url,
1377
- method: request.method,
1378
- status: response.status,
1379
- statusText: response.statusText
1380
- });
1381
- }
1382
- return response;
1383
- }
1384
- ],
1385
- beforeError: [
1386
- async (error) => {
1387
- const { response, request } = error;
1388
- if (response) {
1389
- const body = await response.json().catch(() => ({}));
1390
- if (this.logger) {
1391
- this.logger.error("HTTP Request Failed", {
1392
- type: "http_error",
1393
- url: request.url,
1394
- method: request.method,
1395
- status: response.status,
1396
- statusText: response.statusText,
1397
- error: body.message || `HTTP ${response.status}: ${response.statusText}`
1398
- });
1399
- }
1400
- throw new APIError(
1401
- body.message || `HTTP ${response.status}: ${response.statusText}`,
1402
- response.status,
1403
- response.statusText,
1404
- body
1405
- );
1406
- }
1407
- return error;
1408
- }
1409
- ]
1410
- }
1411
- });
1412
- this.token$ = config.token$ ?? new BehaviorSubject(null);
1413
- const getToken = () => this.token$.getValue() ?? void 0;
1414
- this.browse = new BrowseNamespace(this, this.eventBus, getToken, this.actor);
1415
- this.mark = new MarkNamespace(this, this.eventBus, getToken, this.actor);
1416
- this.bind = new BindNamespace(this.actor);
1417
- this.gather = new GatherNamespace(this.eventBus, this.actor);
1418
- this.match = new MatchNamespace(this.eventBus, this.actor);
1419
- this.yield = new YieldNamespace(this, this.eventBus, getToken, this.actor);
1420
- this.beckon = new BeckonNamespace(this.actor);
1421
- this.job = new JobNamespace(this.actor);
1422
- this.auth = new AuthNamespace(this, getToken);
1423
- this.admin = new AdminNamespace(this, getToken);
1424
- this.token$.subscribe((token) => {
1425
- if (token && !this._actorStarted) {
1426
- this._actorStarted = true;
1427
- this.actor.start();
1428
- }
1429
- });
1430
- }
1431
- _actorStarted = false;
1432
- get actor() {
1433
- if (!this._actor) {
1434
- this._actor = createActorVM({
1435
- baseUrl: this.baseUrl,
1436
- token: () => this.token$.getValue() ?? "",
1437
- channels: [...BUS_RESULT_CHANNELS]
1438
- });
1439
- for (const channel of ACTOR_TO_LOCAL_BRIDGES) {
1440
- this._actor.on$(channel).subscribe((payload) => {
1441
- this.eventBus.get(channel).next(payload);
1442
- });
1443
- }
1444
- }
1445
- return this._actor;
1446
- }
1447
- activeResource = null;
1448
- /**
1449
- * Subscribe the bus actor to the resource-scoped SSE stream for a single
1450
- * resource and bridge incoming scoped events into the workspace event bus.
1451
- *
1452
- * **One distinct scope at a time**: the client supports subscriptions
1453
- * to a single resource scope concurrently. Multiple calls with the
1454
- * **same** resourceId are ref-counted — each returns an independent
1455
- * unsubscribe; the underlying SSE scope is torn down only when the
1456
- * last unsubscribe fires. Calling with a **different** resourceId
1457
- * while a subscription is live throws. Widening this to multiple
1458
- * concurrent scopes is deferred until a product requirement (e.g.
1459
- * split-pane viewer, headless fleet-watcher) forces the design.
1460
- *
1461
- * @returns a disposer that decrements the ref count (and tears down
1462
- * the SSE scope + bridges when it reaches zero).
1463
- */
1464
- subscribeToResource(resourceId) {
1465
- if (this.activeResource) {
1466
- if (this.activeResource.resourceId !== resourceId) {
1467
- throw new Error(
1468
- `SemiontApiClient already subscribed to resource ${this.activeResource.resourceId}; call the unsubscribe returned from the previous subscribeToResource before subscribing to ${resourceId}.`
1469
- );
1470
- }
1471
- this.activeResource.refCount++;
1472
- return this.makeUnsubscriber();
1473
- }
1474
- this.actor.addChannels([...RESOURCE_SCOPED_CHANNELS], resourceId);
1475
- const bridgeSubs = [];
1476
- for (const channel of RESOURCE_SCOPED_CHANNELS) {
1477
- bridgeSubs.push(
1478
- this.actor.on$(channel).subscribe((payload) => {
1479
- this.eventBus.get(channel).next(payload);
1480
- })
1481
- );
1482
- }
1483
- this.activeResource = { resourceId, refCount: 1, bridgeSubs };
1484
- return this.makeUnsubscriber();
1485
- }
1486
- makeUnsubscriber() {
1487
- let called = false;
1488
- return () => {
1489
- if (called) return;
1490
- called = true;
1491
- if (!this.activeResource) return;
1492
- this.activeResource.refCount--;
1493
- if (this.activeResource.refCount > 0) return;
1494
- for (const sub of this.activeResource.bridgeSubs) sub.unsubscribe();
1495
- this.actor.removeChannels([...RESOURCE_SCOPED_CHANNELS]);
1496
- this.activeResource = null;
1497
- };
1498
- }
1499
- dispose() {
1500
- if (this.activeResource) {
1501
- for (const sub of this.activeResource.bridgeSubs) sub.unsubscribe();
1502
- this.activeResource = null;
1503
- }
1504
- if (this._actor) {
1505
- this._actor.dispose();
1506
- this._actor = null;
1507
- }
1508
- }
1509
- // ── Event bus surface ─────────────────────────────────────────
1510
- // The ONE public path to the workspace bus. VMs, session, and
1511
- // components route through these methods; `this.eventBus` remains
1512
- // the internal owner and will be privatized once all callers
1513
- // have migrated.
1514
- /** Emit an event on the internal bus. */
1515
- emit(channel, payload) {
1516
- this.eventBus.get(channel).next(payload);
1517
- }
1518
- /** Subscribe to an event on the internal bus; returns unsubscribe. */
1519
- on(channel, handler) {
1520
- const sub = this.eventBus.get(channel).subscribe(handler);
1521
- return () => sub.unsubscribe();
1522
- }
1523
- /** Read-only observable for a bus channel. Consumers `.pipe(...)` over this. */
1524
- stream(channel) {
1525
- return this.eventBus.get(channel).asObservable();
1526
- }
1527
- /**
1528
- * Build the `Authorization: Bearer <token>` header. If the caller passed
1529
- * an explicit `{ auth }` it wins (used by session-internal throwaway
1530
- * clients that need to run a validation request with a specific token).
1531
- * Otherwise the current value of `this.token$` is used, so external
1532
- * callers never have to plumb the token themselves.
1533
- */
1534
- authHeaders(options) {
1535
- const token = options?.auth ?? this.token$.getValue() ?? void 0;
1536
- return token ? { Authorization: `Bearer ${token}` } : {};
1537
- }
1538
- // ============================================================================
1539
- // AUTHENTICATION
1540
- // ============================================================================
1541
- async authenticatePassword(email, password, options) {
1542
- return this.http.post(`${this.baseUrl}/api/tokens/password`, {
1543
- json: { email, password },
1544
- headers: this.authHeaders(options)
1545
- }).json();
1546
- }
1547
- async refreshToken(token, options) {
1548
- return this.http.post(`${this.baseUrl}/api/tokens/refresh`, {
1549
- json: { refreshToken: token },
1550
- headers: this.authHeaders(options)
1551
- }).json();
1552
- }
1553
- async authenticateGoogle(credential, options) {
1554
- return this.http.post(`${this.baseUrl}/api/tokens/google`, {
1555
- json: { credential },
1556
- headers: this.authHeaders(options)
1557
- }).json();
1558
- }
1559
- async generateMCPToken(options) {
1560
- return this.http.post(`${this.baseUrl}/api/tokens/mcp-generate`, {
1561
- headers: this.authHeaders(options)
1562
- }).json();
1563
- }
1564
- async getMediaToken(resourceId, options) {
1565
- return this.http.post(`${this.baseUrl}/api/tokens/media`, {
1566
- json: { resourceId },
1567
- headers: this.authHeaders(options)
1568
- }).json();
1569
- }
1570
- // ============================================================================
1571
- // USERS
1572
- // ============================================================================
1573
- async getMe(options) {
1574
- return this.http.get(`${this.baseUrl}/api/users/me`, {
1575
- headers: this.authHeaders(options)
1576
- }).json();
1577
- }
1578
- async acceptTerms(options) {
1579
- return this.http.post(`${this.baseUrl}/api/users/accept-terms`, {
1580
- headers: this.authHeaders(options)
1581
- }).json();
1582
- }
1583
- async logout(options) {
1584
- return this.http.post(`${this.baseUrl}/api/users/logout`, {
1585
- headers: this.authHeaders(options)
1586
- }).json();
1587
- }
1588
- // ============================================================================
1589
- // RESOURCES
1590
- // ============================================================================
1591
- /**
1592
- * Create a new resource with binary content support
1593
- *
1594
- * @param data - Resource creation data
1595
- * @param data.name - Resource name
1596
- * @param data.file - File object or Buffer with binary content
1597
- * @param data.format - MIME type (e.g., 'text/markdown', 'image/png')
1598
- * @param data.entityTypes - Optional array of entity types
1599
- * @param data.language - Optional ISO 639-1 language code
1600
- * @param data.creationMethod - Optional creation method
1601
- * @param data.sourceAnnotationId - Optional source annotation ID
1602
- * @param data.sourceResourceId - Optional source resource ID
1603
- * @param data.generationPrompt - Optional prompt that drove AI generation
1604
- * @param data.generator - Optional Agent(s) that generated the content
1605
- * @param options - Request options including auth
1606
- */
1607
- async yieldResource(data, options) {
1608
- const formData = new FormData();
1609
- formData.append("name", data.name);
1610
- formData.append("format", data.format);
1611
- formData.append("storageUri", data.storageUri);
1612
- if (data.file instanceof File) {
1613
- formData.append("file", data.file);
1614
- } else if (Buffer.isBuffer(data.file)) {
1615
- const blob = new Blob([new Uint8Array(data.file)], { type: data.format });
1616
- formData.append("file", blob, data.name);
1617
- } else {
1618
- throw new Error("file must be a File or Buffer");
1619
- }
1620
- if (data.entityTypes && data.entityTypes.length > 0) {
1621
- formData.append("entityTypes", JSON.stringify(data.entityTypes));
1622
- }
1623
- if (data.language) {
1624
- formData.append("language", data.language);
1625
- }
1626
- if (data.creationMethod) {
1627
- formData.append("creationMethod", data.creationMethod);
1628
- }
1629
- if (data.sourceAnnotationId) {
1630
- formData.append("sourceAnnotationId", data.sourceAnnotationId);
1631
- }
1632
- if (data.sourceResourceId) {
1633
- formData.append("sourceResourceId", data.sourceResourceId);
1634
- }
1635
- if (data.generationPrompt) {
1636
- formData.append("generationPrompt", data.generationPrompt);
1637
- }
1638
- if (data.generator) {
1639
- formData.append("generator", JSON.stringify(data.generator));
1640
- }
1641
- if (data.isDraft !== void 0) {
1642
- formData.append("isDraft", String(data.isDraft));
1643
- }
1644
- return this.http.post(`${this.baseUrl}/resources`, {
1645
- body: formData,
1646
- headers: this.authHeaders(options)
1647
- }).json();
1648
- }
1649
- async browseResource(id, _options) {
1650
- return busRequest(this.actor, "browse:resource-requested", { resourceId: id }, "browse:resource-result", "browse:resource-failed");
1651
- }
1652
- /**
1653
- * Get resource representation using W3C content negotiation
1654
- * Returns raw binary content (images, PDFs, text, etc.) with content type
1655
- *
1656
- * @param resourceUri - Full resource URI
1657
- * @param options - Options including Accept header for content negotiation and auth
1658
- * @returns Object with data (ArrayBuffer) and contentType (string)
1659
- *
1660
- * @example
1661
- * ```typescript
1662
- * // Get markdown representation
1663
- * const { data, contentType } = await client.getResourceRepresentation(rUri, { accept: 'text/markdown', auth: token });
1664
- * const markdown = new TextDecoder().decode(data);
1665
- *
1666
- * // Get image representation
1667
- * const { data, contentType } = await client.getResourceRepresentation(rUri, { accept: 'image/png', auth: token });
1668
- * const blob = new Blob([data], { type: contentType });
1669
- *
1670
- * // Get PDF representation
1671
- * const { data, contentType } = await client.getResourceRepresentation(rUri, { accept: 'application/pdf', auth: token });
1672
- * ```
1673
- */
1674
- async getResourceRepresentation(id, options) {
1675
- const response = await this.http.get(`${this.baseUrl}/resources/${id}`, {
1676
- headers: {
1677
- Accept: options?.accept || "text/plain",
1678
- ...this.authHeaders(options)
1679
- }
1680
- });
1681
- const contentType = response.headers.get("content-type") || "application/octet-stream";
1682
- const data = await response.arrayBuffer();
1683
- return { data, contentType };
1684
- }
1685
- /**
1686
- * Get resource representation as a stream using W3C content negotiation
1687
- * Returns streaming binary content (for large files: videos, large PDFs, etc.)
1688
- *
1689
- * Use this for large files to avoid loading entire content into memory.
1690
- * The stream is consumed incrementally and the backend connection stays open
1691
- * until the stream is fully consumed or closed.
1692
- *
1693
- * @param resourceUri - Full resource URI
1694
- * @param options - Options including Accept header for content negotiation and auth
1695
- * @returns Object with stream (ReadableStream) and contentType (string)
1696
- *
1697
- * @example
1698
- * ```typescript
1699
- * // Stream large file
1700
- * const { stream, contentType } = await client.getResourceRepresentationStream(rUri, {
1701
- * accept: 'video/mp4',
1702
- * auth: token
1703
- * });
1704
- *
1705
- * // Consume stream chunk by chunk (never loads entire file into memory)
1706
- * for await (const chunk of stream) {
1707
- * // Process chunk
1708
- * console.log(`Received ${chunk.length} bytes`);
1709
- * }
1710
- *
1711
- * // Or pipe to a file in Node.js
1712
- * const fileStream = fs.createWriteStream('output.mp4');
1713
- * const reader = stream.getReader();
1714
- * while (true) {
1715
- * const { done, value } = await reader.read();
1716
- * if (done) break;
1717
- * fileStream.write(value);
1718
- * }
1719
- * ```
1720
- */
1721
- async getResourceRepresentationStream(id, options) {
1722
- const response = await this.http.get(`${this.baseUrl}/resources/${id}`, {
1723
- headers: {
1724
- Accept: options?.accept || "text/plain",
1725
- ...this.authHeaders(options)
1726
- }
1727
- });
1728
- const contentType = response.headers.get("content-type") || "application/octet-stream";
1729
- if (!response.body) {
1730
- throw new Error("Response body is null - cannot create stream");
1731
- }
1732
- return { stream: response.body, contentType };
1733
- }
1734
- async browseResources(limit, archived, query, _options) {
1735
- return busRequest(
1736
- this.actor,
1737
- "browse:resources-requested",
1738
- { search: query, archived, limit: limit ?? 100, offset: 0 },
1739
- "browse:resources-result",
1740
- "browse:resources-failed"
1741
- );
1742
- }
1743
- async updateResource(id, data, options) {
1744
- await this.http.patch(`${this.baseUrl}/resources/${id}`, {
1745
- json: data,
1746
- headers: this.authHeaders(options)
1747
- }).text();
1748
- }
1749
- async getResourceEvents(id, _options) {
1750
- return busRequest(this.actor, "browse:events-requested", { resourceId: id }, "browse:events-result", "browse:events-failed");
1751
- }
1752
- async browseReferences(id, _options) {
1753
- return busRequest(this.actor, "browse:referenced-by-requested", { resourceId: id }, "browse:referenced-by-result", "browse:referenced-by-failed");
1754
- }
1755
- async generateCloneToken(id, _options) {
1756
- return busRequest(this.actor, "yield:clone-token-requested", { resourceId: id }, "yield:clone-token-generated", "yield:clone-token-failed");
1757
- }
1758
- async getResourceByToken(token, _options) {
1759
- return busRequest(this.actor, "yield:clone-resource-requested", { token }, "yield:clone-resource-result", "yield:clone-resource-failed");
1760
- }
1761
- async createResourceFromToken(data, _options) {
1762
- return busRequest(this.actor, "yield:clone-create", data, "yield:clone-created", "yield:clone-create-failed");
1763
- }
1764
- // ============================================================================
1765
- // ANNOTATIONS
1766
- // ============================================================================
1767
- async markAnnotation(id, data, _options) {
1768
- return busRequest(
1769
- this.actor,
1770
- "mark:create-request",
1771
- { resourceId: id, request: data },
1772
- "mark:create-ok",
1773
- "mark:create-failed"
1774
- );
1775
- }
1776
- async getAnnotation(id, _options) {
1777
- return busRequest(this.actor, "browse:annotation-requested", { annotationId: id }, "browse:annotation-result", "browse:annotation-failed");
1778
- }
1779
- async browseAnnotation(resourceId, annotationId, _options) {
1780
- return busRequest(this.actor, "browse:annotation-requested", { resourceId, annotationId }, "browse:annotation-result", "browse:annotation-failed");
1781
- }
1782
- async browseAnnotations(id, _motivation, _options) {
1783
- return busRequest(this.actor, "browse:annotations-requested", { resourceId: id }, "browse:annotations-result", "browse:annotations-failed");
1784
- }
1785
- async deleteAnnotation(resourceId, annotationId, _options) {
1786
- await this.actor.emit("mark:delete", { annotationId, resourceId });
1787
- }
1788
- async bindAnnotation(resourceId, annotationId, data, _options) {
1789
- const correlationId = crypto.randomUUID();
1790
- await this.actor.emit("bind:update-body", { correlationId, annotationId, resourceId, operations: data.operations });
1791
- return { correlationId };
1792
- }
1793
- async getAnnotationHistory(resourceId, annotationId, _options) {
1794
- return busRequest(this.actor, "browse:annotation-history-requested", { resourceId, annotationId }, "browse:annotation-history-result", "browse:annotation-history-failed");
1795
- }
1796
- async annotateReferences(resourceId, data, _options) {
1797
- const { jobId } = await busRequest(
1798
- this.actor,
1799
- "job:create",
1800
- { jobType: "reference-annotation", resourceId, params: data },
1801
- "job:created",
1802
- "job:create-failed"
1803
- );
1804
- return { correlationId: crypto.randomUUID(), jobId };
1805
- }
1806
- async annotateHighlights(resourceId, data, _options) {
1807
- const { jobId } = await busRequest(
1808
- this.actor,
1809
- "job:create",
1810
- { jobType: "highlight-annotation", resourceId, params: data },
1811
- "job:created",
1812
- "job:create-failed"
1813
- );
1814
- return { correlationId: crypto.randomUUID(), jobId };
1815
- }
1816
- async annotateAssessments(resourceId, data, _options) {
1817
- const { jobId } = await busRequest(
1818
- this.actor,
1819
- "job:create",
1820
- { jobType: "assessment-annotation", resourceId, params: data },
1821
- "job:created",
1822
- "job:create-failed"
1823
- );
1824
- return { correlationId: crypto.randomUUID(), jobId };
1825
- }
1826
- async annotateComments(resourceId, data, _options) {
1827
- const { jobId } = await busRequest(
1828
- this.actor,
1829
- "job:create",
1830
- { jobType: "comment-annotation", resourceId, params: data },
1831
- "job:created",
1832
- "job:create-failed"
1833
- );
1834
- return { correlationId: crypto.randomUUID(), jobId };
1835
- }
1836
- async annotateTags(resourceId, data, _options) {
1837
- const { jobId } = await busRequest(
1838
- this.actor,
1839
- "job:create",
1840
- { jobType: "tag-annotation", resourceId, params: data },
1841
- "job:created",
1842
- "job:create-failed"
1843
- );
1844
- return { correlationId: crypto.randomUUID(), jobId };
1845
- }
1846
- async yieldResourceFromAnnotation(resourceId, annotationId, data, _options) {
1847
- const { jobId } = await busRequest(
1848
- this.actor,
1849
- "job:create",
1850
- { jobType: "generation", resourceId, params: { referenceId: annotationId, ...data } },
1851
- "job:created",
1852
- "job:create-failed"
1853
- );
1854
- return { correlationId: crypto.randomUUID(), jobId };
1855
- }
1856
- async gatherAnnotationContext(resourceId, annotationId, data, _options) {
1857
- await this.actor.emit("gather:requested", {
1858
- correlationId: data.correlationId,
1859
- annotationId,
1860
- resourceId,
1861
- contextWindow: data.contextWindow ?? 2e3
1862
- });
1863
- return { correlationId: data.correlationId };
1864
- }
1865
- async matchSearch(resourceId, data, _options) {
1866
- await this.actor.emit("match:search-requested", {
1867
- correlationId: data.correlationId,
1868
- resourceId,
1869
- referenceId: data.referenceId,
1870
- context: data.context,
1871
- limit: data.limit ?? 10,
1872
- useSemanticScoring: data.useSemanticScoring ?? true
1873
- });
1874
- return { correlationId: data.correlationId };
1875
- }
1876
- // ============================================================================
1877
- // ENTITY TYPES
1878
- // ============================================================================
1879
- async addEntityType(type, _options) {
1880
- await this.actor.emit("mark:add-entity-type", { tag: type });
1881
- }
1882
- async addEntityTypesBulk(types, _options) {
1883
- for (const tag of types) {
1884
- await this.actor.emit("mark:add-entity-type", { tag });
1885
- }
1886
- }
1887
- async listEntityTypes(_options) {
1888
- return busRequest(this.actor, "browse:entity-types-requested", {}, "browse:entity-types-result", "browse:entity-types-failed");
1889
- }
1890
- // ============================================================================
1891
- // PARTICIPANTS
1892
- // ============================================================================
1893
- async beckonAttention(_participantId, data, _options) {
1894
- await this.actor.emit("beckon:focus", data);
1895
- return {};
1896
- }
1897
- // ============================================================================
1898
- // ADMIN
1899
- // ============================================================================
1900
- async listUsers(options) {
1901
- return this.http.get(`${this.baseUrl}/api/admin/users`, {
1902
- headers: this.authHeaders(options)
1903
- }).json();
1904
- }
1905
- async getUserStats(options) {
1906
- return this.http.get(`${this.baseUrl}/api/admin/users/stats`, {
1907
- headers: this.authHeaders(options)
1908
- }).json();
1909
- }
1910
- /**
1911
- * Update a user by ID
1912
- * Note: Users use DID identifiers (did:web:domain:users:id), not HTTP URIs.
1913
- */
1914
- async updateUser(id, data, options) {
1915
- return this.http.patch(`${this.baseUrl}/api/admin/users/${id}`, {
1916
- json: data,
1917
- headers: this.authHeaders(options)
1918
- }).json();
1919
- }
1920
- async getOAuthConfig(options) {
1921
- return this.http.get(`${this.baseUrl}/api/admin/oauth/config`, {
1922
- headers: this.authHeaders(options)
1923
- }).json();
1924
- }
1925
- // ============================================================================
1926
- // ADMIN — EXCHANGE (Backup/Restore)
1927
- // ============================================================================
1928
- /**
1929
- * Create a backup of the knowledge base. Returns raw Response for streaming download.
1930
- * Caller should use response.blob() to trigger a file download.
1931
- */
1932
- async backupKnowledgeBase(options) {
1933
- return this.http.post(`${this.baseUrl}/api/admin/exchange/backup`, {
1934
- headers: this.authHeaders(options)
1935
- });
1936
- }
1937
- /**
1938
- * Restore knowledge base from a backup file. Parses SSE progress events and calls onProgress.
1939
- * Returns the final SSE event (phase: 'complete' or 'error').
1940
- */
1941
- async restoreKnowledgeBase(file, options) {
1942
- const formData = new FormData();
1943
- formData.append("file", file);
1944
- const response = await this.http.post(`${this.baseUrl}/api/admin/exchange/restore`, {
1945
- body: formData,
1946
- headers: this.authHeaders(options)
1947
- });
1948
- return this.parseSSEStream(response, options?.onProgress);
1949
- }
1950
- // ============================================================================
1951
- // ADMIN — EXCHANGE (Linked Data Export/Import)
1952
- // ============================================================================
1953
- /**
1954
- * Export the knowledge base as a JSON-LD Linked Data archive. Returns raw Response for streaming download.
1955
- * Caller should use response.blob() to trigger a file download.
1956
- */
1957
- async exportKnowledgeBase(params, options) {
1958
- const searchParams = params?.includeArchived ? new URLSearchParams({ includeArchived: "true" }) : void 0;
1959
- return this.http.post(`${this.baseUrl}/api/moderate/exchange/export`, {
1960
- headers: this.authHeaders(options),
1961
- ...searchParams ? { searchParams } : {}
1962
- });
1963
- }
1964
- /**
1965
- * Import a JSON-LD Linked Data archive into the knowledge base. Parses SSE progress events and calls onProgress.
1966
- * Returns the final SSE event (phase: 'complete' or 'error').
1967
- */
1968
- async importKnowledgeBase(file, options) {
1969
- const formData = new FormData();
1970
- formData.append("file", file);
1971
- const response = await this.http.post(`${this.baseUrl}/api/moderate/exchange/import`, {
1972
- body: formData,
1973
- headers: this.authHeaders(options)
1974
- });
1975
- return this.parseSSEStream(response, options?.onProgress);
1976
- }
1977
- async parseSSEStream(response, onProgress) {
1978
- const reader = response.body.getReader();
1979
- const decoder = new TextDecoder();
1980
- let buffer = "";
1981
- let finalResult = { phase: "unknown" };
1982
- while (true) {
1983
- const { done, value } = await reader.read();
1984
- if (done) break;
1985
- buffer += decoder.decode(value, { stream: true });
1986
- const lines = buffer.split("\n");
1987
- buffer = lines.pop();
1988
- for (const line of lines) {
1989
- if (line.startsWith("data: ")) {
1990
- const event = JSON.parse(line.slice(6));
1991
- onProgress?.(event);
1992
- finalResult = event;
1993
- }
1994
- }
1995
- }
1996
- return finalResult;
1997
- }
1998
- // ============================================================================
1999
- // JOB STATUS
2000
- // ============================================================================
2001
- async getJobStatus(id, _options) {
2002
- return busRequest(this.actor, "job:status-requested", { jobId: id }, "job:status-result", "job:status-failed");
2003
- }
2004
- /**
2005
- * Poll a job until it completes or fails
2006
- * @param id - The job ID to poll
2007
- * @param options - Polling options
2008
- * @returns The final job status
2009
- */
2010
- async pollJobUntilComplete(id, options) {
2011
- const interval = options?.interval ?? 1e3;
2012
- const timeout7 = options?.timeout ?? 6e4;
2013
- const startTime = Date.now();
2014
- while (true) {
2015
- const status = await this.getJobStatus(id, { auth: options?.auth });
2016
- if (options?.onProgress) {
2017
- options.onProgress(status);
2018
- }
2019
- if (status.status === "complete" || status.status === "failed" || status.status === "cancelled") {
2020
- return status;
2021
- }
2022
- if (Date.now() - startTime > timeout7) {
2023
- throw new Error(`Job polling timeout after ${timeout7}ms`);
2024
- }
2025
- await new Promise((resolve) => setTimeout(resolve, interval));
2026
- }
2027
- }
2028
- // ============================================================================
2029
- // SYSTEM STATUS
2030
- // ============================================================================
2031
- async healthCheck(options) {
2032
- return this.http.get(`${this.baseUrl}/api/health`, {
2033
- headers: this.authHeaders(options)
2034
- }).json();
2035
- }
2036
- async getStatus(options) {
2037
- return this.http.get(`${this.baseUrl}/api/status`, {
2038
- headers: this.authHeaders(options)
2039
- }).json();
2040
- }
2041
- async browseFiles(dirPath, sort, _options) {
2042
- return busRequest(
2043
- this.actor,
2044
- "browse:directory-requested",
2045
- { path: dirPath ?? ".", sort: sort ?? "name" },
2046
- "browse:directory-result",
2047
- "browse:directory-failed"
2048
- );
2049
- }
2050
- };
2051
-
2052
- // src/session/storage.ts
2053
- var SESSION_PREFIX = "semiont.session.";
2054
- var STORAGE_KEY = "semiont.knowledgeBases";
2055
- var ACTIVE_KEY = "semiont.activeKnowledgeBaseId";
2056
- var REFRESH_BEFORE_EXP_MS = 5 * 60 * 1e3;
2057
- function sessionKey(kbId) {
2058
- return `${SESSION_PREFIX}${kbId}`;
2059
- }
2060
- function getStoredSession(storage, kbId) {
2061
- const raw = storage.get(sessionKey(kbId));
2062
- if (!raw) return null;
2063
- try {
2064
- const parsed = JSON.parse(raw);
2065
- if (parsed && typeof parsed.access === "string" && typeof parsed.refresh === "string") {
2066
- return { access: parsed.access, refresh: parsed.refresh };
2067
- }
2068
- } catch {
2069
- }
2070
- return null;
2071
- }
2072
- function setStoredSession(storage, kbId, session) {
2073
- storage.set(sessionKey(kbId), JSON.stringify(session));
2074
- }
2075
- function clearStoredSession(storage, kbId) {
2076
- storage.delete(sessionKey(kbId));
2077
- }
2078
- function parseJwtExpiry(token) {
2079
- try {
2080
- const parts = token.split(".");
2081
- if (parts.length !== 3 || !parts[1]) return null;
2082
- const payload = JSON.parse(atob(parts[1]));
2083
- if (!payload.exp) return null;
2084
- return new Date(payload.exp * 1e3);
2085
- } catch {
2086
- return null;
2087
- }
2088
- }
2089
- function isJwtExpired(token) {
2090
- const expiry = parseJwtExpiry(token);
2091
- if (!expiry) return true;
2092
- return expiry.getTime() < Date.now();
2093
- }
2094
- function migrateLegacyEntry(entry) {
2095
- if (entry.host !== void 0) return entry;
2096
- try {
2097
- const url = new URL(entry.backendUrl);
2098
- return {
2099
- id: entry.id,
2100
- label: entry.label,
2101
- host: url.hostname,
2102
- port: parseInt(url.port, 10) || (url.protocol === "https:" ? 443 : 80),
2103
- protocol: url.protocol === "https:" ? "https" : "http",
2104
- email: ""
2105
- };
2106
- } catch {
2107
- return {
2108
- id: entry.id,
2109
- label: entry.label || "Unknown",
2110
- host: "localhost",
2111
- port: 4e3,
2112
- protocol: "http",
2113
- email: ""
2114
- };
2115
- }
2116
- }
2117
- function loadKnowledgeBases(storage) {
2118
- try {
2119
- const raw = storage.get(STORAGE_KEY);
2120
- if (!raw) return [];
2121
- const entries = JSON.parse(raw);
2122
- return entries.map(migrateLegacyEntry);
2123
- } catch {
2124
- return [];
2125
- }
2126
- }
2127
- function saveKnowledgeBases(storage, knowledgeBases) {
2128
- storage.set(STORAGE_KEY, JSON.stringify(knowledgeBases));
2129
- }
2130
- function defaultProtocol(host) {
2131
- return host === "localhost" || host === "127.0.0.1" ? "http" : "https";
2132
- }
2133
- var HOSTNAME_RE = /^(([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?|localhost|\d{1,3}(\.\d{1,3}){3})$/;
2134
- function isValidHostname(host) {
2135
- return HOSTNAME_RE.test(host);
2136
- }
2137
- function kbBackendUrl(kb) {
2138
- if (!isValidHostname(kb.host)) {
2139
- throw new Error(`Invalid KB hostname: "${kb.host}"`);
2140
- }
2141
- const url = new URL("http://x");
2142
- url.protocol = kb.protocol + ":";
2143
- url.hostname = kb.host;
2144
- url.port = String(kb.port);
2145
- return `${kb.protocol}://${url.hostname}:${kb.port}`;
2146
- }
2147
- function generateKbId() {
2148
- return crypto.randomUUID();
2149
- }
2150
-
2151
- // src/session/errors.ts
2152
- var SemiontError = class extends Error {
2153
- code;
2154
- kbId;
2155
- constructor(code, message, kbId = null) {
2156
- super(message);
2157
- this.name = "SemiontError";
2158
- this.code = code;
2159
- this.kbId = kbId;
2160
- }
2161
- };
2162
-
2163
- // src/session/semiont-session.ts
2164
- var SemiontSession = class {
2165
- kb;
2166
- client;
2167
- token$;
2168
- user$;
2169
- streamState$;
2170
- /** Resolves after the initial validation round-trip completes (success or failure). */
2171
- ready;
2172
- storage;
2173
- doRefresh;
2174
- doValidate;
2175
- onAuthFailed;
2176
- onError;
2177
- refreshTimer = null;
2178
- unsubscribeStorage = null;
2179
- disposed = false;
2180
- constructor(config) {
2181
- this.kb = config.kb;
2182
- this.storage = config.storage;
2183
- this.doRefresh = config.refresh;
2184
- this.doValidate = config.validate;
2185
- this.onAuthFailed = config.onAuthFailed ?? (() => {
2186
- });
2187
- this.onError = config.onError ?? (() => {
2188
- });
2189
- const stored = getStoredSession(this.storage, this.kb.id);
2190
- const initialToken = stored && !isJwtExpired(stored.access) ? accessToken(stored.access) : null;
2191
- this.token$ = new BehaviorSubject(initialToken);
2192
- this.user$ = new BehaviorSubject(null);
2193
- this.client = new SemiontApiClient({
2194
- baseUrl: baseUrl(kbBackendUrl(this.kb)),
2195
- token$: this.token$,
2196
- tokenRefresher: () => this.refresh().then((t) => t ?? null)
2197
- });
2198
- this.streamState$ = this.client.actor.state$;
2199
- if (initialToken) {
2200
- this.scheduleProactiveRefresh(initialToken);
2201
- }
2202
- this.unsubscribeStorage = this.storage.subscribe?.((key, newValue) => {
2203
- this.handleStorageChange(key, newValue);
2204
- }) ?? null;
2205
- this.ready = this.validate(stored);
2206
- }
2207
- /**
2208
- * Run the initial mount-time validation. If a stored access token is
2209
- * present and unexpired, call the configured `validate` with it to
2210
- * confirm it still works and populate `user$`. If expired, try
2211
- * refresh first. On 401 from validate, try refresh once. Surfaces
2212
- * auth-failed on terminal failure.
2213
- *
2214
- * When no `validate` callback is provided (service principals), this
2215
- * still runs through the refresh-if-expired step so the stored
2216
- * token is current — it just skips the user-validation round trip.
2217
- */
2218
- async validate(stored) {
2219
- if (!stored) return;
2220
- const startToken = isJwtExpired(stored.access) ? await this.doRefresh() : stored.access;
2221
- if (!startToken) {
2222
- if (isJwtExpired(stored.access)) {
2223
- clearStoredSession(this.storage, this.kb.id);
2224
- }
2225
- return;
2226
- }
2227
- if (startToken !== stored.access) {
2228
- this.token$.next(accessToken(startToken));
2229
- this.scheduleProactiveRefresh(startToken);
2230
- }
2231
- if (!this.doValidate) return;
2232
- const attempt = async (token) => {
2233
- if (this.disposed) return;
2234
- try {
2235
- const data = await this.doValidate(accessToken(token));
2236
- if (this.disposed) return;
2237
- this.user$.next(data);
2238
- } catch (err) {
2239
- if (this.disposed) return;
2240
- if (err instanceof APIError && err.status === 401) {
2241
- const refreshed = await this.doRefresh();
2242
- if (this.disposed) return;
2243
- if (refreshed) {
2244
- this.token$.next(accessToken(refreshed));
2245
- this.scheduleProactiveRefresh(refreshed);
2246
- await attempt(refreshed);
2247
- return;
2248
- }
2249
- clearStoredSession(this.storage, this.kb.id);
2250
- this.token$.next(null);
2251
- this.onAuthFailed("Your session has expired. Please sign in again.");
2252
- } else {
2253
- this.onError(
2254
- new SemiontError(
2255
- "session.auth-failed",
2256
- err instanceof Error ? err.message : String(err),
2257
- this.kb.id
2258
- )
2259
- );
2260
- }
2261
- }
2262
- };
2263
- await attempt(startToken);
2264
- }
2265
- /**
2266
- * Refresh the access token via the configured `refresh` callback.
2267
- * On success, pushes the new token into `token$` and schedules the
2268
- * next proactive refresh. On failure, clears persisted state and
2269
- * fires `onAuthFailed` — the frontend's wiring of that callback is
2270
- * what surfaces the session-expired modal.
2271
- */
2272
- async refresh() {
2273
- if (this.disposed) return null;
2274
- const newAccess = await this.doRefresh();
2275
- if (this.disposed) return null;
2276
- if (newAccess) {
2277
- const tok = accessToken(newAccess);
2278
- this.token$.next(tok);
2279
- this.scheduleProactiveRefresh(newAccess);
2280
- return tok;
2281
- }
2282
- this.token$.next(null);
2283
- clearStoredSession(this.storage, this.kb.id);
2284
- this.onAuthFailed("Your session has expired. Please sign in again.");
2285
- this.onError(
2286
- new SemiontError("session.refresh-exhausted", "Token refresh failed", this.kb.id)
2287
- );
2288
- return null;
2289
- }
2290
- scheduleProactiveRefresh(token) {
2291
- this.clearRefreshTimer();
2292
- const expiresAt = parseJwtExpiry(token);
2293
- if (!expiresAt) return;
2294
- const refreshAt = expiresAt.getTime() - REFRESH_BEFORE_EXP_MS;
2295
- const delay = Math.max(0, refreshAt - Date.now());
2296
- this.refreshTimer = setTimeout(() => {
2297
- this.refreshTimer = null;
2298
- if (!this.disposed) void this.refresh();
2299
- }, delay);
2300
- }
2301
- clearRefreshTimer() {
2302
- if (this.refreshTimer) {
2303
- clearTimeout(this.refreshTimer);
2304
- this.refreshTimer = null;
2305
- }
2306
- }
2307
- /**
2308
- * Cross-context sync: another tab/process refreshed or signed out this
2309
- * KB. Mirror the change into our in-memory state.
2310
- */
2311
- handleStorageChange(key, newValue) {
2312
- if (this.disposed) return;
2313
- if (key !== sessionKey(this.kb.id)) return;
2314
- if (!newValue) {
2315
- this.token$.next(null);
2316
- this.user$.next(null);
2317
- this.clearRefreshTimer();
2318
- return;
2319
- }
2320
- try {
2321
- const parsed = JSON.parse(newValue);
2322
- if (typeof parsed.access === "string") {
2323
- this.token$.next(accessToken(parsed.access));
2324
- this.scheduleProactiveRefresh(parsed.access);
2325
- }
2326
- } catch {
2327
- }
2328
- }
2329
- get expiresAt() {
2330
- const token = this.token$.getValue();
2331
- return token ? parseJwtExpiry(token) : null;
2332
- }
2333
- async dispose() {
2334
- if (this.disposed) return;
2335
- this.disposed = true;
2336
- this.clearRefreshTimer();
2337
- if (this.unsubscribeStorage) {
2338
- this.unsubscribeStorage();
2339
- this.unsubscribeStorage = null;
2340
- }
2341
- this.client.dispose();
2342
- this.token$.complete();
2343
- this.user$.complete();
2344
- }
2345
- };
2346
-
2347
- // src/session/notify.ts
2348
- var activeOnSessionExpired = null;
2349
- var activeOnPermissionDenied = null;
2350
- function notifySessionExpired(message) {
2351
- activeOnSessionExpired?.(message);
2352
- }
2353
- function notifyPermissionDenied(message) {
2354
- activeOnPermissionDenied?.(message);
2355
- }
2356
- function registerAuthNotifyHandlers(handlers) {
2357
- activeOnSessionExpired = handlers.onSessionExpired;
2358
- activeOnPermissionDenied = handlers.onPermissionDenied;
2359
- return () => {
2360
- activeOnSessionExpired = null;
2361
- activeOnPermissionDenied = null;
2362
- };
2363
- }
2364
- var FrontendSessionSignals = class {
2365
- sessionExpiredAt$;
2366
- sessionExpiredMessage$;
2367
- permissionDeniedAt$;
2368
- permissionDeniedMessage$;
2369
- constructor() {
2370
- this.sessionExpiredAt$ = new BehaviorSubject(null);
2371
- this.sessionExpiredMessage$ = new BehaviorSubject(null);
2372
- this.permissionDeniedAt$ = new BehaviorSubject(null);
2373
- this.permissionDeniedMessage$ = new BehaviorSubject(null);
2374
- }
2375
- notifySessionExpired(message) {
2376
- this.sessionExpiredMessage$.next(
2377
- message ?? "Your session has expired. Please sign in again."
2378
- );
2379
- this.sessionExpiredAt$.next(Date.now());
2380
- }
2381
- notifyPermissionDenied(message) {
2382
- this.permissionDeniedMessage$.next(
2383
- message ?? "You do not have permission to perform this action."
2384
- );
2385
- this.permissionDeniedAt$.next(Date.now());
2386
- }
2387
- acknowledgeSessionExpired() {
2388
- this.sessionExpiredAt$.next(null);
2389
- this.sessionExpiredMessage$.next(null);
2390
- }
2391
- acknowledgePermissionDenied() {
2392
- this.permissionDeniedAt$.next(null);
2393
- this.permissionDeniedMessage$.next(null);
2394
- }
2395
- dispose() {
2396
- this.sessionExpiredAt$.complete();
2397
- this.sessionExpiredMessage$.complete();
2398
- this.permissionDeniedAt$.complete();
2399
- this.permissionDeniedMessage$.complete();
2400
- }
2401
- };
2402
-
2403
- // src/session/semiont-browser.ts
2404
- var OPEN_RESOURCES_KEY = "openDocuments";
2405
- function sortOpenResources(resources) {
2406
- return [...resources].sort((a, b) => {
2407
- if (a.order !== void 0 && b.order !== void 0) return a.order - b.order;
2408
- return a.openedAt - b.openedAt;
2409
- });
2410
- }
2411
- function loadOpenResources(storage) {
2412
- try {
2413
- const stored = storage.get(OPEN_RESOURCES_KEY);
2414
- if (stored) return sortOpenResources(JSON.parse(stored));
2415
- } catch {
2416
- }
2417
- return [];
2418
- }
2419
- var SemiontBrowser = class {
2420
- kbs$;
2421
- activeKbId$;
2422
- activeSession$;
2423
- /**
2424
- * Modal signals (session-expired / permission-denied) for the
2425
- * currently-active session. Parallels `activeSession$` — always
2426
- * non-null when `activeSession$` is non-null, always null when it
2427
- * is. Extracted from the session itself so headless sessions
2428
- * (workers, CLIs, tests) don't carry dead modal observables.
2429
- * See [FrontendSessionSignals](./frontend-session-signals.ts).
2430
- */
2431
- activeSignals$;
2432
- /**
2433
- * True while a session is actively being constructed (setActiveKb /
2434
- * signIn in flight, awaiting `session.ready`). Distinguishes the
2435
- * "session about to arrive" intermediate state from "session
2436
- * intentionally null" (after signOut, or when the active KB has no
2437
- * stored credentials). UIs that want a loading spinner should gate
2438
- * on this; otherwise they get stuck spinning after every signOut.
2439
- */
2440
- sessionActivating$;
2441
- openResources$;
2442
- error$;
2443
- identityToken$;
2444
- storage;
2445
- /**
2446
- * App-scoped EventBus. Hosts UI-shell events that must work regardless
2447
- * of whether a KB session is active: panel toggles, sidebar state,
2448
- * tab reorders, routing, settings, etc. Disjoint from the per-session
2449
- * bus inside `SemiontApiClient`, which carries KB-content events
2450
- * (mark:*, beckon:*, gather:*, match:*, bind:*, yield:*, browse:click).
2451
- */
2452
- eventBus = new EventBus();
2453
- unregisterNotify = null;
2454
- unsubscribeStorage = null;
2455
- disposed = false;
2456
- activating = null;
2457
- /**
2458
- * Per-KB in-flight refresh dedup. Simultaneous 401s for the same
2459
- * KB converge on a single `/api/tokens/refresh` network call.
2460
- * Was previously module-scoped in `refresh.ts`; moved here when
2461
- * that file was deleted — SemiontBrowser is a singleton so the
2462
- * scoping is equivalent.
2463
- */
2464
- inFlightRefreshes = /* @__PURE__ */ new Map();
2465
- constructor(config) {
2466
- this.storage = config.storage;
2467
- const kbs = loadKnowledgeBases(this.storage);
2468
- const storedActive = this.storage.get(ACTIVE_KEY);
2469
- const initialActive = storedActive && kbs.some((kb) => kb.id === storedActive) ? storedActive : kbs[0]?.id ?? null;
2470
- this.kbs$ = new BehaviorSubject(kbs);
2471
- this.activeKbId$ = new BehaviorSubject(initialActive);
2472
- this.activeSession$ = new BehaviorSubject(null);
2473
- this.activeSignals$ = new BehaviorSubject(null);
2474
- this.sessionActivating$ = new BehaviorSubject(false);
2475
- this.openResources$ = new BehaviorSubject(loadOpenResources(this.storage));
2476
- this.error$ = new Subject();
2477
- this.identityToken$ = new BehaviorSubject(null);
2478
- this.kbs$.subscribe((next) => saveKnowledgeBases(this.storage, next));
2479
- this.activeKbId$.subscribe((id) => {
2480
- if (id) this.storage.set(ACTIVE_KEY, id);
2481
- else this.storage.delete(ACTIVE_KEY);
2482
- });
2483
- this.openResources$.subscribe((list) => {
2484
- this.storage.set(OPEN_RESOURCES_KEY, JSON.stringify(list));
2485
- });
2486
- this.unsubscribeStorage = this.storage.subscribe?.((key, newValue) => {
2487
- if (key !== OPEN_RESOURCES_KEY || !newValue) return;
2488
- try {
2489
- this.openResources$.next(sortOpenResources(JSON.parse(newValue)));
2490
- } catch {
2491
- }
2492
- }) ?? null;
2493
- this.unregisterNotify = registerAuthNotifyHandlers({
2494
- onSessionExpired: (message) => {
2495
- this.activeSignals$.getValue()?.notifySessionExpired(message ?? null);
2496
- },
2497
- onPermissionDenied: (message) => {
2498
- this.activeSignals$.getValue()?.notifyPermissionDenied(message ?? null);
2499
- }
2500
- });
2501
- if (initialActive) {
2502
- void this.setActiveKb(initialActive);
2503
- }
2504
- }
2505
- // ── App-scoped event bus ──────────────────────────────────────────────
2506
- /** Emit an event on the browser's app-scoped bus. */
2507
- emit(channel, payload) {
2508
- if (this.disposed) return;
2509
- this.eventBus.get(channel).next(payload);
2510
- }
2511
- /** Subscribe to an event; returns unsubscribe. */
2512
- on(channel, handler) {
2513
- const sub = this.eventBus.get(channel).subscribe(handler);
2514
- return () => sub.unsubscribe();
2515
- }
2516
- /** Read-only observable for an app-scoped channel. */
2517
- stream(channel) {
2518
- return this.eventBus.get(channel).asObservable();
2519
- }
2520
- // ── Identity token (NextAuth bridge; D1) ──────────────────────────────
2521
- /**
2522
- * Set the app-level identity token (from NextAuth's useSession).
2523
- * Called at the root layout via a single `useEffect`. No other site
2524
- * in the codebase should call this.
2525
- */
2526
- setIdentityToken(token) {
2527
- if (this.disposed) return;
2528
- this.identityToken$.next(token);
2529
- }
2530
- // ── KB list management ────────────────────────────────────────────────
2531
- addKb(input, access, refresh) {
2532
- const kb = { id: generateKbId(), ...input };
2533
- setStoredSession(this.storage, kb.id, { access, refresh });
2534
- this.kbs$.next([...this.kbs$.getValue(), kb]);
2535
- void this.setActiveKb(kb.id);
2536
- return kb;
2537
- }
2538
- removeKb(id) {
2539
- clearStoredSession(this.storage, id);
2540
- const next = this.kbs$.getValue().filter((kb) => kb.id !== id);
2541
- this.kbs$.next(next);
2542
- if (this.activeKbId$.getValue() === id) {
2543
- void this.setActiveKb(next[0]?.id ?? null);
2544
- }
2545
- }
2546
- updateKb(id, updates) {
2547
- this.kbs$.next(
2548
- this.kbs$.getValue().map((kb) => kb.id === id ? { ...kb, ...updates } : kb)
2549
- );
2550
- }
2551
- /**
2552
- * Read the locally-stored credential status for a KB. Pure / synchronous —
2553
- * does not subscribe to context changes. Used by KB-list UI to color status
2554
- * dots without requiring re-renders on every tick.
2555
- */
2556
- getKbSessionStatus(kbId) {
2557
- const stored = getStoredSession(this.storage, kbId);
2558
- if (!stored) return "signed-out";
2559
- return isJwtExpired(stored.access) ? "expired" : "authenticated";
2560
- }
2561
- /**
2562
- * Switch the active KB. Follows the D2 disposal contract:
2563
- * 1. Synchronously announce the new id on `activeKbId$` and null out
2564
- * `activeSession$` so views see a safe empty state first.
2565
- * 2. Serialize overlapping calls — if an activation is in flight, wait
2566
- * for it before proceeding.
2567
- * 3. Dispose whatever session is currently live.
2568
- * 4. Construct the next session and await `session.ready`.
2569
- * 5. Before emitting, re-check `activeKbId$` — if a newer call superseded
2570
- * us while we waited, dispose our session and skip the emit.
2571
- * 6. Emit the new session.
2572
- */
2573
- async setActiveKb(id) {
2574
- if (this.disposed) return;
2575
- const prevId = this.activeKbId$.getValue();
2576
- const prevSession = this.activeSession$.getValue();
2577
- if (id === prevId && prevSession) return;
2578
- if (prevId !== id) this.activeKbId$.next(id);
2579
- if (prevSession) {
2580
- this.activeSession$.next(null);
2581
- this.activeSignals$.next(null);
2582
- }
2583
- while (this.activating) {
2584
- const current = this.activating;
2585
- await current;
2586
- if (this.disposed) return;
2587
- if (this.activeKbId$.getValue() !== id) return;
2588
- }
2589
- const activation = (async () => {
2590
- const toDispose = this.activeSession$.getValue();
2591
- const signalsToDispose = this.activeSignals$.getValue();
2592
- if (toDispose) {
2593
- this.activeSession$.next(null);
2594
- this.activeSignals$.next(null);
2595
- await toDispose.dispose();
2596
- signalsToDispose?.dispose();
2597
- }
2598
- if (!id) return;
2599
- const kb = this.kbs$.getValue().find((k) => k.id === id);
2600
- if (!kb) return;
2601
- const signals = new FrontendSessionSignals();
2602
- const session = new SemiontSession({
2603
- kb,
2604
- storage: this.storage,
2605
- refresh: () => this.performRefresh(kb),
2606
- validate: (token) => this.performValidate(kb, token),
2607
- onAuthFailed: (msg) => signals.notifySessionExpired(msg),
2608
- onError: (err) => this.error$.next(err)
2609
- });
2610
- try {
2611
- await session.ready;
2612
- } catch (err) {
2613
- this.error$.next(
2614
- new SemiontError(
2615
- "session.construct-failed",
2616
- err instanceof Error ? err.message : String(err),
2617
- id
2618
- )
2619
- );
2620
- await session.dispose();
2621
- signals.dispose();
2622
- return;
2623
- }
2624
- if (this.disposed || this.activeKbId$.getValue() !== id) {
2625
- await session.dispose();
2626
- signals.dispose();
2627
- return;
2628
- }
2629
- this.activeSession$.next(session);
2630
- this.activeSignals$.next(signals);
2631
- })();
2632
- this.activating = activation;
2633
- this.sessionActivating$.next(true);
2634
- try {
2635
- await activation;
2636
- } finally {
2637
- if (this.activating === activation) {
2638
- this.activating = null;
2639
- this.sessionActivating$.next(false);
2640
- }
2641
- }
2642
- }
2643
- /**
2644
- * Sign in to an existing KB: store the tokens and (re)activate the
2645
- * session. If the KB is already active, the current session is disposed
2646
- * and replaced so the new tokens take effect.
2647
- */
2648
- async signIn(id, access, refresh) {
2649
- if (this.disposed) return;
2650
- setStoredSession(this.storage, id, { access, refresh });
2651
- if (this.activeKbId$.getValue() === id) {
2652
- const prevSession = this.activeSession$.getValue();
2653
- const prevSignals = this.activeSignals$.getValue();
2654
- this.activeSession$.next(null);
2655
- this.activeSignals$.next(null);
2656
- if (prevSession) await prevSession.dispose();
2657
- prevSignals?.dispose();
2658
- await this.setActiveKb(id);
2659
- return;
2660
- }
2661
- await this.setActiveKb(id);
2662
- }
2663
- /**
2664
- * Sign out of a KB: clear stored tokens. If the KB is active, dispose
2665
- * its session + signals and emit null for both.
2666
- */
2667
- async signOut(id) {
2668
- if (this.disposed) return;
2669
- clearStoredSession(this.storage, id);
2670
- this.kbs$.next([...this.kbs$.getValue()]);
2671
- if (this.activeKbId$.getValue() === id) {
2672
- const prevSession = this.activeSession$.getValue();
2673
- const prevSignals = this.activeSignals$.getValue();
2674
- this.activeSession$.next(null);
2675
- this.activeSignals$.next(null);
2676
- if (prevSession) await prevSession.dispose();
2677
- prevSignals?.dispose();
2678
- }
2679
- }
2680
- // ── Open resources ────────────────────────────────────────────────────
2681
- addOpenResource(id, name, mediaType, storageUri) {
2682
- const existing = this.openResources$.getValue();
2683
- const idx = existing.findIndex((r) => r.id === id);
2684
- if (idx >= 0) {
2685
- const prev = existing[idx];
2686
- const updated = {
2687
- ...prev,
2688
- name,
2689
- ...mediaType !== void 0 ? { mediaType } : {},
2690
- ...storageUri !== void 0 ? { storageUri } : {}
2691
- };
2692
- const next = [...existing];
2693
- next[idx] = updated;
2694
- this.openResources$.next(next);
2695
- return;
2696
- }
2697
- const resource = {
2698
- id,
2699
- name,
2700
- openedAt: Date.now(),
2701
- order: existing.length,
2702
- ...mediaType !== void 0 ? { mediaType } : {},
2703
- ...storageUri !== void 0 ? { storageUri } : {}
2704
- };
2705
- this.openResources$.next([...existing, resource]);
2706
- }
2707
- removeOpenResource(id) {
2708
- this.openResources$.next(this.openResources$.getValue().filter((r) => r.id !== id));
2709
- }
2710
- updateOpenResourceName(id, name) {
2711
- this.openResources$.next(
2712
- this.openResources$.getValue().map((r) => r.id === id ? { ...r, name } : r)
2713
- );
2714
- }
2715
- reorderOpenResources(oldIndex, newIndex) {
2716
- const list = [...this.openResources$.getValue()];
2717
- if (oldIndex < 0 || oldIndex >= list.length || newIndex < 0 || newIndex >= list.length) {
2718
- return;
2719
- }
2720
- const [moved] = list.splice(oldIndex, 1);
2721
- if (moved) list.splice(newIndex, 0, moved);
2722
- this.openResources$.next(list);
2723
- }
2724
- // ── Auth callbacks bound per session ──────────────────────────────────
2725
- //
2726
- // These closures back the `refresh` and `validate` callbacks passed
2727
- // to `SemiontSession` in `setActiveKb`. Factored out as methods
2728
- // (rather than inline in the activation closure) so test-doubles
2729
- // can override them cleanly, and so the in-flight dedup map
2730
- // survives across activations of the same KB.
2731
- /**
2732
- * Refresh the active KB's access token. Returns the new token on
2733
- * success, null on failure. Concurrent calls for the same KB
2734
- * dedupe through `inFlightRefreshes`, so simultaneous 401s trigger
2735
- * only one `/api/tokens/refresh` round trip.
2736
- *
2737
- * Uses a throwaway `SemiontApiClient` with no `tokenRefresher` —
2738
- * a refresh call returning 401 would otherwise re-enter this
2739
- * function infinitely.
2740
- */
2741
- async performRefresh(kb) {
2742
- const existing = this.inFlightRefreshes.get(kb.id);
2743
- if (existing) return existing;
2744
- const promise = (async () => {
2745
- const stored = getStoredSession(this.storage, kb.id);
2746
- if (!stored) return null;
2747
- const throwaway = new SemiontApiClient({
2748
- baseUrl: baseUrl(kbBackendUrl(kb))
2749
- });
2750
- try {
2751
- const response = await throwaway.refreshToken(refreshToken(stored.refresh));
2752
- const newAccess = response.access_token;
2753
- if (!newAccess) return null;
2754
- setStoredSession(this.storage, kb.id, { access: newAccess, refresh: stored.refresh });
2755
- return newAccess;
2756
- } catch {
2757
- return null;
2758
- } finally {
2759
- throwaway.dispose();
2760
- }
2761
- })();
2762
- this.inFlightRefreshes.set(kb.id, promise);
2763
- try {
2764
- return await promise;
2765
- } finally {
2766
- this.inFlightRefreshes.delete(kb.id);
2767
- }
2768
- }
2769
- /**
2770
- * Validate an access token by calling `getMe` on a throwaway
2771
- * client. The session uses this once at startup to populate
2772
- * `user$`; 401 triggers a refresh-then-retry inside the session.
2773
- */
2774
- async performValidate(kb, token) {
2775
- const throwaway = new SemiontApiClient({
2776
- baseUrl: baseUrl(kbBackendUrl(kb))
2777
- });
2778
- try {
2779
- const data = await throwaway.getMe({ auth: token });
2780
- return data;
2781
- } finally {
2782
- throwaway.dispose();
2783
- }
2784
- }
2785
- // ── Lifecycle ─────────────────────────────────────────────────────────
2786
- async dispose() {
2787
- if (this.disposed) return;
2788
- this.disposed = true;
2789
- this.unregisterNotify?.();
2790
- this.unregisterNotify = null;
2791
- if (this.unsubscribeStorage) {
2792
- this.unsubscribeStorage();
2793
- this.unsubscribeStorage = null;
2794
- }
2795
- const prevSession = this.activeSession$.getValue();
2796
- const prevSignals = this.activeSignals$.getValue();
2797
- this.activeSession$.next(null);
2798
- this.activeSignals$.next(null);
2799
- if (prevSession) await prevSession.dispose();
2800
- prevSignals?.dispose();
2801
- this.kbs$.complete();
2802
- this.activeKbId$.complete();
2803
- this.activeSession$.complete();
2804
- this.activeSignals$.complete();
2805
- this.openResources$.complete();
2806
- this.error$.complete();
2807
- this.identityToken$.complete();
2808
- this.eventBus.destroy();
2809
- }
2810
- };
2811
-
2812
- // src/session/registry.ts
2813
- var instance = null;
2814
- function getBrowser(options) {
2815
- if (!instance) {
2816
- instance = new SemiontBrowser({ storage: options.storage });
2817
- }
2818
- return instance;
2819
- }
2820
-
2821
- // src/session/session-storage.ts
2822
- var InMemorySessionStorage = class {
2823
- map = /* @__PURE__ */ new Map();
2824
- get(key) {
2825
- return this.map.has(key) ? this.map.get(key) : null;
2826
- }
2827
- set(key, value) {
2828
- this.map.set(key, value);
2829
- }
2830
- delete(key) {
2831
- this.map.delete(key);
2832
- }
2833
- };
2834
- function createDisposer() {
2835
- const sub = new Subscription();
2836
- return {
2837
- add: (item) => sub.add(typeof item === "function" ? item : () => item.dispose()),
2838
- dispose: () => sub.unsubscribe()
2839
- };
2840
- }
2841
- function createSearchPipeline(fetch2, options = {}) {
2842
- const debounceMs = options.debounceMs ?? 250;
2843
- const initial = options.initialQuery ?? "";
2844
- const input$ = new Subject();
2845
- const query$ = input$.pipe(startWith(initial));
2846
- const state$ = input$.pipe(
2847
- startWith(initial),
2848
- debounceTime(debounceMs),
2849
- distinctUntilChanged$1(),
2850
- switchMap((q) => {
2851
- const trimmed = q.trim();
2852
- if (!trimmed) {
2853
- return of({ results: [], isSearching: false });
2854
- }
2855
- return fetch2(trimmed).pipe(
2856
- map((results) => ({
2857
- results: results ?? [],
2858
- isSearching: results === void 0
2859
- })),
2860
- startWith({ results: [], isSearching: true })
2861
- );
2862
- })
2863
- );
2864
- return {
2865
- query$,
2866
- state$,
2867
- setQuery: (value) => input$.next(value),
2868
- dispose: () => input$.complete()
2869
- };
2870
- }
2871
- function createBeckonVM(client) {
2872
- const subs = [];
2873
- const hovered$ = new BehaviorSubject(null);
2874
- subs.push(client.stream("beckon:hover").subscribe(({ annotationId }) => {
2875
- hovered$.next(annotationId);
2876
- if (annotationId) {
2877
- client.emit("beckon:sparkle", { annotationId });
2878
- }
2879
- }));
2880
- subs.push(client.stream("browse:click").subscribe(({ annotationId }) => {
2881
- client.emit("beckon:focus", { annotationId });
2882
- }));
2883
- return {
2884
- hoveredAnnotationId$: hovered$.asObservable(),
2885
- hover: (annotationId) => client.emit("beckon:hover", { annotationId }),
2886
- focus: (annotationId) => client.emit("beckon:focus", { annotationId }),
2887
- sparkle: (annotationId) => client.emit("beckon:sparkle", { annotationId }),
2888
- dispose() {
2889
- subs.forEach((s) => s.unsubscribe());
2890
- hovered$.complete();
2891
- }
2892
- };
2893
- }
2894
- var HOVER_DELAY_MS = 150;
2895
- function createHoverHandlers(emit, delayMs) {
2896
- let currentHover = null;
2897
- let timer = null;
2898
- const cancelTimer = () => {
2899
- if (timer !== null) {
2900
- clearTimeout(timer);
2901
- timer = null;
2902
- }
2903
- };
2904
- const handleMouseEnter = (annotationId) => {
2905
- if (currentHover === annotationId) return;
2906
- cancelTimer();
2907
- timer = setTimeout(() => {
2908
- timer = null;
2909
- currentHover = annotationId;
2910
- emit(annotationId);
2911
- }, delayMs);
2912
- };
2913
- const handleMouseLeave = () => {
2914
- cancelTimer();
2915
- if (currentHover !== null) {
2916
- currentHover = null;
2917
- emit(null);
2918
- }
2919
- };
2920
- return { handleMouseEnter, handleMouseLeave, cleanup: cancelTimer };
2921
- }
2922
- var COMMON_PANELS = ["knowledge-base", "user", "settings"];
2923
- var RESOURCE_PANELS = ["history", "info", "annotations", "collaboration", "jsonld"];
2924
- var MOTIVATION_TO_TAB = {
2925
- "linking": "reference",
2926
- "commenting": "comment",
2927
- "tagging": "tag",
2928
- "highlighting": "highlight",
2929
- "assessing": "assessment"
2930
- };
2931
- var tabGenerationCounter = 0;
2932
- function createShellVM(browser, options) {
2933
- const subs = [];
2934
- const activePanel$ = new BehaviorSubject(options?.initialPanel ?? null);
2935
- const scrollToAnnotationId$ = new BehaviorSubject(null);
2936
- const panelInitialTab$ = new BehaviorSubject(null);
2937
- if (options?.onPanelChange) {
2938
- const cb = options.onPanelChange;
2939
- subs.push(activePanel$.subscribe(cb));
2940
- }
2941
- subs.push(browser.stream("panel:toggle").subscribe(({ panel }) => {
2942
- const current = activePanel$.getValue();
2943
- activePanel$.next(current === panel ? null : panel);
2944
- }));
2945
- subs.push(browser.stream("panel:open").subscribe(({ panel, scrollToAnnotationId, motivation }) => {
2946
- if (scrollToAnnotationId) {
2947
- scrollToAnnotationId$.next(scrollToAnnotationId);
2948
- }
2949
- if (motivation) {
2950
- const tab = MOTIVATION_TO_TAB[motivation] || "highlight";
2951
- panelInitialTab$.next({ tab, generation: ++tabGenerationCounter });
2952
- }
2953
- activePanel$.next(panel);
2954
- }));
2955
- subs.push(browser.stream("panel:close").subscribe(() => {
2956
- activePanel$.next(null);
2957
- }));
2958
- return {
2959
- activePanel$: activePanel$.asObservable(),
2960
- scrollToAnnotationId$: scrollToAnnotationId$.asObservable(),
2961
- panelInitialTab$: panelInitialTab$.asObservable(),
2962
- openPanel: (panel) => browser.emit("panel:open", { panel }),
2963
- closePanel: () => browser.emit("panel:close", void 0),
2964
- togglePanel: (panel) => browser.emit("panel:toggle", { panel }),
2965
- onScrollCompleted: () => scrollToAnnotationId$.next(null),
2966
- dispose() {
2967
- subs.forEach((s) => s.unsubscribe());
2968
- activePanel$.complete();
2969
- scrollToAnnotationId$.complete();
2970
- panelInitialTab$.complete();
2971
- }
2972
- };
2973
- }
2974
- function createGatherVM(client, resourceId) {
2975
- const subs = [];
2976
- const context$ = new BehaviorSubject(null);
2977
- const loading$ = new BehaviorSubject(false);
2978
- const error$ = new BehaviorSubject(null);
2979
- const annotationId$ = new BehaviorSubject(null);
2980
- subs.push(client.stream("gather:requested").subscribe((event) => {
2981
- loading$.next(true);
2982
- error$.next(null);
2983
- context$.next(null);
2984
- annotationId$.next(annotationId(event.annotationId));
2985
- const gatherSub = client.gather.annotation(
2986
- annotationId(event.annotationId),
2987
- resourceId,
2988
- { contextWindow: event.options?.contextWindow ?? 2e3 }
2989
- ).pipe(
2990
- timeout(6e4)
2991
- ).subscribe({
2992
- next: (progress) => {
2993
- if ("response" in progress && progress.response) {
2994
- context$.next(
2995
- progress.response.context ?? null
2996
- );
2997
- loading$.next(false);
2998
- }
2999
- },
3000
- error: (err) => {
3001
- error$.next(err instanceof Error ? err : new Error(String(err)));
3002
- loading$.next(false);
3003
- },
3004
- complete: () => {
3005
- loading$.next(false);
3006
- }
3007
- });
3008
- subs.push(gatherSub);
3009
- }));
3010
- return {
3011
- context$: context$.asObservable(),
3012
- loading$: loading$.asObservable(),
3013
- error$: error$.asObservable(),
3014
- annotationId$: annotationId$.asObservable(),
3015
- dispose() {
3016
- subs.forEach((s) => s.unsubscribe());
3017
- context$.complete();
3018
- loading$.complete();
3019
- error$.complete();
3020
- annotationId$.complete();
3021
- }
3022
- };
3023
- }
3024
- function createMatchVM(client, _resourceId) {
3025
- const subs = [];
3026
- subs.push(client.stream("match:search-requested").subscribe((event) => {
3027
- const searchSub = client.match.search(
3028
- resourceId(event.resourceId),
3029
- event.referenceId,
3030
- event.context,
3031
- { limit: event.limit, useSemanticScoring: event.useSemanticScoring }
3032
- ).pipe(
3033
- timeout(6e4)
3034
- ).subscribe({
3035
- next: (result) => client.emit("match:search-results", result),
3036
- error: (err) => client.emit("match:search-failed", {
3037
- correlationId: event.correlationId,
3038
- referenceId: event.referenceId,
3039
- error: err instanceof Error ? err.message : String(err)
3040
- })
3041
- });
3042
- subs.push(searchSub);
3043
- }));
3044
- return {
3045
- dispose() {
3046
- subs.forEach((s) => s.unsubscribe());
3047
- }
3048
- };
3049
- }
3050
- function createYieldVM(client, resourceId$1, locale) {
3051
- const subs = [];
3052
- const isGenerating$ = new BehaviorSubject(false);
3053
- const progress$ = new BehaviorSubject(null);
3054
- let clearTimer = null;
3055
- const generate = (referenceId, options) => {
3056
- const genSub = client.yield.fromAnnotation(
3057
- resourceId(resourceId$1),
3058
- annotationId(referenceId),
3059
- { ...options, language: options.language || locale }
3060
- ).pipe(
3061
- timeout({ each: 3e5 })
3062
- ).subscribe({
3063
- next: (chunk) => {
3064
- progress$.next(chunk);
3065
- isGenerating$.next(true);
3066
- },
3067
- complete: () => {
3068
- isGenerating$.next(false);
3069
- if (clearTimer) clearTimeout(clearTimer);
3070
- clearTimer = setTimeout(() => {
3071
- progress$.next(null);
3072
- clearTimer = null;
3073
- }, 2e3);
3074
- },
3075
- error: () => {
3076
- progress$.next(null);
3077
- isGenerating$.next(false);
3078
- }
3079
- });
3080
- subs.push(genSub);
3081
- };
3082
- return {
3083
- isGenerating$: isGenerating$.asObservable(),
3084
- progress$: progress$.asObservable(),
3085
- generate,
3086
- dispose() {
3087
- subs.forEach((s) => s.unsubscribe());
3088
- if (clearTimer) clearTimeout(clearTimer);
3089
- isGenerating$.complete();
3090
- progress$.complete();
3091
- }
3092
- };
3093
- }
3094
- function selectionToSelector(selection) {
3095
- if (selection.svgSelector) return { type: "SvgSelector", value: selection.svgSelector };
3096
- if (selection.fragmentSelector) {
3097
- const selectors = [{ type: "FragmentSelector", value: selection.fragmentSelector, ...selection.conformsTo && { conformsTo: selection.conformsTo } }];
3098
- if (selection.exact) selectors.push({ type: "TextQuoteSelector", exact: selection.exact, ...selection.prefix && { prefix: selection.prefix }, ...selection.suffix && { suffix: selection.suffix } });
3099
- return selectors;
3100
- }
3101
- return { type: "TextQuoteSelector", exact: selection.exact, ...selection.prefix && { prefix: selection.prefix }, ...selection.suffix && { suffix: selection.suffix } };
3102
- }
3103
- function createMarkVM(client, resourceId) {
3104
- const subs = [];
3105
- const pendingAnnotation$ = new BehaviorSubject(null);
3106
- const assistingMotivation$ = new BehaviorSubject(null);
3107
- const progress$ = new BehaviorSubject(null);
3108
- let progressDismissTimer = null;
3109
- const clearProgressTimer = () => {
3110
- if (progressDismissTimer) {
3111
- clearTimeout(progressDismissTimer);
3112
- progressDismissTimer = null;
3113
- }
3114
- };
3115
- const handleAnnotationRequested = (pending) => {
3116
- pendingAnnotation$.next(pending);
3117
- };
3118
- subs.push(client.stream("mark:requested").subscribe(handleAnnotationRequested));
3119
- subs.push(client.stream("mark:select-comment").subscribe((s) => handleAnnotationRequested({ selector: selectionToSelector(s), motivation: "commenting" })));
3120
- subs.push(client.stream("mark:select-tag").subscribe((s) => handleAnnotationRequested({ selector: selectionToSelector(s), motivation: "tagging" })));
3121
- subs.push(client.stream("mark:select-assessment").subscribe((s) => handleAnnotationRequested({ selector: selectionToSelector(s), motivation: "assessing" })));
3122
- subs.push(client.stream("mark:select-reference").subscribe((s) => handleAnnotationRequested({ selector: selectionToSelector(s), motivation: "linking" })));
3123
- subs.push(client.stream("mark:cancel-pending").subscribe(() => pendingAnnotation$.next(null)));
3124
- subs.push(client.stream("mark:create-ok").subscribe(() => pendingAnnotation$.next(null)));
3125
- subs.push(client.stream("mark:submit").subscribe(async (event) => {
3126
- try {
3127
- const result = await client.mark.annotation(resourceId, {
3128
- motivation: event.motivation,
3129
- target: { source: resourceId, selector: event.selector },
3130
- body: event.body
3131
- });
3132
- client.emit("mark:create-ok", { annotationId: result.annotationId });
3133
- } catch (error) {
3134
- client.emit("mark:create-failed", { message: error instanceof Error ? error.message : String(error) });
3135
- }
3136
- }));
3137
- subs.push(client.stream("mark:delete").subscribe(async (event) => {
3138
- try {
3139
- await client.mark.delete(resourceId, event.annotationId);
3140
- client.emit("mark:delete-ok", { annotationId: event.annotationId });
3141
- } catch (error) {
3142
- client.emit("mark:delete-failed", { message: error instanceof Error ? error.message : String(error) });
3143
- }
3144
- }));
3145
- subs.push(client.stream("mark:assist-request").subscribe((event) => {
3146
- clearProgressTimer();
3147
- assistingMotivation$.next(event.motivation);
3148
- progress$.next(null);
3149
- const assistSub = client.mark.assist(resourceId, event.motivation, event.options).pipe(
3150
- timeout({ each: 18e4 })
3151
- ).subscribe({
3152
- next: (p) => progress$.next(p),
3153
- complete: () => {
3154
- assistingMotivation$.next(null);
3155
- clearProgressTimer();
3156
- progressDismissTimer = setTimeout(() => {
3157
- progress$.next(null);
3158
- progressDismissTimer = null;
3159
- }, 5e3);
3160
- },
3161
- error: () => {
3162
- clearProgressTimer();
3163
- assistingMotivation$.next(null);
3164
- progress$.next(null);
3165
- }
3166
- });
3167
- subs.push(assistSub);
3168
- }));
3169
- subs.push(client.stream("mark:progress-dismiss").subscribe(() => {
3170
- clearProgressTimer();
3171
- progress$.next(null);
3172
- }));
3173
- return {
3174
- pendingAnnotation$: pendingAnnotation$.asObservable(),
3175
- assistingMotivation$: assistingMotivation$.asObservable(),
3176
- progress$: progress$.asObservable(),
3177
- dispose() {
3178
- subs.forEach((s) => s.unsubscribe());
3179
- clearProgressTimer();
3180
- pendingAnnotation$.complete();
3181
- assistingMotivation$.complete();
3182
- progress$.complete();
3183
- }
3184
- };
3185
- }
3186
- var RECENT_LIMIT = 10;
3187
- var SEARCH_LIMIT = 20;
3188
- function createDiscoverVM(client, browse) {
3189
- const disposer = createDisposer();
3190
- const search = createSearchPipeline(
3191
- (q) => client.browse.resources({ search: q, limit: SEARCH_LIMIT })
3192
- );
3193
- disposer.add(search);
3194
- disposer.add(browse);
3195
- const recent$ = client.browse.resources({ limit: RECENT_LIMIT, archived: false });
3196
- const recentResources$ = recent$.pipe(
3197
- map$1((r) => r ?? [])
3198
- );
3199
- const isLoadingRecent$ = recent$.pipe(
3200
- map$1((r) => r === void 0)
3201
- );
3202
- const entityTypes$ = client.browse.entityTypes().pipe(
3203
- map$1((e) => e ?? [])
3204
- );
3205
- return {
3206
- browse,
3207
- search,
3208
- recentResources$,
3209
- entityTypes$,
3210
- isLoadingRecent$,
3211
- dispose: () => disposer.dispose()
3212
- };
3213
- }
3214
- function createEntityTagsVM(client, browse) {
3215
- const disposer = createDisposer();
3216
- disposer.add(browse);
3217
- const newTag$ = new BehaviorSubject("");
3218
- const error$ = new BehaviorSubject("");
3219
- const isAdding$ = new BehaviorSubject(false);
3220
- const raw$ = client.browse.entityTypes();
3221
- const entityTypes$ = raw$.pipe(map$1((e) => e ?? []));
3222
- const isLoading$ = raw$.pipe(map$1((e) => e === void 0));
3223
- const addTag = async () => {
3224
- const tag = newTag$.getValue().trim();
3225
- if (!tag) return;
3226
- error$.next("");
3227
- isAdding$.next(true);
3228
- try {
3229
- await client.mark.entityType(tag);
3230
- newTag$.next("");
3231
- } catch (err) {
3232
- error$.next(err instanceof Error ? err.message : "Failed to add entity type");
3233
- } finally {
3234
- isAdding$.next(false);
3235
- }
3236
- };
3237
- return {
3238
- browse,
3239
- entityTypes$,
3240
- isLoading$,
3241
- newTag$: newTag$.asObservable(),
3242
- error$: error$.asObservable(),
3243
- isAdding$: isAdding$.asObservable(),
3244
- setNewTag: (v) => newTag$.next(v),
3245
- addTag,
3246
- dispose: () => {
3247
- newTag$.complete();
3248
- error$.complete();
3249
- isAdding$.complete();
3250
- disposer.dispose();
3251
- }
3252
- };
3253
- }
3254
- function createExchangeVM(browse, exportFn, importFn) {
3255
- const disposer = createDisposer();
3256
- disposer.add(browse);
3257
- const selectedFile$ = new BehaviorSubject(null);
3258
- const preview$ = new BehaviorSubject(null);
3259
- const importPhase$ = new BehaviorSubject(null);
3260
- const importMessage$ = new BehaviorSubject(void 0);
3261
- const importResult$ = new BehaviorSubject(void 0);
3262
- const isExporting$ = new BehaviorSubject(false);
3263
- const isImporting$ = new BehaviorSubject(false);
3264
- const selectFile = (file) => {
3265
- selectedFile$.next(file);
3266
- importPhase$.next(null);
3267
- importMessage$.next(void 0);
3268
- importResult$.next(void 0);
3269
- preview$.next({
3270
- format: file.name.endsWith(".tar.gz") || file.name.endsWith(".gz") ? "semiont-linked-data" : "unknown",
3271
- version: 1,
3272
- sourceUrl: "",
3273
- stats: {}
3274
- });
3275
- };
3276
- const cancelImport = () => {
3277
- selectedFile$.next(null);
3278
- preview$.next(null);
3279
- importPhase$.next(null);
3280
- importMessage$.next(void 0);
3281
- importResult$.next(void 0);
3282
- };
3283
- const doExport = async () => {
3284
- isExporting$.next(true);
3285
- try {
3286
- const response = await exportFn();
3287
- if (!response.ok) throw new Error(`Export failed: ${response.status} ${response.statusText}`);
3288
- const blob = await response.blob();
3289
- const contentDisposition = response.headers.get("Content-Disposition");
3290
- const filename = contentDisposition?.match(/filename="(.+?)"/)?.[1] ?? `semiont-export-${Date.now()}.tar.gz`;
3291
- return { blob, filename };
3292
- } finally {
3293
- isExporting$.next(false);
3294
- }
3295
- };
3296
- const doImport = async () => {
3297
- const file = selectedFile$.getValue();
3298
- if (!file) return;
3299
- isImporting$.next(true);
3300
- importPhase$.next("started");
3301
- importMessage$.next(void 0);
3302
- importResult$.next(void 0);
3303
- try {
3304
- await importFn(file, {
3305
- onProgress: (event) => {
3306
- importPhase$.next(event.phase);
3307
- importMessage$.next(event.message);
3308
- if (event.result) importResult$.next(event.result);
3309
- }
3310
- });
3311
- } finally {
3312
- isImporting$.next(false);
3313
- }
3314
- };
3315
- return {
3316
- browse,
3317
- selectedFile$: selectedFile$.asObservable(),
3318
- preview$: preview$.asObservable(),
3319
- importPhase$: importPhase$.asObservable(),
3320
- importMessage$: importMessage$.asObservable(),
3321
- importResult$: importResult$.asObservable(),
3322
- isExporting$: isExporting$.asObservable(),
3323
- isImporting$: isImporting$.asObservable(),
3324
- selectFile,
3325
- cancelImport,
3326
- doExport,
3327
- doImport,
3328
- dispose: () => {
3329
- selectedFile$.complete();
3330
- preview$.complete();
3331
- importPhase$.complete();
3332
- importMessage$.complete();
3333
- importResult$.complete();
3334
- isExporting$.complete();
3335
- isImporting$.complete();
3336
- disposer.dispose();
3337
- }
3338
- };
3339
- }
3340
- function createAdminUsersVM(client, browse) {
3341
- const disposer = createDisposer();
3342
- disposer.add(browse);
3343
- const users$ = new BehaviorSubject([]);
3344
- const stats$ = new BehaviorSubject(null);
3345
- const usersLoading$ = new BehaviorSubject(true);
3346
- const statsLoading$ = new BehaviorSubject(true);
3347
- const fetchUsers = () => {
3348
- usersLoading$.next(true);
3349
- client.listUsers().then((data) => {
3350
- users$.next(data.users ?? []);
3351
- usersLoading$.next(false);
3352
- }).catch(() => usersLoading$.next(false));
3353
- };
3354
- const fetchStats = () => {
3355
- statsLoading$.next(true);
3356
- client.getUserStats().then((data) => {
3357
- stats$.next(data.stats ?? null);
3358
- statsLoading$.next(false);
3359
- }).catch(() => statsLoading$.next(false));
3360
- };
3361
- fetchUsers();
3362
- fetchStats();
3363
- const updateUser = async (id, data) => {
3364
- await client.updateUser(userDID(id), data);
3365
- fetchUsers();
3366
- fetchStats();
3367
- };
3368
- return {
3369
- browse,
3370
- users$: users$.asObservable(),
3371
- stats$: stats$.asObservable(),
3372
- usersLoading$: usersLoading$.asObservable(),
3373
- statsLoading$: statsLoading$.asObservable(),
3374
- updateUser,
3375
- dispose: () => {
3376
- users$.complete();
3377
- stats$.complete();
3378
- usersLoading$.complete();
3379
- statsLoading$.complete();
3380
- disposer.dispose();
3381
- }
3382
- };
3383
- }
3384
- function createAdminSecurityVM(client, browse) {
3385
- const disposer = createDisposer();
3386
- disposer.add(browse);
3387
- const providers$ = new BehaviorSubject([]);
3388
- const allowedDomains$ = new BehaviorSubject([]);
3389
- const isLoading$ = new BehaviorSubject(true);
3390
- client.getOAuthConfig().then((data) => {
3391
- const config = data;
3392
- providers$.next(config.providers ?? []);
3393
- allowedDomains$.next(config.allowedDomains ?? []);
3394
- isLoading$.next(false);
3395
- }).catch(() => isLoading$.next(false));
3396
- return {
3397
- browse,
3398
- providers$: providers$.asObservable(),
3399
- allowedDomains$: allowedDomains$.asObservable(),
3400
- isLoading$: isLoading$.asObservable(),
3401
- dispose: () => {
3402
- providers$.complete();
3403
- allowedDomains$.complete();
3404
- isLoading$.complete();
3405
- disposer.dispose();
3406
- }
3407
- };
3408
- }
3409
- function createWelcomeVM(client) {
3410
- const disposer = createDisposer();
3411
- const userData$ = new BehaviorSubject(null);
3412
- const isProcessing$ = new BehaviorSubject(false);
3413
- client.getMe().then((data) => userData$.next(data)).catch(() => {
3414
- });
3415
- const acceptTerms = async () => {
3416
- isProcessing$.next(true);
3417
- try {
3418
- await client.acceptTerms();
3419
- userData$.next({ ...userData$.getValue(), termsAcceptedAt: (/* @__PURE__ */ new Date()).toISOString() });
3420
- } finally {
3421
- isProcessing$.next(false);
3422
- }
3423
- };
3424
- return {
3425
- userData$: userData$.asObservable(),
3426
- isProcessing$: isProcessing$.asObservable(),
3427
- acceptTerms,
3428
- dispose: () => {
3429
- userData$.complete();
3430
- isProcessing$.complete();
3431
- disposer.dispose();
3432
- }
3433
- };
3434
- }
3435
- function createResourceLoaderVM(client, resourceId) {
3436
- const raw$ = client.browse.resource(resourceId);
3437
- const resource$ = raw$;
3438
- const isLoading$ = raw$.pipe(map$1((r) => r === void 0));
3439
- return {
3440
- resource$,
3441
- isLoading$,
3442
- invalidate: () => client.browse.invalidateResourceDetail(resourceId),
3443
- dispose: () => {
3444
- }
3445
- };
3446
- }
3447
- function createSessionVM(client) {
3448
- const isLoggingOut$ = new BehaviorSubject(false);
3449
- const logout = async () => {
3450
- isLoggingOut$.next(true);
3451
- try {
3452
- await client.logout();
3453
- } catch {
3454
- } finally {
3455
- isLoggingOut$.next(false);
3456
- }
3457
- };
3458
- return {
3459
- isLoggingOut$: isLoggingOut$.asObservable(),
3460
- logout,
3461
- dispose: () => {
3462
- isLoggingOut$.complete();
3463
- }
3464
- };
3465
- }
3466
- var SMELTER_CHANNELS = [
3467
- "yield:created",
3468
- "yield:updated",
3469
- "yield:representation-added",
3470
- "mark:archived",
3471
- "mark:added",
3472
- "mark:removed"
3473
- ];
3474
- function createSmelterActorVM(options) {
3475
- const actor = createActorVM({
3476
- baseUrl: options.baseUrl,
3477
- token: options.token,
3478
- channels: [...SMELTER_CHANNELS],
3479
- reconnectMs: options.reconnectMs
3480
- });
3481
- const events$ = merge(
3482
- ...SMELTER_CHANNELS.map(
3483
- (channel) => actor.on$(channel).pipe(
3484
- map((payload) => ({
3485
- type: channel,
3486
- resourceId: payload.resourceId,
3487
- payload
3488
- }))
3489
- )
3490
- )
3491
- );
3492
- return {
3493
- events$,
3494
- state$: actor.state$,
3495
- emit: (channel, payload) => actor.emit(channel, payload),
3496
- start: () => actor.start(),
3497
- stop: () => actor.stop(),
3498
- dispose: () => actor.dispose()
3499
- };
3500
- }
3501
- function createJobClaimAdapter(options) {
3502
- const { actor, jobTypes } = options;
3503
- const activeJob$ = new BehaviorSubject(null);
3504
- const isProcessing$ = new BehaviorSubject(false);
3505
- const jobsCompleted$ = new BehaviorSubject(0);
3506
- const errors$ = new Subject();
3507
- let jobSubscription = null;
3508
- let started = false;
3509
- const claimJob = async (assignment) => {
3510
- try {
3511
- const correlationId = crypto.randomUUID();
3512
- const result$ = merge(
3513
- actor.on$("job:claimed").pipe(
3514
- filter$1((e) => e.correlationId === correlationId),
3515
- map$1((e) => ({ ok: true, response: e.response }))
3516
- ),
3517
- actor.on$("job:claim-failed").pipe(
3518
- filter$1((e) => e.correlationId === correlationId),
3519
- map$1(() => ({ ok: false }))
3520
- )
3521
- ).pipe(take$1(1), timeout$1(1e4));
3522
- const resultPromise = firstValueFrom(result$);
3523
- await actor.emit("job:claim", { correlationId, jobId: assignment.jobId });
3524
- const result = await resultPromise;
3525
- if (!result.ok) return null;
3526
- const job = result.response;
3527
- return {
3528
- jobId: assignment.jobId,
3529
- type: assignment.type,
3530
- resourceId: assignment.resourceId,
3531
- userId: job.metadata?.userId ?? "",
3532
- params: job.params ?? {}
3533
- };
3534
- } catch {
3535
- return null;
3536
- }
3537
- };
3538
- return {
3539
- activeJob$: activeJob$.asObservable(),
3540
- isProcessing$: isProcessing$.asObservable(),
3541
- jobsCompleted$: jobsCompleted$.asObservable(),
3542
- errors$: errors$.asObservable(),
3543
- start: () => {
3544
- if (started) return;
3545
- started = true;
3546
- actor.addChannels(["job:queued"]);
3547
- jobSubscription = actor.on$("job:queued").subscribe((event) => {
3548
- const jobType = event.jobType;
3549
- if (jobTypes.length > 0 && !jobTypes.includes(jobType)) return;
3550
- if (isProcessing$.getValue()) return;
3551
- isProcessing$.next(true);
3552
- claimJob({ jobId: event.jobId, type: jobType, resourceId: event.resourceId }).then((job) => {
3553
- if (job) {
3554
- activeJob$.next(job);
3555
- } else {
3556
- isProcessing$.next(false);
3557
- }
3558
- }).catch(() => {
3559
- isProcessing$.next(false);
3560
- });
3561
- });
3562
- },
3563
- stop: () => {
3564
- jobSubscription?.unsubscribe();
3565
- jobSubscription = null;
3566
- started = false;
3567
- },
3568
- completeJob: () => {
3569
- activeJob$.next(null);
3570
- isProcessing$.next(false);
3571
- jobsCompleted$.next(jobsCompleted$.getValue() + 1);
3572
- },
3573
- failJob: (jid, error) => {
3574
- activeJob$.next(null);
3575
- isProcessing$.next(false);
3576
- errors$.next({ jobId: jid, error });
3577
- },
3578
- dispose: () => {
3579
- jobSubscription?.unsubscribe();
3580
- jobSubscription = null;
3581
- started = false;
3582
- activeJob$.complete();
3583
- isProcessing$.complete();
3584
- jobsCompleted$.complete();
3585
- errors$.complete();
3586
- }
3587
- };
3588
- }
3589
- function createJobQueueVM(client) {
3590
- const jobs$ = new BehaviorSubject([]);
3591
- const jobCreated$ = new Subject();
3592
- const jobCompleted$ = new Subject();
3593
- const jobFailed$ = new Subject();
3594
- const pendingByType$ = jobs$.pipe(
3595
- map$1((all) => {
3596
- const counts = /* @__PURE__ */ new Map();
3597
- for (const j of all) {
3598
- if (j.status === "pending") {
3599
- counts.set(j.type, (counts.get(j.type) ?? 0) + 1);
3600
- }
3601
- }
3602
- return counts;
3603
- })
3604
- );
3605
- const runningJobs$ = jobs$.pipe(
3606
- map$1((all) => all.filter((j) => j.status === "running"))
3607
- );
3608
- const addOrUpdate = (job) => {
3609
- const current = jobs$.getValue();
3610
- const idx = current.findIndex((j) => j.jobId === job.jobId);
3611
- if (idx >= 0) {
3612
- const next = [...current];
3613
- next[idx] = job;
3614
- jobs$.next(next);
3615
- } else {
3616
- jobs$.next([...current, job]);
3617
- }
3618
- };
3619
- const subs = [
3620
- client.stream("job:queued").subscribe((event) => {
3621
- const job = {
3622
- jobId: event.jobId,
3623
- type: event.jobType,
3624
- status: "pending",
3625
- resourceId: event.resourceId ?? "",
3626
- userId: "",
3627
- created: (/* @__PURE__ */ new Date()).toISOString()
3628
- };
3629
- addOrUpdate(job);
3630
- jobCreated$.next(job);
3631
- }),
3632
- client.stream("job:complete").subscribe((event) => {
3633
- const job = {
3634
- jobId: event.jobId,
3635
- type: event.jobType ?? "",
3636
- status: "complete",
3637
- resourceId: event.resourceId ?? "",
3638
- userId: event.userId ?? "",
3639
- created: "",
3640
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
3641
- result: event.result
3642
- };
3643
- addOrUpdate(job);
3644
- jobCompleted$.next(job);
3645
- }),
3646
- client.stream("job:fail").subscribe((event) => {
3647
- const job = {
3648
- jobId: event.jobId,
3649
- type: event.jobType ?? "",
3650
- status: "failed",
3651
- resourceId: event.resourceId ?? "",
3652
- userId: event.userId ?? "",
3653
- created: "",
3654
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
3655
- error: event.error ?? "Unknown error"
3656
- };
3657
- addOrUpdate(job);
3658
- jobFailed$.next(job);
3659
- })
3660
- ];
3661
- return {
3662
- jobs$: jobs$.asObservable(),
3663
- pendingByType$,
3664
- runningJobs$,
3665
- jobCreated$: jobCreated$.asObservable(),
3666
- jobCompleted$: jobCompleted$.asObservable(),
3667
- jobFailed$: jobFailed$.asObservable(),
3668
- dispose: () => {
3669
- subs.forEach((s) => s.unsubscribe());
3670
- jobs$.complete();
3671
- jobCreated$.complete();
3672
- jobCompleted$.complete();
3673
- jobFailed$.complete();
3674
- }
3675
- };
3676
- }
3677
-
3678
- // src/utils/text-encoding.ts
3679
- function extractCharset(mediaType) {
3680
- const charsetMatch = mediaType.match(/charset=([^\s;]+)/i);
3681
- return (charsetMatch?.[1] || "utf-8").toLowerCase();
3682
- }
3683
- function decodeWithCharset(buffer, mediaType) {
3684
- const charset = extractCharset(mediaType);
3685
- const decoder = new TextDecoder(charset);
3686
- return decoder.decode(buffer);
3687
- }
3688
- function getBodySource(body) {
3689
- if (Array.isArray(body)) {
3690
- for (const item of body) {
3691
- if (typeof item === "object" && item !== null && "type" in item && "source" in item) {
3692
- const itemType = item.type;
3693
- const itemSource = item.source;
3694
- if (itemType === "SpecificResource" && typeof itemSource === "string") {
3695
- return itemSource;
3696
- }
3697
- }
3698
- }
3699
- return null;
3700
- }
3701
- if (typeof body === "object" && body !== null && "type" in body && "source" in body) {
3702
- const bodyType = body.type;
3703
- const bodySource = body.source;
3704
- if (bodyType === "SpecificResource" && typeof bodySource === "string") {
3705
- return bodySource;
3706
- }
3707
- }
3708
- return null;
3709
- }
3710
- function getBodyType(body) {
3711
- if (Array.isArray(body)) {
3712
- if (body.length === 0) {
3713
- return null;
3714
- }
3715
- if (typeof body[0] === "object" && body[0] !== null && "type" in body[0]) {
3716
- const firstType = body[0].type;
3717
- if (firstType === "TextualBody" || firstType === "SpecificResource") {
3718
- return firstType;
3719
- }
3720
- }
3721
- return null;
3722
- }
3723
- if (typeof body === "object" && body !== null && "type" in body) {
3724
- const bodyType = body.type;
3725
- if (bodyType === "TextualBody" || bodyType === "SpecificResource") {
3726
- return bodyType;
3727
- }
3728
- }
3729
- return null;
3730
- }
3731
- function isBodyResolved(body) {
3732
- return getBodySource(body) !== null;
3733
- }
3734
- function getTargetSource(target) {
3735
- if (typeof target === "string") {
3736
- return target;
3737
- }
3738
- return target.source;
3739
- }
3740
- function getTargetSelector(target) {
3741
- if (typeof target === "string") {
3742
- return void 0;
3743
- }
3744
- return target.selector;
3745
- }
3746
- function hasTargetSelector(target) {
3747
- return typeof target !== "string" && target.selector !== void 0;
3748
- }
3749
- function isHighlight(annotation) {
3750
- return annotation.motivation === "highlighting";
3751
- }
3752
- function isReference(annotation) {
3753
- return annotation.motivation === "linking";
3754
- }
3755
- function isAssessment(annotation) {
3756
- return annotation.motivation === "assessing";
3757
- }
3758
- function isComment(annotation) {
3759
- return annotation.motivation === "commenting";
3760
- }
3761
- function isTag(annotation) {
3762
- return annotation.motivation === "tagging";
3763
- }
3764
- function getCommentText(annotation) {
3765
- if (!isComment(annotation)) return void 0;
3766
- const body = Array.isArray(annotation.body) ? annotation.body[0] : annotation.body;
3767
- if (body && "value" in body) {
3768
- return body.value;
3769
- }
3770
- return void 0;
3771
- }
3772
- function isStubReference(annotation) {
3773
- return isReference(annotation) && !isBodyResolved(annotation.body);
3774
- }
3775
- function isResolvedReference(annotation) {
3776
- return isReference(annotation) && isBodyResolved(annotation.body);
3777
- }
3778
- function getExactText(selector) {
3779
- if (!selector) {
3780
- return "";
3781
- }
3782
- const selectors = Array.isArray(selector) ? selector : [selector];
3783
- const quoteSelector = selectors.find((s) => s.type === "TextQuoteSelector");
3784
- if (quoteSelector) {
3785
- return quoteSelector.exact;
3786
- }
3787
- return "";
3788
- }
3789
- function getAnnotationExactText(annotation) {
3790
- const selector = getTargetSelector(annotation.target);
3791
- return getExactText(selector);
3792
- }
3793
- function getPrimarySelector(selector) {
3794
- if (Array.isArray(selector)) {
3795
- if (selector.length === 0) {
3796
- throw new Error("Empty selector array");
3797
- }
3798
- const first = selector[0];
3799
- if (!first) {
3800
- throw new Error("Invalid selector array");
3801
- }
3802
- return first;
3803
- }
3804
- return selector;
3805
- }
3806
- function getTextQuoteSelector(selector) {
3807
- const selectors = Array.isArray(selector) ? selector : [selector];
3808
- const found = selectors.find((s) => s.type === "TextQuoteSelector");
3809
- if (!found) return null;
3810
- return found.type === "TextQuoteSelector" ? found : null;
3811
- }
3812
- function extractBoundingBox(svg) {
3813
- const viewBoxMatch = svg.match(/<svg[^>]*viewBox="([^"]+)"/);
3814
- if (viewBoxMatch) {
3815
- const values = viewBoxMatch[1].split(/\s+/).map(parseFloat);
3816
- if (values.length === 4 && values.every((v) => !isNaN(v))) {
3817
- return {
3818
- x: values[0],
3819
- y: values[1],
3820
- width: values[2],
3821
- height: values[3]
3822
- };
3823
- }
3824
- }
3825
- const svgTagMatch = svg.match(/<svg[^>]*>/);
3826
- if (svgTagMatch) {
3827
- const svgTag = svgTagMatch[0];
3828
- const widthMatch = svgTag.match(/width="([^"]+)"/);
3829
- const heightMatch = svgTag.match(/height="([^"]+)"/);
3830
- if (widthMatch && heightMatch) {
3831
- const width = parseFloat(widthMatch[1]);
3832
- const height = parseFloat(heightMatch[1]);
3833
- if (!isNaN(width) && !isNaN(height)) {
3834
- return { x: 0, y: 0, width, height };
3835
- }
3836
- }
3837
- }
3838
- return null;
3839
- }
3840
-
3841
- // src/view-models/pages/resource-viewer-page-vm.ts
3842
- var WIZARD_CLOSED = {
3843
- open: false,
3844
- annotationId: null,
3845
- resourceId: null,
3846
- defaultTitle: "",
3847
- entityTypes: []
3848
- };
3849
- function createResourceViewerPageVM(client, resourceId, locale, browse, options) {
3850
- const disposer = createDisposer();
3851
- const beckon = createBeckonVM(client);
3852
- const mark = createMarkVM(client, resourceId);
3853
- const gather = createGatherVM(client, resourceId);
3854
- const matchVM = createMatchVM(client);
3855
- const yieldVM = createYieldVM(client, resourceId, locale);
3856
- disposer.add(beckon);
3857
- disposer.add(browse);
3858
- disposer.add(mark);
3859
- disposer.add(gather);
3860
- disposer.add(matchVM);
3861
- disposer.add(yieldVM);
3862
- const annotations$ = client.browse.annotations(resourceId).pipe(
3863
- map$1((a) => a ?? [])
3864
- );
3865
- const annotationGroups$ = annotations$.pipe(
3866
- map$1((anns) => {
3867
- const groups = { highlights: [], comments: [], assessments: [], references: [], tags: [] };
3868
- for (const ann of anns) {
3869
- if (isHighlight(ann)) groups.highlights.push(ann);
3870
- else if (isComment(ann)) groups.comments.push(ann);
3871
- else if (isAssessment(ann)) groups.assessments.push(ann);
3872
- else if (isReference(ann)) groups.references.push(ann);
3873
- else if (isTag(ann)) groups.tags.push(ann);
3874
- }
3875
- return groups;
3876
- })
3877
- );
3878
- const entityTypes$ = client.browse.entityTypes().pipe(
3879
- map$1((e) => e ?? [])
3880
- );
3881
- const events$ = client.browse.events(resourceId).pipe(
3882
- map$1((e) => e ?? [])
3883
- );
3884
- const referencedBy$ = client.browse.referencedBy(resourceId).pipe(
3885
- map$1((r) => r ?? [])
3886
- );
3887
- const content$ = new BehaviorSubject("");
3888
- const contentLoading$ = new BehaviorSubject(false);
3889
- const mediaToken$ = new BehaviorSubject(null);
3890
- const mediaType = options?.mediaType || "text/plain";
3891
- const isBinaryType = mediaType.startsWith("image/") || mediaType === "application/pdf";
3892
- if (!isBinaryType && mediaType) {
3893
- contentLoading$.next(true);
3894
- client.browse.resourceRepresentation(resourceId, { accept: mediaType }).then(({ data }) => {
3895
- content$.next(decodeWithCharset(data, mediaType));
3896
- contentLoading$.next(false);
3897
- }).catch(() => {
3898
- contentLoading$.next(false);
3899
- });
3900
- }
3901
- if (isBinaryType) {
3902
- client.auth.mediaToken(resourceId).then(({ token }) => mediaToken$.next(token)).catch(() => {
3903
- });
3904
- }
3905
- const wizard$ = new BehaviorSubject(WIZARD_CLOSED);
3906
- const unsubscribeResource = client.subscribeToResource(resourceId);
3907
- disposer.add(unsubscribeResource);
3908
- const bindInitiateSub = client.stream("bind:initiate").subscribe((event) => {
3909
- wizard$.next({
3910
- open: true,
3911
- annotationId: event.annotationId,
3912
- resourceId: event.resourceId,
3913
- defaultTitle: event.defaultTitle,
3914
- entityTypes: event.entityTypes
3915
- });
3916
- client.emit("gather:requested", {
3917
- correlationId: crypto.randomUUID(),
3918
- annotationId: event.annotationId,
3919
- resourceId: event.resourceId,
3920
- options: { contextWindow: 2e3 }
3921
- });
3922
- });
3923
- disposer.add(() => bindInitiateSub.unsubscribe());
3924
- return {
3925
- beckon,
3926
- browse,
3927
- mark,
3928
- gather,
3929
- yield: yieldVM,
3930
- annotations$,
3931
- annotationGroups$,
3932
- entityTypes$,
3933
- events$,
3934
- referencedBy$,
3935
- content$: content$.asObservable(),
3936
- contentLoading$: contentLoading$.asObservable(),
3937
- mediaToken$: mediaToken$.asObservable(),
3938
- wizard$: wizard$.asObservable(),
3939
- closeWizard: () => wizard$.next(WIZARD_CLOSED),
3940
- dispose: () => {
3941
- wizard$.complete();
3942
- content$.complete();
3943
- contentLoading$.complete();
3944
- mediaToken$.complete();
3945
- disposer.dispose();
3946
- }
3947
- };
3948
- }
3949
-
3950
- // src/utils/fuzzy-anchor.ts
3951
- function normalizeText(text) {
3952
- return text.replace(/\s+/g, " ").replace(/[\u2018\u2019]/g, "'").replace(/[\u201C\u201D]/g, '"').replace(/\u2014/g, "--").replace(/\u2013/g, "-").trim();
3953
- }
3954
- function levenshteinDistance(str1, str2) {
3955
- const len1 = str1.length;
3956
- const len2 = str2.length;
3957
- const matrix = [];
3958
- for (let i = 0; i <= len1; i++) {
3959
- matrix[i] = [i];
3960
- }
3961
- for (let j = 0; j <= len2; j++) {
3962
- matrix[0][j] = j;
3963
- }
3964
- for (let i = 1; i <= len1; i++) {
3965
- for (let j = 1; j <= len2; j++) {
3966
- const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
3967
- const deletion = matrix[i - 1][j] + 1;
3968
- const insertion = matrix[i][j - 1] + 1;
3969
- const substitution = matrix[i - 1][j - 1] + cost;
3970
- matrix[i][j] = Math.min(deletion, insertion, substitution);
3971
- }
3972
- }
3973
- return matrix[len1][len2];
3974
- }
3975
- function buildContentCache(content) {
3976
- return {
3977
- normalizedContent: normalizeText(content),
3978
- lowerContent: content.toLowerCase()
3979
- };
3980
- }
3981
- function findBestTextMatch(content, searchText, positionHint, cache) {
3982
- const maxFuzzyDistance = Math.max(5, Math.floor(searchText.length * 0.05));
3983
- const exactIndex = content.indexOf(searchText);
3984
- if (exactIndex !== -1) {
3985
- return {
3986
- start: exactIndex,
3987
- end: exactIndex + searchText.length,
3988
- matchQuality: "exact"
3989
- };
3990
- }
3991
- const normalizedSearch = normalizeText(searchText);
3992
- const normalizedIndex = cache.normalizedContent.indexOf(normalizedSearch);
3993
- if (normalizedIndex !== -1) {
3994
- let actualPos = 0;
3995
- let normalizedPos = 0;
3996
- while (normalizedPos < normalizedIndex && actualPos < content.length) {
3997
- const char = content[actualPos];
3998
- const normalizedChar = normalizeText(char);
3999
- if (normalizedChar) {
4000
- normalizedPos += normalizedChar.length;
4001
- }
4002
- actualPos++;
4003
- }
4004
- return {
4005
- start: actualPos,
4006
- end: actualPos + searchText.length,
4007
- matchQuality: "normalized"
4008
- };
4009
- }
4010
- const lowerSearch = searchText.toLowerCase();
4011
- const caseInsensitiveIndex = cache.lowerContent.indexOf(lowerSearch);
4012
- if (caseInsensitiveIndex !== -1) {
4013
- return {
4014
- start: caseInsensitiveIndex,
4015
- end: caseInsensitiveIndex + searchText.length,
4016
- matchQuality: "case-insensitive"
4017
- };
4018
- }
4019
- const windowSize = searchText.length;
4020
- const searchRadius = Math.min(500, content.length);
4021
- const searchStart = positionHint !== void 0 ? Math.max(0, positionHint - searchRadius) : 0;
4022
- const searchEnd = positionHint !== void 0 ? Math.min(content.length, positionHint + searchRadius) : content.length;
4023
- let bestMatch = null;
4024
- for (let i = searchStart; i <= searchEnd - windowSize; i++) {
4025
- const candidate = content.substring(i, i + windowSize);
4026
- const distance = levenshteinDistance(searchText, candidate);
4027
- if (distance <= maxFuzzyDistance) {
4028
- if (!bestMatch || distance < bestMatch.distance) {
4029
- bestMatch = { start: i, distance };
4030
- }
4031
- }
4032
- }
4033
- if (bestMatch) {
4034
- return {
4035
- start: bestMatch.start,
4036
- end: bestMatch.start + windowSize,
4037
- matchQuality: "fuzzy"
4038
- };
4039
- }
4040
- return null;
4041
- }
4042
- function findTextWithContext(content, exact, prefix, suffix, positionHint, cache) {
4043
- if (!exact) return null;
4044
- if (positionHint !== void 0 && positionHint >= 0 && positionHint + exact.length <= content.length) {
4045
- if (content.substring(positionHint, positionHint + exact.length) === exact) {
4046
- return { start: positionHint, end: positionHint + exact.length };
4047
- }
4048
- }
4049
- const occurrences = [];
4050
- let index = content.indexOf(exact);
4051
- while (index !== -1) {
4052
- occurrences.push(index);
4053
- index = content.indexOf(exact, index + 1);
4054
- }
4055
- if (occurrences.length === 0) {
4056
- const fuzzyMatch = findBestTextMatch(content, exact, positionHint, cache);
4057
- if (fuzzyMatch) {
4058
- return { start: fuzzyMatch.start, end: fuzzyMatch.end };
4059
- }
4060
- return null;
4061
- }
4062
- if (occurrences.length === 1) {
4063
- const pos2 = occurrences[0];
4064
- return { start: pos2, end: pos2 + exact.length };
4065
- }
4066
- if (prefix || suffix) {
4067
- for (const pos2 of occurrences) {
4068
- const actualPrefixStart = Math.max(0, pos2 - (prefix?.length || 0));
4069
- const actualPrefix = content.substring(actualPrefixStart, pos2);
4070
- const actualSuffixEnd = Math.min(content.length, pos2 + exact.length + (suffix?.length || 0));
4071
- const actualSuffix = content.substring(pos2 + exact.length, actualSuffixEnd);
4072
- const prefixMatch = !prefix || actualPrefix.endsWith(prefix);
4073
- const suffixMatch = !suffix || actualSuffix.startsWith(suffix);
4074
- if (prefixMatch && suffixMatch) {
4075
- return { start: pos2, end: pos2 + exact.length };
4076
- }
4077
- }
4078
- for (const pos2 of occurrences) {
4079
- const actualPrefix = content.substring(Math.max(0, pos2 - (prefix?.length || 0)), pos2);
4080
- const actualSuffix = content.substring(pos2 + exact.length, pos2 + exact.length + (suffix?.length || 0));
4081
- const fuzzyPrefixMatch = !prefix || actualPrefix.includes(prefix.trim());
4082
- const fuzzySuffixMatch = !suffix || actualSuffix.includes(suffix.trim());
4083
- if (fuzzyPrefixMatch && fuzzySuffixMatch) {
4084
- return { start: pos2, end: pos2 + exact.length };
4085
- }
4086
- }
4087
- }
4088
- const pos = occurrences[0];
4089
- return { start: pos, end: pos + exact.length };
4090
- }
4091
- function verifyPosition(content, position, expectedExact) {
4092
- const actualText = content.substring(position.start, position.end);
4093
- return actualText === expectedExact;
4094
- }
4095
-
4096
- // src/utils/locales.ts
4097
- var LOCALES = [
4098
- { code: "ar", nativeName: "\u0627\u0644\u0639\u0631\u0628\u064A\u0629", englishName: "Arabic" },
4099
- { code: "bn", nativeName: "\u09AC\u09BE\u0982\u09B2\u09BE", englishName: "Bengali" },
4100
- { code: "cs", nativeName: "\u010Ce\u0161tina", englishName: "Czech" },
4101
- { code: "da", nativeName: "Dansk", englishName: "Danish" },
4102
- { code: "de", nativeName: "Deutsch", englishName: "German" },
4103
- { code: "el", nativeName: "\u0395\u03BB\u03BB\u03B7\u03BD\u03B9\u03BA\u03AC", englishName: "Greek" },
4104
- { code: "en", nativeName: "English", englishName: "English" },
4105
- { code: "es", nativeName: "Espa\xF1ol", englishName: "Spanish" },
4106
- { code: "fa", nativeName: "\u0641\u0627\u0631\u0633\u06CC", englishName: "Persian" },
4107
- { code: "fi", nativeName: "Suomi", englishName: "Finnish" },
4108
- { code: "fr", nativeName: "Fran\xE7ais", englishName: "French" },
4109
- { code: "he", nativeName: "\u05E2\u05D1\u05E8\u05D9\u05EA", englishName: "Hebrew" },
4110
- { code: "hi", nativeName: "\u0939\u093F\u0928\u094D\u0926\u0940", englishName: "Hindi" },
4111
- { code: "id", nativeName: "Bahasa Indonesia", englishName: "Indonesian" },
4112
- { code: "it", nativeName: "Italiano", englishName: "Italian" },
4113
- { code: "ja", nativeName: "\u65E5\u672C\u8A9E", englishName: "Japanese" },
4114
- { code: "ko", nativeName: "\uD55C\uAD6D\uC5B4", englishName: "Korean" },
4115
- { code: "ms", nativeName: "Bahasa Melayu", englishName: "Malay" },
4116
- { code: "nl", nativeName: "Nederlands", englishName: "Dutch" },
4117
- { code: "no", nativeName: "Norsk", englishName: "Norwegian" },
4118
- { code: "pl", nativeName: "Polski", englishName: "Polish" },
4119
- { code: "pt", nativeName: "Portugu\xEAs", englishName: "Portuguese" },
4120
- { code: "ro", nativeName: "Rom\xE2n\u0103", englishName: "Romanian" },
4121
- { code: "sv", nativeName: "Svenska", englishName: "Swedish" },
4122
- { code: "th", nativeName: "\u0E44\u0E17\u0E22", englishName: "Thai" },
4123
- { code: "tr", nativeName: "T\xFCrk\xE7e", englishName: "Turkish" },
4124
- { code: "uk", nativeName: "\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430", englishName: "Ukrainian" },
4125
- { code: "vi", nativeName: "Ti\u1EBFng Vi\u1EC7t", englishName: "Vietnamese" },
4126
- { code: "zh", nativeName: "\u4E2D\u6587", englishName: "Chinese" }
4127
- ];
4128
- var localeByCode = new Map(
4129
- LOCALES.map((locale) => [locale.code.toLowerCase(), locale])
4130
- );
4131
- function getLocaleInfo(code) {
4132
- if (!code) return void 0;
4133
- return localeByCode.get(code.toLowerCase());
4134
- }
4135
- function getLocaleNativeName(code) {
4136
- return getLocaleInfo(code)?.nativeName;
4137
- }
4138
- function getLocaleEnglishName(code) {
4139
- return getLocaleInfo(code)?.englishName;
4140
- }
4141
- function formatLocaleDisplay(code) {
4142
- if (!code) return void 0;
4143
- const info = getLocaleInfo(code);
4144
- if (!info) return code;
4145
- return `${info.nativeName} (${code.toLowerCase()})`;
4146
- }
4147
- function getAllLocaleCodes() {
4148
- return LOCALES.map((l) => l.code);
4149
- }
4150
-
4151
- // src/utils/resources.ts
4152
- function getResourceId(resource) {
4153
- if (!resource) return void 0;
4154
- return resource["@id"] || void 0;
4155
- }
4156
- function getPrimaryRepresentation(resource) {
4157
- if (!resource?.representations) return void 0;
4158
- const reps = Array.isArray(resource.representations) ? resource.representations : [resource.representations];
4159
- return reps[0];
4160
- }
4161
- function getPrimaryMediaType(resource) {
4162
- return getPrimaryRepresentation(resource)?.mediaType;
4163
- }
4164
- function getChecksum(resource) {
4165
- return getPrimaryRepresentation(resource)?.checksum;
4166
- }
4167
- function getLanguage(resource) {
4168
- return getPrimaryRepresentation(resource)?.language;
4169
- }
4170
- function getStorageUri(resource) {
4171
- return getPrimaryRepresentation(resource)?.storageUri;
4172
- }
4173
- function getCreator(resource) {
4174
- if (!resource?.wasAttributedTo) return void 0;
4175
- return Array.isArray(resource.wasAttributedTo) ? resource.wasAttributedTo[0] : resource.wasAttributedTo;
4176
- }
4177
- function getDerivedFrom(resource) {
4178
- if (!resource?.wasDerivedFrom) return void 0;
4179
- return Array.isArray(resource.wasDerivedFrom) ? resource.wasDerivedFrom[0] : resource.wasDerivedFrom;
4180
- }
4181
- function isArchived(resource) {
4182
- return resource?.archived === true;
4183
- }
4184
- function getResourceEntityTypes(resource) {
4185
- return resource?.entityTypes || [];
4186
- }
4187
- function isDraft(resource) {
4188
- return resource?.isDraft === true;
4189
- }
4190
- function getNodeEncoding(charset) {
4191
- const normalized = charset.toLowerCase().replace(/[-_]/g, "");
4192
- const charsetMap = {
4193
- "utf8": "utf8",
4194
- "iso88591": "latin1",
4195
- "latin1": "latin1",
4196
- "ascii": "ascii",
4197
- "usascii": "ascii",
4198
- "utf16le": "utf16le",
4199
- "ucs2": "ucs2",
4200
- "binary": "binary",
4201
- "windows1252": "latin1",
4202
- // Windows-1252 is a superset of Latin-1
4203
- "cp1252": "latin1"
4204
- };
4205
- return charsetMap[normalized] || "utf8";
4206
- }
4207
- function decodeRepresentation(buffer, mediaType) {
4208
- const charsetMatch = mediaType.match(/charset=([^\s;]+)/i);
4209
- const charset = (charsetMatch?.[1] || "utf-8").toLowerCase();
4210
- const encoding = getNodeEncoding(charset);
4211
- return buffer.toString(encoding);
4212
- }
4213
-
4214
- // src/utils/svg-utils.ts
4215
- function createRectangleSvg(start, end) {
4216
- const x = Math.min(start.x, end.x);
4217
- const y = Math.min(start.y, end.y);
4218
- const width = Math.abs(end.x - start.x);
4219
- const height = Math.abs(end.y - start.y);
4220
- return `<svg xmlns="http://www.w3.org/2000/svg"><rect x="${x}" y="${y}" width="${width}" height="${height}"/></svg>`;
4221
- }
4222
- function createPolygonSvg(points) {
4223
- if (points.length < 3) {
4224
- throw new Error("Polygon requires at least 3 points");
4225
- }
4226
- const pointsStr = points.map((p) => `${p.x},${p.y}`).join(" ");
4227
- return `<svg xmlns="http://www.w3.org/2000/svg"><polygon points="${pointsStr}"/></svg>`;
4228
- }
4229
- function createCircleSvg(center, radius) {
4230
- if (radius <= 0) {
4231
- throw new Error("Circle radius must be positive");
4232
- }
4233
- return `<svg xmlns="http://www.w3.org/2000/svg"><circle cx="${center.x}" cy="${center.y}" r="${radius}"/></svg>`;
4234
- }
4235
- function parseSvgSelector(svg) {
4236
- const rectMatch = svg.match(/<rect\s+([^>]+)\/>/);
4237
- if (rectMatch && rectMatch[1]) {
4238
- const attrs = rectMatch[1];
4239
- const x = parseFloat(attrs.match(/x="([^"]+)"/)?.[1] || "0");
4240
- const y = parseFloat(attrs.match(/y="([^"]+)"/)?.[1] || "0");
4241
- const width = parseFloat(attrs.match(/width="([^"]+)"/)?.[1] || "0");
4242
- const height = parseFloat(attrs.match(/height="([^"]+)"/)?.[1] || "0");
4243
- return {
4244
- type: "rect",
4245
- data: { x, y, width, height }
4246
- };
4247
- }
4248
- const polygonMatch = svg.match(/<polygon\s+points="([^"]+)"/);
4249
- if (polygonMatch && polygonMatch[1]) {
4250
- const pointsStr = polygonMatch[1];
4251
- const points = pointsStr.split(/\s+/).map((pair) => {
4252
- const [x, y] = pair.split(",").map(parseFloat);
4253
- return { x, y };
4254
- });
4255
- return {
4256
- type: "polygon",
4257
- data: { points }
4258
- };
4259
- }
4260
- const circleMatch = svg.match(/<circle\s+([^>]+)\/>/);
4261
- if (circleMatch && circleMatch[1]) {
4262
- const attrs = circleMatch[1];
4263
- const cx = parseFloat(attrs.match(/cx="([^"]+)"/)?.[1] || "0");
4264
- const cy = parseFloat(attrs.match(/cy="([^"]+)"/)?.[1] || "0");
4265
- const r = parseFloat(attrs.match(/r="([^"]+)"/)?.[1] || "0");
4266
- return {
4267
- type: "circle",
4268
- data: { cx, cy, r }
4269
- };
4270
- }
4271
- return null;
4272
- }
4273
- function normalizeCoordinates(point, displayWidth, displayHeight, imageWidth, imageHeight) {
4274
- return {
4275
- x: point.x / displayWidth * imageWidth,
4276
- y: point.y / displayHeight * imageHeight
4277
- };
4278
- }
4279
- function scaleSvgToNative(svg, displayWidth, displayHeight, imageWidth, imageHeight) {
4280
- const parsed = parseSvgSelector(svg);
4281
- if (!parsed) return svg;
4282
- const scaleX = imageWidth / displayWidth;
4283
- const scaleY = imageHeight / displayHeight;
4284
- switch (parsed.type) {
4285
- case "rect": {
4286
- const { x, y, width, height } = parsed.data;
4287
- return createRectangleSvg(
4288
- { x: x * scaleX, y: y * scaleY },
4289
- { x: (x + width) * scaleX, y: (y + height) * scaleY }
4290
- );
4291
- }
4292
- case "circle": {
4293
- const { cx, cy, r } = parsed.data;
4294
- return createCircleSvg(
4295
- { x: cx * scaleX, y: cy * scaleY },
4296
- r * Math.min(scaleX, scaleY)
4297
- );
4298
- }
4299
- case "polygon": {
4300
- const points = parsed.data.points.map((p) => ({
4301
- x: p.x * scaleX,
4302
- y: p.y * scaleY
4303
- }));
4304
- return createPolygonSvg(points);
4305
- }
4306
- }
4307
- return svg;
4308
- }
4309
-
4310
- // src/utils/text-context.ts
4311
- function extractContext(content, start, end) {
4312
- const CONTEXT_LENGTH = 64;
4313
- const MAX_EXTENSION = 32;
4314
- let prefix;
4315
- if (start > 0) {
4316
- let prefixStart = Math.max(0, start - CONTEXT_LENGTH);
4317
- let extensionCount = 0;
4318
- while (prefixStart > 0 && extensionCount < MAX_EXTENSION) {
4319
- const char = content[prefixStart - 1];
4320
- if (!char || /[\s.,;:!?'"()\[\]{}<>\/\\]/.test(char)) {
4321
- break;
4322
- }
4323
- prefixStart--;
4324
- extensionCount++;
4325
- }
4326
- prefix = content.substring(prefixStart, start);
4327
- }
4328
- let suffix;
4329
- if (end < content.length) {
4330
- let suffixEnd = Math.min(content.length, end + CONTEXT_LENGTH);
4331
- let extensionCount = 0;
4332
- while (suffixEnd < content.length && extensionCount < MAX_EXTENSION) {
4333
- const char = content[suffixEnd];
4334
- if (!char || /[\s.,;:!?'"()\[\]{}<>\/\\]/.test(char)) {
4335
- break;
4336
- }
4337
- suffixEnd++;
4338
- extensionCount++;
4339
- }
4340
- suffix = content.substring(end, suffixEnd);
4341
- }
4342
- return { prefix, suffix };
4343
- }
4344
- function validateAndCorrectOffsets(content, aiStart, aiEnd, exact) {
4345
- const textAtOffset = content.substring(aiStart, aiEnd);
4346
- if (textAtOffset === exact) {
4347
- const context2 = extractContext(content, aiStart, aiEnd);
4348
- return {
4349
- start: aiStart,
4350
- end: aiEnd,
4351
- exact,
4352
- prefix: context2.prefix,
4353
- suffix: context2.suffix,
4354
- corrected: false,
4355
- matchQuality: "exact"
4356
- };
4357
- }
4358
- const cache = buildContentCache(content);
4359
- const match = findBestTextMatch(content, exact, aiStart, cache);
4360
- if (!match) {
4361
- throw new Error(
4362
- "Cannot find acceptable match for text in content. All search strategies failed. Text may be hallucinated."
4363
- );
4364
- }
4365
- const actualText = content.substring(match.start, match.end);
4366
- const context = extractContext(content, match.start, match.end);
4367
- return {
4368
- start: match.start,
4369
- end: match.end,
4370
- exact: actualText,
4371
- // Use actual text from document, not AI's version
4372
- prefix: context.prefix,
4373
- suffix: context.suffix,
4374
- corrected: true,
4375
- fuzzyMatched: match.matchQuality !== "exact",
4376
- matchQuality: match.matchQuality
4377
- };
4378
- }
4379
-
4380
- // src/utils/validation.ts
4381
- var JWTTokenSchema = {
4382
- parse(token) {
4383
- if (typeof token !== "string") {
4384
- throw new Error("Token must be a string");
4385
- }
4386
- if (!token || token.length === 0) {
4387
- throw new Error("Token is required");
4388
- }
4389
- const jwtRegex = /^[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]*$/;
4390
- if (!jwtRegex.test(token)) {
4391
- throw new Error("Invalid JWT token format");
4392
- }
4393
- return token;
4394
- },
4395
- safeParse(token) {
4396
- try {
4397
- const validated = this.parse(token);
4398
- return { success: true, data: validated };
4399
- } catch (error) {
4400
- return {
4401
- success: false,
4402
- error: error instanceof Error ? error.message : "Invalid JWT token"
4403
- };
4404
- }
4405
- }
4406
- };
4407
- function validateData(schema, data) {
4408
- try {
4409
- const validated = schema.parse(data);
4410
- return { success: true, data: validated };
4411
- } catch (error) {
4412
- return {
4413
- success: false,
4414
- error: error instanceof Error ? error.message : "Validation failed"
4415
- };
4416
- }
4417
- }
4418
- function isValidEmail(email) {
4419
- if (email.length < 1 || email.length > 255) {
4420
- return false;
4421
- }
4422
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
4423
- return emailRegex.test(email);
4424
- }
4425
-
4426
- // src/view-models/pages/compose-page-vm.ts
4427
- function createComposePageVM(client, browse, params, auth) {
4428
- const disposer = createDisposer();
4429
- disposer.add(browse);
4430
- const isReferenceMode = Boolean(params.annotationUri && params.sourceDocumentId && params.name);
4431
- const isCloneMode = params.mode === "clone" && Boolean(params.token);
4432
- const pageMode = isCloneMode ? "clone" : isReferenceMode ? "reference" : "new";
4433
- const mode$ = new BehaviorSubject(pageMode);
4434
- const loading$ = new BehaviorSubject(true);
4435
- const cloneData$ = new BehaviorSubject(null);
4436
- const referenceData$ = new BehaviorSubject(null);
4437
- const gatheredContext$ = new BehaviorSubject(null);
4438
- const entityTypes$ = client.browse.entityTypes().pipe(
4439
- map$1((e) => e ?? [])
4440
- );
4441
- if (isReferenceMode) {
4442
- const entityTypes = params.entityTypes ? params.entityTypes.split(",") : [];
4443
- referenceData$.next({
4444
- annotationUri: params.annotationUri,
4445
- sourceDocumentId: params.sourceDocumentId,
4446
- name: params.name,
4447
- entityTypes
4448
- });
4449
- if (params.storedContext) {
4450
- try {
4451
- gatheredContext$.next(JSON.parse(params.storedContext));
4452
- } catch {
4453
- }
4454
- }
4455
- loading$.next(false);
4456
- } else if (isCloneMode) {
4457
- void (async () => {
4458
- try {
4459
- const tokenResult = await client.yield.fromToken(params.token);
4460
- if (tokenResult && auth) {
4461
- const rId = resourceId(tokenResult["@id"]);
4462
- const mediaType = getPrimaryMediaType(tokenResult) || "text/plain";
4463
- const { data } = await client.getResourceRepresentation(rId, {
4464
- accept: mediaType,
4465
- auth
4466
- });
4467
- const content = decodeWithCharset(data, mediaType);
4468
- cloneData$.next({ sourceResource: tokenResult, sourceContent: content });
4469
- }
4470
- } catch {
4471
- }
4472
- loading$.next(false);
4473
- })();
4474
- } else {
4475
- loading$.next(false);
4476
- }
4477
- const save = async (saveParams) => {
4478
- if (saveParams.mode === "clone") {
4479
- const response2 = await client.yield.createFromToken({
4480
- token: params.token,
4481
- name: saveParams.name,
4482
- content: saveParams.content,
4483
- archiveOriginal: saveParams.archiveOriginal ?? true
4484
- });
4485
- return response2.resourceId;
4486
- }
4487
- let fileToUpload;
4488
- let mimeType;
4489
- if (saveParams.file) {
4490
- fileToUpload = saveParams.file;
4491
- mimeType = saveParams.format ?? "application/octet-stream";
4492
- } else {
4493
- const blob = new Blob([saveParams.content || ""], { type: saveParams.format ?? "application/octet-stream" });
4494
- const extension = saveParams.format === "text/plain" ? ".txt" : saveParams.format === "text/html" ? ".html" : ".md";
4495
- fileToUpload = new File([blob], saveParams.name + extension, { type: saveParams.format ?? "application/octet-stream" });
4496
- mimeType = saveParams.format ?? "application/octet-stream";
4497
- }
4498
- const format = saveParams.charset && !saveParams.file ? `${mimeType}; charset=${saveParams.charset}` : mimeType;
4499
- const response = await client.yield.resource({
4500
- name: saveParams.name,
4501
- file: fileToUpload,
4502
- format,
4503
- entityTypes: saveParams.entityTypes || [],
4504
- language: saveParams.language,
4505
- creationMethod: "ui",
4506
- storageUri: saveParams.storageUri
4507
- });
4508
- const newResourceId = response.resourceId;
4509
- if (saveParams.mode === "reference" && saveParams.annotationUri && saveParams.sourceDocumentId) {
4510
- await client.bind.body(
4511
- resourceId(saveParams.sourceDocumentId),
4512
- annotationId(saveParams.annotationUri),
4513
- [{ op: "add", item: { type: "SpecificResource", source: newResourceId, purpose: "linking" } }]
4514
- );
4515
- }
4516
- return newResourceId;
4517
- };
4518
- return {
4519
- browse,
4520
- mode$: mode$.asObservable(),
4521
- loading$: loading$.asObservable(),
4522
- cloneData$: cloneData$.asObservable(),
4523
- referenceData$: referenceData$.asObservable(),
4524
- gatheredContext$: gatheredContext$.asObservable(),
4525
- entityTypes$,
4526
- save,
4527
- dispose: () => {
4528
- mode$.complete();
4529
- loading$.complete();
4530
- cloneData$.complete();
4531
- referenceData$.complete();
4532
- gatheredContext$.complete();
4533
- disposer.dispose();
4534
- }
4535
- };
4536
- }
4537
-
4538
- // src/mime-utils.ts
4539
- function getExtensionForMimeType(mimeType) {
4540
- const map15 = {
4541
- "text/plain": "txt",
4542
- "text/markdown": "md",
4543
- "image/png": "png",
4544
- "image/jpeg": "jpg",
4545
- "application/pdf": "pdf"
4546
- };
4547
- return map15[mimeType] || "dat";
4548
- }
4549
- function isImageMimeType(mimeType) {
4550
- return mimeType === "image/png" || mimeType === "image/jpeg";
4551
- }
4552
- function isTextMimeType(mimeType) {
4553
- return mimeType === "text/plain" || mimeType === "text/markdown";
4554
- }
4555
- function isPdfMimeType(mimeType) {
4556
- return mimeType === "application/pdf";
4557
- }
4558
- function getMimeCategory(mimeType) {
4559
- if (isTextMimeType(mimeType)) {
4560
- return "text";
4561
- }
4562
- if (isImageMimeType(mimeType) || isPdfMimeType(mimeType)) {
4563
- return "image";
4564
- }
4565
- return "unsupported";
4566
- }
4567
771
 
4568
- export { APIError, AdminNamespace, AuthNamespace, BeckonNamespace, BindNamespace, BrowseNamespace, BusRequestError, COMMON_PANELS, DEGRADED_THRESHOLD_MS, FrontendSessionSignals, GatherNamespace, HOVER_DELAY_MS, InMemorySessionStorage, JWTTokenSchema, JobNamespace, LOCALES, MarkNamespace, MatchNamespace, RESOURCE_PANELS, SemiontApiClient, SemiontBrowser, SemiontError, SemiontSession, YieldNamespace, buildContentCache, busRequest, createActorVM, createAdminSecurityVM, createAdminUsersVM, createBeckonVM, createCache, createCircleSvg, createComposePageVM, createDiscoverVM, createDisposer, createEntityTagsVM, createExchangeVM, createGatherVM, createHoverHandlers, createJobClaimAdapter, createJobQueueVM, createMarkVM, createMatchVM, createPolygonSvg, createRectangleSvg, createResourceLoaderVM, createResourceViewerPageVM, createSearchPipeline, createSessionVM, createShellVM, createSmelterActorVM, createWelcomeVM, createYieldVM, decodeRepresentation, decodeWithCharset, defaultProtocol, extractBoundingBox, extractCharset, extractContext, findBestTextMatch, findTextWithContext, formatLocaleDisplay, getAllLocaleCodes, getAnnotationExactText, getBodySource, getBodyType, getBrowser, 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, isValidHostname, kbBackendUrl, normalizeCoordinates, normalizeText, notifyPermissionDenied, notifySessionExpired, parseSvgSelector, scaleSvgToNative, setStoredSession, validateAndCorrectOffsets, validateData, verifyPosition };
772
+ export { APIError, DEGRADED_THRESHOLD_MS, HttpContentTransport, HttpTransport, createActorVM };
4569
773
  //# sourceMappingURL=index.js.map
4570
774
  //# sourceMappingURL=index.js.map