@verentis/sdk 0.2.4 → 0.2.5

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
@@ -167,6 +167,8 @@ var ContextModule = class {
167
167
  _file = null;
168
168
  _entryPoint = null;
169
169
  _apiUrl = null;
170
+ _application = null;
171
+ _installation = null;
170
172
  /** The current workspace */
171
173
  get workspace() {
172
174
  if (!this._workspace) {
@@ -207,13 +209,24 @@ var ContextModule = class {
207
209
  get hasEntryPoint() {
208
210
  return this._entryPoint !== null;
209
211
  }
212
+ get application() {
213
+ return this._application ? { ...this._application } : null;
214
+ }
215
+ get installation() {
216
+ return this._installation ? { ...this._installation } : null;
217
+ }
210
218
  /** Update context from the host's init message */
211
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
+ }
212
223
  this._workspace = ctx.workspace;
213
224
  this._surface = ctx.surface ?? "file";
214
225
  this._file = ctx.file ?? null;
215
226
  this._entryPoint = ctx.entryPoint ?? null;
216
227
  this._apiUrl = ctx.apiUrl;
228
+ this._application = ctx.application ? { ...ctx.application } : null;
229
+ this._installation = ctx.installation ? { ...ctx.installation } : null;
217
230
  }
218
231
  /** Set workspace info directly (standalone mode) */
219
232
  setWorkspace(workspace) {
@@ -441,6 +454,49 @@ var FilesModule = class {
441
454
  headers: this.workspaceHeaders()
442
455
  });
443
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
+ }
444
500
  /**
445
501
  * Read the raw content of a file as a Blob.
446
502
  */
@@ -532,11 +588,18 @@ var FilesModule = class {
532
588
  if (options.contentType) metadata.contentType = options.contentType;
533
589
  if (options.branch) metadata.branch = options.branch;
534
590
  formData.append("metadata", JSON.stringify(metadata));
535
- await this.http.request("POST", `/files/${normalizedPath}`, {
591
+ await this.http.request("POST", `/files/${normalizedPath.split("/").map(encodeURIComponent).join("/")}`, {
536
592
  body: formData,
537
- headers: this.workspaceHeaders()
593
+ headers: {
594
+ ...this.workspaceHeaders(),
595
+ ...options.createOnly ? { "If-None-Match": "*" } : {}
596
+ }
538
597
  });
539
598
  }
599
+ /** Create without a read-before-write race or overwrite of an existing live file. */
600
+ async create(path, options) {
601
+ await this.write(path, { ...options, createOnly: true });
602
+ }
540
603
  /**
541
604
  * Move or rename a file.
542
605
  */
@@ -557,14 +620,19 @@ var FilesModule = class {
557
620
  * Delete a file.
558
621
  */
559
622
  async remove(options) {
560
- await this.http.request("DELETE", "/files", {
561
- body: JSON.stringify({
562
- path: normalizePath(options.path),
563
- branch: options.branch
564
- }),
623
+ const path = normalizePath(options.path);
624
+ requireNonBlank(path, "File path");
625
+ const branch = options.branch ?? "main";
626
+ requireNonBlank(branch, "Branch");
627
+ if (options.headStamp !== void 0) requireNonBlank(options.headStamp, "Path head stamp");
628
+ if (options.idempotencyKey !== void 0) requireNonBlank(options.idempotencyKey, "Idempotency key");
629
+ await this.http.request("DELETE", `/files/${path.split("/").map(encodeURIComponent).join("/")}`, {
630
+ params: { branch },
565
631
  headers: {
566
632
  ...this.workspaceHeaders(),
567
- "Content-Type": "application/json"
633
+ ...options.headStamp === void 0 ? {} : { "If-Match": options.headStamp },
634
+ ...options.idempotencyKey === void 0 ? {} : { "Idempotency-Key": options.idempotencyKey },
635
+ ...options.mode === void 0 ? {} : { "X-Delete-Mode": options.mode === "deep" ? "Deep" : "Shallow" }
568
636
  }
569
637
  });
570
638
  }
@@ -576,6 +644,9 @@ var FilesModule = class {
576
644
  return headers;
577
645
  }
578
646
  };
647
+ function requireNonBlank(value, name) {
648
+ if (value.trim().length === 0) throw new Error(`${name} is required.`);
649
+ }
579
650
  function normalizePath(path) {
580
651
  return path.replace(/\/+/g, "/").replace(/^\//, "").replace(/\/$/, "");
581
652
  }
@@ -592,6 +663,52 @@ var InstallationsModule = class {
592
663
  }
593
664
  http;
594
665
  workspaceId;
666
+ /** Resolve canonical publisher/package identity, never the installation's runtime display name. */
667
+ async resolvePackageId(identity, options) {
668
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*\/[a-z0-9]+(?:-[a-z0-9]+)*$/.test(identity)) {
669
+ throw new Error("A canonical publisher/package identity is required");
670
+ }
671
+ const result = await this.http.request("GET", `/packages/by-package-id/${identity}`, {
672
+ params: { workspaceId: this.requireWorkspaceId(options?.workspaceId) }
673
+ });
674
+ if (!result || typeof result.id !== "string" || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(result.id)) {
675
+ throw new Error("The catalog response omitted its stable package identity");
676
+ }
677
+ return result.id;
678
+ }
679
+ /** Resolves the complete required graph without installing anything or granting capabilities. */
680
+ async preview(packageId, options) {
681
+ return this.http.request("POST", "/installations/graphs/preview", {
682
+ body: JSON.stringify({
683
+ workspaceId: this.requireWorkspaceId(options?.workspaceId),
684
+ packageId,
685
+ packageVersion: options?.packageVersion,
686
+ installPath: options?.installPath,
687
+ branch: options?.branch,
688
+ installMode: options?.installMode
689
+ }),
690
+ headers: { "Content-Type": "application/json" }
691
+ });
692
+ }
693
+ /** Explicit, single-use consent to a displayed preview. Never automatically retries accepted installs. */
694
+ async install(preview, consent) {
695
+ if (consent !== true) throw new Error("Explicit consent to the installation graph is required");
696
+ return this.http.request("POST", "/installations/graphs", {
697
+ body: JSON.stringify({
698
+ workspaceId: preview.workspaceId,
699
+ operationId: preview.operationId,
700
+ fingerprint: preview.fingerprint,
701
+ consent
702
+ }),
703
+ headers: { "Content-Type": "application/json" }
704
+ });
705
+ }
706
+ /** Inspect durable outcomes after a failure or lost response instead of resubmitting consent. */
707
+ async graphResult(operationId, options) {
708
+ return this.http.request("GET", `/installations/graphs/${encodeURIComponent(operationId)}`, {
709
+ params: { workspaceId: this.requireWorkspaceId(options?.workspaceId) }
710
+ });
711
+ }
595
712
  async list(options) {
596
713
  return this.http.request("GET", "/installations", {
597
714
  params: {
@@ -772,6 +889,176 @@ var SettingsModule = class {
772
889
  }
773
890
  };
774
891
 
892
+ // src/modules/email.ts
893
+ var EmailModule = class {
894
+ constructor(http, workspaceId) {
895
+ this.http = http;
896
+ this.workspaceId = workspaceId;
897
+ }
898
+ http;
899
+ workspaceId;
900
+ async preview(installationId, options) {
901
+ return this.http.request("GET", `/installations/${encodeURIComponent(installationId)}/email-consent-preview`, {
902
+ params: { workspaceId: this.workspace(options) }
903
+ });
904
+ }
905
+ async consents(options) {
906
+ return this.http.request("GET", "/email/consents", { params: { workspaceId: this.workspace(options) } });
907
+ }
908
+ /** Approve exactly the reviewed signed candidate; tracking stays off unless explicitly opted in. */
909
+ async consent(preview, approval, options) {
910
+ if (approval !== true) throw new Error("Explicit email consent is required");
911
+ if (!options?.senderGrants?.length) throw new Error("Explicit purpose/sender grants are required");
912
+ return this.http.request("PUT", `/email/consents/${encodeURIComponent(preview.installationId)}`, {
913
+ params: { workspaceId: preview.workspaceId },
914
+ body: JSON.stringify({
915
+ candidateDigest: preview.candidateDigest,
916
+ enabled: true,
917
+ allowOpenTracking: options?.allowOpenTracking ?? false,
918
+ allowClickTracking: options?.allowClickTracking ?? false,
919
+ senderGrants: options.senderGrants.map((grant) => ({ purpose: grant.purpose, senderId: grant.senderId }))
920
+ }),
921
+ headers: { "Content-Type": "application/json" }
922
+ });
923
+ }
924
+ async revoke(consent, options) {
925
+ return this.http.request("PUT", `/email/consents/${encodeURIComponent(consent.installationId)}`, {
926
+ params: { workspaceId: this.workspace(options) },
927
+ body: JSON.stringify({ candidateDigest: consent.candidateDigest, enabled: false, allowOpenTracking: false, allowClickTracking: false }),
928
+ headers: { "Content-Type": "application/json" }
929
+ });
930
+ }
931
+ async senders(options) {
932
+ return this.http.request("GET", "/email/senders", { params: { workspaceId: this.workspace(options) } });
933
+ }
934
+ async invoicePdf(recordId, installationId, options) {
935
+ this.mainBranch(options);
936
+ return this.http.request("POST", "/email/artifacts/invoice", {
937
+ body: JSON.stringify({ workspaceId: this.workspace(options), recordId, installationId }),
938
+ headers: { "Content-Type": "application/json" }
939
+ });
940
+ }
941
+ /** Authenticated immutable bytes; no public URL. The caller owns any local object URL it creates. */
942
+ async artifactContent(artifactId, options) {
943
+ this.mainBranch(options);
944
+ return this.http.request("GET", `/email/artifacts/${encodeURIComponent(artifactId)}/content`, {
945
+ params: { workspaceId: this.workspace(options), branch: "main" }
946
+ });
947
+ }
948
+ /** A stable key binds this exact request. After a lost response inspect status; never automatically resend. */
949
+ async dispatch(options) {
950
+ this.mainBranch(options);
951
+ return this.http.request("POST", "/email/dispatches", {
952
+ body: JSON.stringify({
953
+ workspaceId: this.workspace(options),
954
+ installationId: options.installationId,
955
+ recordId: options.recordId,
956
+ purpose: options.purpose,
957
+ templateId: options.templateId,
958
+ senderId: options.senderId,
959
+ recipients: options.recipients,
960
+ artifactIds: options.artifactIds,
961
+ idempotencyKey: options.idempotencyKey,
962
+ replyTo: options.replyTo
963
+ }),
964
+ headers: { "Content-Type": "application/json" }
965
+ });
966
+ }
967
+ async outcome(id, options) {
968
+ this.mainBranch(options);
969
+ return this.http.request("GET", `/email/dispatches/${encodeURIComponent(id)}`, {
970
+ params: { workspaceId: this.workspace(options) }
971
+ });
972
+ }
973
+ async history(options) {
974
+ this.mainBranch(options);
975
+ return this.http.request("GET", "/email/dispatches", {
976
+ params: { workspaceId: this.workspace(options), recordId: options?.recordId, installationId: options?.installationId }
977
+ });
978
+ }
979
+ async status(id, options) {
980
+ return this.outcome(id, options);
981
+ }
982
+ workspace(options) {
983
+ const value = options?.workspaceId ?? this.workspaceId;
984
+ if (!value) throw new Error("A workspace is required for transactional email");
985
+ return value;
986
+ }
987
+ mainBranch(options) {
988
+ if (options?.branch !== void 0 && options.branch !== "main")
989
+ throw new Error("Transactional email currently supports only the main business branch");
990
+ }
991
+ };
992
+
993
+ // src/modules/business.ts
994
+ var BusinessModule = class _BusinessModule {
995
+ constructor(http, branch = "main") {
996
+ this.http = http;
997
+ this.branch = branch;
998
+ }
999
+ http;
1000
+ branch;
1001
+ withBranch(branch) {
1002
+ return new _BusinessModule(this.http, branch);
1003
+ }
1004
+ async context() {
1005
+ return this.http.request("GET", "/business/context", { headers: this.headers() });
1006
+ }
1007
+ async command(command) {
1008
+ if (!command.idempotencyKey?.trim() || command.idempotencyKey.length > 200 || /[\u0000-\u001f\u007f-\u009f]/.test(command.idempotencyKey)) {
1009
+ throw new Error("A bounded printable idempotency key is required");
1010
+ }
1011
+ if (command.expectedRevision !== void 0 && !/^"[a-f0-9]{32}"$/.test(command.expectedRevision)) {
1012
+ throw new Error("An exact strong quoted canonical revision is required");
1013
+ }
1014
+ return this.http.request("POST", "/business/commands", {
1015
+ headers: this.headers(),
1016
+ body: JSON.stringify(command)
1017
+ });
1018
+ }
1019
+ async read(id) {
1020
+ return this.http.request("GET", `/business/records/${encodeURIComponent(id)}`, { headers: this.headers() });
1021
+ }
1022
+ async list(kind) {
1023
+ return this.http.request("GET", "/business/records", {
1024
+ headers: this.headers(),
1025
+ params: kind ? { kind } : {}
1026
+ });
1027
+ }
1028
+ /** null means unresolved, not success; do not invent a new operation key. */
1029
+ async outcome(request) {
1030
+ return this.http.request("POST", "/business/outcomes", {
1031
+ headers: this.headers(),
1032
+ body: JSON.stringify(request)
1033
+ });
1034
+ }
1035
+ /** Recover using the exact original command, without reproducing server fingerprint encoding. */
1036
+ async outcomeFor(command) {
1037
+ return this.outcome({ operation: command.operation, idempotencyKey: command.idempotencyKey, command });
1038
+ }
1039
+ async report(report, range = {}) {
1040
+ return this.http.request("GET", `/business/reports/${report}`, {
1041
+ headers: this.headers(),
1042
+ params: range
1043
+ });
1044
+ }
1045
+ /** Retained historical expense receipt bytes, even after the original native file is removed. */
1046
+ async receipt(nodeId, revision) {
1047
+ const response = await this.http.requestRaw("GET", `/business/receipts/${encodeURIComponent(nodeId)}/content`, {
1048
+ headers: this.headers(),
1049
+ params: { revision }
1050
+ });
1051
+ await this.http.ensureSuccess(response);
1052
+ return response.blob();
1053
+ }
1054
+ headers() {
1055
+ if (!this.branch || this.branch.length > 128 || this.branch !== this.branch.trim().toLowerCase() || /[\u0000-\u001f\u007f-\u009f]/.test(this.branch)) {
1056
+ throw new Error("Business operations require a canonical branch");
1057
+ }
1058
+ return { "X-Branch": this.branch, "Content-Type": "application/json" };
1059
+ }
1060
+ };
1061
+
775
1062
  // src/transport/header-actions.ts
776
1063
  function isHeaderAction(value) {
777
1064
  if (!value || typeof value !== "object") return false;
@@ -779,7 +1066,113 @@ function isHeaderAction(value) {
779
1066
  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");
780
1067
  }
781
1068
 
1069
+ // src/collaboration/native.ts
1070
+ var NativeCollaborationError = class extends Error {
1071
+ constructor(message, code) {
1072
+ super(message);
1073
+ this.code = code;
1074
+ this.name = "NativeCollaborationError";
1075
+ }
1076
+ code;
1077
+ };
1078
+ function record(value) {
1079
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1080
+ }
1081
+ function natural(value) {
1082
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1083
+ }
1084
+ function identifier(value) {
1085
+ return typeof value === "string" && value.length > 0 && value.length <= 256;
1086
+ }
1087
+ function pointer(value) {
1088
+ return record(value) && typeof value.x === "number" && Number.isFinite(value.x) && typeof value.y === "number" && Number.isFinite(value.y);
1089
+ }
1090
+ function element(value) {
1091
+ return record(value) && identifier(value.id) && natural(value.version) && (value.versionNonce === void 0 || natural(value.versionNonce)) && (value.isDeleted === void 0 || typeof value.isDeleted === "boolean");
1092
+ }
1093
+ function files(value) {
1094
+ return record(value) && Object.keys(value).length <= 1e4 && Object.entries(value).every(([id, file]) => record(file) && identifier(id) && file.id === id && typeof file.dataURL === "string" && file.dataURL.length <= 10 * 1024 * 1024 && typeof file.mimeType === "string" && file.mimeType.length > 0 && file.mimeType.length <= 128 && natural(file.created) && (file.lastRetrieved === void 0 || natural(file.lastRetrieved)));
1095
+ }
1096
+ function isNativeSnapshot(value) {
1097
+ if (!record(value) || !natural(value.sequence) || !natural(value.savedSequence) || value.savedSequence > value.sequence || typeof value.revision !== "string" || !value.revision || value.revision.length > 1024 || typeof value.readOnly !== "boolean" || !record(value.scene) || !Array.isArray(value.scene.elements) || value.scene.elements.length > 1e4 || !value.scene.elements.every(element) || !files(value.scene.files) || !record(value.scene.appState) || !Array.isArray(value.participants) || value.participants.length > 20)
1098
+ return false;
1099
+ const ids = value.scene.elements.map((item) => item.id);
1100
+ if (new Set(ids).size !== ids.length) return false;
1101
+ const connectionIds = value.participants.map((item) => record(item) ? item.id : void 0);
1102
+ if (new Set(connectionIds).size !== connectionIds.length) return false;
1103
+ return value.participants.every((item) => record(item) && identifier(item.id) && typeof item.name === "string" && item.name.length <= 1024 && typeof item.readOnly === "boolean" && (item.pointer === null || pointer(item.pointer)) && (item.kind === void 0 || item.kind === "user" || item.kind === "agent"));
1104
+ }
1105
+ function isNativeSaveResult(value) {
1106
+ return isNativeSnapshot(value) && record(value) && record(value.receipt) && natural(value.receipt.generation) && identifier(value.receipt.correlation) && typeof value.receipt.revision === "string" && value.receipt.revision.length > 0 && value.receipt.revision.length <= 1024;
1107
+ }
1108
+ function validateNativeCommand(command) {
1109
+ if (!record(command)) throw new Error("Invalid collaboration command.");
1110
+ switch (command.operation) {
1111
+ case "join":
1112
+ case "snapshot":
1113
+ case "close":
1114
+ break;
1115
+ case "presence":
1116
+ if (command.pointer !== null && !pointer(command.pointer))
1117
+ throw new Error("Invalid collaboration pointer.");
1118
+ break;
1119
+ case "save":
1120
+ if (!natural(command.generation) || !identifier(command.correlation))
1121
+ throw new Error("Invalid collaboration save frontier.");
1122
+ break;
1123
+ case "apply": {
1124
+ const changes = command.changes;
1125
+ if (!record(changes) || !identifier(changes.operationId) || !Array.isArray(changes.changes) || changes.changes.length > 1e4 || !changes.changes.every((change) => record(change) && identifier(change.id) && (change.expectedVersion === null || natural(change.expectedVersion)) && (change.element === null || element(change.element) && change.element.id === change.id)) || changes.files !== void 0 && !files(changes.files) || (changes.appState !== void 0 ? !record(changes.appState) || !natural(changes.appStateExpectedSequence) : changes.appStateExpectedSequence !== void 0))
1126
+ throw new Error("Invalid collaboration changes.");
1127
+ const ids = changes.changes.map((change) => change.id);
1128
+ if (new Set(ids).size !== ids.length) throw new Error("Duplicate collaboration element changes.");
1129
+ break;
1130
+ }
1131
+ default:
1132
+ throw new Error("Unsupported collaboration command.");
1133
+ }
1134
+ if (new TextEncoder().encode(JSON.stringify(command)).byteLength > 10 * 1024 * 1024)
1135
+ throw new Error("Collaboration request exceeds the document limit.");
1136
+ }
1137
+ var NativeCollaborationClient = class {
1138
+ constructor(bridge) {
1139
+ this.bridge = bridge;
1140
+ }
1141
+ bridge;
1142
+ async snapshotRequest(command) {
1143
+ if (!this.bridge) throw new Error("Native collaboration requires an installed workspace host.");
1144
+ const result = await this.bridge.requestNativeCollaboration(command);
1145
+ if (!isNativeSnapshot(result)) throw new Error("Invalid native collaboration snapshot.");
1146
+ return result;
1147
+ }
1148
+ join() {
1149
+ return this.snapshotRequest({ operation: "join" });
1150
+ }
1151
+ snapshot() {
1152
+ return this.snapshotRequest({ operation: "snapshot" });
1153
+ }
1154
+ apply(changes) {
1155
+ return this.snapshotRequest({ operation: "apply", changes });
1156
+ }
1157
+ presence(value) {
1158
+ return this.snapshotRequest({ operation: "presence", pointer: value });
1159
+ }
1160
+ async save(generation, correlation) {
1161
+ if (!this.bridge) throw new Error("Native collaboration requires an installed workspace host.");
1162
+ const result = await this.bridge.requestNativeCollaboration({ operation: "save", generation, correlation });
1163
+ if (!isNativeSaveResult(result) || result.receipt.generation !== generation || result.receipt.correlation !== correlation)
1164
+ throw new Error("Invalid native collaboration save receipt.");
1165
+ return result;
1166
+ }
1167
+ async close() {
1168
+ if (!this.bridge) throw new Error("Native collaboration requires an installed workspace host.");
1169
+ const result = await this.bridge.requestNativeCollaboration({ operation: "close" });
1170
+ if (result !== null) throw new Error("Invalid native collaboration close response.");
1171
+ }
1172
+ };
1173
+
782
1174
  // src/transport/bridge.transport.ts
1175
+ var HOST_MESSAGE_CHANNEL_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
783
1176
  var Bridge = class {
784
1177
  constructor(expectedHostOrigin) {
785
1178
  this.expectedHostOrigin = expectedHostOrigin;
@@ -806,8 +1199,10 @@ var Bridge = class {
806
1199
  sessionId = null;
807
1200
  actions = /* @__PURE__ */ new Map();
808
1201
  requests = /* @__PURE__ */ new Set();
809
- backendRequests = /* @__PURE__ */ new Map();
1202
+ hostMessageHandlers = /* @__PURE__ */ new Map();
1203
+ hostMessageRequests = /* @__PURE__ */ new Set();
810
1204
  wopiRequests = /* @__PURE__ */ new Map();
1205
+ collaborationRequests = /* @__PURE__ */ new Map();
811
1206
  /** Post `verentis:ready` to the host to start the handshake */
812
1207
  sendReady(capabilities) {
813
1208
  console.info("[Verentis SDK] Sending verentis:ready to host");
@@ -869,28 +1264,6 @@ var Bridge = class {
869
1264
  setDirty(isDirty) {
870
1265
  this.postToHost({ type: "verentis:dirty", version: 1, isDirty });
871
1266
  }
872
- /** The host, not the app, selects the installation, file, branch and permission ceiling. */
873
- requestBackendCredential() {
874
- if (this._destroyed || !this.sessionId || !this._hostOrigin)
875
- return Promise.reject(new Error("An initialized workspace host is required."));
876
- if (this.backendRequests.size >= 4)
877
- return Promise.reject(new Error("A backend credential request is already pending."));
878
- const requestId = crypto.randomUUID();
879
- return new Promise((resolve, reject) => {
880
- const timer = setTimeout(() => {
881
- this.backendRequests.delete(requestId);
882
- reject(new Error("Backend authorization timed out."));
883
- }, 2e4);
884
- this.backendRequests.set(requestId, { resolve, reject, timer });
885
- try {
886
- this.postToHost({ type: "verentis:backend:request", version: 1, requestId, sessionId: this.sessionId });
887
- } catch {
888
- clearTimeout(timer);
889
- this.backendRequests.delete(requestId);
890
- reject(new Error("The workspace host is unavailable."));
891
- }
892
- });
893
- }
894
1267
  /** The trusted parent selects the approved installation, document and action. */
895
1268
  requestWopiLaunch() {
896
1269
  return this.requestWopi("launch");
@@ -938,6 +1311,38 @@ var Bridge = class {
938
1311
  }
939
1312
  });
940
1313
  }
1314
+ requestNativeCollaboration(command) {
1315
+ if (this._destroyed || !this.sessionId || !this._hostOrigin)
1316
+ return Promise.reject(new Error("An initialized workspace host is required."));
1317
+ if (this.collaborationRequests.size >= 4)
1318
+ return Promise.reject(new Error("Too many native collaboration requests."));
1319
+ try {
1320
+ validateNativeCommand(command);
1321
+ } catch (error) {
1322
+ return Promise.reject(error);
1323
+ }
1324
+ const requestId = crypto.randomUUID();
1325
+ return new Promise((resolve, reject) => {
1326
+ const timer = setTimeout(() => {
1327
+ this.collaborationRequests.delete(requestId);
1328
+ reject(new NativeCollaborationError("Native collaboration timed out.", "unavailable"));
1329
+ }, 3e4);
1330
+ this.collaborationRequests.set(requestId, { resolve, reject, timer });
1331
+ try {
1332
+ this.postToHost({
1333
+ type: "verentis:collaboration:request",
1334
+ version: 1,
1335
+ requestId,
1336
+ sessionId: this.sessionId,
1337
+ command
1338
+ });
1339
+ } catch (error) {
1340
+ clearTimeout(timer);
1341
+ this.collaborationRequests.delete(requestId);
1342
+ reject(error);
1343
+ }
1344
+ });
1345
+ }
941
1346
  registerAction(descriptor, handler) {
942
1347
  if (!this.sessionId || this._destroyed) throw new Error("Header actions require an initialized workspace host.");
943
1348
  if (!isHeaderAction(descriptor)) throw new Error("Invalid header action.");
@@ -1001,6 +1406,49 @@ var Bridge = class {
1001
1406
  this.publishActions();
1002
1407
  }
1003
1408
  }
1409
+ /**
1410
+ * Handle messages the workspace sends on a channel declared in the manifest `spec.messages` — for
1411
+ * example an agent asking the open app to select a record. The handler's return value is the reply;
1412
+ * a thrown error is reported back as the failure. May be called before the init handshake.
1413
+ */
1414
+ onHostMessage(channel, handler) {
1415
+ if (this._destroyed) throw new Error("Bridge has been destroyed.");
1416
+ if (!HOST_MESSAGE_CHANNEL_PATTERN.test(channel)) throw new Error(`Invalid host message channel '${channel}'.`);
1417
+ if (this.hostMessageHandlers.has(channel)) throw new Error(`Channel '${channel}' already has a handler.`);
1418
+ this.hostMessageHandlers.set(channel, handler);
1419
+ return () => {
1420
+ if (this.hostMessageHandlers.get(channel) === handler) this.hostMessageHandlers.delete(channel);
1421
+ };
1422
+ }
1423
+ async receiveHostMessage(message) {
1424
+ if (!this._hostOrigin || message.sessionId !== this.sessionId || typeof message.channel !== "string" || typeof message.requestId !== "string" || !message.requestId || message.requestId.length > 128 || this.hostMessageRequests.has(message.requestId)) return;
1425
+ this.hostMessageRequests.add(message.requestId);
1426
+ if (this.hostMessageRequests.size > 1e3) this.hostMessageRequests.delete(this.hostMessageRequests.values().next().value);
1427
+ const reply = {
1428
+ type: "verentis:host:reply",
1429
+ version: 1,
1430
+ sessionId: message.sessionId,
1431
+ requestId: message.requestId,
1432
+ channel: message.channel
1433
+ };
1434
+ const handler = this.hostMessageHandlers.get(message.channel);
1435
+ if (!handler) {
1436
+ this.postToHost({ ...reply, error: `The application does not handle '${message.channel.slice(0, 64)}' messages.` });
1437
+ return;
1438
+ }
1439
+ let response;
1440
+ try {
1441
+ response = { result: await handler(message.payload) };
1442
+ } catch (error) {
1443
+ response = { error: (error instanceof Error ? error.message : "Message failed.").slice(0, 500) || "Message failed." };
1444
+ }
1445
+ if (this._destroyed || this.sessionId !== message.sessionId) return;
1446
+ try {
1447
+ this.postToHost({ ...reply, ...response });
1448
+ } catch {
1449
+ this.postToHost({ ...reply, error: "The reply could not be sent to the workspace." });
1450
+ }
1451
+ }
1004
1452
  /** Subscribe to a specific message type from the host */
1005
1453
  on(type, listener) {
1006
1454
  if (!this.listeners.has(type)) {
@@ -1019,17 +1467,19 @@ var Bridge = class {
1019
1467
  if (!this._hostOrigin) this._initReject(new Error("Bridge has been destroyed."));
1020
1468
  this.actions.clear();
1021
1469
  this.requests.clear();
1470
+ this.hostMessageHandlers.clear();
1471
+ this.hostMessageRequests.clear();
1022
1472
  this.listeners.clear();
1023
- for (const pending of this.backendRequests.values()) {
1473
+ for (const pending of this.wopiRequests.values()) {
1024
1474
  clearTimeout(pending.timer);
1025
1475
  pending.reject(new Error("Bridge has been destroyed."));
1026
1476
  }
1027
- this.backendRequests.clear();
1028
- for (const pending of this.wopiRequests.values()) {
1477
+ this.wopiRequests.clear();
1478
+ for (const pending of this.collaborationRequests.values()) {
1029
1479
  clearTimeout(pending.timer);
1030
1480
  pending.reject(new Error("Bridge has been destroyed."));
1031
1481
  }
1032
- this.wopiRequests.clear();
1482
+ this.collaborationRequests.clear();
1033
1483
  if (typeof window !== "undefined") {
1034
1484
  window.removeEventListener("message", this._messageHandler);
1035
1485
  }
@@ -1055,26 +1505,28 @@ var Bridge = class {
1055
1505
  const initData = data;
1056
1506
  this._initResolve({ context: initData.context, token: initData.token });
1057
1507
  }
1058
- if (data.type === "verentis:backend:response") {
1508
+ if (data.type === "verentis:wopi:response") {
1059
1509
  if (!this._hostOrigin || data.sessionId !== this.sessionId) return;
1060
- const pending = this.backendRequests.get(data.requestId);
1510
+ const pending = this.wopiRequests.get(data.requestId);
1061
1511
  if (!pending) return;
1062
1512
  clearTimeout(pending.timer);
1063
- this.backendRequests.delete(data.requestId);
1064
- const value = data.credential;
1065
- 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())
1066
- pending.reject(new Error("Backend authorization was denied or is not configured."));
1067
- else pending.resolve(value);
1513
+ this.wopiRequests.delete(data.requestId);
1514
+ if (data.error || !Object.hasOwn(data, "result"))
1515
+ pending.reject(new Error("Collaboration authorization is unavailable."));
1516
+ else pending.resolve(data.result);
1068
1517
  return;
1069
1518
  }
1070
- if (data.type === "verentis:wopi:response") {
1519
+ if (data.type === "verentis:collaboration:response") {
1071
1520
  if (!this._hostOrigin || data.sessionId !== this.sessionId) return;
1072
- const pending = this.wopiRequests.get(data.requestId);
1521
+ const pending = this.collaborationRequests.get(data.requestId);
1073
1522
  if (!pending) return;
1074
1523
  clearTimeout(pending.timer);
1075
- this.wopiRequests.delete(data.requestId);
1524
+ this.collaborationRequests.delete(data.requestId);
1076
1525
  if (data.error || !Object.hasOwn(data, "result"))
1077
- pending.reject(new Error("Collaboration authorization is unavailable."));
1526
+ pending.reject(new NativeCollaborationError(
1527
+ typeof data.error === "string" ? data.error.slice(0, 500) : "Native collaboration is unavailable.",
1528
+ typeof data.code === "string" ? data.code.slice(0, 64) : "unavailable"
1529
+ ));
1078
1530
  else pending.resolve(data.result);
1079
1531
  return;
1080
1532
  }
@@ -1082,6 +1534,10 @@ var Bridge = class {
1082
1534
  void this.invokeAction(data);
1083
1535
  return;
1084
1536
  }
1537
+ if (data.type === "verentis:host:message") {
1538
+ void this.receiveHostMessage(data);
1539
+ return;
1540
+ }
1085
1541
  const typeListeners = this.listeners.get(data.type);
1086
1542
  if (typeListeners) {
1087
1543
  for (const listener of typeListeners) {
@@ -1093,16 +1549,18 @@ var Bridge = class {
1093
1549
 
1094
1550
  // src/types.ts
1095
1551
  var ApiError = class extends Error {
1096
- constructor(status, detail, title) {
1552
+ constructor(status, detail, title, code) {
1097
1553
  super(title ?? `API error ${status}: ${detail}`);
1098
1554
  this.status = status;
1099
1555
  this.detail = detail;
1100
1556
  this.title = title;
1557
+ this.code = code;
1101
1558
  this.name = "ApiError";
1102
1559
  }
1103
1560
  status;
1104
1561
  detail;
1105
1562
  title;
1563
+ code;
1106
1564
  };
1107
1565
 
1108
1566
  // src/transport/http.transport.ts
@@ -1131,24 +1589,7 @@ var HttpTransport = class {
1131
1589
  headers,
1132
1590
  body: options?.body
1133
1591
  });
1134
- if (!response.ok) {
1135
- let detail = "";
1136
- let title;
1137
- try {
1138
- const body = await response.json();
1139
- detail = body.detail ?? body.message ?? JSON.stringify(body);
1140
- title = body.title;
1141
- } catch {
1142
- detail = await response.text().catch(() => response.statusText);
1143
- }
1144
- console.error("[Verentis SDK] Request failed", {
1145
- method,
1146
- path,
1147
- status: response.status,
1148
- detail
1149
- });
1150
- throw new ApiError(response.status, detail, title);
1151
- }
1592
+ await this.ensureSuccess(response);
1152
1593
  console.info("[Verentis SDK] Request succeeded", {
1153
1594
  method,
1154
1595
  path,
@@ -1182,6 +1623,26 @@ var HttpTransport = class {
1182
1623
  });
1183
1624
  return response;
1184
1625
  }
1626
+ async ensureSuccess(response) {
1627
+ if (response.ok) return;
1628
+ const raw = await response.text();
1629
+ let detail = raw || response.statusText;
1630
+ let title;
1631
+ let code;
1632
+ try {
1633
+ const body = JSON.parse(raw);
1634
+ if (body !== null && typeof body === "object") {
1635
+ if ("detail" in body && typeof body.detail === "string") detail = body.detail;
1636
+ else if ("message" in body && typeof body.message === "string") detail = body.message;
1637
+ if ("title" in body && typeof body.title === "string") title = body.title;
1638
+ if ("code" in body && typeof body.code === "string") code = body.code;
1639
+ }
1640
+ } catch (error) {
1641
+ if (!(error instanceof SyntaxError)) throw error;
1642
+ }
1643
+ console.error("[Verentis SDK] Request failed", { status: response.status, code });
1644
+ throw new ApiError(response.status, detail, title, code);
1645
+ }
1185
1646
  buildUrl(path, params) {
1186
1647
  const base = `${this.baseUrl}/${this.apiVersion}${path}`;
1187
1648
  if (!params) return base;
@@ -1211,8 +1672,11 @@ var VerentisClient = class {
1211
1672
  installations;
1212
1673
  resources;
1213
1674
  settings;
1675
+ email;
1676
+ business;
1214
1677
  context;
1215
1678
  bridge;
1679
+ collaboration;
1216
1680
  _auth;
1217
1681
  _http;
1218
1682
  _ready;
@@ -1221,11 +1685,13 @@ var VerentisClient = class {
1221
1685
  const environment = detectEnvironment();
1222
1686
  if (environment === "iframe" && !options?.auth) {
1223
1687
  this.bridge = new Bridge();
1688
+ this.collaboration = new NativeCollaborationClient(this.bridge);
1224
1689
  const bridgeAuth = new BridgeAuth("*");
1225
1690
  this._auth = bridgeAuth;
1226
1691
  this._ready = this.initFromBridge(bridgeAuth);
1227
1692
  } else {
1228
1693
  this.bridge = null;
1694
+ this.collaboration = new NativeCollaborationClient(null);
1229
1695
  this._auth = createAuthStrategy(options);
1230
1696
  const apiUrl = options?.apiUrl;
1231
1697
  if (!apiUrl) {
@@ -1242,6 +1708,8 @@ var VerentisClient = class {
1242
1708
  this.installations = new InstallationsModule(this._http, options?.workspaceId);
1243
1709
  this.resources = new ResourcesModule(this._http, options?.workspaceId);
1244
1710
  this.settings = new SettingsModule(this._http, options?.workspaceId);
1711
+ this.email = new EmailModule(this._http, options?.workspaceId);
1712
+ this.business = new BusinessModule(this._http);
1245
1713
  this._ready = Promise.resolve();
1246
1714
  return;
1247
1715
  }
@@ -1254,6 +1722,14 @@ var VerentisClient = class {
1254
1722
  whenReady() {
1255
1723
  return this._ready;
1256
1724
  }
1725
+ /**
1726
+ * The current `Authorization` header value, refreshed when near expiry.
1727
+ * Forward it to the app's own backend, which exchanges it at Security's
1728
+ * `/connect/token` (RFC 8693) as the installation's confidential client.
1729
+ */
1730
+ getAuthorizationHeader() {
1731
+ return this._auth.getAuthHeader();
1732
+ }
1257
1733
  /** Clean up resources (timers, event listeners) */
1258
1734
  destroy() {
1259
1735
  this._auth.destroy?.();
@@ -1278,6 +1754,8 @@ var VerentisClient = class {
1278
1754
  this.installations = new InstallationsModule(http, context.workspace.id);
1279
1755
  this.resources = new ResourcesModule(http, context.workspace.id);
1280
1756
  this.settings = new SettingsModule(http, context.workspace.id);
1757
+ this.email = new EmailModule(http, context.workspace.id);
1758
+ this.business = new BusinessModule(http);
1281
1759
  }
1282
1760
  };
1283
1761
  function detectEnvironment() {
@@ -1330,11 +1808,49 @@ function stripParams(mime) {
1330
1808
  return idx === -1 ? mime.trim() : mime.slice(0, idx).trim();
1331
1809
  }
1332
1810
 
1811
+ // src/collaboration/lifecycle.ts
1812
+ function isRecord(value) {
1813
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1814
+ }
1815
+ function exactHttpsOrigin(value) {
1816
+ if (typeof value !== "string" || value.length > 256)
1817
+ throw new Error("Invalid framing origin.");
1818
+ const origin = new URL(value);
1819
+ if (origin.protocol !== "https:" || origin.origin !== value || origin.href !== `${value}/` || !/^[a-z0-9-]+(?:\.[a-z0-9-]+)*$/.test(origin.hostname))
1820
+ throw new Error("Invalid framing origin.");
1821
+ return value;
1822
+ }
1823
+ function isTrustedMessage(event, source, origin) {
1824
+ return Boolean(source && origin && event.source === source && event.origin === origin);
1825
+ }
1826
+ function validateCollaborationDocument(value, context, now = Date.now()) {
1827
+ const branch = context.file?.branch;
1828
+ const node = context.file?.nodeId;
1829
+ const workspace = context.workspace?.id;
1830
+ if (!branch || !node || !workspace || !isRecord(value) || value.nodeId !== node || value.workspaceId !== workspace || value.branch !== branch.trim().toLowerCase() || typeof value.fileId !== "string" || !/^[a-f0-9]{32}$/.test(value.fileId) || typeof value.sessionId !== "string" || !/^(?:[a-f0-9]{32}|[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12})$/.test(value.sessionId) || typeof value.accessTokenTtl !== "number" || !Number.isSafeInteger(value.accessTokenTtl) || value.accessTokenTtl <= now || typeof value.name !== "string" || !value.name || value.name.length > 1024 || typeof value.readOnly !== "boolean")
1831
+ throw new Error("Invalid live launch.");
1832
+ return {
1833
+ sessionId: value.sessionId,
1834
+ workspaceId: value.workspaceId,
1835
+ branch: value.branch,
1836
+ nodeId: value.nodeId,
1837
+ fileId: value.fileId,
1838
+ name: value.name,
1839
+ readOnly: value.readOnly,
1840
+ accessTokenTtl: value.accessTokenTtl
1841
+ };
1842
+ }
1843
+ function isDurableSaveConfirmed(result, request, currentGeneration, editorModified) {
1844
+ if (!isRecord(request) || !isRecord(result) || !isRecord(result.receipt))
1845
+ return false;
1846
+ return editorModified === false && result.state === "ready" && result.continuationAvailable !== true && Number.isSafeInteger(currentGeneration) && currentGeneration >= 0 && currentGeneration === request.generation && typeof request.correlation === "string" && request.correlation.length > 0 && result.receipt.generation === request.generation && result.receipt.correlation === request.correlation && typeof result.receipt.revision === "string" && result.receipt.revision.length > 0;
1847
+ }
1848
+
1333
1849
  // src/index.ts
1334
1850
  function createVerentisClient(options) {
1335
1851
  return new VerentisClient(options);
1336
1852
  }
1337
1853
 
1338
- export { ApiKeyAuth, Bridge, BridgeAuth, ContextModule, EnginesModule, ExecutionsModule, FilesModule, HttpTransport, InstallationsModule, ResourcesModule, SettingsModule, TokenAuth, VerentisClient, createVerentisClient, findBestMatch, isHeaderAction, isTerminalStatus, matchMimeType };
1854
+ export { ApiError, ApiKeyAuth, Bridge, BridgeAuth, BusinessModule, ContextModule, EmailModule, EnginesModule, ExecutionsModule, FilesModule, HOST_MESSAGE_CHANNEL_PATTERN, HttpTransport, InstallationsModule, NativeCollaborationClient, NativeCollaborationError, ResourcesModule, SettingsModule, TokenAuth, VerentisClient, createVerentisClient, exactHttpsOrigin, findBestMatch, isDurableSaveConfirmed, isHeaderAction, isNativeSaveResult, isNativeSnapshot, isTerminalStatus, isTrustedMessage, matchMimeType, validateCollaborationDocument, validateNativeCommand };
1339
1855
  //# sourceMappingURL=index.js.map
1340
1856
  //# sourceMappingURL=index.js.map