aisubs 0.3.7 → 0.3.8
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/CHANGELOG.md +7 -0
- package/README.md +29 -29
- package/dist/auth.d.ts +3 -1
- package/dist/auth.js +123 -15
- package/dist/cli.js +5 -3
- package/dist/http.js +1 -0
- package/dist/index.d.ts +2 -7
- package/dist/index.js +1 -6
- package/dist/memory-store.d.ts +9 -0
- package/dist/memory-store.js +34 -0
- package/dist/node.d.ts +2 -0
- package/dist/node.js +2 -0
- package/dist/providers/chatgpt.d.ts +2 -1
- package/dist/providers/chatgpt.js +64 -2
- package/dist/providers/opencode.d.ts +6 -2
- package/dist/providers/opencode.js +65 -21
- package/dist/sqlite-store.d.ts +61 -0
- package/dist/sqlite-store.js +323 -0
- package/dist/store.d.ts +0 -23
- package/dist/store.js +1 -198
- package/dist/types.d.ts +40 -0
- package/dist/utils.js +14 -6
- package/examples/direct.mjs +8 -11
- package/examples/server.mjs +10 -15
- package/package.json +9 -4
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { bearerRequest, isRecord, numberValue, requireAllowedHost, responseJson, stringArray, stringValue, } from "../utils.js";
|
|
2
3
|
const API_HOST = "opencode.ai";
|
|
3
4
|
const API_KEY_LIFETIME_MS = 365 * 24 * 60 * 60_000;
|
|
5
|
+
const DEFAULT_COMPATIBILITY_VERSION = "1.18.31";
|
|
6
|
+
const COMPATIBILITY_VERSION_TTL_MS = 60 * 60_000;
|
|
7
|
+
const ID_PATTERN = /^(ses|msg)_[0-9a-f]{12}[0-9A-Za-z]{14}$/;
|
|
8
|
+
const ID_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
4
9
|
function openCodeUsageMeter(id, label, raw) {
|
|
5
10
|
if (!isRecord(raw))
|
|
6
11
|
return null;
|
|
@@ -33,18 +38,53 @@ export function parseOpenCodeGoUsage(raw) {
|
|
|
33
38
|
note: "Provider-reported quota usage. API-equivalent costs shown in chats are estimates, not charges.",
|
|
34
39
|
};
|
|
35
40
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
41
|
+
function openCodeProvider(config, options = {}) {
|
|
42
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
43
|
+
let versionRequest;
|
|
44
|
+
let versionExpiresAt = 0;
|
|
45
|
+
const compatibilityVersion = () => {
|
|
46
|
+
if (options.compatibilityVersion)
|
|
47
|
+
return Promise.resolve(options.compatibilityVersion);
|
|
48
|
+
if (!versionRequest || Date.now() >= versionExpiresAt) {
|
|
49
|
+
versionExpiresAt = Date.now() + COMPATIBILITY_VERSION_TTL_MS;
|
|
50
|
+
versionRequest = fetcher("https://api.github.com/repos/anomalyco/opencode/releases/latest", {
|
|
51
|
+
headers: { accept: "application/vnd.github+json", "user-agent": "aisubs" },
|
|
52
|
+
})
|
|
53
|
+
.then(async (response) => {
|
|
54
|
+
if (!response.ok)
|
|
55
|
+
throw new Error(`OpenCode version lookup failed: ${response.status}`);
|
|
56
|
+
const raw = await response.json();
|
|
57
|
+
return isRecord(raw) ? stringValue(raw.tag_name)?.replace(/^v/, "") : undefined;
|
|
58
|
+
})
|
|
59
|
+
.then((version) => version || DEFAULT_COMPATIBILITY_VERSION)
|
|
60
|
+
.catch(() => DEFAULT_COMPATIBILITY_VERSION);
|
|
61
|
+
}
|
|
62
|
+
return versionRequest;
|
|
63
|
+
};
|
|
64
|
+
const routingIds = new Map();
|
|
65
|
+
let lastTimestamp = 0;
|
|
66
|
+
let counter = 0;
|
|
67
|
+
const identifier = (prefix, source) => {
|
|
68
|
+
if (source && ID_PATTERN.test(source) && source.startsWith(`${prefix}_`))
|
|
69
|
+
return source;
|
|
70
|
+
const key = source ? `${prefix}:${source}` : undefined;
|
|
71
|
+
const cached = key ? routingIds.get(key) : undefined;
|
|
72
|
+
if (cached)
|
|
73
|
+
return cached;
|
|
74
|
+
const timestamp = Date.now();
|
|
75
|
+
counter = timestamp === lastTimestamp ? counter + 1 : 1;
|
|
76
|
+
lastTimestamp = timestamp;
|
|
77
|
+
const encoded = BigInt(timestamp) * 0x1000n + BigInt(counter);
|
|
78
|
+
const time = Buffer.alloc(6);
|
|
79
|
+
for (let index = 0; index < time.length; index += 1) {
|
|
80
|
+
time[index] = Number((encoded >> BigInt(40 - 8 * index)) & 0xffn);
|
|
81
|
+
}
|
|
82
|
+
const random = [...randomBytes(14)].map((byte) => ID_CHARS[byte % ID_CHARS.length]).join("");
|
|
83
|
+
const value = `${prefix}_${time.toString("hex")}${random}`;
|
|
84
|
+
if (key)
|
|
85
|
+
routingIds.set(key, value);
|
|
86
|
+
return value;
|
|
87
|
+
};
|
|
48
88
|
const credential = (apiKey) => ({
|
|
49
89
|
accessToken: apiKey,
|
|
50
90
|
expiresAt: Date.now() + API_KEY_LIFETIME_MS,
|
|
@@ -79,9 +119,15 @@ function openCodeProvider(config) {
|
|
|
79
119
|
async refresh(current) {
|
|
80
120
|
return { ...current, expiresAt: Date.now() + API_KEY_LIFETIME_MS };
|
|
81
121
|
},
|
|
82
|
-
authorize(request, current) {
|
|
122
|
+
async authorize(request, current) {
|
|
83
123
|
requireAllowedHost(request, [API_HOST]);
|
|
84
|
-
|
|
124
|
+
const client = request.headers.get("x-opencode-client")?.trim() || "aisubs";
|
|
125
|
+
return bearerRequest(request, current, {
|
|
126
|
+
"user-agent": `opencode/latest/${await compatibilityVersion()}/${client}`,
|
|
127
|
+
"x-opencode-client": client,
|
|
128
|
+
"x-opencode-session": identifier("ses", request.headers.get("x-opencode-session")),
|
|
129
|
+
"x-opencode-request": identifier("msg", request.headers.get("x-opencode-request")),
|
|
130
|
+
});
|
|
85
131
|
},
|
|
86
132
|
async getModels({ fetch, signal }) {
|
|
87
133
|
const raw = await responseJson(await fetch(`${config.baseUrl}/models`, {
|
|
@@ -103,9 +149,7 @@ function openCodeProvider(config) {
|
|
|
103
149
|
id,
|
|
104
150
|
name: stringValue(value.name),
|
|
105
151
|
description: stringValue(value.description),
|
|
106
|
-
endpoints: stringArray(value.supported_endpoints) ??
|
|
107
|
-
stringArray(value.endpoints) ??
|
|
108
|
-
documentedEndpoints(config.id, id),
|
|
152
|
+
endpoints: stringArray(value.supported_endpoints) ?? stringArray(value.endpoints),
|
|
109
153
|
available: true,
|
|
110
154
|
selectable: true,
|
|
111
155
|
},
|
|
@@ -129,19 +173,19 @@ function openCodeProvider(config) {
|
|
|
129
173
|
},
|
|
130
174
|
};
|
|
131
175
|
}
|
|
132
|
-
export function openCodeGoProvider() {
|
|
176
|
+
export function openCodeGoProvider(options = {}) {
|
|
133
177
|
return openCodeProvider({
|
|
134
178
|
id: "opencode-go",
|
|
135
179
|
name: "OpenCode Go",
|
|
136
180
|
description: "OpenCode Go subscription access with an OpenCode API key.",
|
|
137
181
|
baseUrl: "https://opencode.ai/zen/go/v1",
|
|
138
|
-
});
|
|
182
|
+
}, options);
|
|
139
183
|
}
|
|
140
|
-
export function openCodeZenProvider() {
|
|
184
|
+
export function openCodeZenProvider(options = {}) {
|
|
141
185
|
return openCodeProvider({
|
|
142
186
|
id: "opencode-zen",
|
|
143
187
|
name: "OpenCode Zen",
|
|
144
188
|
description: "OpenCode Zen pay-as-you-go access with an OpenCode API key.",
|
|
145
189
|
baseUrl: "https://opencode.ai/zen/v1",
|
|
146
|
-
});
|
|
190
|
+
}, options);
|
|
147
191
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import type { CoordinatedCredentialStore, OAuthCredential, ProviderId, VersionedCredential } from "./types.js";
|
|
3
|
+
/** Node SQLite persistence. Pass a DatabaseSync to share a host-owned connection. */
|
|
4
|
+
export declare class SqliteCredentialStore implements CoordinatedCredentialStore {
|
|
5
|
+
private readonly database;
|
|
6
|
+
private readonly ownsConnection;
|
|
7
|
+
private readonly queues;
|
|
8
|
+
constructor(database: string | DatabaseSync);
|
|
9
|
+
read(provider: ProviderId): Promise<OAuthCredential | null>;
|
|
10
|
+
listKeys(): Promise<string[]>;
|
|
11
|
+
readVersioned(provider: ProviderId): Promise<VersionedCredential>;
|
|
12
|
+
replaceCredential(input: {
|
|
13
|
+
provider: ProviderId;
|
|
14
|
+
credential: OAuthCredential;
|
|
15
|
+
expectedGeneration: number;
|
|
16
|
+
operationId: string;
|
|
17
|
+
}): Promise<{
|
|
18
|
+
applied: boolean;
|
|
19
|
+
record: VersionedCredential;
|
|
20
|
+
}>;
|
|
21
|
+
deleteCredential(input: {
|
|
22
|
+
provider: ProviderId;
|
|
23
|
+
operationId: string;
|
|
24
|
+
}): Promise<void>;
|
|
25
|
+
claimRefresh(input: {
|
|
26
|
+
provider: ProviderId;
|
|
27
|
+
expectedVersion: number;
|
|
28
|
+
expectedGeneration: number;
|
|
29
|
+
claimId: string;
|
|
30
|
+
operationId: string;
|
|
31
|
+
}): Promise<"claimed" | "busy" | "changed" | "missing">;
|
|
32
|
+
commitRefresh(input: {
|
|
33
|
+
provider: ProviderId;
|
|
34
|
+
claimId: string;
|
|
35
|
+
expectedGeneration: number;
|
|
36
|
+
credential: OAuthCredential;
|
|
37
|
+
operationId: string;
|
|
38
|
+
}): Promise<{
|
|
39
|
+
applied: boolean;
|
|
40
|
+
record: VersionedCredential;
|
|
41
|
+
}>;
|
|
42
|
+
modify(provider: ProviderId, update: (current: OAuthCredential | null) => OAuthCredential | null | Promise<OAuthCredential | null>): Promise<OAuthCredential | null>;
|
|
43
|
+
delete(provider: ProviderId): Promise<void>;
|
|
44
|
+
close(): void;
|
|
45
|
+
private row;
|
|
46
|
+
private generation;
|
|
47
|
+
private setGeneration;
|
|
48
|
+
private writeCredential;
|
|
49
|
+
private versioned;
|
|
50
|
+
private operation;
|
|
51
|
+
private rememberOperation;
|
|
52
|
+
}
|
|
53
|
+
export declare class SqliteApiKeyStore {
|
|
54
|
+
private readonly database;
|
|
55
|
+
private readonly ownsConnection;
|
|
56
|
+
constructor(database: string | DatabaseSync);
|
|
57
|
+
readOrCreate(): Promise<string>;
|
|
58
|
+
regenerate(): Promise<string>;
|
|
59
|
+
close(): void;
|
|
60
|
+
private read;
|
|
61
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { mkdirSync } from "node:fs";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
/** Node SQLite persistence. Pass a DatabaseSync to share a host-owned connection. */
|
|
6
|
+
export class SqliteCredentialStore {
|
|
7
|
+
database;
|
|
8
|
+
ownsConnection;
|
|
9
|
+
queues = new Map();
|
|
10
|
+
constructor(database) {
|
|
11
|
+
if (typeof database === "string") {
|
|
12
|
+
mkdirSync(dirname(database), { recursive: true, mode: 0o700 });
|
|
13
|
+
this.database = new DatabaseSync(database, { timeout: 5_000 });
|
|
14
|
+
this.ownsConnection = true;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
this.database = database;
|
|
18
|
+
this.ownsConnection = false;
|
|
19
|
+
}
|
|
20
|
+
this.database.exec(`
|
|
21
|
+
PRAGMA foreign_keys = ON;
|
|
22
|
+
CREATE TABLE IF NOT EXISTS aisubs_credentials (
|
|
23
|
+
account_key TEXT PRIMARY KEY,
|
|
24
|
+
credential_json TEXT NOT NULL,
|
|
25
|
+
version INTEGER NOT NULL,
|
|
26
|
+
updated_at INTEGER NOT NULL
|
|
27
|
+
) STRICT;
|
|
28
|
+
CREATE TABLE IF NOT EXISTS aisubs_refresh_claims (
|
|
29
|
+
account_key TEXT PRIMARY KEY,
|
|
30
|
+
claim_id TEXT NOT NULL UNIQUE,
|
|
31
|
+
credential_version INTEGER NOT NULL,
|
|
32
|
+
account_generation INTEGER NOT NULL,
|
|
33
|
+
created_at INTEGER NOT NULL
|
|
34
|
+
) STRICT;
|
|
35
|
+
CREATE TABLE IF NOT EXISTS aisubs_account_generations (
|
|
36
|
+
account_key TEXT PRIMARY KEY,
|
|
37
|
+
generation INTEGER NOT NULL,
|
|
38
|
+
updated_at INTEGER NOT NULL
|
|
39
|
+
) STRICT;
|
|
40
|
+
CREATE TABLE IF NOT EXISTS aisubs_operations (
|
|
41
|
+
operation_id TEXT PRIMARY KEY,
|
|
42
|
+
result_json TEXT NOT NULL,
|
|
43
|
+
created_at INTEGER NOT NULL
|
|
44
|
+
) STRICT;
|
|
45
|
+
CREATE TABLE IF NOT EXISTS aisubs_api_keys (
|
|
46
|
+
name TEXT PRIMARY KEY,
|
|
47
|
+
value TEXT NOT NULL,
|
|
48
|
+
updated_at INTEGER NOT NULL
|
|
49
|
+
) STRICT;
|
|
50
|
+
`);
|
|
51
|
+
}
|
|
52
|
+
async read(provider) {
|
|
53
|
+
const row = this.row(provider);
|
|
54
|
+
return row ? JSON.parse(row.credential_json) : null;
|
|
55
|
+
}
|
|
56
|
+
async listKeys() {
|
|
57
|
+
return this.database.prepare("SELECT account_key FROM aisubs_credentials ORDER BY rowid").all().map(({ account_key }) => account_key);
|
|
58
|
+
}
|
|
59
|
+
async readVersioned(provider) {
|
|
60
|
+
const row = this.row(provider);
|
|
61
|
+
return {
|
|
62
|
+
credential: row ? JSON.parse(row.credential_json) : null,
|
|
63
|
+
version: row?.version ?? 0,
|
|
64
|
+
generation: this.generation(provider),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async replaceCredential(input) {
|
|
68
|
+
const replay = this.operation(input.operationId);
|
|
69
|
+
if (replay)
|
|
70
|
+
return replay;
|
|
71
|
+
this.database.exec("BEGIN IMMEDIATE");
|
|
72
|
+
try {
|
|
73
|
+
const generation = this.generation(input.provider);
|
|
74
|
+
if (generation !== input.expectedGeneration) {
|
|
75
|
+
const result = { applied: false, record: this.versioned(input.provider) };
|
|
76
|
+
this.rememberOperation(input.operationId, result);
|
|
77
|
+
this.database.exec("COMMIT");
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
this.writeCredential(input.provider, input.credential);
|
|
81
|
+
this.setGeneration(input.provider, generation + 1);
|
|
82
|
+
this.database
|
|
83
|
+
.prepare("DELETE FROM aisubs_refresh_claims WHERE account_key = ?")
|
|
84
|
+
.run(input.provider);
|
|
85
|
+
const result = { applied: true, record: this.versioned(input.provider) };
|
|
86
|
+
this.rememberOperation(input.operationId, result);
|
|
87
|
+
this.database.exec("COMMIT");
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
this.database.exec("ROLLBACK");
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async deleteCredential(input) {
|
|
96
|
+
if (this.operation(input.operationId))
|
|
97
|
+
return;
|
|
98
|
+
this.database.exec("BEGIN IMMEDIATE");
|
|
99
|
+
try {
|
|
100
|
+
this.database
|
|
101
|
+
.prepare("DELETE FROM aisubs_credentials WHERE account_key = ?")
|
|
102
|
+
.run(input.provider);
|
|
103
|
+
this.database
|
|
104
|
+
.prepare("DELETE FROM aisubs_refresh_claims WHERE account_key = ?")
|
|
105
|
+
.run(input.provider);
|
|
106
|
+
this.setGeneration(input.provider, this.generation(input.provider) + 1);
|
|
107
|
+
this.rememberOperation(input.operationId, { applied: true });
|
|
108
|
+
this.database.exec("COMMIT");
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
this.database.exec("ROLLBACK");
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async claimRefresh(input) {
|
|
116
|
+
const replay = this.operation(input.operationId);
|
|
117
|
+
if (replay?.value)
|
|
118
|
+
return replay.value;
|
|
119
|
+
this.database.exec("BEGIN IMMEDIATE");
|
|
120
|
+
try {
|
|
121
|
+
const row = this.row(input.provider);
|
|
122
|
+
const generation = this.generation(input.provider);
|
|
123
|
+
const value = !row
|
|
124
|
+
? "missing"
|
|
125
|
+
: row.version !== input.expectedVersion || generation !== input.expectedGeneration
|
|
126
|
+
? "changed"
|
|
127
|
+
: this.database
|
|
128
|
+
.prepare("SELECT 1 FROM aisubs_refresh_claims WHERE account_key = ?")
|
|
129
|
+
.get(input.provider)
|
|
130
|
+
? "busy"
|
|
131
|
+
: "claimed";
|
|
132
|
+
if (value === "claimed") {
|
|
133
|
+
this.database
|
|
134
|
+
.prepare(`INSERT INTO aisubs_refresh_claims(account_key, claim_id, credential_version, account_generation, created_at)
|
|
135
|
+
VALUES(?, ?, ?, ?, ?)`)
|
|
136
|
+
.run(input.provider, input.claimId, input.expectedVersion, input.expectedGeneration, Date.now());
|
|
137
|
+
}
|
|
138
|
+
this.rememberOperation(input.operationId, { value });
|
|
139
|
+
this.database.exec("COMMIT");
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
this.database.exec("ROLLBACK");
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async commitRefresh(input) {
|
|
148
|
+
const replay = this.operation(input.operationId);
|
|
149
|
+
if (replay)
|
|
150
|
+
return replay;
|
|
151
|
+
this.database.exec("BEGIN IMMEDIATE");
|
|
152
|
+
try {
|
|
153
|
+
const claim = this.database
|
|
154
|
+
.prepare(`SELECT account_generation FROM aisubs_refresh_claims
|
|
155
|
+
WHERE account_key = ? AND claim_id = ?`)
|
|
156
|
+
.get(input.provider, input.claimId);
|
|
157
|
+
const applied = Boolean(claim &&
|
|
158
|
+
claim.account_generation === input.expectedGeneration &&
|
|
159
|
+
this.generation(input.provider) === input.expectedGeneration);
|
|
160
|
+
if (applied) {
|
|
161
|
+
this.writeCredential(input.provider, input.credential);
|
|
162
|
+
this.database
|
|
163
|
+
.prepare("DELETE FROM aisubs_refresh_claims WHERE account_key = ? AND claim_id = ?")
|
|
164
|
+
.run(input.provider, input.claimId);
|
|
165
|
+
}
|
|
166
|
+
const result = { applied, record: this.versioned(input.provider) };
|
|
167
|
+
this.rememberOperation(input.operationId, result);
|
|
168
|
+
this.database.exec("COMMIT");
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
this.database.exec("ROLLBACK");
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async modify(provider, update) {
|
|
177
|
+
const previous = this.queues.get(provider) ?? Promise.resolve();
|
|
178
|
+
let result = null;
|
|
179
|
+
const current = previous.then(async () => {
|
|
180
|
+
for (;;) {
|
|
181
|
+
const observed = this.row(provider);
|
|
182
|
+
result = await update(observed ? JSON.parse(observed.credential_json) : null);
|
|
183
|
+
this.database.exec("BEGIN IMMEDIATE");
|
|
184
|
+
try {
|
|
185
|
+
const latest = this.row(provider);
|
|
186
|
+
if ((latest?.version ?? 0) !== (observed?.version ?? 0)) {
|
|
187
|
+
this.database.exec("ROLLBACK");
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (result) {
|
|
191
|
+
this.database
|
|
192
|
+
.prepare(`INSERT INTO aisubs_credentials(account_key, credential_json, version, updated_at)
|
|
193
|
+
VALUES(?, ?, 1, ?)
|
|
194
|
+
ON CONFLICT(account_key) DO UPDATE SET credential_json = excluded.credential_json,
|
|
195
|
+
version = aisubs_credentials.version + 1, updated_at = excluded.updated_at`)
|
|
196
|
+
.run(provider, JSON.stringify(result), Date.now());
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
this.database
|
|
200
|
+
.prepare("DELETE FROM aisubs_credentials WHERE account_key = ?")
|
|
201
|
+
.run(provider);
|
|
202
|
+
}
|
|
203
|
+
this.database.exec("COMMIT");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
this.database.exec("ROLLBACK");
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
const settled = current.catch(() => { });
|
|
213
|
+
this.queues.set(provider, settled);
|
|
214
|
+
try {
|
|
215
|
+
await current;
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
if (this.queues.get(provider) === settled)
|
|
219
|
+
this.queues.delete(provider);
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
async delete(provider) {
|
|
224
|
+
await this.modify(provider, () => null);
|
|
225
|
+
}
|
|
226
|
+
close() {
|
|
227
|
+
if (this.ownsConnection)
|
|
228
|
+
this.database.close();
|
|
229
|
+
}
|
|
230
|
+
row(provider) {
|
|
231
|
+
return this.database
|
|
232
|
+
.prepare("SELECT credential_json, version FROM aisubs_credentials WHERE account_key = ?")
|
|
233
|
+
.get(provider);
|
|
234
|
+
}
|
|
235
|
+
generation(provider) {
|
|
236
|
+
const row = this.database
|
|
237
|
+
.prepare("SELECT generation FROM aisubs_account_generations WHERE account_key = ?")
|
|
238
|
+
.get(provider);
|
|
239
|
+
return row?.generation ?? 0;
|
|
240
|
+
}
|
|
241
|
+
setGeneration(provider, generation) {
|
|
242
|
+
this.database
|
|
243
|
+
.prepare(`INSERT INTO aisubs_account_generations(account_key, generation, updated_at) VALUES(?, ?, ?)
|
|
244
|
+
ON CONFLICT(account_key) DO UPDATE SET generation = excluded.generation, updated_at = excluded.updated_at`)
|
|
245
|
+
.run(provider, generation, Date.now());
|
|
246
|
+
}
|
|
247
|
+
writeCredential(provider, credential) {
|
|
248
|
+
this.database
|
|
249
|
+
.prepare(`INSERT INTO aisubs_credentials(account_key, credential_json, version, updated_at)
|
|
250
|
+
VALUES(?, ?, 1, ?)
|
|
251
|
+
ON CONFLICT(account_key) DO UPDATE SET credential_json = excluded.credential_json,
|
|
252
|
+
version = aisubs_credentials.version + 1, updated_at = excluded.updated_at`)
|
|
253
|
+
.run(provider, JSON.stringify(credential), Date.now());
|
|
254
|
+
}
|
|
255
|
+
versioned(provider) {
|
|
256
|
+
const row = this.row(provider);
|
|
257
|
+
return {
|
|
258
|
+
credential: row ? JSON.parse(row.credential_json) : null,
|
|
259
|
+
version: row?.version ?? 0,
|
|
260
|
+
generation: this.generation(provider),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
operation(operationId) {
|
|
264
|
+
const row = this.database
|
|
265
|
+
.prepare("SELECT result_json FROM aisubs_operations WHERE operation_id = ?")
|
|
266
|
+
.get(operationId);
|
|
267
|
+
return row ? JSON.parse(row.result_json) : undefined;
|
|
268
|
+
}
|
|
269
|
+
rememberOperation(operationId, result) {
|
|
270
|
+
this.database
|
|
271
|
+
.prepare("INSERT OR IGNORE INTO aisubs_operations(operation_id, result_json, created_at) VALUES(?, ?, ?)")
|
|
272
|
+
.run(operationId, JSON.stringify(result), Date.now());
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
export class SqliteApiKeyStore {
|
|
276
|
+
database;
|
|
277
|
+
ownsConnection;
|
|
278
|
+
constructor(database) {
|
|
279
|
+
if (typeof database === "string") {
|
|
280
|
+
mkdirSync(dirname(database), { recursive: true, mode: 0o700 });
|
|
281
|
+
this.database = new DatabaseSync(database, { timeout: 5_000 });
|
|
282
|
+
this.ownsConnection = true;
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
this.database = database;
|
|
286
|
+
this.ownsConnection = false;
|
|
287
|
+
}
|
|
288
|
+
this.database.exec(`CREATE TABLE IF NOT EXISTS aisubs_api_keys (
|
|
289
|
+
name TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL
|
|
290
|
+
) STRICT;`);
|
|
291
|
+
}
|
|
292
|
+
async readOrCreate() {
|
|
293
|
+
const existing = this.read();
|
|
294
|
+
if (existing)
|
|
295
|
+
return existing;
|
|
296
|
+
const value = apiKey();
|
|
297
|
+
this.database
|
|
298
|
+
.prepare("INSERT OR IGNORE INTO aisubs_api_keys(name, value, updated_at) VALUES('default', ?, ?)")
|
|
299
|
+
.run(value, Date.now());
|
|
300
|
+
return this.read() ?? value;
|
|
301
|
+
}
|
|
302
|
+
async regenerate() {
|
|
303
|
+
const value = apiKey();
|
|
304
|
+
this.database
|
|
305
|
+
.prepare(`INSERT INTO aisubs_api_keys(name, value, updated_at) VALUES('default', ?, ?)
|
|
306
|
+
ON CONFLICT(name) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`)
|
|
307
|
+
.run(value, Date.now());
|
|
308
|
+
return value;
|
|
309
|
+
}
|
|
310
|
+
close() {
|
|
311
|
+
if (this.ownsConnection)
|
|
312
|
+
this.database.close();
|
|
313
|
+
}
|
|
314
|
+
read() {
|
|
315
|
+
const row = this.database
|
|
316
|
+
.prepare("SELECT value FROM aisubs_api_keys WHERE name = 'default'")
|
|
317
|
+
.get();
|
|
318
|
+
return row?.value ?? null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function apiKey() {
|
|
322
|
+
return `aisubs_${randomBytes(32).toString("base64url")}_${randomUUID().slice(0, 8)}`;
|
|
323
|
+
}
|
package/dist/store.d.ts
CHANGED
|
@@ -1,24 +1 @@
|
|
|
1
|
-
import type { CredentialStore, OAuthCredential, ProviderId } from "./types.js";
|
|
2
1
|
export declare function defaultAiSubsDataDir(): string;
|
|
3
|
-
export declare class FileApiKeyStore {
|
|
4
|
-
readonly file: string;
|
|
5
|
-
constructor(file: string);
|
|
6
|
-
readOrCreate(): Promise<string>;
|
|
7
|
-
regenerate(): Promise<string>;
|
|
8
|
-
}
|
|
9
|
-
export declare class FileCredentialStore implements CredentialStore {
|
|
10
|
-
readonly file: string;
|
|
11
|
-
constructor(file: string);
|
|
12
|
-
read(provider: ProviderId): Promise<OAuthCredential | null>;
|
|
13
|
-
listKeys(): Promise<string[]>;
|
|
14
|
-
modify(provider: ProviderId, update: (current: OAuthCredential | null) => OAuthCredential | null | Promise<OAuthCredential | null>): Promise<OAuthCredential | null>;
|
|
15
|
-
delete(provider: ProviderId): Promise<void>;
|
|
16
|
-
}
|
|
17
|
-
export declare class MemoryCredentialStore implements CredentialStore {
|
|
18
|
-
private readonly values;
|
|
19
|
-
private readonly queues;
|
|
20
|
-
read(provider: ProviderId): Promise<OAuthCredential | null>;
|
|
21
|
-
listKeys(): Promise<string[]>;
|
|
22
|
-
modify(provider: ProviderId, update: (current: OAuthCredential | null) => OAuthCredential | null | Promise<OAuthCredential | null>): Promise<OAuthCredential | null>;
|
|
23
|
-
delete(provider: ProviderId): Promise<void>;
|
|
24
|
-
}
|