dsh-claude-subscription 0.3.1 → 0.4.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.
@@ -0,0 +1,20 @@
1
+ function isDisplayableWindow(window) {
2
+ return (Number.isFinite(window?.remainingPercent) &&
3
+ window.remainingPercent >= 0 &&
4
+ window.remainingPercent <= 100);
5
+ }
6
+ /** Pick the tightest API-reported quota window that applies to the model. */
7
+ export function selectModelQuota(usage, model) {
8
+ const windows = Array.isArray(usage?.windows)
9
+ ? usage.windows.filter((window) => (window.appliesTo === "all" || window.appliesTo === model) &&
10
+ isDisplayableWindow(window))
11
+ : [];
12
+ if (windows.length === 0)
13
+ return undefined;
14
+ const selected = windows.reduce((lowest, candidate) => candidate.remainingPercent < lowest.remainingPercent ? candidate : lowest);
15
+ return {
16
+ id: selected.id,
17
+ remainingPercent: selected.remainingPercent,
18
+ ...(Number.isSafeInteger(selected.resetsAt) ? { resetsAt: selected.resetsAt } : {}),
19
+ };
20
+ }
@@ -0,0 +1,151 @@
1
+ const PROVIDER = "anthropic";
2
+ function abortIfNeeded(options) {
3
+ options?.signal?.throwIfAborted();
4
+ }
5
+ function clone(value) {
6
+ return value === undefined ? undefined : structuredClone(value);
7
+ }
8
+ function assertProvider(providerId) {
9
+ if (providerId !== PROVIDER)
10
+ throw new Error(`Claude credential store does not own provider ${JSON.stringify(providerId)}`);
11
+ }
12
+ function assertOAuthCredential(value) {
13
+ if (value === undefined)
14
+ return undefined;
15
+ if (value === null ||
16
+ typeof value !== "object" ||
17
+ value.type !== "oauth" ||
18
+ typeof value.access !== "string" ||
19
+ value.access.length === 0 ||
20
+ typeof value.refresh !== "string" ||
21
+ value.refresh.length === 0 ||
22
+ typeof value.expires !== "number" ||
23
+ !Number.isFinite(value.expires))
24
+ throw new Error("Claude credential store received a malformed OAuth credential");
25
+ return clone(value);
26
+ }
27
+ function parseOAuthCredential(value) {
28
+ try {
29
+ return assertOAuthCredential(JSON.parse(value));
30
+ }
31
+ catch (error) {
32
+ if (error instanceof Error &&
33
+ error.message === "Claude credential store received a malformed OAuth credential")
34
+ throw error;
35
+ throw new Error("Claude credential store contains malformed OAuth JSON", { cause: error });
36
+ }
37
+ }
38
+ /**
39
+ * Adapt DSH's managed string credential service to pi-ai's typed OAuth store.
40
+ * Refresh/login/logout operations are serialized so an older refresh response
41
+ * cannot overwrite a newer rotated token.
42
+ */
43
+ export class DshOAuthCredentialStore {
44
+ credentials;
45
+ ref;
46
+ legacyRefs;
47
+ #chains = new Map();
48
+ constructor(credentials, ref, legacyRefs = []) {
49
+ if (credentials === undefined || credentials === null)
50
+ throw new Error("Claude OAuth requires the DSH credentials service");
51
+ this.credentials = credentials;
52
+ this.ref = ref;
53
+ this.legacyRefs = Object.freeze([...legacyRefs]);
54
+ }
55
+ #enqueue(providerId, operation, options) {
56
+ assertProvider(providerId);
57
+ const current = (this.#chains.get(providerId) ?? Promise.resolve())
58
+ .catch(() => undefined)
59
+ .then(async () => {
60
+ abortIfNeeded(options);
61
+ return operation();
62
+ });
63
+ const tail = current.catch(() => undefined).then(() => undefined);
64
+ this.#chains.set(providerId, tail);
65
+ tail.finally(() => {
66
+ if (this.#chains.get(providerId) === tail)
67
+ this.#chains.delete(providerId);
68
+ });
69
+ return current;
70
+ }
71
+ async read(providerId, options) {
72
+ assertProvider(providerId);
73
+ abortIfNeeded(options);
74
+ let hit = await this.credentials.resolve(this.ref);
75
+ if (hit?.value === undefined || hit.value === "") {
76
+ for (const legacyRef of this.legacyRefs) {
77
+ const legacy = await this.credentials.resolve(legacyRef);
78
+ if (legacy?.value === undefined || legacy.value === "")
79
+ continue;
80
+ const migrated = parseOAuthCredential(legacy.value);
81
+ await this.credentials.set(this.ref, JSON.stringify(migrated));
82
+ await this.credentials.unset(legacyRef);
83
+ hit = { value: JSON.stringify(migrated) };
84
+ break;
85
+ }
86
+ }
87
+ abortIfNeeded(options);
88
+ if (hit?.value === undefined || hit.value === "")
89
+ return undefined;
90
+ return parseOAuthCredential(hit.value);
91
+ }
92
+ async list(options) {
93
+ abortIfNeeded(options);
94
+ return (await this.read(PROVIDER, options)) === undefined
95
+ ? []
96
+ : [
97
+ {
98
+ providerId: PROVIDER,
99
+ type: "oauth",
100
+ },
101
+ ];
102
+ }
103
+ modify(providerId, update, options) {
104
+ return this.#enqueue(providerId, async () => {
105
+ const current = await this.read(providerId, options);
106
+ const next = await update(clone(current));
107
+ abortIfNeeded(options);
108
+ if (next === undefined)
109
+ return current;
110
+ const validated = assertOAuthCredential(next);
111
+ await this.credentials.set(this.ref, JSON.stringify(validated));
112
+ for (const legacyRef of this.legacyRefs)
113
+ await this.credentials.unset(legacyRef);
114
+ abortIfNeeded(options);
115
+ return clone(validated);
116
+ }, options);
117
+ }
118
+ delete(providerId, options) {
119
+ return this.#enqueue(providerId, async () => {
120
+ await this.credentials.unset(this.ref);
121
+ for (const legacyRef of this.legacyRefs)
122
+ await this.credentials.unset(legacyRef);
123
+ abortIfNeeded(options);
124
+ }, options);
125
+ }
126
+ }
127
+ /** Return only account state that is safe to expose to the browser client. */
128
+ export function createClaudeAuthService(models, store) {
129
+ return Object.freeze({
130
+ async status(options) {
131
+ const current = await store.read(PROVIDER, options);
132
+ if (current === undefined)
133
+ return {
134
+ authenticated: false,
135
+ provider: PROVIDER,
136
+ };
137
+ return {
138
+ authenticated: true,
139
+ provider: PROVIDER,
140
+ type: "oauth",
141
+ expiresAt: current.expires,
142
+ };
143
+ },
144
+ login(interaction) {
145
+ return models.login(PROVIDER, "oauth", interaction);
146
+ },
147
+ logout(options) {
148
+ return models.logout(PROVIDER, options);
149
+ },
150
+ });
151
+ }
@@ -0,0 +1,57 @@
1
+ import { spawn } from "node:child_process";
2
+ export const CLAUDE_AUTH_ORIGIN = "https://claude.ai";
3
+ /** Validate the only external origin this plugin may launch. */
4
+ export function assertClaudeAuthUrl(value) {
5
+ let url;
6
+ try {
7
+ url = new URL(value);
8
+ }
9
+ catch {
10
+ throw new Error("Claude auth URL is invalid");
11
+ }
12
+ if (url.protocol !== "https:")
13
+ throw new Error("Claude auth URL must use HTTPS");
14
+ if (url.origin !== CLAUDE_AUTH_ORIGIN || url.username !== "" || url.password !== "")
15
+ throw new Error("Claude auth URL must use the claude.ai origin");
16
+ return url.href;
17
+ }
18
+ /** Return a shell-free native opener command for the current desktop. */
19
+ export function commandForClaudeAuthUrl(value, platform = process.platform) {
20
+ const url = assertClaudeAuthUrl(value);
21
+ if (platform === "win32")
22
+ return {
23
+ file: "rundll32.exe",
24
+ args: ["url.dll,FileProtocolHandler", url],
25
+ shell: false,
26
+ };
27
+ if (platform === "darwin")
28
+ return {
29
+ file: "open",
30
+ args: [url],
31
+ shell: false,
32
+ };
33
+ if (platform === "linux")
34
+ return {
35
+ file: "xdg-open",
36
+ args: [url],
37
+ shell: false,
38
+ };
39
+ throw new Error(`Claude auth URL opener is unsupported on ${platform}`);
40
+ }
41
+ export function openClaudeAuthUrl(value, options = {}) {
42
+ const command = commandForClaudeAuthUrl(value, options.platform);
43
+ const spawnProcess = options.spawn ?? spawn;
44
+ return new Promise((resolve, reject) => {
45
+ const child = spawnProcess(command.file, [...command.args], {
46
+ detached: true,
47
+ stdio: "ignore",
48
+ windowsHide: true,
49
+ shell: command.shell,
50
+ });
51
+ child.once("error", reject);
52
+ child.once("spawn", () => {
53
+ child.unref();
54
+ resolve();
55
+ });
56
+ });
57
+ }