ardent-cli-beta 0.0.6 → 0.0.8
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 +6 -13
- package/dist/auth/credentials.d.ts +10 -0
- package/dist/auth/credentials.js +43 -1
- package/dist/auth/token.js +6 -2
- package/dist/commands/project/create.d.ts +10 -0
- package/dist/commands/project/create.js +29 -0
- package/dist/commands/project/list.d.ts +9 -0
- package/dist/commands/project/list.js +29 -0
- package/dist/commands/project/switch.d.ts +12 -0
- package/dist/commands/project/switch.js +28 -0
- package/dist/projects/client.d.ts +9 -0
- package/dist/projects/client.js +30 -0
- package/dist/projects/context.d.ts +5 -0
- package/dist/projects/context.js +29 -0
- package/dist/terminal.d.ts +1 -0
- package/dist/terminal.js +9 -0
- package/oclif.manifest.json +77 -61
- package/package.json +1 -1
- package/dist/cloud/source-credentials.d.ts +0 -18
- package/dist/cloud/source-credentials.js +0 -143
- package/dist/commands/connectors/credentials/set.d.ts +0 -20
- package/dist/commands/connectors/credentials/set.js +0 -59
package/README.md
CHANGED
|
@@ -10,19 +10,9 @@ 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
|
-
|
|
15
|
-
|
|
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
|
+
context is retained from login. Project listing, creation, and local context
|
|
15
|
+
switching are implemented; other resource commands are not implemented yet.
|
|
26
16
|
|
|
27
17
|
Set `ARDENT_API_URL` to point the CLI at a development control plane. Without
|
|
28
18
|
it, the CLI uses `https://production.tryardent.com`.
|
|
@@ -33,6 +23,9 @@ npm run build
|
|
|
33
23
|
./bin/run.js --help
|
|
34
24
|
./bin/run.js login
|
|
35
25
|
./bin/run.js login --token <token>
|
|
26
|
+
./bin/run.js project list
|
|
27
|
+
./bin/run.js project create "My project"
|
|
28
|
+
./bin/run.js project switch "My project"
|
|
36
29
|
ARDENT_API_URL=http://localhost:8080 ./bin/run.js login --token <token>
|
|
37
30
|
```
|
|
38
31
|
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
export interface Session {
|
|
2
|
+
organization?: {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
};
|
|
6
|
+
projects?: ProjectIdentity[];
|
|
7
|
+
selectedProject?: ProjectIdentity;
|
|
2
8
|
token: string;
|
|
3
9
|
}
|
|
10
|
+
export interface ProjectIdentity {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
}
|
|
4
14
|
export declare function loadSession(directory: string): Promise<Session | undefined>;
|
|
5
15
|
export declare function saveSession(directory: string, session: Session): Promise<void>;
|
|
6
16
|
export declare function clearSession(directory: string): Promise<void>;
|
package/dist/auth/credentials.js
CHANGED
|
@@ -16,12 +16,54 @@ export async function loadSession(directory) {
|
|
|
16
16
|
const session = JSON.parse(contents);
|
|
17
17
|
if (typeof session.token !== 'string' || session.token.length === 0)
|
|
18
18
|
throw new Error();
|
|
19
|
-
|
|
19
|
+
if (session.organization === undefined) {
|
|
20
|
+
if (session.projects !== undefined || session.selectedProject !== undefined)
|
|
21
|
+
throw new Error();
|
|
22
|
+
return { token: session.token };
|
|
23
|
+
}
|
|
24
|
+
if (typeof session.organization !== 'object' ||
|
|
25
|
+
session.organization === null ||
|
|
26
|
+
typeof session.organization.id !== 'string' ||
|
|
27
|
+
session.organization.id.length === 0 ||
|
|
28
|
+
typeof session.organization.name !== 'string' ||
|
|
29
|
+
session.organization.name.length === 0) {
|
|
30
|
+
throw new Error();
|
|
31
|
+
}
|
|
32
|
+
let projects;
|
|
33
|
+
if (session.projects !== undefined) {
|
|
34
|
+
if (!Array.isArray(session.projects) || session.projects.some((project) => !isProjectIdentity(project)))
|
|
35
|
+
throw new Error();
|
|
36
|
+
projects = session.projects.map(({ id, name }) => ({ id, name }));
|
|
37
|
+
if (new Set(projects.map(({ id }) => id)).size !== projects.length)
|
|
38
|
+
throw new Error();
|
|
39
|
+
if (new Set(projects.map(({ name }) => name)).size !== projects.length)
|
|
40
|
+
throw new Error();
|
|
41
|
+
}
|
|
42
|
+
let selectedProject;
|
|
43
|
+
if (session.selectedProject !== undefined) {
|
|
44
|
+
if (!isProjectIdentity(session.selectedProject) || projects === undefined)
|
|
45
|
+
throw new Error();
|
|
46
|
+
selectedProject = projects.find(({ id, name }) => id === session.selectedProject?.id && name === session.selectedProject.name);
|
|
47
|
+
if (!selectedProject)
|
|
48
|
+
throw new Error();
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
organization: session.organization,
|
|
52
|
+
...(projects === undefined ? {} : { projects }),
|
|
53
|
+
...(selectedProject === undefined ? {} : { selectedProject }),
|
|
54
|
+
token: session.token,
|
|
55
|
+
};
|
|
20
56
|
}
|
|
21
57
|
catch {
|
|
22
58
|
throw new Error(`Ardent configuration at ${configPath(directory)} is invalid. Run ardent logout, then log in again.`);
|
|
23
59
|
}
|
|
24
60
|
}
|
|
61
|
+
function isProjectIdentity(value) {
|
|
62
|
+
if (typeof value !== 'object' || value === null)
|
|
63
|
+
return false;
|
|
64
|
+
const project = value;
|
|
65
|
+
return typeof project.id === 'string' && project.id.length > 0 && typeof project.name === 'string' && project.name.length > 0;
|
|
66
|
+
}
|
|
25
67
|
export async function saveSession(directory, session) {
|
|
26
68
|
await mkdir(directory, { mode: 0o700, recursive: true });
|
|
27
69
|
await chmod(directory, 0o700);
|
package/dist/auth/token.js
CHANGED
|
@@ -9,10 +9,14 @@ export async function loginWithToken(directory, value, signal) {
|
|
|
9
9
|
organizations.some((organization) => typeof organization !== 'object' ||
|
|
10
10
|
organization === null ||
|
|
11
11
|
typeof organization.id !== 'string' ||
|
|
12
|
-
|
|
12
|
+
organization.id.length === 0 ||
|
|
13
|
+
typeof organization.name !== 'string' ||
|
|
14
|
+
organization.name.length === 0)) {
|
|
13
15
|
throw new Error('Ardent API returned an invalid organization response.');
|
|
14
16
|
}
|
|
17
|
+
if (organizations.length !== 1)
|
|
18
|
+
throw new Error('Ardent API token must belong to exactly one organization.');
|
|
15
19
|
signal?.throwIfAborted();
|
|
16
|
-
await saveSession(directory, { token });
|
|
20
|
+
await saveSession(directory, { organization: organizations[0], token });
|
|
17
21
|
return organizations;
|
|
18
22
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ArdentCommand } from '../../command.js';
|
|
2
|
+
import { type Project } from '../../projects/client.js';
|
|
3
|
+
export default class ProjectCreate extends ArdentCommand {
|
|
4
|
+
static args: {
|
|
5
|
+
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static description: string;
|
|
8
|
+
static helpGroup: string;
|
|
9
|
+
run(): Promise<Project>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { loadSession } from '../../auth/credentials.js';
|
|
3
|
+
import { ArdentCommand } from '../../command.js';
|
|
4
|
+
import { createProject } from '../../projects/client.js';
|
|
5
|
+
import { selectProject } from '../../projects/context.js';
|
|
6
|
+
import { terminalText } from '../../terminal.js';
|
|
7
|
+
export default class ProjectCreate extends ArdentCommand {
|
|
8
|
+
static args = {
|
|
9
|
+
name: Args.string({ description: 'Project name', required: true }),
|
|
10
|
+
};
|
|
11
|
+
static description = 'Create a project';
|
|
12
|
+
static helpGroup = 'Projects';
|
|
13
|
+
async run() {
|
|
14
|
+
const { args } = await this.parse(ProjectCreate);
|
|
15
|
+
const name = args.name.trim();
|
|
16
|
+
if (!name)
|
|
17
|
+
this.error('Project name cannot be empty.');
|
|
18
|
+
const session = await loadSession(this.config.configDir);
|
|
19
|
+
if (!session)
|
|
20
|
+
this.error('Not logged in to Ardent. Run ardent login.');
|
|
21
|
+
if (!session.organization)
|
|
22
|
+
this.error('You must be logged in to run this command. Run ardent login.');
|
|
23
|
+
const project = await createProject(session.token, session.organization.id, name);
|
|
24
|
+
await selectProject(this.config.configDir, session, project);
|
|
25
|
+
if (!this.jsonEnabled())
|
|
26
|
+
this.log(`✓ Created project ${terminalText(project.name)}`);
|
|
27
|
+
return project;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ArdentCommand } from '../../command.js';
|
|
2
|
+
import { type Project } from '../../projects/client.js';
|
|
3
|
+
export default class ProjectList extends ArdentCommand {
|
|
4
|
+
static description: string;
|
|
5
|
+
static helpGroup: string;
|
|
6
|
+
run(): Promise<{
|
|
7
|
+
projects: Project[];
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ux } from '@oclif/core';
|
|
2
|
+
import { loadSession } from '../../auth/credentials.js';
|
|
3
|
+
import { ArdentCommand } from '../../command.js';
|
|
4
|
+
import { listProjects } from '../../projects/client.js';
|
|
5
|
+
import { replaceProjectCache } from '../../projects/context.js';
|
|
6
|
+
import { terminalText } from '../../terminal.js';
|
|
7
|
+
export default class ProjectList extends ArdentCommand {
|
|
8
|
+
static description = 'List projects';
|
|
9
|
+
static helpGroup = 'Projects';
|
|
10
|
+
async run() {
|
|
11
|
+
const session = await loadSession(this.config.configDir);
|
|
12
|
+
if (!session)
|
|
13
|
+
this.error('Not logged in to Ardent. Run ardent login.');
|
|
14
|
+
if (!session.organization)
|
|
15
|
+
this.error('You must be logged in to run this command. Run ardent login.');
|
|
16
|
+
const projects = await listProjects(session.token, session.organization.id);
|
|
17
|
+
const refreshedSession = await replaceProjectCache(this.config.configDir, session, projects);
|
|
18
|
+
if (!this.jsonEnabled()) {
|
|
19
|
+
if (projects.length === 0)
|
|
20
|
+
this.log('No projects found.');
|
|
21
|
+
for (const project of projects) {
|
|
22
|
+
const name = terminalText(project.name);
|
|
23
|
+
const line = project.id === refreshedSession.selectedProject?.id ? `● ${name}` : ` ${name}`;
|
|
24
|
+
this.log(project.id === refreshedSession.selectedProject?.id ? ux.colorize('green', line) : line);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { projects };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ArdentCommand } from '../../command.js';
|
|
2
|
+
export default class ProjectSwitch extends ArdentCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static helpGroup: string;
|
|
8
|
+
run(): Promise<{
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { loadSession } from '../../auth/credentials.js';
|
|
3
|
+
import { ArdentCommand } from '../../command.js';
|
|
4
|
+
import { cachedProjectNamed, selectProject } from '../../projects/context.js';
|
|
5
|
+
import { terminalText } from '../../terminal.js';
|
|
6
|
+
export default class ProjectSwitch extends ArdentCommand {
|
|
7
|
+
static args = {
|
|
8
|
+
name: Args.string({ description: 'Project name', required: true }),
|
|
9
|
+
};
|
|
10
|
+
static description = 'Switch to a project from the local project list';
|
|
11
|
+
static helpGroup = 'Projects';
|
|
12
|
+
async run() {
|
|
13
|
+
const { args } = await this.parse(ProjectSwitch);
|
|
14
|
+
const name = args.name.trim();
|
|
15
|
+
if (!name)
|
|
16
|
+
this.error('Project name cannot be empty.');
|
|
17
|
+
const session = await loadSession(this.config.configDir);
|
|
18
|
+
if (!session?.organization)
|
|
19
|
+
this.error('You must be logged in to run this command. Run ardent login.');
|
|
20
|
+
const project = cachedProjectNamed(session, name);
|
|
21
|
+
if (!project)
|
|
22
|
+
this.error(`Project ${JSON.stringify(name)} not found. Run ardent project list.`);
|
|
23
|
+
await selectProject(this.config.configDir, session, project);
|
|
24
|
+
if (!this.jsonEnabled())
|
|
25
|
+
this.log(`✓ Switched to project ${terminalText(project.name)}`);
|
|
26
|
+
return project;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface Project {
|
|
2
|
+
created_at: string;
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
organization_id: string;
|
|
6
|
+
updated_at: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function listProjects(token: string, organizationID: string): Promise<Project[]>;
|
|
9
|
+
export declare function createProject(token: string, organizationID: string, name: string): Promise<Project>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { request } from '../api/client.js';
|
|
3
|
+
export async function listProjects(token, organizationID) {
|
|
4
|
+
const projects = await request(token, `/projects?organization_id=${encodeURIComponent(organizationID)}`);
|
|
5
|
+
if (!Array.isArray(projects) || projects.some((project) => !isProject(project, organizationID))) {
|
|
6
|
+
throw new Error('Ardent API returned an invalid projects response.');
|
|
7
|
+
}
|
|
8
|
+
return projects;
|
|
9
|
+
}
|
|
10
|
+
export async function createProject(token, organizationID, name) {
|
|
11
|
+
const project = await request(token, '/projects', {
|
|
12
|
+
body: JSON.stringify({ name, organization_id: organizationID }),
|
|
13
|
+
headers: { 'Idempotency-Key': randomUUID() },
|
|
14
|
+
method: 'POST',
|
|
15
|
+
});
|
|
16
|
+
if (!isProject(project, organizationID))
|
|
17
|
+
throw new Error('Ardent API returned an invalid project response.');
|
|
18
|
+
return project;
|
|
19
|
+
}
|
|
20
|
+
function isProject(value, organizationID) {
|
|
21
|
+
if (typeof value !== 'object' || value === null)
|
|
22
|
+
return false;
|
|
23
|
+
const project = value;
|
|
24
|
+
return (typeof project.id === 'string' &&
|
|
25
|
+
project.id.length > 0 &&
|
|
26
|
+
project.organization_id === organizationID &&
|
|
27
|
+
typeof project.name === 'string' &&
|
|
28
|
+
typeof project.created_at === 'string' &&
|
|
29
|
+
typeof project.updated_at === 'string');
|
|
30
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type ProjectIdentity, type Session } from '../auth/credentials.js';
|
|
2
|
+
import type { Project } from './client.js';
|
|
3
|
+
export declare function replaceProjectCache(directory: string, session: Session, projects: Project[]): Promise<Session>;
|
|
4
|
+
export declare function selectProject(directory: string, session: Session, project: ProjectIdentity): Promise<Session>;
|
|
5
|
+
export declare function cachedProjectNamed(session: Session, name: string): ProjectIdentity | undefined;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { saveSession } from '../auth/credentials.js';
|
|
2
|
+
export async function replaceProjectCache(directory, session, projects) {
|
|
3
|
+
const identities = projects.map(projectIdentity);
|
|
4
|
+
const selectedProject = identities.find(({ id }) => id === session.selectedProject?.id) ??
|
|
5
|
+
(identities.length === 1 ? identities[0] : undefined);
|
|
6
|
+
const updated = { ...session, projects: identities, selectedProject };
|
|
7
|
+
if (selectedProject === undefined)
|
|
8
|
+
delete updated.selectedProject;
|
|
9
|
+
await saveSession(directory, updated);
|
|
10
|
+
return updated;
|
|
11
|
+
}
|
|
12
|
+
export async function selectProject(directory, session, project) {
|
|
13
|
+
const selectedProject = projectIdentity(project);
|
|
14
|
+
const projects = [...(session.projects ?? [])];
|
|
15
|
+
const existingIndex = projects.findIndex(({ id }) => id === selectedProject.id);
|
|
16
|
+
if (existingIndex === -1)
|
|
17
|
+
projects.push(selectedProject);
|
|
18
|
+
else
|
|
19
|
+
projects[existingIndex] = selectedProject;
|
|
20
|
+
const updated = { ...session, projects, selectedProject };
|
|
21
|
+
await saveSession(directory, updated);
|
|
22
|
+
return updated;
|
|
23
|
+
}
|
|
24
|
+
export function cachedProjectNamed(session, name) {
|
|
25
|
+
return session.projects?.find((project) => project.name === name);
|
|
26
|
+
}
|
|
27
|
+
function projectIdentity(project) {
|
|
28
|
+
return { id: project.id, name: project.name };
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function terminalText(value: string): string;
|
package/dist/terminal.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const terminalControlCharacter = /[\u0000-\u001F\u007F-\u009F\u202A-\u202E\u2066-\u2069]/g;
|
|
2
|
+
export function terminalText(value) {
|
|
3
|
+
return value.replace(terminalControlCharacter, (character) => {
|
|
4
|
+
const codePoint = character.codePointAt(0);
|
|
5
|
+
if (codePoint === undefined)
|
|
6
|
+
return '';
|
|
7
|
+
return `\\u${codePoint.toString(16).padStart(4, '0')}`;
|
|
8
|
+
});
|
|
9
|
+
}
|
package/oclif.manifest.json
CHANGED
|
@@ -95,10 +95,46 @@
|
|
|
95
95
|
"status.js"
|
|
96
96
|
]
|
|
97
97
|
},
|
|
98
|
-
"
|
|
98
|
+
"project:create": {
|
|
99
|
+
"aliases": [],
|
|
100
|
+
"args": {
|
|
101
|
+
"name": {
|
|
102
|
+
"description": "Project name",
|
|
103
|
+
"name": "name",
|
|
104
|
+
"required": true
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"description": "Create a project",
|
|
108
|
+
"flags": {
|
|
109
|
+
"json": {
|
|
110
|
+
"description": "Format output as json.",
|
|
111
|
+
"helpGroup": "GLOBAL",
|
|
112
|
+
"name": "json",
|
|
113
|
+
"allowNo": false,
|
|
114
|
+
"type": "boolean"
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"hasDynamicHelp": false,
|
|
118
|
+
"hiddenAliases": [],
|
|
119
|
+
"id": "project:create",
|
|
120
|
+
"pluginAlias": "ardent-cli-beta",
|
|
121
|
+
"pluginName": "ardent-cli-beta",
|
|
122
|
+
"pluginType": "core",
|
|
123
|
+
"strict": true,
|
|
124
|
+
"enableJsonFlag": true,
|
|
125
|
+
"helpGroup": "Projects",
|
|
126
|
+
"isESM": true,
|
|
127
|
+
"relativePath": [
|
|
128
|
+
"dist",
|
|
129
|
+
"commands",
|
|
130
|
+
"project",
|
|
131
|
+
"create.js"
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
"project:list": {
|
|
99
135
|
"aliases": [],
|
|
100
136
|
"args": {},
|
|
101
|
-
"description": "
|
|
137
|
+
"description": "List projects",
|
|
102
138
|
"flags": {
|
|
103
139
|
"json": {
|
|
104
140
|
"description": "Format output as json.",
|
|
@@ -106,81 +142,61 @@
|
|
|
106
142
|
"name": "json",
|
|
107
143
|
"allowNo": false,
|
|
108
144
|
"type": "boolean"
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"
|
|
141
|
-
|
|
142
|
-
|
|
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,
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
"hasDynamicHelp": false,
|
|
148
|
+
"hiddenAliases": [],
|
|
149
|
+
"id": "project:list",
|
|
150
|
+
"pluginAlias": "ardent-cli-beta",
|
|
151
|
+
"pluginName": "ardent-cli-beta",
|
|
152
|
+
"pluginType": "core",
|
|
153
|
+
"strict": true,
|
|
154
|
+
"enableJsonFlag": true,
|
|
155
|
+
"helpGroup": "Projects",
|
|
156
|
+
"isESM": true,
|
|
157
|
+
"relativePath": [
|
|
158
|
+
"dist",
|
|
159
|
+
"commands",
|
|
160
|
+
"project",
|
|
161
|
+
"list.js"
|
|
162
|
+
]
|
|
163
|
+
},
|
|
164
|
+
"project:switch": {
|
|
165
|
+
"aliases": [],
|
|
166
|
+
"args": {
|
|
167
|
+
"name": {
|
|
168
|
+
"description": "Project name",
|
|
169
|
+
"name": "name",
|
|
170
|
+
"required": true
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
"description": "Switch to a project from the local project list",
|
|
174
|
+
"flags": {
|
|
175
|
+
"json": {
|
|
176
|
+
"description": "Format output as json.",
|
|
177
|
+
"helpGroup": "GLOBAL",
|
|
178
|
+
"name": "json",
|
|
162
179
|
"allowNo": false,
|
|
163
180
|
"type": "boolean"
|
|
164
181
|
}
|
|
165
182
|
},
|
|
166
183
|
"hasDynamicHelp": false,
|
|
167
184
|
"hiddenAliases": [],
|
|
168
|
-
"id": "
|
|
185
|
+
"id": "project:switch",
|
|
169
186
|
"pluginAlias": "ardent-cli-beta",
|
|
170
187
|
"pluginName": "ardent-cli-beta",
|
|
171
188
|
"pluginType": "core",
|
|
172
189
|
"strict": true,
|
|
173
190
|
"enableJsonFlag": true,
|
|
174
|
-
"helpGroup": "
|
|
191
|
+
"helpGroup": "Projects",
|
|
175
192
|
"isESM": true,
|
|
176
193
|
"relativePath": [
|
|
177
194
|
"dist",
|
|
178
195
|
"commands",
|
|
179
|
-
"
|
|
180
|
-
"
|
|
181
|
-
"set.js"
|
|
196
|
+
"project",
|
|
197
|
+
"switch.js"
|
|
182
198
|
]
|
|
183
199
|
}
|
|
184
200
|
},
|
|
185
|
-
"version": "0.0.
|
|
201
|
+
"version": "0.0.8"
|
|
186
202
|
}
|
package/package.json
CHANGED
|
@@ -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
|
-
}
|