@jeffjassky/oauth-host 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffjassky/oauth-host",
3
- "version": "0.2.0",
3
+ "version": "0.3.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>",
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. Only CIMD registrations are
404
- * public today; a `confidential` client can never downgrade itself by
405
- * omitting its secret.
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
  /**
@@ -574,16 +578,65 @@ export interface CreateClientSpec {
574
578
  trusted?: boolean;
575
579
  /** Supply a fixed id for a re-provisioned client. Generated when omitted. */
576
580
  clientId?: string;
581
+ /**
582
+ * How this client authenticates at `/token`. **Defaults to `confidential`**,
583
+ * so an existing caller is unaffected.
584
+ *
585
+ * `public` generates no secret at all: the registration is `client_id` plus
586
+ * PKCE, `secrets` is empty, and `rotateSecret()` on it throws. Register one
587
+ * for a client that takes a `client_id` and nothing else — Codex CLI's MCP
588
+ * login has `oauth_client_id` and no `oauth_client_secret` field — and cannot
589
+ * use CIMD because it publishes no metadata document.
590
+ */
591
+ type?: 'confidential' | 'public';
577
592
  }
578
593
 
579
- export interface CreatedClient {
594
+ /** What `clients.create({ type: 'confidential' })` and `rotateSecret()` return. */
595
+ export interface CreatedConfidentialClient {
580
596
  client: PublicClient;
581
597
  clientId: string;
598
+ type: 'confidential';
582
599
  /** Returned once. Only its SHA-256 is stored; there is no way to read it back. */
583
600
  clientSecret: string;
584
601
  }
585
602
 
603
+ /**
604
+ * What `clients.create({ type: 'public' })` returns.
605
+ *
606
+ * `clientSecret` is declared as `?: undefined` rather than omitted so that
607
+ * `created.clientSecret` still type-checks against the union below — and lands
608
+ * as `string | undefined`, which is what stops a provisioning script printing
609
+ * the word `undefined` into somebody's connector setup screen.
610
+ */
611
+ export interface CreatedPublicClient {
612
+ client: PublicClient;
613
+ clientId: string;
614
+ type: 'public';
615
+ clientSecret?: undefined;
616
+ }
617
+
618
+ /**
619
+ * Discriminated on `type`, not a single interface with an optional secret.
620
+ *
621
+ * The alternative — widening `clientSecret` to `string | undefined` on one
622
+ * shape — reads as source-compatible and is not: `const s: string =
623
+ * created.clientSecret` stops compiling either way. The difference is what
624
+ * happens to the code that does not annotate. With an optional field a
625
+ * provisioning script keeps compiling and prints `undefined`; with a union the
626
+ * caller has to say which kind of registration it asked for before it can reach
627
+ * the secret at all.
628
+ */
629
+ export type CreatedClient = CreatedConfidentialClient | CreatedPublicClient;
630
+
586
631
  export interface ClientsApi {
632
+ /**
633
+ * The overloads exist so the default path keeps its precise type. A spec with
634
+ * no `type` (or `type: 'confidential'`) returns a `clientSecret: string`
635
+ * exactly as before; only a caller that asked for `public`, or that passes a
636
+ * spec whose `type` is not known statically, has to narrow.
637
+ */
638
+ create(spec: CreateClientSpec & { type: 'public' }): Promise<CreatedPublicClient>;
639
+ create(spec: CreateClientSpec & { type?: 'confidential' }): Promise<CreatedConfidentialClient>;
587
640
  create(spec: CreateClientSpec): Promise<CreatedClient>;
588
641
  /**
589
642
  * Issue a second valid secret and retire the current one after `retireAfter`
@@ -591,9 +644,10 @@ export interface ClientsApi {
591
644
  * deployable without downtime.
592
645
  *
593
646
  * 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.
647
+ * one would hand the caller a credential the token endpoint refuses — which
648
+ * is also why the return type is the confidential member alone.
595
649
  */
596
- rotateSecret(clientId: string, opts?: { retireAfter?: number; label?: string }): Promise<CreatedClient>;
650
+ rotateSecret(clientId: string, opts?: { retireAfter?: number; label?: string }): Promise<CreatedConfidentialClient>;
597
651
  update(clientId: string, patch: Partial<Omit<CreateClientSpec, 'clientId'>>): Promise<PublicClient>;
598
652
  list(query?: { status?: 'active' | 'disabled'; limit?: number; skip?: number }): Promise<{ items: PublicClient[]; limit: number }>;
599
653
  get(clientId: string): Promise<PublicClient | null>;