hammerkit 1.22.0 → 1.24.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.
Files changed (54) hide show
  1. package/build.schema.json +92 -2
  2. package/dist/cli.js +25 -0
  3. package/dist/executer/environment-mock.js +4 -1
  4. package/dist/executer/execute-command.js +7 -1
  5. package/dist/index.js +4 -1
  6. package/dist/planner/service-definition.js +3 -1
  7. package/dist/planner/utils/append-work-service.js +1 -1
  8. package/dist/planner/utils/append-work-task.js +2 -1
  9. package/dist/planner/work-item-status.js +3 -2
  10. package/dist/planner/work-secret.js +87 -10
  11. package/dist/program.js +4 -0
  12. package/dist/remote/bundle.js +59 -0
  13. package/dist/remote/collect-bundle.js +41 -0
  14. package/dist/remote/credentials.js +93 -0
  15. package/dist/remote/device-login.js +177 -0
  16. package/dist/remote/host-config.js +170 -0
  17. package/dist/remote/location.js +46 -0
  18. package/dist/remote/remote-commands.js +154 -0
  19. package/dist/remote/remote-work-commands.js +152 -0
  20. package/dist/remote/server-client.js +88 -0
  21. package/dist/remote/source-manifest.js +79 -0
  22. package/dist/remote/text-diff.js +89 -0
  23. package/dist/schema/build-file-base-task-schema.js +6 -0
  24. package/dist/schema/build-file-container-service-schema.js +2 -0
  25. package/dist/schema/build-file-schema.js +6 -0
  26. package/dist/schema/build-file-secret-provider-schema.js +36 -0
  27. package/dist/schema/build-file-secret-schema.js +6 -3
  28. package/dist/schema/reference-parser.js +4 -0
  29. package/dist/schema/runner-schema.js +13 -0
  30. package/dist/secrets/command-secret-provider.js +108 -0
  31. package/dist/secrets/prefetch-secrets.js +27 -0
  32. package/dist/secrets/provider-values.js +47 -0
  33. package/dist/secrets/secret-catalog.js +78 -0
  34. package/dist/secrets/secret-provider.js +19 -0
  35. package/dist/server/approvals-api.js +99 -0
  36. package/dist/server/definitions-api.js +119 -0
  37. package/dist/server/definitions.js +248 -0
  38. package/dist/server/discovery.js +9 -0
  39. package/dist/server/executor.js +238 -0
  40. package/dist/server/git-checkout.js +63 -0
  41. package/dist/server/http.js +39 -0
  42. package/dist/server/oauth.js +226 -0
  43. package/dist/server/policy.js +90 -0
  44. package/dist/server/proposals-api.js +123 -0
  45. package/dist/server/proposals.js +126 -0
  46. package/dist/server/run-request.js +67 -0
  47. package/dist/server/runs.js +279 -0
  48. package/dist/server/server-command.js +43 -0
  49. package/dist/server/server-config.js +233 -0
  50. package/dist/server/server.js +338 -0
  51. package/dist/server/sources-api.js +106 -0
  52. package/dist/server/sources.js +258 -0
  53. package/dist/utils/redact.js +75 -19
  54. package/package.json +1 -1
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCommandSecretProvider = void 0;
4
+ const child_process_1 = require("child_process");
5
+ const abort_1 = require("../executer/abort");
6
+ const STDERR_LIMIT = 500;
7
+ // Runs the provider's command and takes its stdout, verbatim, as the value: a
8
+ // key file needs its trailing newline. No shell, so a reference is one argument
9
+ // and never syntax. The command sees PATH, the provider's own `env` and the
10
+ // account's `env` and nothing else of this process: a personal login (`gcloud
11
+ // auth login`, the 1Password app) is never picked up, and a missing credential
12
+ // fails instead of falling back to one.
13
+ function createCommandSecretProvider(binding) {
14
+ const { definition, account } = binding;
15
+ return {
16
+ async resolve(ref, environment) {
17
+ const [command, ...args] = definition.command.map((part) => expand(part, environment, `provider ${definition.name}`).replace(/\{\{ref\}\}/g, () => ref));
18
+ const env = {
19
+ ...minimalEnvironment(environment),
20
+ ...expandAll(definition.env, environment, `provider ${definition.name}`),
21
+ ...expandAll(account.env, environment, `account ${account.name}, provider ${definition.name}`),
22
+ };
23
+ return run(command, args, env, definition.timeoutMs, environment);
24
+ },
25
+ };
26
+ }
27
+ exports.createCommandSecretProvider = createCommandSecretProvider;
28
+ function minimalEnvironment(environment) {
29
+ const names = process.platform === 'win32' ? ['PATH', 'Path', 'PATHEXT', 'SystemRoot'] : ['PATH'];
30
+ const result = {};
31
+ for (const name of names) {
32
+ const value = environment.processEnvs[name];
33
+ if (value !== undefined && value !== null) {
34
+ result[name] = value;
35
+ }
36
+ }
37
+ return result;
38
+ }
39
+ function expandAll(values, environment, owner) {
40
+ return Object.fromEntries(Object.entries(values).map(([key, value]) => [key, expand(value, environment, owner)]));
41
+ }
42
+ // `${NAME}` from the process environment; one that is not set stops the secret
43
+ // before the command runs.
44
+ function expand(value, environment, owner) {
45
+ return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => {
46
+ const resolved = environment.processEnvs[name];
47
+ if (resolved === undefined || resolved === null || resolved === '') {
48
+ throw new Error(`${owner}: ${name} is not set`);
49
+ }
50
+ return resolved;
51
+ });
52
+ }
53
+ function run(command, args, env, timeoutMs, environment) {
54
+ return new Promise((resolve, reject) => {
55
+ const signal = environment.abortCtrl.signal;
56
+ if (signal.aborted) {
57
+ reject(new abort_1.AbortError());
58
+ return;
59
+ }
60
+ const child = (0, child_process_1.spawn)(command, args, { env, shell: false, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
61
+ const stdout = [];
62
+ const stderr = [];
63
+ let failure = null;
64
+ const finish = (error, value) => {
65
+ clearTimeout(timer);
66
+ signal.removeEventListener('abort', onAbort);
67
+ if (error) {
68
+ reject(error);
69
+ }
70
+ else {
71
+ resolve(value ?? '');
72
+ }
73
+ };
74
+ const stop = (error) => {
75
+ failure = failure ?? error;
76
+ child.kill('SIGKILL');
77
+ };
78
+ const onAbort = () => stop(new abort_1.AbortError());
79
+ const timer = setTimeout(() => stop(new Error(`${command} did not finish within ${timeoutMs}ms`)), timeoutMs);
80
+ signal.addEventListener('abort', onAbort);
81
+ child.stdout.on('data', (chunk) => stdout.push(chunk));
82
+ child.stderr.on('data', (chunk) => stderr.push(chunk));
83
+ child.on('error', (error) => {
84
+ finish(error.code === 'ENOENT' ? new Error(`${command} was not found on PATH`) : error);
85
+ });
86
+ child.on('close', (code) => {
87
+ if (failure) {
88
+ finish(failure);
89
+ }
90
+ else if (code !== 0) {
91
+ finish(new Error(`${command} exited with code ${code}${describeStderr(stderr, environment)}`));
92
+ }
93
+ else {
94
+ finish(null, Buffer.concat(stdout).toString('utf8'));
95
+ }
96
+ });
97
+ });
98
+ }
99
+ // What the CLI said, as the first lines of a failure; its own output may hold a
100
+ // value it was asked for, so it is masked like all output.
101
+ function describeStderr(chunks, environment) {
102
+ const text = environment.secrets.redact(Buffer.concat(chunks).toString('utf8').trim());
103
+ if (text.length === 0) {
104
+ return '';
105
+ }
106
+ return `: ${text.length > STDERR_LIMIT ? `${text.substring(0, STDERR_LIMIT)}…` : text}`;
107
+ }
108
+ //# sourceMappingURL=command-secret-provider.js.map
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prefetchSecrets = void 0;
4
+ const plan_work_tasks_1 = require("../planner/utils/plan-work-tasks");
5
+ const work_secret_1 = require("../planner/work-secret");
6
+ const provider_values_1 = require("./provider-values");
7
+ // Fetches the values a task id needs; a run drops the ones of the previous run
8
+ // first (`newRun`), any other command reuses what the run already fetched. A task id is computed synchronously and holds the
9
+ // digest of each `cache: true` secret, so those values must be in memory before
10
+ // anything asks for an id. Secrets that don't affect the id are read when their
11
+ // task starts, and a plan without provider secrets calls no provider.
12
+ async function prefetchSecrets(workTree, environment, newRun = false) {
13
+ if (newRun) {
14
+ (0, provider_values_1.beginSecretRun)(environment);
15
+ }
16
+ const fetches = [];
17
+ for (const task of (0, plan_work_tasks_1.iterateWorkTasks)(workTree)) {
18
+ for (const secret of task.data.secrets) {
19
+ if (secret.cache && secret.source.type === 'provider') {
20
+ fetches.push((0, work_secret_1.resolveSecretValue)(secret, environment));
21
+ }
22
+ }
23
+ }
24
+ await Promise.all(fetches);
25
+ }
26
+ exports.prefetchSecrets = prefetchSecrets;
27
+ //# sourceMappingURL=prefetch-secrets.js.map
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.peekProviderValue = exports.fetchProviderValue = exports.beginSecretRun = void 0;
4
+ const secret_provider_1 = require("./secret-provider");
5
+ // The values fetched in the current run, by provider, account and reference.
6
+ // Concurrent requests share one fetch: a secret manager is rate limited (a
7
+ // 1Password service account per token) and several tasks read the same value.
8
+ // Held in memory only, never written anywhere, and dropped when the next run
9
+ // begins so a rotated value is picked up by it.
10
+ const runs = new WeakMap();
11
+ function beginSecretRun(environment) {
12
+ runs.set(environment, new Map());
13
+ }
14
+ exports.beginSecretRun = beginSecretRun;
15
+ function keyOf(binding, ref) {
16
+ return [binding.definition.name, binding.account.name, ref].join('\0');
17
+ }
18
+ function fetchProviderValue(binding, ref, environment) {
19
+ let values = runs.get(environment);
20
+ if (!values) {
21
+ values = new Map();
22
+ runs.set(environment, values);
23
+ }
24
+ const key = keyOf(binding, ref);
25
+ let fetched = values.get(key);
26
+ if (!fetched) {
27
+ environment.status
28
+ .context({ type: 'cli', name: 'hammerkit' })
29
+ .write('debug', `secret ${ref} via ${binding.definition.name}/${binding.account.name}`);
30
+ const entry = { promise: (0, secret_provider_1.createSecretProvider)(binding).resolve(ref, environment) };
31
+ entry.promise = entry.promise.then((value) => {
32
+ entry.value = value;
33
+ return value;
34
+ });
35
+ fetched = entry;
36
+ values.set(key, fetched);
37
+ }
38
+ return fetched.promise;
39
+ }
40
+ exports.fetchProviderValue = fetchProviderValue;
41
+ // The value of a fetch that has finished in this run, for the sync part of
42
+ // planning (a task id); undefined when it was not fetched, see prefetchSecrets.
43
+ function peekProviderValue(binding, ref, environment) {
44
+ return runs.get(environment)?.get(keyOf(binding, ref))?.value;
45
+ }
46
+ exports.peekProviderValue = peekProviderValue;
47
+ //# sourceMappingURL=provider-values.js.map
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.collectSecretCatalog = exports.emptySecretCatalog = void 0;
4
+ const merge_environment_variables_1 = require("../environment/merge-environment-variables");
5
+ const replace_env_variables_1 = require("../environment/replace-env-variables");
6
+ const units_1 = require("../utils/units");
7
+ const DEFAULT_TIMEOUT = '30s';
8
+ function emptySecretCatalog() {
9
+ return { providers: {}, accounts: {} };
10
+ }
11
+ exports.emptySecretCatalog = emptySecretCatalog;
12
+ const FULL_COMMIT = /^[0-9a-f]{40}$/i;
13
+ // Providers and accounts are global names, like caches, collected from every
14
+ // loaded build file. A name declared twice is an error rather than a silent
15
+ // override: which definition runs with a service account's credential must not
16
+ // depend on include order.
17
+ function collectSecretCatalog(files, environment, context) {
18
+ const catalog = emptySecretCatalog();
19
+ for (const file of files) {
20
+ const providers = Object.entries(file.schema.secretProviders ?? {});
21
+ const accounts = Object.entries(file.schema.secretAccounts ?? {});
22
+ if (providers.length === 0 && accounts.length === 0) {
23
+ continue;
24
+ }
25
+ requirePinnedSource(file);
26
+ const envs = (0, replace_env_variables_1.buildEnvironmentVariables)((0, merge_environment_variables_1.mergeEnvironmentVariables)(file.schema.envs, null), environment, context).variables;
27
+ for (const [name, provider] of providers) {
28
+ if (catalog.providers[name]) {
29
+ throw new Error(`secret provider ${name} is declared in ${catalog.providers[name].declaredIn} and in ${file.fileName}`);
30
+ }
31
+ catalog.providers[name] = {
32
+ name,
33
+ type: 'command',
34
+ command: provider.command.map((part) => applyEnvs(part, envs)),
35
+ env: provider.env ?? {},
36
+ timeoutMs: (0, units_1.parseDuration)(provider.timeout ?? DEFAULT_TIMEOUT),
37
+ declaredIn: file.fileName,
38
+ };
39
+ }
40
+ for (const [name, account] of accounts) {
41
+ if (catalog.accounts[name]) {
42
+ throw new Error(`secret account ${name} is declared in ${catalog.accounts[name].declaredIn} and in ${file.fileName}`);
43
+ }
44
+ catalog.accounts[name] = {
45
+ name,
46
+ default: account.default ?? false,
47
+ providers: account.providers,
48
+ declaredIn: file.fileName,
49
+ };
50
+ }
51
+ }
52
+ const defaults = Object.values(catalog.accounts).filter((account) => account.default);
53
+ if (defaults.length > 1) {
54
+ throw new Error(`more than one default secret account: ${defaults.map((a) => a.name).join(', ')}`);
55
+ }
56
+ for (const account of Object.values(catalog.accounts)) {
57
+ for (const provider of Object.keys(account.providers)) {
58
+ if (!catalog.providers[provider]) {
59
+ throw new Error(`secret account ${account.name} (${account.declaredIn}) names provider ${provider}, which is not declared`);
60
+ }
61
+ }
62
+ }
63
+ return catalog;
64
+ }
65
+ exports.collectSecretCatalog = collectSecretCatalog;
66
+ // A tag or branch can be moved, and a moved file runs with the credentials of a
67
+ // service account: providers and accounts from a git include are pinned.
68
+ function requirePinnedSource(file) {
69
+ if (file.remote && !FULL_COMMIT.test(file.remote.ref ?? '')) {
70
+ throw new Error(`${file.fileName} declares secret providers or accounts, so its git include ${file.remote.git} needs ref to be a full commit SHA`);
71
+ }
72
+ }
73
+ // `${NAME}` that the build file's own `envs` define; any other reference stays
74
+ // for the host environment.
75
+ function applyEnvs(value, envs) {
76
+ return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (match, name) => name in envs && !envs[name].includes('$') ? envs[name] : match);
77
+ }
78
+ //# sourceMappingURL=secret-catalog.js.map
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSecretProvider = exports.registerSecretProvider = void 0;
4
+ const command_secret_provider_1 = require("./command-secret-provider");
5
+ const factories = new Map();
6
+ function registerSecretProvider(type, factory) {
7
+ factories.set(type, factory);
8
+ }
9
+ exports.registerSecretProvider = registerSecretProvider;
10
+ function createSecretProvider(binding) {
11
+ const factory = factories.get(binding.definition.type);
12
+ if (!factory) {
13
+ throw new Error(`unknown secret provider type: ${binding.definition.type}`);
14
+ }
15
+ return factory(binding);
16
+ }
17
+ exports.createSecretProvider = createSecretProvider;
18
+ registerSecretProvider('command', command_secret_provider_1.createCommandSecretProvider);
19
+ //# sourceMappingURL=secret-provider.js.map
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleApprovals = void 0;
4
+ const zod_1 = require("zod");
5
+ const http_1 = require("./http");
6
+ const oauth_1 = require("./oauth");
7
+ const policy_1 = require("./policy");
8
+ const runs_1 = require("./runs");
9
+ const decisionSchema = zod_1.z.object({ reason: zod_1.z.string().max(500).optional() }).strict();
10
+ // What an approver is shown of a run: everything that decides whether to let it
11
+ // go ahead, and who asked.
12
+ function toApprovalView(run) {
13
+ return {
14
+ id: run.id,
15
+ state: run.state,
16
+ requestedBy: run.owner,
17
+ createdAt: run.createdAt,
18
+ definition: run.definition,
19
+ tasks: run.tasks,
20
+ account: run.account,
21
+ source: run.source,
22
+ options: run.options,
23
+ approval: run.approval,
24
+ };
25
+ }
26
+ /**
27
+ * The runs waiting for an approval, which an admin of the service account they
28
+ * use gives:
29
+ *
30
+ * GET /v1/approvals the runs the caller may approve
31
+ * GET /v1/approvals/<id> one of them
32
+ * POST /v1/approvals/<id>/approve let it run (body: optional {reason})
33
+ * POST /v1/approvals/<id>/reject end it
34
+ *
35
+ * What the caller may not approve does not exist for them; their own run is the
36
+ * exception, they are told why it is not theirs to decide.
37
+ */
38
+ async function handleApprovals(config, registry, log, principal, req, res, url) {
39
+ const match = /^\/v1\/approvals(?:\/([^/]+)(?:\/(approve|reject))?)?$/.exec(url.pathname);
40
+ if (!match) {
41
+ return false;
42
+ }
43
+ const [, id, action] = match;
44
+ const grant = (0, policy_1.getGrant)(config.entitlements, principal);
45
+ const canDecide = (account) => (0, policy_1.isAdmin)(grant, account);
46
+ if (!id) {
47
+ if (req.method !== 'GET') {
48
+ (0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET' });
49
+ return true;
50
+ }
51
+ const open = registry
52
+ .waiting()
53
+ .filter((run) => !(run.owner.iss === principal.iss && run.owner.sub === principal.sub) && canDecide(run.account));
54
+ (0, http_1.send)(res, 200, { approvals: open.map(toApprovalView) });
55
+ return true;
56
+ }
57
+ if (!runs_1.RUN_ID_PATTERN.test(id)) {
58
+ (0, http_1.send)(res, 404, { error: 'not found' });
59
+ return true;
60
+ }
61
+ if (!action) {
62
+ if (req.method !== 'GET') {
63
+ (0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET' });
64
+ return true;
65
+ }
66
+ const run = registry.getForApproval(id, canDecide);
67
+ run ? (0, http_1.send)(res, 200, toApprovalView(run)) : (0, http_1.send)(res, 404, { error: 'not found' });
68
+ return true;
69
+ }
70
+ if (req.method !== 'POST') {
71
+ (0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'POST' });
72
+ return true;
73
+ }
74
+ const body = decisionSchema.safeParse(req.headers['content-type'] ? await (0, http_1.readJson)(req) : {});
75
+ if (!body.success) {
76
+ const issue = body.error.issues[0];
77
+ (0, http_1.send)(res, 400, { error: `${issue.path.join('.') || 'request'}: ${issue.message}` });
78
+ return true;
79
+ }
80
+ const outcome = action === 'approve' ? 'approved' : 'rejected';
81
+ const result = registry.decide(principal, id, outcome, canDecide, body.data.reason);
82
+ log(`${action} run=${id} iss=${principal.iss} sub=${principal.sub} result=${result.status}${body.data.reason ? ` reason=${(0, oauth_1.quote)(body.data.reason)}` : ''}`);
83
+ switch (result.status) {
84
+ case 'ok':
85
+ (0, http_1.send)(res, 200, toApprovalView(result.run));
86
+ break;
87
+ case 'own-run':
88
+ (0, http_1.send)(res, 403, { error: 'a run is approved by somebody else than who asked for it' });
89
+ break;
90
+ case 'not-waiting':
91
+ (0, http_1.send)(res, 409, { error: `the run is ${result.state}, it does not wait for an approval` });
92
+ break;
93
+ default:
94
+ (0, http_1.send)(res, 404, { error: 'not found' });
95
+ }
96
+ return true;
97
+ }
98
+ exports.handleApprovals = handleApprovals;
99
+ //# sourceMappingURL=approvals-api.js.map
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleDefinitions = void 0;
4
+ const bundle_1 = require("../remote/bundle");
5
+ const definitions_1 = require("./definitions");
6
+ const http_1 = require("./http");
7
+ const policy_1 = require("./policy");
8
+ const proposals_1 = require("./proposals");
9
+ const server_config_1 = require("./server-config");
10
+ // A bundle is JSON text with escapes, so the body may be somewhat larger than the files.
11
+ const MAX_PUBLISH_BYTES = bundle_1.MAX_BUNDLE_BYTES * 2;
12
+ /**
13
+ * The published build files:
14
+ *
15
+ * PUT /v1/definitions/<name> publish a bundle, or propose it when the
16
+ * caller may only propose (answers 202)
17
+ * GET /v1/definitions the names, with their latest version
18
+ * GET /v1/definitions/<name> the versions of one
19
+ * GET /v1/definitions/<name>/<hash|latest> one version
20
+ * GET /v1/definitions/<name>/<hash|latest>/files the files of one version
21
+ */
22
+ async function handleDefinitions(config, store, proposals, log, principal, req, res, url) {
23
+ const match = /^\/v1\/definitions(?:\/([^/]+)(?:\/([^/]+)(\/files)?)?)?$/.exec(url.pathname);
24
+ if (!match) {
25
+ return false;
26
+ }
27
+ const [, name, version, files] = match;
28
+ const grant = (0, policy_1.getGrant)(config.entitlements, principal);
29
+ if (name !== undefined && !server_config_1.definitionName.safeParse(name).success) {
30
+ (0, http_1.send)(res, 404, { error: 'not found' });
31
+ return true;
32
+ }
33
+ if (name !== undefined && version === undefined && req.method === 'PUT') {
34
+ await publish(config, store, proposals, log, principal, name, req, res);
35
+ return true;
36
+ }
37
+ if (req.method !== 'GET') {
38
+ (0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: name !== undefined && !version ? 'GET, PUT' : 'GET' });
39
+ return true;
40
+ }
41
+ // what the caller may not see does not exist for them
42
+ if (!(0, policy_1.maySeeDefinitions)(grant)) {
43
+ (0, http_1.send)(res, 404, { error: 'not found' });
44
+ return true;
45
+ }
46
+ if (name === undefined) {
47
+ (0, http_1.send)(res, 200, {
48
+ definitions: store.names().map((n) => ({ name: n, latest: store.find(n, 'latest')?.hash })),
49
+ });
50
+ return true;
51
+ }
52
+ if (version === undefined) {
53
+ const versions = store.list(name);
54
+ versions.length > 0
55
+ ? (0, http_1.send)(res, 200, { name, latest: versions[0].hash, versions })
56
+ : (0, http_1.send)(res, 404, { error: 'not found' });
57
+ return true;
58
+ }
59
+ const found = version === 'latest' || bundle_1.HASH_PATTERN.test(version) ? store.find(name, version) : undefined;
60
+ if (!found) {
61
+ (0, http_1.send)(res, 404, { error: 'not found' });
62
+ return true;
63
+ }
64
+ if (files) {
65
+ const bundle = await store.files(found);
66
+ bundle ? (0, http_1.send)(res, 200, bundle) : (0, http_1.send)(res, 404, { error: 'not found' });
67
+ }
68
+ else {
69
+ (0, http_1.send)(res, 200, found);
70
+ }
71
+ return true;
72
+ }
73
+ exports.handleDefinitions = handleDefinitions;
74
+ async function publish(config, store, proposals, log, principal, name, req, res) {
75
+ const grant = (0, policy_1.getGrant)(config.entitlements, principal);
76
+ const direct = (0, policy_1.mayPublish)(grant, name);
77
+ if (!direct && !(0, policy_1.mayPropose)(grant, name)) {
78
+ log(`refuse publish iss=${principal.iss} sub=${principal.sub}: build file ${name} is not granted`);
79
+ (0, http_1.send)(res, 403, { error: `you may not publish ${name}` });
80
+ return;
81
+ }
82
+ const parsed = bundle_1.bundleSchema.safeParse(await (0, http_1.readJson)(req, MAX_PUBLISH_BYTES));
83
+ if (!parsed.success) {
84
+ const issue = parsed.error.issues[0];
85
+ (0, http_1.send)(res, 400, { error: `${issue.path.join('.') || 'request'}: ${issue.message}` });
86
+ return;
87
+ }
88
+ try {
89
+ if (!direct) {
90
+ // a version can't run before it is approved, but it has to load, as it would
91
+ // have when published, so the approver is not asked about a broken one
92
+ const { version, existing } = await store.prepare(name, parsed.data, principal);
93
+ if (existing) {
94
+ (0, http_1.send)(res, 200, { ...existing, created: false });
95
+ return;
96
+ }
97
+ const { proposal, created } = proposals.propose(version, parsed.data, principal);
98
+ log(`propose ${name} ${proposal.hash} iss=${principal.iss} sub=${principal.sub} created=${created} tasks=${proposal.tasks.length}`);
99
+ (0, http_1.send)(res, created ? 202 : 200, { ...proposal, created });
100
+ return;
101
+ }
102
+ const { version, created } = await store.publish(name, parsed.data, principal);
103
+ log(`publish ${name} ${version.hash} iss=${principal.iss} sub=${principal.sub} created=${created} tasks=${version.tasks.length}`);
104
+ (0, http_1.send)(res, created ? 201 : 200, { ...version, created });
105
+ }
106
+ catch (e) {
107
+ if (e instanceof definitions_1.DefinitionError) {
108
+ log(`refuse publish iss=${principal.iss} sub=${principal.sub}: ${name} does not load`);
109
+ (0, http_1.send)(res, 400, { error: `the build file does not load: ${e.message}` });
110
+ return;
111
+ }
112
+ if (e instanceof proposals_1.ProposalError) {
113
+ (0, http_1.send)(res, 503, { error: e.message });
114
+ return;
115
+ }
116
+ throw e;
117
+ }
118
+ }
119
+ //# sourceMappingURL=definitions-api.js.map