@astrale-os/cli 0.8.1-alpha.6 → 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -5
- package/dist/astrale.js +2661 -2714
- package/dist/public/connect-core.js +2019 -3050
- package/dist/public/keys/index.js +1851 -2885
- package/dist/public/paths/index.js +1830 -2872
- package/dist/types/connection/auth.d.ts +3 -0
- package/dist/types/lib/instance.d.ts +10 -0
- package/package.json +9 -9
- package/src/commands/__tests__/domain-install-operation.test.ts +121 -0
- package/src/commands/__tests__/domain-install-owned.test.ts +66 -0
- package/src/commands/__tests__/domain-uninstall.test.ts +53 -0
- package/src/commands/__tests__/install-direct.test.ts +3 -2
- package/src/commands/__tests__/install-identity-override.test.ts +14 -3
- package/src/commands/__tests__/instance-bookmark.test.ts +66 -1
- package/src/commands/__tests__/instance-list-rows.test.ts +1 -0
- package/src/commands/__tests__/instance-use.test.ts +67 -0
- package/src/commands/__tests__/view-build.test.ts +58 -0
- package/src/commands/domain/install.ts +84 -20
- package/src/commands/domain/uninstall.ts +128 -0
- package/src/commands/instance/active.ts +13 -1
- package/src/commands/instance/bookmark.ts +26 -3
- package/src/commands/instance/list.ts +18 -4
- package/src/commands/instance/use.ts +54 -7
- package/src/commands/view.ts +28 -16
- package/src/connection/.spec/architecture.md +5 -0
- package/src/connection/.spec/laws/connection.ts +20 -0
- package/src/connection/.spec/layout.ts +1 -0
- package/src/connection/__tests__/auth.test.ts +27 -1
- package/src/connection/__tests__/ca-fetch.test.ts +8 -1
- package/src/connection/__tests__/errors.test.ts +483 -33
- package/src/connection/__tests__/exchange.test.ts +46 -5
- package/src/connection/__tests__/reasons.test.ts +78 -0
- package/src/connection/auth.ts +11 -9
- package/src/connection/command.ts +9 -1
- package/src/connection/errors.ts +149 -159
- package/src/connection/exchange.ts +14 -2
- package/src/connection/index.ts +1 -1
- package/src/connection/reasons.ts +193 -0
- package/src/lib/__tests__/instance.test.ts +51 -1
- package/src/lib/__tests__/view-assets.test.ts +33 -1
- package/src/lib/__tests__/view-server.test.ts +68 -0
- package/src/lib/ca-fetch.ts +9 -3
- package/src/lib/instance.ts +31 -0
- package/src/lib/view/assets.ts +16 -2
- package/src/program/__tests__/program.test.ts +2 -1
- package/src/program/build.ts +2 -1
- package/studio/client/dist/assets/{elk-api-D0cBetPW.js → elk-api-D2xgMJvi.js} +1 -1
- package/studio/client/dist/assets/{index-BQnJ5sgd.css → index-BMdnsIJA.css} +1 -1
- package/studio/client/dist/assets/index-D-vRV8w7.js +8 -0
- package/studio/client/dist/assets/index-LGSWRrk8.js +81 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/package.json +8 -9
- package/studio/server/agent/prompts/anchors.test.ts +74 -0
- package/studio/server/agent/prompts/anchors.ts +85 -15
- package/studio/server/agent/prompts/system.test.ts +12 -0
- package/studio/server/agent/prompts/system.ts +6 -6
- package/studio/server/api.ts +1 -5
- package/studio/server/cache.ts +5 -2
- package/studio/server/domain.test.ts +67 -0
- package/studio/server/domain.ts +29 -6
- package/studio/server/index.ts +1 -3
- package/studio/server/introspect/anatomy-extras.test.ts +104 -1
- package/studio/server/introspect/anatomy-extras.ts +340 -8
- package/studio/server/introspect/anatomy.test.ts +33 -0
- package/studio/server/introspect/anatomy.ts +22 -7
- package/studio/server/introspect/bundle.ts +7 -1
- package/studio/server/introspect/canonical-schema.test.ts +395 -0
- package/studio/server/introspect/canonical-schema.ts +751 -0
- package/studio/server/introspect/core-extractor.ts +30 -10
- package/studio/server/introspect/core.ts +4 -2
- package/studio/server/introspect/diff.test.ts +124 -0
- package/studio/server/introspect/diff.ts +252 -23
- package/studio/server/introspect/extractor.ts +46 -14
- package/studio/server/introspect/overlay-tsmorph.test.ts +164 -1
- package/studio/server/introspect/overlay-tsmorph.ts +381 -106
- package/studio/server/introspect/overlay.test.ts +72 -0
- package/studio/server/introspect/overlay.ts +16 -6
- package/studio/server/introspect/runtime.test.ts +217 -0
- package/studio/server/introspect/runtime.ts +18 -6
- package/studio/server/introspect/schema-refs.test.ts +100 -0
- package/studio/server/introspect/schema-refs.ts +23 -2
- package/studio/server/state/baseline.test.ts +51 -0
- package/studio/server/state/baseline.ts +31 -2
- package/studio/server/state/create.test.ts +37 -0
- package/studio/server/state/create.ts +21 -15
- package/studio/server/state/instance.test.ts +63 -0
- package/studio/server/state/instance.ts +65 -29
- package/studio/server/state/views.test.ts +209 -9
- package/studio/server/state/views.ts +176 -69
- package/studio/server/watch.test.ts +36 -0
- package/studio/server/watch.ts +24 -16
- package/studio/server/workspace-watch.ts +8 -3
- package/studio/shared/types.ts +164 -33
- package/viewer/dist/main.js +57 -57
- package/studio/client/dist/assets/index-Dspir4w7.js +0 -81
- package/studio/client/dist/assets/index-bVD2KJgz.js +0 -8
- package/studio/server/view-dev-server.test.ts +0 -111
- package/studio/server/view-dev-server.ts +0 -372
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import { functionInputIssues, queryInputRepair } from '../reasons'
|
|
4
|
+
|
|
5
|
+
describe('public reason presentation admission', () => {
|
|
6
|
+
/** @evidence TEST-CLI-CONNECTION-ADMITS-BOUNDED-REASONS */
|
|
7
|
+
test('admits bounded public callable issues and filters unsafe additions', () => {
|
|
8
|
+
expect(
|
|
9
|
+
functionInputIssues({
|
|
10
|
+
code: 'FUNCTION_INPUT_INVALID',
|
|
11
|
+
details: {
|
|
12
|
+
issues: [
|
|
13
|
+
{
|
|
14
|
+
code: 'VALUE_SCHEMA_INSTANCE_INVALID',
|
|
15
|
+
path: '/issuer',
|
|
16
|
+
message: 'Object is missing required property issuer.',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
code: 'VALUE_SCHEMA_INSTANCE_INVALID',
|
|
20
|
+
path: '/slug',
|
|
21
|
+
message: 'Object is missing required property slug.',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
code: 'VALUE_SCHEMA_INSTANCE_INVALID',
|
|
25
|
+
path: '/unsafe',
|
|
26
|
+
message: 'private\ndiagnostic',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
).toEqual([
|
|
32
|
+
{
|
|
33
|
+
code: 'VALUE_SCHEMA_INSTANCE_INVALID',
|
|
34
|
+
path: '/issuer',
|
|
35
|
+
message: 'Object is missing required property issuer.',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
code: 'VALUE_SCHEMA_INSTANCE_INVALID',
|
|
39
|
+
path: '/slug',
|
|
40
|
+
message: 'Object is missing required property slug.',
|
|
41
|
+
},
|
|
42
|
+
])
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('admits exact Query repair variants and rejects unbounded additions', () => {
|
|
46
|
+
expect(
|
|
47
|
+
queryInputRepair({
|
|
48
|
+
code: 'QUERY_INPUT_INVALID',
|
|
49
|
+
details: {
|
|
50
|
+
phase: 'plan',
|
|
51
|
+
issue: 'QUERY_DEFINITION_NOT_EDGE',
|
|
52
|
+
path: '/steps/0/via/0',
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
).toEqual({
|
|
56
|
+
phase: 'plan',
|
|
57
|
+
issue: 'QUERY_DEFINITION_NOT_EDGE',
|
|
58
|
+
path: '/steps/0/via/0',
|
|
59
|
+
})
|
|
60
|
+
expect(
|
|
61
|
+
queryInputRepair({
|
|
62
|
+
code: 'QUERY_INPUT_INVALID',
|
|
63
|
+
details: { phase: 'limit', limit: 'steps', maximum: 4, actual: 5 },
|
|
64
|
+
}),
|
|
65
|
+
).toEqual({ phase: 'limit', limit: 'steps', maximum: 4, actual: 5 })
|
|
66
|
+
expect(
|
|
67
|
+
queryInputRepair({
|
|
68
|
+
code: 'QUERY_INPUT_INVALID',
|
|
69
|
+
details: {
|
|
70
|
+
phase: 'plan',
|
|
71
|
+
issue: 'QUERY_DEFINITION_NOT_EDGE',
|
|
72
|
+
path: '/steps/0/via/0',
|
|
73
|
+
provider: 'private',
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
).toBeUndefined()
|
|
77
|
+
})
|
|
78
|
+
})
|
package/src/connection/auth.ts
CHANGED
|
@@ -162,7 +162,7 @@ async function resolveIdpAccessToken(
|
|
|
162
162
|
)
|
|
163
163
|
}
|
|
164
164
|
if (e instanceof IdpSessionNoRefreshTokenError) {
|
|
165
|
-
throw
|
|
165
|
+
throw classifyNoRefreshTokenError(audience, identity.audience, e)
|
|
166
166
|
}
|
|
167
167
|
// An audience mismatch means the session is healthy but the IdP won't
|
|
168
168
|
// mint this audience — re-login is futile, so propagate it verbatim for
|
|
@@ -182,6 +182,16 @@ async function resolveIdpAccessToken(
|
|
|
182
182
|
return token
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
+
export function classifyNoRefreshTokenError(
|
|
186
|
+
requestedAudience: string,
|
|
187
|
+
sourceAudience: string | undefined,
|
|
188
|
+
error: IdpSessionNoRefreshTokenError,
|
|
189
|
+
): IdpSessionNoRefreshTokenError | IdpAudienceMismatchError {
|
|
190
|
+
return sourceAudience !== undefined && sourceAudience !== requestedAudience
|
|
191
|
+
? new IdpAudienceMismatchError(requestedAudience, sourceAudience)
|
|
192
|
+
: error
|
|
193
|
+
}
|
|
194
|
+
|
|
185
195
|
/** A refresh attempt failed for a reason that re-login will NOT fix. */
|
|
186
196
|
export class IdpRefreshTransientError extends Error {
|
|
187
197
|
constructor(message: string) {
|
|
@@ -224,11 +234,3 @@ function refreshFailureError(identityName: string, identity: Identity, cause: un
|
|
|
224
234
|
)
|
|
225
235
|
}
|
|
226
236
|
}
|
|
227
|
-
|
|
228
|
-
function wrongAudienceHint(identityName: string, identity: Identity, audience: string): string {
|
|
229
|
-
return (
|
|
230
|
-
`IdP token for "${identityName}" was not minted for target audience ${audience}, ` +
|
|
231
|
-
'and the cached session cannot be refreshed. ' +
|
|
232
|
-
`Run: astrale auth login --name ${identityName} --idp ${identity.idp ?? '<idp>'} --audience ${audience}`
|
|
233
|
-
)
|
|
234
|
-
}
|
|
@@ -16,6 +16,11 @@ export interface KernelCommandOpts extends ConnectionOptions {
|
|
|
16
16
|
readonly debug?: boolean
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export interface OperationRecovery {
|
|
20
|
+
readonly operation: string
|
|
21
|
+
readonly retry: string
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
/**
|
|
20
25
|
* Encapsulates the standard kernel command lifecycle:
|
|
21
26
|
* spinner → connect → call → timing → output → error handling.
|
|
@@ -27,6 +32,7 @@ export interface KernelCommandOpts extends ConnectionOptions {
|
|
|
27
32
|
export async function runKernelCommand<T>(input: {
|
|
28
33
|
readonly opts: KernelCommandOpts
|
|
29
34
|
readonly label: string
|
|
35
|
+
readonly recovery?: OperationRecovery
|
|
30
36
|
readonly fn: (context: ConnectionContext) => Promise<T>
|
|
31
37
|
readonly format?: (
|
|
32
38
|
result: T,
|
|
@@ -53,7 +59,9 @@ export async function runKernelCommand<T>(input: {
|
|
|
53
59
|
}
|
|
54
60
|
} catch (error) {
|
|
55
61
|
if (!isRaw && spin) spin.fail(`${label} failed`)
|
|
56
|
-
await formatKernelError(error, isRaw, undefined, opts.debug, {
|
|
62
|
+
await formatKernelError(error, isRaw, undefined, opts.debug, {
|
|
63
|
+
recovery: input.recovery,
|
|
64
|
+
})
|
|
57
65
|
process.exit(1)
|
|
58
66
|
}
|
|
59
67
|
}
|
package/src/connection/errors.ts
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import chalk from 'chalk'
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { OperationRecovery } from './command'
|
|
4
4
|
|
|
5
5
|
import { AstraleError } from '../errors'
|
|
6
|
-
import {
|
|
6
|
+
import { readLocalStatus, type LocalStatus } from '../lib/local-status'
|
|
7
7
|
import { log } from '../lib/log'
|
|
8
|
+
import {
|
|
9
|
+
functionInputIssues,
|
|
10
|
+
queryInputRepair,
|
|
11
|
+
reasonCode,
|
|
12
|
+
schemaUpgradeDetails,
|
|
13
|
+
schemaUpgradeHint,
|
|
14
|
+
type FunctionInputIssue,
|
|
15
|
+
type QueryInputRepair,
|
|
16
|
+
} from './reasons'
|
|
8
17
|
|
|
9
|
-
|
|
10
|
-
type InvariantError = { code: string; message: string; context?: unknown }
|
|
18
|
+
export { functionInputIssues, schemaUpgradeHint } from './reasons'
|
|
11
19
|
|
|
12
20
|
/**
|
|
13
21
|
* Format and display a kernel client error.
|
|
14
22
|
*
|
|
15
|
-
* Handles
|
|
16
|
-
* @astrale-os/kernel-client: ConnectionError, DisconnectedError,
|
|
17
|
-
* TimeoutError, AuthenticationError, PermissionDeniedError, NotFoundError,
|
|
18
|
-
* KernelError and its subclasses (ValidationError, InvariantViolationError).
|
|
23
|
+
* Handles CLI-local errors plus the current Kernel Client public error families.
|
|
19
24
|
*
|
|
20
25
|
* When `debug` is true, additional diagnostic information (class name, full
|
|
21
26
|
* error chain, attached url/details) is printed after the user-facing line.
|
|
@@ -25,12 +30,11 @@ export async function formatKernelError(
|
|
|
25
30
|
isRaw: boolean,
|
|
26
31
|
urlArg = '',
|
|
27
32
|
debug = false,
|
|
28
|
-
opts: {
|
|
33
|
+
opts: { recovery?: OperationRecovery } = {},
|
|
29
34
|
): Promise<void> {
|
|
30
35
|
const url =
|
|
31
36
|
urlArg || (error instanceof Error ? ((error as Error & { url?: string }).url ?? '') : '')
|
|
32
37
|
const localContext = await contextForError(error)
|
|
33
|
-
const credentialExpiration = opts.credential ? decodeJwtExpiration(opts.credential) : null
|
|
34
38
|
// Handle AstraleError (AuthError, etc.) with structured hints
|
|
35
39
|
if (error instanceof AstraleError) {
|
|
36
40
|
if (isRaw) {
|
|
@@ -52,167 +56,58 @@ export async function formatKernelError(
|
|
|
52
56
|
const name = error.name
|
|
53
57
|
|
|
54
58
|
switch (name) {
|
|
55
|
-
case '
|
|
56
|
-
|
|
57
|
-
writeRaw({ error: 'CONNECTION_ERROR', message: error.message, url, context: localContext })
|
|
58
|
-
else {
|
|
59
|
-
log.error(`Could not connect to ${chalk.bold(url || 'kernel')}`)
|
|
60
|
-
log.dim(` ${error.message}`)
|
|
61
|
-
log.dim(' Is the kernel running? Try: astrale status')
|
|
62
|
-
printLocalContext(localContext)
|
|
63
|
-
}
|
|
64
|
-
break
|
|
65
|
-
|
|
66
|
-
case 'DisconnectedError':
|
|
67
|
-
if (isRaw) writeRaw({ error: 'DISCONNECTED', message: error.message })
|
|
68
|
-
else {
|
|
69
|
-
log.error('Connection closed while request was pending')
|
|
70
|
-
log.dim(' The kernel may have been stopped or restarted. Retry the command.')
|
|
71
|
-
}
|
|
72
|
-
break
|
|
73
|
-
|
|
74
|
-
case 'TimeoutError': {
|
|
75
|
-
const timeoutMs = (error as { timeoutMs?: number }).timeoutMs
|
|
76
|
-
if (isRaw) writeRaw({ error: 'TIMEOUT', message: error.message, timeoutMs })
|
|
77
|
-
else {
|
|
78
|
-
log.error(`Request timed out after ${timeoutMs ?? '?'}ms`)
|
|
79
|
-
log.dim(' Try increasing with --timeout')
|
|
80
|
-
}
|
|
81
|
-
break
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
case 'AuthenticationError': {
|
|
85
|
-
const reason = (error as { reason?: string }).reason ?? 'unknown'
|
|
86
|
-
if (isRaw)
|
|
87
|
-
writeRaw({
|
|
88
|
-
error: 'AUTH_ERROR',
|
|
89
|
-
reason,
|
|
90
|
-
message: error.message,
|
|
91
|
-
credential: credentialExpiration,
|
|
92
|
-
context: localContext,
|
|
93
|
-
})
|
|
94
|
-
else {
|
|
95
|
-
log.error(`Authentication failed: ${error.message}`)
|
|
96
|
-
if (reason === 'missing')
|
|
97
|
-
log.dim(' No credential was sent. Run: astrale identity create <name>')
|
|
98
|
-
else if (reason === 'invalid')
|
|
99
|
-
log.dim(' Credential is invalid — check issuer/keypair. Try: astrale identity whoami')
|
|
100
|
-
else if (reason === 'expired') log.dim(' Credential expired — sign a fresh one')
|
|
101
|
-
if (credentialExpiration) {
|
|
102
|
-
const state = credentialExpiration.expired ? 'expired' : 'expires'
|
|
103
|
-
log.dim(` Credential ${state} at ${credentialExpiration.expiresAt}`)
|
|
104
|
-
}
|
|
105
|
-
printLocalContext(localContext)
|
|
106
|
-
}
|
|
107
|
-
break
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
case 'PermissionDeniedError':
|
|
111
|
-
if (isRaw) writeRaw({ error: 'PERMISSION_DENIED', message: error.message })
|
|
112
|
-
else {
|
|
113
|
-
log.error(`Permission denied: ${error.message}`)
|
|
114
|
-
log.dim(' Your identity does not have the required permissions for this operation')
|
|
115
|
-
}
|
|
59
|
+
case 'TransportError':
|
|
60
|
+
presentTransportError(error, isRaw, url, localContext, opts.recovery)
|
|
116
61
|
break
|
|
117
62
|
|
|
118
|
-
case 'NotFoundError': {
|
|
119
|
-
const cleanMsg = stripMethodSuffix(error.message)
|
|
120
|
-
const selfMeta = (error as Error & { expandedFromSelf?: SelfExpansionMeta }).expandedFromSelf
|
|
121
|
-
// kernel-client maps both NOT_FOUND (the node doesn't exist) and
|
|
122
|
-
// METHOD_NOT_FOUND (the method doesn't exist on a real node) to
|
|
123
|
-
// `NotFoundError`. Firing the authenticated-principal hint for the
|
|
124
|
-
// method case is misleading. Gate on the message referencing the
|
|
125
|
-
// expanded id — node lookup errors mention `@<id>` whereas method
|
|
126
|
-
// errors mention the method path.
|
|
127
|
-
const selfHintApplies = selfMeta && error.message.includes(`@${selfMeta.selfId}`)
|
|
128
|
-
if (isRaw) {
|
|
129
|
-
const payload: Record<string, unknown> = { error: 'NOT_FOUND', message: cleanMsg }
|
|
130
|
-
if (selfHintApplies) payload.expandedFromSelf = selfMeta
|
|
131
|
-
writeRaw(payload)
|
|
132
|
-
} else {
|
|
133
|
-
log.error(`Not found: ${cleanMsg}`)
|
|
134
|
-
log.dim(' Check the path/ID and that the instance is booted')
|
|
135
|
-
if (selfHintApplies && selfMeta) {
|
|
136
|
-
const where = selfMeta.slug ? ` on "${selfMeta.slug}"` : ''
|
|
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.')
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
break
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
case 'ValidationError': {
|
|
147
|
-
const errors = (error as { errors?: FieldError[] }).errors ?? []
|
|
148
|
-
if (isRaw) writeRaw({ error: 'VALIDATION_ERROR', message: error.message, details: errors })
|
|
149
|
-
else {
|
|
150
|
-
log.error('Validation Error')
|
|
151
|
-
if (errors.length > 0) {
|
|
152
|
-
for (const e of errors) {
|
|
153
|
-
console.log(chalk.red(` ${e.path.join('.')}: ${e.message} (${chalk.dim(e.code)})`))
|
|
154
|
-
}
|
|
155
|
-
} else {
|
|
156
|
-
// Server often sends details in message but empty errors array
|
|
157
|
-
console.log(chalk.red(` ${error.message}`))
|
|
158
|
-
}
|
|
159
|
-
log.dim(' Use `astrale introspect <path>` to see the expected schema')
|
|
160
|
-
}
|
|
161
|
-
break
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
case 'InvariantViolationError': {
|
|
165
|
-
const errors = (error as { errors?: InvariantError[] }).errors ?? []
|
|
166
|
-
if (isRaw) writeRaw({ error: 'INVARIANT_VIOLATION', message: error.message, details: errors })
|
|
167
|
-
else {
|
|
168
|
-
log.error('Invariant Violation')
|
|
169
|
-
for (const e of errors) {
|
|
170
|
-
console.log(chalk.red(` ${e.code}: ${e.message}`))
|
|
171
|
-
if (e.context) console.log(chalk.dim(` ${JSON.stringify(e.context)}`))
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
break
|
|
175
|
-
}
|
|
176
|
-
|
|
177
63
|
case 'ResponseError': {
|
|
178
64
|
const code = (error as { readonly code?: unknown }).code
|
|
179
65
|
const reason = (error as { readonly reason?: unknown }).reason
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
66
|
+
const codeOfReason = reasonCode(reason)
|
|
67
|
+
const inputIssues = functionInputIssues(reason)
|
|
68
|
+
const queryRepair = queryInputRepair(reason)
|
|
69
|
+
const upgrade = schemaUpgradeDetails(reason)
|
|
70
|
+
const removalHint = schemaDataRemovalHint(reason)
|
|
71
|
+
const domainAddressNotPublic = codeOfReason === 'SCHEMA_DOMAIN_ADDRESS_NOT_PUBLIC'
|
|
72
|
+
const displayMessage = domainAddressNotPublic
|
|
73
|
+
? 'Expose the Domain through a public HTTPS URL or public tunnel, then retry.'
|
|
74
|
+
: removalHint !== undefined && !isRaw
|
|
75
|
+
? 'Existing business data still uses schema definitions being removed.'
|
|
76
|
+
: error.message
|
|
186
77
|
const hint =
|
|
187
|
-
|
|
78
|
+
codeOfReason === 'FUNCTION_INPUT_INVALID' && (isRaw || inputIssues.length === 0)
|
|
188
79
|
? 'Use `astrale introspect <path>` to see the callable input.'
|
|
189
|
-
: undefined
|
|
80
|
+
: (removalHint ?? (upgrade === undefined ? undefined : schemaUpgradeHint(upgrade)))
|
|
190
81
|
if (isRaw) {
|
|
191
82
|
writeRaw({
|
|
192
83
|
error: 'RESPONSE_ERROR',
|
|
193
84
|
...(code === undefined ? {} : { code }),
|
|
194
|
-
message:
|
|
85
|
+
message: displayMessage,
|
|
195
86
|
...(reason === undefined ? {} : { reason }),
|
|
196
87
|
...(hint === undefined ? {} : { hint }),
|
|
197
88
|
})
|
|
198
89
|
} else {
|
|
199
90
|
log.error(
|
|
200
|
-
|
|
91
|
+
domainAddressNotPublic
|
|
92
|
+
? `${chalk.bold('SCHEMA_DOMAIN_ADDRESS_NOT_PUBLIC')}: ${displayMessage}`
|
|
93
|
+
: removalHint !== undefined
|
|
94
|
+
? `${chalk.bold('DATA_MIGRATION_REQUIRED')}: ${displayMessage}`
|
|
95
|
+
: `${chalk.bold(code === undefined ? 'RESPONSE_ERROR' : `RESPONSE_ERROR(${String(code)})`)}: ${displayMessage}`,
|
|
201
96
|
)
|
|
202
|
-
if (
|
|
97
|
+
if (codeOfReason !== undefined && !domainAddressNotPublic && removalHint === undefined) {
|
|
98
|
+
log.dim(` reason: ${codeOfReason}`)
|
|
99
|
+
}
|
|
100
|
+
presentFunctionInputIssues(inputIssues)
|
|
101
|
+
if (queryRepair !== undefined) presentQueryInputRepair(queryRepair)
|
|
102
|
+
if (upgrade?.issue === 'issuer-changed') {
|
|
103
|
+
log.dim(` installed issuer: ${upgrade.installedIssuer}`)
|
|
104
|
+
log.dim(` replacement issuer: ${upgrade.replacementIssuer}`)
|
|
105
|
+
}
|
|
203
106
|
if (hint !== undefined) log.dim(` ${hint}`)
|
|
204
107
|
}
|
|
205
108
|
break
|
|
206
109
|
}
|
|
207
110
|
|
|
208
|
-
case 'KernelError': {
|
|
209
|
-
const code = (error as { code?: number | string }).code ?? 'UNKNOWN'
|
|
210
|
-
const type = (error as { type?: string }).type ?? 'KERNEL_ERROR'
|
|
211
|
-
if (isRaw) writeRaw({ error: type, code, message: error.message })
|
|
212
|
-
else log.error(`${chalk.bold(`${type}(${code})`)}: ${error.message}`)
|
|
213
|
-
break
|
|
214
|
-
}
|
|
215
|
-
|
|
216
111
|
default: {
|
|
217
112
|
const mapped = mapPublicError(error)
|
|
218
113
|
if (isRaw) {
|
|
@@ -232,9 +127,28 @@ export async function formatKernelError(
|
|
|
232
127
|
if (debug) printDebug(error, url)
|
|
233
128
|
}
|
|
234
129
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
130
|
+
function schemaDataRemovalHint(reason: unknown): string | undefined {
|
|
131
|
+
if (reason === null || typeof reason !== 'object') return undefined
|
|
132
|
+
const value = reason as { readonly code?: unknown; readonly details?: unknown }
|
|
133
|
+
if (value.code !== 'DATA_MIGRATION_REQUIRED') return undefined
|
|
134
|
+
if (value.details === null || typeof value.details !== 'object') return undefined
|
|
135
|
+
|
|
136
|
+
const requirements = (value.details as { readonly requirements?: unknown }).requirements
|
|
137
|
+
if (
|
|
138
|
+
!Array.isArray(requirements) ||
|
|
139
|
+
requirements.length === 0 ||
|
|
140
|
+
!requirements.every(
|
|
141
|
+
(requirement) =>
|
|
142
|
+
requirement !== null &&
|
|
143
|
+
typeof requirement === 'object' &&
|
|
144
|
+
(requirement as { readonly operation?: unknown }).operation === 'remove-facts' &&
|
|
145
|
+
(requirement as { readonly reason?: unknown }).reason === 'destructive-change',
|
|
146
|
+
)
|
|
147
|
+
) {
|
|
148
|
+
return undefined
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return 'Delete this data explicitly, then retry. No data was deleted.'
|
|
238
152
|
}
|
|
239
153
|
|
|
240
154
|
function mapPublicError(error: Error): {
|
|
@@ -273,14 +187,90 @@ function mapPublicError(error: Error): {
|
|
|
273
187
|
hint: 'Pass the Kernel issuer URL (no /invoke suffix), e.g. https://host/kernel/host',
|
|
274
188
|
}
|
|
275
189
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
190
|
+
return { code: name && name !== 'Error' ? name : 'UNKNOWN', message: error.message }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function presentTransportError(
|
|
194
|
+
error: Error,
|
|
195
|
+
isRaw: boolean,
|
|
196
|
+
url: string,
|
|
197
|
+
context: LocalStatus | undefined,
|
|
198
|
+
recovery: OperationRecovery | undefined,
|
|
199
|
+
): void {
|
|
200
|
+
const phase = transportPhase(error)
|
|
201
|
+
const delivery = transportDelivery(error)
|
|
202
|
+
const code =
|
|
203
|
+
phase === 'connect'
|
|
204
|
+
? 'CONNECTION_ERROR'
|
|
205
|
+
: phase === 'timeout'
|
|
206
|
+
? 'TIMEOUT'
|
|
207
|
+
: phase === 'closed'
|
|
208
|
+
? 'DISCONNECTED'
|
|
209
|
+
: 'TRANSPORT_ERROR'
|
|
210
|
+
if (isRaw) {
|
|
211
|
+
writeRaw({
|
|
212
|
+
error: code,
|
|
279
213
|
message: error.message,
|
|
280
|
-
|
|
281
|
-
|
|
214
|
+
...(url === '' ? {} : { url }),
|
|
215
|
+
...(phase === undefined ? {} : { phase }),
|
|
216
|
+
...(delivery === undefined ? {} : { delivery }),
|
|
217
|
+
...(context === undefined ? {} : { context }),
|
|
218
|
+
...(delivery === 'unknown' && recovery !== undefined ? recovery : {}),
|
|
219
|
+
})
|
|
220
|
+
return
|
|
282
221
|
}
|
|
283
|
-
|
|
222
|
+
log.error(`${chalk.bold(code)}: ${error.message}`)
|
|
223
|
+
if (url !== '') log.dim(` target: ${url}`)
|
|
224
|
+
if (phase !== undefined) log.dim(` phase: ${phase}`)
|
|
225
|
+
if (phase === 'connect') log.dim(' Check the target and run `astrale status`.')
|
|
226
|
+
else if (phase === 'timeout') log.dim(' Try increasing `--timeout`.')
|
|
227
|
+
else if (delivery === 'unknown') printOperationRecovery(recovery)
|
|
228
|
+
printLocalContext(context)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function printOperationRecovery(recovery: OperationRecovery | undefined): void {
|
|
232
|
+
if (recovery === undefined) {
|
|
233
|
+
log.dim(' Delivery is unknown; do not automatically retry a mutating call.')
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
log.dim(' Delivery is unknown; retry with the same operation id:')
|
|
237
|
+
log.dim(` operation: ${recovery.operation}`)
|
|
238
|
+
log.dim(` ${recovery.retry}`)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function presentFunctionInputIssues(issues: readonly FunctionInputIssue[]): void {
|
|
242
|
+
for (const issue of issues) {
|
|
243
|
+
const location = issue.path === undefined || issue.path === '' ? '<input>' : issue.path
|
|
244
|
+
console.log(chalk.red(` ${location}: ${issue.message} (${chalk.dim(issue.code)})`))
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function presentQueryInputRepair(repair: QueryInputRepair): void {
|
|
249
|
+
if (repair.phase === 'plan') {
|
|
250
|
+
log.dim(` ${repair.path ?? '/'} ${repair.issue}`)
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
if (repair.phase === 'limit') {
|
|
254
|
+
log.dim(` ${repair.path ?? '/'} ${repair.limit} limit ${repair.actual}/${repair.maximum}`)
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
log.dim(` ${repair.path} ${repair.phase} input`)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function transportPhase(error: Error): string | undefined {
|
|
261
|
+
const phase = (error as Error & { readonly phase?: unknown }).phase
|
|
262
|
+
return phase === 'connect' ||
|
|
263
|
+
phase === 'send' ||
|
|
264
|
+
phase === 'receive' ||
|
|
265
|
+
phase === 'timeout' ||
|
|
266
|
+
phase === 'closed'
|
|
267
|
+
? phase
|
|
268
|
+
: undefined
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function transportDelivery(error: Error): string | undefined {
|
|
272
|
+
const delivery = (error as Error & { readonly delivery?: unknown }).delivery
|
|
273
|
+
return delivery === 'not-sent' || delivery === 'unknown' ? delivery : undefined
|
|
284
274
|
}
|
|
285
275
|
|
|
286
276
|
function writeRaw(payload: Record<string, unknown>): void {
|
|
@@ -289,7 +279,7 @@ function writeRaw(payload: Record<string, unknown>): void {
|
|
|
289
279
|
|
|
290
280
|
async function contextForError(error: unknown): Promise<LocalStatus | undefined> {
|
|
291
281
|
if (!(error instanceof Error)) return undefined
|
|
292
|
-
if (error.name !== '
|
|
282
|
+
if (error.name !== 'TransportError') return undefined
|
|
293
283
|
return readLocalStatus().catch(() => undefined)
|
|
294
284
|
}
|
|
295
285
|
|
|
@@ -156,6 +156,7 @@ async function exchange(
|
|
|
156
156
|
try {
|
|
157
157
|
admitted = exchangeProtocol.acceptErrorResponse(body)
|
|
158
158
|
} catch (cause) {
|
|
159
|
+
if (!(cause instanceof TypeError)) throw cause
|
|
159
160
|
throw new AstraleError(
|
|
160
161
|
'TOKEN_EXCHANGE_PROTOCOL_ERROR',
|
|
161
162
|
`Token exchange failed with HTTP ${response.status} and an invalid error response.`,
|
|
@@ -165,8 +166,19 @@ async function exchange(
|
|
|
165
166
|
throw new AstraleError(String(admitted.error.code), admitted.error.message)
|
|
166
167
|
}
|
|
167
168
|
requireExchangeResponseHeaders(response)
|
|
168
|
-
|
|
169
|
-
|
|
169
|
+
let exchanged: exchangeProtocol.Response
|
|
170
|
+
let inspected: ReturnType<typeof credential.inspect>
|
|
171
|
+
try {
|
|
172
|
+
exchanged = exchangeProtocol.acceptResponse(body)
|
|
173
|
+
inspected = credential.inspect(exchanged.token)
|
|
174
|
+
} catch (cause) {
|
|
175
|
+
if (!(cause instanceof TypeError)) throw cause
|
|
176
|
+
throw new AstraleError(
|
|
177
|
+
'TOKEN_EXCHANGE_PROTOCOL_ERROR',
|
|
178
|
+
'Token exchange returned an invalid success response.',
|
|
179
|
+
cause.message,
|
|
180
|
+
)
|
|
181
|
+
}
|
|
170
182
|
if (
|
|
171
183
|
inspected.iss !== domainIssuer ||
|
|
172
184
|
inspected.aud !== kernelIssuer ||
|
package/src/connection/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createPathCall } from './call'
|
|
2
2
|
export { runKernelCommand } from './command'
|
|
3
|
-
export type { KernelCommandOpts } from './command'
|
|
3
|
+
export type { KernelCommandOpts, OperationRecovery } from './command'
|
|
4
4
|
export { expandSelfInCall, expandSelfInPath, withSelfHint } from './self'
|
|
5
5
|
export type { SelfExpansionMeta } from './self'
|
|
6
6
|
export { withAdminClientSession, withClientSession, type ConnectionContext } from './session'
|