indraq_cli 2.2.0 → 2.2.2
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/CHANGELOG_v2.2.0.md +9 -1
- package/CHANGELOG_v2.2.1.md +19 -0
- package/CHANGELOG_v2.2.2.md +18 -0
- package/README.md +41 -15
- package/RELEASE_CHECKS_v2.2.2.md +26 -0
- package/TEST_RESULTS_v2.2.1.txt +11 -0
- package/TEST_RESULTS_v2.2.2.txt +39 -0
- package/UPGRADE_v2.2.1_to_v2.2.2.md +15 -0
- package/cloud-api/README.md +101 -93
- package/cloud-api/dist/index.js +62 -7
- package/cloud-api/node_modules/.package-lock.json +1 -1
- package/cloud-api/package-lock.json +2 -2
- package/cloud-api/package.json +1 -1
- package/cloud-api/sql/010_persistent_auth_sessions.sql +12 -0
- package/cloud-api/src/index.ts +71 -9
- package/dist/cli/help-content.d.ts.map +1 -1
- package/dist/cli/help-content.js +42 -21
- package/dist/cli/help-content.js.map +1 -1
- package/dist/modules/cloud/commands/cloud.command.d.ts +1 -1
- package/dist/modules/cloud/commands/cloud.command.d.ts.map +1 -1
- package/dist/modules/cloud/commands/cloud.command.js +8 -4
- package/dist/modules/cloud/commands/cloud.command.js.map +1 -1
- package/dist/modules/cloud/config/cloud-config.d.ts +1 -0
- package/dist/modules/cloud/config/cloud-config.d.ts.map +1 -1
- package/dist/modules/cloud/config/cloud-config.js +14 -13
- package/dist/modules/cloud/config/cloud-config.js.map +1 -1
- package/dist/modules/cloud/index.js +2 -2
- package/dist/modules/cloud/index.js.map +1 -1
- package/dist/modules/cloud/services/cloud-api.service.d.ts +6 -0
- package/dist/modules/cloud/services/cloud-api.service.d.ts.map +1 -1
- package/dist/modules/cloud/services/cloud-api.service.js +56 -5
- package/dist/modules/cloud/services/cloud-api.service.js.map +1 -1
- package/dist/modules/deploy/index.d.ts.map +1 -1
- package/dist/modules/deploy/index.js +44 -7
- package/dist/modules/deploy/index.js.map +1 -1
- package/dist/modules/scopes/commands/scope.command.d.ts.map +1 -1
- package/dist/modules/scopes/commands/scope.command.js +8 -4
- package/dist/modules/scopes/commands/scope.command.js.map +1 -1
- package/dist/modules/users/index.js +2 -2
- package/dist/modules/users/index.js.map +1 -1
- package/package.json +14 -5
package/CHANGELOG_v2.2.0.md
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
## User/scope operations
|
|
28
28
|
|
|
29
|
-
- Added `indraq user scopes [
|
|
29
|
+
- Added `indraq user scopes [email]`.
|
|
30
30
|
- NPM/Jenkins scope-native account records now retain the user's encrypted password for personal resolution and self-rotation.
|
|
31
31
|
- Existing Jenkins Role Strategy Item Role synchronization remains the project-pipeline authorization mechanism.
|
|
32
32
|
|
|
@@ -50,3 +50,11 @@ Added `cloud-api/sql/009_scope_user_secrets_and_project_lifecycle.sql`:
|
|
|
50
50
|
- `src/modules/scopes/commands/scope.command.ts`
|
|
51
51
|
- `src/modules/users/providers/aws-user.provider.ts`
|
|
52
52
|
- Runtime behavior is unchanged; this is a source/type compatibility correction.
|
|
53
|
+
|
|
54
|
+
## Documentation/help synchronization
|
|
55
|
+
|
|
56
|
+
- Focused `help scope` now lists personal AWS credential/key and NPM/Jenkins password rotation commands.
|
|
57
|
+
- Focused `help users` now documents `user list`, `user scopes [email]`, and self-service Cloud password reset.
|
|
58
|
+
- Focused `help deploy` now explains saved scope reuse and the explicit `--select-scopes` override.
|
|
59
|
+
- Removed stale v2.1 wording from compatibility command descriptions.
|
|
60
|
+
- README now explicitly states that provider-management secrets are never returned to normal `USE` members.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# IndraQ CLI v2.2.1
|
|
2
|
+
|
|
3
|
+
Bug-fix release for project scope selection and shared-environment deployment.
|
|
4
|
+
|
|
5
|
+
## Fixed
|
|
6
|
+
|
|
7
|
+
- Project scope linking now always displays the eligible scopes, even when only one scope is visible.
|
|
8
|
+
- The selected scope is shown with provider/name/company/endpoint/access before it is linked.
|
|
9
|
+
- The audit-reason prompt now comes after scope selection so the account being linked is clear.
|
|
10
|
+
- `indraq deploy` now lists the available shared environments and lets the user choose one interactively.
|
|
11
|
+
- `indraq deploy --env <environment>` validates and deploys the exact shared environment used by prebuild.
|
|
12
|
+
- `indraq deployment run` can also prompt for an environment when `--env` is omitted.
|
|
13
|
+
- `deploy:dev` and `deploy:prod` remain compatibility aliases. If no matching dev/prod environment exists, they open the shared-environment picker instead of trying to deploy a nonexistent environment.
|
|
14
|
+
- Help and README examples now prefer real shared-environment names rather than assuming every organization has `dev` and `prod` environments.
|
|
15
|
+
|
|
16
|
+
## Validation
|
|
17
|
+
|
|
18
|
+
- 16/16 configured regression suites pass, including the new v2.2.1 environment/scope-selection regression.
|
|
19
|
+
- Updated emitted JavaScript files pass `node --check`.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# IndraQ CLI v2.2.2
|
|
2
|
+
|
|
3
|
+
## Persistent Cloud sessions
|
|
4
|
+
|
|
5
|
+
- Replaced the single 12-hour login JWT with a persistent server-side CLI session.
|
|
6
|
+
- Access JWTs are now short-lived (1 hour) and refresh automatically in the background.
|
|
7
|
+
- A refresh/session token is stored alongside the access token in `~/.indraq/cloud.json`.
|
|
8
|
+
- The refresh session has no time-based expiry: normal users stay signed in until they explicitly run `indraq logout`, their account is removed/disabled, or their Cloud password is reset.
|
|
9
|
+
- `indraq logout` now revokes the server-side session as well as clearing local credentials.
|
|
10
|
+
- Every session-backed Cloud request verifies that the server-side session has not been revoked.
|
|
11
|
+
- Existing still-valid v2.2.1 JWTs are automatically upgraded to persistent sessions on the next Cloud request, avoiding an unnecessary login during upgrade when possible.
|
|
12
|
+
- Cloud password reset/admin password reset revokes existing sessions. First-login password change keeps the current session and revokes any other temporary-password sessions.
|
|
13
|
+
|
|
14
|
+
## Cloud API
|
|
15
|
+
|
|
16
|
+
- Cloud API version: `0.8.0`.
|
|
17
|
+
- Added migration `010_persistent_auth_sessions.sql`.
|
|
18
|
+
- Added `/auth/session`, `/auth/refresh`, and `/auth/logout` endpoints.
|
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ Teams often manage several servers, customers and cloud accounts at once. A sing
|
|
|
72
72
|
|
|
73
73
|
### Provider scope
|
|
74
74
|
|
|
75
|
-
One organization-owned provider account/server plus its encrypted management credential and safe metadata.
|
|
75
|
+
One organization-owned provider account/server plus its encrypted management credential and safe metadata. The management secret is used by the Cloud API for provisioning/administration and is **not returned to normal `USE` members**. AWS/NPM/Jenkins users resolve their own native identities instead.
|
|
76
76
|
|
|
77
77
|
### Scope membership
|
|
78
78
|
|
|
@@ -212,7 +212,7 @@ indraq project link
|
|
|
212
212
|
### 7. Choose project provider defaults
|
|
213
213
|
|
|
214
214
|
```bash
|
|
215
|
-
indraq project scopes --env
|
|
215
|
+
indraq project scopes --env indraq-test-airtel
|
|
216
216
|
```
|
|
217
217
|
|
|
218
218
|
A project may use a different scope than your personal default.
|
|
@@ -220,27 +220,33 @@ A project may use a different scope than your personal default.
|
|
|
220
220
|
### 8. Reconcile infrastructure
|
|
221
221
|
|
|
222
222
|
```bash
|
|
223
|
-
indraq prebuild
|
|
223
|
+
indraq prebuild
|
|
224
|
+
# choose one of the shared environments shown by the CLI
|
|
224
225
|
```
|
|
225
226
|
|
|
226
227
|
### 9. Deploy
|
|
227
228
|
|
|
228
229
|
```bash
|
|
229
|
-
indraq deploy
|
|
230
|
-
#
|
|
231
|
-
|
|
230
|
+
indraq deploy
|
|
231
|
+
# choose one of the shared environments shown by the CLI
|
|
232
|
+
# or deploy directly:
|
|
233
|
+
indraq deploy --env indraq-test-airtel
|
|
232
234
|
```
|
|
233
235
|
|
|
234
236
|
Normal deploys reuse the scopes already saved for that project/environment. Use `--select-scopes` only when you intentionally want to review or change those bindings:
|
|
235
237
|
|
|
236
238
|
```bash
|
|
237
|
-
indraq deploy --env
|
|
239
|
+
indraq deploy --env indraq-test-airtel --select-scopes
|
|
238
240
|
```
|
|
239
241
|
|
|
240
242
|
---
|
|
241
243
|
|
|
242
244
|
## Provider scopes
|
|
243
245
|
|
|
246
|
+
### Scope selection is always visible
|
|
247
|
+
|
|
248
|
+
When linking a project to a provider scope, the CLI prints the eligible scopes and asks you to choose one even when only one scope is available. This prevents silent account selection and lets you verify company/account/endpoint before linking.
|
|
249
|
+
|
|
244
250
|
### Unlimited named scopes
|
|
245
251
|
|
|
246
252
|
```text
|
|
@@ -348,7 +354,7 @@ flowchart LR
|
|
|
348
354
|
Use:
|
|
349
355
|
|
|
350
356
|
```bash
|
|
351
|
-
indraq project scopes --env
|
|
357
|
+
indraq project scopes --env indraq-test-airtel
|
|
352
358
|
indraq project scopes --env prod
|
|
353
359
|
```
|
|
354
360
|
|
|
@@ -497,12 +503,25 @@ Provider identities are removed first. Cloud identity is deleted last. Projects
|
|
|
497
503
|
|
|
498
504
|
## Command reference
|
|
499
505
|
|
|
506
|
+
### Built-in focused help
|
|
507
|
+
|
|
508
|
+
The CLI help is aligned with the v2.2 scope-first workflow:
|
|
509
|
+
|
|
510
|
+
```bash
|
|
511
|
+
indraq help scope
|
|
512
|
+
indraq help users
|
|
513
|
+
indraq help deploy
|
|
514
|
+
indraq help configure
|
|
515
|
+
indraq help aws
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
|
|
500
519
|
### Authentication and diagnostics
|
|
501
520
|
|
|
502
521
|
| Command | Use |
|
|
503
522
|
|---|---|
|
|
504
523
|
| `indraq login` | Sign in |
|
|
505
|
-
| `indraq logout` |
|
|
524
|
+
| `indraq logout` | Revoke the persistent Cloud session and remove local session credentials |
|
|
506
525
|
| `indraq whoami` | Show current identity |
|
|
507
526
|
| `indraq doctor` | Diagnose Node/Java/Git/tar/PATH |
|
|
508
527
|
| `indraq logs` | Audit/log access where permitted |
|
|
@@ -559,9 +578,10 @@ Provider identities are removed first. Cloud identity is deleted last. Projects
|
|
|
559
578
|
| Command | Use |
|
|
560
579
|
|---|---|
|
|
561
580
|
| `indraq prebuild --env <env>` | Reconcile infrastructure |
|
|
562
|
-
| `indraq deploy
|
|
563
|
-
| `indraq deploy
|
|
564
|
-
| `indraq deploy
|
|
581
|
+
| `indraq deploy` | Interactively choose an available shared environment and deploy |
|
|
582
|
+
| `indraq deploy --env <env>` | Deploy an exact shared environment using its saved scope bindings |
|
|
583
|
+
| `indraq deploy:dev` | Convenience alias if `dev`/`development` exists; otherwise opens the environment picker |
|
|
584
|
+
| `indraq deploy:prod` | Convenience alias if `prod`/`production` exists; otherwise opens the environment picker |
|
|
565
585
|
| `indraq deploy --env <env> --select-scopes` | Explicitly review/change project scope bindings before deploy |
|
|
566
586
|
| `indraq jenkins pipelines --stage dev` | List visible pipelines |
|
|
567
587
|
| `indraq jenkins run [job] --stage dev` | Run pipeline with discovered parameters |
|
|
@@ -583,6 +603,12 @@ indraq proxy create api.example.com
|
|
|
583
603
|
|
|
584
604
|
---
|
|
585
605
|
|
|
606
|
+
### Persistent login sessions
|
|
607
|
+
|
|
608
|
+
`indraq login` creates a persistent CLI session. The access token is refreshed automatically by the CLI, so normal users do **not** need to log in again every few hours or before each build/deployment. The session continues until `indraq logout` is run, the Cloud account is removed/disabled, or the Cloud password is reset. `indraq logout` revokes the server-side session and removes the local session credentials.
|
|
609
|
+
|
|
610
|
+
Existing v2.2.1 tokens are upgraded automatically when still valid; if an old token has already expired, one final login is required after upgrading.
|
|
611
|
+
|
|
586
612
|
## Common workflows
|
|
587
613
|
|
|
588
614
|
### Admin adds a new customer server
|
|
@@ -608,7 +634,7 @@ The manager can choose only a scope where they already have MANAGE access and ca
|
|
|
608
634
|
```bash
|
|
609
635
|
indraq scope list
|
|
610
636
|
indraq scope describe "AWS Client ABC"
|
|
611
|
-
indraq project scopes --env
|
|
637
|
+
indraq project scopes --env indraq-test-airtel
|
|
612
638
|
indraq prebuild --env dev
|
|
613
639
|
indraq deploy:dev
|
|
614
640
|
```
|
|
@@ -664,7 +690,7 @@ indraq scope add-user
|
|
|
664
690
|
Those two are mandatory. Add the user to the required scope and link/select it:
|
|
665
691
|
|
|
666
692
|
```bash
|
|
667
|
-
indraq project scopes --env
|
|
693
|
+
indraq project scopes --env indraq-test-airtel
|
|
668
694
|
```
|
|
669
695
|
|
|
670
696
|
### DNS is missing but deployment should continue
|
|
@@ -713,7 +739,7 @@ npm --prefix cloud-api run build
|
|
|
713
739
|
<div align="center">
|
|
714
740
|
<img src="docs/assets/indraq-mark.png" alt="IndraQ mark" width="70" />
|
|
715
741
|
|
|
716
|
-
**IndraQ CLI v2.2.
|
|
742
|
+
**IndraQ CLI v2.2.2**
|
|
717
743
|
|
|
718
744
|
Scope-based infrastructure access. Durable projects. Safer deployments.
|
|
719
745
|
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Release checks — v2.2.2
|
|
2
|
+
|
|
3
|
+
## Persistent login/session behavior
|
|
4
|
+
|
|
5
|
+
- [x] Login stores an access token plus a persistent refresh/session token.
|
|
6
|
+
- [x] Expired access token receives HTTP 401, refreshes through `/auth/refresh`, and retries the original Cloud request automatically.
|
|
7
|
+
- [x] `indraq logout` revokes the server-side session when Cloud is reachable and always clears local session credentials.
|
|
8
|
+
- [x] Server checks session revocation on every request for session-backed access tokens.
|
|
9
|
+
- [x] Existing still-valid v2.2.1 JWTs can upgrade via `/auth/session` without a password prompt.
|
|
10
|
+
- [x] Cloud password reset/admin password reset revokes existing sessions.
|
|
11
|
+
- [x] First-login password change keeps the current session and revokes other temporary-password sessions.
|
|
12
|
+
- [x] Refresh tokens are stored hashed in PostgreSQL, never plaintext.
|
|
13
|
+
- [x] Migration `010_persistent_auth_sessions.sql` is included and applied by the existing migration runner.
|
|
14
|
+
|
|
15
|
+
## Regression validation
|
|
16
|
+
|
|
17
|
+
- [x] Existing v2.2.1 environment/scope-selection regression remains green.
|
|
18
|
+
- [x] New v2.2.2 persistent-session source regression passes.
|
|
19
|
+
- [x] New v2.2.2 runtime regression passes against compiled `dist` and verifies login → expiry/refresh/retry → logout → legacy-session upgrade.
|
|
20
|
+
- [x] Full configured regression chain passes (18 commands/suites).
|
|
21
|
+
- [x] Changed CLI session core (`cloud-config.ts` + `cloud-api.service.ts`) passes strict TypeScript checking with `exactOptionalPropertyTypes` enabled.
|
|
22
|
+
- [x] Emitted changed JavaScript and Cloud API `dist/index.js` pass `node --check`.
|
|
23
|
+
|
|
24
|
+
## Build-environment note
|
|
25
|
+
|
|
26
|
+
The sandbox npm registry install timed out and left external dependency directories incomplete, so a dependency-backed full root/cloud `npm run build` could not be repeated here. The changed session core was strict-type-checked independently, Cloud API source was syntax-transpiled with TypeScript, and runtime/regression tests use the rebuilt changed `dist`. Run the normal clean build on the release machine before deployment/publish.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
IndraQ CLI v2.2.1 validation
|
|
2
|
+
============================
|
|
3
|
+
|
|
4
|
+
Result: PASS
|
|
5
|
+
|
|
6
|
+
Configured regression suites: 16/16 PASS
|
|
7
|
+
New regression: v2.2.1 environment/scope-selection PASS
|
|
8
|
+
Updated emitted JavaScript syntax checks: PASS
|
|
9
|
+
|
|
10
|
+
Environment note:
|
|
11
|
+
A fully clean npm install could not complete inside the packaging sandbox because package-registry access timed out. The source package uses the same dependency versions as v2.2.0. Run `npm i && npm run build && npm test` on a normal machine with npm registry access before publishing.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
IndraQ CLI v2.2.2 test results
|
|
2
|
+
================================
|
|
3
|
+
|
|
4
|
+
PASS test:cloud-endpoint
|
|
5
|
+
PASS test:user-credentials
|
|
6
|
+
PASS test:infrastructure
|
|
7
|
+
PASS test:platform
|
|
8
|
+
PASS test:project-access
|
|
9
|
+
PASS test:project-create
|
|
10
|
+
PASS test:jenkins-crumb
|
|
11
|
+
PASS test:jenkins-port
|
|
12
|
+
PASS test:prebuild
|
|
13
|
+
PASS test:docker-context
|
|
14
|
+
PASS test:identity-security
|
|
15
|
+
PASS test:user-lifecycle-jenkins
|
|
16
|
+
PASS test:v2-lifecycle
|
|
17
|
+
PASS test:v21-scopes
|
|
18
|
+
PASS test:v22
|
|
19
|
+
PASS test:v221
|
|
20
|
+
PASS test:v222
|
|
21
|
+
PASS test:v222-runtime
|
|
22
|
+
|
|
23
|
+
Persistent-session runtime coverage:
|
|
24
|
+
- login stores access + refresh tokens
|
|
25
|
+
- expired access token -> /auth/refresh -> original request retry
|
|
26
|
+
- refreshed access token persisted locally
|
|
27
|
+
- logout calls /auth/logout and removes local credentials
|
|
28
|
+
- still-valid legacy v2.2.1 JWT upgrades through /auth/session without password prompt
|
|
29
|
+
|
|
30
|
+
Focused strict TypeScript check:
|
|
31
|
+
PASS src/modules/cloud/config/cloud-config.ts
|
|
32
|
+
PASS src/modules/cloud/services/cloud-api.service.ts
|
|
33
|
+
|
|
34
|
+
JavaScript syntax checks:
|
|
35
|
+
PASS changed CLI dist files
|
|
36
|
+
PASS cloud-api/dist/index.js
|
|
37
|
+
|
|
38
|
+
Environment limitation:
|
|
39
|
+
A clean npm dependency install could not complete in this sandbox because registry access timed out. See RELEASE_CHECKS_v2.2.2.md.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Upgrade v2.2.1 → v2.2.2
|
|
2
|
+
|
|
3
|
+
1. Deploy the updated `cloud-api` first.
|
|
4
|
+
2. Ensure startup migrations apply `cloud-api/sql/010_persistent_auth_sessions.sql`.
|
|
5
|
+
3. Install/link the v2.2.2 CLI.
|
|
6
|
+
4. Existing v2.2.1 logins that still have a valid 12-hour JWT are automatically converted into persistent sessions on the next Cloud request.
|
|
7
|
+
5. If the old JWT had already expired before the upgrade, log in one final time. After that the CLI automatically refreshes its access token until `indraq logout` is run.
|
|
8
|
+
|
|
9
|
+
Normal usage:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
indraq login
|
|
13
|
+
# ... use the CLI for days/weeks without logging in again ...
|
|
14
|
+
indraq logout
|
|
15
|
+
```
|
package/cloud-api/README.md
CHANGED
|
@@ -1,93 +1,101 @@
|
|
|
1
|
-
# IndraQ Cloud API
|
|
2
|
-
|
|
3
|
-
Control plane for IndraQ CLI v2.
|
|
4
|
-
|
|
5
|
-
## Responsibilities
|
|
6
|
-
|
|
7
|
-
- authentication and organization RBAC (`user`, `manager`, `admin`)
|
|
8
|
-
- provider scopes for AWS/NPM/Jenkins/GHCR
|
|
9
|
-
- scope USE/MANAGE membership and audited delegation
|
|
10
|
-
- encrypted provider scope secrets
|
|
11
|
-
- per-user scope defaults
|
|
12
|
-
- per-project/environment scope bindings
|
|
13
|
-
- scope-native AWS/NPM/Jenkins identity mappings
|
|
14
|
-
- self-service AWS key rotation and password lifecycle
|
|
15
|
-
- projects, environments and project resource grants
|
|
16
|
-
- audit logging
|
|
17
|
-
|
|
18
|
-
## Requirements
|
|
19
|
-
|
|
20
|
-
- Node.js 22+
|
|
21
|
-
- PostgreSQL
|
|
22
|
-
|
|
23
|
-
## Configure
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
npm install
|
|
27
|
-
cp .env.example .env
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Required values include:
|
|
31
|
-
|
|
32
|
-
```env
|
|
33
|
-
PORT=4010
|
|
34
|
-
DATABASE_URL=postgresql://indraq:change-me@127.0.0.1:5432/indraq_cloud
|
|
35
|
-
JWT_SECRET=replace-with-at-least-32-random-characters
|
|
36
|
-
MASTER_KEY=replace-with-exactly-64-hex-characters
|
|
37
|
-
BOOTSTRAP_ADMIN_EMAIL=admin@example.com
|
|
38
|
-
BOOTSTRAP_ADMIN_PASSWORD=use-a-strong-password
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
`MASTER_KEY` must remain stable because provider scope secrets use AES-256-GCM encryption.
|
|
42
|
-
|
|
43
|
-
## Build / run
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
npm run build
|
|
47
|
-
npm start
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
Development:
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
npm run dev
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Numbered migrations in `sql/` are applied automatically at startup.
|
|
57
|
-
|
|
58
|
-
```text
|
|
59
|
-
008_provider_scopes.sql
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
- **
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
- `
|
|
79
|
-
- `
|
|
80
|
-
- `
|
|
81
|
-
- `
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
Projects
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
-
|
|
92
|
-
|
|
93
|
-
-
|
|
1
|
+
# IndraQ Cloud API
|
|
2
|
+
|
|
3
|
+
Control plane for IndraQ CLI v2.2.2. The public CLI endpoint is `https://cli.indraq.com`.
|
|
4
|
+
|
|
5
|
+
## Responsibilities
|
|
6
|
+
|
|
7
|
+
- persistent CLI authentication sessions and organization RBAC (`user`, `manager`, `admin`)
|
|
8
|
+
- provider scopes for AWS/NPM/Jenkins/GHCR
|
|
9
|
+
- scope USE/MANAGE membership and audited delegation
|
|
10
|
+
- encrypted provider scope secrets
|
|
11
|
+
- per-user scope defaults
|
|
12
|
+
- per-project/environment scope bindings
|
|
13
|
+
- scope-native AWS/NPM/Jenkins identity mappings
|
|
14
|
+
- self-service AWS key rotation and password lifecycle
|
|
15
|
+
- projects, environments and project resource grants
|
|
16
|
+
- audit logging
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
20
|
+
- Node.js 22+
|
|
21
|
+
- PostgreSQL
|
|
22
|
+
|
|
23
|
+
## Configure
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install
|
|
27
|
+
cp .env.example .env
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Required values include:
|
|
31
|
+
|
|
32
|
+
```env
|
|
33
|
+
PORT=4010
|
|
34
|
+
DATABASE_URL=postgresql://indraq:change-me@127.0.0.1:5432/indraq_cloud
|
|
35
|
+
JWT_SECRET=replace-with-at-least-32-random-characters
|
|
36
|
+
MASTER_KEY=replace-with-exactly-64-hex-characters
|
|
37
|
+
BOOTSTRAP_ADMIN_EMAIL=admin@example.com
|
|
38
|
+
BOOTSTRAP_ADMIN_PASSWORD=use-a-strong-password
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`MASTER_KEY` must remain stable because provider scope secrets use AES-256-GCM encryption.
|
|
42
|
+
|
|
43
|
+
## Build / run
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm run build
|
|
47
|
+
npm start
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Development:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm run dev
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Numbered migrations in `sql/` are applied automatically at startup. Current scope/session migrations include:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
008_provider_scopes.sql
|
|
60
|
+
009_scope_user_secrets_and_project_lifecycle.sql
|
|
61
|
+
010_persistent_auth_sessions.sql
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Authorization model
|
|
65
|
+
|
|
66
|
+
- **Admin** — creates/deletes scopes, grants MANAGE/USE, creates managers/admins, changes organization roles.
|
|
67
|
+
- **Manager** — can add/remove normal users only inside scopes where the manager has MANAGE.
|
|
68
|
+
- **User** — can use only assigned scopes and rotate only their own managed provider credentials.
|
|
69
|
+
|
|
70
|
+
Scope membership operations and secret resolution require an audit reason and are written to `audit_logs`.
|
|
71
|
+
|
|
72
|
+
## Provider scopes
|
|
73
|
+
|
|
74
|
+
`provider_scopes` stores named organization accounts/controllers. `provider_scope_members` controls visibility and use. A user cannot discover a scope simply because it exists.
|
|
75
|
+
|
|
76
|
+
Related scope tables:
|
|
77
|
+
|
|
78
|
+
- `provider_scopes`
|
|
79
|
+
- `provider_scope_members`
|
|
80
|
+
- `user_scope_defaults`
|
|
81
|
+
- `project_environment_scopes`
|
|
82
|
+
- `scope_external_accounts`
|
|
83
|
+
- `scope_aws_credentials`
|
|
84
|
+
|
|
85
|
+
## Projects
|
|
86
|
+
|
|
87
|
+
Projects and infrastructure are durable. Deleting a user removes scope memberships/native identity mappings and the Cloud identity but retains shared project/environment/resource definitions. Ownership is reassigned before hard deletion where required.
|
|
88
|
+
|
|
89
|
+
## Persistent CLI sessions
|
|
90
|
+
|
|
91
|
+
A successful `/auth/login` creates a server-side `auth_sessions` row and returns an access JWT plus an opaque refresh token. The database stores only a SHA-256 hash of the refresh token. The CLI automatically calls `/auth/refresh` after access-token expiry and retries the original request, so normal users remain signed in until they explicitly logout or a security event revokes the session.
|
|
92
|
+
|
|
93
|
+
`/auth/logout` revokes the session. Session-backed access JWTs include a session ID and the auth middleware checks that the corresponding session has not been revoked on every protected request. Still-valid legacy v2.2.1 JWTs may use `/auth/session` once to upgrade to the persistent model without another password prompt.
|
|
94
|
+
|
|
95
|
+
## Security notes
|
|
96
|
+
|
|
97
|
+
- Passwords are bcrypt-hashed.
|
|
98
|
+
- Provider secrets are encrypted with AES-256-GCM.
|
|
99
|
+
- Secret-resolution responses are `Cache-Control: no-store` and audited.
|
|
100
|
+
- Role/account state is re-read from PostgreSQL on protected requests so demotion/disable takes effect without waiting for JWT expiry.
|
|
101
|
+
- Keep bootstrap credentials and `MASTER_KEY` out of source control.
|
package/cloud-api/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import jwt from 'jsonwebtoken';
|
|
|
5
5
|
import pg from 'pg';
|
|
6
6
|
import nodemailer from 'nodemailer';
|
|
7
7
|
import { IAMClient, CreateAccessKeyCommand, DeleteAccessKeyCommand, ListAccessKeysCommand } from '@aws-sdk/client-iam';
|
|
8
|
-
import { createCipheriv, createDecipheriv, randomBytes, randomInt } from 'node:crypto';
|
|
8
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes, randomInt } from 'node:crypto';
|
|
9
9
|
import { isIP } from 'node:net';
|
|
10
10
|
import { readdir, readFile } from 'node:fs/promises';
|
|
11
11
|
import { dirname, resolve } from 'node:path';
|
|
@@ -21,6 +21,13 @@ const masterHex = process.env.MASTER_KEY || '';
|
|
|
21
21
|
if (!/^[a-fA-F0-9]{64}$/.test(masterHex))
|
|
22
22
|
throw new Error('MASTER_KEY must be exactly 64 hex characters (32 bytes).');
|
|
23
23
|
const masterKey = Buffer.from(masterHex, 'hex');
|
|
24
|
+
const accessTokenFor = (user, sessionId) => jwt.sign({ id: user.id, email: user.email, role: user.role, sid: sessionId }, jwtSecret, { expiresIn: '1h' });
|
|
25
|
+
const refreshHash = (token) => createHash('sha256').update(token).digest('hex');
|
|
26
|
+
const createAuthSession = async (userId) => {
|
|
27
|
+
const refreshToken = randomBytes(48).toString('base64url');
|
|
28
|
+
const result = await pool.query('INSERT INTO auth_sessions(user_id,refresh_token_hash) VALUES($1,$2) RETURNING id', [userId, refreshHash(refreshToken)]);
|
|
29
|
+
return { sessionId: result.rows[0].id, refreshToken };
|
|
30
|
+
};
|
|
24
31
|
const encrypt = (text) => {
|
|
25
32
|
const iv = randomBytes(12);
|
|
26
33
|
const cipher = createCipheriv('aes-256-gcm', masterKey, iv);
|
|
@@ -49,15 +56,21 @@ const auth = async (req, res, next) => {
|
|
|
49
56
|
return;
|
|
50
57
|
}
|
|
51
58
|
// Roles and account state are resolved from the database on every request.
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
59
|
+
// Session-backed tokens also verify server-side revocation on every request,
|
|
60
|
+
// so `indraq logout` takes effect immediately. Legacy tokens without sid are
|
|
61
|
+
// temporarily accepted so a v2.2.1 CLI session can upgrade without re-login.
|
|
62
|
+
const current = token.sid
|
|
63
|
+
? await pool.query(`SELECT u.id,u.email,u.name,u.role,u.is_active,u.must_change_password
|
|
64
|
+
FROM users u
|
|
65
|
+
JOIN auth_sessions s ON s.id=$2 AND s.user_id=u.id AND s.revoked_at IS NULL
|
|
66
|
+
WHERE u.id=$1`, [token.id, token.sid])
|
|
67
|
+
: await pool.query('SELECT id,email,name,role,is_active,must_change_password FROM users WHERE id=$1', [token.id]);
|
|
55
68
|
const user = current.rows[0];
|
|
56
69
|
if (!user || !user.is_active || !['user', 'manager', 'admin'].includes(user.role)) {
|
|
57
70
|
res.status(401).json({ error: 'Account is disabled or no longer available' });
|
|
58
71
|
return;
|
|
59
72
|
}
|
|
60
|
-
req.auth = { id: user.id, email: user.email, name: user.name, role: user.role, mustChangePassword: !!user.must_change_password };
|
|
73
|
+
req.auth = { id: user.id, email: user.email, name: user.name, role: user.role, mustChangePassword: !!user.must_change_password, ...(token.sid ? { sessionId: token.sid } : {}) };
|
|
61
74
|
if (user.must_change_password && !['/auth/me', '/auth/first-login-password'].includes(req.path)) {
|
|
62
75
|
res.status(428).json({ error: 'Temporary password must be changed before using IndraQ. Run `indraq login` and complete the first-login password change.' });
|
|
63
76
|
return;
|
|
@@ -467,8 +480,46 @@ app.post('/auth/login', async (req, res) => {
|
|
|
467
480
|
return;
|
|
468
481
|
}
|
|
469
482
|
await pool.query('UPDATE users SET last_login_at=now() WHERE id=$1', [user.id]);
|
|
470
|
-
const
|
|
471
|
-
|
|
483
|
+
const session = await createAuthSession(user.id);
|
|
484
|
+
const token = accessTokenFor({ id: user.id, email: user.email, role: user.role }, session.sessionId);
|
|
485
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
486
|
+
res.json({ token, refreshToken: session.refreshToken, user: { id: user.id, email: user.email, name: user.name, role: user.role, mustChangePassword: !!user.must_change_password } });
|
|
487
|
+
});
|
|
488
|
+
app.post('/auth/session', auth, async (req, res) => {
|
|
489
|
+
if (req.auth.sessionId) {
|
|
490
|
+
res.status(409).json({ error: 'This access token already belongs to a persistent session.' });
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const session = await createAuthSession(req.auth.id);
|
|
494
|
+
const token = accessTokenFor({ id: req.auth.id, email: req.auth.email, role: req.auth.role }, session.sessionId);
|
|
495
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
496
|
+
res.json({ token, refreshToken: session.refreshToken });
|
|
497
|
+
});
|
|
498
|
+
app.post('/auth/refresh', async (req, res) => {
|
|
499
|
+
const refreshToken = String(req.body?.refreshToken || '');
|
|
500
|
+
if (!refreshToken) {
|
|
501
|
+
res.status(401).json({ error: 'Refresh token required' });
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const result = await pool.query(`SELECT s.id AS session_id,u.id,u.email,u.role,u.is_active
|
|
505
|
+
FROM auth_sessions s
|
|
506
|
+
JOIN users u ON u.id=s.user_id
|
|
507
|
+
WHERE s.refresh_token_hash=$1 AND s.revoked_at IS NULL`, [refreshHash(refreshToken)]);
|
|
508
|
+
const row = result.rows[0];
|
|
509
|
+
if (!row || !row.is_active) {
|
|
510
|
+
res.status(401).json({ error: 'Session is no longer valid' });
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
await pool.query('UPDATE auth_sessions SET last_used_at=now() WHERE id=$1', [row.session_id]);
|
|
514
|
+
const token = accessTokenFor({ id: row.id, email: row.email, role: row.role }, row.session_id);
|
|
515
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
516
|
+
res.json({ token, refreshToken });
|
|
517
|
+
});
|
|
518
|
+
app.post('/auth/logout', async (req, res) => {
|
|
519
|
+
const refreshToken = String(req.body?.refreshToken || '');
|
|
520
|
+
if (refreshToken)
|
|
521
|
+
await pool.query('UPDATE auth_sessions SET revoked_at=now() WHERE refresh_token_hash=$1 AND revoked_at IS NULL', [refreshHash(refreshToken)]);
|
|
522
|
+
res.status(204).end();
|
|
472
523
|
});
|
|
473
524
|
app.get('/auth/me', auth, (req, res) => res.json(req.auth));
|
|
474
525
|
app.post('/auth/first-login-password', auth, async (req, res) => {
|
|
@@ -488,6 +539,8 @@ app.post('/auth/first-login-password', auth, async (req, res) => {
|
|
|
488
539
|
return;
|
|
489
540
|
}
|
|
490
541
|
await pool.query('UPDATE users SET password_hash=$1,must_change_password=false,password_changed_at=now(),updated_at=now() WHERE id=$2', [await bcrypt.hash(newPassword, 12), req.auth.id]);
|
|
542
|
+
if (req.auth.sessionId)
|
|
543
|
+
await pool.query('UPDATE auth_sessions SET revoked_at=now() WHERE user_id=$1 AND id<>$2 AND revoked_at IS NULL', [req.auth.id, req.auth.sessionId]);
|
|
491
544
|
await audit(req.auth.id, 'user.password.first-login', 'user', req.auth.id, 'Required first-login password change', { providers: external.map((item) => item.provider) });
|
|
492
545
|
res.json({ ok: true, results: [{ provider: 'cloud', ok: true }, ...external] });
|
|
493
546
|
});
|
|
@@ -553,6 +606,7 @@ app.post('/auth/password-reset/verify', async (req, res) => {
|
|
|
553
606
|
results.push(...await resetManagedExternalPasswords(user.id, newPassword, targets));
|
|
554
607
|
if (targets.includes('cloud')) {
|
|
555
608
|
await pool.query('UPDATE users SET password_hash=$1,must_change_password=false,password_changed_at=now(),updated_at=now() WHERE id=$2', [await bcrypt.hash(newPassword, 12), user.id]);
|
|
609
|
+
await pool.query('UPDATE auth_sessions SET revoked_at=now() WHERE user_id=$1 AND revoked_at IS NULL', [user.id]);
|
|
556
610
|
results.unshift({ provider: 'cloud', ok: true });
|
|
557
611
|
}
|
|
558
612
|
await pool.query('UPDATE password_reset_otps SET used_at=now() WHERE id=$1', [row.id]);
|
|
@@ -606,6 +660,7 @@ app.patch('/users/:id/password', ...allow('manager', 'admin'), async (req, res)
|
|
|
606
660
|
return;
|
|
607
661
|
}
|
|
608
662
|
await pool.query('UPDATE users SET password_hash=$1,must_change_password=true,updated_at=now() WHERE id=$2', [await bcrypt.hash(password, 12), id]);
|
|
663
|
+
await pool.query('UPDATE auth_sessions SET revoked_at=now() WHERE user_id=$1 AND revoked_at IS NULL', [id]);
|
|
609
664
|
await audit(req.auth.id, 'user.password.update', 'user', id, reason);
|
|
610
665
|
res.status(204).end();
|
|
611
666
|
});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indraq/cloud-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@indraq/cloud-api",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.8.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@aws-sdk/client-iam": "^3.1123.0",
|
|
12
12
|
"bcryptjs": "^3.0.2",
|