@cleocode/core 2026.10.3 → 2026.10.4
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/dist/build-provenance.json +2 -2
- package/dist/cloud/nexus-enrol.d.ts +9 -0
- package/dist/cloud/nexus-enrol.d.ts.map +1 -1
- package/dist/cloud/nexus-enrol.js +45 -2
- package/dist/cloud/nexus-enrol.js.map +1 -1
- package/dist/cloud/nexus-first-run.d.ts +142 -0
- package/dist/cloud/nexus-first-run.d.ts.map +1 -0
- package/dist/cloud/nexus-first-run.js +352 -0
- package/dist/cloud/nexus-first-run.js.map +1 -0
- package/dist/cloud/nexus-project-names.d.ts +173 -0
- package/dist/cloud/nexus-project-names.d.ts.map +1 -0
- package/dist/cloud/nexus-project-names.js +314 -0
- package/dist/cloud/nexus-project-names.js.map +1 -0
- package/dist/cloud/nexus-vault-keys.d.ts +33 -2
- package/dist/cloud/nexus-vault-keys.d.ts.map +1 -1
- package/dist/cloud/nexus-vault-keys.js +132 -12
- package/dist/cloud/nexus-vault-keys.js.map +1 -1
- package/dist/dispatch/contracts/derive-output-contract.d.ts +1 -1
- package/dist/dispatch/contracts/derive-output-contract.d.ts.map +1 -1
- package/dist/dispatch/contracts/derive-output-contract.js +2 -1
- package/dist/dispatch/contracts/derive-output-contract.js.map +1 -1
- package/dist/dispatch/contracts/output-contracts.d.ts +2 -1
- package/dist/dispatch/contracts/output-contracts.d.ts.map +1 -1
- package/dist/dispatch/contracts/output-contracts.js +1 -1
- package/dist/dispatch/contracts/output-contracts.js.map +1 -1
- package/dist/dispatch/mvi-projection.d.ts +1 -1
- package/dist/dispatch/mvi-projection.d.ts.map +1 -1
- package/dist/dispatch/mvi-projection.js +1 -1
- package/dist/dispatch/mvi-projection.js.map +1 -1
- package/dist/docs/export-document.js +24462 -24572
- package/dist/docs/export-document.js.map +3 -3
- package/dist/doctor/db-substrate.d.ts +6 -9
- package/dist/doctor/db-substrate.d.ts.map +1 -1
- package/dist/doctor/db-substrate.js +10 -14
- package/dist/doctor/db-substrate.js.map +1 -1
- package/dist/doctor/migrations.d.ts +18 -0
- package/dist/doctor/migrations.d.ts.map +1 -1
- package/dist/doctor/migrations.js +54 -2
- package/dist/doctor/migrations.js.map +1 -1
- package/dist/error-catalog.js +1 -1
- package/dist/error-catalog.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +21 -5
- package/dist/init.js.map +1 -1
- package/dist/llm/conversation.d.ts.map +1 -1
- package/dist/llm/conversation.js +15 -4
- package/dist/llm/conversation.js.map +1 -1
- package/dist/llm/plugin-facade.js +277 -376
- package/dist/llm/plugin-facade.js.map +3 -3
- package/dist/llm/transports/bedrock.d.ts +1 -0
- package/dist/llm/transports/bedrock.d.ts.map +1 -1
- package/dist/llm/transports/bedrock.js +30 -12
- package/dist/llm/transports/bedrock.js.map +1 -1
- package/dist/memory/decision-contradiction.d.ts.map +1 -1
- package/dist/memory/decision-contradiction.js +10 -5
- package/dist/memory/decision-contradiction.js.map +1 -1
- package/dist/memory/dialectic-evaluator.d.ts.map +1 -1
- package/dist/memory/dialectic-evaluator.js +4 -1
- package/dist/memory/dialectic-evaluator.js.map +1 -1
- package/dist/memory/transcript-extractor.d.ts.map +1 -1
- package/dist/memory/transcript-extractor.js +3 -2
- package/dist/memory/transcript-extractor.js.map +1 -1
- package/dist/metrics/token-service.d.ts +16 -0
- package/dist/metrics/token-service.d.ts.map +1 -1
- package/dist/metrics/token-service.js +18 -0
- package/dist/metrics/token-service.js.map +1 -1
- package/dist/nexus/api-contracts.d.ts.map +1 -1
- package/dist/nexus/api-contracts.js +27 -11
- package/dist/nexus/api-contracts.js.map +1 -1
- package/dist/nexus/api-extractors/http-extractor.d.ts +2 -2
- package/dist/nexus/api-extractors/http-extractor.d.ts.map +1 -1
- package/dist/nexus/api-extractors/http-extractor.js +3 -3
- package/dist/nexus/api-extractors/http-extractor.js.map +1 -1
- package/dist/nexus/clusters.js +1 -1
- package/dist/nexus/clusters.js.map +1 -1
- package/dist/nexus/diff.d.ts +1 -1
- package/dist/nexus/diff.d.ts.map +1 -1
- package/dist/nexus/diff.js +3 -2
- package/dist/nexus/diff.js.map +1 -1
- package/dist/nexus/flows.js +1 -1
- package/dist/nexus/flows.js.map +1 -1
- package/dist/nexus/identity.d.ts +8 -0
- package/dist/nexus/identity.d.ts.map +1 -1
- package/dist/nexus/identity.js +17 -1
- package/dist/nexus/identity.js.map +1 -1
- package/dist/nexus/index.d.ts +1 -1
- package/dist/nexus/index.d.ts.map +1 -1
- package/dist/nexus/index.js +2 -2
- package/dist/nexus/index.js.map +1 -1
- package/dist/nexus/nexus-bridge.d.ts.map +1 -1
- package/dist/nexus/nexus-bridge.js +5 -5
- package/dist/nexus/nexus-bridge.js.map +1 -1
- package/dist/nexus/registry.d.ts +22 -0
- package/dist/nexus/registry.d.ts.map +1 -1
- package/dist/nexus/registry.js +76 -1
- package/dist/nexus/registry.js.map +1 -1
- package/dist/paths.js +1 -1
- package/dist/paths.js.map +1 -1
- package/dist/project-scope.js +1 -1
- package/dist/project-scope.js.map +1 -1
- package/dist/project-tombstone.js +1 -1
- package/dist/project-tombstone.js.map +1 -1
- package/dist/release/open.d.ts +3 -6
- package/dist/release/open.d.ts.map +1 -1
- package/dist/release/open.js +59 -23
- package/dist/release/open.js.map +1 -1
- package/dist/release/preflight-skip.d.ts +69 -10
- package/dist/release/preflight-skip.d.ts.map +1 -1
- package/dist/release/preflight-skip.js +195 -47
- package/dist/release/preflight-skip.js.map +1 -1
- package/dist/render/colors.js +1 -19
- package/dist/render/colors.js.map +1 -1
- package/dist/render/tasks/colors.js +1 -1
- package/dist/render/tasks/colors.js.map +1 -1
- package/dist/resources/governor.d.ts +12 -0
- package/dist/resources/governor.d.ts.map +1 -1
- package/dist/resources/governor.js +5 -1
- package/dist/resources/governor.js.map +1 -1
- package/dist/resources/heavy-command-hook-delivery.d.ts +144 -0
- package/dist/resources/heavy-command-hook-delivery.d.ts.map +1 -0
- package/dist/resources/heavy-command-hook-delivery.js +204 -0
- package/dist/resources/heavy-command-hook-delivery.js.map +1 -0
- package/dist/resources/heavy-command.d.ts +69 -0
- package/dist/resources/heavy-command.d.ts.map +1 -1
- package/dist/resources/heavy-command.js +479 -7
- package/dist/resources/heavy-command.js.map +1 -1
- package/dist/resources/run-class.d.ts.map +1 -1
- package/dist/resources/run-class.js +1 -0
- package/dist/resources/run-class.js.map +1 -1
- package/dist/routing/build-command-groups.d.ts +1 -1
- package/dist/routing/build-command-groups.d.ts.map +1 -1
- package/dist/routing/build-command-groups.js +4 -1
- package/dist/routing/build-command-groups.js.map +1 -1
- package/dist/scaffold/ensure-skills.d.ts.map +1 -1
- package/dist/scaffold/ensure-skills.js +58 -2
- package/dist/scaffold/ensure-skills.js.map +1 -1
- package/dist/selfimprove/fix-gen.js +277 -376
- package/dist/selfimprove/fix-gen.js.map +3 -3
- package/dist/sessions/briefing.d.ts.map +1 -1
- package/dist/sessions/briefing.js +11 -0
- package/dist/sessions/briefing.js.map +1 -1
- package/dist/store/dual-scope-db.d.ts.map +1 -1
- package/dist/store/dual-scope-db.js +6 -1
- package/dist/store/dual-scope-db.js.map +1 -1
- package/dist/store/table-classification.d.ts +27 -0
- package/dist/store/table-classification.d.ts.map +1 -1
- package/dist/store/table-classification.js +32 -0
- package/dist/store/table-classification.js.map +1 -1
- package/dist/store/vault-manifest.d.ts +5 -1
- package/dist/store/vault-manifest.d.ts.map +1 -1
- package/dist/store/vault-manifest.js +9 -2
- package/dist/store/vault-manifest.js.map +1 -1
- package/dist/system/health.d.ts +13 -0
- package/dist/system/health.d.ts.map +1 -1
- package/dist/system/health.js +96 -1
- package/dist/system/health.js.map +1 -1
- package/dist/tasks/affected-packages.d.ts +20 -7
- package/dist/tasks/affected-packages.d.ts.map +1 -1
- package/dist/tasks/affected-packages.js +39 -13
- package/dist/tasks/affected-packages.js.map +1 -1
- package/dist/tasks/affected-template.d.ts +93 -0
- package/dist/tasks/affected-template.d.ts.map +1 -0
- package/dist/tasks/affected-template.js +198 -0
- package/dist/tasks/affected-template.js.map +1 -0
- package/dist/tasks/command-line.d.ts +4 -1
- package/dist/tasks/command-line.d.ts.map +1 -1
- package/dist/tasks/command-line.js +7 -2
- package/dist/tasks/command-line.js.map +1 -1
- package/dist/tasks/done-plan.d.ts.map +1 -1
- package/dist/tasks/done-plan.js +12 -2
- package/dist/tasks/done-plan.js.map +1 -1
- package/dist/tasks/duplicate-detector.d.ts.map +1 -1
- package/dist/tasks/duplicate-detector.js +11 -5
- package/dist/tasks/duplicate-detector.js.map +1 -1
- package/dist/tasks/evidence.d.ts.map +1 -1
- package/dist/tasks/evidence.js +185 -11
- package/dist/tasks/evidence.js.map +1 -1
- package/dist/tasks/heavy-tool-env.d.ts +385 -18
- package/dist/tasks/heavy-tool-env.d.ts.map +1 -1
- package/dist/tasks/heavy-tool-env.js +719 -40
- package/dist/tasks/heavy-tool-env.js.map +1 -1
- package/dist/tasks/index.d.ts +2 -2
- package/dist/tasks/index.d.ts.map +1 -1
- package/dist/tasks/index.js +1 -1
- package/dist/tasks/index.js.map +1 -1
- package/dist/tasks/tool-cache-env.d.ts +20 -13
- package/dist/tasks/tool-cache-env.d.ts.map +1 -1
- package/dist/tasks/tool-cache-env.js +27 -48
- package/dist/tasks/tool-cache-env.js.map +1 -1
- package/dist/tasks/tool-cache.d.ts +8 -0
- package/dist/tasks/tool-cache.d.ts.map +1 -1
- package/dist/tasks/tool-cache.js +19 -6
- package/dist/tasks/tool-cache.js.map +1 -1
- package/dist/tasks/tool-semaphore.d.ts +66 -23
- package/dist/tasks/tool-semaphore.d.ts.map +1 -1
- package/dist/tasks/tool-semaphore.js +96 -34
- package/dist/tasks/tool-semaphore.js.map +1 -1
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +37 -4
- package/dist/upgrade.js.map +1 -1
- package/dist/validation/doctor/checks.d.ts +17 -0
- package/dist/validation/doctor/checks.d.ts.map +1 -1
- package/dist/validation/doctor/checks.js +83 -0
- package/dist/validation/doctor/checks.js.map +1 -1
- package/package.json +13 -13
- package/schemas/project-context.schema.json +5 -1
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The account's Cleo Nexus projects by name (T13102): the list the guided
|
|
3
|
+
* first run of `cleo login nexus` shows on a machine with no linked project,
|
|
4
|
+
* and the name resolution behind `cleo cloud restore <name>`.
|
|
5
|
+
*
|
|
6
|
+
* - A project's display name is its plaintext label, else its id. Its
|
|
7
|
+
* `encryptedName` is NOT opened by default: the format is not specified yet
|
|
8
|
+
* (cleo-nexus T098: cipher, AAD with the key version, re-encryption on
|
|
9
|
+
* rotation), and nothing in CLEO writes it. A caller may pass `openName`
|
|
10
|
+
* (the seam the spec's reader will fill); a name that does not open falls
|
|
11
|
+
* back to the label with one warning and never fails the list.
|
|
12
|
+
* - Names are matched exactly first, then case-insensitively, against the
|
|
13
|
+
* display name, the label and the id. One match restores; several are
|
|
14
|
+
* refused with the candidates listed (`E_NEXUS_PROJECT_AMBIGUOUS`); none is
|
|
15
|
+
* `E_NEXUS_PROJECT_NOT_FOUND`. A UUID is taken as an id without listing.
|
|
16
|
+
* - Names and organization names from the server are shown with control,
|
|
17
|
+
* zero-width, separator and bidi characters removed; a listed id that is
|
|
18
|
+
* not a CLEO `ProjectId` is left out; commands quote every word for a POSIX
|
|
19
|
+
* shell, and the machine-read restore command names the immutable id.
|
|
20
|
+
*
|
|
21
|
+
* Every request is a `GET`. No function here logs, and no result or error
|
|
22
|
+
* carries a key or a token.
|
|
23
|
+
*
|
|
24
|
+
* @task T13102
|
|
25
|
+
* @epic T12322
|
|
26
|
+
*/
|
|
27
|
+
import os from 'node:os';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import { ProjectId } from '@cleocode/contracts/cloud';
|
|
30
|
+
import { readDeclaredProjectIdentity } from '@cleocode/paths';
|
|
31
|
+
import { NexusAccountError, resolveNexusApiUrl } from './nexus-auth.js';
|
|
32
|
+
import { listNexusCloudProjects } from './nexus-cloud.js';
|
|
33
|
+
/** Warning code: some project names are encrypted and could not be opened on this device. */
|
|
34
|
+
export const W_NEXUS_PROJECT_NAME_LOCKED = 'W_NEXUS_PROJECT_NAME_LOCKED';
|
|
35
|
+
/** Warning code: the server listed a project whose id is not a CLEO project id; it was left out. */
|
|
36
|
+
export const W_NEXUS_PROJECT_ID_INVALID = 'W_NEXUS_PROJECT_ID_INVALID';
|
|
37
|
+
/** A UUID project id: taken as an id without listing. */
|
|
38
|
+
const UUID_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
39
|
+
/** A legacy 12-hex project id (`ProjectId`): an id when no name matches it. */
|
|
40
|
+
const LEGACY_ID = /^[0-9a-f]{12}$/;
|
|
41
|
+
/**
|
|
42
|
+
* Never echoed from a server-held name: C0/C1 controls, the Arabic letter
|
|
43
|
+
* mark, zero-width characters, line/paragraph separators, bidirectional
|
|
44
|
+
* embeddings, overrides and isolates, and the byte-order mark.
|
|
45
|
+
*/
|
|
46
|
+
const UNSAFE_NAME_CHARS = /[\u0000-\u001f\u007f-\u009f\u061c\u200b-\u200f\u2028-\u202e\u2066-\u2069\ufeff]/g;
|
|
47
|
+
/** A word a POSIX shell takes literally without quotes. */
|
|
48
|
+
const SHELL_SAFE = /^[A-Za-z0-9._@%+=:,/-]+$/;
|
|
49
|
+
/**
|
|
50
|
+
* A reference that matched several projects or none. `publicDetails` is
|
|
51
|
+
* secret-free and the CLI forwards it as the envelope's `error.details`.
|
|
52
|
+
*/
|
|
53
|
+
export class NexusProjectRefError extends NexusAccountError {
|
|
54
|
+
/** The reference and its candidates. */
|
|
55
|
+
publicDetails;
|
|
56
|
+
/**
|
|
57
|
+
* @param code - `E_NEXUS_PROJECT_AMBIGUOUS` or `E_NEXUS_PROJECT_NOT_FOUND`.
|
|
58
|
+
* @param message - Human message.
|
|
59
|
+
* @param fix - Remedy.
|
|
60
|
+
* @param publicDetails - The reference and its candidates.
|
|
61
|
+
*/
|
|
62
|
+
constructor(code, message, fix, publicDetails) {
|
|
63
|
+
super(code, message, fix);
|
|
64
|
+
this.publicDetails = publicDetails;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A server-held name made safe to print: control and bidi-override characters removed.
|
|
69
|
+
*
|
|
70
|
+
* @param raw - Name as the server or the decryption gave it.
|
|
71
|
+
* @returns The cleaned name, or `null` when nothing printable is left.
|
|
72
|
+
*/
|
|
73
|
+
export function safeNexusProjectName(raw) {
|
|
74
|
+
const clean = (raw ?? '').replace(UNSAFE_NAME_CHARS, '').trim();
|
|
75
|
+
return clean === '' ? null : clean;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Quote a word for a POSIX shell (single quotes; `'` becomes `'\''`).
|
|
79
|
+
*
|
|
80
|
+
* @param word - The word.
|
|
81
|
+
* @returns It unchanged when it needs no quoting, else single-quoted.
|
|
82
|
+
*/
|
|
83
|
+
export function shellQuoteWord(word) {
|
|
84
|
+
return SHELL_SAFE.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`;
|
|
85
|
+
}
|
|
86
|
+
/** Open every `encryptedName` with `opener`, if any; failures become one warning, never an error. */
|
|
87
|
+
async function openNames(items, opener, warnings) {
|
|
88
|
+
const opened = new Map();
|
|
89
|
+
if (opener === undefined)
|
|
90
|
+
return opened;
|
|
91
|
+
let failed = 0;
|
|
92
|
+
let firstReason = '';
|
|
93
|
+
for (const item of items) {
|
|
94
|
+
if (!item.encryptedName)
|
|
95
|
+
continue;
|
|
96
|
+
try {
|
|
97
|
+
opened.set(item.projectId, await opener(item.projectId, item.encryptedName));
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
failed += 1;
|
|
101
|
+
if (firstReason === '')
|
|
102
|
+
firstReason = err instanceof Error ? err.message : String(err);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (failed > 0) {
|
|
106
|
+
warnings.push({
|
|
107
|
+
code: W_NEXUS_PROJECT_NAME_LOCKED,
|
|
108
|
+
message: `${failed} project name(s) are encrypted and could not be opened on this device (${firstReason}); showing the label or id instead`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return opened;
|
|
112
|
+
}
|
|
113
|
+
/** One list item as a named project, without its restore command yet. */
|
|
114
|
+
function namedProject(item, decrypted, deviceId) {
|
|
115
|
+
const fromName = safeNexusProjectName(decrypted);
|
|
116
|
+
const label = safeNexusProjectName(item.label);
|
|
117
|
+
const [name, nameSource] = fromName
|
|
118
|
+
? [fromName, 'encrypted-name']
|
|
119
|
+
: label
|
|
120
|
+
? [label, 'label']
|
|
121
|
+
: [item.projectId, 'id'];
|
|
122
|
+
return {
|
|
123
|
+
projectId: item.projectId,
|
|
124
|
+
name,
|
|
125
|
+
nameSource,
|
|
126
|
+
label,
|
|
127
|
+
organizationName: safeNexusProjectName(item.organizationName),
|
|
128
|
+
lastSyncAt: item.lastSyncAt ?? null,
|
|
129
|
+
hasBackup: item.headCheckpointId === undefined ? null : item.headCheckpointId !== null,
|
|
130
|
+
onThisDevice: deviceId !== null && item.replicas.some((r) => r.deviceId === deviceId),
|
|
131
|
+
restoreCommand: null,
|
|
132
|
+
restoreByNameCommand: null,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The projects a reference names: exact matches on the id, display name or
|
|
137
|
+
* label first; when there are none, case-insensitive matches on the names.
|
|
138
|
+
*
|
|
139
|
+
* @param projects - The account's projects.
|
|
140
|
+
* @param ref - A project name, label or id.
|
|
141
|
+
* @returns Every match (one: resolved; several: ambiguous; none: not found).
|
|
142
|
+
*/
|
|
143
|
+
export function matchNexusProjects(projects, ref) {
|
|
144
|
+
const want = ref.trim();
|
|
145
|
+
if (want === '')
|
|
146
|
+
return [];
|
|
147
|
+
const names = (p) => [p.name, p.label].filter((n) => n !== null);
|
|
148
|
+
const exact = projects.filter((p) => p.projectId === want || names(p).includes(want));
|
|
149
|
+
if (exact.length > 0)
|
|
150
|
+
return exact;
|
|
151
|
+
const folded = want.toLowerCase();
|
|
152
|
+
return projects.filter((p) => names(p).some((n) => n.toLowerCase() === folded));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* ` --api-url <origin>` when the origin is not the one a bare `cleo` command
|
|
156
|
+
* would use (`$CLEO_NEXUS_API_URL`, else the default), so a printed command
|
|
157
|
+
* reaches the same server.
|
|
158
|
+
*
|
|
159
|
+
* @param apiUrl - API origin.
|
|
160
|
+
* @returns The flag with a leading space, or `''`.
|
|
161
|
+
*/
|
|
162
|
+
export function nexusApiUrlFlag(apiUrl) {
|
|
163
|
+
let fallback;
|
|
164
|
+
try {
|
|
165
|
+
fallback = resolveNexusApiUrl();
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
fallback = null;
|
|
169
|
+
}
|
|
170
|
+
return apiUrl === fallback ? '' : ` --api-url ${shellQuoteWord(apiUrl)}`;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The exact `cleo cloud restore` command for a project, by its id: the id
|
|
174
|
+
* never changes, while a name can be renamed or matched by a newer project
|
|
175
|
+
* between printing and running (review LOW-4). For agents and scripts.
|
|
176
|
+
*
|
|
177
|
+
* @param project - The project (its id is a validated `ProjectId`).
|
|
178
|
+
* @param apiUrl - API origin (added as `--api-url` when not the default).
|
|
179
|
+
* @returns The command.
|
|
180
|
+
*/
|
|
181
|
+
export function nexusRestoreCommand(project, apiUrl) {
|
|
182
|
+
return `cleo cloud restore ${shellQuoteWord(project.projectId)}${nexusApiUrlFlag(apiUrl)}`;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The same restore by name, for a person to type: only when the name resolves
|
|
186
|
+
* to this project alone and cannot be read as a flag or an id; else `null`.
|
|
187
|
+
*
|
|
188
|
+
* @param project - The project.
|
|
189
|
+
* @param all - Every project of the account, for the uniqueness check.
|
|
190
|
+
* @param apiUrl - API origin (added as `--api-url` when not the default).
|
|
191
|
+
* @returns The command, or `null`.
|
|
192
|
+
*/
|
|
193
|
+
export function nexusRestoreByNameCommand(project, all, apiUrl) {
|
|
194
|
+
const matches = matchNexusProjects(all, project.name);
|
|
195
|
+
const byName = project.nameSource !== 'id' &&
|
|
196
|
+
!project.name.startsWith('-') &&
|
|
197
|
+
!UUID_ID.test(project.name) &&
|
|
198
|
+
matches.length === 1 &&
|
|
199
|
+
matches[0]?.projectId === project.projectId;
|
|
200
|
+
return byName
|
|
201
|
+
? `cleo cloud restore ${shellQuoteWord(project.name)}${nexusApiUrlFlag(apiUrl)}`
|
|
202
|
+
: null;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* The list items whose id is a CLEO project id (a UUID or a legacy 12-hex
|
|
206
|
+
* id); any other id is left out with a warning, so a hostile or broken
|
|
207
|
+
* server cannot put arbitrary text into a printed command (review MED-1).
|
|
208
|
+
*/
|
|
209
|
+
function validItems(items, warnings) {
|
|
210
|
+
const valid = items.filter((item) => ProjectId.safeParse(item.projectId).success);
|
|
211
|
+
if (valid.length < items.length) {
|
|
212
|
+
warnings.push({
|
|
213
|
+
code: W_NEXUS_PROJECT_ID_INVALID,
|
|
214
|
+
message: `left out ${items.length - valid.length} project(s) whose id the server sent is not a CLEO project id`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return valid;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* `GET /v1/projects` (E13, every page) with display names and, for each
|
|
221
|
+
* project this machine can restore, the exact restore commands.
|
|
222
|
+
*
|
|
223
|
+
* @param opts - Device id, name opener, API URL, stores and test overrides.
|
|
224
|
+
* @returns The named projects, whether the list is complete, and warnings.
|
|
225
|
+
* @throws {NexusAccountError} When the list itself cannot be read.
|
|
226
|
+
*/
|
|
227
|
+
export async function listNexusNamedProjects(opts = {}) {
|
|
228
|
+
const { deviceId, openName, ...vault } = opts;
|
|
229
|
+
const list = await listNexusCloudProjects(vault);
|
|
230
|
+
const warnings = [...list.warnings];
|
|
231
|
+
const items = validItems(list.projects, warnings);
|
|
232
|
+
const opened = await openNames(items, openName, warnings);
|
|
233
|
+
const named = items.map((item) => namedProject(item, opened.get(item.projectId) ?? null, deviceId ?? null));
|
|
234
|
+
const projects = named.map((p) => {
|
|
235
|
+
const restorable = p.hasBackup !== false && !p.onThisDevice;
|
|
236
|
+
return {
|
|
237
|
+
...p,
|
|
238
|
+
restoreCommand: restorable ? nexusRestoreCommand(p, list.apiUrl) : null,
|
|
239
|
+
restoreByNameCommand: restorable ? nexusRestoreByNameCommand(p, named, list.apiUrl) : null,
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
return {
|
|
243
|
+
apiUrl: list.apiUrl,
|
|
244
|
+
projects,
|
|
245
|
+
incomplete: list.paging.pageLimitReached || list.paging.projectsTruncated,
|
|
246
|
+
warnings,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/** One candidate line: name, id, organization and last sync. */
|
|
250
|
+
function candidateLine(p) {
|
|
251
|
+
const org = p.organizationName ? `, ${p.organizationName}` : '';
|
|
252
|
+
const sync = p.lastSyncAt ? `, last sync ${p.lastSyncAt}` : '';
|
|
253
|
+
return `${p.name} (${p.projectId}${org}${sync})`;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Resolve a project name, label or id to the server's project id, for
|
|
257
|
+
* `cleo cloud restore <name>`. A UUID is taken as the id without a request.
|
|
258
|
+
*
|
|
259
|
+
* @param ref - What the user typed.
|
|
260
|
+
* @param opts - Name opener, API URL, stores and test overrides.
|
|
261
|
+
* @returns The project id and how it matched.
|
|
262
|
+
* @throws {NexusProjectRefError} `E_NEXUS_PROJECT_AMBIGUOUS` (the candidates
|
|
263
|
+
* are listed) or `E_NEXUS_PROJECT_NOT_FOUND`.
|
|
264
|
+
* @throws {NexusAccountError} When the project list cannot be read.
|
|
265
|
+
*/
|
|
266
|
+
export async function resolveNexusProjectRef(ref, opts = {}) {
|
|
267
|
+
const want = ref.trim();
|
|
268
|
+
if (UUID_ID.test(want))
|
|
269
|
+
return { projectId: want.toLowerCase(), name: null, matchedBy: 'id' };
|
|
270
|
+
const listed = await listNexusNamedProjects({ ...opts, deviceId: null });
|
|
271
|
+
const matches = matchNexusProjects(listed.projects, want);
|
|
272
|
+
const only = matches.length === 1 ? matches[0] : undefined;
|
|
273
|
+
if (only) {
|
|
274
|
+
return only.projectId === want
|
|
275
|
+
? { projectId: only.projectId, name: null, matchedBy: 'id' }
|
|
276
|
+
: { projectId: only.projectId, name: only.name, matchedBy: 'name' };
|
|
277
|
+
}
|
|
278
|
+
if (matches.length > 1) {
|
|
279
|
+
const candidates = matches.map((p) => ({
|
|
280
|
+
...p,
|
|
281
|
+
restoreCommand: nexusRestoreCommand(p, listed.apiUrl),
|
|
282
|
+
}));
|
|
283
|
+
throw new NexusProjectRefError('E_NEXUS_PROJECT_AMBIGUOUS', `"${safeNexusProjectName(want) ?? want}" matches ${matches.length} projects: ${matches.map(candidateLine).join('; ')}`, `restore one by its id: ${candidates.map((c) => c.restoreCommand).join(' | ')}`, { ref: want, candidates });
|
|
284
|
+
}
|
|
285
|
+
if (LEGACY_ID.test(want))
|
|
286
|
+
return { projectId: want, name: null, matchedBy: 'id' };
|
|
287
|
+
throw new NexusProjectRefError('E_NEXUS_PROJECT_NOT_FOUND', `no project of this account at ${listed.apiUrl} is named "${safeNexusProjectName(want) ?? want}"${listed.incomplete ? ' (the project list was cut short, so it may exist; use its id)' : ''}`, 'run `cleo cloud projects` to list the projects, then pass a name or id from it', { ref: want, candidates: [] });
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Refuse to restore a project into a directory nested inside another CLEO
|
|
291
|
+
* project (review LOW-5): `cleo cloud restore <name>` without `--into`
|
|
292
|
+
* restores into the current directory, and from a subdirectory of a project
|
|
293
|
+
* that would create a second project inside the first. The directory itself
|
|
294
|
+
* may be a project root (the restore checks it holds the same project); the
|
|
295
|
+
* home directory never counts as an enclosing project.
|
|
296
|
+
*
|
|
297
|
+
* @param into - The restore target; defaults to the current directory.
|
|
298
|
+
* @throws {NexusAccountError} `E_NEXUS_VAULT_TARGET_OCCUPIED` naming the enclosing project.
|
|
299
|
+
*/
|
|
300
|
+
export function assertNexusRestoreTarget(into) {
|
|
301
|
+
const target = path.resolve(into ?? process.cwd()); // CWD-OK: restore target directory, not a project root lookup
|
|
302
|
+
const home = path.resolve(os.homedir());
|
|
303
|
+
let dir = path.dirname(target);
|
|
304
|
+
for (;;) {
|
|
305
|
+
if (dir !== home && readDeclaredProjectIdentity(dir)) {
|
|
306
|
+
throw new NexusAccountError('E_NEXUS_VAULT_TARGET_OCCUPIED', `${target} is inside the CLEO project at ${dir}; restoring here would nest a second project in it`, `run it in ${dir} (or pass --into ${shellQuoteWord(dir)}) to restore that project there, or pass --into an empty directory outside it`);
|
|
307
|
+
}
|
|
308
|
+
const parent = path.dirname(dir);
|
|
309
|
+
if (parent === dir)
|
|
310
|
+
return;
|
|
311
|
+
dir = parent;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
//# sourceMappingURL=nexus-project-names.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nexus-project-names.js","sourceRoot":"","sources":["../../src/cloud/nexus-project-names.ts"],"names":[],"mappings":"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"}
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
* 2. No escrow and no account keys: this is the account's first vault use.
|
|
14
14
|
* The client mints MK and escrows it (`PUT`, insert-only). A concurrent
|
|
15
15
|
* first device loses the insert with 409 and reads the winner's key.
|
|
16
|
+
* Since T13100 `cleo login nexus` runs steps 1 to 4 right after enrolment
|
|
17
|
+
* ({@link provisionNexusAccount}); `cleo cloud push` still does them for a
|
|
18
|
+
* device that logged in before that.
|
|
16
19
|
* 3. The device certifies itself under MK (a self-grant, `PUT
|
|
17
20
|
* /v1/devices/:id/key`), so other devices can verify its snapshots.
|
|
18
21
|
* 4. Signer trust comes from `certifiedSigners` over `GET /v1/devices/trust`,
|
|
@@ -33,15 +36,16 @@
|
|
|
33
36
|
* No function here logs a key, and no error carries one.
|
|
34
37
|
*
|
|
35
38
|
* @task T12336
|
|
39
|
+
* @task T13100
|
|
36
40
|
* @epic T12322
|
|
37
41
|
*/
|
|
38
|
-
import type { CloudWarning } from '@cleocode/contracts';
|
|
42
|
+
import type { CloudWarning, NexusAccountSetup } from '@cleocode/contracts';
|
|
39
43
|
import { type KeyPair } from './crypto.js';
|
|
40
44
|
import { type FetchLike, Http, type ResponseSchema } from './http.js';
|
|
41
45
|
import { type TrustedSigners } from './keys.js';
|
|
42
46
|
import { resolveNexusApiUrl } from './nexus-auth.js';
|
|
43
47
|
import { type NexusCloudOptions } from './nexus-cloud.js';
|
|
44
|
-
import type
|
|
48
|
+
import { type SealedNexusDevice } from './nexus-device.js';
|
|
45
49
|
import { NexusVaultState } from './nexus-vault-state.js';
|
|
46
50
|
/** Timeout of a snapshot blob upload or download. */
|
|
47
51
|
export declare const NEXUS_VAULT_BLOB_TIMEOUT_MS: number;
|
|
@@ -107,6 +111,33 @@ export interface NexusAccountKey {
|
|
|
107
111
|
export declare function unlockNexusAccountKey(conn: NexusVaultConnection, opts?: {
|
|
108
112
|
readOnly?: boolean;
|
|
109
113
|
}): Promise<NexusAccountKey>;
|
|
114
|
+
/** Options of {@link provisionNexusAccount}. */
|
|
115
|
+
export interface ProvisionNexusAccountOptions extends NexusVaultOptions {
|
|
116
|
+
/**
|
|
117
|
+
* The device entry to act as. `cleo login nexus` passes the entry it just
|
|
118
|
+
* stored, so the setup runs on that new device credential (the escrow routes
|
|
119
|
+
* accept only device credentials). Default: the stored credential, as
|
|
120
|
+
* {@link connectNexusVault} finds it.
|
|
121
|
+
*/
|
|
122
|
+
device?: SealedNexusDevice;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Make the account ready for encrypted backups on this device (onboarding A,
|
|
126
|
+
* T13100): read the escrowed account master key, or mint and escrow it when
|
|
127
|
+
* the account has none (a 409 means another device won: its key is read,
|
|
128
|
+
* never re-minted), then certify this device under it and record the signer
|
|
129
|
+
* trust state in {@link NexusVaultState}. It is {@link unlockNexusAccountKey}
|
|
130
|
+
* with a report, so a later `cleo cloud push` finds everything in place and
|
|
131
|
+
* mints nothing; it is idempotent.
|
|
132
|
+
*
|
|
133
|
+
* It never throws: a server without key escrow answers `unsupported`, any
|
|
134
|
+
* other failure answers `failed` with the step and the remedy. No key is in
|
|
135
|
+
* the result, and a failure message is redacted of the device credential.
|
|
136
|
+
*
|
|
137
|
+
* @param opts - API URL, stores, the device to act as, and test overrides.
|
|
138
|
+
* @returns What the setup did.
|
|
139
|
+
*/
|
|
140
|
+
export declare function provisionNexusAccount(opts?: ProvisionNexusAccountOptions): Promise<NexusAccountSetup>;
|
|
110
141
|
/**
|
|
111
142
|
* The data key of a project stream: unwrap the newest wrapped key, or mint
|
|
112
143
|
* and store one when the project has none yet (its first push).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nexus-vault-keys.d.ts","sourceRoot":"","sources":["../../src/cloud/nexus-vault-keys.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"nexus-vault-keys.d.ts","sourceRoot":"","sources":["../../src/cloud/nexus-vault-keys.ts"],"names":[],"mappings":"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"}
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
* 2. No escrow and no account keys: this is the account's first vault use.
|
|
14
14
|
* The client mints MK and escrows it (`PUT`, insert-only). A concurrent
|
|
15
15
|
* first device loses the insert with 409 and reads the winner's key.
|
|
16
|
+
* Since T13100 `cleo login nexus` runs steps 1 to 4 right after enrolment
|
|
17
|
+
* ({@link provisionNexusAccount}); `cleo cloud push` still does them for a
|
|
18
|
+
* device that logged in before that.
|
|
16
19
|
* 3. The device certifies itself under MK (a self-grant, `PUT
|
|
17
20
|
* /v1/devices/:id/key`), so other devices can verify its snapshots.
|
|
18
21
|
* 4. Signer trust comes from `certifiedSigners` over `GET /v1/devices/trust`,
|
|
@@ -33,6 +36,7 @@
|
|
|
33
36
|
* No function here logs a key, and no error carries one.
|
|
34
37
|
*
|
|
35
38
|
* @task T12336
|
|
39
|
+
* @task T13100
|
|
36
40
|
* @epic T12322
|
|
37
41
|
*/
|
|
38
42
|
import { nexusProjectKeysSchema, nexusUserKeysSchema } from '@cleocode/contracts';
|
|
@@ -43,6 +47,7 @@ import { Http, NexusError } from './http.js';
|
|
|
43
47
|
import { certifiedSigners, createDeviceGrant, homeStreamKey, masterKeyVerifier, newProjectKey, unwrapProjectKey, wrapProjectKey, } from './keys.js';
|
|
44
48
|
import { NexusAccountError, resolveNexusApiUrl } from './nexus-auth.js';
|
|
45
49
|
import { assertNexusCloudDeviceMode, NEXUS_CLOUD_TIMEOUT_MS, nexusCredentialWarnings, } from './nexus-cloud.js';
|
|
50
|
+
import { redactNexusDeviceSecrets } from './nexus-device.js';
|
|
46
51
|
import { ensureNexusDeviceCredential, nexusApiErrorToAccountError } from './nexus-enrol.js';
|
|
47
52
|
import { NexusVaultState } from './nexus-vault-state.js';
|
|
48
53
|
/** Timeout of a snapshot blob upload or download. */
|
|
@@ -64,7 +69,10 @@ export async function connectNexusVault(opts = {}) {
|
|
|
64
69
|
assertNexusCloudDeviceMode();
|
|
65
70
|
const apiUrl = resolveNexusApiUrl(opts.apiUrl);
|
|
66
71
|
const handle = await ensureNexusDeviceCredential({ ...opts, apiUrl });
|
|
67
|
-
|
|
72
|
+
return vaultConnectionOf(handle.device, apiUrl, opts, handle.warnings);
|
|
73
|
+
}
|
|
74
|
+
/** A vault connection acting as `device` (its current credential and keys). */
|
|
75
|
+
function vaultConnectionOf(device, apiUrl, opts, credentialWarnings) {
|
|
68
76
|
const bearer = device.currentBearer();
|
|
69
77
|
if (bearer === null) {
|
|
70
78
|
throw new NexusAccountError('E_NEXUS_NOT_SIGNED_IN', `not signed in to Cleo Nexus at ${apiUrl}`, 'run `cleo login nexus`');
|
|
@@ -93,7 +101,7 @@ export async function connectNexusVault(opts = {}) {
|
|
|
93
101
|
http,
|
|
94
102
|
blobFetch: (input, init) => base(input, { ...init, signal: AbortSignal.timeout(blobTimeoutMs) }),
|
|
95
103
|
state: opts.vaultState ?? new NexusVaultState(),
|
|
96
|
-
warnings: nexusCredentialWarnings(
|
|
104
|
+
warnings: nexusCredentialWarnings(credentialWarnings),
|
|
97
105
|
raw,
|
|
98
106
|
async call(method, path, schema, body) {
|
|
99
107
|
try {
|
|
@@ -166,6 +174,10 @@ async function openEscrow(conn) {
|
|
|
166
174
|
}
|
|
167
175
|
return { mk, kv: grant.keyVersion };
|
|
168
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Mint the account master key and escrow it (insert-only). `adopted` is `true` when another
|
|
179
|
+
* device escrowed first (409): its key is read and used, and this device never mints again.
|
|
180
|
+
*/
|
|
169
181
|
async function mintEscrow(conn) {
|
|
170
182
|
const existing = await conn.find('/v1/account/keys', nexusUserKeysSchema);
|
|
171
183
|
if (existing !== null) {
|
|
@@ -178,7 +190,7 @@ async function mintEscrow(conn) {
|
|
|
178
190
|
keyVersion: 1,
|
|
179
191
|
masterKeyVerifier: masterKeyVerifier(mk),
|
|
180
192
|
});
|
|
181
|
-
return { mk, kv: 1 };
|
|
193
|
+
return { mk, kv: 1, adopted: false };
|
|
182
194
|
}
|
|
183
195
|
catch (err) {
|
|
184
196
|
// Backstop: unreachable while GET and PUT escrow ship together (cleo-nexus #24), since
|
|
@@ -191,15 +203,16 @@ async function mintEscrow(conn) {
|
|
|
191
203
|
const won = await openEscrow(conn);
|
|
192
204
|
if (won === null)
|
|
193
205
|
throw keyUnavailable('the account key was escrowed concurrently but is not readable');
|
|
194
|
-
return won;
|
|
206
|
+
return { ...won, adopted: true };
|
|
195
207
|
}
|
|
196
208
|
}
|
|
209
|
+
/** Certify this device under MK unless it already is; `wrote` says whether this call did. */
|
|
197
210
|
async function ensureCertified(conn, mk, kv) {
|
|
198
211
|
const trust = await conn.call('GET', '/v1/devices/trust', DeviceTrustSchema);
|
|
199
212
|
const mine = conn.keys.signing.publicKey.toString('base64');
|
|
200
213
|
const certified = trust.certificates.some((c) => c.deviceId === conn.deviceId && c.keyVersion === kv && c.signingPublicKey === mine && c.live);
|
|
201
214
|
if (certified)
|
|
202
|
-
return trust;
|
|
215
|
+
return { trust, wrote: false };
|
|
203
216
|
const grant = createDeviceGrant({
|
|
204
217
|
masterKey: mk,
|
|
205
218
|
userId: conn.userId,
|
|
@@ -212,7 +225,7 @@ async function ensureCertified(conn, mk, kv) {
|
|
|
212
225
|
signer: { deviceId: conn.deviceId, signing: conn.keys.signing },
|
|
213
226
|
});
|
|
214
227
|
await conn.call('PUT', `/v1/devices/${encodeURIComponent(conn.deviceId)}/key`, z.looseObject({}), grant);
|
|
215
|
-
return conn.call('GET', '/v1/devices/trust', DeviceTrustSchema);
|
|
228
|
+
return { trust: await conn.call('GET', '/v1/devices/trust', DeviceTrustSchema), wrote: true };
|
|
216
229
|
}
|
|
217
230
|
/**
|
|
218
231
|
* Get the account master key for this device, minting and escrowing it on
|
|
@@ -227,16 +240,46 @@ async function ensureCertified(conn, mk, kv) {
|
|
|
227
240
|
* has no key escrow route; a mapped API error otherwise.
|
|
228
241
|
*/
|
|
229
242
|
export async function unlockNexusAccountKey(conn, opts = {}) {
|
|
243
|
+
return (await unlockAccount(conn, opts.readOnly === true, { step: 'escrow-read' })).key;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* {@link unlockNexusAccountKey}, reporting how the key was obtained. `progress.step` names the
|
|
247
|
+
* step running, so a caller that catches a failure can say where it happened.
|
|
248
|
+
*/
|
|
249
|
+
async function unlockAccount(conn, readOnly, progress) {
|
|
250
|
+
progress.step = 'escrow-read';
|
|
230
251
|
const escrowed = await openEscrow(conn);
|
|
231
|
-
if (escrowed === null &&
|
|
252
|
+
if (escrowed === null && readOnly) {
|
|
232
253
|
// Nothing was ever pushed from any device: a read has nothing to read, and
|
|
233
254
|
// must not mint the account key as a side effect.
|
|
234
255
|
throw new NexusAccountError('E_NEXUS_VAULT_EMPTY', 'the cloud vault is empty: no device has pushed a snapshot to this account yet', 'run `cleo cloud push` on a device that has the data');
|
|
235
256
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
257
|
+
let unlocked;
|
|
258
|
+
let escrow;
|
|
259
|
+
if (escrowed !== null) {
|
|
260
|
+
unlocked = escrowed;
|
|
261
|
+
escrow = 'fetched';
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
// The account's first device. Since T13100 `cleo login nexus` does this right after
|
|
265
|
+
// enrolment; a push still gets here for a device that logged in before that.
|
|
266
|
+
progress.step = 'escrow-mint';
|
|
267
|
+
const minted = await mintEscrow(conn);
|
|
268
|
+
unlocked = minted;
|
|
269
|
+
escrow = minted.adopted ? 'adopted' : 'minted';
|
|
270
|
+
}
|
|
271
|
+
progress.step = 'certify';
|
|
272
|
+
let trust;
|
|
273
|
+
let certificate = null;
|
|
274
|
+
if (readOnly) {
|
|
275
|
+
trust = await conn.call('GET', '/v1/devices/trust', DeviceTrustSchema);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
const certified = await ensureCertified(conn, unlocked.mk, unlocked.kv);
|
|
279
|
+
trust = certified.trust;
|
|
280
|
+
certificate = certified.wrote ? 'new' : 'existing';
|
|
281
|
+
}
|
|
282
|
+
progress.step = 'trust';
|
|
240
283
|
// Read, evaluate and persist the trust state under one lock, so two
|
|
241
284
|
// concurrent commands cannot interleave and lose a narrowed pin.
|
|
242
285
|
const evaluation = conn.state.updateTrust(conn.apiUrl, conn.userId, (current) => {
|
|
@@ -248,7 +291,84 @@ export async function unlockNexusAccountKey(conn, opts = {}) {
|
|
|
248
291
|
? 'the account key was rotated and this device does not hold the new one'
|
|
249
292
|
: 'the server declared an account key version no rotation explains');
|
|
250
293
|
}
|
|
251
|
-
return {
|
|
294
|
+
return {
|
|
295
|
+
key: { masterKey: unlocked.mk, keyVersion: unlocked.kv, signers: evaluation.signers },
|
|
296
|
+
escrow,
|
|
297
|
+
certificate,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
/** What each setup step does, for a failure message. */
|
|
301
|
+
const SETUP_STEP_LABELS = {
|
|
302
|
+
connect: 'opening the vault connection with the device credential',
|
|
303
|
+
'escrow-read': 'reading the escrowed account key',
|
|
304
|
+
'escrow-mint': 'creating and escrowing the account key',
|
|
305
|
+
certify: 'certifying this device under the account key',
|
|
306
|
+
trust: 'recording the device trust state',
|
|
307
|
+
};
|
|
308
|
+
/** The remedy of a failed setup step whose error carried none. */
|
|
309
|
+
const SETUP_RETRY_FIX = 'run `cleo login nexus` again; `cleo cloud push` also finishes the setup on this device';
|
|
310
|
+
/**
|
|
311
|
+
* Make the account ready for encrypted backups on this device (onboarding A,
|
|
312
|
+
* T13100): read the escrowed account master key, or mint and escrow it when
|
|
313
|
+
* the account has none (a 409 means another device won: its key is read,
|
|
314
|
+
* never re-minted), then certify this device under it and record the signer
|
|
315
|
+
* trust state in {@link NexusVaultState}. It is {@link unlockNexusAccountKey}
|
|
316
|
+
* with a report, so a later `cleo cloud push` finds everything in place and
|
|
317
|
+
* mints nothing; it is idempotent.
|
|
318
|
+
*
|
|
319
|
+
* It never throws: a server without key escrow answers `unsupported`, any
|
|
320
|
+
* other failure answers `failed` with the step and the remedy. No key is in
|
|
321
|
+
* the result, and a failure message is redacted of the device credential.
|
|
322
|
+
*
|
|
323
|
+
* @param opts - API URL, stores, the device to act as, and test overrides.
|
|
324
|
+
* @returns What the setup did.
|
|
325
|
+
*/
|
|
326
|
+
export async function provisionNexusAccount(opts = {}) {
|
|
327
|
+
const progress = { step: 'connect' };
|
|
328
|
+
// The bearer in use, so a failure message that quotes it (fetch quotes a malformed header
|
|
329
|
+
// value) is redacted even when it does not have the credential's shape.
|
|
330
|
+
let bearer = opts.device?.currentBearer() ?? null;
|
|
331
|
+
try {
|
|
332
|
+
const conn = opts.device !== undefined
|
|
333
|
+
? vaultConnectionOf(opts.device, resolveNexusApiUrl(opts.apiUrl), opts, [])
|
|
334
|
+
: await connectNexusVault(opts);
|
|
335
|
+
bearer = conn.device.currentBearer();
|
|
336
|
+
const unlocked = await unlockAccount(conn, false, progress);
|
|
337
|
+
const how = unlocked.escrow === 'minted'
|
|
338
|
+
? 'the account key was created and escrowed on Cleo Nexus'
|
|
339
|
+
: 'this device received the account key from Cleo Nexus escrow';
|
|
340
|
+
return {
|
|
341
|
+
status: 'ready',
|
|
342
|
+
escrow: unlocked.escrow,
|
|
343
|
+
certificate: unlocked.certificate ?? 'existing',
|
|
344
|
+
keyVersion: unlocked.key.keyVersion,
|
|
345
|
+
summary: `Your account is ready for encrypted backups: ${how}, and this device is certified to use it.`,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
if (err instanceof NexusAccountError && err.code === 'E_NEXUS_VAULT_UNSUPPORTED') {
|
|
350
|
+
const fix = err.fix ?? 'upgrade the Cleo Nexus server';
|
|
351
|
+
return {
|
|
352
|
+
status: 'unsupported',
|
|
353
|
+
code: err.code,
|
|
354
|
+
fix,
|
|
355
|
+
summary: `signed in, but encrypted backups are not available: ${err.message}. Fix: ${fix}`,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
const code = err instanceof NexusAccountError ? err.code : 'E_NEXUS_REQUEST_FAILED';
|
|
359
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
360
|
+
const message = redactNexusDeviceSecrets(bearer !== null && bearer !== '' ? raw.split(bearer).join('[redacted]') : raw);
|
|
361
|
+
const fix = (err instanceof NexusAccountError ? err.fix : undefined) ?? SETUP_RETRY_FIX;
|
|
362
|
+
const step = progress.step;
|
|
363
|
+
return {
|
|
364
|
+
status: 'failed',
|
|
365
|
+
step,
|
|
366
|
+
code,
|
|
367
|
+
message,
|
|
368
|
+
fix,
|
|
369
|
+
summary: `signed in, but encrypted backups are not set up: step ${step} (${SETUP_STEP_LABELS[step]}) failed with ${code}: ${message}. Fix: ${fix}`,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
252
372
|
}
|
|
253
373
|
/**
|
|
254
374
|
* Why Cleo Nexus refused this device's first key for a project (403), as an
|