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