Spreadsheet-UX op een echte relationele engine

main

Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.

9.0 kB Ruw bestand
/**
 * Verifying a licence.
 *
 * Vendored from Grant, the tool that issues them, and deliberately identical
 * to the copy there: nothing imported but `node:crypto`, no dependencies, no
 * network, no filesystem. Wellformed embeds its own public key, reads the
 * token out of its configuration, and calls `verifyLicence`.
 *
 * Do not grow this file. Anything Wellformed wants on top of it belongs in
 * `index.ts` beside it, so that this one stays a copy that can be replaced
 * wholesale when Grant's changes.
 */
import { createHash, createPublicKey, verify as verifySignature, type KeyObject } from 'node:crypto';

/**
 * Version tag on every token.
 *
 * It is the first thing read and the first thing to change if the format ever
 * does, so that an old product meeting a new token says "I do not know this
 * kind of licence" instead of guessing at the bytes.
 */
export const TOKEN_PREFIX = 'grant1';

/** A licence, as it was signed. */
export interface Licence {
  /** Slug of the software this is a licence for, e.g. `wellformed`. */
  product: string;
  /** Reference as it appears on the deed, e.g. `WF-2026-001`. */
  ref: string;
  /** Legal name of the party it was granted to. */
  to: string;
  /** Date of the grant, `YYYY-MM-DD`. */
  from: string;
  /** `perpetual`, or the `YYYY-MM-DD` on which it ends. */
  term: string;
  /** Fingerprint of the key that signed it. */
  keyId: string;
  /** Anything the grant added on top. Free-form, and product-specific. */
  note?: string;
}

/** Why a token was not accepted. */
export type LicenceFault =
  | 'missing'
  | 'malformed'
  | 'unknown-format'
  | 'bad-signature'
  | 'wrong-product'
  | 'expired';

export class LicenceError extends Error {
  readonly fault: LicenceFault;

  constructor(fault: LicenceFault, message: string) {
    super(message);
    this.name = 'LicenceError';
    this.fault = fault;
  }
}

export interface VerifyOptions {
  /**
   * Refuse a licence issued for other software.
   *
   * Worth setting always. Without it a licence for one product is a licence
   * for every product signed by the same key, which is exactly the mistake a
   * per-product key pair exists to prevent.
   */
  product?: string;
  /** Overridable only so that tests can sit at a date. */
  now?: Date;
}

/**
 * Check a token against one or more public keys and return what it says.
 *
 * Throws `LicenceError` and returns nothing on failure — there is no partly
 * good licence, and a caller that wants to keep running past a bad one should
 * have to say so in a `catch`.
 *
 * Several keys may be passed so that a key can be rotated without invalidating
 * what was signed with the old one: keep the retired public key in the list
 * until the licences it signed have been reissued.
 */
export function verifyLicence(
  token: string | undefined | null,
  keys: PublicKeyInput | readonly PublicKeyInput[],
  options: VerifyOptions = {},
): Licence {
  const raw = (token ?? '').trim();
  if (raw === '') throw new LicenceError('missing', 'No licence is set.');

  const parts = raw.split('.');
  if (parts.length !== 3) {
    throw new LicenceError('malformed', 'A licence is three parts separated by dots.');
  }
  const [prefix, payloadSegment, signatureSegment] = parts as [string, string, string];
  if (prefix !== TOKEN_PREFIX) {
    throw new LicenceError('unknown-format', `Licence format ${prefix} is not one this version knows.`);
  }

  // The signature covers the payload segment exactly as it was written, not a
  // re-serialisation of the object inside it. That way nothing depends on two
  // JSON encoders agreeing about key order or whitespace.
  const signed = Buffer.from(payloadSegment, 'ascii');
  const signature = Buffer.from(signatureSegment, 'base64url');
  const candidates = Array.isArray(keys) ? keys : [keys as PublicKeyInput];

  const accepted = candidates.some((key) => {
    try {
      return verifySignature(null, signed, publicKeyFrom(key), signature);
    } catch {
      // A key that cannot be read is a key that does not accept this licence.
      return false;
    }
  });
  if (!accepted) {
    throw new LicenceError('bad-signature', 'This licence was not signed by the key this software trusts.');
  }

  const licence = decodePayload(payloadSegment);

  if (options.product !== undefined && licence.product !== options.product) {
    throw new LicenceError(
      'wrong-product',
      `This is a licence for ${licence.product}, not for ${options.product}.`,
    );
  }

  const expiry = expiryOf(licence);
  if (expiry !== null && (options.now ?? new Date()) > expiry) {
    throw new LicenceError('expired', `This licence ran to ${licence.term}.`);
  }

  return licence;
}

/**
 * Read a token without checking the signature.
 *
 * For showing a human what a token claims — in a CLI, in a support mail — and
 * for nothing else. Anyone can write anything here. Never branch on it.
 */
export function decodeUnverified(token: string): Licence {
  const segment = token.trim().split('.')[1];
  if (segment === undefined) throw new LicenceError('malformed', 'A licence is three parts separated by dots.');
  return decodePayload(segment);
}

/** The moment a licence stops covering anything, or `null` if it is perpetual. */
export function expiryOf(licence: Licence): Date | null {
  if (licence.term === 'perpetual') return null;
  const end = new Date(`${licence.term}T23:59:59.999Z`);
  if (Number.isNaN(end.getTime())) {
    throw new LicenceError('malformed', `Licence term ${licence.term} is not a date.`);
  }
  return end;
}

/**
 * One line naming who holds this, for a log or a settings screen.
 *
 * `Acme B.V. · WF-2026-001 · since 3 March 2026 · perpetual`
 */
export function describeLicence(licence: Licence): string {
  const term = licence.term === 'perpetual' ? 'perpetual' : `until ${formatDate(licence.term)}`;
  return `${licence.to} · ${licence.ref} · since ${formatDate(licence.from)} · ${term}`;
}

/** `2026-03-03` as `3 March 2026`, for anything a person reads. */
export function formatDate(iso: string): string {
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
  if (match === null) return iso;
  const [, year, month, day] = match as unknown as [string, string, string, string];
  const name = MONTHS[Number(month) - 1] ?? month;
  return `${Number(day)} ${name} ${year}`;
}

const MONTHS = [
  'January',
  'February',
  'March',
  'April',
  'May',
  'June',
  'July',
  'August',
  'September',
  'October',
  'November',
  'December',
] as const;

/**
 * A public key, in whichever of the three shapes is at hand.
 *
 * PEM is what `grant keygen` writes and what a product embeds. The bare
 * base64 form is there because a 44-character string survives being pasted
 * into a config file, a Dockerfile or a systemd unit, and a PEM block does
 * not.
 */
export type PublicKeyInput = string | Buffer | KeyObject;

/** Ed25519 SPKI is a fixed 12-byte header and then the 32-byte key. */
const SPKI_ED25519_HEADER = Buffer.from('302a300506032b6570032100', 'hex');

/** Turn any of the accepted shapes into a key `node:crypto` will use. */
export function publicKeyFrom(value: PublicKeyInput): KeyObject {
  if (typeof value === 'object' && !Buffer.isBuffer(value)) return value;

  const bytes = Buffer.isBuffer(value) ? value : null;
  const text = bytes === null ? String(value).trim() : '';

  if (text.includes('BEGIN')) {
    return createPublicKey(text);
  }

  const raw = bytes ?? Buffer.from(text, 'base64');
  if (raw.length === 32) {
    return createPublicKey({
      key: Buffer.concat([SPKI_ED25519_HEADER, raw]),
      format: 'der',
      type: 'spki',
    });
  }
  return createPublicKey({ key: raw, format: 'der', type: 'spki' });
}

/**
 * A short, stable name for a key.
 *
 * It travels in the token so that a product holding several public keys knows
 * which one to reach for, and so that a person can tell two keys apart in a
 * log without handling either.
 */
export function fingerprint(key: PublicKeyInput): string {
  const der = publicKeyFrom(key).export({ format: 'der', type: 'spki' });
  return createHash('sha256').update(der).digest('hex').slice(0, 16);
}

function decodePayload(segment: string): Licence {
  let parsed: unknown;
  try {
    parsed = JSON.parse(Buffer.from(segment, 'base64url').toString('utf8'));
  } catch {
    throw new LicenceError('malformed', 'The body of this licence is not readable.');
  }
  if (typeof parsed !== 'object' || parsed === null) {
    throw new LicenceError('malformed', 'The body of this licence is not readable.');
  }

  const body = parsed as Record<string, unknown>;
  for (const field of ['product', 'ref', 'to', 'from', 'term', 'key'] as const) {
    if (typeof body[field] !== 'string' || body[field] === '') {
      throw new LicenceError('malformed', `This licence has no ${field}.`);
    }
  }

  const licence: Licence = {
    product: body.product as string,
    ref: body.ref as string,
    to: body.to as string,
    from: body.from as string,
    term: body.term as string,
    keyId: body.key as string,
  };
  if (typeof body.note === 'string' && body.note !== '') licence.note = body.note;
  return licence;
}