@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.
- package/README.md +6 -1
- package/binary-digests.json +4 -4
- package/dist/ads/auditLog.d.ts +23 -0
- package/dist/ads/auditLog.js +33 -0
- package/dist/ads/auditLog.js.map +1 -0
- package/dist/ads/config.d.ts +83 -0
- package/dist/ads/config.js +107 -0
- package/dist/ads/config.js.map +1 -0
- package/dist/ads/luluSource.d.ts +32 -0
- package/dist/ads/luluSource.js +60 -0
- package/dist/ads/luluSource.js.map +1 -0
- package/dist/ads/sanitize.d.ts +16 -0
- package/dist/ads/sanitize.js +187 -0
- package/dist/ads/sanitize.js.map +1 -0
- package/dist/ads/slot.d.ts +63 -0
- package/dist/ads/slot.js +159 -0
- package/dist/ads/slot.js.map +1 -0
- package/dist/ads/supporter.d.ts +73 -0
- package/dist/ads/supporter.js +119 -0
- package/dist/ads/supporter.js.map +1 -0
- package/dist/ads/supporterKey.d.ts +45 -0
- package/dist/ads/supporterKey.js +51 -0
- package/dist/ads/supporterKey.js.map +1 -0
- package/dist/ads/supporterStore.d.ts +61 -0
- package/dist/ads/supporterStore.js +113 -0
- package/dist/ads/supporterStore.js.map +1 -0
- package/dist/ads/supporterToken.d.ts +133 -0
- package/dist/ads/supporterToken.js +233 -0
- package/dist/ads/supporterToken.js.map +1 -0
- package/dist/ads/types.d.ts +53 -0
- package/dist/ads/types.js +15 -0
- package/dist/ads/types.js.map +1 -0
- package/dist/cli/support.d.ts +46 -0
- package/dist/cli/support.js +276 -0
- package/dist/cli/support.js.map +1 -0
- package/dist/cli/view.js +27 -6
- package/dist/cli/view.js.map +1 -1
- package/dist/index.d.ts +8 -56
- package/dist/index.js +45 -402
- package/dist/index.js.map +1 -1
- package/dist/indexer/indexLock.d.ts +13 -0
- package/dist/indexer/indexLock.js +66 -0
- package/dist/indexer/indexLock.js.map +1 -0
- package/dist/indexer/runIndexer.d.ts +8 -0
- package/dist/indexer/runIndexer.js +12 -0
- package/dist/indexer/runIndexer.js.map +1 -1
- package/dist/serve/serve.d.ts +95 -0
- package/dist/serve/serve.js +581 -0
- package/dist/serve/serve.js.map +1 -0
- package/dist/serve/tokens.d.ts +49 -0
- package/dist/serve/tokens.js +108 -0
- package/dist/serve/tokens.js.map +1 -0
- package/dist/serve/watch.d.ts +73 -0
- package/dist/serve/watch.js +135 -0
- package/dist/serve/watch.js.map +1 -0
- package/dist/server/createMcpServer.d.ts +134 -0
- package/dist/server/createMcpServer.js +473 -0
- package/dist/server/createMcpServer.js.map +1 -0
- package/dist/store/JsonlGraphStore.d.ts +5 -1
- package/dist/store/JsonlGraphStore.js +6 -2
- package/dist/store/JsonlGraphStore.js.map +1 -1
- package/dist/store/teamConfig.d.ts +20 -6
- package/dist/store/teamConfig.js +70 -17
- package/dist/store/teamConfig.js.map +1 -1
- package/dist/tools/recordDecision.d.ts +3 -0
- package/dist/tools/recordDecision.js +1 -1
- package/dist/tools/recordDecision.js.map +1 -1
- package/dist/tools/writeSemanticSummary.d.ts +4 -1
- package/dist/tools/writeSemanticSummary.js +5 -2
- package/dist/tools/writeSemanticSummary.js.map +1 -1
- package/dist/viz/assets/index-BnqIVYFE.css +1 -0
- package/dist/viz/assets/index-IhtszTp0.js +5 -0
- package/dist/viz/assets/{r3f-BZY3QR6_.js → r3f-Bn9htB92.js} +1 -1
- package/dist/viz/assets/{tanstack-CjXr-L-s.js → tanstack-CSzx6NDA.js} +7 -7
- package/dist/viz/index.html +4 -4
- package/package.json +3 -2
- package/dist/viz/assets/index-CxqeGuRS.css +0 -1
- package/dist/viz/assets/index-DiNZyrXx.js +0 -9
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type SponsoredSlot, type SponsoredSource } from "./types.js";
|
|
2
|
+
/** Minimal shape the hook needs from a tool result — structurally compatible
|
|
3
|
+
* with `tools/readCypher.ts`'s `ToolResult` without importing it (the hook
|
|
4
|
+
* must stay usable around any handler). */
|
|
5
|
+
export interface AdResultLike {
|
|
6
|
+
isError?: boolean;
|
|
7
|
+
_meta?: Record<string, unknown>;
|
|
8
|
+
structuredContent?: unknown;
|
|
9
|
+
}
|
|
10
|
+
export interface AdsHookOptions {
|
|
11
|
+
/** The active repo root for `[ads] enabled`, resolved lazily so a disabled
|
|
12
|
+
* integration costs nothing. */
|
|
13
|
+
resolveRepoPath: () => string | undefined;
|
|
14
|
+
env?: NodeJS.ProcessEnv;
|
|
15
|
+
/** Injected in tests. Omitted in production: the real source is built (and
|
|
16
|
+
* the `lulu-ads` package first imported) only after the gating chain
|
|
17
|
+
* passes. Pass `null` to guarantee no source can ever be built. */
|
|
18
|
+
source?: SponsoredSource | null;
|
|
19
|
+
isSupporter?: () => boolean;
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
/** Test seam: `[ads] enabled` lookup. */
|
|
22
|
+
readOptIn?: (repoPath: string) => boolean | null;
|
|
23
|
+
/** Test seams for the local request audit (`.reposkein/local/ads-requests.jsonl`).
|
|
24
|
+
* Defaults: the real appender, deferred with `setImmediate`. */
|
|
25
|
+
audit?: (repoPath: string, record: {
|
|
26
|
+
ts: string;
|
|
27
|
+
tool: string;
|
|
28
|
+
}) => void;
|
|
29
|
+
schedule?: (fn: () => void) => void;
|
|
30
|
+
}
|
|
31
|
+
export interface AdsHook {
|
|
32
|
+
/** Wraps a tool handler so its RESPONSE ENVELOPE may carry a sponsored slot.
|
|
33
|
+
* Wrap OUTSIDE `withLog` (`withAds(name, withLog(name, handler))`): the
|
|
34
|
+
* slot must not exist yet when the session logger captures the result, or
|
|
35
|
+
* sponsored bytes would show up in `.reposkein/local/sessions` byte counts.
|
|
36
|
+
* Attachment is copy-on-write for the same reason — the object the logger
|
|
37
|
+
* holds by reference is never mutated. */
|
|
38
|
+
withAds<Args, R extends AdResultLike>(tool: string, cb: (args: Args) => Promise<R>): (args: Args) => Promise<R>;
|
|
39
|
+
}
|
|
40
|
+
/** Builds the sponsorship hook for ONE server connection (mirrors
|
|
41
|
+
* `createToolLogger`: per-connection state, no module globals).
|
|
42
|
+
*
|
|
43
|
+
* Everything about this is fail-open. A disabled integration, absent
|
|
44
|
+
* credentials, a rejecting source, a hanging source, a hostile payload and an
|
|
45
|
+
* outright bug all land in the same place: the tool's own result, returned
|
|
46
|
+
* unchanged. There is no code path here that can make a tool call fail, and
|
|
47
|
+
* none that can slow it by more than `timeoutMs`. */
|
|
48
|
+
export declare function createAdsHook(opts: AdsHookOptions): AdsHook;
|
|
49
|
+
/** Attaches the slot to the response ENVELOPE, copy-on-write.
|
|
50
|
+
*
|
|
51
|
+
* `_meta` under a namespaced key — not `content`, and never a
|
|
52
|
+
* `structuredContent` object invented for the purpose. Rationale: `content`
|
|
53
|
+
* is the tool's answer and the one surface an agent reads as prose, so
|
|
54
|
+
* sponsored data must never appear there in any form (concatenated or
|
|
55
|
+
* appended); and materializing a `structuredContent` that only holds an ad
|
|
56
|
+
* would let a host that prefers structured output over text render the ad
|
|
57
|
+
* INSTEAD of the answer. When a tool already returns structuredContent, the
|
|
58
|
+
* slot is mirrored there as a clearly separate `sponsored` key so hosts that
|
|
59
|
+
* render structured fields can disclose it.
|
|
60
|
+
*
|
|
61
|
+
* The `sponsored` label travels inside the slot object and the envelope key
|
|
62
|
+
* itself is fixed: nothing here can rename, hide, or restyle the disclosure. */
|
|
63
|
+
export declare function attachSponsored<R extends AdResultLike>(result: R, slot: SponsoredSlot): R;
|
package/dist/ads/slot.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { AD_ELIGIBLE_TOOLS, SLOT_TIMEOUT_MS, resolveAdsVerdict, } from "./config.js";
|
|
2
|
+
import { luluAdsSource } from "./luluSource.js";
|
|
3
|
+
import { sanitizeSponsored } from "./sanitize.js";
|
|
4
|
+
import { isSupporter as defaultIsSupporter } from "./supporter.js";
|
|
5
|
+
import { readConfigBool } from "../store/teamConfig.js";
|
|
6
|
+
import { appendAdsRequest } from "./auditLog.js";
|
|
7
|
+
import { SPONSORED_META_KEY } from "./types.js";
|
|
8
|
+
/** Builds the sponsorship hook for ONE server connection (mirrors
|
|
9
|
+
* `createToolLogger`: per-connection state, no module globals).
|
|
10
|
+
*
|
|
11
|
+
* Everything about this is fail-open. A disabled integration, absent
|
|
12
|
+
* credentials, a rejecting source, a hanging source, a hostile payload and an
|
|
13
|
+
* outright bug all land in the same place: the tool's own result, returned
|
|
14
|
+
* unchanged. There is no code path here that can make a tool call fail, and
|
|
15
|
+
* none that can slow it by more than `timeoutMs`. */
|
|
16
|
+
export function createAdsHook(opts) {
|
|
17
|
+
const env = opts.env ?? process.env;
|
|
18
|
+
const timeoutMs = opts.timeoutMs ?? SLOT_TIMEOUT_MS;
|
|
19
|
+
// Bound to THIS hook's env so an injected environment reaches the
|
|
20
|
+
// entitlement-file lookup too (`REPOSKEIN_SUPPORTER_FILE`), rather than the
|
|
21
|
+
// supporter check silently reading the real `process.env` while everything
|
|
22
|
+
// else in the chain reads the injected one.
|
|
23
|
+
const isSupporter = opts.isSupporter ?? (() => defaultIsSupporter(env));
|
|
24
|
+
const audit = opts.audit ?? appendAdsRequest;
|
|
25
|
+
const schedule = opts.schedule ?? setImmediate;
|
|
26
|
+
// The config.toml opt-in is memoized for the connection's lifetime: one
|
|
27
|
+
// filesystem read per repo, never one per tool call. The env kill switch and
|
|
28
|
+
// the credential/supporter checks are re-evaluated on every call (they are
|
|
29
|
+
// pure reads of already-loaded state), so `REPOSKEIN_ADS=off` takes effect
|
|
30
|
+
// immediately while a config.toml edit needs a reconnect.
|
|
31
|
+
const optInMemo = new Map();
|
|
32
|
+
const readOptIn = (repoPath) => {
|
|
33
|
+
if (!optInMemo.has(repoPath)) {
|
|
34
|
+
const read = opts.readOptIn;
|
|
35
|
+
try {
|
|
36
|
+
optInMemo.set(repoPath, read ? read(repoPath) : resolveOptInFromDisk(repoPath));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
optInMemo.set(repoPath, null);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return optInMemo.get(repoPath) ?? null;
|
|
43
|
+
};
|
|
44
|
+
let source = opts.source;
|
|
45
|
+
function sourceFor(verdict) {
|
|
46
|
+
if (source !== undefined)
|
|
47
|
+
return source;
|
|
48
|
+
source = luluAdsSource({
|
|
49
|
+
publisherId: verdict.publisherId,
|
|
50
|
+
apiKey: verdict.apiKey,
|
|
51
|
+
baseUrl: verdict.baseUrl,
|
|
52
|
+
});
|
|
53
|
+
return source;
|
|
54
|
+
}
|
|
55
|
+
/** One bounded slot request, or null. Never throws, never exceeds the
|
|
56
|
+
* budget: an `AbortController` is signalled at the deadline and the race
|
|
57
|
+
* resolves null regardless of what the source does afterwards, so even a
|
|
58
|
+
* source that ignores the signal entirely cannot delay a tool result. */
|
|
59
|
+
async function requestSlot(src, ctx, clickHosts) {
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
let timer;
|
|
62
|
+
const deadline = new Promise((resolve) => {
|
|
63
|
+
timer = setTimeout(() => {
|
|
64
|
+
controller.abort();
|
|
65
|
+
resolve(null);
|
|
66
|
+
}, timeoutMs);
|
|
67
|
+
timer.unref?.();
|
|
68
|
+
});
|
|
69
|
+
try {
|
|
70
|
+
const raw = await Promise.race([
|
|
71
|
+
src.requestSlot(ctx, { timeoutMs, signal: controller.signal }).catch(() => null),
|
|
72
|
+
deadline,
|
|
73
|
+
]);
|
|
74
|
+
if (raw === null || raw === undefined)
|
|
75
|
+
return null;
|
|
76
|
+
return sanitizeSponsored(raw, { clickHosts });
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
if (timer)
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function maybeSlot(tool) {
|
|
87
|
+
if (!AD_ELIGIBLE_TOOLS.includes(tool))
|
|
88
|
+
return null;
|
|
89
|
+
const repoPath = opts.resolveRepoPath();
|
|
90
|
+
const verdict = resolveAdsVerdict({
|
|
91
|
+
repoPath,
|
|
92
|
+
env,
|
|
93
|
+
isSupporter,
|
|
94
|
+
readOptIn,
|
|
95
|
+
});
|
|
96
|
+
if (!verdict.enabled)
|
|
97
|
+
return null;
|
|
98
|
+
const src = sourceFor(verdict);
|
|
99
|
+
if (!src)
|
|
100
|
+
return null;
|
|
101
|
+
// Audit BEFORE the call, deferred off the hot path (the instrumentTool
|
|
102
|
+
// pattern): the line records that a request left this machine, so it must
|
|
103
|
+
// be written even if the request then hangs, fails, or is aborted at the
|
|
104
|
+
// deadline. Never blocks and never throws.
|
|
105
|
+
if (repoPath) {
|
|
106
|
+
const ts = new Date().toISOString();
|
|
107
|
+
schedule(() => audit(repoPath, { ts, tool }));
|
|
108
|
+
}
|
|
109
|
+
return requestSlot(src, { tool }, verdict.clickHosts);
|
|
110
|
+
}
|
|
111
|
+
function withAds(tool, cb) {
|
|
112
|
+
return async (args) => {
|
|
113
|
+
const result = await cb(args);
|
|
114
|
+
try {
|
|
115
|
+
// Error results are off-limits: a failure is never a sales surface,
|
|
116
|
+
// and a caller debugging one should see nothing but the failure.
|
|
117
|
+
if (!result || typeof result !== "object" || result.isError === true)
|
|
118
|
+
return result;
|
|
119
|
+
const slot = await maybeSlot(tool);
|
|
120
|
+
if (!slot)
|
|
121
|
+
return result;
|
|
122
|
+
return attachSponsored(result, slot);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// A sponsorship bug must be invisible to the caller.
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return { withAds };
|
|
131
|
+
}
|
|
132
|
+
/** `[ads] enabled` from `.reposkein/config.toml`. */
|
|
133
|
+
function resolveOptInFromDisk(repoPath) {
|
|
134
|
+
return readConfigBool(repoPath, "ads", "enabled");
|
|
135
|
+
}
|
|
136
|
+
/** Attaches the slot to the response ENVELOPE, copy-on-write.
|
|
137
|
+
*
|
|
138
|
+
* `_meta` under a namespaced key — not `content`, and never a
|
|
139
|
+
* `structuredContent` object invented for the purpose. Rationale: `content`
|
|
140
|
+
* is the tool's answer and the one surface an agent reads as prose, so
|
|
141
|
+
* sponsored data must never appear there in any form (concatenated or
|
|
142
|
+
* appended); and materializing a `structuredContent` that only holds an ad
|
|
143
|
+
* would let a host that prefers structured output over text render the ad
|
|
144
|
+
* INSTEAD of the answer. When a tool already returns structuredContent, the
|
|
145
|
+
* slot is mirrored there as a clearly separate `sponsored` key so hosts that
|
|
146
|
+
* render structured fields can disclose it.
|
|
147
|
+
*
|
|
148
|
+
* The `sponsored` label travels inside the slot object and the envelope key
|
|
149
|
+
* itself is fixed: nothing here can rename, hide, or restyle the disclosure. */
|
|
150
|
+
export function attachSponsored(result, slot) {
|
|
151
|
+
const meta = { ...(result._meta ?? {}), [SPONSORED_META_KEY]: slot };
|
|
152
|
+
const out = { ...result, _meta: meta };
|
|
153
|
+
const structured = result.structuredContent;
|
|
154
|
+
if (structured && typeof structured === "object" && !Array.isArray(structured)) {
|
|
155
|
+
out.structuredContent = { ...structured, sponsored: slot };
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=slot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slot.js","sourceRoot":"","sources":["../../src/ads/slot.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Supporter check — the "you already paid, you see no ads" gate.
|
|
2
|
+
*
|
|
3
|
+
* REP-28 wired a stub of this into `resolveAdsVerdict` so the sponsorship
|
|
4
|
+
* path could never ship without it. REP-29 filled it in. The signature is
|
|
5
|
+
* unchanged: still synchronous, still consulted before any slot is
|
|
6
|
+
* requested, still returning a plain boolean.
|
|
7
|
+
*
|
|
8
|
+
* ## Entirely local
|
|
9
|
+
*
|
|
10
|
+
* Verification is a signature check against a public key compiled into this
|
|
11
|
+
* package (`supporterKey.ts`) over a file the user placed at
|
|
12
|
+
* `~/.config/reposkein/supporter.jwt`. There is no licence server, no
|
|
13
|
+
* activation, no revocation fetch, no telemetry — and the import graph
|
|
14
|
+
* reachable from this module contains nothing that can open a socket, which
|
|
15
|
+
* a test asserts statically rather than trusting this comment. Being a
|
|
16
|
+
* supporter is therefore something you can be on a plane, behind a
|
|
17
|
+
* corporate proxy, or on a machine that has never resolved DNS.
|
|
18
|
+
*
|
|
19
|
+
* ## Staying off the hot path
|
|
20
|
+
*
|
|
21
|
+
* This runs inside a tool call's gating chain, so the expensive parts are
|
|
22
|
+
* cached for the process:
|
|
23
|
+
*
|
|
24
|
+
* - The signature verification result is cached against the entitlement
|
|
25
|
+
* file's (mtime, size). Editing or replacing the file invalidates it.
|
|
26
|
+
* - The `statSync` that detects such an edit is THROTTLED: at most one every
|
|
27
|
+
* `RECHECK_INTERVAL_MS`. Between probes the cached claims are reused, so a
|
|
28
|
+
* steady stream of tool calls costs one integer comparison each, not one
|
|
29
|
+
* syscall each.
|
|
30
|
+
* - Expiry is re-evaluated on every call regardless, since it depends on the
|
|
31
|
+
* clock rather than on the file. A token that lapses mid-session stops
|
|
32
|
+
* entitling immediately, without waiting for a stat.
|
|
33
|
+
*
|
|
34
|
+
* The bound this buys: a token added, renewed, or deleted mid-session takes
|
|
35
|
+
* effect within `RECHECK_INTERVAL_MS` (five seconds) rather than instantly.
|
|
36
|
+
* For a gate whose only job is to suppress an ad, that is the right trade. */
|
|
37
|
+
import { type SupporterClaims, type SupporterRejection, type SupporterVerdict } from "./supporterToken.js";
|
|
38
|
+
/** How often the entitlement file may be re-probed on the hot path. */
|
|
39
|
+
export declare const RECHECK_INTERVAL_MS = 5000;
|
|
40
|
+
/** What `support --status` (and `doctor`, later) needs to say something
|
|
41
|
+
* useful. `state: "none"` means no entitlement file at all — the state of
|
|
42
|
+
* every install that has not run `reposkein-mcp support`. */
|
|
43
|
+
export type SupporterStatus = {
|
|
44
|
+
state: "none";
|
|
45
|
+
path: string;
|
|
46
|
+
} | {
|
|
47
|
+
state: "valid" | "grace" | "expired";
|
|
48
|
+
path: string;
|
|
49
|
+
claims: SupporterClaims;
|
|
50
|
+
mode: number | null;
|
|
51
|
+
} | {
|
|
52
|
+
state: "invalid";
|
|
53
|
+
path: string;
|
|
54
|
+
reason: SupporterRejection;
|
|
55
|
+
mode: number | null;
|
|
56
|
+
};
|
|
57
|
+
/** Drops the memoized verification. Tests call it; nothing in production
|
|
58
|
+
* does — the mtime/size probe is what keeps the cache honest at runtime. */
|
|
59
|
+
export declare function resetSupporterCache(): void;
|
|
60
|
+
/** Reads + verifies with no caching and no clock-independent shortcuts. The
|
|
61
|
+
* CLI path: correctness and detail matter, a syscall does not. */
|
|
62
|
+
export declare function readSupporterStatus(env?: NodeJS.ProcessEnv, now?: number): SupporterStatus;
|
|
63
|
+
/** The cached verdict used by the gating chain. Exported for tests and for
|
|
64
|
+
* anything that wants the reason as well as the boolean. */
|
|
65
|
+
export declare function supporterVerdict(env?: NodeJS.ProcessEnv, now?: number): SupporterVerdict;
|
|
66
|
+
/** The gate itself. True only for a signed, untampered, correctly-tiered
|
|
67
|
+
* token that is inside its validity window or its grace period.
|
|
68
|
+
*
|
|
69
|
+
* Fail-CLOSED with respect to entitlement (any doubt → "not a supporter",
|
|
70
|
+
* which merely means the normal, already-opt-in-gated ad path applies) and
|
|
71
|
+
* fail-OPEN with respect to errors: nothing here can throw into a tool
|
|
72
|
+
* call. */
|
|
73
|
+
export declare function isSupporter(env?: NodeJS.ProcessEnv, now?: number): boolean;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** Supporter check — the "you already paid, you see no ads" gate.
|
|
2
|
+
*
|
|
3
|
+
* REP-28 wired a stub of this into `resolveAdsVerdict` so the sponsorship
|
|
4
|
+
* path could never ship without it. REP-29 filled it in. The signature is
|
|
5
|
+
* unchanged: still synchronous, still consulted before any slot is
|
|
6
|
+
* requested, still returning a plain boolean.
|
|
7
|
+
*
|
|
8
|
+
* ## Entirely local
|
|
9
|
+
*
|
|
10
|
+
* Verification is a signature check against a public key compiled into this
|
|
11
|
+
* package (`supporterKey.ts`) over a file the user placed at
|
|
12
|
+
* `~/.config/reposkein/supporter.jwt`. There is no licence server, no
|
|
13
|
+
* activation, no revocation fetch, no telemetry — and the import graph
|
|
14
|
+
* reachable from this module contains nothing that can open a socket, which
|
|
15
|
+
* a test asserts statically rather than trusting this comment. Being a
|
|
16
|
+
* supporter is therefore something you can be on a plane, behind a
|
|
17
|
+
* corporate proxy, or on a machine that has never resolved DNS.
|
|
18
|
+
*
|
|
19
|
+
* ## Staying off the hot path
|
|
20
|
+
*
|
|
21
|
+
* This runs inside a tool call's gating chain, so the expensive parts are
|
|
22
|
+
* cached for the process:
|
|
23
|
+
*
|
|
24
|
+
* - The signature verification result is cached against the entitlement
|
|
25
|
+
* file's (mtime, size). Editing or replacing the file invalidates it.
|
|
26
|
+
* - The `statSync` that detects such an edit is THROTTLED: at most one every
|
|
27
|
+
* `RECHECK_INTERVAL_MS`. Between probes the cached claims are reused, so a
|
|
28
|
+
* steady stream of tool calls costs one integer comparison each, not one
|
|
29
|
+
* syscall each.
|
|
30
|
+
* - Expiry is re-evaluated on every call regardless, since it depends on the
|
|
31
|
+
* clock rather than on the file. A token that lapses mid-session stops
|
|
32
|
+
* entitling immediately, without waiting for a stat.
|
|
33
|
+
*
|
|
34
|
+
* The bound this buys: a token added, renewed, or deleted mid-session takes
|
|
35
|
+
* effect within `RECHECK_INTERVAL_MS` (five seconds) rather than instantly.
|
|
36
|
+
* For a gate whose only job is to suppress an ad, that is the right trade. */
|
|
37
|
+
import { classifyClaims, isEntitled, verifySupporterTokenClaims, } from "./supporterToken.js";
|
|
38
|
+
import { readSupporterTokenFile, statSupporterTokenFile, supporterTokenPath } from "./supporterStore.js";
|
|
39
|
+
/** How often the entitlement file may be re-probed on the hot path. */
|
|
40
|
+
export const RECHECK_INTERVAL_MS = 5_000;
|
|
41
|
+
/** Module-level and deliberately so: the entitlement is a property of the
|
|
42
|
+
* machine, not of a server connection, and re-verifying an Ed25519 signature
|
|
43
|
+
* per connection would be pure waste. Reset in tests via
|
|
44
|
+
* `resetSupporterCache()`. */
|
|
45
|
+
let cache = null;
|
|
46
|
+
/** Drops the memoized verification. Tests call it; nothing in production
|
|
47
|
+
* does — the mtime/size probe is what keeps the cache honest at runtime. */
|
|
48
|
+
export function resetSupporterCache() {
|
|
49
|
+
cache = null;
|
|
50
|
+
}
|
|
51
|
+
/** Reads + verifies with no caching and no clock-independent shortcuts. The
|
|
52
|
+
* CLI path: correctness and detail matter, a syscall does not. */
|
|
53
|
+
export function readSupporterStatus(env = process.env, now = Date.now()) {
|
|
54
|
+
const path = supporterTokenPath(env);
|
|
55
|
+
const st = statSupporterTokenFile(env);
|
|
56
|
+
if (!st)
|
|
57
|
+
return { state: "none", path };
|
|
58
|
+
const text = readSupporterTokenFile(env);
|
|
59
|
+
if (text === null)
|
|
60
|
+
return { state: "invalid", path, reason: "empty", mode: st.mode };
|
|
61
|
+
const parsed = verifySupporterTokenClaims(text);
|
|
62
|
+
if (!parsed.ok)
|
|
63
|
+
return { state: "invalid", path, reason: parsed.reason, mode: st.mode };
|
|
64
|
+
const verdict = classifyClaims(parsed.claims, now);
|
|
65
|
+
if (verdict.state === "invalid")
|
|
66
|
+
return { state: "invalid", path, reason: verdict.reason, mode: st.mode };
|
|
67
|
+
return { state: verdict.state, path, claims: verdict.claims, mode: st.mode };
|
|
68
|
+
}
|
|
69
|
+
/** The cached verdict used by the gating chain. Exported for tests and for
|
|
70
|
+
* anything that wants the reason as well as the boolean. */
|
|
71
|
+
export function supporterVerdict(env = process.env, now = Date.now()) {
|
|
72
|
+
const path = supporterTokenPath(env);
|
|
73
|
+
// Reuse the cached claims without touching the filesystem while the
|
|
74
|
+
// throttle window is open and the path has not changed under us.
|
|
75
|
+
if (cache && cache.path === path && now - cache.probedAt < RECHECK_INTERVAL_MS) {
|
|
76
|
+
return fromCache(cache, now);
|
|
77
|
+
}
|
|
78
|
+
const st = statSupporterTokenFile(env);
|
|
79
|
+
if (!st) {
|
|
80
|
+
cache = { mtimeMs: -1, size: -1, path, claims: null, reason: "empty", probedAt: now };
|
|
81
|
+
return { state: "invalid", reason: "empty" };
|
|
82
|
+
}
|
|
83
|
+
if (cache && cache.path === path && cache.mtimeMs === st.mtimeMs && cache.size === st.size) {
|
|
84
|
+
cache.probedAt = now;
|
|
85
|
+
return fromCache(cache, now);
|
|
86
|
+
}
|
|
87
|
+
const text = readSupporterTokenFile(env);
|
|
88
|
+
const parsed = text === null ? { ok: false, reason: "empty" } : verifySupporterTokenClaims(text);
|
|
89
|
+
cache = {
|
|
90
|
+
mtimeMs: st.mtimeMs,
|
|
91
|
+
size: st.size,
|
|
92
|
+
path,
|
|
93
|
+
claims: parsed.ok ? parsed.claims : null,
|
|
94
|
+
reason: parsed.ok ? null : parsed.reason,
|
|
95
|
+
probedAt: now,
|
|
96
|
+
};
|
|
97
|
+
return fromCache(cache, now);
|
|
98
|
+
}
|
|
99
|
+
function fromCache(entry, now) {
|
|
100
|
+
if (!entry.claims)
|
|
101
|
+
return { state: "invalid", reason: entry.reason ?? "empty" };
|
|
102
|
+
return classifyClaims(entry.claims, now);
|
|
103
|
+
}
|
|
104
|
+
/** The gate itself. True only for a signed, untampered, correctly-tiered
|
|
105
|
+
* token that is inside its validity window or its grace period.
|
|
106
|
+
*
|
|
107
|
+
* Fail-CLOSED with respect to entitlement (any doubt → "not a supporter",
|
|
108
|
+
* which merely means the normal, already-opt-in-gated ad path applies) and
|
|
109
|
+
* fail-OPEN with respect to errors: nothing here can throw into a tool
|
|
110
|
+
* call. */
|
|
111
|
+
export function isSupporter(env = process.env, now = Date.now()) {
|
|
112
|
+
try {
|
|
113
|
+
return isEntitled(supporterVerdict(env, now));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=supporter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supporter.js","sourceRoot":"","sources":["../../src/ads/supporter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** The public half of the supporter-entitlement signing key (REP-29).
|
|
2
|
+
*
|
|
3
|
+
* Baked into the published package on purpose. Verification of a supporter
|
|
4
|
+
* token is a purely local operation — signature, tier, expiry — and this
|
|
5
|
+
* constant is the whole trust root for it. Nothing about entitlement needs a
|
|
6
|
+
* server, so nothing about entitlement contacts one: no licence check, no
|
|
7
|
+
* activation call, no heartbeat, no way for us to learn that you ran the
|
|
8
|
+
* tool. That property is only achievable if the verifier ships with the key.
|
|
9
|
+
*
|
|
10
|
+
* ## Provenance
|
|
11
|
+
*
|
|
12
|
+
* - Algorithm: Ed25519 (RFC 8032), SPKI/PEM encoding.
|
|
13
|
+
* - Generated 2026-08-21 with `node:crypto` `generateKeyPairSync("ed25519")`
|
|
14
|
+
* on the maintainer's machine, during the REP-29 implementation.
|
|
15
|
+
* - The PRIVATE half was never committed and never left that machine except
|
|
16
|
+
* into a secret store. Its only consumer is the Ko-fi fulfilment worker
|
|
17
|
+
* (`workers/kofi-fulfillment/`), which holds it as the Cloudflare secret
|
|
18
|
+
* `SUPPORTER_SIGNING_KEY`. No developer workflow, no test, and no part of
|
|
19
|
+
* the published package needs it: tests that must sign generate their own
|
|
20
|
+
* throwaway keypair.
|
|
21
|
+
*
|
|
22
|
+
* ## What this key can and cannot do for us
|
|
23
|
+
*
|
|
24
|
+
* Publishing the public half means anyone can *verify* a token; only the
|
|
25
|
+
* holder of the private half can *mint* one. It also means a supporter can
|
|
26
|
+
* read their own token's payload — which is deliberate, and why the payload
|
|
27
|
+
* contains nothing but an opaque subject hash, a tier name, and two
|
|
28
|
+
* timestamps (see `supporterToken.ts`). There is nothing in it to hide,
|
|
29
|
+
* because there is nothing in it about you.
|
|
30
|
+
*
|
|
31
|
+
* ## Rotation
|
|
32
|
+
*
|
|
33
|
+
* Tokens carry a `kid`. To rotate: add the new public key here alongside the
|
|
34
|
+
* old one, keep verifying both until every issued token has expired, then
|
|
35
|
+
* drop the old entry. Removing a key invalidates every token signed with it,
|
|
36
|
+
* so a rotation that is not additive-then-subtractive will strand paying
|
|
37
|
+
* supporters. The verifier already selects by `kid`, so the only change a
|
|
38
|
+
* rotation needs is to this map. */
|
|
39
|
+
/** Key id → SPKI PEM. Every entry is a key tokens may legitimately be signed
|
|
40
|
+
* with; a token naming a `kid` absent from this map is rejected before any
|
|
41
|
+
* signature check happens. */
|
|
42
|
+
export declare const SUPPORTER_PUBLIC_KEYS: Readonly<Record<string, string>>;
|
|
43
|
+
/** The key the minter should be signing with today. Only the fulfilment
|
|
44
|
+
* worker cares; the verifier accepts anything in `SUPPORTER_PUBLIC_KEYS`. */
|
|
45
|
+
export declare const CURRENT_SUPPORTER_KID = "skein-2026-08";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** The public half of the supporter-entitlement signing key (REP-29).
|
|
2
|
+
*
|
|
3
|
+
* Baked into the published package on purpose. Verification of a supporter
|
|
4
|
+
* token is a purely local operation — signature, tier, expiry — and this
|
|
5
|
+
* constant is the whole trust root for it. Nothing about entitlement needs a
|
|
6
|
+
* server, so nothing about entitlement contacts one: no licence check, no
|
|
7
|
+
* activation call, no heartbeat, no way for us to learn that you ran the
|
|
8
|
+
* tool. That property is only achievable if the verifier ships with the key.
|
|
9
|
+
*
|
|
10
|
+
* ## Provenance
|
|
11
|
+
*
|
|
12
|
+
* - Algorithm: Ed25519 (RFC 8032), SPKI/PEM encoding.
|
|
13
|
+
* - Generated 2026-08-21 with `node:crypto` `generateKeyPairSync("ed25519")`
|
|
14
|
+
* on the maintainer's machine, during the REP-29 implementation.
|
|
15
|
+
* - The PRIVATE half was never committed and never left that machine except
|
|
16
|
+
* into a secret store. Its only consumer is the Ko-fi fulfilment worker
|
|
17
|
+
* (`workers/kofi-fulfillment/`), which holds it as the Cloudflare secret
|
|
18
|
+
* `SUPPORTER_SIGNING_KEY`. No developer workflow, no test, and no part of
|
|
19
|
+
* the published package needs it: tests that must sign generate their own
|
|
20
|
+
* throwaway keypair.
|
|
21
|
+
*
|
|
22
|
+
* ## What this key can and cannot do for us
|
|
23
|
+
*
|
|
24
|
+
* Publishing the public half means anyone can *verify* a token; only the
|
|
25
|
+
* holder of the private half can *mint* one. It also means a supporter can
|
|
26
|
+
* read their own token's payload — which is deliberate, and why the payload
|
|
27
|
+
* contains nothing but an opaque subject hash, a tier name, and two
|
|
28
|
+
* timestamps (see `supporterToken.ts`). There is nothing in it to hide,
|
|
29
|
+
* because there is nothing in it about you.
|
|
30
|
+
*
|
|
31
|
+
* ## Rotation
|
|
32
|
+
*
|
|
33
|
+
* Tokens carry a `kid`. To rotate: add the new public key here alongside the
|
|
34
|
+
* old one, keep verifying both until every issued token has expired, then
|
|
35
|
+
* drop the old entry. Removing a key invalidates every token signed with it,
|
|
36
|
+
* so a rotation that is not additive-then-subtractive will strand paying
|
|
37
|
+
* supporters. The verifier already selects by `kid`, so the only change a
|
|
38
|
+
* rotation needs is to this map. */
|
|
39
|
+
/** Key id → SPKI PEM. Every entry is a key tokens may legitimately be signed
|
|
40
|
+
* with; a token naming a `kid` absent from this map is rejected before any
|
|
41
|
+
* signature check happens. */
|
|
42
|
+
export const SUPPORTER_PUBLIC_KEYS = Object.freeze({
|
|
43
|
+
"skein-2026-08": `-----BEGIN PUBLIC KEY-----
|
|
44
|
+
MCowBQYDK2VwAyEAV7vHvsTIlioxm9pWOJ83/sEZW/ps8ymd2I+LZeucnOk=
|
|
45
|
+
-----END PUBLIC KEY-----
|
|
46
|
+
`,
|
|
47
|
+
});
|
|
48
|
+
/** The key the minter should be signing with today. Only the fulfilment
|
|
49
|
+
* worker cares; the verifier accepts anything in `SUPPORTER_PUBLIC_KEYS`. */
|
|
50
|
+
export const CURRENT_SUPPORTER_KID = "skein-2026-08";
|
|
51
|
+
//# sourceMappingURL=supporterKey.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supporterKey.js","sourceRoot":"","sources":["../../src/ads/supporterKey.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qCAqCqC;AAErC;;+BAE+B;AAC/B,MAAM,CAAC,MAAM,qBAAqB,GAAqC,MAAM,CAAC,MAAM,CAAC;IACnF,eAAe,EAAE;;;CAGlB;CACA,CAAC,CAAC;AAEH;8EAC8E;AAC9E,MAAM,CAAC,MAAM,qBAAqB,GAAG,eAAe,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
/** Overrides the entitlement file location. Exists for tests and for anyone
|
|
29
|
+
* running with an unusual HOME; it is NOT a bypass — the file it points at
|
|
30
|
+
* still has to contain a validly signed token, so redirecting it cannot
|
|
31
|
+
* manufacture entitlement. */
|
|
32
|
+
export declare const SUPPORTER_FILE_ENV = "REPOSKEIN_SUPPORTER_FILE";
|
|
33
|
+
/** Resolves the entitlement file path, in order:
|
|
34
|
+
*
|
|
35
|
+
* 1. `REPOSKEIN_SUPPORTER_FILE`, if absolute.
|
|
36
|
+
* 2. `$XDG_CONFIG_HOME/reposkein/supporter.jwt`, if absolute.
|
|
37
|
+
* 3. `~/.config/reposkein/supporter.jwt` — the documented location. */
|
|
38
|
+
export declare function supporterTokenPath(env?: NodeJS.ProcessEnv): string;
|
|
39
|
+
export interface SupporterFileStat {
|
|
40
|
+
mtimeMs: number;
|
|
41
|
+
size: number;
|
|
42
|
+
/** POSIX permission bits (`mode & 0o777`). */
|
|
43
|
+
mode: number;
|
|
44
|
+
}
|
|
45
|
+
/** Cheap existence + identity probe. Returns null for "no file" and for any
|
|
46
|
+
* error reading it — an unreadable entitlement file is indistinguishable
|
|
47
|
+
* from an absent one as far as entitlement goes, and this runs on a tool
|
|
48
|
+
* call's hot path, so it must never throw. */
|
|
49
|
+
export declare function statSupporterTokenFile(env?: NodeJS.ProcessEnv): SupporterFileStat | null;
|
|
50
|
+
/** Reads the token text, or null if there is nothing readable there. Never
|
|
51
|
+
* throws. Size is bounded by the caller's own token-length limit; a file
|
|
52
|
+
* larger than that fails verification rather than being read into memory
|
|
53
|
+
* blindly, so the read is capped here too. */
|
|
54
|
+
export declare function readSupporterTokenFile(env?: NodeJS.ProcessEnv, maxBytes?: number): string | null;
|
|
55
|
+
/** Writes the token, creating `~/.config/reposkein/` as 0700 if needed, and
|
|
56
|
+
* returns the path written. Throws on failure — this one is called from the
|
|
57
|
+
* CLI, where a failure must be reported, not swallowed. */
|
|
58
|
+
export declare function writeSupporterTokenFile(token: string, env?: NodeJS.ProcessEnv): string;
|
|
59
|
+
/** Deletes the entitlement file. Returns true if a file was removed, false if
|
|
60
|
+
* there was nothing to remove. */
|
|
61
|
+
export declare function removeSupporterTokenFile(env?: NodeJS.ProcessEnv): boolean;
|