UNPKG

zephyr-agent

Version:
139 lines (138 loc) • 6.64 kB
import "node:module"; import { createHash } from "node:crypto"; import { join } from "node:path"; import { writeFileSync } from "node:fs"; import { ZE_API_ENDPOINT, ze_api_gateway } from "zephyr-edge-contract"; import { getToken } from "../node-persist/token.mjs"; import { makeRequest } from "../http/http-request.mjs"; import { readAttributionConfig } from "./config.mjs"; import { attributionRepository, loadSourceRecord, sourceCommitBaseline } from "./source.mjs"; import { compareSourceContents } from "./compare.mjs"; function validPolicy(policy) { return Boolean(policy && 1 === policy.schemaVersion && 'string' == typeof policy.repositoryId && /^[a-zA-Z0-9._-]{1,128}$/.test(policy.repositoryId) && 'string' == typeof policy.revision && policy.revision.length > 0 && policy.revision.length <= 128 && [ 'free', 'paid', 'byoc' ].includes(policy.tier) && [ 'local', 'remote' ].includes(policy.storage) && 'boolean' == typeof policy.content?.patch && 'boolean' == typeof policy.content?.lines); } function resolveRemoteContent(config, policy) { if (!validPolicy(policy) || 'remote' !== policy.storage) throw new Error('Remote attribution needs an enabled repository policy from the authenticated service'); if (config.repositoryId && config.repositoryId !== policy.repositoryId) throw new Error('Attribution repository does not match the authenticated policy'); if ('free' === policy.tier) { if (config.content?.patch === false || config.content?.lines === false || !policy.content.patch || !policy.content.lines) throw new Error('Free remote attribution requires patches and changed lines; choose local storage to keep them local'); return { patch: true, lines: true }; } const content = { patch: config.content?.patch ?? false, lines: config.content?.lines ?? false }; if (content.patch && !policy.content.patch || content.lines && !policy.content.lines) throw new Error('Repository content settings exceed the authenticated attribution policy'); return content; } async function publishAttribution({ directory, summary, applicationUid, buildId, snapshotId, appConfig }) { let repository; try { repository = attributionRepository(directory); } catch { return; } const { root, gitDir } = repository; let config; try { config = readAttributionConfig(root, gitDir); } catch { return; } if (!config?.enabled || (config.storage ?? 'local') === 'local') return; if (!summary || 'captured' !== summary.status || !summary.sourceId) throw new Error('Remote attribution cannot publish without a complete local source receipt'); if ('remote' !== summary.storage) throw new Error('Attribution storage changed during publication; capture again'); if (appConfig.application_uid !== applicationUid || !applicationUid || !buildId || !snapshotId) throw new Error('Attribution publication requires matching application and build identities'); const policy = appConfig.ATTRIBUTION_POLICY; const content = resolveRemoteContent(config, policy); const record = loadSourceRecord(root, summary.sourceId); if (record.fingerprint !== summary.sourceFingerprint) throw new Error('Attribution source receipt does not match its fingerprint'); const payload = { schemaVersion: 1, applicationUid, repositoryId: policy.repositoryId, buildId, snapshotId, policyRevision: policy.revision, content, attribution: summary }; if (content.patch || content.lines) { const difference = compareSourceContents(sourceCommitBaseline(root, record), record, root); payload.comparison = { base: 'git-head', baseCommit: record.baseCommit, changes: difference.changes.map(({ patch, lines, ...metadata })=>({ ...metadata, ...content.patch ? { patch } : {}, ...content.lines ? { lines } : {} })) }; } const body = JSON.stringify(payload); if (Buffer.byteLength(body) > 20971520) throw new Error('Remote attribution exceeds 20 MiB; reduce the repository capture scope'); const url = new URL(ze_api_gateway.attribution, ZE_API_ENDPOINT()); if ('https:' !== url.protocol && !('http:' === url.protocol && [ 'localhost', '127.0.0.1', '[::1]' ].includes(url.hostname))) throw new Error('Remote attribution requires HTTPS'); if (url.username || url.password) throw new Error('Attribution endpoints must not contain credentials'); const token = await getToken(); if (!token) throw new Error('Remote attribution requires Zephyr authentication'); const idempotencyKey = createHash('sha256').update(JSON.stringify([ applicationUid, policy.repositoryId, buildId ])).digest('hex'); const [ok, , response] = await makeRequest(url, { method: 'POST', redirect: 'error', sensitiveResponse: true, headers: { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: `Bearer ${token}`, 'Idempotency-Key': idempotencyKey }, credentialToken: token }, body); if (!ok || response?.status !== 'ok' || response.applicationUid !== applicationUid || response.repositoryId !== policy.repositoryId || response.buildId !== buildId || response.snapshotId !== snapshotId || response.sourceFingerprint !== record.fingerprint || 'string' != typeof response.recordId || !/^[a-zA-Z0-9._-]{1,128}$/.test(response.recordId)) throw new Error('Remote attribution was not acknowledged for this repository/build; private evidence remains local'); const remote = { recordId: response.recordId, repositoryId: policy.repositoryId, policyRevision: policy.revision }; writeFileSync(join(gitDir, 'zephyr-attribution', `remote-${idempotencyKey}.json`), JSON.stringify({ applicationUid, buildId, snapshotId, sourceId: record.id, sourceFingerprint: record.fingerprint, remote }), { mode: 384 }); const { files: _files, sessions: _sessions, workspaceHuman: _human, ...reference } = summary; return { ...reference, storage: 'remote', remote }; } export { publishAttribution, resolveRemoteContent }; //# sourceMappingURL=remote.mjs.map