buckyos 0.7.119 → 0.7.121
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 +24 -3
- package/cli/core/app.ts +28 -0
- package/cli/deno.json +2 -1
- package/cli/dist/cli.mjs +777 -169
- package/cli/dist/cli.mjs.map +1 -1
- package/cli/modules/app.ts +213 -94
- package/cli/modules/provision.ts +690 -0
- package/cli/runtime/host.ts +25 -2
- package/cli/runtime/host_deno.ts +4 -0
- package/cli/runtime/host_node.ts +4 -0
- package/cli/version.ts +1 -1
- package/dist/browser.cjs +1 -1
- package/dist/browser.mjs +124 -124
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +124 -124
- package/dist/{ndm_proxy-c00bc561.mjs → ndm_proxy-8f547002.mjs} +113 -216
- package/dist/ndm_proxy-8f547002.mjs.map +1 -0
- package/dist/ndm_proxy-93a8869b.js +13 -0
- package/dist/ndm_proxy-93a8869b.js.map +1 -0
- package/dist/ndn_types-44c69036.js +2 -0
- package/dist/ndn_types-44c69036.js.map +1 -0
- package/dist/{ndn_types-f6c08d20.mjs → ndn_types-d768245f.mjs} +149 -39
- package/dist/ndn_types-d768245f.mjs.map +1 -0
- package/dist/node.cjs +1 -1
- package/dist/node.mjs +124 -124
- package/dist/provision.cjs +6 -6
- package/dist/provision.cjs.map +1 -1
- package/dist/provision.d.ts +2 -0
- package/dist/provision.d.ts.map +1 -1
- package/dist/provision.mjs +1084 -35
- package/dist/provision.mjs.map +1 -1
- package/dist/provision_activate.d.ts +146 -0
- package/dist/provision_activate.d.ts.map +1 -0
- package/dist/{tus_client-573066b3.mjs → tus_client-b1c35e8c.mjs} +2 -2
- package/dist/{tus_client-573066b3.mjs.map → tus_client-b1c35e8c.mjs.map} +1 -1
- package/dist/{tus_client-83ad0f4f.js → tus_client-d0798623.js} +2 -2
- package/dist/{tus_client-83ad0f4f.js.map → tus_client-d0798623.js.map} +1 -1
- package/package.json +1 -1
- package/dist/ndm_proxy-68354a89.js +0 -13
- package/dist/ndm_proxy-68354a89.js.map +0 -1
- package/dist/ndm_proxy-c00bc561.mjs.map +0 -1
- package/dist/ndn_types-859bbd48.js +0 -2
- package/dist/ndn_types-859bbd48.js.map +0 -1
- package/dist/ndn_types-f6c08d20.mjs.map +0 -1
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CommandDefinition,
|
|
3
|
+
CommandModule,
|
|
4
|
+
JsonSchema,
|
|
5
|
+
OptionDefinition,
|
|
6
|
+
} from '../core/command.ts'
|
|
7
|
+
import type { CommandContext } from '../core/context.ts'
|
|
8
|
+
import {
|
|
9
|
+
EXIT_OPERATION,
|
|
10
|
+
EXIT_PARTIAL,
|
|
11
|
+
EXIT_PERMISSION,
|
|
12
|
+
EXIT_USAGE,
|
|
13
|
+
ToolError,
|
|
14
|
+
UsageError,
|
|
15
|
+
} from '../core/errors.ts'
|
|
16
|
+
import { getHost, isHostError } from '../runtime/host.ts'
|
|
17
|
+
import { TOOL_VERSION } from '../version.ts'
|
|
18
|
+
import * as provisionSdk from 'buckyos/provision'
|
|
19
|
+
|
|
20
|
+
// provision: first activation of an installed, not yet activated BuckyOS root on
|
|
21
|
+
// this machine (single OOD, did:web, no SN/BNS). Every command is local: no
|
|
22
|
+
// Zone session, no verify-hub, no TaskManager, no network. The activation
|
|
23
|
+
// implementation lives in the SDK (buckyos/provision); this module is the thin
|
|
24
|
+
// argv/confirmation/secret/output layer described in doc/modules/provision.md.
|
|
25
|
+
|
|
26
|
+
// The built SDK entry is untyped for `deno check` (as buckyos/node is for the
|
|
27
|
+
// core); these interfaces mirror the API documented in src/provision_activate.ts.
|
|
28
|
+
export type ActivationState = 'not_installed' | 'unactivated' | 'partial' | 'configured' | 'invalid'
|
|
29
|
+
|
|
30
|
+
export interface ActivationProblem {
|
|
31
|
+
code: string
|
|
32
|
+
message: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ActivationFileEntry {
|
|
36
|
+
path: string
|
|
37
|
+
role: string
|
|
38
|
+
required: boolean
|
|
39
|
+
secret: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ObservedActivationFile extends ActivationFileEntry {
|
|
43
|
+
present: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CommittedActivationFile extends ActivationFileEntry {
|
|
47
|
+
sha256: string | null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ActivationDnsRecord {
|
|
51
|
+
type: 'A' | 'AAAA'
|
|
52
|
+
name: string
|
|
53
|
+
value: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ActivationLockInfo {
|
|
57
|
+
path: string
|
|
58
|
+
schemaVersion: number | null
|
|
59
|
+
traceId: string | null
|
|
60
|
+
startedAt: number | null
|
|
61
|
+
domain: string | null
|
|
62
|
+
stage: string | null
|
|
63
|
+
committedFiles: string[]
|
|
64
|
+
ownerKeyBackupPath: string | null
|
|
65
|
+
corrupt: boolean
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ActivationStatus {
|
|
69
|
+
rootDir: string
|
|
70
|
+
state: ActivationState
|
|
71
|
+
startupRequired: boolean
|
|
72
|
+
observedAt: number
|
|
73
|
+
zoneDid: string | null
|
|
74
|
+
ownerDid: string | null
|
|
75
|
+
deviceDid: string | null
|
|
76
|
+
deviceName: string | null
|
|
77
|
+
accessHostname: string | null
|
|
78
|
+
files: ObservedActivationFile[]
|
|
79
|
+
problems: ActivationProblem[]
|
|
80
|
+
warnings: string[]
|
|
81
|
+
lock: ActivationLockInfo | null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface OfflineActivationCheckOptions {
|
|
85
|
+
rootDir: string
|
|
86
|
+
domain: string
|
|
87
|
+
ownerName: string
|
|
88
|
+
ownerKeyBackupPath: string
|
|
89
|
+
rtcpPort?: number
|
|
90
|
+
guestAccess?: boolean
|
|
91
|
+
publicIp?: string
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface OfflineActivationOptions extends OfflineActivationCheckOptions {
|
|
95
|
+
adminPassword: string
|
|
96
|
+
traceId?: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface OfflineActivationPrecheck {
|
|
100
|
+
ready: boolean
|
|
101
|
+
rootDir: string
|
|
102
|
+
state: ActivationState
|
|
103
|
+
domain: string
|
|
104
|
+
ownerName: string
|
|
105
|
+
ownerDid: string
|
|
106
|
+
zoneDid: string
|
|
107
|
+
deviceDid: string
|
|
108
|
+
deviceName: string
|
|
109
|
+
accessHostname: string
|
|
110
|
+
rtcpPort: number
|
|
111
|
+
guestAccess: boolean
|
|
112
|
+
publicIp: string | null
|
|
113
|
+
dnsRecords: ActivationDnsRecord[]
|
|
114
|
+
ownerKeyBackup: { path: string; directoryExists: boolean; exists: boolean }
|
|
115
|
+
plannedFiles: ActivationFileEntry[]
|
|
116
|
+
problems: ActivationProblem[]
|
|
117
|
+
status: ActivationStatus
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface OfflineActivationResult {
|
|
121
|
+
rootDir: string
|
|
122
|
+
state: 'configured'
|
|
123
|
+
startupRequired: true
|
|
124
|
+
domain: string
|
|
125
|
+
ownerName: string
|
|
126
|
+
ownerDid: string
|
|
127
|
+
zoneDid: string
|
|
128
|
+
deviceDid: string
|
|
129
|
+
deviceName: string
|
|
130
|
+
accessHostname: string
|
|
131
|
+
rtcpPort: number
|
|
132
|
+
guestAccess: boolean
|
|
133
|
+
publicIp: string | null
|
|
134
|
+
documentIat: number
|
|
135
|
+
documentExp: number
|
|
136
|
+
ownerKeyBackup: { path: string; saved: true }
|
|
137
|
+
dnsRecords: ActivationDnsRecord[]
|
|
138
|
+
committedFiles: CommittedActivationFile[]
|
|
139
|
+
warnings: string[]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface ProvisionSdk {
|
|
143
|
+
inspectActivationRoot(rootDir: string): Promise<ActivationStatus>
|
|
144
|
+
checkOfflineActivation(options: OfflineActivationCheckOptions): Promise<OfflineActivationPrecheck>
|
|
145
|
+
activateOfflineZone(options: OfflineActivationOptions): Promise<OfflineActivationResult>
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const sdk = provisionSdk as unknown as ProvisionSdk
|
|
149
|
+
|
|
150
|
+
export interface ProvisionModuleDependencies {
|
|
151
|
+
inspect?: ProvisionSdk['inspectActivationRoot']
|
|
152
|
+
check?: ProvisionSdk['checkOfflineActivation']
|
|
153
|
+
activate?: ProvisionSdk['activateOfflineZone']
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const OBJECT_OUTPUT: JsonSchema = { type: 'object', additionalProperties: true }
|
|
157
|
+
|
|
158
|
+
const ROOT_OPTION: OptionDefinition = {
|
|
159
|
+
name: 'root',
|
|
160
|
+
description:
|
|
161
|
+
'Installed BuckyOS root on this machine (BUCKYOS_ROOT); read by status/check, written by activate',
|
|
162
|
+
type: 'string',
|
|
163
|
+
required: true,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const TARGET_OPTIONS: OptionDefinition[] = [
|
|
167
|
+
ROOT_OPTION,
|
|
168
|
+
{
|
|
169
|
+
name: 'domain',
|
|
170
|
+
description: 'Fixed public host name; Owner DID and Zone DID become did:web:<domain>',
|
|
171
|
+
type: 'string',
|
|
172
|
+
required: true,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'owner-name',
|
|
176
|
+
property: 'owner_name',
|
|
177
|
+
description: 'Local administrator / OwnerDocument name (lowercase DNS label)',
|
|
178
|
+
type: 'string',
|
|
179
|
+
required: true,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'owner-key-backup',
|
|
183
|
+
property: 'owner_key_backup',
|
|
184
|
+
description:
|
|
185
|
+
'New file for the Owner private-key recovery copy; its directory must exist and the file must not',
|
|
186
|
+
type: 'string',
|
|
187
|
+
required: true,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: 'public-ip',
|
|
191
|
+
property: 'public_ip',
|
|
192
|
+
description: 'Fixed public IP used only for the A/AAAA record instructions',
|
|
193
|
+
type: 'string',
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: 'rtcp-port',
|
|
197
|
+
property: 'rtcp_port',
|
|
198
|
+
description: 'RTCP port, default 2980',
|
|
199
|
+
type: 'integer',
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: 'guest-access',
|
|
203
|
+
property: 'guest_access',
|
|
204
|
+
description: 'Enable guest access in the first-boot configuration',
|
|
205
|
+
type: 'boolean',
|
|
206
|
+
},
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
const TARGET_INPUT_PROPERTIES: Record<string, JsonSchema> = {
|
|
210
|
+
root: { type: 'string', minLength: 1 },
|
|
211
|
+
domain: { type: 'string', minLength: 1 },
|
|
212
|
+
owner_name: { type: 'string', minLength: 1 },
|
|
213
|
+
owner_key_backup: { type: 'string', minLength: 1 },
|
|
214
|
+
public_ip: { type: 'string' },
|
|
215
|
+
rtcp_port: { type: 'integer', minimum: 1 },
|
|
216
|
+
guest_access: { type: 'boolean' },
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const TARGET_REQUIRED = ['root', 'domain', 'owner_name', 'owner_key_backup']
|
|
220
|
+
|
|
221
|
+
const POLICY_NOTE =
|
|
222
|
+
'Paths are read from argv by the launcher to grant the minimal filesystem permissions; ' +
|
|
223
|
+
'root and owner_key_backup given only inside --input are limited to the policy roots (cwd, ' +
|
|
224
|
+
'and the paired BUCKYOS_ROOT of the system distribution).'
|
|
225
|
+
|
|
226
|
+
export function createProvisionModule(
|
|
227
|
+
dependencies: ProvisionModuleDependencies = {},
|
|
228
|
+
): CommandModule {
|
|
229
|
+
const deps: ResolvedDependencies = {
|
|
230
|
+
inspect: dependencies.inspect ?? sdk.inspectActivationRoot,
|
|
231
|
+
check: dependencies.check ?? sdk.checkOfflineActivation,
|
|
232
|
+
activate: dependencies.activate ?? sdk.activateOfflineZone,
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
name: 'provision',
|
|
236
|
+
summary: 'Activate an installed BuckyOS root on this machine (first Zone, Owner, and OOD)',
|
|
237
|
+
commands: [statusCommand(deps), checkCommand(deps), activateCommand(deps)],
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
type ResolvedDependencies = Required<ProvisionModuleDependencies>
|
|
242
|
+
|
|
243
|
+
function statusCommand(deps: ResolvedDependencies): CommandDefinition {
|
|
244
|
+
return {
|
|
245
|
+
verb: 'status',
|
|
246
|
+
summary: 'Inspect the local activation material of an installed BuckyOS root',
|
|
247
|
+
description:
|
|
248
|
+
'Read-only. Reports not_installed, unactivated, partial, configured, or invalid from the ' +
|
|
249
|
+
'files under the root; configured only means the first-boot material is complete and internally ' +
|
|
250
|
+
'consistent (startup_required=true). Service health, DNS, TLS, and public DID discovery are not checked. ' +
|
|
251
|
+
POLICY_NOTE,
|
|
252
|
+
options: [ROOT_OPTION],
|
|
253
|
+
inputSchema: {
|
|
254
|
+
type: 'object',
|
|
255
|
+
properties: { root: { type: 'string', minLength: 1 } },
|
|
256
|
+
required: ['root'],
|
|
257
|
+
additionalProperties: false,
|
|
258
|
+
},
|
|
259
|
+
outputSchema: {
|
|
260
|
+
...OBJECT_OUTPUT,
|
|
261
|
+
properties: {
|
|
262
|
+
root: { type: 'string' },
|
|
263
|
+
state: {
|
|
264
|
+
type: 'string',
|
|
265
|
+
enum: ['not_installed', 'unactivated', 'partial', 'configured', 'invalid'],
|
|
266
|
+
},
|
|
267
|
+
startup_required: { type: 'boolean' },
|
|
268
|
+
},
|
|
269
|
+
required: ['root', 'state', 'startup_required'],
|
|
270
|
+
},
|
|
271
|
+
resultSchemaVersion: 1,
|
|
272
|
+
access: { mode: 'fixed', level: 'read' },
|
|
273
|
+
asyncMode: 'sync',
|
|
274
|
+
requiresSession: false,
|
|
275
|
+
execution: 'local',
|
|
276
|
+
networkAccess: false,
|
|
277
|
+
examples: ['buckyos provision status --root /opt/buckyos'],
|
|
278
|
+
handler: async (ctx, input) => {
|
|
279
|
+
const root = resolvePath(ctx, requiredString(input, 'root'))
|
|
280
|
+
await assertHostAccess(root, 'read')
|
|
281
|
+
return statusView(await runSdk(() => deps.inspect(root)))
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function checkCommand(deps: ResolvedDependencies): CommandDefinition {
|
|
287
|
+
return {
|
|
288
|
+
verb: 'check',
|
|
289
|
+
summary: 'Validate activation parameters and the target root without writing anything',
|
|
290
|
+
description:
|
|
291
|
+
'Read-only precheck shared with activate: parameters, target state, and the owner key ' +
|
|
292
|
+
'backup location. No keys are generated, no backup is written, and no operation is produced; ' +
|
|
293
|
+
'activate re-validates the target itself. Fails with the first problem code when the target is not ready. ' +
|
|
294
|
+
POLICY_NOTE,
|
|
295
|
+
options: TARGET_OPTIONS,
|
|
296
|
+
inputSchema: {
|
|
297
|
+
type: 'object',
|
|
298
|
+
properties: TARGET_INPUT_PROPERTIES,
|
|
299
|
+
required: TARGET_REQUIRED,
|
|
300
|
+
additionalProperties: false,
|
|
301
|
+
},
|
|
302
|
+
outputSchema: {
|
|
303
|
+
...OBJECT_OUTPUT,
|
|
304
|
+
properties: {
|
|
305
|
+
ready: { type: 'boolean' },
|
|
306
|
+
root: { type: 'string' },
|
|
307
|
+
state: { type: 'string' },
|
|
308
|
+
},
|
|
309
|
+
required: ['ready', 'root', 'state'],
|
|
310
|
+
},
|
|
311
|
+
resultSchemaVersion: 1,
|
|
312
|
+
access: { mode: 'fixed', level: 'read' },
|
|
313
|
+
asyncMode: 'sync',
|
|
314
|
+
requiresSession: false,
|
|
315
|
+
execution: 'local',
|
|
316
|
+
networkAccess: false,
|
|
317
|
+
examples: [
|
|
318
|
+
'buckyos provision check --root /opt/buckyos --domain corp.example.com --owner-name admin --owner-key-backup /secure-backup/corp-owner.pem',
|
|
319
|
+
],
|
|
320
|
+
handler: async (ctx, input) => {
|
|
321
|
+
const options = activationOptions(ctx, input)
|
|
322
|
+
await assertHostAccess(options.rootDir, 'read')
|
|
323
|
+
await assertHostAccess(getHost().path.dirname(options.ownerKeyBackupPath), 'read')
|
|
324
|
+
const precheck = await runSdk(() => deps.check(options))
|
|
325
|
+
if (!precheck.ready) throw precheckError(precheck)
|
|
326
|
+
return precheckView(precheck)
|
|
327
|
+
},
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function activateCommand(deps: ResolvedDependencies): CommandDefinition {
|
|
332
|
+
return {
|
|
333
|
+
verb: 'activate',
|
|
334
|
+
summary:
|
|
335
|
+
'Activate the root: create the first Zone, Owner, and OOD identity and the first-boot configuration',
|
|
336
|
+
description:
|
|
337
|
+
'Privileged local write. Generates random Ed25519 Owner/device keys and current-time documents, ' +
|
|
338
|
+
'saves the Owner private-key backup, then commits the identity, Zone, and start_config files; ' +
|
|
339
|
+
'etc/node_identity.json is written last because node-daemon treats it as the activation switch. ' +
|
|
340
|
+
'The administrator password is read from the secret admin_password field of --input JSON or from a ' +
|
|
341
|
+
'hidden terminal prompt; there is no --admin-password option. Non-interactive runs require --yes. ' +
|
|
342
|
+
'Repeated, partial, and invalid targets are rejected; there is no --force. After success restart ' +
|
|
343
|
+
'BuckyOS through the installer or node control entry; this command never reports the Zone as online. ' +
|
|
344
|
+
POLICY_NOTE,
|
|
345
|
+
options: TARGET_OPTIONS,
|
|
346
|
+
inputSchema: {
|
|
347
|
+
type: 'object',
|
|
348
|
+
properties: {
|
|
349
|
+
...TARGET_INPUT_PROPERTIES,
|
|
350
|
+
admin_password: {
|
|
351
|
+
type: 'string',
|
|
352
|
+
secret: true,
|
|
353
|
+
description: 'Administrator password (secret; --input JSON or hidden prompt only)',
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
required: TARGET_REQUIRED,
|
|
357
|
+
additionalProperties: false,
|
|
358
|
+
},
|
|
359
|
+
outputSchema: {
|
|
360
|
+
...OBJECT_OUTPUT,
|
|
361
|
+
properties: {
|
|
362
|
+
root: { type: 'string' },
|
|
363
|
+
state: { type: 'string', enum: ['configured'] },
|
|
364
|
+
startup_required: { type: 'boolean' },
|
|
365
|
+
},
|
|
366
|
+
required: ['root', 'state', 'startup_required'],
|
|
367
|
+
},
|
|
368
|
+
resultSchemaVersion: 1,
|
|
369
|
+
access: { mode: 'fixed', level: 'privileged' },
|
|
370
|
+
asyncMode: 'sync',
|
|
371
|
+
requiresSession: false,
|
|
372
|
+
execution: 'local',
|
|
373
|
+
networkAccess: false,
|
|
374
|
+
examples: [
|
|
375
|
+
'buckyos provision activate --root /opt/buckyos --domain corp.example.com --owner-name admin --owner-key-backup /secure-backup/corp-owner.pem',
|
|
376
|
+
'buckyos --non-interactive --yes --input activation.json provision activate --root /opt/buckyos --owner-key-backup /secure-backup/corp-owner.pem',
|
|
377
|
+
],
|
|
378
|
+
handler: async (ctx, input) => {
|
|
379
|
+
const options = activationOptions(ctx, input)
|
|
380
|
+
const backupDirectory = getHost().path.dirname(options.ownerKeyBackupPath)
|
|
381
|
+
await assertHostAccess(options.rootDir, 'read')
|
|
382
|
+
await assertHostAccess(options.rootDir, 'write')
|
|
383
|
+
await assertHostAccess(backupDirectory, 'read')
|
|
384
|
+
await assertHostAccess(backupDirectory, 'write')
|
|
385
|
+
const precheck = await runSdk(() => deps.check(options))
|
|
386
|
+
if (!precheck.ready) throw precheckError(precheck)
|
|
387
|
+
const adminPassword = await obtainAdminPassword(ctx, input)
|
|
388
|
+
await confirmActivation(ctx, precheck)
|
|
389
|
+
const result = await runSdk(() =>
|
|
390
|
+
deps.activate({ ...options, adminPassword, traceId: ctx.traceId })
|
|
391
|
+
)
|
|
392
|
+
return resultView(ctx, input, result)
|
|
393
|
+
},
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
// input handling
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
|
|
401
|
+
function activationOptions(
|
|
402
|
+
ctx: CommandContext,
|
|
403
|
+
input: Record<string, unknown>,
|
|
404
|
+
): OfflineActivationCheckOptions {
|
|
405
|
+
return {
|
|
406
|
+
rootDir: resolvePath(ctx, requiredString(input, 'root')),
|
|
407
|
+
domain: requiredString(input, 'domain'),
|
|
408
|
+
ownerName: requiredString(input, 'owner_name'),
|
|
409
|
+
ownerKeyBackupPath: resolvePath(ctx, requiredString(input, 'owner_key_backup')),
|
|
410
|
+
publicIp: optionalString(input, 'public_ip'),
|
|
411
|
+
rtcpPort: typeof input.rtcp_port === 'number' ? input.rtcp_port : undefined,
|
|
412
|
+
guestAccess: input.guest_access === true,
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function requiredString(input: Record<string, unknown>, key: string): string {
|
|
417
|
+
const value = optionalString(input, key)
|
|
418
|
+
if (!value) throw new UsageError('INVALID_ARGUMENT', `${key} is required`)
|
|
419
|
+
return value
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function optionalString(input: Record<string, unknown>, key: string): string | undefined {
|
|
423
|
+
const value = input[key]
|
|
424
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function resolvePath(ctx: CommandContext, value: string): string {
|
|
428
|
+
const path = getHost().path
|
|
429
|
+
return path.isAbsolute(value) ? path.resolve(value) : path.resolve(ctx.cwd, value)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function assertHostAccess(path: string, operation: 'read' | 'write'): Promise<void> {
|
|
433
|
+
try {
|
|
434
|
+
await getHost().assertAccess(path, operation)
|
|
435
|
+
} catch (error) {
|
|
436
|
+
if (isHostError(error, 'PermissionDenied')) {
|
|
437
|
+
throw new ToolError(
|
|
438
|
+
'HOST_ACCESS_DENIED',
|
|
439
|
+
`${error.message}; pass the path on the command line (--root / --owner-key-backup) so the launcher can grant it`,
|
|
440
|
+
EXIT_PERMISSION,
|
|
441
|
+
false,
|
|
442
|
+
{ path, operation },
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
throw error
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function obtainAdminPassword(
|
|
450
|
+
ctx: CommandContext,
|
|
451
|
+
input: Record<string, unknown>,
|
|
452
|
+
): Promise<string> {
|
|
453
|
+
if (typeof input.admin_password === 'string') return input.admin_password
|
|
454
|
+
if (ctx.config.nonInteractive) {
|
|
455
|
+
throw new UsageError(
|
|
456
|
+
'SECRET_REQUIRED',
|
|
457
|
+
'admin_password must be provided through --input JSON in non-interactive mode',
|
|
458
|
+
)
|
|
459
|
+
}
|
|
460
|
+
if (!ctx.io.inputIsTerminal) {
|
|
461
|
+
throw new UsageError(
|
|
462
|
+
'SECRET_INPUT_UNAVAILABLE',
|
|
463
|
+
'the administrator password requires an interactive terminal or --input JSON',
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
const host = getHost()
|
|
467
|
+
let password: string
|
|
468
|
+
let confirmation: string
|
|
469
|
+
try {
|
|
470
|
+
password = await host.readSecret('Administrator password: ')
|
|
471
|
+
confirmation = await host.readSecret('Confirm administrator password: ')
|
|
472
|
+
} catch (error) {
|
|
473
|
+
if (isHostError(error, 'PermissionDenied')) {
|
|
474
|
+
throw new UsageError('SECRET_INPUT_UNAVAILABLE', error.message)
|
|
475
|
+
}
|
|
476
|
+
throw error
|
|
477
|
+
}
|
|
478
|
+
if (password !== confirmation) {
|
|
479
|
+
throw new UsageError('PASSWORD_MISMATCH', 'the administrator passwords do not match')
|
|
480
|
+
}
|
|
481
|
+
return password
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function confirmActivation(
|
|
485
|
+
ctx: CommandContext,
|
|
486
|
+
precheck: OfflineActivationPrecheck,
|
|
487
|
+
): Promise<void> {
|
|
488
|
+
if (ctx.confirmed) return
|
|
489
|
+
if (ctx.config.nonInteractive) {
|
|
490
|
+
throw new ToolError(
|
|
491
|
+
'CONFIRMATION_REQUIRED',
|
|
492
|
+
'provision activate requires --yes in non-interactive mode',
|
|
493
|
+
EXIT_PERMISSION,
|
|
494
|
+
)
|
|
495
|
+
}
|
|
496
|
+
if (!ctx.io.inputIsTerminal) {
|
|
497
|
+
throw new ToolError(
|
|
498
|
+
'CONFIRMATION_REQUIRED',
|
|
499
|
+
'provision activate requires an interactive terminal or --yes',
|
|
500
|
+
EXIT_PERMISSION,
|
|
501
|
+
)
|
|
502
|
+
}
|
|
503
|
+
await ctx.io.stderr(
|
|
504
|
+
[
|
|
505
|
+
'Activation summary',
|
|
506
|
+
` BUCKYOS_ROOT : ${precheck.rootDir}`,
|
|
507
|
+
` Owner/Zone : ${precheck.zoneDid}`,
|
|
508
|
+
` Device : ${precheck.deviceDid}`,
|
|
509
|
+
` Admin : ${precheck.ownerName}`,
|
|
510
|
+
` Network : ${'wan'} (no SN, no BNS)`,
|
|
511
|
+
` RTCP port : ${precheck.rtcpPort}`,
|
|
512
|
+
` Guest access : ${precheck.guestAccess}`,
|
|
513
|
+
` Owner backup : ${precheck.ownerKeyBackup.path}`,
|
|
514
|
+
'',
|
|
515
|
+
].join('\n'),
|
|
516
|
+
)
|
|
517
|
+
const answer = (await ctx.io.prompt('Write this activation to disk? [y/N] '))?.trim()
|
|
518
|
+
.toLowerCase()
|
|
519
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
520
|
+
throw new ToolError('CONFIRMATION_DECLINED', 'activation was declined', EXIT_PERMISSION)
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ---------------------------------------------------------------------------
|
|
525
|
+
// SDK error mapping
|
|
526
|
+
// ---------------------------------------------------------------------------
|
|
527
|
+
|
|
528
|
+
interface SdkError extends Error {
|
|
529
|
+
code: string
|
|
530
|
+
details: Record<string, unknown>
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function isSdkError(error: unknown): error is SdkError {
|
|
534
|
+
return error instanceof Error && error.name === 'ActivationError' &&
|
|
535
|
+
typeof (error as { code?: unknown }).code === 'string'
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function runSdk<T>(run: () => Promise<T>): Promise<T> {
|
|
539
|
+
try {
|
|
540
|
+
return await run()
|
|
541
|
+
} catch (error) {
|
|
542
|
+
if (isSdkError(error)) {
|
|
543
|
+
const exitCode = error.code === 'INVALID_ARGUMENT'
|
|
544
|
+
? EXIT_USAGE
|
|
545
|
+
: error.code === 'ACTIVATION_COMMIT_FAILED'
|
|
546
|
+
? EXIT_PARTIAL
|
|
547
|
+
: EXIT_OPERATION
|
|
548
|
+
throw new ToolError(error.code, error.message, exitCode, false, error.details ?? {})
|
|
549
|
+
}
|
|
550
|
+
throw error
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function precheckError(precheck: OfflineActivationPrecheck): ToolError {
|
|
555
|
+
const [first] = precheck.problems
|
|
556
|
+
const single = precheck.problems.length === 1
|
|
557
|
+
return new ToolError(
|
|
558
|
+
single ? first.code : 'ACTIVATION_PRECHECK_FAILED',
|
|
559
|
+
single
|
|
560
|
+
? first.message
|
|
561
|
+
: `activation precheck failed: ${precheck.problems.map((p) => p.message).join('; ')}`,
|
|
562
|
+
EXIT_OPERATION,
|
|
563
|
+
false,
|
|
564
|
+
precheckView(precheck),
|
|
565
|
+
)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ---------------------------------------------------------------------------
|
|
569
|
+
// output views (snake_case, never secrets)
|
|
570
|
+
// ---------------------------------------------------------------------------
|
|
571
|
+
|
|
572
|
+
function statusView(status: ActivationStatus): Record<string, unknown> {
|
|
573
|
+
return {
|
|
574
|
+
root: status.rootDir,
|
|
575
|
+
state: status.state,
|
|
576
|
+
startup_required: status.startupRequired,
|
|
577
|
+
observed_at: status.observedAt,
|
|
578
|
+
zone_did: status.zoneDid,
|
|
579
|
+
owner_did: status.ownerDid,
|
|
580
|
+
device_did: status.deviceDid,
|
|
581
|
+
device_name: status.deviceName,
|
|
582
|
+
access_hostname: status.accessHostname,
|
|
583
|
+
files: status.files.map((file) => ({
|
|
584
|
+
path: file.path,
|
|
585
|
+
role: file.role,
|
|
586
|
+
required: file.required,
|
|
587
|
+
secret: file.secret,
|
|
588
|
+
present: file.present,
|
|
589
|
+
})),
|
|
590
|
+
problems: status.problems,
|
|
591
|
+
warnings: status.warnings,
|
|
592
|
+
lock: status.lock
|
|
593
|
+
? {
|
|
594
|
+
path: status.lock.path,
|
|
595
|
+
stage: status.lock.stage,
|
|
596
|
+
trace_id: status.lock.traceId,
|
|
597
|
+
started_at: status.lock.startedAt,
|
|
598
|
+
domain: status.lock.domain,
|
|
599
|
+
committed_files: status.lock.committedFiles,
|
|
600
|
+
owner_key_backup: status.lock.ownerKeyBackupPath,
|
|
601
|
+
corrupt: status.lock.corrupt,
|
|
602
|
+
}
|
|
603
|
+
: null,
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function precheckView(precheck: OfflineActivationPrecheck): Record<string, unknown> {
|
|
608
|
+
return {
|
|
609
|
+
ready: precheck.ready,
|
|
610
|
+
root: precheck.rootDir,
|
|
611
|
+
state: precheck.state,
|
|
612
|
+
domain: precheck.domain,
|
|
613
|
+
owner_name: precheck.ownerName,
|
|
614
|
+
owner_did: precheck.ownerDid,
|
|
615
|
+
zone_did: precheck.zoneDid,
|
|
616
|
+
device_did: precheck.deviceDid,
|
|
617
|
+
device_name: precheck.deviceName,
|
|
618
|
+
access_hostname: precheck.accessHostname,
|
|
619
|
+
rtcp_port: precheck.rtcpPort,
|
|
620
|
+
guest_access: precheck.guestAccess,
|
|
621
|
+
public_ip: precheck.publicIp,
|
|
622
|
+
dns_records: precheck.dnsRecords,
|
|
623
|
+
owner_key_backup: {
|
|
624
|
+
path: precheck.ownerKeyBackup.path,
|
|
625
|
+
directory_exists: precheck.ownerKeyBackup.directoryExists,
|
|
626
|
+
exists: precheck.ownerKeyBackup.exists,
|
|
627
|
+
},
|
|
628
|
+
planned_files: precheck.plannedFiles,
|
|
629
|
+
problems: precheck.problems,
|
|
630
|
+
target: statusView(precheck.status),
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function resultView(
|
|
635
|
+
ctx: CommandContext,
|
|
636
|
+
input: Record<string, unknown>,
|
|
637
|
+
result: OfflineActivationResult,
|
|
638
|
+
): Record<string, unknown> {
|
|
639
|
+
const dnsNotes = [
|
|
640
|
+
...result.dnsRecords.map((record) => `${record.type} ${record.name} ${record.value}`),
|
|
641
|
+
...(result.publicIp ? [] : [`A/AAAA ${result.domain} <this node's fixed public IP>`]),
|
|
642
|
+
`The address record is also saved in etc/zone_dns_records.json under ${result.rootDir}.`,
|
|
643
|
+
`Local boot uses etc/${result.domain}.zone.json; no DNS BOOT/PKX/DEV records are required.`,
|
|
644
|
+
'TLS/ACME is not provided by SN in this mode; configure it on the public gateway.',
|
|
645
|
+
]
|
|
646
|
+
return {
|
|
647
|
+
root: result.rootDir,
|
|
648
|
+
state: result.state,
|
|
649
|
+
startup_required: result.startupRequired,
|
|
650
|
+
domain: result.domain,
|
|
651
|
+
owner_name: result.ownerName,
|
|
652
|
+
owner_did: result.ownerDid,
|
|
653
|
+
zone_did: result.zoneDid,
|
|
654
|
+
device_did: result.deviceDid,
|
|
655
|
+
device_name: result.deviceName,
|
|
656
|
+
access_hostname: result.accessHostname,
|
|
657
|
+
rtcp_port: result.rtcpPort,
|
|
658
|
+
guest_access: result.guestAccess,
|
|
659
|
+
public_ip: result.publicIp,
|
|
660
|
+
document_iat: result.documentIat,
|
|
661
|
+
document_exp: result.documentExp,
|
|
662
|
+
owner_key_backup: result.ownerKeyBackup,
|
|
663
|
+
dns_records: result.dnsRecords,
|
|
664
|
+
dns_notes: dnsNotes,
|
|
665
|
+
committed_files: result.committedFiles,
|
|
666
|
+
warnings: result.warnings,
|
|
667
|
+
next_steps: [
|
|
668
|
+
'Restart BuckyOS through the installer or the node control entry to leave activation mode and boot the new Zone.',
|
|
669
|
+
'Check the running Zone afterwards with system status; activation does not verify service health, DNS, or TLS.',
|
|
670
|
+
`Keep the Owner private-key backup ${result.ownerKeyBackup.path} offline; it is the only recovery copy.`,
|
|
671
|
+
],
|
|
672
|
+
audit: {
|
|
673
|
+
module: ctx.command.module,
|
|
674
|
+
verb: ctx.command.verb,
|
|
675
|
+
trace_id: ctx.traceId,
|
|
676
|
+
tool_version: TOOL_VERSION,
|
|
677
|
+
input_fingerprint: inputFingerprint(input),
|
|
678
|
+
},
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function inputFingerprint(input: Record<string, unknown>): string {
|
|
683
|
+
const material: Record<string, unknown> = {}
|
|
684
|
+
for (const key of Object.keys(TARGET_INPUT_PROPERTIES).sort()) {
|
|
685
|
+
if (input[key] !== undefined) material[key] = input[key]
|
|
686
|
+
}
|
|
687
|
+
const hash = getHost().createHash('sha256')
|
|
688
|
+
hash.update(new TextEncoder().encode(JSON.stringify(material)))
|
|
689
|
+
return hash.digestHex()
|
|
690
|
+
}
|