@zincapp/znvault-cli 3.3.1 → 4.0.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 (221) hide show
  1. package/README.md +32 -28
  2. package/dist/commands/advisor.d.ts +14 -1
  3. package/dist/commands/advisor.d.ts.map +1 -1
  4. package/dist/commands/advisor.js +355 -331
  5. package/dist/commands/advisor.js.map +1 -1
  6. package/dist/commands/agent/index.d.ts +2 -1
  7. package/dist/commands/agent/index.d.ts.map +1 -1
  8. package/dist/commands/agent/index.js +23 -19
  9. package/dist/commands/agent/index.js.map +1 -1
  10. package/dist/commands/apikey/conditions.d.ts.map +1 -1
  11. package/dist/commands/apikey/conditions.js +4 -3
  12. package/dist/commands/apikey/conditions.js.map +1 -1
  13. package/dist/commands/apikey/create.d.ts.map +1 -1
  14. package/dist/commands/apikey/create.js +4 -2
  15. package/dist/commands/apikey/create.js.map +1 -1
  16. package/dist/commands/apikey/delete.d.ts.map +1 -1
  17. package/dist/commands/apikey/delete.js +4 -2
  18. package/dist/commands/apikey/delete.js.map +1 -1
  19. package/dist/commands/apikey/enable-disable.d.ts.map +1 -1
  20. package/dist/commands/apikey/enable-disable.js +7 -4
  21. package/dist/commands/apikey/enable-disable.js.map +1 -1
  22. package/dist/commands/apikey/helpers.d.ts +16 -0
  23. package/dist/commands/apikey/helpers.d.ts.map +1 -1
  24. package/dist/commands/apikey/helpers.js +9 -0
  25. package/dist/commands/apikey/helpers.js.map +1 -1
  26. package/dist/commands/apikey/index.d.ts +2 -1
  27. package/dist/commands/apikey/index.d.ts.map +1 -1
  28. package/dist/commands/apikey/index.js +23 -14
  29. package/dist/commands/apikey/index.js.map +1 -1
  30. package/dist/commands/apikey/list.d.ts.map +1 -1
  31. package/dist/commands/apikey/list.js +4 -3
  32. package/dist/commands/apikey/list.js.map +1 -1
  33. package/dist/commands/apikey/managed/bind.d.ts.map +1 -1
  34. package/dist/commands/apikey/managed/bind.js +4 -3
  35. package/dist/commands/apikey/managed/bind.js.map +1 -1
  36. package/dist/commands/apikey/managed/conditions.d.ts.map +1 -1
  37. package/dist/commands/apikey/managed/conditions.js +5 -4
  38. package/dist/commands/apikey/managed/conditions.js.map +1 -1
  39. package/dist/commands/apikey/managed/config.d.ts.map +1 -1
  40. package/dist/commands/apikey/managed/config.js +4 -3
  41. package/dist/commands/apikey/managed/config.js.map +1 -1
  42. package/dist/commands/apikey/managed/create.d.ts.map +1 -1
  43. package/dist/commands/apikey/managed/create.js +4 -1
  44. package/dist/commands/apikey/managed/create.js.map +1 -1
  45. package/dist/commands/apikey/managed/delete.d.ts.map +1 -1
  46. package/dist/commands/apikey/managed/delete.js +4 -2
  47. package/dist/commands/apikey/managed/delete.js.map +1 -1
  48. package/dist/commands/apikey/managed/get.d.ts.map +1 -1
  49. package/dist/commands/apikey/managed/get.js +4 -2
  50. package/dist/commands/apikey/managed/get.js.map +1 -1
  51. package/dist/commands/apikey/managed/list.d.ts.map +1 -1
  52. package/dist/commands/apikey/managed/list.js +4 -2
  53. package/dist/commands/apikey/managed/list.js.map +1 -1
  54. package/dist/commands/apikey/managed/permissions.d.ts.map +1 -1
  55. package/dist/commands/apikey/managed/permissions.js +5 -3
  56. package/dist/commands/apikey/managed/permissions.js.map +1 -1
  57. package/dist/commands/apikey/managed/rotate.d.ts.map +1 -1
  58. package/dist/commands/apikey/managed/rotate.js +4 -3
  59. package/dist/commands/apikey/managed/rotate.js.map +1 -1
  60. package/dist/commands/apikey/permissions.d.ts.map +1 -1
  61. package/dist/commands/apikey/permissions.js +5 -3
  62. package/dist/commands/apikey/permissions.js.map +1 -1
  63. package/dist/commands/apikey/policies.d.ts.map +1 -1
  64. package/dist/commands/apikey/policies.js +10 -7
  65. package/dist/commands/apikey/policies.js.map +1 -1
  66. package/dist/commands/apikey/rotate.d.ts.map +1 -1
  67. package/dist/commands/apikey/rotate.js +4 -3
  68. package/dist/commands/apikey/rotate.js.map +1 -1
  69. package/dist/commands/apikey/show.d.ts.map +1 -1
  70. package/dist/commands/apikey/show.js +5 -4
  71. package/dist/commands/apikey/show.js.map +1 -1
  72. package/dist/commands/audit.d.ts +2 -1
  73. package/dist/commands/audit.d.ts.map +1 -1
  74. package/dist/commands/audit.js +15 -3
  75. package/dist/commands/audit.js.map +1 -1
  76. package/dist/commands/cluster.d.ts.map +1 -1
  77. package/dist/commands/cluster.js +31 -14
  78. package/dist/commands/cluster.js.map +1 -1
  79. package/dist/commands/group.d.ts +2 -1
  80. package/dist/commands/group.d.ts.map +1 -1
  81. package/dist/commands/group.js +61 -39
  82. package/dist/commands/group.js.map +1 -1
  83. package/dist/commands/health.d.ts.map +1 -1
  84. package/dist/commands/health.js +6 -5
  85. package/dist/commands/health.js.map +1 -1
  86. package/dist/commands/host/index.d.ts +2 -1
  87. package/dist/commands/host/index.d.ts.map +1 -1
  88. package/dist/commands/host/index.js +22 -18
  89. package/dist/commands/host/index.js.map +1 -1
  90. package/dist/commands/kms/crud.d.ts +1 -1
  91. package/dist/commands/kms/crud.d.ts.map +1 -1
  92. package/dist/commands/kms/crud.js +6 -6
  93. package/dist/commands/kms/crud.js.map +1 -1
  94. package/dist/commands/kms/index.d.ts +2 -1
  95. package/dist/commands/kms/index.d.ts.map +1 -1
  96. package/dist/commands/kms/index.js +14 -6
  97. package/dist/commands/kms/index.js.map +1 -1
  98. package/dist/commands/kms/lifecycle.d.ts +1 -1
  99. package/dist/commands/kms/lifecycle.d.ts.map +1 -1
  100. package/dist/commands/kms/lifecycle.js +7 -7
  101. package/dist/commands/kms/lifecycle.js.map +1 -1
  102. package/dist/commands/kms/routing.d.ts +5 -0
  103. package/dist/commands/kms/routing.d.ts.map +1 -1
  104. package/dist/commands/kms/routing.js +41 -5
  105. package/dist/commands/kms/routing.js.map +1 -1
  106. package/dist/commands/policy/attachments.d.ts +9 -5
  107. package/dist/commands/policy/attachments.d.ts.map +1 -1
  108. package/dist/commands/policy/attachments.js +16 -13
  109. package/dist/commands/policy/attachments.js.map +1 -1
  110. package/dist/commands/policy/crud.d.ts +6 -5
  111. package/dist/commands/policy/crud.d.ts.map +1 -1
  112. package/dist/commands/policy/crud.js +16 -11
  113. package/dist/commands/policy/crud.js.map +1 -1
  114. package/dist/commands/policy/helpers.d.ts +16 -0
  115. package/dist/commands/policy/helpers.d.ts.map +1 -1
  116. package/dist/commands/policy/helpers.js +9 -0
  117. package/dist/commands/policy/helpers.js.map +1 -1
  118. package/dist/commands/policy/index.d.ts +2 -1
  119. package/dist/commands/policy/index.d.ts.map +1 -1
  120. package/dist/commands/policy/index.js +149 -101
  121. package/dist/commands/policy/index.js.map +1 -1
  122. package/dist/commands/policy/io.d.ts +3 -2
  123. package/dist/commands/policy/io.d.ts.map +1 -1
  124. package/dist/commands/policy/io.js +7 -5
  125. package/dist/commands/policy/io.js.map +1 -1
  126. package/dist/commands/policy/list.d.ts +6 -2
  127. package/dist/commands/policy/list.d.ts.map +1 -1
  128. package/dist/commands/policy/list.js +7 -6
  129. package/dist/commands/policy/list.js.map +1 -1
  130. package/dist/commands/quarantine.d.ts +2 -1
  131. package/dist/commands/quarantine.d.ts.map +1 -1
  132. package/dist/commands/quarantine.js +394 -339
  133. package/dist/commands/quarantine.js.map +1 -1
  134. package/dist/commands/role.d.ts +2 -1
  135. package/dist/commands/role.d.ts.map +1 -1
  136. package/dist/commands/role.js +92 -49
  137. package/dist/commands/role.js.map +1 -1
  138. package/dist/commands/ssh/index.d.ts +2 -1
  139. package/dist/commands/ssh/index.d.ts.map +1 -1
  140. package/dist/commands/ssh/index.js +18 -13
  141. package/dist/commands/ssh/index.js.map +1 -1
  142. package/dist/commands/sso/crud.d.ts +1 -1
  143. package/dist/commands/sso/crud.d.ts.map +1 -1
  144. package/dist/commands/sso/crud.js +16 -16
  145. package/dist/commands/sso/crud.js.map +1 -1
  146. package/dist/commands/sso/helpers.d.ts +7 -1
  147. package/dist/commands/sso/helpers.d.ts.map +1 -1
  148. package/dist/commands/sso/helpers.js +32 -1
  149. package/dist/commands/sso/helpers.js.map +1 -1
  150. package/dist/commands/sso/index.d.ts +2 -1
  151. package/dist/commands/sso/index.d.ts.map +1 -1
  152. package/dist/commands/sso/index.js +13 -5
  153. package/dist/commands/sso/index.js.map +1 -1
  154. package/dist/commands/superadmin/accounts.d.ts +10 -0
  155. package/dist/commands/superadmin/accounts.d.ts.map +1 -0
  156. package/dist/commands/{superadmin.js → superadmin/accounts.js} +16 -15
  157. package/dist/commands/superadmin/accounts.js.map +1 -0
  158. package/dist/commands/superadmin/index.d.ts +20 -0
  159. package/dist/commands/superadmin/index.d.ts.map +1 -0
  160. package/dist/commands/superadmin/index.js +60 -0
  161. package/dist/commands/superadmin/index.js.map +1 -0
  162. package/dist/commands/user.d.ts +2 -1
  163. package/dist/commands/user.d.ts.map +1 -1
  164. package/dist/commands/user.js +26 -13
  165. package/dist/commands/user.js.map +1 -1
  166. package/dist/index.js +18 -13
  167. package/dist/index.js.map +1 -1
  168. package/dist/lib/client/apikeys.d.ts +21 -13
  169. package/dist/lib/client/apikeys.d.ts.map +1 -1
  170. package/dist/lib/client/apikeys.js +30 -26
  171. package/dist/lib/client/apikeys.js.map +1 -1
  172. package/dist/lib/client/audit.d.ts +54 -1
  173. package/dist/lib/client/audit.d.ts.map +1 -1
  174. package/dist/lib/client/audit.js +83 -9
  175. package/dist/lib/client/audit.js.map +1 -1
  176. package/dist/lib/client/index.d.ts +37 -36
  177. package/dist/lib/client/index.d.ts.map +1 -1
  178. package/dist/lib/client/index.js +37 -36
  179. package/dist/lib/client/index.js.map +1 -1
  180. package/dist/lib/client/managed-keys.d.ts +10 -7
  181. package/dist/lib/client/managed-keys.d.ts.map +1 -1
  182. package/dist/lib/client/managed-keys.js +16 -16
  183. package/dist/lib/client/managed-keys.js.map +1 -1
  184. package/dist/lib/client/policies.d.ts +14 -10
  185. package/dist/lib/client/policies.d.ts.map +1 -1
  186. package/dist/lib/client/policies.js +22 -22
  187. package/dist/lib/client/policies.js.map +1 -1
  188. package/dist/lib/client/quarantine.d.ts +25 -9
  189. package/dist/lib/client/quarantine.d.ts.map +1 -1
  190. package/dist/lib/client/quarantine.js +44 -27
  191. package/dist/lib/client/quarantine.js.map +1 -1
  192. package/dist/lib/client/users.d.ts +30 -3
  193. package/dist/lib/client/users.d.ts.map +1 -1
  194. package/dist/lib/client/users.js +39 -15
  195. package/dist/lib/client/users.js.map +1 -1
  196. package/dist/lib/command-context.d.ts +64 -0
  197. package/dist/lib/command-context.d.ts.map +1 -0
  198. package/dist/lib/command-context.js +90 -0
  199. package/dist/lib/command-context.js.map +1 -0
  200. package/dist/lib/context-help.d.ts.map +1 -1
  201. package/dist/lib/context-help.js +5 -0
  202. package/dist/lib/context-help.js.map +1 -1
  203. package/dist/lib/db/health.d.ts.map +1 -1
  204. package/dist/lib/db/health.js +13 -4
  205. package/dist/lib/db/health.js.map +1 -1
  206. package/dist/lib/mode.d.ts +18 -4
  207. package/dist/lib/mode.d.ts.map +1 -1
  208. package/dist/lib/mode.js +13 -6
  209. package/dist/lib/mode.js.map +1 -1
  210. package/dist/tui/hooks/useApi.d.ts +14 -7
  211. package/dist/tui/hooks/useApi.d.ts.map +1 -1
  212. package/dist/tui/hooks/useApi.js.map +1 -1
  213. package/dist/tui/screens/Dashboard.d.ts.map +1 -1
  214. package/dist/tui/screens/Dashboard.js +2 -3
  215. package/dist/tui/screens/Dashboard.js.map +1 -1
  216. package/dist/types/index.d.ts +59 -12
  217. package/dist/types/index.d.ts.map +1 -1
  218. package/package.json +1 -1
  219. package/dist/commands/superadmin.d.ts +0 -3
  220. package/dist/commands/superadmin.d.ts.map +0 -1
  221. package/dist/commands/superadmin.js.map +0 -1
@@ -3,391 +3,446 @@ import { client } from '../lib/client.js';
3
3
  import { promptConfirm } from '../lib/prompts.js';
4
4
  import * as output from '../lib/output.js';
5
5
  import { formatTtl as formatDuration } from '../lib/format-helpers.js';
6
+ import { resolveContext, withRegisterContext, } from '../lib/command-context.js';
6
7
  function formatBackoffLevel(level) {
7
8
  const labels = ['', '1m', '5m', '30m', '4h', '24h'];
8
9
  return labels[level] ?? `Level ${level}`;
9
10
  }
10
- export function registerQuarantineCommands(program) {
11
- const quarantine = program
11
+ export function registerQuarantineCommands(parent, opts) {
12
+ const ctx = resolveContext(opts);
13
+ withRegisterContext(ctx, () => { registerQuarantineCommandsInner(parent, ctx); });
14
+ }
15
+ function registerQuarantineCommandsInner(parent, ctx) {
16
+ const asSuperadmin = ctx === 'superadmin';
17
+ // Helper: only register --tenant in superadmin context
18
+ const t = (cmd, desc, shortFlag = true) => asSuperadmin
19
+ ? cmd.option(shortFlag ? '-t, --tenant <tenant>' : '--tenant <tenant>', desc)
20
+ : cmd;
21
+ // Build the scope object used by every quarantine client call. In tenant
22
+ // mode this stays `undefined` (routes to `/v1/quarantine/*`). In
23
+ // superadmin mode it always routes to `/v1/superadmin/quarantine/*`,
24
+ // optionally narrowed by --tenant.
25
+ const scope = (tenant) => asSuperadmin ? { asSuperadmin: true, tenantId: tenant } : undefined;
26
+ const quarantine = parent
12
27
  .command('quarantine')
13
- .description('IP quarantine management commands');
28
+ .description(ctx === 'superadmin'
29
+ ? 'IP quarantine management (cross-tenant)'
30
+ : 'IP quarantine management commands');
14
31
  // List quarantined IPs
15
- quarantine
16
- .command('list')
17
- .description('List quarantined IPs')
18
- .option('-s, --status <status>', 'Filter by status (active, released, expired)')
19
- .option('-t, --tenant <tenant>', 'Filter by tenant ID')
20
- .option('--include-expired', 'Include expired quarantines')
21
- .option('--limit <number>', 'Number of entries to show', '50')
22
- .option('--json', 'Output as JSON')
23
- .action(async (options) => {
24
- const spinner = output.spinner('Fetching quarantined IPs...').start();
25
- try {
26
- const result = await client.listQuarantines({
27
- status: options.status,
28
- tenantId: options.tenant,
29
- includeExpired: options.includeExpired,
30
- limit: parseInt(options.limit, 10),
31
- });
32
- spinner.stop();
33
- if (options.json) {
34
- output.json(result);
35
- return;
32
+ {
33
+ const listCmd = quarantine
34
+ .command('list')
35
+ .description('List quarantined IPs')
36
+ .option('-s, --status <status>', 'Filter by status (active, released, expired)');
37
+ t(listCmd, 'Filter by tenant ID');
38
+ listCmd
39
+ .option('--include-expired', 'Include expired quarantines')
40
+ .option('--limit <number>', 'Number of entries to show', '50')
41
+ .option('--json', 'Output as JSON')
42
+ .action(async (options) => {
43
+ const spinner = output.spinner('Fetching quarantined IPs...').start();
44
+ try {
45
+ const result = await client.listQuarantines({
46
+ status: options.status,
47
+ tenantId: options.tenant,
48
+ includeExpired: options.includeExpired,
49
+ limit: parseInt(options.limit, 10),
50
+ asSuperadmin,
51
+ });
52
+ spinner.stop();
53
+ if (options.json) {
54
+ output.json(result);
55
+ return;
56
+ }
57
+ if (result.items.length === 0) {
58
+ output.info('No quarantined IPs found');
59
+ return;
60
+ }
61
+ output.table(['IP Address', 'Status', 'Failures', 'Level', 'Blocked Until', 'Tenant', 'Reason'], result.items.map(q => [
62
+ q.ipAddress,
63
+ output.formatStatus(q.status),
64
+ q.failureCount,
65
+ formatBackoffLevel(q.backoffLevel),
66
+ q.status === 'active' ? output.formatRelativeTime(q.blockedUntil) : '-',
67
+ q.tenantId ?? '-',
68
+ q.reason.substring(0, 30),
69
+ ]));
70
+ // The server returns {items, total} for quarantine list (legacy shape)
71
+ // rather than the standard {items, pagination:{total,…}} envelope used
72
+ // elsewhere. Accept either to stay robust against server-side cleanups.
73
+ const total = result.pagination?.total ??
74
+ result.total ??
75
+ result.items.length;
76
+ output.info(`Showing ${result.items.length} of ${total} quarantines`);
36
77
  }
37
- if (result.items.length === 0) {
38
- output.info('No quarantined IPs found');
39
- return;
78
+ catch (err) {
79
+ spinner.fail('Failed to list quarantines');
80
+ output.error(err instanceof Error ? err.message : String(err));
81
+ process.exit(1);
40
82
  }
41
- output.table(['IP Address', 'Status', 'Failures', 'Level', 'Blocked Until', 'Tenant', 'Reason'], result.items.map(q => [
42
- q.ipAddress,
43
- output.formatStatus(q.status),
44
- q.failureCount,
45
- formatBackoffLevel(q.backoffLevel),
46
- q.status === 'active' ? output.formatRelativeTime(q.blockedUntil) : '-',
47
- q.tenantId ?? '-',
48
- q.reason.substring(0, 30),
49
- ]));
50
- output.info(`Showing ${result.items.length} of ${result.pagination.total} quarantines`);
51
- }
52
- catch (err) {
53
- spinner.fail('Failed to list quarantines');
54
- output.error(err instanceof Error ? err.message : String(err));
55
- process.exit(1);
56
- }
57
- });
83
+ });
84
+ }
58
85
  // Get quarantine details
59
- quarantine
60
- .command('get <id>')
61
- .description('Get quarantine details by ID')
62
- .option('-t, --tenant <tenant>', 'Tenant ID (superadmin only, cross-tenant)')
63
- .option('--json', 'Output as JSON')
64
- .action(async (id, options) => {
65
- const spinner = output.spinner('Fetching quarantine details...').start();
66
- try {
67
- const q = await client.getQuarantine(id, options.tenant);
68
- spinner.stop();
69
- if (options.json) {
70
- output.json(q);
71
- return;
72
- }
73
- output.section('Quarantine Details');
74
- output.keyValue({
75
- 'ID': q.id,
76
- 'IP Address': q.ipAddress,
77
- 'Status': output.formatStatus(q.status),
78
- 'Tenant': q.tenantId ?? 'N/A (System)',
79
- 'Failure Count': q.failureCount,
80
- 'Backoff Level': `${q.backoffLevel} (${formatBackoffLevel(q.backoffLevel)})`,
81
- 'Blocked Until': q.status === 'active' ? output.formatDate(q.blockedUntil) : '-',
82
- 'Reason': q.reason,
83
- 'First Failure': output.formatDate(q.firstFailureAt),
84
- 'Last Failure': output.formatDate(q.lastFailureAt),
85
- });
86
- if (q.lastFailurePath) {
87
- output.section('Last Request');
86
+ {
87
+ const getCmd = quarantine
88
+ .command('get <id>')
89
+ .description('Get quarantine details by ID');
90
+ t(getCmd, 'Tenant ID (superadmin only, cross-tenant)');
91
+ getCmd
92
+ .option('--json', 'Output as JSON')
93
+ .action(async (id, options) => {
94
+ const spinner = output.spinner('Fetching quarantine details...').start();
95
+ try {
96
+ const q = await client.getQuarantine(id, scope(options.tenant));
97
+ spinner.stop();
98
+ if (options.json) {
99
+ output.json(q);
100
+ return;
101
+ }
102
+ output.section('Quarantine Details');
88
103
  output.keyValue({
89
- 'Path': q.lastFailurePath,
90
- 'Method': q.lastFailureMethod ?? '-',
91
- 'User Agent': q.userAgent?.substring(0, 50) ?? '-',
92
- 'API Key ID': q.apiKeyId ?? '-',
93
- 'Agent ID': q.agentId ?? '-',
104
+ 'ID': q.id,
105
+ 'IP Address': q.ipAddress,
106
+ 'Status': output.formatStatus(q.status),
107
+ 'Tenant': q.tenantId ?? 'N/A (System)',
108
+ 'Failure Count': q.failureCount,
109
+ 'Backoff Level': `${q.backoffLevel} (${formatBackoffLevel(q.backoffLevel)})`,
110
+ 'Blocked Until': q.status === 'active' ? output.formatDate(q.blockedUntil) : '-',
111
+ 'Reason': q.reason,
112
+ 'First Failure': output.formatDate(q.firstFailureAt),
113
+ 'Last Failure': output.formatDate(q.lastFailureAt),
94
114
  });
115
+ if (q.lastFailurePath) {
116
+ output.section('Last Request');
117
+ output.keyValue({
118
+ 'Path': q.lastFailurePath,
119
+ 'Method': q.lastFailureMethod ?? '-',
120
+ 'User Agent': q.userAgent?.substring(0, 50) ?? '-',
121
+ 'API Key ID': q.apiKeyId ?? '-',
122
+ 'Agent ID': q.agentId ?? '-',
123
+ });
124
+ }
125
+ if (q.releasedAt) {
126
+ output.section('Release Info');
127
+ output.keyValue({
128
+ 'Released At': output.formatDate(q.releasedAt),
129
+ 'Released By': q.releasedBy ?? '-',
130
+ 'Release Reason': q.releaseReason ?? '-',
131
+ });
132
+ }
133
+ console.log();
95
134
  }
96
- if (q.releasedAt) {
97
- output.section('Release Info');
98
- output.keyValue({
99
- 'Released At': output.formatDate(q.releasedAt),
100
- 'Released By': q.releasedBy ?? '-',
101
- 'Release Reason': q.releaseReason ?? '-',
102
- });
135
+ catch (err) {
136
+ spinner.fail('Failed to get quarantine');
137
+ output.error(err instanceof Error ? err.message : String(err));
138
+ process.exit(1);
103
139
  }
104
- console.log();
105
- }
106
- catch (err) {
107
- spinner.fail('Failed to get quarantine');
108
- output.error(err instanceof Error ? err.message : String(err));
109
- process.exit(1);
110
- }
111
- });
140
+ });
141
+ }
112
142
  // Release a quarantine by ID
113
- quarantine
114
- .command('release <id> <reason>')
115
- .description('Release a quarantine by ID')
116
- .option('-t, --tenant <tenant>', 'Tenant ID (superadmin only, cross-tenant)')
117
- .option('-y, --yes', 'Skip confirmation')
118
- .option('--json', 'Output as JSON')
119
- .action(async (id, reason, options) => {
120
- try {
121
- // Get quarantine info first
122
- const q = await client.getQuarantine(id, options.tenant);
123
- if (!options.yes) {
124
- const confirmed = await promptConfirm(`Are you sure you want to release quarantine for IP ${q.ipAddress}?`);
125
- if (!confirmed) {
126
- output.info('Cancelled');
127
- return;
143
+ {
144
+ const releaseCmd = quarantine
145
+ .command('release <id> <reason>')
146
+ .description('Release a quarantine by ID');
147
+ t(releaseCmd, 'Tenant ID (superadmin only, cross-tenant)');
148
+ releaseCmd
149
+ .option('-y, --yes', 'Skip confirmation')
150
+ .option('--json', 'Output as JSON')
151
+ .action(async (id, reason, options) => {
152
+ try {
153
+ // Get quarantine info first
154
+ const q = await client.getQuarantine(id, scope(options.tenant));
155
+ if (!options.yes) {
156
+ const confirmed = await promptConfirm(`Are you sure you want to release quarantine for IP ${q.ipAddress}?`);
157
+ if (!confirmed) {
158
+ output.info('Cancelled');
159
+ return;
160
+ }
161
+ }
162
+ const spinner = output.spinner('Releasing quarantine...').start();
163
+ try {
164
+ const result = await client.releaseQuarantine(id, reason, scope(options.tenant));
165
+ spinner.succeed('Quarantine released');
166
+ if (options.json) {
167
+ output.json(result);
168
+ return;
169
+ }
170
+ output.success(result.message);
128
171
  }
172
+ catch (err) {
173
+ spinner.fail('Failed to release quarantine');
174
+ throw err;
175
+ }
176
+ }
177
+ catch (err) {
178
+ output.error(err instanceof Error ? err.message : String(err));
179
+ process.exit(1);
129
180
  }
130
- const spinner = output.spinner('Releasing quarantine...').start();
181
+ });
182
+ }
183
+ // Release all quarantines for an IP
184
+ {
185
+ const releaseIpCmd = quarantine
186
+ .command('release-ip <ip> <reason>')
187
+ .description('Release all quarantines for an IP address');
188
+ t(releaseIpCmd, 'Filter by tenant ID');
189
+ releaseIpCmd
190
+ .option('-y, --yes', 'Skip confirmation')
191
+ .option('--json', 'Output as JSON')
192
+ .action(async (ip, reason, options) => {
193
+ try {
194
+ if (!options.yes) {
195
+ const confirmed = await promptConfirm(`Are you sure you want to release all quarantines for IP ${ip}?`);
196
+ if (!confirmed) {
197
+ output.info('Cancelled');
198
+ return;
199
+ }
200
+ }
201
+ const spinner = output.spinner('Releasing IP quarantines...').start();
202
+ try {
203
+ const result = await client.releaseQuarantineIp(ip, reason, scope(options.tenant));
204
+ spinner.succeed('Quarantine(s) released');
205
+ if (options.json) {
206
+ output.json(result);
207
+ return;
208
+ }
209
+ output.success(result.message);
210
+ if (result.releasedCount !== undefined) {
211
+ output.info(`Released ${result.releasedCount} quarantine(s)`);
212
+ }
213
+ }
214
+ catch (err) {
215
+ spinner.fail('Failed to release IP quarantines');
216
+ throw err;
217
+ }
218
+ }
219
+ catch (err) {
220
+ output.error(err instanceof Error ? err.message : String(err));
221
+ process.exit(1);
222
+ }
223
+ });
224
+ }
225
+ // Get failure history for an IP
226
+ {
227
+ const historyCmd = quarantine
228
+ .command('history <ip>')
229
+ .description('Get failure history for an IP address');
230
+ t(historyCmd, 'Filter by tenant ID');
231
+ historyCmd
232
+ .option('--limit <number>', 'Number of entries to show', '100')
233
+ .option('--json', 'Output as JSON')
234
+ .action(async (ip, options) => {
235
+ const spinner = output.spinner('Fetching failure history...').start();
131
236
  try {
132
- const result = await client.releaseQuarantine(id, reason, options.tenant);
133
- spinner.succeed('Quarantine released');
237
+ const result = await client.getQuarantineHistory(ip, {
238
+ limit: parseInt(options.limit, 10),
239
+ tenantId: options.tenant,
240
+ asSuperadmin,
241
+ });
242
+ spinner.stop();
134
243
  if (options.json) {
135
244
  output.json(result);
136
245
  return;
137
246
  }
138
- output.success(result.message);
247
+ if (result.failures.length === 0) {
248
+ output.info(`No failure history found for IP ${ip}`);
249
+ return;
250
+ }
251
+ output.section(`Failure History for ${ip}`);
252
+ output.table(['Time', 'Type', 'Path', 'Method', 'Username', 'API Key', 'Quarantine'], result.failures.map(f => [
253
+ output.formatRelativeTime(f.ts),
254
+ f.failureType,
255
+ f.path?.substring(0, 25) ?? '-',
256
+ f.method ?? '-',
257
+ f.username ?? '-',
258
+ f.apiKeyPrefix ?? '-',
259
+ f.quarantineId ? 'Yes' : '-',
260
+ ]));
261
+ output.info(`Showing ${result.failures.length} entries`);
139
262
  }
140
263
  catch (err) {
141
- spinner.fail('Failed to release quarantine');
142
- throw err;
264
+ spinner.fail('Failed to get failure history');
265
+ output.error(err instanceof Error ? err.message : String(err));
266
+ process.exit(1);
143
267
  }
144
- }
145
- catch (err) {
146
- output.error(err instanceof Error ? err.message : String(err));
147
- process.exit(1);
148
- }
149
- });
150
- // Release all quarantines for an IP
151
- quarantine
152
- .command('release-ip <ip> <reason>')
153
- .description('Release all quarantines for an IP address')
154
- .option('-t, --tenant <tenant>', 'Filter by tenant ID')
155
- .option('-y, --yes', 'Skip confirmation')
156
- .option('--json', 'Output as JSON')
157
- .action(async (ip, reason, options) => {
158
- try {
159
- if (!options.yes) {
160
- const confirmed = await promptConfirm(`Are you sure you want to release all quarantines for IP ${ip}?`);
161
- if (!confirmed) {
162
- output.info('Cancelled');
268
+ });
269
+ }
270
+ // Get quarantine statistics
271
+ {
272
+ const statsCmd = quarantine
273
+ .command('stats')
274
+ .description('Get quarantine statistics');
275
+ t(statsCmd, 'Filter by tenant ID');
276
+ statsCmd
277
+ .option('--json', 'Output as JSON')
278
+ .action(async (options) => {
279
+ const spinner = output.spinner('Fetching statistics...').start();
280
+ try {
281
+ const stats = await client.getQuarantineStats(scope(options.tenant));
282
+ spinner.stop();
283
+ if (options.json) {
284
+ output.json(stats);
163
285
  return;
164
286
  }
287
+ output.section('Quarantine Statistics');
288
+ output.keyValue({
289
+ 'Active Quarantines': stats.activeQuarantines,
290
+ 'Quarantines (24h)': stats.totalQuarantines24h,
291
+ 'Unique IPs (24h)': stats.uniqueIpsQuarantined24h,
292
+ 'Failures (1h)': stats.failuresLast1h,
293
+ 'Failures (24h)': stats.failuresLast24h,
294
+ });
295
+ if (stats.topFailingIps.length > 0) {
296
+ output.section('Top Failing IPs (24h)');
297
+ output.table(['IP Address', 'Failures'], stats.topFailingIps.map(ip => [ip.ip, ip.count]));
298
+ }
299
+ console.log();
300
+ }
301
+ catch (err) {
302
+ spinner.fail('Failed to get statistics');
303
+ output.error(err instanceof Error ? err.message : String(err));
304
+ process.exit(1);
165
305
  }
166
- const spinner = output.spinner('Releasing IP quarantines...').start();
306
+ });
307
+ }
308
+ // Get quarantine configuration
309
+ //
310
+ // Semantics:
311
+ // - Tenant context (top-level `quarantine config`): operates on caller's
312
+ // own tenant (via JWT).
313
+ // - Superadmin context without --tenant: operates on system (null-tenant) config.
314
+ // - Superadmin context with --tenant X: operates on tenant X's config.
315
+ {
316
+ const configCmd = quarantine
317
+ .command('config')
318
+ .description('Get quarantine configuration');
319
+ t(configCmd, 'Tenant ID (omit for system config)');
320
+ configCmd
321
+ .option('--json', 'Output as JSON')
322
+ .action(async (options) => {
323
+ const spinner = output.spinner('Fetching configuration...').start();
167
324
  try {
168
- const result = await client.releaseQuarantineIp(ip, reason, options.tenant);
169
- spinner.succeed('Quarantine(s) released');
325
+ const config = await client.getQuarantineConfig(scope(options.tenant));
326
+ spinner.stop();
170
327
  if (options.json) {
171
- output.json(result);
328
+ output.json(config);
172
329
  return;
173
330
  }
174
- output.success(result.message);
175
- if (result.releasedCount !== undefined) {
176
- output.info(`Released ${result.releasedCount} quarantine(s)`);
331
+ output.section('Quarantine Configuration');
332
+ output.keyValue({
333
+ 'Enabled': config.enabled ? 'Yes' : 'No',
334
+ 'Failures Before Quarantine': config.failuresBeforeQuarantine,
335
+ 'Lockdown Threshold (IPs)': config.lockdownThresholdIps,
336
+ 'Lockdown Window': formatDuration(config.lockdownThresholdWindowSeconds),
337
+ 'Auto-Expire After': formatDuration(config.autoExpireAfterSeconds),
338
+ });
339
+ output.section('Backoff Durations');
340
+ output.keyValue({
341
+ 'Level 1': formatDuration(config.backoffLevel1Seconds),
342
+ 'Level 2': formatDuration(config.backoffLevel2Seconds),
343
+ 'Level 3': formatDuration(config.backoffLevel3Seconds),
344
+ 'Level 4': formatDuration(config.backoffLevel4Seconds),
345
+ 'Level 5': formatDuration(config.backoffLevel5Seconds),
346
+ });
347
+ if (config.ipAllowlist.length > 0) {
348
+ output.section('IP Allowlist');
349
+ config.ipAllowlist.forEach(ip => output.info(` ${ip}`));
177
350
  }
351
+ console.log();
178
352
  }
179
353
  catch (err) {
180
- spinner.fail('Failed to release IP quarantines');
181
- throw err;
354
+ spinner.fail('Failed to get configuration');
355
+ output.error(err instanceof Error ? err.message : String(err));
356
+ process.exit(1);
182
357
  }
183
- }
184
- catch (err) {
185
- output.error(err instanceof Error ? err.message : String(err));
186
- process.exit(1);
187
- }
188
- });
189
- // Get failure history for an IP
190
- quarantine
191
- .command('history <ip>')
192
- .description('Get failure history for an IP address')
193
- .option('-t, --tenant <tenant>', 'Filter by tenant ID')
194
- .option('--limit <number>', 'Number of entries to show', '100')
195
- .option('--json', 'Output as JSON')
196
- .action(async (ip, options) => {
197
- const spinner = output.spinner('Fetching failure history...').start();
198
- try {
199
- const result = await client.getQuarantineHistory(ip, {
200
- limit: parseInt(options.limit, 10),
201
- tenantId: options.tenant,
202
- });
203
- spinner.stop();
204
- if (options.json) {
205
- output.json(result);
206
- return;
358
+ });
359
+ }
360
+ // Set quarantine configuration
361
+ {
362
+ const setConfigCmd = quarantine
363
+ .command('set-config')
364
+ .description('Update quarantine configuration');
365
+ t(setConfigCmd, 'Tenant ID (omit for system config)');
366
+ setConfigCmd
367
+ .option('--enabled <bool>', 'Enable/disable quarantine')
368
+ .option('--failures <num>', 'Failures before quarantine')
369
+ .option('--lockdown-ips <num>', 'Number of IPs to trigger lockdown')
370
+ .option('--lockdown-window <sec>', 'Lockdown threshold window (seconds)')
371
+ .option('--level1 <sec>', 'Level 1 backoff (seconds)')
372
+ .option('--level2 <sec>', 'Level 2 backoff (seconds)')
373
+ .option('--level3 <sec>', 'Level 3 backoff (seconds)')
374
+ .option('--level4 <sec>', 'Level 4 backoff (seconds)')
375
+ .option('--level5 <sec>', 'Level 5 backoff (seconds)')
376
+ .option('--auto-expire <sec>', 'Auto-expire after (seconds)')
377
+ .option('-y, --yes', 'Skip confirmation')
378
+ .option('--json', 'Output as JSON')
379
+ .action(async (options) => {
380
+ const updates = {};
381
+ if (options.enabled !== undefined) {
382
+ updates.enabled = options.enabled === 'true';
207
383
  }
208
- if (result.failures.length === 0) {
209
- output.info(`No failure history found for IP ${ip}`);
210
- return;
384
+ if (options.failures !== undefined) {
385
+ updates.failuresBeforeQuarantine = parseInt(options.failures, 10);
211
386
  }
212
- output.section(`Failure History for ${ip}`);
213
- output.table(['Time', 'Type', 'Path', 'Method', 'Username', 'API Key', 'Quarantine'], result.failures.map(f => [
214
- output.formatRelativeTime(f.ts),
215
- f.failureType,
216
- f.path?.substring(0, 25) ?? '-',
217
- f.method ?? '-',
218
- f.username ?? '-',
219
- f.apiKeyPrefix ?? '-',
220
- f.quarantineId ? 'Yes' : '-',
221
- ]));
222
- output.info(`Showing ${result.failures.length} entries`);
223
- }
224
- catch (err) {
225
- spinner.fail('Failed to get failure history');
226
- output.error(err instanceof Error ? err.message : String(err));
227
- process.exit(1);
228
- }
229
- });
230
- // Get quarantine statistics
231
- quarantine
232
- .command('stats')
233
- .description('Get quarantine statistics')
234
- .option('-t, --tenant <tenant>', 'Filter by tenant ID')
235
- .option('--json', 'Output as JSON')
236
- .action(async (options) => {
237
- const spinner = output.spinner('Fetching statistics...').start();
238
- try {
239
- const stats = await client.getQuarantineStats(options.tenant);
240
- spinner.stop();
241
- if (options.json) {
242
- output.json(stats);
243
- return;
387
+ if (options.lockdownIps !== undefined) {
388
+ updates.lockdownThresholdIps = parseInt(options.lockdownIps, 10);
244
389
  }
245
- output.section('Quarantine Statistics');
246
- output.keyValue({
247
- 'Active Quarantines': stats.activeQuarantines,
248
- 'Quarantines (24h)': stats.totalQuarantines24h,
249
- 'Unique IPs (24h)': stats.uniqueIpsQuarantined24h,
250
- 'Failures (1h)': stats.failuresLast1h,
251
- 'Failures (24h)': stats.failuresLast24h,
252
- });
253
- if (stats.topFailingIps.length > 0) {
254
- output.section('Top Failing IPs (24h)');
255
- output.table(['IP Address', 'Failures'], stats.topFailingIps.map(ip => [ip.ip, ip.count]));
390
+ if (options.lockdownWindow !== undefined) {
391
+ updates.lockdownThresholdWindowSeconds = parseInt(options.lockdownWindow, 10);
256
392
  }
257
- console.log();
258
- }
259
- catch (err) {
260
- spinner.fail('Failed to get statistics');
261
- output.error(err instanceof Error ? err.message : String(err));
262
- process.exit(1);
263
- }
264
- });
265
- // Get quarantine configuration
266
- quarantine
267
- .command('config')
268
- .description('Get quarantine configuration')
269
- .option('-t, --tenant <tenant>', 'Tenant ID (superadmin only)')
270
- .option('--json', 'Output as JSON')
271
- .action(async (options) => {
272
- const spinner = output.spinner('Fetching configuration...').start();
273
- try {
274
- const config = await client.getQuarantineConfig(options.tenant);
275
- spinner.stop();
276
- if (options.json) {
277
- output.json(config);
278
- return;
393
+ if (options.level1 !== undefined) {
394
+ updates.backoffLevel1Seconds = parseInt(options.level1, 10);
279
395
  }
280
- output.section('Quarantine Configuration');
281
- output.keyValue({
282
- 'Enabled': config.enabled ? 'Yes' : 'No',
283
- 'Failures Before Quarantine': config.failuresBeforeQuarantine,
284
- 'Lockdown Threshold (IPs)': config.lockdownThresholdIps,
285
- 'Lockdown Window': formatDuration(config.lockdownThresholdWindowSeconds),
286
- 'Auto-Expire After': formatDuration(config.autoExpireAfterSeconds),
287
- });
288
- output.section('Backoff Durations');
289
- output.keyValue({
290
- 'Level 1': formatDuration(config.backoffLevel1Seconds),
291
- 'Level 2': formatDuration(config.backoffLevel2Seconds),
292
- 'Level 3': formatDuration(config.backoffLevel3Seconds),
293
- 'Level 4': formatDuration(config.backoffLevel4Seconds),
294
- 'Level 5': formatDuration(config.backoffLevel5Seconds),
295
- });
296
- if (config.ipAllowlist.length > 0) {
297
- output.section('IP Allowlist');
298
- config.ipAllowlist.forEach(ip => output.info(` ${ip}`));
396
+ if (options.level2 !== undefined) {
397
+ updates.backoffLevel2Seconds = parseInt(options.level2, 10);
299
398
  }
300
- console.log();
301
- }
302
- catch (err) {
303
- spinner.fail('Failed to get configuration');
304
- output.error(err instanceof Error ? err.message : String(err));
305
- process.exit(1);
306
- }
307
- });
308
- // Set quarantine configuration
309
- quarantine
310
- .command('set-config')
311
- .description('Update quarantine configuration')
312
- .option('-t, --tenant <tenant>', 'Tenant ID (superadmin only)')
313
- .option('--enabled <bool>', 'Enable/disable quarantine')
314
- .option('--failures <num>', 'Failures before quarantine')
315
- .option('--lockdown-ips <num>', 'Number of IPs to trigger lockdown')
316
- .option('--lockdown-window <sec>', 'Lockdown threshold window (seconds)')
317
- .option('--level1 <sec>', 'Level 1 backoff (seconds)')
318
- .option('--level2 <sec>', 'Level 2 backoff (seconds)')
319
- .option('--level3 <sec>', 'Level 3 backoff (seconds)')
320
- .option('--level4 <sec>', 'Level 4 backoff (seconds)')
321
- .option('--level5 <sec>', 'Level 5 backoff (seconds)')
322
- .option('--auto-expire <sec>', 'Auto-expire after (seconds)')
323
- .option('-y, --yes', 'Skip confirmation')
324
- .option('--json', 'Output as JSON')
325
- .action(async (options) => {
326
- const updates = {};
327
- if (options.enabled !== undefined) {
328
- updates.enabled = options.enabled === 'true';
329
- }
330
- if (options.failures !== undefined) {
331
- updates.failuresBeforeQuarantine = parseInt(options.failures, 10);
332
- }
333
- if (options.lockdownIps !== undefined) {
334
- updates.lockdownThresholdIps = parseInt(options.lockdownIps, 10);
335
- }
336
- if (options.lockdownWindow !== undefined) {
337
- updates.lockdownThresholdWindowSeconds = parseInt(options.lockdownWindow, 10);
338
- }
339
- if (options.level1 !== undefined) {
340
- updates.backoffLevel1Seconds = parseInt(options.level1, 10);
341
- }
342
- if (options.level2 !== undefined) {
343
- updates.backoffLevel2Seconds = parseInt(options.level2, 10);
344
- }
345
- if (options.level3 !== undefined) {
346
- updates.backoffLevel3Seconds = parseInt(options.level3, 10);
347
- }
348
- if (options.level4 !== undefined) {
349
- updates.backoffLevel4Seconds = parseInt(options.level4, 10);
350
- }
351
- if (options.level5 !== undefined) {
352
- updates.backoffLevel5Seconds = parseInt(options.level5, 10);
353
- }
354
- if (options.autoExpire !== undefined) {
355
- updates.autoExpireAfterSeconds = parseInt(options.autoExpire, 10);
356
- }
357
- if (Object.keys(updates).length === 0) {
358
- output.error('No configuration options provided');
359
- output.info('Use --help to see available options');
360
- process.exit(1);
361
- }
362
- try {
363
- if (!options.yes) {
364
- output.section('Configuration Changes');
365
- output.keyValue(updates);
366
- const confirmed = await promptConfirm('Apply these changes?');
367
- if (!confirmed) {
368
- output.info('Cancelled');
369
- return;
370
- }
399
+ if (options.level3 !== undefined) {
400
+ updates.backoffLevel3Seconds = parseInt(options.level3, 10);
401
+ }
402
+ if (options.level4 !== undefined) {
403
+ updates.backoffLevel4Seconds = parseInt(options.level4, 10);
404
+ }
405
+ if (options.level5 !== undefined) {
406
+ updates.backoffLevel5Seconds = parseInt(options.level5, 10);
407
+ }
408
+ if (options.autoExpire !== undefined) {
409
+ updates.autoExpireAfterSeconds = parseInt(options.autoExpire, 10);
410
+ }
411
+ if (Object.keys(updates).length === 0) {
412
+ output.error('No configuration options provided');
413
+ output.info('Use --help to see available options');
414
+ process.exit(1);
371
415
  }
372
- const spinner = output.spinner('Updating configuration...').start();
373
416
  try {
374
- const result = await client.updateQuarantineConfig(updates, options.tenant);
375
- spinner.succeed('Configuration updated');
376
- if (options.json) {
377
- output.json(result);
378
- return;
417
+ if (!options.yes) {
418
+ output.section('Configuration Changes');
419
+ output.keyValue(updates);
420
+ const confirmed = await promptConfirm('Apply these changes?');
421
+ if (!confirmed) {
422
+ output.info('Cancelled');
423
+ return;
424
+ }
425
+ }
426
+ const spinner = output.spinner('Updating configuration...').start();
427
+ try {
428
+ const result = await client.updateQuarantineConfig(updates, scope(options.tenant));
429
+ spinner.succeed('Configuration updated');
430
+ if (options.json) {
431
+ output.json(result);
432
+ return;
433
+ }
434
+ output.success('Quarantine configuration updated successfully');
435
+ }
436
+ catch (err) {
437
+ spinner.fail('Failed to update configuration');
438
+ throw err;
379
439
  }
380
- output.success('Quarantine configuration updated successfully');
381
440
  }
382
441
  catch (err) {
383
- spinner.fail('Failed to update configuration');
384
- throw err;
442
+ output.error(err instanceof Error ? err.message : String(err));
443
+ process.exit(1);
385
444
  }
386
- }
387
- catch (err) {
388
- output.error(err instanceof Error ? err.message : String(err));
389
- process.exit(1);
390
- }
391
- });
445
+ });
446
+ }
392
447
  }
393
448
  //# sourceMappingURL=quarantine.js.map