pazair-verify 1.10.0 → 1.11.0

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.
package/README.md CHANGED
@@ -131,6 +131,7 @@ console.log(await verifyReceipt(receipt, { delivery }));
131
131
  | `verifyCredential(vc, { fetch?, didDocument?, keysDocument?, now? })` | Section 18: the pass as a W3C Verifiable Credential: VC 2.0, the did:web resolved, the `eddsa-jcs-2022` proof, the subject on the issuer's origin, `validFrom`/`validUntil`, the pass inside; a verdict and a sentence |
132
132
  | `verifyDataIntegrity(doc, didDocument)` / `signDataIntegrity(doc, privateKey, verificationMethod, created?)` | any W3C Data Integrity proof with `eddsa-jcs-2022`, checked or made (the W3C Recommendation's vector holds) |
133
133
  | `resolveDidWeb(did, fetch?)`, `didWebUrl(did)`, `multikeyOf(x)`, `multikeyToX(multibase)`, `base58btcEncode`/`base58btcDecode` | did:web, Multikey (Ed25519) and base58btc, the pieces of section 18 |
134
+ | `verifyLicence(licence, { keys?, pqKeys?, now? })` / `checkLicence(url, { fetch?, now? })` | Section 19, the buyer's licence from an apprenticeship: both signatures, the rules of its level (a signed licence under its pass mark is invalid), whether it is current; `checkLicence` reads a licence URL against the issuer's own keys (`npx pazair-verify licence <url>`) |
134
135
  | `leafOf(pass)`, `canonical(obj)`, `sha256hex(s)` | the building blocks of the spec |
135
136
  | `fetchKeys(origin?)` | the issuer's keys from `/.well-known/pazair-receipts.json` |
136
137
 
package/bin/cli.js CHANGED
@@ -3,9 +3,10 @@
3
3
  // npx pazair-verify remember [origin] [--json]: Remember, the issuer's chain of daily roots, recomputed here.
4
4
  // npx pazair-verify witness <day:head> [origin] [--json]: a head you kept, against the chain you recompute.
5
5
  // npx pazair-verify credential <credential url or file> [--json]: the Word Pass as a W3C Verifiable Credential (SPEC 18), then its anchors.
6
+ // npx pazair-verify licence <licence url> [--json]: the buyer's licence from the apprenticeship (SPEC 19), against the issuer's keys.
6
7
  // Exit codes: 0 trusted / consistent, 1 invalid / inconsistent (do not rely on it), 2 usage, 3 unreachable (could not check).
7
8
  import { readFileSync } from 'node:fs';
8
- import { checkHead, checkRemember, checkWordPass, verifyCredential } from '../src/index.js';
9
+ import { checkHead, checkLicence, checkRemember, checkWordPass, verifyCredential } from '../src/index.js';
9
10
 
10
11
  const args = process.argv.slice(2);
11
12
  if (args.includes('-v') || args.includes('--version')) {
@@ -15,10 +16,10 @@ if (args.includes('-v') || args.includes('--version')) {
15
16
  const words = args.filter((a) => !a.startsWith('-'));
16
17
  const help = args.includes('-h') || args.includes('--help');
17
18
  const json = args.includes('--json');
18
- const usage = 'Usage: npx pazair-verify <word pass url> [--json]\n npx pazair-verify remember [origin] [--json]\n npx pazair-verify witness <day:head> [origin] [--json]\n npx pazair-verify credential <credential url or file> [--json]\nExamples: npx pazair-verify https://pazair.kulalabs.ch/v1/agents/ag_xyz/pass\n npx pazair-verify remember https://pazair.kulalabs.ch\n npx pazair-verify witness 2026-10-10:3f9c… https://pazair.kulalabs.ch\n npx pazair-verify credential https://pazair.ch/v1/agents/ag_xyz/pass.vc';
19
+ const usage = 'Usage: npx pazair-verify <word pass url> [--json]\n npx pazair-verify remember [origin] [--json]\n npx pazair-verify witness <day:head> [origin] [--json]\n npx pazair-verify credential <credential url or file> [--json]\n npx pazair-verify licence <licence url> [--json]\nExamples: npx pazair-verify https://pazair.kulalabs.ch/v1/agents/ag_xyz/pass\n npx pazair-verify remember https://pazair.kulalabs.ch\n npx pazair-verify witness 2026-10-10:3f9c… https://pazair.kulalabs.ch\n npx pazair-verify credential https://pazair.ch/v1/agents/ag_xyz/pass.vc\n npx pazair-verify licence https://pazair.ch/v1/agents/ag_xyz/licence';
19
20
  if (!words.length || help) { (help ? console.log : console.error)(usage); process.exit(help ? 0 : 2); }
20
21
  const clean = (s) => String(s).replace(/[\u0000-\u001f\u007f-\u009f]/g, '?'); // nothing from the network rewrites your terminal
21
- const codes = { valid: 0, consistent: 0, kept_its_word: 0, no_badge_yet: 0, signed_unanchored: 0, inconsistent: 1, invalid: 1, unreachable: 3 };
22
+ const codes = { valid: 0, consistent: 0, expired: 1, none: 1, kept_its_word: 0, no_badge_yet: 0, signed_unanchored: 0, inconsistent: 1, invalid: 1, unreachable: 3 };
22
23
 
23
24
  if (words[0] === 'remember') {
24
25
  const r = await checkRemember(words[1] ?? 'https://pazair.kulalabs.ch');
@@ -48,6 +49,13 @@ if (words[0] === 'credential') {
48
49
  if (json) console.log(JSON.stringify({ ...r, anchors: anchors ? { ...anchors, pass: undefined } : null }, null, 2));
49
50
  process.exit(!r.valid ? (codes[r.trust] ?? 1) : (codes[anchors.trust] ?? 1));
50
51
  }
52
+ if (words[0] === 'licence' || words[0] === 'license') {
53
+ if (!words[1]) { console.error(usage); process.exit(2); }
54
+ const r = await checkLicence(words[1]);
55
+ console.log(clean(r.say));
56
+ if (json) console.log(JSON.stringify(r, null, 2));
57
+ process.exit(codes[r.verdict] ?? 1);
58
+ }
51
59
  const r = await checkWordPass(words[0]);
52
60
  console.log(clean(r.say));
53
61
  if (json) console.log(JSON.stringify({ ...r, pass: undefined }, null, 2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pazair-verify",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "Verify Word Passes, receipts, Merkle proofs and Remember (the chain of daily roots, Ed25519 + ML-DSA-65): checked by anyone, for any issuer, without trusting it. Zero dependencies.",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/index.d.ts CHANGED
@@ -88,3 +88,11 @@ export type CredentialCheck = {
88
88
  pass: Awaited<ReturnType<typeof verifyPass>> | null; same_key: boolean | null;
89
89
  };
90
90
  export function verifyCredential(vc: unknown, opts?: { fetch?: typeof fetch; didDocument?: DidDocument; keysDocument?: { keys: Jwk[]; pq_keys?: PqKey[] }; now?: number }): Promise<CredentialCheck>;
91
+
92
+ // The buyer's licence (SPEC section 19, the apprenticeship).
93
+ export const LICENCE_LEVELS: Record<'basic' | 'professional' | 'critical', { tasks: number; need: number; valid_days: number }>;
94
+ export type LicenceCheck = { valid: boolean; current: boolean; signature_valid: boolean; pq_signed: boolean; ml_dsa_65: boolean | null; rules_hold: boolean; level: string | null; agent: string | null; valid_until: string | null };
95
+ /** Section 19: both signatures and the rules of its level; `current` says whether it has expired. */
96
+ export function verifyLicence(licence: unknown, opts?: { keys?: Jwk[]; pqKeys?: PqKey[]; now?: Date }): Promise<LicenceCheck>;
97
+ /** Section 19: a licence URL checked against the issuer's own keys. */
98
+ export function checkLicence(url: string, opts?: { fetch?: typeof fetch; now?: Date }): Promise<{ verdict: 'valid' | 'expired' | 'none' | 'invalid' | 'unreachable'; agent?: string | null; level?: string; say: string; licences: LicenceCheck[] }>;
package/src/index.js CHANGED
@@ -404,3 +404,50 @@ export { FINGERPRINT, rsEncode, fingerprintOf, fingerprintDistance, fingerprintS
404
404
 
405
405
  // The Word Pass as a W3C Verifiable Credential 2.0 (SPEC section 18): did:web, Multikey, Data Integrity eddsa-jcs-2022.
406
406
  export { base58btcEncode, base58btcDecode, multikeyOf, multikeyToX, didWebUrl, resolveDidWeb, verifyDataIntegrity, verifyCredential, signDataIntegrity } from './credential.js';
407
+
408
+ // The buyer's licence (SPEC section 19, the apprenticeship): an agent passed an exam of buying for someone else.
409
+ /** Section 19: the levels: tasks in the exam, the pass mark, the days a licence is valid. */
410
+ export const LICENCE_LEVELS = { basic: { tasks: 5, need: 4, valid_days: 365 }, professional: { tasks: 8, need: 7, valid_days: 180 }, critical: { tasks: 10, need: 10, valid_days: 90 } };
411
+
412
+ /**
413
+ * Section 19: a buyer's licence. Both signatures, and the rules of its level: the score reaches the pass mark, the
414
+ * tasks match the level, and it is valid no longer than the level allows. A signed licence that breaks its own level's
415
+ * rules is invalid. `current` says whether it has expired; an expired licence is genuine but no longer shows anything.
416
+ */
417
+ export async function verifyLicence(licence, { keys, pqKeys, now = new Date() } = {}) {
418
+ keys ??= await fetchKeys();
419
+ const l = licence && typeof licence === 'object' ? licence : {}, lv = LICENCE_LEVELS[l.level];
420
+ const signature_valid = l.kind === 'buyer_licence' && typeof l.issuer === 'string' && (await verifySignature(l, keys));
421
+ const pq = await verifyPqSignature(l, pqKeys);
422
+ const [right, of] = String(l.score ?? '').split('/').map(Number);
423
+ const days = (Date.parse(l.valid_until) - Date.parse(l.issued_at)) / 864e5;
424
+ const rules_hold = !!lv && of === lv.tasks && right >= lv.need && right <= of && Array.isArray(l.tasks) && l.tasks.length === of && days > 0 && days <= lv.valid_days;
425
+ const current = typeof l.valid_until === 'string' && l.valid_until > now.toISOString();
426
+ return { valid: signature_valid && pq.valid !== false && rules_hold, current, signature_valid, pq_signed: pq.signed, ml_dsa_65: pq.valid, rules_hold, level: l.level ?? null, agent: l.agent ?? null, valid_until: l.valid_until ?? null };
427
+ }
428
+
429
+ /**
430
+ * Section 19: "Has this agent passed the apprenticeship?", from a licence URL (e.g. <origin>/v1/agents/<id>/licence),
431
+ * checked against the issuer's own keys. verdict: valid (a current licence), expired, none, invalid or unreachable.
432
+ */
433
+ export async function checkLicence(url, { fetch: f = fetch, now = new Date() } = {}) {
434
+ let u; try { u = new URL(url); } catch { return { verdict: 'invalid', say: 'Not a URL.', licences: [] }; }
435
+ if (u.protocol !== 'https:') return { verdict: 'invalid', say: 'Only https URLs are checked.', licences: [] };
436
+ let doc, kd;
437
+ try { const r = await f(u.href, { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(8000) }); if (!r.ok) throw new Error(); doc = await r.json(); }
438
+ catch { return { verdict: 'unreachable', say: `No licence could be read at ${u.host}.`, licences: [] }; }
439
+ try { kd = await fetchKeysDocument(u.origin, f); } catch { return { verdict: 'unreachable', say: `${u.host} publishes no keys at /.well-known/pazair-receipts.json.`, licences: [] }; }
440
+ const list = Array.isArray(doc?.licences) ? doc.licences.map((x) => x?.licence ?? x) : doc?.kind === 'buyer_licence' ? [doc] : [];
441
+ const agent = typeof doc?.agent_id === 'string' ? doc.agent_id : list[0]?.agent ?? null;
442
+ const licences = await Promise.all(list.map(async (l) => {
443
+ const v = await verifyLicence(l, { keys: kd.keys, pqKeys: kd.pq_keys, now });
444
+ // A licence for another agent, served under this one, proves nothing about this one.
445
+ return agent && l?.agent !== agent ? { ...v, valid: false, rules_hold: false } : v;
446
+ }));
447
+ const day = (t) => String(t ?? '').slice(0, 10);
448
+ if (!licences.length) return { verdict: 'none', agent, say: `${agent ?? 'This agent'} holds no buyer's licence from ${u.host}.`, licences };
449
+ if (licences.some((v) => !v.valid)) return { verdict: 'invalid', agent, say: `Do not rely on it: a licence from ${u.host} does not verify against the issuer's keys or breaks its level's rules.`, licences };
450
+ const best = [...licences].reverse().find((v) => v.current);
451
+ if (!best) return { verdict: 'expired', agent, say: `${agent}: every buyer's licence from ${u.host} has expired (the last on ${day(licences.at(-1).valid_until)}).`, licences };
452
+ return { verdict: 'valid', agent, level: best.level, say: `${agent}: ${best.level} buyer's licence from ${u.host}, signatures valid${best.ml_dsa_65 ? ' (Ed25519 and ML-DSA-65)' : ''}, current until ${day(best.valid_until)}. It shows the exam was passed that day; it is no regulatory certification.`, licences };
453
+ }