@reposkein/mcp 0.4.0 → 0.6.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.
Files changed (78) hide show
  1. package/README.md +6 -1
  2. package/binary-digests.json +4 -4
  3. package/dist/ads/auditLog.d.ts +23 -0
  4. package/dist/ads/auditLog.js +33 -0
  5. package/dist/ads/auditLog.js.map +1 -0
  6. package/dist/ads/config.d.ts +83 -0
  7. package/dist/ads/config.js +107 -0
  8. package/dist/ads/config.js.map +1 -0
  9. package/dist/ads/luluSource.d.ts +32 -0
  10. package/dist/ads/luluSource.js +60 -0
  11. package/dist/ads/luluSource.js.map +1 -0
  12. package/dist/ads/sanitize.d.ts +16 -0
  13. package/dist/ads/sanitize.js +187 -0
  14. package/dist/ads/sanitize.js.map +1 -0
  15. package/dist/ads/slot.d.ts +63 -0
  16. package/dist/ads/slot.js +159 -0
  17. package/dist/ads/slot.js.map +1 -0
  18. package/dist/ads/supporter.d.ts +73 -0
  19. package/dist/ads/supporter.js +119 -0
  20. package/dist/ads/supporter.js.map +1 -0
  21. package/dist/ads/supporterKey.d.ts +45 -0
  22. package/dist/ads/supporterKey.js +51 -0
  23. package/dist/ads/supporterKey.js.map +1 -0
  24. package/dist/ads/supporterStore.d.ts +61 -0
  25. package/dist/ads/supporterStore.js +113 -0
  26. package/dist/ads/supporterStore.js.map +1 -0
  27. package/dist/ads/supporterToken.d.ts +133 -0
  28. package/dist/ads/supporterToken.js +233 -0
  29. package/dist/ads/supporterToken.js.map +1 -0
  30. package/dist/ads/types.d.ts +53 -0
  31. package/dist/ads/types.js +15 -0
  32. package/dist/ads/types.js.map +1 -0
  33. package/dist/cli/support.d.ts +46 -0
  34. package/dist/cli/support.js +276 -0
  35. package/dist/cli/support.js.map +1 -0
  36. package/dist/cli/view.js +27 -6
  37. package/dist/cli/view.js.map +1 -1
  38. package/dist/index.d.ts +8 -56
  39. package/dist/index.js +45 -402
  40. package/dist/index.js.map +1 -1
  41. package/dist/indexer/indexLock.d.ts +13 -0
  42. package/dist/indexer/indexLock.js +66 -0
  43. package/dist/indexer/indexLock.js.map +1 -0
  44. package/dist/indexer/runIndexer.d.ts +8 -0
  45. package/dist/indexer/runIndexer.js +12 -0
  46. package/dist/indexer/runIndexer.js.map +1 -1
  47. package/dist/serve/serve.d.ts +95 -0
  48. package/dist/serve/serve.js +581 -0
  49. package/dist/serve/serve.js.map +1 -0
  50. package/dist/serve/tokens.d.ts +49 -0
  51. package/dist/serve/tokens.js +108 -0
  52. package/dist/serve/tokens.js.map +1 -0
  53. package/dist/serve/watch.d.ts +73 -0
  54. package/dist/serve/watch.js +135 -0
  55. package/dist/serve/watch.js.map +1 -0
  56. package/dist/server/createMcpServer.d.ts +134 -0
  57. package/dist/server/createMcpServer.js +473 -0
  58. package/dist/server/createMcpServer.js.map +1 -0
  59. package/dist/store/JsonlGraphStore.d.ts +5 -1
  60. package/dist/store/JsonlGraphStore.js +6 -2
  61. package/dist/store/JsonlGraphStore.js.map +1 -1
  62. package/dist/store/teamConfig.d.ts +20 -6
  63. package/dist/store/teamConfig.js +70 -17
  64. package/dist/store/teamConfig.js.map +1 -1
  65. package/dist/tools/recordDecision.d.ts +3 -0
  66. package/dist/tools/recordDecision.js +1 -1
  67. package/dist/tools/recordDecision.js.map +1 -1
  68. package/dist/tools/writeSemanticSummary.d.ts +4 -1
  69. package/dist/tools/writeSemanticSummary.js +5 -2
  70. package/dist/tools/writeSemanticSummary.js.map +1 -1
  71. package/dist/viz/assets/index-BnqIVYFE.css +1 -0
  72. package/dist/viz/assets/index-IhtszTp0.js +5 -0
  73. package/dist/viz/assets/{r3f-BZY3QR6_.js → r3f-Bn9htB92.js} +1 -1
  74. package/dist/viz/assets/{tanstack-CjXr-L-s.js → tanstack-CSzx6NDA.js} +7 -7
  75. package/dist/viz/index.html +4 -4
  76. package/package.json +3 -2
  77. package/dist/viz/assets/index-CxqeGuRS.css +0 -1
  78. package/dist/viz/assets/index-DiNZyrXx.js +0 -9
@@ -0,0 +1,113 @@
1
+ /** Where a supporter token lives on disk, and the only code that puts it
2
+ * there.
3
+ *
4
+ * ## User-level, never repo-level
5
+ *
6
+ * Entitlement belongs to a PERSON, not to a checkout. Storing it under
7
+ * `.reposkein/` would be wrong three times over: it would ride into git (or
8
+ * need yet another ignore rule to stop it), it would be shared with everyone
9
+ * who clones the repo — handing them a token they did not pay for — and it
10
+ * would have to be re-supplied in every working copy by the one person who
11
+ * did. So it lives under the user's config directory and nothing in this
12
+ * package ever writes an entitlement byte inside a repository. There is a
13
+ * test that asserts exactly that by byte-comparing a repo tree across a
14
+ * `support` invocation.
15
+ *
16
+ * ## Permissions
17
+ *
18
+ * Directory 0700, file 0600, and the file mode is re-applied with an
19
+ * explicit `chmodSync` after every write: `writeFileSync`'s `mode` option is
20
+ * honoured only when the file is CREATED, so overwriting an existing
21
+ * world-readable token would otherwise silently leave it world-readable.
22
+ * The token is not a password — the worst a thief gets is no ads — but a
23
+ * paid entitlement is still the holder's to keep, and 0600 costs nothing.
24
+ *
25
+ * On Windows these modes are approximated by the runtime; `--status` reports
26
+ * the mode it actually observes rather than asserting a POSIX guarantee the
27
+ * platform does not offer. */
28
+ import { chmodSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
29
+ import { homedir } from "node:os";
30
+ import { dirname, isAbsolute, join } from "node:path";
31
+ /** Overrides the entitlement file location. Exists for tests and for anyone
32
+ * running with an unusual HOME; it is NOT a bypass — the file it points at
33
+ * still has to contain a validly signed token, so redirecting it cannot
34
+ * manufacture entitlement. */
35
+ export const SUPPORTER_FILE_ENV = "REPOSKEIN_SUPPORTER_FILE";
36
+ const DIR_NAME = "reposkein";
37
+ const FILE_NAME = "supporter.jwt";
38
+ /** Resolves the entitlement file path, in order:
39
+ *
40
+ * 1. `REPOSKEIN_SUPPORTER_FILE`, if absolute.
41
+ * 2. `$XDG_CONFIG_HOME/reposkein/supporter.jwt`, if absolute.
42
+ * 3. `~/.config/reposkein/supporter.jwt` — the documented location. */
43
+ export function supporterTokenPath(env = process.env) {
44
+ const override = (env[SUPPORTER_FILE_ENV] ?? "").trim();
45
+ if (override && isAbsolute(override))
46
+ return override;
47
+ const xdg = (env.XDG_CONFIG_HOME ?? "").trim();
48
+ const base = xdg && isAbsolute(xdg) ? xdg : join(homedir(), ".config");
49
+ return join(base, DIR_NAME, FILE_NAME);
50
+ }
51
+ /** Cheap existence + identity probe. Returns null for "no file" and for any
52
+ * error reading it — an unreadable entitlement file is indistinguishable
53
+ * from an absent one as far as entitlement goes, and this runs on a tool
54
+ * call's hot path, so it must never throw. */
55
+ export function statSupporterTokenFile(env = process.env) {
56
+ try {
57
+ const st = statSync(supporterTokenPath(env));
58
+ if (!st.isFile())
59
+ return null;
60
+ return { mtimeMs: st.mtimeMs, size: st.size, mode: st.mode & 0o777 };
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ /** Reads the token text, or null if there is nothing readable there. Never
67
+ * throws. Size is bounded by the caller's own token-length limit; a file
68
+ * larger than that fails verification rather than being read into memory
69
+ * blindly, so the read is capped here too. */
70
+ export function readSupporterTokenFile(env = process.env, maxBytes = 64 * 1024) {
71
+ try {
72
+ const path = supporterTokenPath(env);
73
+ const st = statSync(path);
74
+ if (!st.isFile() || st.size > maxBytes)
75
+ return null;
76
+ const text = readFileSync(path, "utf8").trim();
77
+ return text.length === 0 ? null : text;
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
83
+ /** Writes the token, creating `~/.config/reposkein/` as 0700 if needed, and
84
+ * returns the path written. Throws on failure — this one is called from the
85
+ * CLI, where a failure must be reported, not swallowed. */
86
+ export function writeSupporterTokenFile(token, env = process.env) {
87
+ const path = supporterTokenPath(env);
88
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
89
+ writeFileSync(path, `${token.trim()}\n`, { mode: 0o600 });
90
+ // Re-assert: `mode` above applies only on creation.
91
+ try {
92
+ chmodSync(path, 0o600);
93
+ }
94
+ catch {
95
+ // Filesystems without POSIX modes (some Windows/network mounts). The
96
+ // write itself succeeded, which is what the caller asked for.
97
+ }
98
+ return path;
99
+ }
100
+ /** Deletes the entitlement file. Returns true if a file was removed, false if
101
+ * there was nothing to remove. */
102
+ export function removeSupporterTokenFile(env = process.env) {
103
+ const path = supporterTokenPath(env);
104
+ try {
105
+ statSync(path);
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ rmSync(path, { force: true });
111
+ return true;
112
+ }
113
+ //# sourceMappingURL=supporterStore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supporterStore.js","sourceRoot":"","sources":["../../src/ads/supporterStore.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,133 @@
1
+ /** The supporter-entitlement token: format, minting, and verification.
2
+ *
3
+ * ## Why not a JWT
4
+ *
5
+ * A JWT's header is attacker-controlled — the `alg` field in particular has
6
+ * a long history of `none`/`HS256`-confusion bugs, and every JWT library
7
+ * carries the machinery to be configured wrong. This token has no header at
8
+ * all. The algorithm is Ed25519 because it is fixed in this file, the key is
9
+ * whichever committed public key the payload's `kid` names, and there is no
10
+ * wire field that can change either. That removes the entire class of
11
+ * algorithm-confusion attacks and removes a dependency at the same time:
12
+ * `node:crypto` speaks Ed25519 natively, so nothing new is installed.
13
+ *
14
+ * ## Wire format
15
+ *
16
+ * rsk1.<base64url(payload JSON)>.<base64url(signature)>
17
+ *
18
+ * The signature covers the ASCII bytes of `rsk1.<payloadSegment>` — the
19
+ * ENCODED segment, not the decoded JSON. Signing the encoding sidesteps
20
+ * every JSON-canonicalization question (key order, whitespace, number
21
+ * formatting): there is exactly one byte string that was signed, and it is
22
+ * the one sitting in the token. The `rsk1.` prefix is inside the signed
23
+ * input too, so a future `rsk2` format cannot be produced by re-labelling an
24
+ * `rsk1` token.
25
+ *
26
+ * ## Payload
27
+ *
28
+ * { v: 1, kid: "skein-2026-08", sub: "<opaque>", tier: "skein",
29
+ * iat: <unix seconds>, exp: <unix seconds> }
30
+ *
31
+ * `sub` is an opaque hash minted by the fulfilment worker (an HMAC of the
32
+ * supporter's Ko-fi email under a server-side salt), present only so a
33
+ * re-issued token can be recognised as the same subscription. It is not
34
+ * reversible to an email, it is never transmitted anywhere by this package,
35
+ * and nothing in RepoSkein reads it except `support --status`, which prints
36
+ * a prefix of it so a supporter can tell two tokens apart. There is no
37
+ * account, no device id, and no counter.
38
+ *
39
+ * ## What verification deliberately does NOT do
40
+ *
41
+ * No revocation list, and therefore no network call. A revocation check is a
42
+ * phone-home by another name, and the thing being protected is the absence
43
+ * of an ad — the least valuable secret in the system. A leaked token buys a
44
+ * stranger an ad-free experience they could equally have by setting
45
+ * `REPOSKEIN_ADS=off`. Expiry plus re-issue is the entire enforcement model,
46
+ * and that is proportionate. */
47
+ /** Format marker and the first component of the signed input. */
48
+ export declare const SUPPORTER_TOKEN_PREFIX = "rsk1";
49
+ /** The only tier that entitles anyone to anything today. Compared exactly. */
50
+ export declare const SUPPORTER_TIER = "skein";
51
+ /** How long a token keeps working after `exp`.
52
+ *
53
+ * Three days, and the reason is renewal timing, not generosity: Ko-fi bills
54
+ * a membership on its own schedule and the fulfilment worker mints the
55
+ * replacement only when that payment lands. Without a grace window a
56
+ * supporter whose renewal posts a few hours late would watch a paid-for
57
+ * feature switch itself off. Three days covers a weekend plus a payment
58
+ * retry; it is short enough that a cancelled membership stops mattering
59
+ * within the week. */
60
+ export declare const SUPPORTER_GRACE_MS: number;
61
+ /** Tolerance for a clock that is behind the minter's. A token whose `iat` is
62
+ * further in the future than this is rejected — a legitimate minter cannot
63
+ * produce one, so it means either a badly wrong clock or a forged payload,
64
+ * and both should fail loudly rather than quietly grant entitlement later. */
65
+ export declare const SUPPORTER_MAX_CLOCK_SKEW_MS: number;
66
+ /** Longest lifetime any single token may claim (400 days). Caps the blast
67
+ * radius if the signing key is ever compromised: an attacker with the key
68
+ * still cannot mint a token that outlives one rotation cycle. Renewal, not
69
+ * longevity, is the model. */
70
+ export declare const SUPPORTER_MAX_LIFETIME_MS: number;
71
+ /** Refuse to even parse anything larger. A supporter token is ~200 bytes; a
72
+ * megabyte of base64 in the entitlement file is not a token. */
73
+ export declare const SUPPORTER_MAX_TOKEN_BYTES = 4096;
74
+ export interface SupporterClaims {
75
+ v: 1;
76
+ kid: string;
77
+ sub: string;
78
+ tier: string;
79
+ /** Issued-at, unix SECONDS (not milliseconds). */
80
+ iat: number;
81
+ /** Expiry, unix SECONDS. */
82
+ exp: number;
83
+ }
84
+ /** Why a token was refused. Diagnostic only — shown by `support --status`,
85
+ * never transmitted. */
86
+ export type SupporterRejection = "empty" | "too_large" | "malformed" | "unknown_key" | "bad_signature" | "bad_payload" | "wrong_tier" | "implausible_lifetime" | "not_yet_valid";
87
+ export type SupporterVerdict =
88
+ /** Signature good, tier right, `exp` in the future. */
89
+ {
90
+ state: "valid";
91
+ claims: SupporterClaims;
92
+ }
93
+ /** Signature good, past `exp`, still inside the grace window. */
94
+ | {
95
+ state: "grace";
96
+ claims: SupporterClaims;
97
+ }
98
+ /** Signature good, past `exp` + grace. Claims are returned so the CLI can
99
+ * say *when* it lapsed rather than just "no". */
100
+ | {
101
+ state: "expired";
102
+ claims: SupporterClaims;
103
+ }
104
+ /** Never trustworthy at any time. */
105
+ | {
106
+ state: "invalid";
107
+ reason: SupporterRejection;
108
+ };
109
+ /** True when the verdict entitles the holder. The one place the
110
+ * valid/grace distinction collapses. */
111
+ export declare function isEntitled(verdict: SupporterVerdict): boolean;
112
+ /** Mints a token. Needs the PRIVATE key, which lives only in the fulfilment
113
+ * worker's secret store (see `supporterKey.ts` provenance) — nothing in the
114
+ * published package or the test suite uses the real one. Exported because a
115
+ * format is only trustworthy if the thing that writes it and the thing that
116
+ * reads it are specified together, and because the round-trip tests need to
117
+ * sign with a throwaway key. */
118
+ export declare function signSupporterToken(claims: SupporterClaims, privateKeyPem: string): string;
119
+ /** Full verification: shape, signature, tier, then expiry against `now`.
120
+ * Pure and synchronous — no filesystem, no clock injection beyond `now`, and
121
+ * nothing that can touch a socket. */
122
+ export declare function verifySupporterToken(token: string, now?: number): SupporterVerdict;
123
+ /** The time-dependent half, over claims whose signature is already trusted. */
124
+ export declare function classifyClaims(claims: SupporterClaims, now?: number): SupporterVerdict;
125
+ /** Verifies shape + signature only, deferring every time-dependent decision
126
+ * to `classifyClaims`. `isSupporter()`'s cache is built on this split. */
127
+ export declare function verifySupporterTokenClaims(token: string): {
128
+ ok: true;
129
+ claims: SupporterClaims;
130
+ } | {
131
+ ok: false;
132
+ reason: SupporterRejection;
133
+ };
@@ -0,0 +1,233 @@
1
+ /** The supporter-entitlement token: format, minting, and verification.
2
+ *
3
+ * ## Why not a JWT
4
+ *
5
+ * A JWT's header is attacker-controlled — the `alg` field in particular has
6
+ * a long history of `none`/`HS256`-confusion bugs, and every JWT library
7
+ * carries the machinery to be configured wrong. This token has no header at
8
+ * all. The algorithm is Ed25519 because it is fixed in this file, the key is
9
+ * whichever committed public key the payload's `kid` names, and there is no
10
+ * wire field that can change either. That removes the entire class of
11
+ * algorithm-confusion attacks and removes a dependency at the same time:
12
+ * `node:crypto` speaks Ed25519 natively, so nothing new is installed.
13
+ *
14
+ * ## Wire format
15
+ *
16
+ * rsk1.<base64url(payload JSON)>.<base64url(signature)>
17
+ *
18
+ * The signature covers the ASCII bytes of `rsk1.<payloadSegment>` — the
19
+ * ENCODED segment, not the decoded JSON. Signing the encoding sidesteps
20
+ * every JSON-canonicalization question (key order, whitespace, number
21
+ * formatting): there is exactly one byte string that was signed, and it is
22
+ * the one sitting in the token. The `rsk1.` prefix is inside the signed
23
+ * input too, so a future `rsk2` format cannot be produced by re-labelling an
24
+ * `rsk1` token.
25
+ *
26
+ * ## Payload
27
+ *
28
+ * { v: 1, kid: "skein-2026-08", sub: "<opaque>", tier: "skein",
29
+ * iat: <unix seconds>, exp: <unix seconds> }
30
+ *
31
+ * `sub` is an opaque hash minted by the fulfilment worker (an HMAC of the
32
+ * supporter's Ko-fi email under a server-side salt), present only so a
33
+ * re-issued token can be recognised as the same subscription. It is not
34
+ * reversible to an email, it is never transmitted anywhere by this package,
35
+ * and nothing in RepoSkein reads it except `support --status`, which prints
36
+ * a prefix of it so a supporter can tell two tokens apart. There is no
37
+ * account, no device id, and no counter.
38
+ *
39
+ * ## What verification deliberately does NOT do
40
+ *
41
+ * No revocation list, and therefore no network call. A revocation check is a
42
+ * phone-home by another name, and the thing being protected is the absence
43
+ * of an ad — the least valuable secret in the system. A leaked token buys a
44
+ * stranger an ad-free experience they could equally have by setting
45
+ * `REPOSKEIN_ADS=off`. Expiry plus re-issue is the entire enforcement model,
46
+ * and that is proportionate. */
47
+ import { createPublicKey, createPrivateKey, sign as cryptoSign, verify as cryptoVerify } from "node:crypto";
48
+ import { SUPPORTER_PUBLIC_KEYS } from "./supporterKey.js";
49
+ /** Format marker and the first component of the signed input. */
50
+ export const SUPPORTER_TOKEN_PREFIX = "rsk1";
51
+ /** The only tier that entitles anyone to anything today. Compared exactly. */
52
+ export const SUPPORTER_TIER = "skein";
53
+ /** How long a token keeps working after `exp`.
54
+ *
55
+ * Three days, and the reason is renewal timing, not generosity: Ko-fi bills
56
+ * a membership on its own schedule and the fulfilment worker mints the
57
+ * replacement only when that payment lands. Without a grace window a
58
+ * supporter whose renewal posts a few hours late would watch a paid-for
59
+ * feature switch itself off. Three days covers a weekend plus a payment
60
+ * retry; it is short enough that a cancelled membership stops mattering
61
+ * within the week. */
62
+ export const SUPPORTER_GRACE_MS = 3 * 24 * 60 * 60 * 1000;
63
+ /** Tolerance for a clock that is behind the minter's. A token whose `iat` is
64
+ * further in the future than this is rejected — a legitimate minter cannot
65
+ * produce one, so it means either a badly wrong clock or a forged payload,
66
+ * and both should fail loudly rather than quietly grant entitlement later. */
67
+ export const SUPPORTER_MAX_CLOCK_SKEW_MS = 24 * 60 * 60 * 1000;
68
+ /** Longest lifetime any single token may claim (400 days). Caps the blast
69
+ * radius if the signing key is ever compromised: an attacker with the key
70
+ * still cannot mint a token that outlives one rotation cycle. Renewal, not
71
+ * longevity, is the model. */
72
+ export const SUPPORTER_MAX_LIFETIME_MS = 400 * 24 * 60 * 60 * 1000;
73
+ /** Refuse to even parse anything larger. A supporter token is ~200 bytes; a
74
+ * megabyte of base64 in the entitlement file is not a token. */
75
+ export const SUPPORTER_MAX_TOKEN_BYTES = 4096;
76
+ /** True when the verdict entitles the holder. The one place the
77
+ * valid/grace distinction collapses. */
78
+ export function isEntitled(verdict) {
79
+ return verdict.state === "valid" || verdict.state === "grace";
80
+ }
81
+ const SEGMENT_RE = /^[A-Za-z0-9_-]+$/;
82
+ const KID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
83
+ const SUB_RE = /^[A-Za-z0-9_-]{8,64}$/;
84
+ function b64uEncode(buf) {
85
+ return buf.toString("base64url");
86
+ }
87
+ /** Decodes a segment only if it is canonical base64url: the alphabet, no
88
+ * padding, no whitespace. `Buffer.from(..., "base64url")` is famously
89
+ * lenient (it will happily skip garbage), so the charset gate happens first
90
+ * and a token with a sloppy encoding is simply not a token. */
91
+ function b64uDecode(seg) {
92
+ if (!SEGMENT_RE.test(seg))
93
+ return null;
94
+ const buf = Buffer.from(seg, "base64url");
95
+ if (buf.length === 0)
96
+ return null;
97
+ // Round-trip guard: re-encoding must reproduce the segment exactly, which
98
+ // rules out the trailing-bit slack base64 otherwise tolerates.
99
+ if (buf.toString("base64url") !== seg)
100
+ return null;
101
+ return buf;
102
+ }
103
+ /** Mints a token. Needs the PRIVATE key, which lives only in the fulfilment
104
+ * worker's secret store (see `supporterKey.ts` provenance) — nothing in the
105
+ * published package or the test suite uses the real one. Exported because a
106
+ * format is only trustworthy if the thing that writes it and the thing that
107
+ * reads it are specified together, and because the round-trip tests need to
108
+ * sign with a throwaway key. */
109
+ export function signSupporterToken(claims, privateKeyPem) {
110
+ const key = createPrivateKey(privateKeyPem);
111
+ if (key.asymmetricKeyType !== "ed25519") {
112
+ throw new Error(`supporter token: signing key must be ed25519, got ${key.asymmetricKeyType ?? "unknown"}`);
113
+ }
114
+ const payloadSeg = b64uEncode(Buffer.from(JSON.stringify(claims), "utf8"));
115
+ const signingInput = `${SUPPORTER_TOKEN_PREFIX}.${payloadSeg}`;
116
+ // `null` algorithm: Ed25519 hashes internally and node:crypto requires the
117
+ // digest argument to be null for it.
118
+ const sig = cryptoSign(null, Buffer.from(signingInput, "ascii"), key);
119
+ return `${signingInput}.${b64uEncode(sig)}`;
120
+ }
121
+ /** Structural + cryptographic checks, with no notion of "now". Split out so
122
+ * the time-dependent part is a pure function of already-verified claims —
123
+ * which is what lets `isSupporter()` cache the expensive half and re-decide
124
+ * expiry on every call for free. */
125
+ function verifyShapeAndSignature(token) {
126
+ const trimmed = token.trim();
127
+ if (trimmed.length === 0)
128
+ return { ok: false, reason: "empty" };
129
+ if (Buffer.byteLength(trimmed, "utf8") > SUPPORTER_MAX_TOKEN_BYTES)
130
+ return { ok: false, reason: "too_large" };
131
+ const parts = trimmed.split(".");
132
+ if (parts.length !== 3)
133
+ return { ok: false, reason: "malformed" };
134
+ const [prefix, payloadSeg, sigSeg] = parts;
135
+ if (prefix !== SUPPORTER_TOKEN_PREFIX)
136
+ return { ok: false, reason: "malformed" };
137
+ const sigBytes = b64uDecode(sigSeg);
138
+ // Ed25519 signatures are exactly 64 bytes; anything else is not one.
139
+ if (!sigBytes || sigBytes.length !== 64)
140
+ return { ok: false, reason: "malformed" };
141
+ const payloadBytes = b64uDecode(payloadSeg);
142
+ if (!payloadBytes)
143
+ return { ok: false, reason: "malformed" };
144
+ let parsed;
145
+ try {
146
+ parsed = JSON.parse(payloadBytes.toString("utf8"));
147
+ }
148
+ catch {
149
+ return { ok: false, reason: "bad_payload" };
150
+ }
151
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
152
+ return { ok: false, reason: "bad_payload" };
153
+ const p = parsed;
154
+ if (p.v !== 1)
155
+ return { ok: false, reason: "bad_payload" };
156
+ const kid = p.kid;
157
+ if (typeof kid !== "string" || !KID_RE.test(kid))
158
+ return { ok: false, reason: "bad_payload" };
159
+ const sub = p.sub;
160
+ if (typeof sub !== "string" || !SUB_RE.test(sub))
161
+ return { ok: false, reason: "bad_payload" };
162
+ const tier = p.tier;
163
+ if (typeof tier !== "string")
164
+ return { ok: false, reason: "bad_payload" };
165
+ const iat = p.iat;
166
+ const exp = p.exp;
167
+ if (!Number.isSafeInteger(iat) || !Number.isSafeInteger(exp))
168
+ return { ok: false, reason: "bad_payload" };
169
+ const iatN = iat;
170
+ const expN = exp;
171
+ if (iatN <= 0 || expN <= iatN)
172
+ return { ok: false, reason: "bad_payload" };
173
+ // Key selection happens BEFORE the signature check and is driven entirely
174
+ // by the committed key map: an unknown `kid` can never reach verification,
175
+ // so the token cannot nominate its own trust root.
176
+ const pem = Object.prototype.hasOwnProperty.call(SUPPORTER_PUBLIC_KEYS, kid)
177
+ ? SUPPORTER_PUBLIC_KEYS[kid]
178
+ : undefined;
179
+ if (!pem)
180
+ return { ok: false, reason: "unknown_key" };
181
+ let signatureOk = false;
182
+ try {
183
+ const publicKey = createPublicKey(pem);
184
+ // Belt and braces against a future edit to the committed key map putting
185
+ // a non-Ed25519 key in it. `crypto.verify(null, …)` would throw for most
186
+ // other key types and be caught below, but an explicit refusal is
187
+ // clearer than relying on which algorithms happen to reject a null
188
+ // digest — and it is the check that keeps "the algorithm is Ed25519
189
+ // because this file says so" literally true.
190
+ if (publicKey.asymmetricKeyType !== "ed25519")
191
+ return { ok: false, reason: "unknown_key" };
192
+ signatureOk = cryptoVerify(null, Buffer.from(`${SUPPORTER_TOKEN_PREFIX}.${payloadSeg}`, "ascii"), publicKey, sigBytes);
193
+ }
194
+ catch {
195
+ signatureOk = false;
196
+ }
197
+ if (!signatureOk)
198
+ return { ok: false, reason: "bad_signature" };
199
+ // Tier and lifetime are checked only AFTER the signature: a rejection
200
+ // before that would be a statement about an unauthenticated payload.
201
+ if (tier !== SUPPORTER_TIER)
202
+ return { ok: false, reason: "wrong_tier" };
203
+ if ((expN - iatN) * 1000 > SUPPORTER_MAX_LIFETIME_MS)
204
+ return { ok: false, reason: "implausible_lifetime" };
205
+ return { ok: true, claims: { v: 1, kid, sub, tier, iat: iatN, exp: expN } };
206
+ }
207
+ /** Full verification: shape, signature, tier, then expiry against `now`.
208
+ * Pure and synchronous — no filesystem, no clock injection beyond `now`, and
209
+ * nothing that can touch a socket. */
210
+ export function verifySupporterToken(token, now = Date.now()) {
211
+ const base = verifyShapeAndSignature(token);
212
+ if (!base.ok)
213
+ return { state: "invalid", reason: base.reason };
214
+ return classifyClaims(base.claims, now);
215
+ }
216
+ /** The time-dependent half, over claims whose signature is already trusted. */
217
+ export function classifyClaims(claims, now = Date.now()) {
218
+ if (claims.iat * 1000 > now + SUPPORTER_MAX_CLOCK_SKEW_MS) {
219
+ return { state: "invalid", reason: "not_yet_valid" };
220
+ }
221
+ const expMs = claims.exp * 1000;
222
+ if (now <= expMs)
223
+ return { state: "valid", claims };
224
+ if (now <= expMs + SUPPORTER_GRACE_MS)
225
+ return { state: "grace", claims };
226
+ return { state: "expired", claims };
227
+ }
228
+ /** Verifies shape + signature only, deferring every time-dependent decision
229
+ * to `classifyClaims`. `isSupporter()`'s cache is built on this split. */
230
+ export function verifySupporterTokenClaims(token) {
231
+ return verifyShapeAndSignature(token);
232
+ }
233
+ //# sourceMappingURL=supporterToken.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supporterToken.js","sourceRoot":"","sources":["../../src/ads/supporterToken.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ /** The ONLY shape sponsored content is ever allowed to take inside this
2
+ * process (see .reposkein/decisions/2026-08-21-sponsorship-placement-*.json:
3
+ * "Sponsor payloads are fixed-schema and length-capped, not freeform text or
4
+ * markup"). Anything a network returns is projected onto this and rejected if
5
+ * it doesn't fit — never merged, never spread, never passed through.
6
+ *
7
+ * `label` is a literal, constructed locally by `sanitizeSponsored` and never
8
+ * copied from the payload: the disclosure is a property of OUR envelope, not
9
+ * sponsor-supplied content, which is the same immutability rule the ADR fixes
10
+ * for the viewer chip. */
11
+ export interface SponsoredSlot {
12
+ readonly label: typeof SPONSORED_LABEL;
13
+ /** Headline, <= TITLE_MAX chars, plain text. */
14
+ readonly title?: string;
15
+ /** Body copy, <= BODY_MAX chars, plain text. */
16
+ readonly body?: string;
17
+ /** https, host in the ad network's click-domain allowlist. */
18
+ readonly url: string;
19
+ }
20
+ /** Immutable. Renaming this constant renames a disclosure, which is exactly
21
+ * what the governing ADR forbids — the "label is immutable" block in
22
+ * `mcp/test/adsSanitize.test.ts` asserts the literal survives every payload
23
+ * that tries to rename, blank, or drop it. */
24
+ export declare const SPONSORED_LABEL = "sponsored";
25
+ /** Response-envelope key the slot is attached under. `_meta` (not `content`,
26
+ * not a new `structuredContent`) so the slot can never displace, reorder, or
27
+ * concatenate itself into the tool's own answer. */
28
+ export declare const SPONSORED_META_KEY = "reposkein/sponsored";
29
+ export declare const TITLE_MAX = 80;
30
+ export declare const BODY_MAX = 200;
31
+ export declare const URL_MAX = 512;
32
+ /** Anything larger than this isn't a slot payload; reject before parsing fields. */
33
+ export declare const PAYLOAD_MAX_BYTES = 4096;
34
+ /** Everything that leaves this machine for a slot request.
35
+ *
36
+ * Deliberately not extensible: `tool` is a tool NAME from this server's own
37
+ * registry (a fixed, public string — `get_context_profile`), and `keywords`
38
+ * is coarse free text the caller opted to share. No file paths, no node ids,
39
+ * no code, no summaries, no repo id, no identity. See docs/SPONSORSHIP.md. */
40
+ export interface SlotContext {
41
+ readonly tool: string;
42
+ readonly keywords?: string;
43
+ }
44
+ /** A source of raw (untrusted) slot payloads. One implementation talks to the
45
+ * ad network (`luluAdsSource`); tests inject fakes. The return value is
46
+ * `unknown` on purpose — a source is a transport, never a validator; the
47
+ * ONLY validator is `sanitizeSponsored`. */
48
+ export interface SponsoredSource {
49
+ requestSlot(ctx: SlotContext, opts: {
50
+ timeoutMs: number;
51
+ signal: AbortSignal;
52
+ }): Promise<unknown>;
53
+ }
@@ -0,0 +1,15 @@
1
+ /** Immutable. Renaming this constant renames a disclosure, which is exactly
2
+ * what the governing ADR forbids — the "label is immutable" block in
3
+ * `mcp/test/adsSanitize.test.ts` asserts the literal survives every payload
4
+ * that tries to rename, blank, or drop it. */
5
+ export const SPONSORED_LABEL = "sponsored";
6
+ /** Response-envelope key the slot is attached under. `_meta` (not `content`,
7
+ * not a new `structuredContent`) so the slot can never displace, reorder, or
8
+ * concatenate itself into the tool's own answer. */
9
+ export const SPONSORED_META_KEY = "reposkein/sponsored";
10
+ export const TITLE_MAX = 80;
11
+ export const BODY_MAX = 200;
12
+ export const URL_MAX = 512;
13
+ /** Anything larger than this isn't a slot payload; reject before parsing fields. */
14
+ export const PAYLOAD_MAX_BYTES = 4096;
15
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/ads/types.ts"],"names":[],"mappings":"AAoBA;;;+CAG+C;AAC/C,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAC;AAE3C;;qDAEqD;AACrD,MAAM,CAAC,MAAM,kBAAkB,GAAG,qBAAqB,CAAC;AAExD,MAAM,CAAC,MAAM,SAAS,GAAG,EAAE,CAAC;AAC5B,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAG,CAAC;AAC5B,MAAM,CAAC,MAAM,OAAO,GAAG,GAAG,CAAC;AAC3B,oFAAoF;AACpF,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC"}
@@ -0,0 +1,46 @@
1
+ /** `reposkein-mcp support` — install, inspect, and remove a supporter
2
+ * entitlement token.
3
+ *
4
+ * Four shapes, no subcommands:
5
+ *
6
+ * reposkein-mcp support <token> verify and store it
7
+ * reposkein-mcp support - read the token from stdin instead
8
+ * reposkein-mcp support --status what is installed and until when
9
+ * reposkein-mcp support --remove delete it
10
+ *
11
+ * `-` exists because a token passed as an argument is not private: it lands
12
+ * in `~/.bash_history`, and while the process runs it is visible in `ps` and
13
+ * `/proc/<pid>/cmdline` to every other user on the machine. Piping it in
14
+ * keeps it off both. It is not a secret worth much — the worst outcome of a
15
+ * leak is somebody else getting no ads — but "it barely matters" is a poor
16
+ * reason to make the private option unavailable.
17
+ *
18
+ * The token is verified BEFORE it is written: an install that stores
19
+ * unverified bytes and only complains later leaves the user with a file that
20
+ * looks installed and does nothing. Nothing here contacts the network — see
21
+ * `ads/supporter.ts` — so `support` works offline, which matters because the
22
+ * token typically arrives by copy-paste into a terminal that may not be the
23
+ * one that fetched it. */
24
+ import { type SupporterStatus } from "../ads/supporter.js";
25
+ import { type SupporterRejection } from "../ads/supporterToken.js";
26
+ export interface SupportArgs {
27
+ mode: "install" | "status" | "remove";
28
+ token?: string;
29
+ /** `-` was given: the token comes from stdin, not from argv. */
30
+ stdin?: boolean;
31
+ json: boolean;
32
+ error?: string;
33
+ }
34
+ export declare function parseSupportArgs(argv: string[]): SupportArgs;
35
+ /** Human-readable explanation of a rejection. Deliberately specific: "invalid
36
+ * token" tells a paying supporter nothing about whether to re-copy it or ask
37
+ * for a new one. */
38
+ export declare function explainRejection(reason: SupporterRejection): string;
39
+ export declare function renderStatus(status: SupporterStatus, now: number, color: boolean): string;
40
+ export declare function renderStatusJson(status: SupporterStatus, now: number): string;
41
+ /** Entry point. Returns the process exit code: 0 when the machine ends up
42
+ * entitled (or a removal succeeded), 1 otherwise — so a script can branch on
43
+ * `reposkein-mcp support --status` without parsing text.
44
+ *
45
+ * `readStdin` is a test seam; production always reads fd 0. */
46
+ export declare function runSupport(argv: string[], env?: NodeJS.ProcessEnv, now?: number, readStdin?: () => string): number;