champollion 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/LICENSE +133 -0
  2. package/README.md +387 -0
  3. package/bin/cli.js +278 -0
  4. package/index.js +135 -0
  5. package/lib/api-key.js +127 -0
  6. package/lib/autofix.js +432 -0
  7. package/lib/bridge/method_bridge.py +430 -0
  8. package/lib/card-source-resolution.mjs +284 -0
  9. package/lib/cards/cache.js +169 -0
  10. package/lib/cards/env.js +82 -0
  11. package/lib/cards/fetch-card-child.js +38 -0
  12. package/lib/cards/reader.js +435 -0
  13. package/lib/cards/refresh.js +111 -0
  14. package/lib/cards/remote.js +387 -0
  15. package/lib/cldf-export.mjs +540 -0
  16. package/lib/cldf-terms.mjs +62 -0
  17. package/lib/command-help.js +790 -0
  18. package/lib/commands/audit.js +49 -0
  19. package/lib/commands/card.js +454 -0
  20. package/lib/commands/doctor.js +559 -0
  21. package/lib/commands/fonts.js +489 -0
  22. package/lib/commands/help.js +91 -0
  23. package/lib/commands/init.js +1259 -0
  24. package/lib/commands/integrity.js +148 -0
  25. package/lib/commands/leaderboard.js +478 -0
  26. package/lib/commands/lint.js +30 -0
  27. package/lib/commands/models.js +177 -0
  28. package/lib/commands/plugin.js +103 -0
  29. package/lib/commands/provenance.js +45 -0
  30. package/lib/commands/recommend.js +75 -0
  31. package/lib/commands/register-corpus.js +678 -0
  32. package/lib/commands/repair-script.js +42 -0
  33. package/lib/commands/seal-corpus.js +355 -0
  34. package/lib/commands/seo.js +72 -0
  35. package/lib/commands/serve.js +147 -0
  36. package/lib/commands/status.js +265 -0
  37. package/lib/commands/submit.js +332 -0
  38. package/lib/commands/sync.js +89 -0
  39. package/lib/commands/tm.js +573 -0
  40. package/lib/commands/verify.js +39 -0
  41. package/lib/commands/watch.js +20 -0
  42. package/lib/commands/wrap.js +138 -0
  43. package/lib/commands/xliff.js +327 -0
  44. package/lib/commercial-eligibility.js +235 -0
  45. package/lib/concurrent.js +87 -0
  46. package/lib/config.js +523 -0
  47. package/lib/contamination-lane.js +76 -0
  48. package/lib/content-sync.js +731 -0
  49. package/lib/content.js +733 -0
  50. package/lib/corpus-registration.mjs +608 -0
  51. package/lib/cost-report.js +346 -0
  52. package/lib/diff.js +155 -0
  53. package/lib/docusaurus-sync.js +1256 -0
  54. package/lib/flatten.js +55 -0
  55. package/lib/format.js +954 -0
  56. package/lib/hash.js +159 -0
  57. package/lib/icu.js +473 -0
  58. package/lib/integrity.js +689 -0
  59. package/lib/license-gate.mjs +478 -0
  60. package/lib/license-identify.mjs +229 -0
  61. package/lib/lint.js +629 -0
  62. package/lib/method-manifest.js +60 -0
  63. package/lib/methods/anthropic.js +140 -0
  64. package/lib/methods/apertium.js +163 -0
  65. package/lib/methods/api.js +316 -0
  66. package/lib/methods/base.js +184 -0
  67. package/lib/methods/content-separator.js +45 -0
  68. package/lib/methods/deepl.js +426 -0
  69. package/lib/methods/direct-llm.js +586 -0
  70. package/lib/methods/external.js +332 -0
  71. package/lib/methods/fetch-with-retry.js +124 -0
  72. package/lib/methods/gemini.js +147 -0
  73. package/lib/methods/google-translate.js +402 -0
  74. package/lib/methods/http-utils.js +122 -0
  75. package/lib/methods/libretranslate.js +314 -0
  76. package/lib/methods/llm-coached.js +670 -0
  77. package/lib/methods/llm.js +592 -0
  78. package/lib/methods/local.js +76 -0
  79. package/lib/methods/microsoft-translator.js +331 -0
  80. package/lib/methods/openai.js +131 -0
  81. package/lib/methods/openrouter-client.js +327 -0
  82. package/lib/methods/openrouter-pricing.js +156 -0
  83. package/lib/methods/provider-env.js +115 -0
  84. package/lib/methods/provider-pricing.js +310 -0
  85. package/lib/methods/tilde.js +150 -0
  86. package/lib/methods/translated.js +229 -0
  87. package/lib/methods/translation-error.js +80 -0
  88. package/lib/models.js +258 -0
  89. package/lib/no-translate.js +233 -0
  90. package/lib/output.js +238 -0
  91. package/lib/pairs.js +547 -0
  92. package/lib/plugins.js +447 -0
  93. package/lib/provenance.js +323 -0
  94. package/lib/recommend.js +648 -0
  95. package/lib/registers.js +1185 -0
  96. package/lib/repair-script.js +266 -0
  97. package/lib/scripts.js +994 -0
  98. package/lib/seal.mjs +464 -0
  99. package/lib/sealed-qualifier.mjs +211 -0
  100. package/lib/security.js +59 -0
  101. package/lib/segment.js +369 -0
  102. package/lib/seo.js +275 -0
  103. package/lib/serve.js +854 -0
  104. package/lib/string-classify.js +85 -0
  105. package/lib/submit.mjs +344 -0
  106. package/lib/sync.js +969 -0
  107. package/lib/tags/bcp47.js +202 -0
  108. package/lib/tags/resolve.js +314 -0
  109. package/lib/terminology.js +111 -0
  110. package/lib/tm-seed.js +294 -0
  111. package/lib/tm.js +515 -0
  112. package/lib/translate-pair.js +197 -0
  113. package/lib/translate.js +203 -0
  114. package/lib/types.js +230 -0
  115. package/lib/validate.js +510 -0
  116. package/lib/verify.js +451 -0
  117. package/lib/watch.js +145 -0
  118. package/lib/xliff.js +184 -0
  119. package/package.json +93 -0
  120. package/shared/ATTRIBUTION.md +145 -0
  121. package/shared/CORPORA-CARDS.md +288 -0
  122. package/shared/DATA-SOVEREIGNTY.md +500 -0
  123. package/shared/LANGUAGE-CARD-FIELDS.md +532 -0
  124. package/shared/card-lint-baseline.json +3189 -0
  125. package/shared/cards-fallback.json +1 -0
  126. package/shared/catalogue/card-config.json +6091 -0
  127. package/shared/catalogue/external-results.json +3888 -0
  128. package/shared/catalogue/gender-guidance.json +1038 -0
  129. package/shared/catalogue/method-coverage.json +1751 -0
  130. package/shared/catalogue/metric-coverage.json +170 -0
  131. package/shared/catalogue/metric-reliability.json +1 -0
  132. package/shared/catalogue/register-presets.json +3180 -0
  133. package/shared/catalogue/vitality-scales.json +55 -0
  134. package/shared/cldr-index.json +1115 -0
  135. package/shared/code-bridge.json +253 -0
  136. package/shared/corpora-cards-v1-reference.md +281 -0
  137. package/shared/curated-dictionary-flags.json +35 -0
  138. package/shared/curated-endonyms.json +35 -0
  139. package/shared/curated-fsts.json +51 -0
  140. package/shared/curated-orthography-conventions.json +26 -0
  141. package/shared/curated-sil-resources.json +374 -0
  142. package/shared/curated-tools.json +41 -0
  143. package/shared/docent/corpus.json +11333 -0
  144. package/shared/docent/faq.en.json +564 -0
  145. package/shared/docent/register-blocks.json +60 -0
  146. package/shared/docent/system-prompt.md +144 -0
  147. package/shared/domain-taxonomy.json +35 -0
  148. package/shared/explainers/glossary.json +2975 -0
  149. package/shared/explainers/tc-features.json +20112 -0
  150. package/shared/explainers/term-watchlist.json +147 -0
  151. package/shared/human-services.json +59 -0
  152. package/shared/license-corrections.json +261 -0
  153. package/shared/license-evidence.json +13452 -0
  154. package/shared/licenses.json +6781 -0
  155. package/shared/method-registry.json +236 -0
  156. package/shared/metric-registry.json +620 -0
  157. package/shared/model-aliases.json +7 -0
  158. package/shared/schemas/champollion-plugin.schema.json +206 -0
  159. package/shared/schemas/corpora-card.schema.json +957 -0
  160. package/shared/schemas/domain-taxonomy.schema.json +64 -0
  161. package/shared/schemas/external-results.schema.json +314 -0
  162. package/shared/schemas/human-services.schema.json +90 -0
  163. package/shared/schemas/language-card.schema.json +1308 -0
  164. package/shared/schemas/licenses.schema.json +155 -0
  165. package/shared/schemas/method-card.schema.json +412 -0
  166. package/shared/schemas/method-registry.schema.json +85 -0
  167. package/shared/schemas/metric-registry.schema.json +96 -0
  168. package/shared/schemas/metric-reliability.schema.json +178 -0
  169. package/shared/schemas/model-aliases.schema.json +27 -0
  170. package/shared/schemas/source-snapshot.schema.json +96 -0
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Command: repair-script
3
+ *
4
+ * Reverses script conversion that should never have happened: PUA-encoded
5
+ * values (pIqaD / Tengwar / Kryptonian) in locales whose configuration says
6
+ * conversion is off are restored to the working script via the converter's
7
+ * own reverse table. See lib/repair-script.js for what is and is not touched.
8
+ *
9
+ * Exit codes: 0 = clean (including --dry previews); 1 = a file could not be
10
+ * read/written, or PUA remained that no registered converter can reverse
11
+ * (the file still cannot render — the repair is incomplete).
12
+ */
13
+
14
+ import { runRepairScript, printRepairReport } from '../repair-script.js';
15
+ import { output } from '../output.js';
16
+
17
+ /**
18
+ * @param {import('../types.js').CLIArgs} args - Parsed CLI arguments
19
+ * @param {string} cwd - Working directory
20
+ * @returns {Promise<number>} Exit code
21
+ */
22
+ async function run(args, cwd) {
23
+ const json = !!args.json;
24
+ if (json) output.setMode('quiet');
25
+
26
+ const result = runRepairScript({ cwd, cliArgs: args });
27
+
28
+ if (json) {
29
+ console.log(JSON.stringify({
30
+ command: 'repair-script',
31
+ dry: result.dry,
32
+ locales: result.locales,
33
+ totals: result.totals,
34
+ }, null, 2));
35
+ } else {
36
+ printRepairReport(result);
37
+ }
38
+
39
+ return args['warn-only'] ? 0 : result.exitCode;
40
+ }
41
+
42
+ export { run };
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Command: seal-corpus — the crypto verbs around lib/seal.mjs.
3
+ *
4
+ * The crypto itself is single-sourced in lib/seal.mjs (X25519-ECDH → HKDF-SHA256
5
+ * → AES-256-GCM, node:crypto only); this command exposes it to scripts and to
6
+ * the Python organizer node (arena/mt_eval_harness/contest_node.py shells out
7
+ * here — the harness never re-implements the cipher). Three verbs:
8
+ *
9
+ * keygen — generate a WAVE-1 STAND-IN threshold keypair. Honesty first: this
10
+ * is a SINGLE keypair, not the 3-of-5 FROST group key of the Wave-2
11
+ * key ceremony. Whoever holds the private-key file can decrypt —
12
+ * fine for an organizer-run contest node (the organizer holds their
13
+ * own refs anyway), NOT the zero-knowledge custody the sovereignty
14
+ * docs promise. The files say so on their face.
15
+ * seal — encrypt a corpus file on THIS machine to a threshold public key;
16
+ * write the ciphertext artifact + the content-free card block.
17
+ * (`champollion register-corpus --tier sealed` wraps this same path
18
+ * with card registration; this verb is the standalone piece.)
19
+ * open — decrypt a sealed artifact with the threshold private key. This is
20
+ * the controlled-eval-context verb: the organizer node calls it at
21
+ * scoring time, writes plaintext ONLY to its scratch dir, and wipes
22
+ * it after scoring. Refuses to write into a tracked repo directory.
23
+ *
24
+ * Exit codes: 0 = done; 1 = invalid request / crypto failure.
25
+ */
26
+
27
+ import fs from 'node:fs';
28
+ import path from 'node:path';
29
+ import {
30
+ generateThresholdKeypair,
31
+ sealPlaintext,
32
+ buildSealedArtifact,
33
+ buildSealedCardBlock,
34
+ buildAad,
35
+ openSealed,
36
+ SEAL_CIPHER,
37
+ SIGN_SCHEME,
38
+ generateSigningKeypair,
39
+ signPayload,
40
+ verifyPayload,
41
+ } from '../seal.mjs';
42
+
43
+ /** Wave-1 custody label stamped wherever this stand-in scheme is used. */
44
+ export const WAVE1_KEY_SCHEME = 'single-keypair-wave1';
45
+
46
+ function fail(msg) {
47
+ console.error(`[ERR] seal-corpus: ${msg}`);
48
+ return 1;
49
+ }
50
+
51
+ /**
52
+ * Read a value that may be an inline base64/PEM string or a file path.
53
+ *
54
+ * A keygen output file carries JSON with one or both key halves (the
55
+ * .key.json embeds publicKeyDerB64 alongside privateKeyDerB64 so it can
56
+ * serve as a --threshold-pubkey source too) — `kind` picks the half the
57
+ * flag actually needs, so a .key.json passed to --privkey never yields
58
+ * the public half by accident.
59
+ *
60
+ * @param {string} value - Inline key material or a file path
61
+ * @param {string} label - Flag name for error messages
62
+ * @param {'public'|'private'} kind - Which half a keygen JSON should yield
63
+ */
64
+ function readKeyMaterial(value, label, kind = 'public') {
65
+ if (!value) throw new Error(`${label} is required.`);
66
+ const p = path.resolve(String(value));
67
+ if (fs.existsSync(p)) {
68
+ const raw = fs.readFileSync(p, 'utf-8').trim();
69
+ // A keygen output file carries JSON; accept it directly.
70
+ if (raw.startsWith('{')) {
71
+ const parsed = JSON.parse(raw);
72
+ if (kind === 'private') {
73
+ return parsed.privateKeyDerB64 || parsed.publicKeyDerB64 || raw;
74
+ }
75
+ return parsed.publicKeyDerB64 || parsed.privateKeyDerB64 || raw;
76
+ }
77
+ return raw;
78
+ }
79
+ return String(value).trim();
80
+ }
81
+
82
+ /** Never write plaintext or private keys into the tracked repo tree. */
83
+ function refuseTrackedPath(target, what) {
84
+ const norm = path.resolve(target).split(path.sep).join('/');
85
+ if (/\/(shared\/corpora-cards|cli\/shared|arena\/datasets)(\/|$)/.test(norm)) {
86
+ throw new Error(
87
+ `${what} must not be written into a tracked data directory (${target}) — ` +
88
+ `choose a scratch/organizer-local path.`);
89
+ }
90
+ }
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // keygen
94
+ // ---------------------------------------------------------------------------
95
+
96
+ function runKeygen(args, cwd) {
97
+ const outDir = path.resolve(cwd, args.out || '.');
98
+ fs.mkdirSync(outDir, { recursive: true });
99
+ const pair = generateThresholdKeypair();
100
+
101
+ const pubPath = path.join(outDir, `threshold-${pair.keyId}.pub.json`);
102
+ const privPath = path.join(outDir, `threshold-${pair.keyId}.key.json`);
103
+ const note =
104
+ 'WAVE-1 STAND-IN: a SINGLE X25519 keypair, not a threshold (M-of-N) group key. ' +
105
+ 'Whoever holds the private key can decrypt alone. Suitable for an organizer-run ' +
106
+ 'contest node holding its own refs; NOT zero-knowledge custody (that is the ' +
107
+ 'Wave-2 FROST key ceremony, champollion.dev/docs/network/sovereignty/sovereign-eval-node).';
108
+
109
+ fs.writeFileSync(pubPath, JSON.stringify({
110
+ keyId: pair.keyId,
111
+ keyScheme: WAVE1_KEY_SCHEME,
112
+ publicKeyDerB64: pair.publicKeyDerB64,
113
+ _note: note,
114
+ }, null, 2) + '\n');
115
+ // The .key.json carries BOTH halves: the embedded public key lets the file
116
+ // double as a --threshold-pubkey source (readKeyMaterial picks the half a
117
+ // flag needs), so a keygen dir with only the .key.json can still seal.
118
+ fs.writeFileSync(privPath, JSON.stringify({
119
+ keyId: pair.keyId,
120
+ keyScheme: WAVE1_KEY_SCHEME,
121
+ publicKeyDerB64: pair.publicKeyDerB64,
122
+ privateKeyDerB64: pair.privateKeyDerB64,
123
+ _note: note + ' Contains both halves (the public key is embedded so this file '
124
+ + 'also works as a --threshold-pubkey source). Keep this file OFF git and '
125
+ + 'off any shared storage.',
126
+ }, null, 2) + '\n', { mode: 0o600 });
127
+
128
+ console.log(` Key id: ${pair.keyId}`);
129
+ console.log(` Scheme: ${WAVE1_KEY_SCHEME} (stand-in — see note in the files)`);
130
+ console.log(` Public key: ${pubPath}`);
131
+ console.log(` Private key: ${privPath} (mode 600 — never commit, never upload)`);
132
+ return 0;
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // seal
137
+ // ---------------------------------------------------------------------------
138
+
139
+ function runSeal(args, cwd) {
140
+ const input = args['seal-input'];
141
+ const cardId = args.id;
142
+ const custodianGroupId = args['custodian-group'];
143
+ if (!input) return fail('--seal-input <corpus file> is required.');
144
+ if (!cardId) return fail('--id <card id> is required (the AAD binds ciphertext to card).');
145
+ if (!custodianGroupId) return fail('--custodian-group <opaque id> is required (never a real org name pre-consent).');
146
+ if (!args['threshold-pubkey']) return fail('--threshold-pubkey <file|b64|PEM> is required.');
147
+
148
+ let plaintext;
149
+ try {
150
+ plaintext = fs.readFileSync(path.resolve(cwd, input));
151
+ } catch (e) {
152
+ return fail(`cannot read --seal-input ${input}: ${e.message}`);
153
+ }
154
+ if (!plaintext.length) return fail(`--seal-input ${input} is empty — nothing to seal.`);
155
+
156
+ const aad = buildAad({ cardId, custodianGroupId });
157
+ let sealed;
158
+ try {
159
+ sealed = sealPlaintext({
160
+ plaintext,
161
+ thresholdPublicKey: readKeyMaterial(args['threshold-pubkey'], '--threshold-pubkey'),
162
+ aad,
163
+ });
164
+ } catch (e) {
165
+ return fail(e.message);
166
+ }
167
+ plaintext = null;
168
+
169
+ const artifactPath = path.resolve(cwd, args['seal-out'] || `${cardId}.sealed.json`);
170
+ try {
171
+ refuseTrackedPath(artifactPath, 'the sealed (ciphertext) artifact');
172
+ fs.writeFileSync(artifactPath, JSON.stringify(buildSealedArtifact({
173
+ sealed, cardId, custodianGroupId, createdAt: new Date().toISOString(),
174
+ }), null, 2) + '\n');
175
+ } catch (e) {
176
+ return fail(e.message);
177
+ }
178
+
179
+ const cardBlock = buildSealedCardBlock({
180
+ sealed,
181
+ custodianGroupId,
182
+ keyScheme: args['key-scheme'] || WAVE1_KEY_SCHEME,
183
+ qualifierId: args['qualifier-id'] || null,
184
+ qualifierThreshold: args['qualifier-threshold']
185
+ ? Number(args['qualifier-threshold']) : null,
186
+ artifactRef: artifactPath,
187
+ });
188
+ const blockPath = args['card-block-out']
189
+ ? path.resolve(cwd, args['card-block-out']) : null;
190
+ if (blockPath) {
191
+ fs.writeFileSync(blockPath, JSON.stringify(cardBlock, null, 2) + '\n');
192
+ }
193
+
194
+ console.log(` Cipher: ${SEAL_CIPHER}`);
195
+ console.log(` Ciphertext digest: ${sealed.ciphertextDigest}`);
196
+ console.log(` Artifact: ${artifactPath} (ciphertext only — keep OFF git)`);
197
+ if (blockPath) console.log(` Card block: ${blockPath}`);
198
+ else console.log(` Card block: ${JSON.stringify(cardBlock)}`);
199
+ // Registration is the harness's lane (this CLI only ever reads Supabase):
200
+ // organizers register the digest content-free themselves — no curator
201
+ // issue, no service key (migration 046 self-serve door).
202
+ console.log(' Register (self-serve, your own sign-in): ' +
203
+ 'mt-eval contest register --manifest <local/manifest.json> — or ' +
204
+ '`mt-eval contest prepare --self-serve` end-to-end.');
205
+ return 0;
206
+ }
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // open — the controlled-eval-context decrypt.
210
+ // ---------------------------------------------------------------------------
211
+
212
+ function runOpen(args, cwd) {
213
+ if (!args.artifact) return fail('--artifact <sealed artifact json> is required.');
214
+ if (!args.privkey) return fail('--privkey <file|b64|PEM> is required.');
215
+ if (!args.out) return fail('--out <plaintext destination> is required (write to your scratch dir; wipe after use).');
216
+
217
+ let artifact;
218
+ try {
219
+ artifact = JSON.parse(fs.readFileSync(path.resolve(cwd, args.artifact), 'utf-8'));
220
+ } catch (e) {
221
+ return fail(`cannot read --artifact ${args.artifact}: ${e.message}`);
222
+ }
223
+
224
+ let plaintext;
225
+ try {
226
+ plaintext = openSealed({
227
+ artifact,
228
+ thresholdPrivateKey: readKeyMaterial(args.privkey, '--privkey', 'private'),
229
+ });
230
+ } catch (e) {
231
+ return fail(`decryption failed (wrong key, tampered ciphertext, or AAD mismatch): ${e.message}`);
232
+ }
233
+
234
+ const outPath = path.resolve(cwd, args.out);
235
+ try {
236
+ refuseTrackedPath(outPath, 'decrypted plaintext');
237
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
238
+ fs.writeFileSync(outPath, plaintext, { mode: 0o600 });
239
+ } catch (e) {
240
+ return fail(e.message);
241
+ }
242
+ console.log(` Decrypted ${plaintext.length} bytes -> ${outPath}`);
243
+ console.log(' Scratch plaintext: score, then WIPE (the organizer node does this automatically).');
244
+ return 0;
245
+ }
246
+
247
+ // ---------------------------------------------------------------------------
248
+ // sign-keygen / sign / verify — score-bundle signing for the Phase-B airgap
249
+ // transport (arena/mt_eval_harness/airgap_transport.py shells out here; the
250
+ // crypto lives in lib/seal.mjs). Single-node Ed25519 — NOT steward custody.
251
+ // ---------------------------------------------------------------------------
252
+
253
+ function runSignKeygen(args, cwd) {
254
+ const outDir = path.resolve(cwd, args.out || '.');
255
+ fs.mkdirSync(outDir, { recursive: true });
256
+ const pair = generateSigningKeypair();
257
+ const note =
258
+ `${SIGN_SCHEME}: a SINGLE Ed25519 node signing key. It authenticates one ` +
259
+ 'scoring node’s exported score bundles (airgap transport); it is NOT the ' +
260
+ '3-of-5 steward custody of the sandbox spec §12 (Wave 2).';
261
+
262
+ const pubPath = path.join(outDir, `score-sign-${pair.keyId}.pub.json`);
263
+ const privPath = path.join(outDir, `score-sign-${pair.keyId}.key.json`);
264
+ fs.writeFileSync(pubPath, JSON.stringify({
265
+ keyId: pair.keyId, keyScheme: SIGN_SCHEME,
266
+ publicKeyDerB64: pair.publicKeyDerB64, _note: note,
267
+ }, null, 2) + '\n');
268
+ fs.writeFileSync(privPath, JSON.stringify({
269
+ keyId: pair.keyId, keyScheme: SIGN_SCHEME,
270
+ privateKeyDerB64: pair.privateKeyDerB64,
271
+ _note: note + ' Keep this file on the AIRGAPPED node only.',
272
+ }, null, 2) + '\n', { mode: 0o600 });
273
+
274
+ console.log(` Key id: ${pair.keyId}`);
275
+ console.log(` Scheme: ${SIGN_SCHEME}`);
276
+ console.log(` Public key: ${pubPath} (give this to the RELAY side)`);
277
+ console.log(` Private key: ${privPath} (mode 600 — airgapped node only)`);
278
+ return 0;
279
+ }
280
+
281
+ function runSign(args, cwd) {
282
+ if (!args.payload) return fail('--payload <file> is required.');
283
+ if (!args.privkey) return fail('--privkey <file|b64> is required.');
284
+ let payload;
285
+ try {
286
+ payload = fs.readFileSync(path.resolve(cwd, args.payload));
287
+ } catch (e) {
288
+ return fail(`cannot read --payload ${args.payload}: ${e.message}`);
289
+ }
290
+ let block;
291
+ try {
292
+ block = signPayload({
293
+ payload,
294
+ signingPrivateKey: readKeyMaterial(args.privkey, '--privkey', 'private'),
295
+ });
296
+ } catch (e) {
297
+ return fail(e.message);
298
+ }
299
+ block.signedAt = new Date().toISOString();
300
+ const sigPath = path.resolve(cwd, args['sig-out'] || `${args.payload}.sig.json`);
301
+ fs.writeFileSync(sigPath, JSON.stringify(block, null, 2) + '\n');
302
+ console.log(` Scheme: ${block.scheme}`);
303
+ console.log(` Key id: ${block.keyId}`);
304
+ console.log(` Payload: sha256 ${block.payloadSha256}`);
305
+ console.log(` Signature: ${sigPath}`);
306
+ return 0;
307
+ }
308
+
309
+ function runVerify(args, cwd) {
310
+ if (!args.payload) return fail('--payload <file> is required.');
311
+ if (!args.sig) return fail('--sig <signature block json> is required.');
312
+ if (!args.pubkey) return fail('--pubkey <file|b64> is required.');
313
+ let payload; let block;
314
+ try {
315
+ payload = fs.readFileSync(path.resolve(cwd, args.payload));
316
+ block = JSON.parse(fs.readFileSync(path.resolve(cwd, args.sig), 'utf-8'));
317
+ } catch (e) {
318
+ return fail(`cannot read inputs: ${e.message}`);
319
+ }
320
+ let ok;
321
+ try {
322
+ ok = verifyPayload({
323
+ payload,
324
+ signatureB64: block.signatureB64,
325
+ verifyPublicKey: readKeyMaterial(args.pubkey, '--pubkey'),
326
+ });
327
+ } catch (e) {
328
+ return fail(e.message);
329
+ }
330
+ if (!ok) return fail('SIGNATURE INVALID — the payload is not what the node signed. Refuse it.');
331
+ console.log(` Signature VALID (key ${block.keyId}, ${block.scheme || SIGN_SCHEME}).`);
332
+ return 0;
333
+ }
334
+
335
+ // ---------------------------------------------------------------------------
336
+
337
+ export async function run(args, cwd) {
338
+ const verb = args._[1];
339
+ if (verb === 'keygen') return runKeygen(args, cwd);
340
+ if (verb === 'seal') return runSeal(args, cwd);
341
+ if (verb === 'open') return runOpen(args, cwd);
342
+ if (verb === 'sign-keygen') return runSignKeygen(args, cwd);
343
+ if (verb === 'sign') return runSign(args, cwd);
344
+ if (verb === 'verify') return runVerify(args, cwd);
345
+ console.error('[ERR] Usage: champollion seal-corpus <keygen|seal|open|sign-keygen|sign|verify> [flags]');
346
+ console.error(' keygen --out <dir>');
347
+ console.error(' seal --seal-input <file> --id <card-id> --custodian-group <id>');
348
+ console.error(' --threshold-pubkey <file|b64> [--seal-out <file>] [--card-block-out <file>]');
349
+ console.error(' [--qualifier-id <id> --qualifier-threshold <n>] [--key-scheme <label>]');
350
+ console.error(' open --artifact <file> --privkey <file|b64> --out <scratch path>');
351
+ console.error(' sign-keygen --out <dir>');
352
+ console.error(' sign --payload <file> --privkey <file|b64> [--sig-out <file>]');
353
+ console.error(' verify --payload <file> --sig <file> --pubkey <file|b64>');
354
+ return 1;
355
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Command: seo
3
+ *
4
+ * Generates SEO artifacts for multilingual sites:
5
+ * - hreflang: <link rel="alternate" hreflang> tags
6
+ * - sitemap: multilingual sitemap.xml
7
+ * - jsonld: JSON-LD WebSite language schema
8
+ */
9
+
10
+ import fs from 'node:fs';
11
+ import { resolveConfig } from '../config.js';
12
+ import { generateAllHreflangTags, generateSitemap, generateJsonLd } from '../seo.js';
13
+ import { output } from '../output.js';
14
+
15
+ /**
16
+ * @param {import('../types.js').CLIArgs} args - Parsed CLI arguments
17
+ * @param {string} cwd - Working directory
18
+ * @returns {Promise<number>} Exit code (0 = success, 1 = error)
19
+ */
20
+ async function run(args, cwd) {
21
+ const subcommand = args._[1] || 'help';
22
+ const config = resolveConfig(args, cwd);
23
+
24
+ // CLI override for baseUrl
25
+ if (args['base-url']) {
26
+ if (!config.seo) config.seo = {};
27
+ config.seo.baseUrl = args['base-url'];
28
+ }
29
+
30
+ if (subcommand === 'hreflang') {
31
+ const result = generateAllHreflangTags(config);
32
+ output.raw(result);
33
+ } else if (subcommand === 'sitemap') {
34
+ const result = generateSitemap(config);
35
+ if (args.out) {
36
+ fs.writeFileSync(args.out, result, 'utf-8');
37
+ output.ok(`Sitemap written to ${args.out}`);
38
+ } else {
39
+ output.raw(result);
40
+ }
41
+ } else if (subcommand === 'jsonld') {
42
+ const result = generateJsonLd(config);
43
+ output.raw(result);
44
+ } else if (subcommand === 'help') {
45
+ printHelp();
46
+ } else {
47
+ // Unknown subcommand — print help to stderr and exit non-zero so scripts
48
+ // (and the user) can tell a typo from a successful run. Matches `tm`.
49
+ output.error(`Unknown subcommand: "${subcommand}". Run "champollion seo help" for usage.`);
50
+ printHelp();
51
+ return 1;
52
+ }
53
+
54
+ return 0;
55
+ }
56
+
57
+ function printHelp() {
58
+ output.raw(`
59
+ champollion seo — Generate SEO artifacts
60
+
61
+ SUBCOMMANDS
62
+ hreflang Generate <link rel="alternate" hreflang> tags
63
+ sitemap Generate multilingual sitemap.xml
64
+ jsonld Generate JSON-LD WebSite schema
65
+
66
+ OPTIONS
67
+ --base-url <url> Override site base URL
68
+ --out <path> Write sitemap to file (sitemap subcommand only)
69
+ `);
70
+ }
71
+
72
+ export { run };
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Command: serve
3
+ *
4
+ * Stands the owner's OWN configured translation stack up as an HTTP
5
+ * service speaking the api-method contract (see lib/serve.js), so any
6
+ * consumer project can point `method: "api"` at it. The hosted
7
+ * Champollion Translate API was shelved (founder, 2026-07-19) — this
8
+ * self-hosted path replaces it: "serve through Champollion" is one
9
+ * command on each side.
10
+ *
11
+ * Two modes:
12
+ * champollion serve — run the server (blocks until SIGINT/SIGTERM)
13
+ * champollion serve --emit-manifest — write the method.json a consumer installs
14
+ */
15
+
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+
19
+ import {
20
+ SERVE_DEFAULTS,
21
+ createServeRuntime,
22
+ deriveMethodName,
23
+ resolveManifestEndpoint,
24
+ buildServeManifest,
25
+ startServeServer,
26
+ } from '../serve.js';
27
+ import { parseMaxCost } from '../cost-report.js';
28
+ import { getEnvOrFileVar } from '../api-key.js';
29
+ import { output } from '../output.js';
30
+
31
+ /**
32
+ * Parse a required-integer flag with bounds. Malformed values fail loud —
33
+ * a silently-ignored bad value would defeat the safety knob it configures.
34
+ */
35
+ function parseIntFlag(raw, flagName, { min, max, fallback }) {
36
+ if (raw === undefined || raw === null || raw === false || raw === '') return fallback;
37
+ const val = Number.parseInt(String(raw), 10);
38
+ if (!Number.isInteger(val) || val < min || val > max) {
39
+ throw new Error(`${flagName} must be an integer between ${min} and ${max} (got "${raw}").`);
40
+ }
41
+ return val;
42
+ }
43
+
44
+ /**
45
+ * Resolve where --emit-manifest writes. A path ending in .json is the file
46
+ * itself; anything else is a directory that gets method.json inside it
47
+ * (the shape `champollion plugin install <dir>` expects).
48
+ */
49
+ function resolveManifestOutPath(rawOut, methodName, cwd) {
50
+ if (rawOut) {
51
+ const resolved = path.resolve(cwd, String(rawOut));
52
+ return resolved.endsWith('.json') ? resolved : path.join(resolved, 'method.json');
53
+ }
54
+ return path.join(cwd, methodName, 'method.json');
55
+ }
56
+
57
+ /**
58
+ * @param {import('../types.js').CLIArgs} args - Parsed CLI arguments
59
+ * @param {string} cwd - Working directory (the owner project)
60
+ * @returns {Promise<number>} Exit code
61
+ */
62
+ async function run(args, cwd) {
63
+ if (args.quiet) output.setMode('quiet');
64
+
65
+ const port = parseIntFlag(args.port, '--port', { min: 0, max: 65535, fallback: SERVE_DEFAULTS.port });
66
+ const methodName = args.name ? String(args.name) : deriveMethodName(cwd);
67
+
68
+ // ── Manifest emission — no server, no API calls, no preflight ─────────
69
+ if (args['emit-manifest']) {
70
+ const runtime = await createServeRuntime({ cwd, cliArgs: args, preflight: false });
71
+ const endpointUrl = resolveManifestEndpoint(args.endpoint || null, port);
72
+ const manifest = buildServeManifest(runtime, { name: methodName, endpointUrl });
73
+
74
+ const outPath = resolveManifestOutPath(args.out, methodName, cwd);
75
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
76
+ fs.writeFileSync(outPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
77
+
78
+ output.ok(`Wrote ${outPath}`);
79
+ output.info(`Served method: ${methodName} (type api, quality tier "${manifest.config.qualityTier}")`);
80
+ output.info(`Endpoint: ${endpointUrl}`);
81
+ if (!args.endpoint) {
82
+ output.warn('Endpoint defaults to loopback — pass --endpoint <public-url> when consumers are not on this machine.');
83
+ }
84
+ output.raw('');
85
+ output.raw(' Consumer setup:');
86
+ output.raw(` champollion plugin install ${path.dirname(outPath)}`);
87
+ output.raw(` # champollion.config.json: { "pairs": { "<src>:<tgt>": { "methodPlugin": "${methodName}" } } }`);
88
+ output.raw(' CHAMPOLLION_API_KEY=<this server\'s bearer token> champollion sync');
89
+ output.summary({ command: 'serve', emitted: outPath, name: methodName, endpoint: endpointUrl });
90
+ return 0;
91
+ }
92
+
93
+ // ── Server mode ───────────────────────────────────────────────────────
94
+ const bind = args.bind ? String(args.bind) : SERVE_DEFAULTS.bind;
95
+ const token = args.token ? String(args.token) : getEnvOrFileVar('CHAMPOLLION_SERVE_TOKEN', cwd);
96
+ const noAuth = !!args['no-auth'];
97
+ const rateLimitPerMin = parseIntFlag(args['rate-limit'], '--rate-limit', { min: 0, max: 1_000_000, fallback: SERVE_DEFAULTS.rateLimitPerMin });
98
+ const maxBodyBytes = parseIntFlag(args['max-body-bytes'], '--max-body-bytes', { min: 1024, max: 1_000_000_000, fallback: SERVE_DEFAULTS.maxBodyBytes });
99
+ // parseMaxCost is the same validator `sync --max-cost` uses (loud on junk).
100
+ const maxCostPerRequest = parseMaxCost(args['max-cost-per-request']);
101
+ const maxSessionCost = parseMaxCost(args['max-session-cost']);
102
+
103
+ const handle = await startServeServer({
104
+ cwd,
105
+ cliArgs: args,
106
+ bind,
107
+ port,
108
+ token,
109
+ noAuth,
110
+ rateLimitPerMin,
111
+ maxBodyBytes,
112
+ maxCostPerRequest,
113
+ maxSessionCost,
114
+ methodName,
115
+ });
116
+
117
+ const pairList = [...handle.runtime.resolvedPairs.keys()].sort().join(', ');
118
+ output.ok(`champollion serve listening on http://${bind}:${handle.port}/translate`);
119
+ output.info(`Serving method "${handle.methodName}" — pairs: ${pairList}`);
120
+ output.info(noAuth
121
+ ? 'Auth: DISABLED (--no-auth, loopback only)'
122
+ : 'Auth: bearer token required (Authorization: Bearer <token>)');
123
+ output.info(`Limits: ${rateLimitPerMin === 0 ? 'no rate limit' : `${rateLimitPerMin} req/min/IP`}, body ≤ ${maxBodyBytes} bytes`
124
+ + (maxCostPerRequest !== null ? `, ≤ $${maxCostPerRequest}/request` : '')
125
+ + (maxSessionCost !== null ? `, session ceiling $${maxSessionCost}` : ''));
126
+ if (bind === SERVE_DEFAULTS.bind) {
127
+ output.info('Bound to 127.0.0.1 (this machine only). Use --bind 0.0.0.0 to expose it — with a strong token.');
128
+ }
129
+ output.info(`Consumer manifest: champollion serve --emit-manifest${args.endpoint ? '' : ' --endpoint <public-url>'}`);
130
+ output.info('Press Ctrl+C to stop.');
131
+
132
+ // Block until a shutdown signal; the dispatcher exits with our return code.
133
+ return new Promise((resolve) => {
134
+ let closing = false;
135
+ const shutdown = async (signal) => {
136
+ if (closing) return;
137
+ closing = true;
138
+ output.info(`\n[serve] ${signal} received — shutting down (served ${handle.session.requests} request(s)).`);
139
+ await handle.close();
140
+ resolve(0);
141
+ };
142
+ process.once('SIGINT', () => shutdown('SIGINT'));
143
+ process.once('SIGTERM', () => shutdown('SIGTERM'));
144
+ });
145
+ }
146
+
147
+ export { run };