@j-256/ccam 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.
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/dist/auth/browser-login.d.ts +14 -0
- package/dist/auth/browser-login.js +72 -0
- package/dist/auth/manual-login.d.ts +10 -0
- package/dist/auth/manual-login.js +33 -0
- package/dist/auth/paths.d.ts +4 -0
- package/dist/auth/paths.js +15 -0
- package/dist/auth/profile-resolver.d.ts +26 -0
- package/dist/auth/profile-resolver.js +42 -0
- package/dist/auth/profile-store.d.ts +38 -0
- package/dist/auth/profile-store.js +125 -0
- package/dist/auth/prompt.d.ts +9 -0
- package/dist/auth/prompt.js +70 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.js +4 -0
- package/dist/client-factory.d.ts +6 -0
- package/dist/client-factory.js +40 -0
- package/dist/commands/auth.d.ts +77 -0
- package/dist/commands/auth.js +387 -0
- package/dist/commands/client.d.ts +3 -0
- package/dist/commands/client.js +365 -0
- package/dist/commands/instance.d.ts +11 -0
- package/dist/commands/instance.js +128 -0
- package/dist/commands/org-config.d.ts +3 -0
- package/dist/commands/org-config.js +31 -0
- package/dist/commands/org.d.ts +11 -0
- package/dist/commands/org.js +234 -0
- package/dist/commands/permission.d.ts +3 -0
- package/dist/commands/permission.js +60 -0
- package/dist/commands/realm.d.ts +3 -0
- package/dist/commands/realm.js +58 -0
- package/dist/commands/role.d.ts +3 -0
- package/dist/commands/role.js +77 -0
- package/dist/commands/service-type.d.ts +3 -0
- package/dist/commands/service-type.js +57 -0
- package/dist/commands/user.d.ts +14 -0
- package/dist/commands/user.js +573 -0
- package/dist/error-handler.d.ts +2 -0
- package/dist/error-handler.js +28 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/output/csv.d.ts +3 -0
- package/dist/output/csv.js +57 -0
- package/dist/output/default-columns.d.ts +2 -0
- package/dist/output/default-columns.js +16 -0
- package/dist/output/detect.d.ts +4 -0
- package/dist/output/detect.js +6 -0
- package/dist/output/index.d.ts +15 -0
- package/dist/output/index.js +34 -0
- package/dist/output/json.d.ts +2 -0
- package/dist/output/json.js +10 -0
- package/dist/output/shared.d.ts +13 -0
- package/dist/output/shared.js +41 -0
- package/dist/output/table.d.ts +2 -0
- package/dist/output/table.js +72 -0
- package/dist/output/types.d.ts +2 -0
- package/dist/output/types.js +2 -0
- package/dist/output/yaml-fmt.d.ts +2 -0
- package/dist/output/yaml-fmt.js +11 -0
- package/dist/program.d.ts +3 -0
- package/dist/program.js +37 -0
- package/dist/shared.d.ts +46 -0
- package/dist/shared.js +96 -0
- package/dist/tui/App.d.ts +7 -0
- package/dist/tui/App.js +30 -0
- package/dist/tui/components/AuditTab.d.ts +8 -0
- package/dist/tui/components/AuditTab.js +80 -0
- package/dist/tui/components/FooterBar.d.ts +17 -0
- package/dist/tui/components/FooterBar.js +23 -0
- package/dist/tui/components/FullScreenLayout.d.ts +6 -0
- package/dist/tui/components/FullScreenLayout.js +11 -0
- package/dist/tui/components/HeaderBar.d.ts +5 -0
- package/dist/tui/components/HeaderBar.js +10 -0
- package/dist/tui/components/InfoTab.d.ts +8 -0
- package/dist/tui/components/InfoTab.js +70 -0
- package/dist/tui/components/ResourcePicker.d.ts +10 -0
- package/dist/tui/components/ResourcePicker.js +36 -0
- package/dist/tui/components/SubResourceTab.d.ts +7 -0
- package/dist/tui/components/SubResourceTab.js +193 -0
- package/dist/tui/components/TabBar.d.ts +11 -0
- package/dist/tui/components/TabBar.js +13 -0
- package/dist/tui/components/Table.d.ts +32 -0
- package/dist/tui/components/Table.js +175 -0
- package/dist/tui/context/client.d.ts +7 -0
- package/dist/tui/context/client.js +14 -0
- package/dist/tui/context/navigation.d.ts +14 -0
- package/dist/tui/context/navigation.js +25 -0
- package/dist/tui/context/terminal-size.d.ts +9 -0
- package/dist/tui/context/terminal-size.js +26 -0
- package/dist/tui/format.d.ts +20 -0
- package/dist/tui/format.js +57 -0
- package/dist/tui/hooks/use-audit-log.d.ts +12 -0
- package/dist/tui/hooks/use-audit-log.js +71 -0
- package/dist/tui/hooks/use-local-collection.d.ts +8 -0
- package/dist/tui/hooks/use-local-collection.js +30 -0
- package/dist/tui/hooks/use-paginated-resource.d.ts +23 -0
- package/dist/tui/hooks/use-paginated-resource.js +115 -0
- package/dist/tui/hooks/use-resource-detail.d.ts +7 -0
- package/dist/tui/hooks/use-resource-detail.js +30 -0
- package/dist/tui/hooks/use-scroll-window.d.ts +7 -0
- package/dist/tui/hooks/use-scroll-window.js +29 -0
- package/dist/tui/index.d.ts +2 -0
- package/dist/tui/index.js +22 -0
- package/dist/tui/navigation.d.ts +11 -0
- package/dist/tui/navigation.js +29 -0
- package/dist/tui/resource-configs/api-clients.d.ts +3 -0
- package/dist/tui/resource-configs/api-clients.js +118 -0
- package/dist/tui/resource-configs/index.d.ts +14 -0
- package/dist/tui/resource-configs/index.js +28 -0
- package/dist/tui/resource-configs/instances.d.ts +3 -0
- package/dist/tui/resource-configs/instances.js +24 -0
- package/dist/tui/resource-configs/org-configuration.d.ts +3 -0
- package/dist/tui/resource-configs/org-configuration.js +28 -0
- package/dist/tui/resource-configs/organizations.d.ts +3 -0
- package/dist/tui/resource-configs/organizations.js +104 -0
- package/dist/tui/resource-configs/permissions.d.ts +3 -0
- package/dist/tui/resource-configs/permissions.js +25 -0
- package/dist/tui/resource-configs/realms.d.ts +3 -0
- package/dist/tui/resource-configs/realms.js +36 -0
- package/dist/tui/resource-configs/roles.d.ts +3 -0
- package/dist/tui/resource-configs/roles.js +56 -0
- package/dist/tui/resource-configs/service-types.d.ts +3 -0
- package/dist/tui/resource-configs/service-types.js +24 -0
- package/dist/tui/resource-configs/users.d.ts +3 -0
- package/dist/tui/resource-configs/users.js +126 -0
- package/dist/tui/types.d.ts +99 -0
- package/dist/tui/types.js +23 -0
- package/dist/tui/views/ResourceDetailView.d.ts +7 -0
- package/dist/tui/views/ResourceDetailView.js +123 -0
- package/dist/tui/views/ResourceListView.d.ts +6 -0
- package/dist/tui/views/ResourceListView.js +140 -0
- package/dist/tui/views/ViewRouter.d.ts +2 -0
- package/dist/tui/views/ViewRouter.js +60 -0
- package/package.json +62 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
export async function promptText(opts) {
|
|
3
|
+
const input = (opts.input ?? process.stdin);
|
|
4
|
+
const output = (opts.output ?? process.stdout);
|
|
5
|
+
const rl = readline.createInterface({ input, output });
|
|
6
|
+
const suffix = opts.defaultValue ? ` [${opts.defaultValue}]` : '';
|
|
7
|
+
try {
|
|
8
|
+
const answer = await new Promise((resolve) => {
|
|
9
|
+
rl.question(`${opts.message}${suffix} `, (input) => resolve(input));
|
|
10
|
+
});
|
|
11
|
+
return answer.trim() || opts.defaultValue || '';
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
rl.close();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function promptPassword(opts) {
|
|
18
|
+
const input = (opts.input ?? process.stdin);
|
|
19
|
+
const output = (opts.output ?? process.stdout);
|
|
20
|
+
output.write(`${opts.message} `);
|
|
21
|
+
const tty = input;
|
|
22
|
+
const isTTY = tty.isTTY === true;
|
|
23
|
+
const wasRaw = tty.isRaw === true;
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
let buf = '';
|
|
26
|
+
const cleanup = () => {
|
|
27
|
+
input.removeListener('data', onData);
|
|
28
|
+
input.removeListener('error', onError);
|
|
29
|
+
if (isTTY && tty.setRawMode) {
|
|
30
|
+
tty.setRawMode(wasRaw);
|
|
31
|
+
}
|
|
32
|
+
input.pause();
|
|
33
|
+
};
|
|
34
|
+
const onData = (chunk) => {
|
|
35
|
+
const text = chunk.toString('utf8');
|
|
36
|
+
for (let i = 0; i < text.length; i++) {
|
|
37
|
+
const ch = text[i];
|
|
38
|
+
if (ch === '\n' || ch === '\r') {
|
|
39
|
+
const rest = text.slice(i + 1);
|
|
40
|
+
cleanup();
|
|
41
|
+
output.write('\n');
|
|
42
|
+
if (rest.length > 0 && typeof input.unshift === 'function') {
|
|
43
|
+
input.unshift(Buffer.from(rest, 'utf8'));
|
|
44
|
+
}
|
|
45
|
+
resolve(buf);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (ch === '\u0003') {
|
|
49
|
+
cleanup();
|
|
50
|
+
output.write('\n');
|
|
51
|
+
reject(new Error('Cancelled'));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (ch === '\u007f' || ch === '\b') {
|
|
55
|
+
buf = buf.slice(0, -1);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
buf += ch;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const onError = (err) => { cleanup(); reject(err); };
|
|
62
|
+
input.on('data', onData);
|
|
63
|
+
input.on('error', onError);
|
|
64
|
+
if (isTTY && tty.setRawMode) {
|
|
65
|
+
tty.setRawMode(true);
|
|
66
|
+
}
|
|
67
|
+
input.resume();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=prompt.js.map
|
package/dist/bin.d.ts
ADDED
package/dist/bin.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { CcamClient } from 'ccam-sdk';
|
|
2
|
+
import type { ResolvedProfile } from './auth/profile-resolver.js';
|
|
3
|
+
export declare function createClientFromResolved(resolved: ResolvedProfile, options?: {
|
|
4
|
+
fetch?: typeof fetch;
|
|
5
|
+
}): Promise<CcamClient>;
|
|
6
|
+
//# sourceMappingURL=client-factory.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { CcamClient, TokenManager } from 'ccam-sdk';
|
|
2
|
+
import { ProfileStore } from './auth/profile-store.js';
|
|
3
|
+
export async function createClientFromResolved(resolved, options = {}) {
|
|
4
|
+
if (!resolved.clientId) {
|
|
5
|
+
throw new Error('No client ID resolved. Provide --client-id, set CCAM_CLIENT_ID, or run `ccam auth login`.');
|
|
6
|
+
}
|
|
7
|
+
// A public client (no secret) must rely on a cached token; it can't mint one on demand.
|
|
8
|
+
if (!resolved.clientSecret && !resolved.cachedToken) {
|
|
9
|
+
throw new Error('No client secret and no cached token. Run `ccam auth login` to create a session.');
|
|
10
|
+
}
|
|
11
|
+
const profileName = resolved.profileName;
|
|
12
|
+
const store = new ProfileStore();
|
|
13
|
+
const tokenManager = new TokenManager({
|
|
14
|
+
clientId: resolved.clientId,
|
|
15
|
+
clientSecret: resolved.clientSecret,
|
|
16
|
+
user: resolved.user,
|
|
17
|
+
userPassword: resolved.userPassword,
|
|
18
|
+
host: resolved.host,
|
|
19
|
+
fetch: options.fetch,
|
|
20
|
+
initialCache: resolved.cachedToken
|
|
21
|
+
? {
|
|
22
|
+
accessToken: resolved.cachedToken.accessToken,
|
|
23
|
+
refreshToken: resolved.cachedToken.refreshToken,
|
|
24
|
+
expiresAt: resolved.cachedToken.expiresAt,
|
|
25
|
+
}
|
|
26
|
+
: undefined,
|
|
27
|
+
profileName: resolved.profileName,
|
|
28
|
+
onTokenRefresh: profileName
|
|
29
|
+
? async (cache) => {
|
|
30
|
+
await store.updateCredentials(profileName, {
|
|
31
|
+
accessToken: cache.accessToken,
|
|
32
|
+
refreshToken: cache.refreshToken,
|
|
33
|
+
expiresAt: cache.expiresAt,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
: undefined,
|
|
37
|
+
});
|
|
38
|
+
return new CcamClient({ host: resolved.host, tokenManager, fetch: options.fetch });
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=client-factory.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { runLoopbackLogin } from '../auth/browser-login.js';
|
|
3
|
+
import { ProfileSummary } from '../auth/profile-store.js';
|
|
4
|
+
export declare function registerAuthCommands(program: Command): void;
|
|
5
|
+
export interface ResolveBrowserSecretOptions {
|
|
6
|
+
clientSecret: string | undefined;
|
|
7
|
+
isPublic: boolean;
|
|
8
|
+
prompt: () => Promise<string>;
|
|
9
|
+
}
|
|
10
|
+
export declare function resolveBrowserClientSecret(opts: ResolveBrowserSecretOptions): Promise<string | undefined>;
|
|
11
|
+
export declare function rewriteBrowserLoginError(err: unknown, ctx: {
|
|
12
|
+
hasSecret: boolean;
|
|
13
|
+
}): unknown;
|
|
14
|
+
export interface AuthLoginClientOptions {
|
|
15
|
+
profile: string;
|
|
16
|
+
host: string;
|
|
17
|
+
clientId: string;
|
|
18
|
+
clientSecret: string;
|
|
19
|
+
fetch?: typeof fetch;
|
|
20
|
+
}
|
|
21
|
+
export declare function runAuthLoginClient(opts: AuthLoginClientOptions): Promise<void>;
|
|
22
|
+
export interface AuthLoginPasswordOptions {
|
|
23
|
+
profile: string;
|
|
24
|
+
host: string;
|
|
25
|
+
clientId: string;
|
|
26
|
+
clientSecret: string;
|
|
27
|
+
user: string;
|
|
28
|
+
password: string;
|
|
29
|
+
fetch?: typeof fetch;
|
|
30
|
+
}
|
|
31
|
+
export declare function runAuthLoginPassword(opts: AuthLoginPasswordOptions): Promise<void>;
|
|
32
|
+
export interface AuthLoginBrowserOptions {
|
|
33
|
+
profile: string;
|
|
34
|
+
host: string;
|
|
35
|
+
clientId: string;
|
|
36
|
+
clientSecret?: string;
|
|
37
|
+
redirectPort: number;
|
|
38
|
+
fetch?: typeof fetch;
|
|
39
|
+
openBrowser: (url: string) => void | Promise<void>;
|
|
40
|
+
loopbackRunner?: typeof runLoopbackLogin;
|
|
41
|
+
}
|
|
42
|
+
export declare function runAuthLoginBrowser(opts: AuthLoginBrowserOptions): Promise<void>;
|
|
43
|
+
export interface AuthLogoutOptions {
|
|
44
|
+
profile: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function runAuthLogout(opts: AuthLogoutOptions): Promise<void>;
|
|
47
|
+
export interface AuthListResult {
|
|
48
|
+
activeProfile?: string;
|
|
49
|
+
profiles: ProfileSummary[];
|
|
50
|
+
}
|
|
51
|
+
export declare function runAuthList(): Promise<AuthListResult>;
|
|
52
|
+
export interface AuthShowOptions {
|
|
53
|
+
name: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Returns the non-secret fields for a profile: host, clientId, userEmail, name.
|
|
57
|
+
*
|
|
58
|
+
* Note: clientId and host are not secrets but are semi-sensitive (useful
|
|
59
|
+
* recon for an attacker who has already obtained a secret elsewhere).
|
|
60
|
+
* Treat the contents of `profiles.yaml` accordingly before checking it in.
|
|
61
|
+
*/
|
|
62
|
+
export declare function runAuthShow(opts: AuthShowOptions): Promise<{
|
|
63
|
+
host: string;
|
|
64
|
+
clientId: string;
|
|
65
|
+
userEmail?: string;
|
|
66
|
+
name: string;
|
|
67
|
+
}>;
|
|
68
|
+
export interface AuthUseOptions {
|
|
69
|
+
name: string;
|
|
70
|
+
}
|
|
71
|
+
export declare function runAuthUse(opts: AuthUseOptions): Promise<void>;
|
|
72
|
+
export interface AuthRenameOptions {
|
|
73
|
+
oldName: string;
|
|
74
|
+
newName: string;
|
|
75
|
+
}
|
|
76
|
+
export declare function runAuthRename(opts: AuthRenameOptions): Promise<void>;
|
|
77
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { TokenManager, createPkcePair, buildAuthorizeUrl, generateState, exchangeAuthorizationCode, CcamOAuthError } from 'ccam-sdk';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import YAML from 'yaml';
|
|
4
|
+
import { resolveProfile } from '../auth/profile-resolver.js';
|
|
5
|
+
import { createClientFromResolved } from '../client-factory.js';
|
|
6
|
+
import { promptText, promptPassword } from '../auth/prompt.js';
|
|
7
|
+
import { runLoopbackLogin } from '../auth/browser-login.js';
|
|
8
|
+
import { extractCodeFromInput } from '../auth/manual-login.js';
|
|
9
|
+
import { handleError } from '../error-handler.js';
|
|
10
|
+
import { ProfileStore } from '../auth/profile-store.js';
|
|
11
|
+
export function registerAuthCommands(program) {
|
|
12
|
+
const auth = program
|
|
13
|
+
.command('auth')
|
|
14
|
+
.description('Manage authentication profiles');
|
|
15
|
+
auth
|
|
16
|
+
.command('login')
|
|
17
|
+
.description('Log in to a profile (browser-based by default)')
|
|
18
|
+
.option('--profile <name>', 'Profile name')
|
|
19
|
+
.option('--host <url>', 'AM host URL')
|
|
20
|
+
.option('--client-id <id>', 'API client ID')
|
|
21
|
+
.option('--client-secret <secret>', 'API client secret')
|
|
22
|
+
.option('--redirect-port <port>', 'Loopback redirect port', '65535')
|
|
23
|
+
.option('--manual', 'Skip loopback server; paste the redirect URL manually')
|
|
24
|
+
.option('--public', 'Treat the API client as public (skip client-secret prompt for browser flow)')
|
|
25
|
+
.option('--client', 'Non-interactive client_credentials login')
|
|
26
|
+
.option('--password', 'Non-interactive ROPC login')
|
|
27
|
+
.option('--user <email>', 'User email (for --password)')
|
|
28
|
+
.option('--user-password <pw>', 'User password (for --password)')
|
|
29
|
+
.action(async (_opts, command) => {
|
|
30
|
+
try {
|
|
31
|
+
await loginDispatch(command.optsWithGlobals());
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
handleError(err);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
auth
|
|
38
|
+
.command('logout')
|
|
39
|
+
.description('Remove a profile (defaults to the active profile)')
|
|
40
|
+
.option('--profile <name>', 'Profile name')
|
|
41
|
+
.action(async (_opts, command) => {
|
|
42
|
+
const opts = command.optsWithGlobals();
|
|
43
|
+
try {
|
|
44
|
+
const name = await resolveProfileNameOrActive(opts.profile);
|
|
45
|
+
await runAuthLogout({ profile: name });
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
handleError(err);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
auth
|
|
52
|
+
.command('list')
|
|
53
|
+
.description('List all profiles')
|
|
54
|
+
.action(async () => {
|
|
55
|
+
try {
|
|
56
|
+
const result = await runAuthList();
|
|
57
|
+
printList(result);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
handleError(err);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
auth
|
|
64
|
+
.command('show [name]')
|
|
65
|
+
.description("Show a profile's non-secret fields (defaults to the active profile)")
|
|
66
|
+
.action(async (name, _opts, command) => {
|
|
67
|
+
try {
|
|
68
|
+
const profile = await resolveProfileNameOrActive(name ?? command.optsWithGlobals().profile);
|
|
69
|
+
const shown = await runAuthShow({ name: profile });
|
|
70
|
+
process.stdout.write(YAML.stringify(shown));
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
handleError(err);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
auth
|
|
77
|
+
.command('use <name>')
|
|
78
|
+
.description('Set the active profile')
|
|
79
|
+
.action(async (name) => {
|
|
80
|
+
try {
|
|
81
|
+
await runAuthUse({ name });
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
handleError(err);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
auth
|
|
88
|
+
.command('rename <old> <new>')
|
|
89
|
+
.description('Rename a profile')
|
|
90
|
+
.action(async (oldName, newName) => {
|
|
91
|
+
try {
|
|
92
|
+
await runAuthRename({ oldName, newName });
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
handleError(err);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
auth
|
|
99
|
+
.command('status')
|
|
100
|
+
.description('Show the currently effective auth')
|
|
101
|
+
.action(async () => {
|
|
102
|
+
try {
|
|
103
|
+
await runAuthStatus();
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
handleError(err);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
auth
|
|
110
|
+
.command('token')
|
|
111
|
+
.description('Print an access token (for piping)')
|
|
112
|
+
.action(async () => {
|
|
113
|
+
try {
|
|
114
|
+
await runAuthToken();
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
handleError(err);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
async function loginDispatch(opts) {
|
|
122
|
+
const profile = String(opts.profile ?? process.env.CCAM_PROFILE ?? 'default');
|
|
123
|
+
const host = opts.host ?? (await promptText({ message: 'Host:', defaultValue: 'https://account.demandware.com' }));
|
|
124
|
+
if (opts.client) {
|
|
125
|
+
const clientId = opts.clientId ?? (await promptText({ message: 'Client ID:' }));
|
|
126
|
+
const clientSecret = opts.clientSecret ?? (await promptPassword({ message: 'Client secret:' }));
|
|
127
|
+
await runAuthLoginClient({ profile, host, clientId, clientSecret });
|
|
128
|
+
process.stdout.write(chalk.green(`Logged in (profile: ${profile})\n`));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (opts.password) {
|
|
132
|
+
const clientId = opts.clientId ?? (await promptText({ message: 'Client ID:' }));
|
|
133
|
+
const clientSecret = opts.clientSecret ?? (await promptPassword({ message: 'Client secret:' }));
|
|
134
|
+
const user = opts.user ?? (await promptText({ message: 'User email:' }));
|
|
135
|
+
const password = opts.userPassword ?? (await promptPassword({ message: 'Password:' }));
|
|
136
|
+
await runAuthLoginPassword({ profile, host, clientId, clientSecret, user, password });
|
|
137
|
+
process.stdout.write(chalk.green(`Logged in as ${user} (profile: ${profile})\n`));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Browser flow (default)
|
|
141
|
+
const clientId = opts.clientId ?? (await promptText({ message: 'Client ID:' }));
|
|
142
|
+
const clientSecret = await resolveBrowserClientSecret({
|
|
143
|
+
clientSecret: opts.clientSecret,
|
|
144
|
+
isPublic: Boolean(opts.public),
|
|
145
|
+
prompt: () => promptPassword({ message: 'Client secret (blank for public client):' }),
|
|
146
|
+
});
|
|
147
|
+
const redirectPort = parseInt(String(opts.redirectPort ?? '65535'), 10);
|
|
148
|
+
const openBrowser = await loadOpenImpl(Boolean(opts.manual));
|
|
149
|
+
const loopbackRunner = opts.manual ? manualRunner : undefined;
|
|
150
|
+
try {
|
|
151
|
+
await runAuthLoginBrowser({ profile, host, clientId, clientSecret, redirectPort, openBrowser, loopbackRunner });
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
throw rewriteBrowserLoginError(err, { hasSecret: Boolean(clientSecret) });
|
|
155
|
+
}
|
|
156
|
+
process.stdout.write(chalk.green(`Logged in (profile: ${profile})\n`));
|
|
157
|
+
}
|
|
158
|
+
export async function resolveBrowserClientSecret(opts) {
|
|
159
|
+
if (opts.clientSecret)
|
|
160
|
+
return opts.clientSecret;
|
|
161
|
+
if (opts.isPublic)
|
|
162
|
+
return undefined;
|
|
163
|
+
const entered = await opts.prompt();
|
|
164
|
+
return entered ? entered : undefined;
|
|
165
|
+
}
|
|
166
|
+
export function rewriteBrowserLoginError(err, ctx) {
|
|
167
|
+
if (err instanceof CcamOAuthError && err.oauthCode === 'invalid_client' && !ctx.hasSecret) {
|
|
168
|
+
return new Error('AM rejected the login as a confidential client. Rerun and provide the client secret (--client-secret, or enter it at the prompt). If the API client really is public, keep --public set.');
|
|
169
|
+
}
|
|
170
|
+
return err;
|
|
171
|
+
}
|
|
172
|
+
async function setActiveIfNone(store, profile) {
|
|
173
|
+
const state = await store.read();
|
|
174
|
+
if (!state.activeProfile) {
|
|
175
|
+
await store.setActiveProfile(profile);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function resolveProfileNameOrActive(explicit) {
|
|
179
|
+
if (explicit)
|
|
180
|
+
return explicit;
|
|
181
|
+
if (process.env.CCAM_PROFILE)
|
|
182
|
+
return process.env.CCAM_PROFILE;
|
|
183
|
+
const state = await new ProfileStore().read();
|
|
184
|
+
if (state.activeProfile)
|
|
185
|
+
return state.activeProfile;
|
|
186
|
+
throw new Error('No profile specified and no active profile set. Pass --profile, set CCAM_PROFILE, or run `ccam auth use <name>`.');
|
|
187
|
+
}
|
|
188
|
+
async function loadOpenImpl(manual) {
|
|
189
|
+
if (manual) {
|
|
190
|
+
return (url) => { process.stdout.write(`Open this URL:\n ${url}\n`); };
|
|
191
|
+
}
|
|
192
|
+
const open = (await import('open')).default;
|
|
193
|
+
return (url) => { void open(url); };
|
|
194
|
+
}
|
|
195
|
+
function manualRunner(input) {
|
|
196
|
+
const getCode = async () => {
|
|
197
|
+
process.stdout.write(`Open this URL in a browser:\n ${input.authorizeUrl}\n`);
|
|
198
|
+
const raw = await promptText({ message: 'Paste the redirect URL or code:' });
|
|
199
|
+
return extractCodeFromInput(raw, input.expectedState);
|
|
200
|
+
};
|
|
201
|
+
const done = getCode().then(code => ({ code }));
|
|
202
|
+
Object.assign(done, { port: Promise.resolve(input.port) });
|
|
203
|
+
return done;
|
|
204
|
+
}
|
|
205
|
+
async function runAuthStatus() {
|
|
206
|
+
const resolved = await resolveProfile({ flags: {} });
|
|
207
|
+
process.stdout.write(chalk.bold('Authentication Status:\n\n'));
|
|
208
|
+
process.stdout.write(`Profile: ${resolved.profileName ?? chalk.dim('(none)')}\n`);
|
|
209
|
+
process.stdout.write(`Source: ${resolved.source}\n`);
|
|
210
|
+
process.stdout.write(`Host: ${resolved.host}\n`);
|
|
211
|
+
process.stdout.write(`Client ID: ${resolved.clientId ? chalk.green(resolved.clientId) : chalk.red('(not set)')}\n`);
|
|
212
|
+
if (resolved.user)
|
|
213
|
+
process.stdout.write(`User: ${resolved.user}\n`);
|
|
214
|
+
if (!resolved.clientId || !resolved.clientSecret) {
|
|
215
|
+
process.stdout.write(chalk.red('\nStatus: Not authenticated\n'));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const client = await createClientFromResolved(resolved);
|
|
220
|
+
await client.roles.list({ page: 0, size: 1 });
|
|
221
|
+
process.stdout.write(chalk.green('\nStatus: Valid\n'));
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
process.stdout.write(chalk.red('\nStatus: Invalid or expired\n'));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async function runAuthToken() {
|
|
228
|
+
const resolved = await resolveProfile({ flags: {} });
|
|
229
|
+
if (!resolved.clientId || !resolved.clientSecret) {
|
|
230
|
+
throw new Error('No credentials resolved.');
|
|
231
|
+
}
|
|
232
|
+
const tm = new TokenManager({
|
|
233
|
+
clientId: resolved.clientId,
|
|
234
|
+
clientSecret: resolved.clientSecret,
|
|
235
|
+
host: resolved.host,
|
|
236
|
+
user: resolved.user,
|
|
237
|
+
userPassword: resolved.userPassword,
|
|
238
|
+
initialCache: resolved.cachedToken
|
|
239
|
+
? { accessToken: resolved.cachedToken.accessToken, refreshToken: resolved.cachedToken.refreshToken, expiresAt: resolved.cachedToken.expiresAt }
|
|
240
|
+
: undefined,
|
|
241
|
+
});
|
|
242
|
+
const token = await tm.getToken();
|
|
243
|
+
process.stdout.write(token + '\n');
|
|
244
|
+
}
|
|
245
|
+
function printList(result) {
|
|
246
|
+
for (const p of result.profiles) {
|
|
247
|
+
const marker = result.activeProfile === p.name ? '*' : ' ';
|
|
248
|
+
const badge = p.state === 'ok' ? '' : chalk.yellow(` [${p.state}]`);
|
|
249
|
+
process.stdout.write(`${marker} ${p.name}${badge}\n`);
|
|
250
|
+
}
|
|
251
|
+
if (!result.profiles.length) {
|
|
252
|
+
process.stdout.write(chalk.dim('No profiles configured. Run `ccam auth login` to create one.\n'));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
export async function runAuthLoginClient(opts) {
|
|
256
|
+
let capturedExpiresAt;
|
|
257
|
+
const tm = new TokenManager({
|
|
258
|
+
clientId: opts.clientId,
|
|
259
|
+
clientSecret: opts.clientSecret,
|
|
260
|
+
host: opts.host,
|
|
261
|
+
fetch: opts.fetch,
|
|
262
|
+
onTokenRefresh: (cache) => {
|
|
263
|
+
capturedExpiresAt = cache.expiresAt;
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
const accessToken = await tm.getToken();
|
|
267
|
+
const store = new ProfileStore();
|
|
268
|
+
await store.saveProfile(opts.profile, {
|
|
269
|
+
config: { host: opts.host, clientId: opts.clientId },
|
|
270
|
+
credentials: {
|
|
271
|
+
clientSecret: opts.clientSecret,
|
|
272
|
+
accessToken,
|
|
273
|
+
expiresAt: capturedExpiresAt,
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
await setActiveIfNone(store, opts.profile);
|
|
277
|
+
}
|
|
278
|
+
export async function runAuthLoginPassword(opts) {
|
|
279
|
+
let capturedRefresh;
|
|
280
|
+
let capturedExpiresAt;
|
|
281
|
+
const tm = new TokenManager({
|
|
282
|
+
clientId: opts.clientId,
|
|
283
|
+
clientSecret: opts.clientSecret,
|
|
284
|
+
user: opts.user,
|
|
285
|
+
userPassword: opts.password,
|
|
286
|
+
host: opts.host,
|
|
287
|
+
fetch: opts.fetch,
|
|
288
|
+
onTokenRefresh: (cache) => {
|
|
289
|
+
capturedRefresh = cache.refreshToken;
|
|
290
|
+
capturedExpiresAt = cache.expiresAt;
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
const accessToken = await tm.getToken();
|
|
294
|
+
const store = new ProfileStore();
|
|
295
|
+
await store.saveProfile(opts.profile, {
|
|
296
|
+
config: { host: opts.host, clientId: opts.clientId, userEmail: opts.user },
|
|
297
|
+
credentials: {
|
|
298
|
+
clientSecret: opts.clientSecret,
|
|
299
|
+
userPassword: opts.password,
|
|
300
|
+
refreshToken: capturedRefresh,
|
|
301
|
+
accessToken,
|
|
302
|
+
expiresAt: capturedExpiresAt,
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
await setActiveIfNone(store, opts.profile);
|
|
306
|
+
}
|
|
307
|
+
export async function runAuthLoginBrowser(opts) {
|
|
308
|
+
const { verifier, challenge } = createPkcePair();
|
|
309
|
+
const state = generateState();
|
|
310
|
+
const redirectUri = `http://127.0.0.1:${opts.redirectPort}/callback`;
|
|
311
|
+
const authorizeUrl = buildAuthorizeUrl({
|
|
312
|
+
host: opts.host,
|
|
313
|
+
clientId: opts.clientId,
|
|
314
|
+
redirectUri,
|
|
315
|
+
codeChallenge: challenge,
|
|
316
|
+
state,
|
|
317
|
+
});
|
|
318
|
+
const runner = opts.loopbackRunner ?? runLoopbackLogin;
|
|
319
|
+
const { code } = await runner({
|
|
320
|
+
authorizeUrl,
|
|
321
|
+
expectedState: state,
|
|
322
|
+
port: opts.redirectPort,
|
|
323
|
+
open: opts.openBrowser,
|
|
324
|
+
});
|
|
325
|
+
const result = await exchangeAuthorizationCode({
|
|
326
|
+
clientId: opts.clientId,
|
|
327
|
+
clientSecret: opts.clientSecret,
|
|
328
|
+
host: opts.host,
|
|
329
|
+
code,
|
|
330
|
+
redirectUri,
|
|
331
|
+
codeVerifier: verifier,
|
|
332
|
+
fetch: opts.fetch,
|
|
333
|
+
});
|
|
334
|
+
const store = new ProfileStore();
|
|
335
|
+
await store.saveProfile(opts.profile, {
|
|
336
|
+
config: { host: opts.host, clientId: opts.clientId },
|
|
337
|
+
credentials: {
|
|
338
|
+
clientSecret: opts.clientSecret,
|
|
339
|
+
refreshToken: result.refreshToken,
|
|
340
|
+
accessToken: result.accessToken,
|
|
341
|
+
expiresAt: Date.now() + (result.expiresIn - 60) * 1000,
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
await setActiveIfNone(store, opts.profile);
|
|
345
|
+
}
|
|
346
|
+
export async function runAuthLogout(opts) {
|
|
347
|
+
const store = new ProfileStore();
|
|
348
|
+
const state = await store.read();
|
|
349
|
+
if (!state.profiles[opts.profile] && !state.credentials[opts.profile]) {
|
|
350
|
+
throw new Error(`Profile '${opts.profile}' not found`);
|
|
351
|
+
}
|
|
352
|
+
await store.deleteProfile(opts.profile);
|
|
353
|
+
}
|
|
354
|
+
export async function runAuthList() {
|
|
355
|
+
const store = new ProfileStore();
|
|
356
|
+
const state = await store.read();
|
|
357
|
+
const profiles = await store.listProfiles();
|
|
358
|
+
return { activeProfile: state.activeProfile, profiles };
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Returns the non-secret fields for a profile: host, clientId, userEmail, name.
|
|
362
|
+
*
|
|
363
|
+
* Note: clientId and host are not secrets but are semi-sensitive (useful
|
|
364
|
+
* recon for an attacker who has already obtained a secret elsewhere).
|
|
365
|
+
* Treat the contents of `profiles.yaml` accordingly before checking it in.
|
|
366
|
+
*/
|
|
367
|
+
export async function runAuthShow(opts) {
|
|
368
|
+
const store = new ProfileStore();
|
|
369
|
+
const state = await store.read();
|
|
370
|
+
const profile = state.profiles[opts.name];
|
|
371
|
+
if (!profile) {
|
|
372
|
+
if (state.credentials[opts.name]) {
|
|
373
|
+
throw new Error(`Profile '${opts.name}' is incomplete: credentials present but config is missing. Run \`ccam auth login --profile ${opts.name}\` to repair.`);
|
|
374
|
+
}
|
|
375
|
+
throw new Error(`Profile '${opts.name}' not found`);
|
|
376
|
+
}
|
|
377
|
+
return { ...profile, name: opts.name };
|
|
378
|
+
}
|
|
379
|
+
export async function runAuthUse(opts) {
|
|
380
|
+
const store = new ProfileStore();
|
|
381
|
+
await store.setActiveProfile(opts.name);
|
|
382
|
+
}
|
|
383
|
+
export async function runAuthRename(opts) {
|
|
384
|
+
const store = new ProfileStore();
|
|
385
|
+
await store.renameProfile(opts.oldName, opts.newName);
|
|
386
|
+
}
|
|
387
|
+
//# sourceMappingURL=auth.js.map
|