@verentis/sdk 0.1.0 → 0.2.0

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.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._pendingRefresh = new Promise((resolve) => {
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(), delay2);
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
 
@@ -140,6 +169,8 @@ var ContextModule = class {
140
169
  _file = null;
141
170
  _entryPoint = null;
142
171
  _apiUrl = null;
172
+ _application = null;
173
+ _installation = null;
143
174
  /** The current workspace */
144
175
  get workspace() {
145
176
  if (!this._workspace) {
@@ -180,13 +211,24 @@ var ContextModule = class {
180
211
  get hasEntryPoint() {
181
212
  return this._entryPoint !== null;
182
213
  }
214
+ get application() {
215
+ return this._application ? { ...this._application } : null;
216
+ }
217
+ get installation() {
218
+ return this._installation ? { ...this._installation } : null;
219
+ }
183
220
  /** Update context from the host's init message */
184
221
  setFromAppContext(ctx) {
222
+ if (ctx.application !== void 0 && (typeof ctx.application !== "object" || ctx.application === null || typeof ctx.application.manifestPath !== "string" || !ctx.application.manifestPath.startsWith("/") || typeof ctx.application.manifestNodeId !== "string" || !ctx.application.manifestNodeId.trim() || typeof ctx.application.branch !== "string" || !ctx.application.branch || ctx.application.branch !== ctx.application.branch.trim().toLowerCase() || /[\u0000-\u001f\u007f]/.test(ctx.application.branch))) {
223
+ throw new Error("Invalid application launch metadata");
224
+ }
185
225
  this._workspace = ctx.workspace;
186
226
  this._surface = ctx.surface ?? "file";
187
227
  this._file = ctx.file ?? null;
188
228
  this._entryPoint = ctx.entryPoint ?? null;
189
229
  this._apiUrl = ctx.apiUrl;
230
+ this._application = ctx.application ? { ...ctx.application } : null;
231
+ this._installation = ctx.installation ? { ...ctx.installation } : null;
190
232
  }
191
233
  /** Set workspace info directly (standalone mode) */
192
234
  setWorkspace(workspace) {
@@ -414,6 +456,49 @@ var FilesModule = class {
414
456
  headers: this.workspaceHeaders()
415
457
  });
416
458
  }
459
+ /** Read metadata by stable node identity on an explicit branch. */
460
+ async getNode(nodeId, options) {
461
+ requireNonBlank(nodeId, "Node ID");
462
+ requireNonBlank(options.branch, "Branch");
463
+ return this.http.request("GET", `/nodes/${encodeURIComponent(nodeId)}/file`, {
464
+ headers: { ...this.workspaceHeaders(), "X-Branch": options.branch }
465
+ });
466
+ }
467
+ /** Read bytes and their matching content revision in the same response. */
468
+ async readNodeContent(nodeId, options) {
469
+ requireNonBlank(nodeId, "Node ID");
470
+ requireNonBlank(options.branch, "Branch");
471
+ const response = await this.http.requestRaw("GET", `/nodes/${encodeURIComponent(nodeId)}/content`, {
472
+ headers: { ...this.workspaceHeaders(), "X-Branch": options.branch, Accept: "*/*" }
473
+ });
474
+ await this.http.ensureSuccess(response);
475
+ const revision = response.headers.get("ETag");
476
+ if (!revision || !/^"[a-fA-F0-9]{32}"$/.test(revision)) {
477
+ throw new Error("Node-content response did not include a valid strong revision ETag.");
478
+ }
479
+ return { content: await response.blob(), revision };
480
+ }
481
+ /** Replace existing node bytes conditionally; never creates or changes path/MIME. */
482
+ async writeNodeContent(nodeId, options) {
483
+ requireNonBlank(nodeId, "Node ID");
484
+ requireNonBlank(options.branch, "Branch");
485
+ requireNonBlank(options.idempotencyKey, "Idempotency key");
486
+ const revision = /^[a-fA-F0-9]{32}$/.test(options.revision) ? `"${options.revision}"` : options.revision;
487
+ if (!/^"[a-fA-F0-9]{32}"$/.test(revision)) {
488
+ throw new Error("A single strong node-content revision is required.");
489
+ }
490
+ const content = typeof options.content === "string" ? new Blob([options.content]) : options.content instanceof ArrayBuffer ? new Blob([options.content]) : options.content;
491
+ return this.http.request("PUT", `/nodes/${encodeURIComponent(nodeId)}/content`, {
492
+ body: content,
493
+ headers: {
494
+ ...this.workspaceHeaders(),
495
+ "X-Branch": options.branch,
496
+ "If-Match": revision,
497
+ "Idempotency-Key": options.idempotencyKey,
498
+ "Content-Type": "application/octet-stream"
499
+ }
500
+ });
501
+ }
417
502
  /**
418
503
  * Read the raw content of a file as a Blob.
419
504
  */
@@ -530,14 +615,19 @@ var FilesModule = class {
530
615
  * Delete a file.
531
616
  */
532
617
  async remove(options) {
533
- await this.http.request("DELETE", "/files", {
534
- body: JSON.stringify({
535
- path: normalizePath(options.path),
536
- branch: options.branch
537
- }),
618
+ const path = normalizePath(options.path);
619
+ requireNonBlank(path, "File path");
620
+ const branch = options.branch ?? "main";
621
+ requireNonBlank(branch, "Branch");
622
+ if (options.headStamp !== void 0) requireNonBlank(options.headStamp, "Path head stamp");
623
+ if (options.idempotencyKey !== void 0) requireNonBlank(options.idempotencyKey, "Idempotency key");
624
+ await this.http.request("DELETE", `/files/${path.split("/").map(encodeURIComponent).join("/")}`, {
625
+ params: { branch },
538
626
  headers: {
539
627
  ...this.workspaceHeaders(),
540
- "Content-Type": "application/json"
628
+ ...options.headStamp === void 0 ? {} : { "If-Match": options.headStamp },
629
+ ...options.idempotencyKey === void 0 ? {} : { "Idempotency-Key": options.idempotencyKey },
630
+ ...options.mode === void 0 ? {} : { "X-Delete-Mode": options.mode === "deep" ? "Deep" : "Shallow" }
541
631
  }
542
632
  });
543
633
  }
@@ -549,6 +639,9 @@ var FilesModule = class {
549
639
  return headers;
550
640
  }
551
641
  };
642
+ function requireNonBlank(value, name) {
643
+ if (value.trim().length === 0) throw new Error(`${name} is required.`);
644
+ }
552
645
  function normalizePath(path) {
553
646
  return path.replace(/\/+/g, "/").replace(/^\//, "").replace(/\/$/, "");
554
647
  }
@@ -565,6 +658,52 @@ var InstallationsModule = class {
565
658
  }
566
659
  http;
567
660
  workspaceId;
661
+ /** Resolve canonical publisher/package identity, never the installation's runtime display name. */
662
+ async resolvePackageId(identity, options) {
663
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*\/[a-z0-9]+(?:-[a-z0-9]+)*$/.test(identity)) {
664
+ throw new Error("A canonical publisher/package identity is required");
665
+ }
666
+ const result = await this.http.request("GET", `/packages/by-package-id/${identity}`, {
667
+ params: { workspaceId: this.requireWorkspaceId(options?.workspaceId) }
668
+ });
669
+ if (!result || typeof result.id !== "string" || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(result.id)) {
670
+ throw new Error("The catalog response omitted its stable package identity");
671
+ }
672
+ return result.id;
673
+ }
674
+ /** Resolves the complete required graph without installing anything or granting capabilities. */
675
+ async preview(packageId, options) {
676
+ return this.http.request("POST", "/installations/graphs/preview", {
677
+ body: JSON.stringify({
678
+ workspaceId: this.requireWorkspaceId(options?.workspaceId),
679
+ packageId,
680
+ packageVersion: options?.packageVersion,
681
+ installPath: options?.installPath,
682
+ branch: options?.branch,
683
+ installMode: options?.installMode
684
+ }),
685
+ headers: { "Content-Type": "application/json" }
686
+ });
687
+ }
688
+ /** Explicit, single-use consent to a displayed preview. Never automatically retries accepted installs. */
689
+ async install(preview, consent) {
690
+ if (consent !== true) throw new Error("Explicit consent to the installation graph is required");
691
+ return this.http.request("POST", "/installations/graphs", {
692
+ body: JSON.stringify({
693
+ workspaceId: preview.workspaceId,
694
+ operationId: preview.operationId,
695
+ fingerprint: preview.fingerprint,
696
+ consent
697
+ }),
698
+ headers: { "Content-Type": "application/json" }
699
+ });
700
+ }
701
+ /** Inspect durable outcomes after a failure or lost response instead of resubmitting consent. */
702
+ async graphResult(operationId, options) {
703
+ return this.http.request("GET", `/installations/graphs/${encodeURIComponent(operationId)}`, {
704
+ params: { workspaceId: this.requireWorkspaceId(options?.workspaceId) }
705
+ });
706
+ }
568
707
  async list(options) {
569
708
  return this.http.request("GET", "/installations", {
570
709
  params: {
@@ -745,23 +884,212 @@ var SettingsModule = class {
745
884
  }
746
885
  };
747
886
 
887
+ // src/modules/email.ts
888
+ var EmailModule = class {
889
+ constructor(http, workspaceId) {
890
+ this.http = http;
891
+ this.workspaceId = workspaceId;
892
+ }
893
+ http;
894
+ workspaceId;
895
+ async preview(installationId, options) {
896
+ return this.http.request("GET", `/installations/${encodeURIComponent(installationId)}/email-consent-preview`, {
897
+ params: { workspaceId: this.workspace(options) }
898
+ });
899
+ }
900
+ async consents(options) {
901
+ return this.http.request("GET", "/email/consents", { params: { workspaceId: this.workspace(options) } });
902
+ }
903
+ /** Approve exactly the reviewed signed candidate; tracking stays off unless explicitly opted in. */
904
+ async consent(preview, approval, options) {
905
+ if (approval !== true) throw new Error("Explicit email consent is required");
906
+ if (!options?.senderGrants?.length) throw new Error("Explicit purpose/sender grants are required");
907
+ return this.http.request("PUT", `/email/consents/${encodeURIComponent(preview.installationId)}`, {
908
+ params: { workspaceId: preview.workspaceId },
909
+ body: JSON.stringify({
910
+ candidateDigest: preview.candidateDigest,
911
+ enabled: true,
912
+ allowOpenTracking: options?.allowOpenTracking ?? false,
913
+ allowClickTracking: options?.allowClickTracking ?? false,
914
+ senderGrants: options.senderGrants.map((grant) => ({ purpose: grant.purpose, senderId: grant.senderId }))
915
+ }),
916
+ headers: { "Content-Type": "application/json" }
917
+ });
918
+ }
919
+ async revoke(consent, options) {
920
+ return this.http.request("PUT", `/email/consents/${encodeURIComponent(consent.installationId)}`, {
921
+ params: { workspaceId: this.workspace(options) },
922
+ body: JSON.stringify({ candidateDigest: consent.candidateDigest, enabled: false, allowOpenTracking: false, allowClickTracking: false }),
923
+ headers: { "Content-Type": "application/json" }
924
+ });
925
+ }
926
+ async senders(options) {
927
+ return this.http.request("GET", "/email/senders", { params: { workspaceId: this.workspace(options) } });
928
+ }
929
+ async invoicePdf(recordId, installationId, options) {
930
+ this.mainBranch(options);
931
+ return this.http.request("POST", "/email/artifacts/invoice", {
932
+ body: JSON.stringify({ workspaceId: this.workspace(options), recordId, installationId }),
933
+ headers: { "Content-Type": "application/json" }
934
+ });
935
+ }
936
+ /** Authenticated immutable bytes; no public URL. The caller owns any local object URL it creates. */
937
+ async artifactContent(artifactId, options) {
938
+ this.mainBranch(options);
939
+ return this.http.request("GET", `/email/artifacts/${encodeURIComponent(artifactId)}/content`, {
940
+ params: { workspaceId: this.workspace(options), branch: "main" }
941
+ });
942
+ }
943
+ /** A stable key binds this exact request. After a lost response inspect status; never automatically resend. */
944
+ async dispatch(options) {
945
+ this.mainBranch(options);
946
+ return this.http.request("POST", "/email/dispatches", {
947
+ body: JSON.stringify({
948
+ workspaceId: this.workspace(options),
949
+ installationId: options.installationId,
950
+ recordId: options.recordId,
951
+ purpose: options.purpose,
952
+ templateId: options.templateId,
953
+ senderId: options.senderId,
954
+ recipients: options.recipients,
955
+ artifactIds: options.artifactIds,
956
+ idempotencyKey: options.idempotencyKey,
957
+ replyTo: options.replyTo
958
+ }),
959
+ headers: { "Content-Type": "application/json" }
960
+ });
961
+ }
962
+ async outcome(id, options) {
963
+ this.mainBranch(options);
964
+ return this.http.request("GET", `/email/dispatches/${encodeURIComponent(id)}`, {
965
+ params: { workspaceId: this.workspace(options) }
966
+ });
967
+ }
968
+ async history(options) {
969
+ this.mainBranch(options);
970
+ return this.http.request("GET", "/email/dispatches", {
971
+ params: { workspaceId: this.workspace(options), recordId: options?.recordId, installationId: options?.installationId }
972
+ });
973
+ }
974
+ async status(id, options) {
975
+ return this.outcome(id, options);
976
+ }
977
+ workspace(options) {
978
+ const value = options?.workspaceId ?? this.workspaceId;
979
+ if (!value) throw new Error("A workspace is required for transactional email");
980
+ return value;
981
+ }
982
+ mainBranch(options) {
983
+ if (options?.branch !== void 0 && options.branch !== "main")
984
+ throw new Error("Transactional email currently supports only the main business branch");
985
+ }
986
+ };
987
+
988
+ // src/modules/business.ts
989
+ var BusinessModule = class _BusinessModule {
990
+ constructor(http, branch = "main") {
991
+ this.http = http;
992
+ this.branch = branch;
993
+ }
994
+ http;
995
+ branch;
996
+ withBranch(branch) {
997
+ return new _BusinessModule(this.http, branch);
998
+ }
999
+ async context() {
1000
+ return this.http.request("GET", "/business/context", { headers: this.headers() });
1001
+ }
1002
+ async command(command) {
1003
+ if (!command.idempotencyKey?.trim() || command.idempotencyKey.length > 200 || /[\u0000-\u001f\u007f-\u009f]/.test(command.idempotencyKey)) {
1004
+ throw new Error("A bounded printable idempotency key is required");
1005
+ }
1006
+ if (command.expectedRevision !== void 0 && !/^"[a-f0-9]{32}"$/.test(command.expectedRevision)) {
1007
+ throw new Error("An exact strong quoted canonical revision is required");
1008
+ }
1009
+ return this.http.request("POST", "/business/commands", {
1010
+ headers: this.headers(),
1011
+ body: JSON.stringify(command)
1012
+ });
1013
+ }
1014
+ async read(id) {
1015
+ return this.http.request("GET", `/business/records/${encodeURIComponent(id)}`, { headers: this.headers() });
1016
+ }
1017
+ async list(kind) {
1018
+ return this.http.request("GET", "/business/records", {
1019
+ headers: this.headers(),
1020
+ params: kind ? { kind } : {}
1021
+ });
1022
+ }
1023
+ /** null means unresolved, not success; do not invent a new operation key. */
1024
+ async outcome(request) {
1025
+ return this.http.request("POST", "/business/outcomes", {
1026
+ headers: this.headers(),
1027
+ body: JSON.stringify(request)
1028
+ });
1029
+ }
1030
+ /** Recover using the exact original command, without reproducing server fingerprint encoding. */
1031
+ async outcomeFor(command) {
1032
+ return this.outcome({ operation: command.operation, idempotencyKey: command.idempotencyKey, command });
1033
+ }
1034
+ async report(report, range = {}) {
1035
+ return this.http.request("GET", `/business/reports/${report}`, {
1036
+ headers: this.headers(),
1037
+ params: range
1038
+ });
1039
+ }
1040
+ /** Retained historical expense receipt bytes, even after the original native file is removed. */
1041
+ async receipt(nodeId, revision) {
1042
+ const response = await this.http.requestRaw("GET", `/business/receipts/${encodeURIComponent(nodeId)}/content`, {
1043
+ headers: this.headers(),
1044
+ params: { revision }
1045
+ });
1046
+ await this.http.ensureSuccess(response);
1047
+ return response.blob();
1048
+ }
1049
+ headers() {
1050
+ if (!this.branch || this.branch.length > 128 || this.branch !== this.branch.trim().toLowerCase() || /[\u0000-\u001f\u007f-\u009f]/.test(this.branch)) {
1051
+ throw new Error("Business operations require a canonical branch");
1052
+ }
1053
+ return { "X-Branch": this.branch, "Content-Type": "application/json" };
1054
+ }
1055
+ };
1056
+
1057
+ // src/transport/header-actions.ts
1058
+ function isHeaderAction(value) {
1059
+ if (!value || typeof value !== "object") return false;
1060
+ const a = value;
1061
+ 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");
1062
+ }
1063
+
748
1064
  // src/transport/bridge.transport.ts
749
1065
  var Bridge = class {
750
- listeners = /* @__PURE__ */ new Map();
751
- _initResolve;
752
- _initPromise;
753
- _hostOrigin = null;
754
- _messageHandler;
755
- _destroyed = false;
756
- constructor() {
757
- this._initPromise = new Promise((resolve) => {
1066
+ constructor(expectedHostOrigin) {
1067
+ this.expectedHostOrigin = expectedHostOrigin;
1068
+ this._initPromise = new Promise((resolve, reject) => {
758
1069
  this._initResolve = resolve;
1070
+ this._initReject = reject;
1071
+ });
1072
+ void this._initPromise.catch(() => {
759
1073
  });
760
1074
  this._messageHandler = this.handleMessage.bind(this);
761
1075
  if (typeof window !== "undefined") {
762
1076
  window.addEventListener("message", this._messageHandler);
763
1077
  }
764
1078
  }
1079
+ expectedHostOrigin;
1080
+ listeners = /* @__PURE__ */ new Map();
1081
+ _initResolve;
1082
+ _initReject;
1083
+ _initTimer;
1084
+ _initPromise;
1085
+ _hostOrigin = null;
1086
+ _messageHandler;
1087
+ _destroyed = false;
1088
+ sessionId = null;
1089
+ actions = /* @__PURE__ */ new Map();
1090
+ requests = /* @__PURE__ */ new Set();
1091
+ backendRequests = /* @__PURE__ */ new Map();
1092
+ wopiRequests = /* @__PURE__ */ new Map();
765
1093
  /** Post `verentis:ready` to the host to start the handshake */
766
1094
  sendReady(capabilities) {
767
1095
  console.info("[Verentis SDK] Sending verentis:ready to host");
@@ -770,10 +1098,16 @@ var Bridge = class {
770
1098
  version: 1,
771
1099
  capabilities
772
1100
  };
773
- window.parent.postMessage(message, "*");
1101
+ window.parent.postMessage(message, this.expectedHostOrigin ?? "*");
774
1102
  }
775
1103
  /** Wait for the host to send `verentis:init` with context and token */
776
1104
  waitForInit() {
1105
+ if (!this._hostOrigin && !this._destroyed && !this._initTimer) {
1106
+ this._initTimer = setTimeout(() => {
1107
+ this._initReject(new Error("Workspace handshake timed out."));
1108
+ this.destroy();
1109
+ }, 2e4);
1110
+ }
777
1111
  return this._initPromise;
778
1112
  }
779
1113
  /** Get the host origin (available after init) */
@@ -782,6 +1116,7 @@ var Bridge = class {
782
1116
  }
783
1117
  /** Send a message to the host */
784
1118
  postToHost(message) {
1119
+ if (this._destroyed) throw new Error("Bridge has been destroyed.");
785
1120
  if (!this._hostOrigin) {
786
1121
  throw new Error("Bridge not initialized: host origin unknown. Wait for init first.");
787
1122
  }
@@ -816,6 +1151,138 @@ var Bridge = class {
816
1151
  setDirty(isDirty) {
817
1152
  this.postToHost({ type: "verentis:dirty", version: 1, isDirty });
818
1153
  }
1154
+ /** The host, not the app, selects the installation, file, branch and permission ceiling. */
1155
+ requestBackendCredential() {
1156
+ if (this._destroyed || !this.sessionId || !this._hostOrigin)
1157
+ return Promise.reject(new Error("An initialized workspace host is required."));
1158
+ if (this.backendRequests.size >= 4)
1159
+ return Promise.reject(new Error("A backend credential request is already pending."));
1160
+ const requestId = crypto.randomUUID();
1161
+ return new Promise((resolve, reject) => {
1162
+ const timer = setTimeout(() => {
1163
+ this.backendRequests.delete(requestId);
1164
+ reject(new Error("Backend authorization timed out."));
1165
+ }, 2e4);
1166
+ this.backendRequests.set(requestId, { resolve, reject, timer });
1167
+ try {
1168
+ this.postToHost({ type: "verentis:backend:request", version: 1, requestId, sessionId: this.sessionId });
1169
+ } catch {
1170
+ clearTimeout(timer);
1171
+ this.backendRequests.delete(requestId);
1172
+ reject(new Error("The workspace host is unavailable."));
1173
+ }
1174
+ });
1175
+ }
1176
+ /** The trusted parent selects the approved installation, document and action. */
1177
+ requestWopiLaunch() {
1178
+ return this.requestWopi("launch");
1179
+ }
1180
+ /** Recover only the target selected by a completed server-owned derived operation. */
1181
+ requestWopiContinuation() {
1182
+ return this.requestWopi("continue");
1183
+ }
1184
+ getWopiStatus() {
1185
+ return this.requestWopi("status");
1186
+ }
1187
+ requestWopiSave(generation) {
1188
+ if (!Number.isSafeInteger(generation) || generation < 0)
1189
+ return Promise.reject(new Error("Invalid edit generation."));
1190
+ return this.requestWopi("save", generation);
1191
+ }
1192
+ closeWopiSession() {
1193
+ return this.requestWopi("close");
1194
+ }
1195
+ requestWopi(operation, generation) {
1196
+ if (this._destroyed || !this.sessionId || !this._hostOrigin)
1197
+ return Promise.reject(new Error("An initialized workspace host is required."));
1198
+ if (this.wopiRequests.size >= 4)
1199
+ return Promise.reject(new Error("Too many collaboration requests."));
1200
+ const requestId = crypto.randomUUID();
1201
+ return new Promise((resolve, reject) => {
1202
+ const timer = setTimeout(() => {
1203
+ this.wopiRequests.delete(requestId);
1204
+ reject(new Error("Collaboration authorization timed out."));
1205
+ }, 3e4);
1206
+ this.wopiRequests.set(requestId, { resolve, reject, timer });
1207
+ try {
1208
+ this.postToHost({
1209
+ type: "verentis:wopi:request",
1210
+ version: 1,
1211
+ requestId,
1212
+ sessionId: this.sessionId,
1213
+ operation,
1214
+ ...generation === void 0 ? {} : { generation }
1215
+ });
1216
+ } catch {
1217
+ clearTimeout(timer);
1218
+ this.wopiRequests.delete(requestId);
1219
+ reject(new Error("The workspace host is unavailable."));
1220
+ }
1221
+ });
1222
+ }
1223
+ registerAction(descriptor, handler) {
1224
+ if (!this.sessionId || this._destroyed) throw new Error("Header actions require an initialized workspace host.");
1225
+ if (!isHeaderAction(descriptor)) throw new Error("Invalid header action.");
1226
+ if (this.actions.has(descriptor.id)) throw new Error(`Action '${descriptor.id}' is already registered.`);
1227
+ if (this.actions.size >= 16) throw new Error("At most 16 header actions may be registered.");
1228
+ const action = { descriptor: { ...descriptor, scopes: [...descriptor.scopes] }, handler, busy: false };
1229
+ this.actions.set(descriptor.id, action);
1230
+ this.publishActions();
1231
+ return {
1232
+ update: (next) => {
1233
+ if (this.actions.get(descriptor.id) !== action) throw new Error("Action has been disposed.");
1234
+ if (!isHeaderAction(next) || next.id !== descriptor.id) throw new Error("Invalid header action update.");
1235
+ action.descriptor = { ...next, scopes: [...next.scopes] };
1236
+ this.publishActions();
1237
+ },
1238
+ dispose: () => {
1239
+ if (this.actions.get(descriptor.id) !== action) return;
1240
+ this.actions.delete(descriptor.id);
1241
+ this.publishActions();
1242
+ }
1243
+ };
1244
+ }
1245
+ publishActions() {
1246
+ if (!this.sessionId || this._destroyed) return;
1247
+ this.postToHost({
1248
+ type: "verentis:actions:set",
1249
+ version: 1,
1250
+ sessionId: this.sessionId,
1251
+ actions: [...this.actions.values()].map((a) => ({ ...a.descriptor, busy: a.busy || a.descriptor.busy }))
1252
+ });
1253
+ }
1254
+ async invokeAction(message) {
1255
+ if (message.sessionId !== this.sessionId || typeof message.requestId !== "string" || message.requestId.length > 128 || !message.requestId || this.requests.has(message.requestId)) return;
1256
+ this.requests.add(message.requestId);
1257
+ if (this.requests.size > 1e3) this.requests.delete(this.requests.values().next().value);
1258
+ const action = this.actions.get(message.actionId);
1259
+ const reply = {
1260
+ type: "verentis:action:result",
1261
+ version: 1,
1262
+ sessionId: message.sessionId,
1263
+ requestId: message.requestId,
1264
+ actionId: message.actionId
1265
+ };
1266
+ if (!action || action.busy || action.descriptor.busy || action.descriptor.enabled === false || action.descriptor.visible === false) {
1267
+ this.postToHost({ ...reply, error: "Action is unavailable." });
1268
+ return;
1269
+ }
1270
+ action.busy = true;
1271
+ this.publishActions();
1272
+ try {
1273
+ const result = await action.handler();
1274
+ if (!this._destroyed && this.sessionId === message.sessionId) {
1275
+ this.postToHost({ ...reply, result: result || void 0 });
1276
+ }
1277
+ } catch (error) {
1278
+ if (!this._destroyed && this.sessionId === message.sessionId) {
1279
+ this.postToHost({ ...reply, error: (error instanceof Error ? error.message : "Action failed.").slice(0, 500) });
1280
+ }
1281
+ } finally {
1282
+ action.busy = false;
1283
+ this.publishActions();
1284
+ }
1285
+ }
819
1286
  /** Subscribe to a specific message type from the host */
820
1287
  on(type, listener) {
821
1288
  if (!this.listeners.has(type)) {
@@ -829,7 +1296,22 @@ var Bridge = class {
829
1296
  /** Clean up event listeners */
830
1297
  destroy() {
831
1298
  this._destroyed = true;
1299
+ clearTimeout(this._initTimer);
1300
+ this._initTimer = void 0;
1301
+ if (!this._hostOrigin) this._initReject(new Error("Bridge has been destroyed."));
1302
+ this.actions.clear();
1303
+ this.requests.clear();
832
1304
  this.listeners.clear();
1305
+ for (const pending of this.backendRequests.values()) {
1306
+ clearTimeout(pending.timer);
1307
+ pending.reject(new Error("Bridge has been destroyed."));
1308
+ }
1309
+ this.backendRequests.clear();
1310
+ for (const pending of this.wopiRequests.values()) {
1311
+ clearTimeout(pending.timer);
1312
+ pending.reject(new Error("Bridge has been destroyed."));
1313
+ }
1314
+ this.wopiRequests.clear();
833
1315
  if (typeof window !== "undefined") {
834
1316
  window.removeEventListener("message", this._messageHandler);
835
1317
  }
@@ -837,9 +1319,16 @@ var Bridge = class {
837
1319
  handleMessage(event) {
838
1320
  if (this._destroyed) return;
839
1321
  const data = event.data;
840
- if (!data?.type?.startsWith("verentis:")) return;
1322
+ if (event.source !== window.parent || data?.version !== 1 || typeof data?.type !== "string" || !data.type.startsWith("verentis:")) return;
1323
+ if (this._hostOrigin && event.origin !== this._hostOrigin) return;
1324
+ if (this.expectedHostOrigin && event.origin !== this.expectedHostOrigin) return;
841
1325
  if (data.type === "verentis:init") {
1326
+ if (this._hostOrigin) return;
1327
+ if (!data.context?.workspace?.id || !data.token || event.origin === "null") return;
1328
+ clearTimeout(this._initTimer);
1329
+ this._initTimer = void 0;
842
1330
  this._hostOrigin = event.origin;
1331
+ this.sessionId = typeof data.sessionId === "string" ? data.sessionId : null;
843
1332
  console.info("[Verentis SDK] Received verentis:init from host", {
844
1333
  origin: event.origin,
845
1334
  workspaceId: data.context.workspace.id,
@@ -848,6 +1337,33 @@ var Bridge = class {
848
1337
  const initData = data;
849
1338
  this._initResolve({ context: initData.context, token: initData.token });
850
1339
  }
1340
+ if (data.type === "verentis:backend:response") {
1341
+ if (!this._hostOrigin || data.sessionId !== this.sessionId) return;
1342
+ const pending = this.backendRequests.get(data.requestId);
1343
+ if (!pending) return;
1344
+ clearTimeout(pending.timer);
1345
+ this.backendRequests.delete(data.requestId);
1346
+ const value = data.credential;
1347
+ 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())
1348
+ pending.reject(new Error("Backend authorization was denied or is not configured."));
1349
+ else pending.resolve(value);
1350
+ return;
1351
+ }
1352
+ if (data.type === "verentis:wopi:response") {
1353
+ if (!this._hostOrigin || data.sessionId !== this.sessionId) return;
1354
+ const pending = this.wopiRequests.get(data.requestId);
1355
+ if (!pending) return;
1356
+ clearTimeout(pending.timer);
1357
+ this.wopiRequests.delete(data.requestId);
1358
+ if (data.error || !Object.hasOwn(data, "result"))
1359
+ pending.reject(new Error("Collaboration authorization is unavailable."));
1360
+ else pending.resolve(data.result);
1361
+ return;
1362
+ }
1363
+ if (data.type === "verentis:action:invoke") {
1364
+ void this.invokeAction(data);
1365
+ return;
1366
+ }
851
1367
  const typeListeners = this.listeners.get(data.type);
852
1368
  if (typeListeners) {
853
1369
  for (const listener of typeListeners) {
@@ -859,16 +1375,18 @@ var Bridge = class {
859
1375
 
860
1376
  // src/types.ts
861
1377
  var ApiError = class extends Error {
862
- constructor(status, detail, title) {
1378
+ constructor(status, detail, title, code) {
863
1379
  super(title ?? `API error ${status}: ${detail}`);
864
1380
  this.status = status;
865
1381
  this.detail = detail;
866
1382
  this.title = title;
1383
+ this.code = code;
867
1384
  this.name = "ApiError";
868
1385
  }
869
1386
  status;
870
1387
  detail;
871
1388
  title;
1389
+ code;
872
1390
  };
873
1391
 
874
1392
  // src/transport/http.transport.ts
@@ -897,24 +1415,7 @@ var HttpTransport = class {
897
1415
  headers,
898
1416
  body: options?.body
899
1417
  });
900
- if (!response.ok) {
901
- let detail = "";
902
- let title;
903
- try {
904
- const body = await response.json();
905
- detail = body.detail ?? body.message ?? JSON.stringify(body);
906
- title = body.title;
907
- } catch {
908
- detail = await response.text().catch(() => response.statusText);
909
- }
910
- console.error("[Verentis SDK] Request failed", {
911
- method,
912
- path,
913
- status: response.status,
914
- detail
915
- });
916
- throw new ApiError(response.status, detail, title);
917
- }
1418
+ await this.ensureSuccess(response);
918
1419
  console.info("[Verentis SDK] Request succeeded", {
919
1420
  method,
920
1421
  path,
@@ -948,6 +1449,26 @@ var HttpTransport = class {
948
1449
  });
949
1450
  return response;
950
1451
  }
1452
+ async ensureSuccess(response) {
1453
+ if (response.ok) return;
1454
+ const raw = await response.text();
1455
+ let detail = raw || response.statusText;
1456
+ let title;
1457
+ let code;
1458
+ try {
1459
+ const body = JSON.parse(raw);
1460
+ if (body !== null && typeof body === "object") {
1461
+ if ("detail" in body && typeof body.detail === "string") detail = body.detail;
1462
+ else if ("message" in body && typeof body.message === "string") detail = body.message;
1463
+ if ("title" in body && typeof body.title === "string") title = body.title;
1464
+ if ("code" in body && typeof body.code === "string") code = body.code;
1465
+ }
1466
+ } catch (error) {
1467
+ if (!(error instanceof SyntaxError)) throw error;
1468
+ }
1469
+ console.error("[Verentis SDK] Request failed", { status: response.status, code });
1470
+ throw new ApiError(response.status, detail, title, code);
1471
+ }
951
1472
  buildUrl(path, params) {
952
1473
  const base = `${this.baseUrl}/${this.apiVersion}${path}`;
953
1474
  if (!params) return base;
@@ -977,6 +1498,8 @@ var VerentisClient = class {
977
1498
  installations;
978
1499
  resources;
979
1500
  settings;
1501
+ email;
1502
+ business;
980
1503
  context;
981
1504
  bridge;
982
1505
  _auth;
@@ -1008,6 +1531,8 @@ var VerentisClient = class {
1008
1531
  this.installations = new InstallationsModule(this._http, options?.workspaceId);
1009
1532
  this.resources = new ResourcesModule(this._http, options?.workspaceId);
1010
1533
  this.settings = new SettingsModule(this._http, options?.workspaceId);
1534
+ this.email = new EmailModule(this._http, options?.workspaceId);
1535
+ this.business = new BusinessModule(this._http);
1011
1536
  this._ready = Promise.resolve();
1012
1537
  return;
1013
1538
  }
@@ -1029,7 +1554,7 @@ var VerentisClient = class {
1029
1554
  this.bridge.sendReady();
1030
1555
  const { context, token } = await this.bridge.waitForInit();
1031
1556
  const hostOrigin = this.bridge.hostOrigin;
1032
- Object.assign(bridgeAuth, new BridgeAuth(hostOrigin));
1557
+ bridgeAuth.setHostOrigin(hostOrigin);
1033
1558
  bridgeAuth.setToken(token);
1034
1559
  this.context.setFromAppContext(context);
1035
1560
  const http = new HttpTransport(
@@ -1044,6 +1569,8 @@ var VerentisClient = class {
1044
1569
  this.installations = new InstallationsModule(http, context.workspace.id);
1045
1570
  this.resources = new ResourcesModule(http, context.workspace.id);
1046
1571
  this.settings = new SettingsModule(http, context.workspace.id);
1572
+ this.email = new EmailModule(http, context.workspace.id);
1573
+ this.business = new BusinessModule(http);
1047
1574
  }
1048
1575
  };
1049
1576
  function detectEnvironment() {
@@ -1101,10 +1628,13 @@ function createVerentisClient(options) {
1101
1628
  return new VerentisClient(options);
1102
1629
  }
1103
1630
 
1631
+ exports.ApiError = ApiError;
1104
1632
  exports.ApiKeyAuth = ApiKeyAuth;
1105
1633
  exports.Bridge = Bridge;
1106
1634
  exports.BridgeAuth = BridgeAuth;
1635
+ exports.BusinessModule = BusinessModule;
1107
1636
  exports.ContextModule = ContextModule;
1637
+ exports.EmailModule = EmailModule;
1108
1638
  exports.EnginesModule = EnginesModule;
1109
1639
  exports.ExecutionsModule = ExecutionsModule;
1110
1640
  exports.FilesModule = FilesModule;
@@ -1116,6 +1646,7 @@ exports.TokenAuth = TokenAuth;
1116
1646
  exports.VerentisClient = VerentisClient;
1117
1647
  exports.createVerentisClient = createVerentisClient;
1118
1648
  exports.findBestMatch = findBestMatch;
1649
+ exports.isHeaderAction = isHeaderAction;
1119
1650
  exports.isTerminalStatus = isTerminalStatus;
1120
1651
  exports.matchMimeType = matchMimeType;
1121
1652
  //# sourceMappingURL=index.cjs.map