@objectstack/plugin-sharing 6.8.1 → 7.0.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 +24 -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.js
CHANGED
|
@@ -21,18 +21,22 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
DepartmentGraphService: () => DepartmentGraphService,
|
|
24
|
+
SHARE_LINK_SERVICE: () => import_contracts.SHARE_LINK_SERVICE,
|
|
24
25
|
SHARING_RULE_HOOK_PACKAGE: () => SHARING_RULE_HOOK_PACKAGE,
|
|
26
|
+
ShareLinkService: () => ShareLinkService,
|
|
25
27
|
SharingRuleService: () => SharingRuleService,
|
|
26
28
|
SharingService: () => SharingService,
|
|
27
29
|
SharingServicePlugin: () => SharingServicePlugin,
|
|
28
30
|
SysDepartment: () => import_identity2.SysDepartment,
|
|
29
31
|
SysDepartmentMember: () => import_identity2.SysDepartmentMember,
|
|
30
32
|
SysRecordShare: () => import_security2.SysRecordShare,
|
|
33
|
+
SysShareLink: () => import_security2.SysShareLink,
|
|
31
34
|
SysSharingRule: () => import_security2.SysSharingRule,
|
|
32
35
|
TeamGraphService: () => TeamGraphService,
|
|
33
36
|
bindRuleHooks: () => bindRuleHooks,
|
|
34
37
|
buildSharingMiddleware: () => buildSharingMiddleware,
|
|
35
38
|
expandPrincipal: () => expandPrincipal,
|
|
39
|
+
registerShareLinkRoutes: () => registerShareLinkRoutes,
|
|
36
40
|
unbindAllRuleHooks: () => unbindAllRuleHooks
|
|
37
41
|
});
|
|
38
42
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -796,8 +800,435 @@ var SharingRuleService = class {
|
|
|
796
800
|
}
|
|
797
801
|
};
|
|
798
802
|
|
|
799
|
-
// src/
|
|
803
|
+
// src/share-link-service.ts
|
|
800
804
|
var SYSTEM_CTX5 = { isSystem: true, roles: [], permissions: [] };
|
|
805
|
+
var TOKEN_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
806
|
+
var TOKEN_LENGTH = 24;
|
|
807
|
+
var DEFAULT_MAX_EXPIRY_DAYS = 365;
|
|
808
|
+
function generateToken(length = TOKEN_LENGTH) {
|
|
809
|
+
const g = globalThis;
|
|
810
|
+
const bytes = new Uint8Array(length);
|
|
811
|
+
if (g.crypto?.getRandomValues) {
|
|
812
|
+
g.crypto.getRandomValues(bytes);
|
|
813
|
+
} else {
|
|
814
|
+
for (let i = 0; i < length; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
815
|
+
}
|
|
816
|
+
let out = "";
|
|
817
|
+
for (let i = 0; i < length; i++) {
|
|
818
|
+
out += TOKEN_ALPHABET[bytes[i] % TOKEN_ALPHABET.length];
|
|
819
|
+
}
|
|
820
|
+
return out;
|
|
821
|
+
}
|
|
822
|
+
function getPolicy(schema) {
|
|
823
|
+
const raw = schema?.publicSharing;
|
|
824
|
+
if (!raw || raw.enabled !== true) {
|
|
825
|
+
return {
|
|
826
|
+
enabled: false,
|
|
827
|
+
allowedAudiences: [],
|
|
828
|
+
allowedPermissions: [],
|
|
829
|
+
redactFields: []
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
return {
|
|
833
|
+
enabled: true,
|
|
834
|
+
allowedAudiences: raw.allowedAudiences ?? ["link_only"],
|
|
835
|
+
allowedPermissions: raw.allowedPermissions ?? ["view"],
|
|
836
|
+
maxExpiryDays: typeof raw.maxExpiryDays === "number" ? raw.maxExpiryDays : void 0,
|
|
837
|
+
redactFields: Array.isArray(raw.redactFields) ? raw.redactFields : []
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
function normaliseExpiresAt(input, maxDays) {
|
|
841
|
+
if (!input) return null;
|
|
842
|
+
const now = Date.now();
|
|
843
|
+
const cap = now + maxDays * 864e5;
|
|
844
|
+
const m = /^([0-9]+)(s|m|h|d)$/i.exec(input);
|
|
845
|
+
if (m) {
|
|
846
|
+
const n = Number(m[1]);
|
|
847
|
+
const unit = m[2].toLowerCase();
|
|
848
|
+
const ms = unit === "s" ? n * 1e3 : unit === "m" ? n * 6e4 : unit === "h" ? n * 36e5 : n * 864e5;
|
|
849
|
+
const at = now + ms;
|
|
850
|
+
if (at > cap) {
|
|
851
|
+
throw makeError(422, "EXPIRY_TOO_LONG", `expiresAt exceeds the object's max of ${maxDays} days`);
|
|
852
|
+
}
|
|
853
|
+
return new Date(at).toISOString();
|
|
854
|
+
}
|
|
855
|
+
const t = Date.parse(input);
|
|
856
|
+
if (Number.isNaN(t)) {
|
|
857
|
+
throw makeError(422, "INVALID_EXPIRY", `expiresAt is not a valid ISO timestamp or duration: ${input}`);
|
|
858
|
+
}
|
|
859
|
+
if (t > cap) {
|
|
860
|
+
throw makeError(422, "EXPIRY_TOO_LONG", `expiresAt exceeds the object's max of ${maxDays} days`);
|
|
861
|
+
}
|
|
862
|
+
if (t <= now) {
|
|
863
|
+
throw makeError(422, "EXPIRY_IN_PAST", "expiresAt must be in the future");
|
|
864
|
+
}
|
|
865
|
+
return new Date(t).toISOString();
|
|
866
|
+
}
|
|
867
|
+
async function defaultHashPassword(password) {
|
|
868
|
+
const g = globalThis;
|
|
869
|
+
const subtle = g.crypto?.subtle;
|
|
870
|
+
const salt = generateToken(16);
|
|
871
|
+
if (!subtle) {
|
|
872
|
+
return `weak$${salt}$${password}`;
|
|
873
|
+
}
|
|
874
|
+
const enc = new TextEncoder();
|
|
875
|
+
const buf = await subtle.digest("SHA-256", enc.encode(salt + ":" + password));
|
|
876
|
+
const hex = Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
877
|
+
return `sha256$${salt}$${hex}`;
|
|
878
|
+
}
|
|
879
|
+
async function defaultVerifyPassword(password, hash) {
|
|
880
|
+
if (hash.startsWith("weak$")) {
|
|
881
|
+
const [, , stored] = hash.split("$");
|
|
882
|
+
return stored === password;
|
|
883
|
+
}
|
|
884
|
+
if (hash.startsWith("sha256$")) {
|
|
885
|
+
const [, salt, expected] = hash.split("$");
|
|
886
|
+
const g = globalThis;
|
|
887
|
+
const subtle = g.crypto?.subtle;
|
|
888
|
+
if (!subtle) return false;
|
|
889
|
+
const enc = new TextEncoder();
|
|
890
|
+
const buf = await subtle.digest("SHA-256", enc.encode(salt + ":" + password));
|
|
891
|
+
const hex = Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
892
|
+
return hex === expected;
|
|
893
|
+
}
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
function makeError(status, code, message) {
|
|
897
|
+
const err = new Error(message);
|
|
898
|
+
err.status = status;
|
|
899
|
+
err.code = code;
|
|
900
|
+
return err;
|
|
901
|
+
}
|
|
902
|
+
var ShareLinkService = class {
|
|
903
|
+
constructor(opts) {
|
|
904
|
+
this.engine = opts.engine;
|
|
905
|
+
this.permissive = opts.permissive ?? false;
|
|
906
|
+
this.hashPassword = opts.hashPassword ?? defaultHashPassword;
|
|
907
|
+
this.verifyPassword = opts.verifyPassword ?? defaultVerifyPassword;
|
|
908
|
+
}
|
|
909
|
+
async createLink(input, context) {
|
|
910
|
+
if (!input.object) throw makeError(400, "VALIDATION_FAILED", "object is required");
|
|
911
|
+
if (!input.recordId) throw makeError(400, "VALIDATION_FAILED", "recordId is required");
|
|
912
|
+
const schema = this.engine.getSchema?.(input.object);
|
|
913
|
+
const policy = getPolicy(schema);
|
|
914
|
+
if (!policy.enabled && !this.permissive && !context.isSystem) {
|
|
915
|
+
throw makeError(
|
|
916
|
+
422,
|
|
917
|
+
"SHARING_NOT_ENABLED",
|
|
918
|
+
`Object '${input.object}' has not enabled publicSharing in its schema`
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
const permission = input.permission ?? "view";
|
|
922
|
+
if (policy.enabled && policy.allowedPermissions.length > 0 && !policy.allowedPermissions.includes(permission)) {
|
|
923
|
+
throw makeError(
|
|
924
|
+
422,
|
|
925
|
+
"PERMISSION_NOT_ALLOWED",
|
|
926
|
+
`Object '${input.object}' does not allow share permission '${permission}'. Allowed: ${policy.allowedPermissions.join(", ")}`
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
const audience = input.audience ?? "link_only";
|
|
930
|
+
if (policy.enabled && policy.allowedAudiences.length > 0 && !policy.allowedAudiences.includes(audience)) {
|
|
931
|
+
throw makeError(
|
|
932
|
+
422,
|
|
933
|
+
"AUDIENCE_NOT_ALLOWED",
|
|
934
|
+
`Object '${input.object}' does not allow audience '${audience}'. Allowed: ${policy.allowedAudiences.join(", ")}`
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
if (audience === "email" && (!input.emailAllowlist || input.emailAllowlist.length === 0)) {
|
|
938
|
+
throw makeError(400, "VALIDATION_FAILED", "emailAllowlist is required when audience=email");
|
|
939
|
+
}
|
|
940
|
+
const exists = await this.engine.find(input.object, {
|
|
941
|
+
where: { id: input.recordId },
|
|
942
|
+
fields: ["id"],
|
|
943
|
+
limit: 1,
|
|
944
|
+
context: SYSTEM_CTX5
|
|
945
|
+
});
|
|
946
|
+
if (!Array.isArray(exists) || exists.length === 0) {
|
|
947
|
+
throw makeError(404, "RECORD_NOT_FOUND", `${input.object}/${input.recordId} does not exist`);
|
|
948
|
+
}
|
|
949
|
+
const maxDays = policy.maxExpiryDays ?? DEFAULT_MAX_EXPIRY_DAYS;
|
|
950
|
+
const expires_at = normaliseExpiresAt(input.expiresAt, maxDays);
|
|
951
|
+
const passwordHash = input.password ? await this.hashPassword(input.password) : null;
|
|
952
|
+
const row = {
|
|
953
|
+
id: `shl_${generateToken(16)}`,
|
|
954
|
+
token: generateToken(TOKEN_LENGTH),
|
|
955
|
+
object_name: input.object,
|
|
956
|
+
record_id: input.recordId,
|
|
957
|
+
permission,
|
|
958
|
+
audience,
|
|
959
|
+
expires_at,
|
|
960
|
+
email_allowlist: input.emailAllowlist && input.emailAllowlist.length > 0 ? input.emailAllowlist.map((e) => e.trim().toLowerCase()).filter(Boolean) : null,
|
|
961
|
+
password_hash: passwordHash,
|
|
962
|
+
redact_fields: input.redactFields && input.redactFields.length > 0 ? input.redactFields : null,
|
|
963
|
+
label: input.label ?? null,
|
|
964
|
+
revoked_at: null,
|
|
965
|
+
created_by: context.userId ?? null,
|
|
966
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
967
|
+
last_used_at: null,
|
|
968
|
+
use_count: 0
|
|
969
|
+
};
|
|
970
|
+
await this.engine.insert("sys_share_link", row, { context: SYSTEM_CTX5 });
|
|
971
|
+
return row;
|
|
972
|
+
}
|
|
973
|
+
async revokeLink(idOrToken, _context) {
|
|
974
|
+
if (!idOrToken) throw makeError(400, "VALIDATION_FAILED", "id or token is required");
|
|
975
|
+
const filter = idOrToken.startsWith("shl_") ? { id: idOrToken } : { token: idOrToken };
|
|
976
|
+
const rows = await this.engine.find("sys_share_link", {
|
|
977
|
+
where: filter,
|
|
978
|
+
fields: ["id", "revoked_at"],
|
|
979
|
+
limit: 1,
|
|
980
|
+
context: SYSTEM_CTX5
|
|
981
|
+
});
|
|
982
|
+
const row = Array.isArray(rows) ? rows[0] : void 0;
|
|
983
|
+
if (!row) return;
|
|
984
|
+
if (row.revoked_at) return;
|
|
985
|
+
await this.engine.update(
|
|
986
|
+
"sys_share_link",
|
|
987
|
+
{ id: row.id, revoked_at: (/* @__PURE__ */ new Date()).toISOString() },
|
|
988
|
+
{ context: SYSTEM_CTX5 }
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
async listLinks(filter, context) {
|
|
992
|
+
const where = {};
|
|
993
|
+
if (filter.object) where.object_name = filter.object;
|
|
994
|
+
if (filter.recordId) where.record_id = filter.recordId;
|
|
995
|
+
if (filter.createdBy) where.created_by = filter.createdBy;
|
|
996
|
+
if (!filter.includeRevoked) where.revoked_at = null;
|
|
997
|
+
const rows = await this.engine.find("sys_share_link", {
|
|
998
|
+
where,
|
|
999
|
+
limit: 200,
|
|
1000
|
+
sort: [{ field: "created_at", order: "desc" }],
|
|
1001
|
+
context: context.isSystem ? SYSTEM_CTX5 : context
|
|
1002
|
+
});
|
|
1003
|
+
return Array.isArray(rows) ? rows : [];
|
|
1004
|
+
}
|
|
1005
|
+
async resolveToken(token, probe = {}) {
|
|
1006
|
+
if (!token || typeof token !== "string" || token.length < 8) return null;
|
|
1007
|
+
const rows = await this.engine.find("sys_share_link", {
|
|
1008
|
+
where: { token },
|
|
1009
|
+
limit: 1,
|
|
1010
|
+
context: SYSTEM_CTX5
|
|
1011
|
+
});
|
|
1012
|
+
const row = Array.isArray(rows) ? rows[0] : void 0;
|
|
1013
|
+
if (!row) return null;
|
|
1014
|
+
if (row.revoked_at) return null;
|
|
1015
|
+
if (row.expires_at && Date.parse(row.expires_at) <= Date.now()) return null;
|
|
1016
|
+
if (row.audience === "signed_in" && !probe.signedInUserId) return null;
|
|
1017
|
+
if (row.audience === "email") {
|
|
1018
|
+
const allow = row.email_allowlist ?? [];
|
|
1019
|
+
const supplied = (probe.recipientEmail ?? "").trim().toLowerCase();
|
|
1020
|
+
if (!supplied || !allow.includes(supplied)) return null;
|
|
1021
|
+
}
|
|
1022
|
+
if (row.password_hash) {
|
|
1023
|
+
if (!probe.providedPassword) return null;
|
|
1024
|
+
const ok = await this.verifyPassword(probe.providedPassword, row.password_hash);
|
|
1025
|
+
if (!ok) return null;
|
|
1026
|
+
}
|
|
1027
|
+
const schema = this.engine.getSchema?.(row.object_name);
|
|
1028
|
+
const policy = getPolicy(schema);
|
|
1029
|
+
const redactFields = Array.from(
|
|
1030
|
+
/* @__PURE__ */ new Set([...policy.redactFields ?? [], ...row.redact_fields ?? []])
|
|
1031
|
+
);
|
|
1032
|
+
try {
|
|
1033
|
+
await this.engine.update(
|
|
1034
|
+
"sys_share_link",
|
|
1035
|
+
{
|
|
1036
|
+
id: row.id,
|
|
1037
|
+
last_used_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1038
|
+
use_count: (row.use_count ?? 0) + 1
|
|
1039
|
+
},
|
|
1040
|
+
{ context: SYSTEM_CTX5 }
|
|
1041
|
+
);
|
|
1042
|
+
} catch {
|
|
1043
|
+
}
|
|
1044
|
+
return { link: row, redactFields };
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
// src/share-link-routes.ts
|
|
1049
|
+
var SYSTEM_CTX6 = { isSystem: true, roles: [], permissions: [] };
|
|
1050
|
+
var defaultContext = (req) => {
|
|
1051
|
+
const header = (name) => {
|
|
1052
|
+
const v = req.headers?.[name];
|
|
1053
|
+
return Array.isArray(v) ? v[0] : v;
|
|
1054
|
+
};
|
|
1055
|
+
return {
|
|
1056
|
+
userId: header("x-user-id"),
|
|
1057
|
+
tenantId: header("x-tenant-id")
|
|
1058
|
+
};
|
|
1059
|
+
};
|
|
1060
|
+
function sendError(res, status, code, message) {
|
|
1061
|
+
res.status(status).json({ error: { code, message } });
|
|
1062
|
+
}
|
|
1063
|
+
function applyRedaction(record, redactFields) {
|
|
1064
|
+
if (!record || typeof record !== "object" || redactFields.length === 0) return record;
|
|
1065
|
+
if (Array.isArray(record)) return record.map((r) => applyRedaction(r, redactFields));
|
|
1066
|
+
const out = {};
|
|
1067
|
+
for (const [k, v] of Object.entries(record)) {
|
|
1068
|
+
if (redactFields.includes(k)) continue;
|
|
1069
|
+
out[k] = v;
|
|
1070
|
+
}
|
|
1071
|
+
return out;
|
|
1072
|
+
}
|
|
1073
|
+
function registerShareLinkRoutes(http, service, engine, opts = {}) {
|
|
1074
|
+
const base = opts.basePath ?? "/api/v1/share-links";
|
|
1075
|
+
const ctxOf = opts.contextFromRequest ?? defaultContext;
|
|
1076
|
+
http.post(base, (async (req, res) => {
|
|
1077
|
+
try {
|
|
1078
|
+
const ctx = ctxOf(req);
|
|
1079
|
+
const body = req.body ?? {};
|
|
1080
|
+
if (!body.object || !body.recordId) {
|
|
1081
|
+
return sendError(res, 400, "VALIDATION_FAILED", "object and recordId are required");
|
|
1082
|
+
}
|
|
1083
|
+
const link = await service.createLink(
|
|
1084
|
+
{
|
|
1085
|
+
object: body.object,
|
|
1086
|
+
recordId: body.recordId,
|
|
1087
|
+
permission: body.permission,
|
|
1088
|
+
audience: body.audience,
|
|
1089
|
+
expiresAt: body.expiresAt ?? null,
|
|
1090
|
+
emailAllowlist: body.emailAllowlist,
|
|
1091
|
+
password: body.password,
|
|
1092
|
+
redactFields: body.redactFields,
|
|
1093
|
+
label: body.label
|
|
1094
|
+
},
|
|
1095
|
+
ctx
|
|
1096
|
+
);
|
|
1097
|
+
await res.status(201).json({ link });
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
sendError(res, err?.status ?? 500, err?.code ?? "INTERNAL", err?.message ?? "Failed to create link");
|
|
1100
|
+
}
|
|
1101
|
+
}));
|
|
1102
|
+
http.get(base, (async (req, res) => {
|
|
1103
|
+
try {
|
|
1104
|
+
const ctx = ctxOf(req);
|
|
1105
|
+
const q = req.query ?? {};
|
|
1106
|
+
const link = await service.listLinks(
|
|
1107
|
+
{
|
|
1108
|
+
object: typeof q.object === "string" ? q.object : void 0,
|
|
1109
|
+
recordId: typeof q.recordId === "string" ? q.recordId : void 0,
|
|
1110
|
+
createdBy: typeof q.createdBy === "string" ? q.createdBy : void 0,
|
|
1111
|
+
includeRevoked: q.includeRevoked === "true" || q.includeRevoked === "1"
|
|
1112
|
+
},
|
|
1113
|
+
ctx
|
|
1114
|
+
);
|
|
1115
|
+
await res.json({ links: link });
|
|
1116
|
+
} catch (err) {
|
|
1117
|
+
sendError(res, err?.status ?? 500, err?.code ?? "INTERNAL", err?.message ?? "Failed to list links");
|
|
1118
|
+
}
|
|
1119
|
+
}));
|
|
1120
|
+
http.delete(`${base}/:idOrToken`, (async (req, res) => {
|
|
1121
|
+
try {
|
|
1122
|
+
const ctx = ctxOf(req);
|
|
1123
|
+
await service.revokeLink(req.params.idOrToken, ctx);
|
|
1124
|
+
await res.status(200).json({ ok: true });
|
|
1125
|
+
} catch (err) {
|
|
1126
|
+
sendError(res, err?.status ?? 500, err?.code ?? "INTERNAL", err?.message ?? "Failed to revoke link");
|
|
1127
|
+
}
|
|
1128
|
+
}));
|
|
1129
|
+
http.get(`${base}/:token/resolve`, (async (req, res) => {
|
|
1130
|
+
try {
|
|
1131
|
+
const q = req.query ?? {};
|
|
1132
|
+
const signedInUserId = (() => {
|
|
1133
|
+
const v = req.headers?.["x-user-id"];
|
|
1134
|
+
return Array.isArray(v) ? v[0] : v;
|
|
1135
|
+
})();
|
|
1136
|
+
const recipientEmail = typeof q.email === "string" ? q.email : void 0;
|
|
1137
|
+
const providedPassword = typeof q.password === "string" ? q.password : (() => {
|
|
1138
|
+
const v = req.headers?.["x-share-password"];
|
|
1139
|
+
return Array.isArray(v) ? v[0] : v;
|
|
1140
|
+
})();
|
|
1141
|
+
const resolved = await service.resolveToken(req.params.token, {
|
|
1142
|
+
signedInUserId,
|
|
1143
|
+
recipientEmail,
|
|
1144
|
+
providedPassword
|
|
1145
|
+
});
|
|
1146
|
+
if (!resolved) {
|
|
1147
|
+
const probe = await engine.find("sys_share_link", {
|
|
1148
|
+
where: { token: req.params.token },
|
|
1149
|
+
limit: 1,
|
|
1150
|
+
context: SYSTEM_CTX6
|
|
1151
|
+
});
|
|
1152
|
+
const row = Array.isArray(probe) && probe[0] ? probe[0] : null;
|
|
1153
|
+
if (row && !row.revoked_at && (!row.expires_at || Date.parse(row.expires_at) > Date.now())) {
|
|
1154
|
+
if (row.password_hash) {
|
|
1155
|
+
return sendError(
|
|
1156
|
+
res,
|
|
1157
|
+
401,
|
|
1158
|
+
providedPassword ? "WRONG_PASSWORD" : "NEEDS_PASSWORD",
|
|
1159
|
+
providedPassword ? "Incorrect password" : "This link requires a password"
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
if (row.audience === "signed_in" && !signedInUserId) {
|
|
1163
|
+
return sendError(res, 401, "SIGN_IN_REQUIRED", "Please sign in to view this link");
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (row && (row.revoked_at || row.expires_at && Date.parse(row.expires_at) <= Date.now())) {
|
|
1167
|
+
return sendError(res, 410, "EXPIRED_OR_REVOKED", "Share link has expired or been revoked");
|
|
1168
|
+
}
|
|
1169
|
+
return sendError(res, 404, "INVALID_OR_EXPIRED", "Share link is invalid, expired, or revoked");
|
|
1170
|
+
}
|
|
1171
|
+
const rows = await engine.find(resolved.link.object_name, {
|
|
1172
|
+
where: { id: resolved.link.record_id },
|
|
1173
|
+
limit: 1,
|
|
1174
|
+
context: SYSTEM_CTX6
|
|
1175
|
+
});
|
|
1176
|
+
const record = Array.isArray(rows) && rows[0] ? rows[0] : null;
|
|
1177
|
+
if (!record) {
|
|
1178
|
+
return sendError(res, 410, "RECORD_GONE", "The shared record no longer exists");
|
|
1179
|
+
}
|
|
1180
|
+
await res.json({
|
|
1181
|
+
record: applyRedaction(record, resolved.redactFields),
|
|
1182
|
+
link: {
|
|
1183
|
+
id: resolved.link.id,
|
|
1184
|
+
token: resolved.link.token,
|
|
1185
|
+
object_name: resolved.link.object_name,
|
|
1186
|
+
record_id: resolved.link.record_id,
|
|
1187
|
+
permission: resolved.link.permission,
|
|
1188
|
+
audience: resolved.link.audience,
|
|
1189
|
+
expires_at: resolved.link.expires_at,
|
|
1190
|
+
label: resolved.link.label,
|
|
1191
|
+
created_at: resolved.link.created_at
|
|
1192
|
+
},
|
|
1193
|
+
redactFields: resolved.redactFields
|
|
1194
|
+
});
|
|
1195
|
+
} catch (err) {
|
|
1196
|
+
sendError(res, err?.status ?? 500, err?.code ?? "INTERNAL", err?.message ?? "Failed to resolve link");
|
|
1197
|
+
}
|
|
1198
|
+
}));
|
|
1199
|
+
http.get(`${base}/:token/messages`, (async (req, res) => {
|
|
1200
|
+
try {
|
|
1201
|
+
const password = typeof req.query?.password === "string" ? req.query.password : void 0;
|
|
1202
|
+
const resolved = await service.resolveToken(req.params.token, { providedPassword: password });
|
|
1203
|
+
if (!resolved) {
|
|
1204
|
+
sendError(res, 404, "NOT_FOUND", "Share link not found");
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
if (resolved.link.object_name !== "ai_conversations") {
|
|
1208
|
+
sendError(res, 400, "UNSUPPORTED", "This share link does not expose messages");
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
const SYSTEM_CTX8 = { isSystem: true, roles: [], permissions: [] };
|
|
1212
|
+
const rows = await engine.find("ai_messages", {
|
|
1213
|
+
where: { conversation_id: resolved.link.record_id },
|
|
1214
|
+
sort: [{ field: "created_at", direction: "asc" }],
|
|
1215
|
+
limit: 500,
|
|
1216
|
+
context: SYSTEM_CTX8
|
|
1217
|
+
});
|
|
1218
|
+
res.status(200).json({ data: rows ?? [] });
|
|
1219
|
+
} catch (err) {
|
|
1220
|
+
sendError(
|
|
1221
|
+
res,
|
|
1222
|
+
err?.status ?? 500,
|
|
1223
|
+
err?.code ?? "INTERNAL",
|
|
1224
|
+
err?.message ?? "Failed to load messages"
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
}));
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// src/rule-hooks.ts
|
|
1231
|
+
var SYSTEM_CTX7 = { isSystem: true, roles: [], permissions: [] };
|
|
801
1232
|
var SHARING_RULE_HOOK_PACKAGE = "plugin-sharing:rules";
|
|
802
1233
|
function bindRuleHooks(engine, service, rules, logger) {
|
|
803
1234
|
const objects = /* @__PURE__ */ new Set();
|
|
@@ -812,7 +1243,7 @@ function bindRuleHooks(engine, service, rules, logger) {
|
|
|
812
1243
|
const data = ctx?.result ?? ctx?.input?.data ?? {};
|
|
813
1244
|
const id = String(data?.id ?? ctx?.input?.id ?? "");
|
|
814
1245
|
if (!id) return;
|
|
815
|
-
await service.evaluateAllForRecord(objectName, id,
|
|
1246
|
+
await service.evaluateAllForRecord(objectName, id, SYSTEM_CTX7);
|
|
816
1247
|
} catch (err) {
|
|
817
1248
|
logger?.warn?.("[sharing-rule] hook evaluation failed", { object: objectName, error: err?.message });
|
|
818
1249
|
}
|
|
@@ -846,7 +1277,7 @@ var SharingServicePlugin = class {
|
|
|
846
1277
|
scope: "system",
|
|
847
1278
|
defaultDatasource: "cloud",
|
|
848
1279
|
namespace: "sys",
|
|
849
|
-
objects: [import_security.SysRecordShare, import_security.SysSharingRule, import_identity.SysDepartment, import_identity.SysDepartmentMember]
|
|
1280
|
+
objects: [import_security.SysRecordShare, import_security.SysSharingRule, import_identity.SysDepartment, import_identity.SysDepartmentMember, import_security.SysShareLink]
|
|
850
1281
|
});
|
|
851
1282
|
ctx.logger.info("SharingServicePlugin: schema registered");
|
|
852
1283
|
}
|
|
@@ -898,6 +1329,31 @@ var SharingServicePlugin = class {
|
|
|
898
1329
|
} catch (err) {
|
|
899
1330
|
ctx.logger.warn("SharingServicePlugin: sharing-rule subsystem not started", { error: err?.message });
|
|
900
1331
|
}
|
|
1332
|
+
try {
|
|
1333
|
+
this.linkService = new ShareLinkService({ engine });
|
|
1334
|
+
ctx.registerService("shareLinks", this.linkService);
|
|
1335
|
+
if (this.options.registerShareLinkRoutes !== false) {
|
|
1336
|
+
let http = null;
|
|
1337
|
+
try {
|
|
1338
|
+
http = ctx.getService("http-server");
|
|
1339
|
+
} catch {
|
|
1340
|
+
}
|
|
1341
|
+
if (http) {
|
|
1342
|
+
registerShareLinkRoutes(http, this.linkService, engine, {
|
|
1343
|
+
basePath: this.options.shareLinkBasePath
|
|
1344
|
+
});
|
|
1345
|
+
ctx.logger.info(
|
|
1346
|
+
"SharingServicePlugin: share-link routes mounted at " + (this.options.shareLinkBasePath ?? "/api/v1/share-links")
|
|
1347
|
+
);
|
|
1348
|
+
} else {
|
|
1349
|
+
ctx.logger.warn(
|
|
1350
|
+
'SharingServicePlugin: no HTTP server \u2014 share-link REST routes not registered. ShareLinkService is still reachable via kernel.getService("shareLinks").'
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
} catch (err) {
|
|
1355
|
+
ctx.logger.warn("SharingServicePlugin: share-link subsystem not started", { error: err?.message });
|
|
1356
|
+
}
|
|
901
1357
|
});
|
|
902
1358
|
}
|
|
903
1359
|
};
|
|
@@ -960,21 +1416,28 @@ function inferTargetId(data, options) {
|
|
|
960
1416
|
}
|
|
961
1417
|
return void 0;
|
|
962
1418
|
}
|
|
1419
|
+
|
|
1420
|
+
// src/index.ts
|
|
1421
|
+
var import_contracts = require("@objectstack/spec/contracts");
|
|
963
1422
|
// Annotate the CommonJS export names for ESM import in node:
|
|
964
1423
|
0 && (module.exports = {
|
|
965
1424
|
DepartmentGraphService,
|
|
1425
|
+
SHARE_LINK_SERVICE,
|
|
966
1426
|
SHARING_RULE_HOOK_PACKAGE,
|
|
1427
|
+
ShareLinkService,
|
|
967
1428
|
SharingRuleService,
|
|
968
1429
|
SharingService,
|
|
969
1430
|
SharingServicePlugin,
|
|
970
1431
|
SysDepartment,
|
|
971
1432
|
SysDepartmentMember,
|
|
972
1433
|
SysRecordShare,
|
|
1434
|
+
SysShareLink,
|
|
973
1435
|
SysSharingRule,
|
|
974
1436
|
TeamGraphService,
|
|
975
1437
|
bindRuleHooks,
|
|
976
1438
|
buildSharingMiddleware,
|
|
977
1439
|
expandPrincipal,
|
|
1440
|
+
registerShareLinkRoutes,
|
|
978
1441
|
unbindAllRuleHooks
|
|
979
1442
|
});
|
|
980
1443
|
//# sourceMappingURL=index.js.map
|