@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.
Files changed (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +209 -0
  3. package/dist/auth/browser-login.d.ts +14 -0
  4. package/dist/auth/browser-login.js +72 -0
  5. package/dist/auth/manual-login.d.ts +10 -0
  6. package/dist/auth/manual-login.js +33 -0
  7. package/dist/auth/paths.d.ts +4 -0
  8. package/dist/auth/paths.js +15 -0
  9. package/dist/auth/profile-resolver.d.ts +26 -0
  10. package/dist/auth/profile-resolver.js +42 -0
  11. package/dist/auth/profile-store.d.ts +38 -0
  12. package/dist/auth/profile-store.js +125 -0
  13. package/dist/auth/prompt.d.ts +9 -0
  14. package/dist/auth/prompt.js +70 -0
  15. package/dist/bin.d.ts +3 -0
  16. package/dist/bin.js +4 -0
  17. package/dist/client-factory.d.ts +6 -0
  18. package/dist/client-factory.js +40 -0
  19. package/dist/commands/auth.d.ts +77 -0
  20. package/dist/commands/auth.js +387 -0
  21. package/dist/commands/client.d.ts +3 -0
  22. package/dist/commands/client.js +365 -0
  23. package/dist/commands/instance.d.ts +11 -0
  24. package/dist/commands/instance.js +128 -0
  25. package/dist/commands/org-config.d.ts +3 -0
  26. package/dist/commands/org-config.js +31 -0
  27. package/dist/commands/org.d.ts +11 -0
  28. package/dist/commands/org.js +234 -0
  29. package/dist/commands/permission.d.ts +3 -0
  30. package/dist/commands/permission.js +60 -0
  31. package/dist/commands/realm.d.ts +3 -0
  32. package/dist/commands/realm.js +58 -0
  33. package/dist/commands/role.d.ts +3 -0
  34. package/dist/commands/role.js +77 -0
  35. package/dist/commands/service-type.d.ts +3 -0
  36. package/dist/commands/service-type.js +57 -0
  37. package/dist/commands/user.d.ts +14 -0
  38. package/dist/commands/user.js +573 -0
  39. package/dist/error-handler.d.ts +2 -0
  40. package/dist/error-handler.js +28 -0
  41. package/dist/index.d.ts +2 -0
  42. package/dist/index.js +2 -0
  43. package/dist/output/csv.d.ts +3 -0
  44. package/dist/output/csv.js +57 -0
  45. package/dist/output/default-columns.d.ts +2 -0
  46. package/dist/output/default-columns.js +16 -0
  47. package/dist/output/detect.d.ts +4 -0
  48. package/dist/output/detect.js +6 -0
  49. package/dist/output/index.d.ts +15 -0
  50. package/dist/output/index.js +34 -0
  51. package/dist/output/json.d.ts +2 -0
  52. package/dist/output/json.js +10 -0
  53. package/dist/output/shared.d.ts +13 -0
  54. package/dist/output/shared.js +41 -0
  55. package/dist/output/table.d.ts +2 -0
  56. package/dist/output/table.js +72 -0
  57. package/dist/output/types.d.ts +2 -0
  58. package/dist/output/types.js +2 -0
  59. package/dist/output/yaml-fmt.d.ts +2 -0
  60. package/dist/output/yaml-fmt.js +11 -0
  61. package/dist/program.d.ts +3 -0
  62. package/dist/program.js +37 -0
  63. package/dist/shared.d.ts +46 -0
  64. package/dist/shared.js +96 -0
  65. package/dist/tui/App.d.ts +7 -0
  66. package/dist/tui/App.js +30 -0
  67. package/dist/tui/components/AuditTab.d.ts +8 -0
  68. package/dist/tui/components/AuditTab.js +80 -0
  69. package/dist/tui/components/FooterBar.d.ts +17 -0
  70. package/dist/tui/components/FooterBar.js +23 -0
  71. package/dist/tui/components/FullScreenLayout.d.ts +6 -0
  72. package/dist/tui/components/FullScreenLayout.js +11 -0
  73. package/dist/tui/components/HeaderBar.d.ts +5 -0
  74. package/dist/tui/components/HeaderBar.js +10 -0
  75. package/dist/tui/components/InfoTab.d.ts +8 -0
  76. package/dist/tui/components/InfoTab.js +70 -0
  77. package/dist/tui/components/ResourcePicker.d.ts +10 -0
  78. package/dist/tui/components/ResourcePicker.js +36 -0
  79. package/dist/tui/components/SubResourceTab.d.ts +7 -0
  80. package/dist/tui/components/SubResourceTab.js +193 -0
  81. package/dist/tui/components/TabBar.d.ts +11 -0
  82. package/dist/tui/components/TabBar.js +13 -0
  83. package/dist/tui/components/Table.d.ts +32 -0
  84. package/dist/tui/components/Table.js +175 -0
  85. package/dist/tui/context/client.d.ts +7 -0
  86. package/dist/tui/context/client.js +14 -0
  87. package/dist/tui/context/navigation.d.ts +14 -0
  88. package/dist/tui/context/navigation.js +25 -0
  89. package/dist/tui/context/terminal-size.d.ts +9 -0
  90. package/dist/tui/context/terminal-size.js +26 -0
  91. package/dist/tui/format.d.ts +20 -0
  92. package/dist/tui/format.js +57 -0
  93. package/dist/tui/hooks/use-audit-log.d.ts +12 -0
  94. package/dist/tui/hooks/use-audit-log.js +71 -0
  95. package/dist/tui/hooks/use-local-collection.d.ts +8 -0
  96. package/dist/tui/hooks/use-local-collection.js +30 -0
  97. package/dist/tui/hooks/use-paginated-resource.d.ts +23 -0
  98. package/dist/tui/hooks/use-paginated-resource.js +115 -0
  99. package/dist/tui/hooks/use-resource-detail.d.ts +7 -0
  100. package/dist/tui/hooks/use-resource-detail.js +30 -0
  101. package/dist/tui/hooks/use-scroll-window.d.ts +7 -0
  102. package/dist/tui/hooks/use-scroll-window.js +29 -0
  103. package/dist/tui/index.d.ts +2 -0
  104. package/dist/tui/index.js +22 -0
  105. package/dist/tui/navigation.d.ts +11 -0
  106. package/dist/tui/navigation.js +29 -0
  107. package/dist/tui/resource-configs/api-clients.d.ts +3 -0
  108. package/dist/tui/resource-configs/api-clients.js +118 -0
  109. package/dist/tui/resource-configs/index.d.ts +14 -0
  110. package/dist/tui/resource-configs/index.js +28 -0
  111. package/dist/tui/resource-configs/instances.d.ts +3 -0
  112. package/dist/tui/resource-configs/instances.js +24 -0
  113. package/dist/tui/resource-configs/org-configuration.d.ts +3 -0
  114. package/dist/tui/resource-configs/org-configuration.js +28 -0
  115. package/dist/tui/resource-configs/organizations.d.ts +3 -0
  116. package/dist/tui/resource-configs/organizations.js +104 -0
  117. package/dist/tui/resource-configs/permissions.d.ts +3 -0
  118. package/dist/tui/resource-configs/permissions.js +25 -0
  119. package/dist/tui/resource-configs/realms.d.ts +3 -0
  120. package/dist/tui/resource-configs/realms.js +36 -0
  121. package/dist/tui/resource-configs/roles.d.ts +3 -0
  122. package/dist/tui/resource-configs/roles.js +56 -0
  123. package/dist/tui/resource-configs/service-types.d.ts +3 -0
  124. package/dist/tui/resource-configs/service-types.js +24 -0
  125. package/dist/tui/resource-configs/users.d.ts +3 -0
  126. package/dist/tui/resource-configs/users.js +126 -0
  127. package/dist/tui/types.d.ts +99 -0
  128. package/dist/tui/types.js +23 -0
  129. package/dist/tui/views/ResourceDetailView.d.ts +7 -0
  130. package/dist/tui/views/ResourceDetailView.js +123 -0
  131. package/dist/tui/views/ResourceListView.d.ts +6 -0
  132. package/dist/tui/views/ResourceListView.js +140 -0
  133. package/dist/tui/views/ViewRouter.d.ts +2 -0
  134. package/dist/tui/views/ViewRouter.js +60 -0
  135. package/package.json +62 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 James Klein
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,209 @@
1
+ # ccam
2
+
3
+ TypeScript CLI and SDK for the Salesforce Commerce Cloud Account Manager REST API.
4
+
5
+ The AM API is largely undocumented. **ccam** serves as both a practical tool and a reference implementation for developers working with Commerce Cloud account management.
6
+
7
+ ## Why ccam?
8
+
9
+ ccam is built for people who administer Account Manager: security and compliance teams running access audits, team leads onboarding and offboarding users, and developers integrating AM into their own tooling.
10
+
11
+ - **Complete AM coverage.** Users, organizations, API clients, roles, realms, permissions, service types, org configurations, instances -- every resource, every subresource, every finder.
12
+ - **Exportable.** CSV, TSV, YAML, JSON, or table. `ccam user list --org <id> --format csv` produces a spreadsheet-ready roster.
13
+ - **Typed SDK.** `ccam-sdk` is a first-class, documented TypeScript library with TSDoc on every method, typed sort enums, and a structured error taxonomy.
14
+ - **Named auth profiles.** Switch between orgs or accounts without re-authenticating. Refresh tokens are stored at `~/.config/ccam/credentials` with 0600 permissions.
15
+ - **Interactive TUI.** Run `ccam` with no arguments for a keyboard-driven resource browser with drill-down from user to orgs to realms.
16
+
17
+ Comparisons with the other Commerce Cloud CLIs:
18
+
19
+ - [b2c-cli](https://github.com/SalesforceCommerceCloud/b2c-developer-tooling) is Salesforce's official CLI (GA, supersedes sfcc-ci). Broad Commerce Cloud coverage (OCAPI, sandboxes, SLAS, MRT), basic AM commands. See [docs/vs-b2c-cli.md](docs/vs-b2c-cli.md).
20
+ - [sfcc-ci](https://github.com/SalesforceCommerceCloud/sfcc-ci) is the long-standing community CLI for CI/CD. Minimal AM coverage. See [docs/vs-sfcc-ci.md](docs/vs-sfcc-ci.md).
21
+
22
+ ccam fills the AM-administration gap both leave open.
23
+
24
+ ## Quick Start: CLI
25
+
26
+ ### Installation
27
+
28
+ ```bash
29
+ npm install -g @j-256/ccam
30
+ ```
31
+
32
+ ### First run
33
+
34
+ ccam talks to Account Manager over OAuth2, so you need an AM API client before you can log in. If you already use sfcc-ci or other Commerce Cloud tooling, you can reuse that client; otherwise see [`docs/getting-started.md`](docs/getting-started.md) for a step-by-step walkthrough of creating one.
35
+
36
+ Once you have a client ID (and secret, for confidential clients):
37
+
38
+ ```bash
39
+ ccam auth login --client-id <your-client-id>
40
+ ```
41
+
42
+ This opens a browser to AM, captures the authorization code on a loopback server, and saves a profile to `~/.config/ccam/profiles.yaml` (non-secret) and `~/.config/ccam/credentials` (0600; contains refresh token).
43
+
44
+ Non-interactive alternatives:
45
+
46
+ - `ccam auth login --client` -- client_credentials flow (for CI/automation)
47
+ - `ccam auth login --password` -- ROPC flow (when SSO/MFA are not enforced)
48
+ - `ccam auth login --manual` -- browser flow without a loopback server (for SSH/headless)
49
+ - Set environment variables instead -- see below
50
+
51
+ ### Authentication
52
+
53
+ Set environment variables for your API client credentials:
54
+
55
+ ```bash
56
+ export CCAM_CLIENT_ID="your-client-id"
57
+ export CCAM_CLIENT_SECRET="your-client-secret"
58
+ export CCAM_HOST="https://account.demandware.com" # optional, this is the default
59
+ ```
60
+
61
+ For user-context operations (e.g. `client.users.current()` in the SDK), also set:
62
+
63
+ ```bash
64
+ export CCAM_USER="your-email@example.com"
65
+ export CCAM_USER_PASSWORD="your-password"
66
+ ```
67
+
68
+ ### Example Commands
69
+
70
+ List users:
71
+ ```bash
72
+ ccam user list
73
+ ```
74
+
75
+ List users with filters:
76
+ ```bash
77
+ ccam user list --org abc123 --role def456
78
+ ccam user list --login user@example.com
79
+ ccam user list --org-type customer
80
+ ```
81
+
82
+ Filter organizations:
83
+ ```bash
84
+ ccam org list --name "Acme Corp"
85
+ ccam org list --starts-with "Acme"
86
+ ccam org list --sf-account-id "001..."
87
+ ```
88
+
89
+ List roles:
90
+ ```bash
91
+ ccam role list
92
+ ```
93
+
94
+ Export to CSV:
95
+ ```bash
96
+ ccam user list --org abc123 --format csv > users.csv
97
+ ```
98
+
99
+ ## Quick Start: SDK
100
+
101
+ ### Installation
102
+
103
+ ```bash
104
+ npm install ccam-sdk
105
+ ```
106
+
107
+ ### Usage
108
+
109
+ ```typescript
110
+ import { CcamClient } from 'ccam-sdk';
111
+
112
+ // Create client (uses environment variables or explicit options)
113
+ const client = new CcamClient({
114
+ clientId: 'your-client-id',
115
+ clientSecret: 'your-client-secret',
116
+ host: 'https://account.demandware.com' // optional
117
+ });
118
+
119
+ // List users with pagination
120
+ const result = await client.users.list({
121
+ page: 0,
122
+ size: 25
123
+ });
124
+
125
+ console.log(result.content);
126
+ console.log(`Page ${result.page.number + 1} of ${result.page.totalPages}`);
127
+
128
+ // Get user by login with expanded organizations
129
+ const user = await client.users.getByLogin('user@example.com', {
130
+ expand: 'organizations'
131
+ });
132
+
133
+ console.log(user.mail, user.organizations);
134
+
135
+ // List roles with sorting
136
+ const roles = await client.roles.list({
137
+ page: 0,
138
+ size: 50,
139
+ sort: { field: 'name', direction: 'asc' }
140
+ });
141
+ ```
142
+
143
+ ## Authentication
144
+
145
+ ccam supports three credential sources (first wins):
146
+
147
+ 1. **Explicit options** passed to `CcamClient` constructor
148
+ 2. **Environment variables**: `CCAM_CLIENT_ID`, `CCAM_CLIENT_SECRET`, `CCAM_USER`, `CCAM_USER_PASSWORD`, `CCAM_HOST`
149
+ 3. **Default host**: `https://account.demandware.com`
150
+
151
+ Two authentication modes:
152
+
153
+ - **Client-only** (system context): read all resources, most common mode
154
+ - **Client + user** (user context): required for the `/users/current` endpoint
155
+
156
+ ## Output Formats
157
+
158
+ | Format | Use case | CLI flag |
159
+ |--------|----------|----------|
160
+ | `table` | Human-readable display (TTY default) | `--format table` |
161
+ | `json` | Machine-readable, piping to jq | `--format json` or `-j` |
162
+ | `csv` | Spreadsheet import, data analysis | `--format csv` |
163
+ | `tsv` | Tab-separated (better for whitespace) | `--format tsv` |
164
+ | `yaml` | Config files, human-readable structured | `--format yaml` |
165
+
166
+ When output is piped (not a TTY), JSON is the default format.
167
+
168
+ ## Resources
169
+
170
+ | Resource | CLI command | SDK property |
171
+ |----------|-------------|--------------|
172
+ | Users | `ccam user` | `client.users` |
173
+ | Organizations | `ccam org` | `client.organizations` |
174
+ | API Clients | `ccam client` | `client.apiClients` |
175
+ | Roles | `ccam role` | `client.roles` |
176
+ | Realms | `ccam realm` | `client.realms` |
177
+ | Permissions | `ccam permission` | `client.permissions` |
178
+ | Service Types | `ccam service-type` | `client.serviceTypes` |
179
+
180
+ Most resources support:
181
+ - `list` -- paginated list of resources
182
+ - `get <id>` -- get a single resource by ID
183
+
184
+ Additional commands:
185
+ - Users: `get` by login (default) or by ID (`--id`), `current`, `audit`, `roles`, `instances`, `assigned-realms`, `assigned-instances`, `create`, `update`, `delete`, `reset`, `disable`, `revoke-verifier`. Filter flags on `list` (see below).
186
+ - Organizations: `realms`, `instances`, `audit`, `update`. Filter flags on `list` (`--name`, `--starts-with`, `--sf-account-id`).
187
+ - API Clients: `audit`, `assigned-realms`, `assigned-instances`, `create`, `update`, `delete`, `set-password`, `set-auth-type`.
188
+ - Instances: `validate-filter`.
189
+
190
+ Users and API Clients support `--expand` on `get` to include related resources (`organizations`, `roles`, or `organizations,roles`). Roles and Org Realms support `--expand serviceType` / `--expand instance` respectively.
191
+
192
+ ## User Search Filters
193
+
194
+ The `ccam user list` command maps filter flags to AM API finder methods:
195
+
196
+ | Flag | API finder | Example |
197
+ |------|------------|---------|
198
+ | `--login <email>` | `findByLogin` | `ccam user list --login user@example.com` |
199
+ | `--org <id>` | `findByOrg` | `ccam user list --org abc123` |
200
+ | `--org <id> --all` | `findAllByOrg` | `ccam user list --org abc123 --all` |
201
+ | `--role <id>` | `findByRole` | `ccam user list --role def456` |
202
+ | `--org <id> --role <id>` | `findByOrgAndRole` | `ccam user list --org abc123 --role def456` |
203
+ | `--org-realm-access <id>` | `findByOrgRealmAccess` | `ccam user list --org-realm-access abc123` |
204
+
205
+ Add `--modified-after <date>` with `--role` to filter by modification date.
206
+
207
+ ## License
208
+
209
+ MIT
@@ -0,0 +1,14 @@
1
+ export interface LoopbackLoginOptions {
2
+ authorizeUrl: string;
3
+ expectedState: string;
4
+ port: number;
5
+ open: (url: string) => void | Promise<void>;
6
+ }
7
+ export interface LoopbackLoginResult {
8
+ code: string;
9
+ }
10
+ export type LoopbackPromise = Promise<LoopbackLoginResult> & {
11
+ port: Promise<number>;
12
+ };
13
+ export declare function runLoopbackLogin(options: LoopbackLoginOptions): LoopbackPromise;
14
+ //# sourceMappingURL=browser-login.d.ts.map
@@ -0,0 +1,72 @@
1
+ import http from 'node:http';
2
+ export function runLoopbackLogin(options) {
3
+ let portResolve;
4
+ let portReject;
5
+ const portPromise = new Promise((resolve, reject) => {
6
+ portResolve = resolve;
7
+ portReject = reject;
8
+ });
9
+ const done = new Promise((resolve, reject) => {
10
+ const sockets = new Set();
11
+ const shutdown = () => {
12
+ for (const sock of sockets)
13
+ sock.destroy();
14
+ server.close();
15
+ };
16
+ const server = http.createServer((req, res) => {
17
+ res.setHeader('Connection', 'close');
18
+ const url = new URL(req.url ?? '/', 'http://127.0.0.1');
19
+ if (url.pathname !== '/callback') {
20
+ res.statusCode = 404;
21
+ res.end();
22
+ return;
23
+ }
24
+ const err = url.searchParams.get('error');
25
+ const state = url.searchParams.get('state');
26
+ const code = url.searchParams.get('code');
27
+ if (err === 'access_denied') {
28
+ res.statusCode = 200;
29
+ res.end('Login was cancelled. You can close this tab.');
30
+ shutdown();
31
+ reject(new Error('Login was cancelled.'));
32
+ return;
33
+ }
34
+ if (state !== options.expectedState) {
35
+ res.statusCode = 400;
36
+ res.end('State mismatch. You can close this tab.');
37
+ shutdown();
38
+ reject(new Error('Login failed: state mismatch.'));
39
+ return;
40
+ }
41
+ if (!code) {
42
+ res.statusCode = 400;
43
+ res.end('Missing authorization code.');
44
+ shutdown();
45
+ reject(new Error('Login failed: no code in redirect.'));
46
+ return;
47
+ }
48
+ res.statusCode = 200;
49
+ res.end('Authorization received. Return to the terminal to finish login. You can close this tab.');
50
+ shutdown();
51
+ resolve({ code });
52
+ });
53
+ server.on('connection', (sock) => {
54
+ sockets.add(sock);
55
+ sock.once('close', () => sockets.delete(sock));
56
+ });
57
+ server.on('error', (err) => {
58
+ portReject(err);
59
+ reject(err);
60
+ });
61
+ server.listen(options.port, '127.0.0.1', () => {
62
+ const addr = server.address();
63
+ if (addr && typeof addr === 'object') {
64
+ portResolve(addr.port);
65
+ Promise.resolve(options.open(options.authorizeUrl)).catch(reject);
66
+ }
67
+ });
68
+ });
69
+ Object.assign(done, { port: portPromise });
70
+ return done;
71
+ }
72
+ //# sourceMappingURL=browser-login.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Manual-paste OAuth redirect handling for `auth login --manual`.
3
+ *
4
+ * Note: The `promptText` helper echoes input via plain readline. A user
5
+ * pasting the full redirect URL will briefly display the one-time `code`
6
+ * parameter on the terminal. Auth codes are single-use so the exposure
7
+ * is bounded, but be aware if you are screen-recording or screen-sharing.
8
+ */
9
+ export declare function extractCodeFromInput(input: string, expectedState: string): string;
10
+ //# sourceMappingURL=manual-login.d.ts.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Manual-paste OAuth redirect handling for `auth login --manual`.
3
+ *
4
+ * Note: The `promptText` helper echoes input via plain readline. A user
5
+ * pasting the full redirect URL will briefly display the one-time `code`
6
+ * parameter on the terminal. Auth codes are single-use so the exposure
7
+ * is bounded, but be aware if you are screen-recording or screen-sharing.
8
+ */
9
+ export function extractCodeFromInput(input, expectedState) {
10
+ const trimmed = input.trim();
11
+ if (!trimmed)
12
+ throw new Error('Pasted value was empty.');
13
+ let url;
14
+ try {
15
+ url = new URL(trimmed);
16
+ }
17
+ catch {
18
+ return trimmed;
19
+ }
20
+ const err = url.searchParams.get('error');
21
+ if (err === 'access_denied')
22
+ throw new Error('Login was cancelled.');
23
+ if (err)
24
+ throw new Error(`AM returned an error: ${err}`);
25
+ const state = url.searchParams.get('state');
26
+ if (state !== expectedState)
27
+ throw new Error('Login failed: state mismatch.');
28
+ const code = url.searchParams.get('code');
29
+ if (!code)
30
+ throw new Error('No `code` parameter in the pasted URL.');
31
+ return code;
32
+ }
33
+ //# sourceMappingURL=manual-login.js.map
@@ -0,0 +1,4 @@
1
+ export declare function getConfigDir(): string;
2
+ export declare function getProfilesPath(): string;
3
+ export declare function getCredentialsPath(): string;
4
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1,15 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ export function getConfigDir() {
4
+ const xdg = process.env.XDG_CONFIG_HOME;
5
+ if (xdg && xdg.length > 0)
6
+ return path.join(xdg, 'ccam');
7
+ return path.join(os.homedir(), '.config', 'ccam');
8
+ }
9
+ export function getProfilesPath() {
10
+ return path.join(getConfigDir(), 'profiles.yaml');
11
+ }
12
+ export function getCredentialsPath() {
13
+ return path.join(getConfigDir(), 'credentials');
14
+ }
15
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1,26 @@
1
+ export interface ResolveFlags {
2
+ profile?: string;
3
+ host?: string;
4
+ clientId?: string;
5
+ clientSecret?: string;
6
+ user?: string;
7
+ userPassword?: string;
8
+ }
9
+ export interface ResolvedProfile {
10
+ profileName?: string;
11
+ host: string;
12
+ clientId?: string;
13
+ clientSecret?: string;
14
+ user?: string;
15
+ userPassword?: string;
16
+ cachedToken?: {
17
+ accessToken: string;
18
+ expiresAt: number;
19
+ refreshToken?: string;
20
+ };
21
+ source: 'flags' | 'env' | 'profile' | 'defaults' | 'mixed';
22
+ }
23
+ export declare function resolveProfile(input: {
24
+ flags: ResolveFlags;
25
+ }): Promise<ResolvedProfile>;
26
+ //# sourceMappingURL=profile-resolver.d.ts.map
@@ -0,0 +1,42 @@
1
+ // packages/cli/src/auth/profile-resolver.ts
2
+ import { ProfileStore } from './profile-store.js';
3
+ const DEFAULT_HOST = 'https://account.demandware.com';
4
+ export async function resolveProfile(input) {
5
+ const store = new ProfileStore();
6
+ const state = await store.read();
7
+ const profileName = input.flags.profile ?? process.env.CCAM_PROFILE ?? state.activeProfile;
8
+ const profile = profileName ? state.profiles[profileName] : undefined;
9
+ const creds = profileName ? state.credentials[profileName] : undefined;
10
+ const pick = (flag, env, profileVal) => {
11
+ return flag ?? env ?? profileVal;
12
+ };
13
+ const host = pick(input.flags.host, process.env.CCAM_HOST, profile?.host) ?? DEFAULT_HOST;
14
+ const clientId = pick(input.flags.clientId, process.env.CCAM_CLIENT_ID, profile?.clientId);
15
+ const clientSecret = pick(input.flags.clientSecret, process.env.CCAM_CLIENT_SECRET, creds?.clientSecret);
16
+ const user = pick(input.flags.user, process.env.CCAM_USER, undefined);
17
+ const userPassword = pick(input.flags.userPassword, process.env.CCAM_USER_PASSWORD, creds?.userPassword);
18
+ const credentialOverride = (input.flags.host !== undefined ||
19
+ input.flags.clientId !== undefined ||
20
+ input.flags.clientSecret !== undefined ||
21
+ input.flags.user !== undefined ||
22
+ input.flags.userPassword !== undefined ||
23
+ process.env.CCAM_HOST !== undefined ||
24
+ process.env.CCAM_CLIENT_ID !== undefined ||
25
+ process.env.CCAM_CLIENT_SECRET !== undefined ||
26
+ process.env.CCAM_USER !== undefined ||
27
+ process.env.CCAM_USER_PASSWORD !== undefined);
28
+ const cachedToken = !credentialOverride && creds?.accessToken && creds.expiresAt
29
+ ? { accessToken: creds.accessToken, expiresAt: creds.expiresAt, refreshToken: creds.refreshToken }
30
+ : undefined;
31
+ let source;
32
+ if (input.flags.host || input.flags.clientId || input.flags.clientSecret || input.flags.user || input.flags.userPassword)
33
+ source = 'flags';
34
+ else if (process.env.CCAM_HOST || process.env.CCAM_CLIENT_ID || process.env.CCAM_CLIENT_SECRET || process.env.CCAM_USER || process.env.CCAM_USER_PASSWORD)
35
+ source = 'env';
36
+ else if (profile)
37
+ source = 'profile';
38
+ else
39
+ source = 'defaults';
40
+ return { profileName, host, clientId, clientSecret, user, userPassword, cachedToken, source };
41
+ }
42
+ //# sourceMappingURL=profile-resolver.js.map
@@ -0,0 +1,38 @@
1
+ export interface ProfileConfig {
2
+ host: string;
3
+ clientId: string;
4
+ userEmail?: string;
5
+ }
6
+ export interface ProfileCredentials {
7
+ refreshToken?: string;
8
+ clientSecret?: string;
9
+ userPassword?: string;
10
+ accessToken?: string;
11
+ expiresAt?: number;
12
+ }
13
+ export interface ProfileStoreState {
14
+ activeProfile?: string;
15
+ profiles: Record<string, ProfileConfig>;
16
+ credentials: Record<string, ProfileCredentials>;
17
+ }
18
+ export interface SaveProfileInput {
19
+ config: ProfileConfig;
20
+ credentials: ProfileCredentials;
21
+ }
22
+ export type ProfileState = 'ok' | 'missing-credentials' | 'missing-config';
23
+ export interface ProfileSummary {
24
+ name: string;
25
+ state: ProfileState;
26
+ config?: ProfileConfig;
27
+ }
28
+ export declare class ProfileStore {
29
+ read(): Promise<ProfileStoreState>;
30
+ saveProfile(name: string, input: SaveProfileInput): Promise<void>;
31
+ setActiveProfile(name: string): Promise<void>;
32
+ deleteProfile(name: string): Promise<void>;
33
+ renameProfile(oldName: string, newName: string): Promise<void>;
34
+ updateCredentials(name: string, patch: Partial<ProfileCredentials>): Promise<void>;
35
+ listProfiles(): Promise<ProfileSummary[]>;
36
+ private writeState;
37
+ }
38
+ //# sourceMappingURL=profile-store.d.ts.map
@@ -0,0 +1,125 @@
1
+ import fs from 'node:fs/promises';
2
+ import { randomBytes } from 'node:crypto';
3
+ import YAML from 'yaml';
4
+ import { getProfilesPath, getCredentialsPath, getConfigDir } from './paths.js';
5
+ export class ProfileStore {
6
+ async read() {
7
+ const profilesText = await readIfExists(getProfilesPath());
8
+ const credentialsText = await readIfExists(getCredentialsPath());
9
+ const profilesDoc = profilesText ? YAML.parse(profilesText) : {};
10
+ const credentialsDoc = credentialsText ? YAML.parse(credentialsText) : {};
11
+ return {
12
+ activeProfile: profilesDoc?.activeProfile,
13
+ profiles: profilesDoc?.profiles ?? {},
14
+ credentials: credentialsDoc ?? {},
15
+ };
16
+ }
17
+ async saveProfile(name, input) {
18
+ await ensureConfigDir();
19
+ const state = await this.read();
20
+ state.profiles[name] = input.config;
21
+ state.credentials[name] = input.credentials;
22
+ await this.writeState(state);
23
+ }
24
+ async setActiveProfile(name) {
25
+ const state = await this.read();
26
+ if (!state.profiles[name]) {
27
+ if (state.credentials[name]) {
28
+ throw new Error(`Profile '${name}' is incomplete: credentials present but config is missing. Run \`ccam auth login --profile ${name}\` to repair.`);
29
+ }
30
+ throw new Error(`Profile '${name}' not found`);
31
+ }
32
+ state.activeProfile = name;
33
+ await this.writeState(state);
34
+ }
35
+ async deleteProfile(name) {
36
+ const state = await this.read();
37
+ delete state.profiles[name];
38
+ delete state.credentials[name];
39
+ if (state.activeProfile === name) {
40
+ state.activeProfile = undefined;
41
+ }
42
+ await this.writeState(state);
43
+ }
44
+ async renameProfile(oldName, newName) {
45
+ const state = await this.read();
46
+ if (!state.profiles[oldName]) {
47
+ throw new Error(`Profile '${oldName}' not found`);
48
+ }
49
+ if (state.profiles[newName]) {
50
+ throw new Error(`Profile '${newName}' already exists`);
51
+ }
52
+ state.profiles[newName] = state.profiles[oldName];
53
+ delete state.profiles[oldName];
54
+ if (state.credentials[oldName]) {
55
+ state.credentials[newName] = state.credentials[oldName];
56
+ delete state.credentials[oldName];
57
+ }
58
+ if (state.activeProfile === oldName) {
59
+ state.activeProfile = newName;
60
+ }
61
+ await this.writeState(state);
62
+ }
63
+ async updateCredentials(name, patch) {
64
+ await ensureConfigDir();
65
+ const credentialsText = await readIfExists(getCredentialsPath());
66
+ const credentialsDoc = credentialsText
67
+ ? (YAML.parse(credentialsText) ?? {})
68
+ : {};
69
+ credentialsDoc[name] = { ...(credentialsDoc[name] ?? {}), ...patch };
70
+ await atomicWrite(getCredentialsPath(), YAML.stringify(credentialsDoc), 0o600);
71
+ }
72
+ async listProfiles() {
73
+ const state = await this.read();
74
+ const names = new Set([
75
+ ...Object.keys(state.profiles),
76
+ ...Object.keys(state.credentials),
77
+ ]);
78
+ return Array.from(names).sort().map((name) => {
79
+ const hasConfig = Boolean(state.profiles[name]);
80
+ const hasCreds = Boolean(state.credentials[name]);
81
+ let s;
82
+ if (hasConfig && hasCreds)
83
+ s = 'ok';
84
+ else if (!hasConfig)
85
+ s = 'missing-config';
86
+ else
87
+ s = 'missing-credentials';
88
+ return { name, state: s, config: state.profiles[name] };
89
+ });
90
+ }
91
+ async writeState(state) {
92
+ const profilesDoc = {
93
+ activeProfile: state.activeProfile,
94
+ profiles: state.profiles,
95
+ };
96
+ await atomicWrite(getProfilesPath(), YAML.stringify(profilesDoc), 0o644);
97
+ await atomicWrite(getCredentialsPath(), YAML.stringify(state.credentials), 0o600);
98
+ }
99
+ }
100
+ async function readIfExists(path) {
101
+ try {
102
+ return await fs.readFile(path, 'utf8');
103
+ }
104
+ catch (err) {
105
+ if (err.code === 'ENOENT')
106
+ return null;
107
+ throw err;
108
+ }
109
+ }
110
+ async function ensureConfigDir() {
111
+ const dir = getConfigDir();
112
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
113
+ // mkdir with `recursive: true` will not re-chmod an existing directory, so
114
+ // enforce 0o700 explicitly to close the loophole where a pre-existing dir
115
+ // (e.g. created by hand or by an older ccam version) has looser perms.
116
+ if (process.platform !== 'win32') {
117
+ await fs.chmod(dir, 0o700);
118
+ }
119
+ }
120
+ async function atomicWrite(target, content, mode) {
121
+ const tmp = `${target}.tmp-${randomBytes(4).toString('hex')}`;
122
+ await fs.writeFile(tmp, content, { mode });
123
+ await fs.rename(tmp, target);
124
+ }
125
+ //# sourceMappingURL=profile-store.js.map
@@ -0,0 +1,9 @@
1
+ export interface PromptOptions {
2
+ message: string;
3
+ defaultValue?: string;
4
+ input?: NodeJS.ReadableStream;
5
+ output?: NodeJS.WritableStream;
6
+ }
7
+ export declare function promptText(opts: PromptOptions): Promise<string>;
8
+ export declare function promptPassword(opts: PromptOptions): Promise<string>;
9
+ //# sourceMappingURL=prompt.d.ts.map