buckyos 0.7.116 → 0.7.118
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/cli/README.md +14 -10
- package/cli/core/argv.ts +0 -4
- package/cli/core/auth.ts +104 -20
- package/cli/core/config.ts +0 -38
- package/cli/core/development_mode.ts +103 -0
- package/cli/core/identity.ts +39 -31
- package/cli/core/registry.ts +0 -2
- package/cli/dist/cli.mjs +320 -230
- package/cli/dist/cli.mjs.map +1 -1
- package/cli/runtime/host.ts +8 -10
- package/cli/version.ts +1 -1
- package/dist/browser.cjs +1 -1
- package/dist/browser.mjs +58 -57
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +58 -57
- package/dist/ndm_proxy-20e0a471.js +13 -0
- package/dist/ndm_proxy-20e0a471.js.map +1 -0
- package/dist/{ndm_proxy-a719bc58.mjs → ndm_proxy-6cb0701b.mjs} +62 -56
- package/dist/ndm_proxy-6cb0701b.mjs.map +1 -0
- package/dist/node.cjs +1 -1
- package/dist/node.mjs +58 -57
- package/dist/task_mgr_client.d.ts +10 -0
- package/dist/task_mgr_client.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/ndm_proxy-36879436.js +0 -13
- package/dist/ndm_proxy-36879436.js.map +0 -1
- package/dist/ndm_proxy-a719bc58.mjs.map +0 -1
package/cli/README.md
CHANGED
|
@@ -44,16 +44,20 @@ build scripts. Set `SOURCE_DATE_EPOCH` for byte-reproducible Node/Deno PIKG buil
|
|
|
44
44
|
|
|
45
45
|
## Policy and identity
|
|
46
46
|
|
|
47
|
-
The developer policy grants the package root, cwd, Tool config,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
Docker inputs may invoke
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
The developer policy grants the package root, cwd, Tool config, the read-only `~/.buckyos` and
|
|
48
|
+
`~/.buckycli` roots, and explicit non-identity input/output paths. It does not include
|
|
49
|
+
`$BUCKYOS_ROOT` or scan system credentials. The system policy additionally grants its paired BuckyOS
|
|
50
|
+
root. Both policies constrain environment names and subprocesses; only PIKG Docker inputs may invoke
|
|
51
|
+
`docker`.
|
|
52
|
+
|
|
53
|
+
`~/.buckyos` is the regular operations identity root and is always eligible. Before touching the
|
|
54
|
+
developer-only `~/.buckycli` root, the Tool reads `system.dev_mode.get` from the target Zone and
|
|
55
|
+
requires a valid `BuckyOSDevConfig` with `enabled: true`. Automatic discovery tries `~/.buckyos`
|
|
56
|
+
first and consults developer mode only if no operations identity succeeds; the combined search is
|
|
57
|
+
limited to eight usable candidates. An explicitly selected identity is tried once. Rotation occurs
|
|
58
|
+
only during session creation and only for
|
|
59
|
+
`IDENTITY_KIND_NOT_ACCEPTED` or `AUTHENTICATION_REJECTED`; timeouts, network errors, capability
|
|
60
|
+
mismatches and RBAC denial never rotate. Private keys and tokens are never shown.
|
|
57
61
|
|
|
58
62
|
## Verification
|
|
59
63
|
|
package/cli/core/argv.ts
CHANGED
|
@@ -12,8 +12,6 @@ export interface GlobalOptions {
|
|
|
12
12
|
zone?: string
|
|
13
13
|
endpoint?: string
|
|
14
14
|
identity?: string
|
|
15
|
-
identityRoot?: string
|
|
16
|
-
securityRoot?: string
|
|
17
15
|
sessionToken?: string
|
|
18
16
|
sessionTokenFile?: string
|
|
19
17
|
cli?: boolean
|
|
@@ -50,8 +48,6 @@ const STRING_GLOBALS = new Map<string, keyof GlobalOptions>([
|
|
|
50
48
|
['zone', 'zone'],
|
|
51
49
|
['endpoint', 'endpoint'],
|
|
52
50
|
['identity', 'identity'],
|
|
53
|
-
['identity-root', 'identityRoot'],
|
|
54
|
-
['security-root', 'securityRoot'],
|
|
55
51
|
['session-token', 'sessionToken'],
|
|
56
52
|
['session-token-file', 'sessionTokenFile'],
|
|
57
53
|
['output', 'output'],
|
package/cli/core/auth.ts
CHANGED
|
@@ -5,9 +5,16 @@ import {
|
|
|
5
5
|
VerifyHubClient,
|
|
6
6
|
} from 'buckyos/node'
|
|
7
7
|
import { type Environment, readEnvironment, type ResolvedConfig } from './config.ts'
|
|
8
|
+
import {
|
|
9
|
+
type DevelopmentModeDecision,
|
|
10
|
+
type DevelopmentModeReader,
|
|
11
|
+
readTargetDevelopmentMode,
|
|
12
|
+
} from './development_mode.ts'
|
|
8
13
|
import { EXIT_AUTH, sanitizeMessage, ToolError } from './errors.ts'
|
|
9
14
|
import {
|
|
15
|
+
IDENTITY_CANDIDATE_LIMIT,
|
|
10
16
|
IDENTITY_REJECTION_CODES,
|
|
17
|
+
type IdentityCandidate,
|
|
11
18
|
type IdentityMaterial,
|
|
12
19
|
resolveIdentityMaterial,
|
|
13
20
|
scanIdentityCandidates,
|
|
@@ -57,6 +64,7 @@ export interface AuthenticationDependencies {
|
|
|
57
64
|
transport?: AuthenticationTransport
|
|
58
65
|
readPassword?: (prompt: string) => Promise<string>
|
|
59
66
|
readUsername?: (prompt: string) => Promise<string>
|
|
67
|
+
readDevelopmentMode?: DevelopmentModeReader
|
|
60
68
|
now?: () => number
|
|
61
69
|
}
|
|
62
70
|
|
|
@@ -78,9 +86,11 @@ export class AuthenticationSession implements SessionController {
|
|
|
78
86
|
readonly #transport: AuthenticationTransport
|
|
79
87
|
readonly #readPassword: (prompt: string) => Promise<string>
|
|
80
88
|
readonly #readUsername: (prompt: string) => Promise<string>
|
|
89
|
+
readonly #readDevelopmentMode: DevelopmentModeReader
|
|
81
90
|
readonly #now: () => number
|
|
82
91
|
#session?: AuthenticatedSession
|
|
83
92
|
#acceptedIdentity?: IdentityMaterial
|
|
93
|
+
#developmentMode?: DevelopmentModeDecision
|
|
84
94
|
#identityAttempts: Array<{ identity: string; code: string; message: string }> = []
|
|
85
95
|
|
|
86
96
|
constructor(
|
|
@@ -93,6 +103,7 @@ export class AuthenticationSession implements SessionController {
|
|
|
93
103
|
this.#transport = dependencies.transport ?? new SdkAuthenticationTransport()
|
|
94
104
|
this.#readPassword = dependencies.readPassword ?? readSecret
|
|
95
105
|
this.#readUsername = dependencies.readUsername ?? readVisible
|
|
106
|
+
this.#readDevelopmentMode = dependencies.readDevelopmentMode ?? readTargetDevelopmentMode
|
|
96
107
|
this.#now = dependencies.now ?? Date.now
|
|
97
108
|
}
|
|
98
109
|
|
|
@@ -146,6 +157,7 @@ export class AuthenticationSession implements SessionController {
|
|
|
146
157
|
renewable: session.renewable,
|
|
147
158
|
expires_at: expiresAt ?? null,
|
|
148
159
|
remaining_seconds: remainingSeconds,
|
|
160
|
+
developer_mode: this.#developmentMode?.state ?? null,
|
|
149
161
|
identity_attempts: this.#identityAttempts,
|
|
150
162
|
}
|
|
151
163
|
}
|
|
@@ -180,34 +192,43 @@ export class AuthenticationSession implements SessionController {
|
|
|
180
192
|
const injected = this.#environment.BUCKYOS_APPCLIENT_SESSION_TOKEN?.trim()
|
|
181
193
|
if (injected) return externalSession(injected, 'environment', this.#now())
|
|
182
194
|
|
|
183
|
-
if (this.#acceptedIdentity)
|
|
195
|
+
if (this.#acceptedIdentity) {
|
|
196
|
+
if (this.#acceptedIdentity.source === 'buckycli-home') {
|
|
197
|
+
await this.#requireDeveloperIdentityAccess()
|
|
198
|
+
}
|
|
199
|
+
return await this.#loginWithIdentity(this.#acceptedIdentity)
|
|
200
|
+
}
|
|
184
201
|
|
|
185
202
|
if (this.config.identity) {
|
|
186
|
-
const material = await
|
|
187
|
-
this.config.identity,
|
|
188
|
-
this.config,
|
|
189
|
-
this.#environment,
|
|
190
|
-
)
|
|
203
|
+
const material = await this.#resolveExplicitIdentity(this.config.identity)
|
|
191
204
|
const session = await this.#loginWithIdentity(material)
|
|
192
205
|
this.#acceptedIdentity = material
|
|
193
206
|
return session
|
|
194
207
|
}
|
|
195
208
|
|
|
196
|
-
const candidates = await scanIdentityCandidates(this.config, this.#environment)
|
|
197
209
|
this.#identityAttempts = []
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
210
|
+
const operations = await scanIdentityCandidates(this.config, this.#environment)
|
|
211
|
+
const operationsSession = await this.#tryIdentityCandidates(operations.candidates)
|
|
212
|
+
if (operationsSession) return operationsSession
|
|
213
|
+
|
|
214
|
+
const developmentMode = await this.#checkDevelopmentMode()
|
|
215
|
+
if (
|
|
216
|
+
developmentMode.state === 'enabled' &&
|
|
217
|
+
operations.candidates.length < IDENTITY_CANDIDATE_LIMIT
|
|
218
|
+
) {
|
|
219
|
+
const operationsDids = new Set(
|
|
220
|
+
operations.candidates.map((candidate) => candidate.material.did),
|
|
221
|
+
)
|
|
222
|
+
const developer = await scanIdentityCandidates(this.config, this.#environment, {
|
|
223
|
+
includeOperations: false,
|
|
224
|
+
includeDeveloper: true,
|
|
225
|
+
})
|
|
226
|
+
const remaining = IDENTITY_CANDIDATE_LIMIT - operations.candidates.length
|
|
227
|
+
const candidates = developer.candidates.filter((candidate) =>
|
|
228
|
+
!operationsDids.has(candidate.material.did)
|
|
229
|
+
).slice(0, remaining)
|
|
230
|
+
const developerSession = await this.#tryIdentityCandidates(candidates)
|
|
231
|
+
if (developerSession) return developerSession
|
|
211
232
|
}
|
|
212
233
|
if (this.#identityAttempts.length > 0) {
|
|
213
234
|
throw new ToolError(
|
|
@@ -241,6 +262,44 @@ export class AuthenticationSession implements SessionController {
|
|
|
241
262
|
return authenticatedSession(token, 'password', true, this.#now())
|
|
242
263
|
}
|
|
243
264
|
|
|
265
|
+
async #resolveExplicitIdentity(selectedIdentity: string): Promise<IdentityMaterial> {
|
|
266
|
+
if (this.config.implicitDeviceIdentity) {
|
|
267
|
+
return await resolveIdentityMaterial(selectedIdentity, this.config, this.#environment)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
return await resolveIdentityMaterial(selectedIdentity, this.config, this.#environment)
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (!(error instanceof ToolError) || error.code !== 'IDENTITY_NOT_FOUND') throw error
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
await this.#requireDeveloperIdentityAccess()
|
|
277
|
+
return await resolveIdentityMaterial(selectedIdentity, this.config, this.#environment, {
|
|
278
|
+
includeOperations: false,
|
|
279
|
+
includeDeveloper: true,
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async #tryIdentityCandidates(
|
|
284
|
+
candidates: readonly IdentityCandidate[],
|
|
285
|
+
): Promise<AuthenticatedSession | undefined> {
|
|
286
|
+
for (const candidate of candidates) {
|
|
287
|
+
try {
|
|
288
|
+
const session = await this.#loginWithIdentity(candidate.material)
|
|
289
|
+
this.#acceptedIdentity = candidate.material
|
|
290
|
+
return session
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (!isIdentityRejection(error)) throw error
|
|
293
|
+
this.#identityAttempts.push({
|
|
294
|
+
identity: candidate.material.did,
|
|
295
|
+
code: error.code,
|
|
296
|
+
message: sanitizeMessage(error.message),
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return undefined
|
|
301
|
+
}
|
|
302
|
+
|
|
244
303
|
async #loginWithIdentity(material: IdentityMaterial): Promise<AuthenticatedSession> {
|
|
245
304
|
const loginJwt = await createLoginJwt(
|
|
246
305
|
material.subject,
|
|
@@ -256,6 +315,31 @@ export class AuthenticationSession implements SessionController {
|
|
|
256
315
|
)
|
|
257
316
|
return authenticatedSession(token, 'identity', true, this.#now())
|
|
258
317
|
}
|
|
318
|
+
|
|
319
|
+
async #checkDevelopmentMode(): Promise<DevelopmentModeDecision> {
|
|
320
|
+
this.#developmentMode = await this.#readDevelopmentMode(this.config)
|
|
321
|
+
return this.#developmentMode
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async #requireDeveloperIdentityAccess(): Promise<void> {
|
|
325
|
+
const decision = await this.#checkDevelopmentMode()
|
|
326
|
+
if (decision.state === 'enabled') return
|
|
327
|
+
this.#acceptedIdentity = undefined
|
|
328
|
+
if (decision.state === 'disabled') {
|
|
329
|
+
throw new ToolError(
|
|
330
|
+
'DEVELOPER_IDENTITY_DISABLED',
|
|
331
|
+
'the target Zone has developer mode disabled; local developer identities are unavailable',
|
|
332
|
+
EXIT_AUTH,
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
throw new ToolError(
|
|
336
|
+
'DEVELOPER_MODE_UNAVAILABLE',
|
|
337
|
+
'the target Zone developer-mode configuration could not be verified',
|
|
338
|
+
EXIT_AUTH,
|
|
339
|
+
true,
|
|
340
|
+
{ reason: sanitizeMessage(decision.reason) },
|
|
341
|
+
)
|
|
342
|
+
}
|
|
259
343
|
}
|
|
260
344
|
|
|
261
345
|
function isIdentityRejection(error: unknown): error is ToolError {
|
package/cli/core/config.ts
CHANGED
|
@@ -69,8 +69,6 @@ export const ENVIRONMENT_NAMES = [
|
|
|
69
69
|
'BUCKYOS_TOOL_ENDPOINT',
|
|
70
70
|
'BUCKYOS_TOOL_IDENTITY',
|
|
71
71
|
'BUCKYOS_TOOL_OUTPUT',
|
|
72
|
-
'BUCKYOS_IDENTITY_ROOT',
|
|
73
|
-
'BUCKYOS_SECURITY_ROOT',
|
|
74
72
|
'BUCKYOS_APPCLIENT_SESSION_TOKEN',
|
|
75
73
|
'BUCKYOS_ROOT',
|
|
76
74
|
] as const
|
|
@@ -229,38 +227,6 @@ export async function resolveConfig(
|
|
|
229
227
|
undefined,
|
|
230
228
|
sources,
|
|
231
229
|
)
|
|
232
|
-
const identityRoot = select(
|
|
233
|
-
'identity_root',
|
|
234
|
-
explicit.identityRoot,
|
|
235
|
-
environment.BUCKYOS_IDENTITY_ROOT,
|
|
236
|
-
undefined,
|
|
237
|
-
undefined,
|
|
238
|
-
sources,
|
|
239
|
-
)
|
|
240
|
-
const securityRoot = select(
|
|
241
|
-
'security_root',
|
|
242
|
-
explicit.securityRoot,
|
|
243
|
-
environment.BUCKYOS_SECURITY_ROOT,
|
|
244
|
-
undefined,
|
|
245
|
-
undefined,
|
|
246
|
-
sources,
|
|
247
|
-
)
|
|
248
|
-
if (!!explicit.identityRoot !== !!explicit.securityRoot) {
|
|
249
|
-
throw new UsageError(
|
|
250
|
-
'IDENTITY_ROOT_PAIR_REQUIRED',
|
|
251
|
-
'--identity-root and --security-root must be provided together',
|
|
252
|
-
)
|
|
253
|
-
}
|
|
254
|
-
if (
|
|
255
|
-
!!environment.BUCKYOS_IDENTITY_ROOT !== !!environment.BUCKYOS_SECURITY_ROOT &&
|
|
256
|
-
!explicit.identityRoot
|
|
257
|
-
) {
|
|
258
|
-
throw new UsageError(
|
|
259
|
-
'IDENTITY_ROOT_PAIR_REQUIRED',
|
|
260
|
-
'BUCKYOS_IDENTITY_ROOT and BUCKYOS_SECURITY_ROOT must be provided together',
|
|
261
|
-
)
|
|
262
|
-
}
|
|
263
|
-
|
|
264
230
|
let output: OutputFormat
|
|
265
231
|
if (options.interactive && explicit.output === undefined) {
|
|
266
232
|
output = 'table'
|
|
@@ -297,8 +263,6 @@ export async function resolveConfig(
|
|
|
297
263
|
zone,
|
|
298
264
|
endpoint,
|
|
299
265
|
identity,
|
|
300
|
-
identityRoot,
|
|
301
|
-
securityRoot,
|
|
302
266
|
sessionToken,
|
|
303
267
|
sessionTokenFile: explicit.sessionTokenFile,
|
|
304
268
|
output,
|
|
@@ -347,8 +311,6 @@ export function localResolvedConfig(
|
|
|
347
311
|
zone: explicit.zone ?? environment.BUCKYOS_TOOL_ZONE,
|
|
348
312
|
endpoint: explicit.endpoint ?? environment.BUCKYOS_TOOL_ENDPOINT,
|
|
349
313
|
identity: explicit.identity ?? environment.BUCKYOS_TOOL_IDENTITY,
|
|
350
|
-
identityRoot: explicit.identityRoot ?? environment.BUCKYOS_IDENTITY_ROOT,
|
|
351
|
-
securityRoot: explicit.securityRoot ?? environment.BUCKYOS_SECURITY_ROOT,
|
|
352
314
|
sessionToken: explicit.sessionToken ?? environment.BUCKYOS_APPCLIENT_SESSION_TOKEN,
|
|
353
315
|
sessionTokenFile: explicit.sessionTokenFile,
|
|
354
316
|
output,
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { buckyos } from 'buckyos/node'
|
|
2
|
+
import type { ResolvedConfig } from './config.ts'
|
|
3
|
+
import { resolveServiceUrl } from './runtime.ts'
|
|
4
|
+
|
|
5
|
+
export const BUCKYOS_DEV_CONFIG_KEY = 'system/buckyos_dev_config'
|
|
6
|
+
export const BUCKYOS_DEV_CONFIG_SCHEMA_VERSION = 1
|
|
7
|
+
|
|
8
|
+
export interface BuckyOSDevConfig {
|
|
9
|
+
schema_version: 1
|
|
10
|
+
enabled: boolean
|
|
11
|
+
enabled_at: number | null
|
|
12
|
+
enabled_by: string | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type DevelopmentModeDecision =
|
|
16
|
+
| { state: 'enabled'; config: BuckyOSDevConfig }
|
|
17
|
+
| { state: 'disabled'; config: BuckyOSDevConfig }
|
|
18
|
+
| { state: 'unavailable'; reason: string }
|
|
19
|
+
|
|
20
|
+
export type DevelopmentModeReader = (
|
|
21
|
+
config: ResolvedConfig,
|
|
22
|
+
) => Promise<DevelopmentModeDecision>
|
|
23
|
+
|
|
24
|
+
interface PublicRpcClient {
|
|
25
|
+
call<T>(method: string, params: Record<string, unknown>): Promise<T>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read the target Zone's public developer-mode setting.
|
|
30
|
+
*
|
|
31
|
+
* This deliberately uses control-panel's unauthenticated `system.dev_mode.get`
|
|
32
|
+
* endpoint: developer mode must be known before a local identity is touched or
|
|
33
|
+
* exchanged at verify-hub. Every error is converted to `unavailable`, which is
|
|
34
|
+
* a fail-closed result for the identity resolver.
|
|
35
|
+
*/
|
|
36
|
+
export async function readTargetDevelopmentMode(
|
|
37
|
+
config: ResolvedConfig,
|
|
38
|
+
): Promise<DevelopmentModeDecision> {
|
|
39
|
+
try {
|
|
40
|
+
const url = resolveServiceUrl(config, 'control-panel')
|
|
41
|
+
const client = new buckyos.kRPCClient(url) as unknown as PublicRpcClient
|
|
42
|
+
const value = await withTimeout(
|
|
43
|
+
client.call<unknown>('system.dev_mode.get', {}),
|
|
44
|
+
config.timeoutMs,
|
|
45
|
+
)
|
|
46
|
+
const parsed = parseBuckyOSDevConfig(value)
|
|
47
|
+
return parsed.enabled
|
|
48
|
+
? { state: 'enabled', config: parsed }
|
|
49
|
+
: { state: 'disabled', config: parsed }
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return {
|
|
52
|
+
state: 'unavailable',
|
|
53
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function parseBuckyOSDevConfig(value: unknown): BuckyOSDevConfig {
|
|
59
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
60
|
+
throw new Error('BuckyOSDevConfig must be an object')
|
|
61
|
+
}
|
|
62
|
+
const config = value as Record<string, unknown>
|
|
63
|
+
const keys = Object.keys(config).sort()
|
|
64
|
+
const expectedKeys = ['enabled', 'enabled_at', 'enabled_by', 'schema_version']
|
|
65
|
+
if (JSON.stringify(keys) !== JSON.stringify(expectedKeys)) {
|
|
66
|
+
throw new Error('BuckyOSDevConfig contains missing or unsupported fields')
|
|
67
|
+
}
|
|
68
|
+
if (config.schema_version !== BUCKYOS_DEV_CONFIG_SCHEMA_VERSION) {
|
|
69
|
+
throw new Error(`unsupported BuckyOSDevConfig schema_version ${String(config.schema_version)}`)
|
|
70
|
+
}
|
|
71
|
+
if (typeof config.enabled !== 'boolean') {
|
|
72
|
+
throw new Error('BuckyOSDevConfig enabled must be a boolean')
|
|
73
|
+
}
|
|
74
|
+
const enabledAt = config.enabled_at
|
|
75
|
+
const enabledBy = config.enabled_by
|
|
76
|
+
const hasAuditFields = Number.isSafeInteger(enabledAt) && (enabledAt as number) > 0 &&
|
|
77
|
+
typeof enabledBy === 'string' && enabledBy.trim().length > 0
|
|
78
|
+
const hasNoAuditFields = enabledAt === null && enabledBy === null
|
|
79
|
+
if (!hasAuditFields && !hasNoAuditFields) {
|
|
80
|
+
throw new Error('BuckyOSDevConfig enabled_at and enabled_by must be valid and present together')
|
|
81
|
+
}
|
|
82
|
+
if (config.enabled && !hasAuditFields) {
|
|
83
|
+
throw new Error('enabled BuckyOSDevConfig requires enable audit fields')
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
schema_version: 1,
|
|
87
|
+
enabled: config.enabled,
|
|
88
|
+
enabled_at: enabledAt as number | null,
|
|
89
|
+
enabled_by: enabledBy as string | null,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
|
|
94
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
95
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
96
|
+
timer = setTimeout(() => reject(new Error('developer-mode lookup timed out')), timeoutMs)
|
|
97
|
+
})
|
|
98
|
+
try {
|
|
99
|
+
return await Promise.race([promise, timeout])
|
|
100
|
+
} finally {
|
|
101
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
102
|
+
}
|
|
103
|
+
}
|
package/cli/core/identity.ts
CHANGED
|
@@ -17,12 +17,18 @@ export interface IdentityMaterial {
|
|
|
17
17
|
documentPath: string
|
|
18
18
|
privateKeyPath: string
|
|
19
19
|
privateKeyPem: string
|
|
20
|
+
source: IdentityRootPair['source']
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export interface IdentityRootPair {
|
|
23
24
|
publicRoot: string
|
|
24
25
|
securityRoot: string
|
|
25
|
-
source: '
|
|
26
|
+
source: 'buckyos-home' | 'buckycli-home' | 'current-device'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface IdentityRootOptions {
|
|
30
|
+
includeOperations?: boolean
|
|
31
|
+
includeDeveloper?: boolean
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
export const IDENTITY_CANDIDATE_LIMIT = 8
|
|
@@ -137,37 +143,31 @@ export async function readCurrentDeviceIdentity(
|
|
|
137
143
|
export function identityRootPairs(
|
|
138
144
|
config: ResolvedConfig,
|
|
139
145
|
environment: Environment = readEnvironment(),
|
|
146
|
+
options: IdentityRootOptions = {},
|
|
140
147
|
): IdentityRootPair[] {
|
|
148
|
+
const device = config.implicitDeviceIdentity
|
|
149
|
+
if (device) {
|
|
150
|
+
return [{
|
|
151
|
+
publicRoot: device.publicRoot,
|
|
152
|
+
securityRoot: device.securityRoot,
|
|
153
|
+
source: 'current-device',
|
|
154
|
+
}]
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const home = environment.HOME ?? environment.USERPROFILE ?? getHost().homeDir()
|
|
141
158
|
const pairs: IdentityRootPair[] = []
|
|
142
|
-
if (
|
|
159
|
+
if (options.includeOperations ?? true) {
|
|
143
160
|
pairs.push({
|
|
144
|
-
publicRoot:
|
|
145
|
-
securityRoot:
|
|
146
|
-
source: '
|
|
161
|
+
publicRoot: join(home, '.buckyos', 'local', 'identity'),
|
|
162
|
+
securityRoot: join(home, '.buckyos', 'security'),
|
|
163
|
+
source: 'buckyos-home',
|
|
147
164
|
})
|
|
148
165
|
}
|
|
149
|
-
|
|
150
|
-
publicRoot: join(config.configDir, 'local', 'identity'),
|
|
151
|
-
securityRoot: join(config.configDir, 'security'),
|
|
152
|
-
source: 'tool',
|
|
153
|
-
})
|
|
154
|
-
if (environment.BUCKYOS_IDENTITY_ROOT && environment.BUCKYOS_SECURITY_ROOT) {
|
|
155
|
-
const duplicate = config.identityRoot === environment.BUCKYOS_IDENTITY_ROOT &&
|
|
156
|
-
config.securityRoot === environment.BUCKYOS_SECURITY_ROOT
|
|
157
|
-
if (!duplicate) {
|
|
158
|
-
pairs.push({
|
|
159
|
-
publicRoot: environment.BUCKYOS_IDENTITY_ROOT,
|
|
160
|
-
securityRoot: environment.BUCKYOS_SECURITY_ROOT,
|
|
161
|
-
source: 'environment',
|
|
162
|
-
})
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
if (getHost().policy.distribution === 'system') {
|
|
166
|
-
const buckyosRoot = environment.BUCKYOS_ROOT ?? defaultBuckyOSRoot(environment)
|
|
166
|
+
if (options.includeDeveloper ?? false) {
|
|
167
167
|
pairs.push({
|
|
168
|
-
publicRoot: join(
|
|
169
|
-
securityRoot: join(
|
|
170
|
-
source: '
|
|
168
|
+
publicRoot: join(home, '.buckycli', 'local', 'identity'),
|
|
169
|
+
securityRoot: join(home, '.buckycli', 'security'),
|
|
170
|
+
source: 'buckycli-home',
|
|
171
171
|
})
|
|
172
172
|
}
|
|
173
173
|
return deduplicatePairs(pairs)
|
|
@@ -177,11 +177,12 @@ export async function resolveIdentityMaterial(
|
|
|
177
177
|
selectedIdentity: string,
|
|
178
178
|
config: ResolvedConfig,
|
|
179
179
|
environment: Environment = readEnvironment(),
|
|
180
|
+
options: IdentityRootOptions = {},
|
|
180
181
|
): Promise<IdentityMaterial> {
|
|
181
182
|
const selected = selectedIdentity.trim()
|
|
182
183
|
if (!selected) throw new UsageError('IDENTITY_REQUIRED', 'identity is empty')
|
|
183
184
|
|
|
184
|
-
for (const roots of identityRootPairs(config, environment)) {
|
|
185
|
+
for (const roots of identityRootPairs(config, environment, options)) {
|
|
185
186
|
for (const directory of await candidateDirectories(roots.publicRoot, selected)) {
|
|
186
187
|
const material = await loadIdentityMaterial(roots, directory, selected)
|
|
187
188
|
if (material) return material
|
|
@@ -198,11 +199,12 @@ export async function resolveIdentityMaterial(
|
|
|
198
199
|
export async function scanIdentityCandidates(
|
|
199
200
|
config: ResolvedConfig,
|
|
200
201
|
environment: Environment = readEnvironment(),
|
|
202
|
+
options: IdentityRootOptions = {},
|
|
201
203
|
): Promise<IdentityCandidateScan> {
|
|
202
204
|
const candidates: IdentityCandidate[] = []
|
|
203
205
|
const skipped: IdentityCandidateScan['skipped'] = []
|
|
204
206
|
const seen = new Set<string>()
|
|
205
|
-
const roots = identityRootPairs(config, environment)
|
|
207
|
+
const roots = identityRootPairs(config, environment, options)
|
|
206
208
|
for (const pair of roots) {
|
|
207
209
|
let directories: string[]
|
|
208
210
|
try {
|
|
@@ -256,7 +258,7 @@ export async function scanIdentityCandidates(
|
|
|
256
258
|
if (candidates.length >= IDENTITY_CANDIDATE_LIMIT) break
|
|
257
259
|
}
|
|
258
260
|
return {
|
|
259
|
-
order: Object.freeze(
|
|
261
|
+
order: Object.freeze(roots.map((pair) => pair.source)),
|
|
260
262
|
limit: IDENTITY_CANDIDATE_LIMIT,
|
|
261
263
|
candidates,
|
|
262
264
|
skipped,
|
|
@@ -266,10 +268,13 @@ export async function scanIdentityCandidates(
|
|
|
266
268
|
export async function identityCandidateView(
|
|
267
269
|
config: ResolvedConfig,
|
|
268
270
|
environment: Environment = readEnvironment(),
|
|
271
|
+
developerModeEnabled = false,
|
|
269
272
|
): Promise<Record<string, unknown>> {
|
|
270
273
|
if (config.identity) {
|
|
271
274
|
try {
|
|
272
|
-
const material = await resolveIdentityMaterial(config.identity, config, environment
|
|
275
|
+
const material = await resolveIdentityMaterial(config.identity, config, environment, {
|
|
276
|
+
includeDeveloper: developerModeEnabled,
|
|
277
|
+
})
|
|
273
278
|
return {
|
|
274
279
|
mode: 'explicit',
|
|
275
280
|
order: [material.did],
|
|
@@ -294,7 +299,9 @@ export async function identityCandidateView(
|
|
|
294
299
|
}
|
|
295
300
|
}
|
|
296
301
|
}
|
|
297
|
-
const scan = await scanIdentityCandidates(config, environment
|
|
302
|
+
const scan = await scanIdentityCandidates(config, environment, {
|
|
303
|
+
includeDeveloper: developerModeEnabled,
|
|
304
|
+
})
|
|
298
305
|
return {
|
|
299
306
|
mode: 'search',
|
|
300
307
|
order: scan.order,
|
|
@@ -336,6 +343,7 @@ async function loadIdentityMaterial(
|
|
|
336
343
|
documentPath,
|
|
337
344
|
privateKeyPath,
|
|
338
345
|
privateKeyPem,
|
|
346
|
+
source: roots.source,
|
|
339
347
|
}
|
|
340
348
|
} catch (error) {
|
|
341
349
|
if (!isHostError(error, 'NotFound')) throw error
|
package/cli/core/registry.ts
CHANGED
|
@@ -17,8 +17,6 @@ const GLOBAL_OPTIONS = [
|
|
|
17
17
|
{ name: 'zone', property: 'zone', type: 'string', scope: 'session' },
|
|
18
18
|
{ name: 'endpoint', property: 'endpoint', type: 'string', scope: 'session' },
|
|
19
19
|
{ name: 'identity', property: 'identity', type: 'string', scope: 'session' },
|
|
20
|
-
{ name: 'identity-root', property: 'identityRoot', type: 'string', scope: 'session' },
|
|
21
|
-
{ name: 'security-root', property: 'securityRoot', type: 'string', scope: 'session' },
|
|
22
20
|
{
|
|
23
21
|
name: 'session-token',
|
|
24
22
|
property: 'sessionToken',
|