@taico/taico 0.6.0 → 0.8.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/README.md +1 -1
- package/dist/ai-providers/ai-model-selections.service.js +24 -2
- package/dist/ai-providers/ai-model-selections.service.js.map +1 -1
- package/dist/ai-providers/ai-provider-authorization-attempt.entity.d.ts +17 -0
- package/dist/ai-providers/ai-provider-authorization-attempt.entity.js +90 -0
- package/dist/ai-providers/ai-provider-authorization-attempt.entity.js.map +1 -0
- package/dist/ai-providers/ai-provider-connection.entity.d.ts +24 -0
- package/dist/ai-providers/ai-provider-connection.entity.js +131 -0
- package/dist/ai-providers/ai-provider-connection.entity.js.map +1 -0
- package/dist/ai-providers/ai-provider-connections.controller.d.ts +15 -0
- package/dist/ai-providers/ai-provider-connections.controller.js +167 -0
- package/dist/ai-providers/ai-provider-connections.controller.js.map +1 -0
- package/dist/ai-providers/ai-provider-models.service.d.ts +5 -2
- package/dist/ai-providers/ai-provider-models.service.js +44 -5
- package/dist/ai-providers/ai-provider-models.service.js.map +1 -1
- package/dist/ai-providers/ai-provider-models.service.spec.d.ts +1 -0
- package/dist/ai-providers/ai-provider-models.service.spec.js +110 -0
- package/dist/ai-providers/ai-provider-models.service.spec.js.map +1 -0
- package/dist/ai-providers/ai-provider.entity.d.ts +1 -0
- package/dist/ai-providers/ai-provider.entity.js +5 -0
- package/dist/ai-providers/ai-provider.entity.js.map +1 -1
- package/dist/ai-providers/ai-providers.controller.d.ts +1 -1
- package/dist/ai-providers/ai-providers.controller.js +4 -3
- package/dist/ai-providers/ai-providers.controller.js.map +1 -1
- package/dist/ai-providers/ai-providers.module.js +30 -4
- package/dist/ai-providers/ai-providers.module.js.map +1 -1
- package/dist/ai-providers/ai-providers.scopes.d.ts +4 -0
- package/dist/ai-providers/ai-providers.scopes.js +4 -0
- package/dist/ai-providers/ai-providers.scopes.js.map +1 -1
- package/dist/ai-providers/ai-providers.service.d.ts +3 -1
- package/dist/ai-providers/ai-providers.service.js +23 -3
- package/dist/ai-providers/ai-providers.service.js.map +1 -1
- package/dist/ai-providers/chatgpt-subscription/chatgpt-connections.service.d.ts +50 -0
- package/dist/ai-providers/chatgpt-subscription/chatgpt-connections.service.js +607 -0
- package/dist/ai-providers/chatgpt-subscription/chatgpt-connections.service.js.map +1 -0
- package/dist/ai-providers/chatgpt-subscription/chatgpt-oauth.client.d.ts +59 -0
- package/dist/ai-providers/chatgpt-subscription/chatgpt-oauth.client.js +247 -0
- package/dist/ai-providers/chatgpt-subscription/chatgpt-oauth.client.js.map +1 -0
- package/dist/ai-providers/chatgpt-subscription/chatgpt-responses.model.d.ts +21 -0
- package/dist/ai-providers/chatgpt-subscription/chatgpt-responses.model.js +298 -0
- package/dist/ai-providers/chatgpt-subscription/chatgpt-responses.model.js.map +1 -0
- package/dist/ai-providers/chatgpt-subscription/chatgpt-responses.model.spec.d.ts +1 -0
- package/dist/ai-providers/chatgpt-subscription/chatgpt-responses.model.spec.js +345 -0
- package/dist/ai-providers/chatgpt-subscription/chatgpt-responses.model.spec.js.map +1 -0
- package/dist/ai-providers/chatgpt-subscription/chatgpt-subscription.constants.d.ts +16 -0
- package/dist/ai-providers/chatgpt-subscription/chatgpt-subscription.constants.js +26 -0
- package/dist/ai-providers/chatgpt-subscription/chatgpt-subscription.constants.js.map +1 -0
- package/dist/ai-providers/chatgpt-subscription/chatgpt-subscription.errors.d.ts +12 -0
- package/dist/ai-providers/chatgpt-subscription/chatgpt-subscription.errors.js +55 -0
- package/dist/ai-providers/chatgpt-subscription/chatgpt-subscription.errors.js.map +1 -0
- package/dist/ai-providers/chatgpt-subscription/chatgpt-subscription.service.d.ts +15 -0
- package/dist/ai-providers/chatgpt-subscription/chatgpt-subscription.service.js +78 -0
- package/dist/ai-providers/chatgpt-subscription/chatgpt-subscription.service.js.map +1 -0
- package/dist/ai-providers/dto/http/ai-provider-connection.dto.d.ts +30 -0
- package/dist/ai-providers/dto/http/ai-provider-connection.dto.js +182 -0
- package/dist/ai-providers/dto/http/ai-provider-connection.dto.js.map +1 -0
- package/dist/ai-providers/dto/service/ai-providers.service.types.d.ts +32 -1
- package/dist/ai-providers/enums/ai-model-job.enum.d.ts +1 -0
- package/dist/ai-providers/enums/ai-model-job.enum.js +5 -1
- package/dist/ai-providers/enums/ai-model-job.enum.js.map +1 -1
- package/dist/ai-providers/enums/ai-provider-connection-status.enum.d.ts +6 -0
- package/dist/ai-providers/enums/ai-provider-connection-status.enum.js +11 -0
- package/dist/ai-providers/enums/ai-provider-connection-status.enum.js.map +1 -0
- package/dist/ai-providers/enums/ai-provider-type.enum.d.ts +4 -1
- package/dist/ai-providers/enums/ai-provider-type.enum.js +9 -1
- package/dist/ai-providers/enums/ai-provider-type.enum.js.map +1 -1
- package/dist/ai-providers/enums/index.d.ts +1 -0
- package/dist/ai-providers/enums/index.js +1 -0
- package/dist/ai-providers/enums/index.js.map +1 -1
- package/dist/ai-providers/errors/ai-providers.errors.d.ts +33 -0
- package/dist/ai-providers/errors/ai-providers.errors.js +66 -1
- package/dist/ai-providers/errors/ai-providers.errors.js.map +1 -1
- package/dist/ai-providers/google-environment.d.ts +1 -0
- package/dist/ai-providers/google-environment.js +4 -0
- package/dist/ai-providers/google-environment.js.map +1 -1
- package/dist/app.module.js +8 -0
- package/dist/app.module.js.map +1 -1
- package/dist/config/env.config.d.ts +3 -0
- package/dist/config/env.config.js +17 -0
- package/dist/config/env.config.js.map +1 -1
- package/dist/errors/http/error-catalog.js +48 -0
- package/dist/errors/http/error-catalog.js.map +1 -1
- package/dist/migrations/1761300000000-ThreadSecret.d.ts +6 -0
- package/dist/migrations/1761300000000-ThreadSecret.js +14 -0
- package/dist/migrations/1761300000000-ThreadSecret.js.map +1 -0
- package/dist/migrations/1761400000000-ThreadReadStates.d.ts +6 -0
- package/dist/migrations/1761400000000-ThreadReadStates.js +19 -0
- package/dist/migrations/1761400000000-ThreadReadStates.js.map +1 -0
- package/dist/migrations/1761500000000-ThreadLastActivity.d.ts +6 -0
- package/dist/migrations/1761500000000-ThreadLastActivity.js +19 -0
- package/dist/migrations/1761500000000-ThreadLastActivity.js.map +1 -0
- package/dist/migrations/1761600000000-AiProviderConnections.d.ts +6 -0
- package/dist/migrations/1761600000000-AiProviderConnections.js +61 -0
- package/dist/migrations/1761600000000-AiProviderConnections.js.map +1 -0
- package/dist/public/assets/index-G5Ht4law.js +118 -0
- package/dist/public/assets/index-NjAtMwFI.css +1 -0
- package/dist/public/beta/assets/{index-B9fCnAUe.js → index-BO6PlAsO.js} +30 -30
- package/dist/public/beta/index.html +1 -1
- package/dist/public/index.html +2 -2
- package/dist/public/package.json +4 -4
- package/dist/search/index/search-embeddings.service.js +2 -0
- package/dist/search/index/search-embeddings.service.js.map +1 -1
- package/dist/secrets/secret-storage-format.d.ts +5 -0
- package/dist/secrets/secret-storage-format.js +101 -0
- package/dist/secrets/secret-storage-format.js.map +1 -0
- package/dist/secrets/secret-storage-format.spec.d.ts +1 -0
- package/dist/secrets/secret-storage-format.spec.js +32 -0
- package/dist/secrets/secret-storage-format.spec.js.map +1 -0
- package/dist/secrets/secrets-encryption.service.d.ts +1 -0
- package/dist/secrets/secrets-encryption.service.js +3 -0
- package/dist/secrets/secrets-encryption.service.js.map +1 -1
- package/dist/secrets/secrets.module.js +1 -1
- package/dist/secrets/secrets.module.js.map +1 -1
- package/dist/threads/agent/agent-context.utils.d.ts +3 -0
- package/dist/threads/agent/agent-context.utils.js +14 -0
- package/dist/threads/agent/agent-context.utils.js.map +1 -1
- package/dist/threads/agent/agent-context.utils.spec.js +25 -0
- package/dist/threads/agent/agent-context.utils.spec.js.map +1 -1
- package/dist/threads/agent/chat-model.factory.d.ts +6 -1
- package/dist/threads/agent/chat-model.factory.js +30 -8
- package/dist/threads/agent/chat-model.factory.js.map +1 -1
- package/dist/threads/agent/chat-model.factory.spec.js +96 -2
- package/dist/threads/agent/chat-model.factory.spec.js.map +1 -1
- package/dist/threads/agent/hooks/compaction.hook.d.ts +1 -0
- package/dist/threads/agent/hooks/compaction.hook.js +33 -8
- package/dist/threads/agent/hooks/compaction.hook.js.map +1 -1
- package/dist/threads/agent/hooks/easter-egg.hook.js +4 -3
- package/dist/threads/agent/hooks/easter-egg.hook.js.map +1 -1
- package/dist/threads/agent/hooks/hooks.spec.js +123 -0
- package/dist/threads/agent/hooks/hooks.spec.js.map +1 -1
- package/dist/threads/agent/hooks/thread-agent-hook.interface.js.map +1 -1
- package/dist/threads/agent/hooks/thread-agent-hooks.runner.js +2 -1
- package/dist/threads/agent/hooks/thread-agent-hooks.runner.js.map +1 -1
- package/dist/threads/agent/thread-agent-log.service.d.ts +3 -0
- package/dist/threads/agent/thread-agent-log.service.js +35 -1
- package/dist/threads/agent/thread-agent-log.service.js.map +1 -1
- package/dist/threads/agent/thread-agent.prompts.d.ts +6 -1
- package/dist/threads/agent/thread-agent.prompts.js +34 -7
- package/dist/threads/agent/thread-agent.prompts.js.map +1 -1
- package/dist/threads/agent/thread-agent.prompts.spec.d.ts +1 -0
- package/dist/threads/agent/thread-agent.prompts.spec.js +51 -0
- package/dist/threads/agent/thread-agent.prompts.spec.js.map +1 -0
- package/dist/threads/agent/thread-agent.service.d.ts +3 -3
- package/dist/threads/agent/thread-agent.service.js +11 -16
- package/dist/threads/agent/thread-agent.service.js.map +1 -1
- package/dist/threads/agent/thread-agent.service.spec.js +41 -9
- package/dist/threads/agent/thread-agent.service.spec.js.map +1 -1
- package/dist/threads/chat.service.d.ts +3 -0
- package/dist/threads/chat.service.js +37 -9
- package/dist/threads/chat.service.js.map +1 -1
- package/dist/threads/dto/service/threads.service.types.d.ts +9 -0
- package/dist/threads/dto/thread-list-item-response.dto.d.ts +2 -0
- package/dist/threads/dto/thread-list-item-response.dto.js +16 -0
- package/dist/threads/dto/thread-list-item-response.dto.js.map +1 -1
- package/dist/threads/dto/thread-token-usage-response.dto.d.ts +8 -0
- package/dist/threads/dto/thread-token-usage-response.dto.js +63 -0
- package/dist/threads/dto/thread-token-usage-response.dto.js.map +1 -0
- package/dist/threads/events/threads.events.d.ts +8 -0
- package/dist/threads/events/threads.events.js +8 -1
- package/dist/threads/events/threads.events.js.map +1 -1
- package/dist/threads/read-state/thread-read-state.entity.d.ts +9 -0
- package/dist/threads/read-state/thread-read-state.entity.js +49 -0
- package/dist/threads/read-state/thread-read-state.entity.js.map +1 -0
- package/dist/threads/read-state/thread-read-state.service.d.ts +14 -0
- package/dist/threads/read-state/thread-read-state.service.js +81 -0
- package/dist/threads/read-state/thread-read-state.service.js.map +1 -0
- package/dist/threads/thread-message-writer.service.d.ts +13 -0
- package/dist/threads/thread-message-writer.service.js +48 -0
- package/dist/threads/thread-message-writer.service.js.map +1 -0
- package/dist/threads/thread-message.entity.js +1 -0
- package/dist/threads/thread-message.entity.js.map +1 -1
- package/dist/threads/thread-state-reconciler.service.js +5 -0
- package/dist/threads/thread-state-reconciler.service.js.map +1 -1
- package/dist/threads/thread-state-reconciler.service.spec.d.ts +1 -0
- package/dist/threads/thread-state-reconciler.service.spec.js +44 -0
- package/dist/threads/thread-state-reconciler.service.spec.js.map +1 -0
- package/dist/threads/thread.entity.d.ts +2 -0
- package/dist/threads/thread.entity.js +24 -0
- package/dist/threads/thread.entity.js.map +1 -1
- package/dist/threads/threads-state.service.spec.js +1 -0
- package/dist/threads/threads-state.service.spec.js.map +1 -1
- package/dist/threads/threads.controller.d.ts +9 -2
- package/dist/threads/threads.controller.js +56 -6
- package/dist/threads/threads.controller.js.map +1 -1
- package/dist/threads/threads.gateway.d.ts +2 -1
- package/dist/threads/threads.gateway.js +40 -0
- package/dist/threads/threads.gateway.js.map +1 -1
- package/dist/threads/threads.module.js +6 -0
- package/dist/threads/threads.module.js.map +1 -1
- package/dist/threads/threads.service.d.ts +5 -1
- package/dist/threads/threads.service.js +19 -14
- package/dist/threads/threads.service.js.map +1 -1
- package/dist/threads/threads.service.spec.js +1 -0
- package/dist/threads/threads.service.spec.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -5
- package/dist/public/assets/index-Bu8fSAx2.js +0 -117
- package/dist/public/assets/index-DOm5BmlC.css +0 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { DataSource, Repository } from 'typeorm';
|
|
2
|
+
import { User } from "../../identity-provider/user.entity";
|
|
3
|
+
import { SecretsEncryptionService } from "../../secrets/secrets-encryption.service";
|
|
4
|
+
import { AiProviderAuthorizationAttemptEntity } from '../ai-provider-authorization-attempt.entity';
|
|
5
|
+
import { AiProviderConnectionEntity } from '../ai-provider-connection.entity';
|
|
6
|
+
import { AiProviderEntity } from '../ai-provider.entity';
|
|
7
|
+
import { AiProviderConnectionResult, CompleteAiProviderAuthorizationInput, DisconnectAiProviderResult, OnBehalfOf, StartAiProviderAuthorizationResult } from '../dto/service/ai-providers.service.types';
|
|
8
|
+
import { ChatgptOAuthClient } from './chatgpt-oauth.client';
|
|
9
|
+
export declare function chatgptSubscriptionUnavailableReason(encryption: Pick<SecretsEncryptionService, 'isEncrypting'>): string | null;
|
|
10
|
+
export declare class ChatgptConnectionsService {
|
|
11
|
+
private readonly connections;
|
|
12
|
+
private readonly attempts;
|
|
13
|
+
private readonly providers;
|
|
14
|
+
private readonly users;
|
|
15
|
+
private readonly dataSource;
|
|
16
|
+
private readonly encryption;
|
|
17
|
+
private readonly oauth;
|
|
18
|
+
private readonly logger;
|
|
19
|
+
private readonly refreshing;
|
|
20
|
+
constructor(connections: Repository<AiProviderConnectionEntity>, attempts: Repository<AiProviderAuthorizationAttemptEntity>, providers: Repository<AiProviderEntity>, users: Repository<User>, dataSource: DataSource, encryption: SecretsEncryptionService, oauth: ChatgptOAuthClient);
|
|
21
|
+
assertAvailable(): void;
|
|
22
|
+
getConnection(providerId: string, actorId: string): Promise<AiProviderConnectionResult>;
|
|
23
|
+
startAuthorization(providerId: string, actorId: string): Promise<StartAiProviderAuthorizationResult>;
|
|
24
|
+
completeAuthorization(providerId: string, actorId: string, callback: CompleteAiProviderAuthorizationInput): Promise<AiProviderConnectionResult>;
|
|
25
|
+
setAutomatedTurnsAllowed(providerId: string, actorId: string, allowed: boolean): Promise<AiProviderConnectionResult>;
|
|
26
|
+
disconnect(providerId: string, actorId: string): Promise<DisconnectAiProviderResult>;
|
|
27
|
+
disconnectAll(providerId: string): Promise<void>;
|
|
28
|
+
authorizeTurn(providerId: string, onBehalfOf: OnBehalfOf): Promise<{
|
|
29
|
+
connectionId: string;
|
|
30
|
+
}>;
|
|
31
|
+
requireOwnConnection(providerId: string, actorId: string): Promise<{
|
|
32
|
+
connectionId: string;
|
|
33
|
+
}>;
|
|
34
|
+
getAccessToken(connectionId: string): Promise<string>;
|
|
35
|
+
recordOutcome(connectionId: string, upstreamCode: string | null): Promise<void>;
|
|
36
|
+
private refresh;
|
|
37
|
+
private signInAgainError;
|
|
38
|
+
private markNeedsReauthorization;
|
|
39
|
+
private clear;
|
|
40
|
+
private usableAccessToken;
|
|
41
|
+
private readCredentials;
|
|
42
|
+
private writeCredentials;
|
|
43
|
+
private loadWithCredentials;
|
|
44
|
+
private describe;
|
|
45
|
+
private toResult;
|
|
46
|
+
private getProvider;
|
|
47
|
+
private ensureHostId;
|
|
48
|
+
private findUser;
|
|
49
|
+
private requireUser;
|
|
50
|
+
}
|
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var ChatgptConnectionsService_1;
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.ChatgptConnectionsService = void 0;
|
|
17
|
+
exports.chatgptSubscriptionUnavailableReason = chatgptSubscriptionUnavailableReason;
|
|
18
|
+
const common_1 = require("@nestjs/common");
|
|
19
|
+
const typeorm_1 = require("@nestjs/typeorm");
|
|
20
|
+
const node_crypto_1 = require("node:crypto");
|
|
21
|
+
const typeorm_2 = require("typeorm");
|
|
22
|
+
const env_config_1 = require("../../config/env.config");
|
|
23
|
+
const user_entity_1 = require("../../identity-provider/user.entity");
|
|
24
|
+
const secrets_encryption_service_1 = require("../../secrets/secrets-encryption.service");
|
|
25
|
+
const ai_provider_authorization_attempt_entity_1 = require("../ai-provider-authorization-attempt.entity");
|
|
26
|
+
const ai_provider_connection_entity_1 = require("../ai-provider-connection.entity");
|
|
27
|
+
const ai_provider_entity_1 = require("../ai-provider.entity");
|
|
28
|
+
const enums_1 = require("../enums");
|
|
29
|
+
const ai_providers_errors_1 = require("../errors/ai-providers.errors");
|
|
30
|
+
const chatgpt_oauth_client_1 = require("./chatgpt-oauth.client");
|
|
31
|
+
const chatgpt_subscription_constants_1 = require("./chatgpt-subscription.constants");
|
|
32
|
+
const REFRESH_SKEW_MS = 60_000;
|
|
33
|
+
const NO_FALLBACK = 'No other provider was used.';
|
|
34
|
+
const WHERE_TO_CONNECT = 'Settings → AI models';
|
|
35
|
+
function withoutQuery(address) {
|
|
36
|
+
const url = new URL(address);
|
|
37
|
+
return `${url.origin}${url.pathname}`;
|
|
38
|
+
}
|
|
39
|
+
function hashState(state) {
|
|
40
|
+
return (0, node_crypto_1.createHash)('sha256').update(state).digest('hex');
|
|
41
|
+
}
|
|
42
|
+
function grantsPlanUsage(scope) {
|
|
43
|
+
return !!scope && scope.split(/\s+/).includes(chatgpt_subscription_constants_1.PLAN_USAGE_SCOPE);
|
|
44
|
+
}
|
|
45
|
+
function chatgptSubscriptionUnavailableReason(encryption) {
|
|
46
|
+
if (!(0, env_config_1.isChatgptSubscriptionEnabled)()) {
|
|
47
|
+
return ('Sign in with ChatGPT is turned off on this server. It is only for a Taico you run ' +
|
|
48
|
+
'yourself: set CHATGPT_SUBSCRIPTION_ENABLED=true if that is the case.');
|
|
49
|
+
}
|
|
50
|
+
if (!encryption.isEncrypting()) {
|
|
51
|
+
return ('Sign in with ChatGPT needs encrypted secret storage. Set SECRETS_ENABLED=true and ' +
|
|
52
|
+
'SECRETS_ENCRYPTION_KEY; plaintext mode is not accepted for these tokens.');
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
let ChatgptConnectionsService = ChatgptConnectionsService_1 = class ChatgptConnectionsService {
|
|
57
|
+
connections;
|
|
58
|
+
attempts;
|
|
59
|
+
providers;
|
|
60
|
+
users;
|
|
61
|
+
dataSource;
|
|
62
|
+
encryption;
|
|
63
|
+
oauth;
|
|
64
|
+
logger = new common_1.Logger(ChatgptConnectionsService_1.name);
|
|
65
|
+
refreshing = new Map();
|
|
66
|
+
constructor(connections, attempts, providers, users, dataSource, encryption, oauth) {
|
|
67
|
+
this.connections = connections;
|
|
68
|
+
this.attempts = attempts;
|
|
69
|
+
this.providers = providers;
|
|
70
|
+
this.users = users;
|
|
71
|
+
this.dataSource = dataSource;
|
|
72
|
+
this.encryption = encryption;
|
|
73
|
+
this.oauth = oauth;
|
|
74
|
+
}
|
|
75
|
+
assertAvailable() {
|
|
76
|
+
const reason = chatgptSubscriptionUnavailableReason(this.encryption);
|
|
77
|
+
if (reason) {
|
|
78
|
+
throw new ai_providers_errors_1.AiProviderTypeDisabledError(enums_1.AiProviderType.CHATGPT_SUBSCRIPTION, reason);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async getConnection(providerId, actorId) {
|
|
82
|
+
await this.getProvider(providerId);
|
|
83
|
+
const user = await this.findUser(actorId);
|
|
84
|
+
if (!user) {
|
|
85
|
+
return this.toResult(providerId, null, false);
|
|
86
|
+
}
|
|
87
|
+
return this.describe(providerId, user.id);
|
|
88
|
+
}
|
|
89
|
+
async startAuthorization(providerId, actorId) {
|
|
90
|
+
this.assertAvailable();
|
|
91
|
+
const provider = await this.getProvider(providerId);
|
|
92
|
+
const user = await this.requireUser(providerId, actorId);
|
|
93
|
+
const connection = await this.loadWithCredentials(this.connections.manager, { aiProviderId: providerId, userId: user.id });
|
|
94
|
+
const issuedClientId = connection &&
|
|
95
|
+
connection.status !== enums_1.AiProviderConnectionStatus.DISCONNECTED
|
|
96
|
+
? connection.externalClientId
|
|
97
|
+
: null;
|
|
98
|
+
const credentials = connection ? this.readCredentials(connection) : null;
|
|
99
|
+
const state = (0, node_crypto_1.randomBytes)(32).toString('base64url');
|
|
100
|
+
const nonce = (0, node_crypto_1.randomBytes)(32).toString('base64url');
|
|
101
|
+
const codeVerifier = (0, node_crypto_1.randomBytes)(48).toString('base64url');
|
|
102
|
+
const expiresAt = new Date(Date.now() + chatgpt_subscription_constants_1.AUTHORIZATION_ATTEMPT_TTL_MS);
|
|
103
|
+
const redirectUri = `http://${chatgpt_subscription_constants_1.REDIRECT_HOST}:${(0, env_config_1.getChatgptSubscriptionCallbackPort)()}${chatgpt_subscription_constants_1.REDIRECT_PATH}`;
|
|
104
|
+
await this.attempts.delete({ aiProviderId: providerId, userId: user.id });
|
|
105
|
+
await this.attempts.delete({ expiresAt: (0, typeorm_2.LessThan)(new Date()) });
|
|
106
|
+
await this.attempts.insert({
|
|
107
|
+
aiProviderId: providerId,
|
|
108
|
+
userId: user.id,
|
|
109
|
+
stateHash: hashState(state),
|
|
110
|
+
nonce,
|
|
111
|
+
codeVerifier: this.encryption.encrypt(codeVerifier),
|
|
112
|
+
redirectUri,
|
|
113
|
+
clientId: issuedClientId ?? chatgpt_subscription_constants_1.REGISTRATION_CLIENT_ID,
|
|
114
|
+
isRegistration: !issuedClientId,
|
|
115
|
+
expiresAt,
|
|
116
|
+
});
|
|
117
|
+
const authorizationUrl = this.oauth.buildAuthorizationUrl({
|
|
118
|
+
issuedClientId,
|
|
119
|
+
redirectUri,
|
|
120
|
+
state,
|
|
121
|
+
nonce,
|
|
122
|
+
codeVerifier,
|
|
123
|
+
hostId: await this.ensureHostId(provider),
|
|
124
|
+
idTokenHint: credentials?.idToken,
|
|
125
|
+
loginHint: connection?.externalEmail,
|
|
126
|
+
});
|
|
127
|
+
this.logger.log({
|
|
128
|
+
message: 'Started Sign in with ChatGPT',
|
|
129
|
+
providerId,
|
|
130
|
+
userId: user.id,
|
|
131
|
+
isRegistration: !issuedClientId,
|
|
132
|
+
});
|
|
133
|
+
return { authorizationUrl, expiresAt };
|
|
134
|
+
}
|
|
135
|
+
async completeAuthorization(providerId, actorId, callback) {
|
|
136
|
+
this.assertAvailable();
|
|
137
|
+
await this.getProvider(providerId);
|
|
138
|
+
const user = await this.requireUser(providerId, actorId);
|
|
139
|
+
const claimed = await this.attempts
|
|
140
|
+
.createQueryBuilder()
|
|
141
|
+
.delete()
|
|
142
|
+
.where('state_hash = :stateHash AND user_id = :userId AND ai_provider_id = :providerId', {
|
|
143
|
+
stateHash: hashState(callback.state),
|
|
144
|
+
userId: user.id,
|
|
145
|
+
providerId,
|
|
146
|
+
})
|
|
147
|
+
.returning('*')
|
|
148
|
+
.execute();
|
|
149
|
+
const row = claimed.raw[0];
|
|
150
|
+
if (!row || new Date(row.expires_at).getTime() < Date.now()) {
|
|
151
|
+
throw new ai_providers_errors_1.AiProviderAuthorizationFailedError('This sign-in is unknown, was already used, or has expired. Start again.');
|
|
152
|
+
}
|
|
153
|
+
const attempt = {
|
|
154
|
+
nonce: row.nonce,
|
|
155
|
+
codeVerifier: this.encryption.decrypt(row.code_verifier),
|
|
156
|
+
redirectUri: row.redirect_uri,
|
|
157
|
+
clientId: row.client_id,
|
|
158
|
+
isRegistration: row.is_registration,
|
|
159
|
+
};
|
|
160
|
+
if (callback.redirectAddress &&
|
|
161
|
+
callback.redirectAddress !== withoutQuery(attempt.redirectUri)) {
|
|
162
|
+
throw new ai_providers_errors_1.AiProviderAuthorizationFailedError(`That address is not where OpenAI sends this sign-in back to (${attempt.redirectUri}). Start again and paste the address your browser lands on.`);
|
|
163
|
+
}
|
|
164
|
+
if (callback.error) {
|
|
165
|
+
throw new ai_providers_errors_1.AiProviderAuthorizationFailedError(callback.error === 'access_denied'
|
|
166
|
+
? 'You did not approve the request at OpenAI, so nothing was connected.'
|
|
167
|
+
: `OpenAI did not complete the sign-in (${callback.error}). Nothing was connected.`, {
|
|
168
|
+
oauthError: callback.error,
|
|
169
|
+
description: callback.errorDescription ?? null,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (!callback.code) {
|
|
173
|
+
throw new ai_providers_errors_1.AiProviderAuthorizationFailedError('The address OpenAI sent you back to has no authorization code. Start again.');
|
|
174
|
+
}
|
|
175
|
+
let clientId;
|
|
176
|
+
if (attempt.isRegistration) {
|
|
177
|
+
if (!callback.clientId || callback.clientId === chatgpt_subscription_constants_1.REGISTRATION_CLIENT_ID) {
|
|
178
|
+
throw new ai_providers_errors_1.AiProviderAuthorizationFailedError('OpenAI did not issue a client for this installation. Start again.');
|
|
179
|
+
}
|
|
180
|
+
clientId = callback.clientId;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
if (callback.clientId && callback.clientId !== attempt.clientId) {
|
|
184
|
+
throw new ai_providers_errors_1.AiProviderAuthorizationFailedError('OpenAI answered for a different client than the one that was connected. Nothing was changed.');
|
|
185
|
+
}
|
|
186
|
+
clientId = attempt.clientId;
|
|
187
|
+
}
|
|
188
|
+
let tokens;
|
|
189
|
+
try {
|
|
190
|
+
tokens = await this.oauth.exchangeCode({
|
|
191
|
+
clientId,
|
|
192
|
+
code: callback.code,
|
|
193
|
+
codeVerifier: attempt.codeVerifier,
|
|
194
|
+
redirectUri: attempt.redirectUri,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
throw new ai_providers_errors_1.AiProviderAuthorizationFailedError('OpenAI rejected the sign-in when Taico tried to finish it. Start again.', { reason: error instanceof Error ? error.message : String(error) });
|
|
199
|
+
}
|
|
200
|
+
if (!tokens.idToken) {
|
|
201
|
+
await this.oauth.revoke({ clientId, refreshToken: tokens.refreshToken });
|
|
202
|
+
throw new ai_providers_errors_1.AiProviderAuthorizationFailedError('OpenAI did not say which account signed in. Nothing was connected.');
|
|
203
|
+
}
|
|
204
|
+
let identity;
|
|
205
|
+
try {
|
|
206
|
+
identity = await this.oauth.verifyIdToken({
|
|
207
|
+
idToken: tokens.idToken,
|
|
208
|
+
clientId,
|
|
209
|
+
nonce: attempt.nonce,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
await this.oauth.revoke({ clientId, refreshToken: tokens.refreshToken });
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
const existing = await this.connections.findOne({
|
|
217
|
+
where: { aiProviderId: providerId, userId: user.id },
|
|
218
|
+
});
|
|
219
|
+
if (!attempt.isRegistration &&
|
|
220
|
+
existing?.externalSubject &&
|
|
221
|
+
(existing.externalSubject !== identity.subject ||
|
|
222
|
+
existing.externalIssuer !== identity.issuer)) {
|
|
223
|
+
await this.oauth.revoke({ clientId, refreshToken: tokens.refreshToken });
|
|
224
|
+
throw new ai_providers_errors_1.AiProviderAuthorizationFailedError('You signed in with a different ChatGPT account than the one that was connected. ' +
|
|
225
|
+
'Disconnect first to switch accounts.');
|
|
226
|
+
}
|
|
227
|
+
const canUsePlan = grantsPlanUsage(tokens.scope);
|
|
228
|
+
const connection = existing ??
|
|
229
|
+
this.connections.create({ aiProviderId: providerId, userId: user.id });
|
|
230
|
+
connection.status = canUsePlan
|
|
231
|
+
? enums_1.AiProviderConnectionStatus.CONNECTED
|
|
232
|
+
: enums_1.AiProviderConnectionStatus.PLAN_ACCESS_NOT_GRANTED;
|
|
233
|
+
connection.externalClientId = clientId;
|
|
234
|
+
connection.externalIssuer = identity.issuer;
|
|
235
|
+
connection.externalSubject = identity.subject;
|
|
236
|
+
connection.externalEmail = identity.email;
|
|
237
|
+
connection.grantedScopes = tokens.scope;
|
|
238
|
+
connection.credentials = this.writeCredentials({
|
|
239
|
+
accessToken: tokens.accessToken,
|
|
240
|
+
refreshToken: tokens.refreshToken,
|
|
241
|
+
idToken: tokens.idToken,
|
|
242
|
+
});
|
|
243
|
+
connection.accessTokenExpiresAt = new Date(Date.now() + tokens.expiresInSeconds * 1000);
|
|
244
|
+
connection.automatedTurnsAllowedAt =
|
|
245
|
+
existing?.automatedTurnsAllowedAt ?? null;
|
|
246
|
+
connection.lastErrorCode = null;
|
|
247
|
+
connection.lastErrorAt = null;
|
|
248
|
+
await this.connections.save(connection);
|
|
249
|
+
this.logger.log({
|
|
250
|
+
message: 'Connected a ChatGPT account',
|
|
251
|
+
providerId,
|
|
252
|
+
userId: user.id,
|
|
253
|
+
canUsePlan,
|
|
254
|
+
});
|
|
255
|
+
return this.describe(providerId, user.id);
|
|
256
|
+
}
|
|
257
|
+
async setAutomatedTurnsAllowed(providerId, actorId, allowed) {
|
|
258
|
+
await this.getProvider(providerId);
|
|
259
|
+
const user = await this.requireUser(providerId, actorId);
|
|
260
|
+
const connection = await this.connections.findOne({
|
|
261
|
+
where: { aiProviderId: providerId, userId: user.id },
|
|
262
|
+
});
|
|
263
|
+
if (connection?.status !== enums_1.AiProviderConnectionStatus.CONNECTED) {
|
|
264
|
+
throw new ai_providers_errors_1.AiProviderConnectionRequiredError(providerId, 'Connect your ChatGPT account before changing this.');
|
|
265
|
+
}
|
|
266
|
+
if (allowed !== !!connection.automatedTurnsAllowedAt) {
|
|
267
|
+
connection.automatedTurnsAllowedAt = allowed ? new Date() : null;
|
|
268
|
+
await this.connections.save(connection);
|
|
269
|
+
}
|
|
270
|
+
return this.describe(providerId, user.id);
|
|
271
|
+
}
|
|
272
|
+
async disconnect(providerId, actorId) {
|
|
273
|
+
await this.getProvider(providerId);
|
|
274
|
+
const user = await this.requireUser(providerId, actorId);
|
|
275
|
+
await this.attempts.delete({ aiProviderId: providerId, userId: user.id });
|
|
276
|
+
const connection = await this.loadWithCredentials(this.connections.manager, {
|
|
277
|
+
aiProviderId: providerId,
|
|
278
|
+
userId: user.id,
|
|
279
|
+
});
|
|
280
|
+
if (!connection) {
|
|
281
|
+
return { revokedAtProvider: true };
|
|
282
|
+
}
|
|
283
|
+
const revokedAtProvider = await this.clear(connection);
|
|
284
|
+
this.logger.log({
|
|
285
|
+
message: 'Disconnected a ChatGPT account',
|
|
286
|
+
providerId,
|
|
287
|
+
userId: user.id,
|
|
288
|
+
revokedAtProvider,
|
|
289
|
+
});
|
|
290
|
+
return { revokedAtProvider };
|
|
291
|
+
}
|
|
292
|
+
async disconnectAll(providerId) {
|
|
293
|
+
const rows = await this.connections.find({
|
|
294
|
+
where: { aiProviderId: providerId },
|
|
295
|
+
});
|
|
296
|
+
for (const row of rows) {
|
|
297
|
+
const connection = await this.loadWithCredentials(this.connections.manager, { id: row.id });
|
|
298
|
+
if (connection) {
|
|
299
|
+
await this.clear(connection);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
await this.attempts.delete({ aiProviderId: providerId });
|
|
303
|
+
}
|
|
304
|
+
async authorizeTurn(providerId, onBehalfOf) {
|
|
305
|
+
this.assertAvailable();
|
|
306
|
+
const whose = onBehalfOf.description ??
|
|
307
|
+
(onBehalfOf.automated
|
|
308
|
+
? 'the person this task update is for'
|
|
309
|
+
: 'the person who wrote the message');
|
|
310
|
+
const user = await this.findUser(onBehalfOf.actorId);
|
|
311
|
+
const connection = user
|
|
312
|
+
? await this.connections.findOne({
|
|
313
|
+
where: { aiProviderId: providerId, userId: user.id },
|
|
314
|
+
})
|
|
315
|
+
: null;
|
|
316
|
+
const status = connection?.status ?? enums_1.AiProviderConnectionStatus.DISCONNECTED;
|
|
317
|
+
if (!connection || status !== enums_1.AiProviderConnectionStatus.CONNECTED) {
|
|
318
|
+
const problem = {
|
|
319
|
+
[enums_1.AiProviderConnectionStatus.DISCONNECTED]: 'has not connected a ChatGPT account',
|
|
320
|
+
[enums_1.AiProviderConnectionStatus.NEEDS_REAUTHORIZATION]: 'has to sign in to ChatGPT again, because the connection expired or was removed',
|
|
321
|
+
[enums_1.AiProviderConnectionStatus.PLAN_ACCESS_NOT_GRANTED]: 'connected a ChatGPT account without allowing use of its plan',
|
|
322
|
+
[enums_1.AiProviderConnectionStatus.CONNECTED]: '',
|
|
323
|
+
}[status];
|
|
324
|
+
throw new ai_providers_errors_1.AiProviderConnectionRequiredError(providerId, `This thread answers with the ChatGPT plan of ${whose}, who ${problem}. ` +
|
|
325
|
+
`Connect it in ${WHERE_TO_CONNECT}. ${NO_FALLBACK}`, { status, automated: onBehalfOf.automated });
|
|
326
|
+
}
|
|
327
|
+
if (onBehalfOf.automated && !connection.automatedTurnsAllowedAt) {
|
|
328
|
+
throw new ai_providers_errors_1.AiProviderConnectionConsentRequiredError(providerId, 'A task update arrived, but answering it would use a ChatGPT plan without anyone ' +
|
|
329
|
+
`asking, and ${whose} has not allowed that (${WHERE_TO_CONNECT}). ${NO_FALLBACK}`);
|
|
330
|
+
}
|
|
331
|
+
return { connectionId: connection.id };
|
|
332
|
+
}
|
|
333
|
+
async requireOwnConnection(providerId, actorId) {
|
|
334
|
+
return this.authorizeTurn(providerId, { actorId, automated: false });
|
|
335
|
+
}
|
|
336
|
+
async getAccessToken(connectionId) {
|
|
337
|
+
const connection = await this.loadWithCredentials(this.connections.manager, {
|
|
338
|
+
id: connectionId,
|
|
339
|
+
});
|
|
340
|
+
const usable = connection ? this.usableAccessToken(connection) : null;
|
|
341
|
+
if (usable) {
|
|
342
|
+
return usable;
|
|
343
|
+
}
|
|
344
|
+
if (!connection ||
|
|
345
|
+
connection.status !== enums_1.AiProviderConnectionStatus.CONNECTED) {
|
|
346
|
+
throw this.signInAgainError(connection?.aiProviderId ?? null);
|
|
347
|
+
}
|
|
348
|
+
let pending = this.refreshing.get(connectionId);
|
|
349
|
+
if (!pending) {
|
|
350
|
+
pending = this.refresh(connectionId).finally(() => this.refreshing.delete(connectionId));
|
|
351
|
+
this.refreshing.set(connectionId, pending);
|
|
352
|
+
}
|
|
353
|
+
return pending;
|
|
354
|
+
}
|
|
355
|
+
async recordOutcome(connectionId, upstreamCode) {
|
|
356
|
+
try {
|
|
357
|
+
if (upstreamCode) {
|
|
358
|
+
await this.connections.update(connectionId, {
|
|
359
|
+
lastErrorCode: upstreamCode,
|
|
360
|
+
lastErrorAt: new Date(),
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
await this.connections
|
|
365
|
+
.createQueryBuilder()
|
|
366
|
+
.update()
|
|
367
|
+
.set({ lastErrorCode: null, lastErrorAt: null })
|
|
368
|
+
.where('id = :id AND last_error_code IS NOT NULL', {
|
|
369
|
+
id: connectionId,
|
|
370
|
+
})
|
|
371
|
+
.execute();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
this.logger.warn({
|
|
376
|
+
message: 'Could not record the outcome of a ChatGPT plan request',
|
|
377
|
+
connectionId,
|
|
378
|
+
error: error instanceof Error ? error.message : String(error),
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async refresh(connectionId) {
|
|
383
|
+
let providerId = null;
|
|
384
|
+
const outcome = await this.dataSource.transaction(async (manager) => {
|
|
385
|
+
const connection = await this.loadWithCredentials(manager, { id: connectionId }, true);
|
|
386
|
+
providerId = connection?.aiProviderId ?? null;
|
|
387
|
+
if (!connection ||
|
|
388
|
+
connection.status !== enums_1.AiProviderConnectionStatus.CONNECTED) {
|
|
389
|
+
return { kind: 'reauthorize' };
|
|
390
|
+
}
|
|
391
|
+
const fresh = this.usableAccessToken(connection);
|
|
392
|
+
if (fresh) {
|
|
393
|
+
return { kind: 'token', accessToken: fresh };
|
|
394
|
+
}
|
|
395
|
+
const credentials = this.readCredentials(connection);
|
|
396
|
+
if (!credentials || !connection.externalClientId) {
|
|
397
|
+
await this.markNeedsReauthorization(manager, connection);
|
|
398
|
+
return { kind: 'reauthorize' };
|
|
399
|
+
}
|
|
400
|
+
let tokens;
|
|
401
|
+
try {
|
|
402
|
+
tokens = await this.oauth.refresh({
|
|
403
|
+
clientId: connection.externalClientId,
|
|
404
|
+
refreshToken: credentials.refreshToken,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
if (error instanceof chatgpt_oauth_client_1.ChatgptTokenEndpointError && error.isTerminal) {
|
|
409
|
+
await this.markNeedsReauthorization(manager, connection);
|
|
410
|
+
return { kind: 'reauthorize' };
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
kind: 'failed',
|
|
414
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
const scope = tokens.scope ?? connection.grantedScopes;
|
|
418
|
+
const canUsePlan = grantsPlanUsage(scope);
|
|
419
|
+
connection.credentials = this.writeCredentials({
|
|
420
|
+
accessToken: tokens.accessToken,
|
|
421
|
+
refreshToken: tokens.refreshToken,
|
|
422
|
+
idToken: tokens.idToken ?? credentials.idToken,
|
|
423
|
+
});
|
|
424
|
+
connection.accessTokenExpiresAt = new Date(Date.now() + tokens.expiresInSeconds * 1000);
|
|
425
|
+
connection.grantedScopes = scope;
|
|
426
|
+
if (!canUsePlan) {
|
|
427
|
+
connection.status =
|
|
428
|
+
enums_1.AiProviderConnectionStatus.PLAN_ACCESS_NOT_GRANTED;
|
|
429
|
+
}
|
|
430
|
+
await manager.save(connection);
|
|
431
|
+
return canUsePlan
|
|
432
|
+
? { kind: 'token', accessToken: tokens.accessToken }
|
|
433
|
+
: { kind: 'plan_access_lost' };
|
|
434
|
+
});
|
|
435
|
+
switch (outcome.kind) {
|
|
436
|
+
case 'token':
|
|
437
|
+
return outcome.accessToken;
|
|
438
|
+
case 'reauthorize':
|
|
439
|
+
throw this.signInAgainError(providerId);
|
|
440
|
+
case 'plan_access_lost':
|
|
441
|
+
throw new ai_providers_errors_1.AiProviderConnectionRequiredError(providerId ?? '', `The connected ChatGPT account no longer allows use of its plan. Connect it again in ${WHERE_TO_CONNECT}. ${NO_FALLBACK}`, { status: enums_1.AiProviderConnectionStatus.PLAN_ACCESS_NOT_GRANTED });
|
|
442
|
+
case 'failed': {
|
|
443
|
+
const oauthError = outcome.error instanceof chatgpt_oauth_client_1.ChatgptTokenEndpointError
|
|
444
|
+
? outcome.error.oauthError
|
|
445
|
+
: null;
|
|
446
|
+
this.logger.warn({
|
|
447
|
+
message: 'Refreshing a ChatGPT connection failed',
|
|
448
|
+
connectionId,
|
|
449
|
+
oauthError,
|
|
450
|
+
error: outcome.error.message,
|
|
451
|
+
});
|
|
452
|
+
throw new ai_providers_errors_1.AiProviderRequestFailedError(oauthError === 'invalid_client'
|
|
453
|
+
? `OpenAI no longer accepts this installation's client. Disconnect and connect again in ${WHERE_TO_CONNECT}. ${NO_FALLBACK}`
|
|
454
|
+
: `Could not renew the ChatGPT sign-in right now. The connection was kept; try again shortly. ${NO_FALLBACK}`, { upstreamCode: oauthError ?? 'refresh_unavailable' });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
signInAgainError(providerId) {
|
|
459
|
+
return new ai_providers_errors_1.AiProviderConnectionRequiredError(providerId ?? '', `The ChatGPT sign-in expired or was removed. Sign in again in ${WHERE_TO_CONNECT}. ${NO_FALLBACK}`, { status: enums_1.AiProviderConnectionStatus.NEEDS_REAUTHORIZATION });
|
|
460
|
+
}
|
|
461
|
+
async markNeedsReauthorization(manager, connection) {
|
|
462
|
+
connection.status = enums_1.AiProviderConnectionStatus.NEEDS_REAUTHORIZATION;
|
|
463
|
+
connection.credentials = null;
|
|
464
|
+
connection.accessTokenExpiresAt = null;
|
|
465
|
+
await manager.save(connection);
|
|
466
|
+
}
|
|
467
|
+
async clear(connection) {
|
|
468
|
+
let revoked = true;
|
|
469
|
+
const credentials = this.readCredentials(connection);
|
|
470
|
+
if (credentials && connection.externalClientId) {
|
|
471
|
+
revoked = await this.oauth.revoke({
|
|
472
|
+
clientId: connection.externalClientId,
|
|
473
|
+
refreshToken: credentials.refreshToken,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
connection.status = enums_1.AiProviderConnectionStatus.DISCONNECTED;
|
|
477
|
+
connection.credentials = null;
|
|
478
|
+
connection.accessTokenExpiresAt = null;
|
|
479
|
+
connection.externalClientId = null;
|
|
480
|
+
connection.externalIssuer = null;
|
|
481
|
+
connection.externalSubject = null;
|
|
482
|
+
connection.externalEmail = null;
|
|
483
|
+
connection.grantedScopes = null;
|
|
484
|
+
connection.automatedTurnsAllowedAt = null;
|
|
485
|
+
connection.lastErrorCode = null;
|
|
486
|
+
connection.lastErrorAt = null;
|
|
487
|
+
await this.connections.save(connection);
|
|
488
|
+
return revoked;
|
|
489
|
+
}
|
|
490
|
+
usableAccessToken(connection) {
|
|
491
|
+
if (connection.status !== enums_1.AiProviderConnectionStatus.CONNECTED ||
|
|
492
|
+
!connection.accessTokenExpiresAt ||
|
|
493
|
+
connection.accessTokenExpiresAt.getTime() - Date.now() <= REFRESH_SKEW_MS) {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
return this.readCredentials(connection)?.accessToken ?? null;
|
|
497
|
+
}
|
|
498
|
+
readCredentials(connection) {
|
|
499
|
+
if (!connection.credentials) {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
return JSON.parse(this.encryption.decrypt(connection.credentials));
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
this.logger.warn({
|
|
507
|
+
message: 'Stored ChatGPT credentials could not be read',
|
|
508
|
+
connectionId: connection.id,
|
|
509
|
+
error: error instanceof Error ? error.message : String(error),
|
|
510
|
+
});
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
writeCredentials(credentials) {
|
|
515
|
+
return this.encryption.encrypt(JSON.stringify(credentials));
|
|
516
|
+
}
|
|
517
|
+
loadWithCredentials(manager, where, lock = false) {
|
|
518
|
+
const query = manager
|
|
519
|
+
.createQueryBuilder(ai_provider_connection_entity_1.AiProviderConnectionEntity, 'connection')
|
|
520
|
+
.addSelect('connection.credentials')
|
|
521
|
+
.where(where);
|
|
522
|
+
if (lock) {
|
|
523
|
+
query.setLock('pessimistic_write');
|
|
524
|
+
}
|
|
525
|
+
return query.getOne();
|
|
526
|
+
}
|
|
527
|
+
async describe(providerId, userId) {
|
|
528
|
+
const [connection, pending] = await Promise.all([
|
|
529
|
+
this.connections.findOne({ where: { aiProviderId: providerId, userId } }),
|
|
530
|
+
this.attempts.count({
|
|
531
|
+
where: {
|
|
532
|
+
aiProviderId: providerId,
|
|
533
|
+
userId,
|
|
534
|
+
expiresAt: (0, typeorm_2.MoreThan)(new Date()),
|
|
535
|
+
},
|
|
536
|
+
}),
|
|
537
|
+
]);
|
|
538
|
+
return this.toResult(providerId, connection, pending > 0);
|
|
539
|
+
}
|
|
540
|
+
toResult(providerId, connection, isAuthorizationPending) {
|
|
541
|
+
const status = connection?.status ?? enums_1.AiProviderConnectionStatus.DISCONNECTED;
|
|
542
|
+
return {
|
|
543
|
+
aiProviderId: providerId,
|
|
544
|
+
status,
|
|
545
|
+
accountEmail: connection?.externalEmail ?? null,
|
|
546
|
+
canUsePlan: status === enums_1.AiProviderConnectionStatus.CONNECTED,
|
|
547
|
+
automatedTurnsAllowed: !!connection?.automatedTurnsAllowedAt,
|
|
548
|
+
isAuthorizationPending,
|
|
549
|
+
lastErrorCode: connection?.lastErrorCode ?? null,
|
|
550
|
+
lastErrorAt: connection?.lastErrorAt ?? null,
|
|
551
|
+
manageUsageUrl: chatgpt_subscription_constants_1.MANAGE_USAGE_URL,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
async getProvider(providerId) {
|
|
555
|
+
const provider = await this.providers.findOne({
|
|
556
|
+
where: { id: providerId },
|
|
557
|
+
});
|
|
558
|
+
if (!provider) {
|
|
559
|
+
throw new ai_providers_errors_1.AiProviderNotFoundError(providerId);
|
|
560
|
+
}
|
|
561
|
+
if (provider.type !== enums_1.AiProviderType.CHATGPT_SUBSCRIPTION) {
|
|
562
|
+
throw new ai_providers_errors_1.AiProviderConnectionNotSupportedError(providerId);
|
|
563
|
+
}
|
|
564
|
+
return provider;
|
|
565
|
+
}
|
|
566
|
+
async ensureHostId(provider) {
|
|
567
|
+
if (provider.hostId) {
|
|
568
|
+
return provider.hostId;
|
|
569
|
+
}
|
|
570
|
+
await this.providers
|
|
571
|
+
.createQueryBuilder()
|
|
572
|
+
.update()
|
|
573
|
+
.set({ hostId: `urn:uuid:${(0, node_crypto_1.randomUUID)()}` })
|
|
574
|
+
.where('id = :id AND host_id IS NULL', { id: provider.id })
|
|
575
|
+
.execute();
|
|
576
|
+
const saved = await this.providers.findOneOrFail({
|
|
577
|
+
where: { id: provider.id },
|
|
578
|
+
});
|
|
579
|
+
return saved.hostId;
|
|
580
|
+
}
|
|
581
|
+
findUser(actorId) {
|
|
582
|
+
return this.users.findOne({ where: { actorId, isActive: true } });
|
|
583
|
+
}
|
|
584
|
+
async requireUser(providerId, actorId) {
|
|
585
|
+
const user = await this.findUser(actorId);
|
|
586
|
+
if (!user) {
|
|
587
|
+
throw new ai_providers_errors_1.AiProviderConnectionRequiredError(providerId, 'Only a person signed in to Taico can connect a ChatGPT account.');
|
|
588
|
+
}
|
|
589
|
+
return user;
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
exports.ChatgptConnectionsService = ChatgptConnectionsService;
|
|
593
|
+
exports.ChatgptConnectionsService = ChatgptConnectionsService = ChatgptConnectionsService_1 = __decorate([
|
|
594
|
+
(0, common_1.Injectable)(),
|
|
595
|
+
__param(0, (0, typeorm_1.InjectRepository)(ai_provider_connection_entity_1.AiProviderConnectionEntity)),
|
|
596
|
+
__param(1, (0, typeorm_1.InjectRepository)(ai_provider_authorization_attempt_entity_1.AiProviderAuthorizationAttemptEntity)),
|
|
597
|
+
__param(2, (0, typeorm_1.InjectRepository)(ai_provider_entity_1.AiProviderEntity)),
|
|
598
|
+
__param(3, (0, typeorm_1.InjectRepository)(user_entity_1.User)),
|
|
599
|
+
__metadata("design:paramtypes", [typeorm_2.Repository,
|
|
600
|
+
typeorm_2.Repository,
|
|
601
|
+
typeorm_2.Repository,
|
|
602
|
+
typeorm_2.Repository,
|
|
603
|
+
typeorm_2.DataSource,
|
|
604
|
+
secrets_encryption_service_1.SecretsEncryptionService,
|
|
605
|
+
chatgpt_oauth_client_1.ChatgptOAuthClient])
|
|
606
|
+
], ChatgptConnectionsService);
|
|
607
|
+
//# sourceMappingURL=chatgpt-connections.service.js.map
|