@cleocode/core 2026.9.25 → 2026.10.1
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/admin/import-tasks.d.ts.map +1 -1
- package/dist/admin/import-tasks.js +5 -2
- package/dist/admin/import-tasks.js.map +1 -1
- package/dist/admin/import.d.ts.map +1 -1
- package/dist/admin/import.js +7 -2
- package/dist/admin/import.js.map +1 -1
- package/dist/build-provenance.json +2 -2
- package/dist/cloud/index.d.ts +1 -0
- package/dist/cloud/index.d.ts.map +1 -1
- package/dist/cloud/index.js +1 -0
- package/dist/cloud/index.js.map +1 -1
- package/dist/cloud/nexus-attach.d.ts +94 -0
- package/dist/cloud/nexus-attach.d.ts.map +1 -0
- package/dist/cloud/nexus-attach.js +192 -0
- package/dist/cloud/nexus-attach.js.map +1 -0
- package/dist/cloud/nexus-auth.d.ts +41 -4
- package/dist/cloud/nexus-auth.d.ts.map +1 -1
- package/dist/cloud/nexus-auth.js +151 -38
- package/dist/cloud/nexus-auth.js.map +1 -1
- package/dist/cloud/nexus-cloud-status.d.ts +132 -0
- package/dist/cloud/nexus-cloud-status.d.ts.map +1 -0
- package/dist/cloud/nexus-cloud-status.js +440 -0
- package/dist/cloud/nexus-cloud-status.js.map +1 -0
- package/dist/cloud/nexus-cloud.d.ts +179 -0
- package/dist/cloud/nexus-cloud.d.ts.map +1 -0
- package/dist/cloud/nexus-cloud.js +296 -0
- package/dist/cloud/nexus-cloud.js.map +1 -0
- package/dist/cloud/nexus-credentials.js +2 -2
- package/dist/cloud/nexus-credentials.js.map +1 -1
- package/dist/cloud/nexus-device.d.ts +834 -0
- package/dist/cloud/nexus-device.d.ts.map +1 -0
- package/dist/cloud/nexus-device.js +1727 -0
- package/dist/cloud/nexus-device.js.map +1 -0
- package/dist/cloud/nexus-enrol.d.ts +241 -0
- package/dist/cloud/nexus-enrol.d.ts.map +1 -0
- package/dist/cloud/nexus-enrol.js +1163 -0
- package/dist/cloud/nexus-enrol.js.map +1 -0
- package/dist/cloud/nexus-link.d.ts +10 -0
- package/dist/cloud/nexus-link.d.ts.map +1 -1
- package/dist/cloud/nexus-link.js +172 -11
- package/dist/cloud/nexus-link.js.map +1 -1
- package/dist/cloud/nexus-logout.d.ts +114 -0
- package/dist/cloud/nexus-logout.d.ts.map +1 -0
- package/dist/cloud/nexus-logout.js +429 -0
- package/dist/cloud/nexus-logout.js.map +1 -0
- package/dist/cloud/signing.d.ts +20 -0
- package/dist/cloud/signing.d.ts.map +1 -1
- package/dist/cloud/signing.js +14 -0
- package/dist/cloud/signing.js.map +1 -1
- package/dist/crypto/credentials.d.ts +61 -0
- package/dist/crypto/credentials.d.ts.map +1 -1
- package/dist/crypto/credentials.js +154 -58
- package/dist/crypto/credentials.js.map +1 -1
- package/dist/docs/export-document.js +1072 -227
- package/dist/docs/export-document.js.map +3 -3
- package/dist/doctor/dependency-cycles.d.ts +41 -0
- package/dist/doctor/dependency-cycles.d.ts.map +1 -0
- package/dist/doctor/dependency-cycles.js +39 -0
- package/dist/doctor/dependency-cycles.js.map +1 -0
- package/dist/doctor/index.d.ts +2 -0
- package/dist/doctor/index.d.ts.map +1 -1
- package/dist/doctor/index.js +1 -0
- package/dist/doctor/index.js.map +1 -1
- package/dist/doctor/row-identity.d.ts +11 -0
- package/dist/doctor/row-identity.d.ts.map +1 -1
- package/dist/doctor/row-identity.js +67 -9
- package/dist/doctor/row-identity.js.map +1 -1
- package/dist/error-catalog.d.ts.map +1 -1
- package/dist/error-catalog.js +4 -0
- package/dist/error-catalog.js.map +1 -1
- package/dist/error-registry.d.ts.map +1 -1
- package/dist/error-registry.js +8 -0
- package/dist/error-registry.js.map +1 -1
- package/dist/llm/plugin-facade.js +1200 -355
- package/dist/llm/plugin-facade.js.map +3 -3
- package/dist/nexus/workspace.d.ts.map +1 -1
- package/dist/nexus/workspace.js +54 -4
- package/dist/nexus/workspace.js.map +1 -1
- package/dist/orchestrate/lifecycle-ops.js +1 -1
- package/dist/orchestrate/lifecycle-ops.js.map +1 -1
- package/dist/orchestrate/pivot.d.ts.map +1 -1
- package/dist/orchestrate/pivot.js +4 -2
- package/dist/orchestrate/pivot.js.map +1 -1
- package/dist/orchestrate/query-ops.d.ts.map +1 -1
- package/dist/orchestrate/query-ops.js +3 -3
- package/dist/orchestrate/query-ops.js.map +1 -1
- package/dist/orchestrate/spawn-ops.js +1 -1
- package/dist/orchestrate/spawn-ops.js.map +1 -1
- package/dist/orchestration/index.js +1 -1
- package/dist/orchestration/index.js.map +1 -1
- package/dist/orchestration/lead-rollup.d.ts.map +1 -1
- package/dist/orchestration/lead-rollup.js +3 -3
- package/dist/orchestration/lead-rollup.js.map +1 -1
- package/dist/orchestration/parallel.js +1 -1
- package/dist/orchestration/parallel.js.map +1 -1
- package/dist/orchestration/wave-topic.d.ts +4 -2
- package/dist/orchestration/wave-topic.d.ts.map +1 -1
- package/dist/orchestration/wave-topic.js +6 -4
- package/dist/orchestration/wave-topic.js.map +1 -1
- package/dist/orchestration/waves.d.ts +2 -1
- package/dist/orchestration/waves.d.ts.map +1 -1
- package/dist/orchestration/waves.js +4 -3
- package/dist/orchestration/waves.js.map +1 -1
- package/dist/phases/index.d.ts.map +1 -1
- package/dist/phases/index.js +4 -2
- package/dist/phases/index.js.map +1 -1
- package/dist/selfimprove/fix-gen.js +1203 -358
- package/dist/selfimprove/fix-gen.js.map +3 -3
- package/dist/session/engine-ops.d.ts.map +1 -1
- package/dist/session/engine-ops.js +77 -49
- package/dist/session/engine-ops.js.map +1 -1
- package/dist/sessions/focus-state-store.d.ts +69 -23
- package/dist/sessions/focus-state-store.d.ts.map +1 -1
- package/dist/sessions/focus-state-store.js +113 -27
- package/dist/sessions/focus-state-store.js.map +1 -1
- package/dist/sessions/handoff.js +1 -1
- package/dist/sessions/handoff.js.map +1 -1
- package/dist/sessions/index.d.ts.map +1 -1
- package/dist/sessions/index.js +24 -13
- package/dist/sessions/index.js.map +1 -1
- package/dist/sessions/terminal-identity.d.ts +165 -17
- package/dist/sessions/terminal-identity.d.ts.map +1 -1
- package/dist/sessions/terminal-identity.js +341 -32
- package/dist/sessions/terminal-identity.js.map +1 -1
- package/dist/setup/sections/nexus-account.d.ts +8 -6
- package/dist/setup/sections/nexus-account.d.ts.map +1 -1
- package/dist/setup/sections/nexus-account.js +53 -9
- package/dist/setup/sections/nexus-account.js.map +1 -1
- package/dist/skills/orchestrator/startup.js +2 -2
- package/dist/skills/orchestrator/startup.js.map +1 -1
- package/dist/snapshot/index.d.ts +4 -0
- package/dist/snapshot/index.d.ts.map +1 -1
- package/dist/snapshot/index.js +50 -3
- package/dist/snapshot/index.js.map +1 -1
- package/dist/store/db-helpers.d.ts +15 -2
- package/dist/store/db-helpers.d.ts.map +1 -1
- package/dist/store/db-helpers.js +84 -3
- package/dist/store/db-helpers.js.map +1 -1
- package/dist/store/dependency-cycles.d.ts +164 -0
- package/dist/store/dependency-cycles.d.ts.map +1 -0
- package/dist/store/dependency-cycles.js +360 -0
- package/dist/store/dependency-cycles.js.map +1 -0
- package/dist/store/display-id-alias.d.ts +93 -37
- package/dist/store/display-id-alias.d.ts.map +1 -1
- package/dist/store/display-id-alias.js +590 -115
- package/dist/store/display-id-alias.js.map +1 -1
- package/dist/store/dual-scope-db.d.ts.map +1 -1
- package/dist/store/dual-scope-db.js +34 -7
- package/dist/store/dual-scope-db.js.map +1 -1
- package/dist/store/exodus/migrate.d.ts.map +1 -1
- package/dist/store/exodus/migrate.js +8 -1
- package/dist/store/exodus/migrate.js.map +1 -1
- package/dist/store/global-salt.d.ts +35 -0
- package/dist/store/global-salt.d.ts.map +1 -1
- package/dist/store/global-salt.js +136 -16
- package/dist/store/global-salt.js.map +1 -1
- package/dist/store/migration-sqlite.d.ts.map +1 -1
- package/dist/store/migration-sqlite.js +19 -11
- package/dist/store/migration-sqlite.js.map +1 -1
- package/dist/store/portable-bundle-scan.d.ts.map +1 -1
- package/dist/store/portable-bundle-scan.js +6 -0
- package/dist/store/portable-bundle-scan.js.map +1 -1
- package/dist/store/row-identity-registry.d.ts.map +1 -1
- package/dist/store/row-identity-registry.js +11 -3
- package/dist/store/row-identity-registry.js.map +1 -1
- package/dist/store/row-identity.d.ts +91 -1
- package/dist/store/row-identity.d.ts.map +1 -1
- package/dist/store/row-identity.js +276 -7
- package/dist/store/row-identity.js.map +1 -1
- package/dist/store/safety-data-accessor.d.ts +3 -2
- package/dist/store/safety-data-accessor.d.ts.map +1 -1
- package/dist/store/safety-data-accessor.js +5 -2
- package/dist/store/safety-data-accessor.js.map +1 -1
- package/dist/store/session-store.d.ts +63 -3
- package/dist/store/session-store.d.ts.map +1 -1
- package/dist/store/session-store.js +121 -8
- package/dist/store/session-store.js.map +1 -1
- package/dist/store/split-brain-import.d.ts.map +1 -1
- package/dist/store/split-brain-import.js +36 -0
- package/dist/store/split-brain-import.js.map +1 -1
- package/dist/store/sqlite-data-accessor.d.ts +50 -3
- package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
- package/dist/store/sqlite-data-accessor.js +246 -13
- package/dist/store/sqlite-data-accessor.js.map +1 -1
- package/dist/store/sync/replica.d.ts +34 -3
- package/dist/store/sync/replica.d.ts.map +1 -1
- package/dist/store/sync/replica.js +39 -2
- package/dist/store/sync/replica.js.map +1 -1
- package/dist/store/sync/schema.d.ts +6 -3
- package/dist/store/sync/schema.d.ts.map +1 -1
- package/dist/store/sync/schema.js +6 -3
- package/dist/store/sync/schema.js.map +1 -1
- package/dist/store/tasks-sqlite.d.ts.map +1 -1
- package/dist/store/tasks-sqlite.js +14 -2
- package/dist/store/tasks-sqlite.js.map +1 -1
- package/dist/store/umbrella-data-accessor.d.ts +3 -2
- package/dist/store/umbrella-data-accessor.d.ts.map +1 -1
- package/dist/store/umbrella-data-accessor.js +5 -2
- package/dist/store/umbrella-data-accessor.js.map +1 -1
- package/dist/system/health.d.ts +2 -0
- package/dist/system/health.d.ts.map +1 -1
- package/dist/system/health.js +54 -0
- package/dist/system/health.js.map +1 -1
- package/dist/task-work/index.d.ts.map +1 -1
- package/dist/task-work/index.js +9 -6
- package/dist/task-work/index.js.map +1 -1
- package/dist/tasks/analyze.d.ts.map +1 -1
- package/dist/tasks/analyze.js +3 -2
- package/dist/tasks/analyze.js.map +1 -1
- package/dist/tasks/evidence.d.ts +19 -1
- package/dist/tasks/evidence.d.ts.map +1 -1
- package/dist/tasks/evidence.js +79 -18
- package/dist/tasks/evidence.js.map +1 -1
- package/dist/tasks/infer-add-params.js +1 -1
- package/dist/tasks/infer-add-params.js.map +1 -1
- package/dist/tasks/task-context.d.ts.map +1 -1
- package/dist/tasks/task-context.js +3 -1
- package/dist/tasks/task-context.js.map +1 -1
- package/dist/tasks/task-import.d.ts.map +1 -1
- package/dist/tasks/task-import.js +11 -3
- package/dist/tasks/task-import.js.map +1 -1
- package/dist/tasks/task-next.d.ts +5 -1
- package/dist/tasks/task-next.d.ts.map +1 -1
- package/dist/tasks/task-next.js +7 -2
- package/dist/tasks/task-next.js.map +1 -1
- package/dist/validation/engine-ops.d.ts.map +1 -1
- package/dist/validation/engine-ops.js +3 -2
- package/dist/validation/engine-ops.js.map +1 -1
- package/dist/workgraph/scaffold-apply.d.ts.map +1 -1
- package/dist/workgraph/scaffold-apply.js +14 -5
- package/dist/workgraph/scaffold-apply.js.map +1 -1
- package/migrations/drizzle-cleo-project/20260930160000_t12886-dependency-cycle-guard/migration.sql +76 -0
- package/migrations/drizzle-cleo-project/20260930160000_t12886-dependency-cycle-guard/revert.sql +7 -0
- package/package.json +13 -13
- package/templates/CLEO-INJECTION.md +2 -2
- package/templates/CLEO-REFERENCE.md +1 -1
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cleo Nexus device store: `<cleoHome>/nexus-device.json`.
|
|
3
|
+
*
|
|
4
|
+
* One file per CLEO home holds every Nexus device identity of that home, keyed
|
|
5
|
+
* by API origin and then by user id (cleo-nexus device contract §2.2, §2.4,
|
|
6
|
+
* §2.5, §3.1, §3.5). Each entry carries:
|
|
7
|
+
*
|
|
8
|
+
* - `deviceId`, the client-minted UUIDv7 (§3.1);
|
|
9
|
+
* - `keys`, the device's X25519 and Ed25519 key pairs, local-only (§2.4);
|
|
10
|
+
* - `current`, the device credential in use (`cnx_d1_…`, §2.2);
|
|
11
|
+
* - `pending`, a client-minted rotation credential not yet confirmed (§2.5);
|
|
12
|
+
* - `pendingSignOut` and `pendingRevoke`, credentials kept only to retry an
|
|
13
|
+
* unconfirmed sign-out (E9) or revoke (E10) (§3.5, M3);
|
|
14
|
+
* - `retired`, the unsettled sign-out or revoke of a device this entry
|
|
15
|
+
* replaced, so a live credential is never dropped on a device change.
|
|
16
|
+
*
|
|
17
|
+
* The file is separate from `nexus-credentials.json`, which stays format
|
|
18
|
+
* version 1 so a downgraded CLI can read it and never touches this file
|
|
19
|
+
* (finding 8).
|
|
20
|
+
*
|
|
21
|
+
* Safety rules, all enforced here:
|
|
22
|
+
*
|
|
23
|
+
* - **Sealed at rest.** Every token and private key is encrypted with
|
|
24
|
+
* AES-256-GCM under the machine key (`crypto/credentials.ts`,
|
|
25
|
+
* KDF id `nexus-device:` + `JSON.stringify([origin, userId])`, which no
|
|
26
|
+
* choice of origin and user id can make collide). Each value carries its
|
|
27
|
+
* field path (for example `current.token`) as GCM associated data, so
|
|
28
|
+
* values cannot be swapped between fields either. A copy of the file on
|
|
29
|
+
* another machine, or under another (origin, user), cannot be opened. The
|
|
30
|
+
* file is also excluded from every backup bundle. Read paths (`get`,
|
|
31
|
+
* `list`) never create the machine key or salt: without them an entry
|
|
32
|
+
* reads as unreadable. A lost key never blocks re-login: the login discards
|
|
33
|
+
* the unreadable entry ({@link NexusDeviceTransaction.discardUnreadable}).
|
|
34
|
+
* - **Owner-only.** The file is created 0600 (`O_EXCL | O_NOFOLLOW`). Every
|
|
35
|
+
* read opens it without following links and checks the open descriptor:
|
|
36
|
+
* a regular file, one link, owned by this user, no group or other bits.
|
|
37
|
+
* The CLEO home must be owned by this user, inside a parent no other user
|
|
38
|
+
* can rewrite; a home the user owns that is group- or world-writable is
|
|
39
|
+
* tightened with `chmod go-w` on the first write, with a warning in
|
|
40
|
+
* {@link NexusDeviceTransaction.warnings}. Each refusal names the fix.
|
|
41
|
+
* - **Locked, re-read, atomic.** Every mutation runs through
|
|
42
|
+
* {@link NexusDeviceStore.update}: it takes this store's own cross-process
|
|
43
|
+
* lock (about a minute of waiting, see {@link NexusDeviceStoreOptions}),
|
|
44
|
+
* re-reads the file **after** acquiring it (M6, N1), and writes a temp file
|
|
45
|
+
* that is fsynced and renamed over the original, then fsyncs the directory.
|
|
46
|
+
* On macOS, Node's fsync is `F_FULLFSYNC` (libuv), so the data reaches the
|
|
47
|
+
* disk, not only the drive cache. No rotated backups are made; any found are
|
|
48
|
+
* purged (L3), and stale temp files are swept under the lock.
|
|
49
|
+
* - **Never held across a network call (contract v2.9).** The lock covers
|
|
50
|
+
* only a local read-modify-write. Every change that involves the server
|
|
51
|
+
* follows one order: under the lock, re-read and persist the intent (a
|
|
52
|
+
* `pending` credential before E8, §2.5 step 1; the E1 intent before E1,
|
|
53
|
+
* §3.3 step 5); **return from `update`, releasing the lock**; make the call
|
|
54
|
+
* (E1, E8, E9, E10) with a plain `AbortSignal.timeout(...)`; then call
|
|
55
|
+
* `update` again and re-check with a compare-and-swap (is the entry still
|
|
56
|
+
* the one this process wrote?) before promoting or clearing. `update`
|
|
57
|
+
* writes when `fn` returns, so the intent is durable before the request.
|
|
58
|
+
* - **Lock loss aborts.** If the lock is taken over (a stale-lock takeover
|
|
59
|
+
* after the machine slept, say), {@link NexusDeviceTransaction.signal} is
|
|
60
|
+
* aborted, no further write happens, and `update` rejects with
|
|
61
|
+
* `E_NEXUS_DEVICE_LOCK_COMPROMISED`.
|
|
62
|
+
* - **No downgrade.** A file whose `version` is newer than this CLI knows is
|
|
63
|
+
* refused, never read as empty and never rewritten. A malformed file is
|
|
64
|
+
* refused the same way. Unknown fields of a known version, at any depth,
|
|
65
|
+
* are kept on rewrite.
|
|
66
|
+
* - **Unreadable entries are carried, not dropped.** An entry whose secrets do
|
|
67
|
+
* not open under this machine's key (copied in from another machine) is
|
|
68
|
+
* written back byte-identical and never decrypted, rewritten or deleted.
|
|
69
|
+
* Updates to other entries go ahead; only an operation on that entry fails
|
|
70
|
+
* (`E_NEXUS_DEVICE_UNSEAL_FAILED`), and {@link NexusDeviceStore.list}
|
|
71
|
+
* reports it as an {@link UnreadableNexusDevice}.
|
|
72
|
+
* - **Fenced writes.** Before each rename the file is compared (inode, size,
|
|
73
|
+
* mtime in ns) with what this transaction last read or wrote; a change
|
|
74
|
+
* made by anyone else aborts with `E_NEXUS_DEVICE_LOCK_COMPROMISED`.
|
|
75
|
+
* - **Sealed handles.** Nothing handed out prints a secret: entries from
|
|
76
|
+
* {@link NexusDeviceTransaction.get}, {@link SealedNexusDevice.unseal} and
|
|
77
|
+
* the `apply*` transitions carry non-enumerable `toJSON` and `util.inspect`
|
|
78
|
+
* guards, and {@link NexusDeviceEnrolment} holds its secrets privately.
|
|
79
|
+
* {@link redactNexusDeviceSecrets} masks credentials and private keys in any
|
|
80
|
+
* diagnostic text. The guards are non-enumerable, so spreading a guarded
|
|
81
|
+
* entry (`{ ...entry }`) produces an unguarded copy: T12868–T12871 must
|
|
82
|
+
* never spread an entry, or any part of one, into a log line or envelope.
|
|
83
|
+
*
|
|
84
|
+
* Behaviour that uses it is on by default and switched off with
|
|
85
|
+
* `CLEO_NEXUS_DEVICE=0` ({@link isNexusDeviceEnabled}, T12904): login
|
|
86
|
+
* enrolment and the 9.24 upgrade (`nexus-enrol.ts`, T12868), logout and
|
|
87
|
+
* revoke (`nexus-logout.ts`, T12870), then rotation and the cloud reads
|
|
88
|
+
* (T12869, T12871).
|
|
89
|
+
*
|
|
90
|
+
* @task T12867
|
|
91
|
+
* @epic T12323
|
|
92
|
+
*/
|
|
93
|
+
import { inspect } from 'node:util';
|
|
94
|
+
import { z } from 'zod';
|
|
95
|
+
/** File name of the store, directly under the CLEO home. */
|
|
96
|
+
export declare const NEXUS_DEVICE_FILE = "nexus-device.json";
|
|
97
|
+
/** The file format version this CLI reads and writes. A newer version is refused. */
|
|
98
|
+
export declare const NEXUS_DEVICE_FILE_VERSION = 1;
|
|
99
|
+
/** Environment switch that turns on the device-credential behaviour (T12867–T12871). */
|
|
100
|
+
export declare const NEXUS_DEVICE_ENV = "CLEO_NEXUS_DEVICE";
|
|
101
|
+
/**
|
|
102
|
+
* The device credential format (contract §2.2, `DEVICE_CREDENTIAL` in §4.1):
|
|
103
|
+
* `cnx_d1_` followed by 43 base64url characters (32 random bytes).
|
|
104
|
+
*/
|
|
105
|
+
export declare const NEXUS_DEVICE_CREDENTIAL_PATTERN: RegExp;
|
|
106
|
+
/** Every scope a device credential can carry (contract §2.3, `DeviceScope`). */
|
|
107
|
+
export declare const NEXUS_DEVICE_SCOPES: readonly ["account:read", "devices:read", "projects:read", "projects:write", "sync:read", "sync:write", "keys:read", "keys:write"];
|
|
108
|
+
/** Credential profiles (contract §2.3, `DeviceProfile`). */
|
|
109
|
+
export declare const NEXUS_DEVICE_PROFILES: readonly ["device", "read-only"];
|
|
110
|
+
/** Why an E1 call is in flight: an interactive login (§3.3) or the 9.24 auto-upgrade (§3.4). */
|
|
111
|
+
export declare const NEXUS_DEVICE_INTENT_KINDS: readonly ["login", "upgrade"];
|
|
112
|
+
/**
|
|
113
|
+
* Most credentials one sign-out or revoke slot may hold. A transition that
|
|
114
|
+
* would need more refuses (`E_NEXUS_DEVICE_SLOT_FULL`) instead of dropping a
|
|
115
|
+
* live credential.
|
|
116
|
+
*/
|
|
117
|
+
export declare const NEXUS_DEVICE_MAX_SLOT_CREDENTIALS = 8;
|
|
118
|
+
/** Most retired-device items one entry may hold; beyond it a device change refuses. */
|
|
119
|
+
export declare const NEXUS_DEVICE_MAX_RETIRED = 8;
|
|
120
|
+
/** Default time {@link NexusDeviceStore.update} waits for the lock. */
|
|
121
|
+
export declare const NEXUS_DEVICE_LOCK_WAIT_MS = 60000;
|
|
122
|
+
/** Default age after which a lock whose holder stopped refreshing it may be taken over. */
|
|
123
|
+
export declare const NEXUS_DEVICE_LOCK_STALE_MS = 30000;
|
|
124
|
+
declare const entrySchema: z.ZodObject<{
|
|
125
|
+
deviceId: z.ZodString;
|
|
126
|
+
createdAt: z.ZodISODateTime;
|
|
127
|
+
keys: z.ZodNullable<z.ZodObject<{
|
|
128
|
+
encryption: z.ZodObject<{
|
|
129
|
+
publicKey: z.ZodString;
|
|
130
|
+
privateKey: z.ZodString;
|
|
131
|
+
}, z.core.$loose>;
|
|
132
|
+
signing: z.ZodObject<{
|
|
133
|
+
publicKey: z.ZodString;
|
|
134
|
+
privateKey: z.ZodString;
|
|
135
|
+
}, z.core.$loose>;
|
|
136
|
+
}, z.core.$loose>>;
|
|
137
|
+
current: z.ZodNullable<z.ZodObject<{
|
|
138
|
+
credentialId: z.ZodString;
|
|
139
|
+
token: z.ZodString;
|
|
140
|
+
profile: z.ZodEnum<{
|
|
141
|
+
device: "device";
|
|
142
|
+
"read-only": "read-only";
|
|
143
|
+
}>;
|
|
144
|
+
scopes: z.ZodArray<z.ZodEnum<{
|
|
145
|
+
"account:read": "account:read";
|
|
146
|
+
"devices:read": "devices:read";
|
|
147
|
+
"projects:read": "projects:read";
|
|
148
|
+
"projects:write": "projects:write";
|
|
149
|
+
"sync:read": "sync:read";
|
|
150
|
+
"sync:write": "sync:write";
|
|
151
|
+
"keys:read": "keys:read";
|
|
152
|
+
"keys:write": "keys:write";
|
|
153
|
+
}>>;
|
|
154
|
+
createdAt: z.ZodISODateTime;
|
|
155
|
+
}, z.core.$loose>>;
|
|
156
|
+
pending: z.ZodNullable<z.ZodObject<{
|
|
157
|
+
/** Server id of the new credential; `null` until E8 answers (the client minted the token). */
|
|
158
|
+
credentialId: z.ZodNullable<z.ZodString>;
|
|
159
|
+
token: z.ZodString;
|
|
160
|
+
createdAt: z.ZodISODateTime;
|
|
161
|
+
}, z.core.$loose>>;
|
|
162
|
+
pendingSignOut: z.ZodNullable<z.ZodObject<{
|
|
163
|
+
credentials: z.ZodArray<z.ZodObject<{
|
|
164
|
+
credentialId: z.ZodNullable<z.ZodString>;
|
|
165
|
+
token: z.ZodString;
|
|
166
|
+
}, z.core.$loose>>;
|
|
167
|
+
requestedAt: z.ZodISODateTime;
|
|
168
|
+
}, z.core.$loose>>;
|
|
169
|
+
pendingRevoke: z.ZodNullable<z.ZodObject<{
|
|
170
|
+
credentials: z.ZodArray<z.ZodObject<{
|
|
171
|
+
credentialId: z.ZodNullable<z.ZodString>;
|
|
172
|
+
token: z.ZodString;
|
|
173
|
+
}, z.core.$loose>>;
|
|
174
|
+
requestedAt: z.ZodISODateTime;
|
|
175
|
+
}, z.core.$loose>>;
|
|
176
|
+
retired: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
177
|
+
deviceId: z.ZodString;
|
|
178
|
+
kind: z.ZodEnum<{
|
|
179
|
+
"sign-out": "sign-out";
|
|
180
|
+
revoke: "revoke";
|
|
181
|
+
}>;
|
|
182
|
+
credentials: z.ZodArray<z.ZodObject<{
|
|
183
|
+
credentialId: z.ZodNullable<z.ZodString>;
|
|
184
|
+
token: z.ZodString;
|
|
185
|
+
}, z.core.$loose>>;
|
|
186
|
+
requestedAt: z.ZodISODateTime;
|
|
187
|
+
}, z.core.$loose>>>;
|
|
188
|
+
/**
|
|
189
|
+
* An E1 call in flight (contract §3.3 step 5, §3.4 step 1): persisted under
|
|
190
|
+
* the lock before E1 is sent, cleared at step 7. Holds no secret.
|
|
191
|
+
*/
|
|
192
|
+
/**
|
|
193
|
+
* A credential E1 issued to this process while the file already held
|
|
194
|
+
* another one for the same device, and the server could not yet say which
|
|
195
|
+
* is live (§3.3 step 7, ruling A). Kept, sealed, until E2 settles it, so
|
|
196
|
+
* the only live credential is never lost.
|
|
197
|
+
*/
|
|
198
|
+
raceCandidate: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
199
|
+
credentialId: z.ZodString;
|
|
200
|
+
token: z.ZodString;
|
|
201
|
+
profile: z.ZodEnum<{
|
|
202
|
+
device: "device";
|
|
203
|
+
"read-only": "read-only";
|
|
204
|
+
}>;
|
|
205
|
+
scopes: z.ZodArray<z.ZodEnum<{
|
|
206
|
+
"account:read": "account:read";
|
|
207
|
+
"devices:read": "devices:read";
|
|
208
|
+
"projects:read": "projects:read";
|
|
209
|
+
"projects:write": "projects:write";
|
|
210
|
+
"sync:read": "sync:read";
|
|
211
|
+
"sync:write": "sync:write";
|
|
212
|
+
"keys:read": "keys:read";
|
|
213
|
+
"keys:write": "keys:write";
|
|
214
|
+
}>>;
|
|
215
|
+
createdAt: z.ZodISODateTime;
|
|
216
|
+
}, z.core.$loose>>>;
|
|
217
|
+
enrolIntent: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
218
|
+
deviceId: z.ZodString;
|
|
219
|
+
kind: z.ZodEnum<{
|
|
220
|
+
upgrade: "upgrade";
|
|
221
|
+
login: "login";
|
|
222
|
+
}>;
|
|
223
|
+
/** Random id of the process that wrote it (a CAS token). */
|
|
224
|
+
owner: z.ZodString;
|
|
225
|
+
startedAt: z.ZodISODateTime;
|
|
226
|
+
}, z.core.$loose>>>;
|
|
227
|
+
}, z.core.$loose>;
|
|
228
|
+
/** One (origin, user) entry of the device file, secrets in plaintext (in memory only). */
|
|
229
|
+
export type NexusDeviceEntry = z.infer<typeof entrySchema>;
|
|
230
|
+
/** The device's two key pairs (contract §2.4). */
|
|
231
|
+
export type NexusDeviceKeys = NonNullable<NexusDeviceEntry['keys']>;
|
|
232
|
+
/** A stored key pair, base64 (32 raw bytes each). */
|
|
233
|
+
export type NexusDeviceKeyPair = NexusDeviceKeys['encryption'];
|
|
234
|
+
/** The credential in use, as issued by E1 or promoted from `pending`. */
|
|
235
|
+
export type NexusDeviceCurrentCredential = NonNullable<NexusDeviceEntry['current']>;
|
|
236
|
+
/** A client-minted rotation credential awaiting confirmation (contract §2.5). */
|
|
237
|
+
export type NexusDevicePendingCredential = NonNullable<NexusDeviceEntry['pending']>;
|
|
238
|
+
/** A `pendingSignOut` or `pendingRevoke` slot (contract §3.5). */
|
|
239
|
+
export type NexusDevicePendingEnd = NonNullable<NexusDeviceEntry['pendingSignOut']>;
|
|
240
|
+
/** One credential kept in a sign-out or revoke slot. */
|
|
241
|
+
export type NexusDeviceSlotCredential = NexusDevicePendingEnd['credentials'][number];
|
|
242
|
+
/** An unsettled sign-out or revoke of a device an entry replaced. */
|
|
243
|
+
export type NexusDeviceRetired = NonNullable<NexusDeviceEntry['retired']>[number];
|
|
244
|
+
/** An E1 call in flight for an entry (§3.3 step 5, §3.4 step 1). */
|
|
245
|
+
export type NexusDeviceEnrolIntent = NonNullable<NexusDeviceEntry['enrolIntent']>;
|
|
246
|
+
/** A racing login's unsettled credential (§3.3 step 7). */
|
|
247
|
+
export type NexusDeviceRaceCandidate = NonNullable<NexusDeviceEntry['raceCandidate']>;
|
|
248
|
+
/** A device credential profile. */
|
|
249
|
+
export type NexusDeviceProfile = (typeof NEXUS_DEVICE_PROFILES)[number];
|
|
250
|
+
/** A device credential scope. */
|
|
251
|
+
export type NexusDeviceScope = (typeof NEXUS_DEVICE_SCOPES)[number];
|
|
252
|
+
/** Codes carried by {@link NexusDeviceStoreError}. */
|
|
253
|
+
export type NexusDeviceStoreErrorCode = 'E_NEXUS_DEVICE_FILE_PERMISSIONS' | 'E_NEXUS_DEVICE_FILE_UNSAFE' | 'E_NEXUS_DEVICE_FILE_SYMLINK' | 'E_NEXUS_DEVICE_DIR_UNSAFE' | 'E_NEXUS_DEVICE_FILE_NEWER' | 'E_NEXUS_DEVICE_FILE_INVALID' | 'E_NEXUS_DEVICE_UNSEAL_FAILED' | 'E_NEXUS_DEVICE_MACHINE_KEY' | 'E_NEXUS_DEVICE_IO' | 'E_NEXUS_DEVICE_ENTRY_INVALID' | 'E_NEXUS_DEVICE_PENDING_EXISTS' | 'E_NEXUS_DEVICE_REVOKE_PENDING' | 'E_NEXUS_DEVICE_SLOT_FULL' | 'E_NEXUS_DEVICE_BUSY' | 'E_NEXUS_DEVICE_REENTRANT' | 'E_NEXUS_DEVICE_LOCK_COMPROMISED';
|
|
254
|
+
/** Thrown when the device file cannot be read or written safely. Never carries a secret. */
|
|
255
|
+
export declare class NexusDeviceStoreError extends Error {
|
|
256
|
+
/** Machine-readable reason. */
|
|
257
|
+
readonly code: NexusDeviceStoreErrorCode;
|
|
258
|
+
/**
|
|
259
|
+
* @param code - Machine-readable reason.
|
|
260
|
+
* @param message - Human-readable message; passed through {@link redactNexusDeviceSecrets}.
|
|
261
|
+
*/
|
|
262
|
+
constructor(code: NexusDeviceStoreErrorCode, message: string);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Whether the device-credential login is on. It is the default (T12904);
|
|
266
|
+
* `CLEO_NEXUS_DEVICE=0` (or `false`, `off`, `no`) switches back to the 9.24
|
|
267
|
+
* session login. Status and logout always handle a stored device credential.
|
|
268
|
+
*
|
|
269
|
+
* @param env - Environment to read; defaults to `process.env`.
|
|
270
|
+
* @returns `false` only for an off value.
|
|
271
|
+
*/
|
|
272
|
+
export declare function isNexusDeviceEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
273
|
+
/**
|
|
274
|
+
* Absolute path of the device store.
|
|
275
|
+
*
|
|
276
|
+
* @returns `<cleoHome>/nexus-device.json`, through `@cleocode/paths` (arch gate 2).
|
|
277
|
+
*/
|
|
278
|
+
export declare function nexusDevicePath(): string;
|
|
279
|
+
/**
|
|
280
|
+
* Mask a device credential down to a safe preview.
|
|
281
|
+
*
|
|
282
|
+
* The preview keeps the last 4 characters (24 of the token's 256 random
|
|
283
|
+
* bits), enough to tell two credentials apart in a diagnostic and far too
|
|
284
|
+
* little to guess the rest. The same rule as `maskNexusToken`.
|
|
285
|
+
*
|
|
286
|
+
* @param value - Plaintext credential (read transiently, not retained).
|
|
287
|
+
* @returns `cnx_d1_…` plus the last 4 characters, or `…` for anything shorter.
|
|
288
|
+
*/
|
|
289
|
+
export declare function maskNexusDeviceCredential(value: string): string;
|
|
290
|
+
/**
|
|
291
|
+
* Mask every device credential and every private-key field in `text`. Use on
|
|
292
|
+
* any diagnostic string (errors, logs, envelopes) that might hold one.
|
|
293
|
+
*
|
|
294
|
+
* @param text - Arbitrary text.
|
|
295
|
+
* @returns The text with every `cnx_d1_…` credential masked and every
|
|
296
|
+
* `privateKey` value replaced by `[redacted]`.
|
|
297
|
+
*/
|
|
298
|
+
export declare function redactNexusDeviceSecrets(text: string): string;
|
|
299
|
+
/**
|
|
300
|
+
* Attach the store's non-enumerable `toJSON` and `util.inspect` guards to any
|
|
301
|
+
* value that holds device secrets (tokens, private keys), at every depth, so
|
|
302
|
+
* printing or serialising it shows only the redacted view. Never spread the
|
|
303
|
+
* result: a spread drops the guards.
|
|
304
|
+
*
|
|
305
|
+
* @param value - An object holding `token` or `privateKey` fields.
|
|
306
|
+
* @returns The same object, guarded.
|
|
307
|
+
*/
|
|
308
|
+
export declare function guardNexusDeviceSecrets<T>(value: T): T;
|
|
309
|
+
/**
|
|
310
|
+
* Secret-free diagnostic view of one entry. Public keys are shown; private
|
|
311
|
+
* keys and tokens never are.
|
|
312
|
+
*
|
|
313
|
+
* @param entry - A stored entry.
|
|
314
|
+
* @returns A view safe to print, log or put in an envelope.
|
|
315
|
+
*/
|
|
316
|
+
export declare function redactNexusDeviceEntry(entry: NexusDeviceEntry): Record<string, unknown>;
|
|
317
|
+
/**
|
|
318
|
+
* Opaque read handle to one entry. Tokens and private keys are held in a
|
|
319
|
+
* private field; serialising or inspecting the handle yields
|
|
320
|
+
* {@link redactNexusDeviceEntry} only. The accessors are for the code that
|
|
321
|
+
* builds an `Authorization` header or signs with the device key.
|
|
322
|
+
*/
|
|
323
|
+
export declare class SealedNexusDevice {
|
|
324
|
+
#private;
|
|
325
|
+
/** Always `true`: this entry opened under this machine's key. */
|
|
326
|
+
readonly readable = true;
|
|
327
|
+
/** API origin. */
|
|
328
|
+
readonly origin: string;
|
|
329
|
+
/** Nexus user id. */
|
|
330
|
+
readonly userId: string;
|
|
331
|
+
/** Nexus device id (UUIDv7). */
|
|
332
|
+
readonly deviceId: string;
|
|
333
|
+
/**
|
|
334
|
+
* @param origin - API origin.
|
|
335
|
+
* @param userId - Nexus user id.
|
|
336
|
+
* @param entry - Stored entry; captured privately as a copy.
|
|
337
|
+
*/
|
|
338
|
+
constructor(origin: string, userId: string, entry: NexusDeviceEntry);
|
|
339
|
+
/** The current credential's token, or `null`. Call only where the request is built. */
|
|
340
|
+
currentBearer(): string | null;
|
|
341
|
+
/** The pending rotation credential's token, or `null` (§2.5: try it before `current`). */
|
|
342
|
+
pendingBearer(): string | null;
|
|
343
|
+
/**
|
|
344
|
+
* A guarded copy of the whole entry, secrets included, for the flows that
|
|
345
|
+
* must act on them. Printing or serialising it shows the redacted view.
|
|
346
|
+
*/
|
|
347
|
+
unseal(): NexusDeviceEntry;
|
|
348
|
+
/** Secret-free JSON form. */
|
|
349
|
+
toJSON(): Record<string, unknown>;
|
|
350
|
+
/** Secret-free string form. */
|
|
351
|
+
toString(): string;
|
|
352
|
+
/** Secret-free `util.inspect` form. */
|
|
353
|
+
[inspect.custom](): string;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* What E1 returned, as it is about to be stored (contract §3.3 step 7). The
|
|
357
|
+
* issued credential and any device keys are held in private fields, so the
|
|
358
|
+
* value never prints a secret.
|
|
359
|
+
*/
|
|
360
|
+
export declare class NexusDeviceEnrolment {
|
|
361
|
+
#private;
|
|
362
|
+
/** The device id that was enrolled. */
|
|
363
|
+
readonly deviceId: string;
|
|
364
|
+
/**
|
|
365
|
+
* @param input - The enrolled device id, the keys when this enrolment
|
|
366
|
+
* created or confirmed them (else `null`), and the credential E1 issued.
|
|
367
|
+
* @throws {NexusDeviceStoreError} `E_NEXUS_DEVICE_ENTRY_INVALID` on a malformed credential or keys.
|
|
368
|
+
*/
|
|
369
|
+
constructor(input: {
|
|
370
|
+
readonly deviceId: string;
|
|
371
|
+
readonly keys: NexusDeviceKeys | null;
|
|
372
|
+
readonly credential: NexusDeviceCurrentCredential;
|
|
373
|
+
});
|
|
374
|
+
/** A guarded copy of the keys, or `null`. */
|
|
375
|
+
keys(): NexusDeviceKeys | null;
|
|
376
|
+
/** A guarded copy of the issued credential. */
|
|
377
|
+
credential(): NexusDeviceCurrentCredential;
|
|
378
|
+
/** Secret-free JSON form. */
|
|
379
|
+
toJSON(): Record<string, unknown>;
|
|
380
|
+
/** Secret-free string form. */
|
|
381
|
+
toString(): string;
|
|
382
|
+
/** Secret-free `util.inspect` form. */
|
|
383
|
+
[inspect.custom](): string;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Throw unless a login may enrol `deviceId` over `entry`. While a revoke of
|
|
387
|
+
* the same device is unconfirmed, enrolling would re-activate a device the
|
|
388
|
+
* user asked to revoke: retry E10 first (`cleo logout nexus --revoke`), and
|
|
389
|
+
* if it cannot be confirmed, revoke the device on cleocode.dev. Call this
|
|
390
|
+
* before E1 is sent; {@link applyEnrolment} checks it again.
|
|
391
|
+
*
|
|
392
|
+
* @param entry - The entry read under the lock, or `null`.
|
|
393
|
+
* @param deviceId - The device id the login would enrol.
|
|
394
|
+
* @throws {NexusDeviceStoreError} `E_NEXUS_DEVICE_REVOKE_PENDING`.
|
|
395
|
+
*/
|
|
396
|
+
export declare function assertEnrolmentAllowed(entry: NexusDeviceEntry | null, deviceId: string): void;
|
|
397
|
+
/**
|
|
398
|
+
* Record a successful E1 (M6).
|
|
399
|
+
*
|
|
400
|
+
* - **Same device:** store the new credential and clear `pending` and
|
|
401
|
+
* `pendingSignOut`, because E1 revoked every older credential of the device
|
|
402
|
+
* on the server. Refused while `pendingRevoke` is set
|
|
403
|
+
* ({@link assertEnrolmentAllowed}).
|
|
404
|
+
* - **New device** (a fresh identity replaced the entry's device): the old
|
|
405
|
+
* device's unsettled revoke, and any credential it still holds, move to
|
|
406
|
+
* `retired`, so they are retried rather than dropped.
|
|
407
|
+
*
|
|
408
|
+
* @param entry - The entry read under the lock, or `null` for a new identity.
|
|
409
|
+
* @param enrolment - E1's result.
|
|
410
|
+
* @param now - Clock, for `createdAt` of a new entry and retired items.
|
|
411
|
+
* @returns The updated entry, guarded.
|
|
412
|
+
* @throws {NexusDeviceStoreError} `E_NEXUS_DEVICE_REVOKE_PENDING`, or
|
|
413
|
+
* `E_NEXUS_DEVICE_SLOT_FULL` when `retired` is full.
|
|
414
|
+
*/
|
|
415
|
+
export declare function applyEnrolment(entry: NexusDeviceEntry | null, enrolment: NexusDeviceEnrolment, now?: Date): NexusDeviceEntry;
|
|
416
|
+
/**
|
|
417
|
+
* Persist the identity a login or upgrade is about to enrol, with its E1
|
|
418
|
+
* intent (§3.3 step 5, §3.4 step 1), before E1 is sent.
|
|
419
|
+
*
|
|
420
|
+
* - **No entry:** a new identity with `deviceId` and `keys`.
|
|
421
|
+
* - **Same device:** the intent is recorded; `keys` fill in only when the
|
|
422
|
+
* entry has none (lost keys, §3.3 step 6 `device-keys-changed`). Every
|
|
423
|
+
* credential and slot is kept: E1 has not run yet.
|
|
424
|
+
* - **Another device** (a fresh identity after 409 `device-revoked`,
|
|
425
|
+
* `device-other-account`, `device-id-taken` or `device-keys-changed`): the
|
|
426
|
+
* old device's credentials and unsettled slots move to `retired`, as in
|
|
427
|
+
* {@link applyEnrolment}, so nothing live is dropped.
|
|
428
|
+
*
|
|
429
|
+
* @param entry - The entry read under the lock, or `null`.
|
|
430
|
+
* @param identity - The device id and keys to enrol, and the intent.
|
|
431
|
+
* @param now - Clock.
|
|
432
|
+
* @returns The updated entry, guarded.
|
|
433
|
+
* @throws {NexusDeviceStoreError} `E_NEXUS_DEVICE_REVOKE_PENDING` for the same
|
|
434
|
+
* device while a revoke is unconfirmed, or `E_NEXUS_DEVICE_SLOT_FULL`.
|
|
435
|
+
*/
|
|
436
|
+
export declare function applyEnrolIntent(entry: NexusDeviceEntry | null, identity: {
|
|
437
|
+
readonly deviceId: string;
|
|
438
|
+
readonly keys: NexusDeviceKeys;
|
|
439
|
+
readonly intent: NexusDeviceEnrolIntent;
|
|
440
|
+
}, now?: Date): NexusDeviceEntry;
|
|
441
|
+
/**
|
|
442
|
+
* Clear an E1 intent, as a compare-and-swap on its owner: another process's
|
|
443
|
+
* intent is never cleared.
|
|
444
|
+
*
|
|
445
|
+
* @param entry - The entry read under the lock.
|
|
446
|
+
* @param owner - The intent owner this process wrote.
|
|
447
|
+
* @returns The updated entry (guarded), or the entry unchanged.
|
|
448
|
+
*/
|
|
449
|
+
export declare function applyClearEnrolIntent(entry: NexusDeviceEntry, owner: string): NexusDeviceEntry;
|
|
450
|
+
/**
|
|
451
|
+
* Drop `current`, as a compare-and-swap on its credential id: used when the
|
|
452
|
+
* server refused it and no other credential is live (§3.3 step 7, both
|
|
453
|
+
* racing credentials answered 401). Nothing is moved to a sign-out slot: a
|
|
454
|
+
* refused credential needs no E9.
|
|
455
|
+
*
|
|
456
|
+
* @param entry - The entry read under the lock.
|
|
457
|
+
* @param credentialId - The credential id the caller saw refused.
|
|
458
|
+
* @returns The updated entry (guarded), or the entry unchanged when `current` moved on.
|
|
459
|
+
*/
|
|
460
|
+
export declare function applyDropCurrent(entry: NexusDeviceEntry, credentialId: string): NexusDeviceEntry;
|
|
461
|
+
/**
|
|
462
|
+
* Refresh this process's E1 intent right before E1 is sent (compare-and-swap
|
|
463
|
+
* on the owner), so a live intent never looks stale while its owner is still
|
|
464
|
+
* working (§3.4 step 1).
|
|
465
|
+
*
|
|
466
|
+
* @param entry - The entry read under the lock.
|
|
467
|
+
* @param owner - The intent owner this process wrote.
|
|
468
|
+
* @param now - Clock.
|
|
469
|
+
* @returns The updated entry (guarded), or `null` when the intent is no
|
|
470
|
+
* longer this process's (another process replaced or took it over).
|
|
471
|
+
*/
|
|
472
|
+
export declare function applyRefreshEnrolIntent(entry: NexusDeviceEntry, owner: string, now?: Date): NexusDeviceEntry | null;
|
|
473
|
+
/**
|
|
474
|
+
* Park a racing login's credential beside `current` until E2 settles which is
|
|
475
|
+
* live (§3.3 step 7). An existing candidate is replaced only by a newer one;
|
|
476
|
+
* the older is dropped, because E1 re-enrolment revoked it.
|
|
477
|
+
*
|
|
478
|
+
* @param entry - The entry read under the lock.
|
|
479
|
+
* @param candidate - The credential E1 issued to this process.
|
|
480
|
+
* @returns The updated entry, guarded.
|
|
481
|
+
*/
|
|
482
|
+
export declare function applySetRaceCandidate(entry: NexusDeviceEntry, candidate: NexusDeviceRaceCandidate): NexusDeviceEntry;
|
|
483
|
+
/**
|
|
484
|
+
* Drop the race candidate, as a compare-and-swap on its credential id (it
|
|
485
|
+
* lost: the server refused it, or `current` is newer).
|
|
486
|
+
*
|
|
487
|
+
* @param entry - The entry read under the lock.
|
|
488
|
+
* @param credentialId - The candidate the caller settled.
|
|
489
|
+
* @returns The updated entry (guarded), or the entry unchanged.
|
|
490
|
+
*/
|
|
491
|
+
export declare function applyDropRaceCandidate(entry: NexusDeviceEntry, credentialId: string): NexusDeviceEntry;
|
|
492
|
+
/**
|
|
493
|
+
* Store a freshly minted rotation credential as `pending` (§2.5 step 1). A
|
|
494
|
+
* second pending credential is never minted: when one exists the caller must
|
|
495
|
+
* replay it instead. The caller MUST let `update` write it (return from the
|
|
496
|
+
* transaction, releasing the lock) before sending E8, never send E8 inside it.
|
|
497
|
+
*
|
|
498
|
+
* @param entry - The entry read under the lock.
|
|
499
|
+
* @param newToken - The client-minted `cnx_d1_…` token.
|
|
500
|
+
* @param now - Clock.
|
|
501
|
+
* @returns The updated entry, guarded.
|
|
502
|
+
* @throws {NexusDeviceStoreError} `E_NEXUS_DEVICE_PENDING_EXISTS` when `pending` is already set.
|
|
503
|
+
*/
|
|
504
|
+
export declare function applyPendingRotation(entry: NexusDeviceEntry, newToken: string, now?: Date): NexusDeviceEntry;
|
|
505
|
+
/**
|
|
506
|
+
* Promote `pending` to `current` (§2.5 step 5, or its first successful use),
|
|
507
|
+
* as a compare-and-swap: only when `pending` still holds `expectedToken`.
|
|
508
|
+
* The profile and scopes carry over: E8 issues the new credential with the
|
|
509
|
+
* same profile and scopes as the one it replaced.
|
|
510
|
+
*
|
|
511
|
+
* @param entry - The entry read under the lock.
|
|
512
|
+
* @param expectedToken - The pending token the caller saw succeed.
|
|
513
|
+
* @param credentialId - The server id of the promoted credential.
|
|
514
|
+
* @returns The updated entry (guarded), or the entry unchanged when `pending`
|
|
515
|
+
* moved on or there is nothing to promote.
|
|
516
|
+
*/
|
|
517
|
+
export declare function applyPromotePending(entry: NexusDeviceEntry, expectedToken: string, credentialId: string): NexusDeviceEntry;
|
|
518
|
+
/**
|
|
519
|
+
* CAS-drop `pending`: only when it still holds `expectedToken` (§2.5).
|
|
520
|
+
*
|
|
521
|
+
* @param entry - The entry read under the lock.
|
|
522
|
+
* @param expectedToken - The pending token the caller saw fail.
|
|
523
|
+
* @returns The updated entry (guarded), or the entry unchanged when `pending` moved on.
|
|
524
|
+
*/
|
|
525
|
+
export declare function applyDropPending(entry: NexusDeviceEntry, expectedToken: string): NexusDeviceEntry;
|
|
526
|
+
/**
|
|
527
|
+
* Begin `cleo logout nexus` (§3.5, M3): move the live credentials into
|
|
528
|
+
* `pendingSignOut`, newest first (`pending`, then `current`, then any
|
|
529
|
+
* credential an earlier unsettled sign-out holds), so they are used for
|
|
530
|
+
* nothing but the E9 retry. The device keys stay.
|
|
531
|
+
*
|
|
532
|
+
* @param entry - The entry read under the lock.
|
|
533
|
+
* @param now - Clock.
|
|
534
|
+
* @returns The updated entry (guarded), unchanged when it holds no credential at all.
|
|
535
|
+
* @throws {NexusDeviceStoreError} `E_NEXUS_DEVICE_SLOT_FULL` rather than drop a credential.
|
|
536
|
+
*/
|
|
537
|
+
export declare function applyBeginSignOut(entry: NexusDeviceEntry, now?: Date): NexusDeviceEntry;
|
|
538
|
+
/**
|
|
539
|
+
* Begin `cleo logout nexus --revoke` (§3.5, M3): move every credential the
|
|
540
|
+
* entry holds into `pendingRevoke`, newest first: `pending`, `current`, an
|
|
541
|
+
* unsettled sign-out's, then an earlier unsettled revoke's. The entry, and its
|
|
542
|
+
* keys, stay until E10 is confirmed.
|
|
543
|
+
*
|
|
544
|
+
* @param entry - The entry read under the lock.
|
|
545
|
+
* @param now - Clock.
|
|
546
|
+
* @returns The updated entry (guarded), unchanged when it holds no credential at all.
|
|
547
|
+
* @throws {NexusDeviceStoreError} `E_NEXUS_DEVICE_SLOT_FULL` rather than drop a credential.
|
|
548
|
+
*/
|
|
549
|
+
export declare function applyBeginRevoke(entry: NexusDeviceEntry, now?: Date): NexusDeviceEntry;
|
|
550
|
+
/**
|
|
551
|
+
* Settle a confirmed sign-out (E9 answered 200, or 401 `device-signed-out` or
|
|
552
|
+
* `device-revoked`): clear `pendingSignOut`.
|
|
553
|
+
*
|
|
554
|
+
* @param entry - The entry read under the lock.
|
|
555
|
+
* @returns The updated entry, guarded.
|
|
556
|
+
*/
|
|
557
|
+
export declare function applySignOutConfirmed(entry: NexusDeviceEntry): NexusDeviceEntry;
|
|
558
|
+
/**
|
|
559
|
+
* Forget this device locally after a revoke ended (contract §3.5, L6): the
|
|
560
|
+
* server confirmed it, found the device signed out (E10 401
|
|
561
|
+
* `device-signed-out`, v2.13), or refused every credential as dead. The keys,
|
|
562
|
+
* every credential and every slot go, so the next login enrols a fresh
|
|
563
|
+
* device id and can never re-activate the one the user asked to burn. Only
|
|
564
|
+
* `retired` survives, because those requests may still hold live credentials
|
|
565
|
+
* of other devices; the caller deletes the whole entry when there are none.
|
|
566
|
+
*
|
|
567
|
+
* @param entry - The entry read under the lock.
|
|
568
|
+
* @returns The stripped entry, guarded.
|
|
569
|
+
*/
|
|
570
|
+
export declare function applyForgetDevice(entry: NexusDeviceEntry): NexusDeviceEntry;
|
|
571
|
+
/**
|
|
572
|
+
* Whether an entry holds nothing worth keeping: no keys, no credential, no
|
|
573
|
+
* slot and no retired request (left by {@link applyForgetDevice} once its
|
|
574
|
+
* retired requests settled).
|
|
575
|
+
*
|
|
576
|
+
* @param entry - The entry read under the lock.
|
|
577
|
+
* @returns `true` when the entry can be deleted.
|
|
578
|
+
*/
|
|
579
|
+
export declare function isForgottenDevice(entry: NexusDeviceEntry): boolean;
|
|
580
|
+
/**
|
|
581
|
+
* Settle a confirmed sign-out or revoke of a retired device.
|
|
582
|
+
*
|
|
583
|
+
* @param entry - The entry read under the lock.
|
|
584
|
+
* @param deviceId - The retired device the server confirmed.
|
|
585
|
+
* @param kind - Which request was confirmed.
|
|
586
|
+
* @returns The updated entry, guarded.
|
|
587
|
+
*/
|
|
588
|
+
export declare function applyRetiredSettled(entry: NexusDeviceEntry, deviceId: string, kind: NexusDeviceRetired['kind']): NexusDeviceEntry;
|
|
589
|
+
/** Read and write access to the file, valid only inside {@link NexusDeviceStore.update}. */
|
|
590
|
+
export interface NexusDeviceTransaction {
|
|
591
|
+
/**
|
|
592
|
+
* Aborted when the lock is lost (taken over as stale). No HTTP call is ever
|
|
593
|
+
* made inside `update` (v2.9): persist, return, call, then re-lock and CAS.
|
|
594
|
+
*/
|
|
595
|
+
readonly signal: AbortSignal;
|
|
596
|
+
/**
|
|
597
|
+
* The entry for (origin of `apiUrl`, `userId`), as re-read under the lock.
|
|
598
|
+
*
|
|
599
|
+
* @returns A guarded copy; change it and pass it to {@link NexusDeviceTransaction.set}.
|
|
600
|
+
*/
|
|
601
|
+
get(apiUrl: string, userId: string): NexusDeviceEntry | null;
|
|
602
|
+
/** Replace the entry for (origin of `apiUrl`, `userId`). Validated before it is kept. */
|
|
603
|
+
set(apiUrl: string, userId: string, entry: NexusDeviceEntry): void;
|
|
604
|
+
/**
|
|
605
|
+
* CAS delete (§2.5): remove the entry only if it still has `expected`'s
|
|
606
|
+
* device id and current credential id (`null` when it must have none).
|
|
607
|
+
* Returns `true` when an entry was removed.
|
|
608
|
+
*/
|
|
609
|
+
delete(apiUrl: string, userId: string, expected: {
|
|
610
|
+
readonly deviceId: string;
|
|
611
|
+
readonly credentialId: string | null;
|
|
612
|
+
}): boolean;
|
|
613
|
+
/**
|
|
614
|
+
* Write the changes so far to disk now, atomically, with the lock still
|
|
615
|
+
* held. `update` also writes when `fn` returns, which is the normal path: a
|
|
616
|
+
* `pending` credential or an E1 intent is persisted by returning from
|
|
617
|
+
* `update` (releasing the lock) before E8 or E1 is sent. `flush` is for a
|
|
618
|
+
* long transaction that must make a change durable before it continues
|
|
619
|
+
* locally; never before a network call made with the lock held.
|
|
620
|
+
*
|
|
621
|
+
* @throws {NexusDeviceStoreError} `E_NEXUS_DEVICE_LOCK_COMPROMISED` once the lock is lost.
|
|
622
|
+
*/
|
|
623
|
+
flush(): Promise<void>;
|
|
624
|
+
/**
|
|
625
|
+
* Every (origin, userId) pair in the file. `readable: false` marks an entry
|
|
626
|
+
* this machine's key cannot open: it is carried through untouched, and any
|
|
627
|
+
* `get`, `set` or `delete` on it fails with `E_NEXUS_DEVICE_UNSEAL_FAILED`.
|
|
628
|
+
*/
|
|
629
|
+
keys(): Array<{
|
|
630
|
+
readonly origin: string;
|
|
631
|
+
readonly userId: string;
|
|
632
|
+
readonly readable: boolean;
|
|
633
|
+
}>;
|
|
634
|
+
/**
|
|
635
|
+
* Drop an entry this machine cannot open (a lost machine key, or one copied
|
|
636
|
+
* in from elsewhere), so a login can enrol afresh. Compare-and-swap on the
|
|
637
|
+
* device id, which is stored in clear. The server-side device stays active,
|
|
638
|
+
* so the returned warning (also added to {@link NexusDeviceTransaction.warnings})
|
|
639
|
+
* names the device id to revoke on cleocode.dev. A readable entry is never
|
|
640
|
+
* discarded this way; use `delete`.
|
|
641
|
+
*
|
|
642
|
+
* @returns Whether the entry was dropped, and the warning when it was.
|
|
643
|
+
*/
|
|
644
|
+
discardUnreadable(apiUrl: string, userId: string, expected: {
|
|
645
|
+
readonly deviceId: string;
|
|
646
|
+
}): {
|
|
647
|
+
readonly discarded: boolean;
|
|
648
|
+
readonly warning: string | null;
|
|
649
|
+
};
|
|
650
|
+
/**
|
|
651
|
+
* One-line warnings from this transaction (for example, the CLEO home was
|
|
652
|
+
* tightened with `chmod go-w`). The caller puts them in its result envelope.
|
|
653
|
+
*/
|
|
654
|
+
readonly warnings: readonly string[];
|
|
655
|
+
}
|
|
656
|
+
/** An entry this machine's key cannot open, as {@link NexusDeviceStore.list} reports it. No secrets. */
|
|
657
|
+
export declare class UnreadableNexusDevice {
|
|
658
|
+
/** Always `false`: the entry could not be opened here. */
|
|
659
|
+
readonly readable = false;
|
|
660
|
+
/** API origin. */
|
|
661
|
+
readonly origin: string;
|
|
662
|
+
/** Nexus user id. */
|
|
663
|
+
readonly userId: string;
|
|
664
|
+
/** The device id stored in clear, when present. */
|
|
665
|
+
readonly deviceId: string | null;
|
|
666
|
+
/** Why it could not be opened. */
|
|
667
|
+
readonly reason: string;
|
|
668
|
+
/**
|
|
669
|
+
* @param origin - API origin.
|
|
670
|
+
* @param userId - Nexus user id.
|
|
671
|
+
* @param deviceId - The device id stored in clear, when present.
|
|
672
|
+
* @param reason - Why it could not be opened.
|
|
673
|
+
*/
|
|
674
|
+
constructor(origin: string, userId: string, deviceId: string | null, reason: string);
|
|
675
|
+
}
|
|
676
|
+
/** Tuning for {@link NexusDeviceStore}. The defaults suit production; tests shorten them. */
|
|
677
|
+
export interface NexusDeviceStoreOptions {
|
|
678
|
+
/** How long `update` waits for the lock before `E_NEXUS_DEVICE_BUSY`. Default one minute. */
|
|
679
|
+
readonly lockWaitMs?: number;
|
|
680
|
+
/** Lock staleness threshold (minimum 2000). Default 30 s. */
|
|
681
|
+
readonly lockStaleMs?: number;
|
|
682
|
+
/** CLEO home whose machine key seals the secrets. Default: the store file's directory. */
|
|
683
|
+
readonly cleoHome?: string;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* File-backed store for Nexus device identities and credentials. See the
|
|
687
|
+
* module comment for the safety rules.
|
|
688
|
+
*/
|
|
689
|
+
export declare class NexusDeviceStore {
|
|
690
|
+
#private;
|
|
691
|
+
/** Absolute path of the store file. */
|
|
692
|
+
readonly location: string;
|
|
693
|
+
/**
|
|
694
|
+
* @param path - Store path; defaults to {@link nexusDevicePath}.
|
|
695
|
+
* @param options - Lock and sealing options.
|
|
696
|
+
*/
|
|
697
|
+
constructor(path?: string, options?: NexusDeviceStoreOptions);
|
|
698
|
+
/**
|
|
699
|
+
* A sealed snapshot of one entry, read without the lock. Use it to decide;
|
|
700
|
+
* any change goes through {@link NexusDeviceStore.update}, which re-reads.
|
|
701
|
+
*
|
|
702
|
+
* @param apiUrl - Any URL on the API host.
|
|
703
|
+
* @param userId - Nexus user id.
|
|
704
|
+
* @returns The sealed entry, or `null`.
|
|
705
|
+
* @throws {NexusDeviceStoreError} On an unsafe, newer, malformed or foreign-sealed file.
|
|
706
|
+
*/
|
|
707
|
+
get(apiUrl: string, userId: string): Promise<SealedNexusDevice | null>;
|
|
708
|
+
/**
|
|
709
|
+
* Every entry, sorted by origin then user id. An entry this machine's key
|
|
710
|
+
* cannot open is listed as an {@link UnreadableNexusDevice} (no secrets)
|
|
711
|
+
* instead of failing the whole listing.
|
|
712
|
+
*
|
|
713
|
+
* @returns The entries.
|
|
714
|
+
* @throws {NexusDeviceStoreError} On an unsafe, newer or malformed file.
|
|
715
|
+
*/
|
|
716
|
+
list(): Promise<Array<SealedNexusDevice | UnreadableNexusDevice>>;
|
|
717
|
+
/**
|
|
718
|
+
* Run `fn` under this store's cross-process lock (M6, N1).
|
|
719
|
+
*
|
|
720
|
+
* - The file is re-read **after** the lock is acquired, so `fn` always sees
|
|
721
|
+
* the latest state, never a snapshot taken before another process wrote.
|
|
722
|
+
* - `fn` may await local work (another store's lock, taken after this one),
|
|
723
|
+
* but MUST NOT make a network call: the lock is never held across E1,
|
|
724
|
+
* E8, E9 or E10 (v2.9). Persist the intent, return, make the call, then
|
|
725
|
+
* call `update` again and re-check with a CAS.
|
|
726
|
+
* - `tx.flush()` writes mid-transaction; whatever is still unflushed is
|
|
727
|
+
* written when `fn` returns. When `fn` throws, unflushed changes are
|
|
728
|
+
* dropped.
|
|
729
|
+
* - Waiting for the lock gives up after `lockWaitMs` with
|
|
730
|
+
* `E_NEXUS_DEVICE_BUSY`. Calling `update` on the same file from inside
|
|
731
|
+
* `fn` fails at once with `E_NEXUS_DEVICE_REENTRANT` (it would otherwise
|
|
732
|
+
* wait on itself).
|
|
733
|
+
* - If the lock is lost while `fn` runs, `tx.signal` aborts, nothing more
|
|
734
|
+
* is written, and `update` rejects with `E_NEXUS_DEVICE_LOCK_COMPROMISED`.
|
|
735
|
+
*
|
|
736
|
+
* @param fn - The read-modify-write step.
|
|
737
|
+
* @returns What `fn` returned.
|
|
738
|
+
* @throws {NexusDeviceStoreError} As above, or on an unsafe, newer, malformed or foreign-sealed file.
|
|
739
|
+
*/
|
|
740
|
+
update<R>(fn: (tx: NexusDeviceTransaction) => R | Promise<R>): Promise<R>;
|
|
741
|
+
/** The body of {@link NexusDeviceStore.update}, with the lock held. */
|
|
742
|
+
private transact;
|
|
743
|
+
/**
|
|
744
|
+
* Load this home's machine key and salt, mapping any failure other than
|
|
745
|
+
* "missing" to `E_NEXUS_DEVICE_MACHINE_KEY` (review N5), so it is never
|
|
746
|
+
* mistaken for a file copied from another machine.
|
|
747
|
+
*
|
|
748
|
+
* @param create - Whether missing material may be created (write paths only).
|
|
749
|
+
* @returns The material, or `null` when it is missing and `create` is false.
|
|
750
|
+
*/
|
|
751
|
+
private material;
|
|
752
|
+
/**
|
|
753
|
+
* Decrypt one sealed entry and validate the plaintext.
|
|
754
|
+
*
|
|
755
|
+
* @throws {NexusDeviceStoreError} `E_NEXUS_DEVICE_UNSEAL_FAILED` when there
|
|
756
|
+
* is no key material or a value fails authentication.
|
|
757
|
+
*/
|
|
758
|
+
private open;
|
|
759
|
+
/**
|
|
760
|
+
* Seal the in-memory state into the on-disk form, keeping unknown top-level
|
|
761
|
+
* fields, and put every unopenable entry back exactly as it was read.
|
|
762
|
+
*/
|
|
763
|
+
private seal;
|
|
764
|
+
/**
|
|
765
|
+
* Read and validate the file. A missing or empty file (the seed's
|
|
766
|
+
* placeholder) reads as an empty store. Anything else that does not parse
|
|
767
|
+
* as a known version is refused, never read as empty, so it is never
|
|
768
|
+
* overwritten.
|
|
769
|
+
*/
|
|
770
|
+
private readSealed;
|
|
771
|
+
/**
|
|
772
|
+
* Open the file without following links and check the open descriptor
|
|
773
|
+
* (so nothing can be swapped between the check and the read): a regular
|
|
774
|
+
* file with one link, owned by this user, with no group or other bits.
|
|
775
|
+
*
|
|
776
|
+
* @returns The contents and the file's fencing fingerprint, or `null` when
|
|
777
|
+
* the file does not exist.
|
|
778
|
+
*/
|
|
779
|
+
private readPrivate;
|
|
780
|
+
/** The remedy for a mode or owner problem, including the sudo case. */
|
|
781
|
+
private ownerRemedy;
|
|
782
|
+
/** A symlink refusal for `op`. */
|
|
783
|
+
private symlinkError;
|
|
784
|
+
/**
|
|
785
|
+
* Refuse a CLEO home another user could use to replace the file: one owned
|
|
786
|
+
* by someone else, or one inside a parent that another user owns and that
|
|
787
|
+
* is group- or world-writable without the sticky bit. A symlinked home is
|
|
788
|
+
* resolved first, and the checks apply to the real directory and its real
|
|
789
|
+
* parent (review N4). A home owned by the caller that is group- or
|
|
790
|
+
* world-writable is allowed here: reads check the open descriptor, and
|
|
791
|
+
* {@link NexusDeviceStore.prepareDirectory} tightens it on the first write.
|
|
792
|
+
* Skipped on Windows, where the profile ACL governs.
|
|
793
|
+
*
|
|
794
|
+
* @returns The real home, its stat and whether the configured path is a
|
|
795
|
+
* symlink, or `null` when it does not exist (or on Windows).
|
|
796
|
+
*/
|
|
797
|
+
private assertSafeDirectory;
|
|
798
|
+
/**
|
|
799
|
+
* Before a write: create the CLEO home 0700 if it is missing, check it,
|
|
800
|
+
* and tighten a home the caller owns that is group- or world-writable with
|
|
801
|
+
* `chmod go-w` (the CLEO home is CLEO's own directory). A home reached
|
|
802
|
+
* through a symlink is never changed: its target may be a directory CLEO
|
|
803
|
+
* does not own the purpose of, so only a warning is given (review N4).
|
|
804
|
+
*
|
|
805
|
+
* @returns A one-line warning when the home was tightened or should be, else `null`.
|
|
806
|
+
*/
|
|
807
|
+
private prepareDirectory;
|
|
808
|
+
/**
|
|
809
|
+
* Write `state` atomically at 0600: a new temp file (`O_EXCL | O_NOFOLLOW`),
|
|
810
|
+
* fsync (F_FULLFSYNC on macOS, through libuv), rename over the target, then
|
|
811
|
+
* fsync the directory so the rename survives a crash. No backup copy is
|
|
812
|
+
* made. On Windows a rename blocked by a scanner (EPERM, EBUSY) is retried.
|
|
813
|
+
*/
|
|
814
|
+
private writeAtomic;
|
|
815
|
+
/**
|
|
816
|
+
* Create the file owner-only, in an owner-only directory, if it does not
|
|
817
|
+
* exist, so the lock never creates it at the default mode. `O_EXCL` plus
|
|
818
|
+
* `O_NOFOLLOW` close the window between the symlink check and the create.
|
|
819
|
+
*/
|
|
820
|
+
private seed;
|
|
821
|
+
/**
|
|
822
|
+
* Delete temp files a crashed writer left behind. Runs under the lock, so
|
|
823
|
+
* no live writer's temp file can match.
|
|
824
|
+
*/
|
|
825
|
+
private sweepTempFiles;
|
|
826
|
+
/**
|
|
827
|
+
* Delete any rotated backup of the file (L3). This store never makes one,
|
|
828
|
+
* but a generic `withLock` write elsewhere would, and a backup keeps old
|
|
829
|
+
* tokens and keys on disk. Best effort.
|
|
830
|
+
*/
|
|
831
|
+
private purgeBackups;
|
|
832
|
+
}
|
|
833
|
+
export {};
|
|
834
|
+
//# sourceMappingURL=nexus-device.d.ts.map
|