@jeffjassky/oauth-host 0.2.0 → 0.4.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/dist/index.cjs +982 -651
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +982 -651
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
- package/types/index.d.ts +73 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jeffjassky/oauth-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "OAuth 2.1 + OIDC authorization server for Express/Mongoose apps.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jeff Jassky <jeff@jeffjassky.com>",
|
|
@@ -43,6 +43,9 @@
|
|
|
43
43
|
"engines": {
|
|
44
44
|
"node": ">=20"
|
|
45
45
|
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"jose": "^6.2.10"
|
|
48
|
+
},
|
|
46
49
|
"scripts": {
|
|
47
50
|
"build": "tsup",
|
|
48
51
|
"dev": "tsup --watch",
|
package/types/index.d.ts
CHANGED
|
@@ -400,9 +400,13 @@ export interface OAuthClientDoc {
|
|
|
400
400
|
name: string;
|
|
401
401
|
/**
|
|
402
402
|
* `public` clients hold no secret and authenticate with `client_id` alone —
|
|
403
|
-
* PKCE is the binding that stands in for it.
|
|
404
|
-
*
|
|
405
|
-
*
|
|
403
|
+
* PKCE is the binding that stands in for it. A `confidential` client can
|
|
404
|
+
* never downgrade itself by omitting its secret.
|
|
405
|
+
*
|
|
406
|
+
* Orthogonal to `registration`: a CIMD row is always public, but a public row
|
|
407
|
+
* is not always CIMD. `clients.create({ type: 'public' })` registers one by
|
|
408
|
+
* hand, which is the only path open to a client that wants PKCE-only and
|
|
409
|
+
* publishes no metadata document (Codex CLI).
|
|
406
410
|
*/
|
|
407
411
|
type: 'confidential' | 'public';
|
|
408
412
|
/**
|
|
@@ -418,6 +422,19 @@ export interface OAuthClientDoc {
|
|
|
418
422
|
metadataFetchedAt?: Date;
|
|
419
423
|
/** Last `ETag`, replayed as `If-None-Match` so an unchanged document is a 304. */
|
|
420
424
|
metadataEtag?: string;
|
|
425
|
+
/**
|
|
426
|
+
* The usable (intersected) token endpoint auth method set for a CIMD
|
|
427
|
+
* client — what `validateMetadataDocument` computed from the document's
|
|
428
|
+
* offered methods ∩ what this server supports. Absent on a manual
|
|
429
|
+
* registration; its behavior derives from `type` exactly as before. A row
|
|
430
|
+
* with no value here supports `private_key_jwt` never — see
|
|
431
|
+
* `authenticateClient`.
|
|
432
|
+
*/
|
|
433
|
+
tokenEndpointAuthMethods?: string[];
|
|
434
|
+
/** `private_key_jwt` key source: a JWKS URL. Mutually exclusive with `jwks`. */
|
|
435
|
+
jwksUri?: string;
|
|
436
|
+
/** `private_key_jwt` key source: an inline JWK Set. Mutually exclusive with `jwksUri`. */
|
|
437
|
+
jwks?: unknown;
|
|
421
438
|
/** First-party marker. Reserved — v1 never skips consent. */
|
|
422
439
|
trusted: boolean;
|
|
423
440
|
/** Empty for a public client. There is no secret to store. */
|
|
@@ -574,16 +591,65 @@ export interface CreateClientSpec {
|
|
|
574
591
|
trusted?: boolean;
|
|
575
592
|
/** Supply a fixed id for a re-provisioned client. Generated when omitted. */
|
|
576
593
|
clientId?: string;
|
|
594
|
+
/**
|
|
595
|
+
* How this client authenticates at `/token`. **Defaults to `confidential`**,
|
|
596
|
+
* so an existing caller is unaffected.
|
|
597
|
+
*
|
|
598
|
+
* `public` generates no secret at all: the registration is `client_id` plus
|
|
599
|
+
* PKCE, `secrets` is empty, and `rotateSecret()` on it throws. Register one
|
|
600
|
+
* for a client that takes a `client_id` and nothing else — Codex CLI's MCP
|
|
601
|
+
* login has `oauth_client_id` and no `oauth_client_secret` field — and cannot
|
|
602
|
+
* use CIMD because it publishes no metadata document.
|
|
603
|
+
*/
|
|
604
|
+
type?: 'confidential' | 'public';
|
|
577
605
|
}
|
|
578
606
|
|
|
579
|
-
|
|
607
|
+
/** What `clients.create({ type: 'confidential' })` and `rotateSecret()` return. */
|
|
608
|
+
export interface CreatedConfidentialClient {
|
|
580
609
|
client: PublicClient;
|
|
581
610
|
clientId: string;
|
|
611
|
+
type: 'confidential';
|
|
582
612
|
/** Returned once. Only its SHA-256 is stored; there is no way to read it back. */
|
|
583
613
|
clientSecret: string;
|
|
584
614
|
}
|
|
585
615
|
|
|
616
|
+
/**
|
|
617
|
+
* What `clients.create({ type: 'public' })` returns.
|
|
618
|
+
*
|
|
619
|
+
* `clientSecret` is declared as `?: undefined` rather than omitted so that
|
|
620
|
+
* `created.clientSecret` still type-checks against the union below — and lands
|
|
621
|
+
* as `string | undefined`, which is what stops a provisioning script printing
|
|
622
|
+
* the word `undefined` into somebody's connector setup screen.
|
|
623
|
+
*/
|
|
624
|
+
export interface CreatedPublicClient {
|
|
625
|
+
client: PublicClient;
|
|
626
|
+
clientId: string;
|
|
627
|
+
type: 'public';
|
|
628
|
+
clientSecret?: undefined;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Discriminated on `type`, not a single interface with an optional secret.
|
|
633
|
+
*
|
|
634
|
+
* The alternative — widening `clientSecret` to `string | undefined` on one
|
|
635
|
+
* shape — reads as source-compatible and is not: `const s: string =
|
|
636
|
+
* created.clientSecret` stops compiling either way. The difference is what
|
|
637
|
+
* happens to the code that does not annotate. With an optional field a
|
|
638
|
+
* provisioning script keeps compiling and prints `undefined`; with a union the
|
|
639
|
+
* caller has to say which kind of registration it asked for before it can reach
|
|
640
|
+
* the secret at all.
|
|
641
|
+
*/
|
|
642
|
+
export type CreatedClient = CreatedConfidentialClient | CreatedPublicClient;
|
|
643
|
+
|
|
586
644
|
export interface ClientsApi {
|
|
645
|
+
/**
|
|
646
|
+
* The overloads exist so the default path keeps its precise type. A spec with
|
|
647
|
+
* no `type` (or `type: 'confidential'`) returns a `clientSecret: string`
|
|
648
|
+
* exactly as before; only a caller that asked for `public`, or that passes a
|
|
649
|
+
* spec whose `type` is not known statically, has to narrow.
|
|
650
|
+
*/
|
|
651
|
+
create(spec: CreateClientSpec & { type: 'public' }): Promise<CreatedPublicClient>;
|
|
652
|
+
create(spec: CreateClientSpec & { type?: 'confidential' }): Promise<CreatedConfidentialClient>;
|
|
587
653
|
create(spec: CreateClientSpec): Promise<CreatedClient>;
|
|
588
654
|
/**
|
|
589
655
|
* Issue a second valid secret and retire the current one after `retireAfter`
|
|
@@ -591,9 +657,10 @@ export interface ClientsApi {
|
|
|
591
657
|
* deployable without downtime.
|
|
592
658
|
*
|
|
593
659
|
* Throws on a **public** client. There is no secret to rotate, and returning
|
|
594
|
-
* one would hand the caller a credential the token endpoint refuses
|
|
660
|
+
* one would hand the caller a credential the token endpoint refuses — which
|
|
661
|
+
* is also why the return type is the confidential member alone.
|
|
595
662
|
*/
|
|
596
|
-
rotateSecret(clientId: string, opts?: { retireAfter?: number; label?: string }): Promise<
|
|
663
|
+
rotateSecret(clientId: string, opts?: { retireAfter?: number; label?: string }): Promise<CreatedConfidentialClient>;
|
|
597
664
|
update(clientId: string, patch: Partial<Omit<CreateClientSpec, 'clientId'>>): Promise<PublicClient>;
|
|
598
665
|
list(query?: { status?: 'active' | 'disabled'; limit?: number; skip?: number }): Promise<{ items: PublicClient[]; limit: number }>;
|
|
599
666
|
get(clientId: string): Promise<PublicClient | null>;
|