@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,114 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ function redact(text, secret) {
6
+ return text
7
+ .replaceAll(secret, "[REDACTED_SECRET]")
8
+ .replace(/gh[pousr]_[A-Za-z0-9_]+/g, "[REDACTED_GITHUB_TOKEN]")
9
+ .replace(/github_pat_[A-Za-z0-9_]+/g, "[REDACTED_GITHUB_TOKEN]")
10
+ .replace(/Bearer\s+[A-Za-z0-9._-]+/gi, "Bearer [REDACTED_TOKEN]");
11
+ }
12
+ function truncate(text, maxLength) {
13
+ if (text.length <= maxLength) {
14
+ return { value: text, truncated: false };
15
+ }
16
+ return {
17
+ value: `${text.slice(0, maxLength)}\n...[truncated]`,
18
+ truncated: true,
19
+ };
20
+ }
21
+ export class SandboxRunner {
22
+ config;
23
+ constructor(config) {
24
+ this.config = config;
25
+ }
26
+ sanitizeEnv(inputEnv, secretEnvName) {
27
+ if (!inputEnv) {
28
+ return {};
29
+ }
30
+ const reservedNames = new Set(["PATH", "HOME", secretEnvName]);
31
+ const sanitized = {};
32
+ for (const [name, value] of Object.entries(inputEnv)) {
33
+ if (reservedNames.has(name)) {
34
+ throw new Error(`Sandbox env variable is reserved and cannot be overridden: ${name}`);
35
+ }
36
+ if (!this.config.sandboxEnvAllowlist.includes(name)) {
37
+ throw new Error(`Sandbox env variable is not allowlisted: ${name}`);
38
+ }
39
+ sanitized[name] = value;
40
+ }
41
+ return sanitized;
42
+ }
43
+ async run(input, secret, secretEnvName) {
44
+ if (!this.config.sandboxInjectionEnabled) {
45
+ throw new Error("Sandbox injection mode is disabled.");
46
+ }
47
+ if (!this.config.sandboxCommandAllowlist.includes(input.command)) {
48
+ throw new Error(`Command is not allowlisted for sandbox execution: ${input.command}`);
49
+ }
50
+ const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "keylore-sandbox-"));
51
+ const startedAt = Date.now();
52
+ const sanitizedEnv = this.sanitizeEnv(input.env, secretEnvName);
53
+ try {
54
+ const result = await new Promise((resolve, reject) => {
55
+ const child = spawn(input.command, input.args, {
56
+ cwd,
57
+ shell: false,
58
+ stdio: ["ignore", "pipe", "pipe"],
59
+ env: {
60
+ PATH: process.env.PATH,
61
+ HOME: process.env.HOME,
62
+ ...sanitizedEnv,
63
+ [secretEnvName]: secret,
64
+ },
65
+ });
66
+ let stdout = "";
67
+ let stderr = "";
68
+ let timedOut = false;
69
+ const timeoutMs = input.timeoutMs ?? this.config.sandboxDefaultTimeoutMs;
70
+ const timer = setTimeout(() => {
71
+ timedOut = true;
72
+ child.kill("SIGKILL");
73
+ }, timeoutMs);
74
+ child.stdout.setEncoding("utf8");
75
+ child.stderr.setEncoding("utf8");
76
+ child.stdout.on("data", (chunk) => {
77
+ stdout += chunk;
78
+ });
79
+ child.stderr.on("data", (chunk) => {
80
+ stderr += chunk;
81
+ });
82
+ child.on("error", (error) => {
83
+ clearTimeout(timer);
84
+ reject(error);
85
+ });
86
+ child.on("close", (code) => {
87
+ clearTimeout(timer);
88
+ resolve({
89
+ exitCode: code ?? (timedOut ? 137 : 1),
90
+ timedOut,
91
+ stdout,
92
+ stderr,
93
+ });
94
+ });
95
+ });
96
+ const stdout = truncate(redact(result.stdout, secret), this.config.sandboxMaxOutputBytes);
97
+ const stderr = truncate(redact(result.stderr, secret), this.config.sandboxMaxOutputBytes);
98
+ return {
99
+ mode: "sandbox_injection",
100
+ command: input.command,
101
+ args: input.args,
102
+ exitCode: result.exitCode,
103
+ timedOut: result.timedOut,
104
+ durationMs: Date.now() - startedAt,
105
+ stdoutPreview: stdout.value,
106
+ stderrPreview: stderr.value,
107
+ outputTruncated: stdout.truncated || stderr.truncated,
108
+ };
109
+ }
110
+ finally {
111
+ await fs.rm(cwd, { recursive: true, force: true });
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,13 @@
1
+ import { createHash } from "node:crypto";
2
+ export function accessFingerprint(context, input) {
3
+ const serialized = JSON.stringify({
4
+ principal: context.principal,
5
+ tenantId: context.tenantId ?? null,
6
+ credentialId: input.credentialId,
7
+ operation: input.operation,
8
+ targetUrl: input.targetUrl,
9
+ headers: input.headers ?? {},
10
+ payload: input.payload ?? "",
11
+ });
12
+ return createHash("sha256").update(serialized).digest("hex");
13
+ }
@@ -0,0 +1,148 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { approvalRequestSchema } from "../domain/types.js";
3
+ import { accessFingerprint } from "./access-fingerprint.js";
4
+ export class ApprovalService {
5
+ approvals;
6
+ audit;
7
+ approvalTtlSeconds;
8
+ approvalReviewQuorum;
9
+ notifications;
10
+ traces;
11
+ constructor(approvals, audit, approvalTtlSeconds, approvalReviewQuorum, notifications, traces) {
12
+ this.approvals = approvals;
13
+ this.audit = audit;
14
+ this.approvalTtlSeconds = approvalTtlSeconds;
15
+ this.approvalReviewQuorum = approvalReviewQuorum;
16
+ this.notifications = notifications;
17
+ this.traces = traces;
18
+ }
19
+ async createPending(context, input, decision, tenantId) {
20
+ return this.traces.withSpan("approval.create_pending", { credentialId: input.credentialId }, async () => {
21
+ const request = approvalRequestSchema.parse({
22
+ id: randomUUID(),
23
+ tenantId,
24
+ createdAt: new Date().toISOString(),
25
+ expiresAt: new Date(Date.now() + this.approvalTtlSeconds * 1000).toISOString(),
26
+ status: "pending",
27
+ requestedBy: context.principal,
28
+ requestedRoles: context.roles,
29
+ credentialId: input.credentialId,
30
+ operation: input.operation,
31
+ targetUrl: input.targetUrl,
32
+ targetHost: new URL(input.targetUrl).hostname,
33
+ reason: decision.reason,
34
+ ruleId: decision.ruleId,
35
+ correlationId: decision.correlationId,
36
+ fingerprint: accessFingerprint(context, input),
37
+ requiredApprovals: this.approvalReviewQuorum,
38
+ approvalCount: 0,
39
+ denialCount: 0,
40
+ reviews: [],
41
+ });
42
+ const created = await this.approvals.create(request);
43
+ await this.audit.record({
44
+ type: "approval.request",
45
+ action: "approval.request",
46
+ outcome: "success",
47
+ tenantId,
48
+ principal: context.principal,
49
+ correlationId: decision.correlationId,
50
+ metadata: {
51
+ approvalId: created.id,
52
+ tenantId,
53
+ credentialId: created.credentialId,
54
+ operation: created.operation,
55
+ targetHost: created.targetHost,
56
+ ruleId: created.ruleId ?? null,
57
+ requiredApprovals: created.requiredApprovals,
58
+ },
59
+ });
60
+ await this.notifications.send("approval.pending", {
61
+ approvalId: created.id,
62
+ credentialId: created.credentialId,
63
+ requestedBy: created.requestedBy,
64
+ requiredApprovals: created.requiredApprovals,
65
+ targetHost: created.targetHost,
66
+ });
67
+ return created;
68
+ });
69
+ }
70
+ async verifyApproval(context, input) {
71
+ await this.approvals.expireStale();
72
+ if (!input.approvalId) {
73
+ return undefined;
74
+ }
75
+ const approval = await this.approvals.getById(input.approvalId);
76
+ if (!approval || approval.status !== "approved") {
77
+ return undefined;
78
+ }
79
+ if (context.tenantId && approval.tenantId !== context.tenantId) {
80
+ return undefined;
81
+ }
82
+ if (new Date(approval.expiresAt).getTime() <= Date.now()) {
83
+ return undefined;
84
+ }
85
+ return approval.fingerprint === accessFingerprint(context, input) ? approval : undefined;
86
+ }
87
+ async list(context, status) {
88
+ await this.approvals.expireStale();
89
+ return this.approvals.list(status, context.tenantId);
90
+ }
91
+ async review(id, context, status, note) {
92
+ return this.traces.withSpan("approval.review", { approvalId: id, decision: status }, async () => {
93
+ await this.approvals.expireStale();
94
+ const existing = await this.approvals.getById(id);
95
+ if (existing && context.tenantId && existing.tenantId !== context.tenantId) {
96
+ throw new Error("Tenant access denied.");
97
+ }
98
+ const reviewed = await this.approvals.review(id, {
99
+ status,
100
+ reviewedBy: context.principal,
101
+ reviewNote: note,
102
+ });
103
+ if (reviewed) {
104
+ await this.audit.record({
105
+ type: "approval.review",
106
+ action: `approval.${status}`,
107
+ outcome: status === "approved" ? "allowed" : "denied",
108
+ tenantId: reviewed.tenantId,
109
+ principal: context.principal,
110
+ correlationId: reviewed.correlationId,
111
+ metadata: {
112
+ approvalId: reviewed.id,
113
+ tenantId: reviewed.tenantId,
114
+ credentialId: reviewed.credentialId,
115
+ requestedBy: reviewed.requestedBy,
116
+ reviewNote: reviewed.reviewNote ?? null,
117
+ approvalCount: reviewed.approvalCount,
118
+ denialCount: reviewed.denialCount,
119
+ requiredApprovals: reviewed.requiredApprovals,
120
+ currentStatus: reviewed.status,
121
+ },
122
+ });
123
+ await this.notifications.send("approval.reviewed", {
124
+ approvalId: reviewed.id,
125
+ credentialId: reviewed.credentialId,
126
+ requestedBy: reviewed.requestedBy,
127
+ reviewer: context.principal,
128
+ decision: status,
129
+ currentStatus: reviewed.status,
130
+ approvalCount: reviewed.approvalCount,
131
+ denialCount: reviewed.denialCount,
132
+ requiredApprovals: reviewed.requiredApprovals,
133
+ });
134
+ if (reviewed.status === "approved" || reviewed.status === "denied") {
135
+ await this.notifications.send(`approval.${reviewed.status}`, {
136
+ approvalId: reviewed.id,
137
+ credentialId: reviewed.credentialId,
138
+ requestedBy: reviewed.requestedBy,
139
+ approvalCount: reviewed.approvalCount,
140
+ denialCount: reviewed.denialCount,
141
+ requiredApprovals: reviewed.requiredApprovals,
142
+ });
143
+ }
144
+ }
145
+ return reviewed;
146
+ });
147
+ }
148
+ }
@@ -0,0 +1,38 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { auditEventSchema } from "../domain/types.js";
3
+ import { ensureParentDirectory, readTextFile, writeTextFile } from "../repositories/json-file.js";
4
+ export class AuditLogService {
5
+ filePath;
6
+ constructor(filePath) {
7
+ this.filePath = filePath;
8
+ }
9
+ async record(input) {
10
+ const event = auditEventSchema.parse({
11
+ eventId: randomUUID(),
12
+ occurredAt: new Date().toISOString(),
13
+ type: input.type,
14
+ action: input.action,
15
+ outcome: input.outcome,
16
+ principal: input.principal,
17
+ correlationId: input.correlationId ?? randomUUID(),
18
+ metadata: input.metadata ?? {},
19
+ });
20
+ await ensureParentDirectory(this.filePath);
21
+ const existing = (await readTextFile(this.filePath)) ?? "";
22
+ await writeTextFile(this.filePath, `${existing}${JSON.stringify(event)}\n`);
23
+ return event;
24
+ }
25
+ async listRecent(limit = 20) {
26
+ const text = await readTextFile(this.filePath);
27
+ if (!text) {
28
+ return [];
29
+ }
30
+ return text
31
+ .trim()
32
+ .split("\n")
33
+ .filter(Boolean)
34
+ .map((line) => auditEventSchema.parse(JSON.parse(line)))
35
+ .slice(-limit)
36
+ .reverse();
37
+ }
38
+ }
@@ -0,0 +1,43 @@
1
+ import { authContextSchema } from "../domain/types.js";
2
+ const allScopes = [
3
+ "catalog:read",
4
+ "catalog:write",
5
+ "admin:read",
6
+ "admin:write",
7
+ "auth:read",
8
+ "auth:write",
9
+ "broker:use",
10
+ "sandbox:run",
11
+ "audit:read",
12
+ "approval:read",
13
+ "approval:review",
14
+ "system:read",
15
+ "system:write",
16
+ "backup:read",
17
+ "backup:write",
18
+ "breakglass:request",
19
+ "breakglass:read",
20
+ "breakglass:review",
21
+ "mcp:use",
22
+ ];
23
+ const localRoles = [
24
+ "admin",
25
+ "auth_admin",
26
+ "operator",
27
+ "maintenance_operator",
28
+ "backup_operator",
29
+ "breakglass_operator",
30
+ "auditor",
31
+ "approver",
32
+ ];
33
+ export function localOperatorContext(principal) {
34
+ return authContextSchema.parse({
35
+ principal,
36
+ clientId: "local-cli",
37
+ roles: localRoles,
38
+ scopes: allScopes,
39
+ });
40
+ }
41
+ export function authContextFromToken(auth) {
42
+ return authContextSchema.parse(auth);
43
+ }
@@ -0,0 +1,14 @@
1
+ import { randomBytes, scryptSync, timingSafeEqual, createHash } from "node:crypto";
2
+ export function hashSecret(secret) {
3
+ const salt = randomBytes(16).toString("hex");
4
+ const hash = scryptSync(secret, salt, 64).toString("hex");
5
+ return { salt, hash };
6
+ }
7
+ export function verifySecret(secret, salt, expectedHash) {
8
+ const derived = scryptSync(secret, salt, 64);
9
+ const expected = Buffer.from(expectedHash, "hex");
10
+ return derived.length === expected.length && timingSafeEqual(derived, expected);
11
+ }
12
+ export function hashOpaqueToken(token) {
13
+ return createHash("sha256").update(token).digest("hex");
14
+ }