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