hammerkit 1.24.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/dist/optimizer/get-cache-directory.js +5 -0
- package/dist/planner/work-secret.js +6 -2
- package/dist/program.js +2 -0
- package/dist/remote/remote-commands.js +16 -12
- package/dist/remote/remote-work-commands.js +75 -3
- package/dist/schema/reference-parser.js +8 -2
- package/dist/secrets/provider-values.js +4 -0
- package/dist/server/accounts-api.js +42 -0
- package/dist/server/approvals-api.js +11 -6
- package/dist/server/definitions-api.js +65 -21
- package/dist/server/definitions.js +54 -142
- package/dist/server/executor.js +180 -181
- package/dist/server/git-checkout.js +55 -3
- package/dist/server/plan.js +296 -0
- package/dist/server/policy.js +98 -50
- package/dist/server/proposals-api.js +30 -8
- package/dist/server/proposals.js +50 -3
- 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 +23 -5
- 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 +189 -36
- package/dist/server/server-command.js +18 -0
- package/dist/server/server-config.js +203 -42
- package/dist/server/server.js +148 -52
- package/dist/server/sources-api.js +1 -1
- package/package.json +1 -1
package/dist/server/server.js
CHANGED
|
@@ -8,8 +8,14 @@ const server_config_1 = require("./server-config");
|
|
|
8
8
|
const discovery_1 = require("./discovery");
|
|
9
9
|
const oauth_1 = require("./oauth");
|
|
10
10
|
const policy_1 = require("./policy");
|
|
11
|
+
const run_store_1 = require("./run-store");
|
|
11
12
|
const runs_1 = require("./runs");
|
|
13
|
+
const git_checkout_1 = require("./git-checkout");
|
|
14
|
+
const repo_access_1 = require("./repo-access");
|
|
12
15
|
const executor_1 = require("./executor");
|
|
16
|
+
const run_plan_1 = require("./run-plan");
|
|
17
|
+
const run_environment_1 = require("./run-environment");
|
|
18
|
+
const accounts_api_1 = require("./accounts-api");
|
|
13
19
|
const run_request_1 = require("./run-request");
|
|
14
20
|
const http_2 = require("./http");
|
|
15
21
|
const definitions_1 = require("./definitions");
|
|
@@ -43,7 +49,7 @@ exports.getDiscoveryDocument = getDiscoveryDocument;
|
|
|
43
49
|
const HEARTBEAT_MS = 15000;
|
|
44
50
|
// Server-sent events: `id` is the sequence number, so a client that lost the
|
|
45
51
|
// connection continues with Last-Event-ID (or ?after=) and misses nothing.
|
|
46
|
-
function streamEvents(registry, principal, id, req, res, url) {
|
|
52
|
+
function streamEvents(registry, config, principal, id, req, res, url) {
|
|
47
53
|
const after = Number(req.headers['last-event-id'] ?? url.searchParams.get('after') ?? 0);
|
|
48
54
|
const write = (event) => res.write(`id: ${event.seq}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
49
55
|
const watch = registry.watch(principal, id, Number.isFinite(after) && after > 0 ? after : 0, (event) => {
|
|
@@ -51,7 +57,7 @@ function streamEvents(registry, principal, id, req, res, url) {
|
|
|
51
57
|
if (event.type === 'state' && (0, runs_1.isFinal)(event.state)) {
|
|
52
58
|
end();
|
|
53
59
|
}
|
|
54
|
-
});
|
|
60
|
+
}, accountsOf(config, principal));
|
|
55
61
|
if (!watch) {
|
|
56
62
|
(0, http_2.send)(res, 404, { error: 'not found' });
|
|
57
63
|
return;
|
|
@@ -73,7 +79,7 @@ function streamEvents(registry, principal, id, req, res, url) {
|
|
|
73
79
|
watch.stop();
|
|
74
80
|
});
|
|
75
81
|
}
|
|
76
|
-
async function handleRuns(config, registry, definitions, sources, log, principal, req, res, url) {
|
|
82
|
+
async function handleRuns(config, registry, definitions, sources, planning, repos, log, principal, req, res, url) {
|
|
77
83
|
const match = /^\/v1\/runs(?:\/([^/]+)(?:\/(cancel|events))?)?$/.exec(url.pathname);
|
|
78
84
|
if (!match) {
|
|
79
85
|
return false;
|
|
@@ -85,7 +91,7 @@ async function handleRuns(config, registry, definitions, sources, log, principal
|
|
|
85
91
|
(0, http_2.send)(res, 200, { runs: registry.list(principal).map(toRunResponse) });
|
|
86
92
|
}
|
|
87
93
|
else if (method === 'POST') {
|
|
88
|
-
await submitRun(config, registry, definitions, sources, log, principal, req, res);
|
|
94
|
+
await submitRun(config, registry, definitions, sources, planning, repos, log, principal, req, res);
|
|
89
95
|
}
|
|
90
96
|
else {
|
|
91
97
|
(0, http_2.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET, POST' });
|
|
@@ -100,7 +106,7 @@ async function handleRuns(config, registry, definitions, sources, log, principal
|
|
|
100
106
|
(0, http_2.send)(res, 405, { error: 'method not allowed' }, { allow: 'POST' });
|
|
101
107
|
return true;
|
|
102
108
|
}
|
|
103
|
-
const run = registry.cancel(principal, id);
|
|
109
|
+
const run = registry.cancel(principal, id, accountsOf(config, principal));
|
|
104
110
|
log(`cancel iss=${principal.iss} sub=${principal.sub} run=${id} found=${Boolean(run)}`);
|
|
105
111
|
run ? (0, http_2.send)(res, 200, toRunResponse(run)) : (0, http_2.send)(res, 404, { error: 'not found' });
|
|
106
112
|
}
|
|
@@ -108,20 +114,25 @@ async function handleRuns(config, registry, definitions, sources, log, principal
|
|
|
108
114
|
(0, http_2.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET' });
|
|
109
115
|
}
|
|
110
116
|
else if (action === 'events') {
|
|
111
|
-
streamEvents(registry, principal, id, req, res, url);
|
|
117
|
+
streamEvents(registry, config, principal, id, req, res, url);
|
|
112
118
|
}
|
|
113
119
|
else {
|
|
114
|
-
const run = registry.get(principal, id);
|
|
120
|
+
const run = registry.get(principal, id, accountsOf(config, principal));
|
|
115
121
|
run ? (0, http_2.send)(res, 200, toRunResponse(run)) : (0, http_2.send)(res, 404, { error: 'not found' });
|
|
116
122
|
}
|
|
117
123
|
return true;
|
|
118
124
|
}
|
|
125
|
+
// The accounts the caller uses: who approves a run on them follows and stops it.
|
|
126
|
+
function accountsOf(config, principal) {
|
|
127
|
+
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
128
|
+
return (account) => (0, policy_1.canUse)(grant, account);
|
|
129
|
+
}
|
|
119
130
|
function toRunResponse(run) {
|
|
120
131
|
const { owner, ...visible } = run;
|
|
121
132
|
void owner;
|
|
122
133
|
return visible;
|
|
123
134
|
}
|
|
124
|
-
async function submitRun(config, registry, definitions, sources, log, principal, req, res) {
|
|
135
|
+
async function submitRun(config, registry, definitions, sources, planning, repos, log, principal, req, res) {
|
|
125
136
|
const parsed = run_request_1.runRequestSchema.safeParse(await (0, http_2.readJson)(req));
|
|
126
137
|
if (!parsed.success) {
|
|
127
138
|
const issue = parsed.error.issues[0];
|
|
@@ -130,21 +141,27 @@ async function submitRun(config, registry, definitions, sources, log, principal,
|
|
|
130
141
|
}
|
|
131
142
|
const request = parsed.data;
|
|
132
143
|
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
133
|
-
const account = request.account ?? grant.default;
|
|
134
144
|
// what the caller may not do is said plainly: they already know who they are
|
|
135
|
-
if (
|
|
136
|
-
log(`refuse run iss=${principal.iss} sub=${principal.sub}:
|
|
137
|
-
(0, http_2.send)(res, 403, { error:
|
|
145
|
+
if (!(0, policy_1.mayRun)(grant)) {
|
|
146
|
+
log(`refuse run iss=${principal.iss} sub=${principal.sub}: no entitlement`);
|
|
147
|
+
(0, http_2.send)(res, 403, { error: 'you are not entitled to run anything on this server' });
|
|
138
148
|
return;
|
|
139
149
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
(0, http_2.send)(res,
|
|
150
|
+
const version = definitions.find(request.definition.name, request.definition.hash);
|
|
151
|
+
if (!version) {
|
|
152
|
+
(0, http_2.send)(res, 400, { error: `there is no build file ${request.definition.name} at ${request.definition.hash}` });
|
|
143
153
|
return;
|
|
144
154
|
}
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
155
|
+
if (version.yanked) {
|
|
156
|
+
(0, http_2.send)(res, 409, {
|
|
157
|
+
error: `${version.name} at ${version.hash} was yanked${version.yanked.reason ? `: ${version.yanked.reason}` : ''}`,
|
|
158
|
+
});
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const known = [...version.tasks, ...version.services];
|
|
162
|
+
const unknown = request.tasks.filter((task) => !known.includes(task));
|
|
163
|
+
if (unknown.length > 0) {
|
|
164
|
+
(0, http_2.send)(res, 400, { error: `${version.name} has no task ${unknown.map(oauth_1.quote).join(', ')}` });
|
|
148
165
|
return;
|
|
149
166
|
}
|
|
150
167
|
if (request.source.type === 'upload') {
|
|
@@ -163,33 +180,72 @@ async function submitRun(config, registry, definitions, sources, log, principal,
|
|
|
163
180
|
return;
|
|
164
181
|
}
|
|
165
182
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
183
|
+
// the accounts of a run are read from the files it is pinned to, with the engine
|
|
184
|
+
const bundle = version.plan ? await definitions.files(version) : undefined;
|
|
185
|
+
if (!version.plan || !bundle) {
|
|
186
|
+
(0, http_2.send)(res, 409, {
|
|
187
|
+
error: `${version.name} at ${version.hash} was published before accounts were recorded, publish it again`,
|
|
188
|
+
});
|
|
169
189
|
return;
|
|
170
190
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
191
|
+
let planned;
|
|
192
|
+
try {
|
|
193
|
+
planned = await (0, run_plan_1.planRun)(bundle, request, planning.environment, planning.policy);
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
(0, http_2.send)(res, 400, { error: `the run does not load: ${e instanceof Error ? e.message : String(e)}` });
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const decision = (0, policy_1.decideRun)({
|
|
200
|
+
grant,
|
|
201
|
+
accounts: planned.accounts,
|
|
202
|
+
catalog: config.accounts,
|
|
203
|
+
source: request.source.type === 'ref'
|
|
204
|
+
? { type: 'ref', repo: request.source.repo, ref: request.source.ref }
|
|
205
|
+
: { type: 'upload' },
|
|
206
|
+
approvable: (account) => config.entitlements.some((entitlement) => entitlement.accounts[account] === 'use'),
|
|
207
|
+
readable: (repo) => repos.readable(repo),
|
|
208
|
+
});
|
|
209
|
+
if (decision.type === 'refuse') {
|
|
210
|
+
log(`refuse run iss=${principal.iss} sub=${principal.sub}: ${decision.error}`);
|
|
211
|
+
(0, http_2.send)(res, decision.status, { error: decision.error });
|
|
175
212
|
return;
|
|
176
213
|
}
|
|
177
|
-
|
|
178
|
-
|
|
214
|
+
const approvals = decision.type === 'approval' ? decision.accounts : [];
|
|
215
|
+
// the ref is pinned now: the approver sees the commit, and the run executes that one
|
|
216
|
+
let source;
|
|
217
|
+
if (request.source.type === 'ref') {
|
|
218
|
+
try {
|
|
219
|
+
const commit = await repos.resolve(request.source.repo, request.source.ref);
|
|
220
|
+
source = { type: 'ref', repo: request.source.repo, ref: request.source.ref, commit };
|
|
221
|
+
}
|
|
222
|
+
catch (e) {
|
|
223
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
224
|
+
log(`refuse run iss=${principal.iss} sub=${principal.sub}: ${message}`);
|
|
225
|
+
(0, http_2.send)(res, e instanceof git_checkout_1.RefError ? 400 : 502, {
|
|
226
|
+
error: e instanceof git_checkout_1.RefError ? message : `could not resolve ${request.source.ref}: ${message}`,
|
|
227
|
+
});
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
source = request.source;
|
|
233
|
+
}
|
|
179
234
|
try {
|
|
180
235
|
const run = registry.submit(principal, {
|
|
181
236
|
definition: { name: version.name, hash: version.hash },
|
|
182
237
|
tasks: request.tasks,
|
|
183
|
-
|
|
184
|
-
|
|
238
|
+
accounts: planned.accounts,
|
|
239
|
+
plan: planned.plan,
|
|
240
|
+
source,
|
|
185
241
|
options: request.options,
|
|
186
|
-
},
|
|
187
|
-
log(`run ${run.id} iss=${principal.iss} sub=${principal.sub}
|
|
242
|
+
}, approvals);
|
|
243
|
+
log(`run ${run.id} iss=${principal.iss} sub=${principal.sub} accounts=[${planned.accounts.join(',')}] source=${run.source.type} tasks=${run.tasks.length}${approvals.length > 0 ? ` approval=[${approvals.join(',')}]` : ''}`);
|
|
188
244
|
(0, http_2.send)(res, 202, toRunResponse(run));
|
|
189
245
|
}
|
|
190
246
|
catch (e) {
|
|
191
247
|
if (e instanceof runs_1.RunError) {
|
|
192
|
-
(0, http_2.send)(res,
|
|
248
|
+
(0, http_2.send)(res, e.status, { error: e.message });
|
|
193
249
|
return;
|
|
194
250
|
}
|
|
195
251
|
throw e;
|
|
@@ -215,7 +271,7 @@ async function authenticate(verifier, log, req, res) {
|
|
|
215
271
|
return null;
|
|
216
272
|
}
|
|
217
273
|
}
|
|
218
|
-
async function handle(config, registry, definitions, proposals, sources, verifier, log, req, res) {
|
|
274
|
+
async function handle(config, registry, definitions, proposals, sources, planning, repos, verifier, log, req, res) {
|
|
219
275
|
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
220
276
|
const path = url.pathname;
|
|
221
277
|
if (path === discovery_1.DISCOVERY_PATH || path === '/healthz') {
|
|
@@ -230,18 +286,7 @@ async function handle(config, registry, definitions, proposals, sources, verifie
|
|
|
230
286
|
if (!principal) {
|
|
231
287
|
return;
|
|
232
288
|
}
|
|
233
|
-
if (
|
|
234
|
-
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
235
|
-
log(`grant iss=${principal.iss} sub=${principal.sub} accounts=[${grant.accounts.join(',')}] entitlements=[${grant.matched.join(',')}]`);
|
|
236
|
-
(0, http_2.send)(res, 200, {
|
|
237
|
-
iss: principal.iss,
|
|
238
|
-
sub: principal.sub,
|
|
239
|
-
accounts: grant.accounts,
|
|
240
|
-
default: grant.default,
|
|
241
|
-
request: grant.request,
|
|
242
|
-
publish: grant.publish,
|
|
243
|
-
propose: grant.propose,
|
|
244
|
-
});
|
|
289
|
+
if (await (0, accounts_api_1.handleAccounts)(config, log, principal, req, res, url)) {
|
|
245
290
|
return;
|
|
246
291
|
}
|
|
247
292
|
if (await (0, definitions_api_1.handleDefinitions)(config, definitions, proposals, log, principal, req, res, url)) {
|
|
@@ -250,7 +295,7 @@ async function handle(config, registry, definitions, proposals, sources, verifie
|
|
|
250
295
|
if (await (0, sources_api_1.handleSources)(config, sources, log, principal, req, res, url)) {
|
|
251
296
|
return;
|
|
252
297
|
}
|
|
253
|
-
if (await handleRuns(config, registry, definitions, sources, log, principal, req, res, url)) {
|
|
298
|
+
if (await handleRuns(config, registry, definitions, sources, planning, repos, log, principal, req, res, url)) {
|
|
254
299
|
return;
|
|
255
300
|
}
|
|
256
301
|
if (await (0, approvals_api_1.handleApprovals)(config, registry, log, principal, req, res, url)) {
|
|
@@ -262,33 +307,64 @@ async function handle(config, registry, definitions, proposals, sources, verifie
|
|
|
262
307
|
(0, http_2.send)(res, 404, { error: 'not found' });
|
|
263
308
|
}
|
|
264
309
|
async function startServer(config, environment, options = {}) {
|
|
310
|
+
// tasks find their tools through PATH, a server that hides it would fail every run
|
|
311
|
+
if (!config.runs.env.allow.some((name) => name.toUpperCase() === 'PATH') && !('PATH' in config.runs.env.set)) {
|
|
312
|
+
throw new Error('runs.env.allow has no PATH, tasks could not find their tools: add PATH (and on Windows SystemRoot, ComSpec, PATHEXT and TEMP)');
|
|
313
|
+
}
|
|
265
314
|
const verifier = new oauth_1.TokenVerifier(config.issuers, options);
|
|
266
315
|
const write = options.log ?? (() => undefined);
|
|
267
316
|
// request urls and subjects come from callers: no control characters, so no forged log lines
|
|
268
317
|
const log = (line) => write(line.replace(/\p{Cc}/gu, '?').slice(0, 1000));
|
|
269
|
-
|
|
318
|
+
if (config.runtimes.includes('local')) {
|
|
319
|
+
log('warning: the local runtime is allowed, local tasks run as the user of this server and read all it can read; ' +
|
|
320
|
+
'an account is then an approval class, not a credential boundary');
|
|
321
|
+
}
|
|
322
|
+
// build files are read, planned and run as the server's runs see the world: the
|
|
323
|
+
// operator's accounts and the allowlisted environment, nothing of the server's own
|
|
324
|
+
const planning = {
|
|
325
|
+
environment: {
|
|
326
|
+
...environment,
|
|
327
|
+
processEnvs: (0, run_environment_1.runEnvironment)(config, environment.processEnvs),
|
|
328
|
+
hosted: { catalog: (0, server_config_1.getSecretCatalog)(config) },
|
|
329
|
+
},
|
|
330
|
+
policy: { envs: config.runs.env.allow, reserved: (0, server_config_1.getSourceAccounts)(config), runtimes: config.runtimes },
|
|
331
|
+
};
|
|
332
|
+
const storage = config.storage.directory ?? (0, path_1.join)((0, get_cache_directory_1.getHammerkitDirectory)(), 'server');
|
|
333
|
+
const definitions = new definitions_1.DefinitionStore((0, path_1.join)(storage, 'definitions'), planning.environment, {
|
|
334
|
+
policy: planning.policy,
|
|
335
|
+
});
|
|
270
336
|
await definitions.load();
|
|
337
|
+
const repos = new repo_access_1.RepoAccess(config, planning.environment, options.repos);
|
|
271
338
|
const executor = options.executor ??
|
|
272
339
|
new executor_1.HostExecutor({
|
|
273
|
-
config,
|
|
274
340
|
definitions,
|
|
275
|
-
|
|
276
|
-
|
|
341
|
+
planning,
|
|
342
|
+
repos,
|
|
343
|
+
directory: (0, path_1.join)(storage, 'runs'),
|
|
344
|
+
cache: (0, path_1.join)(storage, 'cache'),
|
|
345
|
+
worker: options.worker,
|
|
277
346
|
});
|
|
278
|
-
const sources = new sources_1.SourceStore((0, path_1.join)(
|
|
347
|
+
const sources = new sources_1.SourceStore((0, path_1.join)(storage, 'sources'));
|
|
279
348
|
const registry = new runs_1.RunRegistry({
|
|
280
349
|
executor,
|
|
281
350
|
maxConcurrent: config.runs.maxConcurrent,
|
|
282
351
|
keep: config.runs.keep,
|
|
283
352
|
maxWaiting: config.approvals.maxWaiting,
|
|
284
353
|
approvalTimeout: (0, units_1.parseDuration)(config.approvals.timeout),
|
|
354
|
+
limits: config.runs.limits,
|
|
355
|
+
maxDuration: (0, units_1.parseDuration)(config.runs.maxDuration),
|
|
356
|
+
maxLogBytes: config.runs.maxLogBytes,
|
|
357
|
+
store: new run_store_1.FileRunStore((0, path_1.join)(storage, 'records')),
|
|
358
|
+
log,
|
|
285
359
|
});
|
|
360
|
+
await registry.restore();
|
|
286
361
|
const proposals = new proposals_1.ProposalRegistry({
|
|
287
362
|
maxWaiting: config.approvals.maxWaiting,
|
|
288
363
|
timeout: (0, units_1.parseDuration)(config.approvals.timeout),
|
|
364
|
+
cooldown: (0, units_1.parseDuration)(config.approvals.cooldown),
|
|
289
365
|
});
|
|
290
366
|
const listener = (req, res) => {
|
|
291
|
-
handle(config, registry, definitions, proposals, sources, verifier, log, req, res).catch((e) => {
|
|
367
|
+
handle(config, registry, definitions, proposals, sources, planning, repos, verifier, log, req, res).catch((e) => {
|
|
292
368
|
if (e instanceof http_2.HttpError) {
|
|
293
369
|
(0, http_2.send)(res, e.status, { error: e.message });
|
|
294
370
|
return;
|
|
@@ -324,6 +400,26 @@ async function startServer(config, environment, options = {}) {
|
|
|
324
400
|
const host = address.family === 'IPv6' ? `[${address.address}]` : address.address;
|
|
325
401
|
return {
|
|
326
402
|
url: `${config.tls ? 'https' : 'http'}://${host}:${address.port}`,
|
|
403
|
+
reload: (next) => {
|
|
404
|
+
const readers = (0, server_config_1.getSourceAccounts)(config);
|
|
405
|
+
next.entitlements.forEach((entitlement, index) => {
|
|
406
|
+
const iss = (entitlement.subject ?? entitlement.group ?? entitlement.claims)?.iss;
|
|
407
|
+
if (iss && !config.issuers.some((issuer) => issuer.issuer === iss)) {
|
|
408
|
+
throw new Error(`entitlements.${index}: issuer ${iss} is not accepted by the running server`);
|
|
409
|
+
}
|
|
410
|
+
for (const account of Object.keys(entitlement.accounts)) {
|
|
411
|
+
if (!config.accounts[account] || readers.includes(account)) {
|
|
412
|
+
throw new Error(`entitlements.${index}: the running server has no account ${account} to give`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
const restart = Object.keys(next).filter((key) => key !== 'entitlements' &&
|
|
417
|
+
JSON.stringify(next[key]) !== JSON.stringify(config[key]));
|
|
418
|
+
// every request reads the entitlements of this object
|
|
419
|
+
config.entitlements = next.entitlements;
|
|
420
|
+
log(`reloaded ${next.entitlements.length} entitlements${restart.length > 0 ? `, restart for: ${restart.join(', ')}` : ''}`);
|
|
421
|
+
return { restart };
|
|
422
|
+
},
|
|
327
423
|
close: async () => {
|
|
328
424
|
proposals.close();
|
|
329
425
|
await registry.close();
|
|
@@ -99,7 +99,7 @@ function notFound(res) {
|
|
|
99
99
|
// Sending files is only for callers who may run something from an upload.
|
|
100
100
|
function mustMayUpload(config, principal) {
|
|
101
101
|
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
102
|
-
if (!(0, policy_1.
|
|
102
|
+
if (!(0, policy_1.mayRun)(grant)) {
|
|
103
103
|
throw new sources_1.SourceError(403, 'uploads are not allowed for you');
|
|
104
104
|
}
|
|
105
105
|
}
|
package/package.json
CHANGED