hammerkit 1.23.0 → 1.25.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/build.schema.json +10 -0
- package/dist/cli.js +6 -0
- package/dist/executer/execute-command.js +7 -1
- package/dist/optimizer/get-cache-directory.js +5 -0
- package/dist/planner/utils/append-work-task.js +1 -0
- package/dist/planner/work-secret.js +6 -2
- package/dist/program.js +6 -0
- package/dist/remote/bundle.js +59 -0
- package/dist/remote/collect-bundle.js +41 -0
- package/dist/remote/credentials.js +93 -0
- package/dist/remote/device-login.js +177 -0
- package/dist/remote/host-config.js +170 -0
- package/dist/remote/location.js +46 -0
- package/dist/remote/remote-commands.js +158 -0
- package/dist/remote/remote-work-commands.js +224 -0
- package/dist/remote/server-client.js +88 -0
- package/dist/remote/source-manifest.js +79 -0
- package/dist/remote/text-diff.js +89 -0
- package/dist/schema/build-file-base-task-schema.js +4 -0
- package/dist/schema/build-file-schema.js +3 -0
- package/dist/schema/reference-parser.js +9 -2
- package/dist/schema/runner-schema.js +13 -0
- package/dist/secrets/provider-values.js +4 -0
- package/dist/server/accounts-api.js +42 -0
- package/dist/server/approvals-api.js +104 -0
- package/dist/server/definitions-api.js +163 -0
- package/dist/server/definitions.js +160 -0
- package/dist/server/discovery.js +9 -0
- package/dist/server/executor.js +237 -0
- package/dist/server/git-checkout.js +115 -0
- package/dist/server/http.js +39 -0
- package/dist/server/oauth.js +226 -0
- package/dist/server/plan.js +296 -0
- package/dist/server/policy.js +138 -0
- package/dist/server/proposals-api.js +145 -0
- package/dist/server/proposals.js +173 -0
- package/dist/server/repo-access.js +127 -0
- package/dist/server/repo.js +94 -0
- package/dist/server/run-environment.js +20 -0
- package/dist/server/run-plan.js +44 -0
- package/dist/server/run-request.js +85 -0
- package/dist/server/run-store.js +61 -0
- package/dist/server/run-worker-command.js +17 -0
- package/dist/server/run-worker-main.js +7 -0
- package/dist/server/run-worker.js +179 -0
- package/dist/server/runs.js +432 -0
- package/dist/server/server-command.js +61 -0
- package/dist/server/server-config.js +394 -0
- package/dist/server/server.js +434 -0
- package/dist/server/sources-api.js +106 -0
- package/dist/server/sources.js +258 -0
- package/package.json +1 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SourceStore = exports.subjectKey = exports.DEFAULT_SOURCE_LIMITS = exports.SourceError = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const promises_1 = require("fs/promises");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
const crypto_2 = require("crypto");
|
|
9
|
+
const bundle_1 = require("../remote/bundle");
|
|
10
|
+
const source_manifest_1 = require("../remote/source-manifest");
|
|
11
|
+
class SourceError extends Error {
|
|
12
|
+
constructor(status, message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.status = status;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.SourceError = SourceError;
|
|
18
|
+
exports.DEFAULT_SOURCE_LIMITS = { subjectBytes: 4 * 1024 * 1024 * 1024 };
|
|
19
|
+
// The directory name of a caller: the issuer and subject, hashed.
|
|
20
|
+
function subjectKey(principal) {
|
|
21
|
+
return (0, crypto_1.createHash)('sha256')
|
|
22
|
+
.update(JSON.stringify([principal.iss, principal.sub]))
|
|
23
|
+
.digest('hex')
|
|
24
|
+
.slice(0, 32);
|
|
25
|
+
}
|
|
26
|
+
exports.subjectKey = subjectKey;
|
|
27
|
+
/**
|
|
28
|
+
* The project files that runs on this server work on, kept per caller:
|
|
29
|
+
*
|
|
30
|
+
* <directory>/<caller>/blobs/<sha-256> content, once per digest
|
|
31
|
+
* <directory>/<caller>/manifests/<hash>.json the trees that were announced
|
|
32
|
+
*
|
|
33
|
+
* Content is shared between the manifests of one caller and never between
|
|
34
|
+
* callers: a server that answers "I have this file" to anybody would tell them
|
|
35
|
+
* what other people work on.
|
|
36
|
+
*/
|
|
37
|
+
class SourceStore {
|
|
38
|
+
constructor(directory, limits = exports.DEFAULT_SOURCE_LIMITS, now = Date.now) {
|
|
39
|
+
this.directory = directory;
|
|
40
|
+
this.limits = limits;
|
|
41
|
+
this.now = now;
|
|
42
|
+
this.subjects = new Map();
|
|
43
|
+
}
|
|
44
|
+
// Announces a tree. Answers what has to be sent to complete it.
|
|
45
|
+
async announce(principal, manifest) {
|
|
46
|
+
const denied = manifest.files
|
|
47
|
+
.map((file) => ({ path: file.path, reason: (0, source_manifest_1.deniedSourcePath)(file.path) }))
|
|
48
|
+
.filter((entry) => entry.reason !== null);
|
|
49
|
+
if (denied.length > 0) {
|
|
50
|
+
const shown = denied
|
|
51
|
+
.slice(0, 5)
|
|
52
|
+
.map((entry) => `${entry.path} (${entry.reason})`)
|
|
53
|
+
.join(', ');
|
|
54
|
+
throw new SourceError(400, `${denied.length} file${denied.length === 1 ? '' : 's'} may not be sent: ${shown}${denied.length > 5 ? ', ...' : ''}`);
|
|
55
|
+
}
|
|
56
|
+
const key = subjectKey(principal);
|
|
57
|
+
const subject = await this.subject(key);
|
|
58
|
+
const snapshot = (0, source_manifest_1.hashSource)(manifest);
|
|
59
|
+
const existing = subject.manifests.get(snapshot);
|
|
60
|
+
if (existing) {
|
|
61
|
+
return { status: await this.statusOf(key, snapshot, existing), created: false };
|
|
62
|
+
}
|
|
63
|
+
// one digest has one size
|
|
64
|
+
const sizes = new Map();
|
|
65
|
+
for (const file of manifest.files) {
|
|
66
|
+
const known = sizes.get(file.sha256) ?? subject.wanted.get(file.sha256);
|
|
67
|
+
if (known !== undefined && known !== file.size) {
|
|
68
|
+
throw new SourceError(400, `${file.path}: ${file.sha256} is announced with two sizes`);
|
|
69
|
+
}
|
|
70
|
+
sizes.set(file.sha256, file.size);
|
|
71
|
+
}
|
|
72
|
+
const stored = { manifest, createdAt: new Date(this.now()).toISOString() };
|
|
73
|
+
await (0, promises_1.mkdir)((0, path_1.join)(this.directory, key, 'manifests'), { recursive: true });
|
|
74
|
+
await this.write((0, path_1.join)(this.directory, key, 'manifests', `${snapshot.slice('sha256:'.length)}.json`), stored);
|
|
75
|
+
subject.manifests.set(snapshot, stored);
|
|
76
|
+
for (const [digest, size] of sizes) {
|
|
77
|
+
subject.wanted.set(digest, size);
|
|
78
|
+
}
|
|
79
|
+
return { status: await this.statusOf(key, snapshot, stored), created: true };
|
|
80
|
+
}
|
|
81
|
+
// Keeps the content of one file, once the digest of what arrives is the one
|
|
82
|
+
// that was announced. Only content of an announced tree is taken.
|
|
83
|
+
async put(principal, digest, body) {
|
|
84
|
+
if (!source_manifest_1.DIGEST_PATTERN.test(digest)) {
|
|
85
|
+
throw new SourceError(404, 'not found');
|
|
86
|
+
}
|
|
87
|
+
const key = subjectKey(principal);
|
|
88
|
+
const subject = await this.subject(key);
|
|
89
|
+
const size = subject.wanted.get(digest);
|
|
90
|
+
if (size === undefined) {
|
|
91
|
+
throw new SourceError(400, 'this file is not part of a tree you announced');
|
|
92
|
+
}
|
|
93
|
+
const target = this.blob(key, digest);
|
|
94
|
+
if (await exists(target)) {
|
|
95
|
+
body.resume();
|
|
96
|
+
return 'exists';
|
|
97
|
+
}
|
|
98
|
+
if (subject.stored + size > this.limits.subjectBytes) {
|
|
99
|
+
body.resume();
|
|
100
|
+
throw new SourceError(413, `the server keeps at most ${this.limits.subjectBytes} bytes of files for you`);
|
|
101
|
+
}
|
|
102
|
+
await (0, promises_1.mkdir)((0, path_1.join)(this.directory, key, 'blobs'), { recursive: true });
|
|
103
|
+
const temporary = `${target}.${(0, crypto_2.randomBytes)(8).toString('hex')}.tmp`;
|
|
104
|
+
const hash = (0, crypto_1.createHash)('sha256');
|
|
105
|
+
let received = 0;
|
|
106
|
+
const out = (0, fs_1.createWriteStream)(temporary, { flags: 'wx' });
|
|
107
|
+
try {
|
|
108
|
+
for await (const chunk of body) {
|
|
109
|
+
received += chunk.length;
|
|
110
|
+
if (received > size) {
|
|
111
|
+
throw new SourceError(400, `the file is larger than the ${size} bytes that were announced`);
|
|
112
|
+
}
|
|
113
|
+
hash.update(chunk);
|
|
114
|
+
if (!out.write(chunk)) {
|
|
115
|
+
await new Promise((resolve) => out.once('drain', () => resolve()));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
await new Promise((resolve, reject) => {
|
|
119
|
+
out.once('error', reject);
|
|
120
|
+
out.end(resolve);
|
|
121
|
+
});
|
|
122
|
+
if (received !== size) {
|
|
123
|
+
throw new SourceError(400, `the file has ${received} bytes, ${size} were announced`);
|
|
124
|
+
}
|
|
125
|
+
if (hash.digest('hex') !== digest) {
|
|
126
|
+
throw new SourceError(400, 'the content does not have the digest it was sent for');
|
|
127
|
+
}
|
|
128
|
+
await (0, promises_1.rename)(temporary, target);
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
out.destroy();
|
|
132
|
+
await (0, promises_1.rm)(temporary, { force: true });
|
|
133
|
+
throw e;
|
|
134
|
+
}
|
|
135
|
+
subject.stored += size;
|
|
136
|
+
return 'created';
|
|
137
|
+
}
|
|
138
|
+
async get(principal, snapshot) {
|
|
139
|
+
if (!bundle_1.HASH_PATTERN.test(snapshot)) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
const key = subjectKey(principal);
|
|
143
|
+
const subject = await this.subject(key);
|
|
144
|
+
const stored = subject.manifests.get(snapshot);
|
|
145
|
+
return stored ? this.statusOf(key, snapshot, stored) : undefined;
|
|
146
|
+
}
|
|
147
|
+
async list(principal) {
|
|
148
|
+
const subject = await this.subject(subjectKey(principal));
|
|
149
|
+
const snapshots = [...subject.manifests.entries()]
|
|
150
|
+
.map(([snapshot, stored]) => ({
|
|
151
|
+
snapshot,
|
|
152
|
+
files: stored.manifest.files.length,
|
|
153
|
+
bytes: stored.manifest.files.reduce((sum, file) => sum + file.size, 0),
|
|
154
|
+
createdAt: stored.createdAt,
|
|
155
|
+
}))
|
|
156
|
+
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
157
|
+
return { snapshots, bytes: subject.stored, limit: this.limits.subjectBytes };
|
|
158
|
+
}
|
|
159
|
+
// Forgets a tree and the content nothing else of the caller's needs.
|
|
160
|
+
async remove(principal, snapshot) {
|
|
161
|
+
if (!bundle_1.HASH_PATTERN.test(snapshot)) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
const key = subjectKey(principal);
|
|
165
|
+
const subject = await this.subject(key);
|
|
166
|
+
if (!subject.manifests.delete(snapshot)) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
await (0, promises_1.rm)((0, path_1.join)(this.directory, key, 'manifests', `${snapshot.slice('sha256:'.length)}.json`), { force: true });
|
|
170
|
+
const kept = new Map();
|
|
171
|
+
for (const stored of subject.manifests.values()) {
|
|
172
|
+
for (const file of stored.manifest.files) {
|
|
173
|
+
kept.set(file.sha256, file.size);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
for (const [digest, size] of subject.wanted) {
|
|
177
|
+
if (!kept.has(digest)) {
|
|
178
|
+
subject.wanted.delete(digest);
|
|
179
|
+
if (await exists(this.blob(key, digest))) {
|
|
180
|
+
await (0, promises_1.rm)(this.blob(key, digest), { force: true });
|
|
181
|
+
subject.stored -= size;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
async statusOf(key, snapshot, stored) {
|
|
188
|
+
const missing = [];
|
|
189
|
+
for (const digest of new Set(stored.manifest.files.map((file) => file.sha256))) {
|
|
190
|
+
if (!(await exists(this.blob(key, digest)))) {
|
|
191
|
+
missing.push(digest);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
snapshot,
|
|
196
|
+
files: stored.manifest.files.length,
|
|
197
|
+
bytes: stored.manifest.files.reduce((sum, file) => sum + file.size, 0),
|
|
198
|
+
createdAt: stored.createdAt,
|
|
199
|
+
missing,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
blob(key, digest) {
|
|
203
|
+
return (0, path_1.join)(this.directory, key, 'blobs', digest);
|
|
204
|
+
}
|
|
205
|
+
async write(file, value) {
|
|
206
|
+
const temporary = `${file}.${(0, crypto_2.randomBytes)(8).toString('hex')}.tmp`;
|
|
207
|
+
await (0, promises_1.writeFile)(temporary, JSON.stringify(value), { flag: 'wx' });
|
|
208
|
+
await (0, promises_1.rename)(temporary, file);
|
|
209
|
+
}
|
|
210
|
+
// Reads what is kept for one caller the first time they are seen.
|
|
211
|
+
subject(key) {
|
|
212
|
+
let loaded = this.subjects.get(key);
|
|
213
|
+
if (!loaded) {
|
|
214
|
+
loaded = this.load(key);
|
|
215
|
+
this.subjects.set(key, loaded);
|
|
216
|
+
}
|
|
217
|
+
return loaded;
|
|
218
|
+
}
|
|
219
|
+
async load(key) {
|
|
220
|
+
const subject = { wanted: new Map(), stored: 0, manifests: new Map() };
|
|
221
|
+
const manifests = (0, path_1.join)(this.directory, key, 'manifests');
|
|
222
|
+
let names = [];
|
|
223
|
+
try {
|
|
224
|
+
names = await (0, promises_1.readdir)(manifests);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return subject;
|
|
228
|
+
}
|
|
229
|
+
for (const name of names.filter((n) => n.endsWith('.json'))) {
|
|
230
|
+
const stored = JSON.parse(await (0, promises_1.readFile)((0, path_1.join)(manifests, name), 'utf8'));
|
|
231
|
+
subject.manifests.set((0, source_manifest_1.hashSource)(stored.manifest), stored);
|
|
232
|
+
for (const file of stored.manifest.files) {
|
|
233
|
+
subject.wanted.set(file.sha256, file.size);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
for (const [digest, size] of subject.wanted) {
|
|
237
|
+
try {
|
|
238
|
+
await (0, promises_1.stat)(this.blob(key, digest));
|
|
239
|
+
subject.stored += size;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// announced, not sent yet
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return subject;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
exports.SourceStore = SourceStore;
|
|
249
|
+
async function exists(file) {
|
|
250
|
+
try {
|
|
251
|
+
await (0, promises_1.access)(file);
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
//# sourceMappingURL=sources.js.map
|
package/package.json
CHANGED