indraq_cli 2.2.1 → 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.1.0.md +27 -0
- package/CHANGELOG_v2.2.0.md +60 -0
- package/CHANGELOG_v2.2.1.md +19 -0
- package/CHANGELOG_v2.2.2.md +18 -0
- package/README.md +27 -14
- package/RELEASE_CHECKS_v2.2.0.md +27 -0
- 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.0_to_v2.1.md +68 -0
- package/UPGRADE_v2.1_to_v2.2.md +62 -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 +27 -22
- 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/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/package.json +14 -5
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",
|
package/cloud-api/package.json
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS auth_sessions (
|
|
2
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3
|
+
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
4
|
+
refresh_token_hash text NOT NULL UNIQUE,
|
|
5
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
6
|
+
last_used_at timestamptz NOT NULL DEFAULT now(),
|
|
7
|
+
revoked_at timestamptz
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
CREATE INDEX IF NOT EXISTS auth_sessions_user_active_idx
|
|
11
|
+
ON auth_sessions(user_id)
|
|
12
|
+
WHERE revoked_at IS NULL;
|
package/cloud-api/src/index.ts
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';
|
|
@@ -23,7 +23,22 @@ if (!/^[a-fA-F0-9]{64}$/.test(masterHex)) throw new Error('MASTER_KEY must be ex
|
|
|
23
23
|
const masterKey = Buffer.from(masterHex, 'hex');
|
|
24
24
|
|
|
25
25
|
type Role = 'user' | 'manager' | 'admin';
|
|
26
|
-
type Auth = { id: string; email: string; name: string; role: Role; mustChangePassword: boolean };
|
|
26
|
+
type Auth = { id: string; email: string; name: string; role: Role; mustChangePassword: boolean; sessionId?: string };
|
|
27
|
+
type AccessClaims = { id?: string; sid?: string };
|
|
28
|
+
|
|
29
|
+
const accessTokenFor = (user: { id: string; email: string; role: Role }, sessionId: string): string =>
|
|
30
|
+
jwt.sign({ id: user.id, email: user.email, role: user.role, sid: sessionId }, jwtSecret, { expiresIn: '1h' });
|
|
31
|
+
|
|
32
|
+
const refreshHash = (token: string): string => createHash('sha256').update(token).digest('hex');
|
|
33
|
+
|
|
34
|
+
const createAuthSession = async (userId: string): Promise<{ sessionId: string; refreshToken: string }> => {
|
|
35
|
+
const refreshToken = randomBytes(48).toString('base64url');
|
|
36
|
+
const result = await pool.query(
|
|
37
|
+
'INSERT INTO auth_sessions(user_id,refresh_token_hash) VALUES($1,$2) RETURNING id',
|
|
38
|
+
[userId, refreshHash(refreshToken)]
|
|
39
|
+
);
|
|
40
|
+
return { sessionId: result.rows[0].id, refreshToken };
|
|
41
|
+
};
|
|
27
42
|
interface AuthedRequest extends Request { auth?: Auth }
|
|
28
43
|
|
|
29
44
|
const encrypt = (text: string): string => {
|
|
@@ -46,17 +61,26 @@ const auth = async (req: AuthedRequest, res: Response, next: NextFunction): Prom
|
|
|
46
61
|
const header = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
|
|
47
62
|
if (!header?.startsWith('Bearer ')) { res.status(401).json({ error: 'Authentication required' }); return; }
|
|
48
63
|
try {
|
|
49
|
-
const token = jwt.verify(header.slice(7), jwtSecret) as
|
|
64
|
+
const token = jwt.verify(header.slice(7), jwtSecret) as AccessClaims;
|
|
50
65
|
if (!token.id) { res.status(401).json({ error: 'Invalid or expired token' }); return; }
|
|
51
66
|
// Roles and account state are resolved from the database on every request.
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
67
|
+
// Session-backed tokens also verify server-side revocation on every request,
|
|
68
|
+
// so `indraq logout` takes effect immediately. Legacy tokens without sid are
|
|
69
|
+
// temporarily accepted so a v2.2.1 CLI session can upgrade without re-login.
|
|
70
|
+
const current = token.sid
|
|
71
|
+
? await pool.query(
|
|
72
|
+
`SELECT u.id,u.email,u.name,u.role,u.is_active,u.must_change_password
|
|
73
|
+
FROM users u
|
|
74
|
+
JOIN auth_sessions s ON s.id=$2 AND s.user_id=u.id AND s.revoked_at IS NULL
|
|
75
|
+
WHERE u.id=$1`,
|
|
76
|
+
[token.id, token.sid]
|
|
77
|
+
)
|
|
78
|
+
: await pool.query('SELECT id,email,name,role,is_active,must_change_password FROM users WHERE id=$1', [token.id]);
|
|
55
79
|
const user = current.rows[0];
|
|
56
80
|
if (!user || !user.is_active || !['user','manager','admin'].includes(user.role)) {
|
|
57
81
|
res.status(401).json({ error: 'Account is disabled or no longer available' }); return;
|
|
58
82
|
}
|
|
59
|
-
req.auth = { id: user.id, email: user.email, name: user.name, role: user.role as Role, mustChangePassword: !!user.must_change_password };
|
|
83
|
+
req.auth = { id: user.id, email: user.email, name: user.name, role: user.role as Role, mustChangePassword: !!user.must_change_password, ...(token.sid ? { sessionId: token.sid } : {}) };
|
|
60
84
|
if (user.must_change_password && !['/auth/me','/auth/first-login-password'].includes(req.path)) {
|
|
61
85
|
res.status(428).json({ error: 'Temporary password must be changed before using IndraQ. Run `indraq login` and complete the first-login password change.' }); return;
|
|
62
86
|
}
|
|
@@ -515,9 +539,44 @@ app.post('/auth/login', async (req, res) => {
|
|
|
515
539
|
res.status(401).json({ error: 'Invalid credentials' }); return;
|
|
516
540
|
}
|
|
517
541
|
await pool.query('UPDATE users SET last_login_at=now() WHERE id=$1', [user.id]);
|
|
518
|
-
const
|
|
519
|
-
|
|
542
|
+
const session = await createAuthSession(user.id);
|
|
543
|
+
const token = accessTokenFor({ id: user.id, email: user.email, role: user.role as Role }, session.sessionId);
|
|
544
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
545
|
+
res.json({ token, refreshToken: session.refreshToken, user: { id: user.id, email: user.email, name: user.name, role: user.role, mustChangePassword: !!user.must_change_password } });
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
app.post('/auth/session', auth, async (req: AuthedRequest, res) => {
|
|
549
|
+
if (req.auth!.sessionId) { res.status(409).json({ error: 'This access token already belongs to a persistent session.' }); return; }
|
|
550
|
+
const session = await createAuthSession(req.auth!.id);
|
|
551
|
+
const token = accessTokenFor({ id: req.auth!.id, email: req.auth!.email, role: req.auth!.role }, session.sessionId);
|
|
552
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
553
|
+
res.json({ token, refreshToken: session.refreshToken });
|
|
520
554
|
});
|
|
555
|
+
|
|
556
|
+
app.post('/auth/refresh', async (req, res) => {
|
|
557
|
+
const refreshToken = String(req.body?.refreshToken || '');
|
|
558
|
+
if (!refreshToken) { res.status(401).json({ error: 'Refresh token required' }); return; }
|
|
559
|
+
const result = await pool.query(
|
|
560
|
+
`SELECT s.id AS session_id,u.id,u.email,u.role,u.is_active
|
|
561
|
+
FROM auth_sessions s
|
|
562
|
+
JOIN users u ON u.id=s.user_id
|
|
563
|
+
WHERE s.refresh_token_hash=$1 AND s.revoked_at IS NULL`,
|
|
564
|
+
[refreshHash(refreshToken)]
|
|
565
|
+
);
|
|
566
|
+
const row = result.rows[0];
|
|
567
|
+
if (!row || !row.is_active) { res.status(401).json({ error: 'Session is no longer valid' }); return; }
|
|
568
|
+
await pool.query('UPDATE auth_sessions SET last_used_at=now() WHERE id=$1', [row.session_id]);
|
|
569
|
+
const token = accessTokenFor({ id: row.id, email: row.email, role: row.role as Role }, row.session_id);
|
|
570
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
571
|
+
res.json({ token, refreshToken });
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
app.post('/auth/logout', async (req, res) => {
|
|
575
|
+
const refreshToken = String(req.body?.refreshToken || '');
|
|
576
|
+
if (refreshToken) await pool.query('UPDATE auth_sessions SET revoked_at=now() WHERE refresh_token_hash=$1 AND revoked_at IS NULL', [refreshHash(refreshToken)]);
|
|
577
|
+
res.status(204).end();
|
|
578
|
+
});
|
|
579
|
+
|
|
521
580
|
app.get('/auth/me', auth, (req: AuthedRequest, res) => res.json(req.auth));
|
|
522
581
|
|
|
523
582
|
app.post('/auth/first-login-password', auth, async (req: AuthedRequest, res) => {
|
|
@@ -528,6 +587,7 @@ app.post('/auth/first-login-password', auth, async (req: AuthedRequest, res) =>
|
|
|
528
587
|
const failed = external.filter((item) => !item.ok);
|
|
529
588
|
if (failed.length) { res.status(502).json({ error: 'Could not rotate the temporary password on every managed provider. Retry or ask an admin.', results: external }); return; }
|
|
530
589
|
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]);
|
|
590
|
+
if (req.auth!.sessionId) 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]);
|
|
531
591
|
await audit(req.auth!.id, 'user.password.first-login', 'user', req.auth!.id, 'Required first-login password change', { providers: external.map((item) => item.provider) });
|
|
532
592
|
res.json({ ok: true, results: [{ provider: 'cloud', ok: true }, ...external] });
|
|
533
593
|
});
|
|
@@ -569,6 +629,7 @@ app.post('/auth/password-reset/verify', async (req, res) => {
|
|
|
569
629
|
if (targets.includes('npm') || targets.includes('jenkins')) results.push(...await resetManagedExternalPasswords(user.id, newPassword, targets));
|
|
570
630
|
if (targets.includes('cloud')) {
|
|
571
631
|
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]);
|
|
632
|
+
await pool.query('UPDATE auth_sessions SET revoked_at=now() WHERE user_id=$1 AND revoked_at IS NULL', [user.id]);
|
|
572
633
|
results.unshift({ provider: 'cloud', ok: true });
|
|
573
634
|
}
|
|
574
635
|
await pool.query('UPDATE password_reset_otps SET used_at=now() WHERE id=$1', [row.id]);
|
|
@@ -602,6 +663,7 @@ app.patch('/users/:id/password', ...allow('manager', 'admin'), async (req: Authe
|
|
|
602
663
|
const password = String(req.body.password || '');
|
|
603
664
|
if (password.length < 10) { res.status(400).json({ error: 'Password must be at least 10 characters.' }); return; }
|
|
604
665
|
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]);
|
|
666
|
+
await pool.query('UPDATE auth_sessions SET revoked_at=now() WHERE user_id=$1 AND revoked_at IS NULL', [id]);
|
|
605
667
|
await audit(req.auth!.id, 'user.password.update', 'user', id, reason);
|
|
606
668
|
res.status(204).end();
|
|
607
669
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"help-content.d.ts","sourceRoot":"","sources":["../../src/cli/help-content.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,WAAW,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,GAAG,QAAQ,CAAC;AAMpH,eAAO,MAAM,oBAAoB,QAAO,
|
|
1
|
+
{"version":3,"file":"help-content.d.ts","sourceRoot":"","sources":["../../src/cli/help-content.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,WAAW,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,KAAK,GAAG,QAAQ,CAAC;AAMpH,eAAO,MAAM,oBAAoB,QAAO,MAuGvC,CAAC;AAgJF,eAAO,MAAM,YAAY,GAAI,OAAO,SAAS,KAAG,MAS/C,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,QAAQ,MAAM,KAAG,SAAS,GAAG,SAa3D,CAAC"}
|
package/dist/cli/help-content.js
CHANGED
|
@@ -9,16 +9,18 @@ const heading = (value) => chalk_1.default.bold(value);
|
|
|
9
9
|
const command = (value) => chalk_1.default.cyan(value);
|
|
10
10
|
const note = (value) => chalk_1.default.gray(value);
|
|
11
11
|
const fullCommandReference = () => `
|
|
12
|
-
${heading('IndraQ CLI v2.2 — scope-first infrastructure and deployment')}
|
|
12
|
+
${heading('IndraQ CLI v2.2.2 — scope-first infrastructure and deployment')}
|
|
13
13
|
|
|
14
14
|
${heading('Start here')}
|
|
15
|
-
${command('indraq login')} Sign in
|
|
15
|
+
${command('indraq login')} Sign in once; the session refreshes automatically until logout
|
|
16
|
+
${command('indraq logout')} Revoke the persistent session and clear local session credentials
|
|
17
|
+
${note('Login persists across builds and time; access tokens refresh automatically until you explicitly logout or a security event revokes the session.')}
|
|
16
18
|
${command('indraq scope list')} Show only provider scopes assigned to you
|
|
17
19
|
${command('indraq scope describe [nameOrId]')} Identify a scope safely without revealing secrets
|
|
18
20
|
${command('indraq project status')} Review the current linked project
|
|
19
|
-
${command('indraq project scopes --env
|
|
20
|
-
${command('indraq prebuild
|
|
21
|
-
${command('indraq deploy
|
|
21
|
+
${command('indraq project scopes --env <environment>')} Choose project provider defaults for a shared environment
|
|
22
|
+
${command('indraq prebuild')} Choose a shared environment and reconcile infrastructure
|
|
23
|
+
${command('indraq deploy')} Choose an available shared environment and deploy
|
|
22
24
|
|
|
23
25
|
${heading('Scopes — AWS / NPM / Jenkins / GHCR')}
|
|
24
26
|
${command('indraq scope list')} List scopes visible to your account
|
|
@@ -58,13 +60,13 @@ ${heading('Projects and project access')}
|
|
|
58
60
|
${command('indraq access delete user')} Revoke selected project resources
|
|
59
61
|
|
|
60
62
|
${heading('Prebuild and deploy')}
|
|
61
|
-
${command('indraq prebuild
|
|
62
|
-
${command('indraq
|
|
63
|
-
${command('indraq deploy
|
|
64
|
-
${command('indraq deploy --env
|
|
65
|
-
${command('indraq deploy --env
|
|
66
|
-
${command('indraq deployment create --env
|
|
67
|
-
${command('indraq deployment run --env
|
|
63
|
+
${command('indraq prebuild')} Choose an available shared environment and reconcile infrastructure
|
|
64
|
+
${command('indraq prebuild --env <environment>')} Reconcile one named shared environment directly
|
|
65
|
+
${command('indraq deploy')} Choose an available shared environment and deploy
|
|
66
|
+
${command('indraq deploy --env <environment>')} Deploy one named shared environment directly
|
|
67
|
+
${command('indraq deploy --env <environment> --select-scopes')} Explicitly review/change saved scope bindings before deploy
|
|
68
|
+
${command('indraq deployment create --env <environment>')} Reconcile deployment infrastructure
|
|
69
|
+
${command('indraq deployment run --env <environment>')} Run deployment
|
|
68
70
|
|
|
69
71
|
${note('--env selects an IndraQ deployment/resource environment. It does NOT load .env, .env.dev, or .env.prod files.')}
|
|
70
72
|
|
|
@@ -176,27 +178,30 @@ ${command('indraq password reset')}
|
|
|
176
178
|
const deployHelp = () => `
|
|
177
179
|
${heading('Project scopes, prebuild and deployment')}
|
|
178
180
|
|
|
179
|
-
${command('indraq project scopes --env
|
|
180
|
-
|
|
181
|
-
|
|
181
|
+
${command('indraq project scopes --env <environment>')}
|
|
182
|
+
Shows the scopes visible to you, then lets you explicitly choose which scope is linked
|
|
183
|
+
to that shared environment. Jenkins and GHCR are required; AWS and NPM are optional.
|
|
182
184
|
|
|
183
|
-
${command('indraq prebuild
|
|
185
|
+
${command('indraq prebuild')}
|
|
184
186
|
Required: Jenkins + GHCR
|
|
185
187
|
Optional Route53: Skip | Create/reconcile | Link existing
|
|
186
188
|
Optional NPM: Skip | Create/reconcile | Link existing
|
|
187
189
|
|
|
188
|
-
${command('indraq deploy
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
${command('indraq deploy')}
|
|
191
|
+
Shows the available shared environments and lets you choose one. It then builds the
|
|
192
|
+
Docker image, pushes it to the GHCR scope linked to that exact environment, and runs
|
|
193
|
+
the matching Jenkins deployment. Existing project/environment scope bindings are reused.
|
|
192
194
|
|
|
193
|
-
${command('indraq deploy --env
|
|
195
|
+
${command('indraq deploy --env indraq-test-airtel')}
|
|
196
|
+
Skip the environment picker and deploy that exact shared environment.
|
|
197
|
+
|
|
198
|
+
${command('indraq deploy --env <environment> --select-scopes')}
|
|
194
199
|
Explicitly review or change the saved scope bindings before this deployment.
|
|
195
200
|
|
|
196
201
|
${note('--env is an IndraQ environment selector. Application environment files are part of your own Docker/build process and are not selected by this flag.')}
|
|
197
202
|
`;
|
|
198
203
|
const configureHelp = () => `
|
|
199
|
-
${heading('Configuration in v2.2')}
|
|
204
|
+
${heading('Configuration in v2.2.2')}
|
|
200
205
|
|
|
201
206
|
Organization provider credentials are configured once as named scopes by an ADMIN.
|
|
202
207
|
Managers and users do not create organization AWS/NPM/Jenkins/GHCR credentials.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"help-content.js","sourceRoot":"","sources":["../../src/cli/help-content.ts"],"names":[],"mappings":";;;;;;AAAA,kDAA0B;AAI1B,MAAM,OAAO,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,eAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC7D,MAAM,OAAO,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,eAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC7D,MAAM,IAAI,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,eAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAEnD,MAAM,oBAAoB,GAAG,GAAW,EAAE,CAAC;EAChD,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"help-content.js","sourceRoot":"","sources":["../../src/cli/help-content.ts"],"names":[],"mappings":"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"}
|