The legacy application - its src, tests, landing site, promo assets, docs, release tooling, CI workflows, and root configs - leaves the tree; it stays reachable in history. Everything under AgentCompany-New moves up unchanged: same package name, so the production userData/database path is untouched, and e2e/build scripts already resolved paths relative to the project. The developer cred.txt stays gitignored at its new location. Full verify:ci passes from the new root.
181 lines
6.3 KiB
TypeScript
181 lines
6.3 KiB
TypeScript
import type { DatabaseSync } from "node:sqlite";
|
|
import { createHash } from "node:crypto";
|
|
import type { ProviderAuthType } from "../../shared/domain";
|
|
import { createId } from "../../shared/ids";
|
|
import { AuditService } from "./audit-service";
|
|
import { SecretVault } from "./secret-vault";
|
|
|
|
export interface ProviderSecretVault {
|
|
encryptSecret(secret: string): Buffer;
|
|
decryptSecret(ciphertext: Buffer): string;
|
|
}
|
|
|
|
export interface CreateProviderAuthProfileInput {
|
|
orgId: string;
|
|
providerId: string;
|
|
providerKey: string;
|
|
authType: ProviderAuthType;
|
|
secret: string;
|
|
profileKind: string;
|
|
sourceType: string;
|
|
sourceRef: string | null;
|
|
capabilityTags: string[];
|
|
}
|
|
|
|
export interface ProviderAuthProfileRef {
|
|
authProfileId: string;
|
|
credentialId: string;
|
|
displayHint: string;
|
|
}
|
|
|
|
export interface RevokeProviderAuthProfileInput {
|
|
orgId: string;
|
|
providerId: string;
|
|
providerKey: string;
|
|
}
|
|
|
|
export class AuthBrokerService {
|
|
private readonly audit: AuditService;
|
|
|
|
constructor(
|
|
private readonly db: DatabaseSync,
|
|
private readonly vault: ProviderSecretVault = new SecretVault(),
|
|
) {
|
|
this.audit = new AuditService(db);
|
|
}
|
|
|
|
createProviderProfile(input: CreateProviderAuthProfileInput): ProviderAuthProfileRef {
|
|
const secret = input.secret.trim();
|
|
if (!secret) throw new Error("Credential is required.");
|
|
const credentialId = createId("cred");
|
|
const authProfileId = createId("authp");
|
|
const displayHint = credentialDisplayHint(secret);
|
|
const now = new Date().toISOString();
|
|
const capabilityJson = JSON.stringify({
|
|
providerKey: input.providerKey,
|
|
authType: input.authType,
|
|
capabilityTags: input.capabilityTags,
|
|
});
|
|
|
|
this.db.prepare(`
|
|
insert into credentials(id, org_id, provider_key, kind, ciphertext, display_hint, created_at)
|
|
values (?, ?, ?, ?, ?, ?, ?)
|
|
`).run(credentialId, input.orgId, input.providerKey, input.authType, this.vault.encryptSecret(secret), displayHint, now);
|
|
this.db.prepare(`
|
|
update provider_auth_profiles
|
|
set active = 0
|
|
where org_id = ? and provider_id = ? and active = 1
|
|
`).run(input.orgId, input.providerId);
|
|
this.db.prepare(`
|
|
insert into provider_auth_profiles(
|
|
id, org_id, provider_id, credential_id, provider_key, auth_type,
|
|
profile_kind, display_hint, source_type, source_ref, capability_json,
|
|
active, created_at
|
|
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)
|
|
`).run(
|
|
authProfileId,
|
|
input.orgId,
|
|
input.providerId,
|
|
credentialId,
|
|
input.providerKey,
|
|
input.authType,
|
|
input.profileKind,
|
|
displayHint,
|
|
input.sourceType,
|
|
input.sourceRef,
|
|
capabilityJson,
|
|
now,
|
|
);
|
|
this.audit.record({
|
|
orgId: input.orgId,
|
|
entityType: "provider_auth_profile",
|
|
entityId: authProfileId,
|
|
actorType: "system",
|
|
actorId: null,
|
|
action: "created",
|
|
payload: {
|
|
providerId: input.providerId,
|
|
providerKey: input.providerKey,
|
|
authType: input.authType,
|
|
profileKind: input.profileKind,
|
|
},
|
|
});
|
|
return { authProfileId, credentialId, displayHint };
|
|
}
|
|
|
|
loadProviderSecret(input: {
|
|
orgId: string;
|
|
providerId: string;
|
|
providerKey: string;
|
|
credentialId: string;
|
|
}): string {
|
|
const row = this.db.prepare(`
|
|
select credentials.ciphertext
|
|
from provider_auth_profiles
|
|
join credentials on credentials.id = provider_auth_profiles.credential_id
|
|
where provider_auth_profiles.org_id = ?
|
|
and provider_auth_profiles.provider_id = ?
|
|
and provider_auth_profiles.provider_key = ?
|
|
and provider_auth_profiles.credential_id = ?
|
|
and provider_auth_profiles.active = 1
|
|
and credentials.org_id = provider_auth_profiles.org_id
|
|
and credentials.provider_key = provider_auth_profiles.provider_key
|
|
`).get(input.orgId, input.providerId, input.providerKey, input.credentialId) as { ciphertext: Uint8Array } | undefined;
|
|
if (!row) throw new Error("Provider credential was not found.");
|
|
return this.vault.decryptSecret(Buffer.from(row.ciphertext));
|
|
}
|
|
|
|
/**
|
|
* Re-encrypts a refreshed OAuth credential into its existing credential row,
|
|
* keeping the same credential id (and active auth profile) so request-time
|
|
* token refresh persists without re-binding the provider.
|
|
*/
|
|
updateProviderSecret(input: { orgId: string; providerKey: string; credentialId: string; secret: string }): void {
|
|
const secret = input.secret.trim();
|
|
if (!secret) throw new Error("Credential is required.");
|
|
const displayHint = credentialDisplayHint(secret);
|
|
const result = this.db.prepare(`
|
|
update credentials
|
|
set ciphertext = ?, display_hint = ?
|
|
where id = ? and org_id = ? and provider_key = ?
|
|
`).run(this.vault.encryptSecret(secret), displayHint, input.credentialId, input.orgId, input.providerKey);
|
|
if (result.changes === 0) throw new Error("Provider credential was not found.");
|
|
this.db.prepare("update provider_auth_profiles set display_hint = ? where credential_id = ? and org_id = ?")
|
|
.run(displayHint, input.credentialId, input.orgId);
|
|
}
|
|
|
|
revokeProviderProfile(input: RevokeProviderAuthProfileInput): void {
|
|
const row = this.db.prepare(`
|
|
select id, credential_id
|
|
from provider_auth_profiles
|
|
where org_id = ?
|
|
and provider_id = ?
|
|
and provider_key = ?
|
|
and active = 1
|
|
`).get(input.orgId, input.providerId, input.providerKey) as { id: string; credential_id: string } | undefined;
|
|
if (!row) throw new Error("Provider credential was not found.");
|
|
this.db.prepare("update provider_auth_profiles set active = 0 where id = ? and org_id = ?")
|
|
.run(row.id, input.orgId);
|
|
this.audit.record({
|
|
orgId: input.orgId,
|
|
entityType: "provider_auth_profile",
|
|
entityId: row.id,
|
|
actorType: "system",
|
|
actorId: null,
|
|
action: "revoked",
|
|
payload: {
|
|
providerId: input.providerId,
|
|
providerKey: input.providerKey,
|
|
credentialId: row.credential_id,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
function credentialDisplayHint(secret: string): string {
|
|
const clean = secret.trim();
|
|
if (clean.startsWith("{")) return `json:${createHash("sha256").update(clean).digest("hex").slice(0, 10)}`;
|
|
if (clean.length <= 10) return `${clean.slice(0, 2)}...`;
|
|
return `${clean.slice(0, 6)}...${clean.slice(-4)}`;
|
|
}
|