@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
@@ -0,0 +1,290 @@
1
+ import { StateMachines, } from '@usestatemachines/sdk';
2
+ import { z } from 'zod';
3
+ import { createAnonymousApi, createAuthenticatedApi, createSdkFetch, retryBudgetMs, } from './api-internal.js';
4
+ import { createProjectRepository } from './config/project.js';
5
+ import { apiOriginFromEnvironment, productionOrigin, profileNameFromEnvironment, workspaceIdSchema, } from './context-types.js';
6
+ import { createEnvironmentFiles } from './env-file.js';
7
+ import { CliError } from './errors.js';
8
+ import { parseAgent } from './generated/internal.js';
9
+ import { retryPolicy } from './generated/retry.js';
10
+ import { createAccounts } from './login.js';
11
+ import { createPrograms } from './programs.js';
12
+ export async function allWorkspaces(sdk) {
13
+ const result = [];
14
+ const seen = new Set();
15
+ let cursor = null;
16
+ do {
17
+ const page = await sdk.workspaces.list(cursor === null ? {} : { cursor });
18
+ result.push(...page.data);
19
+ cursor = page.nextCursor;
20
+ if (cursor !== null) {
21
+ if (seen.has(cursor)) {
22
+ throw new CliError('cli.invalid_response');
23
+ }
24
+ seen.add(cursor);
25
+ }
26
+ } while (cursor !== null);
27
+ return result;
28
+ }
29
+ export async function createContextSession(input) {
30
+ const { io, globals, presentation, version } = input;
31
+ const defaults = {
32
+ signal: io.signal,
33
+ deadline: null,
34
+ };
35
+ const environment = io.host.environment;
36
+ const agentName = input.agent?.name;
37
+ const agent = parseAgent(agentName === 'claude'
38
+ ? 'claude_code'
39
+ : agentName === 'codex'
40
+ ? 'codex_cli'
41
+ : agentName === 'gemini'
42
+ ? 'gemini_cli'
43
+ : agentName === 'github-copilot-cli'
44
+ ? 'github-copilot'
45
+ : agentName);
46
+ const userAgent = `statemachines-cli/${version}${agent === null ? '' : ` agent/${agent}`}`;
47
+ let lease = null;
48
+ const fetcher = async (resource, init) => {
49
+ lease ??= io.http({
50
+ http: environment.HTTP_PROXY ?? environment.http_proxy ?? null,
51
+ https: environment.HTTPS_PROXY ?? environment.https_proxy ?? null,
52
+ noProxy: environment.NO_PROXY ?? environment.no_proxy ?? null,
53
+ }, defaults);
54
+ const acquisition = lease;
55
+ let transport;
56
+ try {
57
+ transport = await acquisition;
58
+ }
59
+ catch {
60
+ if (lease === acquisition) {
61
+ lease = null;
62
+ }
63
+ throw new CliError('cli.network');
64
+ }
65
+ const request = new Request(resource, init);
66
+ const headers = new Headers([...request.headers]);
67
+ headers.set('user-agent', userAgent);
68
+ return transport.fetch(new Request(request, { headers, redirect: 'manual' }));
69
+ };
70
+ function sdk(origin, token, options = defaults) {
71
+ if (!/^[\x21-\x7e]+$/.test(token)) {
72
+ throw new CliError('cli.credentials_invalid');
73
+ }
74
+ return new StateMachines({
75
+ apiKey: token,
76
+ baseUrl: origin,
77
+ attemptTimeoutMs: retryBudgetMs + retryPolicy.maxInterval,
78
+ fetch: createSdkFetch({ io, fetch: fetcher, origin, options }),
79
+ });
80
+ }
81
+ const clients = {
82
+ anonymous: (origin) => createAnonymousApi({ io, fetch: fetcher, origin }),
83
+ authenticated: (origin, token) => createAuthenticatedApi({ io, fetch: fetcher, origin }, token),
84
+ async validateKey(origin, key, options) {
85
+ const me = await sdk(origin, key, options).me();
86
+ if (me.credential.kind !== 'api_key' || me.workspaceId === null) {
87
+ throw new CliError('cli.invalid_response');
88
+ }
89
+ const kind = z
90
+ .enum(['dashboard', 'cli_run', 'env_pull'])
91
+ .safeParse(me.credential.apiKeyKind);
92
+ if (!kind.success) {
93
+ throw new CliError('cli.invalid_response');
94
+ }
95
+ return {
96
+ id: me.credential.id,
97
+ workspaceId: workspaceIdSchema.parse(me.workspaceId),
98
+ kind: kind.data,
99
+ expiresAt: me.credential.expiresAt?.toISOString() ?? null,
100
+ };
101
+ },
102
+ async defaultWorkspace(origin, token, options) {
103
+ const workspaces = await allWorkspaces(sdk(origin, token, options));
104
+ const first = workspaces[0];
105
+ return workspaces.length === 1 && first !== undefined
106
+ ? workspaceIdSchema.parse(first.id)
107
+ : null;
108
+ },
109
+ };
110
+ const environmentFiles = createEnvironmentFiles(io);
111
+ const { accounts, authentication, programKeys } = createAccounts({
112
+ io,
113
+ version,
114
+ clients,
115
+ environmentFiles,
116
+ options: defaults,
117
+ });
118
+ const projects = createProjectRepository(io, defaults);
119
+ let publicContext;
120
+ async function bind(account, token, defaultWorkspace, options) {
121
+ const client = sdk(account.origin, token, options);
122
+ async function findWorkspace(value, effect = options) {
123
+ const api = effect === options ? client : sdk(account.origin, token, effect);
124
+ const workspaces = await allWorkspaces(api);
125
+ const exact = workspaces.find((workspace) => workspace.id === value);
126
+ const matches = exact === undefined
127
+ ? workspaces.filter((workspace) => workspace.name === value)
128
+ : [exact];
129
+ const match = matches[0];
130
+ if (matches.length !== 1 || match === undefined) {
131
+ throw new CliError('cli.invalid_arguments', {
132
+ message: matches.length > 1
133
+ ? 'More than one workspace has that name. Use its ID.'
134
+ : 'No accessible workspace matches that ID or name.',
135
+ });
136
+ }
137
+ return { id: workspaceIdSchema.parse(match.id), name: match.name };
138
+ }
139
+ const context = {
140
+ ...publicContext,
141
+ account,
142
+ sdk: client,
143
+ internal: createAuthenticatedApi({ io, fetch: fetcher, origin: account.origin, options }, token),
144
+ findWorkspace,
145
+ forWorkspace(workspace) {
146
+ return {
147
+ ...context,
148
+ workspace,
149
+ programs: createPrograms({
150
+ io,
151
+ origin: account.origin,
152
+ workspace: workspace.id,
153
+ files: environmentFiles,
154
+ credential: account.kind === 'workspace-key'
155
+ ? { kind: 'workspace-key', key: token }
156
+ : { kind: 'cli-token', account, keys: programKeys },
157
+ }),
158
+ };
159
+ },
160
+ async resolveWorkspace(effect = options) {
161
+ const link = await createProjectRepository(io, effect).read();
162
+ if (link !== null &&
163
+ account.kind === 'cli-token' &&
164
+ link.organizationId !== account.organizationId) {
165
+ throw new CliError('cli.organization_mismatch');
166
+ }
167
+ const selection = globals.workspace !== null
168
+ ? { value: globals.workspace, source: 'flag' }
169
+ : environment.STATEMACHINES_WORKSPACE !== undefined
170
+ ? {
171
+ value: environment.STATEMACHINES_WORKSPACE,
172
+ source: 'environment',
173
+ }
174
+ : link !== null
175
+ ? { value: link.workspaceId, source: 'link' }
176
+ : defaultWorkspace !== null
177
+ ? { value: defaultWorkspace, source: 'default' }
178
+ : null;
179
+ let named;
180
+ if (account.kind === 'workspace-key') {
181
+ named = await findWorkspace(account.workspaceId, effect);
182
+ const requested = [
183
+ globals.workspace,
184
+ environment.STATEMACHINES_WORKSPACE,
185
+ link?.workspaceId,
186
+ ].filter((value) => value !== null && value !== undefined);
187
+ if (requested.some((value) => value !== named.id && value !== named.name)) {
188
+ throw new CliError('cli.workspace_mismatch');
189
+ }
190
+ }
191
+ else {
192
+ if (selection === null) {
193
+ return null;
194
+ }
195
+ try {
196
+ named = await findWorkspace(selection.value, effect);
197
+ }
198
+ catch (error) {
199
+ if ((selection.source === 'default' || selection.source === 'link') &&
200
+ error instanceof CliError &&
201
+ error.document.error.code === 'cli.invalid_arguments') {
202
+ throw new CliError('cli.not_linked', {
203
+ message: selection.source === 'default'
204
+ ? 'The saved default workspace is no longer accessible. Choose another workspace.'
205
+ : 'The linked workspace is no longer accessible. Link this repository to another workspace.',
206
+ next: [
207
+ {
208
+ command: selection.source === 'default'
209
+ ? 'statemachines workspaces use <id>'
210
+ : 'statemachines link --force --workspace <id>',
211
+ reason: 'Choose an accessible workspace.',
212
+ },
213
+ ],
214
+ });
215
+ }
216
+ throw error;
217
+ }
218
+ }
219
+ return {
220
+ ...named,
221
+ source: selection?.source ?? 'credential',
222
+ assertMember(actualWorkspaceId) {
223
+ if (actualWorkspaceId !== named.id) {
224
+ throw new CliError('cli.workspace_mismatch');
225
+ }
226
+ },
227
+ };
228
+ },
229
+ };
230
+ return context;
231
+ }
232
+ async function resolveAccount(options = defaults) {
233
+ const origin = apiOriginFromEnvironment(environment.STATEMACHINES_BASE_URL);
234
+ if (environment.STATEMACHINES_API_KEY !== undefined) {
235
+ const token = environment.STATEMACHINES_API_KEY;
236
+ if (!/^[\x21-\x7e]+$/.test(token)) {
237
+ throw new CliError('cli.invalid_environment_credential');
238
+ }
239
+ const selectedOrigin = origin ?? productionOrigin;
240
+ const me = await sdk(selectedOrigin, token, options).me();
241
+ if (me.credential.kind !== 'api_key' || me.workspaceId === null) {
242
+ throw new CliError('cli.invalid_environment_credential');
243
+ }
244
+ return bind({
245
+ kind: 'workspace-key',
246
+ origin: selectedOrigin,
247
+ workspaceId: workspaceIdSchema.parse(me.workspaceId),
248
+ source: 'environment',
249
+ }, token, null, options);
250
+ }
251
+ const profile = globals.profile ??
252
+ profileNameFromEnvironment(environment.STATEMACHINES_PROFILE);
253
+ return authentication.withCredential({ profile, expectedOrigin: origin }, ({ account, token, defaultWorkspace }) => bind(account, token, defaultWorkspace, options), options);
254
+ }
255
+ publicContext = {
256
+ io,
257
+ globals,
258
+ presentation,
259
+ accounts,
260
+ projects,
261
+ version,
262
+ anonymousApi: clients.anonymous,
263
+ resolveAccount,
264
+ };
265
+ return {
266
+ presentation,
267
+ contexts: {
268
+ public: async () => publicContext,
269
+ account: () => resolveAccount(),
270
+ workspace: async () => {
271
+ const account = await resolveAccount();
272
+ const workspace = await account.resolveWorkspace();
273
+ if (workspace === null) {
274
+ throw new CliError('cli.not_linked');
275
+ }
276
+ return account.forWorkspace(workspace);
277
+ },
278
+ },
279
+ async close() {
280
+ const closing = lease;
281
+ lease = null;
282
+ if (closing !== null) {
283
+ try {
284
+ await (await closing).close();
285
+ }
286
+ catch { }
287
+ }
288
+ },
289
+ };
290
+ }
@@ -0,0 +1,10 @@
1
+ import type { Io } from '../io-types.js';
2
+ export declare function createLoginKey(io: Pick<Io, 'randomBytes'>): {
3
+ privateKey: string;
4
+ publicKey: string;
5
+ };
6
+ export declare function unsealLoginToken(input: {
7
+ envelope: unknown;
8
+ privateKey: string;
9
+ loginId: string;
10
+ }): string;
@@ -0,0 +1,72 @@
1
+ import { createDecipheriv, createPrivateKey, createPublicKey, diffieHellman, hkdfSync, } from 'node:crypto';
2
+ import { z } from 'zod';
3
+ import { CliError } from '../errors.js';
4
+ function bytes(value, length) {
5
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) {
6
+ throw new CliError('cli.invalid_response');
7
+ }
8
+ const decoded = Buffer.from(value, 'base64url');
9
+ if (decoded.toString('base64url') !== value ||
10
+ (length !== null && decoded.length !== length)) {
11
+ throw new CliError('cli.invalid_response');
12
+ }
13
+ return decoded;
14
+ }
15
+ function privateKey(value) {
16
+ return createPrivateKey({
17
+ key: Buffer.concat([
18
+ Buffer.from('302e020100300506032b656e04220420', 'hex'),
19
+ bytes(value, 32),
20
+ ]),
21
+ format: 'der',
22
+ type: 'pkcs8',
23
+ });
24
+ }
25
+ export function createLoginKey(io) {
26
+ const secret = Buffer.from(io.randomBytes(32)).toString('base64url');
27
+ const exported = createPublicKey(privateKey(secret)).export({
28
+ format: 'jwk',
29
+ });
30
+ const publicKey = z.object({ x: z.string() }).parse(exported).x;
31
+ return { privateKey: secret, publicKey };
32
+ }
33
+ const envelopeSchema = z.strictObject({
34
+ v: z.literal(1),
35
+ serverPublicKey: z.string(),
36
+ nonce: z.string(),
37
+ ciphertext: z.string(),
38
+ tag: z.string(),
39
+ });
40
+ export function unsealLoginToken(input) {
41
+ try {
42
+ const envelope = envelopeSchema.parse(input.envelope);
43
+ bytes(envelope.serverPublicKey, 32);
44
+ const shared = diffieHellman({
45
+ privateKey: privateKey(input.privateKey),
46
+ publicKey: createPublicKey({
47
+ format: 'jwk',
48
+ key: { kty: 'OKP', crv: 'X25519', x: envelope.serverPublicKey },
49
+ }),
50
+ });
51
+ if (shared.every((byte) => byte === 0)) {
52
+ throw new CliError('cli.invalid_response');
53
+ }
54
+ const key = Buffer.from(hkdfSync('sha256', shared, input.loginId, 'statemachines-cli-login-v1', 32));
55
+ const decipher = createDecipheriv('aes-256-gcm', key, bytes(envelope.nonce, 12));
56
+ decipher.setAAD(Buffer.from(input.loginId));
57
+ decipher.setAuthTag(bytes(envelope.tag, 16));
58
+ const token = Buffer.concat([
59
+ decipher.update(bytes(envelope.ciphertext, null)),
60
+ decipher.final(),
61
+ ]);
62
+ const value = token.toString('utf8');
63
+ if (!/^smcli_[A-Za-z0-9_-]{43}$/.test(value) ||
64
+ !Buffer.from(value).equals(token)) {
65
+ throw new CliError('cli.invalid_response');
66
+ }
67
+ return value;
68
+ }
69
+ catch {
70
+ throw new CliError('cli.invalid_response');
71
+ }
72
+ }
@@ -0,0 +1,17 @@
1
+ import { type SecretRef } from '../config/schema.js';
2
+ import { type ApiOrigin } from '../context-types.js';
3
+ import type { EffectOptions, Io } from '../io-types.js';
4
+ export declare const credentialCopy: {
5
+ fallback: string;
6
+ windowsFallback: string;
7
+ };
8
+ export declare class CredentialStore {
9
+ #private;
10
+ constructor(io: Io);
11
+ select(): SecretRef['store'];
12
+ fallback(reference: SecretRef, error: unknown): Promise<SecretRef>;
13
+ reference(store: SecretRef['store'], origin: ApiOrigin, entry: string): SecretRef;
14
+ get(reference: SecretRef, options: EffectOptions): Promise<string | null>;
15
+ set(reference: SecretRef, value: string, options: EffectOptions): Promise<void>;
16
+ delete(reference: SecretRef, options: EffectOptions): Promise<void>;
17
+ }
@@ -0,0 +1,156 @@
1
+ import { join } from 'node:path';
2
+ import { z } from 'zod';
3
+ import { parseVersioned } from '../config/schema.js';
4
+ import { apiOriginSchema } from '../context-types.js';
5
+ import { CliError, failedWith, keychainFailures } from '../errors.js';
6
+ const credentialsSchema = z
7
+ .strictObject({
8
+ version: z.literal(1),
9
+ entries: z.array(z.strictObject({
10
+ origin: apiOriginSchema,
11
+ entry: z.string().min(1),
12
+ value: z.string(),
13
+ })),
14
+ })
15
+ .superRefine((value, context) => {
16
+ const keys = value.entries.map((entry) => JSON.stringify([entry.origin, entry.entry]));
17
+ if (new Set(keys).size !== keys.length) {
18
+ context.addIssue({ code: 'custom', message: 'Duplicate credential.' });
19
+ }
20
+ });
21
+ export const credentialCopy = {
22
+ fallback: 'The keychain is unavailable. Credentials are saved in a protected file.\n',
23
+ windowsFallback: 'The keychain is unavailable. Credentials are saved in your application data directory, where your user account and administrators have access.\n',
24
+ };
25
+ export class CredentialStore {
26
+ #io;
27
+ #path;
28
+ #forced;
29
+ #warned = false;
30
+ constructor(io) {
31
+ this.#io = io;
32
+ this.#path = join(io.host.configHome, 'credentials.json');
33
+ const selected = io.host.environment.STATEMACHINES_CREDENTIAL_STORE;
34
+ if (selected !== undefined &&
35
+ selected !== 'file' &&
36
+ selected !== 'keychain') {
37
+ throw new CliError('cli.invalid_arguments');
38
+ }
39
+ this.#forced = selected ?? null;
40
+ }
41
+ select() {
42
+ return this.#forced ?? 'keychain';
43
+ }
44
+ async fallback(reference, error) {
45
+ if (reference.store !== 'keychain' ||
46
+ this.#forced !== null ||
47
+ !failedWith(error, keychainFailures)) {
48
+ throw error;
49
+ }
50
+ if (!this.#warned) {
51
+ this.#warned = true;
52
+ await this.#io.stderr.write(this.#io.host.platform === 'win32'
53
+ ? credentialCopy.windowsFallback
54
+ : credentialCopy.fallback);
55
+ }
56
+ return { ...reference, store: 'file' };
57
+ }
58
+ reference(store, origin, entry) {
59
+ return { store, origin, entry };
60
+ }
61
+ #check(reference) {
62
+ if (this.#forced !== null && this.#forced !== reference.store) {
63
+ throw new CliError('cli.credential_store_mismatch', {
64
+ next: [
65
+ {
66
+ command: `STATEMACHINES_CREDENTIAL_STORE=${reference.store} statemachines doctor`,
67
+ reason: 'Use the store that holds this credential.',
68
+ },
69
+ ],
70
+ });
71
+ }
72
+ }
73
+ #failed(result) {
74
+ if (result.kind === 'timed-out') {
75
+ throw new CliError('cli.keychain_timeout');
76
+ }
77
+ if (result.kind === 'module-unavailable') {
78
+ throw new CliError('cli.keychain_unavailable');
79
+ }
80
+ throw new CliError('cli.keychain_denied');
81
+ }
82
+ async #readFile(options) {
83
+ if (this.#io.host.platform !== 'win32') {
84
+ for (const path of [this.#io.host.configHome, this.#path]) {
85
+ const entry = await this.#io.files.inspect(path, options);
86
+ if (entry.kind !== 'missing' && (entry.mode & 0o077) !== 0) {
87
+ throw new CliError('cli.config_io');
88
+ }
89
+ }
90
+ }
91
+ const text = await this.#io.files.read(this.#path, options);
92
+ return text === null
93
+ ? { version: 1, entries: [] }
94
+ : parseVersioned(text, credentialsSchema);
95
+ }
96
+ async get(reference, options) {
97
+ this.#check(reference);
98
+ if (reference.store === 'file') {
99
+ const document = await this.#readFile(options);
100
+ return (document.entries.find((entry) => entry.origin === reference.origin &&
101
+ entry.entry === reference.entry)?.value ?? null);
102
+ }
103
+ const result = await this.#io.nativeKeychain.executeInChild({
104
+ kind: 'get',
105
+ service: `statemachines:${reference.origin}`,
106
+ account: reference.entry,
107
+ }, options);
108
+ if (result.kind === 'value') {
109
+ return result.value;
110
+ }
111
+ if (result.kind === 'missing') {
112
+ return null;
113
+ }
114
+ return this.#failed(result);
115
+ }
116
+ async set(reference, value, options) {
117
+ this.#check(reference);
118
+ if (reference.store === 'file') {
119
+ const document = await this.#readFile(options);
120
+ document.entries = document.entries.filter((entry) => entry.origin !== reference.origin || entry.entry !== reference.entry);
121
+ document.entries.push({
122
+ origin: reference.origin,
123
+ entry: reference.entry,
124
+ value,
125
+ });
126
+ await this.#io.files.atomicWrite(this.#path, JSON.stringify(document), 'private', options);
127
+ return;
128
+ }
129
+ const result = await this.#io.nativeKeychain.executeInChild({
130
+ kind: 'set',
131
+ service: `statemachines:${reference.origin}`,
132
+ account: reference.entry,
133
+ value,
134
+ }, options);
135
+ if (result.kind !== 'written') {
136
+ this.#failed(result);
137
+ }
138
+ }
139
+ async delete(reference, options) {
140
+ this.#check(reference);
141
+ if (reference.store === 'file') {
142
+ const document = await this.#readFile(options);
143
+ document.entries = document.entries.filter((entry) => entry.origin !== reference.origin || entry.entry !== reference.entry);
144
+ await this.#io.files.atomicWrite(this.#path, JSON.stringify(document), 'private', options);
145
+ return;
146
+ }
147
+ const result = await this.#io.nativeKeychain.executeInChild({
148
+ kind: 'delete',
149
+ service: `statemachines:${reference.origin}`,
150
+ account: reference.entry,
151
+ }, options);
152
+ if (result.kind !== 'deleted' && result.kind !== 'missing') {
153
+ this.#failed(result);
154
+ }
155
+ }
156
+ }
@@ -0,0 +1,4 @@
1
+ import type { z } from 'zod';
2
+ import type { doctorInputSchema, doctorResultSchema } from './commands/doctor/command.js';
3
+ import { type PublicContext } from './context-types.js';
4
+ export declare function runDoctor(context: PublicContext, input: z.infer<typeof doctorInputSchema>): Promise<z.infer<typeof doctorResultSchema>>;