@semiont/sdk 0.5.1 → 0.5.3

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,10 +1,10 @@
1
- import { SemiontError, annotationId, resourceId, email, googleCredential, refreshToken, EventBus, baseUrl, accessToken, isHighlight, isComment, isAssessment, isReference, isTag, decodeWithCharset, getPrimaryMediaType, userDID, searchQuery } from '@semiont/core';
1
+ import { SemiontError, annotationId, resourceId, email, googleCredential, refreshToken, EventBus, baseUrl, accessToken, searchQuery } from '@semiont/core';
2
2
  export { SemiontError, accessToken, annotationId, baseUrl, entityType, refreshToken, resourceId, userId } from '@semiont/core';
3
- import { Observable, lastValueFrom, firstValueFrom, merge, TimeoutError, throwError, BehaviorSubject, map as map$1, distinctUntilChanged, Subject, Subscription, of, filter as filter$1, take as take$1, timeout as timeout$1 } from 'rxjs';
3
+ import { Observable, lastValueFrom, firstValueFrom, merge, TimeoutError, throwError, map as map$1, BehaviorSubject, Subject, Subscription, of, distinctUntilChanged as distinctUntilChanged$1 } from 'rxjs';
4
4
  export { firstValueFrom, lastValueFrom } from 'rxjs';
5
- import { filter, map, take, timeout, catchError, takeUntil, startWith, debounceTime, distinctUntilChanged as distinctUntilChanged$1, switchMap } from 'rxjs/operators';
6
- import { HttpTransport, HttpContentTransport, createActorVM, APIError } from '@semiont/api-client';
7
- export { APIError, DEGRADED_THRESHOLD_MS, HttpContentTransport, HttpTransport, createActorVM } from '@semiont/api-client';
5
+ import { filter, map, take, timeout, catchError, takeUntil, startWith, debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
6
+ import { HttpTransport, HttpContentTransport, APIError } from '@semiont/api-client';
7
+ export { APIError, HttpContentTransport, HttpTransport } from '@semiont/api-client';
8
8
 
9
9
  // src/client.ts
10
10
  var StreamObservable = class _StreamObservable extends Observable {
@@ -28,7 +28,7 @@ var CacheObservable = class _CacheObservable extends Observable {
28
28
  * Observable per key (its B4 contract), so this preserves that contract
29
29
  * through the awaitable wrapping. Without the memo, every public-method
30
30
  * call would produce a fresh wrapper and break referential-equality
31
- * guarantees that React-side consumers depend on.
31
+ * guarantees that hook-style reactive consumers depend on.
32
32
  *
33
33
  * Backed by a `WeakMap`, so wrappers are GC'd when their source is.
34
34
  */
@@ -42,6 +42,17 @@ var CacheObservable = class _CacheObservable extends Observable {
42
42
  }
43
43
  };
44
44
  var wrapperCache = /* @__PURE__ */ new WeakMap();
45
+ var UploadObservable = class extends Observable {
46
+ then(onfulfilled, onrejected) {
47
+ return lastValueFrom(this).then((v) => {
48
+ if (v.phase !== "finished") {
49
+ throw new Error(`UploadObservable resolved on a non-finished event: ${v.phase}`);
50
+ }
51
+ const result = { resourceId: v.resourceId };
52
+ return onfulfilled ? onfulfilled(result) : result;
53
+ }, onrejected);
54
+ }
55
+ };
45
56
  var BusRequestError = class extends SemiontError {
46
57
  constructor(message, code, details) {
47
58
  super(message, code, details);
@@ -117,7 +128,7 @@ function createCache(fetchFn) {
117
128
  if (!obs) {
118
129
  obs = store$.pipe(
119
130
  map$1((m) => m.get(key)),
120
- distinctUntilChanged()
131
+ distinctUntilChanged$1()
121
132
  );
122
133
  obsCache.set(key, obs);
123
134
  }
@@ -160,6 +171,7 @@ function createCache(fetchFn) {
160
171
 
161
172
  // src/namespaces/browse.ts
162
173
  var ENTITY_TYPES_KEY = "_";
174
+ var TAG_SCHEMAS_KEY = "_";
163
175
  var BrowseNamespace = class {
164
176
  constructor(transport, bus, content) {
165
177
  this.transport = transport;
@@ -220,6 +232,16 @@ var BrowseNamespace = class {
220
232
  );
221
233
  return result.entityTypes;
222
234
  });
235
+ this.tagSchemasCache = createCache(async () => {
236
+ const result = await busRequest(
237
+ this.transport,
238
+ "browse:tag-schemas-requested",
239
+ {},
240
+ "browse:tag-schemas-result",
241
+ "browse:tag-schemas-failed"
242
+ );
243
+ return result.tagSchemas;
244
+ });
223
245
  this.referencedByCache = createCache(async (resourceId2) => {
224
246
  const result = await busRequest(
225
247
  this.transport,
@@ -263,6 +285,7 @@ var BrowseNamespace = class {
263
285
  annotationDetailCache;
264
286
  annotationResources = /* @__PURE__ */ new Map();
265
287
  entityTypesCache;
288
+ tagSchemasCache;
266
289
  referencedByCache;
267
290
  resourceEventsCache;
268
291
  /** Filter-blob memory so `invalidateResourceLists` can replay per-key. */
@@ -305,6 +328,9 @@ var BrowseNamespace = class {
305
328
  entityTypes() {
306
329
  return CacheObservable.from(this.entityTypesCache.observe(ENTITY_TYPES_KEY));
307
330
  }
331
+ tagSchemas() {
332
+ return CacheObservable.from(this.tagSchemasCache.observe(TAG_SCHEMAS_KEY));
333
+ }
308
334
  referencedBy(resourceId2) {
309
335
  return CacheObservable.from(this.referencedByCache.observe(resourceId2));
310
336
  }
@@ -389,6 +415,9 @@ var BrowseNamespace = class {
389
415
  invalidateEntityTypes() {
390
416
  this.entityTypesCache.invalidate(ENTITY_TYPES_KEY);
391
417
  }
418
+ invalidateTagSchemas() {
419
+ this.tagSchemasCache.invalidate(TAG_SCHEMAS_KEY);
420
+ }
392
421
  invalidateReferencedBy(resourceId2) {
393
422
  this.referencedByCache.invalidate(resourceId2);
394
423
  }
@@ -465,6 +494,7 @@ var BrowseNamespace = class {
465
494
  for (const rId of this.referencedByCache.keys()) this.invalidateReferencedBy(rId);
466
495
  }
467
496
  this.invalidateEntityTypes();
497
+ this.invalidateTagSchemas();
468
498
  });
469
499
  this.on("mark:delete-ok", (event) => {
470
500
  this.removeAnnotationDetail(annotationId(event.annotationId));
@@ -499,7 +529,8 @@ var BrowseNamespace = class {
499
529
  this.on("yield:update-ok", this.onYieldResourceMutated);
500
530
  this.on("mark:archived", this.onArchiveToggled);
501
531
  this.on("mark:unarchived", this.onArchiveToggled);
502
- this.on("mark:entity-type-added", () => this.invalidateEntityTypes());
532
+ this.on("frame:entity-type-added", () => this.invalidateEntityTypes());
533
+ this.on("frame:tag-schema-added", () => this.invalidateTagSchemas());
503
534
  }
504
535
  };
505
536
  var MarkNamespace = class {
@@ -507,7 +538,8 @@ var MarkNamespace = class {
507
538
  this.transport = transport;
508
539
  this.bus = bus;
509
540
  }
510
- async annotation(resourceId2, input) {
541
+ async annotation(input) {
542
+ const resourceId2 = resourceId(input.target.source);
511
543
  const result = await busRequest(
512
544
  this.transport,
513
545
  "mark:create-request",
@@ -520,14 +552,6 @@ var MarkNamespace = class {
520
552
  async delete(resourceId2, annotationId2) {
521
553
  await this.transport.emit("mark:delete", { annotationId: annotationId2, resourceId: resourceId2 });
522
554
  }
523
- async entityType(type) {
524
- await this.transport.emit("mark:add-entity-type", { tag: type });
525
- }
526
- async entityTypes(types) {
527
- for (const tag of types) {
528
- await this.transport.emit("mark:add-entity-type", { tag });
529
- }
530
- }
531
555
  async archive(resourceId2) {
532
556
  await this.transport.emit("mark:archive", { resourceId: resourceId2 });
533
557
  }
@@ -539,6 +563,7 @@ var MarkNamespace = class {
539
563
  let done = false;
540
564
  let pollTimer = null;
541
565
  let pollInterval = null;
566
+ let unsubscribeResource = this.transport.subscribeToResource(resourceId2);
542
567
  const cleanup = () => {
543
568
  done = true;
544
569
  if (pollTimer) {
@@ -549,6 +574,10 @@ var MarkNamespace = class {
549
574
  clearInterval(pollInterval);
550
575
  pollInterval = null;
551
576
  }
577
+ if (unsubscribeResource) {
578
+ unsubscribeResource();
579
+ unsubscribeResource = null;
580
+ }
552
581
  };
553
582
  const resetPollTimer = (jobId) => {
554
583
  if (pollTimer) clearTimeout(pollTimer);
@@ -682,6 +711,7 @@ var MarkNamespace = class {
682
711
  if (options.density !== void 0) params.density = options.density;
683
712
  if (options.tone !== void 0) params.tone = options.tone;
684
713
  if (options.language !== void 0) params.language = options.language;
714
+ if (options.sourceLanguage !== void 0) params.sourceLanguage = options.sourceLanguage;
685
715
  if (options.schemaId !== void 0) params.schemaId = options.schemaId;
686
716
  if (options.categories !== void 0) params.categories = options.categories;
687
717
  return busRequest(
@@ -717,7 +747,7 @@ var GatherNamespace = class {
717
747
  this.transport = transport;
718
748
  this.bus = bus;
719
749
  }
720
- annotation(annotationId2, resourceId2, options) {
750
+ annotation(resourceId2, annotationId2, options) {
721
751
  return new StreamObservable((subscriber) => {
722
752
  const correlationId = crypto.randomUUID();
723
753
  const complete$ = this.bus.get("gather:complete").pipe(
@@ -809,28 +839,60 @@ var YieldNamespace = class {
809
839
  this.bus = bus;
810
840
  this.content = content;
811
841
  }
812
- async resource(data) {
813
- const result = await this.content.putBinary({
814
- name: data.name,
815
- file: data.file,
816
- format: data.format,
817
- storageUri: data.storageUri,
818
- ...data.entityTypes ? { entityTypes: data.entityTypes } : {},
819
- ...data.language ? { language: data.language } : {},
820
- ...data.creationMethod ? { creationMethod: data.creationMethod } : {},
821
- ...data.sourceAnnotationId ? { sourceAnnotationId: data.sourceAnnotationId } : {},
822
- ...data.sourceResourceId ? { sourceResourceId: data.sourceResourceId } : {},
823
- ...data.generationPrompt ? { generationPrompt: data.generationPrompt } : {},
824
- ...data.generator ? { generator: data.generator } : {},
825
- ...data.isDraft !== void 0 ? { isDraft: data.isDraft } : {}
842
+ resource(data) {
843
+ const totalBytes = typeof Buffer !== "undefined" && data.file instanceof Buffer ? data.file.length : data.file.size;
844
+ return new UploadObservable((subscriber) => {
845
+ subscriber.next({ phase: "started", totalBytes });
846
+ let cancelled = false;
847
+ const abortController = new AbortController();
848
+ this.content.putBinary(
849
+ {
850
+ name: data.name,
851
+ file: data.file,
852
+ format: data.format,
853
+ storageUri: data.storageUri,
854
+ ...data.entityTypes ? { entityTypes: data.entityTypes } : {},
855
+ ...data.language ? { language: data.language } : {},
856
+ ...data.creationMethod ? { creationMethod: data.creationMethod } : {},
857
+ ...data.sourceAnnotationId ? { sourceAnnotationId: data.sourceAnnotationId } : {},
858
+ ...data.sourceResourceId ? { sourceResourceId: data.sourceResourceId } : {},
859
+ ...data.generationPrompt ? { generationPrompt: data.generationPrompt } : {},
860
+ ...data.generator ? { generator: data.generator } : {},
861
+ ...data.isDraft !== void 0 ? { isDraft: data.isDraft } : {}
862
+ },
863
+ {
864
+ // Byte-progress hook. Honored by `HttpContentTransport`'s XHR
865
+ // path; ignored by ky-path uploads (no `onProgress` consumer)
866
+ // and by `LocalContentTransport` (no wire to observe).
867
+ onProgress: ({ bytesUploaded, totalBytes: txTotal }) => {
868
+ if (cancelled) return;
869
+ const total = txTotal > 0 ? txTotal : totalBytes;
870
+ subscriber.next({ phase: "progress", bytesUploaded, totalBytes: total });
871
+ },
872
+ signal: abortController.signal
873
+ }
874
+ ).then((result) => {
875
+ if (cancelled) return;
876
+ subscriber.next({
877
+ phase: "finished",
878
+ resourceId: resourceId(result.resourceId)
879
+ });
880
+ subscriber.complete();
881
+ }).catch((err) => {
882
+ if (!cancelled) subscriber.error(err);
883
+ });
884
+ return () => {
885
+ cancelled = true;
886
+ abortController.abort();
887
+ };
826
888
  });
827
- return { resourceId: result.resourceId };
828
889
  }
829
890
  fromAnnotation(resourceId2, annotationId2, options) {
830
891
  return new StreamObservable((subscriber) => {
831
892
  let done = false;
832
893
  let pollTimer = null;
833
894
  let pollInterval = null;
895
+ let unsubscribeResource = this.transport.subscribeToResource(resourceId2);
834
896
  const cleanup = () => {
835
897
  done = true;
836
898
  if (pollTimer) {
@@ -841,6 +903,10 @@ var YieldNamespace = class {
841
903
  clearInterval(pollInterval);
842
904
  pollInterval = null;
843
905
  }
906
+ if (unsubscribeResource) {
907
+ unsubscribeResource();
908
+ unsubscribeResource = null;
909
+ }
844
910
  };
845
911
  const resetPollTimer = (jid) => {
846
912
  if (pollTimer) clearTimeout(pollTimer);
@@ -914,7 +980,9 @@ var YieldNamespace = class {
914
980
  referenceId: annotationId2,
915
981
  title: options.title,
916
982
  prompt: options.prompt,
983
+ entityTypes: options.entityTypes,
917
984
  language: options.language,
985
+ sourceLanguage: options.sourceLanguage,
918
986
  temperature: options.temperature,
919
987
  maxTokens: options.maxTokens,
920
988
  storageUri: options.storageUri,
@@ -960,13 +1028,14 @@ var YieldNamespace = class {
960
1028
  return result.sourceResource;
961
1029
  }
962
1030
  async createFromToken(options) {
963
- return busRequest(
1031
+ const result = await busRequest(
964
1032
  this.transport,
965
1033
  "yield:clone-create",
966
1034
  options,
967
1035
  "yield:clone-created",
968
1036
  "yield:clone-create-failed"
969
1037
  );
1038
+ return { resourceId: resourceId(result.resourceId) };
970
1039
  }
971
1040
  clone() {
972
1041
  this.bus.get("yield:clone").next(void 0);
@@ -979,7 +1048,7 @@ var BeckonNamespace = class {
979
1048
  this.transport = transport;
980
1049
  this.bus = bus;
981
1050
  }
982
- attention(annotationId2, resourceId2) {
1051
+ attention(resourceId2, annotationId2) {
983
1052
  void this.transport.emit("beckon:focus", { annotationId: annotationId2, resourceId: resourceId2 });
984
1053
  }
985
1054
  hover(annotationId2) {
@@ -990,6 +1059,24 @@ var BeckonNamespace = class {
990
1059
  }
991
1060
  };
992
1061
 
1062
+ // src/namespaces/frame.ts
1063
+ var FrameNamespace = class {
1064
+ constructor(transport) {
1065
+ this.transport = transport;
1066
+ }
1067
+ async addEntityType(type) {
1068
+ await this.transport.emit("frame:add-entity-type", { tag: type });
1069
+ }
1070
+ async addEntityTypes(types) {
1071
+ for (const tag of types) {
1072
+ await this.transport.emit("frame:add-entity-type", { tag });
1073
+ }
1074
+ }
1075
+ async addTagSchema(schema) {
1076
+ await this.transport.emit("frame:add-tag-schema", { schema });
1077
+ }
1078
+ };
1079
+
993
1080
  // src/namespaces/job.ts
994
1081
  var JobNamespace = class {
995
1082
  constructor(transport, bus) {
@@ -1027,7 +1114,7 @@ var JobNamespace = class {
1027
1114
  }
1028
1115
  async pollUntilComplete(jobId, options) {
1029
1116
  const interval = options?.interval ?? 1e3;
1030
- const timeout7 = options?.timeout ?? 6e4;
1117
+ const timeout6 = options?.timeout ?? 6e4;
1031
1118
  const startTime = Date.now();
1032
1119
  while (true) {
1033
1120
  const status = await this.status(jobId);
@@ -1035,89 +1122,97 @@ var JobNamespace = class {
1035
1122
  if (status.status === "complete" || status.status === "failed" || status.status === "cancelled") {
1036
1123
  return status;
1037
1124
  }
1038
- if (Date.now() - startTime > timeout7) {
1039
- throw new Error(`Job polling timeout after ${timeout7}ms`);
1125
+ if (Date.now() - startTime > timeout6) {
1126
+ throw new Error(`Job polling timeout after ${timeout6}ms`);
1040
1127
  }
1041
1128
  await new Promise((resolve) => setTimeout(resolve, interval));
1042
1129
  }
1043
1130
  }
1044
- async cancel(_jobId, type) {
1045
- await this.transport.emit("job:cancel-requested", {
1046
- jobType: type === "generation" ? "generation" : "annotation"
1047
- });
1131
+ async cancelByType(jobType) {
1132
+ await this.transport.emit("job:cancel-requested", { jobType });
1048
1133
  }
1049
1134
  cancelRequest(jobType) {
1050
1135
  this.bus.get("job:cancel-requested").next({ jobType });
1051
1136
  }
1052
1137
  };
1053
1138
  var AuthNamespace = class {
1054
- constructor(transport) {
1055
- this.transport = transport;
1139
+ constructor(backend) {
1140
+ this.backend = backend;
1056
1141
  }
1057
1142
  async password(emailStr, passwordStr) {
1058
- return this.transport.authenticatePassword(email(emailStr), passwordStr);
1143
+ return this.backend.authenticatePassword(email(emailStr), passwordStr);
1059
1144
  }
1060
1145
  async google(credential) {
1061
- return this.transport.authenticateGoogle(googleCredential(credential));
1146
+ return this.backend.authenticateGoogle(googleCredential(credential));
1062
1147
  }
1063
1148
  async refresh(token) {
1064
- return this.transport.refreshAccessToken(refreshToken(token));
1149
+ return this.backend.refreshAccessToken(refreshToken(token));
1065
1150
  }
1066
1151
  async logout() {
1067
- await this.transport.logout();
1152
+ await this.backend.logout();
1068
1153
  }
1069
1154
  async me() {
1070
- return this.transport.getCurrentUser();
1155
+ return this.backend.getCurrentUser();
1071
1156
  }
1072
1157
  async acceptTerms() {
1073
- await this.transport.acceptTerms();
1158
+ await this.backend.acceptTerms();
1074
1159
  }
1075
1160
  async mcpToken() {
1076
- return this.transport.generateMcpToken();
1161
+ return this.backend.generateMcpToken();
1077
1162
  }
1078
1163
  async mediaToken(resourceId2) {
1079
- return this.transport.getMediaToken(resourceId2);
1164
+ return this.backend.getMediaToken(resourceId2);
1080
1165
  }
1081
1166
  };
1082
1167
 
1083
1168
  // src/namespaces/admin.ts
1084
1169
  var AdminNamespace = class {
1085
- constructor(transport) {
1086
- this.transport = transport;
1170
+ constructor(backend) {
1171
+ this.backend = backend;
1087
1172
  }
1088
1173
  async users() {
1089
- const result = await this.transport.listUsers();
1174
+ const result = await this.backend.listUsers();
1090
1175
  return result.users;
1091
1176
  }
1092
1177
  async userStats() {
1093
- return this.transport.getUserStats();
1178
+ return this.backend.getUserStats();
1094
1179
  }
1095
1180
  async updateUser(userId2, data) {
1096
- const result = await this.transport.updateUser(userId2, data);
1181
+ const result = await this.backend.updateUser(userId2, data);
1097
1182
  return result.user;
1098
1183
  }
1099
1184
  async oauthConfig() {
1100
- return this.transport.getOAuthConfig();
1185
+ return this.backend.getOAuthConfig();
1101
1186
  }
1102
1187
  async healthCheck() {
1103
- return this.transport.healthCheck();
1188
+ return this.backend.healthCheck();
1104
1189
  }
1105
1190
  async status() {
1106
- return this.transport.getStatus();
1191
+ return this.backend.getStatus();
1107
1192
  }
1108
1193
  async backup() {
1109
- return this.transport.backupKnowledgeBase();
1194
+ return this.backend.backupKnowledgeBase();
1110
1195
  }
1111
- async restore(file, onProgress) {
1112
- return this.transport.restoreKnowledgeBase(file, onProgress);
1196
+ restore(file) {
1197
+ return wrapAsStream(this.backend.restoreKnowledgeBase(file));
1113
1198
  }
1114
1199
  async exportKnowledgeBase(params) {
1115
- return this.transport.exportKnowledgeBase(params);
1200
+ return this.backend.exportKnowledgeBase(params);
1116
1201
  }
1117
- async importKnowledgeBase(file, onProgress) {
1118
- return this.transport.importKnowledgeBase(file, onProgress);
1202
+ importKnowledgeBase(file) {
1203
+ return wrapAsStream(this.backend.importKnowledgeBase(file));
1119
1204
  }
1120
1205
  };
1206
+ function wrapAsStream(source) {
1207
+ return new StreamObservable((subscriber) => {
1208
+ const sub = source.subscribe({
1209
+ next: (v) => subscriber.next(v),
1210
+ error: (e) => subscriber.error(e),
1211
+ complete: () => subscriber.complete()
1212
+ });
1213
+ return () => sub.unsubscribe();
1214
+ });
1215
+ }
1121
1216
  var SemiontClient = class _SemiontClient {
1122
1217
  /**
1123
1218
  * The wire-facing transport. Owns bus actor, HTTP, auth, admin, exchange,
@@ -1137,6 +1232,15 @@ var SemiontClient = class _SemiontClient {
1137
1232
  bus;
1138
1233
  baseUrl;
1139
1234
  // ── Verb-oriented namespace API ──────────────────────────────────────────
1235
+ //
1236
+ // The first nine namespaces are bus-driven and always present. `frame`
1237
+ // is the schema-layer flow's surface (eighth flow); the other eight are
1238
+ // content-layer flows plus `job`. `auth` and `admin` are backend-ops
1239
+ // namespaces — they're only constructed when the caller passes an
1240
+ // `IBackendOperations` instance to the constructor. A `SemiontClient`
1241
+ // over a transport-only setup (e.g. `LocalTransport`) has
1242
+ // `auth === undefined` / `admin === undefined`.
1243
+ frame;
1140
1244
  browse;
1141
1245
  mark;
1142
1246
  bind;
@@ -1159,13 +1263,20 @@ var SemiontClient = class _SemiontClient {
1159
1263
  * Callers do not pass a bus in. If they need to interact with the bus
1160
1264
  * (e.g. for tests or to subscribe to arbitrary channels), they read it
1161
1265
  * back via `client.bus`.
1266
+ *
1267
+ * `backend` is optional. When provided, the `auth` and `admin`
1268
+ * namespaces are constructed against it; when omitted, they're
1269
+ * `undefined`. For HTTP setups this is conventionally the same
1270
+ * `HttpTransport` instance that's also passed as `transport` (HTTP
1271
+ * implements both `ITransport` and `IBackendOperations`).
1162
1272
  */
1163
- constructor(transport, content) {
1273
+ constructor(transport, content, backend) {
1164
1274
  this.transport = transport;
1165
1275
  this.content = content;
1166
1276
  this.baseUrl = transport.baseUrl;
1167
1277
  this.bus = new EventBus();
1168
1278
  this.transport.bridgeInto(this.bus);
1279
+ this.frame = new FrameNamespace(this.transport);
1169
1280
  this.browse = new BrowseNamespace(this.transport, this.bus, this.content);
1170
1281
  this.mark = new MarkNamespace(this.transport, this.bus);
1171
1282
  this.bind = new BindNamespace(this.transport, this.bus);
@@ -1174,8 +1285,8 @@ var SemiontClient = class _SemiontClient {
1174
1285
  this.yield = new YieldNamespace(this.transport, this.bus, this.content);
1175
1286
  this.beckon = new BeckonNamespace(this.transport, this.bus);
1176
1287
  this.job = new JobNamespace(this.transport, this.bus);
1177
- this.auth = new AuthNamespace(this.transport);
1178
- this.admin = new AdminNamespace(this.transport);
1288
+ this.auth = backend ? new AuthNamespace(backend) : void 0;
1289
+ this.admin = backend ? new AdminNamespace(backend) : void 0;
1179
1290
  }
1180
1291
  /** Transport-level connection state. HTTP reflects SSE health; local is always 'connected'. */
1181
1292
  get state$() {
@@ -1197,9 +1308,10 @@ var SemiontClient = class _SemiontClient {
1197
1308
  * Use this for one-shot scripts, CLI commands, or any consumer that
1198
1309
  * doesn't need to drive the token from outside (no manual refresh,
1199
1310
  * no cross-tab sync). For long-running scripts that need refresh,
1200
- * use `SemiontSession.fromHttp(...)` instead it owns the same
1201
- * transport/client wiring plus the proactive-refresh + storage
1202
- * machinery.
1311
+ * use `SemiontSession.fromHttp(...)` (with a token already on hand)
1312
+ * or `SemiontSession.signInHttp(...)` (credentials-first) instead
1313
+ * either owns the same transport/client wiring plus the
1314
+ * proactive-refresh + storage machinery.
1203
1315
  *
1204
1316
  * Strings are accepted for `baseUrl` and `token`; they are branded
1205
1317
  * via `baseUrl()` / `accessToken()` from `@semiont/core` automatically.
@@ -1213,13 +1325,13 @@ var SemiontClient = class _SemiontClient {
1213
1325
  const token$ = new BehaviorSubject(tok);
1214
1326
  const transport = new HttpTransport({ baseUrl: url, token$ });
1215
1327
  const content = new HttpContentTransport(transport);
1216
- return new _SemiontClient(transport, content);
1328
+ return new _SemiontClient(transport, content, transport);
1217
1329
  }
1218
1330
  /**
1219
1331
  * Async factory for the credentials-first script case. Builds a
1220
- * transient transport, calls `auth.password(email, password)` to
1221
- * acquire an access token, and returns the wired client with the
1222
- * token populated.
1332
+ * transient HTTP transport, calls `auth.password(email, password)`
1333
+ * to acquire an access token, and returns the wired client with
1334
+ * the token populated.
1223
1335
  *
1224
1336
  * This is the right entry point for skills, CLI scripts, and any
1225
1337
  * consumer that starts with email + password rather than a JWT
@@ -1228,18 +1340,24 @@ var SemiontClient = class _SemiontClient {
1228
1340
  * `fromHttp({ baseUrl, token })` instead.
1229
1341
  *
1230
1342
  * For long-running scripts that need refresh, use
1231
- * `SemiontSession.signIn(...)` — same credentials shape, plus the
1232
- * session machinery for proactive refresh and persistence.
1343
+ * `SemiontSession.signInHttp(...)` — same credentials shape, plus
1344
+ * the session machinery for proactive refresh and persistence.
1345
+ *
1346
+ * Named `signInHttp` because email+password authentication is
1347
+ * inherently an HTTP-shaped operation in the current backend; an
1348
+ * in-process `LocalTransport` doesn't have a credentials login
1349
+ * path. Non-HTTP transports construct the client directly from
1350
+ * their package's transport instance.
1233
1351
  *
1234
1352
  * Throws if authentication fails. The transient client is disposed
1235
1353
  * before the throw, so no resources leak on failure.
1236
1354
  */
1237
- static async signIn(opts) {
1355
+ static async signInHttp(opts) {
1238
1356
  const url = typeof opts.baseUrl === "string" ? baseUrl(opts.baseUrl) : opts.baseUrl;
1239
1357
  const token$ = new BehaviorSubject(null);
1240
1358
  const transport = new HttpTransport({ baseUrl: url, token$ });
1241
1359
  const content = new HttpContentTransport(transport);
1242
- const client = new _SemiontClient(transport, content);
1360
+ const client = new _SemiontClient(transport, content, transport);
1243
1361
  try {
1244
1362
  const auth = await client.auth.password(opts.email, opts.password);
1245
1363
  token$.next(accessToken(auth.token));
@@ -1293,35 +1411,28 @@ function isJwtExpired(token) {
1293
1411
  if (!expiry) return true;
1294
1412
  return expiry.getTime() < Date.now();
1295
1413
  }
1296
- function migrateLegacyEntry(entry) {
1297
- if (entry.host !== void 0) return entry;
1298
- try {
1299
- const url = new URL(entry.backendUrl);
1300
- return {
1301
- id: entry.id,
1302
- label: entry.label,
1303
- host: url.hostname,
1304
- port: parseInt(url.port, 10) || (url.protocol === "https:" ? 443 : 80),
1305
- protocol: url.protocol === "https:" ? "https" : "http",
1306
- email: ""
1307
- };
1308
- } catch {
1309
- return {
1310
- id: entry.id,
1311
- label: entry.label || "Unknown",
1312
- host: "localhost",
1313
- port: 4e3,
1314
- protocol: "http",
1315
- email: ""
1316
- };
1414
+ function isKnowledgeBase(entry) {
1415
+ if (!entry || typeof entry !== "object") return false;
1416
+ const e = entry;
1417
+ if (typeof e.id !== "string" || typeof e.label !== "string" || typeof e.email !== "string") {
1418
+ return false;
1317
1419
  }
1420
+ const ep = e.endpoint;
1421
+ if (!ep || typeof ep !== "object") return false;
1422
+ if (ep.kind === "http") {
1423
+ return typeof ep.host === "string" && typeof ep.port === "number" && (ep.protocol === "http" || ep.protocol === "https");
1424
+ }
1425
+ if (ep.kind === "local") {
1426
+ return typeof ep.kbId === "string";
1427
+ }
1428
+ return false;
1318
1429
  }
1319
1430
  function loadKnowledgeBases(storage) {
1320
1431
  try {
1321
1432
  const raw = storage.get(STORAGE_KEY);
1322
1433
  if (!raw) return [];
1323
1434
  const entries = JSON.parse(raw);
1324
- return entries.map(migrateLegacyEntry);
1435
+ return entries.filter(isKnowledgeBase);
1325
1436
  } catch {
1326
1437
  return [];
1327
1438
  }
@@ -1336,15 +1447,15 @@ var HOSTNAME_RE = /^(([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]
1336
1447
  function isValidHostname(host) {
1337
1448
  return HOSTNAME_RE.test(host);
1338
1449
  }
1339
- function kbBackendUrl(kb) {
1340
- if (!isValidHostname(kb.host)) {
1341
- throw new Error(`Invalid KB hostname: "${kb.host}"`);
1450
+ function kbBackendUrl(endpoint) {
1451
+ if (!isValidHostname(endpoint.host)) {
1452
+ throw new Error(`Invalid KB hostname: "${endpoint.host}"`);
1342
1453
  }
1343
1454
  const url = new URL("http://x");
1344
- url.protocol = kb.protocol + ":";
1345
- url.hostname = kb.host;
1346
- url.port = String(kb.port);
1347
- return `${kb.protocol}://${url.hostname}:${kb.port}`;
1455
+ url.protocol = endpoint.protocol + ":";
1456
+ url.hostname = endpoint.host;
1457
+ url.port = String(endpoint.port);
1458
+ return `${endpoint.protocol}://${url.hostname}:${endpoint.port}`;
1348
1459
  }
1349
1460
  function generateKbId() {
1350
1461
  return crypto.randomUUID();
@@ -1365,6 +1476,18 @@ var SemiontSession = class _SemiontSession {
1365
1476
  token$;
1366
1477
  user$;
1367
1478
  streamState$;
1479
+ /**
1480
+ * Stream of `SemiontError` instances surfaced by the underlying transport
1481
+ * just before they're thrown to the caller. For `HttpTransport` this is
1482
+ * an `APIError` (status-coded); other transports emit their own subclass.
1483
+ * Surfaced here so a host layer (e.g. `SemiontBrowser`) can route by
1484
+ * `err.code` to global notifications without every call site handling
1485
+ * errors itself. Headless consumers can subscribe for logging.
1486
+ *
1487
+ * Re-published from `client.transport.errors$` per the `ITransport`
1488
+ * contract — the session is purely a passthrough.
1489
+ */
1490
+ errors$;
1368
1491
  /** Resolves after the initial validation round-trip completes (success or failure). */
1369
1492
  ready;
1370
1493
  storage;
@@ -1387,6 +1510,7 @@ var SemiontSession = class _SemiontSession {
1387
1510
  this.client = config.client;
1388
1511
  this.token$ = config.token$;
1389
1512
  this.user$ = new BehaviorSubject(null);
1513
+ this.errors$ = this.client.transport.errors$;
1390
1514
  const stored = getStoredSession(this.storage, this.kb.id);
1391
1515
  if (stored && !isJwtExpired(stored.access) && this.token$.getValue() === null) {
1392
1516
  this.token$.next(accessToken(stored.access));
@@ -1571,7 +1695,7 @@ var SemiontSession = class _SemiontSession {
1571
1695
  const token$ = new BehaviorSubject(tok);
1572
1696
  const transport = new HttpTransport({ baseUrl: url, token$ });
1573
1697
  const content = new HttpContentTransport(transport);
1574
- const client = new SemiontClient(transport, content);
1698
+ const client = new SemiontClient(transport, content, transport);
1575
1699
  const config = { kb: opts.kb, storage: opts.storage, client, token$ };
1576
1700
  if (opts.refresh) config.refresh = opts.refresh;
1577
1701
  if (opts.validate) config.validate = opts.validate;
@@ -1581,11 +1705,11 @@ var SemiontSession = class _SemiontSession {
1581
1705
  }
1582
1706
  /**
1583
1707
  * Async factory for the credentials-first long-running script case.
1584
- * Builds the transport stack, calls `auth.password(email, password)`
1585
- * to acquire access + refresh tokens, persists them via the storage
1586
- * adapter, wires a default `refresh` callback that exchanges the
1587
- * refresh token via `auth.refresh(...)`, and returns the ready
1588
- * session.
1708
+ * Builds the HTTP transport stack, calls `auth.password(email,
1709
+ * password)` to acquire access + refresh tokens, persists them via
1710
+ * the storage adapter, wires a default `refresh` callback that
1711
+ * exchanges the refresh token via `auth.refresh(...)`, and returns
1712
+ * the ready session.
1589
1713
  *
1590
1714
  * The consumer-supplied `refresh` callback becomes optional — only
1591
1715
  * needed for non-standard refresh flows (worker-pool shared secret,
@@ -1598,15 +1722,21 @@ var SemiontSession = class _SemiontSession {
1598
1722
  * trampling each other's tokens. The factory does not synthesize a
1599
1723
  * default; the consumer makes the choice.
1600
1724
  *
1725
+ * Named `signInHttp` because email+password authentication is
1726
+ * inherently an HTTP-shaped operation in the current backend; an
1727
+ * in-process `LocalTransport` doesn't have a credentials login
1728
+ * path. Non-HTTP transports construct the session directly from
1729
+ * their package's transport instance.
1730
+ *
1601
1731
  * Throws on auth failure with no resources leaked. On success, the
1602
1732
  * returned session's `ready` promise has already resolved.
1603
1733
  */
1604
- static async signIn(opts) {
1734
+ static async signInHttp(opts) {
1605
1735
  const url = typeof opts.baseUrl === "string" ? baseUrl(opts.baseUrl) : opts.baseUrl;
1606
1736
  const token$ = new BehaviorSubject(null);
1607
1737
  const transport = new HttpTransport({ baseUrl: url, token$ });
1608
1738
  const content = new HttpContentTransport(transport);
1609
- const client = new SemiontClient(transport, content);
1739
+ const client = new SemiontClient(transport, content, transport);
1610
1740
  let auth;
1611
1741
  try {
1612
1742
  auth = await client.auth.password(opts.email, opts.password);
@@ -1641,25 +1771,7 @@ var SemiontSession = class _SemiontSession {
1641
1771
  return session;
1642
1772
  }
1643
1773
  };
1644
-
1645
- // src/session/notify.ts
1646
- var activeOnSessionExpired = null;
1647
- var activeOnPermissionDenied = null;
1648
- function notifySessionExpired(message) {
1649
- activeOnSessionExpired?.(message);
1650
- }
1651
- function notifyPermissionDenied(message) {
1652
- activeOnPermissionDenied?.(message);
1653
- }
1654
- function registerAuthNotifyHandlers(handlers) {
1655
- activeOnSessionExpired = handlers.onSessionExpired;
1656
- activeOnPermissionDenied = handlers.onPermissionDenied;
1657
- return () => {
1658
- activeOnSessionExpired = null;
1659
- activeOnPermissionDenied = null;
1660
- };
1661
- }
1662
- var FrontendSessionSignals = class {
1774
+ var SessionSignals = class {
1663
1775
  sessionExpiredAt$;
1664
1776
  sessionExpiredMessage$;
1665
1777
  permissionDeniedAt$;
@@ -1724,7 +1836,7 @@ var SemiontBrowser = class {
1724
1836
  * non-null when `activeSession$` is non-null, always null when it
1725
1837
  * is. Extracted from the session itself so headless sessions
1726
1838
  * (workers, CLIs, tests) don't carry dead modal observables.
1727
- * See [FrontendSessionSignals](./frontend-session-signals.ts).
1839
+ * See [SessionSignals](./session-signals.ts).
1728
1840
  */
1729
1841
  activeSignals$;
1730
1842
  /**
@@ -1740,6 +1852,7 @@ var SemiontBrowser = class {
1740
1852
  error$;
1741
1853
  identityToken$;
1742
1854
  storage;
1855
+ sessionFactory;
1743
1856
  /**
1744
1857
  * App-scoped EventBus. Hosts UI-shell events that must work regardless
1745
1858
  * of whether a KB session is active: panel toggles, sidebar state,
@@ -1748,20 +1861,12 @@ var SemiontBrowser = class {
1748
1861
  * (mark:*, beckon:*, gather:*, match:*, bind:*, yield:*, browse:click).
1749
1862
  */
1750
1863
  eventBus = new EventBus();
1751
- unregisterNotify = null;
1752
1864
  unsubscribeStorage = null;
1753
1865
  disposed = false;
1754
1866
  activating = null;
1755
- /**
1756
- * Per-KB in-flight refresh dedup. Simultaneous 401s for the same
1757
- * KB converge on a single `/api/tokens/refresh` network call.
1758
- * Was previously module-scoped in `refresh.ts`; moved here when
1759
- * that file was deleted — SemiontBrowser is a singleton so the
1760
- * scoping is equivalent.
1761
- */
1762
- inFlightRefreshes = /* @__PURE__ */ new Map();
1763
1867
  constructor(config) {
1764
1868
  this.storage = config.storage;
1869
+ this.sessionFactory = config.sessionFactory;
1765
1870
  const kbs = loadKnowledgeBases(this.storage);
1766
1871
  const storedActive = this.storage.get(ACTIVE_KEY);
1767
1872
  const initialActive = storedActive && kbs.some((kb) => kb.id === storedActive) ? storedActive : kbs[0]?.id ?? null;
@@ -1788,14 +1893,6 @@ var SemiontBrowser = class {
1788
1893
  } catch {
1789
1894
  }
1790
1895
  }) ?? null;
1791
- this.unregisterNotify = registerAuthNotifyHandlers({
1792
- onSessionExpired: (message) => {
1793
- this.activeSignals$.getValue()?.notifySessionExpired(message ?? null);
1794
- },
1795
- onPermissionDenied: (message) => {
1796
- this.activeSignals$.getValue()?.notifyPermissionDenied(message ?? null);
1797
- }
1798
- });
1799
1896
  if (initialActive) {
1800
1897
  void this.setActiveKb(initialActive);
1801
1898
  }
@@ -1817,9 +1914,10 @@ var SemiontBrowser = class {
1817
1914
  }
1818
1915
  // ── Identity token (NextAuth bridge; D1) ──────────────────────────────
1819
1916
  /**
1820
- * Set the app-level identity token (from NextAuth's useSession).
1821
- * Called at the root layout via a single `useEffect`. No other site
1822
- * in the codebase should call this.
1917
+ * Set the app-level identity token. Sourced from whatever the host
1918
+ * environment uses for OAuth sessions (e.g. NextAuth in a browser app).
1919
+ * Should be called once from the host's startup-and-on-change site;
1920
+ * no other code should write to this slot.
1823
1921
  */
1824
1922
  setIdentityToken(token) {
1825
1923
  if (this.disposed) return;
@@ -1841,6 +1939,12 @@ var SemiontBrowser = class {
1841
1939
  void this.setActiveKb(next[0]?.id ?? null);
1842
1940
  }
1843
1941
  }
1942
+ /**
1943
+ * Patch a KB in the list. Restricted to the common, endpoint-agnostic
1944
+ * fields (`label`, `email`, `gitBranch`) — the `endpoint` shape isn't
1945
+ * editable in place; remove and re-add to change the connection
1946
+ * target.
1947
+ */
1844
1948
  updateKb(id, updates) {
1845
1949
  this.kbs$.next(
1846
1950
  this.kbs$.getValue().map((kb) => kb.id === id ? { ...kb, ...updates } : kb)
@@ -1896,25 +2000,32 @@ var SemiontBrowser = class {
1896
2000
  if (!id) return;
1897
2001
  const kb = this.kbs$.getValue().find((k) => k.id === id);
1898
2002
  if (!kb) return;
1899
- const signals = new FrontendSessionSignals();
1900
- const token$ = new BehaviorSubject(null);
2003
+ const signals = new SessionSignals();
1901
2004
  let session;
1902
- const transport = new HttpTransport({
1903
- baseUrl: baseUrl(kbBackendUrl(kb)),
1904
- token$,
1905
- tokenRefresher: () => session.refresh().then((t) => t ?? null)
1906
- });
1907
- const content = new HttpContentTransport(transport);
1908
- const client = new SemiontClient(transport, content);
1909
- session = new SemiontSession({
1910
- kb,
1911
- storage: this.storage,
1912
- client,
1913
- token$,
1914
- refresh: () => this.performRefresh(kb),
1915
- validate: (token) => this.performValidate(kb, token),
1916
- onAuthFailed: (msg) => signals.notifySessionExpired(msg),
1917
- onError: (err) => this.error$.next(err)
2005
+ try {
2006
+ session = this.sessionFactory({
2007
+ kb,
2008
+ storage: this.storage,
2009
+ signals,
2010
+ onError: (err) => this.error$.next(err)
2011
+ });
2012
+ } catch (err) {
2013
+ this.error$.next(
2014
+ err instanceof SemiontSessionError ? err : new SemiontSessionError(
2015
+ "session.construct-failed",
2016
+ err instanceof Error ? err.message : String(err),
2017
+ id
2018
+ )
2019
+ );
2020
+ signals.dispose();
2021
+ return;
2022
+ }
2023
+ session.errors$.subscribe((err) => {
2024
+ if (err.code === "unauthorized") {
2025
+ signals.notifySessionExpired(err.message);
2026
+ } else if (err.code === "forbidden") {
2027
+ signals.notifyPermissionDenied(err.message);
2028
+ }
1918
2029
  });
1919
2030
  try {
1920
2031
  await session.ready;
@@ -2030,80 +2141,10 @@ var SemiontBrowser = class {
2030
2141
  if (moved) list.splice(newIndex, 0, moved);
2031
2142
  this.openResources$.next(list);
2032
2143
  }
2033
- // ── Auth callbacks bound per session ──────────────────────────────────
2034
- //
2035
- // These closures back the `refresh` and `validate` callbacks passed
2036
- // to `SemiontSession` in `setActiveKb`. Factored out as methods
2037
- // (rather than inline in the activation closure) so test-doubles
2038
- // can override them cleanly, and so the in-flight dedup map
2039
- // survives across activations of the same KB.
2040
- /**
2041
- * Refresh the active KB's access token. Returns the new token on
2042
- * success, null on failure. Concurrent calls for the same KB
2043
- * dedupe through `inFlightRefreshes`, so simultaneous 401s trigger
2044
- * only one `/api/tokens/refresh` round trip.
2045
- *
2046
- * Uses a throwaway `SemiontClient` with no `tokenRefresher` —
2047
- * a refresh call returning 401 would otherwise re-enter this
2048
- * function infinitely.
2049
- */
2050
- async performRefresh(kb) {
2051
- const existing = this.inFlightRefreshes.get(kb.id);
2052
- if (existing) return existing;
2053
- const promise = (async () => {
2054
- const stored = getStoredSession(this.storage, kb.id);
2055
- if (!stored) return null;
2056
- const throwawayTransport = new HttpTransport({ baseUrl: baseUrl(kbBackendUrl(kb)) });
2057
- const throwaway = new SemiontClient(throwawayTransport, new HttpContentTransport(throwawayTransport));
2058
- try {
2059
- const response = await throwaway.auth.refresh(stored.refresh);
2060
- const newAccess = response.access_token;
2061
- if (!newAccess) return null;
2062
- setStoredSession(this.storage, kb.id, { access: newAccess, refresh: stored.refresh });
2063
- return newAccess;
2064
- } catch {
2065
- return null;
2066
- } finally {
2067
- throwaway.dispose();
2068
- }
2069
- })();
2070
- this.inFlightRefreshes.set(kb.id, promise);
2071
- try {
2072
- return await promise;
2073
- } finally {
2074
- this.inFlightRefreshes.delete(kb.id);
2075
- }
2076
- }
2077
- /**
2078
- * Validate an access token by calling `auth.me` on a throwaway
2079
- * client. The session uses this once at startup to populate
2080
- * `user$`; 401 triggers a refresh-then-retry inside the session.
2081
- *
2082
- * The throwaway transport is seeded with the specific token to
2083
- * validate so the request actually carries it (HttpTransport
2084
- * sources `Authorization` from its `token$`).
2085
- */
2086
- async performValidate(kb, token) {
2087
- const tokenSubject = new BehaviorSubject(token);
2088
- const throwawayTransport = new HttpTransport({
2089
- baseUrl: baseUrl(kbBackendUrl(kb)),
2090
- token$: tokenSubject
2091
- });
2092
- const throwaway = new SemiontClient(throwawayTransport, new HttpContentTransport(throwawayTransport));
2093
- try {
2094
- const data = await throwaway.auth.me();
2095
- return data;
2096
- } finally {
2097
- throwaway.dispose();
2098
- tokenSubject.complete();
2099
- }
2100
- }
2101
2144
  // ── Lifecycle ─────────────────────────────────────────────────────────
2102
2145
  async dispose() {
2103
2146
  if (this.disposed) return;
2104
2147
  this.disposed = true;
2105
- this.unregisterNotify?.();
2106
- this.unregisterNotify = null;
2107
2148
  if (this.unsubscribeStorage) {
2108
2149
  this.unsubscribeStorage();
2109
2150
  this.unsubscribeStorage = null;
@@ -2124,12 +2165,91 @@ var SemiontBrowser = class {
2124
2165
  this.eventBus.destroy();
2125
2166
  }
2126
2167
  };
2168
+ function createHttpSessionFactory() {
2169
+ const inFlightRefreshes = /* @__PURE__ */ new Map();
2170
+ return (opts) => {
2171
+ const { kb, storage, signals, onError } = opts;
2172
+ if (kb.endpoint.kind !== "http") {
2173
+ throw new SemiontSessionError(
2174
+ "session.construct-failed",
2175
+ `HTTP session factory cannot construct a session for endpoint kind "${kb.endpoint.kind}"`,
2176
+ kb.id
2177
+ );
2178
+ }
2179
+ const endpoint = kb.endpoint;
2180
+ const performRefresh = async () => {
2181
+ const existing = inFlightRefreshes.get(kb.id);
2182
+ if (existing) return existing;
2183
+ const promise = (async () => {
2184
+ const stored = getStoredSession(storage, kb.id);
2185
+ if (!stored) return null;
2186
+ const throwawayTransport = new HttpTransport({ baseUrl: baseUrl(kbBackendUrl(endpoint)) });
2187
+ const throwaway = new SemiontClient(throwawayTransport, new HttpContentTransport(throwawayTransport), throwawayTransport);
2188
+ try {
2189
+ const response = await throwaway.auth.refresh(stored.refresh);
2190
+ const newAccess = response.access_token;
2191
+ if (!newAccess) return null;
2192
+ setStoredSession(storage, kb.id, { access: newAccess, refresh: stored.refresh });
2193
+ return newAccess;
2194
+ } catch {
2195
+ return null;
2196
+ } finally {
2197
+ throwaway.dispose();
2198
+ }
2199
+ })();
2200
+ inFlightRefreshes.set(kb.id, promise);
2201
+ try {
2202
+ return await promise;
2203
+ } finally {
2204
+ inFlightRefreshes.delete(kb.id);
2205
+ }
2206
+ };
2207
+ const performValidate = async (token) => {
2208
+ const tokenSubject = new BehaviorSubject(token);
2209
+ const throwawayTransport = new HttpTransport({
2210
+ baseUrl: baseUrl(kbBackendUrl(endpoint)),
2211
+ token$: tokenSubject
2212
+ });
2213
+ const throwaway = new SemiontClient(throwawayTransport, new HttpContentTransport(throwawayTransport), throwawayTransport);
2214
+ try {
2215
+ const data = await throwaway.auth.me();
2216
+ return data;
2217
+ } finally {
2218
+ throwaway.dispose();
2219
+ tokenSubject.complete();
2220
+ }
2221
+ };
2222
+ const token$ = new BehaviorSubject(null);
2223
+ let session;
2224
+ const transport = new HttpTransport({
2225
+ baseUrl: baseUrl(kbBackendUrl(endpoint)),
2226
+ token$,
2227
+ tokenRefresher: () => session.refresh().then((t) => t ?? null)
2228
+ });
2229
+ const content = new HttpContentTransport(transport);
2230
+ const client = new SemiontClient(transport, content, transport);
2231
+ session = new SemiontSession({
2232
+ kb,
2233
+ storage,
2234
+ client,
2235
+ token$,
2236
+ refresh: performRefresh,
2237
+ validate: performValidate,
2238
+ onAuthFailed: (msg) => signals.notifySessionExpired(msg),
2239
+ onError
2240
+ });
2241
+ return session;
2242
+ };
2243
+ }
2127
2244
 
2128
2245
  // src/session/registry.ts
2129
2246
  var instance = null;
2130
2247
  function getBrowser(options) {
2131
2248
  if (!instance) {
2132
- instance = new SemiontBrowser({ storage: options.storage });
2249
+ instance = new SemiontBrowser({
2250
+ storage: options.storage,
2251
+ sessionFactory: options.sessionFactory
2252
+ });
2133
2253
  }
2134
2254
  return instance;
2135
2255
  }
@@ -2147,6 +2267,18 @@ var InMemorySessionStorage = class {
2147
2267
  this.map.delete(key);
2148
2268
  }
2149
2269
  };
2270
+
2271
+ // src/session/knowledge-base.ts
2272
+ function httpKb(opts) {
2273
+ const { id, label, email, host, port, protocol, gitBranch } = opts;
2274
+ return {
2275
+ id,
2276
+ label,
2277
+ email,
2278
+ ...gitBranch !== void 0 ? { gitBranch } : {},
2279
+ endpoint: { kind: "http", host, port, protocol }
2280
+ };
2281
+ }
2150
2282
  function createDisposer() {
2151
2283
  const sub = new Subscription();
2152
2284
  return {
@@ -2162,7 +2294,7 @@ function createSearchPipeline(fetch, options = {}) {
2162
2294
  const state$ = input$.pipe(
2163
2295
  startWith(initial),
2164
2296
  debounceTime(debounceMs),
2165
- distinctUntilChanged$1(),
2297
+ distinctUntilChanged(),
2166
2298
  switchMap((q) => {
2167
2299
  const trimmed = q.trim();
2168
2300
  if (!trimmed) {
@@ -2184,7 +2316,7 @@ function createSearchPipeline(fetch, options = {}) {
2184
2316
  dispose: () => input$.complete()
2185
2317
  };
2186
2318
  }
2187
- function createBeckonVM(client) {
2319
+ function createBeckonStateUnit(client) {
2188
2320
  const subs = [];
2189
2321
  const hovered$ = new BehaviorSubject(null);
2190
2322
  subs.push(client.bus.get("beckon:hover").subscribe(({ annotationId: annotationId2 }) => {
@@ -2235,59 +2367,7 @@ function createHoverHandlers(emit, delayMs) {
2235
2367
  };
2236
2368
  return { handleMouseEnter, handleMouseLeave, cleanup: cancelTimer };
2237
2369
  }
2238
- var COMMON_PANELS = ["knowledge-base", "user", "settings"];
2239
- var RESOURCE_PANELS = ["history", "info", "annotations", "collaboration", "jsonld"];
2240
- var MOTIVATION_TO_TAB = {
2241
- "linking": "reference",
2242
- "commenting": "comment",
2243
- "tagging": "tag",
2244
- "highlighting": "highlight",
2245
- "assessing": "assessment"
2246
- };
2247
- var tabGenerationCounter = 0;
2248
- function createShellVM(browser, options) {
2249
- const subs = [];
2250
- const activePanel$ = new BehaviorSubject(options?.initialPanel ?? null);
2251
- const scrollToAnnotationId$ = new BehaviorSubject(null);
2252
- const panelInitialTab$ = new BehaviorSubject(null);
2253
- if (options?.onPanelChange) {
2254
- const cb = options.onPanelChange;
2255
- subs.push(activePanel$.subscribe(cb));
2256
- }
2257
- subs.push(browser.stream("panel:toggle").subscribe(({ panel }) => {
2258
- const current = activePanel$.getValue();
2259
- activePanel$.next(current === panel ? null : panel);
2260
- }));
2261
- subs.push(browser.stream("panel:open").subscribe(({ panel, scrollToAnnotationId, motivation }) => {
2262
- if (scrollToAnnotationId) {
2263
- scrollToAnnotationId$.next(scrollToAnnotationId);
2264
- }
2265
- if (motivation) {
2266
- const tab = MOTIVATION_TO_TAB[motivation] || "highlight";
2267
- panelInitialTab$.next({ tab, generation: ++tabGenerationCounter });
2268
- }
2269
- activePanel$.next(panel);
2270
- }));
2271
- subs.push(browser.stream("panel:close").subscribe(() => {
2272
- activePanel$.next(null);
2273
- }));
2274
- return {
2275
- activePanel$: activePanel$.asObservable(),
2276
- scrollToAnnotationId$: scrollToAnnotationId$.asObservable(),
2277
- panelInitialTab$: panelInitialTab$.asObservable(),
2278
- openPanel: (panel) => browser.emit("panel:open", { panel }),
2279
- closePanel: () => browser.emit("panel:close", void 0),
2280
- togglePanel: (panel) => browser.emit("panel:toggle", { panel }),
2281
- onScrollCompleted: () => scrollToAnnotationId$.next(null),
2282
- dispose() {
2283
- subs.forEach((s) => s.unsubscribe());
2284
- activePanel$.complete();
2285
- scrollToAnnotationId$.complete();
2286
- panelInitialTab$.complete();
2287
- }
2288
- };
2289
- }
2290
- function createGatherVM(client, resourceId2) {
2370
+ function createGatherStateUnit(client, resourceId2) {
2291
2371
  const subs = [];
2292
2372
  const context$ = new BehaviorSubject(null);
2293
2373
  const loading$ = new BehaviorSubject(false);
@@ -2299,8 +2379,8 @@ function createGatherVM(client, resourceId2) {
2299
2379
  context$.next(null);
2300
2380
  annotationId$.next(annotationId(event.annotationId));
2301
2381
  const gatherSub = client.gather.annotation(
2302
- annotationId(event.annotationId),
2303
2382
  resourceId2,
2383
+ annotationId(event.annotationId),
2304
2384
  { contextWindow: event.options?.contextWindow ?? 2e3 }
2305
2385
  ).pipe(
2306
2386
  timeout(6e4)
@@ -2337,7 +2417,7 @@ function createGatherVM(client, resourceId2) {
2337
2417
  }
2338
2418
  };
2339
2419
  }
2340
- function createMatchVM(client, _resourceId) {
2420
+ function createMatchStateUnit(client, _resourceId) {
2341
2421
  const subs = [];
2342
2422
  subs.push(client.bus.get("match:search-requested").subscribe((event) => {
2343
2423
  const searchSub = client.match.search(
@@ -2363,7 +2443,7 @@ function createMatchVM(client, _resourceId) {
2363
2443
  }
2364
2444
  };
2365
2445
  }
2366
- function createYieldVM(client, resourceId2, locale) {
2446
+ function createYieldStateUnit(client, resourceId2, locale) {
2367
2447
  const subs = [];
2368
2448
  const isGenerating$ = new BehaviorSubject(false);
2369
2449
  const progress$ = new BehaviorSubject(null);
@@ -2418,7 +2498,7 @@ function selectionToSelector(selection) {
2418
2498
  }
2419
2499
  return { type: "TextQuoteSelector", exact: selection.exact, ...selection.prefix && { prefix: selection.prefix }, ...selection.suffix && { suffix: selection.suffix } };
2420
2500
  }
2421
- function createMarkVM(client, resourceId2) {
2501
+ function createMarkStateUnit(client, resourceId2) {
2422
2502
  const subs = [];
2423
2503
  const pendingAnnotation$ = new BehaviorSubject(null);
2424
2504
  const assistingMotivation$ = new BehaviorSubject(null);
@@ -2442,7 +2522,7 @@ function createMarkVM(client, resourceId2) {
2442
2522
  subs.push(client.bus.get("mark:create-ok").subscribe(() => pendingAnnotation$.next(null)));
2443
2523
  subs.push(client.bus.get("mark:submit").subscribe(async (event) => {
2444
2524
  try {
2445
- const result = await client.mark.annotation(resourceId2, {
2525
+ const result = await client.mark.annotation({
2446
2526
  motivation: event.motivation,
2447
2527
  target: { source: resourceId2, selector: event.selector },
2448
2528
  body: event.body
@@ -2503,720 +2583,7 @@ function createMarkVM(client, resourceId2) {
2503
2583
  }
2504
2584
  };
2505
2585
  }
2506
- var RECENT_LIMIT = 10;
2507
- var SEARCH_LIMIT = 20;
2508
- function createDiscoverVM(client, browse) {
2509
- const disposer = createDisposer();
2510
- const search = createSearchPipeline(
2511
- (q) => client.browse.resources({ search: q, limit: SEARCH_LIMIT })
2512
- );
2513
- disposer.add(search);
2514
- disposer.add(browse);
2515
- const recent$ = client.browse.resources({ limit: RECENT_LIMIT, archived: false });
2516
- const recentResources$ = recent$.pipe(
2517
- map$1((r) => r ?? [])
2518
- );
2519
- const isLoadingRecent$ = recent$.pipe(
2520
- map$1((r) => r === void 0)
2521
- );
2522
- const entityTypes$ = client.browse.entityTypes().pipe(
2523
- map$1((e) => e ?? [])
2524
- );
2525
- return {
2526
- browse,
2527
- search,
2528
- recentResources$,
2529
- entityTypes$,
2530
- isLoadingRecent$,
2531
- dispose: () => disposer.dispose()
2532
- };
2533
- }
2534
- function createEntityTagsVM(client, browse) {
2535
- const disposer = createDisposer();
2536
- disposer.add(browse);
2537
- const newTag$ = new BehaviorSubject("");
2538
- const error$ = new BehaviorSubject("");
2539
- const isAdding$ = new BehaviorSubject(false);
2540
- const raw$ = client.browse.entityTypes();
2541
- const entityTypes$ = raw$.pipe(map$1((e) => e ?? []));
2542
- const isLoading$ = raw$.pipe(map$1((e) => e === void 0));
2543
- const addTag = async () => {
2544
- const tag = newTag$.getValue().trim();
2545
- if (!tag) return;
2546
- error$.next("");
2547
- isAdding$.next(true);
2548
- try {
2549
- await client.mark.entityType(tag);
2550
- newTag$.next("");
2551
- } catch (err) {
2552
- error$.next(err instanceof Error ? err.message : "Failed to add entity type");
2553
- } finally {
2554
- isAdding$.next(false);
2555
- }
2556
- };
2557
- return {
2558
- browse,
2559
- entityTypes$,
2560
- isLoading$,
2561
- newTag$: newTag$.asObservable(),
2562
- error$: error$.asObservable(),
2563
- isAdding$: isAdding$.asObservable(),
2564
- setNewTag: (v) => newTag$.next(v),
2565
- addTag,
2566
- dispose: () => {
2567
- newTag$.complete();
2568
- error$.complete();
2569
- isAdding$.complete();
2570
- disposer.dispose();
2571
- }
2572
- };
2573
- }
2574
- function createExchangeVM(browse, exportFn, importFn) {
2575
- const disposer = createDisposer();
2576
- disposer.add(browse);
2577
- const selectedFile$ = new BehaviorSubject(null);
2578
- const preview$ = new BehaviorSubject(null);
2579
- const importPhase$ = new BehaviorSubject(null);
2580
- const importMessage$ = new BehaviorSubject(void 0);
2581
- const importResult$ = new BehaviorSubject(void 0);
2582
- const isExporting$ = new BehaviorSubject(false);
2583
- const isImporting$ = new BehaviorSubject(false);
2584
- const selectFile = (file) => {
2585
- selectedFile$.next(file);
2586
- importPhase$.next(null);
2587
- importMessage$.next(void 0);
2588
- importResult$.next(void 0);
2589
- preview$.next({
2590
- format: file.name.endsWith(".tar.gz") || file.name.endsWith(".gz") ? "semiont-linked-data" : "unknown",
2591
- version: 1,
2592
- sourceUrl: "",
2593
- stats: {}
2594
- });
2595
- };
2596
- const cancelImport = () => {
2597
- selectedFile$.next(null);
2598
- preview$.next(null);
2599
- importPhase$.next(null);
2600
- importMessage$.next(void 0);
2601
- importResult$.next(void 0);
2602
- };
2603
- const doExport = async () => {
2604
- isExporting$.next(true);
2605
- try {
2606
- const response = await exportFn();
2607
- if (!response.ok) throw new Error(`Export failed: ${response.status} ${response.statusText}`);
2608
- const blob = await response.blob();
2609
- const contentDisposition = response.headers.get("Content-Disposition");
2610
- const filename = contentDisposition?.match(/filename="(.+?)"/)?.[1] ?? `semiont-export-${Date.now()}.tar.gz`;
2611
- return { blob, filename };
2612
- } finally {
2613
- isExporting$.next(false);
2614
- }
2615
- };
2616
- const doImport = async () => {
2617
- const file = selectedFile$.getValue();
2618
- if (!file) return;
2619
- isImporting$.next(true);
2620
- importPhase$.next("started");
2621
- importMessage$.next(void 0);
2622
- importResult$.next(void 0);
2623
- try {
2624
- await importFn(file, {
2625
- onProgress: (event) => {
2626
- importPhase$.next(event.phase);
2627
- importMessage$.next(event.message);
2628
- if (event.result) importResult$.next(event.result);
2629
- }
2630
- });
2631
- } finally {
2632
- isImporting$.next(false);
2633
- }
2634
- };
2635
- return {
2636
- browse,
2637
- selectedFile$: selectedFile$.asObservable(),
2638
- preview$: preview$.asObservable(),
2639
- importPhase$: importPhase$.asObservable(),
2640
- importMessage$: importMessage$.asObservable(),
2641
- importResult$: importResult$.asObservable(),
2642
- isExporting$: isExporting$.asObservable(),
2643
- isImporting$: isImporting$.asObservable(),
2644
- selectFile,
2645
- cancelImport,
2646
- doExport,
2647
- doImport,
2648
- dispose: () => {
2649
- selectedFile$.complete();
2650
- preview$.complete();
2651
- importPhase$.complete();
2652
- importMessage$.complete();
2653
- importResult$.complete();
2654
- isExporting$.complete();
2655
- isImporting$.complete();
2656
- disposer.dispose();
2657
- }
2658
- };
2659
- }
2660
- function createAdminUsersVM(client, browse) {
2661
- const disposer = createDisposer();
2662
- disposer.add(browse);
2663
- const users$ = new BehaviorSubject([]);
2664
- const stats$ = new BehaviorSubject(null);
2665
- const usersLoading$ = new BehaviorSubject(true);
2666
- const statsLoading$ = new BehaviorSubject(true);
2667
- const fetchUsers = () => {
2668
- usersLoading$.next(true);
2669
- client.admin.users().then((data) => {
2670
- users$.next(data.users ?? []);
2671
- usersLoading$.next(false);
2672
- }).catch(() => usersLoading$.next(false));
2673
- };
2674
- const fetchStats = () => {
2675
- statsLoading$.next(true);
2676
- client.admin.userStats().then((data) => {
2677
- stats$.next(data.stats ?? null);
2678
- statsLoading$.next(false);
2679
- }).catch(() => statsLoading$.next(false));
2680
- };
2681
- fetchUsers();
2682
- fetchStats();
2683
- const updateUser = async (id, data) => {
2684
- await client.admin.updateUser(userDID(id), data);
2685
- fetchUsers();
2686
- fetchStats();
2687
- };
2688
- return {
2689
- browse,
2690
- users$: users$.asObservable(),
2691
- stats$: stats$.asObservable(),
2692
- usersLoading$: usersLoading$.asObservable(),
2693
- statsLoading$: statsLoading$.asObservable(),
2694
- updateUser,
2695
- dispose: () => {
2696
- users$.complete();
2697
- stats$.complete();
2698
- usersLoading$.complete();
2699
- statsLoading$.complete();
2700
- disposer.dispose();
2701
- }
2702
- };
2703
- }
2704
- function createAdminSecurityVM(client, browse) {
2705
- const disposer = createDisposer();
2706
- disposer.add(browse);
2707
- const providers$ = new BehaviorSubject([]);
2708
- const allowedDomains$ = new BehaviorSubject([]);
2709
- const isLoading$ = new BehaviorSubject(true);
2710
- client.admin.oauthConfig().then((data) => {
2711
- const config = data;
2712
- providers$.next(config.providers ?? []);
2713
- allowedDomains$.next(config.allowedDomains ?? []);
2714
- isLoading$.next(false);
2715
- }).catch(() => isLoading$.next(false));
2716
- return {
2717
- browse,
2718
- providers$: providers$.asObservable(),
2719
- allowedDomains$: allowedDomains$.asObservable(),
2720
- isLoading$: isLoading$.asObservable(),
2721
- dispose: () => {
2722
- providers$.complete();
2723
- allowedDomains$.complete();
2724
- isLoading$.complete();
2725
- disposer.dispose();
2726
- }
2727
- };
2728
- }
2729
- function createWelcomeVM(client) {
2730
- const disposer = createDisposer();
2731
- const userData$ = new BehaviorSubject(null);
2732
- const isProcessing$ = new BehaviorSubject(false);
2733
- client.auth.me().then((data) => userData$.next(data)).catch(() => {
2734
- });
2735
- const acceptTerms = async () => {
2736
- isProcessing$.next(true);
2737
- try {
2738
- await client.auth.acceptTerms();
2739
- userData$.next({ ...userData$.getValue(), termsAcceptedAt: (/* @__PURE__ */ new Date()).toISOString() });
2740
- } finally {
2741
- isProcessing$.next(false);
2742
- }
2743
- };
2744
- return {
2745
- userData$: userData$.asObservable(),
2746
- isProcessing$: isProcessing$.asObservable(),
2747
- acceptTerms,
2748
- dispose: () => {
2749
- userData$.complete();
2750
- isProcessing$.complete();
2751
- disposer.dispose();
2752
- }
2753
- };
2754
- }
2755
- function createResourceLoaderVM(client, resourceId2) {
2756
- const raw$ = client.browse.resource(resourceId2);
2757
- const resource$ = raw$;
2758
- const isLoading$ = raw$.pipe(map$1((r) => r === void 0));
2759
- return {
2760
- resource$,
2761
- isLoading$,
2762
- invalidate: () => client.browse.invalidateResourceDetail(resourceId2),
2763
- dispose: () => {
2764
- }
2765
- };
2766
- }
2767
- function createSessionVM(client) {
2768
- const isLoggingOut$ = new BehaviorSubject(false);
2769
- const logout = async () => {
2770
- isLoggingOut$.next(true);
2771
- try {
2772
- await client.auth.logout();
2773
- } catch {
2774
- } finally {
2775
- isLoggingOut$.next(false);
2776
- }
2777
- };
2778
- return {
2779
- isLoggingOut$: isLoggingOut$.asObservable(),
2780
- logout,
2781
- dispose: () => {
2782
- isLoggingOut$.complete();
2783
- }
2784
- };
2785
- }
2786
- var SMELTER_CHANNELS = [
2787
- "yield:created",
2788
- "yield:updated",
2789
- "yield:representation-added",
2790
- "mark:archived",
2791
- "mark:added",
2792
- "mark:removed"
2793
- ];
2794
- function createSmelterActorVM(options) {
2795
- const actor = createActorVM({
2796
- baseUrl: options.baseUrl,
2797
- token: options.token,
2798
- channels: [...SMELTER_CHANNELS],
2799
- reconnectMs: options.reconnectMs
2800
- });
2801
- const events$ = merge(
2802
- ...SMELTER_CHANNELS.map(
2803
- (channel) => actor.on$(channel).pipe(
2804
- map((payload) => ({
2805
- type: channel,
2806
- resourceId: payload.resourceId,
2807
- payload
2808
- }))
2809
- )
2810
- )
2811
- );
2812
- return {
2813
- events$,
2814
- state$: actor.state$,
2815
- emit: (channel, payload) => actor.emit(channel, payload),
2816
- start: () => actor.start(),
2817
- stop: () => actor.stop(),
2818
- dispose: () => actor.dispose()
2819
- };
2820
- }
2821
- function createJobClaimAdapter(options) {
2822
- const { actor, jobTypes } = options;
2823
- const activeJob$ = new BehaviorSubject(null);
2824
- const isProcessing$ = new BehaviorSubject(false);
2825
- const jobsCompleted$ = new BehaviorSubject(0);
2826
- const errors$ = new Subject();
2827
- let jobSubscription = null;
2828
- let started = false;
2829
- const claimJob = async (assignment) => {
2830
- try {
2831
- const correlationId = crypto.randomUUID();
2832
- const result$ = merge(
2833
- actor.on$("job:claimed").pipe(
2834
- filter$1((e) => e.correlationId === correlationId),
2835
- map$1((e) => ({ ok: true, response: e.response }))
2836
- ),
2837
- actor.on$("job:claim-failed").pipe(
2838
- filter$1((e) => e.correlationId === correlationId),
2839
- map$1(() => ({ ok: false }))
2840
- )
2841
- ).pipe(take$1(1), timeout$1(1e4));
2842
- const resultPromise = firstValueFrom(result$);
2843
- await actor.emit("job:claim", { correlationId, jobId: assignment.jobId });
2844
- const result = await resultPromise;
2845
- if (!result.ok) return null;
2846
- const job = result.response;
2847
- return {
2848
- jobId: assignment.jobId,
2849
- type: assignment.type,
2850
- resourceId: assignment.resourceId,
2851
- userId: job.metadata?.userId ?? "",
2852
- params: job.params ?? {}
2853
- };
2854
- } catch {
2855
- return null;
2856
- }
2857
- };
2858
- return {
2859
- activeJob$: activeJob$.asObservable(),
2860
- isProcessing$: isProcessing$.asObservable(),
2861
- jobsCompleted$: jobsCompleted$.asObservable(),
2862
- errors$: errors$.asObservable(),
2863
- start: () => {
2864
- if (started) return;
2865
- started = true;
2866
- actor.addChannels(["job:queued"]);
2867
- jobSubscription = actor.on$("job:queued").subscribe((event) => {
2868
- const jobType = event.jobType;
2869
- if (jobTypes.length > 0 && !jobTypes.includes(jobType)) return;
2870
- if (isProcessing$.getValue()) return;
2871
- isProcessing$.next(true);
2872
- claimJob({ jobId: event.jobId, type: jobType, resourceId: event.resourceId }).then((job) => {
2873
- if (job) {
2874
- activeJob$.next(job);
2875
- } else {
2876
- isProcessing$.next(false);
2877
- }
2878
- }).catch(() => {
2879
- isProcessing$.next(false);
2880
- });
2881
- });
2882
- },
2883
- stop: () => {
2884
- jobSubscription?.unsubscribe();
2885
- jobSubscription = null;
2886
- started = false;
2887
- },
2888
- completeJob: () => {
2889
- activeJob$.next(null);
2890
- isProcessing$.next(false);
2891
- jobsCompleted$.next(jobsCompleted$.getValue() + 1);
2892
- },
2893
- failJob: (jid, error) => {
2894
- activeJob$.next(null);
2895
- isProcessing$.next(false);
2896
- errors$.next({ jobId: jid, error });
2897
- },
2898
- dispose: () => {
2899
- jobSubscription?.unsubscribe();
2900
- jobSubscription = null;
2901
- started = false;
2902
- activeJob$.complete();
2903
- isProcessing$.complete();
2904
- jobsCompleted$.complete();
2905
- errors$.complete();
2906
- }
2907
- };
2908
- }
2909
- function createJobQueueVM(client) {
2910
- const jobs$ = new BehaviorSubject([]);
2911
- const jobCreated$ = new Subject();
2912
- const jobCompleted$ = new Subject();
2913
- const jobFailed$ = new Subject();
2914
- const pendingByType$ = jobs$.pipe(
2915
- map$1((all) => {
2916
- const counts = /* @__PURE__ */ new Map();
2917
- for (const j of all) {
2918
- if (j.status === "pending") {
2919
- counts.set(j.type, (counts.get(j.type) ?? 0) + 1);
2920
- }
2921
- }
2922
- return counts;
2923
- })
2924
- );
2925
- const runningJobs$ = jobs$.pipe(
2926
- map$1((all) => all.filter((j) => j.status === "running"))
2927
- );
2928
- const addOrUpdate = (job) => {
2929
- const current = jobs$.getValue();
2930
- const idx = current.findIndex((j) => j.jobId === job.jobId);
2931
- if (idx >= 0) {
2932
- const next = [...current];
2933
- next[idx] = job;
2934
- jobs$.next(next);
2935
- } else {
2936
- jobs$.next([...current, job]);
2937
- }
2938
- };
2939
- const subs = [
2940
- client.bus.get("job:queued").subscribe((event) => {
2941
- const job = {
2942
- jobId: event.jobId,
2943
- type: event.jobType,
2944
- status: "pending",
2945
- resourceId: event.resourceId,
2946
- userId: event.userId,
2947
- created: (/* @__PURE__ */ new Date()).toISOString()
2948
- };
2949
- addOrUpdate(job);
2950
- jobCreated$.next(job);
2951
- }),
2952
- client.bus.get("job:complete").subscribe((event) => {
2953
- if (!event._userId) {
2954
- throw new Error("job:complete missing _userId (gateway injection)");
2955
- }
2956
- const job = {
2957
- jobId: event.jobId,
2958
- type: event.jobType,
2959
- status: "complete",
2960
- resourceId: event.resourceId,
2961
- userId: event._userId,
2962
- created: "",
2963
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
2964
- result: event.result
2965
- };
2966
- addOrUpdate(job);
2967
- jobCompleted$.next(job);
2968
- }),
2969
- client.bus.get("job:fail").subscribe((event) => {
2970
- if (!event._userId) {
2971
- throw new Error("job:fail missing _userId (gateway injection)");
2972
- }
2973
- const job = {
2974
- jobId: event.jobId,
2975
- type: event.jobType,
2976
- status: "failed",
2977
- resourceId: event.resourceId,
2978
- userId: event._userId,
2979
- created: "",
2980
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
2981
- error: event.error
2982
- };
2983
- addOrUpdate(job);
2984
- jobFailed$.next(job);
2985
- })
2986
- ];
2987
- return {
2988
- jobs$: jobs$.asObservable(),
2989
- pendingByType$,
2990
- runningJobs$,
2991
- jobCreated$: jobCreated$.asObservable(),
2992
- jobCompleted$: jobCompleted$.asObservable(),
2993
- jobFailed$: jobFailed$.asObservable(),
2994
- dispose: () => {
2995
- subs.forEach((s) => s.unsubscribe());
2996
- jobs$.complete();
2997
- jobCreated$.complete();
2998
- jobCompleted$.complete();
2999
- jobFailed$.complete();
3000
- }
3001
- };
3002
- }
3003
- var WIZARD_CLOSED = {
3004
- open: false,
3005
- annotationId: null,
3006
- resourceId: null,
3007
- defaultTitle: "",
3008
- entityTypes: []
3009
- };
3010
- function createResourceViewerPageVM(client, resourceId2, locale, browse, options) {
3011
- const disposer = createDisposer();
3012
- const beckon = createBeckonVM(client);
3013
- const mark = createMarkVM(client, resourceId2);
3014
- const gather = createGatherVM(client, resourceId2);
3015
- const matchVM = createMatchVM(client);
3016
- const yieldVM = createYieldVM(client, resourceId2, locale);
3017
- disposer.add(beckon);
3018
- disposer.add(browse);
3019
- disposer.add(mark);
3020
- disposer.add(gather);
3021
- disposer.add(matchVM);
3022
- disposer.add(yieldVM);
3023
- const annotations$ = client.browse.annotations(resourceId2).pipe(
3024
- map$1((a) => a ?? [])
3025
- );
3026
- const annotationGroups$ = annotations$.pipe(
3027
- map$1((anns) => {
3028
- const groups = { highlights: [], comments: [], assessments: [], references: [], tags: [] };
3029
- for (const ann of anns) {
3030
- if (isHighlight(ann)) groups.highlights.push(ann);
3031
- else if (isComment(ann)) groups.comments.push(ann);
3032
- else if (isAssessment(ann)) groups.assessments.push(ann);
3033
- else if (isReference(ann)) groups.references.push(ann);
3034
- else if (isTag(ann)) groups.tags.push(ann);
3035
- }
3036
- return groups;
3037
- })
3038
- );
3039
- const entityTypes$ = client.browse.entityTypes().pipe(
3040
- map$1((e) => e ?? [])
3041
- );
3042
- const events$ = client.browse.events(resourceId2).pipe(
3043
- map$1((e) => e ?? [])
3044
- );
3045
- const referencedBy$ = client.browse.referencedBy(resourceId2).pipe(
3046
- map$1((r) => r ?? [])
3047
- );
3048
- const content$ = new BehaviorSubject("");
3049
- const contentLoading$ = new BehaviorSubject(false);
3050
- const mediaToken$ = new BehaviorSubject(null);
3051
- const mediaType = options?.mediaType || "text/plain";
3052
- const isBinaryType = mediaType.startsWith("image/") || mediaType === "application/pdf";
3053
- if (!isBinaryType && mediaType) {
3054
- contentLoading$.next(true);
3055
- client.browse.resourceRepresentation(resourceId2, { accept: mediaType }).then(({ data }) => {
3056
- content$.next(decodeWithCharset(data, mediaType));
3057
- contentLoading$.next(false);
3058
- }).catch(() => {
3059
- contentLoading$.next(false);
3060
- });
3061
- }
3062
- if (isBinaryType) {
3063
- client.auth.mediaToken(resourceId2).then(({ token }) => mediaToken$.next(token)).catch(() => {
3064
- });
3065
- }
3066
- const wizard$ = new BehaviorSubject(WIZARD_CLOSED);
3067
- const unsubscribeResource = client.subscribeToResource(resourceId2);
3068
- disposer.add(unsubscribeResource);
3069
- const bindInitiateSub = client.bus.get("bind:initiate").subscribe((event) => {
3070
- wizard$.next({
3071
- open: true,
3072
- annotationId: event.annotationId,
3073
- resourceId: event.resourceId,
3074
- defaultTitle: event.defaultTitle,
3075
- entityTypes: event.entityTypes
3076
- });
3077
- client.bus.get("gather:requested").next({
3078
- correlationId: crypto.randomUUID(),
3079
- annotationId: event.annotationId,
3080
- resourceId: event.resourceId,
3081
- options: { contextWindow: 2e3 }
3082
- });
3083
- });
3084
- disposer.add(() => bindInitiateSub.unsubscribe());
3085
- return {
3086
- beckon,
3087
- browse,
3088
- mark,
3089
- gather,
3090
- yield: yieldVM,
3091
- annotations$,
3092
- annotationGroups$,
3093
- entityTypes$,
3094
- events$,
3095
- referencedBy$,
3096
- content$: content$.asObservable(),
3097
- contentLoading$: contentLoading$.asObservable(),
3098
- mediaToken$: mediaToken$.asObservable(),
3099
- wizard$: wizard$.asObservable(),
3100
- closeWizard: () => wizard$.next(WIZARD_CLOSED),
3101
- dispose: () => {
3102
- wizard$.complete();
3103
- content$.complete();
3104
- contentLoading$.complete();
3105
- mediaToken$.complete();
3106
- disposer.dispose();
3107
- }
3108
- };
3109
- }
3110
- function createComposePageVM(client, browse, params, auth) {
3111
- const disposer = createDisposer();
3112
- disposer.add(browse);
3113
- const isReferenceMode = Boolean(params.annotationUri && params.sourceDocumentId && params.name);
3114
- const isCloneMode = params.mode === "clone" && Boolean(params.token);
3115
- const pageMode = isCloneMode ? "clone" : isReferenceMode ? "reference" : "new";
3116
- const mode$ = new BehaviorSubject(pageMode);
3117
- const loading$ = new BehaviorSubject(true);
3118
- const cloneData$ = new BehaviorSubject(null);
3119
- const referenceData$ = new BehaviorSubject(null);
3120
- const gatheredContext$ = new BehaviorSubject(null);
3121
- const entityTypes$ = client.browse.entityTypes().pipe(
3122
- map$1((e) => e ?? [])
3123
- );
3124
- if (isReferenceMode) {
3125
- const entityTypes = params.entityTypes ? params.entityTypes.split(",") : [];
3126
- referenceData$.next({
3127
- annotationUri: params.annotationUri,
3128
- sourceDocumentId: params.sourceDocumentId,
3129
- name: params.name,
3130
- entityTypes
3131
- });
3132
- if (params.storedContext) {
3133
- try {
3134
- gatheredContext$.next(JSON.parse(params.storedContext));
3135
- } catch {
3136
- }
3137
- }
3138
- loading$.next(false);
3139
- } else if (isCloneMode) {
3140
- void (async () => {
3141
- try {
3142
- const tokenResult = await client.yield.fromToken(params.token);
3143
- if (tokenResult && auth) {
3144
- const rId = resourceId(tokenResult["@id"]);
3145
- const mediaType = getPrimaryMediaType(tokenResult) || "text/plain";
3146
- const { data } = await client.browse.resourceRepresentation(rId, {
3147
- accept: mediaType
3148
- });
3149
- const content = decodeWithCharset(data, mediaType);
3150
- cloneData$.next({ sourceResource: tokenResult, sourceContent: content });
3151
- }
3152
- } catch {
3153
- }
3154
- loading$.next(false);
3155
- })();
3156
- } else {
3157
- loading$.next(false);
3158
- }
3159
- const save = async (saveParams) => {
3160
- if (saveParams.mode === "clone") {
3161
- const response2 = await client.yield.createFromToken({
3162
- token: params.token,
3163
- name: saveParams.name,
3164
- content: saveParams.content,
3165
- archiveOriginal: saveParams.archiveOriginal ?? true
3166
- });
3167
- return response2.resourceId;
3168
- }
3169
- let fileToUpload;
3170
- let mimeType;
3171
- if (saveParams.file) {
3172
- fileToUpload = saveParams.file;
3173
- mimeType = saveParams.format ?? "application/octet-stream";
3174
- } else {
3175
- const blob = new Blob([saveParams.content || ""], { type: saveParams.format ?? "application/octet-stream" });
3176
- const extension = saveParams.format === "text/plain" ? ".txt" : saveParams.format === "text/html" ? ".html" : ".md";
3177
- fileToUpload = new File([blob], saveParams.name + extension, { type: saveParams.format ?? "application/octet-stream" });
3178
- mimeType = saveParams.format ?? "application/octet-stream";
3179
- }
3180
- const format = saveParams.charset && !saveParams.file ? `${mimeType}; charset=${saveParams.charset}` : mimeType;
3181
- const response = await client.yield.resource({
3182
- name: saveParams.name,
3183
- file: fileToUpload,
3184
- format,
3185
- entityTypes: saveParams.entityTypes || [],
3186
- language: saveParams.language,
3187
- creationMethod: "ui",
3188
- storageUri: saveParams.storageUri
3189
- });
3190
- const newResourceId = response.resourceId;
3191
- if (saveParams.mode === "reference" && saveParams.annotationUri && saveParams.sourceDocumentId) {
3192
- await client.bind.body(
3193
- resourceId(saveParams.sourceDocumentId),
3194
- annotationId(saveParams.annotationUri),
3195
- [{ op: "add", item: { type: "SpecificResource", source: newResourceId, purpose: "linking" } }]
3196
- );
3197
- }
3198
- return newResourceId;
3199
- };
3200
- return {
3201
- browse,
3202
- mode$: mode$.asObservable(),
3203
- loading$: loading$.asObservable(),
3204
- cloneData$: cloneData$.asObservable(),
3205
- referenceData$: referenceData$.asObservable(),
3206
- gatheredContext$: gatheredContext$.asObservable(),
3207
- entityTypes$,
3208
- save,
3209
- dispose: () => {
3210
- mode$.complete();
3211
- loading$.complete();
3212
- cloneData$.complete();
3213
- referenceData$.complete();
3214
- gatheredContext$.complete();
3215
- disposer.dispose();
3216
- }
3217
- };
3218
- }
3219
2586
 
3220
- export { AdminNamespace, AuthNamespace, BeckonNamespace, BindNamespace, BrowseNamespace, BusRequestError, COMMON_PANELS, CacheObservable, FrontendSessionSignals, GatherNamespace, HOVER_DELAY_MS, InMemorySessionStorage, JobNamespace, MarkNamespace, MatchNamespace, RESOURCE_PANELS, SemiontBrowser, SemiontClient, SemiontSession, SemiontSessionError, StreamObservable, YieldNamespace, busRequest, createAdminSecurityVM, createAdminUsersVM, createBeckonVM, createCache, createComposePageVM, createDiscoverVM, createDisposer, createEntityTagsVM, createExchangeVM, createGatherVM, createHoverHandlers, createJobClaimAdapter, createJobQueueVM, createMarkVM, createMatchVM, createResourceLoaderVM, createResourceViewerPageVM, createSearchPipeline, createSessionVM, createShellVM, createSmelterActorVM, createWelcomeVM, createYieldVM, defaultProtocol, getBrowser, isValidHostname, kbBackendUrl, notifyPermissionDenied, notifySessionExpired, setStoredSession };
2587
+ export { AdminNamespace, AuthNamespace, BeckonNamespace, BindNamespace, BrowseNamespace, BusRequestError, CacheObservable, FrameNamespace, GatherNamespace, HOVER_DELAY_MS, InMemorySessionStorage, JobNamespace, MarkNamespace, MatchNamespace, SemiontBrowser, SemiontClient, SemiontSession, SemiontSessionError, SessionSignals, StreamObservable, UploadObservable, YieldNamespace, busRequest, createBeckonStateUnit, createDisposer, createGatherStateUnit, createHoverHandlers, createHttpSessionFactory, createMarkStateUnit, createMatchStateUnit, createSearchPipeline, createYieldStateUnit, defaultProtocol, getBrowser, httpKb, isValidHostname, kbBackendUrl, setStoredSession };
3221
2588
  //# sourceMappingURL=index.js.map
3222
2589
  //# sourceMappingURL=index.js.map