@jeffjassky/oauth-host 0.1.0 → 0.3.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 +96 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +92 -34
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/types/index.d.ts +60 -6
- package/types/test-d.ts +0 -320
package/dist/index.js
CHANGED
|
@@ -22,9 +22,11 @@ function clientSchema() {
|
|
|
22
22
|
{
|
|
23
23
|
clientId: { type: String, required: true, unique: true },
|
|
24
24
|
name: { type: String, required: true },
|
|
25
|
-
// `public`
|
|
26
|
-
//
|
|
27
|
-
//
|
|
25
|
+
// `public` is a client that holds no secret and is bound by PKCE instead.
|
|
26
|
+
// Two ways in and they are independent: a CIMD row is written public, and
|
|
27
|
+
// `clients.create({ type: 'public' })` registers one by hand. The enum was
|
|
28
|
+
// written with this widening in mind, so it was one member rather than a
|
|
29
|
+
// migration.
|
|
28
30
|
type: { type: String, enum: ["confidential", "public"], default: "confidential" },
|
|
29
31
|
// How the row got here. A `cimd` row is re-derived from the client's own
|
|
30
32
|
// metadata document, so this is what tells the re-fetch path which fields
|
|
@@ -493,6 +495,7 @@ function resolveConfig(config) {
|
|
|
493
495
|
}
|
|
494
496
|
const store = rateLimits.store ?? memoryStore();
|
|
495
497
|
const normalizedMount = `/${String(mountPath).replace(/^\/+|\/+$/g, "")}`;
|
|
498
|
+
const indexes = { ready: false };
|
|
496
499
|
const ctx = {
|
|
497
500
|
models,
|
|
498
501
|
issuer,
|
|
@@ -524,6 +527,11 @@ function resolveConfig(config) {
|
|
|
524
527
|
cors: { tokenEndpoint: cors.tokenEndpoint ?? false, origins: cors.origins ?? [] },
|
|
525
528
|
clockSkewMs,
|
|
526
529
|
cimd: resolveCimd(clientIdMetadata, scopeIndex),
|
|
530
|
+
indexes,
|
|
531
|
+
async syncIndexes() {
|
|
532
|
+
await syncModelIndexes(models);
|
|
533
|
+
indexes.ready = true;
|
|
534
|
+
},
|
|
527
535
|
logger,
|
|
528
536
|
track,
|
|
529
537
|
async audit(entry) {
|
|
@@ -1351,6 +1359,15 @@ function assertResources(ctx, input) {
|
|
|
1351
1359
|
}
|
|
1352
1360
|
return [...input];
|
|
1353
1361
|
}
|
|
1362
|
+
function assertClientType(value) {
|
|
1363
|
+
if (value === void 0) return "confidential";
|
|
1364
|
+
if (value !== "confidential" && value !== "public") {
|
|
1365
|
+
throw new TypeError(
|
|
1366
|
+
`oauth-host: client type must be 'confidential' or 'public' (got ${JSON.stringify(value)})`
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
return value;
|
|
1370
|
+
}
|
|
1354
1371
|
function assertName(value) {
|
|
1355
1372
|
if (typeof value !== "string" || !value.trim()) {
|
|
1356
1373
|
throw new TypeError("oauth-host: a client needs a non-empty `name` \u2014 it is what the consent screen shows");
|
|
@@ -1372,35 +1389,38 @@ async function revokeTokensMatching(ctx, filter) {
|
|
|
1372
1389
|
return res.modifiedCount ?? 0;
|
|
1373
1390
|
}
|
|
1374
1391
|
function createClientsApi(ctx) {
|
|
1392
|
+
async function create(spec) {
|
|
1393
|
+
if (!spec || typeof spec !== "object") {
|
|
1394
|
+
throw new TypeError("oauth-host: clients.create(spec) requires a spec object");
|
|
1395
|
+
}
|
|
1396
|
+
const name = assertName(spec.name);
|
|
1397
|
+
const type = assertClientType(spec.type);
|
|
1398
|
+
const redirectUris = assertRedirectUris(spec.redirectUris);
|
|
1399
|
+
const allowedScopes = assertScopes(ctx, spec.allowedScopes);
|
|
1400
|
+
const allowedResources = assertResources(ctx, spec.allowedResources);
|
|
1401
|
+
const clientId = spec.clientId ?? generateClientId();
|
|
1402
|
+
const clientSecret = type === "confidential" ? generateClientSecret() : void 0;
|
|
1403
|
+
const doc = await ctx.models.Client.create({
|
|
1404
|
+
clientId,
|
|
1405
|
+
name,
|
|
1406
|
+
type,
|
|
1407
|
+
registration: "manual",
|
|
1408
|
+
trusted: Boolean(spec.trusted),
|
|
1409
|
+
// Only the digest is stored. `clientSecret` below is the only time the
|
|
1410
|
+
// raw value exists outside the caller's variable. Empty for a public
|
|
1411
|
+
// client, which is the same shape a CIMD row is written with.
|
|
1412
|
+
secrets: clientSecret ? [{ hash: sha256(clientSecret), label: "initial", createdAt: /* @__PURE__ */ new Date() }] : [],
|
|
1413
|
+
redirectUris,
|
|
1414
|
+
allowedScopes,
|
|
1415
|
+
allowedResources,
|
|
1416
|
+
branding: spec.branding ?? {},
|
|
1417
|
+
status: "active"
|
|
1418
|
+
});
|
|
1419
|
+
await ctx.audit({ type: "oauth.client_created", actor: "admin", clientId, meta: { type } });
|
|
1420
|
+
return clientSecret ? { client: toPublicClient(doc), clientId, type: "confidential", clientSecret } : { client: toPublicClient(doc), clientId, type: "public" };
|
|
1421
|
+
}
|
|
1375
1422
|
return {
|
|
1376
|
-
|
|
1377
|
-
if (!spec || typeof spec !== "object") {
|
|
1378
|
-
throw new TypeError("oauth-host: clients.create(spec) requires a spec object");
|
|
1379
|
-
}
|
|
1380
|
-
const name = assertName(spec.name);
|
|
1381
|
-
const redirectUris = assertRedirectUris(spec.redirectUris);
|
|
1382
|
-
const allowedScopes = assertScopes(ctx, spec.allowedScopes);
|
|
1383
|
-
const allowedResources = assertResources(ctx, spec.allowedResources);
|
|
1384
|
-
const clientId = spec.clientId ?? generateClientId();
|
|
1385
|
-
const clientSecret = generateClientSecret();
|
|
1386
|
-
const doc = await ctx.models.Client.create({
|
|
1387
|
-
clientId,
|
|
1388
|
-
name,
|
|
1389
|
-
type: "confidential",
|
|
1390
|
-
registration: "manual",
|
|
1391
|
-
trusted: Boolean(spec.trusted),
|
|
1392
|
-
// Only the digest is stored. `clientSecret` below is the only time the
|
|
1393
|
-
// raw value exists outside the caller's variable.
|
|
1394
|
-
secrets: [{ hash: sha256(clientSecret), label: "initial", createdAt: /* @__PURE__ */ new Date() }],
|
|
1395
|
-
redirectUris,
|
|
1396
|
-
allowedScopes,
|
|
1397
|
-
allowedResources,
|
|
1398
|
-
branding: spec.branding ?? {},
|
|
1399
|
-
status: "active"
|
|
1400
|
-
});
|
|
1401
|
-
await ctx.audit({ type: "oauth.client_created", actor: "admin", clientId });
|
|
1402
|
-
return { client: toPublicClient(doc), clientId, clientSecret };
|
|
1403
|
-
},
|
|
1423
|
+
create,
|
|
1404
1424
|
async rotateSecret(clientId, opts = {}) {
|
|
1405
1425
|
const doc = await ctx.models.Client.findOne({ clientId });
|
|
1406
1426
|
if (!doc) throw notFound(clientId);
|
|
@@ -1429,7 +1449,7 @@ function createClientsApi(ctx) {
|
|
|
1429
1449
|
clientId,
|
|
1430
1450
|
meta: { retireAfter, retiresAt }
|
|
1431
1451
|
});
|
|
1432
|
-
return { client: toPublicClient(doc), clientId, clientSecret };
|
|
1452
|
+
return { client: toPublicClient(doc), clientId, type: "confidential", clientSecret };
|
|
1433
1453
|
},
|
|
1434
1454
|
async update(clientId, patch) {
|
|
1435
1455
|
const doc = await ctx.models.Client.findOne({ clientId });
|
|
@@ -2473,8 +2493,17 @@ function createOAuthRouter(ctx) {
|
|
|
2473
2493
|
const ipKey = (req) => req.ip ?? "unknown";
|
|
2474
2494
|
const clientKey = (req) => clientIdFromRequest(req) ?? ipKey(req);
|
|
2475
2495
|
const cors = corsMiddleware(ctx);
|
|
2496
|
+
const requireIndexes = w((_req, _res, next) => {
|
|
2497
|
+
if (ctx.indexes.ready) return next();
|
|
2498
|
+
throw new OAuthError(
|
|
2499
|
+
503,
|
|
2500
|
+
"server_error",
|
|
2501
|
+
"This authorization server is not ready: `syncIndexes()` has not resolved yet. Await `oauth.syncIndexes()` at boot, before mounting the routers."
|
|
2502
|
+
);
|
|
2503
|
+
});
|
|
2476
2504
|
router.get(
|
|
2477
2505
|
"/authorize",
|
|
2506
|
+
requireIndexes,
|
|
2478
2507
|
rateLimit(ctx, "authorize", ipKey),
|
|
2479
2508
|
w(async (req, res) => {
|
|
2480
2509
|
let validated;
|
|
@@ -2533,6 +2562,7 @@ function createOAuthRouter(ctx) {
|
|
|
2533
2562
|
router.post(
|
|
2534
2563
|
"/token",
|
|
2535
2564
|
cors,
|
|
2565
|
+
requireIndexes,
|
|
2536
2566
|
form,
|
|
2537
2567
|
rateLimit(ctx, "token", clientKey),
|
|
2538
2568
|
w(async (req, res) => {
|
|
@@ -2637,6 +2667,7 @@ function createOAuthRouter(ctx) {
|
|
|
2637
2667
|
);
|
|
2638
2668
|
router.post(
|
|
2639
2669
|
"/consent/:requestId",
|
|
2670
|
+
requireIndexes,
|
|
2640
2671
|
rateLimit(ctx, "consent", ipKey),
|
|
2641
2672
|
w(async (req, res) => {
|
|
2642
2673
|
const user = await requireUser(ctx, req);
|
|
@@ -2738,6 +2769,16 @@ function corsMiddleware(ctx) {
|
|
|
2738
2769
|
};
|
|
2739
2770
|
}
|
|
2740
2771
|
|
|
2772
|
+
// src/server/vendors.ts
|
|
2773
|
+
var CLAUDE_CONNECTOR_REDIRECT_URI = "https://claude.ai/api/mcp/auth_callback";
|
|
2774
|
+
var CLAUDE_CODE_REDIRECT_URIS = [
|
|
2775
|
+
"http://localhost/callback",
|
|
2776
|
+
"http://127.0.0.1/callback"
|
|
2777
|
+
];
|
|
2778
|
+
var CHATGPT_LEGACY_REDIRECT_URI = "https://chatgpt.com/connector_platform_oauth_redirect";
|
|
2779
|
+
var CHATGPT_CONNECTOR_REDIRECT_URI_PATTERN = "https://chatgpt.com/connector/oauth/{callback_id}";
|
|
2780
|
+
var CIMD_ALLOWED_HOSTS = ["claude.ai", "chatgpt.com"];
|
|
2781
|
+
|
|
2741
2782
|
// src/server/index.ts
|
|
2742
2783
|
function createOAuthHost(config) {
|
|
2743
2784
|
const ctx = resolveConfig(config);
|
|
@@ -2761,13 +2802,30 @@ function createOAuthHost(config) {
|
|
|
2761
2802
|
* stops one user holding two live grants for the same client, and mongoose
|
|
2762
2803
|
* builds indexes in the background — a cold database will happily serve
|
|
2763
2804
|
* the write that violates it first. See standards/traps.md #3.
|
|
2805
|
+
*
|
|
2806
|
+
* Resolving this is also what flips `ready` and un-gates `/authorize`,
|
|
2807
|
+
* `POST /consent/:requestId` and `POST /token`.
|
|
2808
|
+
*/
|
|
2809
|
+
syncIndexes: () => ctx.syncIndexes(),
|
|
2810
|
+
/**
|
|
2811
|
+
* Has `syncIndexes()` resolved?
|
|
2812
|
+
*
|
|
2813
|
+
* A boolean, not a promise, and the difference is the failure mode. A
|
|
2814
|
+
* promise would have to exist from construction, so a host that never calls
|
|
2815
|
+
* `syncIndexes()` would await it forever — a boot-order mistake turning into
|
|
2816
|
+
* an unexplained hang with no message. A boolean is plainly `false`, cannot
|
|
2817
|
+
* be awaited by accident, and is backed by three routes that say what is
|
|
2818
|
+
* wrong by name. Gate a mount on it, or simply
|
|
2819
|
+
* `await oauth.syncIndexes()` first, which is the same thing said directly.
|
|
2764
2820
|
*/
|
|
2765
|
-
|
|
2821
|
+
get ready() {
|
|
2822
|
+
return ctx.indexes.ready;
|
|
2823
|
+
},
|
|
2766
2824
|
/** Escape hatch. Prefer the APIs above; these carry no invariants. */
|
|
2767
2825
|
models: ctx.models
|
|
2768
2826
|
};
|
|
2769
2827
|
}
|
|
2770
2828
|
|
|
2771
|
-
export { OAuthError, RedirectableAuthError, UnredirectableError, createModels, createOAuthHost, createUserAdapter, defaultResolveUser, syncModelIndexes };
|
|
2829
|
+
export { CHATGPT_CONNECTOR_REDIRECT_URI_PATTERN, CHATGPT_LEGACY_REDIRECT_URI, CIMD_ALLOWED_HOSTS, CLAUDE_CODE_REDIRECT_URIS, CLAUDE_CONNECTOR_REDIRECT_URI, OAuthError, RedirectableAuthError, UnredirectableError, createModels, createOAuthHost, createUserAdapter, defaultResolveUser, syncModelIndexes };
|
|
2772
2830
|
//# sourceMappingURL=index.js.map
|
|
2773
2831
|
//# sourceMappingURL=index.js.map
|