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