dsh-oauth-login 0.2.4
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/INSTALL.md +59 -0
- package/INSTALL.zh.md +45 -0
- package/LICENSE +201 -0
- package/README.en.md +104 -0
- package/README.md +104 -0
- package/README.zh.md +3 -0
- package/SECURITY.md +33 -0
- package/cordis.patch.yml +5 -0
- package/docs/network-proxy.md +45 -0
- package/docs/openrouter-sync.md +84 -0
- package/docs/screenshots/01-providers-signed-out.png +0 -0
- package/docs/screenshots/03-copilot-signing-in.png +0 -0
- package/docs/screenshots/04-mixed-live-states.png +0 -0
- package/docs/screenshots/05-copilot-sign-in.gif +0 -0
- package/docs/screenshots/06-cli-status-store.png +0 -0
- package/lib/bin.d.ts +5 -0
- package/lib/bin.js +2132 -0
- package/lib/client.js +1915 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +482 -0
- package/lib/index.js +3105 -0
- package/lib/invariant.d.ts +7 -0
- package/lib/invariant.js +8 -0
- package/package.json +180 -0
- package/src/adapter.ts +207 -0
- package/src/auth-routes.ts +654 -0
- package/src/auth.ts +76 -0
- package/src/bin.ts +185 -0
- package/src/catalog.ts +129 -0
- package/src/client/OpenRouterSyncStatus.tsx +66 -0
- package/src/client/PiLoginSettings.tsx +470 -0
- package/src/client/ProxySettings.tsx +123 -0
- package/src/client/SearchableModelSelect.tsx +595 -0
- package/src/client/draft-input.ts +12 -0
- package/src/client/index.tsx +71 -0
- package/src/client/locales.ts +176 -0
- package/src/client/login-input-copy.ts +34 -0
- package/src/client/login-window.ts +24 -0
- package/src/client/model-filter.ts +45 -0
- package/src/client/openrouter-store.ts +122 -0
- package/src/client/proxy-draft.ts +57 -0
- package/src/extra-models.ts +201 -0
- package/src/hosted-capture.ts +41 -0
- package/src/hosted-images.ts +154 -0
- package/src/http.ts +241 -0
- package/src/ids.ts +17 -0
- package/src/index.ts +183 -0
- package/src/invariant.ts +12 -0
- package/src/model-error-hint.ts +98 -0
- package/src/native-tools.ts +442 -0
- package/src/oauth-refresh.ts +49 -0
- package/src/openrouter-catalog.ts +245 -0
- package/src/openrouter-free.ts +76 -0
- package/src/openrouter-models.ts +155 -0
- package/src/openrouter-types.ts +40 -0
- package/src/plugin-config.ts +30 -0
- package/src/provider.ts +137 -0
- package/src/proxy-config.ts +61 -0
- package/src/proxy-store.ts +45 -0
- package/src/proxy-transport.ts +220 -0
- package/src/redact.ts +22 -0
- package/src/responses-tap.ts +97 -0
- package/src/session.ts +104 -0
- package/src/store.ts +234 -0
package/src/store.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-provider subscription credential store. File is $DSH_HOME/.dsh-oauth-auth.json.
|
|
3
|
+
* The old .pi-login-auth.json name is read only as a DSH-owned migration source.
|
|
4
|
+
* Never ~/.codex, ~/.grok, ~/.claude, or ~/.pi/agent/auth.json.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { mkdir, readFile, rm, stat } from 'node:fs/promises'
|
|
8
|
+
import { basename, dirname, join, resolve } from 'node:path'
|
|
9
|
+
import type { AuthOperationOptions, Credential, CredentialInfo, CredentialStore, ProviderEnv } from '@earendil-works/pi-ai'
|
|
10
|
+
import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
|
|
11
|
+
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
|
12
|
+
import { PI_LOGIN_PROVIDERS } from './catalog.ts'
|
|
13
|
+
import { LEGACY_PI_LOGIN_AUTH_FILENAME, PI_LOGIN_AUTH_FILENAME } from './ids.ts'
|
|
14
|
+
|
|
15
|
+
const AUTH_FORMAT_VERSION = 1
|
|
16
|
+
const OAUTH_ALLOWED_FIELDS = new Set([
|
|
17
|
+
'type', 'access', 'refresh', 'expires', 'accountId', 'enterpriseUrl', 'availableModelIds',
|
|
18
|
+
])
|
|
19
|
+
const API_KEY_ALLOWED_FIELDS = new Set(['type', 'key', 'env'])
|
|
20
|
+
|
|
21
|
+
interface AuthDocument {
|
|
22
|
+
version: typeof AUTH_FORMAT_VERSION
|
|
23
|
+
credentials: Record<string, Credential>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isENOENT(error: unknown): boolean {
|
|
27
|
+
return (error as NodeJS.ErrnoException | null)?.code === 'ENOENT'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function assertOwnerOnly(filename: string): Promise<void> {
|
|
31
|
+
let mode: number
|
|
32
|
+
try {
|
|
33
|
+
mode = (await stat(filename)).mode
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (isENOENT(error)) return
|
|
36
|
+
throw error
|
|
37
|
+
}
|
|
38
|
+
if (process.platform === 'win32') return
|
|
39
|
+
if ((mode & 0o077) !== 0) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`pi-login: ${filename} is readable beyond its owner (mode ${(mode & 0o777).toString(8)});`
|
|
42
|
+
+ ` run "chmod 600 ${filename}" before starting again`,
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseProviderEnv(raw: unknown, filename: string, providerId: string): ProviderEnv | undefined {
|
|
48
|
+
if (raw === undefined) return undefined
|
|
49
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
50
|
+
throw new Error(`pi-login: ${filename} credential for ${providerId} env must be an object`)
|
|
51
|
+
}
|
|
52
|
+
const env: ProviderEnv = {}
|
|
53
|
+
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
54
|
+
if (typeof value !== 'string') {
|
|
55
|
+
throw new Error(`pi-login: ${filename} credential for ${providerId} env values must be strings`)
|
|
56
|
+
}
|
|
57
|
+
env[key] = value
|
|
58
|
+
}
|
|
59
|
+
return env
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseCredential(raw: unknown, filename: string, providerId: string): Credential {
|
|
63
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
64
|
+
throw new Error(`pi-login: ${filename} credential for ${providerId} must be an object`)
|
|
65
|
+
}
|
|
66
|
+
const credential = raw as Record<string, unknown>
|
|
67
|
+
const type = credential['type']
|
|
68
|
+
const allowed = type === 'oauth'
|
|
69
|
+
? OAUTH_ALLOWED_FIELDS
|
|
70
|
+
: type === 'api_key'
|
|
71
|
+
? API_KEY_ALLOWED_FIELDS
|
|
72
|
+
: undefined
|
|
73
|
+
if (allowed === undefined) {
|
|
74
|
+
throw new Error(`pi-login: ${filename} credential for ${providerId} type must be oauth or api_key`)
|
|
75
|
+
}
|
|
76
|
+
if (Object.keys(credential).some(key => !allowed.has(key))) {
|
|
77
|
+
throw new Error(`pi-login: ${filename} credential for ${providerId} contains an unknown field`)
|
|
78
|
+
}
|
|
79
|
+
if (type === 'api_key') {
|
|
80
|
+
if (typeof credential['key'] !== 'string' || credential['key'].length === 0) {
|
|
81
|
+
throw new Error(`pi-login: ${filename} credential for ${providerId} key must be a non-empty string`)
|
|
82
|
+
}
|
|
83
|
+
const env = parseProviderEnv(credential['env'], filename, providerId)
|
|
84
|
+
return {
|
|
85
|
+
type: 'api_key',
|
|
86
|
+
key: credential['key'],
|
|
87
|
+
...(env === undefined ? {} : { env }),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (typeof credential['access'] !== 'string' || credential['access'].length === 0) {
|
|
91
|
+
throw new Error(`pi-login: ${filename} credential for ${providerId} access must be a non-empty string`)
|
|
92
|
+
}
|
|
93
|
+
if (typeof credential['refresh'] !== 'string') {
|
|
94
|
+
throw new Error(`pi-login: ${filename} credential for ${providerId} refresh must be a string`)
|
|
95
|
+
}
|
|
96
|
+
if (typeof credential['expires'] !== 'number' || !Number.isFinite(credential['expires']) || credential['expires'] <= 0) {
|
|
97
|
+
throw new Error(`pi-login: ${filename} credential for ${providerId} expires must be a positive finite number`)
|
|
98
|
+
}
|
|
99
|
+
return credential as unknown as Credential
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseDocument(text: string, filename: string): AuthDocument {
|
|
103
|
+
let value: unknown
|
|
104
|
+
try {
|
|
105
|
+
value = JSON.parse(text)
|
|
106
|
+
} catch {
|
|
107
|
+
throw new Error(`pi-login: ${filename} is not valid JSON`)
|
|
108
|
+
}
|
|
109
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
110
|
+
throw new Error(`pi-login: ${filename} must contain an object`)
|
|
111
|
+
}
|
|
112
|
+
const document = value as Record<string, unknown>
|
|
113
|
+
if (document['version'] !== AUTH_FORMAT_VERSION) {
|
|
114
|
+
throw new Error(`pi-login: ${filename} has unsupported auth format version ${String(document['version'])}`)
|
|
115
|
+
}
|
|
116
|
+
if (Object.keys(document).some(key => key !== 'version' && key !== 'credentials')) {
|
|
117
|
+
throw new Error(`pi-login: ${filename} contains an unknown top-level field`)
|
|
118
|
+
}
|
|
119
|
+
const raw = document['credentials']
|
|
120
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
121
|
+
throw new Error(`pi-login: ${filename} credentials must be an object`)
|
|
122
|
+
}
|
|
123
|
+
const owned = new Set(PI_LOGIN_PROVIDERS.map(provider => provider.id))
|
|
124
|
+
const credentials: Record<string, Credential> = {}
|
|
125
|
+
for (const [providerId, entry] of Object.entries(raw as Record<string, unknown>)) {
|
|
126
|
+
if (!owned.has(providerId)) {
|
|
127
|
+
throw new Error(`pi-login: ${filename} contains an unknown provider "${providerId}"`)
|
|
128
|
+
}
|
|
129
|
+
credentials[providerId] = parseCredential(entry, filename, providerId)
|
|
130
|
+
}
|
|
131
|
+
return { version: AUTH_FORMAT_VERSION, credentials }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function cloneCredential(credential: Credential): Credential {
|
|
135
|
+
return structuredClone(credential)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function piLoginAuthPath(dshHome?: string): string {
|
|
139
|
+
return resolve(join(resolveDshHome(dshHome), PI_LOGIN_AUTH_FILENAME))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export class PiLoginCredentialStore implements CredentialStore {
|
|
143
|
+
readonly filename: string
|
|
144
|
+
readonly legacyFilename: string | undefined
|
|
145
|
+
|
|
146
|
+
constructor(filename: string = piLoginAuthPath()) {
|
|
147
|
+
this.filename = resolve(filename)
|
|
148
|
+
this.legacyFilename = basename(this.filename) === PI_LOGIN_AUTH_FILENAME
|
|
149
|
+
? resolve(join(dirname(this.filename), LEGACY_PI_LOGIN_AUTH_FILENAME))
|
|
150
|
+
: undefined
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async readDocument(): Promise<AuthDocument> {
|
|
154
|
+
const candidates = [this.filename, ...(this.legacyFilename === undefined ? [] : [this.legacyFilename])]
|
|
155
|
+
for (const filename of candidates) {
|
|
156
|
+
await assertOwnerOnly(filename)
|
|
157
|
+
try {
|
|
158
|
+
return parseDocument(await readFile(filename, 'utf8'), filename)
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (isENOENT(error)) continue
|
|
161
|
+
throw error
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { version: AUTH_FORMAT_VERSION, credentials: {} }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async read(providerId: string, options?: AuthOperationOptions): Promise<Credential | undefined> {
|
|
168
|
+
options?.signal?.throwIfAborted()
|
|
169
|
+
const credential = (await this.readDocument()).credentials[providerId]
|
|
170
|
+
options?.signal?.throwIfAborted()
|
|
171
|
+
return credential === undefined ? undefined : cloneCredential(credential)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async list(options?: AuthOperationOptions): Promise<readonly CredentialInfo[]> {
|
|
175
|
+
options?.signal?.throwIfAborted()
|
|
176
|
+
const document = await this.readDocument()
|
|
177
|
+
options?.signal?.throwIfAborted()
|
|
178
|
+
return Object.entries(document.credentials).map(([providerId, credential]) => ({
|
|
179
|
+
providerId,
|
|
180
|
+
type: credential.type,
|
|
181
|
+
}))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async modify(
|
|
185
|
+
providerId: string,
|
|
186
|
+
fn: (current: Credential | undefined) => Promise<Credential | undefined>,
|
|
187
|
+
options?: AuthOperationOptions,
|
|
188
|
+
): Promise<Credential | undefined> {
|
|
189
|
+
if (!PI_LOGIN_PROVIDERS.some(provider => provider.id === providerId)) {
|
|
190
|
+
throw new Error(`pi-login: credential store does not own provider "${providerId}"`)
|
|
191
|
+
}
|
|
192
|
+
options?.signal?.throwIfAborted()
|
|
193
|
+
await mkdir(dirname(this.filename), { recursive: true, mode: 0o700 })
|
|
194
|
+
options?.signal?.throwIfAborted()
|
|
195
|
+
return withFileLock(this.filename, async () => {
|
|
196
|
+
options?.signal?.throwIfAborted()
|
|
197
|
+
const document = await this.readDocument()
|
|
198
|
+
options?.signal?.throwIfAborted()
|
|
199
|
+
const current = document.credentials[providerId]
|
|
200
|
+
const candidate = await fn(current === undefined ? undefined : cloneCredential(current))
|
|
201
|
+
options?.signal?.throwIfAborted()
|
|
202
|
+
if (candidate === undefined) return current === undefined ? undefined : cloneCredential(current)
|
|
203
|
+
const next = parseCredential(candidate, this.filename, providerId)
|
|
204
|
+
const credentials = { ...document.credentials, [providerId]: next }
|
|
205
|
+
await writeFileAtomic(this.filename, `${JSON.stringify({ version: AUTH_FORMAT_VERSION, credentials }, null, 2)}\n`, {
|
|
206
|
+
mode: 0o600,
|
|
207
|
+
dirMode: 0o700,
|
|
208
|
+
})
|
|
209
|
+
return cloneCredential(next)
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async delete(providerId: string, options?: AuthOperationOptions): Promise<void> {
|
|
214
|
+
if (!PI_LOGIN_PROVIDERS.some(provider => provider.id === providerId)) return
|
|
215
|
+
options?.signal?.throwIfAborted()
|
|
216
|
+
await mkdir(dirname(this.filename), { recursive: true, mode: 0o700 })
|
|
217
|
+
options?.signal?.throwIfAborted()
|
|
218
|
+
await withFileLock(this.filename, async () => {
|
|
219
|
+
options?.signal?.throwIfAborted()
|
|
220
|
+
const document = await this.readDocument()
|
|
221
|
+
options?.signal?.throwIfAborted()
|
|
222
|
+
if (document.credentials[providerId] === undefined) return
|
|
223
|
+
const { [providerId]: _removed, ...credentials } = document.credentials
|
|
224
|
+
if (Object.keys(credentials).length === 0) {
|
|
225
|
+
await rm(this.filename, { force: true })
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
await writeFileAtomic(this.filename, `${JSON.stringify({ version: AUTH_FORMAT_VERSION, credentials }, null, 2)}\n`, {
|
|
229
|
+
mode: 0o600,
|
|
230
|
+
dirMode: 0o700,
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
}
|