@reaatech/media-pipeline-mcp-keyvault 0.3.0
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.
- package/LICENSE +21 -0
- package/README.md +194 -0
- package/dist/index.cjs +288 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +134 -0
- package/dist/index.d.ts +134 -0
- package/dist/index.js +258 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Media Pipeline MCP Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# @reaatech/media-pipeline-mcp-keyvault
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@reaatech/media-pipeline-mcp-keyvault)
|
|
4
|
+
[](https://github.com/reaatech/media-pipeline-mcp/blob/main/LICENSE)
|
|
5
|
+
[](https://github.com/reaatech/media-pipeline-mcp/actions/workflows/ci.yml)
|
|
6
|
+
|
|
7
|
+
> **Status:** Pre-1.0 — APIs may change in minor versions. Pin to a specific version in production.
|
|
8
|
+
|
|
9
|
+
Multi-tenant API key vault with AWS Secrets Manager, GCP Secret Manager, environment-variable, and in-memory backends. Provides tenant-scoped provider credential resolution with caching and health checks.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @reaatech/media-pipeline-mcp-keyvault
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add @reaatech/media-pipeline-mcp-keyvault
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Cloud backends require optional peer dependencies:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# AWS Secrets Manager
|
|
25
|
+
pnpm add @aws-sdk/client-secrets-manager
|
|
26
|
+
|
|
27
|
+
# GCP Secret Manager
|
|
28
|
+
pnpm add @google-cloud/secret-manager
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Feature Overview
|
|
32
|
+
|
|
33
|
+
- Four vault backends: in-memory (dev/testing), environment variables, AWS Secrets Manager, GCP Secret Manager
|
|
34
|
+
- Per-tenant provider credential resolution with budget caps, allowed provider/model lists, and metadata
|
|
35
|
+
- LRU-style TTL caching to avoid secret manager round-trips on every call
|
|
36
|
+
- Health check probes per backend
|
|
37
|
+
- Tenant resolution strategies for multi-protocol tenant identification (header, JWT, OAuth scope, mTLS CN, static)
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { InMemoryKeyVault, EnvKeyVault } from '@reaatech/media-pipeline-mcp-keyvault';
|
|
43
|
+
|
|
44
|
+
// Development / testing: seed keys programmatically
|
|
45
|
+
const vault = new InMemoryKeyVault();
|
|
46
|
+
vault.set('acme', {
|
|
47
|
+
openai: 'sk-...',
|
|
48
|
+
stability: 'sk-...',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Production-style: read from environment
|
|
52
|
+
// Expects ACME_OPENAI_API_KEY, ACME_STABILITY_API_KEY env vars
|
|
53
|
+
const envVault = new EnvKeyVault();
|
|
54
|
+
|
|
55
|
+
// Resolve all credentials for a tenant
|
|
56
|
+
const ctx = await vault.resolve('acme');
|
|
57
|
+
console.log(ctx.tenantId);
|
|
58
|
+
// → 'acme'
|
|
59
|
+
console.log(ctx.providerKeys.get('openai'));
|
|
60
|
+
// → 'sk-...'
|
|
61
|
+
|
|
62
|
+
// Get a single key
|
|
63
|
+
const key = await vault.get('acme', 'openai');
|
|
64
|
+
|
|
65
|
+
// Check backend health
|
|
66
|
+
const { healthy, latencyMs } = await vault.health();
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### AWS Secrets Manager
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { AwsSecretsManagerKeyVault } from '@reaatech/media-pipeline-mcp-keyvault';
|
|
73
|
+
|
|
74
|
+
const vault = new AwsSecretsManagerKeyVault({
|
|
75
|
+
region: 'us-east-1',
|
|
76
|
+
secretPrefix: 'mp/tenants', // defaults to 'mp/tenants'
|
|
77
|
+
cacheTtlMs: 300_000, // 5 min — default
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Secret stored at mp/tenants/acme:
|
|
81
|
+
// {
|
|
82
|
+
// "openai": "sk-...",
|
|
83
|
+
// "stability": "sk-...",
|
|
84
|
+
// "budgetCaps": { "dailyUsd": 50 },
|
|
85
|
+
// "allowedProviders": ["openai", "stability"],
|
|
86
|
+
// "metadata": { "tier": "enterprise" }
|
|
87
|
+
// }
|
|
88
|
+
|
|
89
|
+
const ctx = await vault.resolve('acme');
|
|
90
|
+
console.log(ctx.budgetCaps?.dailyUsd); // 50
|
|
91
|
+
console.log(ctx.allowedProviders); // ['openai', 'stability']
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### GCP Secret Manager
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { GcpSecretManagerKeyVault } from '@reaatech/media-pipeline-mcp-keyvault';
|
|
98
|
+
|
|
99
|
+
const vault = new GcpSecretManagerKeyVault({
|
|
100
|
+
projectId: 'my-gcp-project',
|
|
101
|
+
secretPrefix: 'mp-tenants', // defaults to 'mp-tenants'
|
|
102
|
+
cacheTtlMs: 300_000,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Secret stored at projects/my-gcp-project/secrets/mp-tenants-acme/versions/latest
|
|
106
|
+
// Same JSON shape as AWS above.
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## API Reference
|
|
110
|
+
|
|
111
|
+
### Types
|
|
112
|
+
|
|
113
|
+
| Type | Description |
|
|
114
|
+
|------|-------------|
|
|
115
|
+
| `TenantContext` | Resolved tenant: `tenantId`, `providerKeys` (ReadonlyMap), optional `budgetCaps`, `allowedProviders`, `allowedModels`, `metadata` |
|
|
116
|
+
| `KeyVault` | Interface: `resolve(tenantId)`, `get(tenantId, key)`, `health()` |
|
|
117
|
+
| `TenantResolutionStrategy` | Discriminated union of tenant identification strategies: `header`, `jwt`, `oauth-scope`, `mtls-cn`, `static`, `custom` |
|
|
118
|
+
|
|
119
|
+
### Classes
|
|
120
|
+
|
|
121
|
+
| Class | Description |
|
|
122
|
+
|-------|-------------|
|
|
123
|
+
| `InMemoryKeyVault` | Programmatic seed for development and testing. Call `set(tenantId, keys, overrides?)` to register tenants. |
|
|
124
|
+
| `EnvKeyVault` | Reads keys from env vars matching `${TENANT_ID}_${PROVIDER}_API_KEY` (case-insensitive). Provider list is discovered dynamically. |
|
|
125
|
+
| `AwsSecretsManagerKeyVault` | AWS Secrets Manager backend. One secret per tenant at `${prefix}/${tenantId}`, JSON payload. Supports configurable region, credentials, and cache TTL. |
|
|
126
|
+
| `GcpSecretManagerKeyVault` | GCP Secret Manager backend. One secret per tenant at `projects/${projectId}/secrets/${prefix}-${tenantId}`, latest version. Supports service-account key file and cache TTL. |
|
|
127
|
+
|
|
128
|
+
**`AwsSecretsManagerKeyVaultConfig`:**
|
|
129
|
+
|
|
130
|
+
| Option | Type | Default | Description |
|
|
131
|
+
|--------|------|---------|-------------|
|
|
132
|
+
| `region` | `string` | _(required)_ | AWS region |
|
|
133
|
+
| `secretPrefix` | `string` | `'mp/tenants'` | Path prefix for tenant secrets |
|
|
134
|
+
| `cacheTtlMs` | `number` | `300_000` | Cache lifetime in ms |
|
|
135
|
+
| `credentials` | `object` | _(none)_ | Explicit AWS credentials (accessKeyId, secretAccessKey, sessionToken) |
|
|
136
|
+
|
|
137
|
+
**`GcpSecretManagerKeyVaultConfig`:**
|
|
138
|
+
|
|
139
|
+
| Option | Type | Default | Description |
|
|
140
|
+
|--------|------|---------|-------------|
|
|
141
|
+
| `projectId` | `string` | _(required)_ | GCP project ID |
|
|
142
|
+
| `secretPrefix` | `string` | `'mp-tenants'` | Prefix segment before `-${tenantId}` |
|
|
143
|
+
| `cacheTtlMs` | `number` | `300_000` | Cache lifetime in ms |
|
|
144
|
+
| `keyFilename` | `string` | _(none)_ | Service account key file path |
|
|
145
|
+
|
|
146
|
+
### Secret Payload Format
|
|
147
|
+
|
|
148
|
+
All cloud backends expect the same JSON shape per tenant secret:
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"openai": "sk-...",
|
|
153
|
+
"stability": "sk-...",
|
|
154
|
+
"replicate": "r8_...",
|
|
155
|
+
"budgetCaps": { "dailyUsd": 100, "monthlyUsd": 3000 },
|
|
156
|
+
"allowedProviders": ["openai", "stability"],
|
|
157
|
+
"allowedModels": ["gpt-4o-mini", "sd3"],
|
|
158
|
+
"metadata": { "tier": "premium" }
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Top-level string values are treated as provider API keys. The reserved keys `budgetCaps`, `allowedProviders`, `allowedModels`, and `metadata` populate the corresponding `TenantContext` fields.
|
|
163
|
+
|
|
164
|
+
## Usage Patterns
|
|
165
|
+
|
|
166
|
+
### Multi-Provider Tenant Setup
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
const vault = new InMemoryKeyVault();
|
|
170
|
+
vault.set('enterprise', {
|
|
171
|
+
openai: process.env.OPENAI_KEY,
|
|
172
|
+
anthropic: process.env.ANTHROPIC_KEY,
|
|
173
|
+
google: process.env.GOOGLE_KEY,
|
|
174
|
+
}, {
|
|
175
|
+
budgetCaps: { dailyUsd: 500, monthlyUsd: 10000 },
|
|
176
|
+
allowedProviders: ['openai', 'anthropic', 'google'],
|
|
177
|
+
allowedModels: ['gpt-4o', 'claude-3-opus', 'gemini-1.5-pro'],
|
|
178
|
+
metadata: { tier: 'enterprise' },
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Health Check
|
|
183
|
+
|
|
184
|
+
The `health()` method returns `{ healthy, latencyMs }`. Cloud backends issue a synthetic secret lookup and treat `NOT_FOUND` / `ResourceNotFoundException` as healthy (the service responded); other errors indicate unavailability.
|
|
185
|
+
|
|
186
|
+
## Related Packages
|
|
187
|
+
|
|
188
|
+
- [@reaatech/media-pipeline-mcp-core](https://www.npmjs.com/package/@reaatech/media-pipeline-mcp-core) — `TenantNotFoundError` and `KeyVaultUnavailableError` types
|
|
189
|
+
- [@reaatech/media-pipeline-mcp-cost](https://www.npmjs.com/package/@reaatech/media-pipeline-mcp-cost) — Budget caps resolved from this vault feed into cost preflight checks
|
|
190
|
+
- [@reaatech/media-pipeline-mcp](https://www.npmjs.com/package/@reaatech/media-pipeline-mcp) — Full MCP server consuming tenant credentials from this vault
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
[MIT](https://github.com/reaatech/media-pipeline-mcp/blob/main/LICENSE)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AwsSecretsManagerKeyVault: () => AwsSecretsManagerKeyVault,
|
|
24
|
+
EnvKeyVault: () => EnvKeyVault,
|
|
25
|
+
GcpSecretManagerKeyVault: () => GcpSecretManagerKeyVault,
|
|
26
|
+
InMemoryKeyVault: () => InMemoryKeyVault
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/vaults.ts
|
|
31
|
+
var import_media_pipeline_mcp_core = require("@reaatech/media-pipeline-mcp-core");
|
|
32
|
+
var InMemoryKeyVault = class {
|
|
33
|
+
tenants = /* @__PURE__ */ new Map();
|
|
34
|
+
set(tenantId, keys, overrides) {
|
|
35
|
+
this.tenants.set(tenantId, { keys, overrides });
|
|
36
|
+
}
|
|
37
|
+
async resolve(tenantId) {
|
|
38
|
+
const entry = this.tenants.get(tenantId);
|
|
39
|
+
if (!entry) throw new import_media_pipeline_mcp_core.TenantNotFoundError();
|
|
40
|
+
return {
|
|
41
|
+
tenantId,
|
|
42
|
+
providerKeys: new Map(Object.entries(entry.keys)),
|
|
43
|
+
...entry.overrides
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async get(tenantId, key) {
|
|
47
|
+
const entry = this.tenants.get(tenantId);
|
|
48
|
+
return entry?.keys[key] ?? null;
|
|
49
|
+
}
|
|
50
|
+
async health() {
|
|
51
|
+
return { healthy: true, latencyMs: 0 };
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var EnvKeyVault = class {
|
|
55
|
+
async resolve(tenantId) {
|
|
56
|
+
const keys = {};
|
|
57
|
+
const prefix = `${tenantId.toUpperCase()}_`;
|
|
58
|
+
const suffix = "_API_KEY";
|
|
59
|
+
for (const [envKey, val] of Object.entries(process.env)) {
|
|
60
|
+
if (envKey.startsWith(prefix) && envKey.endsWith(suffix) && typeof val === "string") {
|
|
61
|
+
const provider = envKey.slice(prefix.length, envKey.length - suffix.length).toLowerCase();
|
|
62
|
+
if (provider.length > 0) keys[provider] = val;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (Object.keys(keys).length === 0) {
|
|
66
|
+
throw new import_media_pipeline_mcp_core.TenantNotFoundError();
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
tenantId,
|
|
70
|
+
providerKeys: new Map(Object.entries(keys))
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async get(tenantId, key) {
|
|
74
|
+
const envKey = `${tenantId.toUpperCase()}_${key.toUpperCase()}`;
|
|
75
|
+
return process.env[envKey] ?? null;
|
|
76
|
+
}
|
|
77
|
+
async health() {
|
|
78
|
+
return { healthy: true, latencyMs: 0 };
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// src/aws-secrets-vault.ts
|
|
83
|
+
var import_client_secrets_manager = require("@aws-sdk/client-secrets-manager");
|
|
84
|
+
var import_media_pipeline_mcp_core2 = require("@reaatech/media-pipeline-mcp-core");
|
|
85
|
+
var AwsSecretsManagerKeyVault = class {
|
|
86
|
+
client;
|
|
87
|
+
cache = /* @__PURE__ */ new Map();
|
|
88
|
+
cacheTtlMs;
|
|
89
|
+
secretPrefix;
|
|
90
|
+
constructor(config) {
|
|
91
|
+
this.cacheTtlMs = config.cacheTtlMs ?? 3e5;
|
|
92
|
+
this.secretPrefix = config.secretPrefix ?? "mp/tenants";
|
|
93
|
+
this.client = new import_client_secrets_manager.SecretsManagerClient({
|
|
94
|
+
region: config.region,
|
|
95
|
+
...config.credentials ? { credentials: config.credentials } : {}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
secretName(tenantId) {
|
|
99
|
+
return `${this.secretPrefix}/${tenantId}`;
|
|
100
|
+
}
|
|
101
|
+
async resolve(tenantId) {
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
const cached = this.cache.get(tenantId);
|
|
104
|
+
if (cached && cached.expiresAtMs > now) {
|
|
105
|
+
return cached.context;
|
|
106
|
+
}
|
|
107
|
+
let payload;
|
|
108
|
+
try {
|
|
109
|
+
const response = await this.client.send(
|
|
110
|
+
new import_client_secrets_manager.GetSecretValueCommand({ SecretId: this.secretName(tenantId) })
|
|
111
|
+
);
|
|
112
|
+
payload = response.SecretString;
|
|
113
|
+
} catch (err) {
|
|
114
|
+
const name = err?.name;
|
|
115
|
+
if (name === "ResourceNotFoundException") {
|
|
116
|
+
throw new import_media_pipeline_mcp_core2.TenantNotFoundError();
|
|
117
|
+
}
|
|
118
|
+
throw new import_media_pipeline_mcp_core2.KeyVaultUnavailableError();
|
|
119
|
+
}
|
|
120
|
+
if (!payload) {
|
|
121
|
+
throw new import_media_pipeline_mcp_core2.TenantNotFoundError();
|
|
122
|
+
}
|
|
123
|
+
let parsed;
|
|
124
|
+
try {
|
|
125
|
+
parsed = JSON.parse(payload);
|
|
126
|
+
} catch {
|
|
127
|
+
throw new import_media_pipeline_mcp_core2.KeyVaultUnavailableError();
|
|
128
|
+
}
|
|
129
|
+
const { providerKeys, extras } = splitTenantPayload(parsed);
|
|
130
|
+
const context = {
|
|
131
|
+
tenantId,
|
|
132
|
+
providerKeys,
|
|
133
|
+
...extras.budgetCaps ? { budgetCaps: extras.budgetCaps } : {},
|
|
134
|
+
...extras.allowedProviders ? { allowedProviders: extras.allowedProviders } : {},
|
|
135
|
+
...extras.allowedModels ? { allowedModels: extras.allowedModels } : {},
|
|
136
|
+
...extras.metadata ? { metadata: extras.metadata } : {}
|
|
137
|
+
};
|
|
138
|
+
this.cache.set(tenantId, { context, expiresAtMs: now + this.cacheTtlMs });
|
|
139
|
+
return context;
|
|
140
|
+
}
|
|
141
|
+
async get(tenantId, key) {
|
|
142
|
+
try {
|
|
143
|
+
const context = await this.resolve(tenantId);
|
|
144
|
+
return context.providerKeys.get(key) ?? null;
|
|
145
|
+
} catch (err) {
|
|
146
|
+
if (err instanceof import_media_pipeline_mcp_core2.TenantNotFoundError) return null;
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async health() {
|
|
151
|
+
const start = Date.now();
|
|
152
|
+
try {
|
|
153
|
+
await this.client.send(
|
|
154
|
+
new import_client_secrets_manager.GetSecretValueCommand({ SecretId: `${this.secretPrefix}/__health_probe__` })
|
|
155
|
+
);
|
|
156
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
157
|
+
} catch (err) {
|
|
158
|
+
const name = err?.name;
|
|
159
|
+
if (name === "ResourceNotFoundException") {
|
|
160
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
161
|
+
}
|
|
162
|
+
return { healthy: false, latencyMs: Date.now() - start };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
function splitTenantPayload(parsed) {
|
|
167
|
+
const reserved = /* @__PURE__ */ new Set(["budgetCaps", "allowedProviders", "allowedModels", "metadata"]);
|
|
168
|
+
const providerKeys = /* @__PURE__ */ new Map();
|
|
169
|
+
const extras = {};
|
|
170
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
171
|
+
if (reserved.has(k)) {
|
|
172
|
+
extras[k] = v;
|
|
173
|
+
} else if (typeof v === "string") {
|
|
174
|
+
providerKeys.set(k, v);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { providerKeys, extras };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/gcp-secret-vault.ts
|
|
181
|
+
var import_secret_manager = require("@google-cloud/secret-manager");
|
|
182
|
+
var import_media_pipeline_mcp_core3 = require("@reaatech/media-pipeline-mcp-core");
|
|
183
|
+
var GcpSecretManagerKeyVault = class {
|
|
184
|
+
client;
|
|
185
|
+
cache = /* @__PURE__ */ new Map();
|
|
186
|
+
cacheTtlMs;
|
|
187
|
+
projectId;
|
|
188
|
+
secretPrefix;
|
|
189
|
+
constructor(config) {
|
|
190
|
+
this.cacheTtlMs = config.cacheTtlMs ?? 3e5;
|
|
191
|
+
this.projectId = config.projectId;
|
|
192
|
+
this.secretPrefix = config.secretPrefix ?? "mp-tenants";
|
|
193
|
+
this.client = new import_secret_manager.SecretManagerServiceClient(
|
|
194
|
+
config.keyFilename ? { keyFilename: config.keyFilename } : void 0
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
secretVersionName(tenantId) {
|
|
198
|
+
return `projects/${this.projectId}/secrets/${this.secretPrefix}-${tenantId}/versions/latest`;
|
|
199
|
+
}
|
|
200
|
+
async resolve(tenantId) {
|
|
201
|
+
const now = Date.now();
|
|
202
|
+
const cached = this.cache.get(tenantId);
|
|
203
|
+
if (cached && cached.expiresAtMs > now) {
|
|
204
|
+
return cached.context;
|
|
205
|
+
}
|
|
206
|
+
let payload;
|
|
207
|
+
try {
|
|
208
|
+
const [response] = await this.client.accessSecretVersion({
|
|
209
|
+
name: this.secretVersionName(tenantId)
|
|
210
|
+
});
|
|
211
|
+
const data = response?.payload?.data;
|
|
212
|
+
if (data) {
|
|
213
|
+
payload = typeof data === "string" ? data : Buffer.from(data).toString("utf8");
|
|
214
|
+
}
|
|
215
|
+
} catch (err) {
|
|
216
|
+
const code = err?.code;
|
|
217
|
+
if (code === 5 || code === "NOT_FOUND") {
|
|
218
|
+
throw new import_media_pipeline_mcp_core3.TenantNotFoundError();
|
|
219
|
+
}
|
|
220
|
+
throw new import_media_pipeline_mcp_core3.KeyVaultUnavailableError();
|
|
221
|
+
}
|
|
222
|
+
if (!payload) {
|
|
223
|
+
throw new import_media_pipeline_mcp_core3.TenantNotFoundError();
|
|
224
|
+
}
|
|
225
|
+
let parsed;
|
|
226
|
+
try {
|
|
227
|
+
parsed = JSON.parse(payload);
|
|
228
|
+
} catch {
|
|
229
|
+
throw new import_media_pipeline_mcp_core3.KeyVaultUnavailableError();
|
|
230
|
+
}
|
|
231
|
+
const { providerKeys, extras } = splitTenantPayload2(parsed);
|
|
232
|
+
const context = {
|
|
233
|
+
tenantId,
|
|
234
|
+
providerKeys,
|
|
235
|
+
...extras.budgetCaps ? { budgetCaps: extras.budgetCaps } : {},
|
|
236
|
+
...extras.allowedProviders ? { allowedProviders: extras.allowedProviders } : {},
|
|
237
|
+
...extras.allowedModels ? { allowedModels: extras.allowedModels } : {},
|
|
238
|
+
...extras.metadata ? { metadata: extras.metadata } : {}
|
|
239
|
+
};
|
|
240
|
+
this.cache.set(tenantId, { context, expiresAtMs: now + this.cacheTtlMs });
|
|
241
|
+
return context;
|
|
242
|
+
}
|
|
243
|
+
async get(tenantId, key) {
|
|
244
|
+
try {
|
|
245
|
+
const context = await this.resolve(tenantId);
|
|
246
|
+
return context.providerKeys.get(key) ?? null;
|
|
247
|
+
} catch (err) {
|
|
248
|
+
if (err instanceof import_media_pipeline_mcp_core3.TenantNotFoundError) return null;
|
|
249
|
+
throw err;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async health() {
|
|
253
|
+
const start = Date.now();
|
|
254
|
+
try {
|
|
255
|
+
await this.client.accessSecretVersion({
|
|
256
|
+
name: this.secretVersionName("__health_probe__")
|
|
257
|
+
});
|
|
258
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
259
|
+
} catch (err) {
|
|
260
|
+
const code = err?.code;
|
|
261
|
+
if (code === 5 || code === "NOT_FOUND") {
|
|
262
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
263
|
+
}
|
|
264
|
+
return { healthy: false, latencyMs: Date.now() - start };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
function splitTenantPayload2(parsed) {
|
|
269
|
+
const reserved = /* @__PURE__ */ new Set(["budgetCaps", "allowedProviders", "allowedModels", "metadata"]);
|
|
270
|
+
const providerKeys = /* @__PURE__ */ new Map();
|
|
271
|
+
const extras = {};
|
|
272
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
273
|
+
if (reserved.has(k)) {
|
|
274
|
+
extras[k] = v;
|
|
275
|
+
} else if (typeof v === "string") {
|
|
276
|
+
providerKeys.set(k, v);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return { providerKeys, extras };
|
|
280
|
+
}
|
|
281
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
282
|
+
0 && (module.exports = {
|
|
283
|
+
AwsSecretsManagerKeyVault,
|
|
284
|
+
EnvKeyVault,
|
|
285
|
+
GcpSecretManagerKeyVault,
|
|
286
|
+
InMemoryKeyVault
|
|
287
|
+
});
|
|
288
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/vaults.ts","../src/aws-secrets-vault.ts","../src/gcp-secret-vault.ts"],"sourcesContent":["export { InMemoryKeyVault, EnvKeyVault } from './vaults.js';\nexport { AwsSecretsManagerKeyVault } from './aws-secrets-vault.js';\nexport type { AwsSecretsManagerKeyVaultConfig } from './aws-secrets-vault.js';\nexport { GcpSecretManagerKeyVault } from './gcp-secret-vault.js';\nexport type { GcpSecretManagerKeyVaultConfig } from './gcp-secret-vault.js';\nexport * from './types.js';\n","import { TenantNotFoundError } from '@reaatech/media-pipeline-mcp-core';\nimport type { KeyVault, TenantContext } from './types.js';\n\nexport class InMemoryKeyVault implements KeyVault {\n private tenants = new Map<\n string,\n { keys: Record<string, string>; overrides?: Partial<TenantContext> }\n >();\n\n set(tenantId: string, keys: Record<string, string>, overrides?: Partial<TenantContext>): void {\n this.tenants.set(tenantId, { keys, overrides });\n }\n\n async resolve(tenantId: string): Promise<TenantContext> {\n const entry = this.tenants.get(tenantId);\n // Missing tenant → TenantNotFoundError (non-retryable, caller error). The earlier\n // KeyVaultUnavailableError was wrong: that signals infra outage and is retryable.\n if (!entry) throw new TenantNotFoundError();\n return {\n tenantId,\n providerKeys: new Map(Object.entries(entry.keys)),\n ...entry.overrides,\n };\n }\n\n async get(tenantId: string, key: string): Promise<string | null> {\n const entry = this.tenants.get(tenantId);\n return entry?.keys[key] ?? null;\n }\n\n async health(): Promise<{ healthy: boolean; latencyMs: number }> {\n return { healthy: true, latencyMs: 0 };\n }\n}\n\n/**\n * Reads per-tenant provider keys from environment variables matching\n * `${TENANT_ID}_${PROVIDER}_API_KEY` (e.g. `ACME_OPENAI_API_KEY`). Provider list is\n * discovered dynamically from env vars matching the pattern — adding a new provider\n * just means setting `${TENANT_ID}_${NEWPROVIDER}_API_KEY` without code changes.\n */\nexport class EnvKeyVault implements KeyVault {\n async resolve(tenantId: string): Promise<TenantContext> {\n const keys: Record<string, string> = {};\n const prefix = `${tenantId.toUpperCase()}_`;\n const suffix = '_API_KEY';\n for (const [envKey, val] of Object.entries(process.env)) {\n if (envKey.startsWith(prefix) && envKey.endsWith(suffix) && typeof val === 'string') {\n const provider = envKey.slice(prefix.length, envKey.length - suffix.length).toLowerCase();\n if (provider.length > 0) keys[provider] = val;\n }\n }\n if (Object.keys(keys).length === 0) {\n throw new TenantNotFoundError();\n }\n return {\n tenantId,\n providerKeys: new Map(Object.entries(keys)),\n };\n }\n\n async get(tenantId: string, key: string): Promise<string | null> {\n const envKey = `${tenantId.toUpperCase()}_${key.toUpperCase()}`;\n return process.env[envKey] ?? null;\n }\n\n async health(): Promise<{ healthy: boolean; latencyMs: number }> {\n return { healthy: true, latencyMs: 0 };\n }\n}\n","import { GetSecretValueCommand, SecretsManagerClient } from '@aws-sdk/client-secrets-manager';\nimport { KeyVaultUnavailableError, TenantNotFoundError } from '@reaatech/media-pipeline-mcp-core';\nimport type { KeyVault, TenantContext } from './types.js';\n\nexport interface AwsSecretsManagerKeyVaultConfig {\n region: string;\n /** Secret name pattern with `${tenantId}` placeholder. Default: `mp/tenants/${tenantId}`. */\n secretPrefix?: string;\n /** Cache resolved TenantContext for this many ms. Default 300_000 (5 min). */\n cacheTtlMs?: number;\n /** Optional AWS credentials override. Falls back to default credential chain. */\n credentials?: { accessKeyId: string; secretAccessKey: string; sessionToken?: string };\n}\n\ninterface CacheEntry {\n context: TenantContext;\n expiresAtMs: number;\n}\n\n/**\n * Resolves per-tenant provider keys from AWS Secrets Manager.\n *\n * Storage convention: one secret per tenant at `${secretPrefix}/${tenantId}` (default\n * `mp/tenants/${tenantId}`), value is JSON `{ \"openai\": \"sk-...\", \"anthropic\": \"sk-...\" }`.\n * Extended fields are passed through to `TenantContext.metadata`.\n */\nexport class AwsSecretsManagerKeyVault implements KeyVault {\n private client: SecretsManagerClient;\n private cache = new Map<string, CacheEntry>();\n private cacheTtlMs: number;\n private secretPrefix: string;\n\n constructor(config: AwsSecretsManagerKeyVaultConfig) {\n this.cacheTtlMs = config.cacheTtlMs ?? 300_000;\n this.secretPrefix = config.secretPrefix ?? 'mp/tenants';\n this.client = new SecretsManagerClient({\n region: config.region,\n ...(config.credentials ? { credentials: config.credentials } : {}),\n });\n }\n\n private secretName(tenantId: string): string {\n return `${this.secretPrefix}/${tenantId}`;\n }\n\n async resolve(tenantId: string): Promise<TenantContext> {\n const now = Date.now();\n const cached = this.cache.get(tenantId);\n if (cached && cached.expiresAtMs > now) {\n return cached.context;\n }\n\n let payload: string | undefined;\n try {\n const response = await this.client.send(\n new GetSecretValueCommand({ SecretId: this.secretName(tenantId) }),\n );\n payload = response.SecretString;\n } catch (err) {\n const name = (err as Error & { name?: string })?.name;\n // The AWS SDK throws ResourceNotFoundException for unknown secrets — that's a\n // tenant-not-found signal, not infra unavailability.\n if (name === 'ResourceNotFoundException') {\n throw new TenantNotFoundError();\n }\n throw new KeyVaultUnavailableError();\n }\n\n if (!payload) {\n throw new TenantNotFoundError();\n }\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(payload) as Record<string, unknown>;\n } catch {\n throw new KeyVaultUnavailableError();\n }\n\n const { providerKeys, extras } = splitTenantPayload(parsed);\n const context: TenantContext = {\n tenantId,\n providerKeys,\n ...(extras.budgetCaps\n ? { budgetCaps: extras.budgetCaps as TenantContext['budgetCaps'] }\n : {}),\n ...(extras.allowedProviders ? { allowedProviders: extras.allowedProviders as string[] } : {}),\n ...(extras.allowedModels ? { allowedModels: extras.allowedModels as string[] } : {}),\n ...(extras.metadata ? { metadata: extras.metadata as Record<string, unknown> } : {}),\n };\n\n this.cache.set(tenantId, { context, expiresAtMs: now + this.cacheTtlMs });\n return context;\n }\n\n async get(tenantId: string, key: string): Promise<string | null> {\n try {\n const context = await this.resolve(tenantId);\n return context.providerKeys.get(key) ?? null;\n } catch (err) {\n if (err instanceof TenantNotFoundError) return null;\n throw err;\n }\n }\n\n async health(): Promise<{ healthy: boolean; latencyMs: number }> {\n const start = Date.now();\n try {\n // No cheap \"list\" API in Secrets Manager — issue a dummy GetSecretValue and treat\n // ResourceNotFoundException as healthy (the service responded). Anything else\n // (auth failure, network) is unhealthy.\n await this.client.send(\n new GetSecretValueCommand({ SecretId: `${this.secretPrefix}/__health_probe__` }),\n );\n return { healthy: true, latencyMs: Date.now() - start };\n } catch (err) {\n const name = (err as Error & { name?: string })?.name;\n if (name === 'ResourceNotFoundException') {\n return { healthy: true, latencyMs: Date.now() - start };\n }\n return { healthy: false, latencyMs: Date.now() - start };\n }\n }\n}\n\n/**\n * Split the parsed secret payload into the `providerKeys` map and the optional\n * TenantContext extras. The convention is: top-level string values are provider keys;\n * the reserved keys `budgetCaps`, `allowedProviders`, `allowedModels`, `metadata` are\n * passed through to the TenantContext.\n */\nfunction splitTenantPayload(parsed: Record<string, unknown>): {\n providerKeys: Map<string, string>;\n extras: Record<string, unknown>;\n} {\n const reserved = new Set(['budgetCaps', 'allowedProviders', 'allowedModels', 'metadata']);\n const providerKeys = new Map<string, string>();\n const extras: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(parsed)) {\n if (reserved.has(k)) {\n extras[k] = v;\n } else if (typeof v === 'string') {\n providerKeys.set(k, v);\n }\n }\n return { providerKeys, extras };\n}\n","import { SecretManagerServiceClient } from '@google-cloud/secret-manager';\nimport { KeyVaultUnavailableError, TenantNotFoundError } from '@reaatech/media-pipeline-mcp-core';\nimport type { KeyVault, TenantContext } from './types.js';\n\nexport interface GcpSecretManagerKeyVaultConfig {\n projectId: string;\n /** Secret name pattern (the trailing segment under `projects/<id>/secrets/`). Default `mp-tenants`. */\n secretPrefix?: string;\n /** Cache resolved TenantContext for this many ms. Default 300_000 (5 min). */\n cacheTtlMs?: number;\n /** Optional service-account key file path. Falls back to default credentials. */\n keyFilename?: string;\n}\n\ninterface CacheEntry {\n context: TenantContext;\n expiresAtMs: number;\n}\n\n/**\n * Resolves per-tenant provider keys from GCP Secret Manager.\n *\n * Storage convention: one secret per tenant at `projects/${projectId}/secrets/${secretPrefix}-${tenantId}`,\n * latest version's payload is JSON `{ \"openai\": \"sk-...\", ... }` plus optional reserved\n * keys (budgetCaps, allowedProviders, allowedModels, metadata).\n */\nexport class GcpSecretManagerKeyVault implements KeyVault {\n private client: SecretManagerServiceClient;\n private cache = new Map<string, CacheEntry>();\n private cacheTtlMs: number;\n private projectId: string;\n private secretPrefix: string;\n\n constructor(config: GcpSecretManagerKeyVaultConfig) {\n this.cacheTtlMs = config.cacheTtlMs ?? 300_000;\n this.projectId = config.projectId;\n this.secretPrefix = config.secretPrefix ?? 'mp-tenants';\n this.client = new SecretManagerServiceClient(\n config.keyFilename ? { keyFilename: config.keyFilename } : undefined,\n );\n }\n\n private secretVersionName(tenantId: string): string {\n return `projects/${this.projectId}/secrets/${this.secretPrefix}-${tenantId}/versions/latest`;\n }\n\n async resolve(tenantId: string): Promise<TenantContext> {\n const now = Date.now();\n const cached = this.cache.get(tenantId);\n if (cached && cached.expiresAtMs > now) {\n return cached.context;\n }\n\n let payload: string | undefined;\n try {\n const [response] = await this.client.accessSecretVersion({\n name: this.secretVersionName(tenantId),\n });\n const data = response?.payload?.data;\n if (data) {\n payload = typeof data === 'string' ? data : Buffer.from(data).toString('utf8');\n }\n } catch (err) {\n const code = (err as Error & { code?: number | string })?.code;\n // gRPC code 5 = NOT_FOUND. Treat as tenant-not-found; anything else as infra-down.\n if (code === 5 || code === 'NOT_FOUND') {\n throw new TenantNotFoundError();\n }\n throw new KeyVaultUnavailableError();\n }\n\n if (!payload) {\n throw new TenantNotFoundError();\n }\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(payload) as Record<string, unknown>;\n } catch {\n throw new KeyVaultUnavailableError();\n }\n\n const { providerKeys, extras } = splitTenantPayload(parsed);\n const context: TenantContext = {\n tenantId,\n providerKeys,\n ...(extras.budgetCaps\n ? { budgetCaps: extras.budgetCaps as TenantContext['budgetCaps'] }\n : {}),\n ...(extras.allowedProviders ? { allowedProviders: extras.allowedProviders as string[] } : {}),\n ...(extras.allowedModels ? { allowedModels: extras.allowedModels as string[] } : {}),\n ...(extras.metadata ? { metadata: extras.metadata as Record<string, unknown> } : {}),\n };\n\n this.cache.set(tenantId, { context, expiresAtMs: now + this.cacheTtlMs });\n return context;\n }\n\n async get(tenantId: string, key: string): Promise<string | null> {\n try {\n const context = await this.resolve(tenantId);\n return context.providerKeys.get(key) ?? null;\n } catch (err) {\n if (err instanceof TenantNotFoundError) return null;\n throw err;\n }\n }\n\n async health(): Promise<{ healthy: boolean; latencyMs: number }> {\n const start = Date.now();\n try {\n // Probe with a synthetic tenantId. NOT_FOUND means the service is reachable.\n await this.client.accessSecretVersion({\n name: this.secretVersionName('__health_probe__'),\n });\n return { healthy: true, latencyMs: Date.now() - start };\n } catch (err) {\n const code = (err as Error & { code?: number | string })?.code;\n if (code === 5 || code === 'NOT_FOUND') {\n return { healthy: true, latencyMs: Date.now() - start };\n }\n return { healthy: false, latencyMs: Date.now() - start };\n }\n }\n}\n\nfunction splitTenantPayload(parsed: Record<string, unknown>): {\n providerKeys: Map<string, string>;\n extras: Record<string, unknown>;\n} {\n const reserved = new Set(['budgetCaps', 'allowedProviders', 'allowedModels', 'metadata']);\n const providerKeys = new Map<string, string>();\n const extras: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(parsed)) {\n if (reserved.has(k)) {\n extras[k] = v;\n } else if (typeof v === 'string') {\n providerKeys.set(k, v);\n }\n }\n return { providerKeys, extras };\n}\n"],"mappings":"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","names":["import_media_pipeline_mcp_core","import_media_pipeline_mcp_core","splitTenantPayload"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
interface TenantContext {
|
|
2
|
+
tenantId: string;
|
|
3
|
+
providerKeys: ReadonlyMap<string, string>;
|
|
4
|
+
budgetCaps?: {
|
|
5
|
+
dailyUsd?: number;
|
|
6
|
+
monthlyUsd?: number;
|
|
7
|
+
};
|
|
8
|
+
allowedProviders?: string[];
|
|
9
|
+
allowedModels?: string[];
|
|
10
|
+
metadata?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
interface KeyVault {
|
|
13
|
+
resolve(tenantId: string): Promise<TenantContext>;
|
|
14
|
+
get(tenantId: string, key: string): Promise<string | null>;
|
|
15
|
+
health(): Promise<{
|
|
16
|
+
healthy: boolean;
|
|
17
|
+
latencyMs: number;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
type TenantResolutionStrategy = {
|
|
21
|
+
kind: 'header';
|
|
22
|
+
headerName: string;
|
|
23
|
+
} | {
|
|
24
|
+
kind: 'jwt';
|
|
25
|
+
jwksUri: string;
|
|
26
|
+
claim: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'oauth-scope';
|
|
29
|
+
scope: string;
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'mtls-cn';
|
|
32
|
+
} | {
|
|
33
|
+
kind: 'static';
|
|
34
|
+
tenantId: string;
|
|
35
|
+
} | {
|
|
36
|
+
kind: 'custom';
|
|
37
|
+
resolver: {
|
|
38
|
+
resolve(request: unknown): Promise<TenantContext | null>;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
declare class InMemoryKeyVault implements KeyVault {
|
|
43
|
+
private tenants;
|
|
44
|
+
set(tenantId: string, keys: Record<string, string>, overrides?: Partial<TenantContext>): void;
|
|
45
|
+
resolve(tenantId: string): Promise<TenantContext>;
|
|
46
|
+
get(tenantId: string, key: string): Promise<string | null>;
|
|
47
|
+
health(): Promise<{
|
|
48
|
+
healthy: boolean;
|
|
49
|
+
latencyMs: number;
|
|
50
|
+
}>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Reads per-tenant provider keys from environment variables matching
|
|
54
|
+
* `${TENANT_ID}_${PROVIDER}_API_KEY` (e.g. `ACME_OPENAI_API_KEY`). Provider list is
|
|
55
|
+
* discovered dynamically from env vars matching the pattern — adding a new provider
|
|
56
|
+
* just means setting `${TENANT_ID}_${NEWPROVIDER}_API_KEY` without code changes.
|
|
57
|
+
*/
|
|
58
|
+
declare class EnvKeyVault implements KeyVault {
|
|
59
|
+
resolve(tenantId: string): Promise<TenantContext>;
|
|
60
|
+
get(tenantId: string, key: string): Promise<string | null>;
|
|
61
|
+
health(): Promise<{
|
|
62
|
+
healthy: boolean;
|
|
63
|
+
latencyMs: number;
|
|
64
|
+
}>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface AwsSecretsManagerKeyVaultConfig {
|
|
68
|
+
region: string;
|
|
69
|
+
/** Secret name pattern with `${tenantId}` placeholder. Default: `mp/tenants/${tenantId}`. */
|
|
70
|
+
secretPrefix?: string;
|
|
71
|
+
/** Cache resolved TenantContext for this many ms. Default 300_000 (5 min). */
|
|
72
|
+
cacheTtlMs?: number;
|
|
73
|
+
/** Optional AWS credentials override. Falls back to default credential chain. */
|
|
74
|
+
credentials?: {
|
|
75
|
+
accessKeyId: string;
|
|
76
|
+
secretAccessKey: string;
|
|
77
|
+
sessionToken?: string;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolves per-tenant provider keys from AWS Secrets Manager.
|
|
82
|
+
*
|
|
83
|
+
* Storage convention: one secret per tenant at `${secretPrefix}/${tenantId}` (default
|
|
84
|
+
* `mp/tenants/${tenantId}`), value is JSON `{ "openai": "sk-...", "anthropic": "sk-..." }`.
|
|
85
|
+
* Extended fields are passed through to `TenantContext.metadata`.
|
|
86
|
+
*/
|
|
87
|
+
declare class AwsSecretsManagerKeyVault implements KeyVault {
|
|
88
|
+
private client;
|
|
89
|
+
private cache;
|
|
90
|
+
private cacheTtlMs;
|
|
91
|
+
private secretPrefix;
|
|
92
|
+
constructor(config: AwsSecretsManagerKeyVaultConfig);
|
|
93
|
+
private secretName;
|
|
94
|
+
resolve(tenantId: string): Promise<TenantContext>;
|
|
95
|
+
get(tenantId: string, key: string): Promise<string | null>;
|
|
96
|
+
health(): Promise<{
|
|
97
|
+
healthy: boolean;
|
|
98
|
+
latencyMs: number;
|
|
99
|
+
}>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface GcpSecretManagerKeyVaultConfig {
|
|
103
|
+
projectId: string;
|
|
104
|
+
/** Secret name pattern (the trailing segment under `projects/<id>/secrets/`). Default `mp-tenants`. */
|
|
105
|
+
secretPrefix?: string;
|
|
106
|
+
/** Cache resolved TenantContext for this many ms. Default 300_000 (5 min). */
|
|
107
|
+
cacheTtlMs?: number;
|
|
108
|
+
/** Optional service-account key file path. Falls back to default credentials. */
|
|
109
|
+
keyFilename?: string;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Resolves per-tenant provider keys from GCP Secret Manager.
|
|
113
|
+
*
|
|
114
|
+
* Storage convention: one secret per tenant at `projects/${projectId}/secrets/${secretPrefix}-${tenantId}`,
|
|
115
|
+
* latest version's payload is JSON `{ "openai": "sk-...", ... }` plus optional reserved
|
|
116
|
+
* keys (budgetCaps, allowedProviders, allowedModels, metadata).
|
|
117
|
+
*/
|
|
118
|
+
declare class GcpSecretManagerKeyVault implements KeyVault {
|
|
119
|
+
private client;
|
|
120
|
+
private cache;
|
|
121
|
+
private cacheTtlMs;
|
|
122
|
+
private projectId;
|
|
123
|
+
private secretPrefix;
|
|
124
|
+
constructor(config: GcpSecretManagerKeyVaultConfig);
|
|
125
|
+
private secretVersionName;
|
|
126
|
+
resolve(tenantId: string): Promise<TenantContext>;
|
|
127
|
+
get(tenantId: string, key: string): Promise<string | null>;
|
|
128
|
+
health(): Promise<{
|
|
129
|
+
healthy: boolean;
|
|
130
|
+
latencyMs: number;
|
|
131
|
+
}>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export { AwsSecretsManagerKeyVault, type AwsSecretsManagerKeyVaultConfig, EnvKeyVault, GcpSecretManagerKeyVault, type GcpSecretManagerKeyVaultConfig, InMemoryKeyVault, type KeyVault, type TenantContext, type TenantResolutionStrategy };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
interface TenantContext {
|
|
2
|
+
tenantId: string;
|
|
3
|
+
providerKeys: ReadonlyMap<string, string>;
|
|
4
|
+
budgetCaps?: {
|
|
5
|
+
dailyUsd?: number;
|
|
6
|
+
monthlyUsd?: number;
|
|
7
|
+
};
|
|
8
|
+
allowedProviders?: string[];
|
|
9
|
+
allowedModels?: string[];
|
|
10
|
+
metadata?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
interface KeyVault {
|
|
13
|
+
resolve(tenantId: string): Promise<TenantContext>;
|
|
14
|
+
get(tenantId: string, key: string): Promise<string | null>;
|
|
15
|
+
health(): Promise<{
|
|
16
|
+
healthy: boolean;
|
|
17
|
+
latencyMs: number;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
type TenantResolutionStrategy = {
|
|
21
|
+
kind: 'header';
|
|
22
|
+
headerName: string;
|
|
23
|
+
} | {
|
|
24
|
+
kind: 'jwt';
|
|
25
|
+
jwksUri: string;
|
|
26
|
+
claim: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'oauth-scope';
|
|
29
|
+
scope: string;
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'mtls-cn';
|
|
32
|
+
} | {
|
|
33
|
+
kind: 'static';
|
|
34
|
+
tenantId: string;
|
|
35
|
+
} | {
|
|
36
|
+
kind: 'custom';
|
|
37
|
+
resolver: {
|
|
38
|
+
resolve(request: unknown): Promise<TenantContext | null>;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
declare class InMemoryKeyVault implements KeyVault {
|
|
43
|
+
private tenants;
|
|
44
|
+
set(tenantId: string, keys: Record<string, string>, overrides?: Partial<TenantContext>): void;
|
|
45
|
+
resolve(tenantId: string): Promise<TenantContext>;
|
|
46
|
+
get(tenantId: string, key: string): Promise<string | null>;
|
|
47
|
+
health(): Promise<{
|
|
48
|
+
healthy: boolean;
|
|
49
|
+
latencyMs: number;
|
|
50
|
+
}>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Reads per-tenant provider keys from environment variables matching
|
|
54
|
+
* `${TENANT_ID}_${PROVIDER}_API_KEY` (e.g. `ACME_OPENAI_API_KEY`). Provider list is
|
|
55
|
+
* discovered dynamically from env vars matching the pattern — adding a new provider
|
|
56
|
+
* just means setting `${TENANT_ID}_${NEWPROVIDER}_API_KEY` without code changes.
|
|
57
|
+
*/
|
|
58
|
+
declare class EnvKeyVault implements KeyVault {
|
|
59
|
+
resolve(tenantId: string): Promise<TenantContext>;
|
|
60
|
+
get(tenantId: string, key: string): Promise<string | null>;
|
|
61
|
+
health(): Promise<{
|
|
62
|
+
healthy: boolean;
|
|
63
|
+
latencyMs: number;
|
|
64
|
+
}>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface AwsSecretsManagerKeyVaultConfig {
|
|
68
|
+
region: string;
|
|
69
|
+
/** Secret name pattern with `${tenantId}` placeholder. Default: `mp/tenants/${tenantId}`. */
|
|
70
|
+
secretPrefix?: string;
|
|
71
|
+
/** Cache resolved TenantContext for this many ms. Default 300_000 (5 min). */
|
|
72
|
+
cacheTtlMs?: number;
|
|
73
|
+
/** Optional AWS credentials override. Falls back to default credential chain. */
|
|
74
|
+
credentials?: {
|
|
75
|
+
accessKeyId: string;
|
|
76
|
+
secretAccessKey: string;
|
|
77
|
+
sessionToken?: string;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolves per-tenant provider keys from AWS Secrets Manager.
|
|
82
|
+
*
|
|
83
|
+
* Storage convention: one secret per tenant at `${secretPrefix}/${tenantId}` (default
|
|
84
|
+
* `mp/tenants/${tenantId}`), value is JSON `{ "openai": "sk-...", "anthropic": "sk-..." }`.
|
|
85
|
+
* Extended fields are passed through to `TenantContext.metadata`.
|
|
86
|
+
*/
|
|
87
|
+
declare class AwsSecretsManagerKeyVault implements KeyVault {
|
|
88
|
+
private client;
|
|
89
|
+
private cache;
|
|
90
|
+
private cacheTtlMs;
|
|
91
|
+
private secretPrefix;
|
|
92
|
+
constructor(config: AwsSecretsManagerKeyVaultConfig);
|
|
93
|
+
private secretName;
|
|
94
|
+
resolve(tenantId: string): Promise<TenantContext>;
|
|
95
|
+
get(tenantId: string, key: string): Promise<string | null>;
|
|
96
|
+
health(): Promise<{
|
|
97
|
+
healthy: boolean;
|
|
98
|
+
latencyMs: number;
|
|
99
|
+
}>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface GcpSecretManagerKeyVaultConfig {
|
|
103
|
+
projectId: string;
|
|
104
|
+
/** Secret name pattern (the trailing segment under `projects/<id>/secrets/`). Default `mp-tenants`. */
|
|
105
|
+
secretPrefix?: string;
|
|
106
|
+
/** Cache resolved TenantContext for this many ms. Default 300_000 (5 min). */
|
|
107
|
+
cacheTtlMs?: number;
|
|
108
|
+
/** Optional service-account key file path. Falls back to default credentials. */
|
|
109
|
+
keyFilename?: string;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Resolves per-tenant provider keys from GCP Secret Manager.
|
|
113
|
+
*
|
|
114
|
+
* Storage convention: one secret per tenant at `projects/${projectId}/secrets/${secretPrefix}-${tenantId}`,
|
|
115
|
+
* latest version's payload is JSON `{ "openai": "sk-...", ... }` plus optional reserved
|
|
116
|
+
* keys (budgetCaps, allowedProviders, allowedModels, metadata).
|
|
117
|
+
*/
|
|
118
|
+
declare class GcpSecretManagerKeyVault implements KeyVault {
|
|
119
|
+
private client;
|
|
120
|
+
private cache;
|
|
121
|
+
private cacheTtlMs;
|
|
122
|
+
private projectId;
|
|
123
|
+
private secretPrefix;
|
|
124
|
+
constructor(config: GcpSecretManagerKeyVaultConfig);
|
|
125
|
+
private secretVersionName;
|
|
126
|
+
resolve(tenantId: string): Promise<TenantContext>;
|
|
127
|
+
get(tenantId: string, key: string): Promise<string | null>;
|
|
128
|
+
health(): Promise<{
|
|
129
|
+
healthy: boolean;
|
|
130
|
+
latencyMs: number;
|
|
131
|
+
}>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export { AwsSecretsManagerKeyVault, type AwsSecretsManagerKeyVaultConfig, EnvKeyVault, GcpSecretManagerKeyVault, type GcpSecretManagerKeyVaultConfig, InMemoryKeyVault, type KeyVault, type TenantContext, type TenantResolutionStrategy };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// src/vaults.ts
|
|
2
|
+
import { TenantNotFoundError } from "@reaatech/media-pipeline-mcp-core";
|
|
3
|
+
var InMemoryKeyVault = class {
|
|
4
|
+
tenants = /* @__PURE__ */ new Map();
|
|
5
|
+
set(tenantId, keys, overrides) {
|
|
6
|
+
this.tenants.set(tenantId, { keys, overrides });
|
|
7
|
+
}
|
|
8
|
+
async resolve(tenantId) {
|
|
9
|
+
const entry = this.tenants.get(tenantId);
|
|
10
|
+
if (!entry) throw new TenantNotFoundError();
|
|
11
|
+
return {
|
|
12
|
+
tenantId,
|
|
13
|
+
providerKeys: new Map(Object.entries(entry.keys)),
|
|
14
|
+
...entry.overrides
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
async get(tenantId, key) {
|
|
18
|
+
const entry = this.tenants.get(tenantId);
|
|
19
|
+
return entry?.keys[key] ?? null;
|
|
20
|
+
}
|
|
21
|
+
async health() {
|
|
22
|
+
return { healthy: true, latencyMs: 0 };
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var EnvKeyVault = class {
|
|
26
|
+
async resolve(tenantId) {
|
|
27
|
+
const keys = {};
|
|
28
|
+
const prefix = `${tenantId.toUpperCase()}_`;
|
|
29
|
+
const suffix = "_API_KEY";
|
|
30
|
+
for (const [envKey, val] of Object.entries(process.env)) {
|
|
31
|
+
if (envKey.startsWith(prefix) && envKey.endsWith(suffix) && typeof val === "string") {
|
|
32
|
+
const provider = envKey.slice(prefix.length, envKey.length - suffix.length).toLowerCase();
|
|
33
|
+
if (provider.length > 0) keys[provider] = val;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (Object.keys(keys).length === 0) {
|
|
37
|
+
throw new TenantNotFoundError();
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
tenantId,
|
|
41
|
+
providerKeys: new Map(Object.entries(keys))
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async get(tenantId, key) {
|
|
45
|
+
const envKey = `${tenantId.toUpperCase()}_${key.toUpperCase()}`;
|
|
46
|
+
return process.env[envKey] ?? null;
|
|
47
|
+
}
|
|
48
|
+
async health() {
|
|
49
|
+
return { healthy: true, latencyMs: 0 };
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// src/aws-secrets-vault.ts
|
|
54
|
+
import { GetSecretValueCommand, SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
|
|
55
|
+
import { KeyVaultUnavailableError, TenantNotFoundError as TenantNotFoundError2 } from "@reaatech/media-pipeline-mcp-core";
|
|
56
|
+
var AwsSecretsManagerKeyVault = class {
|
|
57
|
+
client;
|
|
58
|
+
cache = /* @__PURE__ */ new Map();
|
|
59
|
+
cacheTtlMs;
|
|
60
|
+
secretPrefix;
|
|
61
|
+
constructor(config) {
|
|
62
|
+
this.cacheTtlMs = config.cacheTtlMs ?? 3e5;
|
|
63
|
+
this.secretPrefix = config.secretPrefix ?? "mp/tenants";
|
|
64
|
+
this.client = new SecretsManagerClient({
|
|
65
|
+
region: config.region,
|
|
66
|
+
...config.credentials ? { credentials: config.credentials } : {}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
secretName(tenantId) {
|
|
70
|
+
return `${this.secretPrefix}/${tenantId}`;
|
|
71
|
+
}
|
|
72
|
+
async resolve(tenantId) {
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
const cached = this.cache.get(tenantId);
|
|
75
|
+
if (cached && cached.expiresAtMs > now) {
|
|
76
|
+
return cached.context;
|
|
77
|
+
}
|
|
78
|
+
let payload;
|
|
79
|
+
try {
|
|
80
|
+
const response = await this.client.send(
|
|
81
|
+
new GetSecretValueCommand({ SecretId: this.secretName(tenantId) })
|
|
82
|
+
);
|
|
83
|
+
payload = response.SecretString;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
const name = err?.name;
|
|
86
|
+
if (name === "ResourceNotFoundException") {
|
|
87
|
+
throw new TenantNotFoundError2();
|
|
88
|
+
}
|
|
89
|
+
throw new KeyVaultUnavailableError();
|
|
90
|
+
}
|
|
91
|
+
if (!payload) {
|
|
92
|
+
throw new TenantNotFoundError2();
|
|
93
|
+
}
|
|
94
|
+
let parsed;
|
|
95
|
+
try {
|
|
96
|
+
parsed = JSON.parse(payload);
|
|
97
|
+
} catch {
|
|
98
|
+
throw new KeyVaultUnavailableError();
|
|
99
|
+
}
|
|
100
|
+
const { providerKeys, extras } = splitTenantPayload(parsed);
|
|
101
|
+
const context = {
|
|
102
|
+
tenantId,
|
|
103
|
+
providerKeys,
|
|
104
|
+
...extras.budgetCaps ? { budgetCaps: extras.budgetCaps } : {},
|
|
105
|
+
...extras.allowedProviders ? { allowedProviders: extras.allowedProviders } : {},
|
|
106
|
+
...extras.allowedModels ? { allowedModels: extras.allowedModels } : {},
|
|
107
|
+
...extras.metadata ? { metadata: extras.metadata } : {}
|
|
108
|
+
};
|
|
109
|
+
this.cache.set(tenantId, { context, expiresAtMs: now + this.cacheTtlMs });
|
|
110
|
+
return context;
|
|
111
|
+
}
|
|
112
|
+
async get(tenantId, key) {
|
|
113
|
+
try {
|
|
114
|
+
const context = await this.resolve(tenantId);
|
|
115
|
+
return context.providerKeys.get(key) ?? null;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
if (err instanceof TenantNotFoundError2) return null;
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async health() {
|
|
122
|
+
const start = Date.now();
|
|
123
|
+
try {
|
|
124
|
+
await this.client.send(
|
|
125
|
+
new GetSecretValueCommand({ SecretId: `${this.secretPrefix}/__health_probe__` })
|
|
126
|
+
);
|
|
127
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
128
|
+
} catch (err) {
|
|
129
|
+
const name = err?.name;
|
|
130
|
+
if (name === "ResourceNotFoundException") {
|
|
131
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
132
|
+
}
|
|
133
|
+
return { healthy: false, latencyMs: Date.now() - start };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
function splitTenantPayload(parsed) {
|
|
138
|
+
const reserved = /* @__PURE__ */ new Set(["budgetCaps", "allowedProviders", "allowedModels", "metadata"]);
|
|
139
|
+
const providerKeys = /* @__PURE__ */ new Map();
|
|
140
|
+
const extras = {};
|
|
141
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
142
|
+
if (reserved.has(k)) {
|
|
143
|
+
extras[k] = v;
|
|
144
|
+
} else if (typeof v === "string") {
|
|
145
|
+
providerKeys.set(k, v);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { providerKeys, extras };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/gcp-secret-vault.ts
|
|
152
|
+
import { SecretManagerServiceClient } from "@google-cloud/secret-manager";
|
|
153
|
+
import { KeyVaultUnavailableError as KeyVaultUnavailableError2, TenantNotFoundError as TenantNotFoundError3 } from "@reaatech/media-pipeline-mcp-core";
|
|
154
|
+
var GcpSecretManagerKeyVault = class {
|
|
155
|
+
client;
|
|
156
|
+
cache = /* @__PURE__ */ new Map();
|
|
157
|
+
cacheTtlMs;
|
|
158
|
+
projectId;
|
|
159
|
+
secretPrefix;
|
|
160
|
+
constructor(config) {
|
|
161
|
+
this.cacheTtlMs = config.cacheTtlMs ?? 3e5;
|
|
162
|
+
this.projectId = config.projectId;
|
|
163
|
+
this.secretPrefix = config.secretPrefix ?? "mp-tenants";
|
|
164
|
+
this.client = new SecretManagerServiceClient(
|
|
165
|
+
config.keyFilename ? { keyFilename: config.keyFilename } : void 0
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
secretVersionName(tenantId) {
|
|
169
|
+
return `projects/${this.projectId}/secrets/${this.secretPrefix}-${tenantId}/versions/latest`;
|
|
170
|
+
}
|
|
171
|
+
async resolve(tenantId) {
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
const cached = this.cache.get(tenantId);
|
|
174
|
+
if (cached && cached.expiresAtMs > now) {
|
|
175
|
+
return cached.context;
|
|
176
|
+
}
|
|
177
|
+
let payload;
|
|
178
|
+
try {
|
|
179
|
+
const [response] = await this.client.accessSecretVersion({
|
|
180
|
+
name: this.secretVersionName(tenantId)
|
|
181
|
+
});
|
|
182
|
+
const data = response?.payload?.data;
|
|
183
|
+
if (data) {
|
|
184
|
+
payload = typeof data === "string" ? data : Buffer.from(data).toString("utf8");
|
|
185
|
+
}
|
|
186
|
+
} catch (err) {
|
|
187
|
+
const code = err?.code;
|
|
188
|
+
if (code === 5 || code === "NOT_FOUND") {
|
|
189
|
+
throw new TenantNotFoundError3();
|
|
190
|
+
}
|
|
191
|
+
throw new KeyVaultUnavailableError2();
|
|
192
|
+
}
|
|
193
|
+
if (!payload) {
|
|
194
|
+
throw new TenantNotFoundError3();
|
|
195
|
+
}
|
|
196
|
+
let parsed;
|
|
197
|
+
try {
|
|
198
|
+
parsed = JSON.parse(payload);
|
|
199
|
+
} catch {
|
|
200
|
+
throw new KeyVaultUnavailableError2();
|
|
201
|
+
}
|
|
202
|
+
const { providerKeys, extras } = splitTenantPayload2(parsed);
|
|
203
|
+
const context = {
|
|
204
|
+
tenantId,
|
|
205
|
+
providerKeys,
|
|
206
|
+
...extras.budgetCaps ? { budgetCaps: extras.budgetCaps } : {},
|
|
207
|
+
...extras.allowedProviders ? { allowedProviders: extras.allowedProviders } : {},
|
|
208
|
+
...extras.allowedModels ? { allowedModels: extras.allowedModels } : {},
|
|
209
|
+
...extras.metadata ? { metadata: extras.metadata } : {}
|
|
210
|
+
};
|
|
211
|
+
this.cache.set(tenantId, { context, expiresAtMs: now + this.cacheTtlMs });
|
|
212
|
+
return context;
|
|
213
|
+
}
|
|
214
|
+
async get(tenantId, key) {
|
|
215
|
+
try {
|
|
216
|
+
const context = await this.resolve(tenantId);
|
|
217
|
+
return context.providerKeys.get(key) ?? null;
|
|
218
|
+
} catch (err) {
|
|
219
|
+
if (err instanceof TenantNotFoundError3) return null;
|
|
220
|
+
throw err;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async health() {
|
|
224
|
+
const start = Date.now();
|
|
225
|
+
try {
|
|
226
|
+
await this.client.accessSecretVersion({
|
|
227
|
+
name: this.secretVersionName("__health_probe__")
|
|
228
|
+
});
|
|
229
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
230
|
+
} catch (err) {
|
|
231
|
+
const code = err?.code;
|
|
232
|
+
if (code === 5 || code === "NOT_FOUND") {
|
|
233
|
+
return { healthy: true, latencyMs: Date.now() - start };
|
|
234
|
+
}
|
|
235
|
+
return { healthy: false, latencyMs: Date.now() - start };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
function splitTenantPayload2(parsed) {
|
|
240
|
+
const reserved = /* @__PURE__ */ new Set(["budgetCaps", "allowedProviders", "allowedModels", "metadata"]);
|
|
241
|
+
const providerKeys = /* @__PURE__ */ new Map();
|
|
242
|
+
const extras = {};
|
|
243
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
244
|
+
if (reserved.has(k)) {
|
|
245
|
+
extras[k] = v;
|
|
246
|
+
} else if (typeof v === "string") {
|
|
247
|
+
providerKeys.set(k, v);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return { providerKeys, extras };
|
|
251
|
+
}
|
|
252
|
+
export {
|
|
253
|
+
AwsSecretsManagerKeyVault,
|
|
254
|
+
EnvKeyVault,
|
|
255
|
+
GcpSecretManagerKeyVault,
|
|
256
|
+
InMemoryKeyVault
|
|
257
|
+
};
|
|
258
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/vaults.ts","../src/aws-secrets-vault.ts","../src/gcp-secret-vault.ts"],"sourcesContent":["import { TenantNotFoundError } from '@reaatech/media-pipeline-mcp-core';\nimport type { KeyVault, TenantContext } from './types.js';\n\nexport class InMemoryKeyVault implements KeyVault {\n private tenants = new Map<\n string,\n { keys: Record<string, string>; overrides?: Partial<TenantContext> }\n >();\n\n set(tenantId: string, keys: Record<string, string>, overrides?: Partial<TenantContext>): void {\n this.tenants.set(tenantId, { keys, overrides });\n }\n\n async resolve(tenantId: string): Promise<TenantContext> {\n const entry = this.tenants.get(tenantId);\n // Missing tenant → TenantNotFoundError (non-retryable, caller error). The earlier\n // KeyVaultUnavailableError was wrong: that signals infra outage and is retryable.\n if (!entry) throw new TenantNotFoundError();\n return {\n tenantId,\n providerKeys: new Map(Object.entries(entry.keys)),\n ...entry.overrides,\n };\n }\n\n async get(tenantId: string, key: string): Promise<string | null> {\n const entry = this.tenants.get(tenantId);\n return entry?.keys[key] ?? null;\n }\n\n async health(): Promise<{ healthy: boolean; latencyMs: number }> {\n return { healthy: true, latencyMs: 0 };\n }\n}\n\n/**\n * Reads per-tenant provider keys from environment variables matching\n * `${TENANT_ID}_${PROVIDER}_API_KEY` (e.g. `ACME_OPENAI_API_KEY`). Provider list is\n * discovered dynamically from env vars matching the pattern — adding a new provider\n * just means setting `${TENANT_ID}_${NEWPROVIDER}_API_KEY` without code changes.\n */\nexport class EnvKeyVault implements KeyVault {\n async resolve(tenantId: string): Promise<TenantContext> {\n const keys: Record<string, string> = {};\n const prefix = `${tenantId.toUpperCase()}_`;\n const suffix = '_API_KEY';\n for (const [envKey, val] of Object.entries(process.env)) {\n if (envKey.startsWith(prefix) && envKey.endsWith(suffix) && typeof val === 'string') {\n const provider = envKey.slice(prefix.length, envKey.length - suffix.length).toLowerCase();\n if (provider.length > 0) keys[provider] = val;\n }\n }\n if (Object.keys(keys).length === 0) {\n throw new TenantNotFoundError();\n }\n return {\n tenantId,\n providerKeys: new Map(Object.entries(keys)),\n };\n }\n\n async get(tenantId: string, key: string): Promise<string | null> {\n const envKey = `${tenantId.toUpperCase()}_${key.toUpperCase()}`;\n return process.env[envKey] ?? null;\n }\n\n async health(): Promise<{ healthy: boolean; latencyMs: number }> {\n return { healthy: true, latencyMs: 0 };\n }\n}\n","import { GetSecretValueCommand, SecretsManagerClient } from '@aws-sdk/client-secrets-manager';\nimport { KeyVaultUnavailableError, TenantNotFoundError } from '@reaatech/media-pipeline-mcp-core';\nimport type { KeyVault, TenantContext } from './types.js';\n\nexport interface AwsSecretsManagerKeyVaultConfig {\n region: string;\n /** Secret name pattern with `${tenantId}` placeholder. Default: `mp/tenants/${tenantId}`. */\n secretPrefix?: string;\n /** Cache resolved TenantContext for this many ms. Default 300_000 (5 min). */\n cacheTtlMs?: number;\n /** Optional AWS credentials override. Falls back to default credential chain. */\n credentials?: { accessKeyId: string; secretAccessKey: string; sessionToken?: string };\n}\n\ninterface CacheEntry {\n context: TenantContext;\n expiresAtMs: number;\n}\n\n/**\n * Resolves per-tenant provider keys from AWS Secrets Manager.\n *\n * Storage convention: one secret per tenant at `${secretPrefix}/${tenantId}` (default\n * `mp/tenants/${tenantId}`), value is JSON `{ \"openai\": \"sk-...\", \"anthropic\": \"sk-...\" }`.\n * Extended fields are passed through to `TenantContext.metadata`.\n */\nexport class AwsSecretsManagerKeyVault implements KeyVault {\n private client: SecretsManagerClient;\n private cache = new Map<string, CacheEntry>();\n private cacheTtlMs: number;\n private secretPrefix: string;\n\n constructor(config: AwsSecretsManagerKeyVaultConfig) {\n this.cacheTtlMs = config.cacheTtlMs ?? 300_000;\n this.secretPrefix = config.secretPrefix ?? 'mp/tenants';\n this.client = new SecretsManagerClient({\n region: config.region,\n ...(config.credentials ? { credentials: config.credentials } : {}),\n });\n }\n\n private secretName(tenantId: string): string {\n return `${this.secretPrefix}/${tenantId}`;\n }\n\n async resolve(tenantId: string): Promise<TenantContext> {\n const now = Date.now();\n const cached = this.cache.get(tenantId);\n if (cached && cached.expiresAtMs > now) {\n return cached.context;\n }\n\n let payload: string | undefined;\n try {\n const response = await this.client.send(\n new GetSecretValueCommand({ SecretId: this.secretName(tenantId) }),\n );\n payload = response.SecretString;\n } catch (err) {\n const name = (err as Error & { name?: string })?.name;\n // The AWS SDK throws ResourceNotFoundException for unknown secrets — that's a\n // tenant-not-found signal, not infra unavailability.\n if (name === 'ResourceNotFoundException') {\n throw new TenantNotFoundError();\n }\n throw new KeyVaultUnavailableError();\n }\n\n if (!payload) {\n throw new TenantNotFoundError();\n }\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(payload) as Record<string, unknown>;\n } catch {\n throw new KeyVaultUnavailableError();\n }\n\n const { providerKeys, extras } = splitTenantPayload(parsed);\n const context: TenantContext = {\n tenantId,\n providerKeys,\n ...(extras.budgetCaps\n ? { budgetCaps: extras.budgetCaps as TenantContext['budgetCaps'] }\n : {}),\n ...(extras.allowedProviders ? { allowedProviders: extras.allowedProviders as string[] } : {}),\n ...(extras.allowedModels ? { allowedModels: extras.allowedModels as string[] } : {}),\n ...(extras.metadata ? { metadata: extras.metadata as Record<string, unknown> } : {}),\n };\n\n this.cache.set(tenantId, { context, expiresAtMs: now + this.cacheTtlMs });\n return context;\n }\n\n async get(tenantId: string, key: string): Promise<string | null> {\n try {\n const context = await this.resolve(tenantId);\n return context.providerKeys.get(key) ?? null;\n } catch (err) {\n if (err instanceof TenantNotFoundError) return null;\n throw err;\n }\n }\n\n async health(): Promise<{ healthy: boolean; latencyMs: number }> {\n const start = Date.now();\n try {\n // No cheap \"list\" API in Secrets Manager — issue a dummy GetSecretValue and treat\n // ResourceNotFoundException as healthy (the service responded). Anything else\n // (auth failure, network) is unhealthy.\n await this.client.send(\n new GetSecretValueCommand({ SecretId: `${this.secretPrefix}/__health_probe__` }),\n );\n return { healthy: true, latencyMs: Date.now() - start };\n } catch (err) {\n const name = (err as Error & { name?: string })?.name;\n if (name === 'ResourceNotFoundException') {\n return { healthy: true, latencyMs: Date.now() - start };\n }\n return { healthy: false, latencyMs: Date.now() - start };\n }\n }\n}\n\n/**\n * Split the parsed secret payload into the `providerKeys` map and the optional\n * TenantContext extras. The convention is: top-level string values are provider keys;\n * the reserved keys `budgetCaps`, `allowedProviders`, `allowedModels`, `metadata` are\n * passed through to the TenantContext.\n */\nfunction splitTenantPayload(parsed: Record<string, unknown>): {\n providerKeys: Map<string, string>;\n extras: Record<string, unknown>;\n} {\n const reserved = new Set(['budgetCaps', 'allowedProviders', 'allowedModels', 'metadata']);\n const providerKeys = new Map<string, string>();\n const extras: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(parsed)) {\n if (reserved.has(k)) {\n extras[k] = v;\n } else if (typeof v === 'string') {\n providerKeys.set(k, v);\n }\n }\n return { providerKeys, extras };\n}\n","import { SecretManagerServiceClient } from '@google-cloud/secret-manager';\nimport { KeyVaultUnavailableError, TenantNotFoundError } from '@reaatech/media-pipeline-mcp-core';\nimport type { KeyVault, TenantContext } from './types.js';\n\nexport interface GcpSecretManagerKeyVaultConfig {\n projectId: string;\n /** Secret name pattern (the trailing segment under `projects/<id>/secrets/`). Default `mp-tenants`. */\n secretPrefix?: string;\n /** Cache resolved TenantContext for this many ms. Default 300_000 (5 min). */\n cacheTtlMs?: number;\n /** Optional service-account key file path. Falls back to default credentials. */\n keyFilename?: string;\n}\n\ninterface CacheEntry {\n context: TenantContext;\n expiresAtMs: number;\n}\n\n/**\n * Resolves per-tenant provider keys from GCP Secret Manager.\n *\n * Storage convention: one secret per tenant at `projects/${projectId}/secrets/${secretPrefix}-${tenantId}`,\n * latest version's payload is JSON `{ \"openai\": \"sk-...\", ... }` plus optional reserved\n * keys (budgetCaps, allowedProviders, allowedModels, metadata).\n */\nexport class GcpSecretManagerKeyVault implements KeyVault {\n private client: SecretManagerServiceClient;\n private cache = new Map<string, CacheEntry>();\n private cacheTtlMs: number;\n private projectId: string;\n private secretPrefix: string;\n\n constructor(config: GcpSecretManagerKeyVaultConfig) {\n this.cacheTtlMs = config.cacheTtlMs ?? 300_000;\n this.projectId = config.projectId;\n this.secretPrefix = config.secretPrefix ?? 'mp-tenants';\n this.client = new SecretManagerServiceClient(\n config.keyFilename ? { keyFilename: config.keyFilename } : undefined,\n );\n }\n\n private secretVersionName(tenantId: string): string {\n return `projects/${this.projectId}/secrets/${this.secretPrefix}-${tenantId}/versions/latest`;\n }\n\n async resolve(tenantId: string): Promise<TenantContext> {\n const now = Date.now();\n const cached = this.cache.get(tenantId);\n if (cached && cached.expiresAtMs > now) {\n return cached.context;\n }\n\n let payload: string | undefined;\n try {\n const [response] = await this.client.accessSecretVersion({\n name: this.secretVersionName(tenantId),\n });\n const data = response?.payload?.data;\n if (data) {\n payload = typeof data === 'string' ? data : Buffer.from(data).toString('utf8');\n }\n } catch (err) {\n const code = (err as Error & { code?: number | string })?.code;\n // gRPC code 5 = NOT_FOUND. Treat as tenant-not-found; anything else as infra-down.\n if (code === 5 || code === 'NOT_FOUND') {\n throw new TenantNotFoundError();\n }\n throw new KeyVaultUnavailableError();\n }\n\n if (!payload) {\n throw new TenantNotFoundError();\n }\n\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(payload) as Record<string, unknown>;\n } catch {\n throw new KeyVaultUnavailableError();\n }\n\n const { providerKeys, extras } = splitTenantPayload(parsed);\n const context: TenantContext = {\n tenantId,\n providerKeys,\n ...(extras.budgetCaps\n ? { budgetCaps: extras.budgetCaps as TenantContext['budgetCaps'] }\n : {}),\n ...(extras.allowedProviders ? { allowedProviders: extras.allowedProviders as string[] } : {}),\n ...(extras.allowedModels ? { allowedModels: extras.allowedModels as string[] } : {}),\n ...(extras.metadata ? { metadata: extras.metadata as Record<string, unknown> } : {}),\n };\n\n this.cache.set(tenantId, { context, expiresAtMs: now + this.cacheTtlMs });\n return context;\n }\n\n async get(tenantId: string, key: string): Promise<string | null> {\n try {\n const context = await this.resolve(tenantId);\n return context.providerKeys.get(key) ?? null;\n } catch (err) {\n if (err instanceof TenantNotFoundError) return null;\n throw err;\n }\n }\n\n async health(): Promise<{ healthy: boolean; latencyMs: number }> {\n const start = Date.now();\n try {\n // Probe with a synthetic tenantId. NOT_FOUND means the service is reachable.\n await this.client.accessSecretVersion({\n name: this.secretVersionName('__health_probe__'),\n });\n return { healthy: true, latencyMs: Date.now() - start };\n } catch (err) {\n const code = (err as Error & { code?: number | string })?.code;\n if (code === 5 || code === 'NOT_FOUND') {\n return { healthy: true, latencyMs: Date.now() - start };\n }\n return { healthy: false, latencyMs: Date.now() - start };\n }\n }\n}\n\nfunction splitTenantPayload(parsed: Record<string, unknown>): {\n providerKeys: Map<string, string>;\n extras: Record<string, unknown>;\n} {\n const reserved = new Set(['budgetCaps', 'allowedProviders', 'allowedModels', 'metadata']);\n const providerKeys = new Map<string, string>();\n const extras: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(parsed)) {\n if (reserved.has(k)) {\n extras[k] = v;\n } else if (typeof v === 'string') {\n providerKeys.set(k, v);\n }\n }\n return { providerKeys, extras };\n}\n"],"mappings":";AAAA,SAAS,2BAA2B;AAG7B,IAAM,mBAAN,MAA2C;AAAA,EACxC,UAAU,oBAAI,IAGpB;AAAA,EAEF,IAAI,UAAkB,MAA8B,WAA0C;AAC5F,SAAK,QAAQ,IAAI,UAAU,EAAE,MAAM,UAAU,CAAC;AAAA,EAChD;AAAA,EAEA,MAAM,QAAQ,UAA0C;AACtD,UAAM,QAAQ,KAAK,QAAQ,IAAI,QAAQ;AAGvC,QAAI,CAAC,MAAO,OAAM,IAAI,oBAAoB;AAC1C,WAAO;AAAA,MACL;AAAA,MACA,cAAc,IAAI,IAAI,OAAO,QAAQ,MAAM,IAAI,CAAC;AAAA,MAChD,GAAG,MAAM;AAAA,IACX;AAAA,EACF;AAAA,EAEA,MAAM,IAAI,UAAkB,KAAqC;AAC/D,UAAM,QAAQ,KAAK,QAAQ,IAAI,QAAQ;AACvC,WAAO,OAAO,KAAK,GAAG,KAAK;AAAA,EAC7B;AAAA,EAEA,MAAM,SAA2D;AAC/D,WAAO,EAAE,SAAS,MAAM,WAAW,EAAE;AAAA,EACvC;AACF;AAQO,IAAM,cAAN,MAAsC;AAAA,EAC3C,MAAM,QAAQ,UAA0C;AACtD,UAAM,OAA+B,CAAC;AACtC,UAAM,SAAS,GAAG,SAAS,YAAY,CAAC;AACxC,UAAM,SAAS;AACf,eAAW,CAAC,QAAQ,GAAG,KAAK,OAAO,QAAQ,QAAQ,GAAG,GAAG;AACvD,UAAI,OAAO,WAAW,MAAM,KAAK,OAAO,SAAS,MAAM,KAAK,OAAO,QAAQ,UAAU;AACnF,cAAM,WAAW,OAAO,MAAM,OAAO,QAAQ,OAAO,SAAS,OAAO,MAAM,EAAE,YAAY;AACxF,YAAI,SAAS,SAAS,EAAG,MAAK,QAAQ,IAAI;AAAA,MAC5C;AAAA,IACF;AACA,QAAI,OAAO,KAAK,IAAI,EAAE,WAAW,GAAG;AAClC,YAAM,IAAI,oBAAoB;AAAA,IAChC;AACA,WAAO;AAAA,MACL;AAAA,MACA,cAAc,IAAI,IAAI,OAAO,QAAQ,IAAI,CAAC;AAAA,IAC5C;AAAA,EACF;AAAA,EAEA,MAAM,IAAI,UAAkB,KAAqC;AAC/D,UAAM,SAAS,GAAG,SAAS,YAAY,CAAC,IAAI,IAAI,YAAY,CAAC;AAC7D,WAAO,QAAQ,IAAI,MAAM,KAAK;AAAA,EAChC;AAAA,EAEA,MAAM,SAA2D;AAC/D,WAAO,EAAE,SAAS,MAAM,WAAW,EAAE;AAAA,EACvC;AACF;;;ACrEA,SAAS,uBAAuB,4BAA4B;AAC5D,SAAS,0BAA0B,uBAAAA,4BAA2B;AAyBvD,IAAM,4BAAN,MAAoD;AAAA,EACjD;AAAA,EACA,QAAQ,oBAAI,IAAwB;AAAA,EACpC;AAAA,EACA;AAAA,EAER,YAAY,QAAyC;AACnD,SAAK,aAAa,OAAO,cAAc;AACvC,SAAK,eAAe,OAAO,gBAAgB;AAC3C,SAAK,SAAS,IAAI,qBAAqB;AAAA,MACrC,QAAQ,OAAO;AAAA,MACf,GAAI,OAAO,cAAc,EAAE,aAAa,OAAO,YAAY,IAAI,CAAC;AAAA,IAClE,CAAC;AAAA,EACH;AAAA,EAEQ,WAAW,UAA0B;AAC3C,WAAO,GAAG,KAAK,YAAY,IAAI,QAAQ;AAAA,EACzC;AAAA,EAEA,MAAM,QAAQ,UAA0C;AACtD,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,SAAS,KAAK,MAAM,IAAI,QAAQ;AACtC,QAAI,UAAU,OAAO,cAAc,KAAK;AACtC,aAAO,OAAO;AAAA,IAChB;AAEA,QAAI;AACJ,QAAI;AACF,YAAM,WAAW,MAAM,KAAK,OAAO;AAAA,QACjC,IAAI,sBAAsB,EAAE,UAAU,KAAK,WAAW,QAAQ,EAAE,CAAC;AAAA,MACnE;AACA,gBAAU,SAAS;AAAA,IACrB,SAAS,KAAK;AACZ,YAAM,OAAQ,KAAmC;AAGjD,UAAI,SAAS,6BAA6B;AACxC,cAAM,IAAIA,qBAAoB;AAAA,MAChC;AACA,YAAM,IAAI,yBAAyB;AAAA,IACrC;AAEA,QAAI,CAAC,SAAS;AACZ,YAAM,IAAIA,qBAAoB;AAAA,IAChC;AAEA,QAAI;AACJ,QAAI;AACF,eAAS,KAAK,MAAM,OAAO;AAAA,IAC7B,QAAQ;AACN,YAAM,IAAI,yBAAyB;AAAA,IACrC;AAEA,UAAM,EAAE,cAAc,OAAO,IAAI,mBAAmB,MAAM;AAC1D,UAAM,UAAyB;AAAA,MAC7B;AAAA,MACA;AAAA,MACA,GAAI,OAAO,aACP,EAAE,YAAY,OAAO,WAA0C,IAC/D,CAAC;AAAA,MACL,GAAI,OAAO,mBAAmB,EAAE,kBAAkB,OAAO,iBAA6B,IAAI,CAAC;AAAA,MAC3F,GAAI,OAAO,gBAAgB,EAAE,eAAe,OAAO,cAA0B,IAAI,CAAC;AAAA,MAClF,GAAI,OAAO,WAAW,EAAE,UAAU,OAAO,SAAoC,IAAI,CAAC;AAAA,IACpF;AAEA,SAAK,MAAM,IAAI,UAAU,EAAE,SAAS,aAAa,MAAM,KAAK,WAAW,CAAC;AACxE,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,IAAI,UAAkB,KAAqC;AAC/D,QAAI;AACF,YAAM,UAAU,MAAM,KAAK,QAAQ,QAAQ;AAC3C,aAAO,QAAQ,aAAa,IAAI,GAAG,KAAK;AAAA,IAC1C,SAAS,KAAK;AACZ,UAAI,eAAeA,qBAAqB,QAAO;AAC/C,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,SAA2D;AAC/D,UAAM,QAAQ,KAAK,IAAI;AACvB,QAAI;AAIF,YAAM,KAAK,OAAO;AAAA,QAChB,IAAI,sBAAsB,EAAE,UAAU,GAAG,KAAK,YAAY,oBAAoB,CAAC;AAAA,MACjF;AACA,aAAO,EAAE,SAAS,MAAM,WAAW,KAAK,IAAI,IAAI,MAAM;AAAA,IACxD,SAAS,KAAK;AACZ,YAAM,OAAQ,KAAmC;AACjD,UAAI,SAAS,6BAA6B;AACxC,eAAO,EAAE,SAAS,MAAM,WAAW,KAAK,IAAI,IAAI,MAAM;AAAA,MACxD;AACA,aAAO,EAAE,SAAS,OAAO,WAAW,KAAK,IAAI,IAAI,MAAM;AAAA,IACzD;AAAA,EACF;AACF;AAQA,SAAS,mBAAmB,QAG1B;AACA,QAAM,WAAW,oBAAI,IAAI,CAAC,cAAc,oBAAoB,iBAAiB,UAAU,CAAC;AACxF,QAAM,eAAe,oBAAI,IAAoB;AAC7C,QAAM,SAAkC,CAAC;AACzC,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,GAAG;AAC3C,QAAI,SAAS,IAAI,CAAC,GAAG;AACnB,aAAO,CAAC,IAAI;AAAA,IACd,WAAW,OAAO,MAAM,UAAU;AAChC,mBAAa,IAAI,GAAG,CAAC;AAAA,IACvB;AAAA,EACF;AACA,SAAO,EAAE,cAAc,OAAO;AAChC;;;AClJA,SAAS,kCAAkC;AAC3C,SAAS,4BAAAC,2BAA0B,uBAAAC,4BAA2B;AAyBvD,IAAM,2BAAN,MAAmD;AAAA,EAChD;AAAA,EACA,QAAQ,oBAAI,IAAwB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,QAAwC;AAClD,SAAK,aAAa,OAAO,cAAc;AACvC,SAAK,YAAY,OAAO;AACxB,SAAK,eAAe,OAAO,gBAAgB;AAC3C,SAAK,SAAS,IAAI;AAAA,MAChB,OAAO,cAAc,EAAE,aAAa,OAAO,YAAY,IAAI;AAAA,IAC7D;AAAA,EACF;AAAA,EAEQ,kBAAkB,UAA0B;AAClD,WAAO,YAAY,KAAK,SAAS,YAAY,KAAK,YAAY,IAAI,QAAQ;AAAA,EAC5E;AAAA,EAEA,MAAM,QAAQ,UAA0C;AACtD,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,SAAS,KAAK,MAAM,IAAI,QAAQ;AACtC,QAAI,UAAU,OAAO,cAAc,KAAK;AACtC,aAAO,OAAO;AAAA,IAChB;AAEA,QAAI;AACJ,QAAI;AACF,YAAM,CAAC,QAAQ,IAAI,MAAM,KAAK,OAAO,oBAAoB;AAAA,QACvD,MAAM,KAAK,kBAAkB,QAAQ;AAAA,MACvC,CAAC;AACD,YAAM,OAAO,UAAU,SAAS;AAChC,UAAI,MAAM;AACR,kBAAU,OAAO,SAAS,WAAW,OAAO,OAAO,KAAK,IAAI,EAAE,SAAS,MAAM;AAAA,MAC/E;AAAA,IACF,SAAS,KAAK;AACZ,YAAM,OAAQ,KAA4C;AAE1D,UAAI,SAAS,KAAK,SAAS,aAAa;AACtC,cAAM,IAAIA,qBAAoB;AAAA,MAChC;AACA,YAAM,IAAID,0BAAyB;AAAA,IACrC;AAEA,QAAI,CAAC,SAAS;AACZ,YAAM,IAAIC,qBAAoB;AAAA,IAChC;AAEA,QAAI;AACJ,QAAI;AACF,eAAS,KAAK,MAAM,OAAO;AAAA,IAC7B,QAAQ;AACN,YAAM,IAAID,0BAAyB;AAAA,IACrC;AAEA,UAAM,EAAE,cAAc,OAAO,IAAIE,oBAAmB,MAAM;AAC1D,UAAM,UAAyB;AAAA,MAC7B;AAAA,MACA;AAAA,MACA,GAAI,OAAO,aACP,EAAE,YAAY,OAAO,WAA0C,IAC/D,CAAC;AAAA,MACL,GAAI,OAAO,mBAAmB,EAAE,kBAAkB,OAAO,iBAA6B,IAAI,CAAC;AAAA,MAC3F,GAAI,OAAO,gBAAgB,EAAE,eAAe,OAAO,cAA0B,IAAI,CAAC;AAAA,MAClF,GAAI,OAAO,WAAW,EAAE,UAAU,OAAO,SAAoC,IAAI,CAAC;AAAA,IACpF;AAEA,SAAK,MAAM,IAAI,UAAU,EAAE,SAAS,aAAa,MAAM,KAAK,WAAW,CAAC;AACxE,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,IAAI,UAAkB,KAAqC;AAC/D,QAAI;AACF,YAAM,UAAU,MAAM,KAAK,QAAQ,QAAQ;AAC3C,aAAO,QAAQ,aAAa,IAAI,GAAG,KAAK;AAAA,IAC1C,SAAS,KAAK;AACZ,UAAI,eAAeD,qBAAqB,QAAO;AAC/C,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,SAA2D;AAC/D,UAAM,QAAQ,KAAK,IAAI;AACvB,QAAI;AAEF,YAAM,KAAK,OAAO,oBAAoB;AAAA,QACpC,MAAM,KAAK,kBAAkB,kBAAkB;AAAA,MACjD,CAAC;AACD,aAAO,EAAE,SAAS,MAAM,WAAW,KAAK,IAAI,IAAI,MAAM;AAAA,IACxD,SAAS,KAAK;AACZ,YAAM,OAAQ,KAA4C;AAC1D,UAAI,SAAS,KAAK,SAAS,aAAa;AACtC,eAAO,EAAE,SAAS,MAAM,WAAW,KAAK,IAAI,IAAI,MAAM;AAAA,MACxD;AACA,aAAO,EAAE,SAAS,OAAO,WAAW,KAAK,IAAI,IAAI,MAAM;AAAA,IACzD;AAAA,EACF;AACF;AAEA,SAASC,oBAAmB,QAG1B;AACA,QAAM,WAAW,oBAAI,IAAI,CAAC,cAAc,oBAAoB,iBAAiB,UAAU,CAAC;AACxF,QAAM,eAAe,oBAAI,IAAoB;AAC7C,QAAM,SAAkC,CAAC;AACzC,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,GAAG;AAC3C,QAAI,SAAS,IAAI,CAAC,GAAG;AACnB,aAAO,CAAC,IAAI;AAAA,IACd,WAAW,OAAO,MAAM,UAAU;AAChC,mBAAa,IAAI,GAAG,CAAC;AAAA,IACvB;AAAA,EACF;AACA,SAAO,EAAE,cAAc,OAAO;AAChC;","names":["TenantNotFoundError","KeyVaultUnavailableError","TenantNotFoundError","splitTenantPayload"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reaatech/media-pipeline-mcp-keyvault",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"require": "./dist/index.cjs",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@reaatech/media-pipeline-mcp-core": "0.3.0"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@aws-sdk/client-secrets-manager": "^3.0.0",
|
|
23
|
+
"@google-cloud/secret-manager": "^5.0.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependenciesMeta": {
|
|
26
|
+
"@aws-sdk/client-secrets-manager": {
|
|
27
|
+
"optional": true
|
|
28
|
+
},
|
|
29
|
+
"@google-cloud/secret-manager": {
|
|
30
|
+
"optional": true
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@aws-sdk/client-secrets-manager": "^3.0.0",
|
|
35
|
+
"@google-cloud/secret-manager": "^5.0.0",
|
|
36
|
+
"@types/node": "^20.0.0",
|
|
37
|
+
"tsup": "^8.0.0",
|
|
38
|
+
"typescript": "^5.0.0",
|
|
39
|
+
"vitest": "^3.0.0"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsup src/index.ts --dts --format esm,cjs --sourcemap",
|
|
43
|
+
"test": "vitest run --passWithNoTests",
|
|
44
|
+
"typecheck": "tsc --noEmit"
|
|
45
|
+
}
|
|
46
|
+
}
|