@jeffjassky/oauth-host 0.1.0 → 0.2.0

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 CHANGED
@@ -48,14 +48,22 @@ app.use('/mcp', oauth.protect('contacts.read'), mcpRouter);
48
48
  Register each client once — there is no RFC 7591 dynamic client registration:
49
49
 
50
50
  ```js
51
+ import { CLAUDE_CONNECTOR_REDIRECT_URI, CLAUDE_CODE_REDIRECT_URIS } from '@jeffjassky/oauth-host';
52
+
51
53
  const { clientId, clientSecret } = await oauth.clients.create({
52
54
  name: 'Claude',
53
- redirectUris: ['<Claude connector callback, from their docs>'],
55
+ redirectUris: [CLAUDE_CONNECTOR_REDIRECT_URI, ...CLAUDE_CODE_REDIRECT_URIS],
54
56
  allowedScopes: ['openid', 'contacts.read'],
55
57
  branding: { publisher: 'Anthropic' },
56
58
  }); // secret returned once, never again
57
59
  ```
58
60
 
61
+ The callbacks ship as constants because `create()` compares them byte-for-byte
62
+ and retyping one out of a vendor's docs is the most typo-prone step in the setup.
63
+ ChatGPT's current callback is per-connector and therefore not a constant — copy
64
+ it from its setup screen. See
65
+ [Vendor callback URLs](https://jeffjassky.github.io/oauth-host/guide/mcp#vendor-callback-urls).
66
+
59
67
  Or let Claude and ChatGPT register themselves with a [client ID metadata
60
68
  document](https://jeffjassky.github.io/oauth-host/guide/cimd) — their
61
69
  `client_id` is an `https://` URL serving a JSON description of themselves, and
package/dist/index.cjs CHANGED
@@ -500,6 +500,7 @@ function resolveConfig(config) {
500
500
  }
501
501
  const store = rateLimits.store ?? memoryStore();
502
502
  const normalizedMount = `/${String(mountPath).replace(/^\/+|\/+$/g, "")}`;
503
+ const indexes = { ready: false };
503
504
  const ctx = {
504
505
  models,
505
506
  issuer,
@@ -531,6 +532,11 @@ function resolveConfig(config) {
531
532
  cors: { tokenEndpoint: cors.tokenEndpoint ?? false, origins: cors.origins ?? [] },
532
533
  clockSkewMs,
533
534
  cimd: resolveCimd(clientIdMetadata, scopeIndex),
535
+ indexes,
536
+ async syncIndexes() {
537
+ await syncModelIndexes(models);
538
+ indexes.ready = true;
539
+ },
534
540
  logger,
535
541
  track,
536
542
  async audit(entry) {
@@ -2480,8 +2486,17 @@ function createOAuthRouter(ctx) {
2480
2486
  const ipKey = (req) => req.ip ?? "unknown";
2481
2487
  const clientKey = (req) => clientIdFromRequest(req) ?? ipKey(req);
2482
2488
  const cors = corsMiddleware(ctx);
2489
+ const requireIndexes = w((_req, _res, next) => {
2490
+ if (ctx.indexes.ready) return next();
2491
+ throw new OAuthError(
2492
+ 503,
2493
+ "server_error",
2494
+ "This authorization server is not ready: `syncIndexes()` has not resolved yet. Await `oauth.syncIndexes()` at boot, before mounting the routers."
2495
+ );
2496
+ });
2483
2497
  router.get(
2484
2498
  "/authorize",
2499
+ requireIndexes,
2485
2500
  rateLimit(ctx, "authorize", ipKey),
2486
2501
  w(async (req, res) => {
2487
2502
  let validated;
@@ -2540,6 +2555,7 @@ function createOAuthRouter(ctx) {
2540
2555
  router.post(
2541
2556
  "/token",
2542
2557
  cors,
2558
+ requireIndexes,
2543
2559
  form,
2544
2560
  rateLimit(ctx, "token", clientKey),
2545
2561
  w(async (req, res) => {
@@ -2644,6 +2660,7 @@ function createOAuthRouter(ctx) {
2644
2660
  );
2645
2661
  router.post(
2646
2662
  "/consent/:requestId",
2663
+ requireIndexes,
2647
2664
  rateLimit(ctx, "consent", ipKey),
2648
2665
  w(async (req, res) => {
2649
2666
  const user = await requireUser(ctx, req);
@@ -2745,6 +2762,16 @@ function corsMiddleware(ctx) {
2745
2762
  };
2746
2763
  }
2747
2764
 
2765
+ // src/server/vendors.ts
2766
+ var CLAUDE_CONNECTOR_REDIRECT_URI = "https://claude.ai/api/mcp/auth_callback";
2767
+ var CLAUDE_CODE_REDIRECT_URIS = [
2768
+ "http://localhost/callback",
2769
+ "http://127.0.0.1/callback"
2770
+ ];
2771
+ var CHATGPT_LEGACY_REDIRECT_URI = "https://chatgpt.com/connector_platform_oauth_redirect";
2772
+ var CHATGPT_CONNECTOR_REDIRECT_URI_PATTERN = "https://chatgpt.com/connector/oauth/{callback_id}";
2773
+ var CIMD_ALLOWED_HOSTS = ["claude.ai", "chatgpt.com"];
2774
+
2748
2775
  // src/server/index.ts
2749
2776
  function createOAuthHost(config) {
2750
2777
  const ctx = resolveConfig(config);
@@ -2768,13 +2795,35 @@ function createOAuthHost(config) {
2768
2795
  * stops one user holding two live grants for the same client, and mongoose
2769
2796
  * builds indexes in the background — a cold database will happily serve
2770
2797
  * the write that violates it first. See standards/traps.md #3.
2798
+ *
2799
+ * Resolving this is also what flips `ready` and un-gates `/authorize`,
2800
+ * `POST /consent/:requestId` and `POST /token`.
2771
2801
  */
2772
- syncIndexes: () => syncModelIndexes(ctx.models),
2802
+ syncIndexes: () => ctx.syncIndexes(),
2803
+ /**
2804
+ * Has `syncIndexes()` resolved?
2805
+ *
2806
+ * A boolean, not a promise, and the difference is the failure mode. A
2807
+ * promise would have to exist from construction, so a host that never calls
2808
+ * `syncIndexes()` would await it forever — a boot-order mistake turning into
2809
+ * an unexplained hang with no message. A boolean is plainly `false`, cannot
2810
+ * be awaited by accident, and is backed by three routes that say what is
2811
+ * wrong by name. Gate a mount on it, or simply
2812
+ * `await oauth.syncIndexes()` first, which is the same thing said directly.
2813
+ */
2814
+ get ready() {
2815
+ return ctx.indexes.ready;
2816
+ },
2773
2817
  /** Escape hatch. Prefer the APIs above; these carry no invariants. */
2774
2818
  models: ctx.models
2775
2819
  };
2776
2820
  }
2777
2821
 
2822
+ exports.CHATGPT_CONNECTOR_REDIRECT_URI_PATTERN = CHATGPT_CONNECTOR_REDIRECT_URI_PATTERN;
2823
+ exports.CHATGPT_LEGACY_REDIRECT_URI = CHATGPT_LEGACY_REDIRECT_URI;
2824
+ exports.CIMD_ALLOWED_HOSTS = CIMD_ALLOWED_HOSTS;
2825
+ exports.CLAUDE_CODE_REDIRECT_URIS = CLAUDE_CODE_REDIRECT_URIS;
2826
+ exports.CLAUDE_CONNECTOR_REDIRECT_URI = CLAUDE_CONNECTOR_REDIRECT_URI;
2778
2827
  exports.OAuthError = OAuthError;
2779
2828
  exports.RedirectableAuthError = RedirectableAuthError;
2780
2829
  exports.UnredirectableError = UnredirectableError;