@tickernelz/paperclip-pro-adapter-codex-local 2026.925.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 (268) hide show
  1. package/LICENSE +22 -0
  2. package/dist/cli/format-event.d.ts +2 -0
  3. package/dist/cli/format-event.d.ts.map +1 -0
  4. package/dist/cli/format-event.js +218 -0
  5. package/dist/cli/format-event.js.map +1 -0
  6. package/dist/cli/index.d.ts +2 -0
  7. package/dist/cli/index.d.ts.map +1 -0
  8. package/dist/cli/index.js +2 -0
  9. package/dist/cli/index.js.map +1 -0
  10. package/dist/cli/quota-probe.d.ts +3 -0
  11. package/dist/cli/quota-probe.d.ts.map +1 -0
  12. package/dist/cli/quota-probe.js +104 -0
  13. package/dist/cli/quota-probe.js.map +1 -0
  14. package/dist/index.d.ts +21 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +155 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/index.test.d.ts +2 -0
  19. package/dist/index.test.d.ts.map +1 -0
  20. package/dist/index.test.js +58 -0
  21. package/dist/index.test.js.map +1 -0
  22. package/dist/server/acp.d.ts +30 -0
  23. package/dist/server/acp.d.ts.map +1 -0
  24. package/dist/server/acp.js +532 -0
  25. package/dist/server/acp.js.map +1 -0
  26. package/dist/server/acp.test.d.ts +2 -0
  27. package/dist/server/acp.test.d.ts.map +1 -0
  28. package/dist/server/acp.test.js +1087 -0
  29. package/dist/server/acp.test.js.map +1 -0
  30. package/dist/server/adapter-auth-promotion.d.ts +102 -0
  31. package/dist/server/adapter-auth-promotion.d.ts.map +1 -0
  32. package/dist/server/adapter-auth-promotion.js +210 -0
  33. package/dist/server/adapter-auth-promotion.js.map +1 -0
  34. package/dist/server/adapter-auth-promotion.test.d.ts +2 -0
  35. package/dist/server/adapter-auth-promotion.test.d.ts.map +1 -0
  36. package/dist/server/adapter-auth-promotion.test.js +658 -0
  37. package/dist/server/adapter-auth-promotion.test.js.map +1 -0
  38. package/dist/server/auth-check.d.ts +8 -0
  39. package/dist/server/auth-check.d.ts.map +1 -0
  40. package/dist/server/auth-check.js +8 -0
  41. package/dist/server/auth-check.js.map +1 -0
  42. package/dist/server/auth-precedence.d.ts +16 -0
  43. package/dist/server/auth-precedence.d.ts.map +1 -0
  44. package/dist/server/auth-precedence.js +20 -0
  45. package/dist/server/auth-precedence.js.map +1 -0
  46. package/dist/server/auth-precedence.test.d.ts +2 -0
  47. package/dist/server/auth-precedence.test.d.ts.map +1 -0
  48. package/dist/server/auth-precedence.test.js +100 -0
  49. package/dist/server/auth-precedence.test.js.map +1 -0
  50. package/dist/server/codex-args.d.ts +13 -0
  51. package/dist/server/codex-args.d.ts.map +1 -0
  52. package/dist/server/codex-args.js +76 -0
  53. package/dist/server/codex-args.js.map +1 -0
  54. package/dist/server/codex-args.test.d.ts +2 -0
  55. package/dist/server/codex-args.test.d.ts.map +1 -0
  56. package/dist/server/codex-args.test.js +243 -0
  57. package/dist/server/codex-args.test.js.map +1 -0
  58. package/dist/server/codex-auth-cache.d.ts +206 -0
  59. package/dist/server/codex-auth-cache.d.ts.map +1 -0
  60. package/dist/server/codex-auth-cache.js +501 -0
  61. package/dist/server/codex-auth-cache.js.map +1 -0
  62. package/dist/server/codex-auth-cache.test.d.ts +2 -0
  63. package/dist/server/codex-auth-cache.test.d.ts.map +1 -0
  64. package/dist/server/codex-auth-cache.test.js +497 -0
  65. package/dist/server/codex-auth-cache.test.js.map +1 -0
  66. package/dist/server/codex-auth-copyback.d.ts +53 -0
  67. package/dist/server/codex-auth-copyback.d.ts.map +1 -0
  68. package/dist/server/codex-auth-copyback.js +123 -0
  69. package/dist/server/codex-auth-copyback.js.map +1 -0
  70. package/dist/server/codex-auth-copyback.test.d.ts +2 -0
  71. package/dist/server/codex-auth-copyback.test.d.ts.map +1 -0
  72. package/dist/server/codex-auth-copyback.test.js +507 -0
  73. package/dist/server/codex-auth-copyback.test.js.map +1 -0
  74. package/dist/server/codex-auth-merge-decision.cjs +170 -0
  75. package/dist/server/codex-auth-merge-decision.d.ts +22 -0
  76. package/dist/server/codex-auth-merge-decision.d.ts.map +1 -0
  77. package/dist/server/codex-auth-merge-decision.js +58 -0
  78. package/dist/server/codex-auth-merge-decision.js.map +1 -0
  79. package/dist/server/codex-auth-merge-decision.test.d.ts +2 -0
  80. package/dist/server/codex-auth-merge-decision.test.d.ts.map +1 -0
  81. package/dist/server/codex-auth-merge-decision.test.js +230 -0
  82. package/dist/server/codex-auth-merge-decision.test.js.map +1 -0
  83. package/dist/server/codex-auth-merge-extract.sh +73 -0
  84. package/dist/server/codex-auth-merge-scripts.d.ts +21 -0
  85. package/dist/server/codex-auth-merge-scripts.d.ts.map +1 -0
  86. package/dist/server/codex-auth-merge-scripts.js +40 -0
  87. package/dist/server/codex-auth-merge-scripts.js.map +1 -0
  88. package/dist/server/codex-auth-merge.test.d.ts +2 -0
  89. package/dist/server/codex-auth-merge.test.d.ts.map +1 -0
  90. package/dist/server/codex-auth-merge.test.js +714 -0
  91. package/dist/server/codex-auth-merge.test.js.map +1 -0
  92. package/dist/server/codex-auth-seed-write.d.ts +30 -0
  93. package/dist/server/codex-auth-seed-write.d.ts.map +1 -0
  94. package/dist/server/codex-auth-seed-write.js +41 -0
  95. package/dist/server/codex-auth-seed-write.js.map +1 -0
  96. package/dist/server/codex-home.d.ts +162 -0
  97. package/dist/server/codex-home.d.ts.map +1 -0
  98. package/dist/server/codex-home.js +771 -0
  99. package/dist/server/codex-home.js.map +1 -0
  100. package/dist/server/codex-home.test.d.ts +2 -0
  101. package/dist/server/codex-home.test.d.ts.map +1 -0
  102. package/dist/server/codex-home.test.js +1183 -0
  103. package/dist/server/codex-home.test.js.map +1 -0
  104. package/dist/server/config-schema.d.ts +3 -0
  105. package/dist/server/config-schema.d.ts.map +1 -0
  106. package/dist/server/config-schema.js +67 -0
  107. package/dist/server/config-schema.js.map +1 -0
  108. package/dist/server/device-login-export.d.ts +56 -0
  109. package/dist/server/device-login-export.d.ts.map +1 -0
  110. package/dist/server/device-login-export.js +230 -0
  111. package/dist/server/device-login-export.js.map +1 -0
  112. package/dist/server/device-login-export.test.d.ts +2 -0
  113. package/dist/server/device-login-export.test.d.ts.map +1 -0
  114. package/dist/server/device-login-export.test.js +245 -0
  115. package/dist/server/device-login-export.test.js.map +1 -0
  116. package/dist/server/device-login-parse.d.ts +20 -0
  117. package/dist/server/device-login-parse.d.ts.map +1 -0
  118. package/dist/server/device-login-parse.js +155 -0
  119. package/dist/server/device-login-parse.js.map +1 -0
  120. package/dist/server/device-login-parse.test.d.ts +2 -0
  121. package/dist/server/device-login-parse.test.d.ts.map +1 -0
  122. package/dist/server/device-login-parse.test.js +269 -0
  123. package/dist/server/device-login-parse.test.js.map +1 -0
  124. package/dist/server/device-login-runner.d.ts +69 -0
  125. package/dist/server/device-login-runner.d.ts.map +1 -0
  126. package/dist/server/device-login-runner.js +109 -0
  127. package/dist/server/device-login-runner.js.map +1 -0
  128. package/dist/server/device-login-runner.test.d.ts +2 -0
  129. package/dist/server/device-login-runner.test.d.ts.map +1 -0
  130. package/dist/server/device-login-runner.test.js +201 -0
  131. package/dist/server/device-login-runner.test.js.map +1 -0
  132. package/dist/server/engine-availability.test.d.ts +2 -0
  133. package/dist/server/engine-availability.test.d.ts.map +1 -0
  134. package/dist/server/engine-availability.test.js +41 -0
  135. package/dist/server/engine-availability.test.js.map +1 -0
  136. package/dist/server/execute.acp-fallback.test.d.ts +2 -0
  137. package/dist/server/execute.acp-fallback.test.d.ts.map +1 -0
  138. package/dist/server/execute.acp-fallback.test.js +123 -0
  139. package/dist/server/execute.acp-fallback.test.js.map +1 -0
  140. package/dist/server/execute.auth-precedence.test.d.ts +2 -0
  141. package/dist/server/execute.auth-precedence.test.d.ts.map +1 -0
  142. package/dist/server/execute.auth-precedence.test.js +137 -0
  143. package/dist/server/execute.auth-precedence.test.js.map +1 -0
  144. package/dist/server/execute.auth.test.d.ts +2 -0
  145. package/dist/server/execute.auth.test.d.ts.map +1 -0
  146. package/dist/server/execute.auth.test.js +197 -0
  147. package/dist/server/execute.auth.test.js.map +1 -0
  148. package/dist/server/execute.d.ts +40 -0
  149. package/dist/server/execute.d.ts.map +1 -0
  150. package/dist/server/execute.js +1290 -0
  151. package/dist/server/execute.js.map +1 -0
  152. package/dist/server/execute.remote.test.d.ts +2 -0
  153. package/dist/server/execute.remote.test.d.ts.map +1 -0
  154. package/dist/server/execute.remote.test.js +556 -0
  155. package/dist/server/execute.remote.test.js.map +1 -0
  156. package/dist/server/execute.stderr-error.test.d.ts +2 -0
  157. package/dist/server/execute.stderr-error.test.d.ts.map +1 -0
  158. package/dist/server/execute.stderr-error.test.js +142 -0
  159. package/dist/server/execute.stderr-error.test.js.map +1 -0
  160. package/dist/server/execute.test.d.ts +2 -0
  161. package/dist/server/execute.test.d.ts.map +1 -0
  162. package/dist/server/execute.test.js +240 -0
  163. package/dist/server/execute.test.js.map +1 -0
  164. package/dist/server/index.d.ts +18 -0
  165. package/dist/server/index.d.ts.map +1 -0
  166. package/dist/server/index.js +71 -0
  167. package/dist/server/index.js.map +1 -0
  168. package/dist/server/output-inactivity-monitor.d.ts +62 -0
  169. package/dist/server/output-inactivity-monitor.d.ts.map +1 -0
  170. package/dist/server/output-inactivity-monitor.integration.test.d.ts +2 -0
  171. package/dist/server/output-inactivity-monitor.integration.test.d.ts.map +1 -0
  172. package/dist/server/output-inactivity-monitor.integration.test.js +136 -0
  173. package/dist/server/output-inactivity-monitor.integration.test.js.map +1 -0
  174. package/dist/server/output-inactivity-monitor.js +112 -0
  175. package/dist/server/output-inactivity-monitor.js.map +1 -0
  176. package/dist/server/output-inactivity-monitor.test.d.ts +2 -0
  177. package/dist/server/output-inactivity-monitor.test.d.ts.map +1 -0
  178. package/dist/server/output-inactivity-monitor.test.js +284 -0
  179. package/dist/server/output-inactivity-monitor.test.js.map +1 -0
  180. package/dist/server/parse.d.ts +51 -0
  181. package/dist/server/parse.d.ts.map +1 -0
  182. package/dist/server/parse.js +261 -0
  183. package/dist/server/parse.js.map +1 -0
  184. package/dist/server/parse.test.d.ts +2 -0
  185. package/dist/server/parse.test.d.ts.map +1 -0
  186. package/dist/server/parse.test.js +186 -0
  187. package/dist/server/parse.test.js.map +1 -0
  188. package/dist/server/process-activity-monitor.d.ts +21 -0
  189. package/dist/server/process-activity-monitor.d.ts.map +1 -0
  190. package/dist/server/process-activity-monitor.js +103 -0
  191. package/dist/server/process-activity-monitor.js.map +1 -0
  192. package/dist/server/process-activity-monitor.test.d.ts +2 -0
  193. package/dist/server/process-activity-monitor.test.d.ts.map +1 -0
  194. package/dist/server/process-activity-monitor.test.js +82 -0
  195. package/dist/server/process-activity-monitor.test.js.map +1 -0
  196. package/dist/server/quota-spawn-error.test.d.ts +2 -0
  197. package/dist/server/quota-spawn-error.test.d.ts.map +1 -0
  198. package/dist/server/quota-spawn-error.test.js +174 -0
  199. package/dist/server/quota-spawn-error.test.js.map +1 -0
  200. package/dist/server/quota.d.ts +66 -0
  201. package/dist/server/quota.d.ts.map +1 -0
  202. package/dist/server/quota.js +511 -0
  203. package/dist/server/quota.js.map +1 -0
  204. package/dist/server/runtime-config.d.ts +11 -0
  205. package/dist/server/runtime-config.d.ts.map +1 -0
  206. package/dist/server/runtime-config.js +380 -0
  207. package/dist/server/runtime-config.js.map +1 -0
  208. package/dist/server/runtime-config.test.d.ts +2 -0
  209. package/dist/server/runtime-config.test.d.ts.map +1 -0
  210. package/dist/server/runtime-config.test.js +367 -0
  211. package/dist/server/runtime-config.test.js.map +1 -0
  212. package/dist/server/skills.d.ts +8 -0
  213. package/dist/server/skills.d.ts.map +1 -0
  214. package/dist/server/skills.js +26 -0
  215. package/dist/server/skills.js.map +1 -0
  216. package/dist/server/test.d.ts +3 -0
  217. package/dist/server/test.d.ts.map +1 -0
  218. package/dist/server/test.js +458 -0
  219. package/dist/server/test.js.map +1 -0
  220. package/dist/server/test.remote.test.d.ts +2 -0
  221. package/dist/server/test.remote.test.d.ts.map +1 -0
  222. package/dist/server/test.remote.test.js +396 -0
  223. package/dist/server/test.remote.test.js.map +1 -0
  224. package/dist/ui/build-config.d.ts +5 -0
  225. package/dist/ui/build-config.d.ts.map +1 -0
  226. package/dist/ui/build-config.js +229 -0
  227. package/dist/ui/build-config.js.map +1 -0
  228. package/dist/ui/build-config.test.d.ts +2 -0
  229. package/dist/ui/build-config.test.d.ts.map +1 -0
  230. package/dist/ui/build-config.test.js +272 -0
  231. package/dist/ui/build-config.test.js.map +1 -0
  232. package/dist/ui/index.d.ts +4 -0
  233. package/dist/ui/index.d.ts.map +1 -0
  234. package/dist/ui/index.js +8 -0
  235. package/dist/ui/index.js.map +1 -0
  236. package/dist/ui/parse-stdout.d.ts +3 -0
  237. package/dist/ui/parse-stdout.d.ts.map +1 -0
  238. package/dist/ui/parse-stdout.js +265 -0
  239. package/dist/ui/parse-stdout.js.map +1 -0
  240. package/dist/ui/parse-stdout.test.d.ts +2 -0
  241. package/dist/ui/parse-stdout.test.d.ts.map +1 -0
  242. package/dist/ui/parse-stdout.test.js +77 -0
  243. package/dist/ui/parse-stdout.test.js.map +1 -0
  244. package/package.json +61 -0
  245. package/skills/agentmail/SKILL.md +70 -0
  246. package/skills/paperclip/SKILL.md +712 -0
  247. package/skills/paperclip/references/api-reference.md +1675 -0
  248. package/skills/paperclip/references/artifacts.md +158 -0
  249. package/skills/paperclip/references/cases.md +295 -0
  250. package/skills/paperclip/references/company-skills.md +266 -0
  251. package/skills/paperclip/references/issue-workspaces.md +80 -0
  252. package/skills/paperclip/references/routines.md +231 -0
  253. package/skills/paperclip/references/workflows.md +141 -0
  254. package/skills/paperclip/scripts/paperclip-upload-artifact.sh +592 -0
  255. package/skills/paperclip-board/SKILL.md +619 -0
  256. package/skills/paperclip-converting-plans-to-tasks/SKILL.md +60 -0
  257. package/skills/paperclip-create-agent/SKILL.md +179 -0
  258. package/skills/paperclip-create-agent/references/agent-instruction-templates.md +123 -0
  259. package/skills/paperclip-create-agent/references/agents/coder.md +64 -0
  260. package/skills/paperclip-create-agent/references/agents/qa.md +88 -0
  261. package/skills/paperclip-create-agent/references/agents/securityengineer.md +135 -0
  262. package/skills/paperclip-create-agent/references/agents/uxdesigner.md +115 -0
  263. package/skills/paperclip-create-agent/references/api-reference.md +110 -0
  264. package/skills/paperclip-create-agent/references/baseline-role-guide.md +168 -0
  265. package/skills/paperclip-create-agent/references/draft-review-checklist.md +95 -0
  266. package/skills/para-memory-files/SKILL.md +100 -0
  267. package/skills/para-memory-files/references/schemas.md +35 -0
  268. package/skills/slack/SKILL.md +65 -0
@@ -0,0 +1,658 @@
1
+ import { chmod, lstat, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { checkStagedCredentialReadiness, DeviceLoginReadinessError, promoteDeviceLoginCredential, } from "./adapter-auth-promotion.js";
6
+ import { MAX_AUTH_JSON_BYTES } from "./device-login-export.js";
7
+ import { resolveManagedCodexHomeDir, resolveSharedCodexHomeDir } from "./codex-home.js";
8
+ import { resolveCodexAuthCacheDir, resolveCodexAuthCacheEntryPath } from "./codex-auth-cache.js";
9
+ const COMPANY_A = "company-a";
10
+ const COMPANY_B = "company-b";
11
+ const NEWER = "2026-07-09T02:00:00Z";
12
+ const OLDER = "2026-07-09T01:00:00Z";
13
+ const ACCOUNT = "acct-42";
14
+ const OTHER_ACCOUNT = "acct-99";
15
+ const TOKEN_SENTINEL = "SENTINEL_TOKEN_XYZ";
16
+ // This suite proves the device-login credential promotion helper. The helper
17
+ // runs an independent readiness check on the exact staged credential first, then
18
+ // validates it with the export rules, then writes this account's own home and,
19
+ // as a fallback, the company default home. It writes only while the session
20
+ // holds the sole active claim on the slot (the conditional check). It never
21
+ // writes the instance-global host, and it never logs secret bytes.
22
+ describe("device-login credential promotion", () => {
23
+ const cleanupDirs = [];
24
+ afterEach(async () => {
25
+ while (cleanupDirs.length > 0) {
26
+ const dir = cleanupDirs.pop();
27
+ if (!dir)
28
+ continue;
29
+ await chmod(dir, 0o700).catch(() => undefined);
30
+ await rm(dir, { recursive: true, force: true }).catch(() => undefined);
31
+ }
32
+ });
33
+ async function makeInstanceRoot() {
34
+ const dir = await mkdtemp(path.join(os.tmpdir(), "paperclip-codex-promotion-"));
35
+ cleanupDirs.push(dir);
36
+ return dir;
37
+ }
38
+ function envFor(instanceHome, extra = {}) {
39
+ return {
40
+ PAPERCLIP_HOME: instanceHome,
41
+ PAPERCLIP_INSTANCE_ID: "default",
42
+ // A fixed shared host home, so a test can assert the helper never writes it.
43
+ CODEX_HOME: path.join(instanceHome, "shared-codex"),
44
+ ...extra,
45
+ };
46
+ }
47
+ function subscriptionAuth(input) {
48
+ const suffix = input.marker ?? input.accountId;
49
+ return Buffer.from(JSON.stringify({
50
+ tokens: {
51
+ id_token: `id-token-${suffix}`,
52
+ access_token: `access-token-${suffix}`,
53
+ refresh_token: `${TOKEN_SENTINEL}-${suffix}`,
54
+ account_id: input.accountId,
55
+ },
56
+ ...(input.lastRefresh ? { last_refresh: input.lastRefresh } : {}),
57
+ }));
58
+ }
59
+ const ready = () => ({ ready: true });
60
+ const notReady = () => ({ ready: false, reason: "auth_unusable" });
61
+ const soleOwner = () => true;
62
+ const noopLog = (_line) => { };
63
+ function companyHomeAuthPath(env, companyId) {
64
+ return path.join(resolveManagedCodexHomeDir(env, companyId), "auth.json");
65
+ }
66
+ it("a failed readiness check rejects and writes neither the company home nor the account home", async () => {
67
+ const home = await makeInstanceRoot();
68
+ const env = envFor(home);
69
+ const logs = [];
70
+ await expect(promoteDeviceLoginCredential({
71
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER }),
72
+ companyId: COMPANY_A,
73
+ userInitiated: true,
74
+ checkReadiness: notReady,
75
+ isSoleActiveOwner: soleOwner,
76
+ env,
77
+ log: (line) => {
78
+ logs.push(line);
79
+ },
80
+ })).rejects.toBeInstanceOf(DeviceLoginReadinessError);
81
+ // Neither company target was written.
82
+ await expect(lstat(companyHomeAuthPath(env, COMPANY_A))).rejects.toThrow();
83
+ await expect(lstat(resolveCodexAuthCacheEntryPath(env, ACCOUNT, COMPANY_A))).rejects.toThrow();
84
+ // The instance-global host was never touched.
85
+ await expect(lstat(path.join(resolveSharedCodexHomeDir(env), "auth.json"))).rejects.toThrow();
86
+ // No secret bytes reached the log.
87
+ expect(logs.join("\n")).not.toContain(TOKEN_SENTINEL);
88
+ expect(logs.join("\n")).not.toContain(ACCOUNT);
89
+ });
90
+ it("a user login seeds this account's own home and the company default home", async () => {
91
+ const home = await makeInstanceRoot();
92
+ const env = envFor(home);
93
+ const result = await promoteDeviceLoginCredential({
94
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER }),
95
+ companyId: COMPANY_A,
96
+ userInitiated: true,
97
+ checkReadiness: ready,
98
+ isSoleActiveOwner: soleOwner,
99
+ env,
100
+ log: noopLog,
101
+ });
102
+ expect(result.outcome).toBe("promoted");
103
+ const homeAuth = await readFile(companyHomeAuthPath(env, COMPANY_A), "utf8");
104
+ expect(JSON.parse(homeAuth).tokens.account_id).toBe(ACCOUNT);
105
+ const accountAuth = await readFile(resolveCodexAuthCacheEntryPath(env, ACCOUNT, COMPANY_A), "utf8");
106
+ expect(JSON.parse(accountAuth).tokens.account_id).toBe(ACCOUNT);
107
+ // The instance-global host was never seeded.
108
+ await expect(lstat(path.join(resolveSharedCodexHomeDir(env), "auth.json"))).rejects.toThrow();
109
+ });
110
+ it("a strictly-newer login updates this account's own home", async () => {
111
+ const home = await makeInstanceRoot();
112
+ const env = envFor(home);
113
+ await promoteDeviceLoginCredential({
114
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: OLDER, marker: "old" }),
115
+ companyId: COMPANY_A,
116
+ userInitiated: true,
117
+ checkReadiness: ready,
118
+ isSoleActiveOwner: soleOwner,
119
+ env,
120
+ log: noopLog,
121
+ });
122
+ const result = await promoteDeviceLoginCredential({
123
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER, marker: "new" }),
124
+ companyId: COMPANY_A,
125
+ userInitiated: true,
126
+ checkReadiness: ready,
127
+ isSoleActiveOwner: soleOwner,
128
+ env,
129
+ log: noopLog,
130
+ });
131
+ expect(result.outcome).toBe("promoted");
132
+ const accountAuth = JSON.parse(await readFile(resolveCodexAuthCacheEntryPath(env, ACCOUNT, COMPANY_A), "utf8"));
133
+ expect(accountAuth.last_refresh).toBe(NEWER);
134
+ expect(accountAuth.tokens.refresh_token).toContain("new");
135
+ });
136
+ it("an older login keeps this account's own home", async () => {
137
+ const home = await makeInstanceRoot();
138
+ const env = envFor(home);
139
+ await promoteDeviceLoginCredential({
140
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER, marker: "keep" }),
141
+ companyId: COMPANY_A,
142
+ userInitiated: true,
143
+ checkReadiness: ready,
144
+ isSoleActiveOwner: soleOwner,
145
+ env,
146
+ log: noopLog,
147
+ });
148
+ const result = await promoteDeviceLoginCredential({
149
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: OLDER, marker: "older" }),
150
+ companyId: COMPANY_A,
151
+ userInitiated: true,
152
+ checkReadiness: ready,
153
+ isSoleActiveOwner: soleOwner,
154
+ env,
155
+ log: noopLog,
156
+ });
157
+ expect(result.outcome).toBe("kept");
158
+ const accountAuth = JSON.parse(await readFile(resolveCodexAuthCacheEntryPath(env, ACCOUNT, COMPANY_A), "utf8"));
159
+ expect(accountAuth.tokens.refresh_token).toContain("keep");
160
+ });
161
+ it("promotion writes the account home for a second, different account", async () => {
162
+ const home = await makeInstanceRoot();
163
+ const env = envFor(home);
164
+ await promoteDeviceLoginCredential({
165
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER, marker: "first" }),
166
+ companyId: COMPANY_A,
167
+ userInitiated: true,
168
+ checkReadiness: ready,
169
+ isSoleActiveOwner: soleOwner,
170
+ env,
171
+ log: noopLog,
172
+ });
173
+ const result = await promoteDeviceLoginCredential({
174
+ authBytes: subscriptionAuth({ accountId: OTHER_ACCOUNT, lastRefresh: NEWER, marker: "other" }),
175
+ companyId: COMPANY_A,
176
+ userInitiated: true,
177
+ checkReadiness: ready,
178
+ isSoleActiveOwner: soleOwner,
179
+ env,
180
+ log: noopLog,
181
+ });
182
+ // A second, different account gets its own home, so the second login
183
+ // succeeds instead of failing behind the first account's home.
184
+ expect(result.outcome).toBe("promoted");
185
+ const otherAccountAuth = JSON.parse(await readFile(resolveCodexAuthCacheEntryPath(env, OTHER_ACCOUNT, COMPANY_A), "utf8"));
186
+ expect(otherAccountAuth.tokens.account_id).toBe(OTHER_ACCOUNT);
187
+ });
188
+ it("promotion returns the account id and the account home path", async () => {
189
+ const home = await makeInstanceRoot();
190
+ const env = envFor(home);
191
+ const result = await promoteDeviceLoginCredential({
192
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER }),
193
+ companyId: COMPANY_A,
194
+ userInitiated: true,
195
+ checkReadiness: ready,
196
+ isSoleActiveOwner: soleOwner,
197
+ env,
198
+ log: noopLog,
199
+ });
200
+ expect(result.accountId).toBe(ACCOUNT);
201
+ expect(result.accountHomeDir).toBe(path.dirname(resolveCodexAuthCacheEntryPath(env, ACCOUNT, COMPANY_A)));
202
+ });
203
+ it("promotion reports accountHomeCreated true for a first login of an account", async () => {
204
+ const home = await makeInstanceRoot();
205
+ const env = envFor(home);
206
+ const result = await promoteDeviceLoginCredential({
207
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER }),
208
+ companyId: COMPANY_A,
209
+ userInitiated: true,
210
+ checkReadiness: ready,
211
+ isSoleActiveOwner: soleOwner,
212
+ env,
213
+ log: noopLog,
214
+ });
215
+ expect(result.outcome).toBe("promoted");
216
+ expect(result.accountHomeCreated).toBe(true);
217
+ });
218
+ it("promotion reports accountHomeCreated false for a repeat login of the same account", async () => {
219
+ const home = await makeInstanceRoot();
220
+ const env = envFor(home);
221
+ await promoteDeviceLoginCredential({
222
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: OLDER, marker: "first" }),
223
+ companyId: COMPANY_A,
224
+ userInitiated: true,
225
+ checkReadiness: ready,
226
+ isSoleActiveOwner: soleOwner,
227
+ env,
228
+ log: noopLog,
229
+ });
230
+ const result = await promoteDeviceLoginCredential({
231
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER, marker: "second" }),
232
+ companyId: COMPANY_A,
233
+ userInitiated: true,
234
+ checkReadiness: ready,
235
+ isSoleActiveOwner: soleOwner,
236
+ env,
237
+ log: noopLog,
238
+ });
239
+ expect(result.outcome).toBe("promoted");
240
+ expect(result.accountHomeCreated).toBe(false);
241
+ });
242
+ it("two concurrent promotions for the same account report only one accountHomeCreated true", async () => {
243
+ // Two different logins for the SAME Codex account run their own
244
+ // promotion slot, so they can promote at the same time. Without a lock
245
+ // around the absence check and the directory creation, both calls could
246
+ // see the directory as absent and both report `created: true`; a caller
247
+ // that later deletes the directory on `created: true` would then delete
248
+ // a home the other call's login still uses.
249
+ const home = await makeInstanceRoot();
250
+ const env = envFor(home);
251
+ const [first, second] = await Promise.all([
252
+ promoteDeviceLoginCredential({
253
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER, marker: "racer-a" }),
254
+ companyId: COMPANY_A,
255
+ userInitiated: true,
256
+ checkReadiness: ready,
257
+ isSoleActiveOwner: soleOwner,
258
+ env,
259
+ log: noopLog,
260
+ }),
261
+ promoteDeviceLoginCredential({
262
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER, marker: "racer-b" }),
263
+ companyId: COMPANY_A,
264
+ userInitiated: true,
265
+ checkReadiness: ready,
266
+ isSoleActiveOwner: soleOwner,
267
+ env,
268
+ log: noopLog,
269
+ }),
270
+ ]);
271
+ // Both racers still authenticate; only the timestamp-comparison outcome
272
+ // (`promoted` vs. `kept`) can differ, because they carry the same
273
+ // `lastRefresh` timestamp.
274
+ expect(["promoted", "kept"]).toContain(first.outcome);
275
+ expect(["promoted", "kept"]).toContain(second.outcome);
276
+ const createdFlags = [first.accountHomeCreated, second.accountHomeCreated];
277
+ expect(createdFlags.filter(Boolean)).toHaveLength(1);
278
+ await expect(lstat(resolveCodexAuthCacheEntryPath(env, ACCOUNT, COMPANY_A))).resolves.toBeDefined();
279
+ });
280
+ it("promotion keeps the company default home when it holds another account", async () => {
281
+ const home = await makeInstanceRoot();
282
+ const env = envFor(home);
283
+ await promoteDeviceLoginCredential({
284
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER, marker: "first" }),
285
+ companyId: COMPANY_A,
286
+ userInitiated: true,
287
+ checkReadiness: ready,
288
+ isSoleActiveOwner: soleOwner,
289
+ env,
290
+ log: noopLog,
291
+ });
292
+ const before = await readFile(companyHomeAuthPath(env, COMPANY_A), "utf8");
293
+ await promoteDeviceLoginCredential({
294
+ authBytes: subscriptionAuth({ accountId: OTHER_ACCOUNT, lastRefresh: NEWER, marker: "other" }),
295
+ companyId: COMPANY_A,
296
+ userInitiated: true,
297
+ checkReadiness: ready,
298
+ isSoleActiveOwner: soleOwner,
299
+ env,
300
+ log: noopLog,
301
+ });
302
+ // The company default home fallback still names the first account: a
303
+ // second account never clobbers it once some account has claimed it.
304
+ expect(await readFile(companyHomeAuthPath(env, COMPANY_A), "utf8")).toBe(before);
305
+ });
306
+ it("promotion seeds the company default home when it holds no usable credential", async () => {
307
+ const home = await makeInstanceRoot();
308
+ const env = envFor(home);
309
+ const result = await promoteDeviceLoginCredential({
310
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER }),
311
+ companyId: COMPANY_A,
312
+ userInitiated: true,
313
+ checkReadiness: ready,
314
+ isSoleActiveOwner: soleOwner,
315
+ env,
316
+ log: noopLog,
317
+ });
318
+ expect(result.outcome).toBe("promoted");
319
+ const homeAuth = await readFile(companyHomeAuthPath(env, COMPANY_A), "utf8");
320
+ expect(JSON.parse(homeAuth).tokens.account_id).toBe(ACCOUNT);
321
+ });
322
+ it("a strictly-newer same-account login refreshes the company default home", async () => {
323
+ // The sign-in loop this pins: a company home already holding a shape-usable
324
+ // credential for this account must still pick up the login the user just
325
+ // completed, or every environment test after the login keeps staging the
326
+ // old credential and keeps reporting authentication as missing.
327
+ const home = await makeInstanceRoot();
328
+ const env = envFor(home);
329
+ await promoteDeviceLoginCredential({
330
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: OLDER, marker: "old" }),
331
+ companyId: COMPANY_A,
332
+ userInitiated: true,
333
+ checkReadiness: ready,
334
+ isSoleActiveOwner: soleOwner,
335
+ env,
336
+ log: noopLog,
337
+ });
338
+ await promoteDeviceLoginCredential({
339
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER, marker: "new" }),
340
+ companyId: COMPANY_A,
341
+ userInitiated: true,
342
+ checkReadiness: ready,
343
+ isSoleActiveOwner: soleOwner,
344
+ env,
345
+ log: noopLog,
346
+ });
347
+ const homeAuth = JSON.parse(await readFile(companyHomeAuthPath(env, COMPANY_A), "utf8"));
348
+ expect(homeAuth.last_refresh).toBe(NEWER);
349
+ expect(homeAuth.tokens.refresh_token).toContain("new");
350
+ });
351
+ it("an older same-account login keeps the company default home", async () => {
352
+ const home = await makeInstanceRoot();
353
+ const env = envFor(home);
354
+ await promoteDeviceLoginCredential({
355
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER, marker: "keep" }),
356
+ companyId: COMPANY_A,
357
+ userInitiated: true,
358
+ checkReadiness: ready,
359
+ isSoleActiveOwner: soleOwner,
360
+ env,
361
+ log: noopLog,
362
+ });
363
+ await promoteDeviceLoginCredential({
364
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: OLDER, marker: "older" }),
365
+ companyId: COMPANY_A,
366
+ userInitiated: true,
367
+ checkReadiness: ready,
368
+ isSoleActiveOwner: soleOwner,
369
+ env,
370
+ log: noopLog,
371
+ });
372
+ const homeAuth = JSON.parse(await readFile(companyHomeAuthPath(env, COMPANY_A), "utf8"));
373
+ expect(homeAuth.tokens.refresh_token).toContain("keep");
374
+ });
375
+ it("a strictly-newer same-account login replaces a symlinked company auth.json without writing its target", async () => {
376
+ // The company home often symlinks auth.json at the host login (the shared
377
+ // source seeding does exactly that). The refresh must swap the symlink for
378
+ // a regular file holding the login credential — atomically, at the link
379
+ // itself — and must never write through the link into the file it names.
380
+ const home = await makeInstanceRoot();
381
+ const env = envFor(home);
382
+ const hostAuthPath = path.join(home, "host-auth.json");
383
+ const hostBytes = subscriptionAuth({ accountId: ACCOUNT, lastRefresh: OLDER, marker: "host" });
384
+ await writeFile(hostAuthPath, hostBytes);
385
+ const companyHome = resolveManagedCodexHomeDir(env, COMPANY_A);
386
+ await mkdir(companyHome, { recursive: true, mode: 0o700 });
387
+ await symlink(hostAuthPath, companyHomeAuthPath(env, COMPANY_A));
388
+ await promoteDeviceLoginCredential({
389
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER, marker: "fresh" }),
390
+ companyId: COMPANY_A,
391
+ userInitiated: true,
392
+ checkReadiness: ready,
393
+ isSoleActiveOwner: soleOwner,
394
+ env,
395
+ log: noopLog,
396
+ });
397
+ const stat = await lstat(companyHomeAuthPath(env, COMPANY_A));
398
+ expect(stat.isSymbolicLink()).toBe(false);
399
+ const homeAuth = JSON.parse(await readFile(companyHomeAuthPath(env, COMPANY_A), "utf8"));
400
+ expect(homeAuth.tokens.refresh_token).toContain("fresh");
401
+ // The symlink target — standing in for the host's ~/.codex/auth.json — was
402
+ // never written.
403
+ expect(await readFile(hostAuthPath, "utf8")).toBe(hostBytes.toString("utf8"));
404
+ });
405
+ it("promotion fails the login when the account identifier cannot become a handle", async () => {
406
+ const home = await makeInstanceRoot();
407
+ const env = envFor(home);
408
+ await expect(promoteDeviceLoginCredential({
409
+ authBytes: subscriptionAuth({ accountId: "acct 42", lastRefresh: NEWER }),
410
+ companyId: COMPANY_A,
411
+ userInitiated: true,
412
+ checkReadiness: ready,
413
+ isSoleActiveOwner: soleOwner,
414
+ env,
415
+ log: noopLog,
416
+ })).rejects.toThrow();
417
+ await expect(lstat(companyHomeAuthPath(env, COMPANY_A))).rejects.toThrow();
418
+ });
419
+ it("a whitespace-padded account identifier never shares a home with, or reports as authenticated against, the unpadded account", async () => {
420
+ // A distinct identifier with surrounding whitespace must reject, not
421
+ // alias onto the unpadded account's home. Promote the unpadded account
422
+ // first, then attempt a second login whose identifier differs only by a
423
+ // leading space. The second login must fail, and it must never read or
424
+ // report a "kept" outcome against the first account's home.
425
+ const home = await makeInstanceRoot();
426
+ const env = envFor(home);
427
+ await promoteDeviceLoginCredential({
428
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER, marker: "original" }),
429
+ companyId: COMPANY_A,
430
+ userInitiated: true,
431
+ checkReadiness: ready,
432
+ isSoleActiveOwner: soleOwner,
433
+ env,
434
+ log: noopLog,
435
+ });
436
+ await expect(promoteDeviceLoginCredential({
437
+ authBytes: subscriptionAuth({ accountId: ` ${ACCOUNT}`, lastRefresh: NEWER, marker: "padded" }),
438
+ companyId: COMPANY_A,
439
+ userInitiated: true,
440
+ checkReadiness: ready,
441
+ isSoleActiveOwner: soleOwner,
442
+ env,
443
+ log: noopLog,
444
+ })).rejects.toThrow();
445
+ // The unpadded account's home still holds only the credential the first
446
+ // login wrote. The padded login never read it, never wrote it, and never
447
+ // received a "kept" outcome that would report it as authenticated.
448
+ const accountAuth = JSON.parse(await readFile(resolveCodexAuthCacheEntryPath(env, ACCOUNT, COMPANY_A), "utf8"));
449
+ expect(accountAuth.tokens.refresh_token).toContain("original");
450
+ });
451
+ it("a broken account-home directory fails the whole login", async () => {
452
+ const home = await makeInstanceRoot();
453
+ const env = envFor(home);
454
+ // Force this account's own home write to fail. Plant a regular file where
455
+ // the account expects its own directory, so the private-directory guard
456
+ // throws before any write. That write is now fail-loud (it is the durable
457
+ // result), so the whole login fails, and the best-effort company default
458
+ // home fallback is never reached.
459
+ const cacheDir = resolveCodexAuthCacheDir(env, COMPANY_A);
460
+ await mkdir(cacheDir, { recursive: true, mode: 0o700 });
461
+ const entryPath = resolveCodexAuthCacheEntryPath(env, ACCOUNT, COMPANY_A);
462
+ await writeFile(path.dirname(entryPath), "not-a-directory");
463
+ await expect(promoteDeviceLoginCredential({
464
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER }),
465
+ companyId: COMPANY_A,
466
+ userInitiated: true,
467
+ checkReadiness: ready,
468
+ isSoleActiveOwner: soleOwner,
469
+ env,
470
+ log: noopLog,
471
+ })).rejects.toThrow();
472
+ await expect(lstat(companyHomeAuthPath(env, COMPANY_A))).rejects.toThrow();
473
+ });
474
+ it("a company default home write failure keeps the promotion successful and this account's own home durable", async () => {
475
+ const home = await makeInstanceRoot();
476
+ const env = envFor(home);
477
+ // Force the company default home write to fail. Plant a regular file where
478
+ // the company home directory belongs, so mkdir fails. This account's own
479
+ // home write already ran and is durable, so the login still succeeds.
480
+ const companyHome = resolveManagedCodexHomeDir(env, COMPANY_A);
481
+ await mkdir(path.dirname(companyHome), { recursive: true });
482
+ await writeFile(companyHome, "not-a-directory");
483
+ const logs = [];
484
+ const result = await promoteDeviceLoginCredential({
485
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER }),
486
+ companyId: COMPANY_A,
487
+ userInitiated: true,
488
+ checkReadiness: ready,
489
+ isSoleActiveOwner: soleOwner,
490
+ env,
491
+ log: (line) => {
492
+ logs.push(line);
493
+ },
494
+ });
495
+ expect(result.outcome).toBe("promoted");
496
+ const accountAuth = JSON.parse(await readFile(resolveCodexAuthCacheEntryPath(env, ACCOUNT, COMPANY_A), "utf8"));
497
+ expect(accountAuth.tokens.account_id).toBe(ACCOUNT);
498
+ const haystack = logs.join("\n");
499
+ expect(haystack).toContain("seeding the company default home failed");
500
+ expect(haystack).not.toContain(TOKEN_SENTINEL);
501
+ expect(haystack).not.toContain(ACCOUNT);
502
+ });
503
+ it("rejects malformed, API-key, non-subscription, and oversized credentials and writes nothing", async () => {
504
+ const home = await makeInstanceRoot();
505
+ const env = envFor(home);
506
+ const malformed = Buffer.from("this is not json {");
507
+ const apiKey = Buffer.from(JSON.stringify({ OPENAI_API_KEY: "sk-secret-key" }));
508
+ const nonSubscription = Buffer.from(JSON.stringify({ tokens: { account_id: "" } }));
509
+ const oversized = Buffer.concat([
510
+ subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER }),
511
+ Buffer.alloc(MAX_AUTH_JSON_BYTES + 10, 0x20),
512
+ ]);
513
+ for (const bytes of [malformed, apiKey, nonSubscription, oversized]) {
514
+ await expect(promoteDeviceLoginCredential({
515
+ authBytes: bytes,
516
+ companyId: COMPANY_A,
517
+ userInitiated: true,
518
+ checkReadiness: ready,
519
+ isSoleActiveOwner: soleOwner,
520
+ env,
521
+ log: noopLog,
522
+ })).rejects.toThrow();
523
+ }
524
+ // No company target and no instance-global host was written.
525
+ await expect(lstat(companyHomeAuthPath(env, COMPANY_A))).rejects.toThrow();
526
+ await expect(lstat(path.join(resolveSharedCodexHomeDir(env), "auth.json"))).rejects.toThrow();
527
+ });
528
+ it("a promotion whose session is no longer the sole active owner writes nothing", async () => {
529
+ const home = await makeInstanceRoot();
530
+ const env = envFor(home);
531
+ const result = await promoteDeviceLoginCredential({
532
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER }),
533
+ companyId: COMPANY_A,
534
+ userInitiated: true,
535
+ checkReadiness: ready,
536
+ // The session lost the active claim on the slot.
537
+ isSoleActiveOwner: () => false,
538
+ env,
539
+ log: noopLog,
540
+ });
541
+ expect(result.outcome).toBe("not_sole_owner");
542
+ await expect(lstat(companyHomeAuthPath(env, COMPANY_A))).rejects.toThrow();
543
+ await expect(lstat(resolveCodexAuthCacheEntryPath(env, ACCOUNT, COMPANY_A))).rejects.toThrow();
544
+ });
545
+ it("an automatic background login never seeds an empty home", async () => {
546
+ const home = await makeInstanceRoot();
547
+ const env = envFor(home);
548
+ const result = await promoteDeviceLoginCredential({
549
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER }),
550
+ companyId: COMPANY_A,
551
+ // Not a user-initiated login.
552
+ userInitiated: false,
553
+ checkReadiness: ready,
554
+ isSoleActiveOwner: soleOwner,
555
+ env,
556
+ log: noopLog,
557
+ });
558
+ expect(result.outcome).toBe("background_skipped");
559
+ await expect(lstat(companyHomeAuthPath(env, COMPANY_A))).rejects.toThrow();
560
+ await expect(lstat(resolveCodexAuthCacheEntryPath(env, ACCOUNT, COMPANY_A))).rejects.toThrow();
561
+ });
562
+ it("a Company A login never changes a Company B credential", async () => {
563
+ const home = await makeInstanceRoot();
564
+ const env = envFor(home);
565
+ // Seed Company B first.
566
+ await promoteDeviceLoginCredential({
567
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: OLDER, marker: "b-cred" }),
568
+ companyId: COMPANY_B,
569
+ userInitiated: true,
570
+ checkReadiness: ready,
571
+ isSoleActiveOwner: soleOwner,
572
+ env,
573
+ log: noopLog,
574
+ });
575
+ const before = await readFile(companyHomeAuthPath(env, COMPANY_B), "utf8");
576
+ // A Company A login for the same identity, even newer, must not touch B.
577
+ await promoteDeviceLoginCredential({
578
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER, marker: "a-cred" }),
579
+ companyId: COMPANY_A,
580
+ userInitiated: true,
581
+ checkReadiness: ready,
582
+ isSoleActiveOwner: soleOwner,
583
+ env,
584
+ log: noopLog,
585
+ });
586
+ expect(await readFile(companyHomeAuthPath(env, COMPANY_B), "utf8")).toBe(before);
587
+ const aHome = JSON.parse(await readFile(companyHomeAuthPath(env, COMPANY_A), "utf8"));
588
+ expect(aHome.tokens.refresh_token).toContain("a-cred");
589
+ });
590
+ it("the promotion log never contains token bytes or a raw account_id", async () => {
591
+ const home = await makeInstanceRoot();
592
+ const env = envFor(home);
593
+ const logs = [];
594
+ await promoteDeviceLoginCredential({
595
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER }),
596
+ companyId: COMPANY_A,
597
+ userInitiated: true,
598
+ checkReadiness: ready,
599
+ isSoleActiveOwner: soleOwner,
600
+ env,
601
+ log: (line) => {
602
+ logs.push(line);
603
+ },
604
+ });
605
+ const haystack = logs.join("\n");
606
+ expect(haystack).not.toContain(TOKEN_SENTINEL);
607
+ expect(haystack).not.toContain(ACCOUNT);
608
+ });
609
+ it("rejects an empty companyId, so a promotion can never reach the instance-global home", async () => {
610
+ const home = await makeInstanceRoot();
611
+ const env = envFor(home);
612
+ await expect(promoteDeviceLoginCredential({
613
+ authBytes: subscriptionAuth({ accountId: ACCOUNT, lastRefresh: NEWER }),
614
+ companyId: "",
615
+ userInitiated: true,
616
+ checkReadiness: ready,
617
+ isSoleActiveOwner: soleOwner,
618
+ env,
619
+ log: noopLog,
620
+ })).rejects.toThrow();
621
+ // The instance-global codex-home was never seeded.
622
+ await expect(lstat(path.join(resolveManagedCodexHomeDir(env), "auth.json"))).rejects.toThrow();
623
+ });
624
+ });
625
+ // This suite proves the independent readiness check for a staged credential. It
626
+ // answers whether a run launched now with the exact staged bytes would
627
+ // authenticate. It writes the bytes to a throwaway home only, and it never reads
628
+ // or writes any company scope.
629
+ describe("staged credential readiness", () => {
630
+ it("returns ready for a usable subscription credential", async () => {
631
+ const bytes = Buffer.from(JSON.stringify({
632
+ tokens: {
633
+ id_token: "id-token",
634
+ access_token: "access-token",
635
+ refresh_token: "refresh-token",
636
+ account_id: "acct-1",
637
+ },
638
+ }));
639
+ const result = await checkStagedCredentialReadiness(bytes);
640
+ expect(result.ready).toBe(true);
641
+ });
642
+ it("returns not ready for empty bytes", async () => {
643
+ const result = await checkStagedCredentialReadiness(Buffer.alloc(0));
644
+ expect(result.ready).toBe(false);
645
+ expect(result.reason).toBe("empty_credential");
646
+ });
647
+ it("returns not ready for a credential with no usable auth payload", async () => {
648
+ const result = await checkStagedCredentialReadiness(Buffer.from(JSON.stringify({ tokens: { account_id: "acct-1" } })));
649
+ expect(result.ready).toBe(false);
650
+ expect(result.reason).toBe("no_usable_auth");
651
+ });
652
+ it("returns not ready for malformed bytes", async () => {
653
+ const result = await checkStagedCredentialReadiness(Buffer.from("not-json"));
654
+ expect(result.ready).toBe(false);
655
+ expect(result.reason).toBe("no_usable_auth");
656
+ });
657
+ });
658
+ //# sourceMappingURL=adapter-auth-promotion.test.js.map