@jini-ai/cms 0.3.10 → 0.4.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/CHANGELOG.md +59 -0
- package/README.md +47 -19
- package/dist/content-types/agent-tools.d.ts +3 -56
- package/dist/content-types/agent-tools.js +22 -6
- package/dist/content-types/cleanup.d.ts +18 -13
- package/dist/content-types/cleanup.js +16 -11
- package/dist/content-types/errors.d.ts +57 -24
- package/dist/content-types/errors.js +56 -39
- package/dist/content-types/field-defs.d.ts +8 -4
- package/dist/content-types/field-defs.js +19 -14
- package/dist/content-types/index-provisioning.d.ts +61 -47
- package/dist/content-types/index-provisioning.js +90 -66
- package/dist/content-types/index.d.ts +4 -3
- package/dist/content-types/lifecycle-dispatch.d.ts +11 -5
- package/dist/content-types/lifecycle-dispatch.js +5 -4
- package/dist/content-types/lifecycle.d.ts +23 -18
- package/dist/content-types/lifecycle.js +71 -64
- package/dist/content-types/repo.memory.d.ts +5 -4
- package/dist/content-types/repo.memory.js +3 -2
- package/dist/content-types/tool-registrations.d.ts +6 -8
- package/dist/content-types/tool-registrations.js +24 -28
- package/dist/content-types/types.d.ts +17 -13
- package/dist/content-types/types.js +11 -5
- package/dist/content-types/write-service.d.ts +20 -22
- package/dist/content-types/write-service.js +78 -75
- package/dist/core/authorization.d.ts +15 -0
- package/dist/core/authorization.js +2 -0
- package/dist/core/commands/change-set.d.ts +15 -7
- package/dist/core/commands/command.d.ts +45 -26
- package/dist/core/commands/command.js +73 -63
- package/dist/core/entity-liveness.d.ts +11 -7
- package/dist/core/entity-liveness.js +5 -4
- package/dist/core/index.d.ts +12 -5
- package/dist/core/index.js +10 -4
- package/dist/core/ports.d.ts +25 -24
- package/dist/core/ports.js +1 -0
- package/dist/core/tools/args.d.ts +49 -0
- package/dist/core/tools/args.js +46 -0
- package/dist/core/tools/index.d.ts +3 -0
- package/dist/core/tools/index.js +3 -0
- package/dist/entries/agent-tools.d.ts +1 -73
- package/dist/entries/agent-tools.js +19 -13
- package/dist/entries/errors.d.ts +28 -14
- package/dist/entries/errors.js +17 -11
- package/dist/entries/field-validation.d.ts +5 -3
- package/dist/entries/field-validation.js +7 -5
- package/dist/entries/index.d.ts +2 -2
- package/dist/entries/lifecycle-dispatch.d.ts +5 -2
- package/dist/entries/lifecycle-dispatch.js +2 -1
- package/dist/entries/list.d.ts +4 -2
- package/dist/entries/list.js +4 -3
- package/dist/entries/repo.memory.d.ts +12 -10
- package/dist/entries/repo.memory.js +11 -10
- package/dist/entries/tool-registrations.d.ts +6 -8
- package/dist/entries/tool-registrations.js +42 -41
- package/dist/entries/types.d.ts +0 -7
- package/dist/entries/write-service.d.ts +28 -29
- package/dist/entries/write-service.js +114 -105
- package/dist/http/settings/cms-adapter.d.ts +20 -0
- package/dist/http/settings/cms-adapter.js +124 -0
- package/dist/http/settings/contracts.d.ts +135 -0
- package/dist/http/settings/contracts.js +2 -0
- package/dist/http/settings/http.d.ts +10 -0
- package/dist/http/settings/http.js +211 -0
- package/dist/http/settings/index.d.ts +5 -0
- package/dist/http/settings/index.js +4 -0
- package/dist/http/settings/shared.d.ts +68 -0
- package/dist/http/settings/shared.js +54 -0
- package/dist/http/settings/stream.d.ts +24 -0
- package/dist/http/settings/stream.js +147 -0
- package/dist/media/agent-tools.d.ts +1 -66
- package/dist/media/agent-tools.js +5 -1
- package/dist/media/blob-gc-lock.d.ts +6 -2
- package/dist/media/blob-gc-lock.js +4 -2
- package/dist/media/blob-gc.d.ts +64 -51
- package/dist/media/blob-gc.js +62 -52
- package/dist/media/blob-key.d.ts +1 -1
- package/dist/media/content-type-sniffer.d.ts +3 -1
- package/dist/media/content-type-sniffer.js +1 -1
- package/dist/media/html-attributes.d.ts +9 -5
- package/dist/media/html-attributes.js +8 -6
- package/dist/media/image-transformer.js +1 -1
- package/dist/media/image-transformer.sharp.d.ts +6 -0
- package/dist/media/image-transformer.sharp.js +12 -6
- package/dist/media/import/fetch-image.d.ts +24 -0
- package/dist/media/import/fetch-image.js +137 -0
- package/dist/media/import/index.d.ts +3 -0
- package/dist/media/import/index.js +3 -0
- package/dist/media/import/ports.d.ts +38 -0
- package/dist/media/index.d.ts +7 -1
- package/dist/media/index.js +1 -0
- package/dist/media/media-service.d.ts +14 -10
- package/dist/media/media-service.js +57 -52
- package/dist/media/ports.d.ts +3 -2
- package/dist/media/rendition-service.d.ts +3 -3
- package/dist/media/rendition-service.js +38 -37
- package/dist/media/repo.memory.d.ts +18 -7
- package/dist/media/repo.memory.js +12 -6
- package/dist/media/tool-registrations.d.ts +16 -15
- package/dist/media/tool-registrations.js +84 -72
- package/dist/media/transform-lock.d.ts +22 -12
- package/dist/media/transform-lock.js +14 -13
- package/dist/media/transform-registry.d.ts +3 -3
- package/dist/media/transform-registry.js +19 -18
- package/dist/media/transform-types.d.ts +8 -3
- package/dist/media/transform-types.js +10 -5
- package/dist/media/types.d.ts +21 -4
- package/dist/media/types.js +15 -2
- package/dist/navigation/agent-tools.d.ts +1 -80
- package/dist/navigation/agent-tools.js +15 -3
- package/dist/navigation/contracts.d.ts +1 -1
- package/dist/navigation/index.d.ts +2 -2
- package/dist/navigation/menu-service.d.ts +57 -40
- package/dist/navigation/menu-service.js +80 -69
- package/dist/navigation/ports.d.ts +1 -1
- package/dist/navigation/read-model.d.ts +3 -1
- package/dist/navigation/read-model.js +2 -2
- package/dist/navigation/reconcile.d.ts +2 -2
- package/dist/navigation/reconcile.js +2 -1
- package/dist/navigation/repo.memory.d.ts +7 -3
- package/dist/navigation/repo.memory.js +4 -2
- package/dist/navigation/resolver.d.ts +4 -1
- package/dist/navigation/resolver.js +1 -1
- package/dist/navigation/tool-registrations.d.ts +6 -8
- package/dist/navigation/tool-registrations.js +44 -42
- package/dist/navigation/types.d.ts +1 -1
- package/dist/presentation/presentation.d.ts +11 -3
- package/dist/presentation/presentation.js +13 -6
- package/dist/presentation/repo.memory.d.ts +6 -2
- package/dist/presentation/repo.memory.js +3 -2
- package/dist/settings/agent-tools.d.ts +2 -75
- package/dist/settings/agent-tools.js +49 -45
- package/dist/settings/agent-value-write-tools.d.ts +22 -0
- package/dist/settings/agent-value-write-tools.js +154 -0
- package/dist/settings/agent-writable-preferences.js +8 -20
- package/dist/settings/agent-write-denylist.d.ts +19 -0
- package/dist/settings/agent-write-denylist.js +15 -0
- package/dist/settings/change-feed.d.ts +11 -2
- package/dist/settings/change-feed.js +4 -4
- package/dist/settings/definitions-dispatch.d.ts +8 -3
- package/dist/settings/definitions-dispatch.js +5 -5
- package/dist/settings/dictionaries/index.d.ts +5 -2
- package/dist/settings/dictionaries/index.js +2 -2
- package/dist/settings/dictionaries/settings-dialog.ar.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.bn.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.de.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.es.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.fa.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.fr.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.hi.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.hu.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.id.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.it.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.ja.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.ko.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.pl.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.pt-BR.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.ru.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.th.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.tr.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.uk.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.ur.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-CN.js +0 -1
- package/dist/settings/dictionaries/settings-dialog.zh-TW.js +0 -1
- package/dist/settings/ensure-definitions.d.ts +5 -5
- package/dist/settings/errors.d.ts +7 -1
- package/dist/settings/errors.js +7 -1
- package/dist/settings/index.d.ts +4 -1
- package/dist/settings/index.js +1 -0
- package/dist/settings/ports.d.ts +6 -5
- package/dist/settings/principal-lookup.d.ts +9 -0
- package/dist/settings/principal-lookup.js +2 -0
- package/dist/settings/purge-service.d.ts +5 -4
- package/dist/settings/purge-service.js +3 -2
- package/dist/settings/settings.d.ts +22 -7
- package/dist/settings/settings.js +47 -26
- package/dist/settings/tool-registrations.d.ts +25 -32
- package/dist/settings/tool-registrations.js +66 -56
- package/dist/settings/types.d.ts +1 -1
- package/dist/settings/ui-tab-definitions.d.ts +1 -1
- package/dist/settings/write-service.d.ts +32 -26
- package/dist/settings/write-service.js +26 -20
- package/dist/taxonomy/agent-tools.d.ts +1 -66
- package/dist/taxonomy/agent-tools.js +25 -15
- package/dist/taxonomy/content-lookup.js +1 -1
- package/dist/taxonomy/index.d.ts +2 -2
- package/dist/taxonomy/merge-term.d.ts +7 -4
- package/dist/taxonomy/merge-term.js +7 -5
- package/dist/taxonomy/repo.memory.d.ts +42 -19
- package/dist/taxonomy/repo.memory.js +14 -12
- package/dist/taxonomy/validation-chain.d.ts +39 -18
- package/dist/taxonomy/validation-chain.js +35 -25
- package/dist/taxonomy/write-service.d.ts +106 -45
- package/dist/taxonomy/write-service.js +221 -179
- package/dist/trash/adapters/record-store.d.ts +52 -0
- package/dist/trash/adapters/record-store.js +36 -0
- package/dist/trash/cursor.d.ts +20 -0
- package/dist/trash/cursor.js +36 -0
- package/dist/trash/follow-ups.d.ts +76 -0
- package/dist/trash/follow-ups.js +46 -0
- package/dist/trash/index.d.ts +8 -0
- package/dist/trash/index.js +8 -0
- package/dist/trash/ports.d.ts +308 -0
- package/dist/trash/ports.js +2 -0
- package/dist/trash/repo.memory.d.ts +50 -0
- package/dist/trash/repo.memory.js +114 -0
- package/dist/trash/sweeper.d.ts +77 -0
- package/dist/trash/sweeper.js +127 -0
- package/dist/trash/write-service.d.ts +100 -0
- package/dist/trash/write-service.js +242 -0
- package/dist/workspace/agent-tools.d.ts +1 -59
- package/dist/workspace/agent-tools.js +27 -12
- package/dist/workspace/create.d.ts +31 -6
- package/dist/workspace/create.js +18 -5
- package/dist/workspace/delete.d.ts +18 -13
- package/dist/workspace/delete.js +20 -12
- package/dist/workspace/index.d.ts +1 -1
- package/dist/workspace/repo.memory.d.ts +19 -4
- package/dist/workspace/repo.memory.js +26 -4
- package/dist/workspace/tool-registrations.d.ts +2 -2
- package/dist/workspace/tool-registrations.js +13 -13
- package/dist/workspace/update.d.ts +6 -5
- package/dist/workspace/update.js +9 -8
- package/package.json +44 -38
- package/dist/identity/admin-crud-service.d.ts +0 -280
- package/dist/identity/admin-crud-service.js +0 -468
- package/dist/identity/agent-tool-input.d.ts +0 -58
- package/dist/identity/agent-tool-input.js +0 -126
- package/dist/identity/agent-tools.d.ts +0 -101
- package/dist/identity/agent-tools.js +0 -290
- package/dist/identity/auth-service.d.ts +0 -118
- package/dist/identity/auth-service.js +0 -170
- package/dist/identity/authorize.d.ts +0 -90
- package/dist/identity/authorize.js +0 -94
- package/dist/identity/grant-service.d.ts +0 -198
- package/dist/identity/grant-service.js +0 -361
- package/dist/identity/hasher.d.ts +0 -39
- package/dist/identity/hasher.js +0 -39
- package/dist/identity/index.d.ts +0 -62
- package/dist/identity/index.js +0 -54
- package/dist/identity/password-policy.d.ts +0 -57
- package/dist/identity/password-policy.js +0 -66
- package/dist/identity/permission-migrations.d.ts +0 -86
- package/dist/identity/permission-migrations.js +0 -67
- package/dist/identity/permissions.d.ts +0 -35
- package/dist/identity/permissions.js +0 -603
- package/dist/identity/ports.d.ts +0 -201
- package/dist/identity/repo.memory.d.ts +0 -164
- package/dist/identity/repo.memory.js +0 -224
- package/dist/identity/seed.d.ts +0 -54
- package/dist/identity/seed.js +0 -429
- package/dist/identity/tool-registrations.d.ts +0 -50
- package/dist/identity/tool-registrations.js +0 -419
- package/dist/identity/types.d.ts +0 -185
- package/dist/identity/types.js +0 -64
- package/dist/identity/username.d.ts +0 -12
- package/dist/identity/username.js +0 -14
- /package/dist/{identity → media/import}/ports.js +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/** Starts after HTTP authorization. All scheduling and storage are injected. */
|
|
2
|
+
/** Poll with bounded pages, tenant-scoped ids and retryable reads; close cancels all schedules.
|
|
3
|
+
* O(page size) per tick and O(1) connection state; repository effects live behind the feed port.
|
|
4
|
+
* Invalid scheduler intervals/page sizes throw RangeError before opening; initial read errors propagate.
|
|
5
|
+
*/
|
|
6
|
+
export async function openSettingsChangeStream(required, optional = {}) {
|
|
7
|
+
const { request, response, workspaceId, principalId, changeFeed, scheduler } = required;
|
|
8
|
+
const pageSize = optional.revisionPageSize ?? 200;
|
|
9
|
+
const intervals = [optional.pollIntervalMs ?? 1_000, optional.keepaliveIntervalMs ?? 25_000, optional.reauthorizeIntervalMs ?? 30_000];
|
|
10
|
+
if (!Number.isSafeInteger(pageSize) || pageSize <= 0 || intervals.some((n) => !Number.isFinite(n) || n <= 0)) {
|
|
11
|
+
throw new RangeError("settings feed page size and intervals must be positive");
|
|
12
|
+
}
|
|
13
|
+
let closed = false;
|
|
14
|
+
let ticking = false;
|
|
15
|
+
let checkingAuthorization = false;
|
|
16
|
+
const cancellations = [];
|
|
17
|
+
const reportError = (operation, error) => {
|
|
18
|
+
// Reporting is best-effort: a host hook must never turn recovery into an unhandled rejection.
|
|
19
|
+
try {
|
|
20
|
+
if (optional.onError)
|
|
21
|
+
optional.onError({ operation, error });
|
|
22
|
+
else
|
|
23
|
+
console.error(`[settings] change feed ${operation} failed`, error);
|
|
24
|
+
}
|
|
25
|
+
catch { /* Diagnostic failures cannot interrupt stream cleanup or retries. */ }
|
|
26
|
+
};
|
|
27
|
+
const recover = (operation, effect) => {
|
|
28
|
+
try {
|
|
29
|
+
effect();
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
reportError(operation, error);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const release = () => {
|
|
36
|
+
// Every resource gets its own recovery boundary so one failing cancellation cannot leak others.
|
|
37
|
+
for (const cancel of cancellations.splice(0))
|
|
38
|
+
recover("cancel", cancel);
|
|
39
|
+
recover("request-listener", () => { request.off("close", close); });
|
|
40
|
+
recover("response-listener", () => { response.off("close", close); });
|
|
41
|
+
};
|
|
42
|
+
const close = () => {
|
|
43
|
+
if (closed)
|
|
44
|
+
return;
|
|
45
|
+
closed = true;
|
|
46
|
+
release();
|
|
47
|
+
recover("close", () => { required.onClose({ close }); });
|
|
48
|
+
if (!response.writableEnded && !response.destroyed)
|
|
49
|
+
recover("end", () => { response.end(); });
|
|
50
|
+
};
|
|
51
|
+
request.on("close", close);
|
|
52
|
+
response.on("close", close);
|
|
53
|
+
try {
|
|
54
|
+
const head = await changeFeed.head({});
|
|
55
|
+
if (closed || required.isDisposed() || response.destroyed || response.writableEnded) {
|
|
56
|
+
close();
|
|
57
|
+
return close;
|
|
58
|
+
}
|
|
59
|
+
const resumeFrom = Number(request.headers["last-event-id"]);
|
|
60
|
+
let cursor = Number.isFinite(resumeFrom) && resumeFrom >= 0 ? Math.min(resumeFrom, head) : head;
|
|
61
|
+
let lastEmittedId = cursor;
|
|
62
|
+
const viewer = { workspaceId, principalId };
|
|
63
|
+
response.writeHead(200, {
|
|
64
|
+
"Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform",
|
|
65
|
+
...(request.httpVersionMajor < 2 ? { Connection: "keep-alive" } : {}), "X-Accel-Buffering": "no",
|
|
66
|
+
});
|
|
67
|
+
const tick = async () => {
|
|
68
|
+
if (closed || ticking)
|
|
69
|
+
return;
|
|
70
|
+
ticking = true;
|
|
71
|
+
try {
|
|
72
|
+
// Snapshot head before the page: concurrent writes must never be skipped.
|
|
73
|
+
const ledgerHead = await changeFeed.head({});
|
|
74
|
+
if (closed)
|
|
75
|
+
return;
|
|
76
|
+
const batch = await changeFeed.collect({ sinceSeq: cursor, limit: pageSize, viewer });
|
|
77
|
+
if (closed)
|
|
78
|
+
return;
|
|
79
|
+
const examined = batch.examinedCount < pageSize ? ledgerHead : batch.cursor;
|
|
80
|
+
cursor = Math.max(cursor, examined, batch.cursor);
|
|
81
|
+
if (!batch.namespaces.length)
|
|
82
|
+
return;
|
|
83
|
+
// The internal global head is never disclosed as an event id.
|
|
84
|
+
lastEmittedId = Math.max(lastEmittedId, batch.cursor);
|
|
85
|
+
response.write(`id: ${lastEmittedId}\nevent: settings-changed\ndata: ${JSON.stringify({ namespaces: batch.namespaces })}\n\n`);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (!closed)
|
|
89
|
+
reportError("poll", error);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
ticking = false;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const reauthorize = async () => {
|
|
96
|
+
if (closed || checkingAuthorization)
|
|
97
|
+
return;
|
|
98
|
+
checkingAuthorization = true;
|
|
99
|
+
try {
|
|
100
|
+
const result = await required.authorize({ principalId, workspaceId, permission: required.permission, entityType: "setting-value" });
|
|
101
|
+
if (!closed && !result.allowed)
|
|
102
|
+
close();
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (!closed)
|
|
106
|
+
reportError("re-authorization", error);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
checkingAuthorization = false;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const schedule = (intervalMs, callback) => {
|
|
113
|
+
const cancel = scheduler.every({ intervalMs, callback });
|
|
114
|
+
if (closed)
|
|
115
|
+
recover("cancel", cancel);
|
|
116
|
+
else
|
|
117
|
+
cancellations.push(cancel);
|
|
118
|
+
};
|
|
119
|
+
schedule(intervals[0], () => { void tick(); });
|
|
120
|
+
schedule(intervals[1], () => {
|
|
121
|
+
if (closed)
|
|
122
|
+
return;
|
|
123
|
+
try {
|
|
124
|
+
response.write(": keepalive\n\n");
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
reportError("keepalive", error);
|
|
128
|
+
close();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
schedule(intervals[2], () => { void reauthorize(); });
|
|
132
|
+
if (!closed)
|
|
133
|
+
response.write(": connected\n\n");
|
|
134
|
+
return close;
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
// Cancel schedules even when opening a feed fails. Let the HTTP wrapper map the error.
|
|
138
|
+
if (response.headersSent)
|
|
139
|
+
close();
|
|
140
|
+
else {
|
|
141
|
+
closed = true;
|
|
142
|
+
release();
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=stream.js.map
|
|
@@ -1,69 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* @file The Media agent-tool catalog — this domain's instance of the per-domain
|
|
3
|
-
* `agent-tools.ts` convention Forms/Identity already use.
|
|
4
|
-
*
|
|
5
|
-
* Purpose:
|
|
6
|
-
* A static, in-process catalog describing every agent-callable tool this domain exposes and the
|
|
7
|
-
* permission each carries. Every entry maps 1:1 onto a real exported function of
|
|
8
|
-
* `media/media-service.ts` — this catalog never names an operation the domain cannot perform.
|
|
9
|
-
*
|
|
10
|
-
* Deliberate absences (the point of a catalog, not an oversight):
|
|
11
|
-
* - There is NO `media_purge_asset`. `purgeMedia` is a HARD delete: it removes the `MediaRecord`
|
|
12
|
-
* row permanently and triggers the blob-GC tombstone pass (`tombstoneBlobIfUnreferenced`,
|
|
13
|
-
* `blob-gc.ts`) that can eventually reclaim the underlying bytes. It is gated behind
|
|
14
|
-
* `media.delete.force` — a separate, narrower permission a human admin UI itself treats as a
|
|
15
|
-
* distinct escalation tier from ordinary `media.delete` (mirrors `admin.menus.delete` vs
|
|
16
|
-
* `admin.menus.delete.force`). An assistant is reachable by prompt injection through ordinary
|
|
17
|
-
* operator content (a comment, a form submission) in a way a human clicking the admin "purge"
|
|
18
|
-
* button is not, so an irreversible, storage-reclaiming delete is left human-UI-only — the same
|
|
19
|
-
* discipline `identity/agent-tools.ts` applies to `resetUserPassword`.
|
|
20
|
-
* - There is NO named-transform-registry tool (`registerTransform`, `media/transform-registry.ts`).
|
|
21
|
-
* No admin HTTP route needs to expose it at all — it is a core-declared, boot-time/operational
|
|
22
|
-
* concept, not a human admin-UI operation. Wrapping it here would invent agent capability beyond
|
|
23
|
-
* what a host's admin surface itself exposes, which this catalog's own discipline forbids.
|
|
24
|
-
*
|
|
25
|
-
* How it relates to a host:
|
|
26
|
-
* A host's tool-registration layer maps these entries into `ToolRegistration`s (see
|
|
27
|
-
* `tool-registrations.ts`). Unlike Forms/Identity, `media-service.ts`'s functions perform NO
|
|
28
|
-
* internal `authorize()` call of their own (confirmed: none of `UploadMediaDeps`/
|
|
29
|
-
* `UpdateMediaMetadataDeps`/`TrashMediaDeps` carry an `authorize` field) — a host's admin HTTP
|
|
30
|
-
* routes check authorization inline, before calling the service function. `tool-registrations.ts`
|
|
31
|
-
* therefore performs that SAME inline authorize check itself (the identical permission string and
|
|
32
|
-
* `entityType: "media"` a host route would use) rather than relying on a domain-layer gate that
|
|
33
|
-
* does not exist for this library — a disclosed, deliberate divergence from the Forms/Identity/
|
|
34
|
-
* content-types pass-through-`ToolPolicy` reasoning, not an oversight.
|
|
35
|
-
*
|
|
36
|
-
* Architectural role:
|
|
37
|
-
* `media` domain declaration. Imports only the constant its own domain already enforces
|
|
38
|
-
* (`DEFAULT_ALLOWED_MIME_TYPES` from `media-service.ts`), so the published JSON Schema's allowed
|
|
39
|
-
* content types cannot drift from the validator.
|
|
40
|
-
*
|
|
41
|
-
* `media_upload_asset`'s size-cap wording (2026-09-21): this catalog is a module-load-time constant
|
|
42
|
-
* shared by every host (see this file's own module doc above), but a host can now override
|
|
43
|
-
* `uploadMedia`'s cap per-call via `MediaToolDeps.maxUploadBytes` (`tool-registrations.ts`). Stating
|
|
44
|
-
* `DEFAULT_MAX_UPLOAD_BYTES` (10 MiB) here would be an outright lie for a host that raised its own
|
|
45
|
-
* cap (e.g. Tovu's 50 MiB) — the model would be told a wrong, lower number than what the tool
|
|
46
|
-
* actually accepts. The description and schema below therefore describe the cap generically instead
|
|
47
|
-
* of stating a figure; the size-rejection error itself (`uploadMedia`'s `MediaValidationError`,
|
|
48
|
-
* `media-service.ts`) states the ACTUAL configured limit in MB, which is the one place that can be
|
|
49
|
-
* host-accurate.
|
|
50
|
-
*/
|
|
51
|
-
export type AgentToolSideEffect = "none" | "mutates-durable-state" | "mints-token";
|
|
52
|
-
export type AgentToolActorClassRule = "confirmer-must-equal-own-delegatedBy" | "user-only" | "none";
|
|
53
|
-
export interface AgentToolDefinition {
|
|
54
|
-
name: string;
|
|
55
|
-
description: string;
|
|
56
|
-
sideEffects: AgentToolSideEffect;
|
|
57
|
-
authorization: {
|
|
58
|
-
permission: string;
|
|
59
|
-
};
|
|
60
|
-
actorClassRule?: AgentToolActorClassRule;
|
|
61
|
-
/**
|
|
62
|
-
* JSON Schema for this tool's `input`, published to the model via `ToolDescriptor.inputSchema`
|
|
63
|
-
* (a host's tool-registration layer refuses to wire any tool lacking one).
|
|
64
|
-
*/
|
|
65
|
-
inputSchema?: Readonly<Record<string, unknown>>;
|
|
66
|
-
}
|
|
1
|
+
import type { AgentToolDefinition } from "@jini-ai/core";
|
|
67
2
|
/**
|
|
68
3
|
* The Media domain's fixed agent-tool catalog — the 4 operations a host's admin HTTP surface
|
|
69
4
|
* exposes that are safe to wrap, out of its 5 total (see file header for why `purge` is excluded).
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
* shared by every host (see this file's own module doc above), but a host can now override
|
|
43
43
|
* `uploadMedia`'s cap per-call via `MediaToolDeps.maxUploadBytes` (`tool-registrations.ts`). Stating
|
|
44
44
|
* `DEFAULT_MAX_UPLOAD_BYTES` (10 MiB) here would be an outright lie for a host that raised its own
|
|
45
|
-
* cap (e.g.
|
|
45
|
+
* cap (e.g. a host's 50 MiB) — the model would be told a wrong, lower number than what the tool
|
|
46
46
|
* actually accepts. The description and schema below therefore describe the cap generically instead
|
|
47
47
|
* of stating a figure; the size-rejection error itself (`uploadMedia`'s `MediaValidationError`,
|
|
48
48
|
* `media-service.ts`) states the ACTUAL configured limit in MB, which is the one place that can be
|
|
@@ -50,6 +50,10 @@
|
|
|
50
50
|
*/
|
|
51
51
|
import { MEDIA_HTML_ATTRIBUTE_ALLOWED_NAMES } from "./html-attributes.js";
|
|
52
52
|
import { DEFAULT_ALLOWED_MIME_TYPES } from "./media-service.js";
|
|
53
|
+
/**
|
|
54
|
+
* JSON Schema for this tool's `input`, published to the model via `ToolDescriptor.inputSchema`
|
|
55
|
+
* (a host's tool-registration layer refuses to wire any tool lacking one).
|
|
56
|
+
*/
|
|
53
57
|
const MEDIA_ID_SCHEMA = {
|
|
54
58
|
type: "string",
|
|
55
59
|
description: "The media asset's id, as returned by media_upload_asset or media_list_assets.",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file In-process, per-sha256 mutex for the blob-GC protocol
|
|
3
|
-
* (
|
|
3
|
+
* ("Serialization" clause).
|
|
4
4
|
*
|
|
5
5
|
* Purpose:
|
|
6
6
|
* The real protocol serializes every `asset_blobs` state transition
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* decorative comment. See `blob-gc.ts`'s file header for what this does NOT
|
|
21
21
|
* cover (cross-process safety, which needs a real DB transaction — a host's
|
|
22
22
|
* own SQLite adapter would supply that).
|
|
23
|
+
* See docs/decisions/DR-004-journaled-blob-gc.md.
|
|
23
24
|
*/
|
|
24
25
|
/**
|
|
25
26
|
* Runs `criticalSection` exclusively with respect to any other call
|
|
@@ -33,5 +34,8 @@
|
|
|
33
34
|
* by the number of other calls currently queued for the same key.
|
|
34
35
|
* @overallScore 100
|
|
35
36
|
*/
|
|
36
|
-
export declare function withSha256Lock<T>(
|
|
37
|
+
export declare function withSha256Lock<T>(requiredArgs: {
|
|
38
|
+
key: string;
|
|
39
|
+
criticalSection: () => Promise<T>;
|
|
40
|
+
}, optionalArgs?: Record<string, never>): Promise<T>;
|
|
37
41
|
//# sourceMappingURL=blob-gc-lock.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file In-process, per-sha256 mutex for the blob-GC protocol
|
|
3
|
-
* (
|
|
3
|
+
* ("Serialization" clause).
|
|
4
4
|
*
|
|
5
5
|
* Purpose:
|
|
6
6
|
* The real protocol serializes every `asset_blobs` state transition
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* decorative comment. See `blob-gc.ts`'s file header for what this does NOT
|
|
21
21
|
* cover (cross-process safety, which needs a real DB transaction — a host's
|
|
22
22
|
* own SQLite adapter would supply that).
|
|
23
|
+
* See docs/decisions/DR-004-journaled-blob-gc.md.
|
|
23
24
|
*/
|
|
24
25
|
/**
|
|
25
26
|
* Tracks the tail of the promise chain for each in-flight/queued key. An
|
|
@@ -40,7 +41,8 @@ const lockTails = new Map();
|
|
|
40
41
|
* by the number of other calls currently queued for the same key.
|
|
41
42
|
* @overallScore 100
|
|
42
43
|
*/
|
|
43
|
-
export function withSha256Lock(
|
|
44
|
+
export function withSha256Lock(requiredArgs, optionalArgs = {}) {
|
|
45
|
+
const { key, criticalSection } = requiredArgs;
|
|
44
46
|
const priorTail = lockTails.get(key) ?? Promise.resolve();
|
|
45
47
|
const run = priorTail.then(criticalSection, criticalSection);
|
|
46
48
|
const settledTail = run.then(() => undefined, () => undefined);
|
package/dist/media/blob-gc.d.ts
CHANGED
|
@@ -1,52 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file Blob-GC protocol ("References, deletion, and GC
|
|
3
|
-
* (never-brick)"
|
|
3
|
+
* (never-brick)") — the audited tombstone -> delete-pass -> unlink-pass
|
|
4
4
|
* design, scoped to what an in-memory-repo build can actually run today.
|
|
5
5
|
*
|
|
6
6
|
* Built in this pass:
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
7
|
+
* - The real "unreferenced" predicate's sha256-uniqueness-across-non-purged-
|
|
8
|
+
* media-entries conjunct ({@link isBlobUnreferenced}), backed by the actual
|
|
9
|
+
* `mediaRepo` data `purgeMedia` already has.
|
|
10
|
+
* - Ordering : {@link runBlobGcDeletePass} first writes the `blob_gc_journal`
|
|
11
|
+
* entry, then deletes the `asset_blobs` row in separate awaited operations, before any bytes are
|
|
12
|
+
* touched; {@link runBlobGcUnlinkPass} only ever unlinks bytes for a row
|
|
13
|
+
* whose deletion has already happened. `uploadMedia` (see
|
|
14
|
+
* `media-service.ts`) already writes bytes before the row.
|
|
15
|
+
* - Serialization: every transition below runs inside {@link withSha256Lock}
|
|
16
|
+
* (`blob-gc-lock.ts`) — this build's substitute for `BEGIN IMMEDIATE`
|
|
17
|
+
* against an in-memory table (see that file's header for why).
|
|
18
|
+
* - The two-phase, journaled shape: {@link tombstoneBlobIfUnreferenced} ->
|
|
19
|
+
* {@link runBlobGcDeletePass} (re-checks the predicate + `gc_grace`,
|
|
20
|
+
* journals, deletes the row) -> {@link runBlobGcUnlinkPass} (drains the
|
|
21
|
+
* journal, removes bytes). {@link runBlobGcCycle} is a convenience batch
|
|
22
|
+
* wrapper around the last two for tests/manual operation; nothing calls it
|
|
23
|
+
* on a schedule.
|
|
24
|
+
* - `gc_grace = max(30d default, retained-snapshot age)`, with the snapshot
|
|
25
|
+
* input wired as a stub (see below) so a real value slots in later without
|
|
26
|
+
* reworking callers.
|
|
27
27
|
*
|
|
28
28
|
* Deliberately NOT built (disclosed, matches this task's explicit scope):
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
29
|
+
* - **`entry_refs` where-used index** ({@link hasLiveEntryRefs}) — belongs to
|
|
30
|
+
* the generic entries model, which is accepted design only, not
|
|
31
|
+
* running code yet (see `types.ts`'s file header on `MediaRecord` being a
|
|
32
|
+
* bespoke table for the same reason). Stubbed to always return `false`
|
|
33
|
+
* ("no live refs found").
|
|
34
|
+
* - **Retained-snapshot conjunct** ({@link getOldestRetainedSnapshotAgeMs}) —
|
|
35
|
+
* owned by a pending Storage/Backups snapshot primitive, which doesn't
|
|
36
|
+
* exist yet either. Stubbed to always
|
|
37
|
+
* return `undefined` ("no retained snapshot").
|
|
38
|
+
* - **Monthly orphan sweep** ({@link runMonthlyOrphanSweepStub}) — the fourth
|
|
39
|
+
* protocol phase, reconciling bytes on disk with no `asset_blobs` row at
|
|
40
|
+
* all (e.g. a crash before any GC protocol existed). Needs a scheduler
|
|
41
|
+
* this library doesn't provide. Named so the seam exists; performs no work
|
|
42
|
+
* and is never called.
|
|
43
|
+
* - **Cross-process safety** — `withSha256Lock` only serializes within one
|
|
44
|
+
* Node process. A real multi-instance deployment needs the real DB
|
|
45
|
+
* transaction the original design specifies, which a host's own persistence adapter
|
|
46
|
+
* would supply; this build has no multi-writer story for media at all yet
|
|
47
|
+
* (in-memory repo), so that gap is inherited, not new.
|
|
48
|
+
* See docs/decisions/DR-004-journaled-blob-gc.md.
|
|
48
49
|
*/
|
|
49
|
-
import type {
|
|
50
|
+
import type { Clock, IdGenerator, UUID } from "@jini-ai/core/primitives";
|
|
50
51
|
import type { AssetBlobRepoPort, BlobGcJournalRepoPort, BlobStorePort, MediaRepoPort } from "./ports.js";
|
|
51
52
|
/** Default retention window before a tombstoned blob's delete-pass may run. */
|
|
52
53
|
export declare const DEFAULT_GC_GRACE_MS: number;
|
|
@@ -100,7 +101,7 @@ export declare function isBlobUnreferenced(required: IsBlobUnreferencedRequired)
|
|
|
100
101
|
export interface TombstoneBlobDeps {
|
|
101
102
|
mediaRepo: MediaRepoPort;
|
|
102
103
|
blobRepo: AssetBlobRepoPort;
|
|
103
|
-
clock:
|
|
104
|
+
clock: Clock;
|
|
104
105
|
}
|
|
105
106
|
export interface TombstoneBlobRequired {
|
|
106
107
|
deps: TombstoneBlobDeps;
|
|
@@ -128,8 +129,8 @@ export interface RunBlobGcDeletePassDeps {
|
|
|
128
129
|
mediaRepo: MediaRepoPort;
|
|
129
130
|
blobRepo: AssetBlobRepoPort;
|
|
130
131
|
journalRepo: BlobGcJournalRepoPort;
|
|
131
|
-
clock:
|
|
132
|
-
idGen:
|
|
132
|
+
clock: Clock;
|
|
133
|
+
idGen: IdGenerator;
|
|
133
134
|
}
|
|
134
135
|
export interface RunBlobGcDeletePassRequired {
|
|
135
136
|
deps: RunBlobGcDeletePassDeps;
|
|
@@ -147,11 +148,20 @@ export interface RunBlobGcDeletePassRequired {
|
|
|
147
148
|
* re-check is what the protocol calls for and is what future non-stub
|
|
148
149
|
* entry_refs/snapshot conjuncts would need). On success: writes the
|
|
149
150
|
* `blob_gc_journal` entry and removes the `asset_blobs` row, in that order,
|
|
150
|
-
* inside the same locked section —
|
|
151
|
+
* inside the same locked section — "unlinked only after the row
|
|
151
152
|
* deletion commits" half depends on nothing touching bytes here at all.
|
|
153
|
+
* These operations are ordered, not atomic. A journal-save rejection leaves the row alone;
|
|
154
|
+
* a row-delete rejection leaves a pending entry plus the row. The unlink-pass rechecks ANY
|
|
155
|
+
* row (including a tombstone), skips its bytes and drains that stale entry, so a later delete
|
|
156
|
+
* retry can journal again. If row deletion succeeded before interruption, the pending entry
|
|
157
|
+
* lets unlink resume; unlink failure retains the entry, and byte removal must be idempotent.
|
|
158
|
+
* Durable recovery requires the journal save to commit before row deletion on the same store;
|
|
159
|
+
* the in-memory adapters survive neither a process crash nor restart. Across processes the
|
|
160
|
+
* host must additionally serialize row ownership checks, uploads and unlinking (see header).
|
|
152
161
|
*
|
|
153
162
|
* @complexity O(n) — dominated by the re-check's `isBlobUnreferenced` scan.
|
|
154
163
|
* @overallScore 100
|
|
164
|
+
* See docs/decisions/DR-004-journaled-blob-gc.md.
|
|
155
165
|
*/
|
|
156
166
|
export declare function runBlobGcDeletePass(required: RunBlobGcDeletePassRequired): Promise<{
|
|
157
167
|
deleted: boolean;
|
|
@@ -235,12 +245,15 @@ export declare function runBlobGcCycle(required: RunBlobGcCycleRequired): Promis
|
|
|
235
245
|
* @complexity O(1) (stub — does nothing).
|
|
236
246
|
* @overallScore 100
|
|
237
247
|
*/
|
|
238
|
-
export declare function runMonthlyOrphanSweepStub(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
248
|
+
export declare function runMonthlyOrphanSweepStub(_required: {
|
|
249
|
+
deps: {
|
|
250
|
+
blobStore: BlobStorePort;
|
|
251
|
+
blobRepo: AssetBlobRepoPort;
|
|
252
|
+
};
|
|
253
|
+
input: {
|
|
254
|
+
workspaceId: UUID;
|
|
255
|
+
};
|
|
256
|
+
}, _optional?: Record<string, never>): Promise<{
|
|
244
257
|
implemented: false;
|
|
245
258
|
}>;
|
|
246
259
|
//# sourceMappingURL=blob-gc.d.ts.map
|
package/dist/media/blob-gc.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nowIso as kernelNowIso } from "@jini-ai/core/primitives";
|
|
1
2
|
import { withSha256Lock } from "./blob-gc-lock.js";
|
|
2
3
|
/** Default retention window before a tombstoned blob's delete-pass may run. */
|
|
3
4
|
export const DEFAULT_GC_GRACE_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
@@ -104,18 +105,18 @@ export async function isBlobUnreferenced(required) {
|
|
|
104
105
|
*/
|
|
105
106
|
export async function tombstoneBlobIfUnreferenced(required) {
|
|
106
107
|
const { deps, input } = required;
|
|
107
|
-
return withSha256Lock(input.sha256, async () => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
108
|
+
return withSha256Lock({ key: input.sha256, criticalSection: async () => {
|
|
109
|
+
const row = await deps.blobRepo.findByHash(input);
|
|
110
|
+
if (!row)
|
|
111
|
+
return { tombstoned: false, reason: "no-blob-row" };
|
|
112
|
+
if (row.status === "tombstoned")
|
|
113
|
+
return { tombstoned: true, reason: "already-tombstoned" };
|
|
114
|
+
const unreferenced = await isBlobUnreferenced({ deps, input });
|
|
115
|
+
if (!unreferenced)
|
|
116
|
+
return { tombstoned: false, reason: "still-referenced" };
|
|
117
|
+
await deps.blobRepo.save({ ...row, status: "tombstoned", tombstonedAt: kernelNowIso({ clock: deps.clock }) });
|
|
118
|
+
return { tombstoned: true, reason: "tombstoned" };
|
|
119
|
+
} });
|
|
119
120
|
}
|
|
120
121
|
/**
|
|
121
122
|
* Delete-pass. Gated on the row being `tombstoned` AND `gc_grace` having
|
|
@@ -126,38 +127,47 @@ export async function tombstoneBlobIfUnreferenced(required) {
|
|
|
126
127
|
* re-check is what the protocol calls for and is what future non-stub
|
|
127
128
|
* entry_refs/snapshot conjuncts would need). On success: writes the
|
|
128
129
|
* `blob_gc_journal` entry and removes the `asset_blobs` row, in that order,
|
|
129
|
-
* inside the same locked section —
|
|
130
|
+
* inside the same locked section — "unlinked only after the row
|
|
130
131
|
* deletion commits" half depends on nothing touching bytes here at all.
|
|
132
|
+
* These operations are ordered, not atomic. A journal-save rejection leaves the row alone;
|
|
133
|
+
* a row-delete rejection leaves a pending entry plus the row. The unlink-pass rechecks ANY
|
|
134
|
+
* row (including a tombstone), skips its bytes and drains that stale entry, so a later delete
|
|
135
|
+
* retry can journal again. If row deletion succeeded before interruption, the pending entry
|
|
136
|
+
* lets unlink resume; unlink failure retains the entry, and byte removal must be idempotent.
|
|
137
|
+
* Durable recovery requires the journal save to commit before row deletion on the same store;
|
|
138
|
+
* the in-memory adapters survive neither a process crash nor restart. Across processes the
|
|
139
|
+
* host must additionally serialize row ownership checks, uploads and unlinking (see header).
|
|
131
140
|
*
|
|
132
141
|
* @complexity O(n) — dominated by the re-check's `isBlobUnreferenced` scan.
|
|
133
142
|
* @overallScore 100
|
|
143
|
+
* See docs/decisions/DR-004-journaled-blob-gc.md.
|
|
134
144
|
*/
|
|
135
145
|
export async function runBlobGcDeletePass(required) {
|
|
136
146
|
const { deps, input } = required;
|
|
137
|
-
return withSha256Lock(input.sha256, async () => {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
147
|
+
return withSha256Lock({ key: input.sha256, criticalSection: async () => {
|
|
148
|
+
const row = await deps.blobRepo.findByHash(input);
|
|
149
|
+
if (!row)
|
|
150
|
+
return { deleted: false, reason: "no-blob-row" };
|
|
151
|
+
if (row.status !== "tombstoned" || !row.tombstonedAt) {
|
|
152
|
+
return { deleted: false, reason: "not-tombstoned" };
|
|
153
|
+
}
|
|
154
|
+
const graceMs = resolveGcGraceMs(input);
|
|
155
|
+
const elapsedMs = new Date(kernelNowIso({ clock: deps.clock })).getTime() - new Date(row.tombstonedAt).getTime();
|
|
156
|
+
if (elapsedMs < graceMs)
|
|
157
|
+
return { deleted: false, reason: "grace-period-not-elapsed" };
|
|
158
|
+
const stillUnreferenced = await isBlobUnreferenced({ deps, input });
|
|
159
|
+
if (!stillUnreferenced)
|
|
160
|
+
return { deleted: false, reason: "resurrected" };
|
|
161
|
+
await deps.journalRepo.save({
|
|
162
|
+
id: deps.idGen.newId(),
|
|
163
|
+
workspaceId: input.workspaceId,
|
|
164
|
+
sha256: input.sha256,
|
|
165
|
+
storageKey: row.storageKey,
|
|
166
|
+
journaledAt: kernelNowIso({ clock: deps.clock }),
|
|
167
|
+
});
|
|
168
|
+
await deps.blobRepo.remove(input);
|
|
169
|
+
return { deleted: true, reason: "deleted" };
|
|
170
|
+
} });
|
|
161
171
|
}
|
|
162
172
|
/**
|
|
163
173
|
* Unlink-pass. Drains every journal entry for the workspace; for each, under
|
|
@@ -188,20 +198,20 @@ export async function runBlobGcUnlinkPass(required) {
|
|
|
188
198
|
const unlinked = [];
|
|
189
199
|
const skipped = [];
|
|
190
200
|
for (const entry of entries) {
|
|
191
|
-
await withSha256Lock(entry.sha256, async () => {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
201
|
+
await withSha256Lock({ key: entry.sha256, criticalSection: async () => {
|
|
202
|
+
const liveRow = await deps.blobRepo.findByHash({
|
|
203
|
+
workspaceId: entry.workspaceId,
|
|
204
|
+
sha256: entry.sha256,
|
|
205
|
+
});
|
|
206
|
+
if (liveRow) {
|
|
207
|
+
skipped.push(entry.sha256);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
await deps.blobStore.remove({ storageKey: entry.storageKey });
|
|
211
|
+
unlinked.push(entry.sha256);
|
|
212
|
+
}
|
|
213
|
+
await deps.journalRepo.remove({ workspaceId: entry.workspaceId, id: entry.id });
|
|
214
|
+
} });
|
|
205
215
|
}
|
|
206
216
|
return { unlinked, skipped };
|
|
207
217
|
}
|
|
@@ -248,7 +258,7 @@ export async function runBlobGcCycle(required) {
|
|
|
248
258
|
* @complexity O(1) (stub — does nothing).
|
|
249
259
|
* @overallScore 100
|
|
250
260
|
*/
|
|
251
|
-
export async function runMonthlyOrphanSweepStub(
|
|
261
|
+
export async function runMonthlyOrphanSweepStub(_required, _optional = {}) {
|
|
252
262
|
return { implemented: false };
|
|
253
263
|
}
|
|
254
264
|
//# sourceMappingURL=blob-gc.js.map
|
package/dist/media/blob-key.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* `media-service.ts` file header). A future epoch-aware rewrite would change
|
|
15
15
|
* this function's output shape, not its callers' contract.
|
|
16
16
|
*/
|
|
17
|
-
import type { UUID } from "
|
|
17
|
+
import type { UUID } from "@jini-ai/core/primitives";
|
|
18
18
|
/**
|
|
19
19
|
* Derives `ws/{workspaceId}/blobs/{sha256[0..1]}/{sha256}`.
|
|
20
20
|
*
|
|
@@ -45,5 +45,7 @@ export type SniffedContentType = "image/png" | "image/jpeg" | "image/gif" | "ima
|
|
|
45
45
|
* {@link TEXT_SNIFF_WINDOW} bytes, a fixed constant independent of `bytes.length`.
|
|
46
46
|
* @overallScore 100
|
|
47
47
|
*/
|
|
48
|
-
export declare function sniffContentType(bytes
|
|
48
|
+
export declare function sniffContentType({ bytes }: {
|
|
49
|
+
bytes: Uint8Array;
|
|
50
|
+
}, _optional?: Record<string, never>): SniffedContentType;
|
|
49
51
|
//# sourceMappingURL=content-type-sniffer.d.ts.map
|
|
@@ -162,7 +162,7 @@ function isSvgLike(text) {
|
|
|
162
162
|
* {@link TEXT_SNIFF_WINDOW} bytes, a fixed constant independent of `bytes.length`.
|
|
163
163
|
* @overallScore 100
|
|
164
164
|
*/
|
|
165
|
-
export function sniffContentType(bytes) {
|
|
165
|
+
export function sniffContentType({ bytes }, _optional = {}) {
|
|
166
166
|
if (isPng(bytes))
|
|
167
167
|
return "image/png";
|
|
168
168
|
if (isJpeg(bytes))
|