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.
Files changed (64) hide show
  1. package/INSTALL.md +59 -0
  2. package/INSTALL.zh.md +45 -0
  3. package/LICENSE +201 -0
  4. package/README.en.md +104 -0
  5. package/README.md +104 -0
  6. package/README.zh.md +3 -0
  7. package/SECURITY.md +33 -0
  8. package/cordis.patch.yml +5 -0
  9. package/docs/network-proxy.md +45 -0
  10. package/docs/openrouter-sync.md +84 -0
  11. package/docs/screenshots/01-providers-signed-out.png +0 -0
  12. package/docs/screenshots/03-copilot-signing-in.png +0 -0
  13. package/docs/screenshots/04-mixed-live-states.png +0 -0
  14. package/docs/screenshots/05-copilot-sign-in.gif +0 -0
  15. package/docs/screenshots/06-cli-status-store.png +0 -0
  16. package/lib/bin.d.ts +5 -0
  17. package/lib/bin.js +2132 -0
  18. package/lib/client.js +1915 -0
  19. package/lib/client.js.map +1 -0
  20. package/lib/index.d.ts +482 -0
  21. package/lib/index.js +3105 -0
  22. package/lib/invariant.d.ts +7 -0
  23. package/lib/invariant.js +8 -0
  24. package/package.json +180 -0
  25. package/src/adapter.ts +207 -0
  26. package/src/auth-routes.ts +654 -0
  27. package/src/auth.ts +76 -0
  28. package/src/bin.ts +185 -0
  29. package/src/catalog.ts +129 -0
  30. package/src/client/OpenRouterSyncStatus.tsx +66 -0
  31. package/src/client/PiLoginSettings.tsx +470 -0
  32. package/src/client/ProxySettings.tsx +123 -0
  33. package/src/client/SearchableModelSelect.tsx +595 -0
  34. package/src/client/draft-input.ts +12 -0
  35. package/src/client/index.tsx +71 -0
  36. package/src/client/locales.ts +176 -0
  37. package/src/client/login-input-copy.ts +34 -0
  38. package/src/client/login-window.ts +24 -0
  39. package/src/client/model-filter.ts +45 -0
  40. package/src/client/openrouter-store.ts +122 -0
  41. package/src/client/proxy-draft.ts +57 -0
  42. package/src/extra-models.ts +201 -0
  43. package/src/hosted-capture.ts +41 -0
  44. package/src/hosted-images.ts +154 -0
  45. package/src/http.ts +241 -0
  46. package/src/ids.ts +17 -0
  47. package/src/index.ts +183 -0
  48. package/src/invariant.ts +12 -0
  49. package/src/model-error-hint.ts +98 -0
  50. package/src/native-tools.ts +442 -0
  51. package/src/oauth-refresh.ts +49 -0
  52. package/src/openrouter-catalog.ts +245 -0
  53. package/src/openrouter-free.ts +76 -0
  54. package/src/openrouter-models.ts +155 -0
  55. package/src/openrouter-types.ts +40 -0
  56. package/src/plugin-config.ts +30 -0
  57. package/src/provider.ts +137 -0
  58. package/src/proxy-config.ts +61 -0
  59. package/src/proxy-store.ts +45 -0
  60. package/src/proxy-transport.ts +220 -0
  61. package/src/redact.ts +22 -0
  62. package/src/responses-tap.ts +97 -0
  63. package/src/session.ts +104 -0
  64. 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
+ }