@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,54 @@
1
+ {
2
+ "version": 1,
3
+ "clients": [
4
+ {
5
+ "clientId": "keylore-admin-local",
6
+ "displayName": "KeyLore Local Admin",
7
+ "secretRef": "KEYLORE_BOOTSTRAP_ADMIN_CLIENT_SECRET",
8
+ "roles": [
9
+ "admin",
10
+ "auth_admin",
11
+ "operator",
12
+ "maintenance_operator",
13
+ "backup_operator",
14
+ "breakglass_operator",
15
+ "auditor",
16
+ "approver"
17
+ ],
18
+ "allowedScopes": [
19
+ "catalog:read",
20
+ "catalog:write",
21
+ "admin:read",
22
+ "admin:write",
23
+ "auth:read",
24
+ "auth:write",
25
+ "broker:use",
26
+ "sandbox:run",
27
+ "audit:read",
28
+ "approval:read",
29
+ "approval:review",
30
+ "system:read",
31
+ "system:write",
32
+ "backup:read",
33
+ "backup:write",
34
+ "breakglass:request",
35
+ "breakglass:read",
36
+ "breakglass:review",
37
+ "mcp:use"
38
+ ],
39
+ "status": "active"
40
+ },
41
+ {
42
+ "clientId": "keylore-consumer-local",
43
+ "displayName": "KeyLore Local Consumer",
44
+ "secretRef": "KEYLORE_BOOTSTRAP_CONSUMER_CLIENT_SECRET",
45
+ "roles": ["consumer"],
46
+ "allowedScopes": [
47
+ "catalog:read",
48
+ "broker:use",
49
+ "mcp:use"
50
+ ],
51
+ "status": "active"
52
+ }
53
+ ]
54
+ }
@@ -0,0 +1,53 @@
1
+ {
2
+ "version": 1,
3
+ "credentials": [
4
+ {
5
+ "id": "github-readonly-demo",
6
+ "displayName": "GitHub Read-Only Demo Token",
7
+ "service": "github",
8
+ "owner": "platform",
9
+ "scopeTier": "read_only",
10
+ "sensitivity": "high",
11
+ "allowedDomains": ["api.github.com"],
12
+ "permittedOperations": ["http.get"],
13
+ "expiresAt": null,
14
+ "rotationPolicy": "Rotate every 90 days",
15
+ "lastValidatedAt": "2026-03-13T00:00:00.000Z",
16
+ "selectionNotes": "Use for repository metadata, issue lookup, and release inspection. Never use for write operations.",
17
+ "binding": {
18
+ "adapter": "env",
19
+ "ref": "KEYLORE_SECRET_GITHUB_READONLY",
20
+ "authType": "bearer",
21
+ "headerName": "Authorization",
22
+ "headerPrefix": "Bearer ",
23
+ "injectionEnvName": "GITHUB_TOKEN"
24
+ },
25
+ "tags": ["github", "readonly", "demo"],
26
+ "status": "active"
27
+ },
28
+ {
29
+ "id": "npm-readonly-demo",
30
+ "displayName": "npm Registry Read-Only Demo Token",
31
+ "service": "npm",
32
+ "owner": "platform",
33
+ "scopeTier": "read_only",
34
+ "sensitivity": "high",
35
+ "allowedDomains": ["registry.npmjs.org"],
36
+ "permittedOperations": ["http.get"],
37
+ "expiresAt": null,
38
+ "rotationPolicy": "Rotate every 90 days",
39
+ "lastValidatedAt": "2026-03-13T00:00:00.000Z",
40
+ "selectionNotes": "Use for package metadata inspection only. Publishing is intentionally excluded from the default demo policy.",
41
+ "binding": {
42
+ "adapter": "env",
43
+ "ref": "KEYLORE_SECRET_NPM_READONLY",
44
+ "authType": "bearer",
45
+ "headerName": "Authorization",
46
+ "headerPrefix": "Bearer ",
47
+ "injectionEnvName": "NPM_TOKEN"
48
+ },
49
+ "tags": ["npm", "readonly", "demo"],
50
+ "status": "active"
51
+ }
52
+ ]
53
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "version": 1,
3
+ "rules": [
4
+ {
5
+ "id": "allow-local-github-read",
6
+ "effect": "allow",
7
+ "description": "Allow the local operator to inspect GitHub repository metadata via the proxy.",
8
+ "principals": ["local-operator"],
9
+ "credentialIds": ["github-readonly-demo"],
10
+ "operations": ["http.get"],
11
+ "domainPatterns": ["api.github.com"],
12
+ "environments": ["development", "production"]
13
+ },
14
+ {
15
+ "id": "allow-local-npm-read",
16
+ "effect": "allow",
17
+ "description": "Allow the local operator to inspect npm package metadata via the proxy.",
18
+ "principals": ["local-operator"],
19
+ "credentialIds": ["npm-readonly-demo"],
20
+ "operations": ["http.get"],
21
+ "domainPatterns": ["registry.npmjs.org"],
22
+ "environments": ["development", "production"]
23
+ }
24
+ ]
25
+ }
@@ -0,0 +1,143 @@
1
+ function sleep(ms) {
2
+ return new Promise((resolve) => setTimeout(resolve, ms));
3
+ }
4
+ function transientError(error) {
5
+ const message = error instanceof Error ? error.message : String(error);
6
+ return /(timeout|timed out|ETIMEDOUT|ECONNRESET|ECONNREFUSED|EAI_AGAIN|ENETUNREACH|503|502|504)/i.test(message);
7
+ }
8
+ export class SecretAdapterRegistry {
9
+ config;
10
+ telemetry;
11
+ adapters = new Map();
12
+ state = new Map();
13
+ constructor(adapters, config, telemetry) {
14
+ this.config = config;
15
+ this.telemetry = telemetry;
16
+ for (const adapter of adapters) {
17
+ this.adapters.set(adapter.id, adapter);
18
+ this.state.set(adapter.id, { consecutiveFailures: 0 });
19
+ }
20
+ }
21
+ adapterFor(credential) {
22
+ const adapter = this.adapters.get(credential.binding.adapter);
23
+ if (!adapter) {
24
+ throw new Error(`Unsupported secret adapter: ${credential.binding.adapter}`);
25
+ }
26
+ return adapter;
27
+ }
28
+ stateFor(adapterId) {
29
+ let state = this.state.get(adapterId);
30
+ if (!state) {
31
+ state = { consecutiveFailures: 0 };
32
+ this.state.set(adapterId, state);
33
+ }
34
+ return state;
35
+ }
36
+ circuitOpen(adapterId) {
37
+ const state = this.stateFor(adapterId);
38
+ if (state.circuitOpenUntil && state.circuitOpenUntil > Date.now()) {
39
+ return state;
40
+ }
41
+ if (state.circuitOpenUntil && state.circuitOpenUntil <= Date.now()) {
42
+ state.circuitOpenUntil = undefined;
43
+ }
44
+ return undefined;
45
+ }
46
+ markSuccess(adapterId) {
47
+ const state = this.stateFor(adapterId);
48
+ state.consecutiveFailures = 0;
49
+ state.lastError = undefined;
50
+ state.lastSuccessAt = new Date().toISOString();
51
+ state.circuitOpenUntil = undefined;
52
+ }
53
+ markFailure(adapterId, error) {
54
+ const state = this.stateFor(adapterId);
55
+ state.consecutiveFailures += 1;
56
+ state.lastError = error instanceof Error ? error.message : "Unknown adapter error.";
57
+ if (state.consecutiveFailures >= this.config.adapterCircuitBreakerThreshold) {
58
+ state.circuitOpenUntil = Date.now() + this.config.adapterCircuitBreakerCooldownMs;
59
+ }
60
+ return state;
61
+ }
62
+ async execute(adapter, operation, task) {
63
+ const openCircuit = this.circuitOpen(adapter.id);
64
+ if (openCircuit && operation !== "healthcheck") {
65
+ this.telemetry.recordAdapterOperation(adapter.id, operation, "open_circuit");
66
+ throw new Error(`Adapter circuit is open for ${adapter.id} until ${new Date(openCircuit.circuitOpenUntil ?? Date.now()).toISOString()}.`);
67
+ }
68
+ for (let attempt = 1; attempt <= this.config.adapterMaxAttempts; attempt += 1) {
69
+ try {
70
+ const result = await task();
71
+ this.markSuccess(adapter.id);
72
+ this.telemetry.recordAdapterOperation(adapter.id, operation, "success");
73
+ return result;
74
+ }
75
+ catch (error) {
76
+ const state = this.markFailure(adapter.id, error);
77
+ const shouldRetry = attempt < this.config.adapterMaxAttempts &&
78
+ transientError(error) &&
79
+ !state.circuitOpenUntil;
80
+ if (shouldRetry) {
81
+ this.telemetry.recordAdapterOperation(adapter.id, operation, "retry");
82
+ await sleep(this.config.adapterRetryDelayMs * attempt);
83
+ continue;
84
+ }
85
+ this.telemetry.recordAdapterOperation(adapter.id, operation, "error");
86
+ throw error;
87
+ }
88
+ }
89
+ throw new Error(`Adapter operation failed unexpectedly: ${adapter.id}`);
90
+ }
91
+ async resolve(credential) {
92
+ const adapter = this.adapterFor(credential);
93
+ return this.execute(adapter, "resolve", () => adapter.resolve(credential));
94
+ }
95
+ async inspectCredential(credential) {
96
+ const adapter = this.adapterFor(credential);
97
+ try {
98
+ return await this.execute(adapter, "inspect", () => adapter.inspect(credential));
99
+ }
100
+ catch (error) {
101
+ return {
102
+ adapter: credential.binding.adapter,
103
+ ref: credential.binding.ref,
104
+ status: "error",
105
+ resolved: false,
106
+ notes: [],
107
+ error: error instanceof Error ? error.message : "Inspection failed.",
108
+ };
109
+ }
110
+ }
111
+ async healthchecks() {
112
+ return Promise.all(Array.from(this.adapters.values(), async (adapter) => {
113
+ const state = this.stateFor(adapter.id);
114
+ if (this.circuitOpen(adapter.id)) {
115
+ return {
116
+ adapter: adapter.id,
117
+ available: false,
118
+ status: "error",
119
+ details: `Circuit open after repeated failures: ${state.lastError ?? "unknown error"}`,
120
+ };
121
+ }
122
+ try {
123
+ const health = await this.execute(adapter, "healthcheck", () => adapter.healthcheck());
124
+ if (state.consecutiveFailures > 0 && health.status === "ok") {
125
+ return {
126
+ ...health,
127
+ status: "warning",
128
+ details: `${health.details} Recent failures: ${state.consecutiveFailures}.`,
129
+ };
130
+ }
131
+ return health;
132
+ }
133
+ catch (error) {
134
+ return {
135
+ adapter: adapter.id,
136
+ available: false,
137
+ status: "error",
138
+ details: error instanceof Error ? error.message : "Adapter healthcheck failed.",
139
+ };
140
+ }
141
+ }));
142
+ }
143
+ }
@@ -0,0 +1,99 @@
1
+ import { extractField, parseRef } from "./reference-utils.js";
2
+ export class AwsSecretsManagerAdapter {
3
+ commandRunner;
4
+ binary;
5
+ id = "aws_secrets_manager";
6
+ constructor(commandRunner, binary) {
7
+ this.commandRunner = commandRunner;
8
+ this.binary = binary;
9
+ }
10
+ args(baseCommand, credential) {
11
+ const parsed = parseRef(credential.binding.ref);
12
+ const args = ["secretsmanager", baseCommand, "--secret-id", parsed.resource, "--output", "json"];
13
+ const versionId = parsed.query.get("versionId");
14
+ const versionStage = parsed.query.get("versionStage");
15
+ const region = parsed.query.get("region");
16
+ if (versionId) {
17
+ args.push("--version-id", versionId);
18
+ }
19
+ if (versionStage) {
20
+ args.push("--version-stage", versionStage);
21
+ }
22
+ if (region) {
23
+ args.push("--region", region);
24
+ }
25
+ return args;
26
+ }
27
+ async resolve(credential) {
28
+ const parsed = parseRef(credential.binding.ref);
29
+ const result = await this.commandRunner.run(this.binary, this.args("get-secret-value", credential), {
30
+ env: process.env,
31
+ timeoutMs: 15_000,
32
+ });
33
+ const payload = JSON.parse(result.stdout);
34
+ let secret;
35
+ if (typeof payload.SecretString === "string") {
36
+ try {
37
+ secret = extractField(JSON.parse(payload.SecretString), parsed.field);
38
+ }
39
+ catch {
40
+ secret = parsed.field ? extractField({ value: payload.SecretString }, parsed.field) : payload.SecretString;
41
+ }
42
+ }
43
+ else if (typeof payload.SecretBinary === "string") {
44
+ secret = Buffer.from(payload.SecretBinary, "base64").toString("utf8");
45
+ }
46
+ else {
47
+ throw new Error(`AWS Secrets Manager returned no usable secret for ${credential.binding.ref}.`);
48
+ }
49
+ return {
50
+ secret,
51
+ headerName: credential.binding.headerName,
52
+ headerValue: credential.binding.authType === "bearer"
53
+ ? `${credential.binding.headerPrefix ?? "Bearer "}${secret}`
54
+ : secret,
55
+ inspection: await this.inspect(credential),
56
+ };
57
+ }
58
+ async inspect(credential) {
59
+ const result = await this.commandRunner.run(this.binary, this.args("describe-secret", credential), {
60
+ env: process.env,
61
+ timeoutMs: 15_000,
62
+ });
63
+ const payload = JSON.parse(result.stdout);
64
+ return {
65
+ adapter: this.id,
66
+ ref: credential.binding.ref,
67
+ status: "ok",
68
+ resolved: true,
69
+ updatedAt: typeof payload.LastChangedDate === "string" ? payload.LastChangedDate : undefined,
70
+ expiresAt: typeof payload.DeletedDate === "string" ? payload.DeletedDate : undefined,
71
+ nextRotationAt: typeof payload.NextRotationDate === "string" ? payload.NextRotationDate : undefined,
72
+ rotationEnabled: typeof payload.RotationEnabled === "boolean" ? payload.RotationEnabled : undefined,
73
+ state: typeof payload.DeletedDate === "string" ? "scheduled_for_deletion" : "active",
74
+ notes: ["AWS inspection uses describe-secret metadata from the local aws CLI context."],
75
+ };
76
+ }
77
+ async healthcheck() {
78
+ try {
79
+ await this.commandRunner.run(this.binary, ["--version"], {
80
+ env: process.env,
81
+ timeoutMs: 5_000,
82
+ });
83
+ return {
84
+ adapter: this.id,
85
+ available: true,
86
+ status: "ok",
87
+ details: `${this.binary} is available.`,
88
+ };
89
+ }
90
+ catch (error) {
91
+ return {
92
+ adapter: this.id,
93
+ available: false,
94
+ status: "error",
95
+ details: error instanceof Error ? error.message : "AWS CLI unavailable.",
96
+ };
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,17 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const execFileAsync = promisify(execFile);
4
+ export class ExecFileCommandRunner {
5
+ async run(command, args, options) {
6
+ const result = await execFileAsync(command, args, {
7
+ env: options?.env,
8
+ timeout: options?.timeoutMs,
9
+ maxBuffer: 1024 * 1024,
10
+ encoding: "utf8",
11
+ });
12
+ return {
13
+ stdout: result.stdout,
14
+ stderr: result.stderr,
15
+ };
16
+ }
17
+ }
@@ -0,0 +1,42 @@
1
+ export class EnvSecretAdapter {
2
+ id = "env";
3
+ async resolve(credential) {
4
+ const { ref, authType, headerName, headerPrefix } = credential.binding;
5
+ const secret = process.env[ref];
6
+ if (!secret) {
7
+ throw new Error(`Missing secret material in environment variable ${ref}.`);
8
+ }
9
+ const headerValue = authType === "bearer" ? `${headerPrefix ?? "Bearer "}${secret}` : secret;
10
+ return {
11
+ secret,
12
+ headerName,
13
+ headerValue,
14
+ inspection: {
15
+ adapter: "env",
16
+ ref,
17
+ status: "warning",
18
+ resolved: true,
19
+ notes: ["Environment bindings expose no remote rotation or expiry metadata."],
20
+ },
21
+ };
22
+ }
23
+ async inspect(credential) {
24
+ const secret = process.env[credential.binding.ref];
25
+ return {
26
+ adapter: "env",
27
+ ref: credential.binding.ref,
28
+ status: secret ? "warning" : "error",
29
+ resolved: Boolean(secret),
30
+ notes: ["Environment bindings expose no remote rotation or expiry metadata."],
31
+ error: secret ? undefined : `Missing secret material in environment variable ${credential.binding.ref}.`,
32
+ };
33
+ }
34
+ async healthcheck() {
35
+ return {
36
+ adapter: "env",
37
+ available: true,
38
+ status: "warning",
39
+ details: "Environment adapter is always available but provides no source-side health metadata.",
40
+ };
41
+ }
42
+ }
@@ -0,0 +1,129 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { extractField, parseRef } from "./reference-utils.js";
5
+ export class GcpSecretManagerAdapter {
6
+ commandRunner;
7
+ binary;
8
+ id = "gcp_secret_manager";
9
+ constructor(commandRunner, binary) {
10
+ this.commandRunner = commandRunner;
11
+ this.binary = binary;
12
+ }
13
+ parsed(credential) {
14
+ const parsed = parseRef(credential.binding.ref);
15
+ return {
16
+ parsed,
17
+ secret: parsed.resource,
18
+ version: parsed.query.get("version") ?? "latest",
19
+ project: parsed.query.get("project") ?? undefined,
20
+ location: parsed.query.get("location") ?? undefined,
21
+ };
22
+ }
23
+ async resolve(credential) {
24
+ const { parsed, secret, version, project, location } = this.parsed(credential);
25
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "keylore-gcp-"));
26
+ const outFile = path.join(tempDir, "secret.txt");
27
+ try {
28
+ const args = ["secrets", "versions", "access", version, `--secret=${secret}`, `--out-file=${outFile}`, "--quiet"];
29
+ if (project) {
30
+ args.push(`--project=${project}`);
31
+ }
32
+ if (location) {
33
+ args.push(`--location=${location}`);
34
+ }
35
+ await this.commandRunner.run(this.binary, args, {
36
+ env: process.env,
37
+ timeoutMs: 15_000,
38
+ });
39
+ const raw = await fs.readFile(outFile, "utf8");
40
+ let secretValue;
41
+ try {
42
+ secretValue = extractField(JSON.parse(raw), parsed.field);
43
+ }
44
+ catch {
45
+ secretValue = parsed.field ? extractField({ value: raw }, parsed.field) : raw;
46
+ }
47
+ return {
48
+ secret: secretValue,
49
+ headerName: credential.binding.headerName,
50
+ headerValue: credential.binding.authType === "bearer"
51
+ ? `${credential.binding.headerPrefix ?? "Bearer "}${secretValue}`
52
+ : secretValue,
53
+ inspection: await this.inspect(credential),
54
+ };
55
+ }
56
+ finally {
57
+ await fs.rm(tempDir, { recursive: true, force: true });
58
+ }
59
+ }
60
+ async inspect(credential) {
61
+ const { secret, version, project, location } = this.parsed(credential);
62
+ const secretArgs = ["secrets", "describe", secret, "--format=json", "--quiet"];
63
+ const versionArgs = [
64
+ "secrets",
65
+ "versions",
66
+ "describe",
67
+ version,
68
+ `--secret=${secret}`,
69
+ "--format=json",
70
+ "--quiet",
71
+ ];
72
+ if (project) {
73
+ secretArgs.push(`--project=${project}`);
74
+ versionArgs.push(`--project=${project}`);
75
+ }
76
+ if (location) {
77
+ secretArgs.push(`--location=${location}`);
78
+ versionArgs.push(`--location=${location}`);
79
+ }
80
+ const [secretResult, versionResult] = await Promise.all([
81
+ this.commandRunner.run(this.binary, secretArgs, {
82
+ env: process.env,
83
+ timeoutMs: 15_000,
84
+ }),
85
+ this.commandRunner.run(this.binary, versionArgs, {
86
+ env: process.env,
87
+ timeoutMs: 15_000,
88
+ }),
89
+ ]);
90
+ const secretPayload = JSON.parse(secretResult.stdout);
91
+ const versionPayload = JSON.parse(versionResult.stdout);
92
+ const rotation = secretPayload.rotation;
93
+ return {
94
+ adapter: this.id,
95
+ ref: credential.binding.ref,
96
+ status: "ok",
97
+ resolved: true,
98
+ createdAt: typeof versionPayload.createTime === "string" ? versionPayload.createTime : undefined,
99
+ expiresAt: typeof secretPayload.expireTime === "string" ? secretPayload.expireTime : undefined,
100
+ nextRotationAt: rotation && typeof rotation === "object" && typeof rotation.nextRotationTime === "string"
101
+ ? rotation.nextRotationTime
102
+ : undefined,
103
+ state: typeof versionPayload.state === "string" ? versionPayload.state : undefined,
104
+ notes: ["GCP inspection uses gcloud secret and version metadata from the local gcloud context."],
105
+ };
106
+ }
107
+ async healthcheck() {
108
+ try {
109
+ await this.commandRunner.run(this.binary, ["--version"], {
110
+ env: process.env,
111
+ timeoutMs: 5_000,
112
+ });
113
+ return {
114
+ adapter: this.id,
115
+ available: true,
116
+ status: "ok",
117
+ details: `${this.binary} is available.`,
118
+ };
119
+ }
120
+ catch (error) {
121
+ return {
122
+ adapter: this.id,
123
+ available: false,
124
+ status: "error",
125
+ details: error instanceof Error ? error.message : "gcloud CLI unavailable.",
126
+ };
127
+ }
128
+ }
129
+ }
@@ -0,0 +1,54 @@
1
+ export class LocalSecretAdapter {
2
+ store;
3
+ id = "local";
4
+ constructor(store) {
5
+ this.store = store;
6
+ }
7
+ async resolve(credential) {
8
+ const { ref, authType, headerName, headerPrefix } = credential.binding;
9
+ const secret = await this.store.get(ref);
10
+ if (!secret) {
11
+ throw new Error(`Missing secret material in local secret store for ${ref}.`);
12
+ }
13
+ const headerValue = authType === "bearer" ? `${headerPrefix ?? "Bearer "}${secret}` : secret;
14
+ return {
15
+ secret,
16
+ headerName,
17
+ headerValue,
18
+ inspection: {
19
+ adapter: "local",
20
+ ref,
21
+ status: "ok",
22
+ resolved: true,
23
+ notes: ["Secret is stored in the local encrypted file store."],
24
+ },
25
+ };
26
+ }
27
+ async inspect(credential) {
28
+ const inspection = await this.store.inspect(credential.binding.ref);
29
+ return {
30
+ adapter: "local",
31
+ ref: credential.binding.ref,
32
+ status: inspection.resolved ? "ok" : "error",
33
+ resolved: inspection.resolved,
34
+ notes: inspection.resolved
35
+ ? [
36
+ "Secret is stored in the local encrypted file store.",
37
+ inspection.updatedAt ? `Updated at ${inspection.updatedAt}.` : "Update time unavailable.",
38
+ ]
39
+ : ["Secret is not present in the local encrypted file store."],
40
+ error: inspection.resolved
41
+ ? undefined
42
+ : `Missing secret material in local secret store for ${credential.binding.ref}.`,
43
+ };
44
+ }
45
+ async healthcheck() {
46
+ await this.store.healthcheck();
47
+ return {
48
+ adapter: "local",
49
+ available: true,
50
+ status: "ok",
51
+ details: "Local encrypted file secret store is available.",
52
+ };
53
+ }
54
+ }