@gatemux/dsh-gatemux-login 1.0.0 → 1.0.2
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/README.md +3 -1
- package/account.js +50 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,7 +64,9 @@ Model requests are billed to your own GateMux balance.
|
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
Sign in from the app's account settings. Authorization opens in your browser and
|
|
67
|
-
returns to the app.
|
|
67
|
+
returns to the app. The tab itself is left on a confirmation page — GateMux
|
|
68
|
+
serves no hosted completion page, so there is nothing else to show there — and it
|
|
69
|
+
never auto-closes; you can close it once the app is signed in.
|
|
68
70
|
|
|
69
71
|
## Models
|
|
70
72
|
|
package/account.js
CHANGED
|
@@ -68,6 +68,16 @@ function parseGrant(payload, issuer) {
|
|
|
68
68
|
return { version: 1, accessToken, refreshToken, expiresAt, issuer }
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Read a stored record as a grant this deployment may use.
|
|
73
|
+
* @param record - record at this provider's key, or undefined when none is stored.
|
|
74
|
+
* @param issuer - origin a grant must carry to be usable here.
|
|
75
|
+
* @returns the parsed grant, or null when no record is stored or it is unusable.
|
|
76
|
+
*/
|
|
77
|
+
function usableGrant(record, issuer) {
|
|
78
|
+
return record !== undefined && record.kind === 'grant' ? parseGrant(record.payload, issuer) : null
|
|
79
|
+
}
|
|
80
|
+
|
|
71
81
|
/** Accept a bare HTTPS origin, rejecting paths, credentials, and non-HTTPS schemes. */
|
|
72
82
|
function originOf(value) {
|
|
73
83
|
let url
|
|
@@ -182,22 +192,27 @@ export class GateMuxAccount extends DeepSeekAccount {
|
|
|
182
192
|
/** Discard a stored grant issued by another environment. */
|
|
183
193
|
async [Service.init]() {
|
|
184
194
|
const record = await this.ctx.credentials.readRecord(KEY)
|
|
185
|
-
if (record === undefined) return
|
|
186
|
-
const grant = record.kind === 'grant' ? parseGrant(record.payload, this.issuer) : null
|
|
187
|
-
if (grant !== null) return
|
|
195
|
+
if (record === undefined || usableGrant(record, this.issuer) !== null) return
|
|
188
196
|
await this.ctx.credentials.deleteRecord(KEY)
|
|
189
197
|
this.logger.warn('stored grant discarded', { reason: 'unreadable-or-foreign-issuer' })
|
|
190
198
|
}
|
|
191
199
|
|
|
192
200
|
async getState() {
|
|
193
201
|
const record = await this.ctx.credentials.readRecord(KEY)
|
|
194
|
-
|
|
195
|
-
|
|
202
|
+
const grant = usableGrant(record, this.issuer)
|
|
203
|
+
// A record this deployment cannot use reads as absent instead of raising: every
|
|
204
|
+
// caller turns a throw into a failure with no diagnosis. `startSignIn` returns
|
|
205
|
+
// this outside its own catch, so a raise there rejects the start call and the
|
|
206
|
+
// dialog reports a sign-in failure for a record the person never supplied, and
|
|
207
|
+
// `watch` ends the account stream. The record is left in place for a sign-in to
|
|
208
|
+
// overwrite; discarding it here could delete a grant committed since the read.
|
|
209
|
+
if (record !== undefined && grant === null) {
|
|
210
|
+
this.logger.warn('stored grant is unreadable', { reason: 'unreadable-or-foreign-issuer' })
|
|
196
211
|
}
|
|
197
212
|
const attempt = this.attempt?.view ?? null
|
|
198
213
|
return {
|
|
199
|
-
status:
|
|
200
|
-
attempt:
|
|
214
|
+
status: grant === null ? 'signed-out' : 'credential-stored',
|
|
215
|
+
attempt: grant === null && attempt?.phase === 'succeeded' ? null : attempt,
|
|
201
216
|
links: {
|
|
202
217
|
usageUrl: `${this.consoleOrigin}/app/usage`,
|
|
203
218
|
topUpUrl: `${this.consoleOrigin}/app/billing`,
|
|
@@ -277,6 +292,12 @@ export class GateMuxAccount extends DeepSeekAccount {
|
|
|
277
292
|
this.update(attempt, { phase: outcome.status === 'authorized' ? 'succeeded' : 'cancelled' })
|
|
278
293
|
if (outcome.status === 'authorized' && attempt.completionUrl !== undefined) {
|
|
279
294
|
attempt.callback?.writeHead(302, { location: attempt.completionUrl, 'cache-control': 'no-store' }).end()
|
|
295
|
+
} else if (outcome.status === 'authorized') {
|
|
296
|
+
// GateMux serves no hosted completion page, so this tab is the only place
|
|
297
|
+
// the result is visible; answering 204 would leave it blank.
|
|
298
|
+
this.writeCallbackPage(attempt, 'Signed in', attempt.loginSource === 'web'
|
|
299
|
+
? 'Signed in to GateMux. Close this tab and continue in the original tab.'
|
|
300
|
+
: 'Signed in to GateMux. Close this tab and continue in DeepSeek Harness.')
|
|
280
301
|
} else {
|
|
281
302
|
attempt.callback?.writeHead(204, { 'cache-control': 'no-store' }).end()
|
|
282
303
|
}
|
|
@@ -650,21 +671,30 @@ export class GateMuxAccount extends DeepSeekAccount {
|
|
|
650
671
|
}
|
|
651
672
|
|
|
652
673
|
|
|
674
|
+
/**
|
|
675
|
+
* Answer the browser tab the flow was authorized in with a short page. The
|
|
676
|
+
* application opens that tab outside itself and never reads this response, so
|
|
677
|
+
* the page exists only for the person looking at it; text is static, which is
|
|
678
|
+
* why no script is needed and the policy can forbid every source.
|
|
679
|
+
* @param attempt - in-flight attempt whose callback is being answered.
|
|
680
|
+
* @param title - page title and heading.
|
|
681
|
+
* @param message - what the person should do next.
|
|
682
|
+
*/
|
|
683
|
+
writeCallbackPage(attempt, title, message) {
|
|
684
|
+
attempt.callback?.writeHead(200, {
|
|
685
|
+
'content-type': 'text/html; charset=utf-8',
|
|
686
|
+
'cache-control': 'no-store',
|
|
687
|
+
'content-security-policy': `default-src 'none'; frame-ancestors 'none'`,
|
|
688
|
+
'referrer-policy': 'no-referrer',
|
|
689
|
+
}).end(`<!doctype html><html lang="en"><meta charset="utf-8"><title>${title}</title>`
|
|
690
|
+
+ `<body><p>${message}</p></body></html>`)
|
|
691
|
+
}
|
|
692
|
+
|
|
653
693
|
/** Return the browser to the application after a failed exchange. */
|
|
654
694
|
finishFailedCallback(attempt) {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
attempt.callback?.writeHead(200, {
|
|
659
|
-
'content-type': 'text/html; charset=utf-8',
|
|
660
|
-
'cache-control': 'no-store',
|
|
661
|
-
'content-security-policy': `default-src 'none'; frame-ancestors 'none'`,
|
|
662
|
-
'referrer-policy': 'no-referrer',
|
|
663
|
-
}).end(`<!doctype html><html lang="en"><meta charset="utf-8"><title>${message}</title>`
|
|
664
|
-
+ `<body><p>${message}</p></body></html>`)
|
|
665
|
-
} else {
|
|
666
|
-
attempt.callback?.writeHead(204, { 'cache-control': 'no-store' }).end()
|
|
667
|
-
}
|
|
695
|
+
this.writeCallbackPage(attempt, 'Sign-in failed', attempt.loginSource === 'web'
|
|
696
|
+
? 'Sign-in failed. Close this tab and try again in the original tab.'
|
|
697
|
+
: 'Sign-in failed. Close this tab and try again in DeepSeek Harness.')
|
|
668
698
|
}
|
|
669
699
|
}
|
|
670
700
|
|