@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
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,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
|