@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.
- package/README.md +4 -1
- package/dist/astrale.js +62968 -41240
- package/dist/public/connect-core.js +55030 -0
- package/dist/public/keys/index.js +45631 -0
- package/dist/public/paths/index.js +43990 -0
- package/dist/types/admin/instance/model.d.ts +40 -0
- package/dist/types/connect-core.d.ts +22 -0
- package/dist/types/connection/auth.d.ts +33 -0
- package/dist/types/errors.d.ts +23 -0
- package/dist/types/identity/registry.d.ts +25 -0
- package/dist/types/keys/algorithm.d.ts +6 -0
- package/dist/types/keys/credential.d.ts +21 -0
- package/dist/types/keys/index.d.ts +4 -0
- package/dist/types/keys/pair.d.ts +34 -0
- package/dist/types/lib/admin-target.d.ts +74 -0
- package/dist/types/lib/ca-fetch.d.ts +2 -0
- package/dist/types/lib/config.d.ts +71 -0
- package/dist/types/lib/format.d.ts +1 -0
- package/dist/types/lib/idp-session.d.ts +33 -0
- package/dist/types/lib/idp.d.ts +397 -0
- package/dist/types/lib/instance-target.d.ts +36 -0
- package/dist/types/lib/instance.d.ts +186 -0
- package/dist/types/lib/log.d.ts +30 -0
- package/dist/types/lib/login-flow.d.ts +49 -0
- package/dist/types/lib/meta.d.ts +36 -0
- package/dist/types/lib/output.d.ts +83 -0
- package/dist/types/lib/table.d.ts +18 -0
- package/dist/types/lib/validation.d.ts +11 -0
- package/dist/types/paths/index.d.ts +2 -0
- package/dist/types/state/exchange-credentials.d.ts +25 -0
- package/dist/types/state/files.d.ts +9 -0
- package/dist/types/state/identities.d.ts +42 -0
- package/dist/types/state/index.d.ts +8 -0
- package/dist/types/state/paths.d.ts +32 -0
- package/package.json +30 -8
- package/src/admin/.spec/api.d.ts +3 -0
- package/src/admin/.spec/architecture.md +15 -0
- package/src/admin/.spec/icon.svg +3 -0
- package/src/admin/.spec/layout.ts +6 -0
- package/src/admin/catalog/.spec/api.d.ts +65 -0
- package/src/admin/catalog/.spec/architecture.md +6 -0
- package/src/admin/catalog/.spec/icon.svg +3 -0
- package/src/admin/catalog/.spec/layout.ts +6 -0
- package/src/admin/catalog/__tests__/client.test.ts +171 -0
- package/src/admin/catalog/client.ts +214 -0
- package/src/admin/catalog/index.ts +13 -0
- package/src/admin/catalog/model.ts +42 -0
- package/src/admin/catalog/tsconfig.json +8 -0
- package/src/admin/graph/.spec/api.d.ts +28 -0
- package/src/admin/graph/.spec/architecture.md +5 -0
- package/src/admin/graph/.spec/icon.svg +3 -0
- package/src/admin/graph/.spec/layout.ts +3 -0
- package/src/admin/graph/index.ts +6 -0
- package/src/admin/graph/nodes.ts +56 -0
- package/src/admin/index.ts +2 -0
- package/src/admin/instance/.spec/api.d.ts +66 -0
- package/src/admin/instance/.spec/architecture.md +10 -0
- package/src/admin/instance/.spec/icon.svg +3 -0
- package/src/admin/instance/.spec/layout.ts +6 -0
- package/src/admin/instance/__tests__/client.test.ts +289 -0
- package/src/admin/instance/client.ts +298 -0
- package/src/admin/instance/index.ts +16 -0
- package/src/admin/instance/model.ts +65 -0
- package/src/admin/instance/tsconfig.json +8 -0
- package/src/commands/__tests__/admin-instance.test.ts +3 -36
- package/src/commands/__tests__/call.test.ts +21 -68
- package/src/commands/__tests__/domain-install-owned.test.ts +26 -27
- package/src/commands/__tests__/domain-list.test.ts +2 -6
- package/src/commands/__tests__/logs.test.ts +50 -98
- package/src/commands/__tests__/ls.test.ts +21 -17
- package/src/commands/__tests__/read-commands.test.ts +171 -101
- package/src/commands/__tests__/view.test.ts +100 -51
- package/src/commands/admin/status.ts +3 -3
- package/src/commands/admin/use.ts +29 -6
- package/src/commands/auth/login.ts +1 -1
- package/src/commands/auth/logout.ts +4 -2
- package/src/commands/auth/status.ts +2 -2
- package/src/commands/auth/token.ts +2 -2
- package/src/commands/browser.ts +1 -1
- package/src/commands/call.ts +55 -91
- package/src/commands/describe.ts +52 -147
- package/src/commands/domain/install.ts +81 -50
- package/src/commands/domain/list.ts +4 -8
- package/src/commands/domain/publish.ts +9 -31
- package/src/commands/get.ts +32 -65
- package/src/commands/identity/__tests__/register.test.ts +93 -0
- package/src/commands/identity/create.ts +2 -2
- package/src/commands/identity/delete.ts +2 -2
- package/src/commands/identity/export.ts +7 -33
- package/src/commands/identity/import.ts +21 -70
- package/src/commands/identity/list.ts +2 -2
- package/src/commands/identity/register.ts +148 -117
- package/src/commands/identity/sync.ts +2 -2
- package/src/commands/identity/unsync.ts +2 -2
- package/src/commands/identity/use.ts +2 -2
- package/src/commands/identity/whoami.ts +2 -2
- package/src/commands/idp/add.ts +1 -1
- package/src/commands/idp/list.ts +1 -1
- package/src/commands/idp/refresh.ts +1 -1
- package/src/commands/idp/remove.ts +2 -2
- package/src/commands/idp/show.ts +1 -1
- package/src/commands/instance/active.ts +2 -2
- package/src/commands/instance/bookmark.ts +8 -2
- package/src/commands/instance/create.ts +1 -1
- package/src/commands/instance/delete.ts +4 -10
- package/src/commands/instance/forget.ts +1 -1
- package/src/commands/instance/list.ts +3 -3
- package/src/commands/instance/status.ts +5 -7
- package/src/commands/instance/use.ts +3 -3
- package/src/commands/logs.ts +152 -279
- package/src/commands/ls.ts +78 -204
- package/src/commands/mutate.ts +70 -139
- package/src/commands/query.ts +100 -278
- package/src/commands/session/analyze.ts +1 -1
- package/src/commands/session/list.ts +1 -1
- package/src/commands/setup.ts +1 -1
- package/src/commands/status.ts +1 -1
- package/src/commands/studio.ts +1 -1
- package/src/commands/token.ts +26 -21
- package/src/commands/update.ts +1 -1
- package/src/commands/use.ts +2 -3
- package/src/commands/view-serve.ts +1 -1
- package/src/commands/view.ts +67 -109
- package/src/connect-core.test.ts +38 -0
- package/src/connect-core.ts +42 -0
- package/src/connection/.spec/api.d.ts +111 -0
- package/src/connection/.spec/architecture.md +41 -0
- package/src/connection/.spec/capabilities/connection.ts +13 -0
- package/src/connection/.spec/examples/call.ts +7 -0
- package/src/connection/.spec/flows/command.ts +20 -0
- package/src/connection/.spec/flows/session.ts +61 -0
- package/src/connection/.spec/laws/connection.ts +181 -0
- package/src/connection/.spec/layout.ts +19 -0
- package/src/{kernel → connection}/__tests__/auth.test.ts +26 -2
- package/src/connection/__tests__/ca-fetch.test.ts +53 -0
- package/src/connection/__tests__/call.test.ts +17 -0
- package/src/connection/__tests__/credential.test.ts +109 -0
- package/src/connection/__tests__/errors.test.ts +96 -0
- package/src/connection/__tests__/exchange.test.ts +224 -0
- package/src/connection/__tests__/fixtures/localhost-cert.pem +19 -0
- package/src/connection/__tests__/fixtures/localhost-key.base64 +1 -0
- package/src/connection/__tests__/managed.test.ts +39 -0
- package/src/connection/__tests__/self.test.ts +51 -0
- package/src/connection/__tests__/session.test.ts +140 -0
- package/src/connection/__tests__/target.test.ts +93 -0
- package/src/{kernel → connection}/auth.ts +8 -8
- package/src/connection/call.ts +7 -0
- package/src/{kernel/run.ts → connection/command.ts} +22 -13
- package/src/connection/credential.ts +114 -0
- package/src/{kernel → connection}/errors.ts +32 -9
- package/src/connection/exchange.ts +274 -0
- package/src/connection/index.ts +8 -0
- package/src/connection/self.ts +120 -0
- package/src/connection/session.ts +176 -0
- package/src/connection/target.ts +111 -0
- package/src/connection/tsconfig.json +6 -0
- package/src/graph/.spec/api.d.ts +34 -0
- package/src/graph/.spec/laws/documents.ts +24 -0
- package/src/graph/.spec/laws/projection.ts +13 -0
- package/src/graph/.spec/layout.ts +6 -0
- package/src/graph/__tests__/mutation.test.ts +43 -0
- package/src/graph/__tests__/projection.test.ts +22 -0
- package/src/graph/__tests__/query.test.ts +154 -0
- package/src/graph/index.ts +3 -0
- package/src/graph/mutation.ts +18 -0
- package/src/graph/projection.ts +21 -0
- package/src/graph/query.ts +108 -0
- package/src/graph/tsconfig.json +6 -0
- package/src/identity/.history/ard/bootstrap-root-transfer.md +57 -0
- package/src/identity/.history/ard/domain-mediated-registration.md +36 -0
- package/src/identity/.spec/api.d.ts +124 -0
- package/src/identity/.spec/architecture.md +33 -0
- package/src/identity/.spec/capabilities/identity.ts +7 -0
- package/src/identity/.spec/flows/bootstrap-root.ts +79 -0
- package/src/identity/.spec/flows/domain-mediated-registration.ts +51 -0
- package/src/identity/.spec/flows/import.ts +29 -0
- package/src/identity/.spec/laws/identity.ts +56 -0
- package/src/identity/.spec/layout.ts +13 -0
- package/src/identity/.spec/schemas/identity-export-v1.schema.json +25 -0
- package/src/identity/__tests__/fixtures/registry-journey.ts +53 -0
- package/src/identity/__tests__/registration.test.ts +64 -0
- package/src/identity/__tests__/registry.test.ts +70 -0
- package/src/identity/__tests__/transfer.test.ts +198 -0
- package/src/identity/index.ts +23 -0
- package/src/identity/registration.ts +72 -0
- package/src/identity/registry.ts +172 -0
- package/src/identity/transfer.ts +187 -0
- package/src/identity/tsconfig.json +6 -0
- package/src/index.ts +1 -0
- package/src/keys/.spec/api.d.ts +74 -0
- package/src/keys/.spec/architecture.md +13 -0
- package/src/keys/.spec/laws/keys.ts +43 -0
- package/src/keys/.spec/layout.ts +6 -0
- package/src/keys/__tests__/algorithm.test.ts +39 -0
- package/src/keys/__tests__/keys.test.ts +168 -0
- package/src/keys/algorithm.ts +33 -0
- package/src/keys/credential.ts +112 -0
- package/src/keys/index.ts +14 -0
- package/src/keys/pair.ts +219 -0
- package/src/keys/tsconfig.json +6 -0
- package/src/lib/__tests__/admin-target.test.ts +67 -7
- package/src/lib/__tests__/binary.test.ts +2 -3
- package/src/lib/__tests__/command-dx.test.ts +2 -2
- package/src/lib/__tests__/config.test.ts +3 -2
- package/src/lib/__tests__/idp.test.ts +4 -6
- package/src/lib/__tests__/instance-target.test.ts +7 -4
- package/src/lib/__tests__/local-status.test.ts +1 -1
- package/src/lib/__tests__/log.test.ts +31 -0
- package/src/lib/__tests__/meta.test.ts +55 -0
- package/src/lib/__tests__/sdk-deps.test.ts +1 -1
- package/src/lib/__tests__/self.test.ts +8 -230
- package/src/lib/__tests__/use-target.test.ts +1 -1
- package/src/lib/__tests__/view-open-intent.test.ts +78 -104
- package/src/lib/__tests__/view-session.test.ts +99 -0
- package/src/lib/admin-domain.ts +39 -25
- package/src/lib/admin-instance.ts +38 -43
- package/src/lib/admin-target.ts +67 -13
- package/src/lib/binary.ts +13 -34
- package/src/lib/browser.ts +1 -1
- package/src/{kernel → lib}/ca-fetch.ts +35 -27
- package/src/lib/config.ts +1 -1
- package/src/lib/idp-session.ts +1 -1
- package/src/lib/idp.ts +3 -8
- package/src/lib/instance-target.ts +12 -8
- package/src/lib/instance.ts +34 -2
- package/src/lib/local-status.ts +2 -2
- package/src/lib/log.ts +3 -1
- package/src/lib/login-flow.ts +32 -5
- package/src/lib/meta.ts +15 -11
- package/src/lib/provision-instance.ts +4 -12
- package/src/lib/self.ts +5 -147
- package/src/lib/update.ts +1 -1
- package/src/lib/use-target.ts +1 -1
- package/src/lib/validation.ts +2 -2
- package/src/lib/view/open-intent.ts +7 -37
- package/src/lib/view/port-allocation.ts +1 -1
- package/src/lib/view/resolve.ts +52 -45
- package/src/lib/view/server.ts +16 -33
- package/src/lib/view/session.ts +51 -22
- package/src/paths/.spec/api.d.ts +13 -0
- package/src/paths/.spec/architecture.md +5 -0
- package/src/paths/.spec/layout.ts +3 -0
- package/src/paths/index.ts +13 -0
- package/src/program/.spec/api.d.ts +42 -0
- package/src/program/.spec/architecture.md +18 -0
- package/src/program/.spec/laws/program.ts +29 -0
- package/src/program/.spec/layout.ts +14 -0
- package/src/program/__tests__/program.test.ts +359 -0
- package/src/program/build.ts +190 -0
- package/src/program/command.ts +42 -0
- package/src/program/index.ts +2 -0
- package/src/program/options.ts +40 -0
- package/src/{registry.ts → program/registry.ts} +5 -1
- package/src/program/tsconfig.json +6 -0
- package/src/setup/steps/admin.ts +18 -3
- package/src/setup/steps/instance.ts +1 -1
- package/src/state/.spec/api.d.ts +133 -0
- package/src/state/.spec/architecture.md +31 -0
- package/src/state/.spec/capabilities/state.ts +7 -0
- package/src/state/.spec/flows/identity-update.ts +22 -0
- package/src/state/.spec/laws/state.ts +73 -0
- package/src/state/.spec/layout.ts +14 -0
- package/src/state/__tests__/exchange-credentials.test.ts +188 -0
- package/src/state/__tests__/files.test.ts +113 -0
- package/src/state/__tests__/fixtures/identity-transition.ts +37 -0
- package/src/state/__tests__/identities.test.ts +142 -0
- package/src/state/__tests__/paths.test.ts +27 -0
- package/src/state/exchange-credentials.ts +219 -0
- package/src/state/files.ts +129 -0
- package/src/state/identities.ts +236 -0
- package/src/state/index.ts +29 -0
- package/src/state/paths.ts +61 -0
- package/src/state/tsconfig.json +6 -0
- package/src/telemetry/__tests__/redact.test.ts +3 -3
- package/src/telemetry/__tests__/trigger.test.ts +1 -1
- package/src/telemetry/settings.ts +1 -1
- package/src/telemetry/store.ts +1 -1
- package/src/test-utils.ts +1 -1
- package/studio/client/dist/assets/elk-api-D0cBetPW.js +1 -0
- package/studio/client/dist/assets/elk-worker.min-C9JGDOE-.js +6312 -0
- package/studio/client/dist/assets/index-BQnJ5sgd.css +1 -0
- package/studio/client/dist/assets/index-ChOr3yP0.css +1 -0
- package/studio/client/dist/assets/index-Dspir4w7.js +81 -0
- package/studio/client/dist/assets/index-bVD2KJgz.js +8 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/package.json +10 -1
- package/studio/server/introspect/extractor.ts +1 -1
- package/studio/server/state/views.test.ts +3 -7
- package/studio/server/state/views.ts +23 -30
- package/studio/tsconfig.json +23 -0
- package/tsconfig.json +1 -1
- package/viewer/dist/main.js +65 -39
- package/viewer/tsconfig.json +13 -0
- package/src/command.ts +0 -42
- package/src/commands/__tests__/help-contract.test.ts +0 -164
- package/src/kernel/__tests__/client-owned-lookup.test.ts +0 -51
- package/src/kernel/__tests__/errors.test.ts +0 -43
- package/src/kernel/__tests__/expand.test.ts +0 -123
- package/src/kernel/client.ts +0 -214
- package/src/kernel/expand.ts +0 -192
- package/src/kernel/graph.ts +0 -96
- package/src/kernel/index.ts +0 -29
- package/src/kernel/options.ts +0 -22
- package/src/kernel/types.ts +0 -14
- package/src/lib/__tests__/domain-identity.test.ts +0 -60
- package/src/lib/__tests__/fs-atomic.test.ts +0 -104
- package/src/lib/__tests__/identity.test.ts +0 -79
- package/src/lib/__tests__/keys.test.ts +0 -129
- package/src/lib/domain-identity.ts +0 -49
- package/src/lib/env.ts +0 -49
- package/src/lib/fs-atomic.ts +0 -126
- package/src/lib/identity.ts +0 -256
- package/src/lib/keys.ts +0 -294
- package/src/lib/paths.ts +0 -11
- package/src/program.ts +0 -213
- package/studio/client/dist/assets/index-BaqEuIQJ.js +0 -109
- 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 {
|
|
4
|
-
import type {
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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>(
|
|
28
|
-
|
|
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
|
|
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 (
|
|
41
|
-
await
|
|
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 './
|
|
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
|
|
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(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
+
}
|