@vibe-commit/capture 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.
Files changed (141) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +66 -0
  3. package/dist/agents/claude_code.d.ts +11 -0
  4. package/dist/agents/claude_code.js +162 -0
  5. package/dist/agents/claude_code.js.map +1 -0
  6. package/dist/agents/codex_cli.d.ts +15 -0
  7. package/dist/agents/codex_cli.js +67 -0
  8. package/dist/agents/codex_cli.js.map +1 -0
  9. package/dist/agents/cursor.d.ts +21 -0
  10. package/dist/agents/cursor.js +89 -0
  11. package/dist/agents/cursor.js.map +1 -0
  12. package/dist/agents/registry.d.ts +184 -0
  13. package/dist/agents/registry.js +212 -0
  14. package/dist/agents/registry.js.map +1 -0
  15. package/dist/agents/types.d.ts +279 -0
  16. package/dist/agents/types.js +67 -0
  17. package/dist/agents/types.js.map +1 -0
  18. package/dist/commands/connect.d.ts +36 -0
  19. package/dist/commands/connect.js +499 -0
  20. package/dist/commands/connect.js.map +1 -0
  21. package/dist/commands/context.d.ts +45 -0
  22. package/dist/commands/context.js +14 -0
  23. package/dist/commands/context.js.map +1 -0
  24. package/dist/commands/report.d.ts +105 -0
  25. package/dist/commands/report.js +430 -0
  26. package/dist/commands/report.js.map +1 -0
  27. package/dist/commands/status.d.ts +29 -0
  28. package/dist/commands/status.js +236 -0
  29. package/dist/commands/status.js.map +1 -0
  30. package/dist/commands/why.d.ts +349 -0
  31. package/dist/commands/why.js +937 -0
  32. package/dist/commands/why.js.map +1 -0
  33. package/dist/consent.d.ts +59 -0
  34. package/dist/consent.js +111 -0
  35. package/dist/consent.js.map +1 -0
  36. package/dist/copy/claims.d.ts +79 -0
  37. package/dist/copy/claims.js +343 -0
  38. package/dist/copy/claims.js.map +1 -0
  39. package/dist/copy/grades.d.ts +117 -0
  40. package/dist/copy/grades.js +96 -0
  41. package/dist/copy/grades.js.map +1 -0
  42. package/dist/copy/index.d.ts +12 -0
  43. package/dist/copy/index.js +13 -0
  44. package/dist/copy/index.js.map +1 -0
  45. package/dist/copy/strings.d.ts +1328 -0
  46. package/dist/copy/strings.js +1243 -0
  47. package/dist/copy/strings.js.map +1 -0
  48. package/dist/copy/time.d.ts +27 -0
  49. package/dist/copy/time.js +43 -0
  50. package/dist/copy/time.js.map +1 -0
  51. package/dist/credential.d.ts +211 -0
  52. package/dist/credential.js +298 -0
  53. package/dist/credential.js.map +1 -0
  54. package/dist/exit.d.ts +59 -0
  55. package/dist/exit.js +60 -0
  56. package/dist/exit.js.map +1 -0
  57. package/dist/git.d.ts +299 -0
  58. package/dist/git.js +653 -0
  59. package/dist/git.js.map +1 -0
  60. package/dist/hooks/entry.d.ts +277 -0
  61. package/dist/hooks/entry.js +1092 -0
  62. package/dist/hooks/entry.js.map +1 -0
  63. package/dist/hooks/post_commit.d.ts +59 -0
  64. package/dist/hooks/post_commit.js +160 -0
  65. package/dist/hooks/post_commit.js.map +1 -0
  66. package/dist/hooks/post_rewrite.d.ts +59 -0
  67. package/dist/hooks/post_rewrite.js +161 -0
  68. package/dist/hooks/post_rewrite.js.map +1 -0
  69. package/dist/hooks/session_end.d.ts +101 -0
  70. package/dist/hooks/session_end.js +120 -0
  71. package/dist/hooks/session_end.js.map +1 -0
  72. package/dist/index.d.ts +34 -0
  73. package/dist/index.js +360 -0
  74. package/dist/index.js.map +1 -0
  75. package/dist/install.d.ts +153 -0
  76. package/dist/install.js +284 -0
  77. package/dist/install.js.map +1 -0
  78. package/dist/json.d.ts +100 -0
  79. package/dist/json.js +94 -0
  80. package/dist/json.js.map +1 -0
  81. package/dist/oauth/discovery.d.ts +96 -0
  82. package/dist/oauth/discovery.js +163 -0
  83. package/dist/oauth/discovery.js.map +1 -0
  84. package/dist/oauth/lock.d.ts +100 -0
  85. package/dist/oauth/lock.js +287 -0
  86. package/dist/oauth/lock.js.map +1 -0
  87. package/dist/oauth/loopback.d.ts +85 -0
  88. package/dist/oauth/loopback.js +124 -0
  89. package/dist/oauth/loopback.js.map +1 -0
  90. package/dist/oauth/pkce.d.ts +58 -0
  91. package/dist/oauth/pkce.js +77 -0
  92. package/dist/oauth/pkce.js.map +1 -0
  93. package/dist/oauth/read.d.ts +120 -0
  94. package/dist/oauth/read.js +144 -0
  95. package/dist/oauth/read.js.map +1 -0
  96. package/dist/oauth/session.d.ts +80 -0
  97. package/dist/oauth/session.js +112 -0
  98. package/dist/oauth/session.js.map +1 -0
  99. package/dist/oauth/signin.d.ts +178 -0
  100. package/dist/oauth/signin.js +225 -0
  101. package/dist/oauth/signin.js.map +1 -0
  102. package/dist/oauth/token.d.ts +139 -0
  103. package/dist/oauth/token.js +183 -0
  104. package/dist/oauth/token.js.map +1 -0
  105. package/dist/paths.d.ts +366 -0
  106. package/dist/paths.js +460 -0
  107. package/dist/paths.js.map +1 -0
  108. package/dist/policy.d.ts +195 -0
  109. package/dist/policy.js +244 -0
  110. package/dist/policy.js.map +1 -0
  111. package/dist/post.d.ts +302 -0
  112. package/dist/post.js +384 -0
  113. package/dist/post.js.map +1 -0
  114. package/dist/project.d.ts +99 -0
  115. package/dist/project.js +192 -0
  116. package/dist/project.js.map +1 -0
  117. package/dist/redact.d.ts +175 -0
  118. package/dist/redact.js +700 -0
  119. package/dist/redact.js.map +1 -0
  120. package/dist/runtime.d.ts +30 -0
  121. package/dist/runtime.js +37 -0
  122. package/dist/runtime.js.map +1 -0
  123. package/dist/spawn_budget.d.ts +101 -0
  124. package/dist/spawn_budget.js +124 -0
  125. package/dist/spawn_budget.js.map +1 -0
  126. package/dist/spool.d.ts +390 -0
  127. package/dist/spool.js +556 -0
  128. package/dist/spool.js.map +1 -0
  129. package/dist/state.d.ts +109 -0
  130. package/dist/state.js +217 -0
  131. package/dist/state.js.map +1 -0
  132. package/dist/system_message.d.ts +101 -0
  133. package/dist/system_message.js +141 -0
  134. package/dist/system_message.js.map +1 -0
  135. package/dist/term.d.ts +112 -0
  136. package/dist/term.js +134 -0
  137. package/dist/term.js.map +1 -0
  138. package/dist/version.d.ts +16 -0
  139. package/dist/version.js +17 -0
  140. package/dist/version.js.map +1 -0
  141. package/package.json +50 -0
@@ -0,0 +1,96 @@
1
+ /**
2
+ * RFC 9728 + RFC 8414 discovery — where the authorize and token endpoints are.
3
+ *
4
+ * **The authorize URL is DISCOVERED, never constructed.** `vibecommit-mcp`
5
+ * deliberately does not route `/oauth/authorize`: the authorize UI lives in
6
+ * `vibecommit-web` on a different host in production, and the server carries its
7
+ * address as configuration (`DiscoveryUrls.authorizeEndpointUrl`, whose own
8
+ * docblock says *"there is no correct cross-repo default (any `${issuer}/...`
9
+ * 404s on the MCP host)"*). A hardcoded path has already cost this project once:
10
+ * the constant pointed at `/oauth/authorize` while the live route was
11
+ * `/auth/claude`, and every failure read as a config problem.
12
+ *
13
+ * So the chain is:
14
+ *
15
+ * 1. `GET <resource-origin>/.well-known/oauth-protected-resource` → RFC 9728
16
+ * protected-resource metadata → `authorization_servers[0]` is the issuer.
17
+ * 2. `GET <issuer>/.well-known/oauth-authorization-server` → RFC 8414
18
+ * authorization-server metadata → `authorization_endpoint`, `token_endpoint`.
19
+ *
20
+ * ## The issuer check is not paperwork
21
+ *
22
+ * RFC 8414 §3.3 requires the `issuer` INSIDE the metadata to equal the issuer
23
+ * the document was fetched from. That exact-string equality is the same
24
+ * constraint D26 records on the other side of the wire — the authorization
25
+ * response carries RFC 9207 `iss`, and it must string-equal both the PRM
26
+ * `authorization_servers[]` entry and this document's `issuer`. Three places,
27
+ * one string; a mismatch anywhere is a mix-up attack or a misconfiguration, and
28
+ * either way the flow must stop rather than proceed against an issuer nobody
29
+ * vouched for.
30
+ *
31
+ * Nothing is imported from `vibecommit-mcp` and nothing may be: it is closed
32
+ * source and this package is MIT (D60 §D1a). The two documents below are the
33
+ * WIRE SHAPE, retyped from reading it.
34
+ *
35
+ * @provenance vibecommit-mcp src/oauth/discovery.ts — both metadata documents, retyped
36
+ * @provenance vibecommit-mcp src/transport/server.ts — both `.well-known` routes, retyped
37
+ * @provenance vibecommit-web /auth/* — authorize UI is hosted there, not on the MCP host
38
+ */
39
+ /** RFC 9728 §3.1. */
40
+ export declare const PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource";
41
+ /** RFC 8414 §3. */
42
+ export declare const AUTHORIZATION_SERVER_PATH = "/.well-known/oauth-authorization-server";
43
+ /**
44
+ * The `/mcp` resource this client reads from. One `tools/call` POST goes here
45
+ * with the user token as bearer — there is no second Fly app, no `/read` route
46
+ * and no second hostname (D73, which closed `CR-083` as not needed).
47
+ */
48
+ export declare const DEFAULT_MCP_URL = "https://api.vibecommit.ai/mcp";
49
+ /** Override for tests and for a self-hosted server, same shape as the ingest lane's. */
50
+ export declare function mcpUrl(env: NodeJS.ProcessEnv): string | null;
51
+ /**
52
+ * What this client needs out of the two documents.
53
+ *
54
+ * A subset, deliberately: the server publishes `scopes_supported`,
55
+ * `response_types_supported` and more, and a client that retyped every field
56
+ * would break on an addition it does not use.
57
+ */
58
+ export interface AuthorizationServerMetadata {
59
+ readonly issuer: string;
60
+ readonly authorization_endpoint: string;
61
+ readonly token_endpoint: string;
62
+ readonly code_challenge_methods_supported?: readonly string[];
63
+ }
64
+ export type Discovery = {
65
+ readonly kind: "ok";
66
+ readonly metadata: AuthorizationServerMetadata;
67
+ }
68
+ /** No document, or one that is not JSON. The server may simply be down. */
69
+ | {
70
+ readonly kind: "unreachable";
71
+ readonly url: string;
72
+ }
73
+ /** A document arrived and is not usable. `detail` names WHICH rule failed. */
74
+ | {
75
+ readonly kind: "malformed";
76
+ readonly url: string;
77
+ readonly detail: DiscoveryFault;
78
+ };
79
+ /**
80
+ * Why a document was refused. A named cause rather than a boolean, because
81
+ * "issuer does not match" and "S256 not offered" want different fixes and the
82
+ * error block has to say which.
83
+ */
84
+ export type DiscoveryFault = "no_authorization_server" | "issuer_mismatch" | "no_authorize_endpoint" | "no_token_endpoint" | "insecure_endpoint" | "no_s256";
85
+ /** Injected so tests drive a real local server rather than the network. */
86
+ export interface DiscoveryDeps {
87
+ readonly fetch: typeof fetch;
88
+ readonly timeoutMs: number;
89
+ }
90
+ /**
91
+ * Discover the authorization server for `resourceUrl`.
92
+ *
93
+ * `resourceUrl` is the `/mcp` endpoint. Both documents are public by design
94
+ * (RFC 9728/8414 §"no auth"), so nothing here carries a credential.
95
+ */
96
+ export declare function discover(resourceUrl: string, deps: DiscoveryDeps): Promise<Discovery>;
@@ -0,0 +1,163 @@
1
+ /**
2
+ * RFC 9728 + RFC 8414 discovery — where the authorize and token endpoints are.
3
+ *
4
+ * **The authorize URL is DISCOVERED, never constructed.** `vibecommit-mcp`
5
+ * deliberately does not route `/oauth/authorize`: the authorize UI lives in
6
+ * `vibecommit-web` on a different host in production, and the server carries its
7
+ * address as configuration (`DiscoveryUrls.authorizeEndpointUrl`, whose own
8
+ * docblock says *"there is no correct cross-repo default (any `${issuer}/...`
9
+ * 404s on the MCP host)"*). A hardcoded path has already cost this project once:
10
+ * the constant pointed at `/oauth/authorize` while the live route was
11
+ * `/auth/claude`, and every failure read as a config problem.
12
+ *
13
+ * So the chain is:
14
+ *
15
+ * 1. `GET <resource-origin>/.well-known/oauth-protected-resource` → RFC 9728
16
+ * protected-resource metadata → `authorization_servers[0]` is the issuer.
17
+ * 2. `GET <issuer>/.well-known/oauth-authorization-server` → RFC 8414
18
+ * authorization-server metadata → `authorization_endpoint`, `token_endpoint`.
19
+ *
20
+ * ## The issuer check is not paperwork
21
+ *
22
+ * RFC 8414 §3.3 requires the `issuer` INSIDE the metadata to equal the issuer
23
+ * the document was fetched from. That exact-string equality is the same
24
+ * constraint D26 records on the other side of the wire — the authorization
25
+ * response carries RFC 9207 `iss`, and it must string-equal both the PRM
26
+ * `authorization_servers[]` entry and this document's `issuer`. Three places,
27
+ * one string; a mismatch anywhere is a mix-up attack or a misconfiguration, and
28
+ * either way the flow must stop rather than proceed against an issuer nobody
29
+ * vouched for.
30
+ *
31
+ * Nothing is imported from `vibecommit-mcp` and nothing may be: it is closed
32
+ * source and this package is MIT (D60 §D1a). The two documents below are the
33
+ * WIRE SHAPE, retyped from reading it.
34
+ *
35
+ * @provenance vibecommit-mcp src/oauth/discovery.ts — both metadata documents, retyped
36
+ * @provenance vibecommit-mcp src/transport/server.ts — both `.well-known` routes, retyped
37
+ * @provenance vibecommit-web /auth/* — authorize UI is hosted there, not on the MCP host
38
+ */
39
+ import { isAllowedIngestUrl } from "../post.js";
40
+ import { CHALLENGE_METHOD } from "./pkce.js";
41
+ /** RFC 9728 §3.1. */
42
+ export const PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource";
43
+ /** RFC 8414 §3. */
44
+ export const AUTHORIZATION_SERVER_PATH = "/.well-known/oauth-authorization-server";
45
+ /**
46
+ * The `/mcp` resource this client reads from. One `tools/call` POST goes here
47
+ * with the user token as bearer — there is no second Fly app, no `/read` route
48
+ * and no second hostname (D73, which closed `CR-083` as not needed).
49
+ */
50
+ export const DEFAULT_MCP_URL = "https://api.vibecommit.ai/mcp";
51
+ /** Override for tests and for a self-hosted server, same shape as the ingest lane's. */
52
+ export function mcpUrl(env) {
53
+ const override = env.VIBECOMMIT_MCP_URL;
54
+ if (override === undefined || override.trim() === "")
55
+ return DEFAULT_MCP_URL;
56
+ const candidate = override.trim();
57
+ return isAllowedIngestUrl(candidate) ? candidate : null;
58
+ }
59
+ async function getJson(url, deps) {
60
+ const controller = new AbortController();
61
+ const timer = setTimeout(() => controller.abort(), deps.timeoutMs);
62
+ try {
63
+ const res = await deps.fetch(url, {
64
+ method: "GET",
65
+ headers: { accept: "application/json" },
66
+ signal: controller.signal,
67
+ // A discovery document that redirects is a document from somewhere the
68
+ // operator did not configure. `post.ts` refuses redirects on the same
69
+ // reasoning and for the same lane.
70
+ redirect: "error",
71
+ });
72
+ if (!res.ok)
73
+ return { ok: false };
74
+ return { ok: true, body: (await res.json()) };
75
+ }
76
+ catch {
77
+ return { ok: false };
78
+ }
79
+ finally {
80
+ clearTimeout(timer);
81
+ }
82
+ }
83
+ /** `https://host/mcp` → `https://host`. The `.well-known` paths sit at the root. */
84
+ function originOf(url) {
85
+ return new URL(url).origin;
86
+ }
87
+ /**
88
+ * Discover the authorization server for `resourceUrl`.
89
+ *
90
+ * `resourceUrl` is the `/mcp` endpoint. Both documents are public by design
91
+ * (RFC 9728/8414 §"no auth"), so nothing here carries a credential.
92
+ */
93
+ export async function discover(resourceUrl, deps) {
94
+ const prmUrl = `${originOf(resourceUrl)}${PROTECTED_RESOURCE_PATH}`;
95
+ const prm = await getJson(prmUrl, deps);
96
+ if (!prm.ok)
97
+ return { kind: "unreachable", url: prmUrl };
98
+ const servers = prm.body.authorization_servers;
99
+ const issuer = Array.isArray(servers) && typeof servers[0] === "string" ? servers[0] : null;
100
+ if (issuer === null || issuer === "") {
101
+ return { kind: "malformed", url: prmUrl, detail: "no_authorization_server" };
102
+ }
103
+ // ⚠ `/cso` finding, MEDIUM. VALIDATE BEFORE FETCHING, not after. The issuer
104
+ // arrives INSIDE a document, so it is remote input, and the next line turns it
105
+ // into an outbound request from the user's machine. Validating only the two
106
+ // ENDPOINTS below — which is what this did — left the AS-metadata GET itself
107
+ // unchecked, so a hostile or compromised discovery document could aim it at
108
+ // `http://169.254.169.254`, a private RFC 1918 address, or an arbitrary
109
+ // internal port: a blind SSRF probe with the request coming from inside the
110
+ // network. The response cannot be read back by the attacker, which is what
111
+ // keeps this MEDIUM rather than HIGH.
112
+ //
113
+ // Same predicate the endpoints get, applied one step earlier so the rule is
114
+ // consistent: every URL this module fetches passed it first.
115
+ if (!isAllowedIngestUrl(issuer)) {
116
+ return { kind: "malformed", url: prmUrl, detail: "insecure_endpoint" };
117
+ }
118
+ const asUrl = `${issuer.replace(/\/+$/, "")}${AUTHORIZATION_SERVER_PATH}`;
119
+ const as = await getJson(asUrl, deps);
120
+ if (!as.ok)
121
+ return { kind: "unreachable", url: asUrl };
122
+ const doc = as.body;
123
+ // RFC 8414 §3.3, and D26's exact-string rule. Not normalised, not
124
+ // case-folded, not trailing-slash-tolerant: the whole value of the check is
125
+ // that it is the SAME comparison the RFC 9207 `iss` check makes later, so a
126
+ // server whose three copies of this string disagree fails here rather than
127
+ // three steps further on where it reads as something else.
128
+ if (doc.issuer !== issuer)
129
+ return { kind: "malformed", url: asUrl, detail: "issuer_mismatch" };
130
+ if (typeof doc.authorization_endpoint !== "string" || doc.authorization_endpoint === "") {
131
+ return { kind: "malformed", url: asUrl, detail: "no_authorize_endpoint" };
132
+ }
133
+ if (typeof doc.token_endpoint !== "string" || doc.token_endpoint === "") {
134
+ return { kind: "malformed", url: asUrl, detail: "no_token_endpoint" };
135
+ }
136
+ // The token endpoint receives the verifier and returns the refresh token, and
137
+ // the authorize endpoint receives the challenge. Neither may be plaintext off
138
+ // the loopback. Same predicate as the ingest lane: https, or http to a
139
+ // loopback host. (⚠ NOT the redirect-URI rule — that one additionally
140
+ // forbids the literal `localhost`, and it lives in `loopback.ts` because it is
141
+ // the SERVER's policy about a value we send it, not ours about where we send.)
142
+ for (const endpoint of [doc.authorization_endpoint, doc.token_endpoint]) {
143
+ if (!isAllowedIngestUrl(endpoint)) {
144
+ return { kind: "malformed", url: asUrl, detail: "insecure_endpoint" };
145
+ }
146
+ }
147
+ // Absent means "unstated", and RFC 8414 §2 says a client that needs S256 must
148
+ // not assume it. We need it: it is the only method this client can produce.
149
+ const methods = doc.code_challenge_methods_supported;
150
+ if (!Array.isArray(methods) || !methods.includes(CHALLENGE_METHOD)) {
151
+ return { kind: "malformed", url: asUrl, detail: "no_s256" };
152
+ }
153
+ return {
154
+ kind: "ok",
155
+ metadata: {
156
+ issuer,
157
+ authorization_endpoint: doc.authorization_endpoint,
158
+ token_endpoint: doc.token_endpoint,
159
+ code_challenge_methods_supported: methods,
160
+ },
161
+ };
162
+ }
163
+ //# sourceMappingURL=discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../src/oauth/discovery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Cross-process single-flight for the refresh grant.
3
+ *
4
+ * ## ⚠ WHY THIS FILE EXISTS AT ALL — read before simplifying it away
5
+ *
6
+ * `vibecommit-mcp/src/oauth/refresh.ts` implements refresh-token FAMILY rotation
7
+ * with application-layer reuse detection: presenting a token that already has a
8
+ * child row is treated as a replay and **revokes the ENTIRE FAMILY** with
9
+ * `revoked_reason = 'reuse_detected'`. There is a database-level guard behind it
10
+ * too — a partial unique index on `parent_token_id`, so of two concurrent
11
+ * rotations from the same tail exactly one INSERT wins and the loser's 23505 is
12
+ * *also* read as reuse.
13
+ *
14
+ * Two terminals running a read at the same moment present the same tail token.
15
+ * That is not an edge case; it is Tuesday. Without a lock, one of them wins, the
16
+ * other is classed a replay, and **the user is signed out of every machine at
17
+ * once** — from doing nothing but running the same command twice.
18
+ *
19
+ * So the mutual exclusion has to be BETWEEN PROCESSES. An in-process mutex, a
20
+ * module-level promise, an `async` queue — every in-memory construct is the
21
+ * wrong shape here, because the two racers are two separate `vibecommit`
22
+ * invocations that share nothing but a filesystem.
23
+ *
24
+ * ## The loser waits and RE-READS — it does not queue up to present the old token
25
+ *
26
+ * `withRefreshLock` gives its callback the lock, nothing more. The callback's
27
+ * first act must be to re-read the session from disk, because by the time a
28
+ * waiter acquires the lock the winner has already rotated and saved: the tail
29
+ * the waiter loaded BEFORE waiting is now the parent of a live row, and
30
+ * presenting it is precisely the replay this file exists to prevent. See
31
+ * `signin.ts`'s `authorizedAccessToken`, which is the only caller and is written
32
+ * in exactly that order.
33
+ *
34
+ * ## The stale timeout is DERIVED, not guessed
35
+ *
36
+ * A crashed holder must not wedge every future read, so a lock older than
37
+ * `STALE_MS` is reclaimable. That number is not a feel for how long things take:
38
+ * this project has already paid for one wall-clock guess (`CR-126`/D83 found a
39
+ * `setTimeout(..., 60)` that was a guess at boot duration, sitting between p50
40
+ * and p90 and firing ~14% of the time idle and ~28% under load).
41
+ *
42
+ * The holder does exactly ONE thing while holding this lock: a single POST to
43
+ * the token endpoint, and that POST is bounded by an `AbortController` at
44
+ * `REFRESH_REQUEST_TIMEOUT_MS`. So the longest a HEALTHY holder can hold the
45
+ * lock is that timeout plus the file I/O either side of it, and `STALE_MS` is
46
+ * that bound plus a margin — an arithmetic consequence of the operation it
47
+ * bounds, which means changing the request timeout moves it automatically
48
+ * rather than leaving a stale constant behind.
49
+ *
50
+ * Reclaiming too EARLY is the dangerous direction: it puts two processes into
51
+ * the refresh at once, which is the family revoke. Reclaiming too LATE only
52
+ * makes a rare recovery slower. The margin is therefore generous on purpose.
53
+ *
54
+ * Nothing is imported or copied from the closed core (D60 §D1a): what crossed
55
+ * the boundary is the RULE that concurrent rotation is a revoke, which is a
56
+ * behavioural contract this client must obey, not an implementation.
57
+ *
58
+ * @provenance vibecommit-mcp src/oauth/refresh.ts — family-rotation reuse-detection rule, retyped
59
+ */
60
+ /**
61
+ * The whole budget for one refresh POST. Also the ONLY thing done under the
62
+ * lock, which is what makes `STALE_MS` below derivable.
63
+ */
64
+ export declare const REFRESH_REQUEST_TIMEOUT_MS = 10000;
65
+ /**
66
+ * When a lock is old enough to be a corpse rather than a holder.
67
+ *
68
+ * `REFRESH_REQUEST_TIMEOUT_MS` bounds the healthy hold; the rest is margin for
69
+ * the read/write either side and for a machine under enough load that a process
70
+ * does not get scheduled promptly. Erring long is the cheap direction — see the
71
+ * module note.
72
+ */
73
+ export declare const STALE_MS: number;
74
+ /**
75
+ * How long a waiter waits before giving up.
76
+ *
77
+ * Strictly greater than `STALE_MS`: a waiter that gave up sooner would abandon a
78
+ * refresh that a perfectly healthy holder was still allowed to be running, and
79
+ * the user would see a spurious failure while the thing they needed was in
80
+ * flight one process over.
81
+ */
82
+ export declare const WAIT_TIMEOUT_MS: number;
83
+ /** One lock for the refresh grant. Sits beside the session it protects. */
84
+ export declare function refreshLockPath(home: string): string;
85
+ export type LockFailure = "timeout";
86
+ /**
87
+ * Run `fn` with the refresh lock held.
88
+ *
89
+ * Returns `{ kind: "timeout" }` rather than throwing when the lock cannot be
90
+ * taken: the caller is a read verb, and a read that cannot refresh should tell
91
+ * the user what happened, not take an exception out of a lock file.
92
+ *
93
+ * The lock is released in a `finally`, so a throwing callback still frees it.
94
+ */
95
+ export declare function withRefreshLock<T>(home: string, fn: () => Promise<T>, now?: () => number): Promise<{
96
+ kind: "ok";
97
+ value: T;
98
+ } | {
99
+ kind: LockFailure;
100
+ }>;
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Cross-process single-flight for the refresh grant.
3
+ *
4
+ * ## ⚠ WHY THIS FILE EXISTS AT ALL — read before simplifying it away
5
+ *
6
+ * `vibecommit-mcp/src/oauth/refresh.ts` implements refresh-token FAMILY rotation
7
+ * with application-layer reuse detection: presenting a token that already has a
8
+ * child row is treated as a replay and **revokes the ENTIRE FAMILY** with
9
+ * `revoked_reason = 'reuse_detected'`. There is a database-level guard behind it
10
+ * too — a partial unique index on `parent_token_id`, so of two concurrent
11
+ * rotations from the same tail exactly one INSERT wins and the loser's 23505 is
12
+ * *also* read as reuse.
13
+ *
14
+ * Two terminals running a read at the same moment present the same tail token.
15
+ * That is not an edge case; it is Tuesday. Without a lock, one of them wins, the
16
+ * other is classed a replay, and **the user is signed out of every machine at
17
+ * once** — from doing nothing but running the same command twice.
18
+ *
19
+ * So the mutual exclusion has to be BETWEEN PROCESSES. An in-process mutex, a
20
+ * module-level promise, an `async` queue — every in-memory construct is the
21
+ * wrong shape here, because the two racers are two separate `vibecommit`
22
+ * invocations that share nothing but a filesystem.
23
+ *
24
+ * ## The loser waits and RE-READS — it does not queue up to present the old token
25
+ *
26
+ * `withRefreshLock` gives its callback the lock, nothing more. The callback's
27
+ * first act must be to re-read the session from disk, because by the time a
28
+ * waiter acquires the lock the winner has already rotated and saved: the tail
29
+ * the waiter loaded BEFORE waiting is now the parent of a live row, and
30
+ * presenting it is precisely the replay this file exists to prevent. See
31
+ * `signin.ts`'s `authorizedAccessToken`, which is the only caller and is written
32
+ * in exactly that order.
33
+ *
34
+ * ## The stale timeout is DERIVED, not guessed
35
+ *
36
+ * A crashed holder must not wedge every future read, so a lock older than
37
+ * `STALE_MS` is reclaimable. That number is not a feel for how long things take:
38
+ * this project has already paid for one wall-clock guess (`CR-126`/D83 found a
39
+ * `setTimeout(..., 60)` that was a guess at boot duration, sitting between p50
40
+ * and p90 and firing ~14% of the time idle and ~28% under load).
41
+ *
42
+ * The holder does exactly ONE thing while holding this lock: a single POST to
43
+ * the token endpoint, and that POST is bounded by an `AbortController` at
44
+ * `REFRESH_REQUEST_TIMEOUT_MS`. So the longest a HEALTHY holder can hold the
45
+ * lock is that timeout plus the file I/O either side of it, and `STALE_MS` is
46
+ * that bound plus a margin — an arithmetic consequence of the operation it
47
+ * bounds, which means changing the request timeout moves it automatically
48
+ * rather than leaving a stale constant behind.
49
+ *
50
+ * Reclaiming too EARLY is the dangerous direction: it puts two processes into
51
+ * the refresh at once, which is the family revoke. Reclaiming too LATE only
52
+ * makes a rare recovery slower. The margin is therefore generous on purpose.
53
+ *
54
+ * Nothing is imported or copied from the closed core (D60 §D1a): what crossed
55
+ * the boundary is the RULE that concurrent rotation is a revoke, which is a
56
+ * behavioural contract this client must obey, not an implementation.
57
+ *
58
+ * @provenance vibecommit-mcp src/oauth/refresh.ts — family-rotation reuse-detection rule, retyped
59
+ */
60
+ import { randomBytes } from "node:crypto";
61
+ import { closeSync, linkSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
62
+ import { join } from "node:path";
63
+ import { rootDir } from "../paths.js";
64
+ /**
65
+ * The whole budget for one refresh POST. Also the ONLY thing done under the
66
+ * lock, which is what makes `STALE_MS` below derivable.
67
+ */
68
+ export const REFRESH_REQUEST_TIMEOUT_MS = 10_000;
69
+ /**
70
+ * When a lock is old enough to be a corpse rather than a holder.
71
+ *
72
+ * `REFRESH_REQUEST_TIMEOUT_MS` bounds the healthy hold; the rest is margin for
73
+ * the read/write either side and for a machine under enough load that a process
74
+ * does not get scheduled promptly. Erring long is the cheap direction — see the
75
+ * module note.
76
+ */
77
+ export const STALE_MS = REFRESH_REQUEST_TIMEOUT_MS + 5_000;
78
+ /**
79
+ * How long a waiter waits before giving up.
80
+ *
81
+ * Strictly greater than `STALE_MS`: a waiter that gave up sooner would abandon a
82
+ * refresh that a perfectly healthy holder was still allowed to be running, and
83
+ * the user would see a spurious failure while the thing they needed was in
84
+ * flight one process over.
85
+ */
86
+ export const WAIT_TIMEOUT_MS = STALE_MS + 2_000;
87
+ /**
88
+ * Poll interval while waiting. Short enough that the handoff is imperceptible,
89
+ * long enough that waiting costs nothing: over a full 10s refresh this is a few
90
+ * hundred `readFileSync` calls on a file in the page cache.
91
+ */
92
+ const POLL_MS = 20;
93
+ /** One lock for the refresh grant. Sits beside the session it protects. */
94
+ export function refreshLockPath(home) {
95
+ return join(rootDir(home), "refresh.lock");
96
+ }
97
+ const sleep = (ms) => new Promise((resolve) => {
98
+ setTimeout(resolve, ms);
99
+ });
100
+ /**
101
+ * The lock's contents: enough to tell one holder from another, and nothing
102
+ * secret. Never a token — a lock file has looser handling than a credential
103
+ * file by nature (it is created and destroyed constantly) and is exactly the
104
+ * wrong place to put a bearer secret.
105
+ */
106
+ function stamp() {
107
+ // The nonce is what makes the stamp an IDENTITY rather than a description.
108
+ // `releaseIfMine` compares these bytes to decide whether the lock on disk is
109
+ // still ours, and pids are recycled — so without it, "same pid, same
110
+ // millisecond" would be a (remote) way to delete somebody else's lock.
111
+ return JSON.stringify({
112
+ pid: process.pid,
113
+ at: Date.now(),
114
+ nonce: randomBytes(8).toString("hex"),
115
+ });
116
+ }
117
+ function heldSince(raw) {
118
+ try {
119
+ const at = JSON.parse(raw).at;
120
+ return typeof at === "number" ? at : null;
121
+ }
122
+ catch {
123
+ return null;
124
+ }
125
+ }
126
+ /**
127
+ * Try once to create the lock exclusively.
128
+ *
129
+ * `wx` is `O_CREAT | O_EXCL | O_WRONLY`, which the kernel makes atomic: of any
130
+ * number of processes calling this at the same instant, exactly one gets the
131
+ * file and the rest get EEXIST. That atomicity is the entire mutual exclusion —
132
+ * a "does it exist? then create it" pair would have a window between the two
133
+ * calls, and a window is all this needs to fail.
134
+ */
135
+ function tryAcquire(path) {
136
+ const mine = stamp();
137
+ // ⚠ WRITE FIRST, PUBLISH SECOND — and this ordering is a BUG FIX, not style.
138
+ //
139
+ // The obvious implementation is `openSync(path, "wx")` then `writeFileSync`.
140
+ // That creates the lock EMPTY and fills it a moment later, and in that window
141
+ // a second process reads a zero-byte file. `heldSince` cannot date it, the
142
+ // wait loop's "an undatable lock was not written by this code, treat it as
143
+ // stale" branch fires, and the racer RECLAIMS A LOCK THAT WAS JUST TAKEN.
144
+ // Both processes then refresh, and the server revokes the family — the exact
145
+ // outcome this module exists to prevent, reached through the module itself.
146
+ //
147
+ // Measured, not theorised: the two-process case in `test/oauth-signin.test.ts`
148
+ // failed on 2 of 8 runs with the second child getting `expired`, which is the
149
+ // server's answer to a replayed token.
150
+ //
151
+ // `link()` is the fix because it is ATOMIC and it fails with EEXIST when the
152
+ // target exists. The temp file already holds the full stamp before the link
153
+ // publishes it, so the lock is never observable half-written. The temp name
154
+ // carries a random component as well as the pid: two processes must not
155
+ // collide on the staging file either.
156
+ const staging = `${path}.${process.pid}.${randomBytes(6).toString("hex")}`;
157
+ try {
158
+ // `wx` on the STAGING file also refuses to follow a symlink (`O_CREAT |
159
+ // O_EXCL`), so a pre-planted link cannot be written through.
160
+ const fd = openSync(staging, "wx", 0o600);
161
+ try {
162
+ writeFileSync(fd, mine);
163
+ }
164
+ finally {
165
+ closeSync(fd);
166
+ }
167
+ }
168
+ catch {
169
+ return null;
170
+ }
171
+ try {
172
+ linkSync(staging, path);
173
+ }
174
+ catch {
175
+ // Somebody else holds it. Ours never became visible.
176
+ return null;
177
+ }
178
+ finally {
179
+ try {
180
+ unlinkSync(staging);
181
+ }
182
+ catch {
183
+ // The lock path keeps the inode alive; a stranded staging file is inert.
184
+ }
185
+ }
186
+ // Returned so the RELEASE can prove the lock it deletes is still ours.
187
+ return mine;
188
+ }
189
+ /**
190
+ * Reclaim a lock whose holder is gone.
191
+ *
192
+ * Compare-then-delete: the same bytes we saw when we judged it stale must still
193
+ * be there when we unlink. Without that a third process that acquired the lock
194
+ * in between would have it deleted out from under it.
195
+ *
196
+ * ⚠ Residual, stated rather than papered over: the compare and the unlink are
197
+ * not one atomic operation, so a sufficiently unlucky interleaving can still
198
+ * drop a fresh lock. Reaching it requires a process to crash mid-refresh, a
199
+ * second to wait out the full `STALE_MS`, and a third to acquire inside the
200
+ * microseconds between the re-read and the unlink. The cost of closing it
201
+ * properly is a directory-rename protocol, and this package's failure mode on
202
+ * that path is one avoidable re-sign-in, not data loss.
203
+ */
204
+ function reclaimIfUnchanged(path, observed) {
205
+ try {
206
+ if (readFileSync(path, "utf8") !== observed)
207
+ return;
208
+ unlinkSync(path);
209
+ }
210
+ catch {
211
+ // Gone already, or not ours to remove. Either way the next acquire decides.
212
+ }
213
+ }
214
+ /**
215
+ * Release, but ONLY if the lock on disk is still the one we took.
216
+ *
217
+ * ⚠ `/cso` finding, HIGH, and it is the family revoke arriving by the back door.
218
+ * An unconditional `unlink` on release is wrong whenever a holder outlived
219
+ * `STALE_MS`: a waiter reclaims the stale lock and starts its own refresh, and
220
+ * then the original holder wakes up and deletes THAT lock — letting a third
221
+ * process in while the second is mid-rotation. Two live refreshes is exactly the
222
+ * replay the server reads as `reuse_detected`.
223
+ *
224
+ * The obvious objection is that a healthy holder cannot exceed `STALE_MS`,
225
+ * because its one request is bounded by an `AbortController` well inside it.
226
+ * True — and irrelevant. **A laptop that sleeps mid-refresh does exceed it**, and
227
+ * so does any process the scheduler or a debugger suspends. Wall-clock bounds
228
+ * hold for code, not for the machine underneath it.
229
+ *
230
+ * Same compare-then-delete as the reclaim above, and the same stated residual:
231
+ * the read and the unlink are not one atomic operation.
232
+ */
233
+ function releaseIfMine(path, mine) {
234
+ try {
235
+ if (readFileSync(path, "utf8") !== mine)
236
+ return;
237
+ unlinkSync(path);
238
+ }
239
+ catch {
240
+ // Already gone. Nothing to undo.
241
+ }
242
+ }
243
+ /**
244
+ * Run `fn` with the refresh lock held.
245
+ *
246
+ * Returns `{ kind: "timeout" }` rather than throwing when the lock cannot be
247
+ * taken: the caller is a read verb, and a read that cannot refresh should tell
248
+ * the user what happened, not take an exception out of a lock file.
249
+ *
250
+ * The lock is released in a `finally`, so a throwing callback still frees it.
251
+ */
252
+ export async function withRefreshLock(home, fn, now = Date.now) {
253
+ const path = refreshLockPath(home);
254
+ const deadline = now() + WAIT_TIMEOUT_MS;
255
+ let mine = tryAcquire(path);
256
+ while (mine === null) {
257
+ let raw;
258
+ try {
259
+ raw = readFileSync(path, "utf8");
260
+ }
261
+ catch {
262
+ // Released between our failed acquire and this read. Go straight round.
263
+ mine = tryAcquire(path);
264
+ continue;
265
+ }
266
+ const since = heldSince(raw);
267
+ // An unparseable or timestamp-less lock is treated as stale immediately: it
268
+ // was not written by this code, and waiting `STALE_MS` for a file nobody can
269
+ // date is waiting for nothing.
270
+ if (since === null || now() - since > STALE_MS) {
271
+ reclaimIfUnchanged(path, raw);
272
+ mine = tryAcquire(path);
273
+ continue;
274
+ }
275
+ if (now() >= deadline)
276
+ return { kind: "timeout" };
277
+ await sleep(POLL_MS);
278
+ mine = tryAcquire(path);
279
+ }
280
+ try {
281
+ return { kind: "ok", value: await fn() };
282
+ }
283
+ finally {
284
+ releaseIfMine(path, mine);
285
+ }
286
+ }
287
+ //# sourceMappingURL=lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/oauth/lock.ts"],"names":[],"mappings":"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"}