@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/keys.js ADDED
@@ -0,0 +1,691 @@
1
+ import path from 'node:path';
2
+ import { StateMachinesError } from '@usestatemachines/sdk';
3
+ import { checkEffect } from './api-internal.js';
4
+ import { CliError } from './errors.js';
5
+ import { keyPolicy, parseCreateKey } from './generated/internal.js';
6
+ const RUN_KEY_ROTATION_MS = 7 * 24 * 60 * 60 * 1_000;
7
+ function sameRef(left, right) {
8
+ return (left.store === right.store &&
9
+ left.origin === right.origin &&
10
+ left.entry === right.entry);
11
+ }
12
+ function binding(state, issuer) {
13
+ const profile = state.profiles.find((item) => item.name === issuer.profile);
14
+ return profile?.generation === issuer.generation &&
15
+ profile.session?.origin === issuer.origin &&
16
+ profile.session.tokenId === issuer.tokenId &&
17
+ sameRef(profile.session.token, issuer.token)
18
+ ? profile.session
19
+ : null;
20
+ }
21
+ function selected(state, account) {
22
+ const profile = state.profiles.find((item) => item.name === account.profile);
23
+ if (profile?.session === undefined ||
24
+ profile.session === null ||
25
+ profile.session.origin !== account.origin ||
26
+ profile.session.tokenId !== account.tokenId) {
27
+ throw new CliError('cli.profile_changed');
28
+ }
29
+ return {
30
+ profile: profile.name,
31
+ generation: profile.generation,
32
+ origin: profile.session.origin,
33
+ tokenId: profile.session.tokenId,
34
+ token: profile.session.token,
35
+ };
36
+ }
37
+ function backgroundFailure(error) {
38
+ return (error instanceof CliError &&
39
+ [
40
+ 'cli.config_io',
41
+ 'cli.keychain_denied',
42
+ 'cli.keychain_timeout',
43
+ 'cli.keychain_unavailable',
44
+ 'cli.credential_store_mismatch',
45
+ 'cli.git',
46
+ ].includes(error.document.error.code));
47
+ }
48
+ function usableSecret(secret) {
49
+ return (secret.length > 0 &&
50
+ !secret.startsWith('smcli_') &&
51
+ Array.from(secret).every((character) => character.charCodeAt(0) > 32 && character.charCodeAt(0) !== 127));
52
+ }
53
+ export class KeyOwner {
54
+ #input;
55
+ constructor(input) {
56
+ this.#input = input;
57
+ }
58
+ #id() {
59
+ return Buffer.from(this.#input.io.randomBytes(24)).toString('base64url');
60
+ }
61
+ references(state) {
62
+ return [
63
+ ...state.keyOperations.flatMap((operation) => [
64
+ operation.issuer.token,
65
+ operation.secret,
66
+ ]),
67
+ ...state.keyRevocations.flatMap((job) => [
68
+ job.token,
69
+ ...(job.secret === null ? [] : [job.secret]),
70
+ ]),
71
+ ...state.environments.map((environment) => environment.issuer.token),
72
+ ];
73
+ }
74
+ #queueRevocation(state, issuer, keyId, secret) {
75
+ if (!state.keyRevocations.some((job) => job.origin === issuer.origin &&
76
+ job.keyId === keyId &&
77
+ job.tokenId === issuer.tokenId)) {
78
+ state.keyRevocations.push({
79
+ id: this.#id(),
80
+ keyId,
81
+ origin: issuer.origin,
82
+ tokenId: issuer.tokenId,
83
+ token: issuer.token,
84
+ secret,
85
+ });
86
+ }
87
+ }
88
+ #queueIssuerCleanup(state, token) {
89
+ if (!state.profiles.some((profile) => profile.session !== null && sameRef(profile.session.token, token))) {
90
+ this.#input.queueCleanup(state, token);
91
+ }
92
+ }
93
+ #retire(state, operation) {
94
+ if (operation.phase === 'staged') {
95
+ this.#queueRevocation(state, operation.issuer, operation.keyId, operation.secret);
96
+ }
97
+ state.keyOperations = state.keyOperations.filter((item) => item.id !== operation.id);
98
+ this.#input.queueCleanup(state, operation.secret);
99
+ }
100
+ retireSession(state, session) {
101
+ for (const operation of [...state.keyOperations]) {
102
+ if (operation.phase === 'creating' &&
103
+ operation.issuer.origin === session.origin &&
104
+ operation.issuer.tokenId === session.tokenId) {
105
+ this.#retire(state, operation);
106
+ }
107
+ }
108
+ for (const key of session.runKeys) {
109
+ this.#queueRevocation(state, session, key.id, key.secret);
110
+ this.#input.queueCleanup(state, key.secret);
111
+ }
112
+ for (const environment of state.environments.filter((item) => item.issuer.origin === session.origin &&
113
+ item.issuer.tokenId === session.tokenId)) {
114
+ this.#queueRevocation(state, environment.issuer, environment.keyId, null);
115
+ }
116
+ state.environments = state.environments.filter((item) => item.issuer.origin !== session.origin ||
117
+ item.issuer.tokenId !== session.tokenId);
118
+ }
119
+ cascade(state, issuer) {
120
+ for (const operation of [...state.keyOperations]) {
121
+ if (operation.issuer.origin === issuer.origin &&
122
+ operation.issuer.tokenId === issuer.tokenId) {
123
+ this.#input.queueCleanup(state, operation.secret);
124
+ this.#input.queueCleanup(state, operation.issuer.token);
125
+ state.keyOperations = state.keyOperations.filter((item) => item.id !== operation.id);
126
+ }
127
+ }
128
+ for (const job of [...state.keyRevocations]) {
129
+ if (job.origin === issuer.origin && job.tokenId === issuer.tokenId) {
130
+ this.#input.queueCleanup(state, job.token);
131
+ if (job.secret !== null) {
132
+ this.#input.queueCleanup(state, job.secret);
133
+ }
134
+ state.keyRevocations = state.keyRevocations.filter((item) => item.id !== job.id);
135
+ }
136
+ }
137
+ for (const environment of [...state.environments]) {
138
+ if (environment.issuer.origin === issuer.origin &&
139
+ environment.issuer.tokenId === issuer.tokenId) {
140
+ this.#input.queueCleanup(state, environment.issuer.token);
141
+ state.environments = state.environments.filter((item) => item !== environment);
142
+ }
143
+ }
144
+ }
145
+ async #dead(operation, options) {
146
+ return (operation.owner === null ||
147
+ (operation.owner.hostname === this.#input.io.host.owner.hostname &&
148
+ (await this.#input.io.child.liveness(operation.owner, options)) ===
149
+ 'dead'));
150
+ }
151
+ async #release(operation, options) {
152
+ checkEffect(this.#input.io, options);
153
+ await this.#input.transaction(options, async (transaction) => {
154
+ const current = transaction.state.keyOperations.find((item) => item.id === operation.id &&
155
+ item.owner?.nonce === operation.owner?.nonce);
156
+ if (current !== undefined) {
157
+ current.owner = null;
158
+ await transaction.save();
159
+ }
160
+ });
161
+ }
162
+ async #secret(reference, options) {
163
+ const secret = await this.#input.store.get(reference, options);
164
+ if (secret !== null && !usableSecret(secret)) {
165
+ throw new CliError('cli.credentials_invalid');
166
+ }
167
+ return secret;
168
+ }
169
+ async #install(transaction, operation, secret, recover) {
170
+ const { state, options } = transaction;
171
+ const session = binding(state, operation.issuer);
172
+ if (session === null) {
173
+ this.#retire(state, operation);
174
+ await transaction.save();
175
+ return false;
176
+ }
177
+ if (operation.target.kind === 'run') {
178
+ const previous = session.runKeys.find((key) => key.workspaceId === operation.workspaceId);
179
+ if (previous !== undefined && previous.id !== operation.keyId) {
180
+ this.#input.queueCleanup(state, previous.secret);
181
+ }
182
+ session.runKeys = session.runKeys.filter((key) => key.workspaceId !== operation.workspaceId);
183
+ session.runKeys.push({
184
+ id: operation.keyId,
185
+ workspaceId: operation.workspaceId,
186
+ expiresAt: operation.expiresAt,
187
+ secret: operation.secret,
188
+ });
189
+ }
190
+ else {
191
+ const file = operation.target.file;
192
+ const credential = { key: secret, origin: operation.issuer.origin };
193
+ const commit = async () => {
194
+ const previous = state.environments.find((item) => item.repositoryRoot === file.repositoryRoot &&
195
+ item.issuer.origin === operation.issuer.origin &&
196
+ item.workspaceId === operation.workspaceId);
197
+ if (previous !== undefined && previous.keyId !== operation.keyId) {
198
+ this.#queueRevocation(state, previous.issuer, previous.keyId, null);
199
+ this.#queueIssuerCleanup(state, previous.issuer.token);
200
+ }
201
+ state.environments = state.environments.filter((item) => item !== previous);
202
+ state.environments.push({
203
+ repositoryRoot: file.repositoryRoot,
204
+ path: file.path,
205
+ workspaceId: operation.workspaceId,
206
+ keyId: operation.keyId,
207
+ expiresAt: operation.expiresAt,
208
+ issuer: operation.issuer,
209
+ });
210
+ this.#input.queueCleanup(state, operation.secret);
211
+ state.keyOperations = state.keyOperations.filter((item) => item.id !== operation.id);
212
+ await transaction.save();
213
+ };
214
+ if (recover) {
215
+ try {
216
+ return await this.#input.files.commitIfMatches(file, credential, options, commit);
217
+ }
218
+ catch (error) {
219
+ checkEffect(this.#input.io, options);
220
+ if (error instanceof CliError &&
221
+ error.document.error.code === 'cli.config_locked') {
222
+ return false;
223
+ }
224
+ throw error;
225
+ }
226
+ }
227
+ await this.#input.files.write(file, credential, options, commit);
228
+ return true;
229
+ }
230
+ state.keyOperations = state.keyOperations.filter((item) => item.id !== operation.id);
231
+ await transaction.save();
232
+ return true;
233
+ }
234
+ async recover(transaction) {
235
+ for (const operation of [...transaction.state.keyOperations]) {
236
+ if (operation.phase === 'creating' &&
237
+ binding(transaction.state, operation.issuer) === null &&
238
+ transaction.state.revocations.some((job) => job.origin === operation.issuer.origin &&
239
+ job.tokenId === operation.issuer.tokenId)) {
240
+ this.#retire(transaction.state, operation);
241
+ await transaction.save();
242
+ continue;
243
+ }
244
+ if (!(await this.#dead(operation, transaction.options))) {
245
+ continue;
246
+ }
247
+ if (operation.phase !== 'staged') {
248
+ continue;
249
+ }
250
+ try {
251
+ const secret = await this.#secret(operation.secret, transaction.options);
252
+ if (secret === null ||
253
+ Date.parse(operation.expiresAt) <= this.#input.io.now() ||
254
+ binding(transaction.state, operation.issuer) === null) {
255
+ this.#retire(transaction.state, operation);
256
+ await transaction.save();
257
+ }
258
+ else {
259
+ await this.#install(transaction, operation, secret, true);
260
+ }
261
+ }
262
+ catch (error) {
263
+ checkEffect(this.#input.io, transaction.options);
264
+ if (error instanceof CliError &&
265
+ [
266
+ 'cli.env_changed',
267
+ 'cli.env_path_refused',
268
+ 'cli.env_invalid',
269
+ ].includes(error.document.error.code)) {
270
+ this.#retire(transaction.state, operation);
271
+ await transaction.save();
272
+ }
273
+ else if (!backgroundFailure(error)) {
274
+ throw error;
275
+ }
276
+ }
277
+ }
278
+ }
279
+ async reconcile(options, call = () => ({
280
+ signal: options.signal,
281
+ deadline: Math.min(options.deadline ?? Infinity, this.#input.io.now() + 5_000),
282
+ })) {
283
+ const report = {
284
+ revoked: [],
285
+ pending: [],
286
+ unconfirmed: [],
287
+ };
288
+ const operations = await this.#input.transaction(options, async ({ state }) => [...state.keyOperations]);
289
+ for (const operation of operations) {
290
+ const effect = call();
291
+ if (effect === null ||
292
+ operation.phase !== 'creating' ||
293
+ !(await this.#dead(operation, options))) {
294
+ continue;
295
+ }
296
+ const claimed = await this.#input.transaction(options, async (transaction) => {
297
+ const current = transaction.state.keyOperations.find((item) => item.id === operation.id);
298
+ if (current === undefined ||
299
+ !(await this.#dead(current, options)) ||
300
+ binding(transaction.state, current.issuer) === null) {
301
+ return null;
302
+ }
303
+ current.owner = { ...this.#input.io.host.owner, nonce: this.#id() };
304
+ await transaction.save();
305
+ return current;
306
+ });
307
+ if (claimed === null) {
308
+ continue;
309
+ }
310
+ try {
311
+ const credential = await this.#input.readBackgroundToken(claimed.issuer.token, options, effect);
312
+ if (credential.kind === 'ready') {
313
+ await this.#execute(claimed, effect, true);
314
+ }
315
+ }
316
+ catch (error) {
317
+ checkEffect(this.#input.io, options);
318
+ if (!(error instanceof StateMachinesError ||
319
+ backgroundFailure(error) ||
320
+ (error instanceof CliError &&
321
+ error.document.error.code === 'cli.network'))) {
322
+ throw error;
323
+ }
324
+ }
325
+ finally {
326
+ await this.#release(claimed, options);
327
+ }
328
+ }
329
+ const jobs = await this.#input.transaction(options, async ({ state }) => [
330
+ ...state.keyRevocations,
331
+ ]);
332
+ for (const job of jobs) {
333
+ checkEffect(this.#input.io, options);
334
+ const read = call();
335
+ const credential = read === null
336
+ ? { kind: 'pending' }
337
+ : await this.#input.readBackgroundToken(job.token, options, read);
338
+ if (credential.kind === 'pending') {
339
+ report.pending.push(job.id);
340
+ continue;
341
+ }
342
+ let outcome = credential.kind === 'ready' ? 'revoked' : 'unconfirmed';
343
+ if (credential.kind === 'ready') {
344
+ const effect = call();
345
+ if (effect === null) {
346
+ report.pending.push(job.id);
347
+ continue;
348
+ }
349
+ try {
350
+ await this.#input.clients
351
+ .authenticated(job.origin, credential.token)
352
+ .revokeKey(job.keyId, effect);
353
+ }
354
+ catch (error) {
355
+ checkEffect(this.#input.io, options);
356
+ if (!(error instanceof StateMachinesError &&
357
+ (error.status === 404 ||
358
+ (error.status === 401 && error.code === 'unauthenticated')))) {
359
+ report.pending.push(job.id);
360
+ continue;
361
+ }
362
+ outcome = error.status === 404 ? 'revoked' : 'unconfirmed';
363
+ }
364
+ }
365
+ await this.#input.transaction(options, async (transaction) => {
366
+ const current = transaction.state.keyRevocations.find((item) => item.id === job.id && sameRef(item.token, job.token));
367
+ if (current === undefined) {
368
+ return;
369
+ }
370
+ transaction.state.keyRevocations =
371
+ transaction.state.keyRevocations.filter((item) => item !== current);
372
+ this.#queueIssuerCleanup(transaction.state, current.token);
373
+ if (current.secret !== null) {
374
+ this.#input.queueCleanup(transaction.state, current.secret);
375
+ }
376
+ await transaction.save();
377
+ await this.#input.cleanup(transaction);
378
+ });
379
+ report[outcome].push(outcome === 'revoked' ? job.id : job.tokenId);
380
+ }
381
+ report.pending.push(...(await this.#input.transaction(options, async ({ state }) => state.keyOperations.map((operation) => operation.id))));
382
+ return report;
383
+ }
384
+ async #claim(input, options) {
385
+ const until = Math.min(options.deadline ?? Infinity, this.#input.io.now() + 10_000);
386
+ while (true) {
387
+ checkEffect(this.#input.io, options);
388
+ const result = await this.#input.transaction(options, async (transaction) => {
389
+ const { state } = transaction;
390
+ const issuer = selected(state, input.account);
391
+ const session = binding(state, issuer);
392
+ if (session === null) {
393
+ throw new CliError('cli.profile_changed');
394
+ }
395
+ if (input.target.kind === 'run') {
396
+ const current = session.runKeys.find((key) => key.workspaceId === input.workspace);
397
+ if ((current?.id ?? null) !== (input.expected?.id ?? null)) {
398
+ return 'retry';
399
+ }
400
+ }
401
+ const target = input.target;
402
+ const conflict = state.keyOperations.find((operation) => target.kind === 'run'
403
+ ? operation.target.kind === 'run' &&
404
+ operation.issuer.origin === issuer.origin &&
405
+ operation.issuer.tokenId === issuer.tokenId &&
406
+ operation.workspaceId === input.workspace
407
+ : operation.target.kind === 'environment' &&
408
+ (operation.target.file.path === target.file.path ||
409
+ (operation.target.file.repositoryRoot ===
410
+ target.file.repositoryRoot &&
411
+ operation.issuer.origin === issuer.origin &&
412
+ operation.workspaceId === input.workspace)));
413
+ if (conflict !== undefined) {
414
+ if (!(await this.#dead(conflict, options))) {
415
+ return 'busy';
416
+ }
417
+ if (binding(state, conflict.issuer) === null ||
418
+ conflict.issuer.tokenId !== issuer.tokenId ||
419
+ (conflict.phase === 'staged' &&
420
+ Date.parse(conflict.expiresAt) <= this.#input.io.now()) ||
421
+ (target.kind === 'environment' &&
422
+ conflict.target.kind === 'environment' &&
423
+ conflict.target.file.path !== target.file.path)) {
424
+ if (conflict.phase === 'creating') {
425
+ return 'busy';
426
+ }
427
+ this.#retire(state, conflict);
428
+ }
429
+ else {
430
+ conflict.owner = {
431
+ ...this.#input.io.host.owner,
432
+ nonce: this.#id(),
433
+ };
434
+ if (target.kind === 'environment') {
435
+ conflict.target = target;
436
+ }
437
+ await transaction.save();
438
+ return conflict;
439
+ }
440
+ }
441
+ const id = this.#id();
442
+ const prefix = target.kind === 'run' ? 'CLI run on ' : 'env pull: ';
443
+ const suffix = target.kind === 'run'
444
+ ? this.#input.io.host.owner.hostname
445
+ : `${(this.#input.io.host.platform === 'win32' ? path.win32 : path.posix).basename(target.file.repositoryRoot)} on ${this.#input.io.host.owner.hostname}`;
446
+ const operation = {
447
+ phase: 'creating',
448
+ id,
449
+ owner: { ...this.#input.io.host.owner, nonce: this.#id() },
450
+ issuer,
451
+ workspaceId: input.workspace,
452
+ target,
453
+ idempotencyKey: this.#id(),
454
+ request: parseCreateKey({
455
+ workspaceId: input.workspace,
456
+ name: `${prefix}${suffix}`.slice(0, keyPolicy.maxNameLength ?? undefined),
457
+ permissions: [...keyPolicy.permissions],
458
+ kind: target.kind === 'run' ? 'cli_run' : 'env_pull',
459
+ }),
460
+ secret: this.#input.store.reference(issuer.token.store, issuer.origin, `key:${id}`),
461
+ };
462
+ state.keyOperations.push(operation);
463
+ await transaction.save();
464
+ return operation;
465
+ });
466
+ if (result !== 'busy') {
467
+ return result;
468
+ }
469
+ if (this.#input.io.now() >= until) {
470
+ throw new CliError('cli.config_locked');
471
+ }
472
+ await this.#input.io.sleep(Math.min(50, until - this.#input.io.now()), options);
473
+ }
474
+ }
475
+ async #execute(operation, options, recover = false) {
476
+ let candidate = operation;
477
+ if (candidate.phase === 'creating') {
478
+ const token = await this.#input.readToken(candidate.issuer.token, options);
479
+ if (token === null) {
480
+ throw new CliError('cli.credentials_invalid');
481
+ }
482
+ const result = await this.#input.clients
483
+ .authenticated(candidate.issuer.origin, token)
484
+ .createKey(candidate.request, candidate.idempotencyKey, options);
485
+ const expiresAt = result.apiKey.expiresAt;
486
+ if (result.apiKey.workspaceId !== candidate.workspaceId ||
487
+ result.apiKey.kind !== candidate.request.kind ||
488
+ expiresAt === null ||
489
+ !Number.isFinite(Date.parse(expiresAt)) ||
490
+ Date.parse(expiresAt) <= this.#input.io.now()) {
491
+ throw new CliError('cli.invalid_response');
492
+ }
493
+ candidate = {
494
+ ...candidate,
495
+ phase: 'staged',
496
+ keyId: result.apiKey.id,
497
+ expiresAt,
498
+ };
499
+ const staged = candidate;
500
+ const installed = await this.#input.transaction(options, async (transaction) => {
501
+ const current = transaction.state.keyOperations.find((item) => item.id === staged.id &&
502
+ item.owner?.nonce === staged.owner?.nonce);
503
+ if (current === undefined) {
504
+ throw new CliError('cli.profile_changed');
505
+ }
506
+ transaction.state.keyOperations = transaction.state.keyOperations.map((item) => (item === current ? staged : item));
507
+ await transaction.save();
508
+ if (binding(transaction.state, staged.issuer) === null ||
509
+ result.kind === 'replayed-without-secret') {
510
+ this.#retire(transaction.state, staged);
511
+ await transaction.save();
512
+ return false;
513
+ }
514
+ if (!usableSecret(result.secret)) {
515
+ this.#retire(transaction.state, staged);
516
+ await transaction.save();
517
+ throw new CliError('cli.invalid_response');
518
+ }
519
+ await this.#input.store.set(staged.secret, result.secret, options);
520
+ if ((await this.#secret(staged.secret, options)) !== result.secret) {
521
+ throw new CliError('cli.credentials_invalid');
522
+ }
523
+ return true;
524
+ });
525
+ if (!installed) {
526
+ return 'retry';
527
+ }
528
+ }
529
+ const staged = candidate;
530
+ const secret = await this.#secret(staged.secret, options);
531
+ if (secret === null) {
532
+ await this.#input.transaction(options, async (transaction) => {
533
+ const current = transaction.state.keyOperations.find((item) => item.id === staged.id && item.owner?.nonce === staged.owner?.nonce);
534
+ if (current !== undefined) {
535
+ this.#retire(transaction.state, current);
536
+ await transaction.save();
537
+ }
538
+ });
539
+ return 'retry';
540
+ }
541
+ const stored = secret;
542
+ await this.#input.transaction(options, async (transaction) => {
543
+ const current = transaction.state.keyOperations.find((item) => item.id === staged.id && item.owner?.nonce === staged.owner?.nonce);
544
+ if (current?.phase !== 'staged' ||
545
+ binding(transaction.state, current.issuer) === null) {
546
+ if (current !== undefined) {
547
+ this.#retire(transaction.state, current);
548
+ await transaction.save();
549
+ }
550
+ throw new CliError('cli.profile_changed');
551
+ }
552
+ if ((await this.#secret(current.secret, options)) !== stored) {
553
+ throw new CliError('cli.credentials_invalid');
554
+ }
555
+ try {
556
+ await this.#install(transaction, current, stored, recover);
557
+ }
558
+ catch (error) {
559
+ checkEffect(this.#input.io, options);
560
+ if (error instanceof CliError &&
561
+ [
562
+ 'cli.env_changed',
563
+ 'cli.env_path_refused',
564
+ 'cli.env_invalid',
565
+ ].includes(error.document.error.code)) {
566
+ this.#retire(transaction.state, current);
567
+ await transaction.save();
568
+ }
569
+ throw error;
570
+ }
571
+ await this.#input.cleanup(transaction);
572
+ });
573
+ return staged;
574
+ }
575
+ async withRunKey(input, options) {
576
+ await this.reconcile(options);
577
+ let replays = 0;
578
+ let installedId = null;
579
+ while (true) {
580
+ const saved = await this.#input.transaction(options, async ({ state }) => {
581
+ const issuer = selected(state, input.account);
582
+ const session = binding(state, issuer);
583
+ if (session === null) {
584
+ throw new CliError('cli.profile_changed');
585
+ }
586
+ return {
587
+ issuer,
588
+ tokenExpiresAt: Date.parse(session.expiresAt),
589
+ key: session.runKeys.find((key) => key.workspaceId === input.workspace) ?? null,
590
+ };
591
+ });
592
+ if (saved.key !== null) {
593
+ const secret = await this.#secret(saved.key.secret, options);
594
+ if (secret !== null) {
595
+ let reusable = false;
596
+ try {
597
+ const actual = await this.#input.clients.validateKey(saved.issuer.origin, secret, options);
598
+ if (actual.id !== saved.key.id ||
599
+ actual.workspaceId !== input.workspace ||
600
+ actual.kind !== 'cli_run' ||
601
+ actual.expiresAt === null ||
602
+ !Number.isFinite(Date.parse(actual.expiresAt))) {
603
+ throw new CliError('cli.invalid_response');
604
+ }
605
+ const expiresAt = Date.parse(actual.expiresAt);
606
+ const now = this.#input.io.now();
607
+ reusable =
608
+ expiresAt > now &&
609
+ (actual.id === installedId ||
610
+ expiresAt >=
611
+ Math.min(saved.tokenExpiresAt, now + RUN_KEY_ROTATION_MS));
612
+ }
613
+ catch (error) {
614
+ checkEffect(this.#input.io, options);
615
+ if (!(error instanceof StateMachinesError && error.status === 401) ||
616
+ saved.key.id === installedId) {
617
+ throw error;
618
+ }
619
+ }
620
+ if (reusable) {
621
+ const current = await this.#input.transaction(options, async ({ state }) => binding(state, saved.issuer)?.runKeys.some((key) => key.id === saved.key?.id &&
622
+ sameRef(key.secret, saved.key.secret)) ?? false);
623
+ if (current) {
624
+ checkEffect(this.#input.io, options);
625
+ return input.consume(secret);
626
+ }
627
+ continue;
628
+ }
629
+ }
630
+ }
631
+ const operation = await this.#claim({
632
+ account: input.account,
633
+ workspace: input.workspace,
634
+ target: { kind: 'run' },
635
+ expected: saved.key,
636
+ }, options);
637
+ if (operation === 'retry') {
638
+ continue;
639
+ }
640
+ try {
641
+ const result = await this.#execute(operation, options);
642
+ if (result === 'retry') {
643
+ replays += 1;
644
+ if (replays >= 2) {
645
+ throw new CliError('cli.invalid_response');
646
+ }
647
+ }
648
+ else {
649
+ installedId = result.keyId;
650
+ }
651
+ }
652
+ finally {
653
+ await this.#release(operation, options);
654
+ }
655
+ await this.reconcile(options);
656
+ }
657
+ }
658
+ async pullEnvironment(input, options) {
659
+ await this.reconcile(options);
660
+ for (let attempt = 0; attempt < 2; attempt += 1) {
661
+ const operation = await this.#claim({
662
+ account: input.account,
663
+ workspace: input.workspace,
664
+ target: { kind: 'environment', file: input.target },
665
+ expected: null,
666
+ }, options);
667
+ if (operation === 'retry') {
668
+ continue;
669
+ }
670
+ let result;
671
+ try {
672
+ result = await this.#execute(operation, options);
673
+ }
674
+ finally {
675
+ await this.#release(operation, options);
676
+ }
677
+ await this.reconcile(options);
678
+ if (result !== 'retry') {
679
+ const pendingRevocation = await this.#input.transaction(options, async ({ state }) => state.keyRevocations.length > 0);
680
+ return {
681
+ path: input.target.path,
682
+ workspaceId: input.workspace,
683
+ keyId: result.keyId,
684
+ expiresAt: result.expiresAt,
685
+ pendingRevocation,
686
+ };
687
+ }
688
+ }
689
+ throw new CliError('cli.invalid_response');
690
+ }
691
+ }