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/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 { dirname, join, resolve } from "node:path";
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
- try {
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);
@@ -1,15 +1,12 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
- import {
4
- FileCredentialStore,
5
- chatGptProvider,
6
- claudeProvider,
7
- copilotProvider,
8
- createSubscriptionAuth,
9
- grokProvider,
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 FileCredentialStore(join(homedir(), ".aisubs-demo", "credentials.json")),
26
+ store: new SqliteCredentialStore(join(homedir(), ".aisubs-demo", "aisubs.db")),
30
27
  providers,
31
28
  });
32
29
  const account = auth.account(provider, accountKey);
@@ -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 apiKey =
18
- process.env.AISUBS_API_KEY ??
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 FileCredentialStore(join(directory, "credentials.json")),
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.7",
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.6.0",
130
- "onFail": "warn"
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
  }