@vibe-commit/capture 0.7.0 → 0.9.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 (152) hide show
  1. package/README.md +238 -7
  2. package/dist/account_not_here.d.ts +15 -0
  3. package/dist/account_not_here.js +25 -0
  4. package/dist/account_not_here.js.map +1 -0
  5. package/dist/agents/claude_code.js +13 -0
  6. package/dist/agents/claude_code.js.map +1 -1
  7. package/dist/agents/codex_cli.js +11 -0
  8. package/dist/agents/codex_cli.js.map +1 -1
  9. package/dist/agents/cursor.js +7 -1
  10. package/dist/agents/cursor.js.map +1 -1
  11. package/dist/agents/registry.d.ts +7 -0
  12. package/dist/agents/registry.js +9 -0
  13. package/dist/agents/registry.js.map +1 -1
  14. package/dist/agents/types.d.ts +32 -0
  15. package/dist/agents/types.js.map +1 -1
  16. package/dist/bounded_id.d.ts +29 -0
  17. package/dist/bounded_id.js +42 -0
  18. package/dist/bounded_id.js.map +1 -0
  19. package/dist/codex_folders.d.ts +340 -0
  20. package/dist/codex_folders.js +1448 -0
  21. package/dist/codex_folders.js.map +1 -0
  22. package/dist/commands/auth.d.ts +7 -0
  23. package/dist/commands/auth.js +21 -0
  24. package/dist/commands/auth.js.map +1 -1
  25. package/dist/commands/backfill.d.ts +29 -7
  26. package/dist/commands/backfill.js +387 -109
  27. package/dist/commands/backfill.js.map +1 -1
  28. package/dist/commands/browser_credential.d.ts +4 -2
  29. package/dist/commands/browser_credential.js +5 -2
  30. package/dist/commands/browser_credential.js.map +1 -1
  31. package/dist/commands/connect.d.ts +10 -1
  32. package/dist/commands/connect.js +14 -9
  33. package/dist/commands/connect.js.map +1 -1
  34. package/dist/commands/connect_account.d.ts +135 -0
  35. package/dist/commands/connect_account.js +321 -0
  36. package/dist/commands/connect_account.js.map +1 -0
  37. package/dist/commands/connect_picker.js +204 -29
  38. package/dist/commands/connect_picker.js.map +1 -1
  39. package/dist/commands/device_credential.d.ts +3 -1
  40. package/dist/commands/device_credential.js +1 -0
  41. package/dist/commands/device_credential.js.map +1 -1
  42. package/dist/commands/finalize.d.ts +67 -0
  43. package/dist/commands/finalize.js +450 -114
  44. package/dist/commands/finalize.js.map +1 -1
  45. package/dist/commands/finish_mint.d.ts +28 -0
  46. package/dist/commands/finish_mint.js +16 -4
  47. package/dist/commands/finish_mint.js.map +1 -1
  48. package/dist/commands/scan.d.ts +28 -0
  49. package/dist/commands/scan.js +11 -0
  50. package/dist/commands/scan.js.map +1 -1
  51. package/dist/commands/status.d.ts +19 -1
  52. package/dist/commands/status.js +123 -14
  53. package/dist/commands/status.js.map +1 -1
  54. package/dist/commands/status_machine.d.ts +12 -3
  55. package/dist/commands/status_machine.js +111 -13
  56. package/dist/commands/status_machine.js.map +1 -1
  57. package/dist/commands/sync_reason.d.ts +17 -0
  58. package/dist/commands/sync_reason.js +25 -0
  59. package/dist/commands/sync_reason.js.map +1 -0
  60. package/dist/consent.d.ts +131 -3
  61. package/dist/consent.js +248 -14
  62. package/dist/consent.js.map +1 -1
  63. package/dist/copy/strings.d.ts +165 -4
  64. package/dist/copy/strings.js +190 -7
  65. package/dist/copy/strings.js.map +1 -1
  66. package/dist/folder_grants.d.ts +44 -0
  67. package/dist/folder_grants.js +101 -0
  68. package/dist/folder_grants.js.map +1 -0
  69. package/dist/git.d.ts +26 -4
  70. package/dist/git.js +82 -11
  71. package/dist/git.js.map +1 -1
  72. package/dist/grants_sync.d.ts +165 -0
  73. package/dist/grants_sync.js +289 -0
  74. package/dist/grants_sync.js.map +1 -0
  75. package/dist/grants_verb.d.ts +70 -0
  76. package/dist/grants_verb.js +121 -0
  77. package/dist/grants_verb.js.map +1 -0
  78. package/dist/hooks/entry.d.ts +30 -50
  79. package/dist/hooks/entry.js +601 -561
  80. package/dist/hooks/entry.js.map +1 -1
  81. package/dist/hooks/fire_lock.d.ts +31 -0
  82. package/dist/hooks/fire_lock.js +120 -12
  83. package/dist/hooks/fire_lock.js.map +1 -1
  84. package/dist/hooks/session_grants.d.ts +59 -0
  85. package/dist/hooks/session_grants.js +91 -0
  86. package/dist/hooks/session_grants.js.map +1 -0
  87. package/dist/hooks/session_start.d.ts +28 -7
  88. package/dist/hooks/session_start.js +55 -11
  89. package/dist/hooks/session_start.js.map +1 -1
  90. package/dist/index.js +22 -5
  91. package/dist/index.js.map +1 -1
  92. package/dist/machine_state.d.ts +44 -1
  93. package/dist/machine_state.js +75 -12
  94. package/dist/machine_state.js.map +1 -1
  95. package/dist/oauth/discovery.js +2 -1
  96. package/dist/oauth/discovery.js.map +1 -1
  97. package/dist/oauth/pkce.js +2 -1
  98. package/dist/oauth/pkce.js.map +1 -1
  99. package/dist/oauth/retire.d.ts +10 -0
  100. package/dist/oauth/retire.js +13 -0
  101. package/dist/oauth/retire.js.map +1 -1
  102. package/dist/paths.d.ts +54 -1
  103. package/dist/paths.js +88 -12
  104. package/dist/paths.js.map +1 -1
  105. package/dist/pending_revoke.d.ts +117 -0
  106. package/dist/pending_revoke.js +233 -0
  107. package/dist/pending_revoke.js.map +1 -0
  108. package/dist/pending_revoke_file.d.ts +55 -0
  109. package/dist/pending_revoke_file.js +74 -0
  110. package/dist/pending_revoke_file.js.map +1 -0
  111. package/dist/physical_path.d.ts +46 -0
  112. package/dist/physical_path.js +151 -0
  113. package/dist/physical_path.js.map +1 -0
  114. package/dist/picker.d.ts +82 -5
  115. package/dist/picker.js +183 -10
  116. package/dist/picker.js.map +1 -1
  117. package/dist/picker_checklist.d.ts +27 -3
  118. package/dist/picker_checklist.js +81 -6
  119. package/dist/picker_checklist.js.map +1 -1
  120. package/dist/policy.d.ts +24 -1
  121. package/dist/policy.js +30 -2
  122. package/dist/policy.js.map +1 -1
  123. package/dist/post.d.ts +55 -2
  124. package/dist/post.js +72 -22
  125. package/dist/post.js.map +1 -1
  126. package/dist/project.d.ts +89 -0
  127. package/dist/project.js +123 -0
  128. package/dist/project.js.map +1 -1
  129. package/dist/redact.d.ts +77 -2
  130. package/dist/redact.js +713 -47
  131. package/dist/redact.js.map +1 -1
  132. package/dist/session_targets.d.ts +713 -0
  133. package/dist/session_targets.js +2602 -0
  134. package/dist/session_targets.js.map +1 -0
  135. package/dist/spool.d.ts +12 -0
  136. package/dist/spool.js +29 -1
  137. package/dist/spool.js.map +1 -1
  138. package/dist/spool_union.d.ts +51 -0
  139. package/dist/spool_union.js +57 -0
  140. package/dist/spool_union.js.map +1 -0
  141. package/dist/state.d.ts +5 -0
  142. package/dist/state.js +19 -1
  143. package/dist/state.js.map +1 -1
  144. package/dist/system_message.d.ts +25 -2
  145. package/dist/system_message.js +29 -5
  146. package/dist/system_message.js.map +1 -1
  147. package/dist/text_tail.d.ts +8 -0
  148. package/dist/text_tail.js +19 -0
  149. package/dist/text_tail.js.map +1 -0
  150. package/dist/version.d.ts +1 -1
  151. package/dist/version.js +1 -1
  152. package/package.json +3 -3
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The account-grants client — `POST /ingest/v1/grants/sync` and `GET /ingest/v1/grants` (slice 2, `CR-259`, S4).
3
+ *
4
+ * ```
5
+ * POST <ingest origin>/ingest/v1/grants/sync Authorization: Bearer vcik_…
6
+ * { "schema": 1, "ops": [{ "op": "grant"|"revoke"|"exclude"|"include", "kind": "owner"|"repo", "value": "<normalized>" }],
7
+ * "named": { "owners": [..], "repos": [..] } } 64 KiB, 500 ops; `exclude`/`include` take kind "repo"
8
+ * 200 { "schema": 1, "epoch", "owners", "repos", "excluded", "unsynced_machines" } + X-Grant-Epoch
9
+ * 400 { "error": "invalid_grants_sync" } nothing applied
10
+ * 401 the credential, not the grants
11
+ *
12
+ * GET <ingest origin>/ingest/v1/grants a pure read — it does not mark the credential as synced
13
+ * 200 { "schema": 1, "epoch", "owners", "repos", "excluded" } + X-Grant-Epoch
14
+ * ```
15
+ *
16
+ * The contract is `CR-259`'s (`slice2-build-brief-20261024.md` §1.2), which the server side (`cr259b`) is built to as well: this file
17
+ * was written from that brief and not from any closed source, so it declares no sibling-repo provenance.
18
+ *
19
+ * ## What this file is, and is not, in the PR that adds it
20
+ *
21
+ * Primitives. No verb calls them yet: `connect`, `status` and `off` sync at their start or end, and SessionStart does the one
22
+ * bounded GET, but each of those prints something new and waits on the 0.7.0 error-code copy (PR 7). They are tested here
23
+ * through injected deps, and listed in `dead-symbols.test.ts` as unreferenced until those verbs land.
24
+ *
25
+ * ## Who writes what
26
+ *
27
+ * - `syncGrants` is a VERB's: it may write `grants.json` (to clear what it delivered from `pending`) because a verb is
28
+ * the only kind of caller that may. Hooks never sync, and never write `grants.json` (`consent.ts`).
29
+ * - `fetchAccountGrants` writes NOTHING. SessionStart (a hook) is its caller, and what it learned goes to `grants-account.json`.
30
+ *
31
+ * ## Failure is silent, and a failed sync loses nothing
32
+ *
33
+ * Every function here is bounded (3 s) and NEVER THROWS. A sync that fails for any reason leaves `pending` where it was, so the
34
+ * next verb sends it again — the server applies every op idempotently. The caller says "not yet sent" (`status`); this file
35
+ * says nothing.
36
+ */
37
+ import { type Grants, type PendingOp } from "./consent.js";
38
+ import type { IngestCredential } from "./credential.js";
39
+ export declare const GRANTS_PATH = "/ingest/v1/grants";
40
+ export declare const GRANTS_SYNC_PATH = "/ingest/v1/grants/sync";
41
+ /** Brief §2: each sync is bounded at 3 s IN TOTAL (one deadline shared by its chunks), so a verb never feels hung on it. The same bound serves the GET. */
42
+ export declare const GRANTS_TIMEOUT_MS = 3000;
43
+ /** The server's caps on one sync request (brief §1.2). A request over either is never sent. */
44
+ export declare const SYNC_MAX_OPS = 500;
45
+ export declare const SYNC_MAX_BYTES: number;
46
+ /** The account's grants as the server answered, normalized. */
47
+ export interface AccountSnapshot {
48
+ readonly epoch: number;
49
+ readonly owners: readonly string[];
50
+ readonly repos: readonly string[];
51
+ readonly excluded: readonly string[];
52
+ }
53
+ export type AccountGrantsRead = ({
54
+ readonly kind: "ok";
55
+ } & AccountSnapshot)
56
+ /** 401, and only 401: the credential is not accepted. */
57
+ | {
58
+ readonly kind: "unauthorized";
59
+ }
60
+ /** Everything else: a 5xx, a 404 from a server that predates the route, a timeout, a stalled or oversize body, off-contract JSON. */
61
+ | {
62
+ readonly kind: "unavailable";
63
+ };
64
+ /** What a CLI verb (`connect`, `status`, `off`) passes as `maxBodyBytes`: a verb is not a hook, so eng-2 D2's 64 KiB rule does not bind it. */
65
+ export declare const GRANTS_VERB_BODY_BYTES: number;
66
+ export interface GrantsDeps {
67
+ readonly fetch: typeof fetch;
68
+ readonly timeoutMs?: number;
69
+ /**
70
+ * The most of a response body that is read. **The default is the hook path's 64 KiB** (`readCappedJson`, eng-2 D2): SessionStart's
71
+ * GET must not buffer more. A CLI verb passes `GRANTS_VERB_BODY_BYTES`. An answer over the cap is `unavailable`.
72
+ */
73
+ readonly maxBodyBytes?: number;
74
+ }
75
+ export interface SyncDeps extends GrantsDeps {
76
+ readonly home: string;
77
+ readonly now: () => Date;
78
+ }
79
+ /**
80
+ * `GET /ingest/v1/grants`. A pure read: it changes nothing on the server and writes nothing here. Read through the one capped,
81
+ * signal-raced body reader (`post.ts`), so a 64 KiB cap and the time bound are the same ones the hook's other body reads have; a
82
+ * body that stalls, is cut off, is over the cap or is not JSON is `unavailable`, and the caller's epoch stays as stale as it was.
83
+ */
84
+ export declare function fetchAccountGrants(ingestUrl: string, credential: IngestCredential, deps: GrantsDeps): Promise<AccountGrantsRead>;
85
+ /** One op as the server takes it. */
86
+ export interface SyncOp {
87
+ readonly op: PendingOp["op"];
88
+ readonly kind: PendingOp["kind"];
89
+ readonly value: string;
90
+ }
91
+ /** What one sync sends: the ops in order, and the full set this machine has named. */
92
+ export interface SyncPlan {
93
+ readonly ops: readonly SyncOp[];
94
+ readonly named: {
95
+ readonly owners: readonly string[];
96
+ readonly repos: readonly string[];
97
+ };
98
+ }
99
+ /**
100
+ * What to send. `named` is everything THIS machine has named: its `owners`, and its `repos` plus the slug of every clone it holds
101
+ * a per-clone consent for (`consentedSlugs`, which the caller resolves — it needs git) — minus anything the user turned off
102
+ * here, since a turned-off repo is not named. Local-only (`local:`) slugs are never sent: they name a path, and the server has no
103
+ * grant for one.
104
+ *
105
+ * ⭐ **THE FIRST SYNC** (`account === null`: nothing has ever come back from the server) also sends a `grant` for each of those,
106
+ * so per-clone consents made under 0.6.x/0.7.0 reach the account (R3-21). They go BEFORE the pending ops, so a later user
107
+ * intent (a queued `revoke` of the same repo) is the one that stands. After the first sync only `pending` rides.
108
+ */
109
+ export declare function planSync(grants: Grants, consentedSlugs: readonly string[], firstSync: boolean): SyncPlan;
110
+ export type SyncResult = {
111
+ readonly kind: "synced";
112
+ /** How many of the user's OTHER machines have not confirmed the latest grants (for `off`'s second line). */
113
+ readonly unsyncedMachines: number;
114
+ readonly account: AccountSnapshot;
115
+ }
116
+ /** 401: the credential is not accepted. The caller's credential beat owns that; this only says the sync did not happen. */
117
+ | {
118
+ readonly kind: "unauthorized";
119
+ }
120
+ /**
121
+ * 400: the server applied nothing. Our own bug — the queue is kept (dropping it would lose the user's intent) and will be refused
122
+ * again. `pendingRejected` is how many QUEUED ops were in the request that was refused (the first-sync grants derived from
123
+ * `grants.json` are not counted), so a verb can say "N changes your account didn't accept" and log the 400 once per run.
124
+ */
125
+ | {
126
+ readonly kind: "rejected";
127
+ readonly pendingRejected: number;
128
+ }
129
+ /** The request would exceed the server's caps, so none was sent. `pending` is untouched. */
130
+ | {
131
+ readonly kind: "too-large";
132
+ }
133
+ /** `grants.json` is present and unreadable: `named` and `pending` cannot be known, so nothing is sent. */
134
+ | {
135
+ readonly kind: "unreadable";
136
+ }
137
+ /** No answer, or an answer off-contract, or it could not be recorded here. `pending` is untouched. */
138
+ | {
139
+ readonly kind: "unavailable";
140
+ };
141
+ /**
142
+ * One sync. Reads `grants.json`, sends its `pending` queue (and, the first time, what this machine already named), and on a 200
143
+ * records the answer:
144
+ * - `grants-account.json` gets the account state, with `refused` and `stale` cleared (the server has just said what it thinks);
145
+ * - the ops that were sent leave `pending` — exactly those, matched by value, so an op queued while the request was in flight
146
+ * stays.
147
+ * Anything else leaves both files as they were.
148
+ *
149
+ * **NEVER THROWS**, and it is bounded by `timeoutMs` per request (3 s). `consentedSlugs` is the caller's: the slug of every clone
150
+ * with a per-clone consent, because only the caller can afford the git spawns to know them.
151
+ */
152
+ export declare function syncGrants(ingestUrl: string, credential: IngestCredential, consentedSlugs: readonly string[], deps: SyncDeps): Promise<SyncResult>;
153
+ /**
154
+ * The queue after `ops` are added to it. ⛔ **A later op REPLACES an earlier one on the same axis, kind and value, and is then
155
+ * appended** — only the last word on each axis matters, and keeping the first (the old "already queued, skip it") turned an
156
+ * offline grant, stop, grant into `[grant, revoke]`: the server ended revoked while this machine named the repo, and every upload
157
+ * was refused for ever. The two axes are independent, so `grant X` and `exclude X` both stay, in the order they were queued.
158
+ * Pure: `connect`'s grant write uses it inside its own `updateGrants`, so the grants and the queue land in ONE write.
159
+ */
160
+ export declare function mergePending(pending: readonly PendingOp[], ops: readonly SyncOp[], now: Date): PendingOp[];
161
+ /**
162
+ * Queue writes for the next sync (a verb's, never a hook's: it goes through `updateGrants`). See `mergePending` for the rule.
163
+ * Returns false — and writes nothing — if `grants.json` is unreadable.
164
+ */
165
+ export declare function queueGrantOps(home: string, ops: readonly SyncOp[], now: Date): boolean;
@@ -0,0 +1,289 @@
1
+ /**
2
+ * The account-grants client — `POST /ingest/v1/grants/sync` and `GET /ingest/v1/grants` (slice 2, `CR-259`, S4).
3
+ *
4
+ * ```
5
+ * POST <ingest origin>/ingest/v1/grants/sync Authorization: Bearer vcik_…
6
+ * { "schema": 1, "ops": [{ "op": "grant"|"revoke"|"exclude"|"include", "kind": "owner"|"repo", "value": "<normalized>" }],
7
+ * "named": { "owners": [..], "repos": [..] } } 64 KiB, 500 ops; `exclude`/`include` take kind "repo"
8
+ * 200 { "schema": 1, "epoch", "owners", "repos", "excluded", "unsynced_machines" } + X-Grant-Epoch
9
+ * 400 { "error": "invalid_grants_sync" } nothing applied
10
+ * 401 the credential, not the grants
11
+ *
12
+ * GET <ingest origin>/ingest/v1/grants a pure read — it does not mark the credential as synced
13
+ * 200 { "schema": 1, "epoch", "owners", "repos", "excluded" } + X-Grant-Epoch
14
+ * ```
15
+ *
16
+ * The contract is `CR-259`'s (`slice2-build-brief-20261024.md` §1.2), which the server side (`cr259b`) is built to as well: this file
17
+ * was written from that brief and not from any closed source, so it declares no sibling-repo provenance.
18
+ *
19
+ * ## What this file is, and is not, in the PR that adds it
20
+ *
21
+ * Primitives. No verb calls them yet: `connect`, `status` and `off` sync at their start or end, and SessionStart does the one
22
+ * bounded GET, but each of those prints something new and waits on the 0.7.0 error-code copy (PR 7). They are tested here
23
+ * through injected deps, and listed in `dead-symbols.test.ts` as unreferenced until those verbs land.
24
+ *
25
+ * ## Who writes what
26
+ *
27
+ * - `syncGrants` is a VERB's: it may write `grants.json` (to clear what it delivered from `pending`) because a verb is
28
+ * the only kind of caller that may. Hooks never sync, and never write `grants.json` (`consent.ts`).
29
+ * - `fetchAccountGrants` writes NOTHING. SessionStart (a hook) is its caller, and what it learned goes to `grants-account.json`.
30
+ *
31
+ * ## Failure is silent, and a failed sync loses nothing
32
+ *
33
+ * Every function here is bounded (3 s) and NEVER THROWS. A sync that fails for any reason leaves `pending` where it was, so the
34
+ * next verb sends it again — the server applies every op idempotently. The caller says "not yet sent" (`status`); this file
35
+ * says nothing.
36
+ */
37
+ import { EMPTY_GRANTS, GrantsUnreadableError, isLocalSlug, normalizeOwner, normalizeSlug, readAccountState, readGrants, updateGrants, writeAccountState, } from "./consent.js";
38
+ import { MAX_ERROR_BODY_BYTES, readCappedJson } from "./post.js";
39
+ import { CLIENT_VERSION, CLIENT_VERSION_HEADER } from "./version.js";
40
+ export const GRANTS_PATH = "/ingest/v1/grants";
41
+ export const GRANTS_SYNC_PATH = "/ingest/v1/grants/sync";
42
+ /** Brief §2: each sync is bounded at 3 s IN TOTAL (one deadline shared by its chunks), so a verb never feels hung on it. The same bound serves the GET. */
43
+ export const GRANTS_TIMEOUT_MS = 3_000;
44
+ /** The server's caps on one sync request (brief §1.2). A request over either is never sent. */
45
+ export const SYNC_MAX_OPS = 500;
46
+ export const SYNC_MAX_BYTES = 64 * 1024;
47
+ /** What a CLI verb (`connect`, `status`, `off`) passes as `maxBodyBytes`: a verb is not a hook, so eng-2 D2's 64 KiB rule does not bind it. */
48
+ export const GRANTS_VERB_BODY_BYTES = 1024 * 1024;
49
+ function grantsUrl(ingestUrl, path) {
50
+ return new URL(path, ingestUrl).toString();
51
+ }
52
+ function headersFor(credential, json) {
53
+ return {
54
+ // The request boundary — a credential's plaintext is used here, in `post.ts` and in `oauth/whoami.ts`.
55
+ authorization: `Bearer ${credential.expose()}`,
56
+ accept: "application/json",
57
+ ...(json ? { "content-type": "application/json" } : {}),
58
+ [CLIENT_VERSION_HEADER.toLowerCase()]: CLIENT_VERSION,
59
+ };
60
+ }
61
+ function normalizedList(value, normalize) {
62
+ if (!Array.isArray(value))
63
+ return null;
64
+ const out = [];
65
+ for (const item of value) {
66
+ if (typeof item !== "string")
67
+ return null;
68
+ const n = normalize(item);
69
+ if (n !== "" && !out.includes(n))
70
+ out.push(n);
71
+ }
72
+ return out;
73
+ }
74
+ /** The account state in a sync or GET body, or `null` if it is not what the contract says. Schema 1 only. */
75
+ function parseSnapshot(body) {
76
+ if (body === null || typeof body !== "object" || Array.isArray(body))
77
+ return null;
78
+ const o = body;
79
+ if (o.schema !== 1)
80
+ return null;
81
+ if (typeof o.epoch !== "number" || !Number.isSafeInteger(o.epoch) || o.epoch < 0)
82
+ return null;
83
+ const owners = normalizedList(o.owners, normalizeOwner);
84
+ const repos = normalizedList(o.repos, normalizeSlug);
85
+ const excluded = normalizedList(o.excluded, normalizeSlug);
86
+ if (owners === null || repos === null || excluded === null)
87
+ return null;
88
+ return { epoch: o.epoch, owners, repos, excluded };
89
+ }
90
+ /**
91
+ * `GET /ingest/v1/grants`. A pure read: it changes nothing on the server and writes nothing here. Read through the one capped,
92
+ * signal-raced body reader (`post.ts`), so a 64 KiB cap and the time bound are the same ones the hook's other body reads have; a
93
+ * body that stalls, is cut off, is over the cap or is not JSON is `unavailable`, and the caller's epoch stays as stale as it was.
94
+ */
95
+ export async function fetchAccountGrants(ingestUrl, credential, deps) {
96
+ const controller = new AbortController();
97
+ const timer = setTimeout(() => controller.abort(), deps.timeoutMs ?? GRANTS_TIMEOUT_MS);
98
+ try {
99
+ const res = await deps.fetch(grantsUrl(ingestUrl, GRANTS_PATH), {
100
+ method: "GET",
101
+ headers: headersFor(credential, false),
102
+ signal: controller.signal,
103
+ redirect: "error",
104
+ });
105
+ if (res.status === 401)
106
+ return { kind: "unauthorized" };
107
+ if (res.status !== 200)
108
+ return { kind: "unavailable" };
109
+ const snapshot = parseSnapshot(await readCappedJson(res, controller.signal, deps.maxBodyBytes ?? MAX_ERROR_BODY_BYTES));
110
+ return snapshot === null ? { kind: "unavailable" } : { kind: "ok", ...snapshot };
111
+ }
112
+ catch {
113
+ return { kind: "unavailable" };
114
+ }
115
+ finally {
116
+ clearTimeout(timer);
117
+ }
118
+ }
119
+ const wire = (p) => ({ op: p.op, kind: p.kind, value: p.value });
120
+ /**
121
+ * What to send. `named` is everything THIS machine has named: its `owners`, and its `repos` plus the slug of every clone it holds
122
+ * a per-clone consent for (`consentedSlugs`, which the caller resolves — it needs git) — minus anything the user turned off
123
+ * here, since a turned-off repo is not named. Local-only (`local:`) slugs are never sent: they name a path, and the server has no
124
+ * grant for one.
125
+ *
126
+ * ⭐ **THE FIRST SYNC** (`account === null`: nothing has ever come back from the server) also sends a `grant` for each of those,
127
+ * so per-clone consents made under 0.6.x/0.7.0 reach the account (R3-21). They go BEFORE the pending ops, so a later user
128
+ * intent (a queued `revoke` of the same repo) is the one that stands. After the first sync only `pending` rides.
129
+ */
130
+ export function planSync(grants, consentedSlugs, firstSync) {
131
+ const off = new Set(grants.excluded);
132
+ const owners = [...new Set(grants.owners.map(normalizeOwner))].filter((o) => o !== "");
133
+ const repos = [...new Set([...grants.repos, ...consentedSlugs].map(normalizeSlug))].filter((r) => r !== "" && !isLocalSlug(r) && !off.has(r));
134
+ const derived = firstSync
135
+ ? [
136
+ ...owners.map((value) => ({ op: "grant", kind: "owner", value })),
137
+ ...repos.map((value) => ({ op: "grant", kind: "repo", value })),
138
+ ]
139
+ : [];
140
+ return { ops: [...derived, ...grants.pending.map(wire)], named: { owners, repos } };
141
+ }
142
+ /** Cut `ops` into requests of at most 500, each carrying the whole `named` set (the server replaces it each time). */
143
+ function requestBodies(plan) {
144
+ const bodies = [];
145
+ const chunks = [];
146
+ for (let i = 0; i < plan.ops.length; i += SYNC_MAX_OPS)
147
+ chunks.push(plan.ops.slice(i, i + SYNC_MAX_OPS));
148
+ if (chunks.length === 0)
149
+ chunks.push([]);
150
+ for (const ops of chunks) {
151
+ const body = JSON.stringify({ schema: 1, ops, named: plan.named });
152
+ if (Buffer.byteLength(body, "utf8") > SYNC_MAX_BYTES)
153
+ return null;
154
+ bodies.push(body);
155
+ }
156
+ return bodies;
157
+ }
158
+ /**
159
+ * One sync. Reads `grants.json`, sends its `pending` queue (and, the first time, what this machine already named), and on a 200
160
+ * records the answer:
161
+ * - `grants-account.json` gets the account state, with `refused` and `stale` cleared (the server has just said what it thinks);
162
+ * - the ops that were sent leave `pending` — exactly those, matched by value, so an op queued while the request was in flight
163
+ * stays.
164
+ * Anything else leaves both files as they were.
165
+ *
166
+ * **NEVER THROWS**, and it is bounded by `timeoutMs` per request (3 s). `consentedSlugs` is the caller's: the slug of every clone
167
+ * with a per-clone consent, because only the caller can afford the git spawns to know them.
168
+ */
169
+ export async function syncGrants(ingestUrl, credential, consentedSlugs, deps) {
170
+ try {
171
+ const read = readGrants(deps.home);
172
+ if (read.kind === "unreadable")
173
+ return { kind: "unreadable" };
174
+ const grants = read.kind === "ok" ? read.grants : EMPTY_GRANTS;
175
+ // ⛔ "First" is decided by grants.json's own `synced_at` and never by the account file, which reads as empty when it is corrupt
176
+ // or gone — and a re-sent first sync would re-grant whatever was revoked on another machine since.
177
+ const plan = planSync(grants, consentedSlugs, grants.synced_at === null);
178
+ const bodies = requestBodies(plan);
179
+ if (bodies === null)
180
+ return { kind: "too-large" };
181
+ const derivedCount = plan.ops.length - grants.pending.length;
182
+ let last = null;
183
+ // ⛔ ONE deadline for the whole sync, shared by every chunk: a verb "never feels hung on it" (brief §2) however many
184
+ // requests more than 500 ops make. A sync that runs out of time is `unavailable`, `pending` untouched, and the next verb
185
+ // sends it again (every op is idempotent on the server).
186
+ const deadline = Date.now() + (deps.timeoutMs ?? GRANTS_TIMEOUT_MS);
187
+ for (const [index, body] of bodies.entries()) {
188
+ const remaining = deadline - Date.now();
189
+ if (remaining <= 0)
190
+ return { kind: "unavailable" };
191
+ const controller = new AbortController();
192
+ const timer = setTimeout(() => controller.abort(), remaining);
193
+ try {
194
+ const res = await deps.fetch(grantsUrl(ingestUrl, GRANTS_SYNC_PATH), {
195
+ method: "POST",
196
+ headers: headersFor(credential, true),
197
+ body,
198
+ signal: controller.signal,
199
+ redirect: "error",
200
+ });
201
+ if (res.status === 401)
202
+ return { kind: "unauthorized" };
203
+ if (res.status === 400) {
204
+ const from = index * SYNC_MAX_OPS;
205
+ const to = Math.min(plan.ops.length, from + SYNC_MAX_OPS);
206
+ return { kind: "rejected", pendingRejected: Math.max(0, to - Math.max(from, derivedCount)) };
207
+ }
208
+ if (res.status !== 200)
209
+ return { kind: "unavailable" };
210
+ const json = await readCappedJson(res, controller.signal, deps.maxBodyBytes ?? MAX_ERROR_BODY_BYTES);
211
+ const snapshot = parseSnapshot(json);
212
+ const unsynced = json?.unsynced_machines;
213
+ if (snapshot === null || typeof unsynced !== "number" || !Number.isSafeInteger(unsynced) || unsynced < 0) {
214
+ return { kind: "unavailable" };
215
+ }
216
+ last = { ...snapshot, unsynced };
217
+ }
218
+ finally {
219
+ clearTimeout(timer);
220
+ }
221
+ }
222
+ if (last === null)
223
+ return { kind: "unavailable" };
224
+ // The server has it. Record that: pending first (a failure there leaves the queue to be re-sent, which is idempotent),
225
+ // then the account file.
226
+ const sent = new Set(grants.pending.map((p) => `${p.op}\n${p.kind}\n${p.value}\n${p.at}`));
227
+ const stamp = deps.now().toISOString();
228
+ if (sent.size > 0 || grants.synced_at === null) {
229
+ try {
230
+ updateGrants(deps.home, (g) => ({
231
+ ...g,
232
+ pending: g.pending.filter((p) => !sent.has(`${p.op}\n${p.kind}\n${p.value}\n${p.at}`)),
233
+ // Once, on the first 200, in the same write that clears what was delivered.
234
+ synced_at: g.synced_at ?? stamp,
235
+ }));
236
+ }
237
+ catch (error) {
238
+ if (error instanceof GrantsUnreadableError)
239
+ return { kind: "unavailable" };
240
+ throw error;
241
+ }
242
+ }
243
+ const { unsynced, ...account } = last;
244
+ writeAccountState(deps.home, {
245
+ account: { owners: account.owners, repos: account.repos, excluded: account.excluded, epoch: account.epoch, synced_at: stamp },
246
+ refused: [],
247
+ stale: false,
248
+ last_get_at: readAccountState(deps.home).last_get_at,
249
+ });
250
+ return { kind: "synced", unsyncedMachines: unsynced, account };
251
+ }
252
+ catch {
253
+ return { kind: "unavailable" };
254
+ }
255
+ }
256
+ /** `grant` / `revoke` are one axis (is the repo granted?) and `exclude` / `include` another (is it turned off?). */
257
+ const axisOf = (op) => (op === "grant" || op === "revoke" ? "grant" : "exclude");
258
+ /**
259
+ * The queue after `ops` are added to it. ⛔ **A later op REPLACES an earlier one on the same axis, kind and value, and is then
260
+ * appended** — only the last word on each axis matters, and keeping the first (the old "already queued, skip it") turned an
261
+ * offline grant, stop, grant into `[grant, revoke]`: the server ended revoked while this machine named the repo, and every upload
262
+ * was refused for ever. The two axes are independent, so `grant X` and `exclude X` both stay, in the order they were queued.
263
+ * Pure: `connect`'s grant write uses it inside its own `updateGrants`, so the grants and the queue land in ONE write.
264
+ */
265
+ export function mergePending(pending, ops, now) {
266
+ let queued = [...pending];
267
+ for (const op of ops) {
268
+ const value = op.kind === "owner" ? normalizeOwner(op.value) : normalizeSlug(op.value);
269
+ if (value === "")
270
+ continue;
271
+ queued = queued.filter((p) => !(axisOf(p.op) === axisOf(op.op) && p.kind === op.kind && p.value === value));
272
+ queued.push({ op: op.op, kind: op.kind, value, at: now.toISOString() });
273
+ }
274
+ return queued;
275
+ }
276
+ /**
277
+ * Queue writes for the next sync (a verb's, never a hook's: it goes through `updateGrants`). See `mergePending` for the rule.
278
+ * Returns false — and writes nothing — if `grants.json` is unreadable.
279
+ */
280
+ export function queueGrantOps(home, ops, now) {
281
+ try {
282
+ updateGrants(home, (g) => ({ ...g, pending: mergePending(g.pending, ops, now) }));
283
+ return true;
284
+ }
285
+ catch {
286
+ return false;
287
+ }
288
+ }
289
+ //# sourceMappingURL=grants_sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grants_sync.js","sourceRoot":"","sources":["../src/grants_sync.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * What a CLI verb does with the account's grants (slice 2, S4): `status` and `off` sync at the start or the end, and read what
3
+ * the last sync recorded. `connect` (the picker's "on your account" group, the joining-machine prompt, `g`) comes in a later PR.
4
+ *
5
+ * ```
6
+ * syncForVerb(ctx, …) one bounded sync, silent on failure (a verb's, never a hook's)
7
+ * accountView(…) the account rows' data, read from the files (no network)
8
+ * ```
9
+ *
10
+ * ## The first sync says what it saves, or it waits
11
+ *
12
+ * The first sync uploads what this machine already named — its owners and repos, and the slug of every per-clone consent
13
+ * made under 0.6.x/0.7.0 — to the user's account (R3-21). That is a disclosure the user must be able to read, so it runs
14
+ * only on a verb that can print it: a terminal, and not `--json`. Anywhere else it WAITS (nothing is sent, `pending` is
15
+ * untouched), and the next verb that can say so does it. The line names slugs, never paths.
16
+ *
17
+ * ## Nothing here is a hook
18
+ *
19
+ * Hooks never sync (`grants_sync.ts`). A verb reads the response with the 1 MiB cap a verb may use (`GRANTS_VERB_BODY_BYTES`).
20
+ */
21
+ import { ownerName } from "./account_not_here.js";
22
+ import { type AccountGrants, type Grants } from "./consent.js";
23
+ import type { CommandContext } from "./commands/context.js";
24
+ import { type SyncResult } from "./grants_sync.js";
25
+ /** What a verb injects: the network. A test passes a stub; `index.ts` passes the real `fetch`. */
26
+ export interface VerbSyncDeps {
27
+ readonly fetch: typeof fetch;
28
+ readonly timeoutMs?: number;
29
+ }
30
+ export type VerbSync =
31
+ /** No sync was attempted: no usable credential or ingest address, an unreadable `grants.json`, or a first sync that must wait. */
32
+ {
33
+ readonly kind: "skipped";
34
+ } | {
35
+ readonly kind: "ran";
36
+ readonly result: SyncResult;
37
+ /** The first-sync line was printed by THIS call. A verb that syncs twice passes it back as `quietFirst`, so it is said once. */
38
+ readonly disclosed?: boolean;
39
+ };
40
+ /** `owner/` for an owner, the slug for a repo — the way every account row and the first-sync line name them (`account_not_here.ts`). */
41
+ export { ownerName };
42
+ /**
43
+ * One sync for a verb. `json` is true for `--json`, which can print no line and so never does a FIRST sync.
44
+ *
45
+ * Prints, to stdout, only the first-sync line (and its full list past three names) and a blank line after it. Never throws.
46
+ */
47
+ export declare function syncForVerb(ctx: CommandContext, json: boolean, deps: VerbSyncDeps,
48
+ /** A verb that syncs more than once (`connect`: at its start and after its writes) says what the first sync saves only once. */
49
+ options?: {
50
+ readonly quietFirst?: boolean;
51
+ }): Promise<VerbSync>;
52
+ /** What the account rows show, read from `grants.json` and `grants-account.json` (no network). */
53
+ export interface AccountView {
54
+ /** The last synced account state, or `null` before the first sync. */
55
+ readonly account: AccountGrants | null;
56
+ /** What the account grants that this machine has not named: an owner as `owner/`, a repo as its slug. */
57
+ readonly notHere: readonly string[];
58
+ /** Queued changes no sync has delivered. */
59
+ readonly pending: number;
60
+ /** Of those, how many the server refused in THIS run's sync (a 400). */
61
+ readonly pendingRejected: number;
62
+ }
63
+ /**
64
+ * ⛔ "Not on this machine" is what THIS machine has not named: not in its owners, its repos, or a per-clone consent, and not
65
+ * turned off here or on the account. A repo under an owner the account grants is covered by that owner's row, not listed
66
+ * again. An owner or repo the user turned off is a choice, not a gap.
67
+ */
68
+ export declare function accountView(grants: Grants, account: AccountGrants | null, perCloneSlugs: readonly string[], sync: VerbSync, pendingRevokes?: number): AccountView;
69
+ /** The account state and grants for a view, read fresh from disk. `null` when `grants.json` is unreadable. */
70
+ export declare function readAccountView(home: string, perCloneSlugs: readonly string[], sync: VerbSync): AccountView | null;