@verentis/sdk 0.2.4 → 0.2.6

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
@@ -169,6 +169,9 @@ var ContextModule = class {
169
169
  _file = null;
170
170
  _entryPoint = null;
171
171
  _apiUrl = null;
172
+ _application = null;
173
+ _installation = null;
174
+ _grants = null;
172
175
  /** The current workspace */
173
176
  get workspace() {
174
177
  if (!this._workspace) {
@@ -209,13 +212,39 @@ var ContextModule = class {
209
212
  get hasEntryPoint() {
210
213
  return this._entryPoint !== null;
211
214
  }
215
+ get application() {
216
+ return this._application ? { ...this._application } : null;
217
+ }
218
+ get installation() {
219
+ return this._installation ? { ...this._installation } : null;
220
+ }
221
+ /**
222
+ * The boundaries of a brokered token (canvas documents), as the host sent them with the token; `null` when the
223
+ * token is not boundary-confined by a broker (or in standalone mode).
224
+ */
225
+ get grants() {
226
+ return this._grants ? this._grants.map((grant) => ({ ...grant })) : null;
227
+ }
228
+ /** Update the token's grants from the host's init or token refresh message */
229
+ setGrants(grants) {
230
+ if (!Array.isArray(grants)) {
231
+ this._grants = null;
232
+ return;
233
+ }
234
+ this._grants = grants.filter((grant) => typeof grant === "object" && grant !== null && typeof grant.type === "string" && typeof grant.identifier === "string" && typeof grant.path === "string" && grant.path.startsWith("/") && (grant.mode === "read" || grant.mode === "write") && typeof grant.source === "string").map((grant) => ({ type: grant.type, identifier: grant.identifier, path: grant.path, mode: grant.mode, source: grant.source }));
235
+ }
212
236
  /** Update context from the host's init message */
213
237
  setFromAppContext(ctx) {
238
+ 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))) {
239
+ throw new Error("Invalid application launch metadata");
240
+ }
214
241
  this._workspace = ctx.workspace;
215
242
  this._surface = ctx.surface ?? "file";
216
243
  this._file = ctx.file ?? null;
217
244
  this._entryPoint = ctx.entryPoint ?? null;
218
245
  this._apiUrl = ctx.apiUrl;
246
+ this._application = ctx.application ? { ...ctx.application } : null;
247
+ this._installation = ctx.installation ? { ...ctx.installation } : null;
219
248
  }
220
249
  /** Set workspace info directly (standalone mode) */
221
250
  setWorkspace(workspace) {
@@ -443,6 +472,49 @@ var FilesModule = class {
443
472
  headers: this.workspaceHeaders()
444
473
  });
445
474
  }
475
+ /** Read metadata by stable node identity on an explicit branch. */
476
+ async getNode(nodeId, options) {
477
+ requireNonBlank(nodeId, "Node ID");
478
+ requireNonBlank(options.branch, "Branch");
479
+ return this.http.request("GET", `/nodes/${encodeURIComponent(nodeId)}/file`, {
480
+ headers: { ...this.workspaceHeaders(), "X-Branch": options.branch }
481
+ });
482
+ }
483
+ /** Read bytes and their matching content revision in the same response. */
484
+ async readNodeContent(nodeId, options) {
485
+ requireNonBlank(nodeId, "Node ID");
486
+ requireNonBlank(options.branch, "Branch");
487
+ const response = await this.http.requestRaw("GET", `/nodes/${encodeURIComponent(nodeId)}/content`, {
488
+ headers: { ...this.workspaceHeaders(), "X-Branch": options.branch, Accept: "*/*" }
489
+ });
490
+ await this.http.ensureSuccess(response);
491
+ const revision = response.headers.get("ETag");
492
+ if (!revision || !/^"[a-fA-F0-9]{32}"$/.test(revision)) {
493
+ throw new Error("Node-content response did not include a valid strong revision ETag.");
494
+ }
495
+ return { content: await response.blob(), revision };
496
+ }
497
+ /** Replace existing node bytes conditionally; never creates or changes path/MIME. */
498
+ async writeNodeContent(nodeId, options) {
499
+ requireNonBlank(nodeId, "Node ID");
500
+ requireNonBlank(options.branch, "Branch");
501
+ requireNonBlank(options.idempotencyKey, "Idempotency key");
502
+ const revision = /^[a-fA-F0-9]{32}$/.test(options.revision) ? `"${options.revision}"` : options.revision;
503
+ if (!/^"[a-fA-F0-9]{32}"$/.test(revision)) {
504
+ throw new Error("A single strong node-content revision is required.");
505
+ }
506
+ const content = typeof options.content === "string" ? new Blob([options.content]) : options.content instanceof ArrayBuffer ? new Blob([options.content]) : options.content;
507
+ return this.http.request("PUT", `/nodes/${encodeURIComponent(nodeId)}/content`, {
508
+ body: content,
509
+ headers: {
510
+ ...this.workspaceHeaders(),
511
+ "X-Branch": options.branch,
512
+ "If-Match": revision,
513
+ "Idempotency-Key": options.idempotencyKey,
514
+ "Content-Type": "application/octet-stream"
515
+ }
516
+ });
517
+ }
446
518
  /**
447
519
  * Read the raw content of a file as a Blob.
448
520
  */
@@ -534,11 +606,18 @@ var FilesModule = class {
534
606
  if (options.contentType) metadata.contentType = options.contentType;
535
607
  if (options.branch) metadata.branch = options.branch;
536
608
  formData.append("metadata", JSON.stringify(metadata));
537
- await this.http.request("POST", `/files/${normalizedPath}`, {
609
+ await this.http.request("POST", `/files/${normalizedPath.split("/").map(encodeURIComponent).join("/")}`, {
538
610
  body: formData,
539
- headers: this.workspaceHeaders()
611
+ headers: {
612
+ ...this.workspaceHeaders(),
613
+ ...options.createOnly ? { "If-None-Match": "*" } : {}
614
+ }
540
615
  });
541
616
  }
617
+ /** Create without a read-before-write race or overwrite of an existing live file. */
618
+ async create(path, options) {
619
+ await this.write(path, { ...options, createOnly: true });
620
+ }
542
621
  /**
543
622
  * Move or rename a file.
544
623
  */
@@ -559,14 +638,19 @@ var FilesModule = class {
559
638
  * Delete a file.
560
639
  */
561
640
  async remove(options) {
562
- await this.http.request("DELETE", "/files", {
563
- body: JSON.stringify({
564
- path: normalizePath(options.path),
565
- branch: options.branch
566
- }),
641
+ const path = normalizePath(options.path);
642
+ requireNonBlank(path, "File path");
643
+ const branch = options.branch ?? "main";
644
+ requireNonBlank(branch, "Branch");
645
+ if (options.headStamp !== void 0) requireNonBlank(options.headStamp, "Path head stamp");
646
+ if (options.idempotencyKey !== void 0) requireNonBlank(options.idempotencyKey, "Idempotency key");
647
+ await this.http.request("DELETE", `/files/${path.split("/").map(encodeURIComponent).join("/")}`, {
648
+ params: { branch },
567
649
  headers: {
568
650
  ...this.workspaceHeaders(),
569
- "Content-Type": "application/json"
651
+ ...options.headStamp === void 0 ? {} : { "If-Match": options.headStamp },
652
+ ...options.idempotencyKey === void 0 ? {} : { "Idempotency-Key": options.idempotencyKey },
653
+ ...options.mode === void 0 ? {} : { "X-Delete-Mode": options.mode === "deep" ? "Deep" : "Shallow" }
570
654
  }
571
655
  });
572
656
  }
@@ -578,6 +662,9 @@ var FilesModule = class {
578
662
  return headers;
579
663
  }
580
664
  };
665
+ function requireNonBlank(value, name) {
666
+ if (value.trim().length === 0) throw new Error(`${name} is required.`);
667
+ }
581
668
  function normalizePath(path) {
582
669
  return path.replace(/\/+/g, "/").replace(/^\//, "").replace(/\/$/, "");
583
670
  }
@@ -594,6 +681,52 @@ var InstallationsModule = class {
594
681
  }
595
682
  http;
596
683
  workspaceId;
684
+ /** Resolve canonical publisher/package identity, never the installation's runtime display name. */
685
+ async resolvePackageId(identity, options) {
686
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*\/[a-z0-9]+(?:-[a-z0-9]+)*$/.test(identity)) {
687
+ throw new Error("A canonical publisher/package identity is required");
688
+ }
689
+ const result = await this.http.request("GET", `/packages/by-package-id/${identity}`, {
690
+ params: { workspaceId: this.requireWorkspaceId(options?.workspaceId) }
691
+ });
692
+ if (!result || typeof result.id !== "string" || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(result.id)) {
693
+ throw new Error("The catalog response omitted its stable package identity");
694
+ }
695
+ return result.id;
696
+ }
697
+ /** Resolves the complete required graph without installing anything or granting capabilities. */
698
+ async preview(packageId, options) {
699
+ return this.http.request("POST", "/installations/graphs/preview", {
700
+ body: JSON.stringify({
701
+ workspaceId: this.requireWorkspaceId(options?.workspaceId),
702
+ packageId,
703
+ packageVersion: options?.packageVersion,
704
+ installPath: options?.installPath,
705
+ branch: options?.branch,
706
+ installMode: options?.installMode
707
+ }),
708
+ headers: { "Content-Type": "application/json" }
709
+ });
710
+ }
711
+ /** Explicit, single-use consent to a displayed preview. Never automatically retries accepted installs. */
712
+ async install(preview, consent) {
713
+ if (consent !== true) throw new Error("Explicit consent to the installation graph is required");
714
+ return this.http.request("POST", "/installations/graphs", {
715
+ body: JSON.stringify({
716
+ workspaceId: preview.workspaceId,
717
+ operationId: preview.operationId,
718
+ fingerprint: preview.fingerprint,
719
+ consent
720
+ }),
721
+ headers: { "Content-Type": "application/json" }
722
+ });
723
+ }
724
+ /** Inspect durable outcomes after a failure or lost response instead of resubmitting consent. */
725
+ async graphResult(operationId, options) {
726
+ return this.http.request("GET", `/installations/graphs/${encodeURIComponent(operationId)}`, {
727
+ params: { workspaceId: this.requireWorkspaceId(options?.workspaceId) }
728
+ });
729
+ }
597
730
  async list(options) {
598
731
  return this.http.request("GET", "/installations", {
599
732
  params: {
@@ -774,6 +907,176 @@ var SettingsModule = class {
774
907
  }
775
908
  };
776
909
 
910
+ // src/modules/email.ts
911
+ var EmailModule = class {
912
+ constructor(http, workspaceId) {
913
+ this.http = http;
914
+ this.workspaceId = workspaceId;
915
+ }
916
+ http;
917
+ workspaceId;
918
+ async preview(installationId, options) {
919
+ return this.http.request("GET", `/installations/${encodeURIComponent(installationId)}/email-consent-preview`, {
920
+ params: { workspaceId: this.workspace(options) }
921
+ });
922
+ }
923
+ async consents(options) {
924
+ return this.http.request("GET", "/email/consents", { params: { workspaceId: this.workspace(options) } });
925
+ }
926
+ /** Approve exactly the reviewed signed candidate; tracking stays off unless explicitly opted in. */
927
+ async consent(preview, approval, options) {
928
+ if (approval !== true) throw new Error("Explicit email consent is required");
929
+ if (!options?.senderGrants?.length) throw new Error("Explicit purpose/sender grants are required");
930
+ return this.http.request("PUT", `/email/consents/${encodeURIComponent(preview.installationId)}`, {
931
+ params: { workspaceId: preview.workspaceId },
932
+ body: JSON.stringify({
933
+ candidateDigest: preview.candidateDigest,
934
+ enabled: true,
935
+ allowOpenTracking: options?.allowOpenTracking ?? false,
936
+ allowClickTracking: options?.allowClickTracking ?? false,
937
+ senderGrants: options.senderGrants.map((grant) => ({ purpose: grant.purpose, senderId: grant.senderId }))
938
+ }),
939
+ headers: { "Content-Type": "application/json" }
940
+ });
941
+ }
942
+ async revoke(consent, options) {
943
+ return this.http.request("PUT", `/email/consents/${encodeURIComponent(consent.installationId)}`, {
944
+ params: { workspaceId: this.workspace(options) },
945
+ body: JSON.stringify({ candidateDigest: consent.candidateDigest, enabled: false, allowOpenTracking: false, allowClickTracking: false }),
946
+ headers: { "Content-Type": "application/json" }
947
+ });
948
+ }
949
+ async senders(options) {
950
+ return this.http.request("GET", "/email/senders", { params: { workspaceId: this.workspace(options) } });
951
+ }
952
+ async invoicePdf(recordId, installationId, options) {
953
+ this.mainBranch(options);
954
+ return this.http.request("POST", "/email/artifacts/invoice", {
955
+ body: JSON.stringify({ workspaceId: this.workspace(options), recordId, installationId }),
956
+ headers: { "Content-Type": "application/json" }
957
+ });
958
+ }
959
+ /** Authenticated immutable bytes; no public URL. The caller owns any local object URL it creates. */
960
+ async artifactContent(artifactId, options) {
961
+ this.mainBranch(options);
962
+ return this.http.request("GET", `/email/artifacts/${encodeURIComponent(artifactId)}/content`, {
963
+ params: { workspaceId: this.workspace(options), branch: "main" }
964
+ });
965
+ }
966
+ /** A stable key binds this exact request. After a lost response inspect status; never automatically resend. */
967
+ async dispatch(options) {
968
+ this.mainBranch(options);
969
+ return this.http.request("POST", "/email/dispatches", {
970
+ body: JSON.stringify({
971
+ workspaceId: this.workspace(options),
972
+ installationId: options.installationId,
973
+ recordId: options.recordId,
974
+ purpose: options.purpose,
975
+ templateId: options.templateId,
976
+ senderId: options.senderId,
977
+ recipients: options.recipients,
978
+ artifactIds: options.artifactIds,
979
+ idempotencyKey: options.idempotencyKey,
980
+ replyTo: options.replyTo
981
+ }),
982
+ headers: { "Content-Type": "application/json" }
983
+ });
984
+ }
985
+ async outcome(id, options) {
986
+ this.mainBranch(options);
987
+ return this.http.request("GET", `/email/dispatches/${encodeURIComponent(id)}`, {
988
+ params: { workspaceId: this.workspace(options) }
989
+ });
990
+ }
991
+ async history(options) {
992
+ this.mainBranch(options);
993
+ return this.http.request("GET", "/email/dispatches", {
994
+ params: { workspaceId: this.workspace(options), recordId: options?.recordId, installationId: options?.installationId }
995
+ });
996
+ }
997
+ async status(id, options) {
998
+ return this.outcome(id, options);
999
+ }
1000
+ workspace(options) {
1001
+ const value = options?.workspaceId ?? this.workspaceId;
1002
+ if (!value) throw new Error("A workspace is required for transactional email");
1003
+ return value;
1004
+ }
1005
+ mainBranch(options) {
1006
+ if (options?.branch !== void 0 && options.branch !== "main")
1007
+ throw new Error("Transactional email currently supports only the main business branch");
1008
+ }
1009
+ };
1010
+
1011
+ // src/modules/business.ts
1012
+ var BusinessModule = class _BusinessModule {
1013
+ constructor(http, branch = "main") {
1014
+ this.http = http;
1015
+ this.branch = branch;
1016
+ }
1017
+ http;
1018
+ branch;
1019
+ withBranch(branch) {
1020
+ return new _BusinessModule(this.http, branch);
1021
+ }
1022
+ async context() {
1023
+ return this.http.request("GET", "/business/context", { headers: this.headers() });
1024
+ }
1025
+ async command(command) {
1026
+ if (!command.idempotencyKey?.trim() || command.idempotencyKey.length > 200 || /[\u0000-\u001f\u007f-\u009f]/.test(command.idempotencyKey)) {
1027
+ throw new Error("A bounded printable idempotency key is required");
1028
+ }
1029
+ if (command.expectedRevision !== void 0 && !/^"[a-f0-9]{32}"$/.test(command.expectedRevision)) {
1030
+ throw new Error("An exact strong quoted canonical revision is required");
1031
+ }
1032
+ return this.http.request("POST", "/business/commands", {
1033
+ headers: this.headers(),
1034
+ body: JSON.stringify(command)
1035
+ });
1036
+ }
1037
+ async read(id) {
1038
+ return this.http.request("GET", `/business/records/${encodeURIComponent(id)}`, { headers: this.headers() });
1039
+ }
1040
+ async list(kind) {
1041
+ return this.http.request("GET", "/business/records", {
1042
+ headers: this.headers(),
1043
+ params: kind ? { kind } : {}
1044
+ });
1045
+ }
1046
+ /** null means unresolved, not success; do not invent a new operation key. */
1047
+ async outcome(request) {
1048
+ return this.http.request("POST", "/business/outcomes", {
1049
+ headers: this.headers(),
1050
+ body: JSON.stringify(request)
1051
+ });
1052
+ }
1053
+ /** Recover using the exact original command, without reproducing server fingerprint encoding. */
1054
+ async outcomeFor(command) {
1055
+ return this.outcome({ operation: command.operation, idempotencyKey: command.idempotencyKey, command });
1056
+ }
1057
+ async report(report, range = {}) {
1058
+ return this.http.request("GET", `/business/reports/${report}`, {
1059
+ headers: this.headers(),
1060
+ params: range
1061
+ });
1062
+ }
1063
+ /** Retained historical expense receipt bytes, even after the original native file is removed. */
1064
+ async receipt(nodeId, revision) {
1065
+ const response = await this.http.requestRaw("GET", `/business/receipts/${encodeURIComponent(nodeId)}/content`, {
1066
+ headers: this.headers(),
1067
+ params: { revision }
1068
+ });
1069
+ await this.http.ensureSuccess(response);
1070
+ return response.blob();
1071
+ }
1072
+ headers() {
1073
+ if (!this.branch || this.branch.length > 128 || this.branch !== this.branch.trim().toLowerCase() || /[\u0000-\u001f\u007f-\u009f]/.test(this.branch)) {
1074
+ throw new Error("Business operations require a canonical branch");
1075
+ }
1076
+ return { "X-Branch": this.branch, "Content-Type": "application/json" };
1077
+ }
1078
+ };
1079
+
777
1080
  // src/transport/header-actions.ts
778
1081
  function isHeaderAction(value) {
779
1082
  if (!value || typeof value !== "object") return false;
@@ -781,7 +1084,113 @@ function isHeaderAction(value) {
781
1084
  return typeof a.id === "string" && /^[a-z][a-z0-9.-]{0,63}$/.test(a.id) && typeof a.label === "string" && a.label.trim().length > 0 && a.label.length <= 100 && Array.isArray(a.scopes) && a.scopes.length <= 32 && a.scopes.every((s) => typeof s === "string" && /^[a-zA-Z0-9._:-]{1,128}$/.test(s)) && (a.icon === void 0 || typeof a.icon === "string" && /^lucide:[a-z0-9-]{1,64}$/.test(a.icon)) && (a.variant === void 0 || ["default", "outline", "destructive"].includes(String(a.variant))) && ["enabled", "visible", "busy"].every((key) => a[key] === void 0 || typeof a[key] === "boolean") && (a.disabledReason === void 0 || typeof a.disabledReason === "string" && a.disabledReason.length <= 200) && (a.kind === void 0 || a.kind === "clipboard");
782
1085
  }
783
1086
 
1087
+ // src/collaboration/native.ts
1088
+ var NativeCollaborationError = class extends Error {
1089
+ constructor(message, code) {
1090
+ super(message);
1091
+ this.code = code;
1092
+ this.name = "NativeCollaborationError";
1093
+ }
1094
+ code;
1095
+ };
1096
+ function record(value) {
1097
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1098
+ }
1099
+ function natural(value) {
1100
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1101
+ }
1102
+ function identifier(value) {
1103
+ return typeof value === "string" && value.length > 0 && value.length <= 256;
1104
+ }
1105
+ function pointer(value) {
1106
+ return record(value) && typeof value.x === "number" && Number.isFinite(value.x) && typeof value.y === "number" && Number.isFinite(value.y);
1107
+ }
1108
+ function element(value) {
1109
+ return record(value) && identifier(value.id) && natural(value.version) && (value.versionNonce === void 0 || natural(value.versionNonce)) && (value.isDeleted === void 0 || typeof value.isDeleted === "boolean");
1110
+ }
1111
+ function files(value) {
1112
+ 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)));
1113
+ }
1114
+ function isNativeSnapshot(value) {
1115
+ 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)
1116
+ return false;
1117
+ const ids = value.scene.elements.map((item) => item.id);
1118
+ if (new Set(ids).size !== ids.length) return false;
1119
+ const connectionIds = value.participants.map((item) => record(item) ? item.id : void 0);
1120
+ if (new Set(connectionIds).size !== connectionIds.length) return false;
1121
+ 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"));
1122
+ }
1123
+ function isNativeSaveResult(value) {
1124
+ 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;
1125
+ }
1126
+ function validateNativeCommand(command) {
1127
+ if (!record(command)) throw new Error("Invalid collaboration command.");
1128
+ switch (command.operation) {
1129
+ case "join":
1130
+ case "snapshot":
1131
+ case "close":
1132
+ break;
1133
+ case "presence":
1134
+ if (command.pointer !== null && !pointer(command.pointer))
1135
+ throw new Error("Invalid collaboration pointer.");
1136
+ break;
1137
+ case "save":
1138
+ if (!natural(command.generation) || !identifier(command.correlation))
1139
+ throw new Error("Invalid collaboration save frontier.");
1140
+ break;
1141
+ case "apply": {
1142
+ const changes = command.changes;
1143
+ 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))
1144
+ throw new Error("Invalid collaboration changes.");
1145
+ const ids = changes.changes.map((change) => change.id);
1146
+ if (new Set(ids).size !== ids.length) throw new Error("Duplicate collaboration element changes.");
1147
+ break;
1148
+ }
1149
+ default:
1150
+ throw new Error("Unsupported collaboration command.");
1151
+ }
1152
+ if (new TextEncoder().encode(JSON.stringify(command)).byteLength > 10 * 1024 * 1024)
1153
+ throw new Error("Collaboration request exceeds the document limit.");
1154
+ }
1155
+ var NativeCollaborationClient = class {
1156
+ constructor(bridge) {
1157
+ this.bridge = bridge;
1158
+ }
1159
+ bridge;
1160
+ async snapshotRequest(command) {
1161
+ if (!this.bridge) throw new Error("Native collaboration requires an installed workspace host.");
1162
+ const result = await this.bridge.requestNativeCollaboration(command);
1163
+ if (!isNativeSnapshot(result)) throw new Error("Invalid native collaboration snapshot.");
1164
+ return result;
1165
+ }
1166
+ join() {
1167
+ return this.snapshotRequest({ operation: "join" });
1168
+ }
1169
+ snapshot() {
1170
+ return this.snapshotRequest({ operation: "snapshot" });
1171
+ }
1172
+ apply(changes) {
1173
+ return this.snapshotRequest({ operation: "apply", changes });
1174
+ }
1175
+ presence(value) {
1176
+ return this.snapshotRequest({ operation: "presence", pointer: value });
1177
+ }
1178
+ async save(generation, correlation) {
1179
+ if (!this.bridge) throw new Error("Native collaboration requires an installed workspace host.");
1180
+ const result = await this.bridge.requestNativeCollaboration({ operation: "save", generation, correlation });
1181
+ if (!isNativeSaveResult(result) || result.receipt.generation !== generation || result.receipt.correlation !== correlation)
1182
+ throw new Error("Invalid native collaboration save receipt.");
1183
+ return result;
1184
+ }
1185
+ async close() {
1186
+ if (!this.bridge) throw new Error("Native collaboration requires an installed workspace host.");
1187
+ const result = await this.bridge.requestNativeCollaboration({ operation: "close" });
1188
+ if (result !== null) throw new Error("Invalid native collaboration close response.");
1189
+ }
1190
+ };
1191
+
784
1192
  // src/transport/bridge.transport.ts
1193
+ var HOST_MESSAGE_CHANNEL_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
785
1194
  var Bridge = class {
786
1195
  constructor(expectedHostOrigin) {
787
1196
  this.expectedHostOrigin = expectedHostOrigin;
@@ -808,8 +1217,10 @@ var Bridge = class {
808
1217
  sessionId = null;
809
1218
  actions = /* @__PURE__ */ new Map();
810
1219
  requests = /* @__PURE__ */ new Set();
811
- backendRequests = /* @__PURE__ */ new Map();
1220
+ hostMessageHandlers = /* @__PURE__ */ new Map();
1221
+ hostMessageRequests = /* @__PURE__ */ new Set();
812
1222
  wopiRequests = /* @__PURE__ */ new Map();
1223
+ collaborationRequests = /* @__PURE__ */ new Map();
813
1224
  /** Post `verentis:ready` to the host to start the handshake */
814
1225
  sendReady(capabilities) {
815
1226
  console.info("[Verentis SDK] Sending verentis:ready to host");
@@ -871,28 +1282,6 @@ var Bridge = class {
871
1282
  setDirty(isDirty) {
872
1283
  this.postToHost({ type: "verentis:dirty", version: 1, isDirty });
873
1284
  }
874
- /** The host, not the app, selects the installation, file, branch and permission ceiling. */
875
- requestBackendCredential() {
876
- if (this._destroyed || !this.sessionId || !this._hostOrigin)
877
- return Promise.reject(new Error("An initialized workspace host is required."));
878
- if (this.backendRequests.size >= 4)
879
- return Promise.reject(new Error("A backend credential request is already pending."));
880
- const requestId = crypto.randomUUID();
881
- return new Promise((resolve, reject) => {
882
- const timer = setTimeout(() => {
883
- this.backendRequests.delete(requestId);
884
- reject(new Error("Backend authorization timed out."));
885
- }, 2e4);
886
- this.backendRequests.set(requestId, { resolve, reject, timer });
887
- try {
888
- this.postToHost({ type: "verentis:backend:request", version: 1, requestId, sessionId: this.sessionId });
889
- } catch {
890
- clearTimeout(timer);
891
- this.backendRequests.delete(requestId);
892
- reject(new Error("The workspace host is unavailable."));
893
- }
894
- });
895
- }
896
1285
  /** The trusted parent selects the approved installation, document and action. */
897
1286
  requestWopiLaunch() {
898
1287
  return this.requestWopi("launch");
@@ -940,6 +1329,38 @@ var Bridge = class {
940
1329
  }
941
1330
  });
942
1331
  }
1332
+ requestNativeCollaboration(command) {
1333
+ if (this._destroyed || !this.sessionId || !this._hostOrigin)
1334
+ return Promise.reject(new Error("An initialized workspace host is required."));
1335
+ if (this.collaborationRequests.size >= 4)
1336
+ return Promise.reject(new Error("Too many native collaboration requests."));
1337
+ try {
1338
+ validateNativeCommand(command);
1339
+ } catch (error) {
1340
+ return Promise.reject(error);
1341
+ }
1342
+ const requestId = crypto.randomUUID();
1343
+ return new Promise((resolve, reject) => {
1344
+ const timer = setTimeout(() => {
1345
+ this.collaborationRequests.delete(requestId);
1346
+ reject(new NativeCollaborationError("Native collaboration timed out.", "unavailable"));
1347
+ }, 3e4);
1348
+ this.collaborationRequests.set(requestId, { resolve, reject, timer });
1349
+ try {
1350
+ this.postToHost({
1351
+ type: "verentis:collaboration:request",
1352
+ version: 1,
1353
+ requestId,
1354
+ sessionId: this.sessionId,
1355
+ command
1356
+ });
1357
+ } catch (error) {
1358
+ clearTimeout(timer);
1359
+ this.collaborationRequests.delete(requestId);
1360
+ reject(error);
1361
+ }
1362
+ });
1363
+ }
943
1364
  registerAction(descriptor, handler) {
944
1365
  if (!this.sessionId || this._destroyed) throw new Error("Header actions require an initialized workspace host.");
945
1366
  if (!isHeaderAction(descriptor)) throw new Error("Invalid header action.");
@@ -1003,6 +1424,49 @@ var Bridge = class {
1003
1424
  this.publishActions();
1004
1425
  }
1005
1426
  }
1427
+ /**
1428
+ * Handle messages the workspace sends on a channel declared in the manifest `spec.messages` — for
1429
+ * example an agent asking the open app to select a record. The handler's return value is the reply;
1430
+ * a thrown error is reported back as the failure. May be called before the init handshake.
1431
+ */
1432
+ onHostMessage(channel, handler) {
1433
+ if (this._destroyed) throw new Error("Bridge has been destroyed.");
1434
+ if (!HOST_MESSAGE_CHANNEL_PATTERN.test(channel)) throw new Error(`Invalid host message channel '${channel}'.`);
1435
+ if (this.hostMessageHandlers.has(channel)) throw new Error(`Channel '${channel}' already has a handler.`);
1436
+ this.hostMessageHandlers.set(channel, handler);
1437
+ return () => {
1438
+ if (this.hostMessageHandlers.get(channel) === handler) this.hostMessageHandlers.delete(channel);
1439
+ };
1440
+ }
1441
+ async receiveHostMessage(message) {
1442
+ 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;
1443
+ this.hostMessageRequests.add(message.requestId);
1444
+ if (this.hostMessageRequests.size > 1e3) this.hostMessageRequests.delete(this.hostMessageRequests.values().next().value);
1445
+ const reply = {
1446
+ type: "verentis:host:reply",
1447
+ version: 1,
1448
+ sessionId: message.sessionId,
1449
+ requestId: message.requestId,
1450
+ channel: message.channel
1451
+ };
1452
+ const handler = this.hostMessageHandlers.get(message.channel);
1453
+ if (!handler) {
1454
+ this.postToHost({ ...reply, error: `The application does not handle '${message.channel.slice(0, 64)}' messages.` });
1455
+ return;
1456
+ }
1457
+ let response;
1458
+ try {
1459
+ response = { result: await handler(message.payload) };
1460
+ } catch (error) {
1461
+ response = { error: (error instanceof Error ? error.message : "Message failed.").slice(0, 500) || "Message failed." };
1462
+ }
1463
+ if (this._destroyed || this.sessionId !== message.sessionId) return;
1464
+ try {
1465
+ this.postToHost({ ...reply, ...response });
1466
+ } catch {
1467
+ this.postToHost({ ...reply, error: "The reply could not be sent to the workspace." });
1468
+ }
1469
+ }
1006
1470
  /** Subscribe to a specific message type from the host */
1007
1471
  on(type, listener) {
1008
1472
  if (!this.listeners.has(type)) {
@@ -1021,17 +1485,19 @@ var Bridge = class {
1021
1485
  if (!this._hostOrigin) this._initReject(new Error("Bridge has been destroyed."));
1022
1486
  this.actions.clear();
1023
1487
  this.requests.clear();
1488
+ this.hostMessageHandlers.clear();
1489
+ this.hostMessageRequests.clear();
1024
1490
  this.listeners.clear();
1025
- for (const pending of this.backendRequests.values()) {
1491
+ for (const pending of this.wopiRequests.values()) {
1026
1492
  clearTimeout(pending.timer);
1027
1493
  pending.reject(new Error("Bridge has been destroyed."));
1028
1494
  }
1029
- this.backendRequests.clear();
1030
- for (const pending of this.wopiRequests.values()) {
1495
+ this.wopiRequests.clear();
1496
+ for (const pending of this.collaborationRequests.values()) {
1031
1497
  clearTimeout(pending.timer);
1032
1498
  pending.reject(new Error("Bridge has been destroyed."));
1033
1499
  }
1034
- this.wopiRequests.clear();
1500
+ this.collaborationRequests.clear();
1035
1501
  if (typeof window !== "undefined") {
1036
1502
  window.removeEventListener("message", this._messageHandler);
1037
1503
  }
@@ -1057,26 +1523,28 @@ var Bridge = class {
1057
1523
  const initData = data;
1058
1524
  this._initResolve({ context: initData.context, token: initData.token });
1059
1525
  }
1060
- if (data.type === "verentis:backend:response") {
1526
+ if (data.type === "verentis:wopi:response") {
1061
1527
  if (!this._hostOrigin || data.sessionId !== this.sessionId) return;
1062
- const pending = this.backendRequests.get(data.requestId);
1528
+ const pending = this.wopiRequests.get(data.requestId);
1063
1529
  if (!pending) return;
1064
1530
  clearTimeout(pending.timer);
1065
- this.backendRequests.delete(data.requestId);
1066
- const value = data.credential;
1067
- if (data.error || !value || typeof value.launchCredential !== "string" || value.launchCredential.length > 512 || !value.launchCredential || typeof value.delegationId !== "string" || typeof value.backendClientId !== "string" || !Number.isFinite(Date.parse(value.expiresAt)) || Date.parse(value.expiresAt) <= Date.now())
1068
- pending.reject(new Error("Backend authorization was denied or is not configured."));
1069
- else pending.resolve(value);
1531
+ this.wopiRequests.delete(data.requestId);
1532
+ if (data.error || !Object.hasOwn(data, "result"))
1533
+ pending.reject(new Error("Collaboration authorization is unavailable."));
1534
+ else pending.resolve(data.result);
1070
1535
  return;
1071
1536
  }
1072
- if (data.type === "verentis:wopi:response") {
1537
+ if (data.type === "verentis:collaboration:response") {
1073
1538
  if (!this._hostOrigin || data.sessionId !== this.sessionId) return;
1074
- const pending = this.wopiRequests.get(data.requestId);
1539
+ const pending = this.collaborationRequests.get(data.requestId);
1075
1540
  if (!pending) return;
1076
1541
  clearTimeout(pending.timer);
1077
- this.wopiRequests.delete(data.requestId);
1542
+ this.collaborationRequests.delete(data.requestId);
1078
1543
  if (data.error || !Object.hasOwn(data, "result"))
1079
- pending.reject(new Error("Collaboration authorization is unavailable."));
1544
+ pending.reject(new NativeCollaborationError(
1545
+ typeof data.error === "string" ? data.error.slice(0, 500) : "Native collaboration is unavailable.",
1546
+ typeof data.code === "string" ? data.code.slice(0, 64) : "unavailable"
1547
+ ));
1080
1548
  else pending.resolve(data.result);
1081
1549
  return;
1082
1550
  }
@@ -1084,6 +1552,10 @@ var Bridge = class {
1084
1552
  void this.invokeAction(data);
1085
1553
  return;
1086
1554
  }
1555
+ if (data.type === "verentis:host:message") {
1556
+ void this.receiveHostMessage(data);
1557
+ return;
1558
+ }
1087
1559
  const typeListeners = this.listeners.get(data.type);
1088
1560
  if (typeListeners) {
1089
1561
  for (const listener of typeListeners) {
@@ -1095,16 +1567,18 @@ var Bridge = class {
1095
1567
 
1096
1568
  // src/types.ts
1097
1569
  var ApiError = class extends Error {
1098
- constructor(status, detail, title) {
1570
+ constructor(status, detail, title, code) {
1099
1571
  super(title ?? `API error ${status}: ${detail}`);
1100
1572
  this.status = status;
1101
1573
  this.detail = detail;
1102
1574
  this.title = title;
1575
+ this.code = code;
1103
1576
  this.name = "ApiError";
1104
1577
  }
1105
1578
  status;
1106
1579
  detail;
1107
1580
  title;
1581
+ code;
1108
1582
  };
1109
1583
 
1110
1584
  // src/transport/http.transport.ts
@@ -1133,24 +1607,7 @@ var HttpTransport = class {
1133
1607
  headers,
1134
1608
  body: options?.body
1135
1609
  });
1136
- if (!response.ok) {
1137
- let detail = "";
1138
- let title;
1139
- try {
1140
- const body = await response.json();
1141
- detail = body.detail ?? body.message ?? JSON.stringify(body);
1142
- title = body.title;
1143
- } catch {
1144
- detail = await response.text().catch(() => response.statusText);
1145
- }
1146
- console.error("[Verentis SDK] Request failed", {
1147
- method,
1148
- path,
1149
- status: response.status,
1150
- detail
1151
- });
1152
- throw new ApiError(response.status, detail, title);
1153
- }
1610
+ await this.ensureSuccess(response);
1154
1611
  console.info("[Verentis SDK] Request succeeded", {
1155
1612
  method,
1156
1613
  path,
@@ -1184,6 +1641,26 @@ var HttpTransport = class {
1184
1641
  });
1185
1642
  return response;
1186
1643
  }
1644
+ async ensureSuccess(response) {
1645
+ if (response.ok) return;
1646
+ const raw = await response.text();
1647
+ let detail = raw || response.statusText;
1648
+ let title;
1649
+ let code;
1650
+ try {
1651
+ const body = JSON.parse(raw);
1652
+ if (body !== null && typeof body === "object") {
1653
+ if ("detail" in body && typeof body.detail === "string") detail = body.detail;
1654
+ else if ("message" in body && typeof body.message === "string") detail = body.message;
1655
+ if ("title" in body && typeof body.title === "string") title = body.title;
1656
+ if ("code" in body && typeof body.code === "string") code = body.code;
1657
+ }
1658
+ } catch (error) {
1659
+ if (!(error instanceof SyntaxError)) throw error;
1660
+ }
1661
+ console.error("[Verentis SDK] Request failed", { status: response.status, code });
1662
+ throw new ApiError(response.status, detail, title, code);
1663
+ }
1187
1664
  buildUrl(path, params) {
1188
1665
  const base = `${this.baseUrl}/${this.apiVersion}${path}`;
1189
1666
  if (!params) return base;
@@ -1213,8 +1690,11 @@ var VerentisClient = class {
1213
1690
  installations;
1214
1691
  resources;
1215
1692
  settings;
1693
+ email;
1694
+ business;
1216
1695
  context;
1217
1696
  bridge;
1697
+ collaboration;
1218
1698
  _auth;
1219
1699
  _http;
1220
1700
  _ready;
@@ -1223,11 +1703,13 @@ var VerentisClient = class {
1223
1703
  const environment = detectEnvironment();
1224
1704
  if (environment === "iframe" && !options?.auth) {
1225
1705
  this.bridge = new Bridge();
1706
+ this.collaboration = new NativeCollaborationClient(this.bridge);
1226
1707
  const bridgeAuth = new BridgeAuth("*");
1227
1708
  this._auth = bridgeAuth;
1228
1709
  this._ready = this.initFromBridge(bridgeAuth);
1229
1710
  } else {
1230
1711
  this.bridge = null;
1712
+ this.collaboration = new NativeCollaborationClient(null);
1231
1713
  this._auth = createAuthStrategy(options);
1232
1714
  const apiUrl = options?.apiUrl;
1233
1715
  if (!apiUrl) {
@@ -1244,6 +1726,8 @@ var VerentisClient = class {
1244
1726
  this.installations = new InstallationsModule(this._http, options?.workspaceId);
1245
1727
  this.resources = new ResourcesModule(this._http, options?.workspaceId);
1246
1728
  this.settings = new SettingsModule(this._http, options?.workspaceId);
1729
+ this.email = new EmailModule(this._http, options?.workspaceId);
1730
+ this.business = new BusinessModule(this._http);
1247
1731
  this._ready = Promise.resolve();
1248
1732
  return;
1249
1733
  }
@@ -1256,6 +1740,14 @@ var VerentisClient = class {
1256
1740
  whenReady() {
1257
1741
  return this._ready;
1258
1742
  }
1743
+ /**
1744
+ * The current `Authorization` header value, refreshed when near expiry.
1745
+ * Forward it to the app's own backend, which exchanges it at Security's
1746
+ * `/connect/token` (RFC 8693) as the installation's confidential client.
1747
+ */
1748
+ getAuthorizationHeader() {
1749
+ return this._auth.getAuthHeader();
1750
+ }
1259
1751
  /** Clean up resources (timers, event listeners) */
1260
1752
  destroy() {
1261
1753
  this._auth.destroy?.();
@@ -1268,6 +1760,10 @@ var VerentisClient = class {
1268
1760
  bridgeAuth.setHostOrigin(hostOrigin);
1269
1761
  bridgeAuth.setToken(token);
1270
1762
  this.context.setFromAppContext(context);
1763
+ this.context.setGrants(token.grants);
1764
+ this.bridge.on("verentis:token:refreshed", (message) => {
1765
+ if (message.type === "verentis:token:refreshed") this.context.setGrants(message.token?.grants);
1766
+ });
1271
1767
  const http = new HttpTransport(
1272
1768
  context.apiUrl,
1273
1769
  bridgeAuth,
@@ -1280,6 +1776,8 @@ var VerentisClient = class {
1280
1776
  this.installations = new InstallationsModule(http, context.workspace.id);
1281
1777
  this.resources = new ResourcesModule(http, context.workspace.id);
1282
1778
  this.settings = new SettingsModule(http, context.workspace.id);
1779
+ this.email = new EmailModule(http, context.workspace.id);
1780
+ this.business = new BusinessModule(http);
1283
1781
  }
1284
1782
  };
1285
1783
  function detectEnvironment() {
@@ -1332,28 +1830,79 @@ function stripParams(mime) {
1332
1830
  return idx === -1 ? mime.trim() : mime.slice(0, idx).trim();
1333
1831
  }
1334
1832
 
1833
+ // src/collaboration/lifecycle.ts
1834
+ function isRecord(value) {
1835
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1836
+ }
1837
+ function exactHttpsOrigin(value) {
1838
+ if (typeof value !== "string" || value.length > 256)
1839
+ throw new Error("Invalid framing origin.");
1840
+ const origin = new URL(value);
1841
+ if (origin.protocol !== "https:" || origin.origin !== value || origin.href !== `${value}/` || !/^[a-z0-9-]+(?:\.[a-z0-9-]+)*$/.test(origin.hostname))
1842
+ throw new Error("Invalid framing origin.");
1843
+ return value;
1844
+ }
1845
+ function isTrustedMessage(event, source, origin) {
1846
+ return Boolean(source && origin && event.source === source && event.origin === origin);
1847
+ }
1848
+ function validateCollaborationDocument(value, context, now = Date.now()) {
1849
+ const branch = context.file?.branch;
1850
+ const node = context.file?.nodeId;
1851
+ const workspace = context.workspace?.id;
1852
+ 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")
1853
+ throw new Error("Invalid live launch.");
1854
+ return {
1855
+ sessionId: value.sessionId,
1856
+ workspaceId: value.workspaceId,
1857
+ branch: value.branch,
1858
+ nodeId: value.nodeId,
1859
+ fileId: value.fileId,
1860
+ name: value.name,
1861
+ readOnly: value.readOnly,
1862
+ accessTokenTtl: value.accessTokenTtl
1863
+ };
1864
+ }
1865
+ function isDurableSaveConfirmed(result, request, currentGeneration, editorModified) {
1866
+ if (!isRecord(request) || !isRecord(result) || !isRecord(result.receipt))
1867
+ return false;
1868
+ 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;
1869
+ }
1870
+
1335
1871
  // src/index.ts
1336
1872
  function createVerentisClient(options) {
1337
1873
  return new VerentisClient(options);
1338
1874
  }
1339
1875
 
1876
+ exports.ApiError = ApiError;
1340
1877
  exports.ApiKeyAuth = ApiKeyAuth;
1341
1878
  exports.Bridge = Bridge;
1342
1879
  exports.BridgeAuth = BridgeAuth;
1880
+ exports.BusinessModule = BusinessModule;
1343
1881
  exports.ContextModule = ContextModule;
1882
+ exports.EmailModule = EmailModule;
1344
1883
  exports.EnginesModule = EnginesModule;
1345
1884
  exports.ExecutionsModule = ExecutionsModule;
1346
1885
  exports.FilesModule = FilesModule;
1886
+ exports.HOST_MESSAGE_CHANNEL_PATTERN = HOST_MESSAGE_CHANNEL_PATTERN;
1347
1887
  exports.HttpTransport = HttpTransport;
1348
1888
  exports.InstallationsModule = InstallationsModule;
1889
+ exports.NativeCollaborationClient = NativeCollaborationClient;
1890
+ exports.NativeCollaborationError = NativeCollaborationError;
1349
1891
  exports.ResourcesModule = ResourcesModule;
1350
1892
  exports.SettingsModule = SettingsModule;
1351
1893
  exports.TokenAuth = TokenAuth;
1352
1894
  exports.VerentisClient = VerentisClient;
1353
1895
  exports.createVerentisClient = createVerentisClient;
1896
+ exports.exactHttpsOrigin = exactHttpsOrigin;
1354
1897
  exports.findBestMatch = findBestMatch;
1898
+ exports.isDurableSaveConfirmed = isDurableSaveConfirmed;
1355
1899
  exports.isHeaderAction = isHeaderAction;
1900
+ exports.isNativeSaveResult = isNativeSaveResult;
1901
+ exports.isNativeSnapshot = isNativeSnapshot;
1356
1902
  exports.isTerminalStatus = isTerminalStatus;
1903
+ exports.isTrustedMessage = isTrustedMessage;
1357
1904
  exports.matchMimeType = matchMimeType;
1905
+ exports.validateCollaborationDocument = validateCollaborationDocument;
1906
+ exports.validateNativeCommand = validateNativeCommand;
1358
1907
  //# sourceMappingURL=index.cjs.map
1359
1908
  //# sourceMappingURL=index.cjs.map