artifacty 0.11.1 → 0.13.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.
@@ -0,0 +1,136 @@
1
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
2
+ import { createStore, getArtifact, insertAuditRow, openDatabase, transaction } from "./storage.js";
3
+
4
+ function error(message, statusCode = 403) {
5
+ return Object.assign(new Error(message), { statusCode });
6
+ }
7
+
8
+ function database(store) {
9
+ const db = openDatabase(store);
10
+ db.exec(`CREATE TABLE IF NOT EXISTS artifact_share_links (
11
+ id TEXT PRIMARY KEY,
12
+ artifact_id TEXT NOT NULL REFERENCES artifacts(id) ON DELETE CASCADE,
13
+ token_hash TEXT NOT NULL UNIQUE,
14
+ version INTEGER,
15
+ expires_at TEXT NOT NULL,
16
+ created_at TEXT NOT NULL,
17
+ revoked_at TEXT,
18
+ issuer_user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
19
+ owner_user_id TEXT
20
+ )`);
21
+ return db;
22
+ }
23
+
24
+ function policy(db) {
25
+ return { enabledInPrivate: db.prepare("SELECT value FROM meta WHERE key = 'share_link_policy'").get()?.value === "true" };
26
+ }
27
+
28
+ function privateMode(db) {
29
+ return db.prepare("SELECT value FROM meta WHERE key = 'access_mode'").get()?.value === "private";
30
+ }
31
+
32
+ function principal(db, access) {
33
+ const user = access?.userId && db.prepare("SELECT id, role FROM users WHERE id = ? AND active = 1").get(access.userId);
34
+ if (!user) throw error("An active personal account is required");
35
+ return user;
36
+ }
37
+
38
+ function managedArtifact(db, artifactId, access) {
39
+ const user = principal(db, access);
40
+ const artifact = db.prepare("SELECT id, owner_user_id, archived_at FROM artifacts WHERE id = ?").get(artifactId);
41
+ if (!artifact) throw error("Artifact not found", 404);
42
+ if (user.role !== "admin" && artifact.owner_user_id !== user.id) throw error("Only the owner or an administrator may manage share links");
43
+ return { artifact, user };
44
+ }
45
+
46
+ function metadata(row) {
47
+ return { id: row.id, version: row.version, expiresAt: row.expires_at, createdAt: row.created_at, revokedAt: row.revoked_at };
48
+ }
49
+
50
+ export function getShareLinkPolicy(store = createStore()) {
51
+ const db = database(store);
52
+ try { return policy(db); } finally { db.close(); }
53
+ }
54
+
55
+ export function setShareLinkPolicy(store = createStore(), { enabledInPrivate } = {}, { access, audit } = {}) {
56
+ const db = database(store);
57
+ try {
58
+ if (principal(db, access).role !== "admin") throw error("Administrator access required");
59
+ if (typeof enabledInPrivate !== "boolean") throw error("enabledInPrivate must be boolean", 400);
60
+ transaction(db, () => {
61
+ db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('share_link_policy', ?)").run(String(enabledInPrivate));
62
+ insertAuditRow(db, { action: "share-link-policy-change", artifactId: "", audit, metadata: { enabledInPrivate } });
63
+ });
64
+ return { enabledInPrivate };
65
+ } finally { db.close(); }
66
+ }
67
+
68
+ export function createShareLink(store = createStore(), artifactId, { version = null, expiresAt } = {}, { access, audit } = {}) {
69
+ const db = database(store);
70
+ try {
71
+ const { artifact, user } = managedArtifact(db, artifactId, access);
72
+ if (privateMode(db) && !policy(db).enabledInPrivate) throw error("Share links are disabled in private mode");
73
+ if (artifact.archived_at) throw error("Archived artifacts cannot be shared", 400);
74
+ if (version !== null && (!Number.isSafeInteger(version) || version < 1 || !db.prepare("SELECT 1 FROM artifact_versions WHERE artifact_id = ? AND version = ?").get(artifactId, version))) {
75
+ throw error("Invalid artifact version", 400);
76
+ }
77
+ const expiry = expiresAt === undefined ? Date.now() + 7 * 86400000 : typeof expiresAt === "string" ? Date.parse(expiresAt) : NaN;
78
+ if (!Number.isFinite(expiry) || expiry <= Date.now()) throw error("Expiration must be a future date", 400);
79
+ const token = randomBytes(32).toString("base64url");
80
+ const id = randomUUID();
81
+ const createdAt = new Date().toISOString();
82
+ expiresAt = new Date(expiry).toISOString();
83
+ transaction(db, () => {
84
+ db.prepare(`INSERT INTO artifact_share_links (id, artifact_id, token_hash, version, expires_at, created_at, issuer_user_id, owner_user_id)
85
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(id, artifactId, createHash("sha256").update(token).digest("hex"), version, expiresAt, createdAt, user.id, artifact.owner_user_id);
86
+ insertAuditRow(db, { action: "share-link-create", artifactId, audit, metadata: { linkId: id, version, expiresAt } });
87
+ });
88
+ return { id, token, version, expiresAt, createdAt, revokedAt: null };
89
+ } finally { db.close(); }
90
+ }
91
+
92
+ export function listShareLinks(store = createStore(), artifactId, { access } = {}) {
93
+ const db = database(store);
94
+ try {
95
+ managedArtifact(db, artifactId, access);
96
+ return db.prepare("SELECT * FROM artifact_share_links WHERE artifact_id = ? ORDER BY created_at DESC, id").all(artifactId).map(metadata);
97
+ } finally { db.close(); }
98
+ }
99
+
100
+ export function revokeShareLink(store = createStore(), artifactId, linkId, { access, audit } = {}) {
101
+ const db = database(store);
102
+ try {
103
+ managedArtifact(db, artifactId, access);
104
+ transaction(db, () => {
105
+ const result = db.prepare("UPDATE artifact_share_links SET revoked_at = COALESCE(revoked_at, ?) WHERE id = ? AND artifact_id = ?").run(new Date().toISOString(), linkId, artifactId);
106
+ if (!result.changes) throw error("Share link not found", 404);
107
+ insertAuditRow(db, { action: "share-link-revoke", artifactId, audit, metadata: { linkId } });
108
+ });
109
+ return { id: linkId, revoked: true };
110
+ } finally { db.close(); }
111
+ }
112
+
113
+ function resolveRow(db, token) {
114
+ if (typeof token !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(token)) throw error("Share link not found", 404);
115
+ const row = db.prepare(`SELECT l.*, a.owner_user_id AS current_owner, a.archived_at, u.active, u.role
116
+ FROM artifact_share_links l JOIN artifacts a ON a.id = l.artifact_id JOIN users u ON u.id = l.issuer_user_id
117
+ WHERE l.token_hash = ?`).get(createHash("sha256").update(token).digest("hex"));
118
+ if (!row || row.revoked_at || row.archived_at || !row.active || row.owner_user_id !== row.current_owner ||
119
+ (row.role !== "admin" && row.issuer_user_id !== row.current_owner) ||
120
+ !Number.isFinite(Date.parse(row.expires_at)) || Date.parse(row.expires_at) <= Date.now() ||
121
+ (privateMode(db) && !policy(db).enabledInPrivate)) throw error("Share link not found", 404);
122
+ return row;
123
+ }
124
+
125
+ export async function resolveShareLink(store = createStore(), token) {
126
+ const db = database(store);
127
+ try {
128
+ const row = resolveRow(db, token);
129
+ let artifact;
130
+ try { artifact = await getArtifact(store, row.artifact_id, { version: row.version, audit: { surface: "share-link", actor: "share-link" } }); }
131
+ catch { throw error("Share link not found", 404); }
132
+ // Recheck after the asynchronous file read to honor revocation during I/O.
133
+ resolveRow(db, token);
134
+ return { artifact, version: artifact.version, content: artifact.content };
135
+ } finally { db.close(); }
136
+ }