import crypto from "node:crypto"; import type { LicenseChallengeResponse } from "./sdk"; /** * @public * Licensing V2: a Norsk-root-signed identity used to answer a V2 engine's * license challenge. * * The cert (role `studio-runtime` or `sdk-client`) plus its Ed25519 private key * are the anti-bare-client anchor: a V2 engine will not complete the handshake * — and so refuses node creation — without a valid identity signing the * challenge nonce. Norsk issues these per integrator; the private key is a * secret, treat it like one. * * A direct-SDK integrator is issued an `sdk-client` identity plus a V2 license * entitling their product in `designer` mode, and uses * {@link createLicenseChallengeResponder} to answer the challenge. (Studio uses * the same mechanism with its baked-in `studio-runtime` identity.) */ export type LicenseIdentity = { /** Root-signed identity cert as its envelope JSON string. */ certJson: string; /** The identity's Ed25519 private key (PKCS#8 PEM). */ keyPem: string; /** The product this identity is licensed for — must appear in the license. */ productName: string; /** Image ref reported to the engine, checked against the license entry. */ imageRef: string; }; /** * @public * Sign an engine license-challenge nonce with an identity's Ed25519 private key * (PKCS#8 PEM). Exposed for runtimes that assemble a richer challenge response * (e.g. relaying a signed workflow); most callers want * {@link createLicenseChallengeResponder}. */ export function signLicenseChallenge(keyPem: string, challenge: Uint8Array): Uint8Array { return new Uint8Array(crypto.sign(null, challenge, crypto.createPrivateKey(keyPem))); } /** * @public * Build an `onLicenseChallenge` responder from an identity. This is the * "designer" response: it proves the caller's identity by signing the nonce and * declares the licensed product, carrying no per-workflow signature — the right * response for a direct-SDK client authoring its own workflows. Pass the result * straight to {@link NorskSettings.onLicenseChallenge}: * * ```ts * const norsk = await Norsk.connect({ * onLicenseChallenge: createLicenseChallengeResponder({ * certJson: fs.readFileSync("identity.cert.json", "utf-8"), * keyPem: fs.readFileSync("identity.key.pem", "utf-8"), * productName: "acme-product", * imageRef: "acme/acme-product:1.0.0", * }), * }); * ``` */ export function createLicenseChallengeResponder( identity: LicenseIdentity, ): (challenge: Uint8Array) => LicenseChallengeResponse { const studioCert = new Uint8Array(Buffer.from(identity.certJson, "utf-8")); // Parse the key once so a malformed PEM fails here, not on the first challenge. const key = crypto.createPrivateKey(identity.keyPem); return (challenge) => ({ studioCert, studioSig: new Uint8Array(crypto.sign(null, challenge, key)), productName: identity.productName, imageRef: identity.imageRef, }); }