aisubs 0.3.6 → 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 +14 -0
- package/README.md +29 -29
- package/dist/auth.d.ts +4 -1
- package/dist/auth.js +143 -25
- package/dist/cli.js +5 -3
- package/dist/compatibility.js +157 -137
- package/dist/dashboard/assets/index-CePTUw7Z.css +2 -0
- package/dist/dashboard/assets/index-Vscu32Dz.js +84 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard.js +15 -26
- package/dist/http.d.ts +3 -0
- package/dist/http.js +13 -31
- 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/proxy-headers.d.ts +2 -0
- package/dist/proxy-headers.js +35 -0
- package/dist/realtime.js +6 -20
- 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 -191
- package/dist/types.d.ts +40 -0
- package/dist/utils.js +15 -7
- package/examples/direct.mjs +8 -11
- package/examples/server.mjs +10 -15
- package/package.json +15 -6
- package/dist/dashboard/assets/index-BJDbjHnw.css +0 -2
- package/dist/dashboard/assets/index-BbN84qld.js +0 -84
|
@@ -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
|
-
}
|
package/dist/store.js
CHANGED
|
@@ -1,196 +1,6 @@
|
|
|
1
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import { chmod, link, mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
1
|
import { homedir } from "node:os";
|
|
4
|
-
import {
|
|
5
|
-
import { abortableDelay, isRecord } from "./utils.js";
|
|
6
|
-
const LOCK_TIMEOUT_MS = 15_000;
|
|
7
|
-
const LOCK_STALE_MS = 120_000;
|
|
8
|
-
function isCredential(value) {
|
|
9
|
-
if (!isRecord(value) || typeof value.accessToken !== "string")
|
|
10
|
-
return false;
|
|
11
|
-
if (typeof value.expiresAt !== "number" || !Number.isFinite(value.expiresAt))
|
|
12
|
-
return false;
|
|
13
|
-
if (value.refreshToken != null && typeof value.refreshToken !== "string")
|
|
14
|
-
return false;
|
|
15
|
-
if (value.account != null &&
|
|
16
|
-
(!isRecord(value.account) ||
|
|
17
|
-
![value.account.id, value.account.label, value.account.email, value.account.plan].every((item) => item == null || typeof item === "string")))
|
|
18
|
-
return false;
|
|
19
|
-
return (value.metadata == null ||
|
|
20
|
-
(isRecord(value.metadata) &&
|
|
21
|
-
Object.values(value.metadata).every((item) => item == null ||
|
|
22
|
-
typeof item === "string" ||
|
|
23
|
-
typeof item === "number" ||
|
|
24
|
-
typeof item === "boolean")));
|
|
25
|
-
}
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
26
3
|
export function defaultAiSubsDataDir() {
|
|
27
4
|
const override = process.env.AISUBS_DATA_DIR?.trim();
|
|
28
5
|
return override ? resolve(override) : join(homedir(), ".aisubs");
|
|
29
6
|
}
|
|
30
|
-
export class FileApiKeyStore {
|
|
31
|
-
file;
|
|
32
|
-
constructor(file) {
|
|
33
|
-
this.file = file;
|
|
34
|
-
}
|
|
35
|
-
async readOrCreate() {
|
|
36
|
-
try {
|
|
37
|
-
const value = (await readFile(this.file, "utf8")).trim();
|
|
38
|
-
if (value)
|
|
39
|
-
return value;
|
|
40
|
-
}
|
|
41
|
-
catch (error) {
|
|
42
|
-
if (error.code !== "ENOENT")
|
|
43
|
-
throw error;
|
|
44
|
-
}
|
|
45
|
-
const value = `aisubs_${randomBytes(32).toString("base64url")}`;
|
|
46
|
-
const temp = `${this.file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
47
|
-
await mkdir(dirname(this.file), { recursive: true, mode: 0o700 });
|
|
48
|
-
try {
|
|
49
|
-
await writeFile(temp, `${value}\n`, { mode: 0o600 });
|
|
50
|
-
try {
|
|
51
|
-
await link(temp, this.file);
|
|
52
|
-
return value;
|
|
53
|
-
}
|
|
54
|
-
catch (error) {
|
|
55
|
-
if (error.code !== "EEXIST")
|
|
56
|
-
throw error;
|
|
57
|
-
const existing = (await readFile(this.file, "utf8")).trim();
|
|
58
|
-
return existing || this.regenerate();
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
finally {
|
|
62
|
-
await rm(temp, { force: true }).catch(() => { });
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
async regenerate() {
|
|
66
|
-
const value = `aisubs_${randomBytes(32).toString("base64url")}`;
|
|
67
|
-
const temp = `${this.file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
68
|
-
await mkdir(dirname(this.file), { recursive: true, mode: 0o700 });
|
|
69
|
-
try {
|
|
70
|
-
await writeFile(temp, `${value}\n`, { mode: 0o600 });
|
|
71
|
-
await rename(temp, this.file);
|
|
72
|
-
return value;
|
|
73
|
-
}
|
|
74
|
-
finally {
|
|
75
|
-
await rm(temp, { force: true }).catch(() => { });
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
async function readEnvelope(file) {
|
|
80
|
-
try {
|
|
81
|
-
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
82
|
-
if (!isRecord(parsed))
|
|
83
|
-
throw new Error("Credential store must contain a JSON object");
|
|
84
|
-
const credentials = {};
|
|
85
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
86
|
-
if (!isCredential(value))
|
|
87
|
-
throw new Error(`Credential store entry ${key} is invalid`);
|
|
88
|
-
credentials[key] = value;
|
|
89
|
-
}
|
|
90
|
-
return credentials;
|
|
91
|
-
}
|
|
92
|
-
catch (error) {
|
|
93
|
-
if (error.code === "ENOENT")
|
|
94
|
-
return {};
|
|
95
|
-
throw error;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
async function withFileLock(path, task) {
|
|
99
|
-
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
100
|
-
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
101
|
-
let handle;
|
|
102
|
-
while (!handle) {
|
|
103
|
-
try {
|
|
104
|
-
handle = await open(path, "wx", 0o600);
|
|
105
|
-
}
|
|
106
|
-
catch (error) {
|
|
107
|
-
if (error.code !== "EEXIST")
|
|
108
|
-
throw error;
|
|
109
|
-
try {
|
|
110
|
-
if (Date.now() - (await stat(path)).mtimeMs > LOCK_STALE_MS)
|
|
111
|
-
await rm(path, { force: true });
|
|
112
|
-
}
|
|
113
|
-
catch { }
|
|
114
|
-
if (Date.now() >= deadline)
|
|
115
|
-
throw new Error("Timed out waiting for credential-store lock");
|
|
116
|
-
await abortableDelay(25);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
try {
|
|
120
|
-
return await task();
|
|
121
|
-
}
|
|
122
|
-
finally {
|
|
123
|
-
await handle.close().catch(() => { });
|
|
124
|
-
await rm(path, { force: true }).catch(() => { });
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
async function writeEnvelope(file, value) {
|
|
128
|
-
const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
129
|
-
await mkdir(dirname(file), { recursive: true, mode: 0o700 });
|
|
130
|
-
try {
|
|
131
|
-
await writeFile(temp, JSON.stringify(value, null, 2), { mode: 0o600 });
|
|
132
|
-
await rename(temp, file);
|
|
133
|
-
if (process.platform !== "win32")
|
|
134
|
-
await chmod(file, 0o600);
|
|
135
|
-
}
|
|
136
|
-
finally {
|
|
137
|
-
await rm(temp, { force: true }).catch(() => { });
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
export class FileCredentialStore {
|
|
141
|
-
file;
|
|
142
|
-
constructor(file) {
|
|
143
|
-
this.file = file;
|
|
144
|
-
}
|
|
145
|
-
async read(provider) {
|
|
146
|
-
return (await readEnvelope(this.file))[provider] ?? null;
|
|
147
|
-
}
|
|
148
|
-
async listKeys() {
|
|
149
|
-
return Object.keys(await readEnvelope(this.file));
|
|
150
|
-
}
|
|
151
|
-
async modify(provider, update) {
|
|
152
|
-
const lockId = createHash("sha256").update(provider).digest("hex");
|
|
153
|
-
return withFileLock(join(dirname(this.file), `${lockId}.credential.lock`), async () => {
|
|
154
|
-
const next = await update(await this.read(provider));
|
|
155
|
-
await withFileLock(`${this.file}.lock`, async () => {
|
|
156
|
-
const all = await readEnvelope(this.file);
|
|
157
|
-
if (next)
|
|
158
|
-
all[provider] = next;
|
|
159
|
-
else
|
|
160
|
-
delete all[provider];
|
|
161
|
-
await writeEnvelope(this.file, all);
|
|
162
|
-
});
|
|
163
|
-
return next;
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
async delete(provider) {
|
|
167
|
-
await this.modify(provider, () => null);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
export class MemoryCredentialStore {
|
|
171
|
-
values = new Map();
|
|
172
|
-
queues = new Map();
|
|
173
|
-
async read(provider) {
|
|
174
|
-
return this.values.get(provider) ?? null;
|
|
175
|
-
}
|
|
176
|
-
async listKeys() {
|
|
177
|
-
return [...this.values.keys()];
|
|
178
|
-
}
|
|
179
|
-
async modify(provider, update) {
|
|
180
|
-
const previous = this.queues.get(provider) ?? Promise.resolve();
|
|
181
|
-
let result = null;
|
|
182
|
-
const current = previous.then(async () => {
|
|
183
|
-
result = await update(this.values.get(provider) ?? null);
|
|
184
|
-
if (result)
|
|
185
|
-
this.values.set(provider, result);
|
|
186
|
-
else
|
|
187
|
-
this.values.delete(provider);
|
|
188
|
-
});
|
|
189
|
-
this.queues.set(provider, current.catch(() => { }));
|
|
190
|
-
await current;
|
|
191
|
-
return result;
|
|
192
|
-
}
|
|
193
|
-
async delete(provider) {
|
|
194
|
-
await this.modify(provider, () => null);
|
|
195
|
-
}
|
|
196
|
-
}
|
package/dist/types.d.ts
CHANGED
|
@@ -18,6 +18,45 @@ export interface CredentialStore {
|
|
|
18
18
|
modify(provider: ProviderId, update: (current: OAuthCredential | null) => OAuthCredential | null | Promise<OAuthCredential | null>): Promise<OAuthCredential | null>;
|
|
19
19
|
delete(provider: ProviderId): Promise<void>;
|
|
20
20
|
}
|
|
21
|
+
export interface VersionedCredential {
|
|
22
|
+
credential: OAuthCredential | null;
|
|
23
|
+
version: number;
|
|
24
|
+
generation: number;
|
|
25
|
+
}
|
|
26
|
+
/** Structured, remote-safe credential mutations used by hosted/shared stores. */
|
|
27
|
+
export interface CoordinatedCredentialStore extends CredentialStore {
|
|
28
|
+
readVersioned(provider: ProviderId): Promise<VersionedCredential>;
|
|
29
|
+
replaceCredential(input: {
|
|
30
|
+
provider: ProviderId;
|
|
31
|
+
credential: OAuthCredential;
|
|
32
|
+
expectedGeneration: number;
|
|
33
|
+
operationId: string;
|
|
34
|
+
}): Promise<{
|
|
35
|
+
applied: boolean;
|
|
36
|
+
record: VersionedCredential;
|
|
37
|
+
}>;
|
|
38
|
+
deleteCredential(input: {
|
|
39
|
+
provider: ProviderId;
|
|
40
|
+
operationId: string;
|
|
41
|
+
}): Promise<void>;
|
|
42
|
+
claimRefresh(input: {
|
|
43
|
+
provider: ProviderId;
|
|
44
|
+
expectedVersion: number;
|
|
45
|
+
expectedGeneration: number;
|
|
46
|
+
claimId: string;
|
|
47
|
+
operationId: string;
|
|
48
|
+
}): Promise<"claimed" | "busy" | "changed" | "missing">;
|
|
49
|
+
commitRefresh(input: {
|
|
50
|
+
provider: ProviderId;
|
|
51
|
+
claimId: string;
|
|
52
|
+
expectedGeneration: number;
|
|
53
|
+
credential: OAuthCredential;
|
|
54
|
+
operationId: string;
|
|
55
|
+
}): Promise<{
|
|
56
|
+
applied: boolean;
|
|
57
|
+
record: VersionedCredential;
|
|
58
|
+
}>;
|
|
59
|
+
}
|
|
21
60
|
export interface UsageMeter {
|
|
22
61
|
id: string;
|
|
23
62
|
label: string;
|
|
@@ -72,6 +111,7 @@ export interface ProviderModel {
|
|
|
72
111
|
maxOutputTokens?: number;
|
|
73
112
|
reasoningEfforts?: string[];
|
|
74
113
|
inputModalities?: string[];
|
|
114
|
+
outputModalities?: string[];
|
|
75
115
|
endpoints?: string[];
|
|
76
116
|
supportsToolCall?: boolean;
|
|
77
117
|
available?: boolean;
|