ardent-cli-beta 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,19 +10,8 @@ bound with PKCE. `ardent login --token <token>` validates and stores an existing
10
10
  API token for CI and automation; `ARDENT_TOKEN` supplies the same flag
11
11
  non-interactively. `ardent status` and `ardent logout` inspect and remove that
12
12
  local session. The CLI always talks to Ardent's production control plane. Oclif
13
- provides help, version, autocomplete, and native JSON command behavior.
14
- Connector credential delivery is the first resource command; general resource
15
- commands and resource context remain owed.
16
-
17
- `ardent connectors credentials set` delivers a source database password and a
18
- verified CA directly to the selected environment's authoritative AWS account and
19
- region. The caller must be logged in to Ardent and authenticated to that AWS account. Supply
20
- the password only as `{"password":"..."}` on standard input and the CA with
21
- `--ca-file`; `--request` is a caller-generated UUID that must be reused for a
22
- retry. The command checks connector ownership through the control-plane API,
23
- checks the local AWS account through STS, prints its target, then creates the CA
24
- and password secrets in that order. It never sends either value to the control
25
- plane or places one on the AWS command's argument vector.
13
+ provides help, version, autocomplete, and native JSON command behavior. Resource
14
+ commands and resource context are not implemented yet.
26
15
 
27
16
  Set `ARDENT_API_URL` to point the CLI at a development control plane. Without
28
17
  it, the CLI uses `https://production.tryardent.com`.
@@ -94,93 +94,7 @@
94
94
  "commands",
95
95
  "status.js"
96
96
  ]
97
- },
98
- "connectors:credentials:set": {
99
- "aliases": [],
100
- "args": {},
101
- "description": "Deliver a connector password and source CA directly to its environment AWS account",
102
- "flags": {
103
- "json": {
104
- "description": "Format output as json.",
105
- "helpGroup": "GLOBAL",
106
- "name": "json",
107
- "allowNo": false,
108
- "type": "boolean"
109
- },
110
- "organization": {
111
- "description": "Owning Ardent organization ID",
112
- "name": "organization",
113
- "required": true,
114
- "hasDynamicHelp": false,
115
- "multiple": false,
116
- "type": "option"
117
- },
118
- "project": {
119
- "description": "Owning Ardent project ID",
120
- "name": "project",
121
- "required": true,
122
- "hasDynamicHelp": false,
123
- "multiple": false,
124
- "type": "option"
125
- },
126
- "environment": {
127
- "description": "Target Ardent environment ID",
128
- "name": "environment",
129
- "required": true,
130
- "hasDynamicHelp": false,
131
- "multiple": false,
132
- "type": "option"
133
- },
134
- "connector": {
135
- "description": "Target Ardent connector ID",
136
- "name": "connector",
137
- "required": true,
138
- "hasDynamicHelp": false,
139
- "multiple": false,
140
- "type": "option"
141
- },
142
- "request": {
143
- "description": "Stable UUID for safe retries",
144
- "name": "request",
145
- "required": true,
146
- "hasDynamicHelp": false,
147
- "multiple": false,
148
- "type": "option"
149
- },
150
- "ca-file": {
151
- "description": "PEM source certificate-authority file",
152
- "name": "ca-file",
153
- "required": true,
154
- "hasDynamicHelp": false,
155
- "multiple": false,
156
- "type": "option"
157
- },
158
- "password-stdin": {
159
- "description": "Read {\"password\":\"…\"} from standard input",
160
- "name": "password-stdin",
161
- "required": true,
162
- "allowNo": false,
163
- "type": "boolean"
164
- }
165
- },
166
- "hasDynamicHelp": false,
167
- "hiddenAliases": [],
168
- "id": "connectors:credentials:set",
169
- "pluginAlias": "ardent-cli-beta",
170
- "pluginName": "ardent-cli-beta",
171
- "pluginType": "core",
172
- "strict": true,
173
- "enableJsonFlag": true,
174
- "helpGroup": "Connectors",
175
- "isESM": true,
176
- "relativePath": [
177
- "dist",
178
- "commands",
179
- "connectors",
180
- "credentials",
181
- "set.js"
182
- ]
183
97
  }
184
98
  },
185
- "version": "0.0.6"
99
+ "version": "0.0.7"
186
100
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ardent-cli-beta",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "The Ardent command-line interface",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,18 +0,0 @@
1
- export interface Target {
2
- account: string;
3
- region: string;
4
- environment: string;
5
- connector: string;
6
- request: string;
7
- }
8
- type Run = (arguments_: string[], input?: string) => Promise<string>;
9
- export declare function validateCA(pem: string, now?: Date): string;
10
- export declare function parsePasswordDocument(raw: string): string;
11
- export declare function deliver(target: Target, ca: string, password: string, run?: Run): Promise<void>;
12
- export declare class AWSFailure extends Error {
13
- readonly notFound: boolean;
14
- constructor(notFound?: boolean);
15
- }
16
- export declare function account(run?: Run): Promise<string>;
17
- export declare function runAWS(arguments_: string[], input?: string, executable?: string, timeoutMilliseconds?: number): Promise<string>;
18
- export {};
@@ -1,143 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { timingSafeEqual, X509Certificate } from 'node:crypto';
3
- const secretName = (target, suffix) => `ardent/${target.environment}/connectors/${target.connector}/${suffix}`;
4
- export function validateCA(pem, now = new Date()) {
5
- if (Buffer.byteLength(pem) > 64 * 1024 || !pem.endsWith('\n'))
6
- throw new Error('The source CA must be a newline-terminated PEM certificate under 64 KiB.');
7
- if ((pem.match(/-----BEGIN CERTIFICATE-----/g) ?? []).length !== 1 || /PRIVATE KEY/.test(pem))
8
- throw new Error('The source CA must contain exactly one certificate and no private key.');
9
- let certificate;
10
- try {
11
- certificate = new X509Certificate(pem);
12
- }
13
- catch {
14
- throw new Error('The source CA is not a valid PEM certificate.');
15
- }
16
- if (!certificate.ca || now < new Date(certificate.validFrom) || now >= new Date(certificate.validTo))
17
- throw new Error('The source CA must be a currently valid certificate authority.');
18
- return pem;
19
- }
20
- export function parsePasswordDocument(raw) {
21
- if (Buffer.byteLength(raw) > 64 * 1024)
22
- throw new Error('The source credential document is too large.');
23
- let parsed;
24
- try {
25
- parsed = JSON.parse(raw);
26
- }
27
- catch {
28
- throw new Error('Standard input must be one JSON object containing password.');
29
- }
30
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed) || Object.keys(parsed).length !== 1 || typeof parsed.password !== 'string' || parsed.password.length === 0 || parsed.password.includes('\0')) {
31
- throw new Error('Standard input must be one JSON object containing a non-empty password and no other fields.');
32
- }
33
- return parsed.password;
34
- }
35
- export async function deliver(target, ca, password, run = runAWS) {
36
- for (const [name, value] of [
37
- [secretName(target, 'source-database-ca'), ca],
38
- [secretName(target, 'source-database-password'), password],
39
- ]) {
40
- const description = await describe(run, target, name);
41
- if (description !== undefined) {
42
- const tags = new Map((description.Tags ?? []).map((tag) => [tag.Key, tag.Value]));
43
- if (tags.get('ardent:credential-request') !== target.request || tags.get('ardent:environment') !== target.environment || tags.get('ardent:connector') !== target.connector) {
44
- throw new Error(`The deterministic ${name.endsWith('-ca') ? 'CA' : 'password'} secret already belongs to another request.`);
45
- }
46
- const encodedExisting = await run(['secretsmanager', 'get-secret-value', '--region', target.region, '--secret-id', name, '--query', 'SecretString', '--output', 'json']);
47
- let existing;
48
- try {
49
- existing = JSON.parse(encodedExisting);
50
- }
51
- catch {
52
- throw new AWSFailure();
53
- }
54
- if (typeof existing !== 'string')
55
- throw new AWSFailure();
56
- const existingBytes = Buffer.from(existing);
57
- const requestedBytes = Buffer.from(value);
58
- if (existingBytes.length !== requestedBytes.length || !timingSafeEqual(existingBytes, requestedBytes)) {
59
- throw new Error(`The deterministic ${name.endsWith('-ca') ? 'CA' : 'password'} secret already has different contents for this request.`);
60
- }
61
- continue;
62
- }
63
- await run([
64
- 'secretsmanager', 'create-secret', '--region', target.region, '--name', name,
65
- '--secret-string', 'file:///dev/stdin', '--tags',
66
- `Key=ardent:credential-request,Value=${target.request}`,
67
- `Key=ardent:environment,Value=${target.environment}`,
68
- `Key=ardent:connector,Value=${target.connector}`,
69
- '--output', 'json',
70
- ], value);
71
- }
72
- }
73
- async function describe(run, target, name) {
74
- try {
75
- return JSON.parse(await run(['secretsmanager', 'describe-secret', '--region', target.region, '--secret-id', name, '--output', 'json']));
76
- }
77
- catch (error) {
78
- if (error instanceof AWSFailure && error.notFound)
79
- return undefined;
80
- throw error;
81
- }
82
- }
83
- export class AWSFailure extends Error {
84
- notFound;
85
- constructor(notFound = false) {
86
- super(notFound ? 'The AWS secret does not exist.' : 'The AWS credential operation failed.');
87
- this.notFound = notFound;
88
- }
89
- }
90
- export async function account(run = runAWS) {
91
- const value = (await run(['sts', 'get-caller-identity', '--query', 'Account', '--output', 'text'])).trim();
92
- if (!/^\d{12}$/.test(value))
93
- throw new Error('AWS returned an invalid account identity.');
94
- return value;
95
- }
96
- export function runAWS(arguments_, input, executable = 'aws', timeoutMilliseconds = 30_000) {
97
- return new Promise((resolve, reject) => {
98
- const child = spawn(executable, arguments_, { shell: false, stdio: ['pipe', 'pipe', 'pipe'] });
99
- let settled = false;
100
- let stdout = '';
101
- let stderr = '';
102
- let timeout;
103
- const finish = (error) => {
104
- if (settled)
105
- return;
106
- settled = true;
107
- if (timeout)
108
- clearTimeout(timeout);
109
- if (error)
110
- reject(error);
111
- else
112
- resolve(stdout);
113
- };
114
- timeout = setTimeout(() => {
115
- child.kill('SIGKILL');
116
- finish(new AWSFailure());
117
- }, timeoutMilliseconds);
118
- const failInput = () => {
119
- child.kill('SIGKILL');
120
- finish(new AWSFailure());
121
- };
122
- child.stdout.setEncoding('utf8').on('data', (chunk) => {
123
- if (stdout.length <= 128 * 1024)
124
- stdout += chunk;
125
- });
126
- child.stderr.setEncoding('utf8').on('data', (chunk) => {
127
- if (stderr.length <= 16 * 1024)
128
- stderr += chunk;
129
- });
130
- child.on('error', () => finish(new AWSFailure()));
131
- child.stdin.on('error', failInput);
132
- child.on('close', (status) => {
133
- if (status === 0 && Buffer.byteLength(stdout) <= 128 * 1024)
134
- finish();
135
- else
136
- finish(new AWSFailure(/ResourceNotFoundException/.test(stderr)));
137
- });
138
- child.stdin.end(input, (error) => {
139
- if (error)
140
- failInput();
141
- });
142
- });
143
- }
@@ -1,20 +0,0 @@
1
- import { ArdentCommand } from '../../../command.js';
2
- export default class SetSourceCredentials extends ArdentCommand {
3
- static description: string;
4
- static helpGroup: string;
5
- static flags: {
6
- organization: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
- project: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
8
- environment: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
9
- connector: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
- request: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
- 'ca-file': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
12
- 'password-stdin': import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
- };
14
- run(): Promise<{
15
- connector: string;
16
- environment: string;
17
- account: string;
18
- delivered: true;
19
- }>;
20
- }
@@ -1,59 +0,0 @@
1
- import { Flags } from '@oclif/core';
2
- import { readFile } from 'node:fs/promises';
3
- import { request } from '../../../api/client.js';
4
- import { loadSession } from '../../../auth/credentials.js';
5
- import { account, deliver, parsePasswordDocument, validateCA } from '../../../cloud/source-credentials.js';
6
- import { ArdentCommand } from '../../../command.js';
7
- export default class SetSourceCredentials extends ArdentCommand {
8
- static description = 'Deliver a connector password and source CA directly to its environment AWS account';
9
- static helpGroup = 'Connectors';
10
- static flags = {
11
- organization: Flags.string({ required: true, description: 'Owning Ardent organization ID' }),
12
- project: Flags.string({ required: true, description: 'Owning Ardent project ID' }),
13
- environment: Flags.string({ required: true, description: 'Target Ardent environment ID' }),
14
- connector: Flags.string({ required: true, description: 'Target Ardent connector ID' }),
15
- request: Flags.string({ required: true, description: 'Stable UUID for safe retries' }),
16
- 'ca-file': Flags.string({ required: true, description: 'PEM source certificate-authority file' }),
17
- 'password-stdin': Flags.boolean({ required: true, description: 'Read {"password":"…"} from standard input' }),
18
- };
19
- async run() {
20
- const { flags } = await this.parse(SetSourceCredentials);
21
- const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}';
22
- if (!new RegExp(`^environment_${uuid}$`).test(flags.environment) || !new RegExp(`^connector_${uuid}$`).test(flags.connector) || !new RegExp(`^${uuid}$`).test(flags.request))
23
- this.error('Environment, connector, or request identity is invalid.');
24
- const session = await loadSession(this.config.configDir);
25
- if (!session)
26
- this.error('Log in before delivering source credentials.');
27
- const connectors = await request(session.token, `/connectors?organization_id=${encodeURIComponent(flags.organization)}&project_id=${encodeURIComponent(flags.project)}`);
28
- if (!connectors.some((connector) => connector.id === flags.connector && connector.environment_id === flags.environment))
29
- this.error('The selected connector does not belong to the selected project and environment.');
30
- const environments = await request(session.token, `/connector-environments?organization_id=${encodeURIComponent(flags.organization)}&project_id=${encodeURIComponent(flags.project)}`);
31
- const environment = environments.find((candidate) => candidate.id === flags.environment);
32
- if (!environment || !/^\d{12}$/.test(environment.cloud_account_id) || !/^[a-z]{2}(?:-gov)?-[a-z]+-\d$/.test(environment.cloud_region))
33
- this.error('The selected environment has no authoritative AWS placement.');
34
- const actualAccount = await account();
35
- if (actualAccount !== environment.cloud_account_id)
36
- this.error(`AWS is authenticated to account ${actualAccount}, not the environment account ${environment.cloud_account_id}.`);
37
- const ca = validateCA(await readFile(flags['ca-file'], 'utf8'));
38
- const input = await readStdin();
39
- const password = parsePasswordDocument(input);
40
- if (!this.jsonEnabled())
41
- this.log(`Delivering source credentials for ${flags.connector} in ${flags.environment} to AWS account ${environment.cloud_account_id} (${environment.cloud_region})`);
42
- await deliver({ account: environment.cloud_account_id, region: environment.cloud_region, environment: flags.environment, connector: flags.connector, request: flags.request }, ca, password);
43
- if (!this.jsonEnabled())
44
- this.log('✓ Source password and CA delivered');
45
- return { connector: flags.connector, environment: flags.environment, account: environment.cloud_account_id, delivered: true };
46
- }
47
- }
48
- async function readStdin() {
49
- const chunks = [];
50
- let size = 0;
51
- for await (const chunk of process.stdin) {
52
- const value = Buffer.from(chunk);
53
- size += value.length;
54
- if (size > 64 * 1024)
55
- throw new Error('The source credential document is too large.');
56
- chunks.push(value);
57
- }
58
- return Buffer.concat(chunks).toString('utf8');
59
- }