@naulon/shared 0.0.1
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/dist/attribution.d.ts +92 -0
- package/dist/attribution.d.ts.map +1 -0
- package/dist/attribution.js +145 -0
- package/dist/attribution.js.map +1 -0
- package/dist/botAuthSign.d.ts +74 -0
- package/dist/botAuthSign.d.ts.map +1 -0
- package/dist/botAuthSign.js +90 -0
- package/dist/botAuthSign.js.map +1 -0
- package/dist/config.d.ts +131 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +400 -0
- package/dist/config.js.map +1 -0
- package/dist/credits.d.ts +7 -0
- package/dist/credits.d.ts.map +1 -0
- package/dist/credits.js +7 -0
- package/dist/credits.js.map +1 -0
- package/dist/eip3009.d.ts +69 -0
- package/dist/eip3009.d.ts.map +1 -0
- package/dist/eip3009.js +34 -0
- package/dist/eip3009.js.map +1 -0
- package/dist/eventsink.d.ts +19 -0
- package/dist/eventsink.d.ts.map +1 -0
- package/dist/eventsink.js +110 -0
- package/dist/eventsink.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/license.d.ts +178 -0
- package/dist/license.d.ts.map +1 -0
- package/dist/license.js +252 -0
- package/dist/license.js.map +1 -0
- package/dist/networks.d.ts +63 -0
- package/dist/networks.d.ts.map +1 -0
- package/dist/networks.js +84 -0
- package/dist/networks.js.map +1 -0
- package/dist/observationsink.d.ts +22 -0
- package/dist/observationsink.d.ts.map +1 -0
- package/dist/observationsink.js +83 -0
- package/dist/observationsink.js.map +1 -0
- package/dist/publisher.d.ts +209 -0
- package/dist/publisher.d.ts.map +1 -0
- package/dist/publisher.js +2 -0
- package/dist/publisher.js.map +1 -0
- package/dist/settlementEmit.d.ts +31 -0
- package/dist/settlementEmit.d.ts.map +1 -0
- package/dist/settlementEmit.js +65 -0
- package/dist/settlementEmit.js.map +1 -0
- package/dist/supabase.d.ts +8 -0
- package/dist/supabase.d.ts.map +1 -0
- package/dist/supabase.js +47 -0
- package/dist/supabase.js.map +1 -0
- package/dist/types.d.ts +154 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +22 -0
- package/dist/types.js.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventSink implementations. The default is an append-only JSONL file keyed off
|
|
3
|
+
* EVENTS_PATH, so the tollgate (writer) and the dashboard/attribution (readers)
|
|
4
|
+
* all agree on one ledger without a database. Swap in another EventSink to move
|
|
5
|
+
* to Postgres/Supabase — callers don't change.
|
|
6
|
+
*/
|
|
7
|
+
import { appendFile, mkdir, readFile } from "node:fs/promises";
|
|
8
|
+
import { dirname, resolve } from "node:path";
|
|
9
|
+
import { getConfig } from "./config.js";
|
|
10
|
+
import { supabaseRest } from "./supabase.js";
|
|
11
|
+
export function jsonlSink(path) {
|
|
12
|
+
const file = resolve(path ?? getConfig().EVENTS_PATH);
|
|
13
|
+
return {
|
|
14
|
+
async record(event) {
|
|
15
|
+
await mkdir(dirname(file), { recursive: true });
|
|
16
|
+
await appendFile(file, JSON.stringify(event) + "\n", "utf8");
|
|
17
|
+
},
|
|
18
|
+
async readAll(publisherId) {
|
|
19
|
+
try {
|
|
20
|
+
const raw = await readFile(file, "utf8");
|
|
21
|
+
const events = raw
|
|
22
|
+
.split("\n")
|
|
23
|
+
.filter((l) => l.trim())
|
|
24
|
+
.map((l) => JSON.parse(l));
|
|
25
|
+
// jsonl is the single-box/dev backend, but honour the optional scope so a
|
|
26
|
+
// scoped read over a local ledger sees only that publisher's events.
|
|
27
|
+
return publisherId === undefined ? events : events.filter((e) => e.publisherId === publisherId);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (err.code === "ENOENT")
|
|
31
|
+
return [];
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
async get(id) {
|
|
36
|
+
try {
|
|
37
|
+
const raw = await readFile(file, "utf8");
|
|
38
|
+
for (const line of raw.split("\n")) {
|
|
39
|
+
if (!line.trim())
|
|
40
|
+
continue;
|
|
41
|
+
const event = JSON.parse(line);
|
|
42
|
+
if (event.id === id)
|
|
43
|
+
return event; // short-circuit on first match
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
if (err.code === "ENOENT")
|
|
49
|
+
return undefined;
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Supabase-backed sink. Each event is stored as one row: its `id` (primary key,
|
|
57
|
+
* so a retried write is idempotent), its `at` timestamp (indexed, for ordering),
|
|
58
|
+
* and the whole `AttributedEvent` as a jsonb `data` column — so `readAll` hands
|
|
59
|
+
* back the exact shape the writer stored, with no field-by-field mapping to drift.
|
|
60
|
+
* This is the backend to use on serverless/multi-instance hosts with no shared disk.
|
|
61
|
+
*/
|
|
62
|
+
export function supabaseSink() {
|
|
63
|
+
const table = getConfig().SUPABASE_EVENTS_TABLE;
|
|
64
|
+
return {
|
|
65
|
+
async record(event) {
|
|
66
|
+
await supabaseRest(`/rest/v1/${table}?on_conflict=id`, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
// Idempotent: a row with this id already? ignore, don't error.
|
|
69
|
+
headers: { Prefer: "resolution=ignore-duplicates" },
|
|
70
|
+
// `publisher` is a top-level column (not just inside `data`) so a scoped
|
|
71
|
+
// `readAll` can filter rows server-side. Null for single-tenant writes.
|
|
72
|
+
body: JSON.stringify([{ id: event.id, at: event.at, publisher: event.publisherId ?? null, data: event }]),
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
async readAll(publisherId) {
|
|
76
|
+
const scope = publisherId === undefined ? "" : `&publisher=eq.${encodeURIComponent(publisherId)}`;
|
|
77
|
+
const rows = (await supabaseRest(`/rest/v1/${table}?select=data&order=at.asc${scope}`));
|
|
78
|
+
return rows.map((r) => r.data);
|
|
79
|
+
},
|
|
80
|
+
async get(id) {
|
|
81
|
+
// Primary-key lookup — never reads the whole table.
|
|
82
|
+
const rows = (await supabaseRest(`/rest/v1/${table}?id=eq.${encodeURIComponent(id)}&select=data&limit=1`));
|
|
83
|
+
return rows[0]?.data;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Pick the EventSink the config asks for. JSONL file by default (offline, no
|
|
89
|
+
* creds); Supabase when EVENTS_BACKEND=supabase. Callers use this instead of
|
|
90
|
+
* naming a sink directly, so switching backends is one env var.
|
|
91
|
+
*/
|
|
92
|
+
export function getSink() {
|
|
93
|
+
return getConfig().EVENTS_BACKEND === "supabase" ? supabaseSink() : jsonlSink();
|
|
94
|
+
}
|
|
95
|
+
/** In-memory sink, handy for tests. */
|
|
96
|
+
export function memorySink(seed = []) {
|
|
97
|
+
const events = [...seed];
|
|
98
|
+
return {
|
|
99
|
+
async record(event) {
|
|
100
|
+
events.push(event);
|
|
101
|
+
},
|
|
102
|
+
async readAll(publisherId) {
|
|
103
|
+
return publisherId === undefined ? [...events] : events.filter((e) => e.publisherId === publisherId);
|
|
104
|
+
},
|
|
105
|
+
async get(id) {
|
|
106
|
+
return events.find((e) => e.id === id);
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=eventsink.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"eventsink.js","sourceRoot":"","sources":["../src/eventsink.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from "./types.ts";
|
|
2
|
+
export * from "./publisher.ts";
|
|
3
|
+
export * from "./config.ts";
|
|
4
|
+
export * from "./attribution.ts";
|
|
5
|
+
export * from "./eventsink.ts";
|
|
6
|
+
export * from "./observationsink.ts";
|
|
7
|
+
export * from "./supabase.ts";
|
|
8
|
+
export * from "./license.ts";
|
|
9
|
+
export * from "./networks.ts";
|
|
10
|
+
export * from "./eip3009.ts";
|
|
11
|
+
export * from "./credits.ts";
|
|
12
|
+
export * from "./settlementEmit.ts";
|
|
13
|
+
export * from "./botAuthSign.ts";
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./publisher.js";
|
|
3
|
+
export * from "./config.js";
|
|
4
|
+
export * from "./attribution.js";
|
|
5
|
+
export * from "./eventsink.js";
|
|
6
|
+
export * from "./observationsink.js";
|
|
7
|
+
export * from "./supabase.js";
|
|
8
|
+
export * from "./license.js";
|
|
9
|
+
export * from "./networks.js";
|
|
10
|
+
export * from "./eip3009.js";
|
|
11
|
+
export * from "./credits.js";
|
|
12
|
+
export * from "./settlementEmit.js";
|
|
13
|
+
export * from "./botAuthSign.js";
|
|
14
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Citation License Token (CLT) — sign + verify. See docs/citation-license.md.
|
|
3
|
+
*
|
|
4
|
+
* A CLT is a signed, independently verifiable receipt the tollgate hands an agent
|
|
5
|
+
* when it pays: proof of who paid, how much USDC, for which essay, to which
|
|
6
|
+
* author wallet(s), settled on-chain. It is a signed projection of the
|
|
7
|
+
* AttributedEvent already recorded per payment (jti = event.id). Re-presenting a
|
|
8
|
+
* valid unexpired CLT re-reads that essay free within a short window — so the toll
|
|
9
|
+
* becomes an asset the payer wants, not a tax it evades.
|
|
10
|
+
*
|
|
11
|
+
* Format: a strict RFC 7519 JWT, EdDSA-signed (Ed25519, node:crypto — no dep),
|
|
12
|
+
* verifiable by an unmodified jose/pyjwt client. Domain data lives under one
|
|
13
|
+
* namespaced `naulon` claim so it never shadows a registered JWT claim.
|
|
14
|
+
*
|
|
15
|
+
* SECURITY — two invariants this module enforces (a regression in either is a
|
|
16
|
+
* free-read bypass; see the adversarial tests in license.test.ts):
|
|
17
|
+
* 1. verify IGNORES the token's `alg` header and hard-pins Ed25519. The public
|
|
18
|
+
* key is world-readable at the JWKS endpoint, so trusting `alg` would allow
|
|
19
|
+
* alg:none and HMAC-with-the-public-key forgery. The verify routine is fixed
|
|
20
|
+
* to crypto.verify over the LITERAL received bytes; claims are parsed only
|
|
21
|
+
* AFTER the signature passes.
|
|
22
|
+
* 2. mint/verify are PURE and take an explicit `now` (no ambient clock in
|
|
23
|
+
* shared). mint reads only in-memory event fields — never the EventSink — so
|
|
24
|
+
* a record() failure after settle still yields a valid receipt.
|
|
25
|
+
*/
|
|
26
|
+
import { type KeyObject } from "node:crypto";
|
|
27
|
+
import { type TieBreak } from "./attribution.ts";
|
|
28
|
+
import type { AttributedEvent, AuthorShare } from "./types.ts";
|
|
29
|
+
/** A JSON Web Key for an Ed25519 public key (OKP). */
|
|
30
|
+
export interface Jwk {
|
|
31
|
+
kty: "OKP";
|
|
32
|
+
crv: "Ed25519";
|
|
33
|
+
x: string;
|
|
34
|
+
kid: string;
|
|
35
|
+
use: "sig";
|
|
36
|
+
alg: "EdDSA";
|
|
37
|
+
}
|
|
38
|
+
export interface JwkSet {
|
|
39
|
+
keys: Jwk[];
|
|
40
|
+
}
|
|
41
|
+
/** A resolved signing key: the private key, its public key, and a stable kid. */
|
|
42
|
+
export interface SigningKey {
|
|
43
|
+
privateKey: KeyObject;
|
|
44
|
+
publicKey: KeyObject;
|
|
45
|
+
kid: string;
|
|
46
|
+
}
|
|
47
|
+
/** The namespaced `naulon` claim — the domain payload. */
|
|
48
|
+
export interface NaulonClaim {
|
|
49
|
+
v: 1;
|
|
50
|
+
slug: string;
|
|
51
|
+
title: string;
|
|
52
|
+
kind: AttributedEvent["kind"];
|
|
53
|
+
/** Integer micro-USDC as a string (e.g. "1000" = $0.001) — never a float. */
|
|
54
|
+
amount: string;
|
|
55
|
+
currency: "USDC";
|
|
56
|
+
network: {
|
|
57
|
+
chainId: number;
|
|
58
|
+
usdc: string;
|
|
59
|
+
gateway: string;
|
|
60
|
+
};
|
|
61
|
+
settlementRef: string;
|
|
62
|
+
/** Present in "full" payees mode. */
|
|
63
|
+
payees?: AuthorShare[];
|
|
64
|
+
/** Present in "hashed" payees mode. */
|
|
65
|
+
payeesHash?: string;
|
|
66
|
+
payTo?: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* RFC 7800 confirmation claim. When present, the license is HOLDER-OF-KEY bound:
|
|
70
|
+
* a re-read must prove possession of the named wallet's key (an EIP-191 signature
|
|
71
|
+
* over a fresh challenge), so capturing the bearer token alone is not enough.
|
|
72
|
+
* `naulon:addr` is the lowercased payer wallet that must sign the proof.
|
|
73
|
+
*/
|
|
74
|
+
export interface ConfirmationClaim {
|
|
75
|
+
"naulon:addr": string;
|
|
76
|
+
}
|
|
77
|
+
/** The full CLT claim set (registered JWT claims + the `naulon` object). */
|
|
78
|
+
export interface CitationLicenseClaims {
|
|
79
|
+
iss: string;
|
|
80
|
+
aud: string;
|
|
81
|
+
sub: string;
|
|
82
|
+
jti: string;
|
|
83
|
+
iat: number;
|
|
84
|
+
nbf: number;
|
|
85
|
+
exp: number;
|
|
86
|
+
naulon: NaulonClaim;
|
|
87
|
+
/** Present only on holder-of-key licenses (LICENSE_POP). RFC 7800. */
|
|
88
|
+
cnf?: ConfirmationClaim;
|
|
89
|
+
}
|
|
90
|
+
export interface MintInput {
|
|
91
|
+
event: AttributedEvent;
|
|
92
|
+
issuer: string;
|
|
93
|
+
audience: string;
|
|
94
|
+
ttlSeconds: number;
|
|
95
|
+
payeesMode: "full" | "hashed";
|
|
96
|
+
title: string;
|
|
97
|
+
network: {
|
|
98
|
+
chainId: number;
|
|
99
|
+
usdc: string;
|
|
100
|
+
gateway: string;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Tie-break for the hashed-mode `payTo` (the advertised primary recipient).
|
|
104
|
+
* MUST match the gate's `PRIMARY_PAYEE_TIEBREAK` so the license names exactly
|
|
105
|
+
* the wallet the on-chain leg paid and the settlement record flags `primary`.
|
|
106
|
+
* Defaults to `"wallet"` (the shared default). Unused in `full` payees mode.
|
|
107
|
+
*/
|
|
108
|
+
tieBreak?: TieBreak;
|
|
109
|
+
/**
|
|
110
|
+
* When set, mint a HOLDER-OF-KEY license bound to this wallet (the payer): a
|
|
111
|
+
* re-read must present a proof-of-possession signed by it. Omit for a v1 bearer
|
|
112
|
+
* license. Must be a real address — never the zero-address fallback.
|
|
113
|
+
*/
|
|
114
|
+
popBindAddress?: string;
|
|
115
|
+
}
|
|
116
|
+
export type VerifyResult = {
|
|
117
|
+
ok: true;
|
|
118
|
+
claims: CitationLicenseClaims;
|
|
119
|
+
} | {
|
|
120
|
+
ok: false;
|
|
121
|
+
error: string;
|
|
122
|
+
};
|
|
123
|
+
/** Stable key id = base64url(SHA-256(raw public key))[:16]. */
|
|
124
|
+
export declare function kidFor(publicKey: KeyObject): string;
|
|
125
|
+
/** The public JWK for a signing key. */
|
|
126
|
+
export declare function publicJwk(key: SigningKey): Jwk;
|
|
127
|
+
/** A JWK Set advertising every supplied key (publish at the JWKS endpoint). */
|
|
128
|
+
export declare function jwksOf(keys: SigningKey[]): JwkSet;
|
|
129
|
+
/**
|
|
130
|
+
* Resolve the signing key from a config secret, or generate an EPHEMERAL one.
|
|
131
|
+
*
|
|
132
|
+
* `secret` may be a PKCS8 PEM or base64-encoded PKCS8 DER Ed25519 private key.
|
|
133
|
+
* If absent, an ephemeral key is generated with a loud warning — acceptable ONLY
|
|
134
|
+
* for single-instance mock/dev; config.ts fails loud when a stable key is
|
|
135
|
+
* required (real money or a supabase/multi-instance backend).
|
|
136
|
+
*/
|
|
137
|
+
export declare function loadSigningKey(secret?: string): SigningKey;
|
|
138
|
+
/**
|
|
139
|
+
* Mint a compact-JWS CLT from a settled event. Pure: takes an explicit `now` and
|
|
140
|
+
* reads only the in-memory event — never the EventSink.
|
|
141
|
+
*/
|
|
142
|
+
export declare function mintLicense(input: MintInput, key: SigningKey, now: number): string;
|
|
143
|
+
/**
|
|
144
|
+
* Verify a CLT against a JWK Set, issuer, audience and clock. Fails closed on any
|
|
145
|
+
* defect. The algorithm is pinned to Ed25519 regardless of the token header, and
|
|
146
|
+
* the signature is checked over the literal received bytes before any claim is
|
|
147
|
+
* read. `now` is epoch ms (the caller's clock); claims are epoch seconds.
|
|
148
|
+
*/
|
|
149
|
+
export declare function verifyLicense(jws: string, opts: {
|
|
150
|
+
now: number;
|
|
151
|
+
expectedIssuer: string;
|
|
152
|
+
expectedAudience: string;
|
|
153
|
+
jwks: JwkSet;
|
|
154
|
+
}): VerifyResult;
|
|
155
|
+
/** The wallet a holder-of-key license is bound to, or null for a v1 bearer license. */
|
|
156
|
+
export declare function popBoundAddress(claims: CitationLicenseClaims): string | null;
|
|
157
|
+
/** The fields a proof-of-possession is bound to. */
|
|
158
|
+
export interface PopChallenge {
|
|
159
|
+
/** The gate's identity (= license aud); pins the proof to one deployment. */
|
|
160
|
+
aud: string;
|
|
161
|
+
/** The license id; pins the proof to one license. */
|
|
162
|
+
jti: string;
|
|
163
|
+
/** The essay slug; pins the proof to one article (defense in depth over jti). */
|
|
164
|
+
slug: string;
|
|
165
|
+
/** Unix seconds the proof was created; the gate enforces a freshness window. */
|
|
166
|
+
ts: number;
|
|
167
|
+
/** Single-use random salt; the gate spends it once to stop replay in-window. */
|
|
168
|
+
nonce: string;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The canonical bytes a holder signs (EIP-191 personal_sign) and the gate
|
|
172
|
+
* reconstructs to recover the signer. Pure and deterministic — no clock, no
|
|
173
|
+
* crypto here, just a fixed-order newline framing so signer and verifier agree
|
|
174
|
+
* byte-for-byte. Reject any `\n` in inputs would be belt-and-braces; jti/nonce
|
|
175
|
+
* are hex, aud/slug are controlled, so the framing is unambiguous in practice.
|
|
176
|
+
*/
|
|
177
|
+
export declare function popMessage(c: PopChallenge): string;
|
|
178
|
+
//# sourceMappingURL=license.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"license.d.ts","sourceRoot":"","sources":["../src/license.ts"],"names":[],"mappings":"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"}
|
package/dist/license.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Citation License Token (CLT) — sign + verify. See docs/citation-license.md.
|
|
3
|
+
*
|
|
4
|
+
* A CLT is a signed, independently verifiable receipt the tollgate hands an agent
|
|
5
|
+
* when it pays: proof of who paid, how much USDC, for which essay, to which
|
|
6
|
+
* author wallet(s), settled on-chain. It is a signed projection of the
|
|
7
|
+
* AttributedEvent already recorded per payment (jti = event.id). Re-presenting a
|
|
8
|
+
* valid unexpired CLT re-reads that essay free within a short window — so the toll
|
|
9
|
+
* becomes an asset the payer wants, not a tax it evades.
|
|
10
|
+
*
|
|
11
|
+
* Format: a strict RFC 7519 JWT, EdDSA-signed (Ed25519, node:crypto — no dep),
|
|
12
|
+
* verifiable by an unmodified jose/pyjwt client. Domain data lives under one
|
|
13
|
+
* namespaced `naulon` claim so it never shadows a registered JWT claim.
|
|
14
|
+
*
|
|
15
|
+
* SECURITY — two invariants this module enforces (a regression in either is a
|
|
16
|
+
* free-read bypass; see the adversarial tests in license.test.ts):
|
|
17
|
+
* 1. verify IGNORES the token's `alg` header and hard-pins Ed25519. The public
|
|
18
|
+
* key is world-readable at the JWKS endpoint, so trusting `alg` would allow
|
|
19
|
+
* alg:none and HMAC-with-the-public-key forgery. The verify routine is fixed
|
|
20
|
+
* to crypto.verify over the LITERAL received bytes; claims are parsed only
|
|
21
|
+
* AFTER the signature passes.
|
|
22
|
+
* 2. mint/verify are PURE and take an explicit `now` (no ambient clock in
|
|
23
|
+
* shared). mint reads only in-memory event fields — never the EventSink — so
|
|
24
|
+
* a record() failure after settle still yields a valid receipt.
|
|
25
|
+
*/
|
|
26
|
+
import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify, } from "node:crypto";
|
|
27
|
+
import { toAtomicUsdc } from "./networks.js";
|
|
28
|
+
import { primaryPayee } from "./attribution.js";
|
|
29
|
+
// ── Key handling ──────────────────────────────────────────────────────────────
|
|
30
|
+
/** Raw 32-byte Ed25519 public key bytes (from the OKP `x` parameter). */
|
|
31
|
+
function rawPublicKey(publicKey) {
|
|
32
|
+
const jwk = publicKey.export({ format: "jwk" });
|
|
33
|
+
if (!jwk.x)
|
|
34
|
+
throw new Error("not an Ed25519 (OKP) public key");
|
|
35
|
+
return Buffer.from(jwk.x, "base64url");
|
|
36
|
+
}
|
|
37
|
+
/** Stable key id = base64url(SHA-256(raw public key))[:16]. */
|
|
38
|
+
export function kidFor(publicKey) {
|
|
39
|
+
return createHash("sha256").update(rawPublicKey(publicKey)).digest("base64url").slice(0, 16);
|
|
40
|
+
}
|
|
41
|
+
/** The public JWK for a signing key. */
|
|
42
|
+
export function publicJwk(key) {
|
|
43
|
+
return {
|
|
44
|
+
kty: "OKP",
|
|
45
|
+
crv: "Ed25519",
|
|
46
|
+
x: rawPublicKey(key.publicKey).toString("base64url"),
|
|
47
|
+
kid: key.kid,
|
|
48
|
+
use: "sig",
|
|
49
|
+
alg: "EdDSA",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** A JWK Set advertising every supplied key (publish at the JWKS endpoint). */
|
|
53
|
+
export function jwksOf(keys) {
|
|
54
|
+
return { keys: keys.map(publicJwk) };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the signing key from a config secret, or generate an EPHEMERAL one.
|
|
58
|
+
*
|
|
59
|
+
* `secret` may be a PKCS8 PEM or base64-encoded PKCS8 DER Ed25519 private key.
|
|
60
|
+
* If absent, an ephemeral key is generated with a loud warning — acceptable ONLY
|
|
61
|
+
* for single-instance mock/dev; config.ts fails loud when a stable key is
|
|
62
|
+
* required (real money or a supabase/multi-instance backend).
|
|
63
|
+
*/
|
|
64
|
+
export function loadSigningKey(secret) {
|
|
65
|
+
let privateKey;
|
|
66
|
+
if (secret && secret.trim()) {
|
|
67
|
+
privateKey = parsePrivateKey(secret.trim());
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
console.warn("[license] LICENSE_SIGNING_KEY not set — generating an EPHEMERAL Ed25519 key. " +
|
|
71
|
+
"Valid only for single-instance mock/dev: outstanding licenses break on restart and " +
|
|
72
|
+
"across instances. Set LICENSE_SIGNING_KEY for any real or multi-instance deploy.");
|
|
73
|
+
privateKey = generateKeyPairSync("ed25519").privateKey;
|
|
74
|
+
}
|
|
75
|
+
const publicKey = createPublicKey(privateKey);
|
|
76
|
+
if (publicKey.asymmetricKeyType !== "ed25519") {
|
|
77
|
+
throw new Error("LICENSE_SIGNING_KEY must be an Ed25519 private key");
|
|
78
|
+
}
|
|
79
|
+
return { privateKey, publicKey, kid: kidFor(publicKey) };
|
|
80
|
+
}
|
|
81
|
+
function parsePrivateKey(secret) {
|
|
82
|
+
if (secret.includes("BEGIN"))
|
|
83
|
+
return createPrivateKey(secret);
|
|
84
|
+
return createPrivateKey({ key: Buffer.from(secret, "base64"), format: "der", type: "pkcs8" });
|
|
85
|
+
}
|
|
86
|
+
// ── Mint ──────────────────────────────────────────────────────────────────────
|
|
87
|
+
function b64urlJson(value) {
|
|
88
|
+
return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
|
|
89
|
+
}
|
|
90
|
+
/** Deterministic canonical JSON of payees (sorted, fixed field order) for hashing. */
|
|
91
|
+
function canonicalPayees(payees) {
|
|
92
|
+
const sorted = [...payees].sort((a, b) => a.authorId.localeCompare(b.authorId) || a.wallet.localeCompare(b.wallet));
|
|
93
|
+
return JSON.stringify(sorted.map((p) => [p.authorId, p.wallet, p.share]));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Mint a compact-JWS CLT from a settled event. Pure: takes an explicit `now` and
|
|
97
|
+
* reads only the in-memory event — never the EventSink.
|
|
98
|
+
*/
|
|
99
|
+
export function mintLicense(input, key, now) {
|
|
100
|
+
const { event } = input;
|
|
101
|
+
const iat = Math.floor(now / 1000);
|
|
102
|
+
const naulon = {
|
|
103
|
+
v: 1,
|
|
104
|
+
slug: event.slug,
|
|
105
|
+
title: input.title,
|
|
106
|
+
kind: event.kind,
|
|
107
|
+
amount: toAtomicUsdc(event.amount),
|
|
108
|
+
currency: "USDC",
|
|
109
|
+
network: input.network,
|
|
110
|
+
settlementRef: event.settlementRef,
|
|
111
|
+
};
|
|
112
|
+
if (input.payeesMode === "hashed") {
|
|
113
|
+
naulon.payeesHash = createHash("sha256").update(canonicalPayees(event.payees)).digest("base64url");
|
|
114
|
+
naulon.payTo = primaryPayee(event.payees, input.tieBreak);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
naulon.payees = event.payees.map((p) => ({ authorId: p.authorId, wallet: p.wallet, share: p.share }));
|
|
118
|
+
}
|
|
119
|
+
const payload = {
|
|
120
|
+
iss: input.issuer,
|
|
121
|
+
aud: input.audience,
|
|
122
|
+
sub: event.payerAddress,
|
|
123
|
+
jti: event.id,
|
|
124
|
+
iat,
|
|
125
|
+
nbf: iat,
|
|
126
|
+
exp: iat + input.ttlSeconds,
|
|
127
|
+
naulon,
|
|
128
|
+
};
|
|
129
|
+
// Holder-of-key: bind the license to the payer wallet so a re-read needs a
|
|
130
|
+
// proof-of-possession, not just the token. Lowercased for a canonical compare.
|
|
131
|
+
if (input.popBindAddress) {
|
|
132
|
+
payload.cnf = { "naulon:addr": input.popBindAddress.toLowerCase() };
|
|
133
|
+
}
|
|
134
|
+
const header = { alg: "EdDSA", typ: "JWT", kid: key.kid };
|
|
135
|
+
const signingInput = `${b64urlJson(header)}.${b64urlJson(payload)}`;
|
|
136
|
+
const sig = sign(null, Buffer.from(signingInput, "ascii"), key.privateKey);
|
|
137
|
+
return `${signingInput}.${sig.toString("base64url")}`;
|
|
138
|
+
}
|
|
139
|
+
// ── Verify ──────────────────────────────────────────────────────────────────
|
|
140
|
+
const BASE64URL = /^[A-Za-z0-9_-]+$/;
|
|
141
|
+
// JOSE header params that change how a token is interpreted — reject them all.
|
|
142
|
+
const FORBIDDEN_HEADER_PARAMS = ["crit", "jku", "x5u", "jwk", "x5c"];
|
|
143
|
+
/** Negative clock-skew tolerance (seconds) on nbf/iat only. */
|
|
144
|
+
const SKEW_SECONDS = 60;
|
|
145
|
+
function fail(error) {
|
|
146
|
+
return { ok: false, error };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Verify a CLT against a JWK Set, issuer, audience and clock. Fails closed on any
|
|
150
|
+
* defect. The algorithm is pinned to Ed25519 regardless of the token header, and
|
|
151
|
+
* the signature is checked over the literal received bytes before any claim is
|
|
152
|
+
* read. `now` is epoch ms (the caller's clock); claims are epoch seconds.
|
|
153
|
+
*/
|
|
154
|
+
export function verifyLicense(jws, opts) {
|
|
155
|
+
if (typeof jws !== "string" || jws.length === 0)
|
|
156
|
+
return fail("empty token");
|
|
157
|
+
if (jws.length > 4096)
|
|
158
|
+
return fail("token too large");
|
|
159
|
+
const parts = jws.split(".");
|
|
160
|
+
if (parts.length !== 3)
|
|
161
|
+
return fail("malformed token (need 3 segments)");
|
|
162
|
+
const [h, p, s] = parts;
|
|
163
|
+
if (!BASE64URL.test(h) || !BASE64URL.test(p) || !BASE64URL.test(s)) {
|
|
164
|
+
return fail("non-base64url segment");
|
|
165
|
+
}
|
|
166
|
+
let header;
|
|
167
|
+
try {
|
|
168
|
+
header = JSON.parse(Buffer.from(h, "base64url").toString("utf8"));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return fail("undecodable header");
|
|
172
|
+
}
|
|
173
|
+
// Pin the algorithm — never select the verify routine from the header. This
|
|
174
|
+
// rejects alg:"none" and any HMAC (HS256-with-the-public-key) forgery.
|
|
175
|
+
if (header.alg !== "EdDSA")
|
|
176
|
+
return fail("unsupported alg (only EdDSA)");
|
|
177
|
+
if (header.typ !== "JWT")
|
|
178
|
+
return fail("unexpected typ");
|
|
179
|
+
for (const param of FORBIDDEN_HEADER_PARAMS) {
|
|
180
|
+
if (param in header)
|
|
181
|
+
return fail(`forbidden header param: ${param}`);
|
|
182
|
+
}
|
|
183
|
+
const kid = header.kid;
|
|
184
|
+
if (typeof kid !== "string")
|
|
185
|
+
return fail("missing kid");
|
|
186
|
+
const jwk = opts.jwks.keys.find((k) => k.kid === kid);
|
|
187
|
+
if (!jwk)
|
|
188
|
+
return fail("kid not in JWKS");
|
|
189
|
+
let publicKey;
|
|
190
|
+
try {
|
|
191
|
+
publicKey = createPublicKey({ key: jwk, format: "jwk" });
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return fail("invalid JWKS key");
|
|
195
|
+
}
|
|
196
|
+
// Verify over the LITERAL received bytes — not a re-serialized payload.
|
|
197
|
+
const sigBytes = Buffer.from(s, "base64url");
|
|
198
|
+
let valid = false;
|
|
199
|
+
try {
|
|
200
|
+
valid = verify(null, Buffer.from(`${h}.${p}`, "ascii"), publicKey, sigBytes);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return fail("verification error");
|
|
204
|
+
}
|
|
205
|
+
if (!valid)
|
|
206
|
+
return fail("invalid signature");
|
|
207
|
+
// Only now is it safe to read claims.
|
|
208
|
+
let claims;
|
|
209
|
+
try {
|
|
210
|
+
claims = JSON.parse(Buffer.from(p, "base64url").toString("utf8"));
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return fail("undecodable payload");
|
|
214
|
+
}
|
|
215
|
+
if (claims.iss !== opts.expectedIssuer)
|
|
216
|
+
return fail("issuer mismatch");
|
|
217
|
+
if (claims.aud !== opts.expectedAudience)
|
|
218
|
+
return fail("audience mismatch");
|
|
219
|
+
const nowSec = Math.floor(opts.now / 1000);
|
|
220
|
+
if (!Number.isFinite(claims.exp) || nowSec >= claims.exp)
|
|
221
|
+
return fail("expired");
|
|
222
|
+
if (Number.isFinite(claims.nbf) && nowSec < claims.nbf - SKEW_SECONDS)
|
|
223
|
+
return fail("not yet valid");
|
|
224
|
+
if (Number.isFinite(claims.iat) && claims.iat > nowSec + SKEW_SECONDS)
|
|
225
|
+
return fail("issued in the future");
|
|
226
|
+
return { ok: true, claims };
|
|
227
|
+
}
|
|
228
|
+
// ── Holder-of-key proof-of-possession (P5) ───────────────────────────────────
|
|
229
|
+
/** The wallet a holder-of-key license is bound to, or null for a v1 bearer license. */
|
|
230
|
+
export function popBoundAddress(claims) {
|
|
231
|
+
const addr = claims.cnf?.["naulon:addr"];
|
|
232
|
+
return typeof addr === "string" && addr ? addr.toLowerCase() : null;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* The canonical bytes a holder signs (EIP-191 personal_sign) and the gate
|
|
236
|
+
* reconstructs to recover the signer. Pure and deterministic — no clock, no
|
|
237
|
+
* crypto here, just a fixed-order newline framing so signer and verifier agree
|
|
238
|
+
* byte-for-byte. Reject any `\n` in inputs would be belt-and-braces; jti/nonce
|
|
239
|
+
* are hex, aud/slug are controlled, so the framing is unambiguous in practice.
|
|
240
|
+
*/
|
|
241
|
+
export function popMessage(c) {
|
|
242
|
+
return [
|
|
243
|
+
"naulon-pop",
|
|
244
|
+
"v=1",
|
|
245
|
+
`aud=${c.aud}`,
|
|
246
|
+
`jti=${c.jti}`,
|
|
247
|
+
`slug=${c.slug}`,
|
|
248
|
+
`ts=${c.ts}`,
|
|
249
|
+
`nonce=${c.nonce}`,
|
|
250
|
+
].join("\n");
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=license.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"license.js","sourceRoot":"","sources":["../src/license.ts"],"names":[],"mappings":"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"}
|