@usestatemachines/cli 0.0.0-stage → 0.1.0

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.
Files changed (169) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +34 -2
  3. package/dist/agents/bundled.d.ts +5 -0
  4. package/dist/agents/bundled.js +6 -0
  5. package/dist/agents/destinations.d.ts +13 -0
  6. package/dist/agents/destinations.js +16 -0
  7. package/dist/api-internal.d.ts +62 -0
  8. package/dist/api-internal.js +348 -0
  9. package/dist/bin.d.ts +2 -0
  10. package/dist/bin.js +4 -0
  11. package/dist/command.d.ts +119 -0
  12. package/dist/command.js +195 -0
  13. package/dist/commands/api/command.d.ts +15 -0
  14. package/dist/commands/api/command.js +38 -0
  15. package/dist/commands/api/run.d.ts +4 -0
  16. package/dist/commands/api/run.js +24 -0
  17. package/dist/commands/apps/command.d.ts +19 -0
  18. package/dist/commands/apps/command.js +29 -0
  19. package/dist/commands/apps/list.d.ts +8 -0
  20. package/dist/commands/apps/list.js +10 -0
  21. package/dist/commands/connect/command.d.ts +34 -0
  22. package/dist/commands/connect/command.js +70 -0
  23. package/dist/commands/connect/run.d.ts +10 -0
  24. package/dist/commands/connect/run.js +71 -0
  25. package/dist/commands/doctor/command.d.ts +29 -0
  26. package/dist/commands/doctor/command.js +36 -0
  27. package/dist/commands/doctor/run.d.ts +13 -0
  28. package/dist/commands/doctor/run.js +12 -0
  29. package/dist/commands/env/command.d.ts +5 -0
  30. package/dist/commands/env/command.js +19 -0
  31. package/dist/commands/env/pull.d.ts +6 -0
  32. package/dist/commands/env/pull.js +6 -0
  33. package/dist/commands/environments/command.d.ts +121 -0
  34. package/dist/commands/environments/command.js +170 -0
  35. package/dist/commands/environments/create.d.ts +9 -0
  36. package/dist/commands/environments/create.js +42 -0
  37. package/dist/commands/environments/delete.d.ts +7 -0
  38. package/dist/commands/environments/delete.js +48 -0
  39. package/dist/commands/environments/get.d.ts +39 -0
  40. package/dist/commands/environments/get.js +5 -0
  41. package/dist/commands/environments/list.d.ts +41 -0
  42. package/dist/commands/environments/list.js +18 -0
  43. package/dist/commands/environments/wait.d.ts +39 -0
  44. package/dist/commands/environments/wait.js +21 -0
  45. package/dist/commands/init/command.d.ts +68 -0
  46. package/dist/commands/init/command.js +90 -0
  47. package/dist/commands/init/run.d.ts +8 -0
  48. package/dist/commands/init/run.js +422 -0
  49. package/dist/commands/link/command.d.ts +13 -0
  50. package/dist/commands/link/command.js +32 -0
  51. package/dist/commands/link/run.d.ts +12 -0
  52. package/dist/commands/link/run.js +7 -0
  53. package/dist/commands/login/command.d.ts +1 -0
  54. package/dist/commands/login/command.js +43 -0
  55. package/dist/commands/login/run.d.ts +30 -0
  56. package/dist/commands/login/run.js +43 -0
  57. package/dist/commands/login/schema.d.ts +33 -0
  58. package/dist/commands/login/schema.js +33 -0
  59. package/dist/commands/logout/command.d.ts +1 -0
  60. package/dist/commands/logout/command.js +24 -0
  61. package/dist/commands/logout/run.d.ts +10 -0
  62. package/dist/commands/logout/run.js +19 -0
  63. package/dist/commands/logout/schema.d.ts +10 -0
  64. package/dist/commands/logout/schema.js +10 -0
  65. package/dist/commands/profiles/command.d.ts +2 -0
  66. package/dist/commands/profiles/command.js +29 -0
  67. package/dist/commands/profiles/list.d.ts +20 -0
  68. package/dist/commands/profiles/list.js +10 -0
  69. package/dist/commands/profiles/schema.d.ts +24 -0
  70. package/dist/commands/profiles/schema.js +19 -0
  71. package/dist/commands/profiles/use.d.ts +7 -0
  72. package/dist/commands/profiles/use.js +7 -0
  73. package/dist/commands/requests/command.d.ts +39 -0
  74. package/dist/commands/requests/command.js +65 -0
  75. package/dist/commands/requests/get.d.ts +32 -0
  76. package/dist/commands/requests/get.js +15 -0
  77. package/dist/commands/requests/list.d.ts +27 -0
  78. package/dist/commands/requests/list.js +25 -0
  79. package/dist/commands/snapshots/command.d.ts +48 -0
  80. package/dist/commands/snapshots/command.js +80 -0
  81. package/dist/commands/snapshots/create.d.ts +7 -0
  82. package/dist/commands/snapshots/create.js +26 -0
  83. package/dist/commands/snapshots/delete.d.ts +34 -0
  84. package/dist/commands/snapshots/delete.js +8 -0
  85. package/dist/commands/snapshots/get.d.ts +35 -0
  86. package/dist/commands/snapshots/get.js +5 -0
  87. package/dist/commands/snapshots/list.d.ts +37 -0
  88. package/dist/commands/snapshots/list.js +10 -0
  89. package/dist/commands/unlink/command.d.ts +1 -0
  90. package/dist/commands/unlink/command.js +15 -0
  91. package/dist/commands/unlink/run.d.ts +7 -0
  92. package/dist/commands/unlink/run.js +8 -0
  93. package/dist/commands/whoami/command.d.ts +28 -0
  94. package/dist/commands/whoami/command.js +44 -0
  95. package/dist/commands/whoami/run.d.ts +5 -0
  96. package/dist/commands/whoami/run.js +39 -0
  97. package/dist/commands/workspaces/command.d.ts +33 -0
  98. package/dist/commands/workspaces/command.js +73 -0
  99. package/dist/commands/workspaces/list.d.ts +5 -0
  100. package/dist/commands/workspaces/list.js +42 -0
  101. package/dist/commands/workspaces/use.d.ts +5 -0
  102. package/dist/commands/workspaces/use.js +18 -0
  103. package/dist/config/project.d.ts +21 -0
  104. package/dist/config/project.js +105 -0
  105. package/dist/config/schema.d.ts +616 -0
  106. package/dist/config/schema.js +282 -0
  107. package/dist/context-types.d.ts +89 -0
  108. package/dist/context-types.js +56 -0
  109. package/dist/context.d.ts +13 -0
  110. package/dist/context.js +290 -0
  111. package/dist/credentials/sealing.d.ts +10 -0
  112. package/dist/credentials/sealing.js +72 -0
  113. package/dist/credentials/store.d.ts +17 -0
  114. package/dist/credentials/store.js +156 -0
  115. package/dist/doctor.d.ts +4 -0
  116. package/dist/doctor.js +239 -0
  117. package/dist/env-file.d.ts +10 -0
  118. package/dist/env-file.js +254 -0
  119. package/dist/errors.d.ts +784 -0
  120. package/dist/errors.js +459 -0
  121. package/dist/flags.d.ts +22 -0
  122. package/dist/flags.js +56 -0
  123. package/dist/generated/internal.d.ts +138 -0
  124. package/dist/generated/internal.js +2627 -0
  125. package/dist/generated/retry.d.ts +1 -0
  126. package/dist/generated/retry.js +11 -0
  127. package/dist/help.d.ts +408 -0
  128. package/dist/help.js +220 -0
  129. package/dist/io-types.d.ts +127 -0
  130. package/dist/io-types.js +1 -0
  131. package/dist/io.d.ts +13 -0
  132. package/dist/io.js +1017 -0
  133. package/dist/keys.d.ts +50 -0
  134. package/dist/keys.js +691 -0
  135. package/dist/link.d.ts +6 -0
  136. package/dist/link.js +58 -0
  137. package/dist/login.d.ts +92 -0
  138. package/dist/login.js +965 -0
  139. package/dist/main.d.ts +2 -0
  140. package/dist/main.js +126 -0
  141. package/dist/native-helper.d.ts +1 -0
  142. package/dist/native-helper.js +2 -0
  143. package/dist/output.d.ts +8 -0
  144. package/dist/output.js +135 -0
  145. package/dist/parser.d.ts +28 -0
  146. package/dist/parser.js +250 -0
  147. package/dist/program-types.d.ts +67 -0
  148. package/dist/program-types.js +14 -0
  149. package/dist/programs.d.ts +2 -0
  150. package/dist/programs.js +44 -0
  151. package/dist/project-roots.d.ts +3 -0
  152. package/dist/project-roots.js +26 -0
  153. package/dist/prompt.d.ts +3 -0
  154. package/dist/prompt.js +61 -0
  155. package/dist/registry.d.ts +2 -0
  156. package/dist/registry.js +44 -0
  157. package/dist/resources/create.d.ts +9 -0
  158. package/dist/resources/create.js +55 -0
  159. package/dist/resources/read.d.ts +15 -0
  160. package/dist/resources/read.js +69 -0
  161. package/dist/resources/schema.d.ts +177 -0
  162. package/dist/resources/schema.js +158 -0
  163. package/dist/text.d.ts +3 -0
  164. package/dist/text.js +19 -0
  165. package/dist/update.d.ts +22 -0
  166. package/dist/update.js +126 -0
  167. package/dist/version.d.ts +2 -0
  168. package/dist/version.js +2 -0
  169. package/package.json +52 -3
package/dist/login.js ADDED
@@ -0,0 +1,965 @@
1
+ import { join } from 'node:path';
2
+ import { StateMachinesError } from '@usestatemachines/sdk';
3
+ import { z } from 'zod';
4
+ import { configSchema, emptyConfig, parseVersioned, pendingPayloadSchema, } from './config/schema.js';
5
+ import { apiOriginFromEnvironment, apiOriginSchema, loginIdSchema, organizationIdSchema, profileNameFromEnvironment, profileNameSchema, tokenIdSchema, } from './context-types.js';
6
+ import { createLoginKey, unsealLoginToken } from './credentials/sealing.js';
7
+ import { CredentialStore } from './credentials/store.js';
8
+ import { ApiError, CliError, failedWith, keychainFailures } from './errors.js';
9
+ import { KeyOwner } from './keys.js';
10
+ export const recoveryReportSchema = z.strictObject({
11
+ acknowledged: z.array(z.string()),
12
+ revoked: z.array(z.string()),
13
+ pending: z.array(z.string()),
14
+ unconfirmed: z.array(z.string()),
15
+ });
16
+ const copy = {
17
+ replace: 'Replace the saved session for this profile?',
18
+ replacementRequired: 'This profile already has a session. Use --yes to replace it.',
19
+ denied: 'Login was denied. Start a new login to try again.',
20
+ expired: 'Login expired. Start a new login to try again.',
21
+ };
22
+ const storeFailures = [
23
+ ...keychainFailures,
24
+ 'cli.config_io',
25
+ 'cli.credential_store_mismatch',
26
+ ];
27
+ function sameRef(left, right) {
28
+ return (left.store === right.store &&
29
+ left.origin === right.origin &&
30
+ left.entry === right.entry);
31
+ }
32
+ function readPendingPayload(text, attempt) {
33
+ const payload = parseVersioned(text, pendingPayloadSchema);
34
+ if (attempt.login === null ||
35
+ payload.loginId !== attempt.login.loginId ||
36
+ payload.origin !== attempt.origin ||
37
+ payload.expiresAt !== attempt.login.expiresAt ||
38
+ payload.interval !== attempt.login.interval) {
39
+ throw new CliError('cli.credentials_invalid');
40
+ }
41
+ return payload;
42
+ }
43
+ class AccountOwner {
44
+ #io;
45
+ #clients;
46
+ #version;
47
+ #store;
48
+ #options;
49
+ #path;
50
+ #nonce;
51
+ programKeys;
52
+ constructor(input) {
53
+ this.#io = input.io;
54
+ this.#clients = input.clients;
55
+ this.#version = input.version;
56
+ this.#store = new CredentialStore(input.io);
57
+ this.#options = input.options ?? {
58
+ signal: input.io.signal,
59
+ deadline: null,
60
+ };
61
+ this.#path = join(input.io.host.configHome, 'config.json');
62
+ this.#nonce = this.#id();
63
+ this.programKeys = new KeyOwner({
64
+ io: input.io,
65
+ clients: input.clients,
66
+ store: this.#store,
67
+ files: input.environmentFiles,
68
+ transaction: (options, action) => this.#transaction(options, action, false),
69
+ readToken: (reference, options) => this.#readToken(reference, options),
70
+ readBackgroundToken: (reference, options, bounded) => this.#readBackgroundToken(reference, options, bounded),
71
+ queueCleanup: (state, reference) => this.#queueCleanup(state, reference),
72
+ cleanup: (transaction) => this.#cleanup(transaction),
73
+ });
74
+ }
75
+ #id() {
76
+ return Buffer.from(this.#io.randomBytes(24)).toString('base64url');
77
+ }
78
+ #check(options) {
79
+ if (options.signal.aborted) {
80
+ throw new CliError('cli.cancelled');
81
+ }
82
+ if (options.deadline !== null && this.#io.now() >= options.deadline) {
83
+ throw new CliError('cli.wait_timeout');
84
+ }
85
+ }
86
+ async #read(options) {
87
+ const text = await this.#io.files.read(this.#path, options);
88
+ return text === null ? emptyConfig() : parseVersioned(text, configSchema);
89
+ }
90
+ async #readToken(reference, options) {
91
+ const token = await this.#store.get(reference, options);
92
+ if (token !== null && !/^smcli_[A-Za-z0-9_-]{43}$/.test(token)) {
93
+ throw new CliError('cli.credentials_invalid');
94
+ }
95
+ return token;
96
+ }
97
+ async #readBackgroundToken(reference, options, bounded) {
98
+ try {
99
+ const token = await this.#readToken(reference, bounded);
100
+ this.#check(options);
101
+ return token === null ? { kind: 'missing' } : { kind: 'ready', token };
102
+ }
103
+ catch (error) {
104
+ this.#check(options);
105
+ if (failedWith(error, [
106
+ ...storeFailures,
107
+ 'cli.wait_timeout',
108
+ 'cli.credentials_invalid',
109
+ ])) {
110
+ return { kind: 'pending' };
111
+ }
112
+ throw error;
113
+ }
114
+ }
115
+ #selectedSession(state, selection) {
116
+ const name = this.#selectedProfile(selection.profile, state);
117
+ if (name === null) {
118
+ throw new CliError('cli.no_credential');
119
+ }
120
+ const profile = state.profiles.find((item) => item.name === name);
121
+ if (profile?.session === undefined || profile.session === null) {
122
+ throw new CliError('cli.profile_not_found');
123
+ }
124
+ const session = profile.session;
125
+ if (selection.expectedOrigin !== null &&
126
+ selection.expectedOrigin !== session.origin) {
127
+ throw new CliError('cli.origin_mismatch');
128
+ }
129
+ if (Date.parse(session.expiresAt) <= this.#io.now()) {
130
+ throw new CliError('cli.credential_expired');
131
+ }
132
+ return { name, profile, session };
133
+ }
134
+ async #transaction(options, action, recover = true) {
135
+ return this.#io.files.withLock(join(this.#io.host.configHome, '.lock'), options, async () => {
136
+ const state = await this.#read(options);
137
+ const transaction = {
138
+ state,
139
+ options,
140
+ save: () => this.#io.files.atomicWrite(this.#path, JSON.stringify(state), 'private', options),
141
+ };
142
+ if (recover) {
143
+ await this.#recover(transaction);
144
+ }
145
+ return action(transaction);
146
+ });
147
+ }
148
+ #selectedProfile(explicit, state) {
149
+ const environment = this.#io.host.environment.STATEMACHINES_PROFILE;
150
+ return (explicit ??
151
+ profileNameFromEnvironment(environment) ??
152
+ state.defaultProfile);
153
+ }
154
+ #queueCleanup(state, reference) {
155
+ if (!state.cleanup.some((item) => sameRef(item, reference))) {
156
+ state.cleanup.push(reference);
157
+ }
158
+ }
159
+ #retireAttempt(state, id) {
160
+ const attempt = state.attempts.find((item) => item.id === id);
161
+ if (attempt?.secret !== null && attempt?.secret !== undefined) {
162
+ this.#queueCleanup(state, attempt.secret);
163
+ }
164
+ state.attempts = state.attempts.filter((item) => item.id !== id);
165
+ }
166
+ async #cleanup(transaction) {
167
+ const { state, options } = transaction;
168
+ for (const reference of [...state.cleanup]) {
169
+ const held = [
170
+ ...this.programKeys.references(state),
171
+ ...state.profiles.flatMap((profile) => profile.session === null
172
+ ? []
173
+ : [
174
+ profile.session.token,
175
+ ...profile.session.runKeys.map((key) => key.secret),
176
+ ]),
177
+ ...state.attempts.flatMap((attempt) => attempt.secret === null ? [] : [attempt.secret]),
178
+ ...state.acknowledgements.map((job) => job.token),
179
+ ...state.revocations.map((job) => job.token),
180
+ ...state.intents.map((intent) => intent.kind === 'install' ? intent.candidate.token : intent.token),
181
+ ];
182
+ if (held.some((item) => sameRef(item, reference))) {
183
+ continue;
184
+ }
185
+ try {
186
+ await this.#store.delete(reference, options);
187
+ }
188
+ catch (error) {
189
+ this.#check(options);
190
+ if (failedWith(error, storeFailures)) {
191
+ continue;
192
+ }
193
+ throw error;
194
+ }
195
+ state.cleanup = state.cleanup.filter((item) => !sameRef(item, reference));
196
+ await transaction.save();
197
+ }
198
+ }
199
+ async #write(transaction, holder, value) {
200
+ try {
201
+ await this.#store.set(holder.token, value, transaction.options);
202
+ }
203
+ catch (error) {
204
+ holder.token = await this.#store.fallback(holder.token, error);
205
+ await transaction.save();
206
+ await this.#store.set(holder.token, value, transaction.options);
207
+ }
208
+ }
209
+ #queueRevocation(state, session) {
210
+ const id = `revoke:${session.origin}:${session.tokenId}`;
211
+ if (!state.revocations.some((job) => job.id === id)) {
212
+ state.revocations.push({
213
+ id,
214
+ origin: session.origin,
215
+ tokenId: session.tokenId,
216
+ token: session.token,
217
+ });
218
+ }
219
+ }
220
+ async #finishInstall(transaction, intent) {
221
+ const { state } = transaction;
222
+ const profile = state.profiles.find((item) => item.name === intent.profile);
223
+ const attempt = state.attempts.find((item) => item.id === intent.attemptId);
224
+ const valid = profile !== undefined &&
225
+ profile.generation === intent.generation &&
226
+ attempt?.generation === intent.generation &&
227
+ attempt.profile === intent.profile &&
228
+ attempt.owner?.nonce === this.#nonce;
229
+ if (!valid) {
230
+ this.#queueRevocation(state, intent.candidate);
231
+ }
232
+ else {
233
+ if (profile.session !== null) {
234
+ this.#queueRevocation(state, profile.session);
235
+ this.programKeys.retireSession(state, profile.session);
236
+ }
237
+ profile.session = intent.candidate;
238
+ profile.defaultWorkspace = null;
239
+ profile.generation += 1;
240
+ state.defaultProfile ??= profile.name;
241
+ state.acknowledgements.push({
242
+ id: `ack:${intent.candidate.origin}:${intent.loginId}:${intent.candidate.tokenId}`,
243
+ origin: intent.candidate.origin,
244
+ loginId: intent.loginId,
245
+ tokenId: intent.candidate.tokenId,
246
+ token: intent.candidate.token,
247
+ });
248
+ }
249
+ this.#retireAttempt(state, intent.attemptId);
250
+ state.intents = state.intents.filter((item) => item.id !== intent.id);
251
+ await transaction.save();
252
+ return valid;
253
+ }
254
+ async #recover(transaction) {
255
+ const { state, options } = transaction;
256
+ for (const intent of [...state.intents]) {
257
+ try {
258
+ if (intent.kind === 'install') {
259
+ const token = await this.#readToken(intent.candidate.token, options);
260
+ if (token !== null) {
261
+ this.#queueRevocation(state, intent.candidate);
262
+ }
263
+ this.#retireAttempt(state, intent.attemptId);
264
+ }
265
+ else {
266
+ const value = await this.#store.get(intent.token, options);
267
+ const attempt = state.attempts.find((item) => item.id === intent.attemptId);
268
+ if (value !== null && attempt !== undefined) {
269
+ readPendingPayload(value, attempt);
270
+ attempt.secret = intent.token;
271
+ attempt.owner = null;
272
+ }
273
+ else {
274
+ this.#retireAttempt(state, intent.attemptId);
275
+ this.#queueCleanup(state, intent.token);
276
+ }
277
+ }
278
+ state.intents = state.intents.filter((item) => item.id !== intent.id);
279
+ await transaction.save();
280
+ }
281
+ catch (error) {
282
+ this.#check(options);
283
+ if (!failedWith(error, [
284
+ ...storeFailures,
285
+ 'cli.credentials_invalid',
286
+ 'cli.config_invalid',
287
+ ])) {
288
+ throw error;
289
+ }
290
+ }
291
+ }
292
+ for (const attempt of [...state.attempts]) {
293
+ if (attempt.owner?.nonce === this.#nonce) {
294
+ continue;
295
+ }
296
+ if (attempt.login === null
297
+ ? Date.parse(attempt.createdAt) + 15 * 60_000 <= this.#io.now()
298
+ : Date.parse(attempt.login.expiresAt) <= this.#io.now()) {
299
+ this.#retireAttempt(state, attempt.id);
300
+ await transaction.save();
301
+ continue;
302
+ }
303
+ if (attempt.owner !== null &&
304
+ attempt.owner.hostname === this.#io.host.owner.hostname &&
305
+ (await this.#io.child.liveness(attempt.owner, options)) === 'dead') {
306
+ if (attempt.secret === null) {
307
+ this.#retireAttempt(state, attempt.id);
308
+ }
309
+ else {
310
+ attempt.owner = null;
311
+ }
312
+ await transaction.save();
313
+ }
314
+ }
315
+ for (const job of [...state.revocations]) {
316
+ const active = state.profiles.find((profile) => profile.session?.origin === job.origin &&
317
+ profile.session.tokenId === job.tokenId);
318
+ if (active?.session !== undefined && active.session !== null) {
319
+ state.revocations = state.revocations.filter((item) => item.id !== job.id);
320
+ if (!sameRef(active.session.token, job.token)) {
321
+ this.#queueCleanup(state, job.token);
322
+ }
323
+ await transaction.save();
324
+ }
325
+ }
326
+ await this.programKeys.recover(transaction);
327
+ await this.#cleanup(transaction);
328
+ }
329
+ async reconcile(reason, options = this.#options) {
330
+ const budget = Math.min(this.#io.now() + 10_000, options.deadline ?? Infinity);
331
+ const bounded = { signal: options.signal, deadline: budget };
332
+ const queued = async (action) => {
333
+ try {
334
+ return { value: await action() };
335
+ }
336
+ catch (error) {
337
+ this.#check(options);
338
+ if (failedWith(error, ['cli.wait_timeout'])) {
339
+ return undefined;
340
+ }
341
+ throw error;
342
+ }
343
+ };
344
+ const jobs = (await queued(() => this.#transaction(bounded, async ({ state }) => state)))?.value ?? (await this.#read(options));
345
+ const report = {
346
+ acknowledged: [],
347
+ revoked: [],
348
+ pending: [],
349
+ unconfirmed: [],
350
+ };
351
+ const call = () => {
352
+ const deadline = Math.min(budget, this.#io.now() + 5_000);
353
+ return deadline > this.#io.now()
354
+ ? { signal: options.signal, deadline }
355
+ : null;
356
+ };
357
+ for (const job of jobs.acknowledgements) {
358
+ this.#check(options);
359
+ const credential = await this.#readBackgroundToken(job.token, options, bounded);
360
+ if (credential.kind === 'pending') {
361
+ report.pending.push(job.id);
362
+ continue;
363
+ }
364
+ if (credential.kind === 'ready') {
365
+ const effect = call();
366
+ if (effect === null) {
367
+ report.pending.push(job.id);
368
+ continue;
369
+ }
370
+ try {
371
+ await this.#clients
372
+ .authenticated(job.origin, credential.token)
373
+ .collected(job.loginId, effect);
374
+ report.acknowledged.push(job.id);
375
+ }
376
+ catch (error) {
377
+ this.#check(options);
378
+ const code = error instanceof StateMachinesError
379
+ ? error.code
380
+ : error instanceof ApiError
381
+ ? error.document.error.code
382
+ : null;
383
+ if (!((error instanceof StateMachinesError &&
384
+ error.status === 401 &&
385
+ code === 'unauthenticated') ||
386
+ code === 'expired_token')) {
387
+ report.pending.push(job.id);
388
+ continue;
389
+ }
390
+ }
391
+ }
392
+ if (!(await queued(() => this.#transaction(bounded, async (transaction) => {
393
+ transaction.state.acknowledgements =
394
+ transaction.state.acknowledgements.filter((item) => item.id !== job.id);
395
+ await transaction.save();
396
+ await this.#cleanup(transaction);
397
+ })))) {
398
+ report.pending.push(job.id);
399
+ }
400
+ }
401
+ if (reason !== 'next-command') {
402
+ for (const job of jobs.revocations) {
403
+ this.#check(options);
404
+ const credential = await this.#readBackgroundToken(job.token, options, bounded);
405
+ let outcome = 'unconfirmed';
406
+ if (credential.kind === 'pending') {
407
+ report.pending.push(job.id);
408
+ continue;
409
+ }
410
+ if (credential.kind === 'ready') {
411
+ const effect = call();
412
+ if (effect === null) {
413
+ report.pending.push(job.id);
414
+ continue;
415
+ }
416
+ try {
417
+ if ((await this.#clients
418
+ .authenticated(job.origin, credential.token)
419
+ .revokeCurrent(effect)) === 'revoked') {
420
+ outcome = 'revoked';
421
+ }
422
+ }
423
+ catch {
424
+ this.#check(options);
425
+ report.pending.push(job.id);
426
+ continue;
427
+ }
428
+ }
429
+ const removed = await queued(() => this.#transaction(bounded, async (transaction) => {
430
+ const { state } = transaction;
431
+ const matching = state.revocations.find((item) => item.id === job.id && sameRef(item.token, job.token));
432
+ if (matching === undefined) {
433
+ return;
434
+ }
435
+ state.revocations = state.revocations.filter((item) => item !== matching);
436
+ state.acknowledgements = state.acknowledgements.filter((item) => item.origin !== job.origin || item.tokenId !== job.tokenId);
437
+ this.programKeys.cascade(state, job);
438
+ this.#queueCleanup(state, job.token);
439
+ await transaction.save();
440
+ await this.#cleanup(transaction);
441
+ }));
442
+ if (!removed) {
443
+ report.pending.push(job.id);
444
+ }
445
+ else if (outcome === 'revoked') {
446
+ report.revoked.push(job.id);
447
+ }
448
+ else {
449
+ report.unconfirmed.push(job.tokenId);
450
+ }
451
+ }
452
+ }
453
+ const keys = await this.programKeys.reconcile(options, call);
454
+ report.revoked.push(...keys.revoked);
455
+ report.pending.push(...keys.pending);
456
+ report.unconfirmed = [
457
+ ...new Set([...report.unconfirmed, ...keys.unconfirmed]),
458
+ ];
459
+ return report;
460
+ }
461
+ async list(options = this.#options) {
462
+ await this.reconcile('next-command', options);
463
+ const state = await this.#read(options);
464
+ return {
465
+ defaultProfile: state.defaultProfile,
466
+ profiles: state.profiles.flatMap((profile) => profile.session === null
467
+ ? []
468
+ : [
469
+ {
470
+ name: profile.name,
471
+ origin: profile.session.origin,
472
+ organization: profile.session.organization,
473
+ email: profile.session.email,
474
+ tokenId: profile.session.tokenId,
475
+ idleExpiresAt: profile.session.idleExpiresAt,
476
+ expiresAt: profile.session.expiresAt,
477
+ defaultWorkspace: profile.defaultWorkspace,
478
+ },
479
+ ]),
480
+ };
481
+ }
482
+ async inspect(options = this.#options) {
483
+ const state = await this.#transaction(options, async (transaction) => transaction.state, false);
484
+ return {
485
+ defaultProfile: state.defaultProfile,
486
+ profiles: state.profiles.flatMap((profile) => profile.session === null
487
+ ? []
488
+ : [
489
+ {
490
+ name: profile.name,
491
+ origin: profile.session.origin,
492
+ organizationId: profile.session.organization.id,
493
+ store: profile.session.token.store,
494
+ idleExpiresAt: profile.session.idleExpiresAt,
495
+ expiresAt: profile.session.expiresAt,
496
+ },
497
+ ]),
498
+ pendingAcknowledgements: state.acknowledgements.length,
499
+ pendingRevocations: state.revocations.length + state.keyRevocations.length,
500
+ pendingCleanup: state.cleanup.length,
501
+ };
502
+ }
503
+ async use(name, options = this.#options) {
504
+ await this.reconcile('next-command', options);
505
+ return this.#transaction(options, async ({ state, save }) => {
506
+ if (!state.profiles.some((profile) => profile.name === name && profile.session !== null)) {
507
+ throw new CliError('cli.invalid_arguments', {
508
+ message: `No saved profile is named ${name}.`,
509
+ next: [
510
+ {
511
+ command: 'statemachines profiles list',
512
+ reason: 'See the saved profiles.',
513
+ },
514
+ ],
515
+ });
516
+ }
517
+ state.defaultProfile = name;
518
+ await save();
519
+ return { profile: name };
520
+ });
521
+ }
522
+ async setWorkspace(input, options = this.#options) {
523
+ const { account, workspace, mode } = input;
524
+ await this.reconcile('next-command', options);
525
+ await this.#transaction(options, async ({ state, save }) => {
526
+ const profile = state.profiles.find((item) => item.name === account.profile);
527
+ if (profile?.session === undefined || profile.session === null) {
528
+ throw new CliError('cli.profile_not_found');
529
+ }
530
+ if (profile.session.tokenId !== account.tokenId ||
531
+ profile.session.origin !== account.origin) {
532
+ throw new CliError('cli.profile_changed');
533
+ }
534
+ if (mode === 'if-unset' && profile.defaultWorkspace !== null) {
535
+ return;
536
+ }
537
+ profile.defaultWorkspace = workspace;
538
+ await save();
539
+ });
540
+ }
541
+ async withCredential(selection, action, options = this.#options) {
542
+ this.#selectedSession(await this.#read(options), selection);
543
+ await this.reconcile('next-command', options);
544
+ const { name, profile, session } = this.#selectedSession(await this.#read(options), selection);
545
+ const token = await this.#readToken(session.token, options);
546
+ if (token === null) {
547
+ throw new CliError('cli.credentials_invalid');
548
+ }
549
+ return action({
550
+ account: {
551
+ kind: 'cli-token',
552
+ origin: session.origin,
553
+ profile: name,
554
+ tokenId: session.tokenId,
555
+ organizationId: session.organization.id,
556
+ },
557
+ defaultWorkspace: profile.defaultWorkspace,
558
+ token,
559
+ });
560
+ }
561
+ async logout(input, options = this.#options) {
562
+ const removed = await this.#transaction(options, async (transaction) => {
563
+ const { state } = transaction;
564
+ const name = input.profiles === 'all'
565
+ ? null
566
+ : this.#selectedProfile(input.profiles, state);
567
+ const selected = input.profiles === 'all'
568
+ ? state.profiles
569
+ : state.profiles.filter((profile) => profile.name === name);
570
+ const names = [];
571
+ const revocations = [];
572
+ for (const profile of selected) {
573
+ if (profile.session !== null) {
574
+ names.push(profile.name);
575
+ revocations.push(`revoke:${profile.session.origin}:${profile.session.tokenId}`);
576
+ this.#queueRevocation(state, profile.session);
577
+ this.programKeys.retireSession(state, profile.session);
578
+ }
579
+ profile.session = null;
580
+ profile.defaultWorkspace = null;
581
+ profile.generation += 1;
582
+ for (const attempt of state.attempts.filter((item) => item.profile === profile.name)) {
583
+ this.#retireAttempt(state, attempt.id);
584
+ }
585
+ if (state.defaultProfile === profile.name) {
586
+ state.defaultProfile = null;
587
+ }
588
+ }
589
+ await transaction.save();
590
+ await this.#cleanup(transaction);
591
+ return { names, revocations };
592
+ });
593
+ const report = await this.reconcile('logout', options);
594
+ const pendingJobs = (await this.#read(options)).revocations;
595
+ const pending = pendingJobs.map((job) => job.tokenId);
596
+ if (pendingJobs.some((job) => removed.revocations.includes(job.id))) {
597
+ throw new CliError('cli.logout_pending', {
598
+ details: {
599
+ profiles: removed.names,
600
+ pendingRevocations: pending,
601
+ unconfirmedRevocations: report.unconfirmed,
602
+ environmentCredentialUnchanged: this.#io.host.environment.STATEMACHINES_API_KEY !== undefined,
603
+ },
604
+ });
605
+ }
606
+ return {
607
+ profiles: removed.names,
608
+ pendingRevocations: pending,
609
+ unconfirmedRevocations: report.unconfirmed,
610
+ environmentCredentialUnchanged: this.#io.host.environment.STATEMACHINES_API_KEY !== undefined,
611
+ };
612
+ }
613
+ async #claim(input, options) {
614
+ return this.#transaction(options, async (transaction) => {
615
+ const { state } = transaction;
616
+ let profile = state.profiles.find((item) => item.name === input.profile);
617
+ if (profile === undefined) {
618
+ profile = {
619
+ name: input.profile,
620
+ generation: 0,
621
+ session: null,
622
+ defaultWorkspace: null,
623
+ };
624
+ state.profiles.push(profile);
625
+ }
626
+ const pending = state.attempts.find((item) => item.profile === input.profile);
627
+ if (pending !== undefined) {
628
+ if (pending.owner !== null) {
629
+ const dead = pending.owner.hostname === this.#io.host.owner.hostname &&
630
+ (await this.#io.child.liveness(pending.owner, options)) === 'dead';
631
+ if (!dead) {
632
+ throw new CliError('cli.login_busy', pending.login === null
633
+ ? {
634
+ next: [
635
+ {
636
+ command: 'statemachines logout --profile <profile> --yes',
637
+ reason: `Cancel the unfinished login for ${input.profile}. This also signs out any existing session in that profile.`,
638
+ },
639
+ ],
640
+ }
641
+ : {});
642
+ }
643
+ }
644
+ if (input.mode === 'resume' &&
645
+ pending.secret !== null &&
646
+ pending.login !== null) {
647
+ if (pending.origin !== input.origin) {
648
+ throw new CliError('cli.origin_mismatch');
649
+ }
650
+ pending.owner = { ...this.#io.host.owner, nonce: this.#nonce };
651
+ await transaction.save();
652
+ return pending;
653
+ }
654
+ if (pending.owner === null && pending.secret !== null) {
655
+ throw new CliError('cli.login_busy', {
656
+ next: [
657
+ {
658
+ command: 'statemachines login --profile <profile> --resume',
659
+ reason: `Continue the saved login for ${input.profile}.`,
660
+ },
661
+ ],
662
+ });
663
+ }
664
+ this.#retireAttempt(state, pending.id);
665
+ }
666
+ if (input.mode === 'resume') {
667
+ throw new CliError('cli.profile_not_found');
668
+ }
669
+ const attempt = {
670
+ id: this.#id(),
671
+ profile: input.profile,
672
+ generation: profile.generation,
673
+ origin: input.origin,
674
+ owner: { ...this.#io.host.owner, nonce: this.#nonce },
675
+ login: null,
676
+ secret: null,
677
+ createdAt: new Date(this.#io.now()).toISOString(),
678
+ };
679
+ state.attempts.push(attempt);
680
+ await transaction.save();
681
+ return attempt;
682
+ });
683
+ }
684
+ async *login(request, options = this.#options) {
685
+ await this.reconcile('login', options);
686
+ const initial = await this.#read(options);
687
+ const profileName = this.#selectedProfile(request.profile, initial) ??
688
+ profileNameSchema.parse('default');
689
+ const saved = initial.profiles.find((profile) => profile.name === profileName);
690
+ const pending = initial.attempts.find((attempt) => attempt.profile === profileName);
691
+ const configuredOrigin = this.#io.host.environment.STATEMACHINES_BASE_URL;
692
+ const origin = request.origin ??
693
+ apiOriginFromEnvironment(configuredOrigin) ??
694
+ (request.mode === 'resume' ? pending?.origin : saved?.session?.origin) ??
695
+ apiOriginSchema.parse('https://api.usestatemachines.com');
696
+ const input = {
697
+ ...request,
698
+ profile: profileName,
699
+ origin,
700
+ };
701
+ const attempt = await this.#claim(input, options);
702
+ let payload;
703
+ let completed = false;
704
+ try {
705
+ const snapshot = await this.#read(options);
706
+ const profile = snapshot.profiles.find((item) => item.name === input.profile);
707
+ if (profile?.session !== undefined &&
708
+ profile.session !== null &&
709
+ !input.replace &&
710
+ input.mode !== 'resume') {
711
+ if (!input.interactive) {
712
+ throw new CliError('cli.input_required', {
713
+ message: copy.replacementRequired,
714
+ next: [
715
+ {
716
+ command: 'statemachines login --profile <profile> --yes',
717
+ reason: 'Replace the saved session for this profile.',
718
+ },
719
+ ],
720
+ });
721
+ }
722
+ if (!(await this.#io.prompt.confirm(copy.replace, options))) {
723
+ throw new CliError('cli.cancelled');
724
+ }
725
+ }
726
+ if (profile?.session !== undefined && profile.session !== null) {
727
+ await this.#readToken(profile.session.token, options);
728
+ }
729
+ const selectedStore = attempt.secret?.store ?? this.#store.select();
730
+ if (input.mode === 'resume') {
731
+ if (attempt.secret === null) {
732
+ throw new CliError('cli.credentials_invalid');
733
+ }
734
+ const stored = await this.#store.get(attempt.secret, options);
735
+ if (stored === null) {
736
+ throw new CliError('cli.credentials_invalid');
737
+ }
738
+ payload = readPendingPayload(stored, attempt);
739
+ }
740
+ else {
741
+ const keys = createLoginKey(this.#io);
742
+ const started = await this.#clients.anonymous(input.origin).start({
743
+ publicKey: keys.publicKey,
744
+ cliVersion: this.#version,
745
+ hostname: this.#io.host.owner.hostname.slice(0, 64),
746
+ platform: this.#io.host.platform,
747
+ }, options);
748
+ attempt.login = {
749
+ loginId: loginIdSchema.parse(started.loginId),
750
+ userCode: started.userCode,
751
+ verificationUrl: started.verificationUrl,
752
+ expiresAt: new Date(this.#io.now() + started.expiresIn * 1000).toISOString(),
753
+ interval: started.interval,
754
+ };
755
+ payload = {
756
+ version: 1,
757
+ loginId: attempt.login.loginId,
758
+ origin: attempt.origin,
759
+ expiresAt: attempt.login.expiresAt,
760
+ interval: attempt.login.interval,
761
+ deviceCode: started.deviceCode,
762
+ privateKey: keys.privateKey,
763
+ };
764
+ await this.#transaction(options, async ({ state, save }) => {
765
+ const current = state.attempts.find((item) => item.id === attempt.id);
766
+ if (current === undefined ||
767
+ current.generation !== attempt.generation) {
768
+ throw new CliError('cli.profile_changed');
769
+ }
770
+ current.login = attempt.login;
771
+ await save();
772
+ });
773
+ }
774
+ const login = attempt.login;
775
+ if (login === null) {
776
+ throw new CliError('cli.config_invalid');
777
+ }
778
+ if (input.mode === 'no-wait') {
779
+ await this.#transaction(options, async (transaction) => {
780
+ const current = transaction.state.attempts.find((item) => item.id === attempt.id);
781
+ if (current === undefined) {
782
+ throw new CliError('cli.profile_changed');
783
+ }
784
+ const intent = {
785
+ kind: 'pending',
786
+ id: `pending:${attempt.id}`,
787
+ attemptId: attempt.id,
788
+ token: this.#store.reference(selectedStore, input.origin, `pending:${attempt.id}`),
789
+ };
790
+ transaction.state.intents.push(intent);
791
+ await transaction.save();
792
+ const serialized = JSON.stringify(payload);
793
+ await this.#write(transaction, intent, serialized);
794
+ if ((await this.#store.get(intent.token, options)) !== serialized) {
795
+ throw new CliError('cli.credentials_invalid');
796
+ }
797
+ current.secret = intent.token;
798
+ current.owner = null;
799
+ transaction.state.intents = transaction.state.intents.filter((item) => item.id !== intent.id);
800
+ await transaction.save();
801
+ });
802
+ completed = true;
803
+ yield {
804
+ kind: 'result',
805
+ value: {
806
+ status: 'awaiting-approval',
807
+ profile: input.profile,
808
+ loginId: login.loginId,
809
+ userCode: login.userCode,
810
+ verificationUrl: login.verificationUrl,
811
+ expiresAt: login.expiresAt,
812
+ userActionRequired: true,
813
+ },
814
+ };
815
+ return;
816
+ }
817
+ yield {
818
+ kind: 'progress',
819
+ value: {
820
+ status: 'waiting',
821
+ userCode: login.userCode,
822
+ verificationUrl: login.verificationUrl,
823
+ expiresAt: login.expiresAt,
824
+ },
825
+ };
826
+ if (input.browser && input.interactive) {
827
+ await this.#io.openBrowser(login.verificationUrl, options);
828
+ }
829
+ let interval = login.interval;
830
+ while (true) {
831
+ this.#check(options);
832
+ if (this.#io.now() >= Date.parse(login.expiresAt)) {
833
+ throw new ApiError({
834
+ status: 400,
835
+ code: 'expired_token',
836
+ message: copy.expired,
837
+ requestId: null,
838
+ details: null,
839
+ });
840
+ }
841
+ await this.#io.sleep(Math.min(interval * 1000, Date.parse(login.expiresAt) - this.#io.now()), options);
842
+ if (this.#io.now() >= Date.parse(login.expiresAt)) {
843
+ continue;
844
+ }
845
+ const response = await this.#clients
846
+ .anonymous(input.origin)
847
+ .poll(payload.deviceCode, options);
848
+ this.#check(options);
849
+ if (response.kind !== 'approved') {
850
+ if (response.kind === 'pending' || response.kind === 'slow-down') {
851
+ interval = Math.max(interval, response.interval);
852
+ continue;
853
+ }
854
+ throw new ApiError({
855
+ status: 400,
856
+ code: response.kind === 'denied' ? 'access_denied' : 'expired_token',
857
+ message: response.kind === 'denied' ? copy.denied : copy.expired,
858
+ requestId: null,
859
+ details: null,
860
+ });
861
+ }
862
+ const token = unsealLoginToken({
863
+ envelope: response.envelope,
864
+ privateKey: payload.privateKey,
865
+ loginId: login.loginId,
866
+ });
867
+ const candidate = {
868
+ origin: input.origin,
869
+ organization: {
870
+ id: organizationIdSchema.parse(response.organization.id),
871
+ name: response.organization.name,
872
+ },
873
+ email: response.user.email,
874
+ tokenId: tokenIdSchema.parse(response.tokenId),
875
+ token: this.#store.reference(selectedStore, input.origin, `token:${attempt.id}`),
876
+ idleExpiresAt: response.idleExpiresAt,
877
+ expiresAt: response.expiresAt,
878
+ runKeys: [],
879
+ };
880
+ const installed = await this.#transaction(options, async (transaction) => {
881
+ const intent = {
882
+ kind: 'install',
883
+ id: `install:${attempt.id}`,
884
+ attemptId: attempt.id,
885
+ profile: input.profile,
886
+ generation: attempt.generation,
887
+ loginId: login.loginId,
888
+ candidate,
889
+ };
890
+ transaction.state.intents.push(intent);
891
+ await transaction.save();
892
+ await this.#write(transaction, candidate, token);
893
+ if ((await this.#store.get(candidate.token, options)) !== token) {
894
+ throw new CliError('cli.credentials_invalid');
895
+ }
896
+ const valid = await this.#finishInstall(transaction, intent);
897
+ await this.#cleanup(transaction);
898
+ return valid;
899
+ });
900
+ completed = true;
901
+ await this.reconcile('login', options);
902
+ if (!installed) {
903
+ throw new CliError('cli.profile_changed');
904
+ }
905
+ const workspace = await this.#clients
906
+ .defaultWorkspace(input.origin, token, {
907
+ signal: options.signal,
908
+ deadline: Math.min(this.#io.now() + 5_000, options.deadline ?? Infinity),
909
+ })
910
+ .catch(() => null);
911
+ this.#check(options);
912
+ if (workspace !== null) {
913
+ await this.#transaction(options, async ({ state, save }) => {
914
+ const current = state.profiles.find((item) => item.name === input.profile);
915
+ if (current?.session?.tokenId === candidate.tokenId) {
916
+ current.defaultWorkspace = workspace;
917
+ await save();
918
+ }
919
+ });
920
+ }
921
+ yield {
922
+ kind: 'result',
923
+ value: {
924
+ status: 'signed-in',
925
+ profile: input.profile,
926
+ organization: candidate.organization,
927
+ email: candidate.email,
928
+ idleExpiresAt: candidate.idleExpiresAt,
929
+ expiresAt: candidate.expiresAt,
930
+ userActionRequired: false,
931
+ },
932
+ };
933
+ return;
934
+ }
935
+ }
936
+ finally {
937
+ if (!completed &&
938
+ !options.signal.aborted &&
939
+ (options.deadline === null || options.deadline > this.#io.now())) {
940
+ await this.#transaction(options, async (transaction) => {
941
+ const { state, save } = transaction;
942
+ const current = state.attempts.find((item) => item.id === attempt.id && item.owner?.nonce === this.#nonce);
943
+ if (current !== undefined) {
944
+ if (current.secret === null) {
945
+ this.#retireAttempt(state, current.id);
946
+ }
947
+ else {
948
+ current.owner = null;
949
+ }
950
+ await save();
951
+ }
952
+ await this.#recover(transaction);
953
+ }, false);
954
+ }
955
+ }
956
+ }
957
+ }
958
+ export function createAccounts(input) {
959
+ const owner = new AccountOwner(input);
960
+ return {
961
+ accounts: owner,
962
+ authentication: owner,
963
+ programKeys: owner.programKeys,
964
+ };
965
+ }