feat(mcp): подготовить сервер и доступ к приёмке
This commit is contained in:
commit
bf3d50e1ff
3835 files changed
+537122
No files matched your search
@@ -0,0 +1,74 @@
|
||||
/** Offline operator-only issuer proposal. Never imported by runtime/startup.
|
||||
* Effective key/registry ACL isolation is required before deployment acceptance;
|
||||
* mode bits and possession of CLI arguments do not establish that isolation. */
|
||||
import { createPrivateKey, createPublicKey, sign } from "node:crypto"
|
||||
import { mkdir, open, realpath } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { z } from "zod"
|
||||
import { DATABASE_SOURCE_SIGNATURE_DOMAINS, sourceBindingBodySchema, sourceBindingBytes, sourceBindingCanonical, sourceBindingSha256,
|
||||
readDatabaseSourceBindingRegistry, type DatabaseSourceDeploymentPin } from "../src/db/database-source-binding.js"
|
||||
|
||||
const requestSchema = sourceBindingBodySchema.omit({ format: true, ordinal: true })
|
||||
export type DatabaseSourceOperatorRequest = z.infer<typeof requestSchema>
|
||||
function outside(parent: string, child: string) {
|
||||
const relative = path.relative(parent, child)
|
||||
return !!relative && (relative.startsWith(".." + path.sep) || relative === ".." || path.isAbsolute(relative))
|
||||
}
|
||||
export async function provisionDatabaseSourceBindingOffline(options: { request: DatabaseSourceOperatorRequest; privateKeyFile: string;
|
||||
outputDirectory: string; previousPin?: DatabaseSourceDeploymentPin }) {
|
||||
const request = requestSchema.parse(options.request)
|
||||
const privatePath = await realpath(options.privateKeyFile), output = path.resolve(options.outputDirectory)
|
||||
const parent = await realpath(path.dirname(output))
|
||||
if (parent !== path.dirname(output) || !outside(output, privatePath)) throw Error("Private operator key must be outside new registry, without ambiguous parent links")
|
||||
const stat = await open(privatePath, "r")
|
||||
let privateBytes: Buffer
|
||||
try { const meta = await stat.stat(); if (!meta.isFile() || meta.size < 1 || meta.size > 8192) throw Error("Bounded operator private key required"); privateBytes = await stat.readFile() }
|
||||
finally { await stat.close() }
|
||||
const privateKey = createPrivateKey(privateBytes); privateBytes.fill(0)
|
||||
if (privateKey.type !== "private" || privateKey.asymmetricKeyType !== "ed25519") throw Error("Independent Ed25519 operator key required")
|
||||
const publicKey = createPublicKey(privateKey), spki = publicKey.export({ type: "spki", format: "der" })
|
||||
const publicKeySpkiSha256 = sourceBindingSha256(spki)
|
||||
const previous = options.previousPin ? await readDatabaseSourceBindingRegistry(options.previousPin) : undefined
|
||||
if (previous && (previous.body.installationId !== request.installationId || previous.body.sourceIdentityId !== request.sourceIdentityId ||
|
||||
previous.publicKeySpkiSha256 !== publicKeySpkiSha256 || previous.body.generation === request.generation || previous.body.ordinal >= Number.MAX_SAFE_INTEGER)) throw Error("Previous current head/identity/key or distinct new generation required")
|
||||
const body = sourceBindingBodySchema.parse({ ...request, format: "database-source-binding-v1", ordinal: previous ? previous.body.ordinal + 1 : 1 })
|
||||
const signed = (domain: string, payload: unknown) => sourceBindingBytes({ body: payload,
|
||||
signature: sign(null, Buffer.from(domain + sourceBindingCanonical(payload)), privateKey).toString("base64url") })
|
||||
const bindingBytes = signed(DATABASE_SOURCE_SIGNATURE_DOMAINS.binding, body), bindingSha256 = sourceBindingSha256(bindingBytes)
|
||||
const head = { format: "database-source-registry-head-v1", installationId: body.installationId, sourceIdentityId: body.sourceIdentityId,
|
||||
generation: body.generation, ordinal: body.ordinal, bindingSha256, previousHeadSha256: previous?.headSha256 ?? null }
|
||||
const headBytes = signed(DATABASE_SOURCE_SIGNATURE_DOMAINS.head, head), currentHeadSha256 = sourceBindingSha256(headBytes)
|
||||
// No recursive mkdir and no existing-state adoption. A partial output remains
|
||||
// for explicit operator review; uncertainty is never retried/overwritten.
|
||||
await mkdir(output, { mode: 0o700 })
|
||||
const publish = async (filename: string, bytes: Buffer | string) => {
|
||||
const handle = await open(path.join(output, filename), "wx", 0o600)
|
||||
try { await handle.writeFile(bytes); await handle.sync() } finally { await handle.close() }
|
||||
}
|
||||
await publish("operator-public.pem", publicKey.export({ type: "spki", format: "pem" }))
|
||||
await publish("binding-" + bindingSha256 + ".json", bindingBytes)
|
||||
await publish("head-" + currentHeadSha256 + ".json", headBytes)
|
||||
const deploymentPin = { registryDirectory: output, installationId: body.installationId, publicKeySpkiSha256, currentHeadSha256 }
|
||||
// Public review artifact only; not installed into any application config.
|
||||
await publish("proposed-deployment-pin.json", sourceBindingBytes(deploymentPin))
|
||||
return Object.freeze({ deploymentPin: Object.freeze(deploymentPin), keyIsolationVerified: false as const,
|
||||
platformProtectionVerified: false as const, generationAuthenticated: false as const, readyAuthorized: false as const })
|
||||
}
|
||||
|
||||
export async function databaseSourceBindingOperatorCli(args: string[]) {
|
||||
const allowed = new Set(["request", "private-key", "output", "previous-pin"]), values = new Map<string, string>()
|
||||
for (const arg of args) {
|
||||
const match = /^--([a-z-]+)=(.+)$/.exec(arg)
|
||||
if (!match || !allowed.has(match[1]) || values.has(match[1])) throw Error("Exact explicit offline operator arguments required")
|
||||
values.set(match[1], match[2])
|
||||
}
|
||||
if (!["request", "private-key", "output"].every(key => values.has(key))) throw Error("Missing explicit offline operator arguments")
|
||||
const boundedJson = async (filename: string) => {
|
||||
const handle = await open(filename, "r")
|
||||
try { const stat = await handle.stat(); if (!stat.isFile() || stat.size < 1 || stat.size > 8192) throw Error("Bounded operator input required"); return JSON.parse(await handle.readFile("utf8")) as unknown }
|
||||
finally { await handle.close() }
|
||||
}
|
||||
const request = requestSchema.parse(await boundedJson(values.get("request")!))
|
||||
const previousPin = values.has("previous-pin") ? await boundedJson(values.get("previous-pin")!) as DatabaseSourceDeploymentPin : undefined
|
||||
return provisionDatabaseSourceBindingOffline({ request, privateKeyFile: values.get("private-key")!, outputDirectory: values.get("output")!, previousPin })
|
||||
}
|
||||
Reference in new issue
Block a user