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
package/dist/store.js
CHANGED
|
@@ -1,203 +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 = Object.create(null);
|
|
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 Object.create(null);
|
|
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
|
-
const settled = current.catch(() => { });
|
|
190
|
-
this.queues.set(provider, settled);
|
|
191
|
-
try {
|
|
192
|
-
await current;
|
|
193
|
-
}
|
|
194
|
-
finally {
|
|
195
|
-
if (this.queues.get(provider) === settled)
|
|
196
|
-
this.queues.delete(provider);
|
|
197
|
-
}
|
|
198
|
-
return result;
|
|
199
|
-
}
|
|
200
|
-
async delete(provider) {
|
|
201
|
-
await this.modify(provider, () => null);
|
|
202
|
-
}
|
|
203
|
-
}
|
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;
|
package/dist/utils.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
2
1
|
export function isRecord(value) {
|
|
3
2
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
3
|
}
|
|
@@ -18,12 +17,21 @@ export function errorMessage(error) {
|
|
|
18
17
|
return error instanceof Error ? error.message : String(error);
|
|
19
18
|
}
|
|
20
19
|
export async function abortableDelay(ms, signal) {
|
|
21
|
-
|
|
22
|
-
await delay(ms, undefined, { signal });
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
20
|
+
if (signal?.aborted)
|
|
25
21
|
throw new Error("Login cancelled");
|
|
26
|
-
|
|
22
|
+
await new Promise((resolve, reject) => {
|
|
23
|
+
const abort = () => {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
reject(new Error("Login cancelled"));
|
|
26
|
+
};
|
|
27
|
+
const timer = setTimeout(() => {
|
|
28
|
+
signal?.removeEventListener("abort", abort);
|
|
29
|
+
resolve();
|
|
30
|
+
}, ms);
|
|
31
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
32
|
+
if (signal?.aborted)
|
|
33
|
+
abort();
|
|
34
|
+
});
|
|
27
35
|
}
|
|
28
36
|
export async function responseJson(response, label) {
|
|
29
37
|
const raw = await response.json().catch(() => null);
|
package/examples/direct.mjs
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
openCodeGoProvider,
|
|
11
|
-
openCodeZenProvider,
|
|
12
|
-
} from "aisubs";
|
|
3
|
+
import { createSubscriptionAuth } from "aisubs";
|
|
4
|
+
import { SqliteCredentialStore } from "aisubs/node";
|
|
5
|
+
import { chatGptProvider } from "aisubs/providers/chatgpt";
|
|
6
|
+
import { claudeProvider } from "aisubs/providers/claude";
|
|
7
|
+
import { copilotProvider } from "aisubs/providers/copilot";
|
|
8
|
+
import { grokProvider } from "aisubs/providers/grok";
|
|
9
|
+
import { openCodeGoProvider, openCodeZenProvider } from "aisubs/providers/opencode";
|
|
13
10
|
|
|
14
11
|
const provider = process.argv[2] ?? "chatgpt";
|
|
15
12
|
const accountKey = process.argv[3] ?? "default";
|
|
@@ -26,7 +23,7 @@ if (!providers.some((candidate) => candidate.id === provider)) {
|
|
|
26
23
|
}
|
|
27
24
|
|
|
28
25
|
const auth = createSubscriptionAuth({
|
|
29
|
-
store: new
|
|
26
|
+
store: new SqliteCredentialStore(join(homedir(), ".aisubs-demo", "aisubs.db")),
|
|
30
27
|
providers,
|
|
31
28
|
});
|
|
32
29
|
const account = auth.account(provider, accountKey);
|
package/examples/server.mjs
CHANGED
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
FileCredentialStore,
|
|
5
|
-
FileApiKeyStore,
|
|
6
|
-
chatGptProvider,
|
|
7
|
-
claudeProvider,
|
|
8
|
-
copilotProvider,
|
|
9
|
-
createSubscriptionAuth,
|
|
10
|
-
grokProvider,
|
|
11
|
-
openCodeGoProvider,
|
|
12
|
-
openCodeZenProvider,
|
|
13
|
-
} from "aisubs";
|
|
3
|
+
import { createSubscriptionAuth } from "aisubs";
|
|
14
4
|
import { createSubscriptionAuthServer } from "aisubs/http";
|
|
5
|
+
import { SqliteApiKeyStore, SqliteCredentialStore } from "aisubs/node";
|
|
6
|
+
import { chatGptProvider } from "aisubs/providers/chatgpt";
|
|
7
|
+
import { claudeProvider } from "aisubs/providers/claude";
|
|
8
|
+
import { copilotProvider } from "aisubs/providers/copilot";
|
|
9
|
+
import { grokProvider } from "aisubs/providers/grok";
|
|
10
|
+
import { openCodeGoProvider, openCodeZenProvider } from "aisubs/providers/opencode";
|
|
15
11
|
|
|
16
12
|
const directory = join(homedir(), ".aisubs-demo");
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
(await new FileApiKeyStore(join(directory, "api-key")).readOrCreate());
|
|
13
|
+
const database = join(directory, "aisubs.db");
|
|
14
|
+
const apiKey = process.env.AISUBS_API_KEY ?? (await new SqliteApiKeyStore(database).readOrCreate());
|
|
20
15
|
const auth = createSubscriptionAuth({
|
|
21
|
-
store: new
|
|
16
|
+
store: new SqliteCredentialStore(database),
|
|
22
17
|
providers: [
|
|
23
18
|
chatGptProvider(),
|
|
24
19
|
claudeProvider(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aisubs",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "Connect AI provider accounts and use those subscriptions from any local tool or as api.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -59,6 +59,10 @@
|
|
|
59
59
|
"types": "./dist/dashboard.d.ts",
|
|
60
60
|
"import": "./dist/dashboard.js"
|
|
61
61
|
},
|
|
62
|
+
"./node": {
|
|
63
|
+
"types": "./dist/node.d.ts",
|
|
64
|
+
"import": "./dist/node.js"
|
|
65
|
+
},
|
|
62
66
|
"./providers/chatgpt": {
|
|
63
67
|
"types": "./dist/providers/chatgpt.d.ts",
|
|
64
68
|
"import": "./dist/providers/chatgpt.js"
|
|
@@ -126,11 +130,12 @@
|
|
|
126
130
|
"devEngines": {
|
|
127
131
|
"packageManager": {
|
|
128
132
|
"name": "nub",
|
|
129
|
-
"version": "^0.
|
|
130
|
-
"onFail": "
|
|
133
|
+
"version": "^0.9.2",
|
|
134
|
+
"onFail": "ignore"
|
|
131
135
|
}
|
|
132
136
|
},
|
|
133
137
|
"engines": {
|
|
134
138
|
"node": ">=24"
|
|
135
|
-
}
|
|
139
|
+
},
|
|
140
|
+
"packageManager": "nub@0.9.2"
|
|
136
141
|
}
|