@agentforge-io/core 2.0.23 → 2.1.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/dist/ai/index.d.ts +2 -0
- package/dist/ai/index.js +5 -1
- package/dist/factory.js +56 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -1
- package/dist/services/agent-runner.service.js +117 -7
- package/dist/services/agent.service.d.ts +21 -1
- package/dist/services/agent.service.js +77 -10
- package/dist/services/orchestrator.service.d.ts +40 -1
- package/dist/services/orchestrator.service.js +220 -0
- package/dist/types/agent.types.d.ts +31 -6
- package/dist/types/config.types.d.ts +8 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/model-strategy.d.ts +97 -0
- package/dist/types/model-strategy.js +83 -0
- package/package.json +1 -1
- package/dist/adapters/billing/billing-adapter.interface.d.ts +0 -41
- package/dist/adapters/billing/billing-adapter.interface.js +0 -5
- package/dist/adapters/billing/stripe/stripe.adapter.d.ts +0 -30
- package/dist/adapters/billing/stripe/stripe.adapter.js +0 -122
- package/dist/adapters/email/email-adapter.interface.d.ts +0 -25
- package/dist/adapters/email/email-adapter.interface.js +0 -6
- package/dist/adapters/email/noop.adapter.d.ts +0 -10
- package/dist/adapters/email/noop.adapter.js +0 -15
- package/dist/adapters/email/resend.adapter.d.ts +0 -8
- package/dist/adapters/email/resend.adapter.js +0 -39
- package/dist/adapters/upload/noop.adapter.d.ts +0 -9
- package/dist/adapters/upload/noop.adapter.js +0 -14
- package/dist/adapters/upload/s3.adapter.d.ts +0 -38
- package/dist/adapters/upload/s3.adapter.js +0 -69
- package/dist/adapters/upload/upload-adapter.interface.d.ts +0 -37
- package/dist/adapters/upload/upload-adapter.interface.js +0 -15
- package/dist/billing/index.d.ts +0 -12
- package/dist/billing/index.js +0 -28
- package/dist/domain/agent.d.ts +0 -59
- package/dist/domain/agent.js +0 -2
- package/dist/domain/api-key.d.ts +0 -28
- package/dist/domain/api-key.js +0 -2
- package/dist/domain/auth-identity.d.ts +0 -10
- package/dist/domain/auth-identity.js +0 -2
- package/dist/domain/email-token.d.ts +0 -11
- package/dist/domain/email-token.js +0 -2
- package/dist/domain/external-user.d.ts +0 -23
- package/dist/domain/external-user.js +0 -2
- package/dist/domain/plan.d.ts +0 -20
- package/dist/domain/plan.js +0 -2
- package/dist/domain/platform-secret.d.ts +0 -24
- package/dist/domain/platform-secret.js +0 -8
- package/dist/domain/refresh-token.d.ts +0 -15
- package/dist/domain/refresh-token.js +0 -2
- package/dist/domain/subscription.d.ts +0 -21
- package/dist/domain/subscription.js +0 -2
- package/dist/domain/tenant.d.ts +0 -21
- package/dist/domain/tenant.js +0 -2
- package/dist/domain/usage-record.d.ts +0 -15
- package/dist/domain/usage-record.js +0 -2
- package/dist/domain/user.d.ts +0 -43
- package/dist/domain/user.js +0 -2
- package/dist/services/agent-config.service.d.ts +0 -45
- package/dist/services/agent-config.service.js +0 -114
- package/dist/services/api-key.service.d.ts +0 -41
- package/dist/services/api-key.service.js +0 -80
- package/dist/services/auth.service.d.ts +0 -133
- package/dist/services/auth.service.js +0 -411
- package/dist/services/billing.service.d.ts +0 -67
- package/dist/services/billing.service.js +0 -254
- package/dist/services/email-templates.d.ts +0 -18
- package/dist/services/email-templates.js +0 -39
- package/dist/services/email.service.d.ts +0 -26
- package/dist/services/email.service.js +0 -42
- package/dist/services/errors.d.ts +0 -7
- package/dist/services/errors.js +0 -27
- package/dist/services/oauth.service.d.ts +0 -73
- package/dist/services/oauth.service.js +0 -174
- package/dist/services/plan.service.d.ts +0 -54
- package/dist/services/plan.service.js +0 -120
- package/dist/services/refresh-token.service.d.ts +0 -38
- package/dist/services/refresh-token.service.js +0 -73
- package/dist/services/secrets/crypto.d.ts +0 -37
- package/dist/services/secrets/crypto.js +0 -110
- package/dist/services/secrets/known-keys.d.ts +0 -38
- package/dist/services/secrets/known-keys.js +0 -50
- package/dist/services/secrets.service.d.ts +0 -91
- package/dist/services/secrets.service.js +0 -193
- package/dist/services/tenant-billing.service.d.ts +0 -121
- package/dist/services/tenant-billing.service.js +0 -290
- package/dist/services/tenant.service.d.ts +0 -54
- package/dist/services/tenant.service.js +0 -96
- package/dist/services/upload.service.d.ts +0 -37
- package/dist/services/upload.service.js +0 -84
- package/dist/services/usage.service.d.ts +0 -34
- package/dist/services/usage.service.js +0 -108
- package/dist/types/billing.types.d.ts +0 -82
- package/dist/types/billing.types.js +0 -3
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { PlanRepository } from '../repositories';
|
|
2
|
-
import type { Plan, NewPlan, PlanPatch } from '../domain/plan';
|
|
3
|
-
import type { PlanDefinition } from '../types/config.types';
|
|
4
|
-
export interface PlanServiceOptions {
|
|
5
|
-
/**
|
|
6
|
-
* Plans to seed the DB with on first boot (i.e. when `af_plans` is empty).
|
|
7
|
-
* After the table has at least one row, this seed is ignored — the DB is
|
|
8
|
-
* the source of truth. Pass your `config.billing.plans` here so existing
|
|
9
|
-
* deployments migrate transparently.
|
|
10
|
-
*/
|
|
11
|
-
seedPlans?: PlanDefinition[];
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Reads and writes plan rows from the DB, with an in-process cache invalidated
|
|
15
|
-
* on every mutation. Hot-path reads (`getPlan(id)`) are O(n) over a tiny set,
|
|
16
|
-
* which is fine for the few plans most apps need.
|
|
17
|
-
*
|
|
18
|
-
* Single-instance assumption: when running multiple API instances, mutations
|
|
19
|
-
* via admin endpoints invalidate only the local cache. The next read on other
|
|
20
|
-
* instances still serves stale data until their own TTL expires. For most B2B
|
|
21
|
-
* scenarios (plan edits are rare, eventually-consistent is OK) this is fine;
|
|
22
|
-
* if you need strict consistency, set `cacheTtlMs: 0`.
|
|
23
|
-
*/
|
|
24
|
-
export declare class PlanService {
|
|
25
|
-
private readonly repo;
|
|
26
|
-
private readonly opts;
|
|
27
|
-
private cache;
|
|
28
|
-
private cacheLoadedAt;
|
|
29
|
-
/** Default TTL: 60 seconds. */
|
|
30
|
-
private readonly cacheTtlMs;
|
|
31
|
-
constructor(repo: PlanRepository, opts?: PlanServiceOptions);
|
|
32
|
-
/**
|
|
33
|
-
* Call once at boot. Seeds the table from `opts.seedPlans` if empty, then
|
|
34
|
-
* warms the cache. Safe to call multiple times.
|
|
35
|
-
*/
|
|
36
|
-
initialize(): Promise<void>;
|
|
37
|
-
list(): Promise<Plan[]>;
|
|
38
|
-
listAll(): Promise<Plan[]>;
|
|
39
|
-
getPlan(id: string): Promise<Plan | undefined>;
|
|
40
|
-
getDefault(): Promise<Plan | undefined>;
|
|
41
|
-
getDefaultId(): Promise<string>;
|
|
42
|
-
create(plan: NewPlan): Promise<Plan>;
|
|
43
|
-
update(id: string, patch: PlanPatch): Promise<Plan>;
|
|
44
|
-
deactivate(id: string): Promise<void>;
|
|
45
|
-
/** Force the next read to bypass the cache. Useful in tests. */
|
|
46
|
-
invalidate(): void;
|
|
47
|
-
private refresh;
|
|
48
|
-
private isCacheFresh;
|
|
49
|
-
}
|
|
50
|
-
export declare class PlanServiceError extends Error {
|
|
51
|
-
status: number;
|
|
52
|
-
code: 'plan_exists' | 'plan_not_found' | 'plan_is_default';
|
|
53
|
-
constructor(code: 'plan_exists' | 'plan_not_found' | 'plan_is_default', message: string);
|
|
54
|
-
}
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PlanServiceError = exports.PlanService = void 0;
|
|
4
|
-
/**
|
|
5
|
-
* Reads and writes plan rows from the DB, with an in-process cache invalidated
|
|
6
|
-
* on every mutation. Hot-path reads (`getPlan(id)`) are O(n) over a tiny set,
|
|
7
|
-
* which is fine for the few plans most apps need.
|
|
8
|
-
*
|
|
9
|
-
* Single-instance assumption: when running multiple API instances, mutations
|
|
10
|
-
* via admin endpoints invalidate only the local cache. The next read on other
|
|
11
|
-
* instances still serves stale data until their own TTL expires. For most B2B
|
|
12
|
-
* scenarios (plan edits are rare, eventually-consistent is OK) this is fine;
|
|
13
|
-
* if you need strict consistency, set `cacheTtlMs: 0`.
|
|
14
|
-
*/
|
|
15
|
-
class PlanService {
|
|
16
|
-
constructor(repo, opts = {}) {
|
|
17
|
-
this.repo = repo;
|
|
18
|
-
this.opts = opts;
|
|
19
|
-
this.cache = null;
|
|
20
|
-
this.cacheLoadedAt = 0;
|
|
21
|
-
/** Default TTL: 60 seconds. */
|
|
22
|
-
this.cacheTtlMs = 60_000;
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Call once at boot. Seeds the table from `opts.seedPlans` if empty, then
|
|
26
|
-
* warms the cache. Safe to call multiple times.
|
|
27
|
-
*/
|
|
28
|
-
async initialize() {
|
|
29
|
-
const all = await this.repo.list({ includeInactive: true });
|
|
30
|
-
if (all.length === 0 && this.opts.seedPlans?.length) {
|
|
31
|
-
for (const p of this.opts.seedPlans) {
|
|
32
|
-
await this.repo.create({ ...p, isActive: true });
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
await this.refresh();
|
|
36
|
-
}
|
|
37
|
-
async list() {
|
|
38
|
-
if (!this.isCacheFresh())
|
|
39
|
-
await this.refresh();
|
|
40
|
-
return this.cache;
|
|
41
|
-
}
|
|
42
|
-
async listAll() {
|
|
43
|
-
return this.repo.list({ includeInactive: true });
|
|
44
|
-
}
|
|
45
|
-
async getPlan(id) {
|
|
46
|
-
if (!this.isCacheFresh())
|
|
47
|
-
await this.refresh();
|
|
48
|
-
// The cache only holds active plans (hot-path callers like UsageService
|
|
49
|
-
// never want inactive ones). For admin lookups by id we fall through to
|
|
50
|
-
// the repo so soft-deleted plans are still reachable for re-activation.
|
|
51
|
-
const cached = this.cache.find((p) => p.id === id);
|
|
52
|
-
if (cached)
|
|
53
|
-
return cached;
|
|
54
|
-
return (await this.repo.findById(id)) ?? undefined;
|
|
55
|
-
}
|
|
56
|
-
async getDefault() {
|
|
57
|
-
const plans = await this.list();
|
|
58
|
-
return plans.find((p) => p.isDefault) ?? plans[0];
|
|
59
|
-
}
|
|
60
|
-
async getDefaultId() {
|
|
61
|
-
return (await this.getDefault())?.id ?? 'free';
|
|
62
|
-
}
|
|
63
|
-
// ─── Admin operations ───────────────────────────────────────────────────
|
|
64
|
-
async create(plan) {
|
|
65
|
-
const existing = await this.repo.findById(plan.id);
|
|
66
|
-
if (existing) {
|
|
67
|
-
throw new PlanServiceError('plan_exists', `Plan "${plan.id}" already exists`);
|
|
68
|
-
}
|
|
69
|
-
const created = await this.repo.create(plan);
|
|
70
|
-
this.invalidate();
|
|
71
|
-
return created;
|
|
72
|
-
}
|
|
73
|
-
async update(id, patch) {
|
|
74
|
-
const existing = await this.repo.findById(id);
|
|
75
|
-
if (!existing) {
|
|
76
|
-
throw new PlanServiceError('plan_not_found', `Plan "${id}" not found`);
|
|
77
|
-
}
|
|
78
|
-
await this.repo.update(id, patch);
|
|
79
|
-
this.invalidate();
|
|
80
|
-
const updated = await this.repo.findById(id);
|
|
81
|
-
return updated;
|
|
82
|
-
}
|
|
83
|
-
async deactivate(id) {
|
|
84
|
-
const existing = await this.repo.findById(id);
|
|
85
|
-
if (!existing) {
|
|
86
|
-
throw new PlanServiceError('plan_not_found', `Plan "${id}" not found`);
|
|
87
|
-
}
|
|
88
|
-
if (existing.isDefault) {
|
|
89
|
-
throw new PlanServiceError('plan_is_default', 'Cannot deactivate the default plan');
|
|
90
|
-
}
|
|
91
|
-
await this.repo.deactivate(id);
|
|
92
|
-
this.invalidate();
|
|
93
|
-
}
|
|
94
|
-
/** Force the next read to bypass the cache. Useful in tests. */
|
|
95
|
-
invalidate() {
|
|
96
|
-
this.cache = null;
|
|
97
|
-
this.cacheLoadedAt = 0;
|
|
98
|
-
}
|
|
99
|
-
async refresh() {
|
|
100
|
-
this.cache = await this.repo.list();
|
|
101
|
-
this.cacheLoadedAt = Date.now();
|
|
102
|
-
}
|
|
103
|
-
isCacheFresh() {
|
|
104
|
-
if (!this.cache)
|
|
105
|
-
return false;
|
|
106
|
-
if (this.cacheTtlMs <= 0)
|
|
107
|
-
return false;
|
|
108
|
-
return Date.now() - this.cacheLoadedAt < this.cacheTtlMs;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
exports.PlanService = PlanService;
|
|
112
|
-
class PlanServiceError extends Error {
|
|
113
|
-
constructor(code, message) {
|
|
114
|
-
super(message);
|
|
115
|
-
this.code = code;
|
|
116
|
-
this.status =
|
|
117
|
-
code === 'plan_not_found' ? 404 : code === 'plan_exists' ? 409 : 400;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
exports.PlanServiceError = PlanServiceError;
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { RefreshTokenRepository } from '../repositories';
|
|
2
|
-
import type { RefreshToken } from '../domain/refresh-token';
|
|
3
|
-
export interface MintedRefreshToken {
|
|
4
|
-
/** Plain token to send to the client. Only available at mint time. */
|
|
5
|
-
token: string;
|
|
6
|
-
entity: RefreshToken;
|
|
7
|
-
}
|
|
8
|
-
export interface IssueOptions {
|
|
9
|
-
family?: string;
|
|
10
|
-
userAgent?: string;
|
|
11
|
-
ip?: string;
|
|
12
|
-
}
|
|
13
|
-
export declare class RefreshTokenError extends Error {
|
|
14
|
-
readonly code: 'invalid' | 'expired' | 'revoked';
|
|
15
|
-
constructor(message: string, code: 'invalid' | 'expired' | 'revoked');
|
|
16
|
-
}
|
|
17
|
-
export interface RefreshTokenServiceOptions {
|
|
18
|
-
/** Refresh token TTL in days. Default: 30. */
|
|
19
|
-
ttlDays?: number;
|
|
20
|
-
/** Hook called when reuse is detected — log/alert from your app. */
|
|
21
|
-
onReuseDetected?: (info: {
|
|
22
|
-
userId: string;
|
|
23
|
-
family: string;
|
|
24
|
-
}) => void;
|
|
25
|
-
}
|
|
26
|
-
export declare class RefreshTokenService {
|
|
27
|
-
private readonly repo;
|
|
28
|
-
private readonly opts;
|
|
29
|
-
constructor(repo: RefreshTokenRepository, opts?: RefreshTokenServiceOptions);
|
|
30
|
-
issue(userId: string, opts?: IssueOptions): Promise<MintedRefreshToken>;
|
|
31
|
-
rotate(rawToken: string, opts?: IssueOptions): Promise<MintedRefreshToken & {
|
|
32
|
-
userId: string;
|
|
33
|
-
}>;
|
|
34
|
-
revoke(rawToken: string): Promise<void>;
|
|
35
|
-
revokeFamily(family: string): Promise<void>;
|
|
36
|
-
revokeAllForUser(userId: string): Promise<void>;
|
|
37
|
-
private hash;
|
|
38
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.RefreshTokenService = exports.RefreshTokenError = void 0;
|
|
4
|
-
const crypto_1 = require("crypto");
|
|
5
|
-
class RefreshTokenError extends Error {
|
|
6
|
-
constructor(message, code) {
|
|
7
|
-
super(message);
|
|
8
|
-
this.code = code;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
exports.RefreshTokenError = RefreshTokenError;
|
|
12
|
-
class RefreshTokenService {
|
|
13
|
-
constructor(repo, opts = {}) {
|
|
14
|
-
this.repo = repo;
|
|
15
|
-
this.opts = opts;
|
|
16
|
-
}
|
|
17
|
-
async issue(userId, opts = {}) {
|
|
18
|
-
const token = (0, crypto_1.randomBytes)(32).toString('hex');
|
|
19
|
-
const tokenHash = this.hash(token);
|
|
20
|
-
const ttlDays = this.opts.ttlDays ?? 30;
|
|
21
|
-
const entity = await this.repo.create({
|
|
22
|
-
userId,
|
|
23
|
-
tokenHash,
|
|
24
|
-
family: opts.family ?? (0, crypto_1.randomUUID)(),
|
|
25
|
-
expiresAt: new Date(Date.now() + ttlDays * 24 * 60 * 60 * 1000),
|
|
26
|
-
userAgent: opts.userAgent,
|
|
27
|
-
ip: opts.ip,
|
|
28
|
-
});
|
|
29
|
-
return { token, entity };
|
|
30
|
-
}
|
|
31
|
-
async rotate(rawToken, opts = {}) {
|
|
32
|
-
const tokenHash = this.hash(rawToken);
|
|
33
|
-
const existing = await this.repo.findByHash(tokenHash);
|
|
34
|
-
if (!existing)
|
|
35
|
-
throw new RefreshTokenError('Invalid refresh token', 'invalid');
|
|
36
|
-
if (existing.expiresAt <= new Date()) {
|
|
37
|
-
throw new RefreshTokenError('Refresh token expired', 'expired');
|
|
38
|
-
}
|
|
39
|
-
if (existing.revokedAt) {
|
|
40
|
-
this.opts.onReuseDetected?.({ userId: existing.userId, family: existing.family });
|
|
41
|
-
await this.repo.revokeFamily(existing.family);
|
|
42
|
-
throw new RefreshTokenError('Refresh token has been revoked', 'revoked');
|
|
43
|
-
}
|
|
44
|
-
const minted = await this.issue(existing.userId, {
|
|
45
|
-
family: existing.family,
|
|
46
|
-
userAgent: opts.userAgent,
|
|
47
|
-
ip: opts.ip,
|
|
48
|
-
});
|
|
49
|
-
await this.repo.update(existing.id, {
|
|
50
|
-
revokedAt: new Date(),
|
|
51
|
-
replacedById: minted.entity.id,
|
|
52
|
-
lastUsedAt: new Date(),
|
|
53
|
-
});
|
|
54
|
-
return { ...minted, userId: existing.userId };
|
|
55
|
-
}
|
|
56
|
-
async revoke(rawToken) {
|
|
57
|
-
const tokenHash = this.hash(rawToken);
|
|
58
|
-
const existing = await this.repo.findByHash(tokenHash);
|
|
59
|
-
if (!existing || existing.revokedAt)
|
|
60
|
-
return;
|
|
61
|
-
await this.repo.update(existing.id, { revokedAt: new Date() });
|
|
62
|
-
}
|
|
63
|
-
async revokeFamily(family) {
|
|
64
|
-
await this.repo.revokeFamily(family);
|
|
65
|
-
}
|
|
66
|
-
async revokeAllForUser(userId) {
|
|
67
|
-
await this.repo.revokeAllForUser(userId);
|
|
68
|
-
}
|
|
69
|
-
hash(token) {
|
|
70
|
-
return (0, crypto_1.createHash)('sha256').update(token).digest('hex');
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
exports.RefreshTokenService = RefreshTokenService;
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
export declare class SecretCryptoError extends Error {
|
|
2
|
-
readonly code: SecretCryptoErrorCode;
|
|
3
|
-
constructor(code: SecretCryptoErrorCode, message: string);
|
|
4
|
-
}
|
|
5
|
-
export type SecretCryptoErrorCode = 'invalid_master_key' | 'invalid_ciphertext' | 'decryption_failed';
|
|
6
|
-
/**
|
|
7
|
-
* Encrypt a UTF-8 string with the given 32-byte master key. Returns a
|
|
8
|
-
* single Buffer that can be persisted as a bytea column.
|
|
9
|
-
*
|
|
10
|
-
* Throws `SecretCryptoError('invalid_master_key', ...)` if `masterKey`
|
|
11
|
-
* isn't exactly 32 bytes — fail loudly at insert time rather than
|
|
12
|
-
* silently corrupt data with a short/long key.
|
|
13
|
-
*/
|
|
14
|
-
export declare function encrypt(plaintext: string, masterKey: Buffer): Buffer;
|
|
15
|
-
/**
|
|
16
|
-
* Reverse of `encrypt`. Throws `SecretCryptoError('decryption_failed', ...)`
|
|
17
|
-
* when the ciphertext was produced with a different key, has been
|
|
18
|
-
* tampered with, or the tag doesn't validate — there's no way to tell
|
|
19
|
-
* these apart with GCM, and we shouldn't reveal which one anyway.
|
|
20
|
-
*/
|
|
21
|
-
export declare function decrypt(blob: Buffer, masterKey: Buffer): string;
|
|
22
|
-
/**
|
|
23
|
-
* Parse a master key from its env-var representation. Accepts:
|
|
24
|
-
* - 64-char hex string (output of `openssl rand -hex 32`)
|
|
25
|
-
* - base64 string that decodes to exactly 32 bytes
|
|
26
|
-
*
|
|
27
|
-
* Throws with a clear message if neither — operators set this once and
|
|
28
|
-
* we don't want them silently running with a 16-byte key.
|
|
29
|
-
*/
|
|
30
|
-
export declare function parseMasterKey(raw: string): Buffer;
|
|
31
|
-
/**
|
|
32
|
-
* Compute a stable hint from the plaintext for the UI. Returns the last
|
|
33
|
-
* 4 chars prefixed with "..." (e.g. "...xZ12"). For values shorter than
|
|
34
|
-
* 4 chars, returns a generic placeholder so we never echo the whole
|
|
35
|
-
* secret back.
|
|
36
|
-
*/
|
|
37
|
-
export declare function valueHint(plaintext: string): string;
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// AES-256-GCM utilities for encrypting platform secrets at rest.
|
|
3
|
-
//
|
|
4
|
-
// Storage layout: a single buffer `iv (12B) | ciphertext (var) | tag (16B)`.
|
|
5
|
-
// Keeping the IV inline (rather than in a separate column) means the row
|
|
6
|
-
// is self-contained — if you ever export/import the table, you only carry
|
|
7
|
-
// one blob per secret instead of juggling related fields.
|
|
8
|
-
//
|
|
9
|
-
// AES-GCM provides authenticated encryption: any tampering with the
|
|
10
|
-
// ciphertext (or wrong key) fails decryption explicitly rather than
|
|
11
|
-
// returning garbage. That's why decrypt() throws on mismatch — callers
|
|
12
|
-
// must treat any error as "this secret is unrecoverable, fall back to
|
|
13
|
-
// env var or ask the operator to re-enter".
|
|
14
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.SecretCryptoError = void 0;
|
|
16
|
-
exports.encrypt = encrypt;
|
|
17
|
-
exports.decrypt = decrypt;
|
|
18
|
-
exports.parseMasterKey = parseMasterKey;
|
|
19
|
-
exports.valueHint = valueHint;
|
|
20
|
-
const crypto_1 = require("crypto");
|
|
21
|
-
const ALGO = 'aes-256-gcm';
|
|
22
|
-
const IV_LEN = 12;
|
|
23
|
-
const TAG_LEN = 16;
|
|
24
|
-
const KEY_LEN = 32;
|
|
25
|
-
class SecretCryptoError extends Error {
|
|
26
|
-
constructor(code, message) {
|
|
27
|
-
super(message);
|
|
28
|
-
this.code = code;
|
|
29
|
-
this.name = 'SecretCryptoError';
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
exports.SecretCryptoError = SecretCryptoError;
|
|
33
|
-
/**
|
|
34
|
-
* Encrypt a UTF-8 string with the given 32-byte master key. Returns a
|
|
35
|
-
* single Buffer that can be persisted as a bytea column.
|
|
36
|
-
*
|
|
37
|
-
* Throws `SecretCryptoError('invalid_master_key', ...)` if `masterKey`
|
|
38
|
-
* isn't exactly 32 bytes — fail loudly at insert time rather than
|
|
39
|
-
* silently corrupt data with a short/long key.
|
|
40
|
-
*/
|
|
41
|
-
function encrypt(plaintext, masterKey) {
|
|
42
|
-
assertMasterKey(masterKey);
|
|
43
|
-
const iv = (0, crypto_1.randomBytes)(IV_LEN);
|
|
44
|
-
const cipher = (0, crypto_1.createCipheriv)(ALGO, masterKey, iv);
|
|
45
|
-
const ct = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
46
|
-
const tag = cipher.getAuthTag();
|
|
47
|
-
return Buffer.concat([iv, ct, tag]);
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Reverse of `encrypt`. Throws `SecretCryptoError('decryption_failed', ...)`
|
|
51
|
-
* when the ciphertext was produced with a different key, has been
|
|
52
|
-
* tampered with, or the tag doesn't validate — there's no way to tell
|
|
53
|
-
* these apart with GCM, and we shouldn't reveal which one anyway.
|
|
54
|
-
*/
|
|
55
|
-
function decrypt(blob, masterKey) {
|
|
56
|
-
assertMasterKey(masterKey);
|
|
57
|
-
if (blob.length < IV_LEN + TAG_LEN) {
|
|
58
|
-
throw new SecretCryptoError('invalid_ciphertext', `ciphertext too short: ${blob.length} bytes (need ≥ ${IV_LEN + TAG_LEN})`);
|
|
59
|
-
}
|
|
60
|
-
const iv = blob.subarray(0, IV_LEN);
|
|
61
|
-
const tag = blob.subarray(blob.length - TAG_LEN);
|
|
62
|
-
const ct = blob.subarray(IV_LEN, blob.length - TAG_LEN);
|
|
63
|
-
const decipher = (0, crypto_1.createDecipheriv)(ALGO, masterKey, iv);
|
|
64
|
-
decipher.setAuthTag(tag);
|
|
65
|
-
try {
|
|
66
|
-
return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8');
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
throw new SecretCryptoError('decryption_failed', 'decryption failed (wrong key or tampered ciphertext)');
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Parse a master key from its env-var representation. Accepts:
|
|
74
|
-
* - 64-char hex string (output of `openssl rand -hex 32`)
|
|
75
|
-
* - base64 string that decodes to exactly 32 bytes
|
|
76
|
-
*
|
|
77
|
-
* Throws with a clear message if neither — operators set this once and
|
|
78
|
-
* we don't want them silently running with a 16-byte key.
|
|
79
|
-
*/
|
|
80
|
-
function parseMasterKey(raw) {
|
|
81
|
-
const trimmed = raw.trim();
|
|
82
|
-
if (/^[0-9a-fA-F]{64}$/.test(trimmed)) {
|
|
83
|
-
return Buffer.from(trimmed, 'hex');
|
|
84
|
-
}
|
|
85
|
-
try {
|
|
86
|
-
const decoded = Buffer.from(trimmed, 'base64');
|
|
87
|
-
if (decoded.length === KEY_LEN)
|
|
88
|
-
return decoded;
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
// fall through to the throw below
|
|
92
|
-
}
|
|
93
|
-
throw new SecretCryptoError('invalid_master_key', `MASTER_KEY must be 32 bytes (64 hex chars or base64-encoded). Generate one with: openssl rand -hex 32`);
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Compute a stable hint from the plaintext for the UI. Returns the last
|
|
97
|
-
* 4 chars prefixed with "..." (e.g. "...xZ12"). For values shorter than
|
|
98
|
-
* 4 chars, returns a generic placeholder so we never echo the whole
|
|
99
|
-
* secret back.
|
|
100
|
-
*/
|
|
101
|
-
function valueHint(plaintext) {
|
|
102
|
-
if (plaintext.length <= 4)
|
|
103
|
-
return '••••';
|
|
104
|
-
return '...' + plaintext.slice(-4);
|
|
105
|
-
}
|
|
106
|
-
function assertMasterKey(key) {
|
|
107
|
-
if (!Buffer.isBuffer(key) || key.length !== KEY_LEN) {
|
|
108
|
-
throw new SecretCryptoError('invalid_master_key', `master key must be a 32-byte Buffer (got ${Buffer.isBuffer(key) ? `${key.length} bytes` : typeof key})`);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
export type SecretSensitivity = 'critical' | 'high' | 'medium' | 'low';
|
|
2
|
-
export interface KnownSecretDefinition {
|
|
3
|
-
/** Stable identifier, matches the env-var name. */
|
|
4
|
-
key: string;
|
|
5
|
-
/** One-line explanation shown next to the field in the admin UI. */
|
|
6
|
-
description: string;
|
|
7
|
-
sensitivity: SecretSensitivity;
|
|
8
|
-
}
|
|
9
|
-
export declare const KNOWN_SECRETS: {
|
|
10
|
-
readonly ANTHROPIC_API_KEY: {
|
|
11
|
-
readonly key: "ANTHROPIC_API_KEY";
|
|
12
|
-
readonly description: "LLM provider key. Required — without it the chat endpoints stop working.";
|
|
13
|
-
readonly sensitivity: "critical";
|
|
14
|
-
};
|
|
15
|
-
readonly STRIPE_SECRET_KEY: {
|
|
16
|
-
readonly key: "STRIPE_SECRET_KEY";
|
|
17
|
-
readonly description: "Stripe secret for B2B + B2C billing. Pair with STRIPE_WEBHOOK_SECRET.";
|
|
18
|
-
readonly sensitivity: "high";
|
|
19
|
-
};
|
|
20
|
-
readonly STRIPE_WEBHOOK_SECRET: {
|
|
21
|
-
readonly key: "STRIPE_WEBHOOK_SECRET";
|
|
22
|
-
readonly description: "Signs incoming Stripe webhooks. Rotate together with STRIPE_SECRET_KEY.";
|
|
23
|
-
readonly sensitivity: "high";
|
|
24
|
-
};
|
|
25
|
-
readonly TAVILY_API_KEY: {
|
|
26
|
-
readonly key: "TAVILY_API_KEY";
|
|
27
|
-
readonly description: "Web search via Tavily. Optional — when missing, web_search falls back to Brave or is omitted.";
|
|
28
|
-
readonly sensitivity: "medium";
|
|
29
|
-
};
|
|
30
|
-
readonly BRAVE_API_KEY: {
|
|
31
|
-
readonly key: "BRAVE_API_KEY";
|
|
32
|
-
readonly description: "Web search via Brave Search. Used only when TAVILY_API_KEY is empty.";
|
|
33
|
-
readonly sensitivity: "medium";
|
|
34
|
-
};
|
|
35
|
-
};
|
|
36
|
-
export type KnownSecretKey = keyof typeof KNOWN_SECRETS;
|
|
37
|
-
export declare function isKnownSecretKey(key: string): key is KnownSecretKey;
|
|
38
|
-
export declare function listKnownSecrets(): KnownSecretDefinition[];
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// The whitelist of platform secrets the operator can manage.
|
|
3
|
-
//
|
|
4
|
-
// Keeping this hardcoded (rather than letting the admin invent new keys
|
|
5
|
-
// from the UI) is deliberate: every secret has to be CONSUMED by code
|
|
6
|
-
// somewhere in the codebase — adding a row that nothing reads is just
|
|
7
|
-
// data clutter and a foot-gun ("why isn't TAVLY_KEY working?"). Adding
|
|
8
|
-
// a new secret is a code change anyway, so the catalog lives in code.
|
|
9
|
-
//
|
|
10
|
-
// `sensitivity` exists so the UI can ask for an extra confirmation step
|
|
11
|
-
// when editing a critical key. Rotating ANTHROPIC_API_KEY mid-conversation
|
|
12
|
-
// breaks every in-flight request; rotating TAVILY_API_KEY just affects
|
|
13
|
-
// the next web search. The UI shouldn't treat them the same.
|
|
14
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.KNOWN_SECRETS = void 0;
|
|
16
|
-
exports.isKnownSecretKey = isKnownSecretKey;
|
|
17
|
-
exports.listKnownSecrets = listKnownSecrets;
|
|
18
|
-
exports.KNOWN_SECRETS = {
|
|
19
|
-
ANTHROPIC_API_KEY: {
|
|
20
|
-
key: 'ANTHROPIC_API_KEY',
|
|
21
|
-
description: 'LLM provider key. Required — without it the chat endpoints stop working.',
|
|
22
|
-
sensitivity: 'critical',
|
|
23
|
-
},
|
|
24
|
-
STRIPE_SECRET_KEY: {
|
|
25
|
-
key: 'STRIPE_SECRET_KEY',
|
|
26
|
-
description: 'Stripe secret for B2B + B2C billing. Pair with STRIPE_WEBHOOK_SECRET.',
|
|
27
|
-
sensitivity: 'high',
|
|
28
|
-
},
|
|
29
|
-
STRIPE_WEBHOOK_SECRET: {
|
|
30
|
-
key: 'STRIPE_WEBHOOK_SECRET',
|
|
31
|
-
description: 'Signs incoming Stripe webhooks. Rotate together with STRIPE_SECRET_KEY.',
|
|
32
|
-
sensitivity: 'high',
|
|
33
|
-
},
|
|
34
|
-
TAVILY_API_KEY: {
|
|
35
|
-
key: 'TAVILY_API_KEY',
|
|
36
|
-
description: 'Web search via Tavily. Optional — when missing, web_search falls back to Brave or is omitted.',
|
|
37
|
-
sensitivity: 'medium',
|
|
38
|
-
},
|
|
39
|
-
BRAVE_API_KEY: {
|
|
40
|
-
key: 'BRAVE_API_KEY',
|
|
41
|
-
description: 'Web search via Brave Search. Used only when TAVILY_API_KEY is empty.',
|
|
42
|
-
sensitivity: 'medium',
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
function isKnownSecretKey(key) {
|
|
46
|
-
return Object.prototype.hasOwnProperty.call(exports.KNOWN_SECRETS, key);
|
|
47
|
-
}
|
|
48
|
-
function listKnownSecrets() {
|
|
49
|
-
return Object.values(exports.KNOWN_SECRETS);
|
|
50
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import type { Logger } from './tool-registry.service';
|
|
2
|
-
import type { PlatformSecretRepository } from '../repositories';
|
|
3
|
-
import type { PlatformSecret } from '../domain/platform-secret';
|
|
4
|
-
import { type KnownSecretKey, type KnownSecretDefinition } from './secrets/known-keys';
|
|
5
|
-
export type SecretsErrorCode = 'unknown_key' | 'env_override_active' | 'master_key_missing';
|
|
6
|
-
export declare class SecretsError extends Error {
|
|
7
|
-
readonly code: SecretsErrorCode;
|
|
8
|
-
readonly status: number;
|
|
9
|
-
constructor(code: SecretsErrorCode, message: string);
|
|
10
|
-
}
|
|
11
|
-
/** What admin endpoints / UI receive about a single known secret. Never
|
|
12
|
-
* carries plaintext or ciphertext — only metadata + hint. */
|
|
13
|
-
export interface PlatformSecretEntry {
|
|
14
|
-
key: KnownSecretKey;
|
|
15
|
-
description: string;
|
|
16
|
-
sensitivity: KnownSecretDefinition['sensitivity'];
|
|
17
|
-
/** `true` when the value can actually be resolved at boot. */
|
|
18
|
-
configured: boolean;
|
|
19
|
-
/** Where the value would come from. `none` when not configured anywhere. */
|
|
20
|
-
source: 'env' | 'db' | 'none';
|
|
21
|
-
/** Last 4 chars of the plaintext, or undefined when source !== 'db'. */
|
|
22
|
-
valueHint?: string;
|
|
23
|
-
/** Set by the UI; undefined for env-sourced entries. */
|
|
24
|
-
updatedAt?: Date;
|
|
25
|
-
updatedByUserId?: string;
|
|
26
|
-
}
|
|
27
|
-
export interface SecretsServiceOptions {
|
|
28
|
-
/** When omitted, the service starts but every DB-path call returns
|
|
29
|
-
* `master_key_missing`. Lets the rest of AgentForge boot when an
|
|
30
|
-
* operator forgot to set MASTER_KEY — they can still log in and fix it
|
|
31
|
-
* from the UI rather than the server refusing to start. */
|
|
32
|
-
masterKey?: Buffer;
|
|
33
|
-
logger?: Logger;
|
|
34
|
-
/** Override for tests. Defaults to `process.env`. */
|
|
35
|
-
env?: NodeJS.ProcessEnv;
|
|
36
|
-
}
|
|
37
|
-
export declare class SecretsService {
|
|
38
|
-
private readonly repo;
|
|
39
|
-
private readonly cache;
|
|
40
|
-
private readonly logger;
|
|
41
|
-
private readonly env;
|
|
42
|
-
private readonly masterKey?;
|
|
43
|
-
constructor(repo: PlatformSecretRepository, opts?: SecretsServiceOptions);
|
|
44
|
-
/**
|
|
45
|
-
* Resolve a known key. Returns the plaintext or `undefined` when neither
|
|
46
|
-
* env nor DB has it set. Callers decide whether undefined is fatal (it
|
|
47
|
-
* is for ANTHROPIC_API_KEY; it's not for TAVILY_API_KEY).
|
|
48
|
-
*
|
|
49
|
-
* Cached in memory after first read. Cache survives until the next
|
|
50
|
-
* upsert/delete on the same key.
|
|
51
|
-
*/
|
|
52
|
-
resolve(key: KnownSecretKey | (string & {})): Promise<string | undefined>;
|
|
53
|
-
/**
|
|
54
|
-
* Bulk variant — resolve every known key at once. Useful at boot when
|
|
55
|
-
* the server wants to decide which adapters to wire up (Stripe on/off,
|
|
56
|
-
* which search provider, etc.). Skips logging on individual misses to
|
|
57
|
-
* keep the boot log readable.
|
|
58
|
-
*/
|
|
59
|
-
resolveAll(): Promise<Partial<Record<KnownSecretKey, string>>>;
|
|
60
|
-
/**
|
|
61
|
-
* Admin-facing catalog. Returns one entry per known key — including
|
|
62
|
-
* the ones that aren't configured anywhere — so the UI can render the
|
|
63
|
-
* whole table in one shot.
|
|
64
|
-
*
|
|
65
|
-
* Never returns plaintext or ciphertext: only hint + metadata.
|
|
66
|
-
*/
|
|
67
|
-
listForAdmin(): Promise<PlatformSecretEntry[]>;
|
|
68
|
-
/**
|
|
69
|
-
* Upsert a known secret. The new ciphertext + hint hit the DB; the
|
|
70
|
-
* cache entry is evicted so the next `resolve` re-reads (and decrypts)
|
|
71
|
-
* fresh data. Returns the row metadata so the UI can echo the hint.
|
|
72
|
-
*
|
|
73
|
-
* Throws `unknown_key` for keys outside the catalog (no free-form
|
|
74
|
-
* inserts), `master_key_missing` when MASTER_KEY isn't configured.
|
|
75
|
-
*/
|
|
76
|
-
upsert(input: {
|
|
77
|
-
key: string;
|
|
78
|
-
value: string;
|
|
79
|
-
description?: string;
|
|
80
|
-
updatedByUserId?: string;
|
|
81
|
-
}): Promise<PlatformSecret>;
|
|
82
|
-
/**
|
|
83
|
-
* Hard-delete a known secret. Refuses while the matching env var is
|
|
84
|
-
* set — otherwise the operator would see "still configured" in the UI
|
|
85
|
-
* after deleting, which is confusing.
|
|
86
|
-
*/
|
|
87
|
-
delete(key: string): Promise<void>;
|
|
88
|
-
/** For tests + admin tools: drop everything from cache so the next
|
|
89
|
-
* resolve goes back to env/DB. */
|
|
90
|
-
clearCache(): void;
|
|
91
|
-
}
|