@discover-cloud/shared 1.0.11 → 1.2.1

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 (46) hide show
  1. package/dist/authorization/permission-cache.service.d.ts +14 -1
  2. package/dist/authorization/permission-cache.service.js +84 -16
  3. package/dist/authorization/permissions.d.ts +19 -15
  4. package/dist/authorization/permissions.js +24 -21
  5. package/dist/dtos/auth-service.dto.d.ts +13 -0
  6. package/dist/dtos/cloud-service.dto.d.ts +25 -10
  7. package/dist/dtos/insights-service.dto.d.ts +24 -8
  8. package/dist/dtos/response.dto.d.ts +80 -10
  9. package/dist/dtos/response.dto.js +23 -4
  10. package/dist/dtos/user-service.dto.d.ts +7 -0
  11. package/dist/enums/domain.enums.d.ts +45 -14
  12. package/dist/enums/domain.enums.js +40 -6
  13. package/dist/enums/permissions.enums.d.ts +12 -52
  14. package/dist/enums/permissions.enums.js +55 -59
  15. package/dist/errors/app-error.d.ts +17 -14
  16. package/dist/errors/app-error.js +19 -15
  17. package/dist/errors/http-errors.d.ts +12 -5
  18. package/dist/errors/http-errors.js +27 -5
  19. package/dist/http/service-client.d.ts +37 -12
  20. package/dist/http/service-client.js +43 -17
  21. package/dist/jwt/internal-jwt-verifier.d.ts +9 -3
  22. package/dist/jwt/internal-jwt-verifier.js +49 -26
  23. package/dist/middleware/authorize.middleware.d.ts +38 -6
  24. package/dist/middleware/authorize.middleware.js +62 -37
  25. package/dist/middleware/error-handler.middleware.d.ts +21 -7
  26. package/dist/middleware/error-handler.middleware.js +42 -14
  27. package/dist/middleware/request-id.middleware.d.ts +12 -10
  28. package/dist/middleware/request-id.middleware.js +15 -15
  29. package/dist/middleware/validate.middleware.d.ts +22 -13
  30. package/dist/middleware/validate.middleware.js +25 -16
  31. package/dist/middleware/validated-merge.middleware.js +1 -2
  32. package/dist/types/express.types.d.ts +37 -32
  33. package/dist/types/express.types.js +13 -11
  34. package/dist/utils/date.utils.d.ts +18 -4
  35. package/dist/utils/date.utils.js +18 -4
  36. package/dist/utils/env.d.ts +46 -0
  37. package/dist/utils/env.js +61 -0
  38. package/dist/utils/env.utils.d.ts +46 -0
  39. package/dist/utils/env.utils.js +61 -0
  40. package/dist/utils/index.d.ts +1 -0
  41. package/dist/utils/index.js +1 -0
  42. package/dist/utils/logger.utils.d.ts +31 -16
  43. package/dist/utils/logger.utils.js +55 -20
  44. package/dist/utils/response.utils.d.ts +47 -5
  45. package/dist/utils/response.utils.js +50 -7
  46. package/package.json +1 -1
@@ -1,11 +1,24 @@
1
1
  import { GlobalPermission, AccountRole } from "../enums";
2
2
  export declare class PermissionCacheService {
3
3
  private readonly client;
4
- private connected;
5
4
  constructor();
5
+ disconnect(): Promise<void>;
6
6
  resolve(accountId: string, accountRole: AccountRole): Promise<GlobalPermission[]>;
7
7
  invalidate(accountId: string): Promise<void>;
8
8
  invalidateMany(accountIds: string[]): Promise<void>;
9
+ /**
10
+ * isReady
11
+ * Authoritative liveness check using the client's own state machine.
12
+ *
13
+ * Replaces a manually tracked `connected` boolean, which could drift
14
+ * after reconnect cycles — the node-redis client reconnects automatically
15
+ * after a transient failure, but a boolean set only on the first "ready"
16
+ * event would not reflect subsequent disconnects and reconnects.
17
+ *
18
+ * `client.isReady` is kept current by the client's internal state machine
19
+ * and is the correct primitive for this guard.
20
+ */
21
+ private isReady;
9
22
  private key;
10
23
  private safeGet;
11
24
  private safeSet;
@@ -24,6 +24,20 @@ const permissions_1 = require("./permissions");
24
24
  * - Suspension → invalidate(accountId)
25
25
  * - Account deletion → invalidate(accountId)
26
26
  *
27
+ * Connection lifecycle:
28
+ * - isReady() is the authoritative liveness check — it reflects the actual
29
+ * client state (ready / connecting / reconnecting / closing) rather than a
30
+ * manually tracked boolean that can drift after automatic reconnects.
31
+ * - Call disconnect() in your SIGTERM handler to drain in-flight commands
32
+ * and release the TCP socket before the process exits.
33
+ *
34
+ * Fault tolerance:
35
+ * - All read/write paths guard via isReady() and fall back gracefully.
36
+ * - On a cold-cache miss during Redis unavailability, permissions are derived
37
+ * from the static role map (degraded but functional — no request is blocked).
38
+ * - safeGet validates the parsed cache value shape before returning it;
39
+ * malformed entries are treated as cache misses rather than runtime errors.
40
+ *
27
41
  * NOTE: Uses console.* for logging — shared cannot depend on a service-specific
28
42
  * logger (pino, winston, etc.). Each service's own logger will capture these
29
43
  * via stdout in production.
@@ -32,21 +46,39 @@ const CACHE_TTL_SECONDS = 24 * 60 * 60; // 24 hours
32
46
  const KEY_PREFIX = "perms";
33
47
  class PermissionCacheService {
34
48
  constructor() {
35
- this.connected = false;
36
49
  this.client = (0, redis_1.createClient)({
37
- url: process.env.REDIS_URL ?? "redis://localhost:6379",
50
+ url: process.env["REDIS_URL"] ?? "redis://localhost:6379",
38
51
  });
39
52
  this.client.on("error", (err) => {
40
53
  console.error("[PermissionCache] Redis client error", err);
41
54
  });
42
55
  this.client.on("ready", () => {
43
- this.connected = true;
44
- console.info("[PermissionCache] Redis connected");
56
+ console.info("[PermissionCache] Redis connected and ready");
45
57
  });
58
+ this.client.on("reconnecting", () => {
59
+ console.warn("[PermissionCache] Redis reconnecting...");
60
+ });
61
+ // Connect asynchronously — all read/write methods guard via isReady()
62
+ // and fall back gracefully, so startup is non-blocking.
46
63
  this.client.connect().catch((err) => {
47
- console.error("[PermissionCache] Redis connection failed", err);
64
+ console.error("[PermissionCache] Redis initial connection failed", err);
48
65
  });
49
66
  }
67
+ /* ----------------------------------------------------------------
68
+ disconnect
69
+ Gracefully closes the Redis connection. Call during service shutdown
70
+ (SIGTERM handler) to drain in-flight commands and release the TCP
71
+ socket before the process exits.
72
+ ---------------------------------------------------------------- */
73
+ async disconnect() {
74
+ try {
75
+ await this.client.quit();
76
+ console.info("[PermissionCache] Redis connection closed");
77
+ }
78
+ catch (err) {
79
+ console.error("[PermissionCache] Error during disconnect", err);
80
+ }
81
+ }
50
82
  /* ----------------------------------------------------------------
51
83
  resolve
52
84
  Returns permissions for an account, loading from cache or deriving
@@ -59,12 +91,12 @@ class PermissionCacheService {
59
91
  const cached = await this.safeGet(accountId);
60
92
  if (cached !== null)
61
93
  return cached;
62
- // Cache miss — derive from role map (no DB call)
63
- const perms = [
64
- ...(permissions_1.globalRolePermissions[accountRole] ?? []),
65
- ];
66
- // Write back (fire-and-forget — don't block the request)
67
- this.safeSet(accountId, perms).catch(() => { });
94
+ // Cache miss — derive from role map (no DB call needed; the role is
95
+ // already verified in the JWT payload by the time we get here).
96
+ const perms = [...(permissions_1.globalRolePermissions[accountRole] ?? [])];
97
+ // Write back fire-and-forget — safeSet swallows errors internally,
98
+ // so we don't await or attach a catch here.
99
+ void this.safeSet(accountId, perms);
68
100
  return perms;
69
101
  }
70
102
  /* ----------------------------------------------------------------
@@ -73,22 +105,32 @@ class PermissionCacheService {
73
105
  Call on: role change, suspension, account deletion.
74
106
  ---------------------------------------------------------------- */
75
107
  async invalidate(accountId) {
108
+ if (!this.isReady()) {
109
+ // Skip deletion — the stale entry will expire via TTL at worst.
110
+ console.warn(`[PermissionCache] Skipping invalidation for ${accountId} — Redis not ready`);
111
+ return;
112
+ }
76
113
  try {
77
114
  await this.client.del(this.key(accountId));
78
115
  console.info(`[PermissionCache] Invalidated: ${accountId}`);
79
116
  }
80
117
  catch (err) {
81
- // Log but don't throw — stale entry expires via TTL at worst
118
+ // Log but don't throw — stale entry expires via TTL at worst.
82
119
  console.error(`[PermissionCache] Failed to invalidate: ${accountId}`, err);
83
120
  }
84
121
  }
85
122
  /* ----------------------------------------------------------------
86
123
  invalidateMany
87
124
  Batch invalidation — use when a role definition changes globally.
125
+ Sends a single DEL with multiple keys (O(n) in Redis, one round-trip).
88
126
  ---------------------------------------------------------------- */
89
127
  async invalidateMany(accountIds) {
90
128
  if (accountIds.length === 0)
91
129
  return;
130
+ if (!this.isReady()) {
131
+ console.warn(`[PermissionCache] Skipping batch invalidation (${accountIds.length} entries) — Redis not ready`);
132
+ return;
133
+ }
92
134
  try {
93
135
  const keys = accountIds.map((id) => this.key(id));
94
136
  await this.client.del(keys);
@@ -101,28 +143,54 @@ class PermissionCacheService {
101
143
  /* ----------------------------------------------------------------
102
144
  Private helpers
103
145
  ---------------------------------------------------------------- */
146
+ /**
147
+ * isReady
148
+ * Authoritative liveness check using the client's own state machine.
149
+ *
150
+ * Replaces a manually tracked `connected` boolean, which could drift
151
+ * after reconnect cycles — the node-redis client reconnects automatically
152
+ * after a transient failure, but a boolean set only on the first "ready"
153
+ * event would not reflect subsequent disconnects and reconnects.
154
+ *
155
+ * `client.isReady` is kept current by the client's internal state machine
156
+ * and is the correct primitive for this guard.
157
+ */
158
+ isReady() {
159
+ return this.client.isReady;
160
+ }
104
161
  key(accountId) {
105
162
  return `${KEY_PREFIX}:${accountId}`;
106
163
  }
107
164
  async safeGet(accountId) {
108
- if (!this.connected)
165
+ if (!this.isReady())
109
166
  return null;
110
167
  try {
111
168
  const raw = await this.client.get(this.key(accountId));
112
169
  if (!raw)
113
170
  return null;
114
- return JSON.parse(raw);
171
+ const parsed = JSON.parse(raw);
172
+ // Validate shape before trusting the cached value. A malformed entry
173
+ // (e.g. from a prior schema change or a manual key mutation) should
174
+ // produce a cache miss, not a silent type error downstream.
175
+ if (!Array.isArray(parsed)) {
176
+ console.warn(`[PermissionCache] Unexpected cache shape for ${accountId}, treating as miss`);
177
+ return null;
178
+ }
179
+ return parsed;
115
180
  }
116
181
  catch (err) {
182
+ // JSON.parse failure or Redis error — bypass the cache gracefully.
117
183
  console.warn(`[PermissionCache] Cache get failed for ${accountId}, bypassing`, err);
118
184
  return null;
119
185
  }
120
186
  }
121
187
  async safeSet(accountId, perms) {
122
- if (!this.connected)
188
+ if (!this.isReady())
123
189
  return;
124
190
  try {
125
- await this.client.set(this.key(accountId), JSON.stringify(perms), { EX: CACHE_TTL_SECONDS });
191
+ await this.client.set(this.key(accountId), JSON.stringify(perms), {
192
+ EX: CACHE_TTL_SECONDS,
193
+ });
126
194
  }
127
195
  catch (err) {
128
196
  console.warn(`[PermissionCache] Cache set failed for ${accountId}`, err);
@@ -8,10 +8,11 @@ import { AccessContext } from "../types";
8
8
  * derives from this map on a cold cache miss.
9
9
  *
10
10
  * Design principles:
11
- * - SUPERADMIN gets all permissions (dynamically, not hardcoded list)
11
+ * - SUPERADMIN gets all permissions (dynamically via Object.values, not a
12
+ * hardcoded list — so new permissions are automatically granted on addition)
12
13
  * - Each role has only the minimum permissions it needs (least privilege)
13
14
  * - Permissions are additive — no role inherits from another at runtime
14
- * - Adding a new permission: add to the enum above, then grant it here
15
+ * - Adding a new permission: add to GlobalPermission enum, then grant it here
15
16
  *
16
17
  * ┌─────────────┬──────────────────────────────────────────────────────┐
17
18
  * │ Role │ Key permissions │
@@ -27,7 +28,7 @@ export declare const globalRolePermissions: Record<AccountRole, readonly GlobalP
27
28
  /**
28
29
  * ORG ROLE PERMISSION MAP (future work)
29
30
  * ────────────────────────────────────────
30
- * Org permissions are NOT in the JWT — they're fetched from the DB
31
+ * Org permissions are NOT embedded in the JWT — they're fetched from the DB
31
32
  * by tenant middleware within each service because:
32
33
  * 1. Org membership changes frequently (invites, role changes, removal)
33
34
  * 2. A user can be in multiple orgs with different roles
@@ -35,15 +36,20 @@ export declare const globalRolePermissions: Record<AccountRole, readonly GlobalP
35
36
  *
36
37
  * This map is used by tenant middleware for the DB-free fast path:
37
38
  * if the org role is already known (e.g. from a prior DB fetch cached in
38
- * Redis), derive permissions from here rather than re-querying the DB.
39
+ * Redis), derive permissions here rather than re-querying the DB.
40
+ *
41
+ * NOTE: All entries use OrgPermission values only — no cross-namespace mixing
42
+ * with GlobalPermission. Org admin membership management is expressed via
43
+ * the dedicated OrgPermission.* values below.
39
44
  */
40
45
  export declare const orgRolePermissions: Record<OrganizationRole, readonly OrgPermission[]>;
41
46
  /**
42
47
  * hasGlobalPermission
43
- * Checks if a GlobalPermission is in the AccessContext's perms array.
44
- * Perms come from the Redis permission cache, not from the JWT directly.
48
+ * Checks if a GlobalPermission is present in the AccessContext's perms array.
49
+ * Perms are populated from the Redis permission cache (derived from the role
50
+ * map on a miss) — never read directly from the JWT.
45
51
  *
46
- * This is the primary check for platform-level authorization.
52
+ * Machine contexts always return false they carry no user permissions.
47
53
  */
48
54
  export declare const hasGlobalPermission: (ctx: AccessContext, permission: GlobalPermission) => boolean;
49
55
  /**
@@ -51,18 +57,16 @@ export declare const hasGlobalPermission: (ctx: AccessContext, permission: Globa
51
57
  * Unified permission check for both GlobalPermission and OrgPermission.
52
58
  *
53
59
  * GlobalPermission → checked against ctx.perms (Redis cache, derived from role)
54
- * OrgPermission → returns false here; handled by tenant middleware in each
55
- * service via DB lookup. This function is intentionally not
56
- * the place for org permission checks.
57
- *
58
- * This makes the boundary explicit: callers that need org permission checks
59
- * should use their service's tenant middleware, not isAllowed().
60
+ * OrgPermission → intentionally returns false here; handled by tenant
61
+ * middleware in each service via DB lookup. This function
62
+ * is not the place for org permission checks — the boundary
63
+ * is explicit by design.
60
64
  */
61
65
  export declare const isAllowed: (ctx: AccessContext, permission: GlobalPermission | OrgPermission) => boolean;
62
66
  /**
63
67
  * isSuperAdmin
64
- * Convenience guard for the rare cases where superadmin-only logic
65
- * is needed beyond a single permission check.
68
+ * Convenience guard for cases where superadmin-only logic is needed
69
+ * beyond a single permission check (e.g. bypassing org scoping entirely).
66
70
  */
67
71
  export declare const isSuperAdmin: (ctx: AccessContext) => boolean;
68
72
  /**
@@ -10,10 +10,11 @@ const enums_1 = require("../enums");
10
10
  * derives from this map on a cold cache miss.
11
11
  *
12
12
  * Design principles:
13
- * - SUPERADMIN gets all permissions (dynamically, not hardcoded list)
13
+ * - SUPERADMIN gets all permissions (dynamically via Object.values, not a
14
+ * hardcoded list — so new permissions are automatically granted on addition)
14
15
  * - Each role has only the minimum permissions it needs (least privilege)
15
16
  * - Permissions are additive — no role inherits from another at runtime
16
- * - Adding a new permission: add to the enum above, then grant it here
17
+ * - Adding a new permission: add to GlobalPermission enum, then grant it here
17
18
  *
18
19
  * ┌─────────────┬──────────────────────────────────────────────────────┐
19
20
  * │ Role │ Key permissions │
@@ -44,25 +45,25 @@ exports.globalRolePermissions = {
44
45
  enums_1.GlobalPermission.SUPPORT_WRITE,
45
46
  ],
46
47
  [enums_1.AccountRole.SUPPORT]: [
47
- // Read-only access to user data and cost data for support purposes
48
- // Cannot write, cannot manage accounts, cannot access system internals
48
+ // Read-only access to user data and cost data for support purposes.
49
+ // Cannot write, cannot manage accounts, cannot access system internals.
49
50
  enums_1.GlobalPermission.VIEW_ANY_ACCOUNT,
50
51
  enums_1.GlobalPermission.VIEW_ANY_COST_DATA,
51
52
  enums_1.GlobalPermission.SUPPORT_VIEW,
52
53
  ],
53
54
  [enums_1.AccountRole.MODERATOR]: [
54
- // Only content moderation — no access to user accounts or cost data
55
+ // Content moderation only — no access to user accounts or cost data.
55
56
  enums_1.GlobalPermission.MODERATE_CONTENT,
56
57
  ],
57
- // Standard users have zero global permissions.
58
- // All their data access is handled at the service level
58
+ // Standard users carry zero global permissions.
59
+ // All their data access is gated at the service layer
59
60
  // (they can only see their own cost data, their own account, etc.)
60
61
  [enums_1.AccountRole.USER]: [],
61
62
  };
62
63
  /**
63
64
  * ORG ROLE PERMISSION MAP (future work)
64
65
  * ────────────────────────────────────────
65
- * Org permissions are NOT in the JWT — they're fetched from the DB
66
+ * Org permissions are NOT embedded in the JWT — they're fetched from the DB
66
67
  * by tenant middleware within each service because:
67
68
  * 1. Org membership changes frequently (invites, role changes, removal)
68
69
  * 2. A user can be in multiple orgs with different roles
@@ -70,12 +71,15 @@ exports.globalRolePermissions = {
70
71
  *
71
72
  * This map is used by tenant middleware for the DB-free fast path:
72
73
  * if the org role is already known (e.g. from a prior DB fetch cached in
73
- * Redis), derive permissions from here rather than re-querying the DB.
74
+ * Redis), derive permissions here rather than re-querying the DB.
75
+ *
76
+ * NOTE: All entries use OrgPermission values only — no cross-namespace mixing
77
+ * with GlobalPermission. Org admin membership management is expressed via
78
+ * the dedicated OrgPermission.* values below.
74
79
  */
75
80
  exports.orgRolePermissions = {
76
81
  [enums_1.OrganizationRole.OWNER]: Object.values(enums_1.OrgPermission), // Full org control
77
82
  [enums_1.OrganizationRole.ADMIN]: [
78
- enums_1.GlobalPermission.MANAGE_ACCOUNTS, // intentional re-use shape — org admin manages org members
79
83
  enums_1.OrgPermission.MANAGE_ORG_SETTINGS,
80
84
  enums_1.OrgPermission.INVITE_MEMBERS,
81
85
  enums_1.OrgPermission.REMOVE_MEMBERS,
@@ -114,10 +118,11 @@ exports.orgRolePermissions = {
114
118
  ==================================================================== */
115
119
  /**
116
120
  * hasGlobalPermission
117
- * Checks if a GlobalPermission is in the AccessContext's perms array.
118
- * Perms come from the Redis permission cache, not from the JWT directly.
121
+ * Checks if a GlobalPermission is present in the AccessContext's perms array.
122
+ * Perms are populated from the Redis permission cache (derived from the role
123
+ * map on a miss) — never read directly from the JWT.
119
124
  *
120
- * This is the primary check for platform-level authorization.
125
+ * Machine contexts always return false they carry no user permissions.
121
126
  */
122
127
  const hasGlobalPermission = (ctx, permission) => {
123
128
  if (ctx.kind !== "human")
@@ -130,12 +135,10 @@ exports.hasGlobalPermission = hasGlobalPermission;
130
135
  * Unified permission check for both GlobalPermission and OrgPermission.
131
136
  *
132
137
  * GlobalPermission → checked against ctx.perms (Redis cache, derived from role)
133
- * OrgPermission → returns false here; handled by tenant middleware in each
134
- * service via DB lookup. This function is intentionally not
135
- * the place for org permission checks.
136
- *
137
- * This makes the boundary explicit: callers that need org permission checks
138
- * should use their service's tenant middleware, not isAllowed().
138
+ * OrgPermission → intentionally returns false here; handled by tenant
139
+ * middleware in each service via DB lookup. This function
140
+ * is not the place for org permission checks — the boundary
141
+ * is explicit by design.
139
142
  */
140
143
  const isAllowed = (ctx, permission) => {
141
144
  if (ctx.kind !== "human")
@@ -150,8 +153,8 @@ const isAllowed = (ctx, permission) => {
150
153
  exports.isAllowed = isAllowed;
151
154
  /**
152
155
  * isSuperAdmin
153
- * Convenience guard for the rare cases where superadmin-only logic
154
- * is needed beyond a single permission check.
156
+ * Convenience guard for cases where superadmin-only logic is needed
157
+ * beyond a single permission check (e.g. bypassing org scoping entirely).
155
158
  */
156
159
  const isSuperAdmin = (ctx) => {
157
160
  if (ctx.kind !== "human")
@@ -1,4 +1,17 @@
1
1
  import { AccountStatus } from "../enums";
2
+ /**
3
+ * AUTH SERVICE DTOs (@discover-cloud/shared)
4
+ * ─────────────────────────────────────────────
5
+ * Internal data shapes produced by the auth service.
6
+ *
7
+ * SECURITY RULE: DTOs containing secrets (token, hash fields) must NEVER
8
+ * be returned directly in HTTP responses. They exist so internal service
9
+ * methods have typed return values — mappers are responsible for stripping
10
+ * secrets before anything reaches the wire.
11
+ *
12
+ * Secret fields are annotated with // SECRET — internal use only
13
+ * to make accidental exposure visible at a glance during code review.
14
+ */
2
15
  export interface AccountDto {
3
16
  id: string;
4
17
  email: string;
@@ -1,18 +1,27 @@
1
1
  import { CloudProvider, CloudAccountStatus, AwsAuthMethod, GcpAuthMethod, AzureAuthMethod, SyncStatus, SyncType } from "../enums";
2
2
  /**
3
- * CLOUD SERVICE DTOs
4
- * ───────────────────
5
- * Shapes returned by the cloud service over HTTP.
3
+ * CLOUD SERVICE DTOs (@discover-cloud/shared)
4
+ * ─────────────────────────────────────────────
5
+ * Shapes produced and consumed by the cloud service.
6
6
  *
7
- * Date fields are ISO 8601 strings at the HTTP boundary —
8
- * JSON has no Date type. Domain models use Date internally;
9
- * these DTOs are the serialized form sent to clients.
7
+ * Date handling:
8
+ * JSON has no Date type. Domain models use Date internally; these DTOs
9
+ * are the serialised form at the HTTP boundary all timestamps are ISO 8601 strings.
10
10
  *
11
- * CloudAccountDto never includes encryptedCredentials —
12
- * credentials are write-only.
11
+ * Credential handling:
12
+ * CloudAccountDto intentionally omits encryptedCredentials — credentials
13
+ * are write-only. The connect DTOs accept credentials as `unknown` because
14
+ * the shape differs per provider and per authMethod; Zod schemas in the
15
+ * cloud service validate the concrete shape at runtime.
13
16
  *
14
- * SyncJobDto is read-only — sync jobs are created internally
15
- * by the orchestrator and exposed for status polling only.
17
+ * SyncJobDto:
18
+ * Read-only. Sync jobs are created internally by the scheduler and exposed
19
+ * for status polling only — clients never POST a sync job directly.
20
+ *
21
+ * Provider push DTOs (CloudCostRecordDto, CloudResourceDto, CloudBudgetDto):
22
+ * Used when cloud-service pushes collected data to insights-service.
23
+ * They include cloudAccountId for scoping — insights-service uses this to
24
+ * associate records with the correct account without re-fetching.
16
25
  */
17
26
  export interface CloudAccountDto {
18
27
  id: string;
@@ -27,16 +36,19 @@ export interface CloudAccountDto {
27
36
  createdAt: string;
28
37
  updatedAt: string;
29
38
  }
39
+ /** Credential payload for a new AWS cloud account connection. */
30
40
  export interface ConnectAwsAccountDto {
31
41
  alias: string;
32
42
  authMethod: AwsAuthMethod;
33
43
  credentials: unknown;
34
44
  }
45
+ /** Credential payload for a new GCP cloud account connection. */
35
46
  export interface ConnectGcpAccountDto {
36
47
  alias: string;
37
48
  authMethod: GcpAuthMethod;
38
49
  credentials: unknown;
39
50
  }
51
+ /** Credential payload for a new Azure cloud account connection. */
40
52
  export interface ConnectAzureAccountDto {
41
53
  alias: string;
42
54
  authMethod: AzureAuthMethod;
@@ -57,6 +69,7 @@ export interface SyncJobDto {
57
69
  updatedAt: string;
58
70
  }
59
71
  export interface CloudCostRecordDto {
72
+ cloudAccountId: string;
60
73
  provider: CloudProvider;
61
74
  service: string;
62
75
  region: string;
@@ -67,6 +80,7 @@ export interface CloudCostRecordDto {
67
80
  usageType: string;
68
81
  }
69
82
  export interface CloudResourceDto {
83
+ cloudAccountId: string;
70
84
  provider: CloudProvider;
71
85
  resourceType: string;
72
86
  resourceId: string;
@@ -77,6 +91,7 @@ export interface CloudResourceDto {
77
91
  metadata: Record<string, unknown>;
78
92
  }
79
93
  export interface CloudBudgetDto {
94
+ cloudAccountId: string;
80
95
  name: string;
81
96
  limitAmount: number;
82
97
  currency: string;
@@ -1,11 +1,21 @@
1
1
  import { CloudProvider } from "../enums";
2
2
  /**
3
- * INSIGHTS SERVICE DTOs
4
- * ──────────────────────
5
- * Push payloads: cloud-service insights-service (internal, over HTTP)
6
- * Record DTOs: insights-service → frontend (public, over HTTP)
7
- * Query DTOs: internal query params (never cross HTTP, Date is fine)
8
- * Summary DTOs: assembled aggregations (no domain model, no mapper)
3
+ * INSIGHTS SERVICE DTOs (@discover-cloud/shared)
4
+ * ─────────────────────────────────────────────────
5
+ * Four groups of types, each with a distinct role:
6
+ *
7
+ * Push payloads — cloud-service insights-service (internal HTTP).
8
+ * Carry raw provider data collected during a sync job.
9
+ *
10
+ * Record DTOs — insights-service → frontend (public HTTP).
11
+ * Stored/enriched records returned to clients.
12
+ * All timestamps are ISO 8601 strings (JSON has no Date).
13
+ *
14
+ * Query DTO — internal query params passed between service layers.
15
+ * Never crosses an HTTP boundary — Date is fine here.
16
+ *
17
+ * Summary DTOs — assembled aggregations with no corresponding domain
18
+ * model or mapper. Built directly from DB query results.
9
19
  */
10
20
  export interface PushCostItem {
11
21
  service: string;
@@ -54,7 +64,7 @@ export interface CostRecordDto {
54
64
  id: string;
55
65
  cloudAccountId: string;
56
66
  userId: string;
57
- provider: string;
67
+ provider: CloudProvider;
58
68
  service: string;
59
69
  region: string;
60
70
  amount: number;
@@ -68,7 +78,7 @@ export interface ResourceRecordDto {
68
78
  id: string;
69
79
  cloudAccountId: string;
70
80
  userId: string;
71
- provider: string;
81
+ provider: CloudProvider;
72
82
  resourceType: string;
73
83
  resourceId: string;
74
84
  region: string;
@@ -93,6 +103,12 @@ export interface BudgetRecordDto {
93
103
  isForecastOver: boolean;
94
104
  recordedAt: string;
95
105
  }
106
+ /**
107
+ * CostQueryDto
108
+ * Used by the insights-service query layer internally.
109
+ * Date fields are fine here — this never reaches JSON serialisation.
110
+ * If this ever needs to cross HTTP, convert from/to to ISO 8601 strings.
111
+ */
96
112
  export interface CostQueryDto {
97
113
  userId?: string;
98
114
  cloudAccountId?: string;
@@ -1,3 +1,26 @@
1
+ /**
2
+ * RESPONSE DTOs (@discover-cloud/shared)
3
+ * ─────────────────────────────────────────
4
+ * Shared HTTP response shapes used across all services.
5
+ * Every response — success or error — is wrapped in the ApiSuccessResponse
6
+ * or ApiErrorResponse envelope so clients have a consistent shape to key on.
7
+ *
8
+ * Envelope shape:
9
+ * {
10
+ * success: true,
11
+ * data: T,
12
+ * meta: { requestId, timestamp }
13
+ * }
14
+ * or
15
+ * {
16
+ * success: false,
17
+ * error: { code, message, details? },
18
+ * meta: { requestId, timestamp }
19
+ * }
20
+ *
21
+ * The success() and failure() utils in shared/utils construct these
22
+ * envelopes — don't build them manually in controllers.
23
+ */
1
24
  export interface ApiMeta {
2
25
  requestId: string;
3
26
  timestamp: string;
@@ -17,25 +40,37 @@ export interface ApiErrorResponse {
17
40
  meta: ApiMeta;
18
41
  }
19
42
  /**
20
- * Returned on login / token refresh.
21
- * accessToken goes in the response body.
22
- * refreshToken goes in an HttpOnly cookie — not in the body.
23
- * Only include refreshToken here if your client explicitly needs it
24
- * (e.g. mobile clients that can't use cookies).
43
+ * TokensResponseDto
44
+ * Returned on successful login or token refresh.
45
+ *
46
+ * accessToken — short-lived JWT returned in the response body.
47
+ * refreshToken long-lived token sent as an HttpOnly cookie by the auth
48
+ * service; it is intentionally absent from this DTO.
49
+ * Mobile clients that cannot use cookies should request a
50
+ * separate endpoint that returns the refresh token explicitly.
25
51
  */
26
52
  export interface TokensResponseDto {
27
53
  accessToken: string;
28
54
  }
29
- /** Generic message — use for simple confirmations */
55
+ /**
56
+ * MessageResponseDto
57
+ * Use for simple confirmations where no structured data is needed.
58
+ * Example: { message: "Email verification sent" }
59
+ */
30
60
  export interface MessageResponseDto {
31
61
  message: string;
32
62
  }
33
63
  /**
34
- * Generic action confirmation — use when the client needs to know
35
- * which action was performed (e.g. in a polling or event-driven context).
64
+ * ActionResponseDto
65
+ * Use when the client needs a machine-readable signal of what happened,
66
+ * e.g. for polling loops, event-driven UIs, or audit trails.
36
67
  *
37
- * action: machine-readable string, e.g. "account.suspended", "session.revoked"
38
- * message: human-readable description
68
+ * action stable machine-readable identifier, e.g. "account.suspended"
69
+ * message human-readable description for display or logging
70
+ *
71
+ * Boolean-only DTOs (LoggedOutResponseDto, SuspendedResponseDto, etc.)
72
+ * are intentionally collapsed into this shape — a boolean that is always
73
+ * true on a 200 response carries no additional information.
39
74
  */
40
75
  export interface ActionResponseDto {
41
76
  action: string;
@@ -49,7 +84,42 @@ export interface PaginationMeta {
49
84
  hasNext: boolean;
50
85
  hasPrev: boolean;
51
86
  }
87
+ /**
88
+ * PaginatedResponseDto
89
+ * Wrap paginated list results in this before passing to success().
90
+ *
91
+ * Usage:
92
+ * success<PaginatedResponseDto<CloudAccountDto>>(res, req, {
93
+ * items: accounts,
94
+ * pagination: { page, pageSize, totalItems, totalPages, hasNext, hasPrev },
95
+ * });
96
+ */
52
97
  export interface PaginatedResponseDto<T> {
53
98
  items: T[];
54
99
  pagination: PaginationMeta;
55
100
  }
101
+ /**
102
+ * LoginResultDto
103
+ * Internal auth service response returned after successful login.
104
+ *
105
+ * The optional cookie metadata allows controllers or gateways
106
+ * to apply refresh-token cookies without coupling shared DTOs
107
+ * to a specific HTTP framework.
108
+ */
109
+ export interface CookieMetadataDto {
110
+ httpOnly?: boolean;
111
+ secure?: boolean;
112
+ sameSite?: "strict" | "lax" | "none";
113
+ maxAge?: number;
114
+ path?: string;
115
+ domain?: string;
116
+ }
117
+ export interface LoginResultCookieDto {
118
+ name: string;
119
+ value: string;
120
+ options: CookieMetadataDto;
121
+ }
122
+ export interface LoginResultDto {
123
+ tokens: TokensResponseDto;
124
+ cookie?: LoginResultCookieDto;
125
+ }