@c4roa0/agentpay-user 0.0.0-stage → 0.3.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/AGENTS.md +137 -0
- package/CONTRIBUTING.md +16 -0
- package/LICENSE +373 -0
- package/NOTICE.md +20 -0
- package/README.md +84 -2
- package/SECURITY.md +16 -0
- package/doc/README.md +52 -0
- package/doc/TODO.md +97 -0
- package/doc/architecture.md +96 -0
- package/doc/design.md +60 -0
- package/doc/develope.md +68 -0
- package/doc/plans/user-wallet-distribution.md +91 -0
- package/doc/reference/harness-skill-runtime.md +120 -0
- package/doc/reference/local-cache-foundation.md +176 -0
- package/doc/reference/local-safe-owner-ui.md +25 -0
- package/doc/reference/local-update.md +18 -0
- package/doc/reference/multi-service-runtime.md +119 -0
- package/doc/reference/npm-integration-verification.md +67 -0
- package/doc/reference/onboarding-writing.md +48 -0
- package/doc/reference/original-order-recovery.md +97 -0
- package/doc/reference/private-anvil-onboarding-config.example.json +18 -0
- package/doc/reference/public-deployment-settings.md +56 -0
- package/doc/reference/random-skill-contract.md +18 -0
- package/doc/reference/result-file-cache.md +166 -0
- package/doc/reference/runtime-address-recovery.md +42 -0
- package/doc/reference/runtime-result-cache.md +157 -0
- package/doc/reference/runtime-startup-recovery.md +141 -0
- package/doc/reference/skill-input-output-cache.md +93 -0
- package/doc/reference/wallet-identity-policy-history.md +23 -0
- package/doc/runbooks/commands.md +89 -0
- package/doc/runbooks/deployment-authorization-update.md +42 -0
- package/doc/runbooks/emergency-payment-stop.md +80 -0
- package/doc/runbooks/local-autostart.md +212 -0
- package/doc/runbooks/local-cache.md +115 -0
- package/doc/runbooks/local-update.md +84 -0
- package/doc/runbooks/market-connection.md +73 -0
- package/doc/runbooks/npm-publication.md +56 -0
- package/doc/runbooks/official-test-package.md +62 -0
- package/doc/runbooks/order-cancellation.md +56 -0
- package/doc/runbooks/order-support.md +41 -0
- package/doc/runbooks/policy-renewal.md +80 -0
- package/doc/runbooks/private-anvil-terminal-setup.md +58 -0
- package/doc/runbooks/runtime-skill-client.md +155 -0
- package/doc/runbooks/safe-owner-web.md +104 -0
- package/doc/runbooks/safe-policy-migration.md +36 -0
- package/doc/runbooks/safe-policy-setup.md +52 -0
- package/doc/runbooks/terminal-authorization-preview.md +38 -0
- package/doc/runbooks/unlock-wallet.md +68 -0
- package/doc/runbooks/user-onboarding.md +87 -0
- package/doc/runbooks/wallet-identity.md +59 -0
- package/doc/security/public-source-review-2026-11-06.md +71 -0
- package/doc/usage.md +54 -0
- package/package.json +49 -4
- package/progress.md +178 -0
- package/run +29 -0
- package/scripts/agentpay-cli.mjs +11 -0
- package/scripts/authorize-existing-safe.mjs +211 -0
- package/scripts/autostart.mjs +186 -0
- package/scripts/cancel-order.mjs +76 -0
- package/scripts/check-installation.mjs +26 -0
- package/scripts/check-official-test-package.mjs +35 -0
- package/scripts/check-purchase-readiness.mjs +275 -0
- package/scripts/configure-runtime-client.mjs +16 -0
- package/scripts/guide-policy.mjs +64 -0
- package/scripts/initialize-caller-credential.mjs +11 -0
- package/scripts/install-skill-bundle.mjs +176 -0
- package/scripts/install-user.mjs +24 -0
- package/scripts/local-service.mjs +60 -0
- package/scripts/lock.mjs +11 -0
- package/scripts/login-notify.mjs +39 -0
- package/scripts/migrate-safe-policy.mjs +165 -0
- package/scripts/official-paid-test.mjs +9 -0
- package/scripts/poll-unconfirmed-notice.mjs +75 -0
- package/scripts/prepare-onboarding.mjs +33 -0
- package/scripts/preview-policy.mjs +65 -0
- package/scripts/random-model-ack-smoke.mjs +235 -0
- package/scripts/random-reject-smoke.mjs +147 -0
- package/scripts/renew.mjs +9 -0
- package/scripts/repair.mjs +10 -0
- package/scripts/revoke.mjs +9 -0
- package/scripts/select-skill-bundle.mjs +39 -0
- package/scripts/setup-local.mjs +199 -0
- package/scripts/status.mjs +13 -0
- package/scripts/test-package.mjs +35 -0
- package/scripts/unlock.mjs +13 -0
- package/scripts/update.mjs +7 -0
- package/scripts/user-setup.mjs +219 -0
- package/scripts/verify-npm-release.mjs +32 -0
- package/src/active-skill.mjs +160 -0
- package/src/anvil-onboarding-state.mjs +96 -0
- package/src/anvil-onboarding.mjs +1036 -0
- package/src/authorization-question-checkpoint.mjs +114 -0
- package/src/autostart-files.mjs +50 -0
- package/src/cache-management-cli.mjs +302 -0
- package/src/cache-management.mjs +47 -0
- package/src/caller-auth.mjs +58 -0
- package/src/client-request-lock.mjs +44 -0
- package/src/connect-market.mjs +408 -0
- package/src/demo-unlock-secret.mjs +64 -0
- package/src/deployment-status.mjs +198 -0
- package/src/file-cache-cli.mjs +83 -0
- package/src/file-cache-owner.mjs +74 -0
- package/src/file-cache-settings.mjs +50 -0
- package/src/file-cache.mjs +790 -0
- package/src/file-download.mjs +281 -0
- package/src/finalization-disclosure.mjs +16 -0
- package/src/guided-setup.mjs +194 -0
- package/src/index.mjs +1231 -0
- package/src/install-and-setup.mjs +38 -0
- package/src/install-preflight.mjs +201 -0
- package/src/install-user.mjs +279 -0
- package/src/installation-paths.mjs +31 -0
- package/src/invocation-result.mjs +148 -0
- package/src/invocation-store.mjs +385 -0
- package/src/local-data-directory.mjs +117 -0
- package/src/local-info.mjs +181 -0
- package/src/local-services.mjs +156 -0
- package/src/local-time.mjs +36 -0
- package/src/local-update.mjs +306 -0
- package/src/local-wallet-ipc.mjs +93 -0
- package/src/login-reminder.mjs +28 -0
- package/src/market-download-url.mjs +21 -0
- package/src/node-version.mjs +12 -0
- package/src/npm-update.mjs +126 -0
- package/src/official-paid-test.mjs +407 -0
- package/src/official-test-package.mjs +41 -0
- package/src/official-test-update.mjs +162 -0
- package/src/order-cancellation.mjs +241 -0
- package/src/order-closure.mjs +42 -0
- package/src/order-list-panel.mjs +110 -0
- package/src/order-review.mjs +424 -0
- package/src/order-status-panel.mjs +65 -0
- package/src/order-support.mjs +365 -0
- package/src/owner-approval-server.mjs +332 -0
- package/src/owner-signer.mjs +67 -0
- package/src/policy-info.mjs +38 -0
- package/src/policy-migration.mjs +84 -0
- package/src/policy-operations.mjs +67 -0
- package/src/policy-preview-server.mjs +236 -0
- package/src/policy-terminal-guide.mjs +377 -0
- package/src/prepare-local-services.mjs +143 -0
- package/src/prepare-onboarding.mjs +210 -0
- package/src/private-journal-files.mjs +29 -0
- package/src/readiness-profile.mjs +30 -0
- package/src/renew-policy.mjs +605 -0
- package/src/repair-market-wallet.mjs +137 -0
- package/src/restore-wallet.mjs +255 -0
- package/src/result-cache-view.mjs +276 -0
- package/src/runtime-client.mjs +1145 -0
- package/src/runtime-connection-settings.mjs +101 -0
- package/src/runtime-recovery.mjs +667 -0
- package/src/runtime-status.mjs +32 -0
- package/src/safe-owner-anvil.mjs +115 -0
- package/src/safe-policy-plan.mjs +195 -0
- package/src/service-reference.mjs +233 -0
- package/src/service-url.mjs +52 -0
- package/src/settlement-response.mjs +69 -0
- package/src/setup-autostart.mjs +197 -0
- package/src/setup-demo.mjs +152 -0
- package/src/skill-bundle.mjs +142 -0
- package/src/skill-client.mjs +198 -0
- package/src/skill-install-prompt.mjs +31 -0
- package/src/skill-package.mjs +50 -0
- package/src/skill-zip.mjs +220 -0
- package/src/status-panel.mjs +589 -0
- package/src/transport.mjs +48 -0
- package/src/uninstall-local.mjs +299 -0
- package/src/user-cli.mjs +354 -0
- package/src/user-setup-guide.mjs +193 -0
- package/src/wallet-control.mjs +182 -0
- package/src/wallet-ipc-error.mjs +16 -0
- package/src/wallet-package.mjs +22 -0
- package/update-pair.json +6 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
// Private content storage with explicit ownership, limits and integrity metadata.
|
|
2
|
+
// Cache cleanup must preserve protected request inputs and original-result recovery.
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
|
|
5
|
+
// If a copy was not distributed with this file, see https://mozilla.org/MPL/2.0/.
|
|
6
|
+
import { cacheOwner } from "./file-cache-owner.mjs";
|
|
7
|
+
import { constants } from "node:fs";
|
|
8
|
+
import { mkdir, lstat, open, rename, unlink, readdir } from "node:fs/promises";
|
|
9
|
+
import { join, dirname, isAbsolute } from "node:path";
|
|
10
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
11
|
+
import { readPrivateJson } from "./order-review.mjs";
|
|
12
|
+
import { downloadFile, fileError } from "./file-download.mjs";
|
|
13
|
+
import {
|
|
14
|
+
fileCacheLimits,
|
|
15
|
+
validateCacheLimits,
|
|
16
|
+
} from "./file-cache-settings.mjs";
|
|
17
|
+
export { fileCacheLimits } from "./file-cache-settings.mjs";
|
|
18
|
+
const digest = (v) => createHash("sha256").update(v).digest("hex");
|
|
19
|
+
const hex = /^[a-f0-9]{64}$/,
|
|
20
|
+
order = /^0x[a-f0-9]{64}$/,
|
|
21
|
+
request = /^[A-Za-z0-9._:-]{16,128}$/;
|
|
22
|
+
const states = ["downloading", "available", "failed", "deleting", "removed"];
|
|
23
|
+
const privateFile = (s) =>
|
|
24
|
+
s.isFile() &&
|
|
25
|
+
s.nlink === 1 &&
|
|
26
|
+
(typeof process.getuid !== "function" ||
|
|
27
|
+
(s.uid === process.getuid() && !(s.mode & 0o077)));
|
|
28
|
+
const scope = (r) => (r.purpose === "input" ? r.requestId : r.orderId);
|
|
29
|
+
// Preserve the original output IDs; inputs occupy an explicitly separate namespace.
|
|
30
|
+
const recordId = (r) =>
|
|
31
|
+
digest(
|
|
32
|
+
r.purpose === "input"
|
|
33
|
+
? "input\n" + r.requestId + "\n" + r.fileKey
|
|
34
|
+
: r.orderId + "\n" + r.fileKey,
|
|
35
|
+
);
|
|
36
|
+
const generation = (r) => digest(JSON.stringify(r));
|
|
37
|
+
const fail = (code, message) => fileError(code, message);
|
|
38
|
+
export function createFileCache(
|
|
39
|
+
statePath,
|
|
40
|
+
{
|
|
41
|
+
download = downloadFile,
|
|
42
|
+
limits = fileCacheLimits,
|
|
43
|
+
privateOrigins = [],
|
|
44
|
+
} = {},
|
|
45
|
+
) {
|
|
46
|
+
limits = validateCacheLimits(limits);
|
|
47
|
+
const directory = statePath + ".files",
|
|
48
|
+
index = join(directory, "index.json");
|
|
49
|
+
let owner;
|
|
50
|
+
async function exclusive(fn) {
|
|
51
|
+
await mkdir(directory, { mode: 0o700 }).catch((e) => {
|
|
52
|
+
if (e.code !== "EEXIST") throw e;
|
|
53
|
+
});
|
|
54
|
+
const s = await lstat(directory);
|
|
55
|
+
if (
|
|
56
|
+
!s.isDirectory() ||
|
|
57
|
+
s.isSymbolicLink() ||
|
|
58
|
+
(typeof process.getuid === "function" &&
|
|
59
|
+
(s.uid !== process.getuid() || s.mode & 0o077))
|
|
60
|
+
)
|
|
61
|
+
throw fail("FILE_CACHE_UNSAFE", "File cache permissions need review.");
|
|
62
|
+
owner = await cacheOwner(directory);
|
|
63
|
+
const next = owner.tail.then(fn);
|
|
64
|
+
owner.tail = next.catch(() => {});
|
|
65
|
+
return next;
|
|
66
|
+
}
|
|
67
|
+
async function sync(path = directory) {
|
|
68
|
+
const h = await open(path, "r");
|
|
69
|
+
try {
|
|
70
|
+
await h.sync();
|
|
71
|
+
} finally {
|
|
72
|
+
await h.close();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function atomic(path, value) {
|
|
76
|
+
const temp = join(dirname(path), ".index-" + randomUUID());
|
|
77
|
+
let h;
|
|
78
|
+
try {
|
|
79
|
+
h = await open(temp, "wx", 0o600);
|
|
80
|
+
await h.writeFile(JSON.stringify(value));
|
|
81
|
+
await h.sync();
|
|
82
|
+
await h.close();
|
|
83
|
+
h = null;
|
|
84
|
+
await rename(temp, path);
|
|
85
|
+
await sync(dirname(path));
|
|
86
|
+
} finally {
|
|
87
|
+
await h?.close();
|
|
88
|
+
await unlink(temp).catch((e) => {
|
|
89
|
+
if (e.code !== "ENOENT") throw e;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function persist(doc) {
|
|
94
|
+
if (Buffer.byteLength(JSON.stringify(doc)) > 8 * 1048576)
|
|
95
|
+
throw fail("FILE_COUNT_LIMIT", "File cache metadata limit reached.");
|
|
96
|
+
await atomic(index, doc);
|
|
97
|
+
}
|
|
98
|
+
async function discard(path) {
|
|
99
|
+
try {
|
|
100
|
+
const st = await lstat(path);
|
|
101
|
+
if (!privateFile(st) || st.isSymbolicLink())
|
|
102
|
+
throw fail(
|
|
103
|
+
"FILE_CACHE_UNSAFE",
|
|
104
|
+
"A cache file needs inspection before cleanup.",
|
|
105
|
+
);
|
|
106
|
+
await unlink(path);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
if (e.code !== "ENOENT") throw e;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function load() {
|
|
112
|
+
const doc = (await readPrivateJson(index, 8 * 1048576)) || {
|
|
113
|
+
version: 2,
|
|
114
|
+
files: {},
|
|
115
|
+
};
|
|
116
|
+
if (
|
|
117
|
+
![1, 2].includes(doc.version) ||
|
|
118
|
+
!doc.files ||
|
|
119
|
+
Array.isArray(doc.files) ||
|
|
120
|
+
typeof doc.files !== "object" ||
|
|
121
|
+
Object.keys(doc.files).length > 100000
|
|
122
|
+
)
|
|
123
|
+
throw fail("FILE_CACHE_INVALID", "File cache metadata needs review.");
|
|
124
|
+
const upgrading = doc.version === 1;
|
|
125
|
+
for (const [id, r] of Object.entries(doc.files)) {
|
|
126
|
+
if (upgrading && r) {
|
|
127
|
+
r.purpose = "output";
|
|
128
|
+
r.sourceKind = "url";
|
|
129
|
+
}
|
|
130
|
+
if (
|
|
131
|
+
!hex.test(id) ||
|
|
132
|
+
!r ||
|
|
133
|
+
!["input", "output"].includes(r.purpose) ||
|
|
134
|
+
!["url", "local"].includes(r.sourceKind) ||
|
|
135
|
+
(r.purpose === "input"
|
|
136
|
+
? !request.test(r.requestId || "")
|
|
137
|
+
: !order.test(r.orderId || "") || r.sourceKind !== "url") ||
|
|
138
|
+
(r.revision !== undefined &&
|
|
139
|
+
(typeof r.revision !== "string" ||
|
|
140
|
+
!/^[a-f0-9-]{36}$/.test(r.revision))) ||
|
|
141
|
+
!states.includes(r.state) ||
|
|
142
|
+
!Number.isSafeInteger(r.bytes) ||
|
|
143
|
+
r.bytes < 0 ||
|
|
144
|
+
typeof r.fileKey !== "string" ||
|
|
145
|
+
!/^[A-Za-z0-9._-]{1,80}$/.test(r.fileKey) ||
|
|
146
|
+
id !== recordId(r) ||
|
|
147
|
+
(r.sourceKind === "url"
|
|
148
|
+
? typeof r.url !== "string" || r.url.length > 8192
|
|
149
|
+
: typeof r.sourcePath !== "string" ||
|
|
150
|
+
!isAbsolute(r.sourcePath) ||
|
|
151
|
+
r.sourcePath.length > 8192) ||
|
|
152
|
+
typeof r.filename !== "string" ||
|
|
153
|
+
r.filename.length > 200 ||
|
|
154
|
+
/[\x00-\x1f\x7f/\\]/.test(r.filename) ||
|
|
155
|
+
!Number.isSafeInteger(r.updatedAtMs) ||
|
|
156
|
+
r.updatedAtMs < 0 ||
|
|
157
|
+
(r.sha256 != null && !hex.test(r.sha256)) ||
|
|
158
|
+
(r.expectedSha256 != null && !hex.test(r.expectedSha256)) ||
|
|
159
|
+
(r.state === "available" && !r.sha256) ||
|
|
160
|
+
(r.purpose === "input" && typeof r.protected !== "boolean") ||
|
|
161
|
+
(r.reservation !== undefined &&
|
|
162
|
+
(!Number.isSafeInteger(r.reservation) || r.reservation < 0))
|
|
163
|
+
)
|
|
164
|
+
throw fail("FILE_CACHE_INVALID", "File cache metadata needs review.");
|
|
165
|
+
}
|
|
166
|
+
doc.version = 2;
|
|
167
|
+
if (!owner.initialized) {
|
|
168
|
+
for (const name of await readdir(directory))
|
|
169
|
+
if (/^\.index-[a-f0-9-]{36}$/.test(name))
|
|
170
|
+
await discard(join(directory, name));
|
|
171
|
+
let changed = upgrading;
|
|
172
|
+
for (const [id, r] of Object.entries(doc.files)) {
|
|
173
|
+
if (["deleting", "removed"].includes(r.state)) {
|
|
174
|
+
await discard(join(directory, id + ".bin"));
|
|
175
|
+
await discard(join(directory, id + ".part"));
|
|
176
|
+
if (r.state === "deleting") {
|
|
177
|
+
r.state = "removed";
|
|
178
|
+
r.updatedAtMs = Date.now();
|
|
179
|
+
changed = true;
|
|
180
|
+
}
|
|
181
|
+
} else if (r.state === "downloading") {
|
|
182
|
+
// A durable final file may precede the index commit. Preserve it only when a
|
|
183
|
+
// saved digest proves this is the completed transfer, otherwise fail closed.
|
|
184
|
+
let recovered = false;
|
|
185
|
+
if (r.sha256) {
|
|
186
|
+
try {
|
|
187
|
+
await verify(id, r);
|
|
188
|
+
recovered = true;
|
|
189
|
+
} catch {
|
|
190
|
+
try {
|
|
191
|
+
await verify(id, r, ".part");
|
|
192
|
+
await rename(
|
|
193
|
+
join(directory, id + ".part"),
|
|
194
|
+
join(directory, id + ".bin"),
|
|
195
|
+
);
|
|
196
|
+
await sync();
|
|
197
|
+
recovered = true;
|
|
198
|
+
} catch {}
|
|
199
|
+
}
|
|
200
|
+
if (recovered) r.state = "available";
|
|
201
|
+
}
|
|
202
|
+
if (!recovered) {
|
|
203
|
+
await discard(join(directory, id + ".bin"));
|
|
204
|
+
r.state = "failed";
|
|
205
|
+
r.error = "FILE_TRANSFER_INTERRUPTED";
|
|
206
|
+
r.bytes = 0;
|
|
207
|
+
}
|
|
208
|
+
await discard(join(directory, id + ".part"));
|
|
209
|
+
delete r.reservation;
|
|
210
|
+
r.updatedAtMs = Date.now();
|
|
211
|
+
changed = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
await sync();
|
|
215
|
+
if (changed) await persist(doc);
|
|
216
|
+
owner.initialized = true;
|
|
217
|
+
} else if (upgrading)
|
|
218
|
+
throw fail(
|
|
219
|
+
"FILE_CACHE_INVALID",
|
|
220
|
+
"Cache metadata changed while this Runtime was running.",
|
|
221
|
+
);
|
|
222
|
+
return doc;
|
|
223
|
+
}
|
|
224
|
+
const publicRow = (id, r) => ({
|
|
225
|
+
id,
|
|
226
|
+
purpose: r.purpose,
|
|
227
|
+
...(r.purpose === "input"
|
|
228
|
+
? { requestId: r.requestId, protected: r.protected }
|
|
229
|
+
: { orderId: r.orderId }),
|
|
230
|
+
fileKey: r.fileKey,
|
|
231
|
+
filename: r.filename,
|
|
232
|
+
transport:
|
|
233
|
+
r.sourceKind === "local"
|
|
234
|
+
? "local"
|
|
235
|
+
: r.url.startsWith("https:")
|
|
236
|
+
? "https"
|
|
237
|
+
: "http",
|
|
238
|
+
state: r.state,
|
|
239
|
+
bytes: r.bytes,
|
|
240
|
+
sha256: r.sha256 || null,
|
|
241
|
+
integrity:
|
|
242
|
+
r.state !== "available"
|
|
243
|
+
? "not_verified"
|
|
244
|
+
: r.expectedSha256
|
|
245
|
+
? "expected_digest_matched"
|
|
246
|
+
: "local_digest_only",
|
|
247
|
+
updatedAtMs: r.updatedAtMs,
|
|
248
|
+
error: r.error || null,
|
|
249
|
+
...(r.state === "available"
|
|
250
|
+
? { localPath: join(directory, id + ".bin") }
|
|
251
|
+
: {}),
|
|
252
|
+
});
|
|
253
|
+
async function usage(doc) {
|
|
254
|
+
let bytes = 0,
|
|
255
|
+
inputBytes = 0,
|
|
256
|
+
outputBytes = 0,
|
|
257
|
+
otherBytes = 0,
|
|
258
|
+
reservedBytes = 0;
|
|
259
|
+
const sizes = new Map();
|
|
260
|
+
for (const name of await readdir(directory)) {
|
|
261
|
+
const st = await lstat(join(directory, name));
|
|
262
|
+
if (!privateFile(st) || st.isSymbolicLink())
|
|
263
|
+
throw fail(
|
|
264
|
+
"FILE_CACHE_UNSAFE",
|
|
265
|
+
"File cache contains an unsafe entry. Inspect it before continuing.",
|
|
266
|
+
);
|
|
267
|
+
if (
|
|
268
|
+
name === "index.json" ||
|
|
269
|
+
name === "owner.lock" ||
|
|
270
|
+
name.startsWith(".index-")
|
|
271
|
+
) {
|
|
272
|
+
otherBytes += st.size;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
bytes += st.size;
|
|
276
|
+
sizes.set(name, st.size);
|
|
277
|
+
const r = doc.files[name.slice(0, 64)];
|
|
278
|
+
if (/^[a-f0-9]{64}\.(bin|part)$/.test(name) && r) {
|
|
279
|
+
if (r.purpose === "input") inputBytes += st.size;
|
|
280
|
+
else outputBytes += st.size;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
for (const [id, r] of Object.entries(doc.files))
|
|
284
|
+
if (r.state === "downloading")
|
|
285
|
+
reservedBytes += Math.max(
|
|
286
|
+
0,
|
|
287
|
+
(r.reservation ?? limits.singleBytes) -
|
|
288
|
+
(sizes.get(id + ".part") || sizes.get(id + ".bin") || 0),
|
|
289
|
+
);
|
|
290
|
+
return {
|
|
291
|
+
bytes,
|
|
292
|
+
inputBytes,
|
|
293
|
+
outputBytes,
|
|
294
|
+
unknownBytes: bytes - inputBytes - outputBytes,
|
|
295
|
+
metadataBytes: otherBytes,
|
|
296
|
+
reservedBytes,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
async function verify(id, row, extension = ".bin") {
|
|
300
|
+
const h = await open(
|
|
301
|
+
join(directory, id + extension),
|
|
302
|
+
constants.O_RDONLY | (constants.O_NOFOLLOW || 0),
|
|
303
|
+
);
|
|
304
|
+
try {
|
|
305
|
+
const s = await h.stat();
|
|
306
|
+
if (!privateFile(s) || s.size !== row.bytes)
|
|
307
|
+
throw Error("Invalid cache file");
|
|
308
|
+
const hash = createHash("sha256");
|
|
309
|
+
for await (const part of h.createReadStream({ autoClose: false }))
|
|
310
|
+
hash.update(part);
|
|
311
|
+
if (hash.digest("hex") !== row.sha256)
|
|
312
|
+
throw Error("Invalid cache digest");
|
|
313
|
+
} finally {
|
|
314
|
+
await h.close();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function rowFor(doc, id) {
|
|
318
|
+
if (!hex.test(id || "") || !doc.files[id])
|
|
319
|
+
throw fail("FILE_NOT_FOUND", "Use a full file ID from agentpay cache list.");
|
|
320
|
+
return doc.files[id];
|
|
321
|
+
}
|
|
322
|
+
async function deleteRow(doc, id) {
|
|
323
|
+
const r = rowFor(doc, id);
|
|
324
|
+
if (r.purpose === "input" && r.protected)
|
|
325
|
+
throw fail(
|
|
326
|
+
"FILE_PROTECTED",
|
|
327
|
+
"This input is kept for request recovery and cannot be cleaned yet.",
|
|
328
|
+
);
|
|
329
|
+
if (r.state === "downloading")
|
|
330
|
+
throw fail("FILE_BUSY", "Wait for this transfer before removing it.");
|
|
331
|
+
r.state = "deleting";
|
|
332
|
+
r.updatedAtMs = Date.now();
|
|
333
|
+
await persist(doc);
|
|
334
|
+
await discard(join(directory, id + ".bin"));
|
|
335
|
+
await discard(join(directory, id + ".part"));
|
|
336
|
+
await sync();
|
|
337
|
+
r.state = "removed";
|
|
338
|
+
r.updatedAtMs = Date.now();
|
|
339
|
+
await persist(doc);
|
|
340
|
+
return publicRow(id, r);
|
|
341
|
+
}
|
|
342
|
+
async function transfer(input, local = false) {
|
|
343
|
+
const {
|
|
344
|
+
orderId,
|
|
345
|
+
requestId,
|
|
346
|
+
fileKey,
|
|
347
|
+
url,
|
|
348
|
+
sourcePath,
|
|
349
|
+
filename = "download",
|
|
350
|
+
sha256 = null,
|
|
351
|
+
} = input;
|
|
352
|
+
const purpose = requestId !== undefined ? "input" : "output";
|
|
353
|
+
if (
|
|
354
|
+
(purpose === "input"
|
|
355
|
+
? !request.test(requestId || "")
|
|
356
|
+
: !order.test(orderId || "")) ||
|
|
357
|
+
(purpose === "input" && orderId !== undefined) ||
|
|
358
|
+
(local && purpose !== "input") ||
|
|
359
|
+
typeof fileKey !== "string" ||
|
|
360
|
+
!/^[A-Za-z0-9._-]{1,80}$/.test(fileKey) ||
|
|
361
|
+
(local
|
|
362
|
+
? typeof sourcePath !== "string" ||
|
|
363
|
+
!isAbsolute(sourcePath) ||
|
|
364
|
+
sourcePath.length > 8192
|
|
365
|
+
: typeof url !== "string" || url.length > 8192) ||
|
|
366
|
+
typeof filename !== "string" ||
|
|
367
|
+
filename.length > 200 ||
|
|
368
|
+
/[\x00-\x1f\x7f/\\]/.test(filename) ||
|
|
369
|
+
(sha256 !== null && !hex.test(sha256))
|
|
370
|
+
)
|
|
371
|
+
throw fail(
|
|
372
|
+
"FILE_INPUT_INVALID",
|
|
373
|
+
"Supply the original scope, file key and explicit source.",
|
|
374
|
+
);
|
|
375
|
+
const identity =
|
|
376
|
+
purpose === "input"
|
|
377
|
+
? { purpose, requestId, protected: true }
|
|
378
|
+
: { purpose, orderId };
|
|
379
|
+
const id = recordId({ ...identity, fileKey });
|
|
380
|
+
let reused, previousDigest, source, sourceStat;
|
|
381
|
+
const part = join(directory, id + ".part");
|
|
382
|
+
let h,
|
|
383
|
+
started = false;
|
|
384
|
+
try {
|
|
385
|
+
await exclusive(async () => {
|
|
386
|
+
const doc = await load(),
|
|
387
|
+
old = doc.files[id];
|
|
388
|
+
previousDigest = old?.sha256;
|
|
389
|
+
if (
|
|
390
|
+
old &&
|
|
391
|
+
(old.expectedSha256 !== sha256 ||
|
|
392
|
+
old.sourceKind !== (local ? "local" : "url") ||
|
|
393
|
+
(!local &&
|
|
394
|
+
old.url !== url &&
|
|
395
|
+
!(old.sha256 || old.expectedSha256)) ||
|
|
396
|
+
(local && old.sourcePath !== sourcePath))
|
|
397
|
+
)
|
|
398
|
+
throw fail(
|
|
399
|
+
"FILE_REFERENCE_CONFLICT",
|
|
400
|
+
"Keep the original file identity and digest when retrying.",
|
|
401
|
+
);
|
|
402
|
+
if (old?.state === "downloading" || old?.state === "deleting")
|
|
403
|
+
throw fail("FILE_BUSY", "This file is being changed. Retry shortly.");
|
|
404
|
+
if (old?.state === "available") {
|
|
405
|
+
try {
|
|
406
|
+
await verify(id, old);
|
|
407
|
+
reused = publicRow(id, old);
|
|
408
|
+
return;
|
|
409
|
+
} catch {
|
|
410
|
+
old.state = "failed";
|
|
411
|
+
old.error = "FILE_CACHE_CORRUPT";
|
|
412
|
+
await persist(doc);
|
|
413
|
+
throw fail(
|
|
414
|
+
"FILE_CACHE_CORRUPT",
|
|
415
|
+
"The saved copy changed or disappeared. Preserve the original request and inspect this file.",
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (old?.purpose === "input" && old.sha256)
|
|
420
|
+
throw fail(
|
|
421
|
+
"FILE_CACHE_CORRUPT",
|
|
422
|
+
"Original input bytes are unavailable. Keep this request for recovery; do not substitute new input.",
|
|
423
|
+
);
|
|
424
|
+
const rows = Object.values(doc.files);
|
|
425
|
+
if (
|
|
426
|
+
rows.filter((r) => r.state === "downloading").length >=
|
|
427
|
+
limits.concurrent
|
|
428
|
+
)
|
|
429
|
+
throw fail(
|
|
430
|
+
"FILE_BUSY",
|
|
431
|
+
"Other files are being saved. Retry shortly.",
|
|
432
|
+
);
|
|
433
|
+
if (
|
|
434
|
+
(!old || old.state === "removed") &&
|
|
435
|
+
rows.filter(
|
|
436
|
+
(r) =>
|
|
437
|
+
r.state !== "removed" &&
|
|
438
|
+
r.purpose === purpose &&
|
|
439
|
+
scope(r) === scope(identity),
|
|
440
|
+
).length >= limits.filesPerResult
|
|
441
|
+
)
|
|
442
|
+
throw fail(
|
|
443
|
+
"FILE_COUNT_LIMIT",
|
|
444
|
+
"The file limit for this request or Order was reached.",
|
|
445
|
+
);
|
|
446
|
+
if (!old && rows.length >= limits.records)
|
|
447
|
+
throw fail(
|
|
448
|
+
"FILE_HISTORY_LIMIT",
|
|
449
|
+
"File history is full. Increase the record limit with agentpay cache settings.",
|
|
450
|
+
);
|
|
451
|
+
let reservation = limits.singleBytes;
|
|
452
|
+
if (local) {
|
|
453
|
+
source = await open(
|
|
454
|
+
sourcePath,
|
|
455
|
+
constants.O_RDONLY |
|
|
456
|
+
(constants.O_NOFOLLOW || 0) |
|
|
457
|
+
(constants.O_NONBLOCK || 0),
|
|
458
|
+
);
|
|
459
|
+
sourceStat = await source.stat();
|
|
460
|
+
if (!sourceStat.isFile())
|
|
461
|
+
throw fail(
|
|
462
|
+
"FILE_INPUT_INVALID",
|
|
463
|
+
"Select a regular file, not a directory or device.",
|
|
464
|
+
);
|
|
465
|
+
if (sourceStat.size > limits.singleBytes)
|
|
466
|
+
throw fail(
|
|
467
|
+
"FILE_TOO_LARGE",
|
|
468
|
+
"Input file exceeds the local size limit.",
|
|
469
|
+
);
|
|
470
|
+
reservation = sourceStat.size;
|
|
471
|
+
}
|
|
472
|
+
const used = await usage(doc);
|
|
473
|
+
if (used.bytes + used.reservedBytes + reservation > limits.totalBytes)
|
|
474
|
+
throw fail(
|
|
475
|
+
"FILE_CACHE_FULL",
|
|
476
|
+
"Cache is full. Run agentpay cache to review space and limits.",
|
|
477
|
+
);
|
|
478
|
+
await discard(part);
|
|
479
|
+
await discard(join(directory, id + ".bin"));
|
|
480
|
+
doc.files[id] = {
|
|
481
|
+
...identity,
|
|
482
|
+
fileKey,
|
|
483
|
+
revision: randomUUID(),
|
|
484
|
+
...(local
|
|
485
|
+
? { sourceKind: "local", sourcePath }
|
|
486
|
+
: { sourceKind: "url", url }),
|
|
487
|
+
filename,
|
|
488
|
+
expectedSha256: sha256,
|
|
489
|
+
...(previousDigest ? { sha256: previousDigest } : {}),
|
|
490
|
+
state: "downloading",
|
|
491
|
+
bytes: 0,
|
|
492
|
+
reservation,
|
|
493
|
+
updatedAtMs: Date.now(),
|
|
494
|
+
};
|
|
495
|
+
await persist(doc);
|
|
496
|
+
started = true;
|
|
497
|
+
});
|
|
498
|
+
if (reused) return reused;
|
|
499
|
+
h = await open(part, "wx", 0o600);
|
|
500
|
+
const hash = createHash("sha256");
|
|
501
|
+
let bytes = 0;
|
|
502
|
+
const write = async (chunk) => {
|
|
503
|
+
bytes += chunk.length;
|
|
504
|
+
if (bytes > limits.singleBytes || (local && bytes > sourceStat.size))
|
|
505
|
+
throw fail("FILE_TOO_LARGE", "File exceeds its reserved size.");
|
|
506
|
+
hash.update(chunk);
|
|
507
|
+
await h.writeFile(chunk);
|
|
508
|
+
};
|
|
509
|
+
if (local) {
|
|
510
|
+
for await (const chunk of source.createReadStream({ autoClose: false }))
|
|
511
|
+
await write(chunk);
|
|
512
|
+
const after = await source.stat();
|
|
513
|
+
if (
|
|
514
|
+
after.size !== sourceStat.size ||
|
|
515
|
+
after.mtimeMs !== sourceStat.mtimeMs ||
|
|
516
|
+
after.ctimeMs !== sourceStat.ctimeMs ||
|
|
517
|
+
bytes !== sourceStat.size
|
|
518
|
+
)
|
|
519
|
+
throw fail(
|
|
520
|
+
"FILE_SOURCE_CHANGED",
|
|
521
|
+
"The source changed while saving. Wait for it to finish before retrying.",
|
|
522
|
+
);
|
|
523
|
+
} else
|
|
524
|
+
await download(url, {
|
|
525
|
+
maxBytes: limits.singleBytes,
|
|
526
|
+
timeoutMs: limits.timeoutMs,
|
|
527
|
+
privateOrigins,
|
|
528
|
+
write,
|
|
529
|
+
});
|
|
530
|
+
const actual = hash.digest("hex");
|
|
531
|
+
if ((sha256 || previousDigest) && actual !== (sha256 || previousDigest))
|
|
532
|
+
throw fail(
|
|
533
|
+
"FILE_DIGEST_MISMATCH",
|
|
534
|
+
"File contents do not match the original digest.",
|
|
535
|
+
);
|
|
536
|
+
await h.sync();
|
|
537
|
+
await h.close();
|
|
538
|
+
h = null;
|
|
539
|
+
return await exclusive(async () => {
|
|
540
|
+
const doc = await load(),
|
|
541
|
+
r = doc.files[id];
|
|
542
|
+
Object.assign(r, { bytes, sha256: actual, updatedAtMs: Date.now() });
|
|
543
|
+
await persist(doc); // durable digest before publishing bytes: restart can reconcile.
|
|
544
|
+
await rename(part, join(directory, id + ".bin"));
|
|
545
|
+
await sync();
|
|
546
|
+
r.state = "available";
|
|
547
|
+
delete r.reservation;
|
|
548
|
+
r.error = null;
|
|
549
|
+
await persist(doc);
|
|
550
|
+
return publicRow(id, r);
|
|
551
|
+
});
|
|
552
|
+
} catch (error) {
|
|
553
|
+
await h?.close();
|
|
554
|
+
h = null;
|
|
555
|
+
if (started)
|
|
556
|
+
await exclusive(async () => {
|
|
557
|
+
const doc = await load(),
|
|
558
|
+
r = doc.files[id];
|
|
559
|
+
// Never discard a completed final file after an index write failure.
|
|
560
|
+
try {
|
|
561
|
+
if (r.sha256) {
|
|
562
|
+
await verify(id, r);
|
|
563
|
+
r.state = "available";
|
|
564
|
+
r.error = null;
|
|
565
|
+
delete r.reservation;
|
|
566
|
+
await persist(doc);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
} catch {}
|
|
570
|
+
await discard(part);
|
|
571
|
+
r.state = "failed";
|
|
572
|
+
r.bytes = 0;
|
|
573
|
+
delete r.reservation;
|
|
574
|
+
r.error = error.code?.startsWith("FILE_")
|
|
575
|
+
? error.code
|
|
576
|
+
: "FILE_STORAGE_FAILED";
|
|
577
|
+
await persist(doc);
|
|
578
|
+
}).catch(() => {});
|
|
579
|
+
if (error.code?.startsWith("FILE_")) throw error;
|
|
580
|
+
throw fail(
|
|
581
|
+
"FILE_STORAGE_FAILED",
|
|
582
|
+
"Could not save this file. Check disk space and permissions; no purchase was made.",
|
|
583
|
+
);
|
|
584
|
+
} finally {
|
|
585
|
+
await source?.close();
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const api = {
|
|
589
|
+
async list(orderId, { offset = 0, limit = 100 } = {}) {
|
|
590
|
+
return (await api.listAll({ orderId, offset, limit })).items;
|
|
591
|
+
},
|
|
592
|
+
async listAll({
|
|
593
|
+
purpose,
|
|
594
|
+
state,
|
|
595
|
+
orderId,
|
|
596
|
+
requestId,
|
|
597
|
+
offset = 0,
|
|
598
|
+
limit = 100,
|
|
599
|
+
} = {}) {
|
|
600
|
+
if (
|
|
601
|
+
(purpose !== undefined && !["input", "output"].includes(purpose)) ||
|
|
602
|
+
(state !== undefined && !states.includes(state)) ||
|
|
603
|
+
(orderId !== undefined && !order.test(orderId)) ||
|
|
604
|
+
(requestId !== undefined && !request.test(requestId)) ||
|
|
605
|
+
!Number.isSafeInteger(offset) ||
|
|
606
|
+
offset < 0 ||
|
|
607
|
+
!Number.isSafeInteger(limit) ||
|
|
608
|
+
limit < 1 ||
|
|
609
|
+
limit > 100
|
|
610
|
+
)
|
|
611
|
+
throw fail(
|
|
612
|
+
"FILE_INPUT_INVALID",
|
|
613
|
+
"Use valid filters and a page size from 1 to 100.",
|
|
614
|
+
);
|
|
615
|
+
return exclusive(async () => {
|
|
616
|
+
const doc = await load(),
|
|
617
|
+
rows = Object.entries(doc.files)
|
|
618
|
+
.filter(
|
|
619
|
+
([, r]) =>
|
|
620
|
+
(!purpose || r.purpose === purpose) &&
|
|
621
|
+
(!state || r.state === state) &&
|
|
622
|
+
(!orderId || r.orderId === orderId) &&
|
|
623
|
+
(!requestId || r.requestId === requestId),
|
|
624
|
+
)
|
|
625
|
+
.sort(
|
|
626
|
+
(a, b) =>
|
|
627
|
+
b[1].updatedAtMs - a[1].updatedAtMs || a[0].localeCompare(b[0]),
|
|
628
|
+
);
|
|
629
|
+
return {
|
|
630
|
+
total: rows.length,
|
|
631
|
+
items: rows
|
|
632
|
+
.slice(offset, offset + limit)
|
|
633
|
+
.map(([id, r]) => publicRow(id, r)),
|
|
634
|
+
};
|
|
635
|
+
});
|
|
636
|
+
},
|
|
637
|
+
async summary() {
|
|
638
|
+
return exclusive(async () => {
|
|
639
|
+
const doc = await load(),
|
|
640
|
+
used = await usage(doc);
|
|
641
|
+
return {
|
|
642
|
+
...used,
|
|
643
|
+
limits,
|
|
644
|
+
records: Object.keys(doc.files).length,
|
|
645
|
+
overLimit: used.bytes + used.reservedBytes > limits.totalBytes,
|
|
646
|
+
directory,
|
|
647
|
+
};
|
|
648
|
+
});
|
|
649
|
+
},
|
|
650
|
+
async details(id) {
|
|
651
|
+
return exclusive(async () => {
|
|
652
|
+
const doc = await load();
|
|
653
|
+
return publicRow(id, rowFor(doc, id));
|
|
654
|
+
});
|
|
655
|
+
},
|
|
656
|
+
async verified(id) {
|
|
657
|
+
return exclusive(async () => {
|
|
658
|
+
const doc = await load(),
|
|
659
|
+
r = rowFor(doc, id);
|
|
660
|
+
if (r.state !== "available")
|
|
661
|
+
throw fail(
|
|
662
|
+
"FILE_NOT_AVAILABLE",
|
|
663
|
+
"This file has no complete local copy.",
|
|
664
|
+
);
|
|
665
|
+
try {
|
|
666
|
+
await verify(id, r);
|
|
667
|
+
} catch {
|
|
668
|
+
throw fail(
|
|
669
|
+
"FILE_CACHE_CORRUPT",
|
|
670
|
+
"The saved bytes could not be verified.",
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
return publicRow(id, r);
|
|
674
|
+
});
|
|
675
|
+
},
|
|
676
|
+
async remove({ orderId, fileKey, confirmed }) {
|
|
677
|
+
if (
|
|
678
|
+
!order.test(orderId || "") ||
|
|
679
|
+
typeof fileKey !== "string" ||
|
|
680
|
+
!/^[A-Za-z0-9._-]{1,80}$/.test(fileKey)
|
|
681
|
+
)
|
|
682
|
+
throw fail(
|
|
683
|
+
"FILE_INPUT_INVALID",
|
|
684
|
+
"Supply the original Order ID and file key.",
|
|
685
|
+
);
|
|
686
|
+
if (confirmed !== true)
|
|
687
|
+
throw fail(
|
|
688
|
+
"FILE_CONFIRM_REQUIRED",
|
|
689
|
+
"Confirm removal of this local copy.",
|
|
690
|
+
);
|
|
691
|
+
return exclusive(async () =>
|
|
692
|
+
deleteRow(await load(), recordId({ orderId, fileKey })),
|
|
693
|
+
);
|
|
694
|
+
},
|
|
695
|
+
async cleanPreview() {
|
|
696
|
+
return exclusive(async () => {
|
|
697
|
+
const doc = await load();
|
|
698
|
+
return Object.entries(doc.files)
|
|
699
|
+
.filter(
|
|
700
|
+
([, r]) =>
|
|
701
|
+
!r.protected && ["available", "failed"].includes(r.state),
|
|
702
|
+
)
|
|
703
|
+
.slice(0, 100)
|
|
704
|
+
.map(([id, r]) => ({
|
|
705
|
+
id,
|
|
706
|
+
generation: generation(r),
|
|
707
|
+
filename: r.filename,
|
|
708
|
+
bytes: r.bytes,
|
|
709
|
+
}));
|
|
710
|
+
});
|
|
711
|
+
},
|
|
712
|
+
async clean({ items, confirmed }) {
|
|
713
|
+
if (
|
|
714
|
+
confirmed !== true ||
|
|
715
|
+
!Array.isArray(items) ||
|
|
716
|
+
items.length > 100 ||
|
|
717
|
+
items.some(
|
|
718
|
+
(i) => !i || !hex.test(i.id || "") || !hex.test(i.generation || ""),
|
|
719
|
+
) ||
|
|
720
|
+
new Set(items.map((i) => i.id)).size !== items.length
|
|
721
|
+
)
|
|
722
|
+
throw fail(
|
|
723
|
+
"FILE_CONFIRM_REQUIRED",
|
|
724
|
+
"Confirm the exact preview before cleanup.",
|
|
725
|
+
);
|
|
726
|
+
return exclusive(async () => {
|
|
727
|
+
const doc = await load();
|
|
728
|
+
for (const i of items) {
|
|
729
|
+
const r = rowFor(doc, i.id);
|
|
730
|
+
if (
|
|
731
|
+
r.protected ||
|
|
732
|
+
!["available", "failed"].includes(r.state) ||
|
|
733
|
+
generation(r) !== i.generation
|
|
734
|
+
)
|
|
735
|
+
throw fail(
|
|
736
|
+
"FILE_PREVIEW_CHANGED",
|
|
737
|
+
"Files changed since preview. Review cleanup again.",
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
for (const i of items) await deleteRow(doc, i.id);
|
|
741
|
+
return { removed: items.length };
|
|
742
|
+
});
|
|
743
|
+
},
|
|
744
|
+
// Internal only: caller must first verify this request's durable delivery.
|
|
745
|
+
async releaseInputs(requestId) {
|
|
746
|
+
if (!request.test(requestId || ""))
|
|
747
|
+
throw fail("FILE_INPUT_INVALID", "Invalid original request ID.");
|
|
748
|
+
return exclusive(async () => {
|
|
749
|
+
const doc = await load();
|
|
750
|
+
let changed = 0;
|
|
751
|
+
for (const r of Object.values(doc.files))
|
|
752
|
+
if (
|
|
753
|
+
r.purpose === "input" &&
|
|
754
|
+
r.requestId === requestId &&
|
|
755
|
+
r.protected
|
|
756
|
+
) {
|
|
757
|
+
if (r.state === "downloading")
|
|
758
|
+
throw fail(
|
|
759
|
+
"FILE_BUSY",
|
|
760
|
+
"Input saving is still running. Retry release later.",
|
|
761
|
+
);
|
|
762
|
+
r.protected = false;
|
|
763
|
+
r.updatedAtMs = Date.now();
|
|
764
|
+
changed++;
|
|
765
|
+
}
|
|
766
|
+
if (changed) await persist(doc);
|
|
767
|
+
return { released: changed };
|
|
768
|
+
});
|
|
769
|
+
},
|
|
770
|
+
async settings(overrides) {
|
|
771
|
+
return exclusive(async () => {
|
|
772
|
+
await load();
|
|
773
|
+
if (overrides === undefined) return limits;
|
|
774
|
+
if (
|
|
775
|
+
!overrides ||
|
|
776
|
+
typeof overrides !== "object" ||
|
|
777
|
+
Array.isArray(overrides)
|
|
778
|
+
)
|
|
779
|
+
throw fail("FILE_INPUT_INVALID", "Supply a settings object.");
|
|
780
|
+
const next = validateCacheLimits({ ...limits, ...overrides });
|
|
781
|
+
await atomic(statePath + ".file-cache-settings.json", next);
|
|
782
|
+
limits = next;
|
|
783
|
+
return limits;
|
|
784
|
+
});
|
|
785
|
+
},
|
|
786
|
+
cache: (input) => transfer(input),
|
|
787
|
+
importInput: (input) => transfer(input, true),
|
|
788
|
+
};
|
|
789
|
+
return api;
|
|
790
|
+
}
|