@verentis/sdk 0.1.1 → 0.2.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
@@ -28,6 +28,14 @@ var BridgeAuth = class {
28
28
  _refreshTimer = null;
29
29
  _pendingRefresh = null;
30
30
  _messageHandler;
31
+ _requestId = null;
32
+ _refreshReject = null;
33
+ _requestTimer = null;
34
+ _destroyed = false;
35
+ setHostOrigin(origin) {
36
+ if (!origin || origin === "*" || origin === "null") throw new Error("A trusted host origin is required.");
37
+ this.hostOrigin = origin;
38
+ }
31
39
  /** Set the initial token received from the host's `verentis:init` message */
32
40
  setToken(info) {
33
41
  this._token = info.accessToken;
@@ -42,7 +50,9 @@ var BridgeAuth = class {
42
50
  return `Bearer ${this._token}`;
43
51
  }
44
52
  destroy() {
53
+ this._destroyed = true;
45
54
  if (this._refreshTimer) clearTimeout(this._refreshTimer);
55
+ this.failRefresh("Authentication has been destroyed.");
46
56
  if (typeof window !== "undefined") {
47
57
  window.removeEventListener("message", this._messageHandler);
48
58
  }
@@ -52,11 +62,16 @@ var BridgeAuth = class {
52
62
  }
53
63
  _refreshResolve = null;
54
64
  requestRefresh() {
65
+ if (this._destroyed || this.hostOrigin === "*")
66
+ return Promise.reject(new Error("Authentication is not initialized."));
55
67
  if (this._pendingRefresh) return this._pendingRefresh;
56
68
  const requestId = crypto.randomUUID();
57
- this._pendingRefresh = new Promise((resolve) => {
69
+ this._requestId = requestId;
70
+ this._pendingRefresh = new Promise((resolve, reject) => {
58
71
  this._refreshResolve = resolve;
72
+ this._refreshReject = reject;
59
73
  });
74
+ this._requestTimer = setTimeout(() => this.failRefresh("Token refresh timed out."), 2e4);
60
75
  const message = {
61
76
  type: "verentis:token:refresh",
62
77
  version: 1,
@@ -66,21 +81,35 @@ var BridgeAuth = class {
66
81
  return this._pendingRefresh;
67
82
  }
68
83
  handleMessage(event) {
69
- if (event.origin !== this.hostOrigin) return;
84
+ if (this._destroyed || event.origin !== this.hostOrigin || event.source !== window.parent) return;
70
85
  const data = event.data;
71
- if (data?.type !== "verentis:token:refreshed") return;
86
+ if (data?.version !== 1 || data?.type !== "verentis:token:refreshed" || !this._requestId || data.requestId !== this._requestId || typeof data.token?.accessToken !== "string" || !Number.isFinite(Date.parse(data.token.expiresAt)) || Date.parse(data.token.expiresAt) <= Date.now() + SKEW_MS) return;
87
+ if (this._requestTimer) clearTimeout(this._requestTimer);
88
+ this._requestTimer = null;
89
+ this._requestId = null;
72
90
  this._token = data.token.accessToken;
73
91
  this._expiresAt = new Date(data.token.expiresAt).getTime();
74
92
  this._pendingRefresh = null;
75
93
  this._refreshResolve?.();
76
94
  this._refreshResolve = null;
95
+ this._refreshReject = null;
77
96
  this.scheduleRefresh();
78
97
  }
79
98
  scheduleRefresh() {
80
99
  if (this._refreshTimer) clearTimeout(this._refreshTimer);
81
100
  if (this._expiresAt === 0) return;
82
101
  const delay2 = Math.max(0, this._expiresAt - Date.now() - SKEW_MS);
83
- this._refreshTimer = setTimeout(() => void this.requestRefresh(), delay2);
102
+ this._refreshTimer = setTimeout(() => void this.requestRefresh().catch(() => {
103
+ }), delay2);
104
+ }
105
+ failRefresh(message) {
106
+ if (this._requestTimer) clearTimeout(this._requestTimer);
107
+ this._requestTimer = null;
108
+ this._requestId = null;
109
+ this._pendingRefresh = null;
110
+ this._refreshReject?.(new Error(message));
111
+ this._refreshReject = null;
112
+ this._refreshResolve = null;
84
113
  }
85
114
  };
86
115
 
@@ -743,23 +772,41 @@ var SettingsModule = class {
743
772
  }
744
773
  };
745
774
 
775
+ // src/transport/header-actions.ts
776
+ function isHeaderAction(value) {
777
+ if (!value || typeof value !== "object") return false;
778
+ const a = value;
779
+ return typeof a.id === "string" && /^[a-z][a-z0-9.-]{0,63}$/.test(a.id) && typeof a.label === "string" && a.label.trim().length > 0 && a.label.length <= 100 && Array.isArray(a.scopes) && a.scopes.length <= 32 && a.scopes.every((s) => typeof s === "string" && /^[a-zA-Z0-9._:-]{1,128}$/.test(s)) && (a.icon === void 0 || typeof a.icon === "string" && /^lucide:[a-z0-9-]{1,64}$/.test(a.icon)) && (a.variant === void 0 || ["default", "outline", "destructive"].includes(String(a.variant))) && ["enabled", "visible", "busy"].every((key) => a[key] === void 0 || typeof a[key] === "boolean") && (a.disabledReason === void 0 || typeof a.disabledReason === "string" && a.disabledReason.length <= 200) && (a.kind === void 0 || a.kind === "clipboard");
780
+ }
781
+
746
782
  // src/transport/bridge.transport.ts
747
783
  var Bridge = class {
748
- listeners = /* @__PURE__ */ new Map();
749
- _initResolve;
750
- _initPromise;
751
- _hostOrigin = null;
752
- _messageHandler;
753
- _destroyed = false;
754
- constructor() {
755
- this._initPromise = new Promise((resolve) => {
784
+ constructor(expectedHostOrigin) {
785
+ this.expectedHostOrigin = expectedHostOrigin;
786
+ this._initPromise = new Promise((resolve, reject) => {
756
787
  this._initResolve = resolve;
788
+ this._initReject = reject;
789
+ });
790
+ void this._initPromise.catch(() => {
757
791
  });
758
792
  this._messageHandler = this.handleMessage.bind(this);
759
793
  if (typeof window !== "undefined") {
760
794
  window.addEventListener("message", this._messageHandler);
761
795
  }
762
796
  }
797
+ expectedHostOrigin;
798
+ listeners = /* @__PURE__ */ new Map();
799
+ _initResolve;
800
+ _initReject;
801
+ _initTimer;
802
+ _initPromise;
803
+ _hostOrigin = null;
804
+ _messageHandler;
805
+ _destroyed = false;
806
+ sessionId = null;
807
+ actions = /* @__PURE__ */ new Map();
808
+ requests = /* @__PURE__ */ new Set();
809
+ backendRequests = /* @__PURE__ */ new Map();
763
810
  /** Post `verentis:ready` to the host to start the handshake */
764
811
  sendReady(capabilities) {
765
812
  console.info("[Verentis SDK] Sending verentis:ready to host");
@@ -772,6 +819,12 @@ var Bridge = class {
772
819
  }
773
820
  /** Wait for the host to send `verentis:init` with context and token */
774
821
  waitForInit() {
822
+ if (!this._hostOrigin && !this._destroyed && !this._initTimer) {
823
+ this._initTimer = setTimeout(() => {
824
+ this._initReject(new Error("Workspace handshake timed out."));
825
+ this.destroy();
826
+ }, 2e4);
827
+ }
775
828
  return this._initPromise;
776
829
  }
777
830
  /** Get the host origin (available after init) */
@@ -780,6 +833,7 @@ var Bridge = class {
780
833
  }
781
834
  /** Send a message to the host */
782
835
  postToHost(message) {
836
+ if (this._destroyed) throw new Error("Bridge has been destroyed.");
783
837
  if (!this._hostOrigin) {
784
838
  throw new Error("Bridge not initialized: host origin unknown. Wait for init first.");
785
839
  }
@@ -814,6 +868,91 @@ var Bridge = class {
814
868
  setDirty(isDirty) {
815
869
  this.postToHost({ type: "verentis:dirty", version: 1, isDirty });
816
870
  }
871
+ /** The host, not the app, selects the installation, file, branch and permission ceiling. */
872
+ requestBackendCredential() {
873
+ if (this._destroyed || !this.sessionId || !this._hostOrigin)
874
+ return Promise.reject(new Error("An initialized workspace host is required."));
875
+ if (this.backendRequests.size >= 4)
876
+ return Promise.reject(new Error("A backend credential request is already pending."));
877
+ const requestId = crypto.randomUUID();
878
+ return new Promise((resolve, reject) => {
879
+ const timer = setTimeout(() => {
880
+ this.backendRequests.delete(requestId);
881
+ reject(new Error("Backend authorization timed out."));
882
+ }, 2e4);
883
+ this.backendRequests.set(requestId, { resolve, reject, timer });
884
+ try {
885
+ this.postToHost({ type: "verentis:backend:request", version: 1, requestId, sessionId: this.sessionId });
886
+ } catch {
887
+ clearTimeout(timer);
888
+ this.backendRequests.delete(requestId);
889
+ reject(new Error("The workspace host is unavailable."));
890
+ }
891
+ });
892
+ }
893
+ registerAction(descriptor, handler) {
894
+ if (!this.sessionId || this._destroyed) throw new Error("Header actions require an initialized workspace host.");
895
+ if (!isHeaderAction(descriptor)) throw new Error("Invalid header action.");
896
+ if (this.actions.has(descriptor.id)) throw new Error(`Action '${descriptor.id}' is already registered.`);
897
+ if (this.actions.size >= 16) throw new Error("At most 16 header actions may be registered.");
898
+ const action = { descriptor: { ...descriptor, scopes: [...descriptor.scopes] }, handler, busy: false };
899
+ this.actions.set(descriptor.id, action);
900
+ this.publishActions();
901
+ return {
902
+ update: (next) => {
903
+ if (this.actions.get(descriptor.id) !== action) throw new Error("Action has been disposed.");
904
+ if (!isHeaderAction(next) || next.id !== descriptor.id) throw new Error("Invalid header action update.");
905
+ action.descriptor = { ...next, scopes: [...next.scopes] };
906
+ this.publishActions();
907
+ },
908
+ dispose: () => {
909
+ if (this.actions.get(descriptor.id) !== action) return;
910
+ this.actions.delete(descriptor.id);
911
+ this.publishActions();
912
+ }
913
+ };
914
+ }
915
+ publishActions() {
916
+ if (!this.sessionId || this._destroyed) return;
917
+ this.postToHost({
918
+ type: "verentis:actions:set",
919
+ version: 1,
920
+ sessionId: this.sessionId,
921
+ actions: [...this.actions.values()].map((a) => ({ ...a.descriptor, busy: a.busy || a.descriptor.busy }))
922
+ });
923
+ }
924
+ async invokeAction(message) {
925
+ if (message.sessionId !== this.sessionId || typeof message.requestId !== "string" || message.requestId.length > 128 || !message.requestId || this.requests.has(message.requestId)) return;
926
+ this.requests.add(message.requestId);
927
+ if (this.requests.size > 1e3) this.requests.delete(this.requests.values().next().value);
928
+ const action = this.actions.get(message.actionId);
929
+ const reply = {
930
+ type: "verentis:action:result",
931
+ version: 1,
932
+ sessionId: message.sessionId,
933
+ requestId: message.requestId,
934
+ actionId: message.actionId
935
+ };
936
+ if (!action || action.busy || action.descriptor.busy || action.descriptor.enabled === false || action.descriptor.visible === false) {
937
+ this.postToHost({ ...reply, error: "Action is unavailable." });
938
+ return;
939
+ }
940
+ action.busy = true;
941
+ this.publishActions();
942
+ try {
943
+ const result = await action.handler();
944
+ if (!this._destroyed && this.sessionId === message.sessionId) {
945
+ this.postToHost({ ...reply, result: result || void 0 });
946
+ }
947
+ } catch (error) {
948
+ if (!this._destroyed && this.sessionId === message.sessionId) {
949
+ this.postToHost({ ...reply, error: (error instanceof Error ? error.message : "Action failed.").slice(0, 500) });
950
+ }
951
+ } finally {
952
+ action.busy = false;
953
+ this.publishActions();
954
+ }
955
+ }
817
956
  /** Subscribe to a specific message type from the host */
818
957
  on(type, listener) {
819
958
  if (!this.listeners.has(type)) {
@@ -827,7 +966,17 @@ var Bridge = class {
827
966
  /** Clean up event listeners */
828
967
  destroy() {
829
968
  this._destroyed = true;
969
+ clearTimeout(this._initTimer);
970
+ this._initTimer = void 0;
971
+ if (!this._hostOrigin) this._initReject(new Error("Bridge has been destroyed."));
972
+ this.actions.clear();
973
+ this.requests.clear();
830
974
  this.listeners.clear();
975
+ for (const pending of this.backendRequests.values()) {
976
+ clearTimeout(pending.timer);
977
+ pending.reject(new Error("Bridge has been destroyed."));
978
+ }
979
+ this.backendRequests.clear();
831
980
  if (typeof window !== "undefined") {
832
981
  window.removeEventListener("message", this._messageHandler);
833
982
  }
@@ -835,9 +984,16 @@ var Bridge = class {
835
984
  handleMessage(event) {
836
985
  if (this._destroyed) return;
837
986
  const data = event.data;
838
- if (!data?.type?.startsWith("verentis:")) return;
987
+ if (event.source !== window.parent || data?.version !== 1 || typeof data?.type !== "string" || !data.type.startsWith("verentis:")) return;
988
+ if (this._hostOrigin && event.origin !== this._hostOrigin) return;
989
+ if (this.expectedHostOrigin && event.origin !== this.expectedHostOrigin) return;
839
990
  if (data.type === "verentis:init") {
991
+ if (this._hostOrigin) return;
992
+ if (!data.context?.workspace?.id || !data.token || event.origin === "null") return;
993
+ clearTimeout(this._initTimer);
994
+ this._initTimer = void 0;
840
995
  this._hostOrigin = event.origin;
996
+ this.sessionId = typeof data.sessionId === "string" ? data.sessionId : null;
841
997
  console.info("[Verentis SDK] Received verentis:init from host", {
842
998
  origin: event.origin,
843
999
  workspaceId: data.context.workspace.id,
@@ -846,6 +1002,22 @@ var Bridge = class {
846
1002
  const initData = data;
847
1003
  this._initResolve({ context: initData.context, token: initData.token });
848
1004
  }
1005
+ if (data.type === "verentis:backend:response") {
1006
+ if (!this._hostOrigin || data.sessionId !== this.sessionId) return;
1007
+ const pending = this.backendRequests.get(data.requestId);
1008
+ if (!pending) return;
1009
+ clearTimeout(pending.timer);
1010
+ this.backendRequests.delete(data.requestId);
1011
+ const value = data.credential;
1012
+ if (data.error || !value || typeof value.launchCredential !== "string" || value.launchCredential.length > 512 || !value.launchCredential || typeof value.delegationId !== "string" || typeof value.backendClientId !== "string" || !Number.isFinite(Date.parse(value.expiresAt)) || Date.parse(value.expiresAt) <= Date.now())
1013
+ pending.reject(new Error("Backend authorization was denied or is not configured."));
1014
+ else pending.resolve(value);
1015
+ return;
1016
+ }
1017
+ if (data.type === "verentis:action:invoke") {
1018
+ void this.invokeAction(data);
1019
+ return;
1020
+ }
849
1021
  const typeListeners = this.listeners.get(data.type);
850
1022
  if (typeListeners) {
851
1023
  for (const listener of typeListeners) {
@@ -1027,7 +1199,7 @@ var VerentisClient = class {
1027
1199
  this.bridge.sendReady();
1028
1200
  const { context, token } = await this.bridge.waitForInit();
1029
1201
  const hostOrigin = this.bridge.hostOrigin;
1030
- Object.assign(bridgeAuth, new BridgeAuth(hostOrigin));
1202
+ bridgeAuth.setHostOrigin(hostOrigin);
1031
1203
  bridgeAuth.setToken(token);
1032
1204
  this.context.setFromAppContext(context);
1033
1205
  const http = new HttpTransport(
@@ -1099,6 +1271,6 @@ function createVerentisClient(options) {
1099
1271
  return new VerentisClient(options);
1100
1272
  }
1101
1273
 
1102
- export { ApiKeyAuth, Bridge, BridgeAuth, ContextModule, EnginesModule, ExecutionsModule, FilesModule, HttpTransport, InstallationsModule, ResourcesModule, SettingsModule, TokenAuth, VerentisClient, createVerentisClient, findBestMatch, isTerminalStatus, matchMimeType };
1274
+ export { ApiKeyAuth, Bridge, BridgeAuth, ContextModule, EnginesModule, ExecutionsModule, FilesModule, HttpTransport, InstallationsModule, ResourcesModule, SettingsModule, TokenAuth, VerentisClient, createVerentisClient, findBestMatch, isHeaderAction, isTerminalStatus, matchMimeType };
1103
1275
  //# sourceMappingURL=index.js.map
1104
1276
  //# sourceMappingURL=index.js.map