@astrale-os/cli 1.0.0-beta.32 → 1.0.0-beta.43

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 (441) hide show
  1. package/CHANGELOG.md +928 -0
  2. package/README.md +53 -43
  3. package/THIRD-PARTY-NOTICES.md +37 -1
  4. package/dist/astrale.js +349631 -83669
  5. package/dist/public/connect-core.js +16 -47
  6. package/dist/public/keys/index.js +1 -1
  7. package/dist/types/admin/contract.d.ts +11 -0
  8. package/dist/types/admin/graph/index.d.ts +1 -0
  9. package/dist/types/admin/graph/nodes.d.ts +12 -0
  10. package/dist/types/admin/instance/client.d.ts +2 -0
  11. package/dist/types/connection/lifetime.d.ts +1 -1
  12. package/dist/types/errors.d.ts +3 -0
  13. package/dist/types/lib/proc.d.ts +1 -0
  14. package/dist/types/lib/update.d.ts +15 -7
  15. package/package.json +8 -6
  16. package/studio/client/dist/assets/index-5C1uT6ML.css +2 -0
  17. package/studio/client/dist/assets/index-C4kNlvwF.js +80 -0
  18. package/studio/client/dist/assets/schema-studio-BR9d0faB.js +8 -0
  19. package/studio/client/dist/index.html +13 -2
  20. package/studio/package.json +2 -1
  21. package/studio/server/agent/ask.test.ts +7 -1
  22. package/studio/server/agent/bridge/grant.ts +15 -3
  23. package/studio/server/agent/prompts/system.test.ts +5 -0
  24. package/studio/server/agent/prompts/system.ts +19 -4
  25. package/studio/server/agent/routes.test.ts +39 -1
  26. package/studio/server/agent/routes.ts +3 -0
  27. package/studio/server/agent/run/coordinator.test.ts +7 -1
  28. package/studio/server/agent/run/preparation.ts +1 -0
  29. package/studio/server/agent/run/transcript.test.ts +47 -2
  30. package/studio/server/agent/run/transcript.ts +32 -3
  31. package/studio/server/api/documents.test.ts +31 -0
  32. package/studio/server/api/documents.ts +35 -1
  33. package/studio/server/api-agent-loadout.test.ts +7 -1
  34. package/studio/server/api.test.ts +7 -1
  35. package/studio/server/cache.ts +4 -4
  36. package/studio/server/detect.ts +9 -12
  37. package/studio/server/domain.test.ts +83 -3
  38. package/studio/server/domain.ts +205 -27
  39. package/studio/server/handoff/service.ts +1 -1
  40. package/studio/server/index.ts +5 -8
  41. package/studio/server/introspect/extractor.ts +111 -17
  42. package/studio/server/introspect/overlay-tsmorph.test.ts +9 -9
  43. package/studio/server/introspect/overlay.ts +1 -1
  44. package/studio/server/introspect/runtime.ts +9 -1
  45. package/studio/server/introspect/source-overlay/spans.ts +2 -5
  46. package/studio/server/lifecycle.ts +1 -1
  47. package/studio/server/state/baseline.test.ts +14 -0
  48. package/studio/server/state/baseline.ts +16 -2
  49. package/studio/server/watch.test.ts +2 -1
  50. package/studio/server/watch.ts +5 -2
  51. package/studio/server/workspace/create.ts +9 -8
  52. package/studio/server/workspace-state.ts +4 -6
  53. package/studio/server/workspace-watch.ts +33 -22
  54. package/studio/shared/contracts/agent.ts +3 -0
  55. package/viewer/dist/main.js +12 -12
  56. package/src/__tests__/fixtures/publication.ts +0 -20
  57. package/src/admin/.spec/api.d.ts +0 -3
  58. package/src/admin/.spec/architecture.md +0 -22
  59. package/src/admin/.spec/icon.svg +0 -3
  60. package/src/admin/.spec/layout.ts +0 -6
  61. package/src/admin/__tests__/fixture.ts +0 -25
  62. package/src/admin/catalog/.spec/api.d.ts +0 -63
  63. package/src/admin/catalog/.spec/architecture.md +0 -7
  64. package/src/admin/catalog/.spec/icon.svg +0 -3
  65. package/src/admin/catalog/.spec/layout.ts +0 -6
  66. package/src/admin/catalog/__tests__/client.test.ts +0 -245
  67. package/src/admin/catalog/client.ts +0 -195
  68. package/src/admin/catalog/index.ts +0 -13
  69. package/src/admin/catalog/model.ts +0 -41
  70. package/src/admin/catalog/tsconfig.json +0 -8
  71. package/src/admin/contract.ts +0 -47
  72. package/src/admin/graph/.spec/api.d.ts +0 -22
  73. package/src/admin/graph/.spec/architecture.md +0 -5
  74. package/src/admin/graph/.spec/icon.svg +0 -3
  75. package/src/admin/graph/.spec/layout.ts +0 -3
  76. package/src/admin/graph/index.ts +0 -6
  77. package/src/admin/graph/nodes.ts +0 -56
  78. package/src/admin/index.ts +0 -2
  79. package/src/admin/instance/.spec/api.d.ts +0 -62
  80. package/src/admin/instance/.spec/architecture.md +0 -10
  81. package/src/admin/instance/.spec/icon.svg +0 -3
  82. package/src/admin/instance/.spec/layout.ts +0 -6
  83. package/src/admin/instance/__tests__/client.test.ts +0 -273
  84. package/src/admin/instance/client.ts +0 -191
  85. package/src/admin/instance/index.ts +0 -15
  86. package/src/admin/instance/model.ts +0 -59
  87. package/src/admin/instance/tsconfig.json +0 -8
  88. package/src/commands/__tests__/admin-instance.test.ts +0 -91
  89. package/src/commands/__tests__/auth-login.test.ts +0 -231
  90. package/src/commands/__tests__/auth-token.test.ts +0 -223
  91. package/src/commands/__tests__/call-describe.test.ts +0 -80
  92. package/src/commands/__tests__/call.test.ts +0 -63
  93. package/src/commands/__tests__/domain-install-operation.test.ts +0 -145
  94. package/src/commands/__tests__/domain-install-owned.test.ts +0 -191
  95. package/src/commands/__tests__/domain-list.test.ts +0 -133
  96. package/src/commands/__tests__/domain-uninstall.test.ts +0 -53
  97. package/src/commands/__tests__/install-direct.test.ts +0 -128
  98. package/src/commands/__tests__/install-identity-override.test.ts +0 -76
  99. package/src/commands/__tests__/instance-bookmark.test.ts +0 -166
  100. package/src/commands/__tests__/instance-list-admin.test.ts +0 -26
  101. package/src/commands/__tests__/instance-list-rows.test.ts +0 -64
  102. package/src/commands/__tests__/instance-status.test.ts +0 -139
  103. package/src/commands/__tests__/instance-use.test.ts +0 -67
  104. package/src/commands/__tests__/introspect-parse.test.ts +0 -100
  105. package/src/commands/__tests__/logs.test.ts +0 -345
  106. package/src/commands/__tests__/read-commands.test.ts +0 -377
  107. package/src/commands/__tests__/setup-plan.test.ts +0 -61
  108. package/src/commands/__tests__/studio-descriptor.test.ts +0 -22
  109. package/src/commands/__tests__/token-ttl.test.ts +0 -66
  110. package/src/commands/__tests__/update.test.ts +0 -95
  111. package/src/commands/__tests__/view-build.test.ts +0 -58
  112. package/src/commands/__tests__/view.test.ts +0 -197
  113. package/src/commands/admin/status.ts +0 -61
  114. package/src/commands/admin/use.ts +0 -100
  115. package/src/commands/auth/login.ts +0 -82
  116. package/src/commands/auth/logout.ts +0 -53
  117. package/src/commands/auth/status.ts +0 -86
  118. package/src/commands/auth/token.ts +0 -174
  119. package/src/commands/browser.ts +0 -236
  120. package/src/commands/call-describe.ts +0 -123
  121. package/src/commands/call.ts +0 -230
  122. package/src/commands/domain/install.ts +0 -575
  123. package/src/commands/domain/list.ts +0 -155
  124. package/src/commands/domain/publish.ts +0 -136
  125. package/src/commands/domain/uninstall.ts +0 -128
  126. package/src/commands/get.ts +0 -69
  127. package/src/commands/identity/__tests__/register.test.ts +0 -93
  128. package/src/commands/identity/create.ts +0 -35
  129. package/src/commands/identity/delete.ts +0 -24
  130. package/src/commands/identity/export.ts +0 -46
  131. package/src/commands/identity/import.ts +0 -61
  132. package/src/commands/identity/list.ts +0 -56
  133. package/src/commands/identity/register.ts +0 -195
  134. package/src/commands/identity/sync.ts +0 -38
  135. package/src/commands/identity/unsync.ts +0 -34
  136. package/src/commands/identity/use.ts +0 -24
  137. package/src/commands/identity/whoami.ts +0 -34
  138. package/src/commands/idp/add.ts +0 -150
  139. package/src/commands/idp/list.ts +0 -57
  140. package/src/commands/idp/refresh.ts +0 -38
  141. package/src/commands/idp/remove.ts +0 -36
  142. package/src/commands/idp/show.ts +0 -29
  143. package/src/commands/instance/active.ts +0 -73
  144. package/src/commands/instance/bookmark.ts +0 -101
  145. package/src/commands/instance/create.ts +0 -61
  146. package/src/commands/instance/delete.ts +0 -69
  147. package/src/commands/instance/forget.ts +0 -26
  148. package/src/commands/instance/list.ts +0 -197
  149. package/src/commands/instance/status.ts +0 -92
  150. package/src/commands/instance/use.ts +0 -263
  151. package/src/commands/introspect.ts +0 -129
  152. package/src/commands/logs.ts +0 -371
  153. package/src/commands/mutate.ts +0 -122
  154. package/src/commands/query.ts +0 -142
  155. package/src/commands/session/analyze.ts +0 -72
  156. package/src/commands/session/list.ts +0 -36
  157. package/src/commands/setup.ts +0 -54
  158. package/src/commands/status.ts +0 -60
  159. package/src/commands/studio.ts +0 -434
  160. package/src/commands/token.ts +0 -121
  161. package/src/commands/ui/__tests__/commands.test.ts +0 -152
  162. package/src/commands/ui/add.ts +0 -51
  163. package/src/commands/ui/doctor.ts +0 -21
  164. package/src/commands/ui/init.ts +0 -38
  165. package/src/commands/ui/list.ts +0 -26
  166. package/src/commands/ui/preset-apply.ts +0 -19
  167. package/src/commands/ui/preset-list.ts +0 -12
  168. package/src/commands/ui/shared.ts +0 -25
  169. package/src/commands/update.ts +0 -356
  170. package/src/commands/use.ts +0 -86
  171. package/src/commands/view-serve.ts +0 -26
  172. package/src/commands/view.ts +0 -658
  173. package/src/connect-core.test.ts +0 -38
  174. package/src/connect-core.ts +0 -42
  175. package/src/connection/.spec/api.d.ts +0 -110
  176. package/src/connection/.spec/architecture.md +0 -58
  177. package/src/connection/.spec/capabilities/connection.ts +0 -13
  178. package/src/connection/.spec/examples/call.ts +0 -7
  179. package/src/connection/.spec/flows/command.ts +0 -20
  180. package/src/connection/.spec/flows/session.ts +0 -62
  181. package/src/connection/.spec/laws/connection.ts +0 -213
  182. package/src/connection/.spec/layout.ts +0 -22
  183. package/src/connection/__tests__/auth.test.ts +0 -129
  184. package/src/connection/__tests__/ca-fetch.test.ts +0 -60
  185. package/src/connection/__tests__/call.test.ts +0 -17
  186. package/src/connection/__tests__/credential.test.ts +0 -254
  187. package/src/connection/__tests__/errors.test.ts +0 -577
  188. package/src/connection/__tests__/exchange.test.ts +0 -404
  189. package/src/connection/__tests__/failure-fixtures.ts +0 -39
  190. package/src/connection/__tests__/failure-safety.test.ts +0 -75
  191. package/src/connection/__tests__/fixtures/localhost-cert.pem +0 -19
  192. package/src/connection/__tests__/fixtures/localhost-key.base64 +0 -1
  193. package/src/connection/__tests__/managed.test.ts +0 -39
  194. package/src/connection/__tests__/reasons.test.ts +0 -78
  195. package/src/connection/__tests__/self.test.ts +0 -71
  196. package/src/connection/__tests__/session.test.ts +0 -144
  197. package/src/connection/__tests__/target.test.ts +0 -95
  198. package/src/connection/auth.ts +0 -265
  199. package/src/connection/call.ts +0 -7
  200. package/src/connection/command.ts +0 -67
  201. package/src/connection/credential.ts +0 -138
  202. package/src/connection/errors.ts +0 -1
  203. package/src/connection/exchange.ts +0 -394
  204. package/src/connection/failure/classify.ts +0 -110
  205. package/src/connection/failure/index.ts +0 -18
  206. package/src/connection/failure/model.ts +0 -28
  207. package/src/connection/failure/render.ts +0 -116
  208. package/src/connection/index.ts +0 -8
  209. package/src/connection/lifetime.ts +0 -25
  210. package/src/connection/reasons.ts +0 -209
  211. package/src/connection/self.ts +0 -138
  212. package/src/connection/session.ts +0 -203
  213. package/src/connection/target.ts +0 -112
  214. package/src/connection/tsconfig.json +0 -6
  215. package/src/errors.ts +0 -66
  216. package/src/graph/.spec/api.d.ts +0 -41
  217. package/src/graph/.spec/laws/documents.ts +0 -24
  218. package/src/graph/.spec/laws/projection.ts +0 -13
  219. package/src/graph/.spec/layout.ts +0 -14
  220. package/src/graph/__tests__/mutation.test.ts +0 -51
  221. package/src/graph/__tests__/projection.test.ts +0 -22
  222. package/src/graph/__tests__/query.test.ts +0 -180
  223. package/src/graph/class.ts +0 -30
  224. package/src/graph/index.ts +0 -4
  225. package/src/graph/mutation.ts +0 -46
  226. package/src/graph/projection.ts +0 -21
  227. package/src/graph/query.ts +0 -95
  228. package/src/graph/tsconfig.json +0 -6
  229. package/src/identity/.history/ard/bootstrap-root-transfer.md +0 -57
  230. package/src/identity/.history/ard/domain-mediated-registration.md +0 -36
  231. package/src/identity/.spec/api.d.ts +0 -124
  232. package/src/identity/.spec/architecture.md +0 -33
  233. package/src/identity/.spec/capabilities/identity.ts +0 -7
  234. package/src/identity/.spec/flows/bootstrap-root.ts +0 -79
  235. package/src/identity/.spec/flows/domain-mediated-registration.ts +0 -51
  236. package/src/identity/.spec/flows/import.ts +0 -29
  237. package/src/identity/.spec/laws/identity.ts +0 -56
  238. package/src/identity/.spec/layout.ts +0 -13
  239. package/src/identity/__tests__/fixtures/registry-journey.ts +0 -65
  240. package/src/identity/__tests__/registration.test.ts +0 -64
  241. package/src/identity/__tests__/registry.test.ts +0 -74
  242. package/src/identity/__tests__/transfer.test.ts +0 -198
  243. package/src/identity/index.ts +0 -23
  244. package/src/identity/registration.ts +0 -72
  245. package/src/identity/registry.ts +0 -181
  246. package/src/identity/transfer.ts +0 -187
  247. package/src/identity/tsconfig.json +0 -6
  248. package/src/index.ts +0 -1
  249. package/src/keys/.spec/api.d.ts +0 -74
  250. package/src/keys/.spec/architecture.md +0 -13
  251. package/src/keys/.spec/laws/keys.ts +0 -43
  252. package/src/keys/.spec/layout.ts +0 -6
  253. package/src/keys/__tests__/algorithm.test.ts +0 -39
  254. package/src/keys/__tests__/keys.test.ts +0 -172
  255. package/src/keys/algorithm.ts +0 -33
  256. package/src/keys/credential.ts +0 -113
  257. package/src/keys/index.ts +0 -14
  258. package/src/keys/pair.ts +0 -219
  259. package/src/keys/tsconfig.json +0 -6
  260. package/src/lib/__tests__/admin-target.test.ts +0 -184
  261. package/src/lib/__tests__/binary.test.ts +0 -70
  262. package/src/lib/__tests__/browser-retention.test.ts +0 -212
  263. package/src/lib/__tests__/command-dx.test.ts +0 -102
  264. package/src/lib/__tests__/concurrency.test.ts +0 -62
  265. package/src/lib/__tests__/config.test.ts +0 -81
  266. package/src/lib/__tests__/design.test.ts +0 -99
  267. package/src/lib/__tests__/domain-publication.test.ts +0 -138
  268. package/src/lib/__tests__/format.test.ts +0 -22
  269. package/src/lib/__tests__/idp-session.driver.ts +0 -60
  270. package/src/lib/__tests__/idp-session.test.ts +0 -404
  271. package/src/lib/__tests__/idp.test.ts +0 -415
  272. package/src/lib/__tests__/instance-candidates.test.ts +0 -146
  273. package/src/lib/__tests__/instance-target.test.ts +0 -213
  274. package/src/lib/__tests__/instance.test.ts +0 -192
  275. package/src/lib/__tests__/local-status.test.ts +0 -202
  276. package/src/lib/__tests__/log-errors.test.ts +0 -64
  277. package/src/lib/__tests__/log.test.ts +0 -31
  278. package/src/lib/__tests__/meta.test.ts +0 -55
  279. package/src/lib/__tests__/output.test.ts +0 -162
  280. package/src/lib/__tests__/panel.test.ts +0 -40
  281. package/src/lib/__tests__/port.test.ts +0 -44
  282. package/src/lib/__tests__/prompt.test.ts +0 -25
  283. package/src/lib/__tests__/sdk-deps.test.ts +0 -68
  284. package/src/lib/__tests__/self.test.ts +0 -50
  285. package/src/lib/__tests__/skills.test.ts +0 -834
  286. package/src/lib/__tests__/studio-server-deps.test.ts +0 -111
  287. package/src/lib/__tests__/table.test.ts +0 -53
  288. package/src/lib/__tests__/update.test.ts +0 -511
  289. package/src/lib/__tests__/use-target.test.ts +0 -56
  290. package/src/lib/__tests__/validation.test.ts +0 -34
  291. package/src/lib/__tests__/view-assets.test.ts +0 -171
  292. package/src/lib/__tests__/view-external-open-intent.test.ts +0 -150
  293. package/src/lib/__tests__/view-external-open-origins.test.ts +0 -27
  294. package/src/lib/__tests__/view-open-intent.test.ts +0 -280
  295. package/src/lib/__tests__/view-port-allocation.test.ts +0 -82
  296. package/src/lib/__tests__/view-server.test.ts +0 -91
  297. package/src/lib/__tests__/view-session.test.ts +0 -100
  298. package/src/lib/__tests__/view-snapshot.test.ts +0 -77
  299. package/src/lib/admin-domain.ts +0 -43
  300. package/src/lib/admin-instance.ts +0 -56
  301. package/src/lib/admin-target.ts +0 -290
  302. package/src/lib/binary.ts +0 -127
  303. package/src/lib/browser-retention.ts +0 -210
  304. package/src/lib/browser.ts +0 -150
  305. package/src/lib/ca-fetch.ts +0 -133
  306. package/src/lib/command-dx.ts +0 -191
  307. package/src/lib/concurrency.ts +0 -31
  308. package/src/lib/config.ts +0 -57
  309. package/src/lib/credential-lifetime.ts +0 -24
  310. package/src/lib/domain-publication.ts +0 -104
  311. package/src/lib/failure-debug.ts +0 -26
  312. package/src/lib/format.ts +0 -4
  313. package/src/lib/idp-session.ts +0 -156
  314. package/src/lib/idp.ts +0 -926
  315. package/src/lib/instance-candidates.ts +0 -49
  316. package/src/lib/instance-target.ts +0 -197
  317. package/src/lib/instance.ts +0 -468
  318. package/src/lib/invocation.ts +0 -11
  319. package/src/lib/local-status.ts +0 -152
  320. package/src/lib/log.ts +0 -143
  321. package/src/lib/login-flow.ts +0 -201
  322. package/src/lib/meta.ts +0 -90
  323. package/src/lib/output.ts +0 -238
  324. package/src/lib/panel.ts +0 -61
  325. package/src/lib/port.ts +0 -41
  326. package/src/lib/proc.ts +0 -87
  327. package/src/lib/prompt.ts +0 -136
  328. package/src/lib/provision-instance.ts +0 -139
  329. package/src/lib/sdk-deps.ts +0 -104
  330. package/src/lib/self.ts +0 -22
  331. package/src/lib/skills/lock.ts +0 -117
  332. package/src/lib/skills/sync.ts +0 -878
  333. package/src/lib/skills.ts +0 -120
  334. package/src/lib/table.ts +0 -62
  335. package/src/lib/update.ts +0 -568
  336. package/src/lib/use-target.ts +0 -24
  337. package/src/lib/validation.ts +0 -59
  338. package/src/lib/view/assets.ts +0 -87
  339. package/src/lib/view/external-open-intent.ts +0 -41
  340. package/src/lib/view/external-open-origins.ts +0 -37
  341. package/src/lib/view/host-capabilities.ts +0 -17
  342. package/src/lib/view/open-intent.ts +0 -67
  343. package/src/lib/view/port-allocation.ts +0 -19
  344. package/src/lib/view/resolve.ts +0 -111
  345. package/src/lib/view/server.ts +0 -259
  346. package/src/lib/view/session.ts +0 -154
  347. package/src/lib/view/snapshot.ts +0 -101
  348. package/src/paths/.spec/api.d.ts +0 -13
  349. package/src/paths/.spec/architecture.md +0 -5
  350. package/src/paths/.spec/layout.ts +0 -3
  351. package/src/paths/index.ts +0 -13
  352. package/src/program/.spec/api.d.ts +0 -43
  353. package/src/program/.spec/architecture.md +0 -18
  354. package/src/program/.spec/laws/program.ts +0 -29
  355. package/src/program/.spec/layout.ts +0 -14
  356. package/src/program/__tests__/program.test.ts +0 -556
  357. package/src/program/argv.ts +0 -14
  358. package/src/program/build.ts +0 -205
  359. package/src/program/command.ts +0 -43
  360. package/src/program/index.ts +0 -3
  361. package/src/program/options.ts +0 -41
  362. package/src/program/registry.ts +0 -64
  363. package/src/program/tsconfig.json +0 -6
  364. package/src/setup/__tests__/instance-step.test.ts +0 -256
  365. package/src/setup/__tests__/util.test.ts +0 -29
  366. package/src/setup/engine.ts +0 -83
  367. package/src/setup/render.ts +0 -107
  368. package/src/setup/steps/admin.ts +0 -93
  369. package/src/setup/steps/agent-browser.ts +0 -81
  370. package/src/setup/steps/auth.ts +0 -54
  371. package/src/setup/steps/domain.ts +0 -68
  372. package/src/setup/steps/index.ts +0 -18
  373. package/src/setup/steps/instance.ts +0 -206
  374. package/src/setup/steps/skills-bridge.ts +0 -70
  375. package/src/setup/steps/skills.ts +0 -61
  376. package/src/setup/types.ts +0 -61
  377. package/src/setup/util.ts +0 -34
  378. package/src/state/.spec/api.d.ts +0 -152
  379. package/src/state/.spec/architecture.md +0 -45
  380. package/src/state/.spec/capabilities/state.ts +0 -7
  381. package/src/state/.spec/flows/identity-update.ts +0 -22
  382. package/src/state/.spec/laws/state.ts +0 -73
  383. package/src/state/.spec/layout.ts +0 -15
  384. package/src/state/__tests__/exchange-credentials.test.ts +0 -341
  385. package/src/state/__tests__/files.test.ts +0 -136
  386. package/src/state/__tests__/fixtures/identity-transition.ts +0 -37
  387. package/src/state/__tests__/fixtures/session-route-process.ts +0 -93
  388. package/src/state/__tests__/identities.test.ts +0 -142
  389. package/src/state/__tests__/paths.test.ts +0 -28
  390. package/src/state/__tests__/session-routes.test.ts +0 -138
  391. package/src/state/exchange-credentials.ts +0 -250
  392. package/src/state/files.ts +0 -170
  393. package/src/state/identities.ts +0 -229
  394. package/src/state/index.ts +0 -31
  395. package/src/state/paths.ts +0 -64
  396. package/src/state/session-routes.ts +0 -34
  397. package/src/state/tsconfig.json +0 -6
  398. package/src/telemetry/__tests__/analyze-log.test.ts +0 -38
  399. package/src/telemetry/__tests__/gate.test.ts +0 -63
  400. package/src/telemetry/__tests__/recorder.test.ts +0 -111
  401. package/src/telemetry/__tests__/redact.test.ts +0 -79
  402. package/src/telemetry/__tests__/retention.test.ts +0 -267
  403. package/src/telemetry/__tests__/session.test.ts +0 -166
  404. package/src/telemetry/__tests__/settings.test.ts +0 -102
  405. package/src/telemetry/__tests__/store-scan.test.ts +0 -128
  406. package/src/telemetry/__tests__/trigger.test.ts +0 -78
  407. package/src/telemetry/adapters/__tests__/claude-code.test.ts +0 -89
  408. package/src/telemetry/adapters/__tests__/codex.test.ts +0 -138
  409. package/src/telemetry/adapters/__tests__/index.test.ts +0 -88
  410. package/src/telemetry/adapters/claude-code.ts +0 -90
  411. package/src/telemetry/adapters/codex.ts +0 -160
  412. package/src/telemetry/adapters/index.ts +0 -45
  413. package/src/telemetry/adapters/types.ts +0 -21
  414. package/src/telemetry/analyze.ts +0 -249
  415. package/src/telemetry/gate.ts +0 -79
  416. package/src/telemetry/recorder.ts +0 -68
  417. package/src/telemetry/redact.ts +0 -53
  418. package/src/telemetry/retention.ts +0 -176
  419. package/src/telemetry/session.ts +0 -94
  420. package/src/telemetry/settings.ts +0 -79
  421. package/src/telemetry/store.ts +0 -174
  422. package/src/telemetry/trigger.ts +0 -107
  423. package/src/telemetry/types.ts +0 -64
  424. package/src/test-utils.ts +0 -18
  425. package/src/ui/.spec/api.d.ts +0 -14
  426. package/src/ui/.spec/architecture.md +0 -14
  427. package/src/ui/.spec/laws.ts +0 -50
  428. package/src/ui/.spec/layout.ts +0 -16
  429. package/src/ui/__tests__/ui.test.ts +0 -1742
  430. package/src/ui/index.ts +0 -13
  431. package/src/ui/lock.ts +0 -89
  432. package/src/ui/model.ts +0 -83
  433. package/src/ui/operations.ts +0 -1044
  434. package/src/ui/project.ts +0 -333
  435. package/src/ui/release.ts +0 -398
  436. package/src/ui/runner.ts +0 -18
  437. package/studio/client/dist/assets/index-BFVFs_8x.js +0 -81
  438. package/studio/client/dist/assets/index-DuB9iUdu.css +0 -2
  439. package/studio/client/dist/assets/schema-studio-DH6oEWME.js +0 -8
  440. package/studio/tsconfig.json +0 -24
  441. package/viewer/tsconfig.json +0 -13
package/src/lib/idp.ts DELETED
@@ -1,926 +0,0 @@
1
- import { decodeJwt, type JWTPayload } from 'jose'
2
- import { mkdir, readFile, readdir, unlink } from 'node:fs/promises'
3
- import { dirname, join } from 'node:path'
4
- import { z } from 'zod'
5
-
6
- import { atomicWrite, IDPS_PATH, IDP_SESSIONS_DIR, paths } from '../state/index'
7
- import { credentialLifetimeCovers } from './credential-lifetime'
8
- import { log } from './log'
9
- import { validateName, validateUrl } from './validation'
10
-
11
- /**
12
- * Thrown when a refresh succeeds but the IdP mints an access token whose `aud`
13
- * does not match the one the caller requested. Distinct from a transport/grant
14
- * failure: the session is *healthy*, the IdP simply will not issue that
15
- * audience (e.g. WorkOS AuthKit ignores the `audience` param and stamps a fixed
16
- * API audience per environment). Re-login does not help — callers must target
17
- * an instance whose audience equals `actual`, or reconfigure the IdP/bookmark.
18
- */
19
- export class IdpAudienceMismatchError extends Error {
20
- readonly requested: string
21
- readonly actual: string | undefined
22
-
23
- constructor(requested: string, actual: string | undefined) {
24
- super(
25
- `IdP minted an access token for audience ${actual ?? '(none)'} but ${requested} was required`,
26
- )
27
- this.name = 'IdpAudienceMismatchError'
28
- this.requested = requested
29
- this.actual = actual
30
- }
31
- }
32
-
33
- /**
34
- * A token-endpoint request that the IdP answered with an OAuth error (or that
35
- * failed at the HTTP layer). Carries the structured OAuth `error` code so
36
- * callers can tell a definitively dead grant (`invalid_grant`) from a
37
- * transient outage — the message string alone cannot.
38
- */
39
- export class OAuthTokenError extends Error {
40
- /** OAuth `error` code, e.g. `invalid_grant`. */
41
- readonly code?: string
42
- /** OAuth `error_description`. */
43
- readonly description?: string
44
- /** HTTP status of the token response. */
45
- readonly status?: number
46
-
47
- constructor(args: { code?: string; description?: string; status?: number }) {
48
- super(
49
- `OAuth token request failed: ${args.description ?? args.code ?? `HTTP ${args.status ?? '?'}`}`,
50
- )
51
- this.name = 'OAuthTokenError'
52
- this.code = args.code
53
- this.description = args.description
54
- this.status = args.status
55
- }
56
- }
57
-
58
- export type RefreshFailureKind = 'session-ended' | 'org-rejected' | 'transient' | 'unknown'
59
-
60
- /**
61
- * Classify a `refreshSession` failure so callers only demand a re-login when
62
- * the grant is actually dead. `org-rejected` = healthy session, wrong/stale
63
- * org scope (re-login can't fix it); 5xx/429 and fetch-layer failures are
64
- * transient — the cached session is likely still valid.
65
- */
66
- export function classifyRefreshFailure(e: unknown): RefreshFailureKind {
67
- if (e instanceof OAuthTokenError) {
68
- if (
69
- /not a member of the organization|organization not found/i.test(e.description ?? e.message)
70
- ) {
71
- return 'org-rejected'
72
- }
73
- if (e.code === 'invalid_grant') return 'session-ended'
74
- if (e.status !== undefined && (e.status >= 500 || e.status === 429)) return 'transient'
75
- return 'unknown'
76
- }
77
- if (e instanceof Error) {
78
- if (e.name === 'AbortError' || e.name === 'TimeoutError') return 'transient'
79
- // Node's fetch rejects network-layer failures as TypeError.
80
- if (e instanceof TypeError) return 'transient'
81
- // Bun stamps codes like ConnectionRefused/ConnectionClosed/FailedToOpenSocket;
82
- // Node uses the classic ECONN*/ETIMEDOUT family.
83
- const code = (e as { code?: string }).code
84
- if (
85
- typeof code === 'string' &&
86
- /^(ECONN|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|ENETUNREACH|Connection|FailedToOpenSocket|Timeout)/.test(
87
- code,
88
- )
89
- ) {
90
- return 'transient'
91
- }
92
- }
93
- return 'unknown'
94
- }
95
-
96
- export const OidcMetadataSchema = z
97
- .object({
98
- issuer: z.string().url(),
99
- authorization_endpoint: z.string().url().optional(),
100
- token_endpoint: z.string().url(),
101
- jwks_uri: z.string().url(),
102
- device_authorization_endpoint: z.string().url().optional(),
103
- revocation_endpoint: z.string().url().optional(),
104
- scopes_supported: z.array(z.string()).optional(),
105
- grant_types_supported: z.array(z.string()).optional(),
106
- response_types_supported: z.array(z.string()).optional(),
107
- code_challenge_methods_supported: z.array(z.string()).optional(),
108
- })
109
- .passthrough()
110
-
111
- export type OidcMetadata = z.infer<typeof OidcMetadataSchema>
112
-
113
- export const IdpClientConfigSchema = z
114
- .object({
115
- client_id: z.string().optional(),
116
- client_secret_env: z.string().optional(),
117
- redirect_uris: z.array(z.string().url()).optional(),
118
- scope: z.string().optional(),
119
- public: z.boolean().optional(),
120
- token_response: z.enum(['oauth', 'workos-authkit']).optional(),
121
- token_request_format: z.enum(['form', 'json']).optional(),
122
- workos_application_id: z.string().optional(),
123
- workos_application_type: z.enum(['oauth', 'm2m']).optional(),
124
- })
125
- .passthrough()
126
-
127
- export type IdpClientConfig = z.infer<typeof IdpClientConfigSchema>
128
-
129
- export const IdpIndexEntrySchema = z.object({
130
- issuer: z.string().url(),
131
- builtIn: z.boolean().optional(),
132
- createdAt: z.string(),
133
- updatedAt: z.string(),
134
- })
135
-
136
- export const IdpStoreSchema = z.object({
137
- idps: z.record(z.string(), IdpIndexEntrySchema),
138
- })
139
-
140
- export type IdpStore = z.infer<typeof IdpStoreSchema>
141
- export type IdpIndexEntry = z.infer<typeof IdpIndexEntrySchema>
142
-
143
- export const IdpSessionSchema = z
144
- .object({
145
- identity: z.string(),
146
- idp: z.string(),
147
- issuer: z.string().url(),
148
- subject: z.string(),
149
- /** OAuth client that issued this session's refresh token. */
150
- clientId: z.string().optional(),
151
- audience: z.string().optional(),
152
- organizationId: z.string().optional(),
153
- access_token: z.string(),
154
- id_token: z.string().optional(),
155
- refresh_token: z.string().optional(),
156
- token_type: z.string().optional(),
157
- scope: z.string().optional(),
158
- expires_at: z.string().optional(),
159
- claims: z.record(z.string(), z.unknown()).optional(),
160
- /**
161
- * Access tokens by audience. WorkOS mints one `aud` per token, so a
162
- * session juggling several instances would otherwise burn a (single-use)
163
- * refresh-token rotation on every instance flip. The top-level
164
- * `access_token`/`expires_at` stay the most recently minted token for
165
- * back-compat with older CLI builds.
166
- */
167
- tokens: z
168
- .record(
169
- z.string(),
170
- z.object({ access_token: z.string(), expires_at: z.string().optional() }).passthrough(),
171
- )
172
- .optional(),
173
- updatedAt: z.string(),
174
- })
175
- .passthrough()
176
-
177
- export type IdpSession = z.infer<typeof IdpSessionSchema>
178
-
179
- export type TokenResponse = {
180
- access_token?: string
181
- accessToken?: string
182
- id_token?: string
183
- idToken?: string
184
- refresh_token?: string
185
- refreshToken?: string
186
- token_type?: string
187
- scope?: string
188
- expires_in?: number
189
- expiresIn?: number
190
- user?: { id?: string; [key: string]: unknown }
191
- organization_id?: string
192
- authentication_method?: string
193
- error?: string
194
- error_description?: string
195
- [key: string]: unknown
196
- }
197
-
198
- export type DeviceAuthorizationResponse = {
199
- device_code: string
200
- user_code?: string
201
- verification_uri?: string
202
- verification_uri_complete?: string
203
- expires_in: number
204
- interval?: number
205
- message?: string
206
- [key: string]: unknown
207
- }
208
-
209
- export type IdpConfig = {
210
- name: string
211
- entry: IdpIndexEntry
212
- metadata: OidcMetadata
213
- client: IdpClientConfig
214
- }
215
-
216
- export const BUILTIN_WORKOS_IDP_NAME = 'workos'
217
- const DEFAULT_WORKOS_API_HOST = 'https://api.workos.com'
218
- const DEFAULT_WORKOS_CLIENT_ID = 'client_01KC29HET5F3QAQ8GNTPZ7F320'
219
- const LEGACY_WORKOS_CLIENT_ID = 'client_01KC29HEGD7B40TV2C4QZ436BG'
220
- const WORKOS_CLIENT_ID_ENV_NAMES = ['WORKOS_CLIENT_ID', 'VITE_WORKOS_CLIENT_ID'] as const
221
-
222
- export function idpDir(name: string): string {
223
- return paths.idpDir(name)
224
- }
225
-
226
- export function idpMetadataPath(name: string): string {
227
- return join(idpDir(name), 'metadata.json')
228
- }
229
-
230
- export function idpClientPath(name: string): string {
231
- return join(idpDir(name), 'client.json')
232
- }
233
-
234
- export function idpSessionPath(identityName: string): string {
235
- validateName(identityName, 'Identity')
236
- return paths.idpSession(identityName)
237
- }
238
-
239
- export async function readIdpStore(): Promise<IdpStore> {
240
- try {
241
- const raw = await readFile(IDPS_PATH, 'utf-8')
242
- return IdpStoreSchema.parse(JSON.parse(raw))
243
- } catch (e) {
244
- if (e instanceof z.ZodError) {
245
- log.warn(`Invalid IdP index at ${IDPS_PATH} — using empty registry`)
246
- } else if ((e as { code?: string }).code !== 'ENOENT') {
247
- log.warn(`Could not read IdP index at ${IDPS_PATH} — using empty registry`)
248
- }
249
- return { idps: {} }
250
- }
251
- }
252
-
253
- export async function writeIdpStore(store: IdpStore): Promise<void> {
254
- await mkdir(dirname(IDPS_PATH), { recursive: true })
255
- await atomicWrite(IDPS_PATH, JSON.stringify(store, null, 2) + '\n')
256
- }
257
-
258
- export async function readIdpConfig(name: string): Promise<IdpConfig> {
259
- validateName(name, 'IdP')
260
- const store = await readIdpStore()
261
- const entry = store.idps[name]
262
- if (!entry)
263
- throw new Error(`IdP "${name}" not found. Run: astrale idp add ${name} --issuer <url>`)
264
- const [metadataRaw, clientRaw] = await Promise.all([
265
- readFile(idpMetadataPath(name), 'utf-8'),
266
- readFile(idpClientPath(name), 'utf-8').catch((e) => {
267
- if ((e as { code?: string }).code === 'ENOENT') return '{}'
268
- throw e
269
- }),
270
- ])
271
- return {
272
- name,
273
- entry,
274
- metadata: OidcMetadataSchema.parse(JSON.parse(metadataRaw)),
275
- client: IdpClientConfigSchema.parse(JSON.parse(clientRaw)),
276
- }
277
- }
278
-
279
- export async function readIdpConfigOrBuiltin(
280
- name: string,
281
- opts: { clientId?: string; persist?: boolean } = {},
282
- ): Promise<IdpConfig> {
283
- validateName(name, 'IdP')
284
- const store = await readIdpStore()
285
- if (store.idps[name]) {
286
- const existing = await readIdpConfig(name)
287
- const replacement = opts.persist
288
- ? legacyBuiltinWorkosReplacement(existing, opts.clientId)
289
- : undefined
290
- if (!replacement) return existing
291
- return upsertIdpConfig({
292
- name: replacement.name,
293
- metadata: replacement.metadata,
294
- client: replacement.client,
295
- builtIn: true,
296
- })
297
- }
298
-
299
- const builtin = builtinIdpConfig(name, opts.clientId)
300
- if (!builtin) {
301
- throw new Error(`IdP "${name}" not found. Run: astrale idp add ${name} --issuer <url>`)
302
- }
303
- if (!opts.persist) return builtin
304
- return upsertIdpConfig({
305
- name: builtin.name,
306
- metadata: builtin.metadata,
307
- client: builtin.client,
308
- builtIn: true,
309
- })
310
- }
311
-
312
- export async function listIdpConfigs(): Promise<IdpConfig[]> {
313
- const store = await readIdpStore()
314
- const entries = await Promise.all(
315
- Object.keys(store.idps).map((name) => readIdpConfig(name).catch(() => undefined)),
316
- )
317
- const configs = entries.filter((entry): entry is IdpConfig => !!entry)
318
- if (!store.idps[BUILTIN_WORKOS_IDP_NAME]) {
319
- const workos = builtinIdpConfig(BUILTIN_WORKOS_IDP_NAME)
320
- if (workos) configs.push(workos)
321
- }
322
- return configs
323
- }
324
-
325
- export async function upsertIdpConfig(args: {
326
- name: string
327
- metadata: OidcMetadata
328
- client?: IdpClientConfig
329
- builtIn?: boolean
330
- }): Promise<IdpConfig> {
331
- validateName(args.name, 'IdP')
332
- const store = await readIdpStore()
333
- const now = new Date().toISOString()
334
- const existing = store.idps[args.name]
335
- store.idps[args.name] = {
336
- issuer: args.metadata.issuer,
337
- builtIn: args.builtIn ?? existing?.builtIn,
338
- createdAt: existing?.createdAt ?? now,
339
- updatedAt: now,
340
- }
341
- await mkdir(idpDir(args.name), { recursive: true })
342
- await Promise.all([
343
- atomicWrite(idpMetadataPath(args.name), JSON.stringify(args.metadata, null, 2) + '\n'),
344
- atomicWrite(idpClientPath(args.name), JSON.stringify(args.client ?? {}, null, 2) + '\n'),
345
- writeIdpStore(store),
346
- ])
347
- return readIdpConfig(args.name)
348
- }
349
-
350
- export async function removeIdpConfig(name: string): Promise<void> {
351
- validateName(name, 'IdP')
352
- const store = await readIdpStore()
353
- const entry = store.idps[name]
354
- if (!entry) throw new Error(`IdP "${name}" not found`)
355
- if (entry.builtIn) throw new Error(`Cannot remove built-in IdP "${name}"`)
356
- delete store.idps[name]
357
- await writeIdpStore(store)
358
- await unlink(idpMetadataPath(name)).catch(() => undefined)
359
- await unlink(idpClientPath(name)).catch(() => undefined)
360
- }
361
-
362
- export async function fetchOidcMetadata(issuer: string): Promise<OidcMetadata> {
363
- validateUrl(issuer)
364
- const discoveryUrl = new URL('/.well-known/openid-configuration', issuer)
365
- const response = await fetch(discoveryUrl)
366
- if (!response.ok) {
367
- throw new Error(`OIDC discovery failed for ${issuer}: HTTP ${response.status}`)
368
- }
369
- const body = await response.json()
370
- let metadata = OidcMetadataSchema.parse(body)
371
- if (normalizeIssuer(metadata.issuer) !== normalizeIssuer(issuer)) {
372
- throw new Error(
373
- `OIDC issuer mismatch: discovery returned ${metadata.issuer}, expected ${issuer}`,
374
- )
375
- }
376
- if (!metadata.device_authorization_endpoint) {
377
- const oauthMetadata = await fetchOAuthAuthorizationServerMetadata(issuer).catch(() => undefined)
378
- if (oauthMetadata) metadata = OidcMetadataSchema.parse({ ...metadata, ...oauthMetadata })
379
- }
380
- return metadata
381
- }
382
-
383
- export function workosAuthKitMetadata(
384
- apiHost = 'https://api.workos.com',
385
- clientId?: string,
386
- ): OidcMetadata {
387
- validateUrl(apiHost)
388
- const base = apiHost.replace(/\/+$/, '')
389
- return OidcMetadataSchema.parse({
390
- issuer: base,
391
- authorization_endpoint: `${base}/user_management/authorize`,
392
- token_endpoint: `${base}/user_management/authenticate`,
393
- device_authorization_endpoint: `${base}/user_management/authorize/device`,
394
- jwks_uri: clientId ? `${base}/sso/jwks/${clientId}` : `${base}/sso/jwks`,
395
- grant_types_supported: ['urn:ietf:params:oauth:grant-type:device_code'],
396
- response_types_supported: ['code'],
397
- })
398
- }
399
-
400
- export function builtinIdpConfig(
401
- name: string,
402
- clientIdOverride?: string,
403
- env: NodeJS.ProcessEnv = process.env,
404
- ): IdpConfig | null {
405
- if (name !== BUILTIN_WORKOS_IDP_NAME) return null
406
- const clientId = clientIdOverride ?? workosClientIdFromEnv(env)
407
- if (!clientId) return null
408
- const apiHost = env.WORKOS_API_HOSTNAME ?? DEFAULT_WORKOS_API_HOST
409
- const now = new Date().toISOString()
410
- return {
411
- name: BUILTIN_WORKOS_IDP_NAME,
412
- entry: {
413
- issuer: apiHost.replace(/\/+$/, ''),
414
- builtIn: true,
415
- createdAt: now,
416
- updatedAt: now,
417
- },
418
- metadata: workosAuthKitMetadata(apiHost, clientId),
419
- client: {
420
- client_id: clientId,
421
- public: true,
422
- token_response: 'workos-authkit',
423
- token_request_format: 'json',
424
- },
425
- }
426
- }
427
-
428
- export function workosClientIdFromEnv(env: NodeJS.ProcessEnv = process.env): string | undefined {
429
- return (
430
- WORKOS_CLIENT_ID_ENV_NAMES.map((name) => env[name]?.trim()).find(
431
- (value): value is string => typeof value === 'string' && value.length > 0,
432
- ) ?? DEFAULT_WORKOS_CLIENT_ID
433
- )
434
- }
435
-
436
- export function legacyBuiltinWorkosReplacement(
437
- existing: IdpConfig,
438
- clientIdOverride?: string,
439
- env: NodeJS.ProcessEnv = process.env,
440
- ): IdpConfig | undefined {
441
- if (
442
- existing.name !== BUILTIN_WORKOS_IDP_NAME ||
443
- !existing.entry.builtIn ||
444
- existing.client.client_id !== LEGACY_WORKOS_CLIENT_ID
445
- ) {
446
- return undefined
447
- }
448
- const replacement = builtinIdpConfig(existing.name, clientIdOverride, env)
449
- if (!replacement || replacement.client.client_id === LEGACY_WORKOS_CLIENT_ID) return undefined
450
- return replacement
451
- }
452
-
453
- async function fetchOAuthAuthorizationServerMetadata(
454
- issuer: string,
455
- ): Promise<Partial<OidcMetadata> | undefined> {
456
- const discoveryUrl = new URL('/.well-known/oauth-authorization-server', issuer)
457
- const response = await fetch(discoveryUrl)
458
- if (!response.ok) return undefined
459
- const body = (await response.json()) as Partial<OidcMetadata>
460
- if (typeof body.issuer === 'string' && normalizeIssuer(body.issuer) !== normalizeIssuer(issuer)) {
461
- return undefined
462
- }
463
- return body
464
- }
465
-
466
- export async function postForm(url: string, params: URLSearchParams): Promise<TokenResponse> {
467
- const response = await fetch(url, {
468
- method: 'POST',
469
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
470
- body: params,
471
- })
472
- return tokenResponseFrom(response)
473
- }
474
-
475
- export async function postJson(
476
- url: string,
477
- bodyInput: Record<string, string>,
478
- ): Promise<TokenResponse> {
479
- const response = await fetch(url, {
480
- method: 'POST',
481
- headers: { 'Content-Type': 'application/json' },
482
- body: JSON.stringify(bodyInput),
483
- })
484
- return tokenResponseFrom(response)
485
- }
486
-
487
- async function tokenResponseFrom(response: Response): Promise<TokenResponse> {
488
- const text = await response.text()
489
- let body: TokenResponse = {}
490
- try {
491
- body = text ? (JSON.parse(text) as TokenResponse) : {}
492
- } catch {
493
- // Non-JSON body (e.g. an HTML 502 from a proxy): surface the HTTP failure,
494
- // keep a snippet for diagnostics.
495
- throw new OAuthTokenError({
496
- status: response.status,
497
- description: `HTTP ${response.status} — non-JSON response: ${text.slice(0, 200)}`,
498
- })
499
- }
500
- if (!response.ok || body.error) {
501
- throw new OAuthTokenError({
502
- code: body.error,
503
- description: body.error_description,
504
- status: response.status,
505
- })
506
- }
507
- return body
508
- }
509
-
510
- export function tokenExpiresAt(token: TokenResponse): string | undefined {
511
- const expiresIn =
512
- typeof token.expires_in === 'number'
513
- ? token.expires_in
514
- : typeof token.expiresIn === 'number'
515
- ? token.expiresIn
516
- : undefined
517
- if (typeof expiresIn === 'number') return new Date(Date.now() + expiresIn * 1000).toISOString()
518
- return tokenExpiresAtFromJwt(token.access_token ?? token.accessToken ?? token.id_token)
519
- }
520
-
521
- export function decodeTokenClaims(token: string | undefined): JWTPayload | undefined {
522
- if (!token) return undefined
523
- try {
524
- return decodeJwt(token)
525
- } catch {
526
- return undefined
527
- }
528
- }
529
-
530
- export function tokenAudienceMatches(token: string | undefined, audience: string): boolean {
531
- const actual = decodeTokenClaims(token)?.aud
532
- if (Array.isArray(actual)) return actual.includes(audience)
533
- return actual === audience
534
- }
535
-
536
- function tokenExpiresAtFromJwt(token: string | undefined): string | undefined {
537
- const claims = decodeTokenClaims(token)
538
- return typeof claims?.exp === 'number' ? new Date(claims.exp * 1000).toISOString() : undefined
539
- }
540
-
541
- export function subjectFromToken(token: TokenResponse, fallback: string): string {
542
- const claims = decodeTokenClaims(token.id_token ?? token.access_token)
543
- const sub = claims?.sub
544
- return typeof sub === 'string' && sub ? sub : (token.user?.id ?? fallback)
545
- }
546
-
547
- export function issuerFromToken(token: TokenResponse, fallback: string): string {
548
- const claims = decodeTokenClaims(token.id_token ?? token.access_token)
549
- const iss = claims?.iss
550
- return typeof iss === 'string' && iss ? iss : fallback
551
- }
552
-
553
- export function normalizeTokenResponse(token: TokenResponse): TokenResponse {
554
- return {
555
- ...token,
556
- access_token: token.access_token ?? token.accessToken,
557
- id_token: token.id_token ?? token.idToken,
558
- refresh_token: token.refresh_token ?? token.refreshToken,
559
- }
560
- }
561
-
562
- export function identityNameFromClaims(claims: JWTPayload | undefined, fallback: string): string {
563
- const preferred = [claims?.email, claims?.preferred_username, claims?.sub, fallback].find(
564
- (v) => typeof v === 'string' && v.length > 0,
565
- ) as string
566
- const normalized = preferred.replace(/[^a-zA-Z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '')
567
- return normalized || fallback
568
- }
569
-
570
- export async function saveIdpSession(session: IdpSession): Promise<void> {
571
- const path = idpSessionPath(session.identity)
572
- await mkdir(dirname(path), { recursive: true })
573
- await atomicWrite(path, JSON.stringify(session, null, 2) + '\n')
574
- }
575
-
576
- export async function readIdpSession(identityName: string): Promise<IdpSession | null> {
577
- try {
578
- const raw = await readFile(idpSessionPath(identityName), 'utf-8')
579
- return IdpSessionSchema.parse(JSON.parse(raw))
580
- } catch (e) {
581
- if ((e as { code?: string }).code === 'ENOENT') return null
582
- if (e instanceof z.ZodError) {
583
- throw new Error(`Invalid IdP session cache for "${identityName}"`)
584
- }
585
- throw e
586
- }
587
- }
588
-
589
- export async function deleteIdpSession(identityName: string): Promise<void> {
590
- await unlink(idpSessionPath(identityName)).catch(() => undefined)
591
- }
592
-
593
- export async function listIdpSessions(): Promise<IdpSession[]> {
594
- const out: IdpSession[] = []
595
- let entries: string[] = []
596
- try {
597
- entries = (await readdir(IDP_SESSIONS_DIR)).filter((entry) => entry.endsWith('.json'))
598
- } catch {
599
- return out
600
- }
601
- for (const entry of entries) {
602
- const name = entry.replace(/\.json$/, '')
603
- const session = await readIdpSession(name).catch(() => null)
604
- if (session) out.push(session)
605
- }
606
- return out
607
- }
608
-
609
- export function isSessionExpired(
610
- session: Pick<IdpSession, 'expires_at'> & { access_token?: string },
611
- skewMs = 60_000,
612
- ): boolean {
613
- const expiresAt = session.expires_at ?? tokenExpiresAtFromJwt(session.access_token)
614
- if (!expiresAt) return false
615
- return new Date(expiresAt).getTime() <= Date.now() + skewMs
616
- }
617
-
618
- /**
619
- * A still-fresh access token usable for `audience`, or undefined when a
620
- * refresh is needed. Checks the per-audience `tokens` map first, then the
621
- * top-level token's own `aud` claim. Without an `audience`, freshness of the
622
- * top-level token is the only requirement.
623
- */
624
- export function accessTokenForAudience(
625
- session: IdpSession,
626
- audience?: string,
627
- minimumRemainingMs = 60_000,
628
- ): string | undefined {
629
- if (audience === undefined) {
630
- return tokenHasMinimumLifetime(session, minimumRemainingMs) ? session.access_token : undefined
631
- }
632
- const entry = session.tokens?.[audience]
633
- if (entry && tokenHasMinimumLifetime(entry, minimumRemainingMs)) return entry.access_token
634
- if (
635
- tokenHasMinimumLifetime(session, minimumRemainingMs) &&
636
- tokenAudienceMatches(session.access_token, audience)
637
- ) {
638
- return session.access_token
639
- }
640
- return undefined
641
- }
642
-
643
- /** Prefer the JWT expiration used by delegation; opaque IdP tokens retain their timestamp path. */
644
- function tokenHasMinimumLifetime(
645
- value: Pick<IdpSession, 'expires_at'> & { access_token: string },
646
- minimumRemainingMs: number,
647
- ): boolean {
648
- const expiration = decodeTokenClaims(value.access_token)?.exp
649
- if (typeof expiration === 'number' && Number.isSafeInteger(expiration)) {
650
- return credentialLifetimeCovers(expiration, Math.ceil(minimumRemainingMs / 1_000))
651
- }
652
- return !isSessionExpired(value, minimumRemainingMs)
653
- }
654
-
655
- /**
656
- * Fold a freshly minted access token into the per-audience map under every
657
- * `aud` it carries, dropping entries that have already expired.
658
- */
659
- export function withCachedToken(
660
- tokens: IdpSession['tokens'],
661
- accessToken: string,
662
- expiresAt: string | undefined,
663
- ): IdpSession['tokens'] {
664
- const next: NonNullable<IdpSession['tokens']> = {}
665
- for (const [aud, entry] of Object.entries(tokens ?? {})) {
666
- if (!isSessionExpired(entry)) next[aud] = entry
667
- }
668
- const aud = decodeTokenClaims(accessToken)?.aud
669
- const audiences = Array.isArray(aud) ? aud : typeof aud === 'string' ? [aud] : []
670
- for (const audience of audiences) {
671
- next[audience] = { access_token: accessToken, expires_at: expiresAt }
672
- }
673
- return Object.keys(next).length > 0 ? next : undefined
674
- }
675
-
676
- export function requireClientId(idp: IdpConfig, override?: string): string {
677
- const clientId = override ?? idp.client.client_id
678
- if (!clientId) {
679
- throw new Error(`IdP "${idp.name}" has no client_id. Re-run idp add with --client-id.`)
680
- }
681
- return clientId
682
- }
683
-
684
- export function resolveClientSecret(idp: IdpConfig, overrideEnv?: string): string | undefined {
685
- const envName = overrideEnv ?? idp.client.client_secret_env
686
- if (!envName) return undefined
687
- const secret = process.env[envName]
688
- if (!secret) throw new Error(`Client secret env var ${envName} is not set`)
689
- return secret
690
- }
691
-
692
- export async function refreshSession(
693
- identityName: string,
694
- session: IdpSession,
695
- opts: { readonly audience?: string; readonly organizationId?: string } = {},
696
- ): Promise<IdpSession> {
697
- if (!session.refresh_token)
698
- throw new Error(`IdP session for "${identityName}" has no refresh token`)
699
- const idp = await readIdpConfig(session.idp)
700
- const clientId = requireClientId(idp, session.clientId)
701
- const params = new URLSearchParams({
702
- grant_type: 'refresh_token',
703
- refresh_token: session.refresh_token,
704
- client_id: clientId,
705
- })
706
- const audience = opts.audience ?? session.audience
707
- if (audience) params.set('audience', audience)
708
- const organizationId = opts.organizationId ?? session.organizationId
709
- if (organizationId) params.set('organization_id', organizationId)
710
- if (process.env.ASTRALE_DEBUG_ORG)
711
- console.error(
712
- '[debug-org] refresh',
713
- JSON.stringify({
714
- audience,
715
- organizationId,
716
- sub: (session.claims as { sub?: string } | undefined)?.sub,
717
- }),
718
- )
719
- const secret = resolveClientSecret(idp)
720
- if (secret) params.set('client_secret', secret)
721
- const token = normalizeTokenResponse(
722
- idp.client.token_request_format === 'json'
723
- ? await postJson(idp.metadata.token_endpoint, Object.fromEntries(params))
724
- : await postForm(idp.metadata.token_endpoint, params),
725
- )
726
- if (!token.access_token) throw new Error('Refresh response did not include access_token')
727
- const claims = decodeTokenClaims(token.id_token ?? token.access_token)
728
- const expiresAt = tokenExpiresAt(token)
729
- const next: IdpSession = {
730
- ...session,
731
- clientId,
732
- access_token: token.access_token,
733
- id_token: token.id_token ?? session.id_token,
734
- refresh_token: token.refresh_token ?? session.refresh_token,
735
- token_type: token.token_type ?? session.token_type,
736
- scope: token.scope ?? session.scope,
737
- audience,
738
- organizationId: organizationId ?? session.organizationId,
739
- expires_at: expiresAt,
740
- tokens: withCachedToken(session.tokens, token.access_token, expiresAt),
741
- claims: claims ? (claims as Record<string, unknown>) : session.claims,
742
- updatedAt: new Date().toISOString(),
743
- }
744
- // Persist the rotated session BEFORE the audience check. WorkOS refresh tokens
745
- // are single-use: the token request above already exchanged the old one, so the
746
- // new refresh_token must be saved even when the audience doesn't match — else
747
- // the cache is stranded on the now-invalid old token and every subsequent
748
- // refresh dies with "Refresh token already exchanged".
749
- await saveIdpSession(next)
750
- if (audience && !tokenAudienceMatches(next.access_token, audience)) {
751
- const actual = decodeTokenClaims(next.access_token)?.aud
752
- throw new IdpAudienceMismatchError(
753
- audience,
754
- Array.isArray(actual) ? actual.join(', ') : (actual as string | undefined),
755
- )
756
- }
757
- return next
758
- }
759
-
760
- export async function requestClientCredentials(args: {
761
- idp: IdpConfig
762
- clientId?: string
763
- clientSecretEnv?: string
764
- scope?: string
765
- audience?: string
766
- }): Promise<TokenResponse> {
767
- const clientId = requireClientId(args.idp, args.clientId)
768
- const secret = resolveClientSecret(args.idp, args.clientSecretEnv)
769
- if (!secret) throw new Error('Client credentials flow requires a client secret env var')
770
- const params = new URLSearchParams({
771
- grant_type: 'client_credentials',
772
- client_id: clientId,
773
- client_secret: secret,
774
- })
775
- if (args.scope) params.set('scope', args.scope)
776
- if (args.audience) params.set('audience', args.audience)
777
- return postForm(args.idp.metadata.token_endpoint, params)
778
- }
779
-
780
- export async function requestDeviceAuthorization(args: {
781
- idp: IdpConfig
782
- clientId?: string
783
- scope?: string
784
- audience?: string
785
- }): Promise<DeviceAuthorizationResponse> {
786
- const endpoint = args.idp.metadata.device_authorization_endpoint
787
- if (!endpoint)
788
- throw new Error(`IdP "${args.idp.name}" does not advertise device_authorization_endpoint`)
789
- const params = new URLSearchParams({
790
- client_id: requireClientId(args.idp, args.clientId),
791
- })
792
- if (args.scope) params.set('scope', args.scope)
793
- if (args.audience) params.set('audience', args.audience)
794
- const response =
795
- args.idp.client.token_request_format === 'json'
796
- ? await fetch(endpoint, {
797
- method: 'POST',
798
- headers: { 'Content-Type': 'application/json' },
799
- body: JSON.stringify(Object.fromEntries(params)),
800
- })
801
- : await fetch(endpoint, {
802
- method: 'POST',
803
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
804
- body: params,
805
- })
806
- const body = (await response.json()) as DeviceAuthorizationResponse & {
807
- error?: string
808
- error_description?: string
809
- }
810
- if (!response.ok || body.error) {
811
- throw new Error(
812
- body.error_description ??
813
- body.error ??
814
- `Device authorization failed: HTTP ${response.status}`,
815
- )
816
- }
817
- return body
818
- }
819
-
820
- export async function pollDeviceToken(args: {
821
- idp: IdpConfig
822
- deviceCode: string
823
- clientId?: string
824
- clientSecretEnv?: string
825
- intervalSec?: number
826
- expiresInSec: number
827
- }): Promise<TokenResponse> {
828
- const clientId = requireClientId(args.idp, args.clientId)
829
- const secret = resolveClientSecret(args.idp, args.clientSecretEnv)
830
- let intervalMs = Math.max(args.intervalSec ?? 5, 1) * 1000
831
- const deadline = Date.now() + args.expiresInSec * 1000
832
- while (Date.now() < deadline) {
833
- await new Promise((r) => setTimeout(r, intervalMs))
834
- const params = new URLSearchParams({
835
- grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
836
- device_code: args.deviceCode,
837
- client_id: clientId,
838
- })
839
- if (secret) params.set('client_secret', secret)
840
- const response =
841
- args.idp.client.token_request_format === 'json'
842
- ? await fetch(args.idp.metadata.token_endpoint, {
843
- method: 'POST',
844
- headers: { 'Content-Type': 'application/json' },
845
- body: JSON.stringify(Object.fromEntries(params)),
846
- })
847
- : await fetch(args.idp.metadata.token_endpoint, {
848
- method: 'POST',
849
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
850
- body: params,
851
- })
852
- const body = (await response.json()) as TokenResponse
853
- const token = normalizeTokenResponse(body)
854
- if (response.ok && token.access_token) return token
855
- if (body.error === 'authorization_pending') continue
856
- if (body.error === 'slow_down') {
857
- intervalMs += 5_000
858
- continue
859
- }
860
- throw new Error(
861
- body.error_description ??
862
- body.error ??
863
- `Device token request failed: HTTP ${response.status}`,
864
- )
865
- }
866
- throw new Error('Device authorization expired before completion')
867
- }
868
-
869
- export async function exchangeAuthorizationCode(args: {
870
- idp: IdpConfig
871
- code: string
872
- redirectUri: string
873
- codeVerifier?: string
874
- clientId?: string
875
- clientSecretEnv?: string
876
- }): Promise<TokenResponse> {
877
- const params = new URLSearchParams({
878
- grant_type: 'authorization_code',
879
- code: args.code,
880
- redirect_uri: args.redirectUri,
881
- client_id: requireClientId(args.idp, args.clientId),
882
- })
883
- if (args.codeVerifier) params.set('code_verifier', args.codeVerifier)
884
- const secret = resolveClientSecret(args.idp, args.clientSecretEnv)
885
- if (secret) params.set('client_secret', secret)
886
- return postForm(args.idp.metadata.token_endpoint, params)
887
- }
888
-
889
- export type WorkosApplication = {
890
- id: string
891
- client_id?: string
892
- name?: string
893
- application_type?: 'oauth' | 'm2m'
894
- scopes?: string[]
895
- redirect_uris?: Array<{ uri?: string; default?: boolean }>
896
- uses_pkce?: boolean
897
- }
898
-
899
- export async function fetchWorkosApplication(args: {
900
- apiKeyEnv: string
901
- app: string
902
- }): Promise<WorkosApplication> {
903
- const apiKey = process.env[args.apiKeyEnv]
904
- if (!apiKey) throw new Error(`WorkOS API key env var ${args.apiKeyEnv} is not set`)
905
- const response = await fetch(
906
- `https://api.workos.com/connect/applications/${encodeURIComponent(args.app)}`,
907
- {
908
- headers: { Authorization: `Bearer ${apiKey}` },
909
- },
910
- )
911
- const body = (await response.json()) as {
912
- error?: string
913
- message?: string
914
- connect_application?: WorkosApplication
915
- } & WorkosApplication
916
- if (!response.ok) {
917
- throw new Error(
918
- body.message ?? body.error ?? `WorkOS application fetch failed: HTTP ${response.status}`,
919
- )
920
- }
921
- return body.connect_application ?? body
922
- }
923
-
924
- function normalizeIssuer(value: string): string {
925
- return value.replace(/\/+$/, '')
926
- }