@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,126 @@
1
+ import { traceExportStatusSchema } from "../domain/types.js";
2
+ export class TraceExportService {
3
+ endpoint;
4
+ authHeader;
5
+ batchSize;
6
+ intervalMs;
7
+ timeoutMs;
8
+ telemetry;
9
+ queue = [];
10
+ timer;
11
+ flushing = false;
12
+ snapshot;
13
+ constructor(endpoint, authHeader, batchSize, intervalMs, timeoutMs, telemetry) {
14
+ this.endpoint = endpoint;
15
+ this.authHeader = authHeader;
16
+ this.batchSize = batchSize;
17
+ this.intervalMs = intervalMs;
18
+ this.timeoutMs = timeoutMs;
19
+ this.telemetry = telemetry;
20
+ this.snapshot = traceExportStatusSchema.parse({
21
+ enabled: Boolean(endpoint),
22
+ endpoint,
23
+ pendingSpans: 0,
24
+ consecutiveFailures: 0,
25
+ running: false,
26
+ });
27
+ }
28
+ start() {
29
+ if (!this.endpoint || this.timer) {
30
+ return;
31
+ }
32
+ this.timer = setInterval(() => {
33
+ void this.flushNow();
34
+ }, this.intervalMs);
35
+ this.timer.unref();
36
+ }
37
+ async stop() {
38
+ if (this.timer) {
39
+ clearInterval(this.timer);
40
+ this.timer = undefined;
41
+ }
42
+ await this.flushNow();
43
+ }
44
+ enqueue(span) {
45
+ if (!this.endpoint) {
46
+ return;
47
+ }
48
+ this.queue.push(span);
49
+ this.snapshot.pendingSpans = this.queue.length;
50
+ if (this.queue.length >= this.batchSize) {
51
+ void this.flushNow();
52
+ }
53
+ }
54
+ status() {
55
+ return traceExportStatusSchema.parse({
56
+ ...this.snapshot,
57
+ pendingSpans: this.queue.length,
58
+ running: this.flushing,
59
+ });
60
+ }
61
+ async flushNow() {
62
+ if (!this.endpoint) {
63
+ this.telemetry.recordTraceExport("disabled", 0);
64
+ return this.status();
65
+ }
66
+ if (this.flushing || this.queue.length === 0) {
67
+ return this.status();
68
+ }
69
+ this.flushing = true;
70
+ this.snapshot.running = true;
71
+ const batch = this.queue.splice(0, this.batchSize);
72
+ this.snapshot.pendingSpans = this.queue.length;
73
+ try {
74
+ const headers = {
75
+ "content-type": "application/json",
76
+ };
77
+ if (this.authHeader) {
78
+ headers.authorization = this.authHeader;
79
+ }
80
+ const response = await fetch(this.endpoint, {
81
+ method: "POST",
82
+ headers,
83
+ body: JSON.stringify({
84
+ exportedAt: new Date().toISOString(),
85
+ spans: batch,
86
+ }),
87
+ signal: AbortSignal.timeout(this.timeoutMs),
88
+ });
89
+ if (!response.ok) {
90
+ throw new Error(`Trace export endpoint responded with ${response.status}.`);
91
+ }
92
+ this.snapshot = traceExportStatusSchema.parse({
93
+ ...this.snapshot,
94
+ endpoint: this.endpoint,
95
+ enabled: true,
96
+ pendingSpans: this.queue.length,
97
+ lastFlushAt: new Date().toISOString(),
98
+ lastBatchSize: batch.length,
99
+ lastError: undefined,
100
+ consecutiveFailures: 0,
101
+ running: false,
102
+ });
103
+ this.telemetry.recordTraceExport("success", batch.length);
104
+ return this.status();
105
+ }
106
+ catch (error) {
107
+ this.queue.unshift(...batch);
108
+ this.snapshot = traceExportStatusSchema.parse({
109
+ ...this.snapshot,
110
+ endpoint: this.endpoint,
111
+ enabled: true,
112
+ pendingSpans: this.queue.length,
113
+ lastBatchSize: batch.length,
114
+ lastError: error instanceof Error ? error.message : "Trace export failed.",
115
+ consecutiveFailures: this.snapshot.consecutiveFailures + 1,
116
+ running: false,
117
+ });
118
+ this.telemetry.recordTraceExport("error", batch.length);
119
+ return this.status();
120
+ }
121
+ finally {
122
+ this.flushing = false;
123
+ this.snapshot.running = false;
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,87 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { randomUUID } from "node:crypto";
3
+ import { traceSpanSchema } from "../domain/types.js";
4
+ export class TraceService {
5
+ enabled;
6
+ recentSpanLimit;
7
+ storage = new AsyncLocalStorage();
8
+ spans = [];
9
+ exporter;
10
+ constructor(enabled, recentSpanLimit) {
11
+ this.enabled = enabled;
12
+ this.recentSpanLimit = recentSpanLimit;
13
+ }
14
+ runWithTrace(traceId, fn) {
15
+ if (!this.enabled) {
16
+ return Promise.resolve(fn());
17
+ }
18
+ return Promise.resolve(this.storage.run({ traceId, spanStack: [] }, fn));
19
+ }
20
+ currentTraceId() {
21
+ return this.storage.getStore()?.traceId;
22
+ }
23
+ async withSpan(name, attributes, fn) {
24
+ if (!this.enabled) {
25
+ return Promise.resolve(fn());
26
+ }
27
+ const current = this.storage.getStore();
28
+ const traceId = current?.traceId ?? randomUUID();
29
+ const spanId = randomUUID();
30
+ const parentSpanId = current?.spanStack.at(-1);
31
+ const startedAt = new Date();
32
+ const context = {
33
+ traceId,
34
+ spanStack: [...(current?.spanStack ?? []), spanId],
35
+ };
36
+ const execute = async () => {
37
+ try {
38
+ const result = await fn();
39
+ this.record({
40
+ spanId,
41
+ traceId,
42
+ parentSpanId,
43
+ name,
44
+ startedAt: startedAt.toISOString(),
45
+ endedAt: new Date().toISOString(),
46
+ durationMs: Math.max(0, Date.now() - startedAt.getTime()),
47
+ status: "ok",
48
+ attributes,
49
+ });
50
+ return result;
51
+ }
52
+ catch (error) {
53
+ this.record({
54
+ spanId,
55
+ traceId,
56
+ parentSpanId,
57
+ name,
58
+ startedAt: startedAt.toISOString(),
59
+ endedAt: new Date().toISOString(),
60
+ durationMs: Math.max(0, Date.now() - startedAt.getTime()),
61
+ status: "error",
62
+ attributes: {
63
+ ...attributes,
64
+ error: error instanceof Error ? error.message : "Unknown error",
65
+ },
66
+ });
67
+ throw error;
68
+ }
69
+ };
70
+ return this.storage.run(context, execute);
71
+ }
72
+ recent(limit = 20, traceId) {
73
+ const filtered = traceId ? this.spans.filter((span) => span.traceId === traceId) : this.spans;
74
+ return filtered.slice(Math.max(0, filtered.length - limit)).reverse();
75
+ }
76
+ attachExporter(exporter) {
77
+ this.exporter = exporter;
78
+ }
79
+ record(span) {
80
+ const parsed = traceSpanSchema.parse(span);
81
+ this.spans.push(parsed);
82
+ if (this.spans.length > this.recentSpanLimit) {
83
+ this.spans.splice(0, this.spans.length - this.recentSpanLimit);
84
+ }
85
+ this.exporter?.enqueue(parsed);
86
+ }
87
+ }
@@ -0,0 +1,68 @@
1
+ import { authClientSeedFileSchema, catalogFileSchema, policyFileSchema } from "../domain/types.js";
2
+ import { readTextFile } from "../repositories/json-file.js";
3
+ import { hashSecret } from "../services/auth-secrets.js";
4
+ export async function bootstrapFromFiles(credentialRepository, policyRepository, authClientRepository, tenantRepository, catalogPath, policyPath, authClientsPath) {
5
+ const ensureTenant = async (tenantId) => {
6
+ const existing = await tenantRepository.getById(tenantId);
7
+ if (!existing) {
8
+ await tenantRepository.create({
9
+ tenantId,
10
+ displayName: tenantId,
11
+ status: "active",
12
+ });
13
+ }
14
+ };
15
+ if ((await credentialRepository.count()) === 0) {
16
+ const catalogText = await readTextFile(catalogPath);
17
+ if (catalogText) {
18
+ const catalog = catalogFileSchema.parse(JSON.parse(catalogText));
19
+ for (const credential of catalog.credentials) {
20
+ await ensureTenant(credential.tenantId);
21
+ await credentialRepository.create(credential);
22
+ }
23
+ }
24
+ }
25
+ if ((await policyRepository.count()) === 0) {
26
+ const policyText = await readTextFile(policyPath);
27
+ if (policyText) {
28
+ const policyFile = policyFileSchema.parse(JSON.parse(policyText));
29
+ for (const rule of policyFile.rules) {
30
+ await ensureTenant(rule.tenantId);
31
+ }
32
+ await policyRepository.replaceAll(policyFile);
33
+ }
34
+ }
35
+ if ((await authClientRepository.count()) === 0) {
36
+ const authClientText = await readTextFile(authClientsPath);
37
+ if (authClientText) {
38
+ const authClients = authClientSeedFileSchema.parse(JSON.parse(authClientText));
39
+ const missingSecrets = [];
40
+ for (const client of authClients.clients) {
41
+ const secret = client.secretRef ? process.env[client.secretRef] : undefined;
42
+ if (!["private_key_jwt", "none"].includes(client.tokenEndpointAuthMethod) && !secret) {
43
+ missingSecrets.push(`${client.clientId}:${client.secretRef}`);
44
+ continue;
45
+ }
46
+ const hashed = secret ? hashSecret(secret) : undefined;
47
+ await ensureTenant(client.tenantId);
48
+ await authClientRepository.upsert({
49
+ clientId: client.clientId,
50
+ tenantId: client.tenantId,
51
+ displayName: client.displayName,
52
+ secretHash: hashed?.hash,
53
+ secretSalt: hashed?.salt,
54
+ roles: client.roles,
55
+ allowedScopes: client.allowedScopes,
56
+ status: client.status,
57
+ tokenEndpointAuthMethod: client.tokenEndpointAuthMethod,
58
+ grantTypes: client.grantTypes,
59
+ redirectUris: client.redirectUris,
60
+ jwks: client.jwks ?? [],
61
+ });
62
+ }
63
+ if (missingSecrets.length > 0) {
64
+ throw new Error(`Missing bootstrap auth client secrets for: ${missingSecrets.join(", ")}`);
65
+ }
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,39 @@
1
+ import { Pool } from "pg";
2
+ class PostgresDatabase {
3
+ pool;
4
+ constructor(pool) {
5
+ this.pool = pool;
6
+ }
7
+ async query(text, params) {
8
+ return this.pool.query(text, params);
9
+ }
10
+ async withTransaction(fn) {
11
+ const client = await this.pool.connect();
12
+ try {
13
+ await client.query("BEGIN");
14
+ const result = await fn(client);
15
+ await client.query("COMMIT");
16
+ return result;
17
+ }
18
+ catch (error) {
19
+ await client.query("ROLLBACK");
20
+ throw error;
21
+ }
22
+ finally {
23
+ client.release();
24
+ }
25
+ }
26
+ async healthcheck() {
27
+ await this.pool.query("SELECT 1");
28
+ }
29
+ async close() {
30
+ await this.pool.end();
31
+ }
32
+ }
33
+ export function createPostgresDatabase(config) {
34
+ const pool = new Pool({
35
+ connectionString: config.databaseUrl,
36
+ max: config.databasePoolMax,
37
+ });
38
+ return new PostgresDatabase(pool);
39
+ }
@@ -0,0 +1,40 @@
1
+ import { newDb } from "pg-mem";
2
+ class InMemoryDatabase {
3
+ pool;
4
+ constructor(pool) {
5
+ this.pool = pool;
6
+ }
7
+ async query(text, params) {
8
+ return this.pool.query(text, params);
9
+ }
10
+ async withTransaction(fn) {
11
+ const client = await this.pool.connect();
12
+ try {
13
+ await client.query("BEGIN");
14
+ const result = await fn(client);
15
+ await client.query("COMMIT");
16
+ return result;
17
+ }
18
+ catch (error) {
19
+ await client.query("ROLLBACK");
20
+ throw error;
21
+ }
22
+ finally {
23
+ client.release();
24
+ }
25
+ }
26
+ async healthcheck() {
27
+ await this.pool.query("SELECT 1");
28
+ }
29
+ async close() {
30
+ await this.pool.end();
31
+ }
32
+ }
33
+ export function createInMemoryDatabase() {
34
+ const db = newDb({
35
+ autoCreateForeignKeyIndices: true,
36
+ });
37
+ const adapter = db.adapters.createPg();
38
+ const pool = new adapter.Pool();
39
+ return new InMemoryDatabase(pool);
40
+ }
@@ -0,0 +1,27 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ export async function runMigrations(database, migrationsDir) {
4
+ await database.query(`
5
+ CREATE TABLE IF NOT EXISTS schema_migrations (
6
+ version TEXT PRIMARY KEY,
7
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
8
+ )
9
+ `);
10
+ const dirEntries = await fs.readdir(migrationsDir, { withFileTypes: true });
11
+ const migrationFiles = dirEntries
12
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".sql"))
13
+ .map((entry) => entry.name)
14
+ .sort();
15
+ for (const fileName of migrationFiles) {
16
+ const version = path.basename(fileName, ".sql");
17
+ const existing = await database.query("SELECT version FROM schema_migrations WHERE version = $1", [version]);
18
+ if ((existing.rowCount ?? 0) > 0) {
19
+ continue;
20
+ }
21
+ const sql = await fs.readFile(path.join(migrationsDir, fileName), "utf8");
22
+ await database.withTransaction(async (client) => {
23
+ await client.query(sql);
24
+ await client.query("INSERT INTO schema_migrations(version) VALUES ($1)", [version]);
25
+ });
26
+ }
27
+ }
@@ -0,0 +1,49 @@
1
+ CREATE TABLE IF NOT EXISTS credentials (
2
+ id TEXT PRIMARY KEY,
3
+ display_name TEXT NOT NULL,
4
+ service TEXT NOT NULL,
5
+ owner TEXT NOT NULL,
6
+ scope_tier TEXT NOT NULL,
7
+ sensitivity TEXT NOT NULL,
8
+ allowed_domains TEXT[] NOT NULL,
9
+ permitted_operations TEXT[] NOT NULL,
10
+ expires_at TIMESTAMPTZ NULL,
11
+ rotation_policy TEXT NOT NULL,
12
+ last_validated_at TIMESTAMPTZ NULL,
13
+ selection_notes TEXT NOT NULL,
14
+ binding JSONB NOT NULL,
15
+ tags TEXT[] NOT NULL DEFAULT '{}',
16
+ status TEXT NOT NULL,
17
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
18
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
19
+ );
20
+
21
+ CREATE TABLE IF NOT EXISTS policy_rules (
22
+ id TEXT PRIMARY KEY,
23
+ effect TEXT NOT NULL,
24
+ description TEXT NOT NULL,
25
+ principals TEXT[] NOT NULL,
26
+ credential_ids TEXT[] NULL,
27
+ services TEXT[] NULL,
28
+ operations TEXT[] NOT NULL,
29
+ domain_patterns TEXT[] NOT NULL,
30
+ environments TEXT[] NULL,
31
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
32
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
33
+ );
34
+
35
+ CREATE TABLE IF NOT EXISTS audit_events (
36
+ event_id UUID PRIMARY KEY,
37
+ occurred_at TIMESTAMPTZ NOT NULL,
38
+ type TEXT NOT NULL,
39
+ action TEXT NOT NULL,
40
+ outcome TEXT NOT NULL,
41
+ principal TEXT NOT NULL,
42
+ correlation_id UUID NOT NULL,
43
+ metadata JSONB NOT NULL DEFAULT '{}'::jsonb
44
+ );
45
+
46
+ CREATE INDEX IF NOT EXISTS idx_credentials_service ON credentials(service);
47
+ CREATE INDEX IF NOT EXISTS idx_credentials_owner ON credentials(owner);
48
+ CREATE INDEX IF NOT EXISTS idx_credentials_status ON credentials(status);
49
+ CREATE INDEX IF NOT EXISTS idx_audit_events_occurred_at ON audit_events(occurred_at DESC);
@@ -0,0 +1,53 @@
1
+ ALTER TABLE policy_rules
2
+ ADD COLUMN IF NOT EXISTS principal_roles TEXT[] NULL;
3
+
4
+ CREATE TABLE IF NOT EXISTS oauth_clients (
5
+ client_id TEXT PRIMARY KEY,
6
+ display_name TEXT NOT NULL,
7
+ secret_hash TEXT NOT NULL,
8
+ secret_salt TEXT NOT NULL,
9
+ roles TEXT[] NOT NULL,
10
+ allowed_scopes TEXT[] NOT NULL,
11
+ status TEXT NOT NULL,
12
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
13
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
14
+ );
15
+
16
+ CREATE TABLE IF NOT EXISTS access_tokens (
17
+ token_id UUID PRIMARY KEY,
18
+ token_hash TEXT NOT NULL UNIQUE,
19
+ client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,
20
+ subject TEXT NOT NULL,
21
+ scopes TEXT[] NOT NULL,
22
+ roles TEXT[] NOT NULL,
23
+ resource TEXT NULL,
24
+ expires_at TIMESTAMPTZ NOT NULL,
25
+ status TEXT NOT NULL,
26
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
27
+ last_used_at TIMESTAMPTZ NULL
28
+ );
29
+
30
+ CREATE INDEX IF NOT EXISTS idx_access_tokens_expires_at ON access_tokens(expires_at);
31
+
32
+ CREATE TABLE IF NOT EXISTS approval_requests (
33
+ id UUID PRIMARY KEY,
34
+ created_at TIMESTAMPTZ NOT NULL,
35
+ expires_at TIMESTAMPTZ NOT NULL,
36
+ status TEXT NOT NULL,
37
+ requested_by TEXT NOT NULL,
38
+ requested_roles TEXT[] NOT NULL,
39
+ credential_id TEXT NOT NULL,
40
+ operation TEXT NOT NULL,
41
+ target_url TEXT NOT NULL,
42
+ target_host TEXT NOT NULL,
43
+ reason TEXT NOT NULL,
44
+ rule_id TEXT NULL,
45
+ correlation_id UUID NOT NULL,
46
+ fingerprint TEXT NOT NULL,
47
+ reviewed_by TEXT NULL,
48
+ reviewed_at TIMESTAMPTZ NULL,
49
+ review_note TEXT NULL
50
+ );
51
+
52
+ CREATE INDEX IF NOT EXISTS idx_approval_requests_status ON approval_requests(status);
53
+ CREATE INDEX IF NOT EXISTS idx_approval_requests_fingerprint ON approval_requests(fingerprint);
@@ -0,0 +1,9 @@
1
+ CREATE TABLE IF NOT EXISTS request_rate_limits (
2
+ bucket_key TEXT PRIMARY KEY,
3
+ window_started_at TIMESTAMPTZ NOT NULL,
4
+ request_count INTEGER NOT NULL,
5
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
6
+ );
7
+
8
+ CREATE INDEX IF NOT EXISTS idx_request_rate_limits_window_started_at
9
+ ON request_rate_limits(window_started_at);
@@ -0,0 +1,28 @@
1
+ CREATE TABLE IF NOT EXISTS break_glass_requests (
2
+ id UUID PRIMARY KEY,
3
+ created_at TIMESTAMPTZ NOT NULL,
4
+ expires_at TIMESTAMPTZ NOT NULL,
5
+ status TEXT NOT NULL,
6
+ requested_by TEXT NOT NULL,
7
+ requested_roles TEXT[] NOT NULL,
8
+ credential_id TEXT NOT NULL,
9
+ operation TEXT NOT NULL,
10
+ target_url TEXT NOT NULL,
11
+ target_host TEXT NOT NULL,
12
+ justification TEXT NOT NULL,
13
+ requested_duration_seconds INTEGER NOT NULL,
14
+ correlation_id UUID NOT NULL,
15
+ fingerprint TEXT NOT NULL,
16
+ reviewed_by TEXT NULL,
17
+ reviewed_at TIMESTAMPTZ NULL,
18
+ review_note TEXT NULL,
19
+ revoked_by TEXT NULL,
20
+ revoked_at TIMESTAMPTZ NULL,
21
+ revoke_note TEXT NULL
22
+ );
23
+
24
+ CREATE INDEX IF NOT EXISTS idx_break_glass_requests_status
25
+ ON break_glass_requests(status);
26
+
27
+ CREATE INDEX IF NOT EXISTS idx_break_glass_requests_fingerprint
28
+ ON break_glass_requests(fingerprint);
@@ -0,0 +1,11 @@
1
+ ALTER TABLE approval_requests
2
+ ADD COLUMN IF NOT EXISTS required_approvals INTEGER NOT NULL DEFAULT 1,
3
+ ADD COLUMN IF NOT EXISTS approval_count INTEGER NOT NULL DEFAULT 0,
4
+ ADD COLUMN IF NOT EXISTS denial_count INTEGER NOT NULL DEFAULT 0,
5
+ ADD COLUMN IF NOT EXISTS reviews JSONB NOT NULL DEFAULT '[]'::jsonb;
6
+
7
+ ALTER TABLE break_glass_requests
8
+ ADD COLUMN IF NOT EXISTS required_approvals INTEGER NOT NULL DEFAULT 1,
9
+ ADD COLUMN IF NOT EXISTS approval_count INTEGER NOT NULL DEFAULT 0,
10
+ ADD COLUMN IF NOT EXISTS denial_count INTEGER NOT NULL DEFAULT 0,
11
+ ADD COLUMN IF NOT EXISTS reviews JSONB NOT NULL DEFAULT '[]'::jsonb;
@@ -0,0 +1,51 @@
1
+ ALTER TABLE oauth_clients
2
+ ADD COLUMN IF NOT EXISTS token_endpoint_auth_method TEXT NOT NULL DEFAULT 'client_secret_basic';
3
+
4
+ ALTER TABLE oauth_clients
5
+ ADD COLUMN IF NOT EXISTS jwks JSONB NULL;
6
+
7
+ ALTER TABLE oauth_clients
8
+ ALTER COLUMN secret_hash DROP NOT NULL;
9
+
10
+ ALTER TABLE oauth_clients
11
+ ALTER COLUMN secret_salt DROP NOT NULL;
12
+
13
+ CREATE TABLE IF NOT EXISTS oauth_client_assertion_jtis (
14
+ client_id TEXT NOT NULL REFERENCES oauth_clients(client_id) ON DELETE CASCADE,
15
+ jti TEXT NOT NULL,
16
+ expires_at TIMESTAMPTZ NOT NULL,
17
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
18
+ PRIMARY KEY (client_id, jti)
19
+ );
20
+
21
+ CREATE INDEX IF NOT EXISTS idx_oauth_client_assertion_jtis_expires_at
22
+ ON oauth_client_assertion_jtis(expires_at);
23
+
24
+ CREATE TABLE IF NOT EXISTS rotation_runs (
25
+ id UUID PRIMARY KEY,
26
+ credential_id TEXT NOT NULL REFERENCES credentials(id) ON DELETE CASCADE,
27
+ status TEXT NOT NULL,
28
+ source TEXT NOT NULL,
29
+ reason TEXT NOT NULL,
30
+ due_at TIMESTAMPTZ NULL,
31
+ planned_at TIMESTAMPTZ NOT NULL,
32
+ started_at TIMESTAMPTZ NULL,
33
+ completed_at TIMESTAMPTZ NULL,
34
+ planned_by TEXT NOT NULL,
35
+ updated_by TEXT NOT NULL,
36
+ note TEXT NULL,
37
+ target_ref TEXT NULL,
38
+ result_note TEXT NULL,
39
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
40
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
41
+ );
42
+
43
+ CREATE INDEX IF NOT EXISTS idx_rotation_runs_status_due_at
44
+ ON rotation_runs(status, due_at);
45
+
46
+ CREATE INDEX IF NOT EXISTS idx_rotation_runs_credential_id
47
+ ON rotation_runs(credential_id);
48
+
49
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_rotation_runs_open_by_credential
50
+ ON rotation_runs(credential_id)
51
+ WHERE status IN ('pending', 'in_progress');
@@ -0,0 +1,32 @@
1
+ ALTER TABLE credentials
2
+ ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'default';
3
+
4
+ ALTER TABLE policy_rules
5
+ ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'default';
6
+
7
+ ALTER TABLE audit_events
8
+ ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'default';
9
+
10
+ ALTER TABLE oauth_clients
11
+ ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'default';
12
+
13
+ ALTER TABLE access_tokens
14
+ ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'default';
15
+
16
+ ALTER TABLE approval_requests
17
+ ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'default';
18
+
19
+ ALTER TABLE break_glass_requests
20
+ ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'default';
21
+
22
+ ALTER TABLE rotation_runs
23
+ ADD COLUMN IF NOT EXISTS tenant_id TEXT NOT NULL DEFAULT 'default';
24
+
25
+ CREATE INDEX IF NOT EXISTS idx_credentials_tenant_id ON credentials(tenant_id);
26
+ CREATE INDEX IF NOT EXISTS idx_policy_rules_tenant_id ON policy_rules(tenant_id);
27
+ CREATE INDEX IF NOT EXISTS idx_audit_events_tenant_id_occurred_at ON audit_events(tenant_id, occurred_at DESC);
28
+ CREATE INDEX IF NOT EXISTS idx_oauth_clients_tenant_id ON oauth_clients(tenant_id);
29
+ CREATE INDEX IF NOT EXISTS idx_access_tokens_tenant_id ON access_tokens(tenant_id);
30
+ CREATE INDEX IF NOT EXISTS idx_approval_requests_tenant_id_status ON approval_requests(tenant_id, status);
31
+ CREATE INDEX IF NOT EXISTS idx_break_glass_requests_tenant_id_status ON break_glass_requests(tenant_id, status);
32
+ CREATE INDEX IF NOT EXISTS idx_rotation_runs_tenant_id_status ON rotation_runs(tenant_id, status);