@objectstack/plugin-sharing 6.8.1 → 6.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +9 -0
- package/dist/index.d.mts +82 -4
- package/dist/index.d.ts +82 -4
- package/dist/index.js +466 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +464 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/index.ts +18 -1
- package/src/share-link-routes.ts +266 -0
- package/src/share-link-service.test.ts +166 -0
- package/src/share-link-service.ts +373 -0
- package/src/sharing-plugin.ts +54 -2
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { SysRecordShare as SysRecordShare2, SysSharingRule as SysSharingRule2 } from "@objectstack/platform-objects/security";
|
|
2
|
+
import { SysRecordShare as SysRecordShare2, SysSharingRule as SysSharingRule2, SysShareLink as SysShareLink2 } from "@objectstack/platform-objects/security";
|
|
3
3
|
import { SysDepartment as SysDepartment2, SysDepartmentMember as SysDepartmentMember2 } from "@objectstack/platform-objects/identity";
|
|
4
4
|
|
|
5
5
|
// src/sharing-service.ts
|
|
@@ -759,8 +759,435 @@ var SharingRuleService = class {
|
|
|
759
759
|
}
|
|
760
760
|
};
|
|
761
761
|
|
|
762
|
-
// src/
|
|
762
|
+
// src/share-link-service.ts
|
|
763
763
|
var SYSTEM_CTX5 = { isSystem: true, roles: [], permissions: [] };
|
|
764
|
+
var TOKEN_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
765
|
+
var TOKEN_LENGTH = 24;
|
|
766
|
+
var DEFAULT_MAX_EXPIRY_DAYS = 365;
|
|
767
|
+
function generateToken(length = TOKEN_LENGTH) {
|
|
768
|
+
const g = globalThis;
|
|
769
|
+
const bytes = new Uint8Array(length);
|
|
770
|
+
if (g.crypto?.getRandomValues) {
|
|
771
|
+
g.crypto.getRandomValues(bytes);
|
|
772
|
+
} else {
|
|
773
|
+
for (let i = 0; i < length; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
774
|
+
}
|
|
775
|
+
let out = "";
|
|
776
|
+
for (let i = 0; i < length; i++) {
|
|
777
|
+
out += TOKEN_ALPHABET[bytes[i] % TOKEN_ALPHABET.length];
|
|
778
|
+
}
|
|
779
|
+
return out;
|
|
780
|
+
}
|
|
781
|
+
function getPolicy(schema) {
|
|
782
|
+
const raw = schema?.publicSharing;
|
|
783
|
+
if (!raw || raw.enabled !== true) {
|
|
784
|
+
return {
|
|
785
|
+
enabled: false,
|
|
786
|
+
allowedAudiences: [],
|
|
787
|
+
allowedPermissions: [],
|
|
788
|
+
redactFields: []
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
return {
|
|
792
|
+
enabled: true,
|
|
793
|
+
allowedAudiences: raw.allowedAudiences ?? ["link_only"],
|
|
794
|
+
allowedPermissions: raw.allowedPermissions ?? ["view"],
|
|
795
|
+
maxExpiryDays: typeof raw.maxExpiryDays === "number" ? raw.maxExpiryDays : void 0,
|
|
796
|
+
redactFields: Array.isArray(raw.redactFields) ? raw.redactFields : []
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
function normaliseExpiresAt(input, maxDays) {
|
|
800
|
+
if (!input) return null;
|
|
801
|
+
const now = Date.now();
|
|
802
|
+
const cap = now + maxDays * 864e5;
|
|
803
|
+
const m = /^([0-9]+)(s|m|h|d)$/i.exec(input);
|
|
804
|
+
if (m) {
|
|
805
|
+
const n = Number(m[1]);
|
|
806
|
+
const unit = m[2].toLowerCase();
|
|
807
|
+
const ms = unit === "s" ? n * 1e3 : unit === "m" ? n * 6e4 : unit === "h" ? n * 36e5 : n * 864e5;
|
|
808
|
+
const at = now + ms;
|
|
809
|
+
if (at > cap) {
|
|
810
|
+
throw makeError(422, "EXPIRY_TOO_LONG", `expiresAt exceeds the object's max of ${maxDays} days`);
|
|
811
|
+
}
|
|
812
|
+
return new Date(at).toISOString();
|
|
813
|
+
}
|
|
814
|
+
const t = Date.parse(input);
|
|
815
|
+
if (Number.isNaN(t)) {
|
|
816
|
+
throw makeError(422, "INVALID_EXPIRY", `expiresAt is not a valid ISO timestamp or duration: ${input}`);
|
|
817
|
+
}
|
|
818
|
+
if (t > cap) {
|
|
819
|
+
throw makeError(422, "EXPIRY_TOO_LONG", `expiresAt exceeds the object's max of ${maxDays} days`);
|
|
820
|
+
}
|
|
821
|
+
if (t <= now) {
|
|
822
|
+
throw makeError(422, "EXPIRY_IN_PAST", "expiresAt must be in the future");
|
|
823
|
+
}
|
|
824
|
+
return new Date(t).toISOString();
|
|
825
|
+
}
|
|
826
|
+
async function defaultHashPassword(password) {
|
|
827
|
+
const g = globalThis;
|
|
828
|
+
const subtle = g.crypto?.subtle;
|
|
829
|
+
const salt = generateToken(16);
|
|
830
|
+
if (!subtle) {
|
|
831
|
+
return `weak$${salt}$${password}`;
|
|
832
|
+
}
|
|
833
|
+
const enc = new TextEncoder();
|
|
834
|
+
const buf = await subtle.digest("SHA-256", enc.encode(salt + ":" + password));
|
|
835
|
+
const hex = Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
836
|
+
return `sha256$${salt}$${hex}`;
|
|
837
|
+
}
|
|
838
|
+
async function defaultVerifyPassword(password, hash) {
|
|
839
|
+
if (hash.startsWith("weak$")) {
|
|
840
|
+
const [, , stored] = hash.split("$");
|
|
841
|
+
return stored === password;
|
|
842
|
+
}
|
|
843
|
+
if (hash.startsWith("sha256$")) {
|
|
844
|
+
const [, salt, expected] = hash.split("$");
|
|
845
|
+
const g = globalThis;
|
|
846
|
+
const subtle = g.crypto?.subtle;
|
|
847
|
+
if (!subtle) return false;
|
|
848
|
+
const enc = new TextEncoder();
|
|
849
|
+
const buf = await subtle.digest("SHA-256", enc.encode(salt + ":" + password));
|
|
850
|
+
const hex = Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
851
|
+
return hex === expected;
|
|
852
|
+
}
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
function makeError(status, code, message) {
|
|
856
|
+
const err = new Error(message);
|
|
857
|
+
err.status = status;
|
|
858
|
+
err.code = code;
|
|
859
|
+
return err;
|
|
860
|
+
}
|
|
861
|
+
var ShareLinkService = class {
|
|
862
|
+
constructor(opts) {
|
|
863
|
+
this.engine = opts.engine;
|
|
864
|
+
this.permissive = opts.permissive ?? false;
|
|
865
|
+
this.hashPassword = opts.hashPassword ?? defaultHashPassword;
|
|
866
|
+
this.verifyPassword = opts.verifyPassword ?? defaultVerifyPassword;
|
|
867
|
+
}
|
|
868
|
+
async createLink(input, context) {
|
|
869
|
+
if (!input.object) throw makeError(400, "VALIDATION_FAILED", "object is required");
|
|
870
|
+
if (!input.recordId) throw makeError(400, "VALIDATION_FAILED", "recordId is required");
|
|
871
|
+
const schema = this.engine.getSchema?.(input.object);
|
|
872
|
+
const policy = getPolicy(schema);
|
|
873
|
+
if (!policy.enabled && !this.permissive && !context.isSystem) {
|
|
874
|
+
throw makeError(
|
|
875
|
+
422,
|
|
876
|
+
"SHARING_NOT_ENABLED",
|
|
877
|
+
`Object '${input.object}' has not enabled publicSharing in its schema`
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
const permission = input.permission ?? "view";
|
|
881
|
+
if (policy.enabled && policy.allowedPermissions.length > 0 && !policy.allowedPermissions.includes(permission)) {
|
|
882
|
+
throw makeError(
|
|
883
|
+
422,
|
|
884
|
+
"PERMISSION_NOT_ALLOWED",
|
|
885
|
+
`Object '${input.object}' does not allow share permission '${permission}'. Allowed: ${policy.allowedPermissions.join(", ")}`
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
const audience = input.audience ?? "link_only";
|
|
889
|
+
if (policy.enabled && policy.allowedAudiences.length > 0 && !policy.allowedAudiences.includes(audience)) {
|
|
890
|
+
throw makeError(
|
|
891
|
+
422,
|
|
892
|
+
"AUDIENCE_NOT_ALLOWED",
|
|
893
|
+
`Object '${input.object}' does not allow audience '${audience}'. Allowed: ${policy.allowedAudiences.join(", ")}`
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
if (audience === "email" && (!input.emailAllowlist || input.emailAllowlist.length === 0)) {
|
|
897
|
+
throw makeError(400, "VALIDATION_FAILED", "emailAllowlist is required when audience=email");
|
|
898
|
+
}
|
|
899
|
+
const exists = await this.engine.find(input.object, {
|
|
900
|
+
where: { id: input.recordId },
|
|
901
|
+
fields: ["id"],
|
|
902
|
+
limit: 1,
|
|
903
|
+
context: SYSTEM_CTX5
|
|
904
|
+
});
|
|
905
|
+
if (!Array.isArray(exists) || exists.length === 0) {
|
|
906
|
+
throw makeError(404, "RECORD_NOT_FOUND", `${input.object}/${input.recordId} does not exist`);
|
|
907
|
+
}
|
|
908
|
+
const maxDays = policy.maxExpiryDays ?? DEFAULT_MAX_EXPIRY_DAYS;
|
|
909
|
+
const expires_at = normaliseExpiresAt(input.expiresAt, maxDays);
|
|
910
|
+
const passwordHash = input.password ? await this.hashPassword(input.password) : null;
|
|
911
|
+
const row = {
|
|
912
|
+
id: `shl_${generateToken(16)}`,
|
|
913
|
+
token: generateToken(TOKEN_LENGTH),
|
|
914
|
+
object_name: input.object,
|
|
915
|
+
record_id: input.recordId,
|
|
916
|
+
permission,
|
|
917
|
+
audience,
|
|
918
|
+
expires_at,
|
|
919
|
+
email_allowlist: input.emailAllowlist && input.emailAllowlist.length > 0 ? input.emailAllowlist.map((e) => e.trim().toLowerCase()).filter(Boolean) : null,
|
|
920
|
+
password_hash: passwordHash,
|
|
921
|
+
redact_fields: input.redactFields && input.redactFields.length > 0 ? input.redactFields : null,
|
|
922
|
+
label: input.label ?? null,
|
|
923
|
+
revoked_at: null,
|
|
924
|
+
created_by: context.userId ?? null,
|
|
925
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
926
|
+
last_used_at: null,
|
|
927
|
+
use_count: 0
|
|
928
|
+
};
|
|
929
|
+
await this.engine.insert("sys_share_link", row, { context: SYSTEM_CTX5 });
|
|
930
|
+
return row;
|
|
931
|
+
}
|
|
932
|
+
async revokeLink(idOrToken, _context) {
|
|
933
|
+
if (!idOrToken) throw makeError(400, "VALIDATION_FAILED", "id or token is required");
|
|
934
|
+
const filter = idOrToken.startsWith("shl_") ? { id: idOrToken } : { token: idOrToken };
|
|
935
|
+
const rows = await this.engine.find("sys_share_link", {
|
|
936
|
+
where: filter,
|
|
937
|
+
fields: ["id", "revoked_at"],
|
|
938
|
+
limit: 1,
|
|
939
|
+
context: SYSTEM_CTX5
|
|
940
|
+
});
|
|
941
|
+
const row = Array.isArray(rows) ? rows[0] : void 0;
|
|
942
|
+
if (!row) return;
|
|
943
|
+
if (row.revoked_at) return;
|
|
944
|
+
await this.engine.update(
|
|
945
|
+
"sys_share_link",
|
|
946
|
+
{ id: row.id, revoked_at: (/* @__PURE__ */ new Date()).toISOString() },
|
|
947
|
+
{ context: SYSTEM_CTX5 }
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
async listLinks(filter, context) {
|
|
951
|
+
const where = {};
|
|
952
|
+
if (filter.object) where.object_name = filter.object;
|
|
953
|
+
if (filter.recordId) where.record_id = filter.recordId;
|
|
954
|
+
if (filter.createdBy) where.created_by = filter.createdBy;
|
|
955
|
+
if (!filter.includeRevoked) where.revoked_at = null;
|
|
956
|
+
const rows = await this.engine.find("sys_share_link", {
|
|
957
|
+
where,
|
|
958
|
+
limit: 200,
|
|
959
|
+
sort: [{ field: "created_at", order: "desc" }],
|
|
960
|
+
context: context.isSystem ? SYSTEM_CTX5 : context
|
|
961
|
+
});
|
|
962
|
+
return Array.isArray(rows) ? rows : [];
|
|
963
|
+
}
|
|
964
|
+
async resolveToken(token, probe = {}) {
|
|
965
|
+
if (!token || typeof token !== "string" || token.length < 8) return null;
|
|
966
|
+
const rows = await this.engine.find("sys_share_link", {
|
|
967
|
+
where: { token },
|
|
968
|
+
limit: 1,
|
|
969
|
+
context: SYSTEM_CTX5
|
|
970
|
+
});
|
|
971
|
+
const row = Array.isArray(rows) ? rows[0] : void 0;
|
|
972
|
+
if (!row) return null;
|
|
973
|
+
if (row.revoked_at) return null;
|
|
974
|
+
if (row.expires_at && Date.parse(row.expires_at) <= Date.now()) return null;
|
|
975
|
+
if (row.audience === "signed_in" && !probe.signedInUserId) return null;
|
|
976
|
+
if (row.audience === "email") {
|
|
977
|
+
const allow = row.email_allowlist ?? [];
|
|
978
|
+
const supplied = (probe.recipientEmail ?? "").trim().toLowerCase();
|
|
979
|
+
if (!supplied || !allow.includes(supplied)) return null;
|
|
980
|
+
}
|
|
981
|
+
if (row.password_hash) {
|
|
982
|
+
if (!probe.providedPassword) return null;
|
|
983
|
+
const ok = await this.verifyPassword(probe.providedPassword, row.password_hash);
|
|
984
|
+
if (!ok) return null;
|
|
985
|
+
}
|
|
986
|
+
const schema = this.engine.getSchema?.(row.object_name);
|
|
987
|
+
const policy = getPolicy(schema);
|
|
988
|
+
const redactFields = Array.from(
|
|
989
|
+
/* @__PURE__ */ new Set([...policy.redactFields ?? [], ...row.redact_fields ?? []])
|
|
990
|
+
);
|
|
991
|
+
try {
|
|
992
|
+
await this.engine.update(
|
|
993
|
+
"sys_share_link",
|
|
994
|
+
{
|
|
995
|
+
id: row.id,
|
|
996
|
+
last_used_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
997
|
+
use_count: (row.use_count ?? 0) + 1
|
|
998
|
+
},
|
|
999
|
+
{ context: SYSTEM_CTX5 }
|
|
1000
|
+
);
|
|
1001
|
+
} catch {
|
|
1002
|
+
}
|
|
1003
|
+
return { link: row, redactFields };
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
// src/share-link-routes.ts
|
|
1008
|
+
var SYSTEM_CTX6 = { isSystem: true, roles: [], permissions: [] };
|
|
1009
|
+
var defaultContext = (req) => {
|
|
1010
|
+
const header = (name) => {
|
|
1011
|
+
const v = req.headers?.[name];
|
|
1012
|
+
return Array.isArray(v) ? v[0] : v;
|
|
1013
|
+
};
|
|
1014
|
+
return {
|
|
1015
|
+
userId: header("x-user-id"),
|
|
1016
|
+
tenantId: header("x-tenant-id")
|
|
1017
|
+
};
|
|
1018
|
+
};
|
|
1019
|
+
function sendError(res, status, code, message) {
|
|
1020
|
+
res.status(status).json({ error: { code, message } });
|
|
1021
|
+
}
|
|
1022
|
+
function applyRedaction(record, redactFields) {
|
|
1023
|
+
if (!record || typeof record !== "object" || redactFields.length === 0) return record;
|
|
1024
|
+
if (Array.isArray(record)) return record.map((r) => applyRedaction(r, redactFields));
|
|
1025
|
+
const out = {};
|
|
1026
|
+
for (const [k, v] of Object.entries(record)) {
|
|
1027
|
+
if (redactFields.includes(k)) continue;
|
|
1028
|
+
out[k] = v;
|
|
1029
|
+
}
|
|
1030
|
+
return out;
|
|
1031
|
+
}
|
|
1032
|
+
function registerShareLinkRoutes(http, service, engine, opts = {}) {
|
|
1033
|
+
const base = opts.basePath ?? "/api/v1/share-links";
|
|
1034
|
+
const ctxOf = opts.contextFromRequest ?? defaultContext;
|
|
1035
|
+
http.post(base, (async (req, res) => {
|
|
1036
|
+
try {
|
|
1037
|
+
const ctx = ctxOf(req);
|
|
1038
|
+
const body = req.body ?? {};
|
|
1039
|
+
if (!body.object || !body.recordId) {
|
|
1040
|
+
return sendError(res, 400, "VALIDATION_FAILED", "object and recordId are required");
|
|
1041
|
+
}
|
|
1042
|
+
const link = await service.createLink(
|
|
1043
|
+
{
|
|
1044
|
+
object: body.object,
|
|
1045
|
+
recordId: body.recordId,
|
|
1046
|
+
permission: body.permission,
|
|
1047
|
+
audience: body.audience,
|
|
1048
|
+
expiresAt: body.expiresAt ?? null,
|
|
1049
|
+
emailAllowlist: body.emailAllowlist,
|
|
1050
|
+
password: body.password,
|
|
1051
|
+
redactFields: body.redactFields,
|
|
1052
|
+
label: body.label
|
|
1053
|
+
},
|
|
1054
|
+
ctx
|
|
1055
|
+
);
|
|
1056
|
+
await res.status(201).json({ link });
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
sendError(res, err?.status ?? 500, err?.code ?? "INTERNAL", err?.message ?? "Failed to create link");
|
|
1059
|
+
}
|
|
1060
|
+
}));
|
|
1061
|
+
http.get(base, (async (req, res) => {
|
|
1062
|
+
try {
|
|
1063
|
+
const ctx = ctxOf(req);
|
|
1064
|
+
const q = req.query ?? {};
|
|
1065
|
+
const link = await service.listLinks(
|
|
1066
|
+
{
|
|
1067
|
+
object: typeof q.object === "string" ? q.object : void 0,
|
|
1068
|
+
recordId: typeof q.recordId === "string" ? q.recordId : void 0,
|
|
1069
|
+
createdBy: typeof q.createdBy === "string" ? q.createdBy : void 0,
|
|
1070
|
+
includeRevoked: q.includeRevoked === "true" || q.includeRevoked === "1"
|
|
1071
|
+
},
|
|
1072
|
+
ctx
|
|
1073
|
+
);
|
|
1074
|
+
await res.json({ links: link });
|
|
1075
|
+
} catch (err) {
|
|
1076
|
+
sendError(res, err?.status ?? 500, err?.code ?? "INTERNAL", err?.message ?? "Failed to list links");
|
|
1077
|
+
}
|
|
1078
|
+
}));
|
|
1079
|
+
http.delete(`${base}/:idOrToken`, (async (req, res) => {
|
|
1080
|
+
try {
|
|
1081
|
+
const ctx = ctxOf(req);
|
|
1082
|
+
await service.revokeLink(req.params.idOrToken, ctx);
|
|
1083
|
+
await res.status(200).json({ ok: true });
|
|
1084
|
+
} catch (err) {
|
|
1085
|
+
sendError(res, err?.status ?? 500, err?.code ?? "INTERNAL", err?.message ?? "Failed to revoke link");
|
|
1086
|
+
}
|
|
1087
|
+
}));
|
|
1088
|
+
http.get(`${base}/:token/resolve`, (async (req, res) => {
|
|
1089
|
+
try {
|
|
1090
|
+
const q = req.query ?? {};
|
|
1091
|
+
const signedInUserId = (() => {
|
|
1092
|
+
const v = req.headers?.["x-user-id"];
|
|
1093
|
+
return Array.isArray(v) ? v[0] : v;
|
|
1094
|
+
})();
|
|
1095
|
+
const recipientEmail = typeof q.email === "string" ? q.email : void 0;
|
|
1096
|
+
const providedPassword = typeof q.password === "string" ? q.password : (() => {
|
|
1097
|
+
const v = req.headers?.["x-share-password"];
|
|
1098
|
+
return Array.isArray(v) ? v[0] : v;
|
|
1099
|
+
})();
|
|
1100
|
+
const resolved = await service.resolveToken(req.params.token, {
|
|
1101
|
+
signedInUserId,
|
|
1102
|
+
recipientEmail,
|
|
1103
|
+
providedPassword
|
|
1104
|
+
});
|
|
1105
|
+
if (!resolved) {
|
|
1106
|
+
const probe = await engine.find("sys_share_link", {
|
|
1107
|
+
where: { token: req.params.token },
|
|
1108
|
+
limit: 1,
|
|
1109
|
+
context: SYSTEM_CTX6
|
|
1110
|
+
});
|
|
1111
|
+
const row = Array.isArray(probe) && probe[0] ? probe[0] : null;
|
|
1112
|
+
if (row && !row.revoked_at && (!row.expires_at || Date.parse(row.expires_at) > Date.now())) {
|
|
1113
|
+
if (row.password_hash) {
|
|
1114
|
+
return sendError(
|
|
1115
|
+
res,
|
|
1116
|
+
401,
|
|
1117
|
+
providedPassword ? "WRONG_PASSWORD" : "NEEDS_PASSWORD",
|
|
1118
|
+
providedPassword ? "Incorrect password" : "This link requires a password"
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
if (row.audience === "signed_in" && !signedInUserId) {
|
|
1122
|
+
return sendError(res, 401, "SIGN_IN_REQUIRED", "Please sign in to view this link");
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
if (row && (row.revoked_at || row.expires_at && Date.parse(row.expires_at) <= Date.now())) {
|
|
1126
|
+
return sendError(res, 410, "EXPIRED_OR_REVOKED", "Share link has expired or been revoked");
|
|
1127
|
+
}
|
|
1128
|
+
return sendError(res, 404, "INVALID_OR_EXPIRED", "Share link is invalid, expired, or revoked");
|
|
1129
|
+
}
|
|
1130
|
+
const rows = await engine.find(resolved.link.object_name, {
|
|
1131
|
+
where: { id: resolved.link.record_id },
|
|
1132
|
+
limit: 1,
|
|
1133
|
+
context: SYSTEM_CTX6
|
|
1134
|
+
});
|
|
1135
|
+
const record = Array.isArray(rows) && rows[0] ? rows[0] : null;
|
|
1136
|
+
if (!record) {
|
|
1137
|
+
return sendError(res, 410, "RECORD_GONE", "The shared record no longer exists");
|
|
1138
|
+
}
|
|
1139
|
+
await res.json({
|
|
1140
|
+
record: applyRedaction(record, resolved.redactFields),
|
|
1141
|
+
link: {
|
|
1142
|
+
id: resolved.link.id,
|
|
1143
|
+
token: resolved.link.token,
|
|
1144
|
+
object_name: resolved.link.object_name,
|
|
1145
|
+
record_id: resolved.link.record_id,
|
|
1146
|
+
permission: resolved.link.permission,
|
|
1147
|
+
audience: resolved.link.audience,
|
|
1148
|
+
expires_at: resolved.link.expires_at,
|
|
1149
|
+
label: resolved.link.label,
|
|
1150
|
+
created_at: resolved.link.created_at
|
|
1151
|
+
},
|
|
1152
|
+
redactFields: resolved.redactFields
|
|
1153
|
+
});
|
|
1154
|
+
} catch (err) {
|
|
1155
|
+
sendError(res, err?.status ?? 500, err?.code ?? "INTERNAL", err?.message ?? "Failed to resolve link");
|
|
1156
|
+
}
|
|
1157
|
+
}));
|
|
1158
|
+
http.get(`${base}/:token/messages`, (async (req, res) => {
|
|
1159
|
+
try {
|
|
1160
|
+
const password = typeof req.query?.password === "string" ? req.query.password : void 0;
|
|
1161
|
+
const resolved = await service.resolveToken(req.params.token, { providedPassword: password });
|
|
1162
|
+
if (!resolved) {
|
|
1163
|
+
sendError(res, 404, "NOT_FOUND", "Share link not found");
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
if (resolved.link.object_name !== "ai_conversations") {
|
|
1167
|
+
sendError(res, 400, "UNSUPPORTED", "This share link does not expose messages");
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
const SYSTEM_CTX8 = { isSystem: true, roles: [], permissions: [] };
|
|
1171
|
+
const rows = await engine.find("ai_messages", {
|
|
1172
|
+
where: { conversation_id: resolved.link.record_id },
|
|
1173
|
+
sort: [{ field: "created_at", direction: "asc" }],
|
|
1174
|
+
limit: 500,
|
|
1175
|
+
context: SYSTEM_CTX8
|
|
1176
|
+
});
|
|
1177
|
+
res.status(200).json({ data: rows ?? [] });
|
|
1178
|
+
} catch (err) {
|
|
1179
|
+
sendError(
|
|
1180
|
+
res,
|
|
1181
|
+
err?.status ?? 500,
|
|
1182
|
+
err?.code ?? "INTERNAL",
|
|
1183
|
+
err?.message ?? "Failed to load messages"
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
}));
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// src/rule-hooks.ts
|
|
1190
|
+
var SYSTEM_CTX7 = { isSystem: true, roles: [], permissions: [] };
|
|
764
1191
|
var SHARING_RULE_HOOK_PACKAGE = "plugin-sharing:rules";
|
|
765
1192
|
function bindRuleHooks(engine, service, rules, logger) {
|
|
766
1193
|
const objects = /* @__PURE__ */ new Set();
|
|
@@ -775,7 +1202,7 @@ function bindRuleHooks(engine, service, rules, logger) {
|
|
|
775
1202
|
const data = ctx?.result ?? ctx?.input?.data ?? {};
|
|
776
1203
|
const id = String(data?.id ?? ctx?.input?.id ?? "");
|
|
777
1204
|
if (!id) return;
|
|
778
|
-
await service.evaluateAllForRecord(objectName, id,
|
|
1205
|
+
await service.evaluateAllForRecord(objectName, id, SYSTEM_CTX7);
|
|
779
1206
|
} catch (err) {
|
|
780
1207
|
logger?.warn?.("[sharing-rule] hook evaluation failed", { object: objectName, error: err?.message });
|
|
781
1208
|
}
|
|
@@ -790,7 +1217,7 @@ function unbindAllRuleHooks(engine) {
|
|
|
790
1217
|
}
|
|
791
1218
|
|
|
792
1219
|
// src/sharing-plugin.ts
|
|
793
|
-
import { SysRecordShare, SysSharingRule } from "@objectstack/platform-objects/security";
|
|
1220
|
+
import { SysRecordShare, SysSharingRule, SysShareLink } from "@objectstack/platform-objects/security";
|
|
794
1221
|
import { SysDepartment, SysDepartmentMember } from "@objectstack/platform-objects/identity";
|
|
795
1222
|
var SharingServicePlugin = class {
|
|
796
1223
|
constructor(options = {}) {
|
|
@@ -809,7 +1236,7 @@ var SharingServicePlugin = class {
|
|
|
809
1236
|
scope: "system",
|
|
810
1237
|
defaultDatasource: "cloud",
|
|
811
1238
|
namespace: "sys",
|
|
812
|
-
objects: [SysRecordShare, SysSharingRule, SysDepartment, SysDepartmentMember]
|
|
1239
|
+
objects: [SysRecordShare, SysSharingRule, SysDepartment, SysDepartmentMember, SysShareLink]
|
|
813
1240
|
});
|
|
814
1241
|
ctx.logger.info("SharingServicePlugin: schema registered");
|
|
815
1242
|
}
|
|
@@ -861,6 +1288,31 @@ var SharingServicePlugin = class {
|
|
|
861
1288
|
} catch (err) {
|
|
862
1289
|
ctx.logger.warn("SharingServicePlugin: sharing-rule subsystem not started", { error: err?.message });
|
|
863
1290
|
}
|
|
1291
|
+
try {
|
|
1292
|
+
this.linkService = new ShareLinkService({ engine });
|
|
1293
|
+
ctx.registerService("shareLinks", this.linkService);
|
|
1294
|
+
if (this.options.registerShareLinkRoutes !== false) {
|
|
1295
|
+
let http = null;
|
|
1296
|
+
try {
|
|
1297
|
+
http = ctx.getService("http-server");
|
|
1298
|
+
} catch {
|
|
1299
|
+
}
|
|
1300
|
+
if (http) {
|
|
1301
|
+
registerShareLinkRoutes(http, this.linkService, engine, {
|
|
1302
|
+
basePath: this.options.shareLinkBasePath
|
|
1303
|
+
});
|
|
1304
|
+
ctx.logger.info(
|
|
1305
|
+
"SharingServicePlugin: share-link routes mounted at " + (this.options.shareLinkBasePath ?? "/api/v1/share-links")
|
|
1306
|
+
);
|
|
1307
|
+
} else {
|
|
1308
|
+
ctx.logger.warn(
|
|
1309
|
+
'SharingServicePlugin: no HTTP server \u2014 share-link REST routes not registered. ShareLinkService is still reachable via kernel.getService("shareLinks").'
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
} catch (err) {
|
|
1314
|
+
ctx.logger.warn("SharingServicePlugin: share-link subsystem not started", { error: err?.message });
|
|
1315
|
+
}
|
|
864
1316
|
});
|
|
865
1317
|
}
|
|
866
1318
|
};
|
|
@@ -923,20 +1375,27 @@ function inferTargetId(data, options) {
|
|
|
923
1375
|
}
|
|
924
1376
|
return void 0;
|
|
925
1377
|
}
|
|
1378
|
+
|
|
1379
|
+
// src/index.ts
|
|
1380
|
+
import { SHARE_LINK_SERVICE } from "@objectstack/spec/contracts";
|
|
926
1381
|
export {
|
|
927
1382
|
DepartmentGraphService,
|
|
1383
|
+
SHARE_LINK_SERVICE,
|
|
928
1384
|
SHARING_RULE_HOOK_PACKAGE,
|
|
1385
|
+
ShareLinkService,
|
|
929
1386
|
SharingRuleService,
|
|
930
1387
|
SharingService,
|
|
931
1388
|
SharingServicePlugin,
|
|
932
1389
|
SysDepartment2 as SysDepartment,
|
|
933
1390
|
SysDepartmentMember2 as SysDepartmentMember,
|
|
934
1391
|
SysRecordShare2 as SysRecordShare,
|
|
1392
|
+
SysShareLink2 as SysShareLink,
|
|
935
1393
|
SysSharingRule2 as SysSharingRule,
|
|
936
1394
|
TeamGraphService,
|
|
937
1395
|
bindRuleHooks,
|
|
938
1396
|
buildSharingMiddleware,
|
|
939
1397
|
expandPrincipal,
|
|
1398
|
+
registerShareLinkRoutes,
|
|
940
1399
|
unbindAllRuleHooks
|
|
941
1400
|
};
|
|
942
1401
|
//# sourceMappingURL=index.mjs.map
|