@simonsbs/keylore 1.0.0-rc4

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 (81) hide show
  1. package/.env.example +64 -0
  2. package/LICENSE +176 -0
  3. package/NOTICE +5 -0
  4. package/README.md +424 -0
  5. package/bin/keylore-http.js +3 -0
  6. package/bin/keylore-stdio.js +3 -0
  7. package/data/auth-clients.json +54 -0
  8. package/data/catalog.json +53 -0
  9. package/data/policies.json +25 -0
  10. package/dist/adapters/adapter-registry.js +143 -0
  11. package/dist/adapters/aws-secrets-manager-adapter.js +99 -0
  12. package/dist/adapters/command-runner.js +17 -0
  13. package/dist/adapters/env-secret-adapter.js +42 -0
  14. package/dist/adapters/gcp-secret-manager-adapter.js +129 -0
  15. package/dist/adapters/local-secret-adapter.js +54 -0
  16. package/dist/adapters/onepassword-secret-adapter.js +83 -0
  17. package/dist/adapters/reference-utils.js +44 -0
  18. package/dist/adapters/types.js +1 -0
  19. package/dist/adapters/vault-secret-adapter.js +103 -0
  20. package/dist/app.js +132 -0
  21. package/dist/cli/args.js +51 -0
  22. package/dist/cli/run.js +483 -0
  23. package/dist/cli.js +18 -0
  24. package/dist/config.js +295 -0
  25. package/dist/domain/types.js +967 -0
  26. package/dist/http/admin-ui.js +3010 -0
  27. package/dist/http/server.js +1210 -0
  28. package/dist/index.js +40 -0
  29. package/dist/mcp/create-server.js +388 -0
  30. package/dist/mcp/stdio.js +7 -0
  31. package/dist/repositories/credential-repository.js +109 -0
  32. package/dist/repositories/interfaces.js +1 -0
  33. package/dist/repositories/json-file.js +20 -0
  34. package/dist/repositories/pg-access-token-repository.js +118 -0
  35. package/dist/repositories/pg-approval-repository.js +157 -0
  36. package/dist/repositories/pg-audit-log.js +62 -0
  37. package/dist/repositories/pg-auth-client-repository.js +98 -0
  38. package/dist/repositories/pg-authorization-code-repository.js +95 -0
  39. package/dist/repositories/pg-break-glass-repository.js +174 -0
  40. package/dist/repositories/pg-credential-repository.js +163 -0
  41. package/dist/repositories/pg-oauth-client-assertion-repository.js +25 -0
  42. package/dist/repositories/pg-policy-repository.js +62 -0
  43. package/dist/repositories/pg-refresh-token-repository.js +125 -0
  44. package/dist/repositories/pg-rotation-run-repository.js +127 -0
  45. package/dist/repositories/pg-tenant-repository.js +56 -0
  46. package/dist/repositories/policy-repository.js +24 -0
  47. package/dist/runtime/sandbox-runner.js +114 -0
  48. package/dist/services/access-fingerprint.js +13 -0
  49. package/dist/services/approval-service.js +148 -0
  50. package/dist/services/audit-log.js +38 -0
  51. package/dist/services/auth-context.js +43 -0
  52. package/dist/services/auth-secrets.js +14 -0
  53. package/dist/services/auth-service.js +784 -0
  54. package/dist/services/backup-service.js +610 -0
  55. package/dist/services/break-glass-service.js +207 -0
  56. package/dist/services/broker-service.js +557 -0
  57. package/dist/services/core-mode-service.js +154 -0
  58. package/dist/services/egress-policy.js +119 -0
  59. package/dist/services/local-secret-store.js +119 -0
  60. package/dist/services/maintenance-service.js +99 -0
  61. package/dist/services/notification-service.js +83 -0
  62. package/dist/services/policy-engine.js +85 -0
  63. package/dist/services/rate-limit-service.js +80 -0
  64. package/dist/services/rotation-service.js +271 -0
  65. package/dist/services/telemetry.js +149 -0
  66. package/dist/services/tenant-service.js +127 -0
  67. package/dist/services/trace-export-service.js +126 -0
  68. package/dist/services/trace-service.js +87 -0
  69. package/dist/storage/bootstrap.js +68 -0
  70. package/dist/storage/database.js +39 -0
  71. package/dist/storage/in-memory-database.js +40 -0
  72. package/dist/storage/migrations.js +27 -0
  73. package/migrations/001_init.sql +49 -0
  74. package/migrations/002_phase2_auth.sql +53 -0
  75. package/migrations/003_v05_operations.sql +9 -0
  76. package/migrations/004_v07_security.sql +28 -0
  77. package/migrations/005_v08_reviews.sql +11 -0
  78. package/migrations/006_v09_auth_trace_rotation.sql +51 -0
  79. package/migrations/007_v010_multi_tenant.sql +32 -0
  80. package/migrations/008_v011_auth_tenant_ops.sql +95 -0
  81. package/package.json +78 -0
@@ -0,0 +1,207 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { breakGlassRequestSchema, breakGlassRequestInputSchema, } from "../domain/types.js";
3
+ import { accessFingerprint } from "./access-fingerprint.js";
4
+ export class BreakGlassService {
5
+ requests;
6
+ audit;
7
+ maxDurationSeconds;
8
+ reviewQuorum;
9
+ notifications;
10
+ traces;
11
+ constructor(requests, audit, maxDurationSeconds, reviewQuorum, notifications, traces) {
12
+ this.requests = requests;
13
+ this.audit = audit;
14
+ this.maxDurationSeconds = maxDurationSeconds;
15
+ this.reviewQuorum = reviewQuorum;
16
+ this.notifications = notifications;
17
+ this.traces = traces;
18
+ }
19
+ async createRequest(context, input, tenantId) {
20
+ const parsedInput = breakGlassRequestInputSchema.parse(input);
21
+ return this.traces.withSpan("breakglass.create_request", { credentialId: parsedInput.credentialId }, async () => {
22
+ const requestedDurationSeconds = Math.min(parsedInput.requestedDurationSeconds ?? this.maxDurationSeconds, this.maxDurationSeconds);
23
+ const created = await this.requests.create(breakGlassRequestSchema.parse({
24
+ id: randomUUID(),
25
+ tenantId,
26
+ createdAt: new Date().toISOString(),
27
+ expiresAt: new Date(Date.now() + requestedDurationSeconds * 1000).toISOString(),
28
+ status: "pending",
29
+ requestedBy: context.principal,
30
+ requestedRoles: context.roles,
31
+ credentialId: parsedInput.credentialId,
32
+ operation: parsedInput.operation,
33
+ targetUrl: parsedInput.targetUrl,
34
+ targetHost: new URL(parsedInput.targetUrl).hostname,
35
+ justification: parsedInput.justification,
36
+ requestedDurationSeconds,
37
+ correlationId: randomUUID(),
38
+ fingerprint: accessFingerprint(context, parsedInput),
39
+ requiredApprovals: this.reviewQuorum,
40
+ approvalCount: 0,
41
+ denialCount: 0,
42
+ reviews: [],
43
+ }));
44
+ await this.audit.record({
45
+ type: "breakglass.request",
46
+ action: "breakglass.request",
47
+ outcome: "success",
48
+ tenantId,
49
+ principal: context.principal,
50
+ correlationId: created.correlationId,
51
+ metadata: {
52
+ breakGlassId: created.id,
53
+ tenantId,
54
+ credentialId: created.credentialId,
55
+ operation: created.operation,
56
+ targetHost: created.targetHost,
57
+ requestedDurationSeconds: created.requestedDurationSeconds,
58
+ requiredApprovals: created.requiredApprovals,
59
+ },
60
+ });
61
+ await this.notifications.send("breakglass.pending", {
62
+ breakGlassId: created.id,
63
+ credentialId: created.credentialId,
64
+ requestedBy: created.requestedBy,
65
+ requiredApprovals: created.requiredApprovals,
66
+ targetHost: created.targetHost,
67
+ });
68
+ return created;
69
+ });
70
+ }
71
+ async verifyActive(context, input) {
72
+ await this.requests.expireStale();
73
+ if (!input.breakGlassId) {
74
+ return undefined;
75
+ }
76
+ const request = await this.requests.getById(input.breakGlassId);
77
+ if (!request || request.status !== "active") {
78
+ return undefined;
79
+ }
80
+ if (context.tenantId && request.tenantId !== context.tenantId) {
81
+ return undefined;
82
+ }
83
+ if (new Date(request.expiresAt).getTime() <= Date.now()) {
84
+ return undefined;
85
+ }
86
+ return request.fingerprint === accessFingerprint(context, input) ? request : undefined;
87
+ }
88
+ async list(context, filter) {
89
+ await this.requests.expireStale();
90
+ return this.requests.list({
91
+ ...filter,
92
+ tenantId: context.tenantId,
93
+ });
94
+ }
95
+ async review(id, context, status, note) {
96
+ return this.traces.withSpan("breakglass.review", { breakGlassId: id, decision: status }, async () => {
97
+ await this.requests.expireStale();
98
+ const existing = await this.requests.getById(id);
99
+ if (existing && context.tenantId && existing.tenantId !== context.tenantId) {
100
+ throw new Error("Tenant access denied.");
101
+ }
102
+ const reviewed = await this.requests.review(id, {
103
+ status,
104
+ reviewedBy: context.principal,
105
+ reviewNote: note,
106
+ });
107
+ if (reviewed) {
108
+ await this.audit.record({
109
+ type: "breakglass.review",
110
+ action: `breakglass.${status === "active" ? "approve" : "deny"}`,
111
+ outcome: status === "active" ? "allowed" : "denied",
112
+ tenantId: reviewed.tenantId,
113
+ principal: context.principal,
114
+ correlationId: reviewed.correlationId,
115
+ metadata: {
116
+ breakGlassId: reviewed.id,
117
+ tenantId: reviewed.tenantId,
118
+ credentialId: reviewed.credentialId,
119
+ requestedBy: reviewed.requestedBy,
120
+ reviewNote: reviewed.reviewNote ?? null,
121
+ approvalCount: reviewed.approvalCount,
122
+ denialCount: reviewed.denialCount,
123
+ requiredApprovals: reviewed.requiredApprovals,
124
+ currentStatus: reviewed.status,
125
+ },
126
+ });
127
+ await this.notifications.send("breakglass.reviewed", {
128
+ breakGlassId: reviewed.id,
129
+ credentialId: reviewed.credentialId,
130
+ requestedBy: reviewed.requestedBy,
131
+ reviewer: context.principal,
132
+ decision: status,
133
+ currentStatus: reviewed.status,
134
+ approvalCount: reviewed.approvalCount,
135
+ denialCount: reviewed.denialCount,
136
+ requiredApprovals: reviewed.requiredApprovals,
137
+ });
138
+ if (reviewed.status === "active" || reviewed.status === "denied") {
139
+ await this.notifications.send(`breakglass.${reviewed.status}`, {
140
+ breakGlassId: reviewed.id,
141
+ credentialId: reviewed.credentialId,
142
+ requestedBy: reviewed.requestedBy,
143
+ approvalCount: reviewed.approvalCount,
144
+ denialCount: reviewed.denialCount,
145
+ requiredApprovals: reviewed.requiredApprovals,
146
+ });
147
+ }
148
+ }
149
+ return reviewed;
150
+ });
151
+ }
152
+ async revoke(id, context, note) {
153
+ return this.traces.withSpan("breakglass.revoke", { breakGlassId: id }, async () => {
154
+ await this.requests.expireStale();
155
+ const existing = await this.requests.getById(id);
156
+ if (existing && context.tenantId && existing.tenantId !== context.tenantId) {
157
+ throw new Error("Tenant access denied.");
158
+ }
159
+ const revoked = await this.requests.revoke(id, {
160
+ revokedBy: context.principal,
161
+ revokeNote: note,
162
+ });
163
+ if (revoked) {
164
+ await this.audit.record({
165
+ type: "breakglass.review",
166
+ action: "breakglass.revoke",
167
+ outcome: "denied",
168
+ tenantId: revoked.tenantId,
169
+ principal: context.principal,
170
+ correlationId: revoked.correlationId,
171
+ metadata: {
172
+ breakGlassId: revoked.id,
173
+ tenantId: revoked.tenantId,
174
+ credentialId: revoked.credentialId,
175
+ requestedBy: revoked.requestedBy,
176
+ revokeNote: revoked.revokeNote ?? null,
177
+ },
178
+ });
179
+ await this.notifications.send("breakglass.revoked", {
180
+ breakGlassId: revoked.id,
181
+ credentialId: revoked.credentialId,
182
+ requestedBy: revoked.requestedBy,
183
+ revokedBy: context.principal,
184
+ });
185
+ }
186
+ return revoked;
187
+ });
188
+ }
189
+ async recordUse(context, request) {
190
+ await this.traces.withSpan("breakglass.use", { breakGlassId: request.id }, async () => {
191
+ await this.audit.record({
192
+ type: "breakglass.use",
193
+ action: "breakglass.use",
194
+ outcome: "allowed",
195
+ tenantId: request.tenantId,
196
+ principal: context.principal,
197
+ correlationId: request.correlationId,
198
+ metadata: {
199
+ breakGlassId: request.id,
200
+ tenantId: request.tenantId,
201
+ credentialId: request.credentialId,
202
+ targetHost: request.targetHost,
203
+ },
204
+ });
205
+ });
206
+ }
207
+ }