buckyos 0.7.115 → 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/README.md +8 -0
- package/cli/README.md +14 -10
- package/cli/core/app.ts +1 -0
- package/cli/core/argv.ts +14 -7
- 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 +7 -2
- package/cli/dist/cli.mjs +341 -233
- package/cli/dist/cli.mjs.map +1 -1
- package/cli/runtime/host.ts +10 -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/README.md
CHANGED
|
@@ -17,6 +17,14 @@ npx buckyos app status example
|
|
|
17
17
|
npx buckyos log tail --app example
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
当命令需要读取项目目录和 Tool 包目录之外的源码时,通过可重复的全局参数显式授权:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx buckyos --allow-read ../shared-assets --allow-read /opt/app-source pikg build ./dapp_meta
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`--allow-read` 只增加读权限,不会增加写权限。
|
|
27
|
+
|
|
20
28
|
`npx buckyos` 和 npm scripts 中的 `buckyos` 固定使用项目 lockfile 对应的 Tool。PATH 中裸 `buckyos`
|
|
21
29
|
由 BuckyOS 系统安装器管理并指向 `$BUCKYOS_ROOT/bin/buckyos`;文档不推荐全局
|
|
22
30
|
`npm install -g buckyos`。两种分发运行同一套命令实现,互不覆盖、互不调用、也不会自更新。
|
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/app.ts
CHANGED
|
@@ -487,6 +487,7 @@ function topLevelHelp(registry: CommandRegistry): string {
|
|
|
487
487
|
'',
|
|
488
488
|
'Global options:',
|
|
489
489
|
' --config-dir <path> --profile <name> --zone <host-or-did>',
|
|
490
|
+
' --allow-read <path> Add a readable filesystem root (repeatable)',
|
|
490
491
|
' --endpoint <url> --identity <did-or-name>',
|
|
491
492
|
' --session-token <token> | --session-token-file <path>',
|
|
492
493
|
' Prefer --session-token-file for automation; argv tokens may appear in process listings.',
|
package/cli/core/argv.ts
CHANGED
|
@@ -7,12 +7,11 @@ export type OutputFormat = typeof OUTPUT_FORMATS[number]
|
|
|
7
7
|
|
|
8
8
|
export interface GlobalOptions {
|
|
9
9
|
configDir?: string
|
|
10
|
+
allowRead?: string[]
|
|
10
11
|
profile?: string
|
|
11
12
|
zone?: string
|
|
12
13
|
endpoint?: string
|
|
13
14
|
identity?: string
|
|
14
|
-
identityRoot?: string
|
|
15
|
-
securityRoot?: string
|
|
16
15
|
sessionToken?: string
|
|
17
16
|
sessionTokenFile?: string
|
|
18
17
|
cli?: boolean
|
|
@@ -49,8 +48,6 @@ const STRING_GLOBALS = new Map<string, keyof GlobalOptions>([
|
|
|
49
48
|
['zone', 'zone'],
|
|
50
49
|
['endpoint', 'endpoint'],
|
|
51
50
|
['identity', 'identity'],
|
|
52
|
-
['identity-root', 'identityRoot'],
|
|
53
|
-
['security-root', 'securityRoot'],
|
|
54
51
|
['session-token', 'sessionToken'],
|
|
55
52
|
['session-token-file', 'sessionTokenFile'],
|
|
56
53
|
['output', 'output'],
|
|
@@ -60,6 +57,10 @@ const STRING_GLOBALS = new Map<string, keyof GlobalOptions>([
|
|
|
60
57
|
['idempotency-key', 'idempotencyKey'],
|
|
61
58
|
])
|
|
62
59
|
|
|
60
|
+
const REPEATABLE_STRING_GLOBALS = new Map<string, keyof GlobalOptions>([
|
|
61
|
+
['allow-read', 'allowRead'],
|
|
62
|
+
])
|
|
63
|
+
|
|
63
64
|
const BOOLEAN_GLOBALS = new Map<string, keyof GlobalOptions>([
|
|
64
65
|
['cli', 'cli'],
|
|
65
66
|
['wait', 'wait'],
|
|
@@ -106,11 +107,16 @@ export function parseInvocation(argv: string[]): Invocation {
|
|
|
106
107
|
continue
|
|
107
108
|
}
|
|
108
109
|
const stringProperty = STRING_GLOBALS.get(parsed.name)
|
|
109
|
-
|
|
110
|
+
const repeatableStringProperty = REPEATABLE_STRING_GLOBALS.get(parsed.name)
|
|
111
|
+
if (!stringProperty && !repeatableStringProperty) {
|
|
110
112
|
throw new UsageError('UNKNOWN_OPTION', `unknown global option: --${parsed.name}`)
|
|
111
113
|
}
|
|
112
114
|
const { value, consumed } = readOptionValue(argv, index, parsed)
|
|
113
|
-
|
|
115
|
+
if (repeatableStringProperty === 'allowRead') {
|
|
116
|
+
global.allowRead = [...(global.allowRead ?? []), value]
|
|
117
|
+
} else {
|
|
118
|
+
setGlobal(global, stringProperty!, normalizeGlobalValue(parsed.name, value))
|
|
119
|
+
}
|
|
114
120
|
index += consumed
|
|
115
121
|
}
|
|
116
122
|
|
|
@@ -183,7 +189,8 @@ export function parseCommandArgs(
|
|
|
183
189
|
}
|
|
184
190
|
if (
|
|
185
191
|
allowReplScopedGlobals &&
|
|
186
|
-
(STRING_GLOBALS.has(parsed.name) ||
|
|
192
|
+
(STRING_GLOBALS.has(parsed.name) || REPEATABLE_STRING_GLOBALS.has(parsed.name) ||
|
|
193
|
+
BOOLEAN_GLOBALS.has(parsed.name))
|
|
187
194
|
) {
|
|
188
195
|
throw new UsageError(
|
|
189
196
|
'SESSION_OPTION_FROZEN',
|
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
|
@@ -6,12 +6,17 @@ const KEBAB_CASE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/
|
|
|
6
6
|
|
|
7
7
|
const GLOBAL_OPTIONS = [
|
|
8
8
|
{ name: 'config-dir', property: 'configDir', type: 'string', scope: 'process' },
|
|
9
|
+
{
|
|
10
|
+
name: 'allow-read',
|
|
11
|
+
property: 'allowRead',
|
|
12
|
+
type: 'string',
|
|
13
|
+
scope: 'process',
|
|
14
|
+
repeatable: true,
|
|
15
|
+
},
|
|
9
16
|
{ name: 'profile', property: 'profile', type: 'string', scope: 'session' },
|
|
10
17
|
{ name: 'zone', property: 'zone', type: 'string', scope: 'session' },
|
|
11
18
|
{ name: 'endpoint', property: 'endpoint', type: 'string', scope: 'session' },
|
|
12
19
|
{ name: 'identity', property: 'identity', type: 'string', scope: 'session' },
|
|
13
|
-
{ name: 'identity-root', property: 'identityRoot', type: 'string', scope: 'session' },
|
|
14
|
-
{ name: 'security-root', property: 'securityRoot', type: 'string', scope: 'session' },
|
|
15
20
|
{
|
|
16
21
|
name: 'session-token',
|
|
17
22
|
property: 'sessionToken',
|