@cleocode/contracts 2026.9.26 → 2026.10.2
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/cloud/api.d.ts +469 -2
- package/dist/cloud/api.d.ts.map +1 -1
- package/dist/cloud/api.js +209 -8
- package/dist/cloud/api.js.map +1 -1
- package/dist/cloud/envelope.d.ts +6 -0
- package/dist/cloud/envelope.d.ts.map +1 -1
- package/dist/cloud/envelope.js +12 -0
- package/dist/cloud/envelope.js.map +1 -1
- package/dist/config.d.ts +13 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/heavy-command-hook.d.ts +123 -0
- package/dist/heavy-command-hook.d.ts.map +1 -0
- package/dist/heavy-command-hook.js +17 -0
- package/dist/heavy-command-hook.js.map +1 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +8 -0
- package/dist/install.d.ts.map +1 -1
- package/dist/invariants/index.d.ts +2 -0
- package/dist/invariants/index.d.ts.map +1 -1
- package/dist/invariants/index.js +1 -0
- package/dist/invariants/index.js.map +1 -1
- package/dist/invariants/sync-write-invariants.d.ts +115 -0
- package/dist/invariants/sync-write-invariants.d.ts.map +1 -0
- package/dist/invariants/sync-write-invariants.js +123 -0
- package/dist/invariants/sync-write-invariants.js.map +1 -0
- package/dist/nexus-account.d.ts +91 -3
- package/dist/nexus-account.d.ts.map +1 -1
- package/dist/nexus-account.js +30 -1
- package/dist/nexus-account.js.map +1 -1
- package/dist/nexus-cloud.d.ts +718 -0
- package/dist/nexus-cloud.d.ts.map +1 -0
- package/dist/nexus-cloud.js +418 -0
- package/dist/nexus-cloud.js.map +1 -0
- package/dist/nexus-vault.d.ts +266 -0
- package/dist/nexus-vault.d.ts.map +1 -0
- package/dist/nexus-vault.js +82 -0
- package/dist/nexus-vault.js.map +1 -0
- package/dist/portable-bundle.d.ts +5 -0
- package/dist/portable-bundle.d.ts.map +1 -1
- package/dist/resource-governor.d.ts +13 -0
- package/dist/resource-governor.d.ts.map +1 -1
- package/dist/resource-governor.js +13 -0
- package/dist/resource-governor.js.map +1 -1
- package/dist/sync-schema.d.ts +34 -0
- package/dist/sync-schema.d.ts.map +1 -0
- package/dist/sync-schema.js +34 -0
- package/dist/sync-schema.js.map +1 -0
- package/dist/task.d.ts +49 -1
- package/dist/task.d.ts.map +1 -1
- package/dist/tasks.d.ts +6 -0
- package/dist/tasks.d.ts.map +1 -1
- package/dist/tasks.js.map +1 -1
- package/package.json +2 -2
- package/src/cloud/api.ts +258 -9
- package/src/cloud/envelope.ts +12 -0
- package/src/config.ts +13 -0
- package/src/heavy-command-hook.ts +136 -0
- package/src/index.ts +108 -0
- package/src/install.ts +9 -0
- package/src/invariants/index.ts +6 -0
- package/src/invariants/sync-write-invariants.ts +460 -0
- package/src/nexus-account.ts +120 -3
- package/src/nexus-cloud.ts +673 -0
- package/src/nexus-vault.ts +280 -0
- package/src/portable-bundle.ts +5 -0
- package/src/resource-governor.ts +15 -0
- package/src/sync-schema.ts +34 -0
- package/src/task.ts +49 -1
- package/src/tasks.ts +6 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cleo Nexus remote reads: what `cleo cloud status|whoami|devices|projects`
|
|
3
|
+
* parse from the API and emit in their LAFS envelopes (cleo-nexus device
|
|
4
|
+
* contract v2.15, §4.1, §4.2 E2/E3/E5/E13/E14/E15, §4.4), plus the error
|
|
5
|
+
* mapping table of §4.0.4.
|
|
6
|
+
*
|
|
7
|
+
* The response schemas are the client's tolerant reading of §4.1: every field
|
|
8
|
+
* the CLI reports is named, unknown fields are stripped (never `.strict()`,
|
|
9
|
+
* §4 preamble), and fields the deployed server does not send on every route
|
|
10
|
+
* yet (E2's `device` is a subset of `DeviceView`, E13 has no
|
|
11
|
+
* `lastPresenceAt`) are optional. They are not part of `./cloud`, which
|
|
12
|
+
* mirrors the server's `@cleo-nexus/shared` package one-to-one.
|
|
13
|
+
*
|
|
14
|
+
* Nothing here carries a token, a key or a filesystem path the server sent;
|
|
15
|
+
* the local paths in {@link CloudStatusLocal} are locations only.
|
|
16
|
+
*
|
|
17
|
+
* This file is types + zod schemas + const data only (arch gate 10).
|
|
18
|
+
*
|
|
19
|
+
* @task T12871
|
|
20
|
+
* @epic T12323
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
import type { NexusAccountErrorCode } from './nexus-account.js';
|
|
25
|
+
|
|
26
|
+
// ---------- shared pieces (§4.1) ----------
|
|
27
|
+
|
|
28
|
+
/** Device states (§4.1 `DeviceState`). */
|
|
29
|
+
export const NEXUS_DEVICE_STATES = ['active', 'signed-out', 'revoked'] as const;
|
|
30
|
+
|
|
31
|
+
/** Device-credential profiles (§2.3, §4.1 `DeviceProfile`). */
|
|
32
|
+
export const NEXUS_CLOUD_PROFILES = ['device', 'read-only'] as const;
|
|
33
|
+
|
|
34
|
+
/** The `state` filter of `GET /v1/devices` (E5, `ListDevicesQuery.state`). */
|
|
35
|
+
export const NEXUS_DEVICE_LIST_STATES = ['active', 'signed-out', 'revoked', 'all'] as const;
|
|
36
|
+
|
|
37
|
+
/** One of {@link NEXUS_DEVICE_LIST_STATES}. */
|
|
38
|
+
export type NexusDeviceListState = (typeof NEXUS_DEVICE_LIST_STATES)[number];
|
|
39
|
+
|
|
40
|
+
/** Largest page the list endpoints accept (§4.0.3). */
|
|
41
|
+
export const NEXUS_PAGE_LIMIT_MAX = 200;
|
|
42
|
+
|
|
43
|
+
/** Default number of pages a `cleo cloud` list follows before it stops and says so. */
|
|
44
|
+
export const NEXUS_CLOUD_MAX_PAGES = 25;
|
|
45
|
+
|
|
46
|
+
/** Seconds within which a replica's presence counts as fresh (§4.1 `PRESENCE_FRESH_SECONDS`). */
|
|
47
|
+
export const NEXUS_PRESENCE_FRESH_SECONDS = 86_400;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The device state a newer server value parses as. Not `active`, so every
|
|
51
|
+
* active-only check fails closed instead of the whole command failing.
|
|
52
|
+
*/
|
|
53
|
+
export const NEXUS_DEVICE_STATE_UNKNOWN = 'unknown';
|
|
54
|
+
|
|
55
|
+
const isoTime = z.iso.datetime({ offset: true });
|
|
56
|
+
/** A profile the CLI does not know reads as none, never as a parse failure. */
|
|
57
|
+
const profile = z.enum(NEXUS_CLOUD_PROFILES).nullable().catch(null);
|
|
58
|
+
/** A device state; a value added by a newer server reads as `unknown` (not active). */
|
|
59
|
+
const deviceState = z
|
|
60
|
+
.enum([...NEXUS_DEVICE_STATES, NEXUS_DEVICE_STATE_UNKNOWN])
|
|
61
|
+
.catch(NEXUS_DEVICE_STATE_UNKNOWN);
|
|
62
|
+
|
|
63
|
+
/** `CredentialInfo` (§4.1): the credential that made the request. */
|
|
64
|
+
export const nexusCloudCredentialSchema = z.object({
|
|
65
|
+
kind: z.enum(['device', 'session']),
|
|
66
|
+
credentialId: z.string().nullable().optional(),
|
|
67
|
+
profile: profile.optional(),
|
|
68
|
+
scopes: z.array(z.string()).default([]),
|
|
69
|
+
createdAt: isoTime.nullable().optional(),
|
|
70
|
+
lastUsedAt: isoTime.nullable().optional(),
|
|
71
|
+
/** When the credential expires if it is not used again; null for sessions. */
|
|
72
|
+
idleExpiresAt: isoTime.nullable().optional(),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/** `DeviceView` (§4.1), as far as the CLI reports it. E2 sends a subset. */
|
|
76
|
+
export const nexusCloudDeviceSchema = z.object({
|
|
77
|
+
deviceId: z.string(),
|
|
78
|
+
name: z.string(),
|
|
79
|
+
platform: z.string().optional(),
|
|
80
|
+
arch: z.string().nullable().optional(),
|
|
81
|
+
cliVersion: z.string().nullable().optional(),
|
|
82
|
+
createdAt: isoTime.optional(),
|
|
83
|
+
lastSeenAt: isoTime.nullable().optional(),
|
|
84
|
+
state: deviceState,
|
|
85
|
+
signedOutAt: isoTime.nullable().optional(),
|
|
86
|
+
/** Base64 device certificate; null until the device holds a key grant. */
|
|
87
|
+
certificate: z.string().nullable().optional(),
|
|
88
|
+
/** The current credential's profile, or null with none (signed out or revoked). */
|
|
89
|
+
profile: profile.optional(),
|
|
90
|
+
/** True for the device whose credential made this request. */
|
|
91
|
+
current: z.boolean().default(false),
|
|
92
|
+
projects: z.number().int().nonnegative().optional(),
|
|
93
|
+
replicas: z.number().int().nonnegative().optional(),
|
|
94
|
+
lastSyncAt: isoTime.nullable().optional(),
|
|
95
|
+
lastPresenceAt: isoTime.nullable().optional(),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/** `OrganizationSummary` (§4.1). */
|
|
99
|
+
export const nexusCloudOrganizationSchema = z.object({
|
|
100
|
+
id: z.string(),
|
|
101
|
+
name: z.string(),
|
|
102
|
+
slug: z.string().nullable().optional(),
|
|
103
|
+
role: z.string().optional(),
|
|
104
|
+
personal: z.boolean().optional(),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/** `AccountUser` (§4.1). */
|
|
108
|
+
export const nexusCloudUserSchema = z.object({
|
|
109
|
+
id: z.string(),
|
|
110
|
+
email: z.string(),
|
|
111
|
+
name: z.string().nullable().optional(),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
/** `Whoami` (E2 `GET /v1/whoami`). */
|
|
115
|
+
export const nexusCloudWhoamiSchema = z.object({
|
|
116
|
+
serverTime: isoTime.optional(),
|
|
117
|
+
user: nexusCloudUserSchema,
|
|
118
|
+
organizations: z.array(nexusCloudOrganizationSchema).default([]),
|
|
119
|
+
credential: nexusCloudCredentialSchema,
|
|
120
|
+
/** The calling device; null for a session caller. */
|
|
121
|
+
device: nexusCloudDeviceSchema.nullable(),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
/** `ListDevicesViewResult` (E5 `GET /v1/devices`), one page. */
|
|
125
|
+
export const nexusCloudDevicePageSchema = z.object({
|
|
126
|
+
devices: z.array(nexusCloudDeviceSchema),
|
|
127
|
+
nextCursor: z.string().max(512).nullable().default(null),
|
|
128
|
+
truncated: z.boolean().default(false),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
/** Path-free replica presence (`ReplicaPresence`), with the R4 `sync` flag; other fields kept as sent. */
|
|
132
|
+
export const nexusCloudPresenceSchema = z.looseObject({
|
|
133
|
+
observedAt: isoTime.optional(),
|
|
134
|
+
sync: z.object({ enabled: z.boolean() }).optional(),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
/** `ReplicaRow` (§4.1). */
|
|
138
|
+
export const nexusCloudReplicaSchema = z.object({
|
|
139
|
+
projectId: z.string(),
|
|
140
|
+
replicaId: z.string(),
|
|
141
|
+
deviceId: z.string(),
|
|
142
|
+
deviceName: z.string(),
|
|
143
|
+
deviceState: deviceState.optional(),
|
|
144
|
+
attachedAt: isoTime.optional(),
|
|
145
|
+
lastSyncAt: isoTime.nullable(),
|
|
146
|
+
presence: nexusCloudPresenceSchema.nullable(),
|
|
147
|
+
presenceAt: isoTime.nullable(),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
/** `DeviceCounts` (§4.1): distinct devices holding a replica. */
|
|
151
|
+
export const nexusCloudDeviceCountsSchema = z.object({
|
|
152
|
+
active: z.number().int().nonnegative(),
|
|
153
|
+
total: z.number().int().nonnegative(),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/** The project fields every project view carries (`Project`, §4.1). */
|
|
157
|
+
const projectFields = {
|
|
158
|
+
projectId: z.string(),
|
|
159
|
+
label: z.string().nullable(),
|
|
160
|
+
organizationId: z.string(),
|
|
161
|
+
organizationName: z.string().optional(),
|
|
162
|
+
remoteUrl: z.string().nullable().optional(),
|
|
163
|
+
createdAt: isoTime.optional(),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/** `ProjectListItem` (§4.1, E13). */
|
|
167
|
+
export const nexusCloudProjectListItemSchema = z.object({
|
|
168
|
+
...projectFields,
|
|
169
|
+
role: z.string(),
|
|
170
|
+
streamId: z.string().optional(),
|
|
171
|
+
headSeq: z.number().int().nonnegative().nullable().optional(),
|
|
172
|
+
openConflicts: z.number().int().nonnegative().optional(),
|
|
173
|
+
replicas: z.array(nexusCloudReplicaSchema).default([]),
|
|
174
|
+
devices: nexusCloudDeviceCountsSchema.optional(),
|
|
175
|
+
replicaCount: z.number().int().nonnegative().optional(),
|
|
176
|
+
lastSyncAt: isoTime.nullable().optional(),
|
|
177
|
+
lastPresenceAt: isoTime.nullable().optional(),
|
|
178
|
+
/** This project's replica list was cut at 50 (T12858). */
|
|
179
|
+
replicasTruncated: z.boolean().default(false),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
/** `ListProjectsResult` (E13 `GET /v1/projects`), one page. */
|
|
183
|
+
export const nexusCloudProjectPageSchema = z.object({
|
|
184
|
+
projects: z.array(nexusCloudProjectListItemSchema),
|
|
185
|
+
nextCursor: z.string().max(512).nullable().default(null),
|
|
186
|
+
/** Anything in the page was cut (the project list or a replica list). */
|
|
187
|
+
truncated: z.boolean().default(false),
|
|
188
|
+
/** The project list itself was cut at 1000 (T12858). */
|
|
189
|
+
projectsTruncated: z.boolean().default(false),
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
/** `ProjectDetail` (E14 `GET /v1/projects/:projectId`). */
|
|
193
|
+
export const nexusCloudProjectDetailSchema = z.object({
|
|
194
|
+
project: z.object({ ...projectFields, deletedAt: isoTime.nullable().optional() }),
|
|
195
|
+
role: z.string(),
|
|
196
|
+
canTrash: z.boolean().optional(),
|
|
197
|
+
openConflicts: z.number().int().nonnegative(),
|
|
198
|
+
/** At most 50, oldest attachment first. */
|
|
199
|
+
replicas: z.array(nexusCloudReplicaSchema),
|
|
200
|
+
devices: nexusCloudDeviceCountsSchema,
|
|
201
|
+
replicaCount: z.number().int().nonnegative().optional(),
|
|
202
|
+
lastSyncAt: isoTime.nullable().optional(),
|
|
203
|
+
/** `replicas` was cut at 50. */
|
|
204
|
+
truncated: z.boolean().default(false),
|
|
205
|
+
stream: z
|
|
206
|
+
.object({
|
|
207
|
+
streamId: z.string(),
|
|
208
|
+
headSeq: z.number().int().nonnegative(),
|
|
209
|
+
headCheckpointId: z.string().nullable(),
|
|
210
|
+
})
|
|
211
|
+
.nullable(),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
/** `ListReplicasResult` (E15 `GET /v1/projects/:projectId/replicas`), one page. */
|
|
215
|
+
export const nexusCloudReplicaPageSchema = z.object({
|
|
216
|
+
replicas: z.array(nexusCloudReplicaSchema),
|
|
217
|
+
devices: nexusCloudDeviceCountsSchema.optional(),
|
|
218
|
+
nextCursor: z.string().max(512).nullable().default(null),
|
|
219
|
+
truncated: z.boolean().default(false),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ---------- status (E3, §4.4) ----------
|
|
223
|
+
|
|
224
|
+
/** E3 check ids, in their contract order (§4.1 `StatusCheckId`). */
|
|
225
|
+
export const NEXUS_STATUS_CHECK_IDS = [
|
|
226
|
+
'credential.valid',
|
|
227
|
+
'device.registered',
|
|
228
|
+
'device.active',
|
|
229
|
+
'device.certified',
|
|
230
|
+
'project.registered',
|
|
231
|
+
'project.writable',
|
|
232
|
+
'replica.attached',
|
|
233
|
+
'presence.fresh',
|
|
234
|
+
'replica.synced',
|
|
235
|
+
'conflicts.none',
|
|
236
|
+
] as const;
|
|
237
|
+
|
|
238
|
+
/** One of {@link NEXUS_STATUS_CHECK_IDS}. */
|
|
239
|
+
export type NexusStatusCheckId = (typeof NEXUS_STATUS_CHECK_IDS)[number];
|
|
240
|
+
|
|
241
|
+
/** The server's verdicts (§4.1 `StatusVerdict`). */
|
|
242
|
+
export const NEXUS_STATUS_VERDICTS = ['ok', 'attention', 'not-linked', 'not-registered'] as const;
|
|
243
|
+
|
|
244
|
+
/** `cleo cloud status` verdicts: the server's plus the local `not-signed-in` (§4.4). */
|
|
245
|
+
export const CLOUD_STATUS_VERDICTS = [...NEXUS_STATUS_VERDICTS, 'not-signed-in'] as const;
|
|
246
|
+
|
|
247
|
+
/** One of {@link CLOUD_STATUS_VERDICTS}. */
|
|
248
|
+
export type CloudStatusVerdict = (typeof CLOUD_STATUS_VERDICTS)[number];
|
|
249
|
+
|
|
250
|
+
const knownCheckId = z.enum(NEXUS_STATUS_CHECK_IDS);
|
|
251
|
+
|
|
252
|
+
/** `StatusCheck` (§4.1). Only required checks drive the verdict. */
|
|
253
|
+
export const nexusCloudStatusCheckSchema = z.object({
|
|
254
|
+
id: z.enum(NEXUS_STATUS_CHECK_IDS),
|
|
255
|
+
ok: z.boolean(),
|
|
256
|
+
required: z.boolean(),
|
|
257
|
+
detail: z.string(),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
/** `NexusStatus` (E3 `GET /v1/status`). */
|
|
261
|
+
export const nexusCloudStatusSchema = z.object({
|
|
262
|
+
serverTime: isoTime.optional(),
|
|
263
|
+
apiVersion: z.string().optional(),
|
|
264
|
+
user: nexusCloudUserSchema,
|
|
265
|
+
credential: nexusCloudCredentialSchema,
|
|
266
|
+
device: nexusCloudDeviceSchema.nullable(),
|
|
267
|
+
project: z
|
|
268
|
+
.object({
|
|
269
|
+
projectId: z.string(),
|
|
270
|
+
registered: z.boolean(),
|
|
271
|
+
organizationId: z.string().nullable(),
|
|
272
|
+
organizationName: z.string().nullable(),
|
|
273
|
+
role: z.string().nullable(),
|
|
274
|
+
})
|
|
275
|
+
.nullable(),
|
|
276
|
+
replica: z
|
|
277
|
+
.object({
|
|
278
|
+
replicaId: z.string(),
|
|
279
|
+
attached: z.boolean(),
|
|
280
|
+
attachedElsewhere: z.boolean(),
|
|
281
|
+
attachedAt: isoTime.nullable(),
|
|
282
|
+
lastSyncAt: isoTime.nullable(),
|
|
283
|
+
presenceAt: isoTime.nullable(),
|
|
284
|
+
lastReplicaSeq: z.number().int().nonnegative().nullable().optional(),
|
|
285
|
+
})
|
|
286
|
+
.nullable(),
|
|
287
|
+
stream: z
|
|
288
|
+
.object({
|
|
289
|
+
streamId: z.string(),
|
|
290
|
+
headSeq: z.number().int().nonnegative(),
|
|
291
|
+
headCheckpointId: z.string().nullable(),
|
|
292
|
+
openConflicts: z.number().int().nonnegative(),
|
|
293
|
+
devices: nexusCloudDeviceCountsSchema,
|
|
294
|
+
})
|
|
295
|
+
.nullable(),
|
|
296
|
+
/** Check ids added by a newer server are dropped, never a parse failure. */
|
|
297
|
+
checks: z.array(nexusCloudStatusCheckSchema.extend({ id: z.string() })).transform((list) =>
|
|
298
|
+
list.flatMap((c) => {
|
|
299
|
+
const id = knownCheckId.safeParse(c.id);
|
|
300
|
+
return id.success ? [{ ...c, id: id.data }] : [];
|
|
301
|
+
}),
|
|
302
|
+
),
|
|
303
|
+
/** A verdict added by a newer server reads as `attention`: never `ok` by accident. */
|
|
304
|
+
verdict: z.enum(NEXUS_STATUS_VERDICTS).catch('attention'),
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
/** Parsed `CredentialInfo`. */
|
|
308
|
+
export type NexusCloudCredential = z.infer<typeof nexusCloudCredentialSchema>;
|
|
309
|
+
/** Parsed `DeviceView`. */
|
|
310
|
+
export type NexusCloudDevice = z.infer<typeof nexusCloudDeviceSchema>;
|
|
311
|
+
/** Parsed `Whoami`. */
|
|
312
|
+
export type NexusCloudWhoami = z.infer<typeof nexusCloudWhoamiSchema>;
|
|
313
|
+
/** Parsed `ReplicaRow`. */
|
|
314
|
+
export type NexusCloudReplica = z.infer<typeof nexusCloudReplicaSchema>;
|
|
315
|
+
/** Parsed `DeviceCounts`. */
|
|
316
|
+
export type NexusCloudDeviceCounts = z.infer<typeof nexusCloudDeviceCountsSchema>;
|
|
317
|
+
/** Parsed `ProjectListItem`. */
|
|
318
|
+
export type NexusCloudProjectListItem = z.infer<typeof nexusCloudProjectListItemSchema>;
|
|
319
|
+
/** Parsed `ProjectDetail`. */
|
|
320
|
+
export type NexusCloudProjectDetail = z.infer<typeof nexusCloudProjectDetailSchema>;
|
|
321
|
+
/** Parsed `StatusCheck`. */
|
|
322
|
+
export type NexusCloudStatusCheck = z.infer<typeof nexusCloudStatusCheckSchema>;
|
|
323
|
+
/** Parsed `NexusStatus`. */
|
|
324
|
+
export type NexusCloudStatus = z.infer<typeof nexusCloudStatusSchema>;
|
|
325
|
+
|
|
326
|
+
// ---------- the cloud.* envelope payloads ----------
|
|
327
|
+
|
|
328
|
+
/** A non-fatal problem a `cleo cloud` read reports next to its data. */
|
|
329
|
+
export interface CloudWarning {
|
|
330
|
+
/** Stable code, e.g. `W_NEXUS_STATUS_COMPOSED`. */
|
|
331
|
+
code: string;
|
|
332
|
+
/** Human message (never a secret). */
|
|
333
|
+
message: string;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** How a list read paged through the server's cursors (§4.0.3). */
|
|
337
|
+
export interface CloudPaging {
|
|
338
|
+
/** Pages fetched. */
|
|
339
|
+
pages: number;
|
|
340
|
+
/** A server hard ceiling cut a response (`truncated` on any page). */
|
|
341
|
+
truncated: boolean;
|
|
342
|
+
/** The client stopped at its page budget while the server still had a `nextCursor`. */
|
|
343
|
+
pageLimitReached: boolean;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** `cleo cloud whoami` (operation `cloud.whoami`). */
|
|
347
|
+
export interface CloudWhoamiResult extends NexusCloudWhoami {
|
|
348
|
+
/** API origin asked. */
|
|
349
|
+
apiUrl: string;
|
|
350
|
+
/** Non-fatal problems (logout retries, upgrades). */
|
|
351
|
+
warnings: CloudWarning[];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** `cleo cloud devices` (operation `cloud.devices.list`). */
|
|
355
|
+
export interface CloudDevicesResult {
|
|
356
|
+
/** API origin asked. */
|
|
357
|
+
apiUrl: string;
|
|
358
|
+
/** The `state` filter sent, or `null` for the server default (`active` for device callers). */
|
|
359
|
+
state: NexusDeviceListState | null;
|
|
360
|
+
/** Every device across the pages followed, newest first. */
|
|
361
|
+
devices: NexusCloudDevice[];
|
|
362
|
+
/** `devices.length`. */
|
|
363
|
+
count: number;
|
|
364
|
+
/** Paging facts. */
|
|
365
|
+
paging: CloudPaging;
|
|
366
|
+
/** Non-fatal problems. */
|
|
367
|
+
warnings: CloudWarning[];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** `cleo cloud projects` (operation `cloud.projects.list`). */
|
|
371
|
+
export interface CloudProjectsResult {
|
|
372
|
+
/** API origin asked. */
|
|
373
|
+
apiUrl: string;
|
|
374
|
+
/** The organization filter sent, or `null`. */
|
|
375
|
+
organizationId: string | null;
|
|
376
|
+
/** Every project across the pages followed, newest first. */
|
|
377
|
+
projects: NexusCloudProjectListItem[];
|
|
378
|
+
/** `projects.length`. */
|
|
379
|
+
count: number;
|
|
380
|
+
/** Paging facts; `truncated` covers project and replica cuts. */
|
|
381
|
+
paging: CloudPaging & {
|
|
382
|
+
/** The project list itself was cut at the server's ceiling. */
|
|
383
|
+
projectsTruncated: boolean;
|
|
384
|
+
};
|
|
385
|
+
/** Non-fatal problems. */
|
|
386
|
+
warnings: CloudWarning[];
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** `cleo cloud projects show [<id>]` (operation `cloud.projects.show`). */
|
|
390
|
+
export interface CloudProjectShowResult extends NexusCloudProjectDetail {
|
|
391
|
+
/** API origin asked. */
|
|
392
|
+
apiUrl: string;
|
|
393
|
+
/** The project id asked for. */
|
|
394
|
+
projectId: string;
|
|
395
|
+
/** `true` when the id came from the current project rather than the command line. */
|
|
396
|
+
currentProject: boolean;
|
|
397
|
+
/**
|
|
398
|
+
* Paging of the replica list: when E14 cut `replicas` at 50, the rest were
|
|
399
|
+
* fetched from E15 and `replicas` holds them all (up to the page budget).
|
|
400
|
+
*/
|
|
401
|
+
replicaPaging: CloudPaging;
|
|
402
|
+
/** Non-fatal problems. */
|
|
403
|
+
warnings: CloudWarning[];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** `CloudStatusSummary` (§4.4): the facts an agent checks. */
|
|
407
|
+
export interface CloudStatusSummary {
|
|
408
|
+
/** A device credential is stored for the origin. */
|
|
409
|
+
signedIn: boolean;
|
|
410
|
+
/** `device.registered` and `device.active` passed. */
|
|
411
|
+
registered: boolean;
|
|
412
|
+
/** The credential's profile. */
|
|
413
|
+
profile: (typeof NEXUS_CLOUD_PROFILES)[number] | null;
|
|
414
|
+
/** The project is registered (and, for the current project, the local link entry exists). */
|
|
415
|
+
linked: boolean;
|
|
416
|
+
/** `replica.attached` passed. */
|
|
417
|
+
replicaAttached: boolean;
|
|
418
|
+
/** Active devices holding this project (`stream.devices.active`). */
|
|
419
|
+
devices: number;
|
|
420
|
+
/** The replica's last presence report. */
|
|
421
|
+
lastPresenceAt: string | null;
|
|
422
|
+
/** The replica's last journal sync. */
|
|
423
|
+
lastSyncAt: string | null;
|
|
424
|
+
/** The project stream's head. */
|
|
425
|
+
headSeq: number | null;
|
|
426
|
+
/** Open conflicts on the project stream. */
|
|
427
|
+
openConflicts: number | null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** `CloudStatusResult.local` (§4.4): what this machine knows. Locations only, never a secret. */
|
|
431
|
+
export interface CloudStatusLocal {
|
|
432
|
+
/** API origin. */
|
|
433
|
+
apiUrl: string;
|
|
434
|
+
/** A device credential is stored for the origin. */
|
|
435
|
+
signedIn: boolean;
|
|
436
|
+
/** This machine's Nexus device id, when enrolled. */
|
|
437
|
+
nexusDeviceId: string | null;
|
|
438
|
+
/** The stored credential's profile. */
|
|
439
|
+
profile: (typeof NEXUS_CLOUD_PROFILES)[number] | null;
|
|
440
|
+
/** The project asked about (`--project`, else the current project), or `null`. */
|
|
441
|
+
projectId: string | null;
|
|
442
|
+
/** The active replica id of the current project's store (read-only; never bound here). */
|
|
443
|
+
replicaId: string | null;
|
|
444
|
+
/** `.cleo/nexus-link.json` when it holds an entry for the origin, else `null`. */
|
|
445
|
+
linkPath: string | null;
|
|
446
|
+
/** The device credential store (`nexus-device.json`). */
|
|
447
|
+
credentialsPath: string;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** `CloudStatusResult` (§4.4): the data of the `cloud.status` envelope. */
|
|
451
|
+
export interface CloudStatusResult {
|
|
452
|
+
/** The remote verdict, downgraded by local facts the server cannot see. */
|
|
453
|
+
verdict: CloudStatusVerdict;
|
|
454
|
+
/** The facts an agent checks. */
|
|
455
|
+
summary: CloudStatusSummary;
|
|
456
|
+
/** Local state. */
|
|
457
|
+
local: CloudStatusLocal;
|
|
458
|
+
/** E3's answer (or the same shape composed from E2/E14/E15), or `null` when not signed in. */
|
|
459
|
+
remote: NexusCloudStatus | null;
|
|
460
|
+
/** This device's global store on the account's home stream (T12952); absent when not signed in. */
|
|
461
|
+
global?: CloudStatusGlobalStore;
|
|
462
|
+
/** Non-fatal problems. */
|
|
463
|
+
warnings: CloudWarning[];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** `cleo cloud status`: this device's global store (the main brain) on Cleo Nexus (T12952). */
|
|
467
|
+
export interface CloudStatusGlobalStore {
|
|
468
|
+
/** The server lists home-stream replicas. */
|
|
469
|
+
supported: boolean;
|
|
470
|
+
/** This device's global store is attached. */
|
|
471
|
+
attached: boolean;
|
|
472
|
+
/** Its replica id. */
|
|
473
|
+
replicaId: string | null;
|
|
474
|
+
/** When its presence was last reported. */
|
|
475
|
+
presenceAt: string | null;
|
|
476
|
+
/** Devices with a global store attached to the account. */
|
|
477
|
+
devices: number;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** `details` of the `E_NEXUS_UNREACHABLE` failure of `cleo cloud status` (§4.4 "Offline"). */
|
|
481
|
+
export interface CloudStatusOfflineDetails {
|
|
482
|
+
/** Local state. */
|
|
483
|
+
local: CloudStatusLocal;
|
|
484
|
+
/** The summary with every remote field null or false. */
|
|
485
|
+
summary: CloudStatusSummary;
|
|
486
|
+
/** Warnings collected before the server stopped answering. */
|
|
487
|
+
warnings: CloudWarning[];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ---------- error mapping (§4.0.4) ----------
|
|
491
|
+
|
|
492
|
+
/** `details.reason` values of a 401 (§4.0.4 `UnauthenticatedReason`). */
|
|
493
|
+
export const NEXUS_UNAUTHENTICATED_REASONS = [
|
|
494
|
+
'missing',
|
|
495
|
+
'invalid',
|
|
496
|
+
'credential-revoked',
|
|
497
|
+
'credential-expired',
|
|
498
|
+
'device-signed-out',
|
|
499
|
+
'device-revoked',
|
|
500
|
+
'session-bearer-retired',
|
|
501
|
+
'session-used',
|
|
502
|
+
] as const;
|
|
503
|
+
|
|
504
|
+
/** `details.reason` values of a 403 (§4.0.4 `ForbiddenReason`). */
|
|
505
|
+
export const NEXUS_FORBIDDEN_REASONS = [
|
|
506
|
+
'insufficient-scope',
|
|
507
|
+
'session-required',
|
|
508
|
+
'device-required',
|
|
509
|
+
'device-mismatch',
|
|
510
|
+
'not-self',
|
|
511
|
+
'project-role',
|
|
512
|
+
'route-undeclared',
|
|
513
|
+
'bearer-session-required',
|
|
514
|
+
'session-not-fresh',
|
|
515
|
+
'device-limit',
|
|
516
|
+
'device-flow-session-required',
|
|
517
|
+
'device-not-enrolled',
|
|
518
|
+
] as const;
|
|
519
|
+
|
|
520
|
+
/** `details.revokedReason` values of a 401 `credential-revoked` (§4.0.4, v2.11). */
|
|
521
|
+
export const NEXUS_REVOKED_REASONS = ['signed-out', 'revoked', 'rotated', 'reenrolled'] as const;
|
|
522
|
+
|
|
523
|
+
/** One mapped failure. `message: null` keeps the server's message. */
|
|
524
|
+
export interface NexusErrorMapping {
|
|
525
|
+
/** The CLI's stable code. */
|
|
526
|
+
readonly code: NexusAccountErrorCode;
|
|
527
|
+
/** The CLI's message, or `null` to report the server's. */
|
|
528
|
+
readonly message: string | null;
|
|
529
|
+
/** The remedy, or `null` to report the server's `details.remedy` (if any). */
|
|
530
|
+
readonly fix: string | null;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const LOGIN = 'run `cleo login nexus`';
|
|
534
|
+
const COMPROMISED: NexusErrorMapping = {
|
|
535
|
+
code: 'E_NEXUS_CREDENTIAL_COMPROMISED',
|
|
536
|
+
message:
|
|
537
|
+
"this device's credential was replaced or revoked elsewhere, possibly used from another machine",
|
|
538
|
+
fix: 'revoke this device on cleocode.dev and sign in again with `cleo login nexus`',
|
|
539
|
+
};
|
|
540
|
+
const DEVICE_REVOKED: NexusErrorMapping = {
|
|
541
|
+
code: 'E_NEXUS_DEVICE_REVOKED',
|
|
542
|
+
message: 'this device was revoked',
|
|
543
|
+
fix: 'run `cleo login nexus` to enrol this machine as a new device',
|
|
544
|
+
};
|
|
545
|
+
const SESSION_EXPIRED_LOGIN: NexusErrorMapping = {
|
|
546
|
+
code: 'E_NEXUS_SESSION_EXPIRED',
|
|
547
|
+
message: 'the session is too old for this request',
|
|
548
|
+
fix: 'run `cleo login nexus` (needs a browser: on a headless machine a human must complete the login)',
|
|
549
|
+
};
|
|
550
|
+
const SERVER_REFUSED: NexusErrorMapping = {
|
|
551
|
+
code: 'E_NEXUS_REQUEST_FAILED',
|
|
552
|
+
message: null,
|
|
553
|
+
fix: null,
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* 401 `credential-revoked`, by `details.revokedReason` (§4.0.4 v2.11). An
|
|
558
|
+
* absent or unknown reason maps as `rotated`: a revocation the CLI cannot
|
|
559
|
+
* explain (R2).
|
|
560
|
+
*/
|
|
561
|
+
export const NEXUS_REVOKED_REASON_ERRORS: Readonly<
|
|
562
|
+
Record<(typeof NEXUS_REVOKED_REASONS)[number], NexusErrorMapping>
|
|
563
|
+
> = {
|
|
564
|
+
reenrolled: {
|
|
565
|
+
code: 'E_NEXUS_NOT_SIGNED_IN',
|
|
566
|
+
message: "this device's credential was replaced by a newer login of the same device",
|
|
567
|
+
fix: LOGIN,
|
|
568
|
+
},
|
|
569
|
+
'signed-out': {
|
|
570
|
+
code: 'E_NEXUS_NOT_SIGNED_IN',
|
|
571
|
+
message: 'this device was signed out and later signed in again with another credential',
|
|
572
|
+
fix: LOGIN,
|
|
573
|
+
},
|
|
574
|
+
revoked: DEVICE_REVOKED,
|
|
575
|
+
rotated: COMPROMISED,
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
/** 401 `E_UNAUTHENTICATED`, by `details.reason` (§4.0.4). Unknown reasons map as `invalid`. */
|
|
579
|
+
export const NEXUS_UNAUTHENTICATED_ERRORS: Readonly<
|
|
580
|
+
Record<(typeof NEXUS_UNAUTHENTICATED_REASONS)[number], NexusErrorMapping>
|
|
581
|
+
> = {
|
|
582
|
+
missing: {
|
|
583
|
+
code: 'E_NEXUS_NOT_SIGNED_IN',
|
|
584
|
+
message: 'not signed in to Cleo Nexus (the credential is missing or invalid)',
|
|
585
|
+
fix: LOGIN,
|
|
586
|
+
},
|
|
587
|
+
invalid: {
|
|
588
|
+
code: 'E_NEXUS_NOT_SIGNED_IN',
|
|
589
|
+
message: 'not signed in to Cleo Nexus (the credential is missing or invalid)',
|
|
590
|
+
fix: LOGIN,
|
|
591
|
+
},
|
|
592
|
+
// Resolved by `details.revokedReason` through NEXUS_REVOKED_REASON_ERRORS.
|
|
593
|
+
'credential-revoked': COMPROMISED,
|
|
594
|
+
'credential-expired': {
|
|
595
|
+
code: 'E_NEXUS_SESSION_EXPIRED',
|
|
596
|
+
message: 'the device credential expired after 90 days without use',
|
|
597
|
+
fix: LOGIN,
|
|
598
|
+
},
|
|
599
|
+
'device-signed-out': {
|
|
600
|
+
code: 'E_NEXUS_DEVICE_SIGNED_OUT',
|
|
601
|
+
message: 'this device was signed out',
|
|
602
|
+
fix: LOGIN,
|
|
603
|
+
},
|
|
604
|
+
'device-revoked': DEVICE_REVOKED,
|
|
605
|
+
'session-bearer-retired': {
|
|
606
|
+
code: 'E_NEXUS_SESSION_EXPIRED',
|
|
607
|
+
message: 'bearer sessions are no longer accepted',
|
|
608
|
+
fix: 'run `cleo login nexus` with the current CLI',
|
|
609
|
+
},
|
|
610
|
+
'session-used': {
|
|
611
|
+
code: 'E_NEXUS_SESSION_EXPIRED',
|
|
612
|
+
message: 'the 9.24 session was already used by another enrolment',
|
|
613
|
+
fix: LOGIN,
|
|
614
|
+
},
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
/** 403 `E_FORBIDDEN`, by `details.reason` (§4.0.4). Unknown reasons are reported as sent. */
|
|
618
|
+
export const NEXUS_FORBIDDEN_ERRORS: Readonly<
|
|
619
|
+
Record<(typeof NEXUS_FORBIDDEN_REASONS)[number], NexusErrorMapping>
|
|
620
|
+
> = {
|
|
621
|
+
'insufficient-scope': {
|
|
622
|
+
code: 'E_NEXUS_INSUFFICIENT_SCOPE',
|
|
623
|
+
message: "this device's credential lacks the scope for that request",
|
|
624
|
+
fix: 'run `cleo login nexus` without --read-only to enrol with the full device profile',
|
|
625
|
+
},
|
|
626
|
+
'session-required': SERVER_REFUSED,
|
|
627
|
+
'device-required': SERVER_REFUSED,
|
|
628
|
+
'device-mismatch': SERVER_REFUSED,
|
|
629
|
+
'not-self': SERVER_REFUSED,
|
|
630
|
+
'project-role': SERVER_REFUSED,
|
|
631
|
+
'route-undeclared': SERVER_REFUSED,
|
|
632
|
+
'bearer-session-required': SESSION_EXPIRED_LOGIN,
|
|
633
|
+
'session-not-fresh': SESSION_EXPIRED_LOGIN,
|
|
634
|
+
'device-limit': {
|
|
635
|
+
code: 'E_NEXUS_REQUEST_FAILED',
|
|
636
|
+
message: 'the account has reached its device limit',
|
|
637
|
+
fix: 'revoke unused devices on cleocode.dev, then retry',
|
|
638
|
+
},
|
|
639
|
+
'device-flow-session-required': {
|
|
640
|
+
code: 'E_NEXUS_SESSION_EXPIRED',
|
|
641
|
+
message:
|
|
642
|
+
'this session was not issued to the CLI by `cleo login nexus`, so it cannot enrol a device',
|
|
643
|
+
fix: SESSION_EXPIRED_LOGIN.fix,
|
|
644
|
+
},
|
|
645
|
+
'device-not-enrolled': {
|
|
646
|
+
code: 'E_NEXUS_NOT_SIGNED_IN',
|
|
647
|
+
message: 'this device is not enrolled with Cleo Nexus',
|
|
648
|
+
fix: LOGIN,
|
|
649
|
+
},
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* 409 `E_CONFLICT` reasons the CLI reports with their own code (§4.0.4). Every
|
|
654
|
+
* other conflict is either handled inside its flow or reported as
|
|
655
|
+
* `E_NEXUS_REQUEST_FAILED` with the server's message.
|
|
656
|
+
*/
|
|
657
|
+
export const NEXUS_CONFLICT_ERRORS: Readonly<
|
|
658
|
+
Partial<Record<'replica-copied' | 'rotation-conflict', NexusErrorMapping>>
|
|
659
|
+
> = {
|
|
660
|
+
'replica-copied': {
|
|
661
|
+
code: 'E_NEXUS_REPLICA_COPIED',
|
|
662
|
+
message: 'this project store is attached from another device (a copied store)',
|
|
663
|
+
fix: null,
|
|
664
|
+
},
|
|
665
|
+
'rotation-conflict': COMPROMISED,
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
/** No answer at all (network error or timeout, §4.0.4). */
|
|
669
|
+
export const NEXUS_UNREACHABLE_ERROR: NexusErrorMapping = {
|
|
670
|
+
code: 'E_NEXUS_UNREACHABLE',
|
|
671
|
+
message: null,
|
|
672
|
+
fix: 'check your network and retry',
|
|
673
|
+
};
|