@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/README.md +138 -11
- package/dist/index.cjs +614 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +642 -88
- package/dist/index.d.ts +642 -88
- package/dist/index.js +602 -66
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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:
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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.
|
|
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.
|
|
1030
|
-
for (const pending of this.
|
|
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.
|
|
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:
|
|
1526
|
+
if (data.type === "verentis:wopi:response") {
|
|
1061
1527
|
if (!this._hostOrigin || data.sessionId !== this.sessionId) return;
|
|
1062
|
-
const pending = this.
|
|
1528
|
+
const pending = this.wopiRequests.get(data.requestId);
|
|
1063
1529
|
if (!pending) return;
|
|
1064
1530
|
clearTimeout(pending.timer);
|
|
1065
|
-
this.
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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:
|
|
1537
|
+
if (data.type === "verentis:collaboration:response") {
|
|
1073
1538
|
if (!this._hostOrigin || data.sessionId !== this.sessionId) return;
|
|
1074
|
-
const pending = this.
|
|
1539
|
+
const pending = this.collaborationRequests.get(data.requestId);
|
|
1075
1540
|
if (!pending) return;
|
|
1076
1541
|
clearTimeout(pending.timer);
|
|
1077
|
-
this.
|
|
1542
|
+
this.collaborationRequests.delete(data.requestId);
|
|
1078
1543
|
if (data.error || !Object.hasOwn(data, "result"))
|
|
1079
|
-
pending.reject(new
|
|
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
|
-
|
|
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
|