@verentis/sdk 0.2.2 → 0.2.4
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 +24 -0
- package/dist/index.cjs +168 -15
- 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 +168 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,6 +57,30 @@ Release the 0.2 SDK before deploying apps that require `^0.2.0`, together with t
|
|
|
57
57
|
Local sibling-repository development can link the built SDK; published applications must resolve
|
|
58
58
|
the released SDK from npm.
|
|
59
59
|
|
|
60
|
+
### WOPI editor launch
|
|
61
|
+
|
|
62
|
+
Signed WOPI applications use `Bridge.requestWopiLaunch()` after `waitForInit()`.
|
|
63
|
+
The authenticated workspace parent chooses the installed editor, document,
|
|
64
|
+
branch and action; the child cannot request arbitrary document identities.
|
|
65
|
+
The returned token is scoped to WOPI and is not a normal workspace API token.
|
|
66
|
+
Submit it using a form POST to the validated discovery action, never a URL.
|
|
67
|
+
|
|
68
|
+
`requestWopiSave(generation)`, `getWopiStatus()` and `closeWopiSession()` use the
|
|
69
|
+
same exact-parent, origin/session/request-correlated bridge. A matching durable
|
|
70
|
+
save receipt—not a CODE postMessage alone—is required before clearing dirty state.
|
|
71
|
+
When status reports `continuationAvailable`, `requestWopiContinuation()` asks
|
|
72
|
+
the parent to bind the target of a completed server-owned Save As operation.
|
|
73
|
+
It accepts no document or acquisition IDs. Its `WopiContinuation` result contains
|
|
74
|
+
only document metadata and the persisted target `accessTokenTtl`, not a token,
|
|
75
|
+
action URL or private control credential. The parent obtains the exact existing
|
|
76
|
+
control IDs from authenticated `/derived`; it does not reconstruct a launch,
|
|
77
|
+
including after signing-key rotation. Keep the initial launch/form separate from
|
|
78
|
+
active document metadata, retain CODE's iframe and existing token, and preserve
|
|
79
|
+
unverified edits until a matching target save receipt arrives.
|
|
80
|
+
These methods do not replace `requestBackendCredential()` or ordinary OAuth
|
|
81
|
+
authentication for apps that need workspace APIs. WOPI-only packages need no
|
|
82
|
+
per-editor OAuth client secret.
|
|
83
|
+
|
|
60
84
|
### Standalone mode (local development)
|
|
61
85
|
|
|
62
86
|
```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
|
|
|
@@ -754,8 +783,24 @@ function isHeaderAction(value) {
|
|
|
754
783
|
|
|
755
784
|
// src/transport/bridge.transport.ts
|
|
756
785
|
var Bridge = class {
|
|
786
|
+
constructor(expectedHostOrigin) {
|
|
787
|
+
this.expectedHostOrigin = expectedHostOrigin;
|
|
788
|
+
this._initPromise = new Promise((resolve, reject) => {
|
|
789
|
+
this._initResolve = resolve;
|
|
790
|
+
this._initReject = reject;
|
|
791
|
+
});
|
|
792
|
+
void this._initPromise.catch(() => {
|
|
793
|
+
});
|
|
794
|
+
this._messageHandler = this.handleMessage.bind(this);
|
|
795
|
+
if (typeof window !== "undefined") {
|
|
796
|
+
window.addEventListener("message", this._messageHandler);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
expectedHostOrigin;
|
|
757
800
|
listeners = /* @__PURE__ */ new Map();
|
|
758
801
|
_initResolve;
|
|
802
|
+
_initReject;
|
|
803
|
+
_initTimer;
|
|
759
804
|
_initPromise;
|
|
760
805
|
_hostOrigin = null;
|
|
761
806
|
_messageHandler;
|
|
@@ -763,15 +808,8 @@ var Bridge = class {
|
|
|
763
808
|
sessionId = null;
|
|
764
809
|
actions = /* @__PURE__ */ new Map();
|
|
765
810
|
requests = /* @__PURE__ */ new Set();
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
this._initResolve = resolve;
|
|
769
|
-
});
|
|
770
|
-
this._messageHandler = this.handleMessage.bind(this);
|
|
771
|
-
if (typeof window !== "undefined") {
|
|
772
|
-
window.addEventListener("message", this._messageHandler);
|
|
773
|
-
}
|
|
774
|
-
}
|
|
811
|
+
backendRequests = /* @__PURE__ */ new Map();
|
|
812
|
+
wopiRequests = /* @__PURE__ */ new Map();
|
|
775
813
|
/** Post `verentis:ready` to the host to start the handshake */
|
|
776
814
|
sendReady(capabilities) {
|
|
777
815
|
console.info("[Verentis SDK] Sending verentis:ready to host");
|
|
@@ -780,10 +818,16 @@ var Bridge = class {
|
|
|
780
818
|
version: 1,
|
|
781
819
|
capabilities
|
|
782
820
|
};
|
|
783
|
-
window.parent.postMessage(message, "*");
|
|
821
|
+
window.parent.postMessage(message, this.expectedHostOrigin ?? "*");
|
|
784
822
|
}
|
|
785
823
|
/** Wait for the host to send `verentis:init` with context and token */
|
|
786
824
|
waitForInit() {
|
|
825
|
+
if (!this._hostOrigin && !this._destroyed && !this._initTimer) {
|
|
826
|
+
this._initTimer = setTimeout(() => {
|
|
827
|
+
this._initReject(new Error("Workspace handshake timed out."));
|
|
828
|
+
this.destroy();
|
|
829
|
+
}, 2e4);
|
|
830
|
+
}
|
|
787
831
|
return this._initPromise;
|
|
788
832
|
}
|
|
789
833
|
/** Get the host origin (available after init) */
|
|
@@ -827,6 +871,75 @@ var Bridge = class {
|
|
|
827
871
|
setDirty(isDirty) {
|
|
828
872
|
this.postToHost({ type: "verentis:dirty", version: 1, isDirty });
|
|
829
873
|
}
|
|
874
|
+
/** The host, not the app, selects the installation, file, branch and permission ceiling. */
|
|
875
|
+
requestBackendCredential() {
|
|
876
|
+
if (this._destroyed || !this.sessionId || !this._hostOrigin)
|
|
877
|
+
return Promise.reject(new Error("An initialized workspace host is required."));
|
|
878
|
+
if (this.backendRequests.size >= 4)
|
|
879
|
+
return Promise.reject(new Error("A backend credential request is already pending."));
|
|
880
|
+
const requestId = crypto.randomUUID();
|
|
881
|
+
return new Promise((resolve, reject) => {
|
|
882
|
+
const timer = setTimeout(() => {
|
|
883
|
+
this.backendRequests.delete(requestId);
|
|
884
|
+
reject(new Error("Backend authorization timed out."));
|
|
885
|
+
}, 2e4);
|
|
886
|
+
this.backendRequests.set(requestId, { resolve, reject, timer });
|
|
887
|
+
try {
|
|
888
|
+
this.postToHost({ type: "verentis:backend:request", version: 1, requestId, sessionId: this.sessionId });
|
|
889
|
+
} catch {
|
|
890
|
+
clearTimeout(timer);
|
|
891
|
+
this.backendRequests.delete(requestId);
|
|
892
|
+
reject(new Error("The workspace host is unavailable."));
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
/** The trusted parent selects the approved installation, document and action. */
|
|
897
|
+
requestWopiLaunch() {
|
|
898
|
+
return this.requestWopi("launch");
|
|
899
|
+
}
|
|
900
|
+
/** Recover only the target selected by a completed server-owned derived operation. */
|
|
901
|
+
requestWopiContinuation() {
|
|
902
|
+
return this.requestWopi("continue");
|
|
903
|
+
}
|
|
904
|
+
getWopiStatus() {
|
|
905
|
+
return this.requestWopi("status");
|
|
906
|
+
}
|
|
907
|
+
requestWopiSave(generation) {
|
|
908
|
+
if (!Number.isSafeInteger(generation) || generation < 0)
|
|
909
|
+
return Promise.reject(new Error("Invalid edit generation."));
|
|
910
|
+
return this.requestWopi("save", generation);
|
|
911
|
+
}
|
|
912
|
+
closeWopiSession() {
|
|
913
|
+
return this.requestWopi("close");
|
|
914
|
+
}
|
|
915
|
+
requestWopi(operation, generation) {
|
|
916
|
+
if (this._destroyed || !this.sessionId || !this._hostOrigin)
|
|
917
|
+
return Promise.reject(new Error("An initialized workspace host is required."));
|
|
918
|
+
if (this.wopiRequests.size >= 4)
|
|
919
|
+
return Promise.reject(new Error("Too many collaboration requests."));
|
|
920
|
+
const requestId = crypto.randomUUID();
|
|
921
|
+
return new Promise((resolve, reject) => {
|
|
922
|
+
const timer = setTimeout(() => {
|
|
923
|
+
this.wopiRequests.delete(requestId);
|
|
924
|
+
reject(new Error("Collaboration authorization timed out."));
|
|
925
|
+
}, 3e4);
|
|
926
|
+
this.wopiRequests.set(requestId, { resolve, reject, timer });
|
|
927
|
+
try {
|
|
928
|
+
this.postToHost({
|
|
929
|
+
type: "verentis:wopi:request",
|
|
930
|
+
version: 1,
|
|
931
|
+
requestId,
|
|
932
|
+
sessionId: this.sessionId,
|
|
933
|
+
operation,
|
|
934
|
+
...generation === void 0 ? {} : { generation }
|
|
935
|
+
});
|
|
936
|
+
} catch {
|
|
937
|
+
clearTimeout(timer);
|
|
938
|
+
this.wopiRequests.delete(requestId);
|
|
939
|
+
reject(new Error("The workspace host is unavailable."));
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
}
|
|
830
943
|
registerAction(descriptor, handler) {
|
|
831
944
|
if (!this.sessionId || this._destroyed) throw new Error("Header actions require an initialized workspace host.");
|
|
832
945
|
if (!isHeaderAction(descriptor)) throw new Error("Invalid header action.");
|
|
@@ -903,9 +1016,22 @@ var Bridge = class {
|
|
|
903
1016
|
/** Clean up event listeners */
|
|
904
1017
|
destroy() {
|
|
905
1018
|
this._destroyed = true;
|
|
1019
|
+
clearTimeout(this._initTimer);
|
|
1020
|
+
this._initTimer = void 0;
|
|
1021
|
+
if (!this._hostOrigin) this._initReject(new Error("Bridge has been destroyed."));
|
|
906
1022
|
this.actions.clear();
|
|
907
1023
|
this.requests.clear();
|
|
908
1024
|
this.listeners.clear();
|
|
1025
|
+
for (const pending of this.backendRequests.values()) {
|
|
1026
|
+
clearTimeout(pending.timer);
|
|
1027
|
+
pending.reject(new Error("Bridge has been destroyed."));
|
|
1028
|
+
}
|
|
1029
|
+
this.backendRequests.clear();
|
|
1030
|
+
for (const pending of this.wopiRequests.values()) {
|
|
1031
|
+
clearTimeout(pending.timer);
|
|
1032
|
+
pending.reject(new Error("Bridge has been destroyed."));
|
|
1033
|
+
}
|
|
1034
|
+
this.wopiRequests.clear();
|
|
909
1035
|
if (typeof window !== "undefined") {
|
|
910
1036
|
window.removeEventListener("message", this._messageHandler);
|
|
911
1037
|
}
|
|
@@ -915,8 +1041,12 @@ var Bridge = class {
|
|
|
915
1041
|
const data = event.data;
|
|
916
1042
|
if (event.source !== window.parent || data?.version !== 1 || typeof data?.type !== "string" || !data.type.startsWith("verentis:")) return;
|
|
917
1043
|
if (this._hostOrigin && event.origin !== this._hostOrigin) return;
|
|
1044
|
+
if (this.expectedHostOrigin && event.origin !== this.expectedHostOrigin) return;
|
|
918
1045
|
if (data.type === "verentis:init") {
|
|
919
1046
|
if (this._hostOrigin) return;
|
|
1047
|
+
if (!data.context?.workspace?.id || !data.token || event.origin === "null") return;
|
|
1048
|
+
clearTimeout(this._initTimer);
|
|
1049
|
+
this._initTimer = void 0;
|
|
920
1050
|
this._hostOrigin = event.origin;
|
|
921
1051
|
this.sessionId = typeof data.sessionId === "string" ? data.sessionId : null;
|
|
922
1052
|
console.info("[Verentis SDK] Received verentis:init from host", {
|
|
@@ -927,6 +1057,29 @@ var Bridge = class {
|
|
|
927
1057
|
const initData = data;
|
|
928
1058
|
this._initResolve({ context: initData.context, token: initData.token });
|
|
929
1059
|
}
|
|
1060
|
+
if (data.type === "verentis:backend:response") {
|
|
1061
|
+
if (!this._hostOrigin || data.sessionId !== this.sessionId) return;
|
|
1062
|
+
const pending = this.backendRequests.get(data.requestId);
|
|
1063
|
+
if (!pending) return;
|
|
1064
|
+
clearTimeout(pending.timer);
|
|
1065
|
+
this.backendRequests.delete(data.requestId);
|
|
1066
|
+
const value = data.credential;
|
|
1067
|
+
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())
|
|
1068
|
+
pending.reject(new Error("Backend authorization was denied or is not configured."));
|
|
1069
|
+
else pending.resolve(value);
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
if (data.type === "verentis:wopi:response") {
|
|
1073
|
+
if (!this._hostOrigin || data.sessionId !== this.sessionId) return;
|
|
1074
|
+
const pending = this.wopiRequests.get(data.requestId);
|
|
1075
|
+
if (!pending) return;
|
|
1076
|
+
clearTimeout(pending.timer);
|
|
1077
|
+
this.wopiRequests.delete(data.requestId);
|
|
1078
|
+
if (data.error || !Object.hasOwn(data, "result"))
|
|
1079
|
+
pending.reject(new Error("Collaboration authorization is unavailable."));
|
|
1080
|
+
else pending.resolve(data.result);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
930
1083
|
if (data.type === "verentis:action:invoke") {
|
|
931
1084
|
void this.invokeAction(data);
|
|
932
1085
|
return;
|
|
@@ -1112,7 +1265,7 @@ var VerentisClient = class {
|
|
|
1112
1265
|
this.bridge.sendReady();
|
|
1113
1266
|
const { context, token } = await this.bridge.waitForInit();
|
|
1114
1267
|
const hostOrigin = this.bridge.hostOrigin;
|
|
1115
|
-
|
|
1268
|
+
bridgeAuth.setHostOrigin(hostOrigin);
|
|
1116
1269
|
bridgeAuth.setToken(token);
|
|
1117
1270
|
this.context.setFromAppContext(context);
|
|
1118
1271
|
const http = new HttpTransport(
|