@cortexkit/antigravity-auth-core 2.2.1 → 3.0.1

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 (253) hide show
  1. package/README.md +24 -2
  2. package/dist/abort-forwarding.d.ts +3 -0
  3. package/dist/abort-forwarding.d.ts.map +1 -0
  4. package/dist/abort-forwarding.js +19 -0
  5. package/dist/abort-forwarding.js.map +1 -0
  6. package/dist/account-identity.d.ts +52 -0
  7. package/dist/account-identity.d.ts.map +1 -0
  8. package/dist/account-identity.js +66 -0
  9. package/dist/account-identity.js.map +1 -0
  10. package/dist/account-manager.d.ts +228 -43
  11. package/dist/account-manager.d.ts.map +1 -1
  12. package/dist/account-manager.js +942 -903
  13. package/dist/account-manager.js.map +1 -1
  14. package/dist/account-migration.d.ts +309 -0
  15. package/dist/account-migration.d.ts.map +1 -0
  16. package/dist/account-migration.js +2284 -0
  17. package/dist/account-migration.js.map +1 -0
  18. package/dist/account-repository-codecs.d.ts +82 -0
  19. package/dist/account-repository-codecs.d.ts.map +1 -0
  20. package/dist/account-repository-codecs.js +609 -0
  21. package/dist/account-repository-codecs.js.map +1 -0
  22. package/dist/account-repository-types.d.ts +831 -0
  23. package/dist/account-repository-types.d.ts.map +1 -0
  24. package/dist/account-repository-types.js +137 -0
  25. package/dist/account-repository-types.js.map +1 -0
  26. package/dist/account-repository.d.ts +442 -0
  27. package/dist/account-repository.d.ts.map +1 -0
  28. package/dist/account-repository.js +2253 -0
  29. package/dist/account-repository.js.map +1 -0
  30. package/dist/account-selector.d.ts +470 -0
  31. package/dist/account-selector.d.ts.map +1 -0
  32. package/dist/account-selector.js +1383 -0
  33. package/dist/account-selector.js.map +1 -0
  34. package/dist/account-storage.d.ts.map +1 -1
  35. package/dist/account-storage.js +5 -0
  36. package/dist/account-storage.js.map +1 -1
  37. package/dist/agy-request-metadata.d.ts.map +1 -1
  38. package/dist/agy-request-metadata.js +8 -21
  39. package/dist/agy-request-metadata.js.map +1 -1
  40. package/dist/agy-transport.d.ts.map +1 -1
  41. package/dist/agy-transport.js +21 -35
  42. package/dist/agy-transport.js.map +1 -1
  43. package/dist/antigravity-command-menu.d.ts +171 -0
  44. package/dist/antigravity-command-menu.d.ts.map +1 -0
  45. package/dist/antigravity-command-menu.js +523 -0
  46. package/dist/antigravity-command-menu.js.map +1 -0
  47. package/dist/atomic-write.d.ts +18 -1
  48. package/dist/atomic-write.d.ts.map +1 -1
  49. package/dist/atomic-write.js +15 -3
  50. package/dist/atomic-write.js.map +1 -1
  51. package/dist/common-auth-embedded/NOTICE.txt +21 -0
  52. package/dist/common-auth-embedded/auth-menu/accounts.d.ts +97 -0
  53. package/dist/common-auth-embedded/auth-menu/accounts.js +387 -0
  54. package/dist/common-auth-embedded/auth-menu/ansi.d.ts +26 -0
  55. package/dist/common-auth-embedded/auth-menu/ansi.js +98 -0
  56. package/dist/common-auth-embedded/auth-menu/confirm.d.ts +7 -0
  57. package/dist/common-auth-embedded/auth-menu/confirm.js +21 -0
  58. package/dist/common-auth-embedded/auth-menu/doctor.d.ts +52 -0
  59. package/dist/common-auth-embedded/auth-menu/doctor.js +81 -0
  60. package/dist/common-auth-embedded/auth-menu/index.d.ts +17 -0
  61. package/dist/common-auth-embedded/auth-menu/index.js +12 -0
  62. package/dist/common-auth-embedded/auth-menu/login.d.ts +55 -0
  63. package/dist/common-auth-embedded/auth-menu/login.js +83 -0
  64. package/dist/common-auth-embedded/auth-menu/menu.d.ts +59 -0
  65. package/dist/common-auth-embedded/auth-menu/menu.js +73 -0
  66. package/dist/common-auth-embedded/auth-menu/opencode-v1.d.ts +49 -0
  67. package/dist/common-auth-embedded/auth-menu/opencode-v1.js +47 -0
  68. package/dist/common-auth-embedded/auth-menu/select.d.ts +21 -0
  69. package/dist/common-auth-embedded/auth-menu/select.js +159 -0
  70. package/dist/common-auth-embedded/auth-menu/terminal.d.ts +43 -0
  71. package/dist/common-auth-embedded/auth-menu/terminal.js +19 -0
  72. package/dist/common-auth-embedded/claustrum/consumer.d.ts +102 -0
  73. package/dist/common-auth-embedded/claustrum/consumer.js +298 -0
  74. package/dist/common-auth-embedded/claustrum/custody.d.ts +175 -0
  75. package/dist/common-auth-embedded/claustrum/custody.js +354 -0
  76. package/dist/common-auth-embedded/claustrum/enrollment.d.ts +158 -0
  77. package/dist/common-auth-embedded/claustrum/enrollment.js +759 -0
  78. package/dist/common-auth-embedded/claustrum/errors.d.ts +16 -0
  79. package/dist/common-auth-embedded/claustrum/errors.js +8 -0
  80. package/dist/common-auth-embedded/claustrum/host-slot.d.ts +39 -0
  81. package/dist/common-auth-embedded/claustrum/host-slot.js +72 -0
  82. package/dist/common-auth-embedded/claustrum/index.d.ts +18 -0
  83. package/dist/common-auth-embedded/claustrum/index.js +22 -0
  84. package/dist/common-auth-embedded/claustrum/interlock.d.ts +27 -0
  85. package/dist/common-auth-embedded/claustrum/interlock.js +29 -0
  86. package/dist/common-auth-embedded/claustrum/roster.d.ts +208 -0
  87. package/dist/common-auth-embedded/claustrum/roster.js +536 -0
  88. package/dist/common-auth-embedded/commands/builtins.d.ts +71 -0
  89. package/dist/common-auth-embedded/commands/builtins.js +534 -0
  90. package/dist/common-auth-embedded/commands/index.d.ts +10 -0
  91. package/dist/common-auth-embedded/commands/index.js +5 -0
  92. package/dist/common-auth-embedded/commands/menu.d.ts +61 -0
  93. package/dist/common-auth-embedded/commands/menu.js +300 -0
  94. package/dist/common-auth-embedded/commands/model.d.ts +238 -0
  95. package/dist/common-auth-embedded/commands/model.js +16 -0
  96. package/dist/common-auth-embedded/commands/pi.d.ts +23 -0
  97. package/dist/common-auth-embedded/commands/pi.js +206 -0
  98. package/dist/common-auth-embedded/commands/seam.d.ts +72 -0
  99. package/dist/common-auth-embedded/commands/seam.js +301 -0
  100. package/dist/common-auth-embedded/fs/atomic-write.d.ts +20 -0
  101. package/dist/common-auth-embedded/fs/atomic-write.js +80 -0
  102. package/dist/common-auth-embedded/fs/index.d.ts +7 -0
  103. package/dist/common-auth-embedded/fs/index.js +6 -0
  104. package/dist/common-auth-embedded/fs/lock-constants.d.ts +14 -0
  105. package/dist/common-auth-embedded/fs/lock-constants.js +14 -0
  106. package/dist/common-auth-embedded/fs/refresh-file-lock.d.ts +33 -0
  107. package/dist/common-auth-embedded/fs/refresh-file-lock.js +501 -0
  108. package/dist/common-auth-embedded/fs/with-lock.d.ts +33 -0
  109. package/dist/common-auth-embedded/fs/with-lock.js +48 -0
  110. package/dist/common-auth-embedded/logger/capture-sink.d.ts +14 -0
  111. package/dist/common-auth-embedded/logger/capture-sink.js +13 -0
  112. package/dist/common-auth-embedded/logger/engine.d.ts +100 -0
  113. package/dist/common-auth-embedded/logger/engine.js +246 -0
  114. package/dist/common-auth-embedded/logger/index.d.ts +6 -0
  115. package/dist/common-auth-embedded/logger/index.js +3 -0
  116. package/dist/common-auth-embedded/logger/redact.d.ts +11 -0
  117. package/dist/common-auth-embedded/logger/redact.js +83 -0
  118. package/dist/common-auth-embedded/pi-slot/index.d.ts +3 -0
  119. package/dist/common-auth-embedded/pi-slot/index.js +2 -0
  120. package/dist/common-auth-embedded/pi-slot/lock.js +212 -0
  121. package/dist/common-auth-embedded/pi-slot/slot.d.ts +49 -0
  122. package/dist/common-auth-embedded/pi-slot/slot.js +383 -0
  123. package/dist/common-auth-embedded/quota/codec.d.ts +11 -0
  124. package/dist/common-auth-embedded/quota/codec.js +12 -0
  125. package/dist/common-auth-embedded/quota/format.d.ts +46 -0
  126. package/dist/common-auth-embedded/quota/format.js +138 -0
  127. package/dist/common-auth-embedded/quota/index.d.ts +10 -0
  128. package/dist/common-auth-embedded/quota/index.js +5 -0
  129. package/dist/common-auth-embedded/quota/map.d.ts +63 -0
  130. package/dist/common-auth-embedded/quota/map.js +105 -0
  131. package/dist/common-auth-embedded/quota/merge.d.ts +48 -0
  132. package/dist/common-auth-embedded/quota/merge.js +162 -0
  133. package/dist/common-auth-embedded/quota/projection.d.ts +64 -0
  134. package/dist/common-auth-embedded/quota/projection.js +134 -0
  135. package/dist/common-auth-embedded/routing/admission.d.ts +93 -0
  136. package/dist/common-auth-embedded/routing/admission.js +142 -0
  137. package/dist/common-auth-embedded/routing/index.d.ts +8 -0
  138. package/dist/common-auth-embedded/routing/index.js +4 -0
  139. package/dist/common-auth-embedded/routing/ordered.d.ts +47 -0
  140. package/dist/common-auth-embedded/routing/ordered.js +58 -0
  141. package/dist/common-auth-embedded/routing/pins.d.ts +22 -0
  142. package/dist/common-auth-embedded/routing/pins.js +34 -0
  143. package/dist/common-auth-embedded/routing/sticky.d.ts +158 -0
  144. package/dist/common-auth-embedded/routing/sticky.js +339 -0
  145. package/dist/common-auth-embedded/source-output.json +1090 -0
  146. package/dist/common-auth-embedded/store/attribution.d.ts +28 -0
  147. package/dist/common-auth-embedded/store/attribution.js +60 -0
  148. package/dist/common-auth-embedded/store/errors.d.ts +55 -0
  149. package/dist/common-auth-embedded/store/errors.js +38 -0
  150. package/dist/common-auth-embedded/store/hooks.d.ts +12 -0
  151. package/dist/common-auth-embedded/store/hooks.js +34 -0
  152. package/dist/common-auth-embedded/store/identity.d.ts +39 -0
  153. package/dist/common-auth-embedded/store/identity.js +79 -0
  154. package/dist/common-auth-embedded/store/index.d.ts +21 -0
  155. package/dist/common-auth-embedded/store/index.js +7 -0
  156. package/dist/common-auth-embedded/store/mutate.d.ts +189 -0
  157. package/dist/common-auth-embedded/store/mutate.js +500 -0
  158. package/dist/common-auth-embedded/store/pool.d.ts +196 -0
  159. package/dist/common-auth-embedded/store/pool.js +120 -0
  160. package/dist/common-auth-embedded/store/provider-state.d.ts +131 -0
  161. package/dist/common-auth-embedded/store/provider-state.js +226 -0
  162. package/dist/common-auth-embedded/store/publication.d.ts +47 -0
  163. package/dist/common-auth-embedded/store/publication.js +304 -0
  164. package/dist/common-auth-embedded/store/pull.d.ts +36 -0
  165. package/dist/common-auth-embedded/store/pull.js +143 -0
  166. package/dist/common-auth-embedded/store/refresh-lock.d.ts +63 -0
  167. package/dist/common-auth-embedded/store/refresh-lock.js +136 -0
  168. package/dist/common-auth-embedded/store/refresh.d.ts +72 -0
  169. package/dist/common-auth-embedded/store/refresh.js +225 -0
  170. package/dist/common-auth-embedded/store/reserved.js +13 -0
  171. package/dist/common-auth-embedded/store/rows.d.ts +313 -0
  172. package/dist/common-auth-embedded/store/rows.js +1258 -0
  173. package/dist/common-auth-embedded/store/runtime.d.ts +36 -0
  174. package/dist/common-auth-embedded/store/runtime.js +68 -0
  175. package/dist/common-auth-embedded/store/schema.d.ts +443 -0
  176. package/dist/common-auth-embedded/store/schema.js +754 -0
  177. package/dist/common-auth-embedded/store/settings.d.ts +63 -0
  178. package/dist/common-auth-embedded/store/settings.js +120 -0
  179. package/dist/common-auth-embedded/store/torn.d.ts +78 -0
  180. package/dist/common-auth-embedded/store/torn.js +294 -0
  181. package/dist/common-auth-embedded/util/any-signal.js +42 -0
  182. package/dist/common-auth-runtime.d.ts +73 -0
  183. package/dist/common-auth-runtime.d.ts.map +1 -0
  184. package/dist/common-auth-runtime.js +65 -0
  185. package/dist/common-auth-runtime.js.map +1 -0
  186. package/dist/credential-redaction.d.ts +13 -0
  187. package/dist/credential-redaction.d.ts.map +1 -0
  188. package/dist/credential-redaction.js +230 -0
  189. package/dist/credential-redaction.js.map +1 -0
  190. package/dist/fetch-timeout.d.ts +4 -2
  191. package/dist/fetch-timeout.d.ts.map +1 -1
  192. package/dist/fetch-timeout.js +121 -14
  193. package/dist/fetch-timeout.js.map +1 -1
  194. package/dist/fingerprint.d.ts +3 -3
  195. package/dist/fingerprint.d.ts.map +1 -1
  196. package/dist/fingerprint.js +4 -4
  197. package/dist/fingerprint.js.map +1 -1
  198. package/dist/index.d.ts +16 -1
  199. package/dist/index.d.ts.map +1 -1
  200. package/dist/index.js +15 -0
  201. package/dist/index.js.map +1 -1
  202. package/dist/model-registry.d.ts +16 -0
  203. package/dist/model-registry.d.ts.map +1 -1
  204. package/dist/model-registry.js +242 -30
  205. package/dist/model-registry.js.map +1 -1
  206. package/dist/quota-manager.d.ts.map +1 -1
  207. package/dist/quota-manager.js +35 -10
  208. package/dist/quota-manager.js.map +1 -1
  209. package/dist/quota-presentation.d.ts +10 -0
  210. package/dist/quota-presentation.d.ts.map +1 -0
  211. package/dist/quota-presentation.js +60 -0
  212. package/dist/quota-presentation.js.map +1 -0
  213. package/dist/quota-types.d.ts +2 -0
  214. package/dist/quota-types.d.ts.map +1 -1
  215. package/dist/rotation.d.ts +8 -0
  216. package/dist/rotation.d.ts.map +1 -1
  217. package/dist/rotation.js +24 -0
  218. package/dist/rotation.js.map +1 -1
  219. package/dist/transform/agy-generation.d.ts +3 -0
  220. package/dist/transform/agy-generation.d.ts.map +1 -0
  221. package/dist/transform/agy-generation.js +28 -0
  222. package/dist/transform/agy-generation.js.map +1 -0
  223. package/dist/transform/claude.d.ts +7 -3
  224. package/dist/transform/claude.d.ts.map +1 -1
  225. package/dist/transform/claude.js +8 -2
  226. package/dist/transform/claude.js.map +1 -1
  227. package/dist/transform/index.d.ts +1 -0
  228. package/dist/transform/index.d.ts.map +1 -1
  229. package/dist/transform/index.js +1 -0
  230. package/dist/transform/index.js.map +1 -1
  231. package/dist/transform/model-resolver.d.ts +2 -1
  232. package/dist/transform/model-resolver.d.ts.map +1 -1
  233. package/dist/transform/model-resolver.js +46 -5
  234. package/dist/transform/model-resolver.js.map +1 -1
  235. package/dist/transform/types.d.ts +1 -1
  236. package/dist/transform/types.d.ts.map +1 -1
  237. package/dist/vault-account-source.d.ts +183 -0
  238. package/dist/vault-account-source.d.ts.map +1 -0
  239. package/dist/vault-account-source.js +296 -0
  240. package/dist/vault-account-source.js.map +1 -0
  241. package/dist/vault-enrollment-authority.d.ts +16 -0
  242. package/dist/vault-enrollment-authority.d.ts.map +1 -0
  243. package/dist/vault-enrollment-authority.js +19 -0
  244. package/dist/vault-enrollment-authority.js.map +1 -0
  245. package/dist/vault-family.d.ts +11 -0
  246. package/dist/vault-family.d.ts.map +1 -0
  247. package/dist/vault-family.js +24 -0
  248. package/dist/vault-family.js.map +1 -0
  249. package/dist/vault-provider-state.d.ts +162 -0
  250. package/dist/vault-provider-state.d.ts.map +1 -0
  251. package/dist/vault-provider-state.js +422 -0
  252. package/dist/vault-provider-state.js.map +1 -0
  253. package/package.json +39 -2
@@ -0,0 +1,49 @@
1
+ export type PiSlotErrorCode = 'invalid-options' | 'invalid-auth' | 'invalid-stash' | 'unsafe-path' | 'missing-stash' | 'conflict' | 'lock-timeout' | 'lock-compromised' | 'io';
2
+ /** Errors deliberately contain neither paths nor credentials nor underlying error messages. */
3
+ export declare class PiSlotError extends Error {
4
+ readonly code: PiSlotErrorCode;
5
+ constructor(code: PiSlotErrorCode);
6
+ }
7
+ export interface PiSlotOptions {
8
+ authPath: string;
9
+ provider: string;
10
+ /**
11
+ * Where the provider's original Pi login is kept while the vault serves.
12
+ * Put it in a private directory, and give each provider its own file: one
13
+ * stash holds one provider's original login, and a second provider writing
14
+ * to it would overwrite the first one's backup.
15
+ */
16
+ stashPath: string;
17
+ /**
18
+ * The api_key Pi stores in the slot while the vault serves. It is a fixed,
19
+ * non-secret string that no provider accepts. Pi runs a key starting with
20
+ * `!` as a shell command and expands `$NAME` from the environment, so the
21
+ * placeholder may contain neither.
22
+ */
23
+ placeholderKey: string;
24
+ lockTimeoutMs?: number;
25
+ }
26
+ export interface PiSlotInspection {
27
+ slot: 'original' | 'placeholder' | 'foreign' | 'empty';
28
+ stash: 'valid' | 'missing';
29
+ }
30
+ export interface PiSlot {
31
+ inspect(): Promise<PiSlotInspection>;
32
+ enterVault(): Promise<void>;
33
+ exitVault(): Promise<'restored' | 'conflict' | 'nothing-to-do'>;
34
+ }
35
+ /**
36
+ * Production entry point. Pi caches which models are usable; after a
37
+ * successful transition the extension calls `ctx.modelRegistry.refresh()` so
38
+ * Pi re-reads the slot it just changed.
39
+ */
40
+ export declare function openPiSlot(options: PiSlotOptions): PiSlot;
41
+ /**
42
+ * Internal test entry point: pause reads, renewals, or renames to simulate races,
43
+ * and observe durable writes/deletion to simulate crashes. Not a public export.
44
+ */
45
+ export declare function createPiSlot(options: PiSlotOptions, onStep?: (step: 'stash-written' | 'auth-written' | 'stash-deleted') => void, hooks?: {
46
+ afterReadCheck?: (path: string) => Promise<void>;
47
+ onRenew?: (renew: () => Promise<void>) => void;
48
+ beforeRename?: (path: string) => Promise<void>;
49
+ }): PiSlot;
@@ -0,0 +1,383 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { lstat, mkdir, open, unlink } from 'node:fs/promises';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { setTimeout as sleep } from 'node:timers/promises';
6
+ import { writeJsonAtomic } from '../fs/atomic-write.js';
7
+ import { acquirePiLock } from './lock.js';
8
+ /** Errors deliberately contain neither paths nor credentials nor underlying error messages. */
9
+ export class PiSlotError extends Error {
10
+ code;
11
+ constructor(code) {
12
+ super(`Pi slot: ${code}`);
13
+ this.code = code;
14
+ this.name = 'PiSlotError';
15
+ }
16
+ }
17
+ // JSON.parse validates grammar first. This scan only locates top-level value text ranges.
18
+ function rootSpans(text) {
19
+ let i = text.indexOf('{') + 1;
20
+ const root = { offset: i - 1, children: [] };
21
+ const whitespace = () => {
22
+ while (/\s/.test(text[i] ?? '') && i < text.length)
23
+ i++;
24
+ };
25
+ const stringEnd = () => {
26
+ i++;
27
+ while (i < text.length) {
28
+ if (text[i++] === '"')
29
+ return;
30
+ if (text[i - 1] === '\\')
31
+ i++;
32
+ }
33
+ };
34
+ whitespace();
35
+ while (text[i] !== '}') {
36
+ const start = i;
37
+ stringEnd();
38
+ const key = JSON.parse(text.slice(start, i));
39
+ whitespace();
40
+ i++; // colon
41
+ whitespace();
42
+ const offset = i;
43
+ let depth = 0;
44
+ while (i < text.length) {
45
+ const char = text[i];
46
+ if (char === '"') {
47
+ stringEnd();
48
+ continue;
49
+ }
50
+ if (char === '{' || char === '[')
51
+ depth++;
52
+ if (char === '}' || char === ']') {
53
+ if (depth === 0)
54
+ break;
55
+ depth--;
56
+ }
57
+ if (char === ',' && depth === 0)
58
+ break;
59
+ i++;
60
+ }
61
+ let end = i;
62
+ while (/\s/.test(text[end - 1] ?? ''))
63
+ end--;
64
+ root.children.push({
65
+ key,
66
+ offset: start,
67
+ length: end - start,
68
+ value: { offset, length: end - offset },
69
+ });
70
+ whitespace();
71
+ if (text[i] === '}')
72
+ break;
73
+ i++; // comma
74
+ whitespace();
75
+ }
76
+ return root;
77
+ }
78
+ function object(value) {
79
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
80
+ }
81
+ function canonical(value) {
82
+ if (Array.isArray(value))
83
+ return `[${value.map(canonical).join(',')}]`;
84
+ if (object(value)) {
85
+ return `{${Object.keys(value)
86
+ .sort()
87
+ .map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`)
88
+ .join(',')}}`;
89
+ }
90
+ return JSON.stringify(value);
91
+ }
92
+ function digest(value) {
93
+ return createHash('sha256').update(canonical(value)).digest('hex');
94
+ }
95
+ function code(error) {
96
+ return object(error) ? error.code : undefined;
97
+ }
98
+ async function safeRead(path, privateFile = false, afterReadCheck) {
99
+ let file;
100
+ try {
101
+ file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
102
+ const stat = await file.stat();
103
+ if (!stat.isFile() || (privateFile && (stat.mode & 0o777) !== 0o600)) {
104
+ throw new PiSlotError('unsafe-path');
105
+ }
106
+ await afterReadCheck?.(path);
107
+ // Refuse a replaced pathname even though the descriptor still pins the checked file.
108
+ const current = await lstat(path);
109
+ if (!current.isFile() ||
110
+ current.dev !== stat.dev ||
111
+ current.ino !== stat.ino) {
112
+ throw new PiSlotError('unsafe-path');
113
+ }
114
+ const bytes = await file.readFile();
115
+ const text = bytes.toString('utf8');
116
+ if (!bytes.equals(Buffer.from(text))) {
117
+ throw new PiSlotError(privateFile ? 'invalid-stash' : 'invalid-auth');
118
+ }
119
+ return text;
120
+ }
121
+ catch (error) {
122
+ if (code(error) === 'ENOENT' && !file)
123
+ return undefined;
124
+ if (code(error) === 'ELOOP' || code(error) === 'ENOENT') {
125
+ throw new PiSlotError('unsafe-path');
126
+ }
127
+ throw error;
128
+ }
129
+ finally {
130
+ await file?.close();
131
+ }
132
+ }
133
+ function parseAuth(text, provider) {
134
+ try {
135
+ const data = JSON.parse(text);
136
+ if (!object(data))
137
+ throw new Error();
138
+ const root = rootSpans(text);
139
+ const properties = root.children;
140
+ // Duplicate provider keys have ambiguous byte ownership, even though JSON.parse accepts them.
141
+ const names = properties.map((node) => node.key);
142
+ if (new Set(names).size !== names.length)
143
+ throw new Error();
144
+ const property = properties.find((node) => node.key === provider);
145
+ const entry = property ? data[provider] : null;
146
+ if (entry !== null && !object(entry))
147
+ throw new Error();
148
+ if (property && entry === null)
149
+ throw new Error();
150
+ return { text, root, property, entry: entry };
151
+ }
152
+ catch {
153
+ throw new PiSlotError('invalid-auth');
154
+ }
155
+ }
156
+ function entryText(auth) {
157
+ const value = auth.property?.value;
158
+ return value
159
+ ? auth.text.slice(value.offset, value.offset + value.length)
160
+ : null;
161
+ }
162
+ function replaceEntry(auth, provider, next) {
163
+ const { text, root, property } = auth;
164
+ const properties = root.children ?? [];
165
+ if (property) {
166
+ const value = property.value;
167
+ if (!value)
168
+ throw new PiSlotError('invalid-auth');
169
+ if (next !== null) {
170
+ return (text.slice(0, value.offset) +
171
+ next +
172
+ text.slice(value.offset + value.length));
173
+ }
174
+ const index = properties.indexOf(property);
175
+ const previous = properties[index - 1];
176
+ const following = properties[index + 1];
177
+ const start = previous ? previous.offset + previous.length : property.offset;
178
+ const end = !previous && following
179
+ ? following.offset
180
+ : property.offset + property.length;
181
+ return text.slice(0, start) + text.slice(end);
182
+ }
183
+ if (next === null)
184
+ return text;
185
+ const last = properties.at(-1);
186
+ // Insert next to the last value, not after its whitespace, so deletion reverses an empty-slot swap.
187
+ const offset = last ? last.offset + last.length : root.offset + 1;
188
+ return `${text.slice(0, offset)}${last ? ',' : ''}${JSON.stringify(provider)}:${next}${text.slice(offset)}`;
189
+ }
190
+ async function syncDir(path) {
191
+ const dir = await open(path, 'r');
192
+ try {
193
+ await dir.sync();
194
+ }
195
+ finally {
196
+ await dir.close();
197
+ }
198
+ }
199
+ async function atomicWrite(path, text, assertLock, beforeRename) {
200
+ await writeJsonAtomic(path, null, {
201
+ serialize: () => text,
202
+ durable: true,
203
+ beforeRename: async () => {
204
+ await beforeRename?.(path);
205
+ assertLock();
206
+ },
207
+ });
208
+ }
209
+ /**
210
+ * Production entry point. Pi caches which models are usable; after a
211
+ * successful transition the extension calls `ctx.modelRegistry.refresh()` so
212
+ * Pi re-reads the slot it just changed.
213
+ */
214
+ export function openPiSlot(options) {
215
+ return createPiSlot(options);
216
+ }
217
+ /**
218
+ * Internal test entry point: pause reads, renewals, or renames to simulate races,
219
+ * and observe durable writes/deletion to simulate crashes. Not a public export.
220
+ */
221
+ export function createPiSlot(options, onStep, hooks) {
222
+ const { provider, placeholderKey } = options;
223
+ const timeout = options.lockTimeoutMs ?? 30_000;
224
+ if (!provider ||
225
+ !options.authPath ||
226
+ !options.stashPath ||
227
+ !placeholderKey ||
228
+ placeholderKey.startsWith('!') ||
229
+ placeholderKey.includes('$') ||
230
+ !Number.isFinite(timeout) ||
231
+ timeout < 0) {
232
+ throw new PiSlotError('invalid-options');
233
+ }
234
+ const authPath = resolve(options.authPath);
235
+ const stashPath = resolve(options.stashPath);
236
+ if (authPath === stashPath || stashPath === `${authPath}.lock`) {
237
+ throw new PiSlotError('invalid-options');
238
+ }
239
+ const authIdentity = digest(authPath);
240
+ const placeholderIdentity = digest(placeholderKey);
241
+ const placeholder = { type: 'api_key', key: placeholderKey };
242
+ const isPlaceholder = (entry) => digest(entry) === digest(placeholder);
243
+ async function loadStash() {
244
+ const text = await safeRead(stashPath, true, hooks?.afterReadCheck);
245
+ if (text === undefined)
246
+ return undefined;
247
+ try {
248
+ const stash = JSON.parse(text);
249
+ if (stash.version !== 1 ||
250
+ stash.provider !== provider ||
251
+ stash.authIdentity !== authIdentity ||
252
+ stash.placeholderIdentity !== placeholderIdentity ||
253
+ (stash.entry !== null && !object(stash.entry)) ||
254
+ stash.sha256 !== digest(stash.entry) ||
255
+ (stash.entry === null
256
+ ? stash.entryJson !== null
257
+ : typeof stash.entryJson !== 'string' ||
258
+ digest(JSON.parse(stash.entryJson)) !== stash.sha256)) {
259
+ throw new Error();
260
+ }
261
+ return stash;
262
+ }
263
+ catch {
264
+ throw new PiSlotError('invalid-stash');
265
+ }
266
+ }
267
+ async function locked(fn) {
268
+ let release;
269
+ let assertFresh;
270
+ let compromised = false;
271
+ const assertLock = () => {
272
+ assertFresh?.();
273
+ if (compromised)
274
+ throw new PiSlotError('lock-compromised');
275
+ };
276
+ try {
277
+ await mkdir(dirname(authPath), { recursive: true, mode: 0o700 });
278
+ const deadline = Date.now() + timeout;
279
+ let retry = 0;
280
+ while (!release) {
281
+ try {
282
+ const lease = await acquirePiLock(authPath, () => {
283
+ compromised = true;
284
+ }, hooks?.onRenew);
285
+ release = lease.release;
286
+ assertFresh = lease.assertFresh;
287
+ }
288
+ catch (error) {
289
+ if (code(error) !== 'ELOCKED')
290
+ throw error;
291
+ const remaining = deadline - Date.now();
292
+ if (remaining <= 0)
293
+ throw new PiSlotError('lock-timeout');
294
+ await sleep(Math.min(Math.round(Math.min(10 * 2 ** retry++, 1_000) * (1 + Math.random())), remaining));
295
+ }
296
+ }
297
+ assertLock();
298
+ const result = await fn(assertLock);
299
+ assertLock();
300
+ return result;
301
+ }
302
+ catch (error) {
303
+ if (error instanceof PiSlotError)
304
+ throw error;
305
+ throw new PiSlotError('io');
306
+ }
307
+ finally {
308
+ await release?.().catch(() => { });
309
+ assertLock();
310
+ }
311
+ }
312
+ async function load() {
313
+ const auth = parseAuth((await safeRead(authPath, false, hooks?.afterReadCheck)) ?? '{}', provider);
314
+ const stash = await loadStash();
315
+ return { auth, stash };
316
+ }
317
+ async function deleteStash(assertLock) {
318
+ assertLock();
319
+ await unlink(stashPath);
320
+ await syncDir(dirname(stashPath));
321
+ onStep?.('stash-deleted');
322
+ }
323
+ return {
324
+ inspect: () => locked(async () => {
325
+ const { auth, stash } = await load();
326
+ const slot = isPlaceholder(auth.entry)
327
+ ? 'placeholder'
328
+ : auth.entry === null
329
+ ? 'empty'
330
+ : !stash || digest(auth.entry) === stash.sha256
331
+ ? 'original'
332
+ : 'foreign';
333
+ return { slot, stash: stash ? 'valid' : 'missing' };
334
+ }),
335
+ enterVault: () => locked(async (assertLock) => {
336
+ const { auth, stash } = await load();
337
+ if (isPlaceholder(auth.entry)) {
338
+ if (!stash)
339
+ throw new PiSlotError('missing-stash');
340
+ return;
341
+ }
342
+ if (stash && digest(auth.entry) !== stash.sha256)
343
+ throw new PiSlotError('conflict');
344
+ if (!stash) {
345
+ await mkdir(dirname(stashPath), { recursive: true, mode: 0o700 });
346
+ const dir = await lstat(dirname(stashPath));
347
+ if (!dir.isDirectory() || (dir.mode & 0o777) !== 0o700)
348
+ throw new PiSlotError('unsafe-path');
349
+ const saved = {
350
+ version: 1,
351
+ provider,
352
+ authIdentity,
353
+ placeholderIdentity,
354
+ entry: auth.entry,
355
+ entryJson: entryText(auth),
356
+ sha256: digest(auth.entry),
357
+ };
358
+ await atomicWrite(stashPath, JSON.stringify(saved), assertLock, hooks?.beforeRename);
359
+ onStep?.('stash-written');
360
+ }
361
+ await atomicWrite(authPath, replaceEntry(auth, provider, JSON.stringify(placeholder)), assertLock, hooks?.beforeRename);
362
+ onStep?.('auth-written');
363
+ }),
364
+ exitVault: () => locked(async (assertLock) => {
365
+ const { auth, stash } = await load();
366
+ if (!stash) {
367
+ if (isPlaceholder(auth.entry))
368
+ throw new PiSlotError('missing-stash');
369
+ return 'nothing-to-do';
370
+ }
371
+ if (isPlaceholder(auth.entry)) {
372
+ await atomicWrite(authPath, replaceEntry(auth, provider, stash.entryJson), assertLock, hooks?.beforeRename);
373
+ onStep?.('auth-written');
374
+ }
375
+ else if (digest(auth.entry) !== stash.sha256) {
376
+ return 'conflict';
377
+ }
378
+ // Also completes a restore interrupted between the auth rename and stash deletion.
379
+ await deleteStash(assertLock);
380
+ return 'restored';
381
+ }),
382
+ };
383
+ }
@@ -0,0 +1,11 @@
1
+ export interface QuotaCodec {
2
+ readonly validate: (value: unknown) => boolean;
3
+ readonly merge: (stored: unknown, observation: unknown) => unknown;
4
+ }
5
+ /**
6
+ * The quota codec the account-pool store (the `/store` subpath) takes when it
7
+ * is opened: `validate` checks a stored per-row quota map and `merge` applies
8
+ * an observation to one, so the store persists the map without interpreting
9
+ * it.
10
+ */
11
+ export declare const quotaCodec: QuotaCodec;
@@ -0,0 +1,12 @@
1
+ import { isQuotaMap } from './map.js';
2
+ import { mergeQuotaObservation } from './merge.js';
3
+ /**
4
+ * The quota codec the account-pool store (the `/store` subpath) takes when it
5
+ * is opened: `validate` checks a stored per-row quota map and `merge` applies
6
+ * an observation to one, so the store persists the map without interpreting
7
+ * it.
8
+ */
9
+ export const quotaCodec = Object.freeze({
10
+ validate: isQuotaMap,
11
+ merge: mergeQuotaObservation,
12
+ });
@@ -0,0 +1,46 @@
1
+ import type { ProjectedLimit, ProjectedQuota } from './projection.js';
2
+ /** A reading older than this is marked with its age. */
3
+ export declare const QUOTA_STALE_AFTER_MS: number;
4
+ /** What `formatQuota` returns when there is no reading at all. */
5
+ export declare const NO_QUOTA_READING = "no quota reading yet";
6
+ /**
7
+ * What `formatQuota` returns when the provider answered but reported no
8
+ * limit for any window (every window is an absence record or a tombstone).
9
+ */
10
+ export declare const NO_LIMITS_REPORTED = "no limits reported";
11
+ /**
12
+ * `compact` fits a footer or a sidebar row: each window's figure, the credit
13
+ * budget and the stale marker, without reset times. `full` adds each reset.
14
+ */
15
+ export type QuotaTextForm = 'compact' | 'full';
16
+ export interface QuotaTextOptions {
17
+ /** The current time in ms, for reset countdowns and the stale marker. */
18
+ now: number;
19
+ /** Defaults to `full`. */
20
+ form?: QuotaTextForm;
21
+ /** Defaults to `QUOTA_STALE_AFTER_MS`. */
22
+ staleAfterMs?: number;
23
+ }
24
+ /**
25
+ * The name a window is shown under. A general window (scope `all`) is named
26
+ * by its stored length; a model-scoped window keeps the label the plugin
27
+ * stored it under, which already names the model (`seven_day_opus`). A
28
+ * general window stored without a length (a snapshot from before lengths
29
+ * were recorded) falls back to its label.
30
+ */
31
+ export declare function quotaWindowName(limit: Pick<ProjectedLimit, 'scope' | 'label' | 'windowMinutes'>): string;
32
+ /**
33
+ * The pieces of a quota text, in order: one per reported window
34
+ * (`5h 58% left, resets 2h`), then the credit budget when there is one, then
35
+ * `checked 3h ago` when the oldest piece shown is stale. Empty when nothing
36
+ * was reported; `formatQuota` turns that into a sentence. A caller printing
37
+ * one line per window (the terminal auth menu) uses these directly.
38
+ */
39
+ export declare function quotaTextParts(quota: ProjectedQuota | undefined, options: QuotaTextOptions): string[];
40
+ /**
41
+ * A row's quota as one line: `5h 58% left · 7d 90% left` in the compact
42
+ * form, with `, resets 2h` after each window in the full form. With nothing
43
+ * reported it is `NO_QUOTA_READING`, or `NO_LIMITS_REPORTED` when the
44
+ * provider answered without a limit for any window.
45
+ */
46
+ export declare function formatQuota(quota: ProjectedQuota | undefined, options: QuotaTextOptions): string;
@@ -0,0 +1,138 @@
1
+ // The one quota wording every surface shows: the command menu's dialogs on
2
+ // OpenCode and Pi, the terminal auth menu, and the plugins' sidebars. Each of
3
+ // them calls `formatQuota` (or `quotaTextParts` for one line per window), so
4
+ // the wording cannot drift between plugins or between a dialog and a sidebar.
5
+ //
6
+ // The rules: a window is named by its length (`5h`, `7d`), or, for a window
7
+ // that binds only one model family, by the label the plugin stored it under.
8
+ // Every figure is "% left". A window the provider did not report (an absence
9
+ // record or a retirement tombstone) is left out rather than written as "not
10
+ // reported". A reset is a short time from now (`resets 2h`). A reading older
11
+ // than fifteen minutes says how old it is (`checked 3h ago`).
12
+ //
13
+ // Like the rest of this module it reads no clock: `now` is a parameter.
14
+ import { ALL_SCOPE } from './map.js';
15
+ /** A reading older than this is marked with its age. */
16
+ export const QUOTA_STALE_AFTER_MS = 15 * 60_000;
17
+ /** What `formatQuota` returns when there is no reading at all. */
18
+ export const NO_QUOTA_READING = 'no quota reading yet';
19
+ /**
20
+ * What `formatQuota` returns when the provider answered but reported no
21
+ * limit for any window (every window is an absence record or a tombstone).
22
+ */
23
+ export const NO_LIMITS_REPORTED = 'no limits reported';
24
+ /**
25
+ * A length or a time span in its largest whole-ish unit: `45m`, `5h`, `3d`.
26
+ * Spans under an hour are minutes, under a day hours, otherwise days, each
27
+ * rounded to the nearest unit and never shown as zero.
28
+ */
29
+ function shortSpan(ms) {
30
+ const minutes = Math.max(1, Math.round(ms / 60_000));
31
+ if (minutes < 60)
32
+ return `${minutes}m`;
33
+ const hours = Math.round(minutes / 60);
34
+ if (hours < 24)
35
+ return `${hours}h`;
36
+ return `${Math.max(1, Math.round(minutes / 1440))}d`;
37
+ }
38
+ /** A stored window length in minutes as its exact name: `5h`, `7d`, `90m`. */
39
+ function lengthName(minutes) {
40
+ if (minutes % 1440 === 0)
41
+ return `${minutes / 1440}d`;
42
+ if (minutes % 60 === 0)
43
+ return `${minutes / 60}h`;
44
+ return `${minutes}m`;
45
+ }
46
+ /**
47
+ * The name a window is shown under. A general window (scope `all`) is named
48
+ * by its stored length; a model-scoped window keeps the label the plugin
49
+ * stored it under, which already names the model (`seven_day_opus`). A
50
+ * general window stored without a length (a snapshot from before lengths
51
+ * were recorded) falls back to its label.
52
+ */
53
+ export function quotaWindowName(limit) {
54
+ if (limit.scope !== ALL_SCOPE || limit.windowMinutes === undefined)
55
+ return limit.label;
56
+ return lengthName(limit.windowMinutes);
57
+ }
58
+ function percentLeft(remaining) {
59
+ return `${Math.round(Math.min(100, Math.max(0, remaining)))}% left`;
60
+ }
61
+ function resetText(resetsAt, now) {
62
+ if (resetsAt === undefined)
63
+ return '';
64
+ const at = Date.parse(resetsAt);
65
+ // A reset already passed says nothing about the future; leave it out.
66
+ if (!Number.isFinite(at) || at <= now)
67
+ return '';
68
+ return `, resets ${shortSpan(at - now)}`;
69
+ }
70
+ /** Shortest window first, so `5h` reads before `7d`; unknown lengths last. */
71
+ function displayOrder(left, right) {
72
+ const l = left.windowMinutes;
73
+ const r = right.windowMinutes;
74
+ if (l !== undefined && r !== undefined && l !== r)
75
+ return l - r;
76
+ if ((l === undefined) !== (r === undefined))
77
+ return l === undefined ? 1 : -1;
78
+ if (left.scope !== right.scope) {
79
+ if (left.scope === ALL_SCOPE)
80
+ return -1;
81
+ if (right.scope === ALL_SCOPE)
82
+ return 1;
83
+ return left.scope < right.scope ? -1 : 1;
84
+ }
85
+ return left.label < right.label ? -1 : left.label > right.label ? 1 : 0;
86
+ }
87
+ /**
88
+ * The pieces of a quota text, in order: one per reported window
89
+ * (`5h 58% left, resets 2h`), then the credit budget when there is one, then
90
+ * `checked 3h ago` when the oldest piece shown is stale. Empty when nothing
91
+ * was reported; `formatQuota` turns that into a sentence. A caller printing
92
+ * one line per window (the terminal auth menu) uses these directly.
93
+ */
94
+ export function quotaTextParts(quota, options) {
95
+ if (!quota)
96
+ return [];
97
+ const { now } = options;
98
+ const full = (options.form ?? 'full') === 'full';
99
+ const parts = [];
100
+ const times = [];
101
+ const readings = quota.limits
102
+ .filter((limit) => limit.kind === 'reading' && limit.remainingPercent !== undefined)
103
+ .sort(displayOrder);
104
+ for (const limit of readings) {
105
+ times.push(limit.checkedAt);
106
+ parts.push(`${quotaWindowName(limit)} ${percentLeft(limit.remainingPercent ?? 0)}${full ? resetText(limit.resetsAt, now) : ''}`);
107
+ }
108
+ const budget = quota.budget;
109
+ if (budget) {
110
+ times.push(budget.checkedAt);
111
+ const figure = budget.reached
112
+ ? 'credits spent'
113
+ : budget.remainingPercent !== undefined
114
+ ? `credits ${percentLeft(budget.remainingPercent)}`
115
+ : 'credits available';
116
+ parts.push(`${figure}${full ? resetText(budget.resetsAt, now) : ''}`);
117
+ }
118
+ if (times.length > 0) {
119
+ const age = now - Math.min(...times);
120
+ if (age > (options.staleAfterMs ?? QUOTA_STALE_AFTER_MS))
121
+ parts.push(`checked ${shortSpan(age)} ago`);
122
+ }
123
+ return parts;
124
+ }
125
+ /**
126
+ * A row's quota as one line: `5h 58% left · 7d 90% left` in the compact
127
+ * form, with `, resets 2h` after each window in the full form. With nothing
128
+ * reported it is `NO_QUOTA_READING`, or `NO_LIMITS_REPORTED` when the
129
+ * provider answered without a limit for any window.
130
+ */
131
+ export function formatQuota(quota, options) {
132
+ const parts = quotaTextParts(quota, options);
133
+ if (parts.length > 0)
134
+ return parts.join(' · ');
135
+ return quota && quota.limits.length > 0
136
+ ? NO_LIMITS_REPORTED
137
+ : NO_QUOTA_READING;
138
+ }
@@ -0,0 +1,10 @@
1
+ export type { QuotaCodec } from './codec.js';
2
+ export { quotaCodec } from './codec.js';
3
+ export type { QuotaTextForm, QuotaTextOptions } from './format.js';
4
+ export { formatQuota, NO_LIMITS_REPORTED, NO_QUOTA_READING, QUOTA_STALE_AFTER_MS, quotaTextParts, quotaWindowName, } from './format.js';
5
+ export type { CreditBudgetCleared, CreditBudgetEntry, CreditBudgetReading, QuotaAbsentEntry, QuotaEntry, QuotaMap, QuotaReadingEntry, QuotaRetiredEntry, } from './map.js';
6
+ export { ALL_SCOPE, DEFAULT_REQUIRED_LABELS, emptyQuotaMap, isQuotaMap, } from './map.js';
7
+ export type { ObservedBudget, ObservedPair, ObservedReading, QuotaObservation, } from './merge.js';
8
+ export { isQuotaObservation, mergeQuotaObservation, QuotaCodecError, } from './merge.js';
9
+ export type { ExhaustionReset, ProjectedBudget, ProjectedLimit, ProjectedQuota, } from './projection.js';
10
+ export { budgetExhaustedResetAt, futureResetAt, projectQuota, readsExhausted, } from './projection.js';
@@ -0,0 +1,5 @@
1
+ export { quotaCodec } from './codec.js';
2
+ export { formatQuota, NO_LIMITS_REPORTED, NO_QUOTA_READING, QUOTA_STALE_AFTER_MS, quotaTextParts, quotaWindowName, } from './format.js';
3
+ export { ALL_SCOPE, DEFAULT_REQUIRED_LABELS, emptyQuotaMap, isQuotaMap, } from './map.js';
4
+ export { isQuotaObservation, mergeQuotaObservation, QuotaCodecError, } from './merge.js';
5
+ export { budgetExhaustedResetAt, futureResetAt, projectQuota, readsExhausted, } from './projection.js';