@cortexkit/opencode-openai-auth 0.11.1 → 1.0.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 (204) hide show
  1. package/README.md +15 -26
  2. package/dist/index-766sfrh5.js +1 -0
  3. package/dist/index-8kj7sa1p.js +12 -0
  4. package/dist/index-8rqfbhfw.js +221 -0
  5. package/dist/index-b3q3j39x.js +2 -0
  6. package/dist/index-bmd7x5aq.js +2 -0
  7. package/dist/index-eqh53k8v.js +2 -0
  8. package/dist/{index-sdt2d2ra.js → index-hy9030jh.js} +1 -1
  9. package/dist/index-jyr8y8k4.js +7 -0
  10. package/dist/index-rv08jmd4.js +2 -0
  11. package/dist/index.d.ts +578 -145
  12. package/dist/index.js +1 -110
  13. package/dist/pool-lifecycle-DbDmgabd.d.ts +2430 -0
  14. package/dist/port-file-Do3lQIEk.d.ts +29 -0
  15. package/dist/protocol-BOBdkqqm.d.ts +198 -0
  16. package/dist/rpc/port-file.d.ts +4 -17
  17. package/dist/rpc/port-file.js +1 -1
  18. package/dist/rpc/protocol.d.ts +2 -2
  19. package/dist/rpc/protocol.js +1 -0
  20. package/dist/rpc/rpc-client.d.ts +5 -4
  21. package/dist/rpc/rpc-client.js +1 -1
  22. package/dist/rpc/rpc-dir.d.ts +2 -0
  23. package/dist/rpc/rpc-dir.js +1 -1
  24. package/dist/sidebar-state-D6cUKgkm.d.ts +385 -0
  25. package/dist/sidebar-state.d.ts +2 -2
  26. package/dist/sidebar-state.js +1 -1
  27. package/dist/token-util-wdza526f.js +1 -0
  28. package/dist/{token-9823y0pc.js → token-ypk61xj0.js} +1 -1
  29. package/dist/tui-preferences.js +5 -2
  30. package/dist/tui.d.ts +9 -4
  31. package/dist/v2/server.d.ts +54 -0
  32. package/dist/v2/server.js +4 -0
  33. package/package.json +21 -9
  34. package/src/tui/entry.mjs +26 -25
  35. package/src/tui-compiled/raw/logger.js +24 -0
  36. package/src/tui-compiled/raw/rpc/protocol.js +3 -0
  37. package/src/tui-compiled/raw/rpc/rpc-client.js +7 -0
  38. package/src/tui-compiled/raw/rpc/rpc-dir.js +27 -0
  39. package/src/tui-compiled/raw/selector.js +21 -0
  40. package/src/tui-compiled/raw/shared/0332701351b3eeec-format.js +138 -0
  41. package/src/tui-compiled/raw/shared/04a6357627de5ed0-errors.js +38 -0
  42. package/src/tui-compiled/raw/shared/053cc17b5419261e-codec.js +12 -0
  43. package/src/tui-compiled/raw/shared/0766d78c6f481f18-watcher.js +157 -0
  44. package/src/tui-compiled/raw/shared/08eef45626a94992-reserved.js +13 -0
  45. package/src/tui-compiled/raw/shared/39c3cb05738fcf6d-projection.js +134 -0
  46. package/src/tui-compiled/raw/shared/3c2d4169d00f907e-tui-support.js +5 -0
  47. package/src/tui-compiled/raw/shared/3e7d45a77c6ad4d5-index.js +4 -0
  48. package/src/tui-compiled/raw/shared/415792e85d80819e-capture-sink.js +13 -0
  49. package/src/tui-compiled/raw/shared/42ae93ab554fcec2-open-url.js +28 -0
  50. package/src/tui-compiled/raw/shared/43fc9e6b994519c7-refresh-lock.js +136 -0
  51. package/src/tui-compiled/raw/shared/458b934e92490331-mutate.js +500 -0
  52. package/src/tui-compiled/raw/shared/45bf5343bbbf9add-admission.js +142 -0
  53. package/src/tui-compiled/raw/shared/46f3dff1266160e8-client.js +2 -0
  54. package/src/tui-compiled/raw/shared/4825e03209d6ae3e-with-lock.js +48 -0
  55. package/src/tui-compiled/raw/shared/4f36d333d081d040-merge.js +162 -0
  56. package/src/tui-compiled/raw/shared/54eba94fd64c8c3c-index.js +7 -0
  57. package/src/tui-compiled/raw/shared/7211cfec06a298bc-atomic-write.js +80 -0
  58. package/src/tui-compiled/raw/shared/754d6017f8e419cf-builtins.js +534 -0
  59. package/src/tui-compiled/raw/shared/75bd8c3bd63b315f-identity.js +79 -0
  60. package/src/tui-compiled/raw/shared/7a91bb630e6f4804-index.js +1 -0
  61. package/src/tui-compiled/raw/shared/826342a4b6a8b89b-model.js +16 -0
  62. package/src/tui-compiled/raw/shared/83068c0dbe8a6f13-publication.js +304 -0
  63. package/src/tui-compiled/raw/shared/836fc615de6afbd9-settings.js +120 -0
  64. package/src/tui-compiled/raw/shared/83bc3ba9d0fe2761-runtime.js +68 -0
  65. package/src/tui-compiled/raw/shared/8a18abe4fa2977c8-menu.js +300 -0
  66. package/src/tui-compiled/raw/shared/901e149c5a0dbbff-sticky.js +339 -0
  67. package/src/tui-compiled/raw/shared/9020c407ce64f672-ordered.js +58 -0
  68. package/src/tui-compiled/raw/shared/905bffc0220f43d0-map.js +105 -0
  69. package/src/tui-compiled/raw/shared/94820e324f2b4c09-seam.js +301 -0
  70. package/src/tui-compiled/raw/shared/97484d9bb98c705b-provider-state.js +226 -0
  71. package/src/tui-compiled/raw/shared/9968e3cade6969a1-index.js +2 -0
  72. package/src/tui-compiled/raw/shared/9b4da614c45f9cd0-lock-constants.js +14 -0
  73. package/src/tui-compiled/raw/shared/9d3fc9e37456504e-refresh.js +225 -0
  74. package/src/tui-compiled/raw/shared/9deae3c4da4fd63b-hooks.js +34 -0
  75. package/src/tui-compiled/raw/shared/a09b6a533c11858f-tui-preferences.js +77 -0
  76. package/src/tui-compiled/raw/shared/a62a27a204266acd-attribution.js +60 -0
  77. package/src/tui-compiled/raw/shared/a910277ac43a8626-pull.js +143 -0
  78. package/src/tui-compiled/raw/shared/b85c5301fddcb98a-paths.js +5 -0
  79. package/src/tui-compiled/raw/shared/c14988621dc60a75-port-file.js +217 -0
  80. package/src/tui-compiled/raw/shared/c64d7afbc14ca125-sidebar-file.js +157 -0
  81. package/src/tui-compiled/raw/shared/c6b176708c446d6d-pins.js +34 -0
  82. package/src/tui-compiled/raw/shared/c7d252bedee01600-pi.js +206 -0
  83. package/src/tui-compiled/raw/shared/c852a15cfe489d12-pool.js +120 -0
  84. package/src/tui-compiled/raw/shared/cc9e0702a752cea7-index.js +3 -0
  85. package/src/tui-compiled/raw/shared/cd94316a888b0a8e-schema.js +754 -0
  86. package/src/tui-compiled/raw/shared/d319517e3ea6ad70-redact.js +83 -0
  87. package/src/tui-compiled/raw/shared/d5a8f1061ee2f114-index.js +5 -0
  88. package/src/tui-compiled/raw/shared/d669d72c40e9e116-logger.js +19 -0
  89. package/src/tui-compiled/raw/shared/d7dcba96fea1685c-engine.js +246 -0
  90. package/src/tui-compiled/raw/shared/dbef697d1df1cd84-index.js +5 -0
  91. package/src/tui-compiled/raw/shared/e4acc4cb713f300c-protocol.js +3 -0
  92. package/src/tui-compiled/raw/shared/e8cd4929507508d8-refresh-file-lock.js +501 -0
  93. package/src/tui-compiled/raw/shared/ec971142548df90e-index.js +6 -0
  94. package/src/tui-compiled/raw/shared/f37fcefc7aedbf25-error.js +11 -0
  95. package/src/tui-compiled/raw/shared/fbcdcd0f925e74c0-rpc-client.js +253 -0
  96. package/src/tui-compiled/raw/shared/fd45e82f2f9c0a0d-torn.js +294 -0
  97. package/src/tui-compiled/raw/shared/fe02abeadc535f77-rows.js +1258 -0
  98. package/src/tui-compiled/raw/sidebar-state.js +960 -0
  99. package/src/tui-compiled/raw/tui/command-dialogs.tsx +424 -0
  100. package/src/tui-compiled/raw/tui-preferences.js +135 -0
  101. package/src/tui-compiled/raw/tui.tsx +135 -143
  102. package/src/tui-compiled/runtime/logger.js +24 -0
  103. package/src/tui-compiled/runtime/rpc/protocol.js +3 -0
  104. package/src/tui-compiled/runtime/rpc/rpc-client.js +7 -0
  105. package/src/tui-compiled/runtime/rpc/rpc-dir.js +27 -0
  106. package/src/tui-compiled/runtime/selector.js +21 -0
  107. package/src/tui-compiled/runtime/shared/0332701351b3eeec-format.js +138 -0
  108. package/src/tui-compiled/runtime/shared/04a6357627de5ed0-errors.js +38 -0
  109. package/src/tui-compiled/runtime/shared/053cc17b5419261e-codec.js +12 -0
  110. package/src/tui-compiled/runtime/shared/0766d78c6f481f18-watcher.js +157 -0
  111. package/src/tui-compiled/runtime/shared/08eef45626a94992-reserved.js +13 -0
  112. package/src/tui-compiled/runtime/shared/39c3cb05738fcf6d-projection.js +134 -0
  113. package/src/tui-compiled/runtime/shared/3c2d4169d00f907e-tui-support.js +5 -0
  114. package/src/tui-compiled/runtime/shared/3e7d45a77c6ad4d5-index.js +4 -0
  115. package/src/tui-compiled/runtime/shared/415792e85d80819e-capture-sink.js +13 -0
  116. package/src/tui-compiled/runtime/shared/42ae93ab554fcec2-open-url.js +28 -0
  117. package/src/tui-compiled/runtime/shared/43fc9e6b994519c7-refresh-lock.js +136 -0
  118. package/src/tui-compiled/runtime/shared/458b934e92490331-mutate.js +500 -0
  119. package/src/tui-compiled/runtime/shared/45bf5343bbbf9add-admission.js +142 -0
  120. package/src/tui-compiled/runtime/shared/46f3dff1266160e8-client.js +2 -0
  121. package/src/tui-compiled/runtime/shared/4825e03209d6ae3e-with-lock.js +48 -0
  122. package/src/tui-compiled/runtime/shared/4f36d333d081d040-merge.js +162 -0
  123. package/src/tui-compiled/runtime/shared/54eba94fd64c8c3c-index.js +7 -0
  124. package/src/tui-compiled/runtime/shared/7211cfec06a298bc-atomic-write.js +80 -0
  125. package/src/tui-compiled/runtime/shared/754d6017f8e419cf-builtins.js +534 -0
  126. package/src/tui-compiled/runtime/shared/75bd8c3bd63b315f-identity.js +79 -0
  127. package/src/tui-compiled/runtime/shared/7a91bb630e6f4804-index.js +1 -0
  128. package/src/tui-compiled/runtime/shared/826342a4b6a8b89b-model.js +16 -0
  129. package/src/tui-compiled/runtime/shared/83068c0dbe8a6f13-publication.js +304 -0
  130. package/src/tui-compiled/runtime/shared/836fc615de6afbd9-settings.js +120 -0
  131. package/src/tui-compiled/runtime/shared/83bc3ba9d0fe2761-runtime.js +68 -0
  132. package/src/tui-compiled/runtime/shared/8a18abe4fa2977c8-menu.js +300 -0
  133. package/src/tui-compiled/runtime/shared/901e149c5a0dbbff-sticky.js +339 -0
  134. package/src/tui-compiled/runtime/shared/9020c407ce64f672-ordered.js +58 -0
  135. package/src/tui-compiled/runtime/shared/905bffc0220f43d0-map.js +105 -0
  136. package/src/tui-compiled/runtime/shared/94820e324f2b4c09-seam.js +301 -0
  137. package/src/tui-compiled/runtime/shared/97484d9bb98c705b-provider-state.js +226 -0
  138. package/src/tui-compiled/runtime/shared/9968e3cade6969a1-index.js +2 -0
  139. package/src/tui-compiled/runtime/shared/9b4da614c45f9cd0-lock-constants.js +14 -0
  140. package/src/tui-compiled/runtime/shared/9d3fc9e37456504e-refresh.js +225 -0
  141. package/src/tui-compiled/runtime/shared/9deae3c4da4fd63b-hooks.js +34 -0
  142. package/src/tui-compiled/runtime/shared/a09b6a533c11858f-tui-preferences.js +77 -0
  143. package/src/tui-compiled/runtime/shared/a62a27a204266acd-attribution.js +60 -0
  144. package/src/tui-compiled/runtime/shared/a910277ac43a8626-pull.js +143 -0
  145. package/src/tui-compiled/runtime/shared/b85c5301fddcb98a-paths.js +5 -0
  146. package/src/tui-compiled/runtime/shared/c14988621dc60a75-port-file.js +217 -0
  147. package/src/tui-compiled/runtime/shared/c64d7afbc14ca125-sidebar-file.js +157 -0
  148. package/src/tui-compiled/runtime/shared/c6b176708c446d6d-pins.js +34 -0
  149. package/src/tui-compiled/runtime/shared/c7d252bedee01600-pi.js +206 -0
  150. package/src/tui-compiled/runtime/shared/c852a15cfe489d12-pool.js +120 -0
  151. package/src/tui-compiled/runtime/shared/cc9e0702a752cea7-index.js +3 -0
  152. package/src/tui-compiled/runtime/shared/cd94316a888b0a8e-schema.js +754 -0
  153. package/src/tui-compiled/runtime/shared/d319517e3ea6ad70-redact.js +83 -0
  154. package/src/tui-compiled/runtime/shared/d5a8f1061ee2f114-index.js +5 -0
  155. package/src/tui-compiled/runtime/shared/d669d72c40e9e116-logger.js +19 -0
  156. package/src/tui-compiled/runtime/shared/d7dcba96fea1685c-engine.js +246 -0
  157. package/src/tui-compiled/runtime/shared/dbef697d1df1cd84-index.js +5 -0
  158. package/src/tui-compiled/runtime/shared/e4acc4cb713f300c-protocol.js +3 -0
  159. package/src/tui-compiled/runtime/shared/e8cd4929507508d8-refresh-file-lock.js +501 -0
  160. package/src/tui-compiled/runtime/shared/ec971142548df90e-index.js +6 -0
  161. package/src/tui-compiled/runtime/shared/f37fcefc7aedbf25-error.js +11 -0
  162. package/src/tui-compiled/runtime/shared/fbcdcd0f925e74c0-rpc-client.js +253 -0
  163. package/src/tui-compiled/runtime/shared/fd45e82f2f9c0a0d-torn.js +294 -0
  164. package/src/tui-compiled/runtime/shared/fe02abeadc535f77-rows.js +1258 -0
  165. package/src/tui-compiled/runtime/sidebar-state.js +960 -0
  166. package/src/tui-compiled/runtime/tui/command-dialogs.js +378 -0
  167. package/src/tui-compiled/runtime/tui-preferences.js +135 -0
  168. package/src/tui-compiled/runtime/{tui.tsx → tui.js} +288 -316
  169. package/dist/index-4effjmr6.js +0 -2
  170. package/dist/index-9ehhb051.js +0 -2
  171. package/dist/index-g3hsdjh4.js +0 -2
  172. package/dist/index-hh52v8sb.js +0 -2
  173. package/dist/index-jwn4r3sw.js +0 -1
  174. package/dist/index-sq36b120.js +0 -211
  175. package/dist/internal-BTndPmW2.d.ts +0 -914
  176. package/dist/quota-normalize-08hpp51v.js +0 -1
  177. package/dist/sidebar-state-CoKYKcLn.d.ts +0 -220
  178. package/dist/token-util-ypgz7rge.js +0 -1
  179. package/src/tui-compiled/raw/core-account-paths.ts +0 -114
  180. package/src/tui-compiled/raw/core-custody-state.ts +0 -243
  181. package/src/tui-compiled/raw/logger.ts +0 -53
  182. package/src/tui-compiled/raw/rpc-port-file.ts +0 -177
  183. package/src/tui-compiled/raw/rpc-protocol.ts +0 -12
  184. package/src/tui-compiled/raw/rpc-rpc-client.ts +0 -82
  185. package/src/tui-compiled/raw/rpc-rpc-dir.ts +0 -46
  186. package/src/tui-compiled/raw/sidebar-state.ts +0 -1659
  187. package/src/tui-compiled/raw/tui-command-dialogs.tsx +0 -1096
  188. package/src/tui-compiled/raw/tui-preferences.ts +0 -328
  189. package/src/tui-compiled/runtime/core-account-paths.ts +0 -114
  190. package/src/tui-compiled/runtime/core-custody-state.ts +0 -243
  191. package/src/tui-compiled/runtime/logger.ts +0 -53
  192. package/src/tui-compiled/runtime/rpc-port-file.ts +0 -177
  193. package/src/tui-compiled/runtime/rpc-protocol.ts +0 -12
  194. package/src/tui-compiled/runtime/rpc-rpc-client.ts +0 -82
  195. package/src/tui-compiled/runtime/rpc-rpc-dir.ts +0 -46
  196. package/src/tui-compiled/runtime/sidebar-state.ts +0 -1659
  197. package/src/tui-compiled/runtime/tui-command-dialogs.tsx +0 -855
  198. package/src/tui-compiled/runtime/tui-preferences.ts +0 -328
  199. package/src/tui-compiled/shared/internal.ts +0 -6
  200. package/src/tui-compiled/shared/logger.ts +0 -264
  201. package/src/tui-compiled/shared/paths.ts +0 -33
  202. package/src/tui-compiled/shared/refresh-file-lock.ts +0 -384
  203. package/src/tui-compiled/shared/util/error.ts +0 -9
  204. package/src/tui-compiled/shared/util/open-url.ts +0 -30
@@ -0,0 +1,253 @@
1
+ import { open } from "node:fs/promises";
2
+ import { connect } from "node:net";
3
+ import { join, resolve } from "node:path";
4
+ import { discoverPortFile, portFileIdentity, } from "./c14988621dc60a75-port-file.js";
5
+ export const DEFAULT_RPC_TIMEOUT_MS = 2_000;
6
+ const selections = new Map();
7
+ const discoveries = new Map();
8
+ // The identity a cached selection is checked against must belong to the bytes
9
+ // that named this server. Statting the path after discovery is not enough: the
10
+ // file can be replaced in between, which would pin the old server to the new
11
+ // file's identity. So read and fstat one open descriptor, and bind only when
12
+ // those bytes still name the discovered entry.
13
+ async function boundIdentity(dir, entry) {
14
+ const path = join(resolve(dir), `port-${entry.pid}.json`);
15
+ let handle;
16
+ try {
17
+ handle = await open(path, 'r');
18
+ const info = await handle.stat();
19
+ const current = JSON.parse(await handle.readFile('utf8'));
20
+ if (current.pid !== entry.pid ||
21
+ current.port !== entry.port ||
22
+ current.token !== entry.token)
23
+ return null;
24
+ return `${path}:${info.dev}:${info.ino}:${info.mtimeMs}:${info.size}`;
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ finally {
30
+ await handle?.close().catch(() => { });
31
+ }
32
+ }
33
+ async function select(key, dir, expectedPid, options) {
34
+ const cached = selections.get(key);
35
+ if (cached && (await portFileIdentity(dir, cached.entry)) === cached.identity)
36
+ return cached;
37
+ selections.delete(key);
38
+ const pending = discoveries.get(key);
39
+ if (pending)
40
+ return pending;
41
+ const discovery = (async () => {
42
+ const entry = await (options.discover ?? discoverPortFile)(dir, expectedPid, options);
43
+ if (!entry)
44
+ return null;
45
+ const identity = await boundIdentity(dir, entry);
46
+ // The file changed under discovery: use what was discovered for this
47
+ // call, as an uncached selection, so the next call discovers again.
48
+ if (!identity)
49
+ return { entry, identity: '' };
50
+ const selected = { entry, identity };
51
+ selections.set(key, selected);
52
+ return selected;
53
+ })();
54
+ discoveries.set(key, discovery);
55
+ try {
56
+ return await discovery;
57
+ }
58
+ finally {
59
+ if (discoveries.get(key) === discovery)
60
+ discoveries.delete(key);
61
+ }
62
+ }
63
+ async function request(entry, method, params, timeoutMs = DEFAULT_RPC_TIMEOUT_MS) {
64
+ // A raw loopback socket never consults runtime HTTP proxy settings, which
65
+ // otherwise can expose the bearer token to a configured proxy. timeoutMs is
66
+ // a total deadline for connect, request and the full response, not idle time.
67
+ return new Promise((resolve) => {
68
+ let socket;
69
+ let settled = false;
70
+ let connected = false;
71
+ const done = (value, stale = false) => {
72
+ if (settled)
73
+ return;
74
+ settled = true;
75
+ clearTimeout(timer);
76
+ socket?.destroy();
77
+ resolve({ value, stale });
78
+ };
79
+ const timer = setTimeout(() => done(null), timeoutMs);
80
+ try {
81
+ const body = JSON.stringify(params);
82
+ socket = connect({ host: '127.0.0.1', port: entry.port });
83
+ socket.on('connect', () => {
84
+ connected = true;
85
+ // HTTP/1.0 avoids chunked responses; explicitly request connection close
86
+ // because older Bun servers can keep delayed HTTP/1.0 replies open.
87
+ socket?.write(`POST /rpc/${method} HTTP/1.0\r\n` +
88
+ `Host: 127.0.0.1:${entry.port}\r\n` +
89
+ 'Connection: close\r\n' +
90
+ 'Content-Type: application/json\r\n' +
91
+ `Content-Length: ${Buffer.byteLength(body)}\r\n` +
92
+ `Authorization: Bearer ${entry.token}\r\n\r\n` +
93
+ body);
94
+ });
95
+ const chunks = [];
96
+ let bytes = 0;
97
+ let headerBytes;
98
+ let contentLength;
99
+ let prefix = Buffer.alloc(0);
100
+ const finishBody = () => {
101
+ if (headerBytes === undefined)
102
+ return done(null);
103
+ try {
104
+ const payload = Buffer.concat(chunks).subarray(headerBytes);
105
+ done(JSON.parse(payload.toString('utf8')));
106
+ }
107
+ catch {
108
+ done(null);
109
+ }
110
+ };
111
+ socket.on('data', (chunk) => {
112
+ if (settled)
113
+ return;
114
+ bytes += chunk.length;
115
+ chunks.push(chunk);
116
+ if (headerBytes === undefined) {
117
+ prefix = Buffer.concat([prefix, chunk]);
118
+ const separator = prefix.indexOf('\r\n\r\n');
119
+ if (separator >= 0)
120
+ headerBytes = separator + 4;
121
+ if ((headerBytes ?? prefix.length) > 16 * 1024)
122
+ return done(null);
123
+ if (headerBytes !== undefined) {
124
+ const headers = prefix
125
+ .subarray(0, separator)
126
+ .toString('latin1')
127
+ .split('\r\n');
128
+ const status = /^HTTP\/1\.[01] (\d{3})(?: |$)/.exec(headers[0] ?? '');
129
+ if (!status)
130
+ return done(null);
131
+ const code = Number(status[1]);
132
+ if (code < 200 || code >= 300)
133
+ // Only statuses meaning "this is not the server you meant" may
134
+ // rediscover and resend. A 504 is this server's apply deadline:
135
+ // the handler can still be running, so resending it to another
136
+ // server could run the command twice. 5xx is never retried.
137
+ return done(null, [401, 403, 404, 410].includes(code));
138
+ for (const header of headers.slice(1)) {
139
+ const colon = header.indexOf(':');
140
+ const name = header.slice(0, colon).toLowerCase();
141
+ const value = header.slice(colon + 1).trim();
142
+ if (name === 'transfer-encoding')
143
+ return done(null);
144
+ if (name === 'content-length') {
145
+ if (contentLength !== undefined || !/^\d+$/.test(value))
146
+ return done(null);
147
+ contentLength = Number(value);
148
+ if (!Number.isSafeInteger(contentLength) ||
149
+ contentLength > 8 * 1024 * 1024)
150
+ return done(null);
151
+ }
152
+ }
153
+ prefix = Buffer.alloc(0);
154
+ }
155
+ }
156
+ const bodyBytes = bytes - (headerBytes ?? bytes);
157
+ if (bodyBytes > 8 * 1024 * 1024)
158
+ return done(null);
159
+ if (contentLength !== undefined) {
160
+ if (bodyBytes > contentLength)
161
+ return done(null);
162
+ // Bun 1.3.14 can keep an async HTTP/1.0 reply open after sending
163
+ // its complete Content-Length body; don't wait for EOF in that case.
164
+ if (bodyBytes === contentLength)
165
+ finishBody();
166
+ }
167
+ });
168
+ socket.on('end', () => {
169
+ if (contentLength !== undefined &&
170
+ bytes - (headerBytes ?? 0) !== contentLength)
171
+ return done(null);
172
+ finishBody();
173
+ });
174
+ socket.on('error', () => done(null, !connected));
175
+ socket.on('close', () => done(null));
176
+ }
177
+ catch {
178
+ done(null, !connected);
179
+ }
180
+ });
181
+ }
182
+ async function call(dir, expectedPid, options, onSelected, method, params, timeoutMs = DEFAULT_RPC_TIMEOUT_MS) {
183
+ const key = JSON.stringify([
184
+ resolve(dir),
185
+ expectedPid ?? null,
186
+ options.exactPid === true,
187
+ ]);
188
+ const selected = await select(key, dir, expectedPid, options);
189
+ onSelected(selected?.entry ?? null);
190
+ if (!selected)
191
+ return null;
192
+ const deadline = Date.now() + timeoutMs;
193
+ const out = await request(selected.entry, method, params, timeoutMs);
194
+ if (!out.stale)
195
+ return out.value;
196
+ if (selections.get(key) === selected)
197
+ selections.delete(key);
198
+ // Retry only definite pre-dispatch/stale-server failures, never an ambiguous
199
+ // response or timeout that could replay an already executed apply command.
200
+ const replacement = await select(key, dir, expectedPid, options);
201
+ if (!replacement || Date.now() >= deadline)
202
+ return null;
203
+ if (replacement.entry.port === selected.entry.port &&
204
+ replacement.entry.pid === selected.entry.pid &&
205
+ replacement.entry.token === selected.entry.token) {
206
+ if (selections.get(key) === replacement)
207
+ selections.delete(key);
208
+ return null;
209
+ }
210
+ const retry = await request(replacement.entry, method, params, deadline - Date.now());
211
+ if (retry.stale && selections.get(key) === replacement)
212
+ selections.delete(key);
213
+ return retry.value;
214
+ }
215
+ /**
216
+ * A client for the server in `dir`, preferring `expectedPid`'s.
217
+ *
218
+ * `onSelected` reports the first selection: it is told which entry (or null)
219
+ * discovery chose, until one call of it returns normally. It is a report, not
220
+ * a gate: an observer that throws rejects that call before any request is
221
+ * sent and is asked again on the next call, but nothing stops a later call
222
+ * once an observer has returned. A caller that must never reach another
223
+ * server passes `{ exactPid: true }`. Validated selections are shared by
224
+ * directory, expected PID and exactPid until their file identity changes or a
225
+ * connect/auth/stale-server failure triggers one rediscovery in the same call.
226
+ */
227
+ export function createRpcClient(dir, expectedPid, onSelected, options = {}) {
228
+ const discoverOptions = {
229
+ exactPid: options.exactPid,
230
+ discover: options.discover,
231
+ };
232
+ let reportedSelection = false;
233
+ const reportSelected = (entry) => {
234
+ if (reportedSelection)
235
+ return;
236
+ onSelected?.(entry);
237
+ // Counted only after the observer returns, so a throwing one is not
238
+ // silently skipped on later calls.
239
+ reportedSelection = true;
240
+ };
241
+ return {
242
+ async pending(lastReceivedId, sessionId, timeoutMs) {
243
+ const out = await call(dir, expectedPid, discoverOptions, reportSelected, 'pending-notifications', { lastReceivedId, sessionId }, timeoutMs);
244
+ return out?.messages ?? [];
245
+ },
246
+ async apply(request, timeoutMs) {
247
+ const out = await call(dir, expectedPid, discoverOptions, reportSelected, 'apply', {
248
+ ...request,
249
+ }, timeoutMs);
250
+ return out ?? { text: 'apply failed', knobs: {} };
251
+ },
252
+ };
253
+ }
@@ -0,0 +1,294 @@
1
+ import { disableIdentityDuplicates, disableIn, enableIn, IDENTITY_CONTRADICTED_REASON_PREFIX, recordIdentityIn, } from "./75bd8c3bd63b315f-identity.js";
2
+ import { buildRawRows, CREDENTIAL_STAMP_KEY, credentialDigest, dispatchDigest, entryIn, isRecord, parseStamp, rosterRowIn, setEntryIn, } from "./cd94316a888b0a8e-schema.js";
3
+ /*
4
+ * A replace changes both files: the state file gets the new credential and
5
+ * the config gets the new epoch, identity or endpoint. It writes the state
6
+ * file first, stamping the credential with the epoch it belongs to and the
7
+ * binding (identity, endpoint) the config is about to get. A crash between
8
+ * the two writes therefore leaves a credential whose stamp is ahead of the
9
+ * config's epoch, and that is how every reader tells such a torn row from a
10
+ * whole one. The stamp holds everything the config write would have written,
11
+ * so a torn row is completed forward: readers are shown the completed row
12
+ * (never a candidate until it is on disk) and the next store write on it
13
+ * writes the config to match.
14
+ *
15
+ * A stamp counts only beside the credential it was written with (its digest
16
+ * matches): a writer that does not know about stamps may put another
17
+ * credential beside an old one, and that stamp then says nothing. A stamp at
18
+ * or behind the config's epoch is never torn as a replace: this store's own
19
+ * writes leave the state file ahead of the config, never behind it.
20
+ *
21
+ * Only a replace's stamp is ever completed as a replace. Since 0.4.4 every
22
+ * stamp carries a binding, so a replace's says so itself (`replace: true`); a
23
+ * stamp written by 0.4.3 or earlier has no dispatch digest, and those
24
+ * versions wrote a binding only on replace, so such a stamp with a binding is
25
+ * a replace's. A stamp from any other write that sits ahead of the config was
26
+ * not left by a crash (those writes keep the row's epoch), and completing it
27
+ * would rewrite the row's identity and drop its quota on a foreign writer's
28
+ * say-so. With `requireCredentialStamps` a 0.4.3-shaped replace stamp is not
29
+ * completed either: it proves nothing about the token sent, so the strict
30
+ * store leaves the row as it is on disk (`legacy`, unbound) rather than act
31
+ * on it; a store without the option completes it as before.
32
+ *
33
+ * A strict store also completes a replace only when its stamp describes what
34
+ * the completed row would send: the stamp's dispatch digest must be that of
35
+ * the credential beside it, with an API key moved to the endpoint the binding
36
+ * names. The lineage digest alone covers the refresh token or API key, so
37
+ * without this check another writer that changed only the access token, the
38
+ * expiry or the endpoint would still have the strict store rewrite the row's
39
+ * epoch, identity and quota before refusing the row as unbound. A store
40
+ * without the option never compared dispatch digests here and still does not.
41
+ *
42
+ * A write that gives a row its first identity (`recordIdentity`, or a
43
+ * `rotate` or refresh that learns one) follows the same order: the stamp
44
+ * naming the identity first, then the config recording it. A crash between
45
+ * leaves a stamp at the row's epoch, matching the credential beside it
46
+ * exactly (digest and dispatch digest), that names an identity the config
47
+ * does not record; that is completed forward the same way, by recording the
48
+ * identity.
49
+ *
50
+ * An attributed `disable` or `enable` that also changes the provider state
51
+ * follows the same order. Its state write carries the new value and, in the
52
+ * stamp, the transition (`transition: {mark, enabled, reason?}`, see
53
+ * `StampedTransition`); its config write then flips the row and records the
54
+ * transition's mark in the row's entry (`transitionMark`). A stamp bound to
55
+ * the row (this credential, epoch and identity, the binding that also shows
56
+ * the provider state beside it) whose transition mark the entry does not
57
+ * record is such a write stopped between its two writes, and is completed
58
+ * forward the same way: readers are shown the row disabled or enabled as the
59
+ * transition says, beside the value written with it. Once the config records
60
+ * the mark the transition says nothing more, so a later `enable` or
61
+ * `disable` of the row is never undone by it; a credential write drops it
62
+ * with the rest of the old stamp.
63
+ */
64
+ /** Key, inside a credential stamp, of an attributed enable or disable. */
65
+ export const TRANSITION_STAMP_KEY = 'transition';
66
+ /**
67
+ * Key, inside a per-row config entry, of the mark of the last transition the
68
+ * config carries out. Older readers ignore it.
69
+ */
70
+ export const TRANSITION_MARK_KEY = 'transitionMark';
71
+ /** The well-formed transition in a raw stamp, or undefined. */
72
+ function stampedTransition(rawStamp) {
73
+ if (!isRecord(rawStamp))
74
+ return undefined;
75
+ const raw = rawStamp[TRANSITION_STAMP_KEY];
76
+ if (!isRecord(raw))
77
+ return undefined;
78
+ if (typeof raw.mark !== 'string' || raw.mark.length === 0)
79
+ return undefined;
80
+ if (raw.enabled === true)
81
+ return { mark: raw.mark, enabled: true };
82
+ if (raw.enabled === false && typeof raw.reason === 'string')
83
+ return { mark: raw.mark, enabled: false, reason: raw.reason };
84
+ return undefined;
85
+ }
86
+ /**
87
+ * Carries a transition out in a config being edited: the row's enabled flag
88
+ * and reason as the transition says, and its mark recorded in the entry (a
89
+ * row without an entry gets one at epoch 1, as `disableIn` gives it). An
90
+ * enable then applies the duplicate-identity rule, as every write that
91
+ * enables an OAuth row with a known identity does: the earlier row in roster
92
+ * order keeps the identity.
93
+ */
94
+ export function applyTransition(editor, id, transition) {
95
+ if (transition.enabled)
96
+ enableIn(editor, id);
97
+ else
98
+ disableIn(editor, id, transition.reason ?? '');
99
+ if (!editor.rosterRow(id))
100
+ return;
101
+ const entry = editor.entry(id) ?? {
102
+ credentialEpoch: 1,
103
+ needsFirstReading: true,
104
+ };
105
+ editor.setEntry(id, { ...entry, [TRANSITION_MARK_KEY]: transition.mark });
106
+ if (!transition.enabled)
107
+ return;
108
+ const row = editor.rows().find((candidate) => candidate.id === id);
109
+ if (row?.type === 'oauth' && row.identity !== undefined)
110
+ disableIdentityDuplicates(editor, row.identity);
111
+ }
112
+ /**
113
+ * The credential a torn replace leaves once its config write lands: the
114
+ * credential loaded beside the stamp, with the endpoint the stamp's binding
115
+ * names. Only an API key's endpoint lives in the config (an OAuth credential
116
+ * is entirely in the state file, which the replace has already written). A
117
+ * replace that moves a key to a new endpoint leaves the old `baseURL` and
118
+ * `authHeader` in the config until its config write, so the binding's
119
+ * `baseURL` and `authHeader` are the ones the replacement will be sent with.
120
+ */
121
+ function projectedReplacement(credential, binding) {
122
+ if (credential.type !== 'api')
123
+ return credential;
124
+ return {
125
+ ...credential,
126
+ ...(binding.baseURL !== undefined ? { baseURL: binding.baseURL } : {}),
127
+ ...(binding.authHeader !== undefined
128
+ ? { authHeader: binding.authHeader }
129
+ : {}),
130
+ };
131
+ }
132
+ /** Whether a well-formed stamp was written by a replace. */
133
+ function isReplaceStamp(stamp) {
134
+ if (!stamp.binding)
135
+ return false;
136
+ return stamp.replace === true || stamp.dispatch === undefined;
137
+ }
138
+ /** Rows left between the two writes of an operation, by row id. */
139
+ export function tornStamps(config, state, codec, options = {}) {
140
+ const accounts = isRecord(state.accounts) ? state.accounts : {};
141
+ const torn = new Map();
142
+ for (const row of buildRawRows(config, state, codec)) {
143
+ if (row.invalid || row.staged || !row.credential)
144
+ continue;
145
+ const account = Object.hasOwn(accounts, row.id)
146
+ ? accounts[row.id]
147
+ : undefined;
148
+ if (!isRecord(account))
149
+ continue;
150
+ const stamp = parseStamp(account[CREDENTIAL_STAMP_KEY]);
151
+ if (!stamp?.binding)
152
+ continue;
153
+ if (stamp.digest !== credentialDigest(row.credential))
154
+ continue;
155
+ // A row without an entry is at epoch 1, as everywhere else.
156
+ const epoch = row.credentialEpoch ?? 1;
157
+ if (stamp.credentialEpoch > epoch) {
158
+ if (!isReplaceStamp(stamp))
159
+ continue;
160
+ if (options.requireCredentialStamps) {
161
+ if (stamp.dispatch === undefined)
162
+ continue;
163
+ // `digest` covers only the refresh token or API key, so another
164
+ // writer may still have changed the access token, the expiry or the
165
+ // endpoint the stamp names. `dispatch` covers all of what a request
166
+ // sends; completing the replace when it disagrees would move the
167
+ // row's epoch, identity and quota on a stamp that does not describe
168
+ // the credential. So the replacement as it would be sent (the loaded
169
+ // credential with the binding's endpoint) must have the stamp's
170
+ // dispatch digest; otherwise the row stays as on disk (unbound).
171
+ if (stamp.dispatch !==
172
+ dispatchDigest(projectedReplacement(row.credential, stamp.binding)))
173
+ continue;
174
+ }
175
+ torn.set(row.id, {
176
+ kind: 'replace',
177
+ stamp: { ...stamp, binding: stamp.binding },
178
+ });
179
+ }
180
+ else if (stamp.credentialEpoch === epoch &&
181
+ stamp.dispatch !== undefined &&
182
+ stamp.dispatch === dispatchDigest(row.credential) &&
183
+ stamp.binding.identity !== undefined &&
184
+ row.identity === undefined) {
185
+ torn.set(row.id, { kind: 'identity', identity: stamp.binding.identity });
186
+ }
187
+ else if (stamp.credentialEpoch === epoch &&
188
+ stamp.binding.identity === row.identity) {
189
+ // The lineage digest alone misses an access-token or expiry edit. A
190
+ // strict store must not repair the config from a transition whose stamp
191
+ // no longer describes the credential; the row stays unbound instead.
192
+ if (options.requireCredentialStamps &&
193
+ stamp.dispatch !== dispatchDigest(row.credential))
194
+ continue;
195
+ const transition = stampedTransition(account[CREDENTIAL_STAMP_KEY]);
196
+ if (transition &&
197
+ entryIn(config, row.id)?.[TRANSITION_MARK_KEY] !== transition.mark)
198
+ torn.set(row.id, { kind: 'transition', transition });
199
+ }
200
+ }
201
+ return torn;
202
+ }
203
+ /**
204
+ * The config half of a replacement: the row's entry moves to the new epoch,
205
+ * loses its quota and needs a first reading; its roster row gets the new
206
+ * identity (or loses the old one) and, for an API key, the new endpoint.
207
+ * Disabling later rows that share the new identity is left to the caller
208
+ * (`disableIdentityDuplicates`), so several rows can be bound first.
209
+ */
210
+ export function bindReplacement(editor, id, credentialEpoch, binding) {
211
+ const entry = editor.entry(id) ?? {};
212
+ const next = {
213
+ ...entry,
214
+ credentialEpoch,
215
+ needsFirstReading: true,
216
+ };
217
+ delete next.quota;
218
+ // An explicit identity supplied with a replacement is the adapter's validated
219
+ // account binding. Clear its quarantine on completion, including crash recovery,
220
+ // but leave the row disabled until a separate enable.
221
+ if (binding.identity !== undefined &&
222
+ typeof next.disabledReason === 'string' &&
223
+ next.disabledReason.startsWith(IDENTITY_CONTRADICTED_REASON_PREFIX))
224
+ delete next.disabledReason;
225
+ editor.setEntry(id, next);
226
+ const raw = editor.rosterRow(id);
227
+ if (!raw)
228
+ return;
229
+ if (binding.identity !== undefined)
230
+ raw.accountId = binding.identity;
231
+ else
232
+ delete raw.accountId;
233
+ if (binding.baseURL !== undefined)
234
+ raw.baseURL = binding.baseURL;
235
+ if (binding.authHeader !== undefined)
236
+ raw.authHeader = binding.authHeader;
237
+ }
238
+ /**
239
+ * The config with every torn row completed as its interrupted write would
240
+ * have left it, and the ids completed. The config passed in is not modified;
241
+ * when nothing is torn it is returned as is.
242
+ */
243
+ export function completeTornRows(config, state, codec, options = {}) {
244
+ const completions = tornStamps(config, state, codec, options);
245
+ if (completions.size === 0)
246
+ return { config, torn: [] };
247
+ const next = structuredClone(config);
248
+ const editor = {
249
+ rows: () => buildRawRows(next, state, codec),
250
+ rosterRow: (id) => rosterRowIn(next, id),
251
+ entry: (id) => entryIn(next, id),
252
+ setEntry: (id, entry) => setEntryIn(next, id, entry),
253
+ };
254
+ for (const [id, completion] of completions)
255
+ if (completion.kind === 'replace')
256
+ bindReplacement(editor, id, completion.stamp.credentialEpoch, completion.stamp.binding);
257
+ for (const [id, completion] of completions) {
258
+ if (completion.kind === 'identity')
259
+ recordIdentityIn(editor, id, completion.identity);
260
+ else if (completion.kind === 'transition')
261
+ applyTransition(editor, id, completion.transition);
262
+ else if (completion.stamp.binding.identity !== undefined)
263
+ disableIdentityDuplicates(editor, completion.stamp.binding.identity);
264
+ }
265
+ return { config: next, torn: [...completions.keys()] };
266
+ }
267
+ /**
268
+ * The rows every reader gets. A torn row is shown completed (the identity,
269
+ * endpoint and epoch its stamp names, beside the credential it stamps), is
270
+ * marked `torn` and is never a candidate; every other row is as on disk.
271
+ * With `requireCredentialStamps`, a row whose stamp is not `bound` is marked
272
+ * `unbound` and is never a candidate either. A torn row is shown with the
273
+ * stamp of the interrupted write, which binds the completed row, so it is not
274
+ * unbound; once a store write puts its completion on disk it is no longer
275
+ * torn and is a candidate again like any other row.
276
+ */
277
+ export function loadRows(config, state, codec, options = {}) {
278
+ const { config: whole, torn } = completeTornRows(config, state, codec, options);
279
+ // A torn replace wrote its provider state beside the new credential, under
280
+ // the stamp naming the new epoch, so the completed row shows the new
281
+ // credential with the state written for it, never with the old one.
282
+ const rows = buildRawRows(whole, state, codec, options.providerState);
283
+ for (const row of rows) {
284
+ if (torn.includes(row.id)) {
285
+ row.torn = true;
286
+ row.candidate = false;
287
+ }
288
+ if (options.requireCredentialStamps && row.stamp !== 'bound') {
289
+ row.unbound = true;
290
+ row.candidate = false;
291
+ }
292
+ }
293
+ return rows;
294
+ }