@naulon/enforce 0.1.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 +89 -0
- package/dist/agentDetect.d.ts +94 -0
- package/dist/agentDetect.d.ts.map +1 -0
- package/dist/agentDetect.js +149 -0
- package/dist/agentDetect.js.map +1 -0
- package/dist/botAuth.d.ts +159 -0
- package/dist/botAuth.d.ts.map +1 -0
- package/dist/botAuth.js +659 -0
- package/dist/botAuth.js.map +1 -0
- package/dist/build402.d.ts +46 -0
- package/dist/build402.d.ts.map +1 -0
- package/dist/build402.js +137 -0
- package/dist/build402.js.map +1 -0
- package/dist/decide.d.ts +125 -0
- package/dist/decide.d.ts.map +1 -0
- package/dist/decide.js +221 -0
- package/dist/decide.js.map +1 -0
- package/dist/discoverability.d.ts +68 -0
- package/dist/discoverability.d.ts.map +1 -0
- package/dist/discoverability.js +62 -0
- package/dist/discoverability.js.map +1 -0
- package/dist/enforce/fetch-handler.d.ts +12 -0
- package/dist/enforce/fetch-handler.d.ts.map +1 -0
- package/dist/enforce/fetch-handler.js +25 -0
- package/dist/enforce/fetch-handler.js.map +1 -0
- package/dist/enforce/index.d.ts +11 -0
- package/dist/enforce/index.d.ts.map +1 -0
- package/dist/enforce/index.js +11 -0
- package/dist/enforce/index.js.map +1 -0
- package/dist/enforce/middleware.d.ts +27 -0
- package/dist/enforce/middleware.d.ts.map +1 -0
- package/dist/enforce/middleware.js +88 -0
- package/dist/enforce/middleware.js.map +1 -0
- package/dist/enforce/next.d.ts +24 -0
- package/dist/enforce/next.d.ts.map +1 -0
- package/dist/enforce/next.js +32 -0
- package/dist/enforce/next.js.map +1 -0
- package/dist/enforce/quote-source.d.ts +31 -0
- package/dist/enforce/quote-source.d.ts.map +1 -0
- package/dist/enforce/quote-source.js +29 -0
- package/dist/enforce/quote-source.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/license.d.ts +16 -0
- package/dist/license.d.ts.map +1 -0
- package/dist/license.js +18 -0
- package/dist/license.js.map +1 -0
- package/dist/nonce.d.ts +45 -0
- package/dist/nonce.d.ts.map +1 -0
- package/dist/nonce.js +122 -0
- package/dist/nonce.js.map +1 -0
- package/dist/pop.d.ts +15 -0
- package/dist/pop.d.ts.map +1 -0
- package/dist/pop.js +77 -0
- package/dist/pop.js.map +1 -0
- package/dist/pricing.d.ts +53 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/pricing.js +44 -0
- package/dist/pricing.js.map +1 -0
- package/dist/revocation.d.ts +6 -0
- package/dist/revocation.d.ts.map +1 -0
- package/dist/revocation.js +45 -0
- package/dist/revocation.js.map +1 -0
- package/package.json +47 -0
package/dist/nonce.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** The payment facts a nonce is cryptographically bound to. */
|
|
2
|
+
export interface NonceBinding {
|
|
3
|
+
amount: string;
|
|
4
|
+
payTo: string;
|
|
5
|
+
network: string;
|
|
6
|
+
}
|
|
7
|
+
/** Mint a nonce bound to a payment requirement, valid for NONCE_TTL_SECONDS. */
|
|
8
|
+
export declare function issueNonce(binding: NonceBinding, now: number): string;
|
|
9
|
+
/**
|
|
10
|
+
* The "has this nonce been spent?" store — the one piece of nonce state that
|
|
11
|
+
* isn't carried inside the nonce itself. Issuance is stateless (the HMAC is the
|
|
12
|
+
* proof); only single-use enforcement needs memory. Behind this seam it can be
|
|
13
|
+
* an in-process Map (default) or a shared table (Supabase) so the guarantee
|
|
14
|
+
* survives across serverless instances.
|
|
15
|
+
*/
|
|
16
|
+
export interface ConsumedStore {
|
|
17
|
+
/**
|
|
18
|
+
* Atomically mark `nonce` as spent. Resolve `true` if this call was the first
|
|
19
|
+
* to spend it, `false` if it was already spent (a replay). Must be a single
|
|
20
|
+
* atomic step — a check-then-set with a gap would let two concurrent replays
|
|
21
|
+
* both win.
|
|
22
|
+
*/
|
|
23
|
+
consume(nonce: string, expMs: number, now: number): Promise<boolean>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build a single-use store using the configured backend. Each caller gets its
|
|
27
|
+
* own instance — the memory backend is a private Map, and the Supabase backend
|
|
28
|
+
* shares the nonces table but callers namespace their keys (e.g. the PoP path
|
|
29
|
+
* prefixes `pop:`) so distinct uses never collide. Reused by the holder-of-key
|
|
30
|
+
* proof verifier (pop.ts) so its replay protection inherits the same seam.
|
|
31
|
+
*/
|
|
32
|
+
export declare function makeConsumedStore(): ConsumedStore;
|
|
33
|
+
export type ConsumeResult = {
|
|
34
|
+
ok: true;
|
|
35
|
+
} | {
|
|
36
|
+
ok: false;
|
|
37
|
+
error: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Validate a nonce against a payment binding and spend it. Fails closed on a
|
|
41
|
+
* bad shape, a forged/mismatched HMAC, expiry, or replay. The signature/expiry
|
|
42
|
+
* checks run before we touch the store, so a forged nonce never hits the DB.
|
|
43
|
+
*/
|
|
44
|
+
export declare function consumeNonce(nonce: string, binding: NonceBinding, now: number): Promise<ConsumeResult>;
|
|
45
|
+
//# sourceMappingURL=nonce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nonce.d.ts","sourceRoot":"","sources":["../src/nonce.ts"],"names":[],"mappings":"AAwCA,+DAA+D;AAC/D,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAQD,gFAAgF;AAChF,wBAAgB,UAAU,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAIrE;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtE;AAyCD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,IAAI,aAAa,CAEjD;AAID,MAAM,MAAM,aAAa,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAExE;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,YAAY,EACrB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,aAAa,CAAC,CAmBxB"}
|
package/dist/nonce.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment nonces — replay protection for the x402 handshake.
|
|
3
|
+
*
|
|
4
|
+
* Without this, a captured `payment-signature` could be replayed forever to read
|
|
5
|
+
* for free (mock mode has no chain to stop it; even live, defence-in-depth is
|
|
6
|
+
* cheap). Each 402 carries a fresh nonce the agent must echo in its payment.
|
|
7
|
+
*
|
|
8
|
+
* Design: issuance is STATELESS — the nonce is its own proof. It's
|
|
9
|
+
* `${expMs}.${rand}.${hmac}`
|
|
10
|
+
* where the HMAC is taken over the expiry, the random salt, AND the payment
|
|
11
|
+
* binding (amount + payTo + network). Two consequences:
|
|
12
|
+
* - a nonce minted for a $0.001 read can't be reused to satisfy a $0.005
|
|
13
|
+
* citation — the binding wouldn't match.
|
|
14
|
+
* - the gate needs no memory of what it issued to validate a nonce later.
|
|
15
|
+
*
|
|
16
|
+
* The only state is a CONSUMED set so a valid nonce works exactly once. It lives
|
|
17
|
+
* behind the `ConsumedStore` seam below: an in-process Map by default (single
|
|
18
|
+
* instance), or a shared Supabase table when NONCE_BACKEND=supabase — which is
|
|
19
|
+
* what makes the gate safe to run as many serverless instances (Vercel). For
|
|
20
|
+
* either multi-instance path, also set TOLLGATE_SECRET so every instance signs
|
|
21
|
+
* and verifies nonces with the same key.
|
|
22
|
+
*/
|
|
23
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
24
|
+
import { getConfig, supabaseRest } from "@naulon/shared";
|
|
25
|
+
const cfg = getConfig();
|
|
26
|
+
const secret = cfg.TOLLGATE_SECRET ??
|
|
27
|
+
(() => {
|
|
28
|
+
const ephemeral = randomBytes(32).toString("hex");
|
|
29
|
+
console.warn("[tollgate] TOLLGATE_SECRET not set — using an ephemeral nonce secret. " +
|
|
30
|
+
"Outstanding 402s are invalidated on restart; set TOLLGATE_SECRET for stability/multi-instance.");
|
|
31
|
+
return ephemeral;
|
|
32
|
+
})();
|
|
33
|
+
const ttlMs = cfg.NONCE_TTL_SECONDS * 1000;
|
|
34
|
+
function sign(expMs, rand, b) {
|
|
35
|
+
return createHmac("sha256", secret)
|
|
36
|
+
.update(`${expMs}.${rand}.${b.amount}.${b.payTo}.${b.network}`)
|
|
37
|
+
.digest("hex");
|
|
38
|
+
}
|
|
39
|
+
/** Mint a nonce bound to a payment requirement, valid for NONCE_TTL_SECONDS. */
|
|
40
|
+
export function issueNonce(binding, now) {
|
|
41
|
+
const expMs = now + ttlMs;
|
|
42
|
+
const rand = randomBytes(12).toString("hex");
|
|
43
|
+
return `${expMs}.${rand}.${sign(expMs, rand, binding)}`;
|
|
44
|
+
}
|
|
45
|
+
/** Default store: an in-process Map. Correct for a single instance. */
|
|
46
|
+
function memoryStore() {
|
|
47
|
+
// nonce -> expiry ms. A present key means "already spent".
|
|
48
|
+
const consumed = new Map();
|
|
49
|
+
const sweep = (now) => {
|
|
50
|
+
for (const [n, exp] of consumed)
|
|
51
|
+
if (exp <= now)
|
|
52
|
+
consumed.delete(n);
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
async consume(nonce, expMs, now) {
|
|
56
|
+
if (consumed.has(nonce))
|
|
57
|
+
return false;
|
|
58
|
+
if (consumed.size > 10_000)
|
|
59
|
+
sweep(now); // amortized cleanup under load
|
|
60
|
+
consumed.set(nonce, expMs);
|
|
61
|
+
return true;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Shared store: a Supabase table with `nonce` as the primary key. The DB is the
|
|
67
|
+
* arbiter of single-use — we INSERT and let the unique constraint decide. With
|
|
68
|
+
* `resolution=ignore-duplicates` a conflicting insert is silently dropped and,
|
|
69
|
+
* thanks to `return=representation`, the response is an EMPTY array — so "a row
|
|
70
|
+
* came back" means we won the insert (fresh), and "[]" means someone already
|
|
71
|
+
* spent it (replay). One round-trip, atomic, race-safe across instances.
|
|
72
|
+
*/
|
|
73
|
+
function supabaseStore() {
|
|
74
|
+
const table = getConfig().SUPABASE_NONCES_TABLE;
|
|
75
|
+
return {
|
|
76
|
+
async consume(nonce, expMs) {
|
|
77
|
+
const rows = (await supabaseRest(`/rest/v1/${table}?on_conflict=nonce`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { Prefer: "resolution=ignore-duplicates,return=representation" },
|
|
80
|
+
body: JSON.stringify([{ nonce, exp_ms: expMs }]),
|
|
81
|
+
}));
|
|
82
|
+
return rows.length > 0;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Build a single-use store using the configured backend. Each caller gets its
|
|
88
|
+
* own instance — the memory backend is a private Map, and the Supabase backend
|
|
89
|
+
* shares the nonces table but callers namespace their keys (e.g. the PoP path
|
|
90
|
+
* prefixes `pop:`) so distinct uses never collide. Reused by the holder-of-key
|
|
91
|
+
* proof verifier (pop.ts) so its replay protection inherits the same seam.
|
|
92
|
+
*/
|
|
93
|
+
export function makeConsumedStore() {
|
|
94
|
+
return cfg.NONCE_BACKEND === "supabase" ? supabaseStore() : memoryStore();
|
|
95
|
+
}
|
|
96
|
+
const store = makeConsumedStore();
|
|
97
|
+
/**
|
|
98
|
+
* Validate a nonce against a payment binding and spend it. Fails closed on a
|
|
99
|
+
* bad shape, a forged/mismatched HMAC, expiry, or replay. The signature/expiry
|
|
100
|
+
* checks run before we touch the store, so a forged nonce never hits the DB.
|
|
101
|
+
*/
|
|
102
|
+
export async function consumeNonce(nonce, binding, now) {
|
|
103
|
+
const parts = nonce.split(".");
|
|
104
|
+
if (parts.length !== 3)
|
|
105
|
+
return { ok: false, error: "malformed nonce" };
|
|
106
|
+
const [expStr, rand, mac] = parts;
|
|
107
|
+
const expMs = Number(expStr);
|
|
108
|
+
if (!Number.isFinite(expMs))
|
|
109
|
+
return { ok: false, error: "malformed nonce" };
|
|
110
|
+
if (expMs <= now)
|
|
111
|
+
return { ok: false, error: "nonce expired" };
|
|
112
|
+
const expected = sign(expMs, rand, binding);
|
|
113
|
+
// Constant-time compare; lengths match (both hex SHA-256) so this is safe.
|
|
114
|
+
if (mac.length !== expected.length || !timingSafeEqual(Buffer.from(mac), Buffer.from(expected))) {
|
|
115
|
+
return { ok: false, error: "invalid nonce signature" };
|
|
116
|
+
}
|
|
117
|
+
if (!(await store.consume(nonce, expMs, now))) {
|
|
118
|
+
return { ok: false, error: "nonce already used (replay)" };
|
|
119
|
+
}
|
|
120
|
+
return { ok: true };
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=nonce.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nonce.js","sourceRoot":"","sources":["../src/nonce.ts"],"names":[],"mappings":"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"}
|
package/dist/pop.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type CitationLicenseClaims } from "@naulon/shared";
|
|
2
|
+
export interface PopContext {
|
|
3
|
+
claims: CitationLicenseClaims;
|
|
4
|
+
slug: string;
|
|
5
|
+
/** The gate identity (= license aud); pins the proof to this deployment. */
|
|
6
|
+
identity: string;
|
|
7
|
+
/** Epoch ms (the gate clock). */
|
|
8
|
+
now: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Verify a proof-of-possession header for a holder-of-key license. Returns true
|
|
12
|
+
* only if the proof is fresh, single-use, and signed by the bound wallet.
|
|
13
|
+
*/
|
|
14
|
+
export declare function verifyPopProof(proofHeader: string, ctx: PopContext): Promise<boolean>;
|
|
15
|
+
//# sourceMappingURL=pop.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pop.d.ts","sourceRoot":"","sources":["../src/pop.ts"],"names":[],"mappings":"AAsBA,OAAO,EAA0C,KAAK,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAepG,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,qBAAqB,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,QAAQ,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAoC3F"}
|
package/dist/pop.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Holder-of-key proof-of-possession (P5). See docs/citation-license.md.
|
|
3
|
+
*
|
|
4
|
+
* A holder-of-key license carries a `cnf` claim binding it to the payer wallet.
|
|
5
|
+
* Re-reading it free is no longer a bearer right: the caller must prove it holds
|
|
6
|
+
* that wallet's key by signing a fresh challenge (EIP-191 personal_sign). So a
|
|
7
|
+
* captured `X-Naulon-License` header is worthless without the private key.
|
|
8
|
+
*
|
|
9
|
+
* The proof rides in `X-Naulon-Proof: <ts>.<nonce>.<sig>` where the agent signed
|
|
10
|
+
* `popMessage({ aud, jti, slug, ts, nonce })`. The gate reconstructs that exact
|
|
11
|
+
* message from the (already signature-verified) license claims plus the proof's
|
|
12
|
+
* own ts/nonce, recovers the signer with viem, and accepts iff:
|
|
13
|
+
* - the recovered address equals the wallet named in `cnf` (constant-compare
|
|
14
|
+
* by lowercased hex),
|
|
15
|
+
* - `ts` is within ±LICENSE_POP_WINDOW_SECONDS of the gate clock (freshness),
|
|
16
|
+
* - the (jti, nonce) pair has not been spent — single-use via the same
|
|
17
|
+
* ConsumedStore seam the 402 nonces use, so replay is closed in-window too.
|
|
18
|
+
*
|
|
19
|
+
* Fails CLOSED: any defect returns false, which drops the caller to the normal
|
|
20
|
+
* 402 path (pay again) — never a 500, never a free pass.
|
|
21
|
+
*/
|
|
22
|
+
import { recoverMessageAddress } from "viem";
|
|
23
|
+
import { getConfig, popBoundAddress, popMessage } from "@naulon/shared";
|
|
24
|
+
import { makeConsumedStore } from "./nonce.js";
|
|
25
|
+
const cfg = getConfig();
|
|
26
|
+
const windowSec = cfg.LICENSE_POP_WINDOW_SECONDS;
|
|
27
|
+
// Single-use store for proof nonces. Keys are namespaced `pop:<jti>:<nonce>` so
|
|
28
|
+
// they never collide with the 402 payment nonces in a shared Supabase table.
|
|
29
|
+
const proofStore = makeConsumedStore();
|
|
30
|
+
/** Hard cap on the proof header before any parsing (an EIP-191 sig is ~132 chars). */
|
|
31
|
+
const MAX_PROOF_LEN = 1024;
|
|
32
|
+
const HEX_NONCE = /^[0-9a-f]{8,64}$/;
|
|
33
|
+
const ETH_SIG = /^0x[0-9a-fA-F]{130}$/;
|
|
34
|
+
/**
|
|
35
|
+
* Verify a proof-of-possession header for a holder-of-key license. Returns true
|
|
36
|
+
* only if the proof is fresh, single-use, and signed by the bound wallet.
|
|
37
|
+
*/
|
|
38
|
+
export async function verifyPopProof(proofHeader, ctx) {
|
|
39
|
+
try {
|
|
40
|
+
if (typeof proofHeader !== "string" || proofHeader.length === 0 || proofHeader.length > MAX_PROOF_LEN) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const bound = popBoundAddress(ctx.claims);
|
|
44
|
+
if (!bound)
|
|
45
|
+
return false; // caller should only invoke this for cnf-bound licenses
|
|
46
|
+
const parts = proofHeader.split(".");
|
|
47
|
+
if (parts.length !== 3)
|
|
48
|
+
return false;
|
|
49
|
+
const [tsStr, nonce, sig] = parts;
|
|
50
|
+
if (!HEX_NONCE.test(nonce) || !ETH_SIG.test(sig))
|
|
51
|
+
return false;
|
|
52
|
+
const ts = Number(tsStr);
|
|
53
|
+
if (!Number.isInteger(ts))
|
|
54
|
+
return false;
|
|
55
|
+
// Freshness: the proof's self-asserted timestamp must sit within the window
|
|
56
|
+
// of the gate clock, in BOTH directions (stale replay and future-dating).
|
|
57
|
+
const nowSec = Math.floor(ctx.now / 1000);
|
|
58
|
+
if (Math.abs(nowSec - ts) > windowSec)
|
|
59
|
+
return false;
|
|
60
|
+
// Reconstruct the exact bytes the holder signed from the trusted claims.
|
|
61
|
+
const message = popMessage({ aud: ctx.identity, jti: ctx.claims.jti, slug: ctx.slug, ts, nonce });
|
|
62
|
+
const recovered = await recoverMessageAddress({ message, signature: sig });
|
|
63
|
+
if (recovered.toLowerCase() !== bound)
|
|
64
|
+
return false;
|
|
65
|
+
// Single-use: spend (jti, nonce) once. expMs bounds how long we must remember
|
|
66
|
+
// it — exactly the freshness window past the proof's timestamp.
|
|
67
|
+
const key = `pop:${ctx.claims.jti}:${nonce}`;
|
|
68
|
+
const expMs = (ts + windowSec) * 1000;
|
|
69
|
+
if (!(await proofStore.consume(key, expMs, ctx.now)))
|
|
70
|
+
return false; // replay
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false; // fail closed on any recover/parse error
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=pop.js.map
|
package/dist/pop.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pop.js","sourceRoot":"","sources":["../src/pop.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Price a read/citation and resolve who gets paid.
|
|
3
|
+
*
|
|
4
|
+
* Pricing is publisher-agnostic: it asks the resolved publisher's `CreditsResolver`
|
|
5
|
+
* for the article's credits graph, then flattens it to payees via the shared
|
|
6
|
+
* attribution logic. Everything publisher-specific (the credits source, the base
|
|
7
|
+
* price) arrives on the `PublisherConfig` — this function reads no global config, so
|
|
8
|
+
* one gate prices many publishers correctly.
|
|
9
|
+
*/
|
|
10
|
+
import { type AuthorShare, type PayoutLeg, type PublisherConfig, type TollKind, type Usdc } from "@naulon/shared";
|
|
11
|
+
export interface Quote {
|
|
12
|
+
slug: string;
|
|
13
|
+
title: string;
|
|
14
|
+
kind: TollKind;
|
|
15
|
+
price: Usdc;
|
|
16
|
+
payees: AuthorShare[];
|
|
17
|
+
/**
|
|
18
|
+
* Additional settlement legs beyond the author payment, resolved from the
|
|
19
|
+
* publisher's `extraLegs` hook. Empty for the single-tenant default and every
|
|
20
|
+
* publisher that declares no extra legs — in which case the 402 and settle path
|
|
21
|
+
* are byte-identical to a plain single-author toll. Additive: these never alter
|
|
22
|
+
* `price`/`payees`; the buyer's total is `price + Σ legs`.
|
|
23
|
+
*/
|
|
24
|
+
extraLegs: PayoutLeg[];
|
|
25
|
+
/**
|
|
26
|
+
* Pay co-authors directly on-chain (split-at-source) — carried from the resolved
|
|
27
|
+
* `PublisherConfig.coauthorSplit`. When true AND `payees.length > 1`, `build402`
|
|
28
|
+
* divides the author `price` into the primary's synchronous leg + one deferred leg
|
|
29
|
+
* per other co-author (custody-free). Off / single-author → the stock single
|
|
30
|
+
* author-leg toll. Never changes `price` or `payees` (the recorded truth); only the
|
|
31
|
+
* on-chain leg recipients/amounts.
|
|
32
|
+
*/
|
|
33
|
+
coauthorSplit: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Optional reconciliation id for the on-chain memo (Arc only). When set AND the
|
|
36
|
+
* active network ships the Memo predeploy, the synchronous author leg settles via
|
|
37
|
+
* the self-relay path and emits a `Memo` event keyed by this id (keccak256'd to
|
|
38
|
+
* `bytes32` if not already 32-byte hex) — tying the settlement to a citation /
|
|
39
|
+
* license id for offchain reconciliation. Absent, or on a memo-less network (Base),
|
|
40
|
+
* the settle path is byte-identical to the stock Circle Gateway toll. Supplied by
|
|
41
|
+
* the control plane; the open-core gate never invents
|
|
42
|
+
* one, so the default single-tenant gate is unaffected.
|
|
43
|
+
*/
|
|
44
|
+
memoId?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Price a toll event for one publisher. Citations cost more than a single read (a
|
|
48
|
+
* citation has downstream reach), but both resolve to the same author payees.
|
|
49
|
+
* Returns undefined for an article the publisher's credits source doesn't know —
|
|
50
|
+
* the gate treats that as "don't gate".
|
|
51
|
+
*/
|
|
52
|
+
export declare function quote(publisher: PublisherConfig, slug: string, kind: TollKind): Promise<Quote | undefined>;
|
|
53
|
+
//# sourceMappingURL=pricing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../src/pricing.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,QAAQ,EACb,KAAK,IAAI,EACV,MAAM,gBAAgB,CAAC;AAExB,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,QAAQ,CAAC;IACf,KAAK,EAAE,IAAI,CAAC;IACZ,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB;;;;;;OAMG;IACH,SAAS,EAAE,SAAS,EAAE,CAAC;IACvB;;;;;;;OAOG;IACH,aAAa,EAAE,OAAO,CAAC;IACvB;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAsB,KAAK,CACzB,SAAS,EAAE,eAAe,EAC1B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,QAAQ,GACb,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,CA2B5B"}
|
package/dist/pricing.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Price a read/citation and resolve who gets paid.
|
|
3
|
+
*
|
|
4
|
+
* Pricing is publisher-agnostic: it asks the resolved publisher's `CreditsResolver`
|
|
5
|
+
* for the article's credits graph, then flattens it to payees via the shared
|
|
6
|
+
* attribution logic. Everything publisher-specific (the credits source, the base
|
|
7
|
+
* price) arrives on the `PublisherConfig` — this function reads no global config, so
|
|
8
|
+
* one gate prices many publishers correctly.
|
|
9
|
+
*/
|
|
10
|
+
import { resolvePayees, usdc, } from "@naulon/shared";
|
|
11
|
+
/**
|
|
12
|
+
* Price a toll event for one publisher. Citations cost more than a single read (a
|
|
13
|
+
* citation has downstream reach), but both resolve to the same author payees.
|
|
14
|
+
* Returns undefined for an article the publisher's credits source doesn't know —
|
|
15
|
+
* the gate treats that as "don't gate".
|
|
16
|
+
*/
|
|
17
|
+
export async function quote(publisher, slug, kind) {
|
|
18
|
+
const credits = await publisher.credits.resolve(slug);
|
|
19
|
+
if (!credits)
|
|
20
|
+
return undefined;
|
|
21
|
+
const price = usdc(kind === "citation" ? publisher.price * publisher.citationMultiplier : publisher.price);
|
|
22
|
+
return {
|
|
23
|
+
slug: credits.slug,
|
|
24
|
+
title: credits.title,
|
|
25
|
+
kind,
|
|
26
|
+
price,
|
|
27
|
+
payees: resolvePayees(credits),
|
|
28
|
+
// Additive secondary legs (none for the single-tenant default). The hook owns
|
|
29
|
+
// all amount math; pricing just carries what it returns through to the quote.
|
|
30
|
+
extraLegs: publisher.extraLegs?.(price, kind) ?? [],
|
|
31
|
+
// Carried through to build402, which owns the split-at-source math (it has the
|
|
32
|
+
// primary-payee tiebreak config). Off unless the resolver opts the publisher in.
|
|
33
|
+
coauthorSplit: publisher.coauthorSplit ?? false,
|
|
34
|
+
// The control plane owns the memo id's format; the core just carries what the
|
|
35
|
+
// hook returns. Spread so an unset hook (or one returning undefined) leaves the
|
|
36
|
+
// key absent entirely — the settle path then keys the memo off the auth nonce,
|
|
37
|
+
// byte-identical to the stock single-tenant toll.
|
|
38
|
+
...(() => {
|
|
39
|
+
const memoId = publisher.memoId?.({ slug: credits.slug, kind });
|
|
40
|
+
return memoId ? { memoId } : {};
|
|
41
|
+
})(),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=pricing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pricing.js","sourceRoot":"","sources":["../src/pricing.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EACL,aAAa,EACb,IAAI,GAML,MAAM,gBAAgB,CAAC;AAsCxB;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,KAAK,CACzB,SAA0B,EAC1B,IAAY,EACZ,IAAc;IAEd,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD,IAAI,CAAC,OAAO;QAAE,OAAO,SAAS,CAAC;IAE/B,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,GAAG,SAAS,CAAC,kBAAkB,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAE3G,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,IAAI;QACJ,KAAK;QACL,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC;QAC9B,8EAA8E;QAC9E,8EAA8E;QAC9E,SAAS,EAAE,SAAS,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE;QACnD,+EAA+E;QAC/E,iFAAiF;QACjF,aAAa,EAAE,SAAS,CAAC,aAAa,IAAI,KAAK;QAC/C,8EAA8E;QAC9E,gFAAgF;QAChF,+EAA+E;QAC/E,kDAAkD;QAClD,GAAG,CAAC,GAAG,EAAE;YACP,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YAChE,OAAO,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClC,CAAC,CAAC,EAAE;KACL,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revocation.d.ts","sourceRoot":"","sources":["../src/revocation.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAkCD,eAAO,MAAM,WAAW,EAAE,eAA0E,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* License revocation seam (the v1 kill switch for a leaked token before its exp).
|
|
3
|
+
*
|
|
4
|
+
* A CLT is a bearer entitlement, so the only defence against a captured-but-
|
|
5
|
+
* unexpired token — beyond the short TTL — is denying its `jti`. This is enforced
|
|
6
|
+
* ONLY on the trusted tiers: the gate's own re-read path and `GET /licenses/:jti`
|
|
7
|
+
* (when LICENSE_ONLINE_CHECK is on). The offline JWKS tier can't consult it by
|
|
8
|
+
* design; that residual window is bounded by the TTL (see docs/citation-license.md).
|
|
9
|
+
*
|
|
10
|
+
* Backend mirrors the event/nonce stores: an in-process Set by default, or a
|
|
11
|
+
* shared Supabase table (`jti` primary key) when a supabase backend is in play so
|
|
12
|
+
* revocations hold across instances.
|
|
13
|
+
*/
|
|
14
|
+
import { getConfig, supabaseRest } from "@naulon/shared";
|
|
15
|
+
const cfg = getConfig();
|
|
16
|
+
function memoryRevocation() {
|
|
17
|
+
const revoked = new Set();
|
|
18
|
+
return {
|
|
19
|
+
async isRevoked(jti) {
|
|
20
|
+
return revoked.has(jti);
|
|
21
|
+
},
|
|
22
|
+
async revoke(jti) {
|
|
23
|
+
revoked.add(jti);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function supabaseRevocation() {
|
|
28
|
+
const table = cfg.SUPABASE_REVOCATIONS_TABLE;
|
|
29
|
+
return {
|
|
30
|
+
async isRevoked(jti) {
|
|
31
|
+
const rows = (await supabaseRest(`/rest/v1/${table}?jti=eq.${encodeURIComponent(jti)}&select=jti&limit=1`));
|
|
32
|
+
return rows.length > 0;
|
|
33
|
+
},
|
|
34
|
+
async revoke(jti) {
|
|
35
|
+
await supabaseRest(`/rest/v1/${table}?on_conflict=jti`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { Prefer: "resolution=ignore-duplicates" },
|
|
38
|
+
body: JSON.stringify([{ jti }]),
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const usesSupabase = cfg.EVENTS_BACKEND === "supabase" || cfg.NONCE_BACKEND === "supabase";
|
|
44
|
+
export const revocations = usesSupabase ? supabaseRevocation() : memoryRevocation();
|
|
45
|
+
//# sourceMappingURL=revocation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revocation.js","sourceRoot":"","sources":["../src/revocation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEzD,MAAM,GAAG,GAAG,SAAS,EAAE,CAAC;AAOxB,SAAS,gBAAgB;IACvB,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,OAAO;QACL,KAAK,CAAC,SAAS,CAAC,GAAG;YACjB,OAAO,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC1B,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,GAAG;YACd,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,kBAAkB;IACzB,MAAM,KAAK,GAAG,GAAG,CAAC,0BAA0B,CAAC;IAC7C,OAAO;QACL,KAAK,CAAC,SAAS,CAAC,GAAG;YACjB,MAAM,IAAI,GAAG,CAAC,MAAM,YAAY,CAC9B,YAAY,KAAK,WAAW,kBAAkB,CAAC,GAAG,CAAC,qBAAqB,CACzE,CAAc,CAAC;YAChB,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;QACzB,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,GAAG;YACd,MAAM,YAAY,CAAC,YAAY,KAAK,kBAAkB,EAAE;gBACtD,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,MAAM,EAAE,8BAA8B,EAAE;gBACnD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC;aAChC,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,YAAY,GAAG,GAAG,CAAC,cAAc,KAAK,UAAU,IAAI,GAAG,CAAC,aAAa,KAAK,UAAU,CAAC;AAC3F,MAAM,CAAC,MAAM,WAAW,GAAoB,YAAY,CAAC,CAAC,CAAC,kBAAkB,EAAE,CAAC,CAAC,CAAC,gBAAgB,EAAE,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@naulon/enforce",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/naulonapp/naulon.git",
|
|
7
|
+
"directory": "packages/enforce"
|
|
8
|
+
},
|
|
9
|
+
"description": "Runtime-agnostic naulon toll-decision kernel + in-app enforcement middleware. The neutral low-level core shared by @naulon/tollgate (the gate) and @naulon/sdk (the publisher SDK) — no dependency cycle.",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./next": {
|
|
20
|
+
"types": "./dist/enforce/next.d.ts",
|
|
21
|
+
"import": "./dist/enforce/next.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc -p tsconfig.json",
|
|
29
|
+
"prepublishOnly": "npm run build",
|
|
30
|
+
"test": "node --import tsx --test \"src/**/*.test.ts\""
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@naulon/shared": "^0.0.1",
|
|
34
|
+
"viem": "^2.52.2"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"next": ">=15"
|
|
38
|
+
},
|
|
39
|
+
"peerDependenciesMeta": {
|
|
40
|
+
"next": {
|
|
41
|
+
"optional": true
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=22"
|
|
46
|
+
}
|
|
47
|
+
}
|