@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.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/account.js +50 -20
  3. 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
- if (record !== undefined && (record.kind !== 'grant' || parseGrant(record.payload, this.issuer) === null)) {
195
- throw new GateMuxAuthError('stored grant is unreadable', 'storage')
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: record === undefined ? 'signed-out' : 'credential-stored',
200
- attempt: record === undefined && attempt?.phase === 'succeeded' ? null : 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
- if (attempt.loginSource === 'web') {
656
- const nonce = randomBytes(16).toString('base64url')
657
- const message = 'Sign-in failed. Close this tab and try again in the original tab.'
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gatemux/dsh-gatemux-login",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "description": "Sign in with GateMux for DeepSeek Harness: users pay from their own GateMux balance.",
6
6
  "publishConfig": {