doxa-maps/library/scripts/posters/pin-poster-render.mjs
Doxa Maps a3e58ac7ca Serve the packet text at print time instead of bundling all 11 languages
A single-file build inlines every import, dynamic ones included, so
packet-strings.json (~115 KB minified: every language's prayer points,
captions and headings) rode in every bundle that can print a packet, list
or card deck. With the wall's print messages translated, country-posters
crossed the 1.5 MB ceiling (1,502,226 bytes).

The table is now served at <assets>/lib/packet-strings.json and fetched at
the first print by library/packet/packetStrings.js, the same move as the
sheet generator and ne-places.json:

- Pinned like them: PACKET_STRINGS_SHA384 in poster-render.integrity.js
  (pin-poster-render.mjs); the text is hashed before it is parsed, and an
  unmatched table is not used. These strings are laid into the print
  document, so an unverified copy never reaches it.
- English is bundled as the fallback (a named import of the JSON, so only
  the en block ships): with no asset base, on file://, on a network error or
  an integrity mismatch the print comes out in English and the console says
  why. A print never fails for want of this file.
- generate-tree copies it into the pool AND the served allowlist; verified
  by regenerating the tree (the served copy is byte-identical to source).
- check:poster-render-pinned, two-sided: the pin matches source and served
  copy, the genuine copy loads all 11 languages, a tampered copy falls back
  to English (mutation-tested: disabling the hash check fails it), and an
  unserved one falls back rather than failing.
- sha384 is one shared helper (library/packet/sha384.js).

Sizes: country-posters 1,378,463 (was 1,484,053), my-upg-100-list 1,143,456
(was 1,243,509), regions-dashboard 973,802 (was 1,079,448).
2026-09-29 08:33:44 +00:00

50 lines
3.7 KiB
JavaScript

#!/usr/bin/env node
// pin-poster-render.mjs — write library/packet/poster-render.integrity.js, the sha384s the browser checks
// before it runs the served map-sheet generator or uses the served capitals-and-cities table. Run it
// whenever library/packet/poster-render.js or library/scripts/posters/data/ne-places.json changes;
// check:poster-render-pinned fails (and names this command) until the pins match the files again.
//
// node library/scripts/posters/pin-poster-render.mjs
import { readFileSync, writeFileSync } from 'node:fs'
import { createHash } from 'node:crypto'
import { fileURLToPath } from 'node:url'
import { join, dirname } from 'node:path'
const LIB = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'packet')
export const pinOf = (text) => `sha384-${createHash('sha384').update(text, 'utf8').digest('base64')}`
// The CLASSIC-SCRIPT copy of the generator, for pages whose Content-Security-Policy forbids blob: scripts:
// loaded with <script integrity=…> the browser verifies it natively. poster-render.js has one export and
// no imports, so the transform is mechanical — strict mode kept (modules are strict), the one export
// becomes window.DoxaPosterRender. generate-tree writes it with THIS function, so the served copy and the
// pin cannot disagree about what it is.
export function globalVariant(source) {
const body = String(source).replace(/^export function createPosterGenerator\(/m, 'function createPosterGenerator(')
if (body === String(source)) throw new Error('poster-render.js no longer exports createPosterGenerator as the transform expects')
return `(function () {\n'use strict';\n${body}\nglobalThis.DoxaPosterRender = { createPosterGenerator };\n})();\n`
}
const isMain = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]
if (isMain) {
const pin = pinOf(readFileSync(join(LIB, 'poster-render.js'), 'utf8'))
const places = pinOf(readFileSync(join(LIB, '..', 'scripts', 'posters', 'data', 'ne-places.json'), 'utf8'))
const classic = pinOf(globalVariant(readFileSync(join(LIB, 'poster-render.js'), 'utf8')))
const strings = pinOf(readFileSync(join(LIB, '..', 'scripts', 'posters', 'packet-strings.json'), 'utf8'))
writeFileSync(join(LIB, 'poster-render.integrity.js'), `// GENERATED by library/scripts/posters/pin-poster-render.mjs — do not edit by hand.
// The sha384 of library/packet/poster-render.js. The browser fetches the served copy of that file
// (<assets>/lib/poster-render.js), hashes it, and runs it only when it matches this pin: code loaded at
// print time is integrity-pinned like every embed, because it executes on partner pages.
export const POSTER_RENDER_SHA384 = '${pin}'
// The sha384 of library/scripts/posters/data/ne-places.json, served at <assets>/boundaries/ne-places.json.
// Data, not code, but pinned the same way: a sheet uses the places only when they are the pinned ones.
export const NE_PLACES_SHA384 = '${places}'
// The sha384 of the classic-script copy (<assets>/lib/poster-render.global.js, globalVariant of the file above),
// loaded with <script integrity> where a page's policy forbids importing a blob: of the verified text.
export const POSTER_RENDER_GLOBAL_SHA384 = '${classic}'
// The sha384 of library/scripts/posters/packet-strings.json, served at <assets>/lib/packet-strings.json. The
// packet, list and card text in every language, fetched at print time (packet/packetStrings.js) and used only
// when it matches; otherwise the print falls back to the bundled English block.
export const PACKET_STRINGS_SHA384 = '${strings}'
`)
console.log(`[pin-poster-render] ${pin}\n[pin-poster-render] ne-places ${places}\n[pin-poster-render] classic ${classic}\n[pin-poster-render] packet-strings ${strings}`)
}