@verentis/sdk 0.1.0 → 0.2.2

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/README.md CHANGED
@@ -10,6 +10,53 @@ npm install @verentis/sdk
10
10
 
11
11
  ## Quick Start
12
12
 
13
+ ### Hosted header actions (SDK 0.2)
14
+
15
+ Applications register buttons after `await client.whenReady()`. The workspace owns their
16
+ presentation; handlers execute inside the application with its existing delegated token.
17
+
18
+ ```ts
19
+ const save = client.bridge!.registerAction({
20
+ id: 'document.save',
21
+ label: 'Save',
22
+ icon: 'lucide:save',
23
+ variant: 'default',
24
+ scopes: ['node.file.read', 'node.node.create', 'node.node.update', 'node.journal.create'],
25
+ enabled: false,
26
+ }, async () => {
27
+ await persistDocument()
28
+ })
29
+
30
+ // Pass the complete descriptor when document state changes.
31
+ save.update({ id: 'document.save', label: 'Save', scopes: [
32
+ 'node.file.read', 'node.node.create', 'node.node.update', 'node.journal.create',
33
+ ], enabled: true })
34
+ save.dispose()
35
+ ```
36
+
37
+ Use stable app-local IDs, up to 16 actions, Lucide icon identifiers, and explicit `scopes`
38
+ (`[]` for client-only operations). `enabled`, `visible`, `busy`, and `disabledReason` communicate
39
+ state without serializing handlers. The host checks required scopes against the delegated grant;
40
+ registration never expands permissions. Throw on failure so the host can display it.
41
+
42
+ For clipboard actions, set `kind: 'clipboard'` and return `{ clipboardText: source }`. The host starts
43
+ the clipboard write in the user's click gesture while awaiting the text, preserving browser
44
+ user-activation requirements. For operations that navigate after completion, return
45
+ `{ navigate: { path: '/applications', newTab: false } }` instead of navigating before the result.
46
+
47
+ The protocol uses session-bound `verentis:actions:set`, `verentis:action:invoke`, and
48
+ `verentis:action:result` messages. Reload/navigation disposes pending work; an unanswered invocation
49
+ times out as **outcome unknown**, never an automatic retry. App handlers must not report successful
50
+ completion before their mutation finishes.
51
+
52
+ Migrated actions are hosted only: there is no legacy-host or standalone-toolbar fallback.
53
+ Fullscreen apps can hide the host header only when they register no actions. Keep specialized
54
+ controls (zoom, editing modes, contextual forms) within the app.
55
+
56
+ Release the 0.2 SDK before deploying apps that require `^0.2.0`, together with the matching host.
57
+ Local sibling-repository development can link the built SDK; published applications must resolve
58
+ the released SDK from npm.
59
+
13
60
  ### Standalone mode (local development)
14
61
 
15
62
  ```ts
package/dist/index.cjs CHANGED
@@ -745,6 +745,13 @@ var SettingsModule = class {
745
745
  }
746
746
  };
747
747
 
748
+ // src/transport/header-actions.ts
749
+ function isHeaderAction(value) {
750
+ if (!value || typeof value !== "object") return false;
751
+ const a = value;
752
+ 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");
753
+ }
754
+
748
755
  // src/transport/bridge.transport.ts
749
756
  var Bridge = class {
750
757
  listeners = /* @__PURE__ */ new Map();
@@ -753,6 +760,9 @@ var Bridge = class {
753
760
  _hostOrigin = null;
754
761
  _messageHandler;
755
762
  _destroyed = false;
763
+ sessionId = null;
764
+ actions = /* @__PURE__ */ new Map();
765
+ requests = /* @__PURE__ */ new Set();
756
766
  constructor() {
757
767
  this._initPromise = new Promise((resolve) => {
758
768
  this._initResolve = resolve;
@@ -782,6 +792,7 @@ var Bridge = class {
782
792
  }
783
793
  /** Send a message to the host */
784
794
  postToHost(message) {
795
+ if (this._destroyed) throw new Error("Bridge has been destroyed.");
785
796
  if (!this._hostOrigin) {
786
797
  throw new Error("Bridge not initialized: host origin unknown. Wait for init first.");
787
798
  }
@@ -816,6 +827,69 @@ var Bridge = class {
816
827
  setDirty(isDirty) {
817
828
  this.postToHost({ type: "verentis:dirty", version: 1, isDirty });
818
829
  }
830
+ registerAction(descriptor, handler) {
831
+ if (!this.sessionId || this._destroyed) throw new Error("Header actions require an initialized workspace host.");
832
+ if (!isHeaderAction(descriptor)) throw new Error("Invalid header action.");
833
+ if (this.actions.has(descriptor.id)) throw new Error(`Action '${descriptor.id}' is already registered.`);
834
+ if (this.actions.size >= 16) throw new Error("At most 16 header actions may be registered.");
835
+ const action = { descriptor: { ...descriptor, scopes: [...descriptor.scopes] }, handler, busy: false };
836
+ this.actions.set(descriptor.id, action);
837
+ this.publishActions();
838
+ return {
839
+ update: (next) => {
840
+ if (this.actions.get(descriptor.id) !== action) throw new Error("Action has been disposed.");
841
+ if (!isHeaderAction(next) || next.id !== descriptor.id) throw new Error("Invalid header action update.");
842
+ action.descriptor = { ...next, scopes: [...next.scopes] };
843
+ this.publishActions();
844
+ },
845
+ dispose: () => {
846
+ if (this.actions.get(descriptor.id) !== action) return;
847
+ this.actions.delete(descriptor.id);
848
+ this.publishActions();
849
+ }
850
+ };
851
+ }
852
+ publishActions() {
853
+ if (!this.sessionId || this._destroyed) return;
854
+ this.postToHost({
855
+ type: "verentis:actions:set",
856
+ version: 1,
857
+ sessionId: this.sessionId,
858
+ actions: [...this.actions.values()].map((a) => ({ ...a.descriptor, busy: a.busy || a.descriptor.busy }))
859
+ });
860
+ }
861
+ async invokeAction(message) {
862
+ if (message.sessionId !== this.sessionId || typeof message.requestId !== "string" || message.requestId.length > 128 || !message.requestId || this.requests.has(message.requestId)) return;
863
+ this.requests.add(message.requestId);
864
+ if (this.requests.size > 1e3) this.requests.delete(this.requests.values().next().value);
865
+ const action = this.actions.get(message.actionId);
866
+ const reply = {
867
+ type: "verentis:action:result",
868
+ version: 1,
869
+ sessionId: message.sessionId,
870
+ requestId: message.requestId,
871
+ actionId: message.actionId
872
+ };
873
+ if (!action || action.busy || action.descriptor.busy || action.descriptor.enabled === false || action.descriptor.visible === false) {
874
+ this.postToHost({ ...reply, error: "Action is unavailable." });
875
+ return;
876
+ }
877
+ action.busy = true;
878
+ this.publishActions();
879
+ try {
880
+ const result = await action.handler();
881
+ if (!this._destroyed && this.sessionId === message.sessionId) {
882
+ this.postToHost({ ...reply, result: result || void 0 });
883
+ }
884
+ } catch (error) {
885
+ if (!this._destroyed && this.sessionId === message.sessionId) {
886
+ this.postToHost({ ...reply, error: (error instanceof Error ? error.message : "Action failed.").slice(0, 500) });
887
+ }
888
+ } finally {
889
+ action.busy = false;
890
+ this.publishActions();
891
+ }
892
+ }
819
893
  /** Subscribe to a specific message type from the host */
820
894
  on(type, listener) {
821
895
  if (!this.listeners.has(type)) {
@@ -829,6 +903,8 @@ var Bridge = class {
829
903
  /** Clean up event listeners */
830
904
  destroy() {
831
905
  this._destroyed = true;
906
+ this.actions.clear();
907
+ this.requests.clear();
832
908
  this.listeners.clear();
833
909
  if (typeof window !== "undefined") {
834
910
  window.removeEventListener("message", this._messageHandler);
@@ -837,9 +913,12 @@ var Bridge = class {
837
913
  handleMessage(event) {
838
914
  if (this._destroyed) return;
839
915
  const data = event.data;
840
- if (!data?.type?.startsWith("verentis:")) return;
916
+ if (event.source !== window.parent || data?.version !== 1 || typeof data?.type !== "string" || !data.type.startsWith("verentis:")) return;
917
+ if (this._hostOrigin && event.origin !== this._hostOrigin) return;
841
918
  if (data.type === "verentis:init") {
919
+ if (this._hostOrigin) return;
842
920
  this._hostOrigin = event.origin;
921
+ this.sessionId = typeof data.sessionId === "string" ? data.sessionId : null;
843
922
  console.info("[Verentis SDK] Received verentis:init from host", {
844
923
  origin: event.origin,
845
924
  workspaceId: data.context.workspace.id,
@@ -848,6 +927,10 @@ var Bridge = class {
848
927
  const initData = data;
849
928
  this._initResolve({ context: initData.context, token: initData.token });
850
929
  }
930
+ if (data.type === "verentis:action:invoke") {
931
+ void this.invokeAction(data);
932
+ return;
933
+ }
851
934
  const typeListeners = this.listeners.get(data.type);
852
935
  if (typeListeners) {
853
936
  for (const listener of typeListeners) {
@@ -1116,6 +1199,7 @@ exports.TokenAuth = TokenAuth;
1116
1199
  exports.VerentisClient = VerentisClient;
1117
1200
  exports.createVerentisClient = createVerentisClient;
1118
1201
  exports.findBestMatch = findBestMatch;
1202
+ exports.isHeaderAction = isHeaderAction;
1119
1203
  exports.isTerminalStatus = isTerminalStatus;
1120
1204
  exports.matchMimeType = matchMimeType;
1121
1205
  //# sourceMappingURL=index.cjs.map