@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 +9 -1
- package/dist/index.cjs +50 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +46 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/types/test-d.ts +0 -320
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: [
|
|
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: () =>
|
|
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;
|