@gydr-ai/mcp-oauth 0.1.2 → 0.1.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.
@@ -0,0 +1 @@
1
+ export {};
@@ -1 +1,5 @@
1
- export {};
1
+ /**
2
+ * `Strict-Transport-Security` value: two years, subdomains included
3
+ * (RFC 6797). No `preload`: that is a decision for the domain owner.
4
+ */
5
+ export declare const STRICT_TRANSPORT_SECURITY = "max-age=63072000; includeSubDomains";
@@ -57,6 +57,13 @@ export interface Lifetimes {
57
57
  * (90 days).
58
58
  */
59
59
  readonly grant?: number;
60
+ /**
61
+ * Sliding limit on a grant: it also ends when it has not been renewed with
62
+ * a refresh token for this long, and each renewal extends it again, never
63
+ * beyond `grant`. `0` (off), or from `accessToken` up to 34,560,000.
64
+ * Default: 0.
65
+ */
66
+ readonly grantIdle?: number;
60
67
  /** Authorization code lifetime, 1 to 600. Default: 60. */
61
68
  readonly authorizationCode?: number;
62
69
  /** Time allowed for sign-in plus consent, 1 to 3600. Default: 900 (15 minutes). */
@@ -291,6 +298,16 @@ export interface McpAuthOptions {
291
298
  readonly scopes?: readonly string[];
292
299
  /** Scopes granted when a client asks for none. Default: all `scopes`. */
293
300
  readonly defaultScopes?: readonly string[];
301
+ /**
302
+ * Grant types a registered client or metadata document gets when it names
303
+ * none. RFC 7591 § 2 reads an absent `grant_types` as
304
+ * `["authorization_code"]`, so such a client receives no refresh token;
305
+ * `["authorization_code", "refresh_token"]` gives it one. Must include
306
+ * `authorization_code`. Applies when a client registers or a metadata
307
+ * document is fetched, not to clients already registered, and not to
308
+ * `clients`, which default to both. Default: `["authorization_code"]`.
309
+ */
310
+ readonly defaultGrantTypes?: readonly ("authorization_code" | "refresh_token")[];
294
311
  /**
295
312
  * Plain-language description per scope, shown on the consent screen (and
296
313
  * escaped). Keys must be listed in `scopes`. Default: none.
@@ -318,8 +335,10 @@ export interface McpAuthOptions {
318
335
  readonly lifetimes?: Lifetimes;
319
336
  /**
320
337
  * Seconds after a refresh token is used during which presenting it again
321
- * returns another token pair (a lost response, a retried request) instead
322
- * of ending the grant; `0` turns the window off. Default: 10.
338
+ * (a lost response, a retried request) returns the pair the first use
339
+ * issued instead of ending the grant; with a store that lacks
340
+ * `putRefreshTokenReplacement`, a new access token only. Never a second
341
+ * refresh token chain. `0` turns the window off. Default: 10.
323
342
  */
324
343
  readonly refreshReuseGraceSeconds?: number;
325
344
  /** Accept Client ID Metadata Document client ids. Default: `true`. */
@@ -566,6 +585,14 @@ export interface McpAuthOptions {
566
585
  * issuer and resource, for development. Default: `false`.
567
586
  */
568
587
  readonly allowInsecureLocalhost?: boolean;
588
+ /**
589
+ * Send `Strict-Transport-Security: max-age=63072000; includeSubDomains`
590
+ * on every response this package builds (endpoints, pages, redirects and
591
+ * `authenticate` / `unauthorized` refusals). Set `false` only when a proxy
592
+ * in front already sets the header, or when the issuer's subdomains must
593
+ * stay reachable over plain HTTP. Default: `true`.
594
+ */
595
+ readonly strictTransportSecurity?: boolean;
569
596
  /**
570
597
  * Clock in milliseconds since the epoch, for tests. A non-finite value
571
598
  * makes the request fail instead of disabling expiry checks.
@@ -279,9 +279,14 @@ export interface GrantRecord {
279
279
  * or `McpAuth.updateGrantData` (JSON, at most `limits.grantDataBytes`).
280
280
  */
281
281
  readonly data?: unknown;
282
+ /** The redirect URI the approved authorization was sent to. */
283
+ readonly redirectUri?: string;
282
284
  /** When the person approved. */
283
285
  readonly createdAt: number;
284
- /** Absolute end of the grant; no token outlives it. */
286
+ /**
287
+ * End of the grant; no token outlives it. With `lifetimes.grantIdle` it
288
+ * moves forward on each refresh, never past `createdAt + lifetimes.grant`.
289
+ */
285
290
  readonly expiresAt: number;
286
291
  }
287
292
  /**
@@ -305,6 +310,8 @@ export interface GrantUpstreamUpdate {
305
310
  readonly upstreamAccessTokenExpiresAt?: number;
306
311
  /** New integrator data for the grant (see `GrantRecord.data`). */
307
312
  readonly data?: unknown;
313
+ /** New end of the grant, after a refresh under `lifetimes.grantIdle`. */
314
+ readonly expiresAt?: number;
308
315
  }
309
316
  /**
310
317
  * An issued access token, stored by hash.
@@ -359,6 +366,14 @@ export interface RefreshTokenRecord {
359
366
  readonly expiresAt: number;
360
367
  /** Set once, atomically, by `OAuthStore.useRefreshToken`. */
361
368
  readonly usedAt?: number;
369
+ /**
370
+ * The access and refresh token pair this token was exchanged for,
371
+ * encrypted by the `SecretSealer`. Set by the optional
372
+ * `OAuthStore.putRefreshTokenReplacement` after the token is spent, and
373
+ * returned by `getRefreshToken` and `useRefreshToken` from then on, so a
374
+ * retry inside `refreshReuseGraceSeconds` receives that same pair.
375
+ */
376
+ readonly sealedReplacement?: string;
362
377
  }
363
378
  /**
364
379
  * Result of `OAuthStore.useRefreshToken`.
@@ -35,8 +35,9 @@ import type { AccessTokenRecord, AuthorizationCodeRecord, ClientRecord, ConsentR
35
35
  * an idle period, and when a size cap is reached evict the least recently
36
36
  * used grant-less dynamic client first; never evict `static` clients.
37
37
  *
38
- * Only hashes of tokens and codes reach the store. Upstream tokens arrive
39
- * already encrypted by the `SecretSealer`.
38
+ * Only hashes of tokens and codes reach the store. Upstream tokens, and the
39
+ * pair a refresh token was exchanged for (`putRefreshTokenReplacement`),
40
+ * arrive already encrypted by the `SecretSealer`.
40
41
  *
41
42
  * @example
42
43
  * ```ts
@@ -147,8 +148,10 @@ export interface OAuthStore {
147
148
  /** Read a grant. */
148
149
  getGrant(grantId: string): Promise<GrantRecord | undefined>;
149
150
  /**
150
- * Update the upstream-token fields of an existing grant. Resolves `false`,
151
- * writing nothing, when the grant no longer exists.
151
+ * Update fields of an existing grant: the upstream tokens, `data`, or
152
+ * `expiresAt` (a refresh under `lifetimes.grantIdle`). Save every field
153
+ * given. Resolves `false`, writing nothing, when the grant no longer
154
+ * exists.
152
155
  */
153
156
  updateGrant(grantId: string, update: GrantUpstreamUpdate): Promise<boolean>;
154
157
  /**
@@ -177,6 +180,20 @@ export interface OAuthStore {
177
180
  getRefreshToken(tokenHash: string): Promise<RefreshTokenRecord | undefined>;
178
181
  /** Atomically set `usedAt` if it is not set yet. */
179
182
  useRefreshToken(tokenHash: string, now: number): Promise<UsedRefreshToken | undefined>;
183
+ /**
184
+ * Optional, recommended: save `sealedReplacement` on the (already used)
185
+ * refresh token record `tokenHash`, keeping every other field, `usedAt`
186
+ * included. `getRefreshToken` and `useRefreshToken` must return it from
187
+ * then on. A later write replaces an earlier one; nothing happens when the
188
+ * record is gone.
189
+ *
190
+ * With it, a refresh token presented again inside
191
+ * `refreshReuseGraceSeconds` receives exactly the pair its first exchange
192
+ * issued, so a copied refresh token can never start a second token chain.
193
+ * Without it, such a retry receives a new access token only, with no
194
+ * refresh token.
195
+ */
196
+ putRefreshTokenReplacement?(tokenHash: string, sealedReplacement: string): Promise<void>;
180
197
  /**
181
198
  * Optional: remember the answer to a consent submission for a few
182
199
  * seconds, keyed by a hash of the browser cookie and the form token, so a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gydr-ai/mcp-oauth",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "OAuth 2.1 authorization server and resource-server guard for remote MCP servers, in front of any OIDC identity provider. No token passthrough, consent on every authorization, DCR and Client ID Metadata Documents.",
5
5
  "keywords": [
6
6
  "mcp",