@astrale-os/cli 0.8.0-alpha.0 → 0.8.1-alpha.2

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 (317) hide show
  1. package/README.md +4 -1
  2. package/dist/astrale.js +62968 -41240
  3. package/dist/public/connect-core.js +55030 -0
  4. package/dist/public/keys/index.js +45631 -0
  5. package/dist/public/paths/index.js +43990 -0
  6. package/dist/types/admin/instance/model.d.ts +40 -0
  7. package/dist/types/connect-core.d.ts +22 -0
  8. package/dist/types/connection/auth.d.ts +33 -0
  9. package/dist/types/errors.d.ts +23 -0
  10. package/dist/types/identity/registry.d.ts +25 -0
  11. package/dist/types/keys/algorithm.d.ts +6 -0
  12. package/dist/types/keys/credential.d.ts +21 -0
  13. package/dist/types/keys/index.d.ts +4 -0
  14. package/dist/types/keys/pair.d.ts +34 -0
  15. package/dist/types/lib/admin-target.d.ts +74 -0
  16. package/dist/types/lib/ca-fetch.d.ts +2 -0
  17. package/dist/types/lib/config.d.ts +71 -0
  18. package/dist/types/lib/format.d.ts +1 -0
  19. package/dist/types/lib/idp-session.d.ts +33 -0
  20. package/dist/types/lib/idp.d.ts +397 -0
  21. package/dist/types/lib/instance-target.d.ts +36 -0
  22. package/dist/types/lib/instance.d.ts +186 -0
  23. package/dist/types/lib/log.d.ts +30 -0
  24. package/dist/types/lib/login-flow.d.ts +49 -0
  25. package/dist/types/lib/meta.d.ts +36 -0
  26. package/dist/types/lib/output.d.ts +83 -0
  27. package/dist/types/lib/table.d.ts +18 -0
  28. package/dist/types/lib/validation.d.ts +11 -0
  29. package/dist/types/paths/index.d.ts +2 -0
  30. package/dist/types/state/exchange-credentials.d.ts +25 -0
  31. package/dist/types/state/files.d.ts +9 -0
  32. package/dist/types/state/identities.d.ts +42 -0
  33. package/dist/types/state/index.d.ts +8 -0
  34. package/dist/types/state/paths.d.ts +32 -0
  35. package/package.json +30 -8
  36. package/src/admin/.spec/api.d.ts +3 -0
  37. package/src/admin/.spec/architecture.md +15 -0
  38. package/src/admin/.spec/icon.svg +3 -0
  39. package/src/admin/.spec/layout.ts +6 -0
  40. package/src/admin/catalog/.spec/api.d.ts +65 -0
  41. package/src/admin/catalog/.spec/architecture.md +6 -0
  42. package/src/admin/catalog/.spec/icon.svg +3 -0
  43. package/src/admin/catalog/.spec/layout.ts +6 -0
  44. package/src/admin/catalog/__tests__/client.test.ts +171 -0
  45. package/src/admin/catalog/client.ts +214 -0
  46. package/src/admin/catalog/index.ts +13 -0
  47. package/src/admin/catalog/model.ts +42 -0
  48. package/src/admin/catalog/tsconfig.json +8 -0
  49. package/src/admin/graph/.spec/api.d.ts +28 -0
  50. package/src/admin/graph/.spec/architecture.md +5 -0
  51. package/src/admin/graph/.spec/icon.svg +3 -0
  52. package/src/admin/graph/.spec/layout.ts +3 -0
  53. package/src/admin/graph/index.ts +6 -0
  54. package/src/admin/graph/nodes.ts +56 -0
  55. package/src/admin/index.ts +2 -0
  56. package/src/admin/instance/.spec/api.d.ts +66 -0
  57. package/src/admin/instance/.spec/architecture.md +10 -0
  58. package/src/admin/instance/.spec/icon.svg +3 -0
  59. package/src/admin/instance/.spec/layout.ts +6 -0
  60. package/src/admin/instance/__tests__/client.test.ts +289 -0
  61. package/src/admin/instance/client.ts +298 -0
  62. package/src/admin/instance/index.ts +16 -0
  63. package/src/admin/instance/model.ts +65 -0
  64. package/src/admin/instance/tsconfig.json +8 -0
  65. package/src/commands/__tests__/admin-instance.test.ts +3 -36
  66. package/src/commands/__tests__/call.test.ts +21 -68
  67. package/src/commands/__tests__/domain-install-owned.test.ts +26 -27
  68. package/src/commands/__tests__/domain-list.test.ts +2 -6
  69. package/src/commands/__tests__/logs.test.ts +50 -98
  70. package/src/commands/__tests__/ls.test.ts +21 -17
  71. package/src/commands/__tests__/read-commands.test.ts +171 -101
  72. package/src/commands/__tests__/view.test.ts +100 -51
  73. package/src/commands/admin/status.ts +3 -3
  74. package/src/commands/admin/use.ts +29 -6
  75. package/src/commands/auth/login.ts +1 -1
  76. package/src/commands/auth/logout.ts +4 -2
  77. package/src/commands/auth/status.ts +2 -2
  78. package/src/commands/auth/token.ts +2 -2
  79. package/src/commands/browser.ts +1 -1
  80. package/src/commands/call.ts +55 -91
  81. package/src/commands/describe.ts +52 -147
  82. package/src/commands/domain/install.ts +81 -50
  83. package/src/commands/domain/list.ts +4 -8
  84. package/src/commands/domain/publish.ts +9 -31
  85. package/src/commands/get.ts +32 -65
  86. package/src/commands/identity/__tests__/register.test.ts +93 -0
  87. package/src/commands/identity/create.ts +2 -2
  88. package/src/commands/identity/delete.ts +2 -2
  89. package/src/commands/identity/export.ts +7 -33
  90. package/src/commands/identity/import.ts +21 -70
  91. package/src/commands/identity/list.ts +2 -2
  92. package/src/commands/identity/register.ts +148 -117
  93. package/src/commands/identity/sync.ts +2 -2
  94. package/src/commands/identity/unsync.ts +2 -2
  95. package/src/commands/identity/use.ts +2 -2
  96. package/src/commands/identity/whoami.ts +2 -2
  97. package/src/commands/idp/add.ts +1 -1
  98. package/src/commands/idp/list.ts +1 -1
  99. package/src/commands/idp/refresh.ts +1 -1
  100. package/src/commands/idp/remove.ts +2 -2
  101. package/src/commands/idp/show.ts +1 -1
  102. package/src/commands/instance/active.ts +2 -2
  103. package/src/commands/instance/bookmark.ts +8 -2
  104. package/src/commands/instance/create.ts +1 -1
  105. package/src/commands/instance/delete.ts +4 -10
  106. package/src/commands/instance/forget.ts +1 -1
  107. package/src/commands/instance/list.ts +3 -3
  108. package/src/commands/instance/status.ts +5 -7
  109. package/src/commands/instance/use.ts +3 -3
  110. package/src/commands/logs.ts +152 -279
  111. package/src/commands/ls.ts +78 -204
  112. package/src/commands/mutate.ts +70 -139
  113. package/src/commands/query.ts +100 -278
  114. package/src/commands/session/analyze.ts +1 -1
  115. package/src/commands/session/list.ts +1 -1
  116. package/src/commands/setup.ts +1 -1
  117. package/src/commands/status.ts +1 -1
  118. package/src/commands/studio.ts +1 -1
  119. package/src/commands/token.ts +26 -21
  120. package/src/commands/update.ts +1 -1
  121. package/src/commands/use.ts +2 -3
  122. package/src/commands/view-serve.ts +1 -1
  123. package/src/commands/view.ts +67 -109
  124. package/src/connect-core.test.ts +38 -0
  125. package/src/connect-core.ts +42 -0
  126. package/src/connection/.spec/api.d.ts +111 -0
  127. package/src/connection/.spec/architecture.md +41 -0
  128. package/src/connection/.spec/capabilities/connection.ts +13 -0
  129. package/src/connection/.spec/examples/call.ts +7 -0
  130. package/src/connection/.spec/flows/command.ts +20 -0
  131. package/src/connection/.spec/flows/session.ts +61 -0
  132. package/src/connection/.spec/laws/connection.ts +181 -0
  133. package/src/connection/.spec/layout.ts +19 -0
  134. package/src/{kernel → connection}/__tests__/auth.test.ts +26 -2
  135. package/src/connection/__tests__/ca-fetch.test.ts +53 -0
  136. package/src/connection/__tests__/call.test.ts +17 -0
  137. package/src/connection/__tests__/credential.test.ts +109 -0
  138. package/src/connection/__tests__/errors.test.ts +96 -0
  139. package/src/connection/__tests__/exchange.test.ts +224 -0
  140. package/src/connection/__tests__/fixtures/localhost-cert.pem +19 -0
  141. package/src/connection/__tests__/fixtures/localhost-key.base64 +1 -0
  142. package/src/connection/__tests__/managed.test.ts +39 -0
  143. package/src/connection/__tests__/self.test.ts +51 -0
  144. package/src/connection/__tests__/session.test.ts +140 -0
  145. package/src/connection/__tests__/target.test.ts +93 -0
  146. package/src/{kernel → connection}/auth.ts +8 -8
  147. package/src/connection/call.ts +7 -0
  148. package/src/{kernel/run.ts → connection/command.ts} +22 -13
  149. package/src/connection/credential.ts +114 -0
  150. package/src/{kernel → connection}/errors.ts +32 -9
  151. package/src/connection/exchange.ts +274 -0
  152. package/src/connection/index.ts +8 -0
  153. package/src/connection/self.ts +120 -0
  154. package/src/connection/session.ts +176 -0
  155. package/src/connection/target.ts +111 -0
  156. package/src/connection/tsconfig.json +6 -0
  157. package/src/graph/.spec/api.d.ts +34 -0
  158. package/src/graph/.spec/laws/documents.ts +24 -0
  159. package/src/graph/.spec/laws/projection.ts +13 -0
  160. package/src/graph/.spec/layout.ts +6 -0
  161. package/src/graph/__tests__/mutation.test.ts +43 -0
  162. package/src/graph/__tests__/projection.test.ts +22 -0
  163. package/src/graph/__tests__/query.test.ts +154 -0
  164. package/src/graph/index.ts +3 -0
  165. package/src/graph/mutation.ts +18 -0
  166. package/src/graph/projection.ts +21 -0
  167. package/src/graph/query.ts +108 -0
  168. package/src/graph/tsconfig.json +6 -0
  169. package/src/identity/.history/ard/bootstrap-root-transfer.md +57 -0
  170. package/src/identity/.history/ard/domain-mediated-registration.md +36 -0
  171. package/src/identity/.spec/api.d.ts +124 -0
  172. package/src/identity/.spec/architecture.md +33 -0
  173. package/src/identity/.spec/capabilities/identity.ts +7 -0
  174. package/src/identity/.spec/flows/bootstrap-root.ts +79 -0
  175. package/src/identity/.spec/flows/domain-mediated-registration.ts +51 -0
  176. package/src/identity/.spec/flows/import.ts +29 -0
  177. package/src/identity/.spec/laws/identity.ts +56 -0
  178. package/src/identity/.spec/layout.ts +13 -0
  179. package/src/identity/.spec/schemas/identity-export-v1.schema.json +25 -0
  180. package/src/identity/__tests__/fixtures/registry-journey.ts +53 -0
  181. package/src/identity/__tests__/registration.test.ts +64 -0
  182. package/src/identity/__tests__/registry.test.ts +70 -0
  183. package/src/identity/__tests__/transfer.test.ts +198 -0
  184. package/src/identity/index.ts +23 -0
  185. package/src/identity/registration.ts +72 -0
  186. package/src/identity/registry.ts +172 -0
  187. package/src/identity/transfer.ts +187 -0
  188. package/src/identity/tsconfig.json +6 -0
  189. package/src/index.ts +1 -0
  190. package/src/keys/.spec/api.d.ts +74 -0
  191. package/src/keys/.spec/architecture.md +13 -0
  192. package/src/keys/.spec/laws/keys.ts +43 -0
  193. package/src/keys/.spec/layout.ts +6 -0
  194. package/src/keys/__tests__/algorithm.test.ts +39 -0
  195. package/src/keys/__tests__/keys.test.ts +168 -0
  196. package/src/keys/algorithm.ts +33 -0
  197. package/src/keys/credential.ts +112 -0
  198. package/src/keys/index.ts +14 -0
  199. package/src/keys/pair.ts +219 -0
  200. package/src/keys/tsconfig.json +6 -0
  201. package/src/lib/__tests__/admin-target.test.ts +67 -7
  202. package/src/lib/__tests__/binary.test.ts +2 -3
  203. package/src/lib/__tests__/command-dx.test.ts +2 -2
  204. package/src/lib/__tests__/config.test.ts +3 -2
  205. package/src/lib/__tests__/idp.test.ts +4 -6
  206. package/src/lib/__tests__/instance-target.test.ts +7 -4
  207. package/src/lib/__tests__/local-status.test.ts +1 -1
  208. package/src/lib/__tests__/log.test.ts +31 -0
  209. package/src/lib/__tests__/meta.test.ts +55 -0
  210. package/src/lib/__tests__/sdk-deps.test.ts +1 -1
  211. package/src/lib/__tests__/self.test.ts +8 -230
  212. package/src/lib/__tests__/use-target.test.ts +1 -1
  213. package/src/lib/__tests__/view-open-intent.test.ts +78 -104
  214. package/src/lib/__tests__/view-session.test.ts +99 -0
  215. package/src/lib/admin-domain.ts +39 -25
  216. package/src/lib/admin-instance.ts +38 -43
  217. package/src/lib/admin-target.ts +67 -13
  218. package/src/lib/binary.ts +13 -34
  219. package/src/lib/browser.ts +1 -1
  220. package/src/{kernel → lib}/ca-fetch.ts +35 -27
  221. package/src/lib/config.ts +1 -1
  222. package/src/lib/idp-session.ts +1 -1
  223. package/src/lib/idp.ts +3 -8
  224. package/src/lib/instance-target.ts +12 -8
  225. package/src/lib/instance.ts +34 -2
  226. package/src/lib/local-status.ts +2 -2
  227. package/src/lib/log.ts +3 -1
  228. package/src/lib/login-flow.ts +32 -5
  229. package/src/lib/meta.ts +15 -11
  230. package/src/lib/provision-instance.ts +4 -12
  231. package/src/lib/self.ts +5 -147
  232. package/src/lib/update.ts +1 -1
  233. package/src/lib/use-target.ts +1 -1
  234. package/src/lib/validation.ts +2 -2
  235. package/src/lib/view/open-intent.ts +7 -37
  236. package/src/lib/view/port-allocation.ts +1 -1
  237. package/src/lib/view/resolve.ts +52 -45
  238. package/src/lib/view/server.ts +16 -33
  239. package/src/lib/view/session.ts +51 -22
  240. package/src/paths/.spec/api.d.ts +13 -0
  241. package/src/paths/.spec/architecture.md +5 -0
  242. package/src/paths/.spec/layout.ts +3 -0
  243. package/src/paths/index.ts +13 -0
  244. package/src/program/.spec/api.d.ts +42 -0
  245. package/src/program/.spec/architecture.md +18 -0
  246. package/src/program/.spec/laws/program.ts +29 -0
  247. package/src/program/.spec/layout.ts +14 -0
  248. package/src/program/__tests__/program.test.ts +359 -0
  249. package/src/program/build.ts +190 -0
  250. package/src/program/command.ts +42 -0
  251. package/src/program/index.ts +2 -0
  252. package/src/program/options.ts +40 -0
  253. package/src/{registry.ts → program/registry.ts} +5 -1
  254. package/src/program/tsconfig.json +6 -0
  255. package/src/setup/steps/admin.ts +18 -3
  256. package/src/setup/steps/instance.ts +1 -1
  257. package/src/state/.spec/api.d.ts +133 -0
  258. package/src/state/.spec/architecture.md +31 -0
  259. package/src/state/.spec/capabilities/state.ts +7 -0
  260. package/src/state/.spec/flows/identity-update.ts +22 -0
  261. package/src/state/.spec/laws/state.ts +73 -0
  262. package/src/state/.spec/layout.ts +14 -0
  263. package/src/state/__tests__/exchange-credentials.test.ts +188 -0
  264. package/src/state/__tests__/files.test.ts +113 -0
  265. package/src/state/__tests__/fixtures/identity-transition.ts +37 -0
  266. package/src/state/__tests__/identities.test.ts +142 -0
  267. package/src/state/__tests__/paths.test.ts +27 -0
  268. package/src/state/exchange-credentials.ts +219 -0
  269. package/src/state/files.ts +129 -0
  270. package/src/state/identities.ts +236 -0
  271. package/src/state/index.ts +29 -0
  272. package/src/state/paths.ts +61 -0
  273. package/src/state/tsconfig.json +6 -0
  274. package/src/telemetry/__tests__/redact.test.ts +3 -3
  275. package/src/telemetry/__tests__/trigger.test.ts +1 -1
  276. package/src/telemetry/settings.ts +1 -1
  277. package/src/telemetry/store.ts +1 -1
  278. package/src/test-utils.ts +1 -1
  279. package/studio/client/dist/assets/elk-api-D0cBetPW.js +1 -0
  280. package/studio/client/dist/assets/elk-worker.min-C9JGDOE-.js +6312 -0
  281. package/studio/client/dist/assets/index-BQnJ5sgd.css +1 -0
  282. package/studio/client/dist/assets/index-ChOr3yP0.css +1 -0
  283. package/studio/client/dist/assets/index-Dspir4w7.js +81 -0
  284. package/studio/client/dist/assets/index-bVD2KJgz.js +8 -0
  285. package/studio/client/dist/index.html +2 -2
  286. package/studio/package.json +10 -1
  287. package/studio/server/introspect/extractor.ts +1 -1
  288. package/studio/server/state/views.test.ts +3 -7
  289. package/studio/server/state/views.ts +23 -30
  290. package/studio/tsconfig.json +23 -0
  291. package/tsconfig.json +1 -1
  292. package/viewer/dist/main.js +65 -39
  293. package/viewer/tsconfig.json +13 -0
  294. package/src/command.ts +0 -42
  295. package/src/commands/__tests__/help-contract.test.ts +0 -164
  296. package/src/kernel/__tests__/client-owned-lookup.test.ts +0 -51
  297. package/src/kernel/__tests__/errors.test.ts +0 -43
  298. package/src/kernel/__tests__/expand.test.ts +0 -123
  299. package/src/kernel/client.ts +0 -214
  300. package/src/kernel/expand.ts +0 -192
  301. package/src/kernel/graph.ts +0 -96
  302. package/src/kernel/index.ts +0 -29
  303. package/src/kernel/options.ts +0 -22
  304. package/src/kernel/types.ts +0 -14
  305. package/src/lib/__tests__/domain-identity.test.ts +0 -60
  306. package/src/lib/__tests__/fs-atomic.test.ts +0 -104
  307. package/src/lib/__tests__/identity.test.ts +0 -79
  308. package/src/lib/__tests__/keys.test.ts +0 -129
  309. package/src/lib/domain-identity.ts +0 -49
  310. package/src/lib/env.ts +0 -49
  311. package/src/lib/fs-atomic.ts +0 -126
  312. package/src/lib/identity.ts +0 -256
  313. package/src/lib/keys.ts +0 -294
  314. package/src/lib/paths.ts +0 -11
  315. package/src/program.ts +0 -213
  316. package/studio/client/dist/assets/index-BaqEuIQJ.js +0 -109
  317. package/studio/client/dist/assets/index-jRdExahh.css +0 -1
@@ -1,19 +1,19 @@
1
1
  import chalk from 'chalk'
2
2
 
3
- import type { ClientContext } from './client'
4
- import type { KernelCommandOpts } from './types'
3
+ import type { ConnectionContext } from './session'
4
+ import type { ConnectionOptions } from './target'
5
5
 
6
6
  import { formatElapsed } from '../lib/format'
7
7
  import { spinner } from '../lib/log'
8
8
  import { isMachine, present } from '../lib/output'
9
- import { withKernelClient } from './client'
10
9
  import { formatKernelError } from './errors'
10
+ import { withClientSession } from './session'
11
11
 
12
- type RunOpts<T> = {
13
- opts: KernelCommandOpts
14
- label: string
15
- fn: (ctx: ClientContext) => Promise<T>
16
- format?: (result: T, opts: KernelCommandOpts, isRaw: boolean) => void | Promise<void>
12
+ export interface KernelCommandOpts extends ConnectionOptions {
13
+ readonly raw?: boolean
14
+ readonly json?: boolean
15
+ readonly format?: 'yaml' | 'json'
16
+ readonly debug?: boolean
17
17
  }
18
18
 
19
19
  /**
@@ -24,21 +24,30 @@ type RunOpts<T> = {
24
24
  * `format` callback for custom output. If `format` is omitted, the
25
25
  * result is passed to the standard `output()` function.
26
26
  */
27
- export async function runKernelCommand<T>(run: RunOpts<T>): Promise<void> {
28
- const { opts, label, fn } = run
27
+ export async function runKernelCommand<T>(input: {
28
+ readonly opts: KernelCommandOpts
29
+ readonly label: string
30
+ readonly fn: (context: ConnectionContext) => Promise<T>
31
+ readonly format?: (
32
+ result: T,
33
+ options: KernelCommandOpts,
34
+ machine: boolean,
35
+ ) => void | Promise<void>
36
+ }): Promise<void> {
37
+ const { opts, label, fn } = input
29
38
  const isRaw = isMachine(opts)
30
39
  const spin = !isRaw ? spinner(`${label}...`) : null
31
40
  const startTime = performance.now()
32
41
 
33
42
  try {
34
- const result = await withKernelClient(opts, fn)
43
+ const result = await withClientSession(opts, fn)
35
44
  const elapsed = performance.now() - startTime
36
45
 
37
46
  spin?.succeed(`${label} ${chalk.dim(formatElapsed(elapsed))}`)
38
47
  if (!isRaw) console.log('')
39
48
 
40
- if (run.format) {
41
- await run.format(result, opts, isRaw)
49
+ if (input.format) {
50
+ await input.format(result, opts, isRaw)
42
51
  } else {
43
52
  present(result, opts)
44
53
  }
@@ -0,0 +1,114 @@
1
+ import type { Fetch } from '@astrale-os/kernel-client'
2
+ import type { SessionAuth } from '@astrale-os/kernel-client/session'
3
+
4
+ import { credential, type IssuerId } from '@astrale-os/sdk/auth'
5
+
6
+ import type { AstraleConfig } from '../lib/config'
7
+ import type { ConnectionOptions, ConnectionTarget } from './target'
8
+
9
+ import { AstraleError } from '../errors'
10
+ import { resolveCredential } from './auth'
11
+ import { createExchangeCredentialResolver } from './exchange'
12
+ import { registrationKeyForTarget } from './target'
13
+
14
+ const DELEGATION_TTL_SECONDS = 60
15
+
16
+ export interface SourceCredentialResolver {
17
+ resolve(audience: IssuerId, signal: AbortSignal): Promise<string>
18
+ }
19
+
20
+ /** Resolve source authority; an omitted Session delegation preserves exact current authority. */
21
+ export function createConnectionCredential(
22
+ expectedSourceIssuer: IssuerId,
23
+ source: SourceCredentialResolver,
24
+ ): SessionAuth {
25
+ const resolveSource = source.resolve.bind(source)
26
+ return Object.freeze({
27
+ ttlSeconds: DELEGATION_TTL_SECONDS,
28
+ async resolve(
29
+ _call: Parameters<SessionAuth['resolve']>[0],
30
+ signal: Parameters<SessionAuth['resolve']>[1],
31
+ ) {
32
+ const resolved = await resolveSource(expectedSourceIssuer, signal)
33
+ const ttlSeconds = sourceBoundDelegationTtl(resolved)
34
+ return Object.freeze({
35
+ credential: resolved,
36
+ ...(ttlSeconds === undefined ? {} : { delegate: { ttlSeconds } }),
37
+ })
38
+ },
39
+ })
40
+ }
41
+
42
+ /** Never request a destination carrier that could outlive its current source bearer. */
43
+ function sourceBoundDelegationTtl(input: string): number | undefined {
44
+ try {
45
+ const expiresAt = credential.inspect(input).claims.exp
46
+ if (typeof expiresAt !== 'number' || !Number.isSafeInteger(expiresAt)) return undefined
47
+ const remaining = expiresAt - Math.ceil(Date.now() / 1_000) - 1
48
+ return Math.max(1, Math.min(DELEGATION_TTL_SECONDS, remaining))
49
+ } catch {
50
+ // Preserve opaque explicit credentials; the Kernel remains their authority.
51
+ return undefined
52
+ }
53
+ }
54
+
55
+ /** Bind CLI identity state and Core Auth delegation to one Session auth capability. */
56
+ export function createCliCredential(
57
+ target: ConnectionTarget,
58
+ options: ConnectionOptions,
59
+ config: AstraleConfig,
60
+ fetch: Fetch = globalThis.fetch,
61
+ timeoutMs = 30_000,
62
+ ): SessionAuth | undefined {
63
+ validateCredentialSelection(options)
64
+ if (options.anonymous === true) return undefined
65
+ const authOptions = Object.freeze({
66
+ ...(options.as === undefined ? {} : { as: options.as }),
67
+ ...(options.creds === undefined ? {} : { creds: options.creds }),
68
+ ...(target.defaultIdentity === undefined ? {} : { defaultIdentity: target.defaultIdentity }),
69
+ })
70
+ const source: SourceCredentialResolver = {
71
+ async resolve(audience, signal) {
72
+ requireLive(signal)
73
+ const credential = await resolveCredential(
74
+ authOptions,
75
+ config,
76
+ audience,
77
+ registrationKeyForTarget(target),
78
+ )
79
+ requireLive(signal)
80
+ return credential
81
+ },
82
+ }
83
+ const effective =
84
+ target.domainIssuer === undefined || options.creds !== undefined
85
+ ? source
86
+ : createExchangeCredentialResolver(
87
+ { ...target, domainIssuer: target.domainIssuer },
88
+ source,
89
+ fetch,
90
+ timeoutMs,
91
+ )
92
+ return createConnectionCredential(target.kernelIssuer, effective)
93
+ }
94
+
95
+ /** Reject contradictory explicit credential selections before identity or network access. */
96
+ export function validateCredentialSelection(options: ConnectionOptions): void {
97
+ if (options.anonymous !== true) return
98
+ const conflicting = [
99
+ ...(options.as === undefined ? [] : ['--as']),
100
+ ...(options.creds === undefined ? [] : ['--creds']),
101
+ ]
102
+ if (conflicting.length === 0) return
103
+ throw new AstraleError(
104
+ 'INVALID_FLAG',
105
+ `--anonymous cannot be combined with ${conflicting.join(' or ')}.`,
106
+ )
107
+ }
108
+
109
+ function requireLive(signal: AbortSignal): void {
110
+ if (!signal.aborted) return
111
+ throw signal.reason instanceof Error
112
+ ? signal.reason
113
+ : new DOMException('The operation was aborted.', 'AbortError')
114
+ }
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk'
2
2
 
3
- import type { SelfExpansionMeta } from './expand'
3
+ import type { SelfExpansionMeta } from './self'
4
4
 
5
5
  import { AstraleError } from '../errors'
6
6
  import { decodeJwtExpiration, readLocalStatus, type LocalStatus } from '../lib/local-status'
@@ -120,7 +120,7 @@ export async function formatKernelError(
120
120
  const selfMeta = (error as Error & { expandedFromSelf?: SelfExpansionMeta }).expandedFromSelf
121
121
  // kernel-client maps both NOT_FOUND (the node doesn't exist) and
122
122
  // METHOD_NOT_FOUND (the method doesn't exist on a real node) to
123
- // `NotFoundError`. Firing the "refresh registration" hint for the
123
+ // `NotFoundError`. Firing the authenticated-principal hint for the
124
124
  // method case is misleading. Gate on the message referencing the
125
125
  // expanded id — node lookup errors mention `@<id>` whereas method
126
126
  // errors mention the method path.
@@ -133,13 +133,11 @@ export async function formatKernelError(
133
133
  log.error(`Not found: ${cleanMsg}`)
134
134
  log.dim(' Check the path/ID and that the instance is booted')
135
135
  if (selfHintApplies && selfMeta) {
136
- const who = selfMeta.identity ?? 'your identity'
137
136
  const where = selfMeta.slug ? ` on "${selfMeta.slug}"` : ''
138
- log.dim(` @self expanded to @${selfMeta.selfId} from ${who}'s registration${where}.`)
139
- const fixCmd = `astrale identity register${
140
- selfMeta.identity ? ` ${selfMeta.identity}` : ''
141
- }${selfMeta.slug ? ` -i ${selfMeta.slug}` : ''}`
142
- log.dim(` If the node was deleted, refresh with: ${fixCmd}`)
137
+ log.dim(
138
+ ` @self resolved through authenticated Identity.whoami to @${selfMeta.selfId}${where}.`,
139
+ )
140
+ log.dim(' Check that the requested node path still exists for that principal.')
143
141
  }
144
142
  }
145
143
  break
@@ -176,6 +174,31 @@ export async function formatKernelError(
176
174
  break
177
175
  }
178
176
 
177
+ case 'ResponseError': {
178
+ const code = (error as { readonly code?: unknown }).code
179
+ const reason = (error as { readonly reason?: unknown }).reason
180
+ if (isRaw) {
181
+ writeRaw({
182
+ error: 'RESPONSE_ERROR',
183
+ ...(code === undefined ? {} : { code }),
184
+ message: error.message,
185
+ ...(reason === undefined ? {} : { reason }),
186
+ })
187
+ } else {
188
+ log.error(
189
+ `${chalk.bold(code === undefined ? 'RESPONSE_ERROR' : `RESPONSE_ERROR(${String(code)})`)}: ${error.message}`,
190
+ )
191
+ if (
192
+ reason !== null &&
193
+ typeof reason === 'object' &&
194
+ typeof (reason as { readonly code?: unknown }).code === 'string'
195
+ ) {
196
+ log.dim(` reason: ${(reason as { readonly code: string }).code}`)
197
+ }
198
+ }
199
+ break
200
+ }
201
+
179
202
  case 'KernelError': {
180
203
  const code = (error as { code?: number | string }).code ?? 'UNKNOWN'
181
204
  const type = (error as { type?: string }).type ?? 'KERNEL_ERROR'
@@ -194,7 +217,7 @@ export async function formatKernelError(
194
217
  }
195
218
 
196
219
  /** Strip internal `::methodName` suffixes from paths in error messages (e.g., "/path::listChildren" → "/path") */
197
- function stripMethodSuffix(msg: string): string {
220
+ export function stripMethodSuffix(msg: string): string {
198
221
  return msg.replace(/(\/[^"\s:]+)::([a-zA-Z]\w*)/g, '$1')
199
222
  }
200
223
 
@@ -0,0 +1,274 @@
1
+ import type { Fetch } from '@astrale-os/kernel-client'
2
+ import type { IssuerId } from '@astrale-os/sdk/auth'
3
+
4
+ import { call, Client } from '@astrale-os/kernel-client'
5
+ import { createAuth } from '@astrale-os/kernel-client/auth'
6
+ import { credential, exchange as exchangeProtocol } from '@astrale-os/sdk/auth'
7
+
8
+ import type { SourceCredentialResolver } from './credential'
9
+ import type { ConnectionTarget } from './target'
10
+
11
+ import { AstraleError } from '../errors'
12
+ import { ExchangeCredentialCache } from '../state/exchange-credentials'
13
+
14
+ const EXCHANGE_TTL_SECONDS = 120
15
+ const MAXIMUM_RESPONSE_BYTES = 256 * 1024
16
+
17
+ /** Exchange exact authenticated User authority for a Domain bearer bound to this Kernel. */
18
+ export function createExchangeCredentialResolver(
19
+ target: ConnectionTarget & { readonly domainIssuer: IssuerId },
20
+ source: SourceCredentialResolver,
21
+ fetch: Fetch,
22
+ timeoutMs: number,
23
+ cache = new ExchangeCredentialCache(),
24
+ ): SourceCredentialResolver {
25
+ requireExchangeTransport(target)
26
+ return Object.freeze({
27
+ async resolve(kernelIssuer: IssuerId, signal: AbortSignal): Promise<string> {
28
+ requireLive(signal)
29
+ const sourceToken = await source.resolve(kernelIssuer, signal)
30
+ const delegationTtlSeconds = delegationLifetime(sourceToken)
31
+ requireLive(signal)
32
+
33
+ const client = new Client({ url: `${kernelIssuer}/invoke`, fetch, timeoutMs })
34
+ try {
35
+ const authenticated = client.as(sourceToken)
36
+ const auth = createAuth((path, input, options) =>
37
+ authenticated.call(call(path, input), {
38
+ ...options,
39
+ delegate: { ttlSeconds: delegationTtlSeconds },
40
+ }),
41
+ )
42
+ const user = await auth.whoami({ signal })
43
+ const key = Object.freeze({
44
+ kernelIssuer,
45
+ domainIssuer: target.domainIssuer,
46
+ user: user.id,
47
+ })
48
+ return await cache.getOrRefresh(key, async () => {
49
+ let envelope: string | undefined
50
+ for (let attempt = 0; attempt < 3; attempt += 1) {
51
+ try {
52
+ envelope = await auth.delegate(
53
+ user.id,
54
+ {
55
+ audience: target.domainIssuer,
56
+ ttlSeconds: delegationTtlSeconds,
57
+ attenuation: { kind: 'identity', self: true },
58
+ },
59
+ { signal },
60
+ )
61
+ break
62
+ } catch (cause) {
63
+ if (attempt === 2 || !unknownFunctionOutcome(cause)) throw cause
64
+ }
65
+ }
66
+ if (envelope === undefined) throw new Error('Token delegation returned no credential.')
67
+ return exchange(target.domainIssuer, kernelIssuer, envelope, fetch, signal)
68
+ })
69
+ } finally {
70
+ client.close()
71
+ }
72
+ },
73
+ })
74
+ }
75
+
76
+ function unknownFunctionOutcome(cause: unknown): boolean {
77
+ if (cause === null || typeof cause !== 'object') return false
78
+ const error = cause as { readonly code?: unknown; readonly reason?: unknown }
79
+ if (error.code === 5002) return true
80
+ if (error.reason === null || typeof error.reason !== 'object') return false
81
+ return (error.reason as { readonly code?: unknown }).code === 'FUNCTION_OUTCOME_UNKNOWN'
82
+ }
83
+
84
+ function delegationLifetime(sourceToken: string): number {
85
+ const expiresAt = credential.inspect(sourceToken).claims.exp
86
+ if (typeof expiresAt !== 'number' || !Number.isSafeInteger(expiresAt)) {
87
+ throw new AstraleError(
88
+ 'TOKEN_EXCHANGE_SOURCE_INVALID',
89
+ 'The source identity credential has no valid expiration.',
90
+ )
91
+ }
92
+ const remaining = expiresAt - Math.ceil(Date.now() / 1_000) - 1
93
+ if (!Number.isSafeInteger(remaining) || remaining < 1) {
94
+ throw new AstraleError(
95
+ 'TOKEN_EXCHANGE_SOURCE_EXPIRED',
96
+ 'The source identity credential has no lifetime available for token exchange.',
97
+ )
98
+ }
99
+ return Math.min(EXCHANGE_TTL_SECONDS, remaining)
100
+ }
101
+
102
+ async function exchange(
103
+ domainIssuer: IssuerId,
104
+ kernelIssuer: IssuerId,
105
+ envelope: string,
106
+ fetch: Fetch,
107
+ signal: AbortSignal,
108
+ ): Promise<{ readonly credential: string; readonly expiresAt: number }> {
109
+ const configurationUrl = new URL(
110
+ exchangeProtocol.paths(domainIssuer).configuration,
111
+ domainIssuer,
112
+ ).toString()
113
+ const configurationResponse = await fetch(configurationUrl, {
114
+ method: 'GET',
115
+ redirect: 'error',
116
+ credentials: 'omit',
117
+ cache: 'no-store',
118
+ referrerPolicy: 'no-referrer',
119
+ headers: { accept: 'application/json' },
120
+ signal,
121
+ })
122
+ if (!configurationResponse.ok) {
123
+ throw new AstraleError(
124
+ 'TOKEN_EXCHANGE_DISCOVERY_FAILED',
125
+ `Domain issuer discovery failed with HTTP ${configurationResponse.status}.`,
126
+ )
127
+ }
128
+ const configuration = exchangeProtocol.acceptConfiguration(
129
+ await boundedJson(configurationResponse, signal),
130
+ domainIssuer,
131
+ )
132
+ const endpoint = configuration.token_exchange_endpoint
133
+ if (endpoint === undefined) {
134
+ throw new AstraleError(
135
+ 'TOKEN_EXCHANGE_UNSUPPORTED',
136
+ `Domain issuer ${domainIssuer} does not advertise token exchange.`,
137
+ 'This command has no legacy token fallback.',
138
+ )
139
+ }
140
+
141
+ const response = await fetch(endpoint, {
142
+ method: 'POST',
143
+ redirect: 'error',
144
+ credentials: 'omit',
145
+ cache: 'no-store',
146
+ referrerPolicy: 'no-referrer',
147
+ headers: {
148
+ authorization: `Bearer ${envelope}`,
149
+ accept: exchangeProtocol.MEDIA_TYPE,
150
+ },
151
+ signal,
152
+ })
153
+ const body = await boundedJson(response, signal)
154
+ if (!response.ok) {
155
+ let admitted: exchangeProtocol.ErrorResponse
156
+ try {
157
+ admitted = exchangeProtocol.acceptErrorResponse(body)
158
+ } catch (cause) {
159
+ throw new AstraleError(
160
+ 'TOKEN_EXCHANGE_PROTOCOL_ERROR',
161
+ `Token exchange failed with HTTP ${response.status} and an invalid error response.`,
162
+ cause instanceof Error ? cause.message : undefined,
163
+ )
164
+ }
165
+ throw new AstraleError(String(admitted.error.code), admitted.error.message)
166
+ }
167
+ requireExchangeResponseHeaders(response)
168
+ const exchanged = exchangeProtocol.acceptResponse(body)
169
+ const inspected = credential.inspect(exchanged.token)
170
+ if (
171
+ inspected.iss !== domainIssuer ||
172
+ inspected.aud !== kernelIssuer ||
173
+ inspected.claims.exp !== exchanged.expiresAt
174
+ ) {
175
+ throw new AstraleError(
176
+ 'TOKEN_EXCHANGE_PROTOCOL_ERROR',
177
+ 'Token exchange returned a credential inconsistent with the requested Domain and Kernel.',
178
+ )
179
+ }
180
+ return Object.freeze({ credential: exchanged.token, expiresAt: exchanged.expiresAt })
181
+ }
182
+
183
+ function requireExchangeTransport(
184
+ target: ConnectionTarget & { readonly domainIssuer: IssuerId },
185
+ ): void {
186
+ const kernel = new URL(target.kernelIssuer)
187
+ const domain = new URL(target.domainIssuer)
188
+ if (domain.protocol === 'https:') return
189
+ if (domain.protocol === 'http:' && kernel.protocol === 'http:') return
190
+ throw new AstraleError(
191
+ 'TOKEN_EXCHANGE_INSECURE',
192
+ 'An HTTP Domain issuer is allowed only with an explicitly configured HTTP Kernel target.',
193
+ )
194
+ }
195
+
196
+ function requireExchangeResponseHeaders(response: Response): void {
197
+ const contentType = response.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase()
198
+ if (contentType !== exchangeProtocol.MEDIA_TYPE) {
199
+ throw new AstraleError(
200
+ 'TOKEN_EXCHANGE_PROTOCOL_ERROR',
201
+ 'Token exchange returned an unsupported Content-Type.',
202
+ )
203
+ }
204
+ if (
205
+ !response.headers
206
+ .get('cache-control')
207
+ ?.toLowerCase()
208
+ .split(',')
209
+ .some((v) => v.trim() === 'no-store')
210
+ ) {
211
+ throw new AstraleError(
212
+ 'TOKEN_EXCHANGE_PROTOCOL_ERROR',
213
+ 'Token exchange response is missing Cache-Control: no-store.',
214
+ )
215
+ }
216
+ }
217
+
218
+ async function boundedJson(response: Response, signal: AbortSignal): Promise<unknown> {
219
+ const declared = response.headers.get('content-length')
220
+ if (
221
+ declared !== null &&
222
+ (!/^\d+$/u.test(declared) || Number(declared) > MAXIMUM_RESPONSE_BYTES)
223
+ ) {
224
+ throw new AstraleError(
225
+ 'TOKEN_EXCHANGE_PROTOCOL_ERROR',
226
+ 'Issuer response exceeds the size limit.',
227
+ )
228
+ }
229
+ if (response.body === null) {
230
+ throw new AstraleError('TOKEN_EXCHANGE_PROTOCOL_ERROR', 'Issuer response body is missing.')
231
+ }
232
+ const reader = response.body.getReader()
233
+ const chunks: Uint8Array[] = []
234
+ let size = 0
235
+ try {
236
+ for (;;) {
237
+ requireLive(signal)
238
+ const next = await reader.read()
239
+ if (next.done) break
240
+ size += next.value.byteLength
241
+ if (size > MAXIMUM_RESPONSE_BYTES) {
242
+ throw new AstraleError(
243
+ 'TOKEN_EXCHANGE_PROTOCOL_ERROR',
244
+ 'Issuer response exceeds the size limit.',
245
+ )
246
+ }
247
+ chunks.push(next.value)
248
+ }
249
+ } finally {
250
+ reader.releaseLock()
251
+ }
252
+ const bytes = new Uint8Array(size)
253
+ let offset = 0
254
+ for (const chunk of chunks) {
255
+ bytes.set(chunk, offset)
256
+ offset += chunk.byteLength
257
+ }
258
+ try {
259
+ return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)) as unknown
260
+ } catch (cause) {
261
+ throw new AstraleError(
262
+ 'TOKEN_EXCHANGE_PROTOCOL_ERROR',
263
+ 'Issuer response is not valid UTF-8 JSON.',
264
+ cause instanceof Error ? cause.message : undefined,
265
+ )
266
+ }
267
+ }
268
+
269
+ function requireLive(signal: AbortSignal): void {
270
+ if (!signal.aborted) return
271
+ throw signal.reason instanceof Error
272
+ ? signal.reason
273
+ : new DOMException('The operation was aborted.', 'AbortError')
274
+ }
@@ -0,0 +1,8 @@
1
+ export { createPathCall } from './call'
2
+ export { runKernelCommand } from './command'
3
+ export type { KernelCommandOpts } from './command'
4
+ export { expandSelfInCall, expandSelfInPath, withSelfHint } from './self'
5
+ export type { SelfExpansionMeta } from './self'
6
+ export { withAdminClientSession, withClientSession, type ConnectionContext } from './session'
7
+ export { registrationKeyForTarget } from './target'
8
+ export type { AdminConnectionOptions, ConnectionOptions, ConnectionTarget } from './target'
@@ -0,0 +1,120 @@
1
+ import type { KernelCommandOpts } from './command'
2
+
3
+ import { containsSelfRef, expandSelfReferences } from '../lib/self'
4
+ import { withClientSession } from './session'
5
+
6
+ /** Metadata attached to errors after an authenticated caller-authored @self expansion. */
7
+ export interface SelfExpansionMeta {
8
+ readonly original: string
9
+ readonly expanded: string
10
+ readonly selfId: string
11
+ readonly slug?: string
12
+ }
13
+
14
+ /**
15
+ * Run `action`; if it throws a node NotFoundError, retain the authenticated
16
+ * expansion for precise diagnostics. The error is re-thrown either way.
17
+ */
18
+ export async function withSelfHint<T>(
19
+ action: () => Promise<T>,
20
+ meta: SelfExpansionMeta | undefined,
21
+ ): Promise<T> {
22
+ if (!meta) return action()
23
+ try {
24
+ return await action()
25
+ } catch (err) {
26
+ if (err instanceof Error && err.name === 'NotFoundError') {
27
+ ;(err as Error & { expandedFromSelf?: SelfExpansionMeta }).expandedFromSelf = meta
28
+ }
29
+ throw err
30
+ }
31
+ }
32
+
33
+ type AuthenticatedSelf = {
34
+ readonly id?: unknown
35
+ readonly slug?: string
36
+ }
37
+
38
+ export type ResolveSelfIdDependencies = {
39
+ readonly whoami?: (opts: KernelCommandOpts) => Promise<AuthenticatedSelf>
40
+ }
41
+
42
+ /** Resolve @self only from the effective principal authenticated by the selected Kernel. */
43
+ export async function resolveSelfIdAuthenticated(
44
+ opts: KernelCommandOpts,
45
+ dependencies: ResolveSelfIdDependencies = {},
46
+ ): Promise<{ readonly id: string; readonly slug?: string }> {
47
+ const resolved = await (dependencies.whoami ?? whoamiSelfId)(opts)
48
+ if (typeof resolved.id !== 'string' || resolved.id.trim().length === 0) {
49
+ const error = new Error(
50
+ '`@self` could not be resolved: authenticated Identity.whoami returned no NodeId.',
51
+ )
52
+ error.name = 'SelfResolutionError'
53
+ throw error
54
+ }
55
+ return resolved.slug === undefined
56
+ ? { id: resolved.id }
57
+ : { id: resolved.id, slug: resolved.slug }
58
+ }
59
+
60
+ async function whoamiSelfId(opts: KernelCommandOpts): Promise<AuthenticatedSelf> {
61
+ return withClientSession(opts, async ({ auth, target }) => ({
62
+ ...(await auth.whoami()),
63
+ ...(target.slug === undefined ? {} : { slug: target.slug }),
64
+ }))
65
+ }
66
+
67
+ /** Expand @self in path-like commands and return metadata for NotFound diagnostics. */
68
+ export async function expandSelfInPath(
69
+ path: string,
70
+ options: KernelCommandOpts,
71
+ ): Promise<{ readonly path: string; readonly meta?: SelfExpansionMeta }> {
72
+ const expanded = await expandSelfValues(path, [], options)
73
+ return expanded.meta === undefined
74
+ ? { path: expanded.path }
75
+ : { path: expanded.path, meta: expanded.meta }
76
+ }
77
+
78
+ /** Expand @self once across one Call path and its CLI-authored string parameters. */
79
+ export async function expandSelfInCall(
80
+ path: string,
81
+ parameters: readonly string[],
82
+ options: KernelCommandOpts,
83
+ ): Promise<{
84
+ readonly path: string
85
+ readonly parameters: readonly string[]
86
+ readonly meta?: SelfExpansionMeta
87
+ }> {
88
+ return expandSelfValues(path, parameters, options)
89
+ }
90
+
91
+ async function expandSelfValues(
92
+ path: string,
93
+ parameters: readonly string[],
94
+ options: KernelCommandOpts,
95
+ ): Promise<{
96
+ readonly path: string
97
+ readonly parameters: readonly string[]
98
+ readonly meta?: SelfExpansionMeta
99
+ }> {
100
+ if (!containsSelfRef(path) && !parameters.some(containsSelfRef)) {
101
+ return { path, parameters }
102
+ }
103
+ const self = await resolveSelfIdAuthenticated(options)
104
+ const expandedPath = expandSelfReferences(path, self.id)
105
+ const expandedParameters = parameters.map((parameter) => expandSelfReferences(parameter, self.id))
106
+ const changed =
107
+ expandedPath !== path ||
108
+ expandedParameters.some((parameter, index) => parameter !== parameters[index])
109
+ if (!changed) return { path, parameters }
110
+ return {
111
+ path: expandedPath,
112
+ parameters: expandedParameters,
113
+ meta: {
114
+ original: path,
115
+ expanded: expandedPath,
116
+ selfId: self.id,
117
+ ...(self.slug === undefined ? {} : { slug: self.slug }),
118
+ },
119
+ }
120
+ }