@nylorun/runtime 0.15.1-beta → 0.16.0-beta
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 +82 -0
- package/README.md +33 -6
- package/dist/adapters/execution/restate.d.ts +9 -1
- package/dist/adapters/execution/restate.js +64 -5
- package/dist/adapters/replication/pgoutput.js +3 -1
- package/dist/adapters/sandbox/local.d.ts +10 -0
- package/dist/adapters/sandbox/local.js +195 -0
- package/dist/adapters/sandbox/virtual.js +36 -1
- package/dist/admin-openapi.json +37 -3
- package/dist/api/a2a/tasks.js +6 -1
- package/dist/api/ag-ui/history.js +6 -1
- package/dist/api/ag-ui/routes.js +5 -1
- package/dist/api/components.d.ts +2673 -655
- package/dist/api/components.js +17 -1
- package/dist/api/gate/keys.d.ts +4 -3
- package/dist/api/gate/routes.d.ts +19 -3
- package/dist/api/gate/routes.js +162 -53
- package/dist/api/http/app.js +4 -0
- package/dist/api/http/define.d.ts +10 -0
- package/dist/api/http/define.js +2 -1
- package/dist/api/http/routes/artifacts.d.ts +11 -0
- package/dist/api/http/routes/artifacts.js +518 -0
- package/dist/api/http/routes/sandboxes.d.ts +9 -0
- package/dist/api/http/routes/sandboxes.js +152 -0
- package/dist/api/http/routes/sessions.js +3 -1
- package/dist/api/http/routes/tenant.js +36 -2
- package/dist/artifacts/config.d.ts +23 -0
- package/dist/artifacts/config.js +27 -0
- package/dist/artifacts/export.d.ts +8 -0
- package/dist/artifacts/export.js +205 -0
- package/dist/artifacts/files.d.ts +36 -0
- package/dist/artifacts/files.js +45 -0
- package/dist/artifacts/folders.d.ts +60 -0
- package/dist/artifacts/folders.js +209 -0
- package/dist/artifacts/links.d.ts +28 -0
- package/dist/artifacts/links.js +92 -0
- package/dist/artifacts/media-types.d.ts +13 -0
- package/dist/artifacts/media-types.js +80 -0
- package/dist/artifacts/parts.d.ts +34 -0
- package/dist/artifacts/parts.js +38 -0
- package/dist/artifacts/service.d.ts +74 -0
- package/dist/artifacts/service.js +301 -0
- package/dist/artifacts/workspace.d.ts +60 -0
- package/dist/artifacts/workspace.js +57 -0
- package/dist/artifacts/zip.d.ts +10 -0
- package/dist/artifacts/zip.js +126 -0
- package/dist/blob/fs.d.ts +13 -0
- package/dist/blob/fs.js +194 -0
- package/dist/blob/index.d.ts +9 -0
- package/dist/blob/index.js +9 -0
- package/dist/blob/s3.d.ts +42 -0
- package/dist/blob/s3.js +366 -0
- package/dist/blob/types.d.ts +108 -0
- package/dist/blob/types.js +85 -0
- package/dist/core/sandbox-routes.d.ts +2 -2
- package/dist/core/sandbox-routes.js +4 -3
- package/dist/execution/memory.d.ts +11 -1
- package/dist/execution/memory.js +77 -3
- package/dist/execution/types.d.ts +54 -1
- package/dist/execution/types.js +31 -0
- package/dist/gates/contract.d.ts +14 -13
- package/dist/gates/contract.js +13 -12
- package/dist/gates/egress.d.ts +72 -0
- package/dist/gates/egress.js +319 -0
- package/dist/gates/handler.d.ts +3 -0
- package/dist/gates/handler.js +3 -2
- package/dist/gates/http-client.d.ts +3 -2
- package/dist/gates/http-client.js +17 -6
- package/dist/gates/in-process.d.ts +13 -1
- package/dist/gates/in-process.js +11 -0
- package/dist/gates/inflight.d.ts +23 -4
- package/dist/gates/inflight.js +18 -4
- package/dist/gates/model-gate.d.ts +6 -2
- package/dist/gates/tool-calls.d.ts +9 -5
- package/dist/gates/tool-calls.js +6 -6
- package/dist/gates/tool-client.d.ts +9 -2
- package/dist/gates/tool-client.js +56 -22
- package/dist/gates/tool-contract.d.ts +12 -7
- package/dist/gates/tool-contract.js +10 -4
- package/dist/gates/tool-gate.d.ts +1 -0
- package/dist/harness/calls.d.ts +52 -0
- package/dist/harness/calls.js +113 -0
- package/dist/harness/executors.d.ts +22 -0
- package/dist/harness/executors.js +96 -0
- package/dist/harness/main.d.ts +6 -0
- package/dist/harness/main.js +103 -0
- package/dist/harness/pod.d.ts +21 -0
- package/dist/harness/pod.js +119 -0
- package/dist/harness/run-tokens.d.ts +18 -0
- package/dist/harness/run-tokens.js +15 -0
- package/dist/harness/service.d.ts +46 -0
- package/dist/harness/service.js +179 -0
- package/dist/harness/ws-client.d.ts +43 -0
- package/dist/harness/ws-client.js +122 -0
- package/dist/harness/ws-port.d.ts +22 -0
- package/dist/harness/ws-port.js +78 -0
- package/dist/harness-api/in-process.d.ts +24 -0
- package/dist/harness-api/in-process.js +119 -0
- package/dist/harness-api/record.d.ts +44 -0
- package/dist/harness-api/record.js +250 -0
- package/dist/harness-api/renew.d.ts +37 -0
- package/dist/harness-api/renew.js +77 -0
- package/dist/harness-api/server.d.ts +104 -0
- package/dist/harness-api/server.js +408 -0
- package/dist/harness-api/start.d.ts +36 -0
- package/dist/harness-api/start.js +118 -0
- package/dist/harness-api/workspace.d.ts +53 -0
- package/dist/harness-api/workspace.js +201 -0
- package/dist/harness-api/ws-server.d.ts +34 -0
- package/dist/harness-api/ws-server.js +227 -0
- package/dist/host/app.js +23 -2
- package/dist/host/gates.d.ts +27 -2
- package/dist/host/gates.js +46 -1
- package/dist/host/http.d.ts +4 -1
- package/dist/host/http.js +6 -1
- package/dist/host/main.js +135 -27
- package/dist/host/stack-config.d.ts +101 -1
- package/dist/host/stack-config.js +199 -8
- package/dist/mcp/snapshot.d.ts +8 -1
- package/dist/mcp/snapshot.js +28 -0
- package/dist/media.d.ts +5 -24
- package/dist/media.js +5 -0
- package/dist/model/pi-model.d.ts +6 -2
- package/dist/model/pi-model.js +39 -22
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +1 -1
- package/dist/openapi.json +15245 -8850
- package/dist/record/index.d.ts +1 -0
- package/dist/record/index.js +1 -0
- package/dist/record/sandbox.d.ts +25 -0
- package/dist/record/sandbox.js +43 -0
- package/dist/sandbox/capability.d.ts +3 -0
- package/dist/sandbox/capability.js +10 -0
- package/dist/sandbox/egress-token.d.ts +26 -0
- package/dist/sandbox/egress-token.js +8 -0
- package/dist/sandbox/join.d.ts +37 -0
- package/dist/sandbox/join.js +165 -0
- package/dist/sandbox/manager.d.ts +99 -34
- package/dist/sandbox/manager.js +216 -84
- package/dist/sandbox/placement.d.ts +27 -0
- package/dist/sandbox/placement.js +44 -0
- package/dist/sandbox/pods/client.d.ts +90 -0
- package/dist/sandbox/pods/client.js +79 -0
- package/dist/sandbox/pods/lifecycle.d.ts +91 -0
- package/dist/sandbox/pods/lifecycle.js +178 -0
- package/dist/sandbox/pods/name.d.ts +1 -0
- package/dist/sandbox/pods/name.js +28 -0
- package/dist/sandbox/pods/network-gate.d.ts +25 -0
- package/dist/sandbox/pods/network-gate.js +71 -0
- package/dist/sandbox/pods/reconcile.d.ts +11 -0
- package/dist/sandbox/pods/reconcile.js +152 -0
- package/dist/sandbox/pods/spec.d.ts +45 -0
- package/dist/sandbox/pods/spec.js +62 -0
- package/dist/sandbox/records.d.ts +29 -0
- package/dist/sandbox/records.js +79 -0
- package/dist/sandbox/resolve.d.ts +5 -0
- package/dist/sandbox/resolve.js +3 -2
- package/dist/sandbox/session-sandbox.js +7 -5
- package/dist/sandbox/share.d.ts +11 -0
- package/dist/sandbox/share.js +10 -0
- package/dist/sandbox/tenant-config.d.ts +16 -1
- package/dist/sandbox/tenant-config.js +9 -0
- package/dist/sandbox/types.d.ts +29 -1
- package/dist/sandbox/types.js +13 -1
- package/dist/store/postgres/drizzle/0004_sandbox_resources.sql +20 -0
- package/dist/store/postgres/drizzle/0005_artifacts.sql +27 -0
- package/dist/store/postgres/drizzle/0006_folder_artifacts.sql +12 -0
- package/dist/store/postgres/drizzle/0007_sandbox_pods.sql +14 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
- package/dist/store/postgres/record-writer.d.ts +6 -1
- package/dist/store/postgres/record-writer.js +20 -2
- package/dist/store/postgres/schema.d.ts +894 -0
- package/dist/store/postgres/schema.js +130 -2
- package/dist/store/postgres/store.js +289 -4
- package/dist/store/types.d.ts +143 -6
- package/dist/streams/control.d.ts +2 -0
- package/dist/streams/control.js +5 -0
- package/dist/streams/types.d.ts +10 -1
- package/dist/tenant/advance.js +218 -160
- package/dist/tenant/artifact-tool.d.ts +5 -0
- package/dist/tenant/artifact-tool.js +119 -0
- package/dist/tenant/commands.js +30 -5
- package/dist/tenant/context.d.ts +64 -5
- package/dist/tenant/effects.d.ts +19 -13
- package/dist/tenant/effects.js +5 -382
- package/dist/tenant/history.d.ts +13 -23
- package/dist/tenant/history.js +14 -62
- package/dist/tenant/host-token.d.ts +92 -0
- package/dist/tenant/host-token.js +179 -0
- package/dist/tenant/module.js +1 -0
- package/dist/tenant/paths.d.ts +2 -1
- package/dist/tenant/paths.js +3 -1
- package/dist/tenant/reset.d.ts +5 -2
- package/dist/tenant/reset.js +22 -2
- package/dist/tenant/run-grants.d.ts +49 -0
- package/dist/tenant/run-grants.js +62 -0
- package/dist/tenant/run-token.d.ts +70 -0
- package/dist/tenant/run-token.js +138 -0
- package/dist/tenant/runtime.d.ts +38 -0
- package/dist/tenant/runtime.js +120 -32
- package/dist/tenant/sandboxes.d.ts +119 -0
- package/dist/tenant/sandboxes.js +551 -0
- package/dist/tenant/session.d.ts +2 -13
- package/dist/tenant/session.js +4 -28
- package/dist/tenant/sessions.d.ts +5 -0
- package/dist/tenant/sessions.js +28 -1
- package/dist/tenant/status.d.ts +8 -3
- package/dist/tenant/status.js +5 -0
- package/dist/tenant/streams.d.ts +7 -0
- package/dist/tenant/streams.js +24 -1
- package/dist/tenant/sweep.js +37 -4
- package/dist/tenant/tokens.js +9 -2
- package/dist/tenant/types.d.ts +18 -1
- package/dist/tenant/worker.d.ts +11 -5
- package/dist/tenant/worker.js +12 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -10
- package/dist/adapters/media.d.ts +0 -24
- package/dist/adapters/media.js +0 -111
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Folder artifacts (blueprint D35, F8.2). A folder version is a **manifest**: JSON
|
|
3
|
+
* `{ format, entries: [{ path, size, sha256, contentType }] }`, sorted by path, stored in the
|
|
4
|
+
* Object store under the version's `blobKey` like a file's bytes. Each file's bytes are stored
|
|
5
|
+
* once, content-addressed, at `blobs/sha256/<hex>`: a file that did not change between versions,
|
|
6
|
+
* or that two folders share, is stored and counted once.
|
|
7
|
+
*
|
|
8
|
+
* Which content-addressed files are in use is indexed in Postgres (`artifact_content`, a row per
|
|
9
|
+
* folder version and distinct hash), so deleting a folder removes only the files nothing else
|
|
10
|
+
* names, and the Tenant total counts each file once.
|
|
11
|
+
*
|
|
12
|
+
* **Collecting content safely.** A writer skips the `put` of a file the store already has
|
|
13
|
+
* (`head`), outside any lock, and a delete could remove that file before the writer commits. So:
|
|
14
|
+
* content is deleted only while holding the quota lock, and every such delete moves the Tenant's
|
|
15
|
+
* content epoch (setting `artifacts.content_epoch`) in the same transaction. A writer reads the
|
|
16
|
+
* epoch before its first `head`; at commit, under the quota lock, a file some committed version
|
|
17
|
+
* references is present (the invariant the lock keeps), and when the epoch moved it checks the
|
|
18
|
+
* others again and refuses to commit a version naming a file that is gone.
|
|
19
|
+
*/
|
|
20
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
21
|
+
import { FOLDER_CONTENT_TYPE, FOLDER_MANIFEST_FORMAT, FolderManifestSchema, } from "@nylorun/core/contracts";
|
|
22
|
+
import { newArtifactId } from "@nylorun/core/compatibility";
|
|
23
|
+
import { lockedSession } from "../tenant/context.js";
|
|
24
|
+
import { fail } from "../tenant/http.js";
|
|
25
|
+
const CONTENT_EPOCH_SETTING = "artifacts.content_epoch";
|
|
26
|
+
/** Where a folder file's bytes are: content-addressed by their SHA-256. */
|
|
27
|
+
export function contentKey(sha256) {
|
|
28
|
+
return `blobs/sha256/${sha256}`;
|
|
29
|
+
}
|
|
30
|
+
export function sha256Hex(bytes) {
|
|
31
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
32
|
+
}
|
|
33
|
+
/** Entries in manifest order: by path, comparing UTF-16 code units. */
|
|
34
|
+
export function sortEntries(entries) {
|
|
35
|
+
return [...entries].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
36
|
+
}
|
|
37
|
+
/** The manifest's bytes, as stored: deterministic for the same entries. */
|
|
38
|
+
export function encodeManifest(entries) {
|
|
39
|
+
const manifest = {
|
|
40
|
+
format: FOLDER_MANIFEST_FORMAT,
|
|
41
|
+
entries: sortEntries(entries).map(({ path, size, sha256, contentType }) => ({
|
|
42
|
+
path,
|
|
43
|
+
size,
|
|
44
|
+
sha256,
|
|
45
|
+
contentType,
|
|
46
|
+
})),
|
|
47
|
+
};
|
|
48
|
+
return new TextEncoder().encode(JSON.stringify(manifest));
|
|
49
|
+
}
|
|
50
|
+
/** A folder version's manifest, read from the Object store. */
|
|
51
|
+
export async function readManifest(blobs, version) {
|
|
52
|
+
const got = await blobs.get(version.blobKey);
|
|
53
|
+
if (!got)
|
|
54
|
+
return fail(404, "Artifact bytes not found");
|
|
55
|
+
const parsed = FolderManifestSchema.safeParse(JSON.parse(await new Response(got.body).text()));
|
|
56
|
+
if (!parsed.success)
|
|
57
|
+
throw new Error(`The manifest of ${version.artifactId} v${version.version} is malformed`);
|
|
58
|
+
return parsed.data;
|
|
59
|
+
}
|
|
60
|
+
/** The entry at `path`, or the 404 of a missing file. */
|
|
61
|
+
export function entryAt(manifest, path) {
|
|
62
|
+
return (manifest.entries.find((entry) => entry.path === path) ??
|
|
63
|
+
fail(404, `No file ${path} in this folder version`));
|
|
64
|
+
}
|
|
65
|
+
/** What changed from `from` (absent: nothing) to `to`. */
|
|
66
|
+
export function diffManifests(artifactId, from, to) {
|
|
67
|
+
const before = new Map((from?.manifest.entries ?? []).map((entry) => [entry.path, entry]));
|
|
68
|
+
const after = new Map(to.manifest.entries.map((entry) => [entry.path, entry]));
|
|
69
|
+
const added = [];
|
|
70
|
+
const changed = [];
|
|
71
|
+
for (const entry of to.manifest.entries) {
|
|
72
|
+
const prior = before.get(entry.path);
|
|
73
|
+
if (!prior)
|
|
74
|
+
added.push(entry);
|
|
75
|
+
else if (prior.sha256 !== entry.sha256 || prior.contentType !== entry.contentType)
|
|
76
|
+
changed.push({ path: entry.path, from: prior, to: entry });
|
|
77
|
+
}
|
|
78
|
+
const removed = (from?.manifest.entries ?? []).filter((entry) => !after.has(entry.path));
|
|
79
|
+
return {
|
|
80
|
+
artifactId,
|
|
81
|
+
...(from === undefined ? {} : { from: from.version }),
|
|
82
|
+
to: to.version,
|
|
83
|
+
added,
|
|
84
|
+
removed,
|
|
85
|
+
changed,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** The Tenant's content epoch: moves whenever content-addressed files are deleted. */
|
|
89
|
+
export async function contentEpoch(t) {
|
|
90
|
+
const raw = await t.getSetting(CONTENT_EPOCH_SETTING);
|
|
91
|
+
return raw === undefined ? 0 : Number(raw);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Deletes those of `shas` no committed folder version references any more, and moves the content
|
|
95
|
+
* epoch when it deletes any. Call it in a transaction holding the quota lock, after the rows that
|
|
96
|
+
* named them are gone: the deletes happen before the transaction commits, under the lock.
|
|
97
|
+
*/
|
|
98
|
+
export async function collectContent(blobs, t, shas) {
|
|
99
|
+
if (shas.length === 0)
|
|
100
|
+
return 0;
|
|
101
|
+
const referenced = await t.referencedArtifactContent(shas);
|
|
102
|
+
const orphans = shas.filter((sha) => !referenced.has(sha));
|
|
103
|
+
if (orphans.length === 0)
|
|
104
|
+
return 0;
|
|
105
|
+
await t.putSetting(CONTENT_EPOCH_SETTING, String((await contentEpoch(t)) + 1));
|
|
106
|
+
for (const sha of orphans)
|
|
107
|
+
await blobs.delete(contentKey(sha));
|
|
108
|
+
return orphans.length;
|
|
109
|
+
}
|
|
110
|
+
/** The commit could not go ahead: a limit, or content removed while the export ran. */
|
|
111
|
+
export class FolderCommitRefused extends Error {
|
|
112
|
+
reason;
|
|
113
|
+
limit;
|
|
114
|
+
constructor(message, reason, limit) {
|
|
115
|
+
super(message);
|
|
116
|
+
this.reason = reason;
|
|
117
|
+
this.limit = limit;
|
|
118
|
+
this.name = "FolderCommitRefused";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Commits a folder version whose files are already stored: its manifest to the Object store,
|
|
123
|
+
* then, in one transaction under the session and quota locks, the version row, its content rows
|
|
124
|
+
* and `artifact.created` or `artifact.version.created` (`claimed: true`, source `export`).
|
|
125
|
+
* Throws `FolderCommitRefused` when the Tenant total would be passed or a file is gone.
|
|
126
|
+
*/
|
|
127
|
+
export async function commitFolderVersion(ctx, input) {
|
|
128
|
+
const entries = sortEntries(input.entries);
|
|
129
|
+
const manifest = encodeManifest(entries);
|
|
130
|
+
const sha256 = sha256Hex(manifest);
|
|
131
|
+
const files = new Map();
|
|
132
|
+
for (const entry of entries)
|
|
133
|
+
files.set(entry.sha256, entry.size);
|
|
134
|
+
const size = entries.reduce((sum, entry) => sum + entry.size, 0);
|
|
135
|
+
const artifactId = input.artifactId ?? newArtifactId();
|
|
136
|
+
const key = `artifacts/${artifactId}/${randomUUID()}`;
|
|
137
|
+
await ctx.blobs.put(key, manifest, { contentType: FOLDER_CONTENT_TYPE });
|
|
138
|
+
const createdAt = new Date().toISOString();
|
|
139
|
+
try {
|
|
140
|
+
return await ctx.store.tx(async (t) => {
|
|
141
|
+
// The session first, then the quota, then the artifact (the module's lock order).
|
|
142
|
+
await lockedSession(t, input.sessionId);
|
|
143
|
+
await t.lockArtifactQuota();
|
|
144
|
+
const shas = [...files.keys()];
|
|
145
|
+
const referenced = await t.referencedArtifactContent(shas);
|
|
146
|
+
const fresh = shas.filter((sha) => !referenced.has(sha));
|
|
147
|
+
if ((await contentEpoch(t)) !== input.epoch)
|
|
148
|
+
for (const sha of fresh)
|
|
149
|
+
if (!(await ctx.blobs.head(contentKey(sha))))
|
|
150
|
+
throw new FolderCommitRefused("A file's bytes were removed while the export ran", "content_gone");
|
|
151
|
+
const added = fresh.reduce((sum, sha) => sum + files.get(sha), 0);
|
|
152
|
+
const total = await t.artifactBytes();
|
|
153
|
+
if (total + added > input.totalBytes)
|
|
154
|
+
throw new FolderCommitRefused(`The Tenant's artifacts may hold at most ${input.totalBytes} bytes; these outputs add ${added} to ${total}`, "tenant_total", input.totalBytes);
|
|
155
|
+
let row;
|
|
156
|
+
const version = {
|
|
157
|
+
artifactId,
|
|
158
|
+
version: 1,
|
|
159
|
+
blobKey: key,
|
|
160
|
+
size,
|
|
161
|
+
sha256,
|
|
162
|
+
contentType: FOLDER_CONTENT_TYPE,
|
|
163
|
+
source: "export",
|
|
164
|
+
createdAt,
|
|
165
|
+
};
|
|
166
|
+
if (input.artifactId !== undefined) {
|
|
167
|
+
const prior = await t.artifact(input.artifactId, { lock: true });
|
|
168
|
+
if (!prior || prior.kind !== "folder" || prior.sessionId !== input.sessionId)
|
|
169
|
+
throw new FolderCommitRefused("The folder was deleted while the export ran", "artifact_gone");
|
|
170
|
+
version.version = prior.latestVersion + 1;
|
|
171
|
+
await t.insertArtifactVersion(version);
|
|
172
|
+
row = { ...prior, latestVersion: version.version, contentType: FOLDER_CONTENT_TYPE, updatedAt: createdAt };
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
row = {
|
|
176
|
+
id: artifactId,
|
|
177
|
+
kind: "folder",
|
|
178
|
+
name: input.name,
|
|
179
|
+
contentType: FOLDER_CONTENT_TYPE,
|
|
180
|
+
sessionId: input.sessionId,
|
|
181
|
+
latestVersion: 1,
|
|
182
|
+
labelsJson: null,
|
|
183
|
+
createdAt,
|
|
184
|
+
updatedAt: createdAt,
|
|
185
|
+
};
|
|
186
|
+
await t.insertArtifact(row, version);
|
|
187
|
+
}
|
|
188
|
+
await t.insertArtifactContent(artifactId, version.version, shas.map((sha) => ({ sha256: sha, size: files.get(sha) })));
|
|
189
|
+
await t.event(input.sessionId, input.turnId, version.version === 1 ? "artifact.created" : "artifact.version.created", {
|
|
190
|
+
artifactId,
|
|
191
|
+
kind: "folder",
|
|
192
|
+
name: row.name,
|
|
193
|
+
contentType: FOLDER_CONTENT_TYPE,
|
|
194
|
+
version: version.version,
|
|
195
|
+
size,
|
|
196
|
+
sha256,
|
|
197
|
+
source: "export",
|
|
198
|
+
fileCount: entries.length,
|
|
199
|
+
claimed: true,
|
|
200
|
+
});
|
|
201
|
+
return { artifact: row, version };
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
// Nothing references the manifest: it was garbage from the start.
|
|
206
|
+
await ctx.blobs.delete(key).catch(() => undefined);
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TenantContext } from "../tenant/context.js";
|
|
2
|
+
/** The audience of every capability link. */
|
|
3
|
+
export declare const ARTIFACT_LINK_AUDIENCE = "nylorun-artifact";
|
|
4
|
+
/** Where a capability link is served: `GET /v1/artifact-links/<token>`. */
|
|
5
|
+
export declare const ARTIFACT_LINK_PATH = "/v1/artifact-links";
|
|
6
|
+
export interface MintedArtifactLink {
|
|
7
|
+
path: string;
|
|
8
|
+
artifactId: string;
|
|
9
|
+
version: number;
|
|
10
|
+
/** The folder's file the link opens. */
|
|
11
|
+
file?: string;
|
|
12
|
+
expiresAt: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function mintArtifactLink(ctx: TenantContext, link: {
|
|
15
|
+
artifactId: string;
|
|
16
|
+
version: number;
|
|
17
|
+
expiresIn?: number;
|
|
18
|
+
file?: string;
|
|
19
|
+
}): Promise<MintedArtifactLink>;
|
|
20
|
+
/**
|
|
21
|
+
* The artifact version a capability link opens. An expired link, or one whose key was revoked,
|
|
22
|
+
* is `401 token_expired`; anything else that does not verify is the opaque 404.
|
|
23
|
+
*/
|
|
24
|
+
export declare function verifyArtifactLink(ctx: TenantContext, raw: string): Promise<{
|
|
25
|
+
artifactId: string;
|
|
26
|
+
version: number;
|
|
27
|
+
file?: string;
|
|
28
|
+
}>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability links (protocol 6): a short-lived URL that opens one artifact version with no
|
|
3
|
+
* other credential, for UIs, `<img>` tags and sharing. The token in the path is an ES256 JWT
|
|
4
|
+
* (`typ: nylorun-artifact+jwt`) signed with the Tenant's current signing key by the keys
|
|
5
|
+
* service, like subject and delivery tokens: `sub` is the artifact, `ver` the version, `aud`
|
|
6
|
+
* `nylorun-artifact`, and it lives at most `ARTIFACT_LINK_MAX_TTL_SECONDS` (the subject-token
|
|
7
|
+
* maximum, so a key rotation never revokes a live link). A link opens nothing once its artifact
|
|
8
|
+
* is deleted. A folder's link (F8.2) opens its zip, or with a `path` claim one of its files.
|
|
9
|
+
*/
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { errors, jwtVerify } from "jose";
|
|
12
|
+
import { ARTIFACT_LINK_DEFAULT_TTL_SECONDS, ARTIFACT_LINK_MAX_TTL_SECONDS, ARTIFACT_LINK_TOKEN_TYPE, subjectTokenIssuer, } from "@nylorun/core/contracts";
|
|
13
|
+
import { isArtifactId } from "@nylorun/core/compatibility";
|
|
14
|
+
import { failOpaque, HttpError } from "../tenant/http.js";
|
|
15
|
+
import { CLOCK_TOLERANCE_SECONDS, tokenKeyId } from "../tenant/jwt.js";
|
|
16
|
+
/** The audience of every capability link. */
|
|
17
|
+
export const ARTIFACT_LINK_AUDIENCE = "nylorun-artifact";
|
|
18
|
+
/** Where a capability link is served: `GET /v1/artifact-links/<token>`. */
|
|
19
|
+
export const ARTIFACT_LINK_PATH = "/v1/artifact-links";
|
|
20
|
+
export async function mintArtifactLink(ctx, link) {
|
|
21
|
+
const ttl = Math.max(1, Math.min(Math.floor(link.expiresIn ?? ARTIFACT_LINK_DEFAULT_TTL_SECONDS), ARTIFACT_LINK_MAX_TTL_SECONDS));
|
|
22
|
+
const iat = Math.floor(Date.now() / 1000);
|
|
23
|
+
// Signed by the keys service (F4.2): this process never holds the private key.
|
|
24
|
+
const { token } = await ctx.keys.sign({
|
|
25
|
+
typ: ARTIFACT_LINK_TOKEN_TYPE,
|
|
26
|
+
claims: {
|
|
27
|
+
iss: subjectTokenIssuer(ctx.config.tenantId),
|
|
28
|
+
aud: ARTIFACT_LINK_AUDIENCE,
|
|
29
|
+
sub: link.artifactId,
|
|
30
|
+
ver: link.version,
|
|
31
|
+
...(link.file === undefined ? {} : { path: link.file }),
|
|
32
|
+
iat,
|
|
33
|
+
exp: iat + ttl,
|
|
34
|
+
jti: randomUUID(),
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
path: `${ARTIFACT_LINK_PATH}/${token}`,
|
|
39
|
+
artifactId: link.artifactId,
|
|
40
|
+
version: link.version,
|
|
41
|
+
...(link.file === undefined ? {} : { file: link.file }),
|
|
42
|
+
expiresAt: new Date((iat + ttl) * 1000).toISOString(),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The artifact version a capability link opens. An expired link, or one whose key was revoked,
|
|
47
|
+
* is `401 token_expired`; anything else that does not verify is the opaque 404.
|
|
48
|
+
*/
|
|
49
|
+
export async function verifyArtifactLink(ctx, raw) {
|
|
50
|
+
const checked = tokenKeyId(raw, ARTIFACT_LINK_TOKEN_TYPE);
|
|
51
|
+
if ("refused" in checked)
|
|
52
|
+
return refused(ctx, `artifact_link_${checked.refused}`);
|
|
53
|
+
const row = await ctx.store.tx((t) => t.signingKey(checked.kid));
|
|
54
|
+
if (!row)
|
|
55
|
+
return refused(ctx, "artifact_link_key_unknown");
|
|
56
|
+
let claims;
|
|
57
|
+
try {
|
|
58
|
+
const verified = await jwtVerify(raw, await ctx.signingKeys.publicKey(row), {
|
|
59
|
+
algorithms: ["ES256"],
|
|
60
|
+
issuer: subjectTokenIssuer(ctx.config.tenantId),
|
|
61
|
+
audience: ARTIFACT_LINK_AUDIENCE,
|
|
62
|
+
typ: ARTIFACT_LINK_TOKEN_TYPE,
|
|
63
|
+
clockTolerance: CLOCK_TOLERANCE_SECONDS,
|
|
64
|
+
requiredClaims: ["sub", "aud", "iat", "exp", "jti"],
|
|
65
|
+
});
|
|
66
|
+
claims = verified.payload;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error instanceof errors.JWTExpired)
|
|
70
|
+
expired(ctx, "artifact_link_expired");
|
|
71
|
+
return refused(ctx, "artifact_link_invalid");
|
|
72
|
+
}
|
|
73
|
+
if (row.state === "revoked")
|
|
74
|
+
expired(ctx, "artifact_link_key_revoked");
|
|
75
|
+
const { sub, ver, path, exp, iat } = claims;
|
|
76
|
+
if (!isArtifactId(sub) ||
|
|
77
|
+
typeof ver !== "number" ||
|
|
78
|
+
!Number.isInteger(ver) ||
|
|
79
|
+
ver < 1 ||
|
|
80
|
+
(path !== undefined && (typeof path !== "string" || path === "")) ||
|
|
81
|
+
exp - iat > ARTIFACT_LINK_MAX_TTL_SECONDS)
|
|
82
|
+
return refused(ctx, "artifact_link_claims");
|
|
83
|
+
return { artifactId: sub, version: ver, ...(typeof path === "string" ? { file: path } : {}) };
|
|
84
|
+
}
|
|
85
|
+
function refused(ctx, reason) {
|
|
86
|
+
ctx.config.logger.warn("credential rejected", { reason });
|
|
87
|
+
return failOpaque();
|
|
88
|
+
}
|
|
89
|
+
function expired(ctx, reason) {
|
|
90
|
+
ctx.config.logger.warn("artifact link refused", { reason });
|
|
91
|
+
throw new HttpError(401, "The link has expired", { code: "token_expired" });
|
|
92
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Images the model reads as images. */
|
|
2
|
+
export declare const MODEL_IMAGE_TYPES: readonly string[];
|
|
3
|
+
/** The largest image or text file a model call carries inline. */
|
|
4
|
+
export declare const MODEL_FILE_MAX_BYTES: number;
|
|
5
|
+
/** The type part of a `Content-Type` value, lowercased, without parameters. */
|
|
6
|
+
export declare function essence(contentType: string): string;
|
|
7
|
+
/** A declared `Content-Type` as stored, or undefined when it is not a media type. */
|
|
8
|
+
export declare function normalizeContentType(value: string | undefined): string | undefined;
|
|
9
|
+
/** The media type a file name implies, or `application/octet-stream`. */
|
|
10
|
+
export declare function contentTypeFor(name: string): string;
|
|
11
|
+
export declare function isModelImage(contentType: string): boolean;
|
|
12
|
+
/** Files the model reads as text: `text/*`, JSON, XML, YAML and their `+json`/`+xml` kin. */
|
|
13
|
+
export declare function isText(contentType: string): boolean;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media types of file artifacts: what an upload may declare, what a name's extension implies,
|
|
3
|
+
* and how the model reads a file (an image as an image, text as text, anything else refused).
|
|
4
|
+
*/
|
|
5
|
+
import { DEFAULT_CONTENT_TYPE } from "../blob/index.js";
|
|
6
|
+
const MEDIA_TYPE = /^[a-z0-9][a-z0-9!#$&^_.+-]{0,126}\/[a-z0-9][a-z0-9!#$&^_.+-]{0,126}$/;
|
|
7
|
+
const BY_EXTENSION = {
|
|
8
|
+
png: "image/png",
|
|
9
|
+
jpg: "image/jpeg",
|
|
10
|
+
jpeg: "image/jpeg",
|
|
11
|
+
gif: "image/gif",
|
|
12
|
+
webp: "image/webp",
|
|
13
|
+
svg: "image/svg+xml",
|
|
14
|
+
pdf: "application/pdf",
|
|
15
|
+
txt: "text/plain",
|
|
16
|
+
md: "text/markdown",
|
|
17
|
+
csv: "text/csv",
|
|
18
|
+
html: "text/html",
|
|
19
|
+
htm: "text/html",
|
|
20
|
+
css: "text/css",
|
|
21
|
+
js: "text/javascript",
|
|
22
|
+
mjs: "text/javascript",
|
|
23
|
+
ts: "text/x-typescript",
|
|
24
|
+
py: "text/x-python",
|
|
25
|
+
sh: "text/x-sh",
|
|
26
|
+
json: "application/json",
|
|
27
|
+
xml: "application/xml",
|
|
28
|
+
yaml: "application/yaml",
|
|
29
|
+
yml: "application/yaml",
|
|
30
|
+
zip: "application/zip",
|
|
31
|
+
tar: "application/x-tar",
|
|
32
|
+
gz: "application/gzip",
|
|
33
|
+
mp3: "audio/mpeg",
|
|
34
|
+
wav: "audio/wav",
|
|
35
|
+
mp4: "video/mp4",
|
|
36
|
+
};
|
|
37
|
+
/** Images the model reads as images. */
|
|
38
|
+
export const MODEL_IMAGE_TYPES = Object.freeze([
|
|
39
|
+
"image/png",
|
|
40
|
+
"image/jpeg",
|
|
41
|
+
"image/webp",
|
|
42
|
+
"image/gif",
|
|
43
|
+
]);
|
|
44
|
+
/** The largest image or text file a model call carries inline. */
|
|
45
|
+
export const MODEL_FILE_MAX_BYTES = 8 * 1024 * 1024;
|
|
46
|
+
/** The type part of a `Content-Type` value, lowercased, without parameters. */
|
|
47
|
+
export function essence(contentType) {
|
|
48
|
+
return contentType.split(";")[0].trim().toLowerCase();
|
|
49
|
+
}
|
|
50
|
+
/** A declared `Content-Type` as stored, or undefined when it is not a media type. */
|
|
51
|
+
export function normalizeContentType(value) {
|
|
52
|
+
if (value === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
const trimmed = value.trim();
|
|
55
|
+
if (trimmed === "" || trimmed.length > 255)
|
|
56
|
+
return undefined;
|
|
57
|
+
return MEDIA_TYPE.test(essence(trimmed)) ? trimmed : undefined;
|
|
58
|
+
}
|
|
59
|
+
/** The media type a file name implies, or `application/octet-stream`. */
|
|
60
|
+
export function contentTypeFor(name) {
|
|
61
|
+
const dot = name.lastIndexOf(".");
|
|
62
|
+
if (dot < 0)
|
|
63
|
+
return DEFAULT_CONTENT_TYPE;
|
|
64
|
+
return BY_EXTENSION[name.slice(dot + 1).toLowerCase()] ?? DEFAULT_CONTENT_TYPE;
|
|
65
|
+
}
|
|
66
|
+
export function isModelImage(contentType) {
|
|
67
|
+
return MODEL_IMAGE_TYPES.includes(essence(contentType));
|
|
68
|
+
}
|
|
69
|
+
/** Files the model reads as text: `text/*`, JSON, XML, YAML and their `+json`/`+xml` kin. */
|
|
70
|
+
export function isText(contentType) {
|
|
71
|
+
const type = essence(contentType);
|
|
72
|
+
return (type.startsWith("text/") ||
|
|
73
|
+
type === "application/json" ||
|
|
74
|
+
type === "application/xml" ||
|
|
75
|
+
type === "application/yaml" ||
|
|
76
|
+
type === "application/x-yaml" ||
|
|
77
|
+
type === "application/javascript" ||
|
|
78
|
+
type.endsWith("+json") ||
|
|
79
|
+
type.endsWith("+xml"));
|
|
80
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message `parts` (protocol 6): what a user message's text and file parts become for the
|
|
3
|
+
* engine. A file part names an artifact the caller may read, of this session or Tenant-wide;
|
|
4
|
+
* its version is pinned when the message is accepted (the latest when the part names none).
|
|
5
|
+
* The engine gets an opaque media part whose `reference` is `{ artifactId, version }`: model-gate
|
|
6
|
+
* resolves it to bytes (`./files.ts`), and the transcript and the record never hold them.
|
|
7
|
+
*/
|
|
8
|
+
import type { MessagePart } from "@nylorun/core/contracts";
|
|
9
|
+
import type { JsonObject, JsonValue } from "@nylorun/core/define";
|
|
10
|
+
import type { Tx } from "../store/types.js";
|
|
11
|
+
import { type ArtifactAccess } from "./service.js";
|
|
12
|
+
/** A user-message content part, as the engine's `MessageInput.content` takes it. */
|
|
13
|
+
export type EnginePart = {
|
|
14
|
+
readonly type: "text";
|
|
15
|
+
readonly text: string;
|
|
16
|
+
} | {
|
|
17
|
+
readonly type: "media";
|
|
18
|
+
readonly mediaType: string;
|
|
19
|
+
readonly reference: JsonObject;
|
|
20
|
+
};
|
|
21
|
+
/** The reference a media part carries: the artifact version, by id. */
|
|
22
|
+
export interface FileReference {
|
|
23
|
+
readonly artifactId: string;
|
|
24
|
+
readonly version: number;
|
|
25
|
+
}
|
|
26
|
+
export declare function fileReferenceOf(value: JsonValue | undefined): FileReference | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* The parts with each file pinned to a version: for the engine (`engine`) and for a workflow's
|
|
29
|
+
* JSON input (`pinned`). Reads in the caller's transaction.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveMessageParts(t: Tx, parts: readonly MessagePart[], sessionId: string, access: ArtifactAccess): Promise<{
|
|
32
|
+
engine: EnginePart[];
|
|
33
|
+
pinned: MessagePart[];
|
|
34
|
+
}>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { fail } from "../tenant/http.js";
|
|
2
|
+
import { readableArtifact, versionOf } from "./service.js";
|
|
3
|
+
export function fileReferenceOf(value) {
|
|
4
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5
|
+
return undefined;
|
|
6
|
+
const { artifactId, version } = value;
|
|
7
|
+
if (typeof artifactId !== "string" || typeof version !== "number" || !Number.isInteger(version))
|
|
8
|
+
return undefined;
|
|
9
|
+
return { artifactId, version };
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The parts with each file pinned to a version: for the engine (`engine`) and for a workflow's
|
|
13
|
+
* JSON input (`pinned`). Reads in the caller's transaction.
|
|
14
|
+
*/
|
|
15
|
+
export async function resolveMessageParts(t, parts, sessionId, access) {
|
|
16
|
+
const engine = [];
|
|
17
|
+
const pinned = [];
|
|
18
|
+
for (const part of parts) {
|
|
19
|
+
if (part.type === "text") {
|
|
20
|
+
engine.push({ type: "text", text: part.text });
|
|
21
|
+
pinned.push(part);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const artifact = await readableArtifact(t, part.artifactId, access);
|
|
25
|
+
if (artifact.sessionId !== null && artifact.sessionId !== sessionId)
|
|
26
|
+
fail(400, `Artifact ${artifact.id} belongs to another session`);
|
|
27
|
+
if (artifact.kind !== "file")
|
|
28
|
+
fail(400, `Artifact ${artifact.id} is a folder; a file part names a file artifact`);
|
|
29
|
+
const version = await versionOf(t, artifact, part.version);
|
|
30
|
+
engine.push({
|
|
31
|
+
type: "media",
|
|
32
|
+
mediaType: version.contentType,
|
|
33
|
+
reference: { artifactId: artifact.id, version: version.version, name: artifact.name },
|
|
34
|
+
});
|
|
35
|
+
pinned.push({ type: "file", artifactId: artifact.id, version: version.version });
|
|
36
|
+
}
|
|
37
|
+
return { engine, pinned };
|
|
38
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ArtifactSource, ArtifactVersionView, ArtifactView, FolderManifest } from "@nylorun/core/contracts";
|
|
2
|
+
import { type BlobBody } from "../blob/index.js";
|
|
3
|
+
import type { ArtifactRow, ArtifactVersionRow, Tx } from "../store/types.js";
|
|
4
|
+
import { type SessionAccess, type TenantContext } from "../tenant/context.js";
|
|
5
|
+
/** What the caller may reach: undefined for the whole Tenant (an application principal). */
|
|
6
|
+
export type ArtifactAccess = SessionAccess | undefined;
|
|
7
|
+
export interface UploadInput {
|
|
8
|
+
/** A new version of this artifact; otherwise a new artifact. */
|
|
9
|
+
readonly artifactId?: string;
|
|
10
|
+
/** The new artifact's name; for a new version, it stays the artifact's. */
|
|
11
|
+
readonly name?: string;
|
|
12
|
+
/** The declared media type; default: what the name's extension implies. */
|
|
13
|
+
readonly contentType?: string;
|
|
14
|
+
/** The session a new artifact belongs to. Required when acting for a person. */
|
|
15
|
+
readonly sessionId?: string;
|
|
16
|
+
readonly labels?: Readonly<Record<string, string>>;
|
|
17
|
+
/** The body's declared length (`Content-Length`): one past the limits is refused unread. */
|
|
18
|
+
readonly declaredBytes?: number;
|
|
19
|
+
readonly source: ArtifactSource;
|
|
20
|
+
/** The turn that saved it (`save_artifact`), for the event. */
|
|
21
|
+
readonly turnId?: string | null;
|
|
22
|
+
/** The tool call that saved it, for the event. */
|
|
23
|
+
readonly callId?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface Uploaded {
|
|
26
|
+
readonly artifact: ArtifactView;
|
|
27
|
+
readonly version: ArtifactVersionView;
|
|
28
|
+
}
|
|
29
|
+
export declare function versionView(row: ArtifactVersionRow): ArtifactVersionView;
|
|
30
|
+
export declare function artifactView(row: ArtifactRow, versions?: ArtifactVersionRow[]): ArtifactView;
|
|
31
|
+
/** An artifact name: 1–255 characters, no path separators or control characters. */
|
|
32
|
+
export declare function checkArtifactName(name: string | undefined): string;
|
|
33
|
+
/**
|
|
34
|
+
* The artifact if the caller may reach it, else the 404 of a missing one. Reads its session
|
|
35
|
+
* (without locking) to check whose it is.
|
|
36
|
+
*/
|
|
37
|
+
export declare function readableArtifact(t: Tx, id: string, access: ArtifactAccess, options?: {
|
|
38
|
+
lock?: boolean;
|
|
39
|
+
}): Promise<ArtifactRow>;
|
|
40
|
+
/** `version`, or the latest when it is undefined; a missing one is a 404. */
|
|
41
|
+
export declare function versionOf(t: Tx, row: ArtifactRow, version: number | undefined): Promise<ArtifactVersionRow>;
|
|
42
|
+
/**
|
|
43
|
+
* Stores `body` as a new artifact, or a new version of one, within the Tenant's limits. See the
|
|
44
|
+
* module comment for how it streams and commits.
|
|
45
|
+
*/
|
|
46
|
+
export declare function uploadArtifact(ctx: TenantContext, input: UploadInput, body: BlobBody, access: ArtifactAccess, signal?: AbortSignal): Promise<Uploaded>;
|
|
47
|
+
/** The caller's artifacts: of one session, or of every session it reaches. */
|
|
48
|
+
export declare function listArtifacts(ctx: TenantContext, filter: {
|
|
49
|
+
sessionId?: string;
|
|
50
|
+
}, access: ArtifactAccess): Promise<{
|
|
51
|
+
artifacts: ArtifactView[];
|
|
52
|
+
}>;
|
|
53
|
+
/** One artifact with every version. */
|
|
54
|
+
export declare function getArtifact(ctx: TenantContext, id: string, access: ArtifactAccess): Promise<ArtifactView>;
|
|
55
|
+
/** One version's row, for a download. */
|
|
56
|
+
export declare function artifactContent(ctx: TenantContext, id: string, version: number | undefined, access: ArtifactAccess): Promise<{
|
|
57
|
+
artifact: ArtifactRow;
|
|
58
|
+
version: ArtifactVersionRow;
|
|
59
|
+
}>;
|
|
60
|
+
/** One version's row, its artifact and, for a folder, its manifest; a file is a 400. */
|
|
61
|
+
export declare function folderContent(ctx: TenantContext, id: string, version: number | undefined, access: ArtifactAccess): Promise<{
|
|
62
|
+
artifact: ArtifactRow;
|
|
63
|
+
version: ArtifactVersionRow;
|
|
64
|
+
manifest: FolderManifest;
|
|
65
|
+
}>;
|
|
66
|
+
/**
|
|
67
|
+
* Deletes the artifact and every version: its rows and, for a session's artifact, its
|
|
68
|
+
* `artifact.deleted` event in one transaction, then its bytes. A folder's content-addressed
|
|
69
|
+
* files go too, those no other version names, under the quota lock (`folders.ts`).
|
|
70
|
+
*/
|
|
71
|
+
export declare function deleteArtifact(ctx: TenantContext, id: string, access: ArtifactAccess): Promise<{
|
|
72
|
+
artifactId: string;
|
|
73
|
+
deleted: true;
|
|
74
|
+
}>;
|