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.
Files changed (41) hide show
  1. package/CHANGELOG_v2.2.0.md +9 -1
  2. package/CHANGELOG_v2.2.1.md +19 -0
  3. package/CHANGELOG_v2.2.2.md +18 -0
  4. package/README.md +41 -15
  5. package/RELEASE_CHECKS_v2.2.2.md +26 -0
  6. package/TEST_RESULTS_v2.2.1.txt +11 -0
  7. package/TEST_RESULTS_v2.2.2.txt +39 -0
  8. package/UPGRADE_v2.2.1_to_v2.2.2.md +15 -0
  9. package/cloud-api/README.md +101 -93
  10. package/cloud-api/dist/index.js +62 -7
  11. package/cloud-api/node_modules/.package-lock.json +1 -1
  12. package/cloud-api/package-lock.json +2 -2
  13. package/cloud-api/package.json +1 -1
  14. package/cloud-api/sql/010_persistent_auth_sessions.sql +12 -0
  15. package/cloud-api/src/index.ts +71 -9
  16. package/dist/cli/help-content.d.ts.map +1 -1
  17. package/dist/cli/help-content.js +42 -21
  18. package/dist/cli/help-content.js.map +1 -1
  19. package/dist/modules/cloud/commands/cloud.command.d.ts +1 -1
  20. package/dist/modules/cloud/commands/cloud.command.d.ts.map +1 -1
  21. package/dist/modules/cloud/commands/cloud.command.js +8 -4
  22. package/dist/modules/cloud/commands/cloud.command.js.map +1 -1
  23. package/dist/modules/cloud/config/cloud-config.d.ts +1 -0
  24. package/dist/modules/cloud/config/cloud-config.d.ts.map +1 -1
  25. package/dist/modules/cloud/config/cloud-config.js +14 -13
  26. package/dist/modules/cloud/config/cloud-config.js.map +1 -1
  27. package/dist/modules/cloud/index.js +2 -2
  28. package/dist/modules/cloud/index.js.map +1 -1
  29. package/dist/modules/cloud/services/cloud-api.service.d.ts +6 -0
  30. package/dist/modules/cloud/services/cloud-api.service.d.ts.map +1 -1
  31. package/dist/modules/cloud/services/cloud-api.service.js +56 -5
  32. package/dist/modules/cloud/services/cloud-api.service.js.map +1 -1
  33. package/dist/modules/deploy/index.d.ts.map +1 -1
  34. package/dist/modules/deploy/index.js +44 -7
  35. package/dist/modules/deploy/index.js.map +1 -1
  36. package/dist/modules/scopes/commands/scope.command.d.ts.map +1 -1
  37. package/dist/modules/scopes/commands/scope.command.js +8 -4
  38. package/dist/modules/scopes/commands/scope.command.js.map +1 -1
  39. package/dist/modules/users/index.js +2 -2
  40. package/dist/modules/users/index.js.map +1 -1
  41. package/package.json +14 -5
@@ -26,7 +26,7 @@
26
26
 
27
27
  ## User/scope operations
28
28
 
29
- - Added `indraq user scopes [username]`.
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 dev
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 --env dev
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:dev
230
- # or
231
- indraq deploy --env staging
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 dev --select-scopes
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 dev
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` | Remove local Cloud token |
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:dev` | Dev deploy |
563
- | `indraq deploy:prod` | Prod deploy |
564
- | `indraq deploy --env <env>` | Named environment deploy using saved scope bindings |
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 dev
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 dev
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.0**
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
+ ```
@@ -1,93 +1,101 @@
1
- # IndraQ Cloud API
2
-
3
- Control plane for IndraQ CLI v2.1. The public CLI endpoint is `https://cli.indraq.com`.
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. v2.1 adds:
57
-
58
- ```text
59
- 008_provider_scopes.sql
60
- ```
61
-
62
- ## v2.1 authorization model
63
-
64
- - **Admin** — creates/deletes scopes, grants MANAGE/USE, creates managers/admins, changes organization roles.
65
- - **Manager** — can add/remove normal users only inside scopes where the manager has MANAGE.
66
- - **User** — can use only assigned scopes and rotate only their own managed provider credentials.
67
-
68
- Scope membership operations and secret resolution require an audit reason and are written to `audit_logs`.
69
-
70
- ## Provider scopes
71
-
72
- `provider_scopes` stores named organization accounts/controllers. `provider_scope_members` controls visibility and use. A user cannot discover a scope simply because it exists.
73
-
74
- Related v2.1 tables:
75
-
76
- - `provider_scopes`
77
- - `provider_scope_members`
78
- - `user_scope_defaults`
79
- - `project_environment_scopes`
80
- - `scope_external_accounts`
81
- - `scope_aws_credentials`
82
-
83
- ## Projects
84
-
85
- 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.
86
-
87
- ## Security notes
88
-
89
- - Passwords are bcrypt-hashed.
90
- - Provider secrets are encrypted with AES-256-GCM.
91
- - Secret-resolution responses are `Cache-Control: no-store` and audited.
92
- - Role/account state is re-read from PostgreSQL on protected requests so demotion/disable takes effect without waiting for JWT expiry.
93
- - Keep bootstrap credentials and `MASTER_KEY` out of source control.
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.
@@ -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
- // This makes admin/manager demotion and account disabling effective
53
- // immediately instead of trusting a stale role embedded in a 12-hour JWT.
54
- const current = await pool.query('SELECT id,email,name,role,is_active,must_change_password FROM users WHERE id=$1', [token.id]);
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 token = jwt.sign({ id: user.id, email: user.email, role: user.role }, jwtSecret, { expiresIn: '12h' });
471
- res.json({ token, user: { id: user.id, email: user.email, name: user.name, role: user.role, mustChangePassword: !!user.must_change_password } });
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@indraq/cloud-api",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@indraq/cloud-api",
3
- "version": "0.7.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.7.0",
9
+ "version": "0.8.0",
10
10
  "dependencies": {
11
11
  "@aws-sdk/client-iam": "^3.1123.0",
12
12
  "bcryptjs": "^3.0.2",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@indraq/cloud-api",
3
3
  "private": true,
4
- "version": "0.7.0",
4
+ "version": "0.8.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "tsx watch src/index.ts",