@cat-factory/integrations 0.6.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/dist/index.d.ts +68 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +84 -0
- package/dist/index.js.map +1 -0
- package/dist/modules/datadog/DatadogClient.d.ts +54 -0
- package/dist/modules/datadog/DatadogClient.d.ts.map +1 -0
- package/dist/modules/datadog/DatadogClient.js +132 -0
- package/dist/modules/datadog/DatadogClient.js.map +1 -0
- package/dist/modules/datadog/DatadogReleaseHealthProvider.d.ts +30 -0
- package/dist/modules/datadog/DatadogReleaseHealthProvider.d.ts.map +1 -0
- package/dist/modules/datadog/DatadogReleaseHealthProvider.js +101 -0
- package/dist/modules/datadog/DatadogReleaseHealthProvider.js.map +1 -0
- package/dist/modules/datadog/datadog.logic.d.ts +37 -0
- package/dist/modules/datadog/datadog.logic.d.ts.map +1 -0
- package/dist/modules/datadog/datadog.logic.js +90 -0
- package/dist/modules/datadog/datadog.logic.js.map +1 -0
- package/dist/modules/documents/ConfluenceProvider.d.ts +29 -0
- package/dist/modules/documents/ConfluenceProvider.d.ts.map +1 -0
- package/dist/modules/documents/ConfluenceProvider.js +180 -0
- package/dist/modules/documents/ConfluenceProvider.js.map +1 -0
- package/dist/modules/documents/DocumentConnectionService.d.ts +30 -0
- package/dist/modules/documents/DocumentConnectionService.d.ts.map +1 -0
- package/dist/modules/documents/DocumentConnectionService.js +69 -0
- package/dist/modules/documents/DocumentConnectionService.js.map +1 -0
- package/dist/modules/documents/DocumentImportService.d.ts +34 -0
- package/dist/modules/documents/DocumentImportService.d.ts.map +1 -0
- package/dist/modules/documents/DocumentImportService.js +83 -0
- package/dist/modules/documents/DocumentImportService.js.map +1 -0
- package/dist/modules/documents/DocumentLinkService.d.ts +31 -0
- package/dist/modules/documents/DocumentLinkService.d.ts.map +1 -0
- package/dist/modules/documents/DocumentLinkService.js +75 -0
- package/dist/modules/documents/DocumentLinkService.js.map +1 -0
- package/dist/modules/documents/DocumentPlannerService.d.ts +23 -0
- package/dist/modules/documents/DocumentPlannerService.d.ts.map +1 -0
- package/dist/modules/documents/DocumentPlannerService.js +96 -0
- package/dist/modules/documents/DocumentPlannerService.js.map +1 -0
- package/dist/modules/documents/GitHubDocsProvider.d.ts +42 -0
- package/dist/modules/documents/GitHubDocsProvider.d.ts.map +1 -0
- package/dist/modules/documents/GitHubDocsProvider.js +86 -0
- package/dist/modules/documents/GitHubDocsProvider.js.map +1 -0
- package/dist/modules/documents/NotionProvider.d.ts +32 -0
- package/dist/modules/documents/NotionProvider.d.ts.map +1 -0
- package/dist/modules/documents/NotionProvider.js +221 -0
- package/dist/modules/documents/NotionProvider.js.map +1 -0
- package/dist/modules/documents/confluence.logic.d.ts +37 -0
- package/dist/modules/documents/confluence.logic.d.ts.map +1 -0
- package/dist/modules/documents/confluence.logic.js +133 -0
- package/dist/modules/documents/confluence.logic.js.map +1 -0
- package/dist/modules/documents/documents.logic.d.ts +22 -0
- package/dist/modules/documents/documents.logic.d.ts.map +1 -0
- package/dist/modules/documents/documents.logic.js +138 -0
- package/dist/modules/documents/documents.logic.js.map +1 -0
- package/dist/modules/documents/github-docs.logic.d.ts +52 -0
- package/dist/modules/documents/github-docs.logic.d.ts.map +1 -0
- package/dist/modules/documents/github-docs.logic.js +94 -0
- package/dist/modules/documents/github-docs.logic.js.map +1 -0
- package/dist/modules/documents/notion.logic.d.ts +31 -0
- package/dist/modules/documents/notion.logic.d.ts.map +1 -0
- package/dist/modules/documents/notion.logic.js +142 -0
- package/dist/modules/documents/notion.logic.js.map +1 -0
- package/dist/modules/email/EmailConnectionService.d.ts +34 -0
- package/dist/modules/email/EmailConnectionService.d.ts.map +1 -0
- package/dist/modules/email/EmailConnectionService.js +82 -0
- package/dist/modules/email/EmailConnectionService.js.map +1 -0
- package/dist/modules/email/adapters.d.ts +39 -0
- package/dist/modules/email/adapters.d.ts.map +1 -0
- package/dist/modules/email/adapters.js +79 -0
- package/dist/modules/email/adapters.js.map +1 -0
- package/dist/modules/environments/EnvironmentConnectionService.d.ts +42 -0
- package/dist/modules/environments/EnvironmentConnectionService.d.ts.map +1 -0
- package/dist/modules/environments/EnvironmentConnectionService.js +120 -0
- package/dist/modules/environments/EnvironmentConnectionService.js.map +1 -0
- package/dist/modules/environments/EnvironmentProvisioningService.d.ts +56 -0
- package/dist/modules/environments/EnvironmentProvisioningService.d.ts.map +1 -0
- package/dist/modules/environments/EnvironmentProvisioningService.js +153 -0
- package/dist/modules/environments/EnvironmentProvisioningService.js.map +1 -0
- package/dist/modules/environments/EnvironmentTeardownService.d.ts +24 -0
- package/dist/modules/environments/EnvironmentTeardownService.d.ts.map +1 -0
- package/dist/modules/environments/EnvironmentTeardownService.js +54 -0
- package/dist/modules/environments/EnvironmentTeardownService.js.map +1 -0
- package/dist/modules/environments/HttpEnvironmentProvider.d.ts +30 -0
- package/dist/modules/environments/HttpEnvironmentProvider.d.ts.map +1 -0
- package/dist/modules/environments/HttpEnvironmentProvider.js +316 -0
- package/dist/modules/environments/HttpEnvironmentProvider.js.map +1 -0
- package/dist/modules/environments/environments.logic.d.ts +50 -0
- package/dist/modules/environments/environments.logic.d.ts.map +1 -0
- package/dist/modules/environments/environments.logic.js +257 -0
- package/dist/modules/environments/environments.logic.js.map +1 -0
- package/dist/modules/github/GitHubInstallationService.d.ts +66 -0
- package/dist/modules/github/GitHubInstallationService.d.ts.map +1 -0
- package/dist/modules/github/GitHubInstallationService.js +143 -0
- package/dist/modules/github/GitHubInstallationService.js.map +1 -0
- package/dist/modules/github/GitHubService.d.ts +29 -0
- package/dist/modules/github/GitHubService.d.ts.map +1 -0
- package/dist/modules/github/GitHubService.js +61 -0
- package/dist/modules/github/GitHubService.js.map +1 -0
- package/dist/modules/github/GitHubSyncService.d.ts +97 -0
- package/dist/modules/github/GitHubSyncService.d.ts.map +1 -0
- package/dist/modules/github/GitHubSyncService.js +241 -0
- package/dist/modules/github/GitHubSyncService.js.map +1 -0
- package/dist/modules/github/RepoProvisioningService.d.ts +26 -0
- package/dist/modules/github/RepoProvisioningService.d.ts.map +1 -0
- package/dist/modules/github/RepoProvisioningService.js +36 -0
- package/dist/modules/github/RepoProvisioningService.js.map +1 -0
- package/dist/modules/github/WebhookService.d.ts +28 -0
- package/dist/modules/github/WebhookService.d.ts.map +1 -0
- package/dist/modules/github/WebhookService.js +156 -0
- package/dist/modules/github/WebhookService.js.map +1 -0
- package/dist/modules/github/projection.logic.d.ts +95 -0
- package/dist/modules/github/projection.logic.d.ts.map +1 -0
- package/dist/modules/github/projection.logic.js +94 -0
- package/dist/modules/github/projection.logic.js.map +1 -0
- package/dist/modules/github/provisioning.logic.d.ts +11 -0
- package/dist/modules/github/provisioning.logic.d.ts.map +1 -0
- package/dist/modules/github/provisioning.logic.js +18 -0
- package/dist/modules/github/provisioning.logic.js.map +1 -0
- package/dist/modules/incident/incident.logic.d.ts +16 -0
- package/dist/modules/incident/incident.logic.d.ts.map +1 -0
- package/dist/modules/incident/incident.logic.js +23 -0
- package/dist/modules/incident/incident.logic.js.map +1 -0
- package/dist/modules/incidentio/IncidentIoEnrichmentProvider.d.ts +26 -0
- package/dist/modules/incidentio/IncidentIoEnrichmentProvider.d.ts.map +1 -0
- package/dist/modules/incidentio/IncidentIoEnrichmentProvider.js +84 -0
- package/dist/modules/incidentio/IncidentIoEnrichmentProvider.js.map +1 -0
- package/dist/modules/pagerduty/PagerDutyEnrichmentProvider.d.ts +27 -0
- package/dist/modules/pagerduty/PagerDutyEnrichmentProvider.d.ts.map +1 -0
- package/dist/modules/pagerduty/PagerDutyEnrichmentProvider.js +65 -0
- package/dist/modules/pagerduty/PagerDutyEnrichmentProvider.js.map +1 -0
- package/dist/modules/providers/ApiKeyService.d.ts +73 -0
- package/dist/modules/providers/ApiKeyService.d.ts.map +1 -0
- package/dist/modules/providers/ApiKeyService.js +122 -0
- package/dist/modules/providers/ApiKeyService.js.map +1 -0
- package/dist/modules/providers/LocalModelEndpointService.d.ts +52 -0
- package/dist/modules/providers/LocalModelEndpointService.d.ts.map +1 -0
- package/dist/modules/providers/LocalModelEndpointService.js +131 -0
- package/dist/modules/providers/LocalModelEndpointService.js.map +1 -0
- package/dist/modules/providers/PersonalSubscriptionService.d.ts +94 -0
- package/dist/modules/providers/PersonalSubscriptionService.d.ts.map +1 -0
- package/dist/modules/providers/PersonalSubscriptionService.js +218 -0
- package/dist/modules/providers/PersonalSubscriptionService.js.map +1 -0
- package/dist/modules/providers/ProviderSubscriptionService.d.ts +75 -0
- package/dist/modules/providers/ProviderSubscriptionService.d.ts.map +1 -0
- package/dist/modules/providers/ProviderSubscriptionService.js +130 -0
- package/dist/modules/providers/ProviderSubscriptionService.js.map +1 -0
- package/dist/modules/providers/localModelUrl.d.ts +7 -0
- package/dist/modules/providers/localModelUrl.d.ts.map +1 -0
- package/dist/modules/providers/localModelUrl.js +67 -0
- package/dist/modules/providers/localModelUrl.js.map +1 -0
- package/dist/modules/providers/providers.logic.d.ts +23 -0
- package/dist/modules/providers/providers.logic.d.ts.map +1 -0
- package/dist/modules/providers/providers.logic.js +46 -0
- package/dist/modules/providers/providers.logic.js.map +1 -0
- package/dist/modules/runners/HttpRunnerPoolProvider.d.ts +51 -0
- package/dist/modules/runners/HttpRunnerPoolProvider.d.ts.map +1 -0
- package/dist/modules/runners/HttpRunnerPoolProvider.js +304 -0
- package/dist/modules/runners/HttpRunnerPoolProvider.js.map +1 -0
- package/dist/modules/runners/RunnerPoolConnectionService.d.ts +47 -0
- package/dist/modules/runners/RunnerPoolConnectionService.d.ts.map +1 -0
- package/dist/modules/runners/RunnerPoolConnectionService.js +98 -0
- package/dist/modules/runners/RunnerPoolConnectionService.js.map +1 -0
- package/dist/modules/runners/RunnerPoolTransport.d.ts +11 -0
- package/dist/modules/runners/RunnerPoolTransport.d.ts.map +1 -0
- package/dist/modules/runners/RunnerPoolTransport.js +61 -0
- package/dist/modules/runners/RunnerPoolTransport.js.map +1 -0
- package/dist/modules/runners/runners.logic.d.ts +16 -0
- package/dist/modules/runners/runners.logic.d.ts.map +1 -0
- package/dist/modules/runners/runners.logic.js +52 -0
- package/dist/modules/runners/runners.logic.js.map +1 -0
- package/dist/modules/slack/SlackApiClient.d.ts +67 -0
- package/dist/modules/slack/SlackApiClient.d.ts.map +1 -0
- package/dist/modules/slack/SlackApiClient.js +132 -0
- package/dist/modules/slack/SlackApiClient.js.map +1 -0
- package/dist/modules/slack/SlackConnectionService.d.ts +41 -0
- package/dist/modules/slack/SlackConnectionService.d.ts.map +1 -0
- package/dist/modules/slack/SlackConnectionService.js +136 -0
- package/dist/modules/slack/SlackConnectionService.js.map +1 -0
- package/dist/modules/slack/SlackMemberMappingService.d.ts +17 -0
- package/dist/modules/slack/SlackMemberMappingService.d.ts.map +1 -0
- package/dist/modules/slack/SlackMemberMappingService.js +28 -0
- package/dist/modules/slack/SlackMemberMappingService.js.map +1 -0
- package/dist/modules/slack/SlackNotificationChannel.d.ts +45 -0
- package/dist/modules/slack/SlackNotificationChannel.d.ts.map +1 -0
- package/dist/modules/slack/SlackNotificationChannel.js +84 -0
- package/dist/modules/slack/SlackNotificationChannel.js.map +1 -0
- package/dist/modules/slack/SlackSettingsService.d.ts +16 -0
- package/dist/modules/slack/SlackSettingsService.d.ts.map +1 -0
- package/dist/modules/slack/SlackSettingsService.js +41 -0
- package/dist/modules/slack/SlackSettingsService.js.map +1 -0
- package/dist/modules/slack/slack.logic.d.ts +55 -0
- package/dist/modules/slack/slack.logic.d.ts.map +1 -0
- package/dist/modules/slack/slack.logic.js +149 -0
- package/dist/modules/slack/slack.logic.js.map +1 -0
- package/dist/modules/tasks/GitHubIssuesProvider.d.ts +50 -0
- package/dist/modules/tasks/GitHubIssuesProvider.d.ts.map +1 -0
- package/dist/modules/tasks/GitHubIssuesProvider.js +92 -0
- package/dist/modules/tasks/GitHubIssuesProvider.js.map +1 -0
- package/dist/modules/tasks/JiraProvider.d.ts +29 -0
- package/dist/modules/tasks/JiraProvider.d.ts.map +1 -0
- package/dist/modules/tasks/JiraProvider.js +114 -0
- package/dist/modules/tasks/JiraProvider.js.map +1 -0
- package/dist/modules/tasks/TaskConnectionService.d.ts +30 -0
- package/dist/modules/tasks/TaskConnectionService.d.ts.map +1 -0
- package/dist/modules/tasks/TaskConnectionService.js +69 -0
- package/dist/modules/tasks/TaskConnectionService.js.map +1 -0
- package/dist/modules/tasks/TaskImportService.d.ts +34 -0
- package/dist/modules/tasks/TaskImportService.d.ts.map +1 -0
- package/dist/modules/tasks/TaskImportService.js +96 -0
- package/dist/modules/tasks/TaskImportService.js.map +1 -0
- package/dist/modules/tasks/TaskLinkService.d.ts +30 -0
- package/dist/modules/tasks/TaskLinkService.d.ts.map +1 -0
- package/dist/modules/tasks/TaskLinkService.js +56 -0
- package/dist/modules/tasks/TaskLinkService.js.map +1 -0
- package/dist/modules/tasks/github-issues.logic.d.ts +35 -0
- package/dist/modules/tasks/github-issues.logic.d.ts.map +1 -0
- package/dist/modules/tasks/github-issues.logic.js +67 -0
- package/dist/modules/tasks/github-issues.logic.js.map +1 -0
- package/dist/modules/tasks/jira.logic.d.ts +28 -0
- package/dist/modules/tasks/jira.logic.d.ts.map +1 -0
- package/dist/modules/tasks/jira.logic.js +151 -0
- package/dist/modules/tasks/jira.logic.js.map +1 -0
- package/dist/modules/tasks/tasks.logic.d.ts +12 -0
- package/dist/modules/tasks/tasks.logic.d.ts.map +1 -0
- package/dist/modules/tasks/tasks.logic.js +17 -0
- package/dist/modules/tasks/tasks.logic.js.map +1 -0
- package/dist/modules/tracker/TicketTrackerService.d.ts +45 -0
- package/dist/modules/tracker/TicketTrackerService.d.ts.map +1 -0
- package/dist/modules/tracker/TicketTrackerService.js +52 -0
- package/dist/modules/tracker/TicketTrackerService.js.map +1 -0
- package/dist/modules/tracker/base64.d.ts +2 -0
- package/dist/modules/tracker/base64.d.ts.map +1 -0
- package/dist/modules/tracker/base64.js +18 -0
- package/dist/modules/tracker/base64.js.map +1 -0
- package/dist/modules/tracker/github.create.logic.d.ts +16 -0
- package/dist/modules/tracker/github.create.logic.d.ts.map +1 -0
- package/dist/modules/tracker/github.create.logic.js +25 -0
- package/dist/modules/tracker/github.create.logic.js.map +1 -0
- package/dist/modules/tracker/jira.create.logic.d.ts +31 -0
- package/dist/modules/tracker/jira.create.logic.d.ts.map +1 -0
- package/dist/modules/tracker/jira.create.logic.js +59 -0
- package/dist/modules/tracker/jira.create.logic.js.map +1 -0
- package/package.json +36 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { ConflictError } from '@cat-factory/kernel';
|
|
2
|
+
import { DEFAULT_USAGE_WINDOW_MS, chooseToken } from './providers.logic.js';
|
|
3
|
+
// ApiKeyService: owns the direct-provider API-key pool (OpenAI/Anthropic/Qwen/
|
|
4
|
+
// DeepSeek/Moonshot). Keys are onboarded via the UI and stored *encrypted* (a
|
|
5
|
+
// SecretCipher envelope — never plaintext), replacing the old deployment-env
|
|
6
|
+
// onboarding. A key lives at one of three SCOPES — account, workspace, or user.
|
|
7
|
+
//
|
|
8
|
+
// When a run in a workspace needs a provider key, the candidate pool is the UNION
|
|
9
|
+
// of the workspace's keys, its owning account's keys, and the run initiator's own
|
|
10
|
+
// user keys; leasing is usage-aware (least-loaded wins, see providers.logic).
|
|
11
|
+
// Mirrors ProviderSubscriptionService, with a scope dimension instead of a vendor.
|
|
12
|
+
// Upper bound on live keys per (scope, scopeId, provider). The rotation pool is
|
|
13
|
+
// meant to hold a handful of keys for quota headroom; a generous ceiling keeps the
|
|
14
|
+
// feature usable while bounding accidental/abusive unbounded growth.
|
|
15
|
+
const MAX_KEYS_PER_PROVIDER = 25;
|
|
16
|
+
export class ApiKeyService {
|
|
17
|
+
deps;
|
|
18
|
+
constructor(deps) {
|
|
19
|
+
this.deps = deps;
|
|
20
|
+
}
|
|
21
|
+
get windowMs() {
|
|
22
|
+
return this.deps.usageWindowMs ?? DEFAULT_USAGE_WINDOW_MS;
|
|
23
|
+
}
|
|
24
|
+
/** Add a key to a scope's pool. */
|
|
25
|
+
async addKey(scope, scopeId, input) {
|
|
26
|
+
const existing = await this.deps.providerApiKeyRepository.listByScope(scope, scopeId, input.provider);
|
|
27
|
+
if (existing.length >= MAX_KEYS_PER_PROVIDER) {
|
|
28
|
+
throw new ConflictError(`This ${scope} already has the maximum of ${MAX_KEYS_PER_PROVIDER} ` +
|
|
29
|
+
`${input.provider} API keys; remove one before adding another`);
|
|
30
|
+
}
|
|
31
|
+
const keyCipher = await this.deps.secretCipher.encrypt(input.key);
|
|
32
|
+
const now = this.deps.clock.now();
|
|
33
|
+
const record = {
|
|
34
|
+
id: this.deps.idGenerator.next('apikey'),
|
|
35
|
+
scope,
|
|
36
|
+
scopeId,
|
|
37
|
+
provider: input.provider,
|
|
38
|
+
label: input.label,
|
|
39
|
+
keyCipher,
|
|
40
|
+
createdAt: now,
|
|
41
|
+
lastUsedAt: null,
|
|
42
|
+
windowStartedAt: null,
|
|
43
|
+
inputTokens: 0,
|
|
44
|
+
outputTokens: 0,
|
|
45
|
+
requestCount: 0,
|
|
46
|
+
deletedAt: null,
|
|
47
|
+
};
|
|
48
|
+
await this.deps.providerApiKeyRepository.add(record);
|
|
49
|
+
return toSummary(record);
|
|
50
|
+
}
|
|
51
|
+
/** All live keys for a scope (optionally filtered by provider), metadata only. */
|
|
52
|
+
async listKeys(scope, scopeId, provider) {
|
|
53
|
+
const rows = await this.deps.providerApiKeyRepository.listByScope(scope, scopeId, provider);
|
|
54
|
+
return rows.map(toSummary);
|
|
55
|
+
}
|
|
56
|
+
/** Remove a key from its scope's pool. */
|
|
57
|
+
async removeKey(scope, scopeId, id) {
|
|
58
|
+
await this.deps.providerApiKeyRepository.softDelete(scope, scopeId, id, this.deps.clock.now());
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The merged candidate scope segments for a run: the workspace, its owning
|
|
62
|
+
* account (resolved when not supplied), and the initiator's user, in that order.
|
|
63
|
+
*/
|
|
64
|
+
async poolScopes(workspaceId, opts) {
|
|
65
|
+
const scopes = [{ scope: 'workspace', scopeId: workspaceId }];
|
|
66
|
+
const accountId = opts?.accountId === undefined
|
|
67
|
+
? await this.deps.workspaceRepository.accountOf(workspaceId)
|
|
68
|
+
: opts.accountId;
|
|
69
|
+
if (accountId)
|
|
70
|
+
scopes.push({ scope: 'account', scopeId: accountId });
|
|
71
|
+
if (opts?.userId)
|
|
72
|
+
scopes.push({ scope: 'user', scopeId: opts.userId });
|
|
73
|
+
return scopes;
|
|
74
|
+
}
|
|
75
|
+
/** Whether the merged pool has at least one live key for a provider. */
|
|
76
|
+
async hasKey(workspaceId, provider, opts) {
|
|
77
|
+
const scopes = await this.poolScopes(workspaceId, opts);
|
|
78
|
+
const rows = await this.deps.providerApiKeyRepository.listForPool(scopes, provider);
|
|
79
|
+
return rows.length > 0;
|
|
80
|
+
}
|
|
81
|
+
/** Distinct providers with at least one live key across the merged pool. */
|
|
82
|
+
async configuredProviders(workspaceId, opts) {
|
|
83
|
+
const scopes = await this.poolScopes(workspaceId, opts);
|
|
84
|
+
return this.deps.providerApiKeyRepository.listConfiguredProviders(scopes);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Lease the least-loaded live key for a provider across the merged pool and
|
|
88
|
+
* return its decrypted secret. Throws ConflictError when the pool is empty so the
|
|
89
|
+
* caller can surface a clear "add an API key" error rather than failing deep in
|
|
90
|
+
* the SDK. Rotation is best-effort, not transactional (see ProviderSubscriptionService).
|
|
91
|
+
*/
|
|
92
|
+
async lease(workspaceId, provider, opts) {
|
|
93
|
+
const scopes = await this.poolScopes(workspaceId, opts);
|
|
94
|
+
const rows = await this.deps.providerApiKeyRepository.listForPool(scopes, provider);
|
|
95
|
+
const chosen = chooseToken(rows, this.deps.clock.now(), this.windowMs);
|
|
96
|
+
if (!chosen) {
|
|
97
|
+
throw new ConflictError(`No ${provider} API key is configured for this workspace, its account, or your user`);
|
|
98
|
+
}
|
|
99
|
+
await this.deps.providerApiKeyRepository.markLeased(chosen.id, this.deps.clock.now());
|
|
100
|
+
const secret = await this.deps.secretCipher.decrypt(chosen.keyCipher);
|
|
101
|
+
return { keyId: chosen.id, provider, secret };
|
|
102
|
+
}
|
|
103
|
+
/** Fold a completed call's usage into the leased key's rolling-window counters. */
|
|
104
|
+
async recordUsage(keyId, usage) {
|
|
105
|
+
await this.deps.providerApiKeyRepository.recordUsage(keyId, usage, this.deps.clock.now(), this.windowMs);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function toSummary(record) {
|
|
109
|
+
return {
|
|
110
|
+
id: record.id,
|
|
111
|
+
scope: record.scope,
|
|
112
|
+
scopeId: record.scopeId,
|
|
113
|
+
provider: record.provider,
|
|
114
|
+
label: record.label,
|
|
115
|
+
createdAt: record.createdAt,
|
|
116
|
+
lastUsedAt: record.lastUsedAt,
|
|
117
|
+
inputTokens: record.inputTokens,
|
|
118
|
+
outputTokens: record.outputTokens,
|
|
119
|
+
requestCount: record.requestCount,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=ApiKeyService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ApiKeyService.js","sourceRoot":"","sources":["../../../src/modules/providers/ApiKeyService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Clock, LocalModelEndpointRepository, SecretCipher } from '@cat-factory/kernel';
|
|
2
|
+
import type { LocalModelEndpoint, LocalModelEndpointTestResult, LocalRunner, TestLocalModelEndpointInput, UpsertLocalModelEndpointInput } from '@cat-factory/contracts';
|
|
3
|
+
export interface LocalModelEndpointServiceDependencies {
|
|
4
|
+
localModelEndpointRepository: LocalModelEndpointRepository;
|
|
5
|
+
/** System encryption layer (master key) for the optional bearer key at rest. */
|
|
6
|
+
secretCipher: SecretCipher;
|
|
7
|
+
clock: Clock;
|
|
8
|
+
/** Injected for tests; defaults to the global fetch. */
|
|
9
|
+
fetch?: typeof fetch;
|
|
10
|
+
}
|
|
11
|
+
/** A resolved endpoint for the run-time path: base URL + the decrypted optional key. */
|
|
12
|
+
export interface ResolvedLocalEndpoint {
|
|
13
|
+
provider: LocalRunner;
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
apiKey: string | null;
|
|
16
|
+
}
|
|
17
|
+
export declare class LocalModelEndpointService {
|
|
18
|
+
private readonly deps;
|
|
19
|
+
constructor(deps: LocalModelEndpointServiceDependencies);
|
|
20
|
+
/** Every endpoint the user has configured (key-free wire shape). */
|
|
21
|
+
list(userId: string): Promise<LocalModelEndpoint[]>;
|
|
22
|
+
/** Create or replace the user's endpoint for a runner. */
|
|
23
|
+
upsert(userId: string, input: UpsertLocalModelEndpointInput): Promise<LocalModelEndpoint>;
|
|
24
|
+
/** Remove the user's endpoint for a runner. */
|
|
25
|
+
remove(userId: string, provider: LocalRunner): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* The set of local-runner providers the user has configured with ≥1 enabled model,
|
|
28
|
+
* plus the enabled models per provider — the input to the per-user model catalog.
|
|
29
|
+
*/
|
|
30
|
+
capabilitiesFor(userId: string): Promise<{
|
|
31
|
+
provider: LocalRunner;
|
|
32
|
+
label: string;
|
|
33
|
+
models: string[];
|
|
34
|
+
}[]>;
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a user's endpoint for run-time forwarding: base URL + decrypted optional key.
|
|
37
|
+
* Used by the LLM proxy, keyed by the run initiator + the locked provider.
|
|
38
|
+
*/
|
|
39
|
+
resolve(userId: string, provider: string): Promise<ResolvedLocalEndpoint | null>;
|
|
40
|
+
/**
|
|
41
|
+
* All of a user's endpoints resolved for run-time forwarding (base URL + decrypted
|
|
42
|
+
* optional key). Used by the inline model provider to register the user's runners.
|
|
43
|
+
*/
|
|
44
|
+
listResolved(userId: string): Promise<ResolvedLocalEndpoint[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Probe a runner's OpenAI-compatible `/models` endpoint server-side, returning
|
|
47
|
+
* reachability + the model ids it serves. Never throws — failures are reported as
|
|
48
|
+
* `{ reachable: false, error }` so the UI can surface them.
|
|
49
|
+
*/
|
|
50
|
+
testConnection(input: TestLocalModelEndpointInput): Promise<LocalModelEndpointTestResult>;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=LocalModelEndpointService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalModelEndpointService.d.ts","sourceRoot":"","sources":["../../../src/modules/providers/LocalModelEndpointService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,KAAK,EAEL,4BAA4B,EAC5B,YAAY,EACb,MAAM,qBAAqB,CAAA;AAE5B,OAAO,KAAK,EACV,kBAAkB,EAClB,4BAA4B,EAC5B,WAAW,EACX,2BAA2B,EAC3B,6BAA6B,EAC9B,MAAM,wBAAwB,CAAA;AAe/B,MAAM,WAAW,qCAAqC;IACpD,4BAA4B,EAAE,4BAA4B,CAAA;IAC1D,gFAAgF;IAChF,YAAY,EAAE,YAAY,CAAA;IAC1B,KAAK,EAAE,KAAK,CAAA;IACZ,wDAAwD;IACxD,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;CACrB;AAED,wFAAwF;AACxF,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,WAAW,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAED,qBAAa,yBAAyB;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAjC,YAA6B,IAAI,EAAE,qCAAqC,EAAI;IAE5E,oEAAoE;IAC9D,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAGxD;IAED,0DAA0D;IACpD,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,6BAA6B,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA8B9F;IAED,+CAA+C;IACzC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAEjE;IAED;;;OAGG;IACG,eAAe,CACnB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;QAAE,QAAQ,EAAE,WAAW,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC,CAKvE;IAED;;;OAGG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAUrF;IAED;;;OAGG;IACG,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAUnE;IAED;;;;OAIG;IACG,cAAc,CAAC,KAAK,EAAE,2BAA2B,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAsB9F;CACF"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { getErrorMessage, ValidationError } from '@cat-factory/kernel';
|
|
2
|
+
import { LOCAL_RUNNER_LABELS } from '@cat-factory/contracts';
|
|
3
|
+
import { localRunnerUrlError } from './localModelUrl.js';
|
|
4
|
+
export class LocalModelEndpointService {
|
|
5
|
+
deps;
|
|
6
|
+
constructor(deps) {
|
|
7
|
+
this.deps = deps;
|
|
8
|
+
}
|
|
9
|
+
/** Every endpoint the user has configured (key-free wire shape). */
|
|
10
|
+
async list(userId) {
|
|
11
|
+
const rows = await this.deps.localModelEndpointRepository.listByUser(userId);
|
|
12
|
+
return rows.map((r) => toWire(r));
|
|
13
|
+
}
|
|
14
|
+
/** Create or replace the user's endpoint for a runner. */
|
|
15
|
+
async upsert(userId, input) {
|
|
16
|
+
// SSRF guard: the stored base URL is later forwarded to server-side (the LLM proxy +
|
|
17
|
+
// inline provider resolve it by the run initiator), so reject a non-local host here at
|
|
18
|
+
// the write boundary — the run-time paths then trust the persisted URL.
|
|
19
|
+
const urlError = localRunnerUrlError(input.baseUrl);
|
|
20
|
+
if (urlError)
|
|
21
|
+
throw new ValidationError(urlError);
|
|
22
|
+
const now = this.deps.clock.now();
|
|
23
|
+
const existing = await this.deps.localModelEndpointRepository.getByUserProvider(userId, input.provider);
|
|
24
|
+
// An omitted apiKey keeps the stored one; an explicit empty string clears it.
|
|
25
|
+
const apiKeyCipher = input.apiKey === undefined
|
|
26
|
+
? (existing?.apiKeyCipher ?? null)
|
|
27
|
+
: input.apiKey.length > 0
|
|
28
|
+
? await this.deps.secretCipher.encrypt(input.apiKey)
|
|
29
|
+
: null;
|
|
30
|
+
const record = {
|
|
31
|
+
userId,
|
|
32
|
+
provider: input.provider,
|
|
33
|
+
label: input.label?.trim() || LOCAL_RUNNER_LABELS[input.provider],
|
|
34
|
+
baseUrl: input.baseUrl,
|
|
35
|
+
apiKeyCipher,
|
|
36
|
+
models: dedupe(input.models),
|
|
37
|
+
createdAt: existing?.createdAt ?? now,
|
|
38
|
+
updatedAt: now,
|
|
39
|
+
};
|
|
40
|
+
await this.deps.localModelEndpointRepository.upsert(record);
|
|
41
|
+
return toWire(record);
|
|
42
|
+
}
|
|
43
|
+
/** Remove the user's endpoint for a runner. */
|
|
44
|
+
async remove(userId, provider) {
|
|
45
|
+
await this.deps.localModelEndpointRepository.remove(userId, provider);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The set of local-runner providers the user has configured with ≥1 enabled model,
|
|
49
|
+
* plus the enabled models per provider — the input to the per-user model catalog.
|
|
50
|
+
*/
|
|
51
|
+
async capabilitiesFor(userId) {
|
|
52
|
+
const rows = await this.deps.localModelEndpointRepository.listByUser(userId);
|
|
53
|
+
return rows
|
|
54
|
+
.filter((r) => r.models.length > 0)
|
|
55
|
+
.map((r) => ({ provider: r.provider, label: r.label, models: r.models }));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolve a user's endpoint for run-time forwarding: base URL + decrypted optional key.
|
|
59
|
+
* Used by the LLM proxy, keyed by the run initiator + the locked provider.
|
|
60
|
+
*/
|
|
61
|
+
async resolve(userId, provider) {
|
|
62
|
+
const record = await this.deps.localModelEndpointRepository.getByUserProvider(userId, provider);
|
|
63
|
+
if (!record)
|
|
64
|
+
return null;
|
|
65
|
+
const apiKey = record.apiKeyCipher
|
|
66
|
+
? await this.deps.secretCipher.decrypt(record.apiKeyCipher)
|
|
67
|
+
: null;
|
|
68
|
+
return { provider: record.provider, baseUrl: record.baseUrl, apiKey };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* All of a user's endpoints resolved for run-time forwarding (base URL + decrypted
|
|
72
|
+
* optional key). Used by the inline model provider to register the user's runners.
|
|
73
|
+
*/
|
|
74
|
+
async listResolved(userId) {
|
|
75
|
+
const rows = await this.deps.localModelEndpointRepository.listByUser(userId);
|
|
76
|
+
const out = [];
|
|
77
|
+
for (const record of rows) {
|
|
78
|
+
const apiKey = record.apiKeyCipher
|
|
79
|
+
? await this.deps.secretCipher.decrypt(record.apiKeyCipher)
|
|
80
|
+
: null;
|
|
81
|
+
out.push({ provider: record.provider, baseUrl: record.baseUrl, apiKey });
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Probe a runner's OpenAI-compatible `/models` endpoint server-side, returning
|
|
87
|
+
* reachability + the model ids it serves. Never throws — failures are reported as
|
|
88
|
+
* `{ reachable: false, error }` so the UI can surface them.
|
|
89
|
+
*/
|
|
90
|
+
async testConnection(input) {
|
|
91
|
+
// SSRF guard: this probe forwards to a user-supplied URL server-side, so refuse a
|
|
92
|
+
// non-local host before issuing the fetch (same allow-list as `upsert`).
|
|
93
|
+
const urlError = localRunnerUrlError(input.baseUrl);
|
|
94
|
+
if (urlError)
|
|
95
|
+
return { reachable: false, models: [], error: urlError };
|
|
96
|
+
const doFetch = this.deps.fetch ?? fetch;
|
|
97
|
+
const url = `${input.baseUrl.replace(/\/+$/, '')}/models`;
|
|
98
|
+
try {
|
|
99
|
+
const headers = {};
|
|
100
|
+
if (input.apiKey)
|
|
101
|
+
headers.authorization = `Bearer ${input.apiKey}`;
|
|
102
|
+
const res = await doFetch(url, { headers, signal: AbortSignal.timeout(8000) });
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
return { reachable: false, models: [], error: `Runner returned HTTP ${res.status}` };
|
|
105
|
+
}
|
|
106
|
+
const body = (await res.json());
|
|
107
|
+
const models = Array.isArray(body.data)
|
|
108
|
+
? body.data.map((m) => String(m?.id ?? '')).filter(Boolean)
|
|
109
|
+
: [];
|
|
110
|
+
return { reachable: true, models };
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
return { reachable: false, models: [], error: getErrorMessage(err) };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function toWire(record) {
|
|
118
|
+
return {
|
|
119
|
+
provider: record.provider,
|
|
120
|
+
label: record.label,
|
|
121
|
+
baseUrl: record.baseUrl,
|
|
122
|
+
hasApiKey: record.apiKeyCipher !== null,
|
|
123
|
+
models: record.models,
|
|
124
|
+
createdAt: record.createdAt,
|
|
125
|
+
updatedAt: record.updatedAt,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function dedupe(models) {
|
|
129
|
+
return [...new Set(models.map((m) => m.trim()).filter(Boolean))];
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=LocalModelEndpointService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalModelEndpointService.js","sourceRoot":"","sources":["../../../src/modules/providers/LocalModelEndpointService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Clock, IdGenerator, PersonalSecretCipher, PersonalSubscriptionRecord, PersonalSubscriptionRepository, SecretCipher, SubscriptionActivationRepository, SubscriptionVendor } from '@cat-factory/kernel';
|
|
2
|
+
import type { PersonalSubscriptionStatus, StorePersonalSubscriptionInput } from '@cat-factory/contracts';
|
|
3
|
+
/**
|
|
4
|
+
* Default per-run activation lifetime (~12h). This bounds the window in which the
|
|
5
|
+
* raw token is recoverable with the SYSTEM key alone (the activation has no password
|
|
6
|
+
* layer), so it is kept deliberately short. It does NOT need to cover a long run: a
|
|
7
|
+
* healthy run deletes its activation the moment it finishes, and any run a user keeps
|
|
8
|
+
* tending transparently RE-MINTS the activation on each interaction (resolve/approve/
|
|
9
|
+
* retry) from the password cached client-side — so the user is only re-prompted once
|
|
10
|
+
* that cache lapses, never because the activation TTL did. 12h is simply long enough to
|
|
11
|
+
* cover a fully-autonomous run (no human touch-points to re-mint at) while keeping the
|
|
12
|
+
* stuck/abandoned-run exposure window an order of magnitude tighter than a week.
|
|
13
|
+
*/
|
|
14
|
+
export declare const DEFAULT_ACTIVATION_TTL_MS: number;
|
|
15
|
+
/** Surface "renew your subscription" once it expires within this horizon (~7 days). */
|
|
16
|
+
export declare const DEFAULT_RENEW_WARNING_MS: number;
|
|
17
|
+
export interface PersonalSubscriptionServiceDependencies {
|
|
18
|
+
personalSubscriptionRepository: PersonalSubscriptionRepository;
|
|
19
|
+
subscriptionActivationRepository: SubscriptionActivationRepository;
|
|
20
|
+
/** System encryption layer (master key); applied OUTSIDE the password layer. */
|
|
21
|
+
secretCipher: SecretCipher;
|
|
22
|
+
/** Password-derived encryption layer; the password is never stored. */
|
|
23
|
+
personalCipher: PersonalSecretCipher;
|
|
24
|
+
idGenerator: IdGenerator;
|
|
25
|
+
clock: Clock;
|
|
26
|
+
activationTtlMs?: number;
|
|
27
|
+
renewWarningMs?: number;
|
|
28
|
+
}
|
|
29
|
+
/** A leased personal credential for a run step — the decrypted raw token. */
|
|
30
|
+
export interface LeasedPersonalToken {
|
|
31
|
+
vendor: SubscriptionVendor;
|
|
32
|
+
secret: string;
|
|
33
|
+
}
|
|
34
|
+
export declare class PersonalSubscriptionService {
|
|
35
|
+
private readonly deps;
|
|
36
|
+
constructor(deps: PersonalSubscriptionServiceDependencies);
|
|
37
|
+
private get activationTtlMs();
|
|
38
|
+
private get renewWarningMs();
|
|
39
|
+
private assertIndividual;
|
|
40
|
+
/** Store (or replace) the user's personal credential for an individual-usage vendor. */
|
|
41
|
+
store(userId: string, input: StorePersonalSubscriptionInput): Promise<PersonalSubscriptionStatus>;
|
|
42
|
+
/**
|
|
43
|
+
* Enforce ONE personal password across all of a user's individual-usage subscriptions.
|
|
44
|
+
* The run gate unlocks every vendor a single run touches with the SAME password (and the
|
|
45
|
+
* client caches just one), so a second credential sealed under a different password would
|
|
46
|
+
* be silently un-unlockable in any run that uses both. Rather than let that latent
|
|
47
|
+
* dead-end ship, we verify the new password decrypts an existing (non-expired) credential
|
|
48
|
+
* and reject up-front otherwise. No-op for the user's first credential. The check unlocks
|
|
49
|
+
* an arbitrary existing credential purely to compare the password — nothing is persisted.
|
|
50
|
+
*/
|
|
51
|
+
private assertSamePasswordAsOthers;
|
|
52
|
+
/** Every personal subscription the user has, metadata only (never the secret). */
|
|
53
|
+
list(userId: string): Promise<PersonalSubscriptionStatus[]>;
|
|
54
|
+
/** Whether the user has a live personal credential for the vendor. */
|
|
55
|
+
has(userId: string, vendor: SubscriptionVendor): Promise<boolean>;
|
|
56
|
+
/** Remove the user's personal credential for a vendor. */
|
|
57
|
+
remove(userId: string, vendor: SubscriptionVendor): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Decrypt the user's credential with their password, returning the raw token.
|
|
60
|
+
* Throws a {@link CredentialRequiredError} when there's no credential, the
|
|
61
|
+
* subscription has lapsed, or the password is wrong. Does NOT persist anything.
|
|
62
|
+
*/
|
|
63
|
+
unlock(userId: string, vendor: SubscriptionVendor, password: string): Promise<string>;
|
|
64
|
+
/**
|
|
65
|
+
* Mint a per-run activation: unlock the credential with the password, re-encrypt the
|
|
66
|
+
* raw token with the system key only, and store it scoped to the run with a TTL so
|
|
67
|
+
* every (async) step of that run can use it without the password. Idempotent —
|
|
68
|
+
* replaces any prior activation for the run+user+vendor.
|
|
69
|
+
*/
|
|
70
|
+
activateForRun(executionId: string, userId: string, vendor: SubscriptionVendor, password: string): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Lease the run's activated token for a step. Throws a {@link CredentialRequiredError}
|
|
73
|
+
* (`password_required`) when the run has no live activation — the dispatch path turns
|
|
74
|
+
* that into a clear, retriable failure (the user re-enters their password on retry).
|
|
75
|
+
*/
|
|
76
|
+
leaseForRun(executionId: string, userId: string, vendor: SubscriptionVendor): Promise<LeasedPersonalToken>;
|
|
77
|
+
/** Whether the run currently has a live activation for the user+vendor. */
|
|
78
|
+
hasActivation(executionId: string, userId: string, vendor: SubscriptionVendor): Promise<boolean>;
|
|
79
|
+
/**
|
|
80
|
+
* On user interaction with a run (approve/retry/etc.), extend its activation TTL when
|
|
81
|
+
* it is at least half spent, so an actively-tended long run doesn't lapse. No-op when
|
|
82
|
+
* the run has no activation.
|
|
83
|
+
*/
|
|
84
|
+
refreshActivations(executionId: string, userId: string): Promise<void>;
|
|
85
|
+
private activatedVendors;
|
|
86
|
+
/** Delete every activation for a finished run (called when a run terminates). */
|
|
87
|
+
clearRun(executionId: string): Promise<void>;
|
|
88
|
+
/** Delete activations whose TTL has passed. Returns the count (for the sweep log). */
|
|
89
|
+
sweepExpiredActivations(): Promise<number>;
|
|
90
|
+
/** Live subscriptions expiring within the renewal-warning horizon (for the nudge sweep). */
|
|
91
|
+
expiringSubscriptions(): Promise<PersonalSubscriptionRecord[]>;
|
|
92
|
+
private toStatus;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=PersonalSubscriptionService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PersonalSubscriptionService.d.ts","sourceRoot":"","sources":["../../../src/modules/providers/PersonalSubscriptionService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { CredentialRequiredError, getErrorMessage, INDIVIDUAL_VENDORS, isIndividualVendor, ValidationError, } from '@cat-factory/kernel';
|
|
2
|
+
// PersonalSubscriptionService: owns each USER's individual-usage subscription
|
|
3
|
+
// credentials (Claude / GLM / Codex) — the per-user analogue of the per-workspace
|
|
4
|
+
// ProviderSubscriptionService pool. The credential is stored DOUBLE-encrypted
|
|
5
|
+
// (`secretCipher.encrypt(personalCipher.seal(token, password))`), so it cannot be
|
|
6
|
+
// recovered without BOTH the system key AND the user's personal password. To let
|
|
7
|
+
// asynchronous container steps use it without the user present, the user supplies
|
|
8
|
+
// their password at task start/retry to mint a short-lived, per-run ACTIVATION
|
|
9
|
+
// (the token re-encrypted with the system key only), which the run's steps lease.
|
|
10
|
+
//
|
|
11
|
+
// See docs/individual-subscription-usage.md for the full model + safeguards.
|
|
12
|
+
/**
|
|
13
|
+
* Default per-run activation lifetime (~12h). This bounds the window in which the
|
|
14
|
+
* raw token is recoverable with the SYSTEM key alone (the activation has no password
|
|
15
|
+
* layer), so it is kept deliberately short. It does NOT need to cover a long run: a
|
|
16
|
+
* healthy run deletes its activation the moment it finishes, and any run a user keeps
|
|
17
|
+
* tending transparently RE-MINTS the activation on each interaction (resolve/approve/
|
|
18
|
+
* retry) from the password cached client-side — so the user is only re-prompted once
|
|
19
|
+
* that cache lapses, never because the activation TTL did. 12h is simply long enough to
|
|
20
|
+
* cover a fully-autonomous run (no human touch-points to re-mint at) while keeping the
|
|
21
|
+
* stuck/abandoned-run exposure window an order of magnitude tighter than a week.
|
|
22
|
+
*/
|
|
23
|
+
export const DEFAULT_ACTIVATION_TTL_MS = 12 * 60 * 60 * 1000;
|
|
24
|
+
/** Surface "renew your subscription" once it expires within this horizon (~7 days). */
|
|
25
|
+
export const DEFAULT_RENEW_WARNING_MS = 7 * 24 * 60 * 60 * 1000;
|
|
26
|
+
export class PersonalSubscriptionService {
|
|
27
|
+
deps;
|
|
28
|
+
constructor(deps) {
|
|
29
|
+
this.deps = deps;
|
|
30
|
+
}
|
|
31
|
+
get activationTtlMs() {
|
|
32
|
+
return this.deps.activationTtlMs ?? DEFAULT_ACTIVATION_TTL_MS;
|
|
33
|
+
}
|
|
34
|
+
get renewWarningMs() {
|
|
35
|
+
return this.deps.renewWarningMs ?? DEFAULT_RENEW_WARNING_MS;
|
|
36
|
+
}
|
|
37
|
+
assertIndividual(vendor) {
|
|
38
|
+
if (!isIndividualVendor(vendor)) {
|
|
39
|
+
throw new CredentialRequiredError(`Vendor '${vendor}' is not an individual-usage subscription; use the workspace credential pool instead.`, { vendor, reason: 'no_subscription' });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Store (or replace) the user's personal credential for an individual-usage vendor. */
|
|
43
|
+
async store(userId, input) {
|
|
44
|
+
this.assertIndividual(input.vendor);
|
|
45
|
+
await this.assertSamePasswordAsOthers(userId, input.vendor, input.password);
|
|
46
|
+
const sealed = await this.deps.personalCipher.seal(input.token, input.password);
|
|
47
|
+
const tokenCipher = await this.deps.secretCipher.encrypt(sealed);
|
|
48
|
+
const now = this.deps.clock.now();
|
|
49
|
+
const existing = await this.deps.personalSubscriptionRepository.getByUserVendor(userId, input.vendor);
|
|
50
|
+
const record = {
|
|
51
|
+
id: existing?.id ?? this.deps.idGenerator.next('psub'),
|
|
52
|
+
userId,
|
|
53
|
+
vendor: input.vendor,
|
|
54
|
+
label: input.label,
|
|
55
|
+
tokenCipher,
|
|
56
|
+
expiresAt: input.expiresAt ?? null,
|
|
57
|
+
createdAt: existing?.createdAt ?? now,
|
|
58
|
+
updatedAt: now,
|
|
59
|
+
lastUsedAt: existing?.lastUsedAt ?? null,
|
|
60
|
+
deletedAt: null,
|
|
61
|
+
};
|
|
62
|
+
await this.deps.personalSubscriptionRepository.upsert(record);
|
|
63
|
+
return this.toStatus(record, now);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Enforce ONE personal password across all of a user's individual-usage subscriptions.
|
|
67
|
+
* The run gate unlocks every vendor a single run touches with the SAME password (and the
|
|
68
|
+
* client caches just one), so a second credential sealed under a different password would
|
|
69
|
+
* be silently un-unlockable in any run that uses both. Rather than let that latent
|
|
70
|
+
* dead-end ship, we verify the new password decrypts an existing (non-expired) credential
|
|
71
|
+
* and reject up-front otherwise. No-op for the user's first credential. The check unlocks
|
|
72
|
+
* an arbitrary existing credential purely to compare the password — nothing is persisted.
|
|
73
|
+
*/
|
|
74
|
+
async assertSamePasswordAsOthers(userId, vendor, password) {
|
|
75
|
+
const now = this.deps.clock.now();
|
|
76
|
+
const other = (await this.deps.personalSubscriptionRepository.listByUser(userId)).find((r) => r.vendor !== vendor && (r.expiresAt === null || r.expiresAt > now));
|
|
77
|
+
if (!other)
|
|
78
|
+
return;
|
|
79
|
+
const sealed = await this.deps.secretCipher.decrypt(other.tokenCipher);
|
|
80
|
+
try {
|
|
81
|
+
await this.deps.personalCipher.open(sealed, password);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new ValidationError(`This personal password doesn't match your other connected subscription(s). Use the ` +
|
|
85
|
+
`same personal password for all of them — one run unlocks every individual-usage ` +
|
|
86
|
+
`vendor it touches with a single password — or remove the others first.`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Every personal subscription the user has, metadata only (never the secret). */
|
|
90
|
+
async list(userId) {
|
|
91
|
+
const now = this.deps.clock.now();
|
|
92
|
+
const rows = await this.deps.personalSubscriptionRepository.listByUser(userId);
|
|
93
|
+
return rows.map((r) => this.toStatus(r, now));
|
|
94
|
+
}
|
|
95
|
+
/** Whether the user has a live personal credential for the vendor. */
|
|
96
|
+
async has(userId, vendor) {
|
|
97
|
+
return (await this.deps.personalSubscriptionRepository.getByUserVendor(userId, vendor)) !== null;
|
|
98
|
+
}
|
|
99
|
+
/** Remove the user's personal credential for a vendor. */
|
|
100
|
+
async remove(userId, vendor) {
|
|
101
|
+
await this.deps.personalSubscriptionRepository.softDelete(userId, vendor, this.deps.clock.now());
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Decrypt the user's credential with their password, returning the raw token.
|
|
105
|
+
* Throws a {@link CredentialRequiredError} when there's no credential, the
|
|
106
|
+
* subscription has lapsed, or the password is wrong. Does NOT persist anything.
|
|
107
|
+
*/
|
|
108
|
+
async unlock(userId, vendor, password) {
|
|
109
|
+
const record = await this.deps.personalSubscriptionRepository.getByUserVendor(userId, vendor);
|
|
110
|
+
if (!record) {
|
|
111
|
+
throw new CredentialRequiredError(`No personal ${vendor} subscription is connected for this user.`, { vendor, reason: 'no_subscription' });
|
|
112
|
+
}
|
|
113
|
+
const now = this.deps.clock.now();
|
|
114
|
+
if (record.expiresAt !== null && record.expiresAt <= now) {
|
|
115
|
+
throw new CredentialRequiredError(`Your ${vendor} subscription expired; renew it before starting runs that use it.`, { vendor, reason: 'subscription_expired' });
|
|
116
|
+
}
|
|
117
|
+
const sealed = await this.deps.secretCipher.decrypt(record.tokenCipher);
|
|
118
|
+
try {
|
|
119
|
+
return await this.deps.personalCipher.open(sealed, password);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
throw new CredentialRequiredError(`Incorrect personal password for your ${vendor} subscription (${getErrorMessage(error)}).`, { vendor, reason: 'wrong_password' });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Mint a per-run activation: unlock the credential with the password, re-encrypt the
|
|
127
|
+
* raw token with the system key only, and store it scoped to the run with a TTL so
|
|
128
|
+
* every (async) step of that run can use it without the password. Idempotent —
|
|
129
|
+
* replaces any prior activation for the run+user+vendor.
|
|
130
|
+
*/
|
|
131
|
+
async activateForRun(executionId, userId, vendor, password) {
|
|
132
|
+
const token = await this.unlock(userId, vendor, password);
|
|
133
|
+
const now = this.deps.clock.now();
|
|
134
|
+
const tokenCipher = await this.deps.secretCipher.encrypt(token);
|
|
135
|
+
const record = {
|
|
136
|
+
id: this.deps.idGenerator.next('act'),
|
|
137
|
+
executionId,
|
|
138
|
+
userId,
|
|
139
|
+
vendor,
|
|
140
|
+
tokenCipher,
|
|
141
|
+
createdAt: now,
|
|
142
|
+
expiresAt: now + this.activationTtlMs,
|
|
143
|
+
};
|
|
144
|
+
await this.deps.subscriptionActivationRepository.upsert(record);
|
|
145
|
+
await this.deps.personalSubscriptionRepository.markUsed(userId, vendor, now);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Lease the run's activated token for a step. Throws a {@link CredentialRequiredError}
|
|
149
|
+
* (`password_required`) when the run has no live activation — the dispatch path turns
|
|
150
|
+
* that into a clear, retriable failure (the user re-enters their password on retry).
|
|
151
|
+
*/
|
|
152
|
+
async leaseForRun(executionId, userId, vendor) {
|
|
153
|
+
const now = this.deps.clock.now();
|
|
154
|
+
const activation = await this.deps.subscriptionActivationRepository.get(executionId, userId, vendor, now);
|
|
155
|
+
if (!activation) {
|
|
156
|
+
throw new CredentialRequiredError(`This run has no active ${vendor} credential; re-enter your personal password to continue.`, { vendor, reason: 'password_required' });
|
|
157
|
+
}
|
|
158
|
+
const secret = await this.deps.secretCipher.decrypt(activation.tokenCipher);
|
|
159
|
+
return { vendor, secret };
|
|
160
|
+
}
|
|
161
|
+
/** Whether the run currently has a live activation for the user+vendor. */
|
|
162
|
+
async hasActivation(executionId, userId, vendor) {
|
|
163
|
+
return ((await this.deps.subscriptionActivationRepository.get(executionId, userId, vendor, this.deps.clock.now())) !== null);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* On user interaction with a run (approve/retry/etc.), extend its activation TTL when
|
|
167
|
+
* it is at least half spent, so an actively-tended long run doesn't lapse. No-op when
|
|
168
|
+
* the run has no activation.
|
|
169
|
+
*/
|
|
170
|
+
async refreshActivations(executionId, userId) {
|
|
171
|
+
const now = this.deps.clock.now();
|
|
172
|
+
for (const vendor of await this.activatedVendors(executionId, userId, now)) {
|
|
173
|
+
await this.deps.subscriptionActivationRepository.refresh(executionId, userId, vendor, now + this.activationTtlMs);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async activatedVendors(executionId, userId, now) {
|
|
177
|
+
const out = [];
|
|
178
|
+
// Only individual-usage vendors are ever activated; refresh those that are present
|
|
179
|
+
// and at least half through their TTL. Driven off INDIVIDUAL_VENDORS (the kernel's
|
|
180
|
+
// single source of truth) so a newly individual-only vendor is covered automatically.
|
|
181
|
+
for (const vendor of INDIVIDUAL_VENDORS) {
|
|
182
|
+
const a = await this.deps.subscriptionActivationRepository.get(executionId, userId, vendor, now);
|
|
183
|
+
if (a && a.expiresAt - now <= this.activationTtlMs / 2)
|
|
184
|
+
out.push(vendor);
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
/** Delete every activation for a finished run (called when a run terminates). */
|
|
189
|
+
async clearRun(executionId) {
|
|
190
|
+
await this.deps.subscriptionActivationRepository.deleteByExecution(executionId);
|
|
191
|
+
}
|
|
192
|
+
/** Delete activations whose TTL has passed. Returns the count (for the sweep log). */
|
|
193
|
+
async sweepExpiredActivations() {
|
|
194
|
+
return this.deps.subscriptionActivationRepository.deleteExpired(this.deps.clock.now());
|
|
195
|
+
}
|
|
196
|
+
/** Live subscriptions expiring within the renewal-warning horizon (for the nudge sweep). */
|
|
197
|
+
async expiringSubscriptions() {
|
|
198
|
+
const now = this.deps.clock.now();
|
|
199
|
+
return this.deps.personalSubscriptionRepository.listExpiring(now, now + this.renewWarningMs);
|
|
200
|
+
}
|
|
201
|
+
toStatus(record, now) {
|
|
202
|
+
const expiresInDays = record.expiresAt === null
|
|
203
|
+
? null
|
|
204
|
+
: Math.floor((record.expiresAt - now) / (24 * 60 * 60 * 1000));
|
|
205
|
+
return {
|
|
206
|
+
vendor: record.vendor,
|
|
207
|
+
label: record.label,
|
|
208
|
+
createdAt: record.createdAt,
|
|
209
|
+
updatedAt: record.updatedAt,
|
|
210
|
+
lastUsedAt: record.lastUsedAt,
|
|
211
|
+
expiresAt: record.expiresAt,
|
|
212
|
+
expiresInDays,
|
|
213
|
+
expired: record.expiresAt !== null && record.expiresAt <= now,
|
|
214
|
+
renewSoon: record.expiresAt !== null && record.expiresAt <= now + this.renewWarningMs,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=PersonalSubscriptionService.js.map
|