@cauthyhq/node 1.2.0 → 1.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +16 -28
  3. package/dist/{cauthy-CCSSj3Lg.d.mts → cauthy-CsWUgyv_.d.mts} +42 -145
  4. package/dist/{cauthy-C7gZ-pbv.d.ts → cauthy-DHLBEuqP.d.ts} +42 -145
  5. package/dist/express.d.mts +2 -5
  6. package/dist/express.d.ts +2 -5
  7. package/dist/express.js +1 -2
  8. package/dist/express.js.map +1 -1
  9. package/dist/express.mjs +1 -2
  10. package/dist/express.mjs.map +1 -1
  11. package/dist/fastify.d.mts +1 -3
  12. package/dist/fastify.d.ts +1 -3
  13. package/dist/fastify.js +0 -2
  14. package/dist/fastify.js.map +1 -1
  15. package/dist/fastify.mjs +0 -2
  16. package/dist/fastify.mjs.map +1 -1
  17. package/dist/hono.d.mts +2 -5
  18. package/dist/hono.d.ts +2 -5
  19. package/dist/hono.js +1 -2
  20. package/dist/hono.js.map +1 -1
  21. package/dist/hono.mjs +1 -2
  22. package/dist/hono.mjs.map +1 -1
  23. package/dist/index.d.mts +1 -3
  24. package/dist/index.d.ts +1 -3
  25. package/dist/index.js +187 -530
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +180 -520
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/mcp.js.map +1 -1
  30. package/dist/mcp.mjs.map +1 -1
  31. package/dist/next.d.mts +1 -3
  32. package/dist/next.d.ts +1 -3
  33. package/dist/testing.d.mts +11 -5
  34. package/dist/testing.d.ts +11 -5
  35. package/dist/testing.js +52 -50
  36. package/dist/testing.js.map +1 -1
  37. package/dist/testing.mjs +52 -50
  38. package/dist/testing.mjs.map +1 -1
  39. package/package.json +5 -14
  40. package/dist/identity-BTJ8NB_w.d.mts +0 -27
  41. package/dist/identity-BTJ8NB_w.d.ts +0 -27
  42. package/dist/keys-D5k0omTb.d.mts +0 -64
  43. package/dist/keys-D5k0omTb.d.ts +0 -64
  44. package/dist/keys.d.mts +0 -8
  45. package/dist/keys.d.ts +0 -8
  46. package/dist/keys.js +0 -157
  47. package/dist/keys.js.map +0 -1
  48. package/dist/keys.mjs +0 -129
  49. package/dist/keys.mjs.map +0 -1
@@ -1,95 +1,5 @@
1
- import { K as KeyStore, P as PactKeys } from './keys-D5k0omTb.js';
2
1
  import { JWTVerifyGetKey, JWK } from 'jose';
3
2
  import { CauthyTheme } from './theme.js';
4
- import { I as Identity } from './identity-BTJ8NB_w.js';
5
-
6
- /** What createCauthy is given to run by contract. */
7
- interface ContractOptions {
8
- /** The context graph, as `cauthy contracts pull` writes it (contracts/graph.json). */
9
- graph: unknown;
10
- /** This application's system in the graph: `"acme-crm"`. */
11
- system: string;
12
- /** The link to read, when the system has more than one with Cauthy. */
13
- link?: string;
14
- /** Cauthy's system in the graph. Default: `"cauthy"`. */
15
- provider?: string;
16
- }
17
- /** What an application reads of its contract. */
18
- interface ResolvedContract {
19
- system: string;
20
- link: string;
21
- /** Cauthy's public address. */
22
- issuer: string;
23
- /** The workspace's id, and its slug (what the application's own screens send). */
24
- tenantId: string;
25
- tenant: string;
26
- /** The public client people sign in with: `pact.<link>`. */
27
- clientId: string;
28
- /** Where people may return after signing in. */
29
- redirectUris: string[];
30
- /** Whether Cauthy asks the system's proof on the code exchange now (`systemProof` is `required`). */
31
- requireSystemProof: boolean;
32
- /** How the link asks for the proof: never, from the first proved sign-in on (`auto`), or always. */
33
- systemProof: 'off' | 'auto' | 'required';
34
- /** Whether Cauthy issues no token through this link before the address is confirmed. */
35
- requireVerifiedEmail: boolean;
36
- /** Whether the link lets its people ask to stay signed in. */
37
- persistentSessions: boolean;
38
- /** The system's own client, when the link gives it one: `pact.<link>.service`. */
39
- serviceClientId?: string;
40
- /** Where the system publishes its keys, and the path of that address. */
41
- jwksUrl?: string;
42
- jwksPath?: string;
43
- /** Where the system receives Cauthy's events, when it says. */
44
- eventsUrl?: string;
45
- }
46
- /**
47
- * The variables of the standard mode. With a contract none may be set: two
48
- * sources of the same thing, one silently winning, is how a system ends up
49
- * signing people in through a client nobody meant.
50
- */
51
- declare const STANDARD_VARIABLES: readonly ["CAUTHY_ISSUER", "CAUTHY_CLIENT_ID", "CAUTHY_CLIENT_SECRET", "CAUTHY_SECRET", "CAUTHY_SYSTEM", "CAUTHY_TENANT", "CAUTHY_SYSTEM_KEY", "CAUTHY_SYSTEM_KEY_ID"];
52
- /** Refuses to run by contract with a variable of the standard mode set: names it. */
53
- declare function failClosed(environment: Record<string, string | undefined> | undefined): void;
54
- /** Reads what the graph says of the system's link with Cauthy. */
55
- declare function readContract(options: ContractOptions): ResolvedContract;
56
- /** One difference between a contract and what Cauthy holds. */
57
- interface PactItem {
58
- kind: 'client' | 'redirect_uri' | 'grant_type' | 'role' | 'organization' | 'require_system_proof' | 'persistent_sessions' | 'jwks_url';
59
- client_id?: string;
60
- value: string;
61
- detail?: string;
62
- }
63
- /** How the system stands against its contract, as Cauthy checks it (GET /v1/pact/status). Moments are UTC. */
64
- interface PactStatus {
65
- system: string;
66
- tenant_id: string;
67
- checked_at: string;
68
- /** When Cauthy's daily reconciliation last checked it; null before the first. */
69
- reconciled_at: string | null;
70
- in_step: boolean;
71
- /** In Cauthy, and the contract does not name it. Nothing is ever deleted for it. */
72
- outside: PactItem[];
73
- /** Named by the contract, and Cauthy does not hold it as it says. */
74
- missing: PactItem[];
75
- contract: {
76
- clients: {
77
- client_id: string;
78
- link: string;
79
- purpose: 'sign_in' | 'service';
80
- redirect_uris?: string[];
81
- grant_types: string[];
82
- require_system_proof?: boolean;
83
- jwks_url?: string;
84
- }[];
85
- roles?: Record<string, string[]>;
86
- };
87
- held: {
88
- clients: Record<string, unknown>[];
89
- roles: string[];
90
- organizations: Record<string, number>;
91
- };
92
- }
93
3
 
94
4
  /** Something that happened in Cauthy, as a subscriber is told. Moments are UTC. */
95
5
  interface CauthyEvent {
@@ -157,7 +67,7 @@ interface CauthyOptions {
157
67
  * `CAUTHY_KEY`. With it nothing else is read: the workspace, the system and
158
68
  * the application's addresses come from Cauthy (`GET /v1/connect/self`),
159
69
  * and passing `issuer`, `clientId`, `clientSecret`, `secret`, `system`,
160
- * `tenant`, `audience`, `tenantId`, `systemKey` or `rootKey` is refused.
70
+ * `tenant`, `audience`, `tenantId` or `systemKey` is refused.
161
71
  */
162
72
  key?: string;
163
73
  /** The key being replaced, during a rotation: read, never written with. Default: `CAUTHY_KEY_PREVIOUS`. */
@@ -203,8 +113,8 @@ interface CauthyOptions {
203
113
  */
204
114
  audience?: string;
205
115
  /**
206
- * The workspace's id (a lowercase UUID) for `auth.verifyToken`. By contract
207
- * it comes from the link's `tenant_id`; otherwise: this, or `CAUTHY_TENANT_ID`.
116
+ * The workspace's id (a lowercase UUID) for `auth.verifyToken`. On
117
+ * CAUTHY_KEY it is Cauthy's answer; otherwise: this, or `CAUTHY_TENANT_ID`.
208
118
  */
209
119
  tenantId?: string;
210
120
  /**
@@ -268,23 +178,18 @@ interface CauthyOptions {
268
178
  request?: number;
269
179
  };
270
180
  /**
271
- * Run by contract: the issuer, the workspace, the clients and where the
272
- * application's keys are published all come from its link in the network's
273
- * context graph, and none of the CAUTHY_* variables may be set. See
274
- * "Connect a system by contract".
275
- *
276
- * createCauthy({ contract: { graph, system: 'acme-crm' }, keys: postgresKeyStore(query) })
277
- */
278
- contract?: ContractOptions;
279
- /** Where the application keeps its own key, with a contract (@cauthyhq/node/keys). */
280
- keys?: KeyStore;
281
- /**
282
- * The one secret of a contracted application: every key it needs is
283
- * derived from it. 32 random bytes or more. Default: `PACT_ROOT_KEY`.
181
+ * Removed in 1.3: contracts are retired. Given, `createCauthy` refuses to
182
+ * start (as it does with `PACT_ROOT_KEY` or `PACT_ROOT_KEY_PREVIOUS` set):
183
+ * run on CAUTHY_KEY alone, and protect sign-in with the "Protect sign-in"
184
+ * switches on the application's card in Cauthy's dashboard.
284
185
  */
285
- rootKey?: string;
286
- /** The root key before the current one, while rotating it. Default: `PACT_ROOT_KEY_PREVIOUS`. */
287
- rootKeyPrevious?: string;
186
+ contract?: never;
187
+ /** Removed in 1.3 with contracts: given, `createCauthy` refuses to start. */
188
+ keys?: never;
189
+ /** Removed in 1.3 with contracts (it was `PACT_ROOT_KEY`): given, `createCauthy` refuses to start. */
190
+ rootKey?: never;
191
+ /** Removed in 1.3 with contracts (it was `PACT_ROOT_KEY_PREVIOUS`): given, `createCauthy` refuses to start. */
192
+ rootKeyPrevious?: never;
288
193
  /**
289
194
  * Where `auth.webhook` remembers the deliveries it took, so one sent twice
290
195
  * is refused. Default: this process's memory. Give a shared one (a table)
@@ -411,7 +316,7 @@ declare class CauthyConfigError extends Error {
411
316
  constructor(message: string);
412
317
  }
413
318
  /** Why a CAUTHY_KEY was refused (key/cauthy-key.md §3, §8, §9). */
414
- type CauthyKeyReason = 'key_format' | 'key_version' | 'key_checksum' | 'key_issuer' | 'key_client' | 'key_secret' | 'key_contract_issuer' | 'key_previous_mismatch';
319
+ type CauthyKeyReason = 'key_format' | 'key_version' | 'key_checksum' | 'key_issuer' | 'key_client' | 'key_secret' | 'key_previous_mismatch';
415
320
  /** A refused CAUTHY_KEY. The message names the reason, never the key. */
416
321
  declare class CauthyKeyError extends CauthyConfigError {
417
322
  readonly reason: CauthyKeyReason;
@@ -703,17 +608,10 @@ interface SystemOptions {
703
608
  * client registered to be sent back to. Default: `CAUTHY_ACCEPT_INVITE_URL`.
704
609
  */
705
610
  acceptInviteUrl?: string;
706
- /**
707
- * The application's own keys, kept in its database (contract mode): it
708
- * signs with the newest and publishes them. Takes the place of `key`.
709
- */
710
- keys?: PactKeys;
711
- /** The client the keys prove: the system's own, beside the sign-in client. */
712
- keysClientId?: string;
713
611
  /**
714
612
  * CAUTHY_KEY's Ed25519 key (key/cauthy-key.md §6) and the client it proves:
715
- * every token is asked with an EdDSA assertion. Takes the place of `key`,
716
- * `keys` and the secret.
613
+ * every token is asked with an EdDSA assertion. Takes the place of `key`
614
+ * and the secret.
717
615
  */
718
616
  signer?: {
719
617
  clientId: string;
@@ -939,8 +837,8 @@ interface VerifyTokenOptions {
939
837
  /**
940
838
  * The client this backend signs people in through. When set, a person's
941
839
  * token issued through another client (an `azp` naming another) is refused
942
- * with `wrong_client` (rule R15); a token with no `azp` passes. In contract
943
- * mode it is the link's sign-in client. Default: none, and `azp` is not checked.
840
+ * with `wrong_client` (rule R15); a token with no `azp` passes. Default:
841
+ * none, and `azp` is not checked.
944
842
  */
945
843
  clientId?: string;
946
844
  }
@@ -988,6 +886,25 @@ declare function createVerifierFromEnv(options?: Omit<VerifyTokenOptions, 'issue
988
886
  */
989
887
  type MountTarget = string | ((request: Request) => Response | Promise<Response>);
990
888
 
889
+ /** What `GET /v1/connect/self` answers. */
890
+ interface Identity {
891
+ issuer: string;
892
+ audience: string;
893
+ tenant: {
894
+ id: string;
895
+ slug: string;
896
+ };
897
+ system: string;
898
+ client_id: string;
899
+ sign_in_client_id: string;
900
+ app_urls: string[];
901
+ callback_path: string;
902
+ key: {
903
+ kid: string;
904
+ retires_at: string | null;
905
+ } | null;
906
+ }
907
+
991
908
  /** What `resolve` answers: who is there, and the cookies that must go back. */
992
909
  interface Resolved {
993
910
  session: Session | null;
@@ -1071,10 +988,9 @@ interface Cauthy {
1071
988
  /**
1072
989
  * Verifies a Cauthy access token (or a request's `Authorization: Bearer`) for
1073
990
  * an API of this application: `verifyToken` with this instance's issuer, its
1074
- * workspace (by contract, the link's `tenant_id`; otherwise `tenantId` or
1075
- * `CAUTHY_TENANT_ID`) and its audience (`audience` or `CAUTHY_AUDIENCE`).
1076
- * By contract it also refuses a person's token issued through another
1077
- * client than the link's (`wrong_client`).
991
+ * workspace (on CAUTHY_KEY, Cauthy's answer; otherwise `tenantId` or
992
+ * `CAUTHY_TENANT_ID`) and its audience (on CAUTHY_KEY, Cauthy's answer;
993
+ * otherwise `audience` or `CAUTHY_AUDIENCE`).
1078
994
  */
1079
995
  verifyToken(from: string | Request | Headers, options?: Partial<VerifyTokenOptions>): Promise<TokenIdentity>;
1080
996
  /** The same, built once: throws `CauthyConfigError` now if the audience or the workspace is missing. */
@@ -1085,11 +1001,6 @@ interface Cauthy {
1085
1001
  * `systemKey`.
1086
1002
  */
1087
1003
  system: CauthySystem;
1088
- /**
1089
- * The application's contract, when it runs by one: what its link says,
1090
- * its own keys, and how it stands against its contract in Cauthy.
1091
- */
1092
- pact?: CauthyPact;
1093
1004
  /** What the browser may be told of a session. */
1094
1005
  toPublic(session: Session): Promise<PublicSession>;
1095
1006
  /** Paths of this application that start a sign-in, a sign-out, or open the account page. */
@@ -1101,20 +1012,6 @@ interface Cauthy {
1101
1012
  };
1102
1013
  options: Readonly<Required<Pick<CauthyOptions, 'issuer' | 'clientId' | 'basePath' | 'afterSignIn' | 'afterSignOut' | 'cookieName'>>>;
1103
1014
  }
1104
- /** A contracted application's view of its contract (`auth.pact`). */
1105
- interface CauthyPact {
1106
- /** What its link in the graph says. */
1107
- contract: ResolvedContract;
1108
- /** Its own keys: the one that signs, the ones published, rotating them. */
1109
- keys: PactKeys;
1110
- /**
1111
- * The response for its jwks_url: the published keys. `handler` serves it
1112
- * at that path too, when the route reaches it.
1113
- */
1114
- jwks(): Promise<Response>;
1115
- /** How it stands against its contract, as Cauthy checks it now. For a health endpoint. */
1116
- status(): Promise<PactStatus>;
1117
- }
1118
1015
  declare function createCauthy(given?: CauthyOptions): Cauthy;
1119
1016
 
1120
- export { createCauthy as $, type AttributeType as A, type PolicyRole as B, type Cauthy as C, type PublicSession as D, type PushPolicyOptions as E, type Resolved as F, type ResolvedContract as G, type RoleOf as H, STANDARD_VARIABLES as I, type JsonValue as J, type SessionOrganization as K, type SessionStatus as L, type MountTarget as M, type NewMember as N, type Organization as O, type PactItem as P, type SessionUser as Q, type ReplayStore as R, type Session as S, type SystemKey as T, type SystemOptions as U, type SystemPolicy as V, type TokenIdentity as W, type TokenRefusal as X, type VerifyTokenOptions as Y, type VerifyWebhookOptions as Z, can as _, type CatalogResource as a, createSystem as a0, createTokenVerifier as a1, createVerifierFromEnv as a2, definePolicy as a3, failClosed as a4, memoryReplayStore as a5, policySource as a6, pullCatalog as a7, pullPolicy as a8, pushCatalog as a9, pushPolicy as aa, readContract as ab, remoteKeys as ac, verifyToken as ad, verifyWebhook as ae, CauthyApiError as b, CauthyConfigError as c, type CauthyEvent as d, CauthyKeyError as e, type CauthyKeyReason as f, type CauthyOptions as g, type CauthyPact as h, type CauthySystem as i, CauthyTokenError as j, CauthyUnavailableError as k, CauthyWebhookError as l, type CheckOf as m, type CheckQuestion as n, type CheckResult as o, type CheckRule as p, type ConditionalPermission as q, type ContractOptions as r, type OrganizationMember as s, type OrganizationRef as t, type PactStatus as u, type PermissionOf as v, type Permitted as w, type Policy as x, type PolicyDefinition as y, type PolicyPermission as z };
1017
+ export { memoryReplayStore as $, type AttributeType as A, type RoleOf as B, type Cauthy as C, type SessionOrganization as D, type SessionStatus as E, type SessionUser as F, type SystemKey as G, type SystemOptions as H, type Identity as I, type JsonValue as J, type SystemPolicy as K, type TokenRefusal as L, type MountTarget as M, type NewMember as N, type Organization as O, type PermissionOf as P, type VerifyWebhookOptions as Q, type ReplayStore as R, type Session as S, type TokenIdentity as T, can as U, type VerifyTokenOptions as V, createCauthy as W, createSystem as X, createTokenVerifier as Y, createVerifierFromEnv as Z, definePolicy as _, type CatalogResource as a, policySource as a0, pullCatalog as a1, pullPolicy as a2, pushCatalog as a3, pushPolicy as a4, remoteKeys as a5, verifyToken as a6, verifyWebhook as a7, CauthyApiError as b, CauthyConfigError as c, type CauthyEvent as d, CauthyKeyError as e, type CauthyKeyReason as f, type CauthyOptions as g, type CauthySystem as h, CauthyTokenError as i, CauthyUnavailableError as j, CauthyWebhookError as k, type CheckOf as l, type CheckQuestion as m, type CheckResult as n, type CheckRule as o, type ConditionalPermission as p, type OrganizationMember as q, type OrganizationRef as r, type Permitted as s, type Policy as t, type PolicyDefinition as u, type PolicyPermission as v, type PolicyRole as w, type PublicSession as x, type PushPolicyOptions as y, type Resolved as z };
@@ -1,9 +1,7 @@
1
1
  import { IncomingMessage, ServerResponse } from 'node:http';
2
- import { S as Session, C as Cauthy } from './cauthy-CCSSj3Lg.mjs';
3
- import './keys-D5k0omTb.mjs';
2
+ import { S as Session, C as Cauthy } from './cauthy-CsWUgyv_.mjs';
4
3
  import 'jose';
5
4
  import './theme.mjs';
6
- import './identity-BTJ8NB_w.mjs';
7
5
 
8
6
  declare global {
9
7
  namespace Express {
@@ -15,8 +13,7 @@ declare global {
15
13
  }
16
14
  type Next = (error?: unknown) => void;
17
15
  /**
18
- * Serves the routes under the handler's `basePath` (and, by contract, the
19
- * application's published key), and gives every other
16
+ * Serves the routes under the handler's `basePath`, and gives every other
20
17
  * request `req.cauthy()`. Mount it once, before the routes that use it.
21
18
  */
22
19
  declare function cauthyExpress(auth: Cauthy): (req: IncomingMessage, res: ServerResponse, next: Next) => void;
package/dist/express.d.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  import { IncomingMessage, ServerResponse } from 'node:http';
2
- import { S as Session, C as Cauthy } from './cauthy-C7gZ-pbv.js';
3
- import './keys-D5k0omTb.js';
2
+ import { S as Session, C as Cauthy } from './cauthy-DHLBEuqP.js';
4
3
  import 'jose';
5
4
  import './theme.js';
6
- import './identity-BTJ8NB_w.js';
7
5
 
8
6
  declare global {
9
7
  namespace Express {
@@ -15,8 +13,7 @@ declare global {
15
13
  }
16
14
  type Next = (error?: unknown) => void;
17
15
  /**
18
- * Serves the routes under the handler's `basePath` (and, by contract, the
19
- * application's published key), and gives every other
16
+ * Serves the routes under the handler's `basePath`, and gives every other
20
17
  * request `req.cauthy()`. Mount it once, before the routes that use it.
21
18
  */
22
19
  declare function cauthyExpress(auth: Cauthy): (req: IncomingMessage, res: ServerResponse, next: Next) => void;
package/dist/express.js CHANGED
@@ -59,11 +59,10 @@ function addCookies(res, cookies) {
59
59
  }
60
60
  function cauthyExpress(auth) {
61
61
  const base = auth.options.basePath;
62
- const key = auth.pact?.contract.jwksPath;
63
62
  return function cauthy(req, res, next) {
64
63
  const request = req;
65
64
  const path = (request.originalUrl ?? request.url ?? "/").split("?")[0];
66
- if (path === base || path.startsWith(`${base}/`) || path === key) {
65
+ if (path === base || path.startsWith(`${base}/`)) {
67
66
  void (async () => {
68
67
  const answer = await auth.handler(await toWebRequest(request, true));
69
68
  res.statusCode = answer.status;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/express.ts"],"sourcesContent":["// Express (and anything else that speaks Node's http): the routes, and who is\n// signed in on every request.\n//\n// app.use(cauthyExpress(auth))\n//\n// app.get('/api/me', async (req, res) => {\n// const session = await req.cauthy()\n// if (!session) return res.status(401).json({ error: 'unauthenticated' })\n// res.json(session.user)\n// })\n//\n// app.get('/api/books', requireSession, (req, res) => { … })\n//\n// A session that needs renewing is renewed on the spot, and its new cookie\n// leaves with whatever the route answers.\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { Cauthy } from './cauthy'\nimport type { Session } from './types'\n\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace Express {\n interface Request {\n /** Who is signed in, or null. Asked once per request. */\n cauthy(): Promise<Session | null>\n }\n }\n}\n\n/** A request as Node gives it, with what Express adds when it is Express. */\ntype NodeRequest = IncomingMessage & { originalUrl?: string; protocol?: string; ip?: string; body?: unknown; cauthy?: () => Promise<Session | null> }\ntype Next = (error?: unknown) => void\n\nfunction originOf(req: NodeRequest): string {\n const secure = (req.socket as { encrypted?: boolean } | undefined)?.encrypted === true\n return `${req.protocol ?? (secure ? 'https' : 'http')}://${req.headers.host ?? 'localhost'}`\n}\n\nasync function bodyOf(req: NodeRequest): Promise<BodyInit | undefined> {\n if (req.method === 'GET' || req.method === 'HEAD') return undefined\n // A body parser was here first (express.json()): what it read is the body.\n if (req.body !== undefined && req.body !== null) {\n if (typeof req.body === 'string') return req.body\n if (req.body instanceof Uint8Array) return new Uint8Array(req.body)\n return JSON.stringify(req.body)\n }\n const chunks: Uint8Array[] = []\n for await (const chunk of req) chunks.push(typeof chunk === 'string' ? new TextEncoder().encode(chunk) : (chunk as Uint8Array))\n return chunks.length === 0 ? undefined : new Blob(chunks as BlobPart[])\n}\n\nasync function toWebRequest(req: NodeRequest, withBody: boolean): Promise<Request> {\n const headers = new Headers()\n for (const [k, v] of Object.entries(req.headers)) {\n if (Array.isArray(v)) for (const one of v) headers.append(k, one)\n else if (v !== undefined) headers.set(k, v)\n }\n // Express knows who is calling (it applies `trust proxy`): that address is\n // the one passed on to Cauthy, whatever the header claimed.\n if (typeof req.ip === 'string' && req.ip !== '') headers.set('x-forwarded-for', req.ip)\n return new Request(new URL(req.originalUrl ?? req.url ?? '/', originOf(req)), {\n method: req.method,\n headers,\n body: withBody ? await bodyOf(req) : undefined,\n })\n}\n\nfunction addCookies(res: ServerResponse, cookies: string[]): void {\n if (cookies.length === 0 || res.headersSent) return\n const had = res.getHeader('set-cookie')\n res.setHeader('set-cookie', [...(Array.isArray(had) ? had : typeof had === 'string' ? [had] : []), ...cookies])\n}\n\n/**\n * Serves the routes under the handler's `basePath` (and, by contract, the\n * application's published key), and gives every other\n * request `req.cauthy()`. Mount it once, before the routes that use it.\n */\nexport function cauthyExpress(auth: Cauthy) {\n const base = auth.options.basePath\n // By contract the application also publishes its key, outside basePath.\n const key = auth.pact?.contract.jwksPath\n return function cauthy(req: IncomingMessage, res: ServerResponse, next: Next): void {\n const request = req as NodeRequest\n const path = (request.originalUrl ?? request.url ?? '/').split('?')[0]\n\n if (path === base || path.startsWith(`${base}/`) || path === key) {\n void (async () => {\n const answer = await auth.handler(await toWebRequest(request, true))\n res.statusCode = answer.status\n answer.headers.forEach((value, name) => {\n if (name !== 'set-cookie') res.setHeader(name, value)\n })\n addCookies(res, answer.headers.getSetCookie())\n res.end(answer.body === null ? undefined : Buffer.from(await answer.arrayBuffer()))\n })().catch(next)\n return\n }\n\n let asked: Promise<Session | null> | undefined\n request.cauthy = () =>\n (asked ??= (async () => {\n const resolved = await auth.resolve(await toWebRequest(request, false))\n addCookies(res, resolved.cookies)\n return resolved.session\n })())\n next()\n }\n}\n\n/** For a route only a signed-in person may call: answers 401 otherwise. Needs `cauthyExpress` mounted before it. */\nexport function requireSession(req: IncomingMessage, res: ServerResponse, next: Next): void {\n const ask = (req as NodeRequest).cauthy\n if (!ask) return next(new Error('@cauthyhq/node: mount cauthyExpress(auth) before requireSession'))\n ask().then((session) => {\n if (session !== null) return next()\n res.statusCode = 401\n res.setHeader('content-type', 'application/json; charset=utf-8')\n res.setHeader('x-cauthy-session', 'none')\n res.end(JSON.stringify({ error: 'unauthenticated' }))\n }, next)\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/express.ts"],"sourcesContent":["// Express (and anything else that speaks Node's http): the routes, and who is\n// signed in on every request.\n//\n// app.use(cauthyExpress(auth))\n//\n// app.get('/api/me', async (req, res) => {\n// const session = await req.cauthy()\n// if (!session) return res.status(401).json({ error: 'unauthenticated' })\n// res.json(session.user)\n// })\n//\n// app.get('/api/books', requireSession, (req, res) => { … })\n//\n// A session that needs renewing is renewed on the spot, and its new cookie\n// leaves with whatever the route answers.\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { Cauthy } from './cauthy'\nimport type { Session } from './types'\n\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace Express {\n interface Request {\n /** Who is signed in, or null. Asked once per request. */\n cauthy(): Promise<Session | null>\n }\n }\n}\n\n/** A request as Node gives it, with what Express adds when it is Express. */\ntype NodeRequest = IncomingMessage & { originalUrl?: string; protocol?: string; ip?: string; body?: unknown; cauthy?: () => Promise<Session | null> }\ntype Next = (error?: unknown) => void\n\nfunction originOf(req: NodeRequest): string {\n const secure = (req.socket as { encrypted?: boolean } | undefined)?.encrypted === true\n return `${req.protocol ?? (secure ? 'https' : 'http')}://${req.headers.host ?? 'localhost'}`\n}\n\nasync function bodyOf(req: NodeRequest): Promise<BodyInit | undefined> {\n if (req.method === 'GET' || req.method === 'HEAD') return undefined\n // A body parser was here first (express.json()): what it read is the body.\n if (req.body !== undefined && req.body !== null) {\n if (typeof req.body === 'string') return req.body\n if (req.body instanceof Uint8Array) return new Uint8Array(req.body)\n return JSON.stringify(req.body)\n }\n const chunks: Uint8Array[] = []\n for await (const chunk of req) chunks.push(typeof chunk === 'string' ? new TextEncoder().encode(chunk) : (chunk as Uint8Array))\n return chunks.length === 0 ? undefined : new Blob(chunks as BlobPart[])\n}\n\nasync function toWebRequest(req: NodeRequest, withBody: boolean): Promise<Request> {\n const headers = new Headers()\n for (const [k, v] of Object.entries(req.headers)) {\n if (Array.isArray(v)) for (const one of v) headers.append(k, one)\n else if (v !== undefined) headers.set(k, v)\n }\n // Express knows who is calling (it applies `trust proxy`): that address is\n // the one passed on to Cauthy, whatever the header claimed.\n if (typeof req.ip === 'string' && req.ip !== '') headers.set('x-forwarded-for', req.ip)\n return new Request(new URL(req.originalUrl ?? req.url ?? '/', originOf(req)), {\n method: req.method,\n headers,\n body: withBody ? await bodyOf(req) : undefined,\n })\n}\n\nfunction addCookies(res: ServerResponse, cookies: string[]): void {\n if (cookies.length === 0 || res.headersSent) return\n const had = res.getHeader('set-cookie')\n res.setHeader('set-cookie', [...(Array.isArray(had) ? had : typeof had === 'string' ? [had] : []), ...cookies])\n}\n\n/**\n * Serves the routes under the handler's `basePath`, and gives every other\n * request `req.cauthy()`. Mount it once, before the routes that use it.\n */\nexport function cauthyExpress(auth: Cauthy) {\n const base = auth.options.basePath\n return function cauthy(req: IncomingMessage, res: ServerResponse, next: Next): void {\n const request = req as NodeRequest\n const path = (request.originalUrl ?? request.url ?? '/').split('?')[0]\n\n if (path === base || path.startsWith(`${base}/`)) {\n void (async () => {\n const answer = await auth.handler(await toWebRequest(request, true))\n res.statusCode = answer.status\n answer.headers.forEach((value, name) => {\n if (name !== 'set-cookie') res.setHeader(name, value)\n })\n addCookies(res, answer.headers.getSetCookie())\n res.end(answer.body === null ? undefined : Buffer.from(await answer.arrayBuffer()))\n })().catch(next)\n return\n }\n\n let asked: Promise<Session | null> | undefined\n request.cauthy = () =>\n (asked ??= (async () => {\n const resolved = await auth.resolve(await toWebRequest(request, false))\n addCookies(res, resolved.cookies)\n return resolved.session\n })())\n next()\n }\n}\n\n/** For a route only a signed-in person may call: answers 401 otherwise. Needs `cauthyExpress` mounted before it. */\nexport function requireSession(req: IncomingMessage, res: ServerResponse, next: Next): void {\n const ask = (req as NodeRequest).cauthy\n if (!ask) return next(new Error('@cauthyhq/node: mount cauthyExpress(auth) before requireSession'))\n ask().then((session) => {\n if (session !== null) return next()\n res.statusCode = 401\n res.setHeader('content-type', 'application/json; charset=utf-8')\n res.setHeader('x-cauthy-session', 'none')\n res.end(JSON.stringify({ error: 'unauthenticated' }))\n }, next)\n}\n"],"mappings":"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","names":[]}
package/dist/express.mjs CHANGED
@@ -34,11 +34,10 @@ function addCookies(res, cookies) {
34
34
  }
35
35
  function cauthyExpress(auth) {
36
36
  const base = auth.options.basePath;
37
- const key = auth.pact?.contract.jwksPath;
38
37
  return function cauthy(req, res, next) {
39
38
  const request = req;
40
39
  const path = (request.originalUrl ?? request.url ?? "/").split("?")[0];
41
- if (path === base || path.startsWith(`${base}/`) || path === key) {
40
+ if (path === base || path.startsWith(`${base}/`)) {
42
41
  void (async () => {
43
42
  const answer = await auth.handler(await toWebRequest(request, true));
44
43
  res.statusCode = answer.status;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/express.ts"],"sourcesContent":["// Express (and anything else that speaks Node's http): the routes, and who is\n// signed in on every request.\n//\n// app.use(cauthyExpress(auth))\n//\n// app.get('/api/me', async (req, res) => {\n// const session = await req.cauthy()\n// if (!session) return res.status(401).json({ error: 'unauthenticated' })\n// res.json(session.user)\n// })\n//\n// app.get('/api/books', requireSession, (req, res) => { … })\n//\n// A session that needs renewing is renewed on the spot, and its new cookie\n// leaves with whatever the route answers.\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { Cauthy } from './cauthy'\nimport type { Session } from './types'\n\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace Express {\n interface Request {\n /** Who is signed in, or null. Asked once per request. */\n cauthy(): Promise<Session | null>\n }\n }\n}\n\n/** A request as Node gives it, with what Express adds when it is Express. */\ntype NodeRequest = IncomingMessage & { originalUrl?: string; protocol?: string; ip?: string; body?: unknown; cauthy?: () => Promise<Session | null> }\ntype Next = (error?: unknown) => void\n\nfunction originOf(req: NodeRequest): string {\n const secure = (req.socket as { encrypted?: boolean } | undefined)?.encrypted === true\n return `${req.protocol ?? (secure ? 'https' : 'http')}://${req.headers.host ?? 'localhost'}`\n}\n\nasync function bodyOf(req: NodeRequest): Promise<BodyInit | undefined> {\n if (req.method === 'GET' || req.method === 'HEAD') return undefined\n // A body parser was here first (express.json()): what it read is the body.\n if (req.body !== undefined && req.body !== null) {\n if (typeof req.body === 'string') return req.body\n if (req.body instanceof Uint8Array) return new Uint8Array(req.body)\n return JSON.stringify(req.body)\n }\n const chunks: Uint8Array[] = []\n for await (const chunk of req) chunks.push(typeof chunk === 'string' ? new TextEncoder().encode(chunk) : (chunk as Uint8Array))\n return chunks.length === 0 ? undefined : new Blob(chunks as BlobPart[])\n}\n\nasync function toWebRequest(req: NodeRequest, withBody: boolean): Promise<Request> {\n const headers = new Headers()\n for (const [k, v] of Object.entries(req.headers)) {\n if (Array.isArray(v)) for (const one of v) headers.append(k, one)\n else if (v !== undefined) headers.set(k, v)\n }\n // Express knows who is calling (it applies `trust proxy`): that address is\n // the one passed on to Cauthy, whatever the header claimed.\n if (typeof req.ip === 'string' && req.ip !== '') headers.set('x-forwarded-for', req.ip)\n return new Request(new URL(req.originalUrl ?? req.url ?? '/', originOf(req)), {\n method: req.method,\n headers,\n body: withBody ? await bodyOf(req) : undefined,\n })\n}\n\nfunction addCookies(res: ServerResponse, cookies: string[]): void {\n if (cookies.length === 0 || res.headersSent) return\n const had = res.getHeader('set-cookie')\n res.setHeader('set-cookie', [...(Array.isArray(had) ? had : typeof had === 'string' ? [had] : []), ...cookies])\n}\n\n/**\n * Serves the routes under the handler's `basePath` (and, by contract, the\n * application's published key), and gives every other\n * request `req.cauthy()`. Mount it once, before the routes that use it.\n */\nexport function cauthyExpress(auth: Cauthy) {\n const base = auth.options.basePath\n // By contract the application also publishes its key, outside basePath.\n const key = auth.pact?.contract.jwksPath\n return function cauthy(req: IncomingMessage, res: ServerResponse, next: Next): void {\n const request = req as NodeRequest\n const path = (request.originalUrl ?? request.url ?? '/').split('?')[0]\n\n if (path === base || path.startsWith(`${base}/`) || path === key) {\n void (async () => {\n const answer = await auth.handler(await toWebRequest(request, true))\n res.statusCode = answer.status\n answer.headers.forEach((value, name) => {\n if (name !== 'set-cookie') res.setHeader(name, value)\n })\n addCookies(res, answer.headers.getSetCookie())\n res.end(answer.body === null ? undefined : Buffer.from(await answer.arrayBuffer()))\n })().catch(next)\n return\n }\n\n let asked: Promise<Session | null> | undefined\n request.cauthy = () =>\n (asked ??= (async () => {\n const resolved = await auth.resolve(await toWebRequest(request, false))\n addCookies(res, resolved.cookies)\n return resolved.session\n })())\n next()\n }\n}\n\n/** For a route only a signed-in person may call: answers 401 otherwise. Needs `cauthyExpress` mounted before it. */\nexport function requireSession(req: IncomingMessage, res: ServerResponse, next: Next): void {\n const ask = (req as NodeRequest).cauthy\n if (!ask) return next(new Error('@cauthyhq/node: mount cauthyExpress(auth) before requireSession'))\n ask().then((session) => {\n if (session !== null) return next()\n res.statusCode = 401\n res.setHeader('content-type', 'application/json; charset=utf-8')\n res.setHeader('x-cauthy-session', 'none')\n res.end(JSON.stringify({ error: 'unauthenticated' }))\n }, next)\n}\n"],"mappings":";AAkCA,SAAS,SAAS,KAA0B;AAC1C,QAAM,SAAU,IAAI,QAAgD,cAAc;AAClF,SAAO,GAAG,IAAI,aAAa,SAAS,UAAU,OAAO,MAAM,IAAI,QAAQ,QAAQ,WAAW;AAC5F;AAEA,eAAe,OAAO,KAAiD;AACrE,MAAI,IAAI,WAAW,SAAS,IAAI,WAAW,OAAQ,QAAO;AAE1D,MAAI,IAAI,SAAS,UAAa,IAAI,SAAS,MAAM;AAC/C,QAAI,OAAO,IAAI,SAAS,SAAU,QAAO,IAAI;AAC7C,QAAI,IAAI,gBAAgB,WAAY,QAAO,IAAI,WAAW,IAAI,IAAI;AAClE,WAAO,KAAK,UAAU,IAAI,IAAI;AAAA,EAChC;AACA,QAAM,SAAuB,CAAC;AAC9B,mBAAiB,SAAS,IAAK,QAAO,KAAK,OAAO,UAAU,WAAW,IAAI,YAAY,EAAE,OAAO,KAAK,IAAK,KAAoB;AAC9H,SAAO,OAAO,WAAW,IAAI,SAAY,IAAI,KAAK,MAAoB;AACxE;AAEA,eAAe,aAAa,KAAkB,UAAqC;AACjF,QAAM,UAAU,IAAI,QAAQ;AAC5B,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,IAAI,OAAO,GAAG;AAChD,QAAI,MAAM,QAAQ,CAAC,EAAG,YAAW,OAAO,EAAG,SAAQ,OAAO,GAAG,GAAG;AAAA,aACvD,MAAM,OAAW,SAAQ,IAAI,GAAG,CAAC;AAAA,EAC5C;AAGA,MAAI,OAAO,IAAI,OAAO,YAAY,IAAI,OAAO,GAAI,SAAQ,IAAI,mBAAmB,IAAI,EAAE;AACtF,SAAO,IAAI,QAAQ,IAAI,IAAI,IAAI,eAAe,IAAI,OAAO,KAAK,SAAS,GAAG,CAAC,GAAG;AAAA,IAC5E,QAAQ,IAAI;AAAA,IACZ;AAAA,IACA,MAAM,WAAW,MAAM,OAAO,GAAG,IAAI;AAAA,EACvC,CAAC;AACH;AAEA,SAAS,WAAW,KAAqB,SAAyB;AAChE,MAAI,QAAQ,WAAW,KAAK,IAAI,YAAa;AAC7C,QAAM,MAAM,IAAI,UAAU,YAAY;AACtC,MAAI,UAAU,cAAc,CAAC,GAAI,MAAM,QAAQ,GAAG,IAAI,MAAM,OAAO,QAAQ,WAAW,CAAC,GAAG,IAAI,CAAC,GAAI,GAAG,OAAO,CAAC;AAChH;AAOO,SAAS,cAAc,MAAc;AAC1C,QAAM,OAAO,KAAK,QAAQ;AAE1B,QAAM,MAAM,KAAK,MAAM,SAAS;AAChC,SAAO,SAAS,OAAO,KAAsB,KAAqB,MAAkB;AAClF,UAAM,UAAU;AAChB,UAAM,QAAQ,QAAQ,eAAe,QAAQ,OAAO,KAAK,MAAM,GAAG,EAAE,CAAC;AAErE,QAAI,SAAS,QAAQ,KAAK,WAAW,GAAG,IAAI,GAAG,KAAK,SAAS,KAAK;AAChE,YAAM,YAAY;AAChB,cAAM,SAAS,MAAM,KAAK,QAAQ,MAAM,aAAa,SAAS,IAAI,CAAC;AACnE,YAAI,aAAa,OAAO;AACxB,eAAO,QAAQ,QAAQ,CAAC,OAAO,SAAS;AACtC,cAAI,SAAS,aAAc,KAAI,UAAU,MAAM,KAAK;AAAA,QACtD,CAAC;AACD,mBAAW,KAAK,OAAO,QAAQ,aAAa,CAAC;AAC7C,YAAI,IAAI,OAAO,SAAS,OAAO,SAAY,OAAO,KAAK,MAAM,OAAO,YAAY,CAAC,CAAC;AAAA,MACpF,GAAG,EAAE,MAAM,IAAI;AACf;AAAA,IACF;AAEA,QAAI;AACJ,YAAQ,SAAS,MACd,WAAW,YAAY;AACtB,YAAM,WAAW,MAAM,KAAK,QAAQ,MAAM,aAAa,SAAS,KAAK,CAAC;AACtE,iBAAW,KAAK,SAAS,OAAO;AAChC,aAAO,SAAS;AAAA,IAClB,GAAG;AACL,SAAK;AAAA,EACP;AACF;AAGO,SAAS,eAAe,KAAsB,KAAqB,MAAkB;AAC1F,QAAM,MAAO,IAAoB;AACjC,MAAI,CAAC,IAAK,QAAO,KAAK,IAAI,MAAM,iEAAiE,CAAC;AAClG,MAAI,EAAE,KAAK,CAAC,YAAY;AACtB,QAAI,YAAY,KAAM,QAAO,KAAK;AAClC,QAAI,aAAa;AACjB,QAAI,UAAU,gBAAgB,iCAAiC;AAC/D,QAAI,UAAU,oBAAoB,MAAM;AACxC,QAAI,IAAI,KAAK,UAAU,EAAE,OAAO,kBAAkB,CAAC,CAAC;AAAA,EACtD,GAAG,IAAI;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/express.ts"],"sourcesContent":["// Express (and anything else that speaks Node's http): the routes, and who is\n// signed in on every request.\n//\n// app.use(cauthyExpress(auth))\n//\n// app.get('/api/me', async (req, res) => {\n// const session = await req.cauthy()\n// if (!session) return res.status(401).json({ error: 'unauthenticated' })\n// res.json(session.user)\n// })\n//\n// app.get('/api/books', requireSession, (req, res) => { … })\n//\n// A session that needs renewing is renewed on the spot, and its new cookie\n// leaves with whatever the route answers.\n\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { Cauthy } from './cauthy'\nimport type { Session } from './types'\n\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace Express {\n interface Request {\n /** Who is signed in, or null. Asked once per request. */\n cauthy(): Promise<Session | null>\n }\n }\n}\n\n/** A request as Node gives it, with what Express adds when it is Express. */\ntype NodeRequest = IncomingMessage & { originalUrl?: string; protocol?: string; ip?: string; body?: unknown; cauthy?: () => Promise<Session | null> }\ntype Next = (error?: unknown) => void\n\nfunction originOf(req: NodeRequest): string {\n const secure = (req.socket as { encrypted?: boolean } | undefined)?.encrypted === true\n return `${req.protocol ?? (secure ? 'https' : 'http')}://${req.headers.host ?? 'localhost'}`\n}\n\nasync function bodyOf(req: NodeRequest): Promise<BodyInit | undefined> {\n if (req.method === 'GET' || req.method === 'HEAD') return undefined\n // A body parser was here first (express.json()): what it read is the body.\n if (req.body !== undefined && req.body !== null) {\n if (typeof req.body === 'string') return req.body\n if (req.body instanceof Uint8Array) return new Uint8Array(req.body)\n return JSON.stringify(req.body)\n }\n const chunks: Uint8Array[] = []\n for await (const chunk of req) chunks.push(typeof chunk === 'string' ? new TextEncoder().encode(chunk) : (chunk as Uint8Array))\n return chunks.length === 0 ? undefined : new Blob(chunks as BlobPart[])\n}\n\nasync function toWebRequest(req: NodeRequest, withBody: boolean): Promise<Request> {\n const headers = new Headers()\n for (const [k, v] of Object.entries(req.headers)) {\n if (Array.isArray(v)) for (const one of v) headers.append(k, one)\n else if (v !== undefined) headers.set(k, v)\n }\n // Express knows who is calling (it applies `trust proxy`): that address is\n // the one passed on to Cauthy, whatever the header claimed.\n if (typeof req.ip === 'string' && req.ip !== '') headers.set('x-forwarded-for', req.ip)\n return new Request(new URL(req.originalUrl ?? req.url ?? '/', originOf(req)), {\n method: req.method,\n headers,\n body: withBody ? await bodyOf(req) : undefined,\n })\n}\n\nfunction addCookies(res: ServerResponse, cookies: string[]): void {\n if (cookies.length === 0 || res.headersSent) return\n const had = res.getHeader('set-cookie')\n res.setHeader('set-cookie', [...(Array.isArray(had) ? had : typeof had === 'string' ? [had] : []), ...cookies])\n}\n\n/**\n * Serves the routes under the handler's `basePath`, and gives every other\n * request `req.cauthy()`. Mount it once, before the routes that use it.\n */\nexport function cauthyExpress(auth: Cauthy) {\n const base = auth.options.basePath\n return function cauthy(req: IncomingMessage, res: ServerResponse, next: Next): void {\n const request = req as NodeRequest\n const path = (request.originalUrl ?? request.url ?? '/').split('?')[0]\n\n if (path === base || path.startsWith(`${base}/`)) {\n void (async () => {\n const answer = await auth.handler(await toWebRequest(request, true))\n res.statusCode = answer.status\n answer.headers.forEach((value, name) => {\n if (name !== 'set-cookie') res.setHeader(name, value)\n })\n addCookies(res, answer.headers.getSetCookie())\n res.end(answer.body === null ? undefined : Buffer.from(await answer.arrayBuffer()))\n })().catch(next)\n return\n }\n\n let asked: Promise<Session | null> | undefined\n request.cauthy = () =>\n (asked ??= (async () => {\n const resolved = await auth.resolve(await toWebRequest(request, false))\n addCookies(res, resolved.cookies)\n return resolved.session\n })())\n next()\n }\n}\n\n/** For a route only a signed-in person may call: answers 401 otherwise. Needs `cauthyExpress` mounted before it. */\nexport function requireSession(req: IncomingMessage, res: ServerResponse, next: Next): void {\n const ask = (req as NodeRequest).cauthy\n if (!ask) return next(new Error('@cauthyhq/node: mount cauthyExpress(auth) before requireSession'))\n ask().then((session) => {\n if (session !== null) return next()\n res.statusCode = 401\n res.setHeader('content-type', 'application/json; charset=utf-8')\n res.setHeader('x-cauthy-session', 'none')\n res.end(JSON.stringify({ error: 'unauthenticated' }))\n }, next)\n}\n"],"mappings":"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","names":[]}
@@ -1,9 +1,7 @@
1
1
  import { FastifyReply, FastifyPluginAsync, FastifyInstance } from 'fastify';
2
- import { S as Session, C as Cauthy } from './cauthy-CCSSj3Lg.mjs';
3
- import './keys-D5k0omTb.mjs';
2
+ import { S as Session, C as Cauthy } from './cauthy-CsWUgyv_.mjs';
4
3
  import 'jose';
5
4
  import './theme.mjs';
6
- import './identity-BTJ8NB_w.mjs';
7
5
 
8
6
  declare module 'fastify' {
9
7
  interface FastifyRequest {
package/dist/fastify.d.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  import { FastifyReply, FastifyPluginAsync, FastifyInstance } from 'fastify';
2
- import { S as Session, C as Cauthy } from './cauthy-C7gZ-pbv.js';
3
- import './keys-D5k0omTb.js';
2
+ import { S as Session, C as Cauthy } from './cauthy-DHLBEuqP.js';
4
3
  import 'jose';
5
4
  import './theme.js';
6
- import './identity-BTJ8NB_w.js';
7
5
 
8
6
  declare module 'fastify' {
9
7
  interface FastifyRequest {
package/dist/fastify.js CHANGED
@@ -73,8 +73,6 @@ var plugin = async (app, { auth, checkMount = true }) => {
73
73
  return reply.send(res.body === null ? null : Buffer.from(await res.arrayBuffer()));
74
74
  };
75
75
  app.route({ method: ["GET", "POST"], url: `${base}/*`, handler: serve });
76
- const key = auth.pact?.contract.jwksPath;
77
- if (key) app.route({ method: ["GET", "HEAD", "POST"], url: key, handler: serve });
78
76
  if (checkMount && process.env.NODE_ENV !== "test") app.addHook("onReady", () => probe(app, auth));
79
77
  };
80
78
  async function probe(app, auth) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/fastify.ts"],"sourcesContent":["// Fastify: the routes, and who is signed in on every request.\n//\n// await app.register(cauthyFastify, { auth })\n//\n// app.get('/api/me', async (req, reply) => {\n// const session = await req.cauthy()\n// if (!session) return reply.code(401).send({ error: 'unauthenticated' })\n// return session.user\n// })\n//\n// app.get('/api/books', { preHandler: app.requireSession }, async (req) => { … })\n//\n// Here a session that needs renewing is renewed on the spot: Fastify has the\n// reply in hand, so the new cookie always goes back.\n\nimport { createServer } from 'node:http'\nimport type { AddressInfo } from 'node:net'\nimport type { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'\nimport type { Cauthy } from './cauthy'\nimport type { Session } from './types'\n\ndeclare module 'fastify' {\n interface FastifyRequest {\n /** Who is signed in, or null. Asked once per request. */\n cauthy(): Promise<Session | null>\n }\n interface FastifyInstance {\n /** A preHandler that answers 401 unless somebody is signed in. */\n requireSession(request: FastifyRequest, reply: FastifyReply): Promise<void>\n }\n}\n\nexport interface CauthyFastifyOptions {\n auth: Cauthy\n /**\n * Check, when Fastify is ready, that this plugin is what answers under\n * basePath (`checkFastifyMount`), and fail startup when another route\n * does. Default: true.\n */\n checkMount?: boolean\n}\n\nfunction originOf(req: FastifyRequest): string {\n return `${req.protocol}://${req.headers.host ?? 'localhost'}`\n}\n\nfunction toWebRequest(req: FastifyRequest): Request {\n const headers = new Headers()\n for (const [k, v] of Object.entries(req.headers)) {\n if (Array.isArray(v)) for (const one of v) headers.append(k, one)\n else if (v !== undefined) headers.set(k, v)\n }\n // Fastify knows who is calling (it applies `trustProxy`): that address is\n // the one passed on to Cauthy, whatever the header claimed.\n headers.set('x-forwarded-for', req.ip)\n // What Fastify parsed is the body: the application's own sign-in screens post JSON.\n const body = req.method === 'GET' || req.method === 'HEAD' || req.body === undefined || req.body === null\n ? undefined\n : typeof req.body === 'string' ? req.body : JSON.stringify(req.body)\n return new Request(new URL(req.url, originOf(req)), { method: req.method, headers, body })\n}\n\nconst kSession = Symbol('cauthy.session')\nconst kReply = Symbol('cauthy.reply')\n\nconst plugin: FastifyPluginAsync<CauthyFastifyOptions> = async (app: FastifyInstance, { auth, checkMount = true }) => {\n const base = auth.options.basePath\n\n app.decorateRequest('cauthy', async function (this: FastifyRequest): Promise<Session | null> {\n const self = this as FastifyRequest & { [kSession]?: Promise<Session | null> }\n self[kSession] ??= (async () => {\n const resolved = await auth.resolve(toWebRequest(this))\n // Set through the raw response so the cookie leaves whatever the route\n // answers with, even an error.\n const reply = (this as FastifyRequest & { [kReply]?: FastifyReply })[kReply]\n if (resolved.cookies.length > 0 && reply) reply.header('set-cookie', resolved.cookies)\n return resolved.session\n })()\n return self[kSession]\n })\n\n app.addHook('onRequest', async (req, reply) => {\n ;(req as FastifyRequest & { [kReply]?: FastifyReply })[kReply] = reply\n })\n\n app.decorate('requireSession', async function (request: FastifyRequest, reply: FastifyReply): Promise<void> {\n if ((await request.cauthy()) === null) {\n await reply.code(401).header('x-cauthy-session', 'none').send({ error: 'unauthenticated' })\n }\n })\n\n const serve = async (req: FastifyRequest, reply: FastifyReply) => {\n const res = await auth.handler(toWebRequest(req))\n reply.code(res.status)\n res.headers.forEach((value, name) => {\n if (name !== 'set-cookie') reply.header(name, value)\n })\n const cookies = res.headers.getSetCookie()\n if (cookies.length > 0) reply.header('set-cookie', cookies)\n return reply.send(res.body === null ? null : Buffer.from(await res.arrayBuffer()))\n }\n app.route({ method: ['GET', 'POST'], url: `${base}/*`, handler: serve })\n // By contract the application also publishes its key, outside basePath.\n const key = auth.pact?.contract.jwksPath\n if (key) app.route({ method: ['GET', 'HEAD', 'POST'], url: key, handler: serve })\n // Tests build many apps with routes of their own, and mount nothing for real.\n if (checkMount && process.env.NODE_ENV !== 'test') app.addHook('onReady', () => probe(app, auth))\n}\n\n/**\n * Sends the probes of checkMount through Fastify's own router, over a private\n * loopback socket that only lives for the check. Not `app.inject`: inside\n * `onReady` it would wait for the very ready it runs in.\n */\nasync function probe(app: FastifyInstance, auth: Cauthy): Promise<void> {\n const server = createServer((req, res) => app.routing(req, res))\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject)\n server.listen(0, '127.0.0.1', resolve)\n })\n try {\n await auth.checkMount(`http://127.0.0.1:${(server.address() as AddressInfo).port}`)\n } finally {\n server.closeAllConnections()\n await new Promise<void>((resolve) => server.close(() => resolve()))\n }\n}\n\n/**\n * Checks that the plugin is what answers under basePath, in process, once\n * Fastify is ready (the plugin already does it by itself at `onReady`; this\n * is for tests and for `checkMount: false`):\n *\n * await app.ready()\n * await checkFastifyMount(app, auth)\n */\nexport function checkFastifyMount(app: FastifyInstance, auth: Cauthy): Promise<void> {\n return probe(app, auth)\n}\n\n\n// Outside encapsulation: `req.cauthy()` and `requireSession` are for the whole\n// application, not only for routes registered beside this plugin.\n;(plugin as unknown as Record<symbol, boolean>)[Symbol.for('skip-override')] = true\n;(plugin as unknown as Record<symbol, unknown>)[Symbol.for('fastify.display-name')] = '@cauthyhq/node'\n\nexport const cauthyFastify = plugin\nexport default plugin\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/fastify.ts"],"sourcesContent":["// Fastify: the routes, and who is signed in on every request.\n//\n// await app.register(cauthyFastify, { auth })\n//\n// app.get('/api/me', async (req, reply) => {\n// const session = await req.cauthy()\n// if (!session) return reply.code(401).send({ error: 'unauthenticated' })\n// return session.user\n// })\n//\n// app.get('/api/books', { preHandler: app.requireSession }, async (req) => { … })\n//\n// Here a session that needs renewing is renewed on the spot: Fastify has the\n// reply in hand, so the new cookie always goes back.\n\nimport { createServer } from 'node:http'\nimport type { AddressInfo } from 'node:net'\nimport type { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'\nimport type { Cauthy } from './cauthy'\nimport type { Session } from './types'\n\ndeclare module 'fastify' {\n interface FastifyRequest {\n /** Who is signed in, or null. Asked once per request. */\n cauthy(): Promise<Session | null>\n }\n interface FastifyInstance {\n /** A preHandler that answers 401 unless somebody is signed in. */\n requireSession(request: FastifyRequest, reply: FastifyReply): Promise<void>\n }\n}\n\nexport interface CauthyFastifyOptions {\n auth: Cauthy\n /**\n * Check, when Fastify is ready, that this plugin is what answers under\n * basePath (`checkFastifyMount`), and fail startup when another route\n * does. Default: true.\n */\n checkMount?: boolean\n}\n\nfunction originOf(req: FastifyRequest): string {\n return `${req.protocol}://${req.headers.host ?? 'localhost'}`\n}\n\nfunction toWebRequest(req: FastifyRequest): Request {\n const headers = new Headers()\n for (const [k, v] of Object.entries(req.headers)) {\n if (Array.isArray(v)) for (const one of v) headers.append(k, one)\n else if (v !== undefined) headers.set(k, v)\n }\n // Fastify knows who is calling (it applies `trustProxy`): that address is\n // the one passed on to Cauthy, whatever the header claimed.\n headers.set('x-forwarded-for', req.ip)\n // What Fastify parsed is the body: the application's own sign-in screens post JSON.\n const body = req.method === 'GET' || req.method === 'HEAD' || req.body === undefined || req.body === null\n ? undefined\n : typeof req.body === 'string' ? req.body : JSON.stringify(req.body)\n return new Request(new URL(req.url, originOf(req)), { method: req.method, headers, body })\n}\n\nconst kSession = Symbol('cauthy.session')\nconst kReply = Symbol('cauthy.reply')\n\nconst plugin: FastifyPluginAsync<CauthyFastifyOptions> = async (app: FastifyInstance, { auth, checkMount = true }) => {\n const base = auth.options.basePath\n\n app.decorateRequest('cauthy', async function (this: FastifyRequest): Promise<Session | null> {\n const self = this as FastifyRequest & { [kSession]?: Promise<Session | null> }\n self[kSession] ??= (async () => {\n const resolved = await auth.resolve(toWebRequest(this))\n // Set through the raw response so the cookie leaves whatever the route\n // answers with, even an error.\n const reply = (this as FastifyRequest & { [kReply]?: FastifyReply })[kReply]\n if (resolved.cookies.length > 0 && reply) reply.header('set-cookie', resolved.cookies)\n return resolved.session\n })()\n return self[kSession]\n })\n\n app.addHook('onRequest', async (req, reply) => {\n ;(req as FastifyRequest & { [kReply]?: FastifyReply })[kReply] = reply\n })\n\n app.decorate('requireSession', async function (request: FastifyRequest, reply: FastifyReply): Promise<void> {\n if ((await request.cauthy()) === null) {\n await reply.code(401).header('x-cauthy-session', 'none').send({ error: 'unauthenticated' })\n }\n })\n\n const serve = async (req: FastifyRequest, reply: FastifyReply) => {\n const res = await auth.handler(toWebRequest(req))\n reply.code(res.status)\n res.headers.forEach((value, name) => {\n if (name !== 'set-cookie') reply.header(name, value)\n })\n const cookies = res.headers.getSetCookie()\n if (cookies.length > 0) reply.header('set-cookie', cookies)\n return reply.send(res.body === null ? null : Buffer.from(await res.arrayBuffer()))\n }\n app.route({ method: ['GET', 'POST'], url: `${base}/*`, handler: serve })\n // Tests build many apps with routes of their own, and mount nothing for real.\n if (checkMount && process.env.NODE_ENV !== 'test') app.addHook('onReady', () => probe(app, auth))\n}\n\n/**\n * Sends the probes of checkMount through Fastify's own router, over a private\n * loopback socket that only lives for the check. Not `app.inject`: inside\n * `onReady` it would wait for the very ready it runs in.\n */\nasync function probe(app: FastifyInstance, auth: Cauthy): Promise<void> {\n const server = createServer((req, res) => app.routing(req, res))\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject)\n server.listen(0, '127.0.0.1', resolve)\n })\n try {\n await auth.checkMount(`http://127.0.0.1:${(server.address() as AddressInfo).port}`)\n } finally {\n server.closeAllConnections()\n await new Promise<void>((resolve) => server.close(() => resolve()))\n }\n}\n\n/**\n * Checks that the plugin is what answers under basePath, in process, once\n * Fastify is ready (the plugin already does it by itself at `onReady`; this\n * is for tests and for `checkMount: false`):\n *\n * await app.ready()\n * await checkFastifyMount(app, auth)\n */\nexport function checkFastifyMount(app: FastifyInstance, auth: Cauthy): Promise<void> {\n return probe(app, auth)\n}\n\n\n// Outside encapsulation: `req.cauthy()` and `requireSession` are for the whole\n// application, not only for routes registered beside this plugin.\n;(plugin as unknown as Record<symbol, boolean>)[Symbol.for('skip-override')] = true\n;(plugin as unknown as Record<symbol, unknown>)[Symbol.for('fastify.display-name')] = '@cauthyhq/node'\n\nexport const cauthyFastify = plugin\nexport default plugin\n"],"mappings":"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","names":[]}
package/dist/fastify.mjs CHANGED
@@ -47,8 +47,6 @@ var plugin = async (app, { auth, checkMount = true }) => {
47
47
  return reply.send(res.body === null ? null : Buffer.from(await res.arrayBuffer()));
48
48
  };
49
49
  app.route({ method: ["GET", "POST"], url: `${base}/*`, handler: serve });
50
- const key = auth.pact?.contract.jwksPath;
51
- if (key) app.route({ method: ["GET", "HEAD", "POST"], url: key, handler: serve });
52
50
  if (checkMount && process.env.NODE_ENV !== "test") app.addHook("onReady", () => probe(app, auth));
53
51
  };
54
52
  async function probe(app, auth) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/fastify.ts"],"sourcesContent":["// Fastify: the routes, and who is signed in on every request.\n//\n// await app.register(cauthyFastify, { auth })\n//\n// app.get('/api/me', async (req, reply) => {\n// const session = await req.cauthy()\n// if (!session) return reply.code(401).send({ error: 'unauthenticated' })\n// return session.user\n// })\n//\n// app.get('/api/books', { preHandler: app.requireSession }, async (req) => { … })\n//\n// Here a session that needs renewing is renewed on the spot: Fastify has the\n// reply in hand, so the new cookie always goes back.\n\nimport { createServer } from 'node:http'\nimport type { AddressInfo } from 'node:net'\nimport type { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'\nimport type { Cauthy } from './cauthy'\nimport type { Session } from './types'\n\ndeclare module 'fastify' {\n interface FastifyRequest {\n /** Who is signed in, or null. Asked once per request. */\n cauthy(): Promise<Session | null>\n }\n interface FastifyInstance {\n /** A preHandler that answers 401 unless somebody is signed in. */\n requireSession(request: FastifyRequest, reply: FastifyReply): Promise<void>\n }\n}\n\nexport interface CauthyFastifyOptions {\n auth: Cauthy\n /**\n * Check, when Fastify is ready, that this plugin is what answers under\n * basePath (`checkFastifyMount`), and fail startup when another route\n * does. Default: true.\n */\n checkMount?: boolean\n}\n\nfunction originOf(req: FastifyRequest): string {\n return `${req.protocol}://${req.headers.host ?? 'localhost'}`\n}\n\nfunction toWebRequest(req: FastifyRequest): Request {\n const headers = new Headers()\n for (const [k, v] of Object.entries(req.headers)) {\n if (Array.isArray(v)) for (const one of v) headers.append(k, one)\n else if (v !== undefined) headers.set(k, v)\n }\n // Fastify knows who is calling (it applies `trustProxy`): that address is\n // the one passed on to Cauthy, whatever the header claimed.\n headers.set('x-forwarded-for', req.ip)\n // What Fastify parsed is the body: the application's own sign-in screens post JSON.\n const body = req.method === 'GET' || req.method === 'HEAD' || req.body === undefined || req.body === null\n ? undefined\n : typeof req.body === 'string' ? req.body : JSON.stringify(req.body)\n return new Request(new URL(req.url, originOf(req)), { method: req.method, headers, body })\n}\n\nconst kSession = Symbol('cauthy.session')\nconst kReply = Symbol('cauthy.reply')\n\nconst plugin: FastifyPluginAsync<CauthyFastifyOptions> = async (app: FastifyInstance, { auth, checkMount = true }) => {\n const base = auth.options.basePath\n\n app.decorateRequest('cauthy', async function (this: FastifyRequest): Promise<Session | null> {\n const self = this as FastifyRequest & { [kSession]?: Promise<Session | null> }\n self[kSession] ??= (async () => {\n const resolved = await auth.resolve(toWebRequest(this))\n // Set through the raw response so the cookie leaves whatever the route\n // answers with, even an error.\n const reply = (this as FastifyRequest & { [kReply]?: FastifyReply })[kReply]\n if (resolved.cookies.length > 0 && reply) reply.header('set-cookie', resolved.cookies)\n return resolved.session\n })()\n return self[kSession]\n })\n\n app.addHook('onRequest', async (req, reply) => {\n ;(req as FastifyRequest & { [kReply]?: FastifyReply })[kReply] = reply\n })\n\n app.decorate('requireSession', async function (request: FastifyRequest, reply: FastifyReply): Promise<void> {\n if ((await request.cauthy()) === null) {\n await reply.code(401).header('x-cauthy-session', 'none').send({ error: 'unauthenticated' })\n }\n })\n\n const serve = async (req: FastifyRequest, reply: FastifyReply) => {\n const res = await auth.handler(toWebRequest(req))\n reply.code(res.status)\n res.headers.forEach((value, name) => {\n if (name !== 'set-cookie') reply.header(name, value)\n })\n const cookies = res.headers.getSetCookie()\n if (cookies.length > 0) reply.header('set-cookie', cookies)\n return reply.send(res.body === null ? null : Buffer.from(await res.arrayBuffer()))\n }\n app.route({ method: ['GET', 'POST'], url: `${base}/*`, handler: serve })\n // By contract the application also publishes its key, outside basePath.\n const key = auth.pact?.contract.jwksPath\n if (key) app.route({ method: ['GET', 'HEAD', 'POST'], url: key, handler: serve })\n // Tests build many apps with routes of their own, and mount nothing for real.\n if (checkMount && process.env.NODE_ENV !== 'test') app.addHook('onReady', () => probe(app, auth))\n}\n\n/**\n * Sends the probes of checkMount through Fastify's own router, over a private\n * loopback socket that only lives for the check. Not `app.inject`: inside\n * `onReady` it would wait for the very ready it runs in.\n */\nasync function probe(app: FastifyInstance, auth: Cauthy): Promise<void> {\n const server = createServer((req, res) => app.routing(req, res))\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject)\n server.listen(0, '127.0.0.1', resolve)\n })\n try {\n await auth.checkMount(`http://127.0.0.1:${(server.address() as AddressInfo).port}`)\n } finally {\n server.closeAllConnections()\n await new Promise<void>((resolve) => server.close(() => resolve()))\n }\n}\n\n/**\n * Checks that the plugin is what answers under basePath, in process, once\n * Fastify is ready (the plugin already does it by itself at `onReady`; this\n * is for tests and for `checkMount: false`):\n *\n * await app.ready()\n * await checkFastifyMount(app, auth)\n */\nexport function checkFastifyMount(app: FastifyInstance, auth: Cauthy): Promise<void> {\n return probe(app, auth)\n}\n\n\n// Outside encapsulation: `req.cauthy()` and `requireSession` are for the whole\n// application, not only for routes registered beside this plugin.\n;(plugin as unknown as Record<symbol, boolean>)[Symbol.for('skip-override')] = true\n;(plugin as unknown as Record<symbol, unknown>)[Symbol.for('fastify.display-name')] = '@cauthyhq/node'\n\nexport const cauthyFastify = plugin\nexport default plugin\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/fastify.ts"],"sourcesContent":["// Fastify: the routes, and who is signed in on every request.\n//\n// await app.register(cauthyFastify, { auth })\n//\n// app.get('/api/me', async (req, reply) => {\n// const session = await req.cauthy()\n// if (!session) return reply.code(401).send({ error: 'unauthenticated' })\n// return session.user\n// })\n//\n// app.get('/api/books', { preHandler: app.requireSession }, async (req) => { … })\n//\n// Here a session that needs renewing is renewed on the spot: Fastify has the\n// reply in hand, so the new cookie always goes back.\n\nimport { createServer } from 'node:http'\nimport type { AddressInfo } from 'node:net'\nimport type { FastifyInstance, FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'\nimport type { Cauthy } from './cauthy'\nimport type { Session } from './types'\n\ndeclare module 'fastify' {\n interface FastifyRequest {\n /** Who is signed in, or null. Asked once per request. */\n cauthy(): Promise<Session | null>\n }\n interface FastifyInstance {\n /** A preHandler that answers 401 unless somebody is signed in. */\n requireSession(request: FastifyRequest, reply: FastifyReply): Promise<void>\n }\n}\n\nexport interface CauthyFastifyOptions {\n auth: Cauthy\n /**\n * Check, when Fastify is ready, that this plugin is what answers under\n * basePath (`checkFastifyMount`), and fail startup when another route\n * does. Default: true.\n */\n checkMount?: boolean\n}\n\nfunction originOf(req: FastifyRequest): string {\n return `${req.protocol}://${req.headers.host ?? 'localhost'}`\n}\n\nfunction toWebRequest(req: FastifyRequest): Request {\n const headers = new Headers()\n for (const [k, v] of Object.entries(req.headers)) {\n if (Array.isArray(v)) for (const one of v) headers.append(k, one)\n else if (v !== undefined) headers.set(k, v)\n }\n // Fastify knows who is calling (it applies `trustProxy`): that address is\n // the one passed on to Cauthy, whatever the header claimed.\n headers.set('x-forwarded-for', req.ip)\n // What Fastify parsed is the body: the application's own sign-in screens post JSON.\n const body = req.method === 'GET' || req.method === 'HEAD' || req.body === undefined || req.body === null\n ? undefined\n : typeof req.body === 'string' ? req.body : JSON.stringify(req.body)\n return new Request(new URL(req.url, originOf(req)), { method: req.method, headers, body })\n}\n\nconst kSession = Symbol('cauthy.session')\nconst kReply = Symbol('cauthy.reply')\n\nconst plugin: FastifyPluginAsync<CauthyFastifyOptions> = async (app: FastifyInstance, { auth, checkMount = true }) => {\n const base = auth.options.basePath\n\n app.decorateRequest('cauthy', async function (this: FastifyRequest): Promise<Session | null> {\n const self = this as FastifyRequest & { [kSession]?: Promise<Session | null> }\n self[kSession] ??= (async () => {\n const resolved = await auth.resolve(toWebRequest(this))\n // Set through the raw response so the cookie leaves whatever the route\n // answers with, even an error.\n const reply = (this as FastifyRequest & { [kReply]?: FastifyReply })[kReply]\n if (resolved.cookies.length > 0 && reply) reply.header('set-cookie', resolved.cookies)\n return resolved.session\n })()\n return self[kSession]\n })\n\n app.addHook('onRequest', async (req, reply) => {\n ;(req as FastifyRequest & { [kReply]?: FastifyReply })[kReply] = reply\n })\n\n app.decorate('requireSession', async function (request: FastifyRequest, reply: FastifyReply): Promise<void> {\n if ((await request.cauthy()) === null) {\n await reply.code(401).header('x-cauthy-session', 'none').send({ error: 'unauthenticated' })\n }\n })\n\n const serve = async (req: FastifyRequest, reply: FastifyReply) => {\n const res = await auth.handler(toWebRequest(req))\n reply.code(res.status)\n res.headers.forEach((value, name) => {\n if (name !== 'set-cookie') reply.header(name, value)\n })\n const cookies = res.headers.getSetCookie()\n if (cookies.length > 0) reply.header('set-cookie', cookies)\n return reply.send(res.body === null ? null : Buffer.from(await res.arrayBuffer()))\n }\n app.route({ method: ['GET', 'POST'], url: `${base}/*`, handler: serve })\n // Tests build many apps with routes of their own, and mount nothing for real.\n if (checkMount && process.env.NODE_ENV !== 'test') app.addHook('onReady', () => probe(app, auth))\n}\n\n/**\n * Sends the probes of checkMount through Fastify's own router, over a private\n * loopback socket that only lives for the check. Not `app.inject`: inside\n * `onReady` it would wait for the very ready it runs in.\n */\nasync function probe(app: FastifyInstance, auth: Cauthy): Promise<void> {\n const server = createServer((req, res) => app.routing(req, res))\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject)\n server.listen(0, '127.0.0.1', resolve)\n })\n try {\n await auth.checkMount(`http://127.0.0.1:${(server.address() as AddressInfo).port}`)\n } finally {\n server.closeAllConnections()\n await new Promise<void>((resolve) => server.close(() => resolve()))\n }\n}\n\n/**\n * Checks that the plugin is what answers under basePath, in process, once\n * Fastify is ready (the plugin already does it by itself at `onReady`; this\n * is for tests and for `checkMount: false`):\n *\n * await app.ready()\n * await checkFastifyMount(app, auth)\n */\nexport function checkFastifyMount(app: FastifyInstance, auth: Cauthy): Promise<void> {\n return probe(app, auth)\n}\n\n\n// Outside encapsulation: `req.cauthy()` and `requireSession` are for the whole\n// application, not only for routes registered beside this plugin.\n;(plugin as unknown as Record<symbol, boolean>)[Symbol.for('skip-override')] = true\n;(plugin as unknown as Record<symbol, unknown>)[Symbol.for('fastify.display-name')] = '@cauthyhq/node'\n\nexport const cauthyFastify = plugin\nexport default plugin\n"],"mappings":"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","names":[]}
package/dist/hono.d.mts CHANGED
@@ -1,9 +1,7 @@
1
1
  import { MiddlewareHandler } from 'hono';
2
- import { S as Session, C as Cauthy } from './cauthy-CCSSj3Lg.mjs';
3
- import './keys-D5k0omTb.mjs';
2
+ import { S as Session, C as Cauthy } from './cauthy-CsWUgyv_.mjs';
4
3
  import 'jose';
5
4
  import './theme.mjs';
6
- import './identity-BTJ8NB_w.mjs';
7
5
 
8
6
  /** What the middleware keeps on the context: `new Hono<{ Variables: CauthyVariables }>()`. */
9
7
  interface CauthyVariables {
@@ -14,8 +12,7 @@ type Env = {
14
12
  Variables: CauthyVariables;
15
13
  };
16
14
  /**
17
- * Serves the routes under the handler's `basePath` (and, by contract, the
18
- * application's published key), and sets `session` for
15
+ * Serves the routes under the handler's `basePath`, and sets `session` for
19
16
  * every other request. Mount it once, before the routes that use it.
20
17
  */
21
18
  declare function cauthyHono(auth: Cauthy): MiddlewareHandler<Env>;
package/dist/hono.d.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  import { MiddlewareHandler } from 'hono';
2
- import { S as Session, C as Cauthy } from './cauthy-C7gZ-pbv.js';
3
- import './keys-D5k0omTb.js';
2
+ import { S as Session, C as Cauthy } from './cauthy-DHLBEuqP.js';
4
3
  import 'jose';
5
4
  import './theme.js';
6
- import './identity-BTJ8NB_w.js';
7
5
 
8
6
  /** What the middleware keeps on the context: `new Hono<{ Variables: CauthyVariables }>()`. */
9
7
  interface CauthyVariables {
@@ -14,8 +12,7 @@ type Env = {
14
12
  Variables: CauthyVariables;
15
13
  };
16
14
  /**
17
- * Serves the routes under the handler's `basePath` (and, by contract, the
18
- * application's published key), and sets `session` for
15
+ * Serves the routes under the handler's `basePath`, and sets `session` for
19
16
  * every other request. Mount it once, before the routes that use it.
20
17
  */
21
18
  declare function cauthyHono(auth: Cauthy): MiddlewareHandler<Env>;
package/dist/hono.js CHANGED
@@ -26,10 +26,9 @@ __export(hono_exports, {
26
26
  module.exports = __toCommonJS(hono_exports);
27
27
  function cauthyHono(auth) {
28
28
  const base = auth.options.basePath;
29
- const key = auth.pact?.contract.jwksPath;
30
29
  return async (c, next) => {
31
30
  const path = new URL(c.req.url).pathname;
32
- if (path === base || path.startsWith(`${base}/`) || path === key) return auth.handler(c.req.raw);
31
+ if (path === base || path.startsWith(`${base}/`)) return auth.handler(c.req.raw);
33
32
  const resolved = await auth.resolve(c.req.raw);
34
33
  c.set("session", resolved.session);
35
34
  await next();