@authdog/fastify 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # @authdog/fastify
2
+
3
+ **Authdog SDK for [Fastify](https://fastify.dev).**
4
+
5
+ A tiny, high-performance plugin that validates Authdog sessions on every
6
+ request and gives you an idiomatic `request.authdog` context plus a
7
+ `requireAuth` guard — built on [`@authdog/node-commons`](../node-commons).
8
+
9
+ - 🔌 **Drop-in plugin** — `app.register(authdogPlugin, { publicKey })`.
10
+ - 🔐 **Secure by default** — public key (and its identity host) validated once
11
+ at registration; tokens only trusted after the identity host confirms them.
12
+ - 🧱 **No assumptions** — parses cookies itself; `@fastify/cookie` not required.
13
+ - 🟦 **Typed** — `request.authdog` and `app.authdog` are fully typed via module
14
+ augmentation.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ bun add @authdog/fastify fastify
20
+ ```
21
+
22
+ Set your Authdog public key (safe to expose):
23
+
24
+ ```bash
25
+ AUTHDOG_PK=pk_xxxxxxxxxxxxxxxx
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```ts
31
+ import Fastify from "fastify";
32
+ import { authdogPlugin } from "@authdog/fastify";
33
+
34
+ const app = Fastify();
35
+
36
+ await app.register(authdogPlugin, {
37
+ publicKey: process.env.AUTHDOG_PK!,
38
+ });
39
+
40
+ // Every request now carries `request.authdog` ({ token, user, isAuthenticated }).
41
+ app.get("/", async (request) => {
42
+ return request.authdog?.isAuthenticated
43
+ ? `Hello ${JSON.stringify(request.authdog.user)}`
44
+ : "Not signed in";
45
+ });
46
+
47
+ // Protect a route with the built-in guard (the real enforcement point).
48
+ app.get(
49
+ "/me",
50
+ { preHandler: app.authdog.requireAuth },
51
+ async (request) => request.authdog!.user,
52
+ );
53
+
54
+ // Logout: clears the session cookie and redirects to a sanitized ?redirect_uri.
55
+ app.get("/logout", (request, reply) => app.authdog.logout(request, reply));
56
+
57
+ await app.listen({ port: 3000 });
58
+ ```
59
+
60
+ ### Token resolution
61
+
62
+ On each request the plugin looks for a token in this order:
63
+
64
+ 1. The `authdog-session` cookie.
65
+ 2. An `Authorization: Bearer <token>` header.
66
+
67
+ If a token is found it is verified against the identity host's `userinfo`
68
+ endpoint and, on success, `request.authdog.isAuthenticated` becomes `true` and
69
+ `request.authdog.user` is populated. A missing or invalid token never throws —
70
+ it simply yields an unauthenticated context.
71
+
72
+ ### Options
73
+
74
+ | Option | Type | Default | Description |
75
+ | --------------- | --------- | ------- | --------------------------------------------------------------------------- |
76
+ | `publicKey` | `string` | — | Authdog public key (`pk_…`). Required. |
77
+ | `secretKey` | `string` | — | Reserved for future server-side session revocation. Currently unused. |
78
+ | `fetchUserInfo` | `boolean` | `true` | When `false`, skips the per-request `userinfo` call (token is not verified). |
79
+
80
+ > ⚠️ `request.authdog` is informational. Always gate protected routes with
81
+ > `app.authdog.requireAuth` (or your own check on `isAuthenticated`).
82
+
83
+ ## License
84
+
85
+ MIT
@@ -0,0 +1,68 @@
1
+ import { preHandlerHookHandler, FastifyRequest, FastifyReply, FastifyPluginCallback } from 'fastify';
2
+
3
+ /** Options accepted by {@link authdogPlugin} when registered. */
4
+ interface AuthdogPluginOptions {
5
+ /** Authdog public key (`pk_…`). Validated once at registration. */
6
+ publicKey: string;
7
+ /**
8
+ * Secret key. Reserved for future server-side session revocation; not yet
9
+ * used by the plugin.
10
+ */
11
+ secretKey?: string;
12
+ /**
13
+ * When `false`, the plugin will not call the identity host's `userinfo`
14
+ * endpoint on every request. The token is still exposed on
15
+ * `request.authdog.token`, but `isAuthenticated` stays `false` because the
16
+ * token has not been verified. Defaults to `true`.
17
+ */
18
+ fetchUserInfo?: boolean;
19
+ }
20
+ /**
21
+ * Per-request authentication context attached to `request.authdog` by the
22
+ * `onRequest` hook. `isAuthenticated` is only ever `true` when a token was
23
+ * present AND the identity host confirmed it (`meta.code === 200`).
24
+ */
25
+ interface AuthdogRequestContext {
26
+ token: string | null;
27
+ user: unknown | null;
28
+ isAuthenticated: boolean;
29
+ }
30
+ /** API decorated onto the Fastify instance as `fastify.authdog`. */
31
+ interface AuthdogInstanceApi {
32
+ /**
33
+ * Route-level `preHandler` that replies `401` unless the request is
34
+ * authenticated. This is the real enforcement point — `request.authdog`
35
+ * alone is informational.
36
+ */
37
+ requireAuth: preHandlerHookHandler;
38
+ /** Returns the validated public-key payload as a JSON string. */
39
+ getPublicKey: () => string;
40
+ /**
41
+ * Clears the session cookie and redirects to a sanitized `redirect_uri`
42
+ * query parameter (open-redirect safe), defaulting to `/`.
43
+ */
44
+ logout: (request: FastifyRequest, reply: FastifyReply) => void;
45
+ }
46
+ declare module "fastify" {
47
+ interface FastifyRequest {
48
+ authdog?: AuthdogRequestContext;
49
+ }
50
+ interface FastifyInstance {
51
+ authdog: AuthdogInstanceApi;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Authdog Fastify plugin. Register it once on your instance:
57
+ *
58
+ * ```ts
59
+ * await app.register(authdogPlugin, { publicKey: process.env.AUTHDOG_PK! });
60
+ * app.get("/me", { preHandler: app.authdog.requireAuth }, async (req) => req.authdog!.user);
61
+ * ```
62
+ *
63
+ * Wrapped with `fastify-plugin` so the request/instance decorations are not
64
+ * encapsulated and are visible to sibling plugins and routes.
65
+ */
66
+ declare const authdogPlugin: FastifyPluginCallback<AuthdogPluginOptions>;
67
+
68
+ export { type AuthdogInstanceApi, type AuthdogPluginOptions, type AuthdogRequestContext, authdogPlugin, authdogPlugin as default };
@@ -0,0 +1,68 @@
1
+ import { preHandlerHookHandler, FastifyRequest, FastifyReply, FastifyPluginCallback } from 'fastify';
2
+
3
+ /** Options accepted by {@link authdogPlugin} when registered. */
4
+ interface AuthdogPluginOptions {
5
+ /** Authdog public key (`pk_…`). Validated once at registration. */
6
+ publicKey: string;
7
+ /**
8
+ * Secret key. Reserved for future server-side session revocation; not yet
9
+ * used by the plugin.
10
+ */
11
+ secretKey?: string;
12
+ /**
13
+ * When `false`, the plugin will not call the identity host's `userinfo`
14
+ * endpoint on every request. The token is still exposed on
15
+ * `request.authdog.token`, but `isAuthenticated` stays `false` because the
16
+ * token has not been verified. Defaults to `true`.
17
+ */
18
+ fetchUserInfo?: boolean;
19
+ }
20
+ /**
21
+ * Per-request authentication context attached to `request.authdog` by the
22
+ * `onRequest` hook. `isAuthenticated` is only ever `true` when a token was
23
+ * present AND the identity host confirmed it (`meta.code === 200`).
24
+ */
25
+ interface AuthdogRequestContext {
26
+ token: string | null;
27
+ user: unknown | null;
28
+ isAuthenticated: boolean;
29
+ }
30
+ /** API decorated onto the Fastify instance as `fastify.authdog`. */
31
+ interface AuthdogInstanceApi {
32
+ /**
33
+ * Route-level `preHandler` that replies `401` unless the request is
34
+ * authenticated. This is the real enforcement point — `request.authdog`
35
+ * alone is informational.
36
+ */
37
+ requireAuth: preHandlerHookHandler;
38
+ /** Returns the validated public-key payload as a JSON string. */
39
+ getPublicKey: () => string;
40
+ /**
41
+ * Clears the session cookie and redirects to a sanitized `redirect_uri`
42
+ * query parameter (open-redirect safe), defaulting to `/`.
43
+ */
44
+ logout: (request: FastifyRequest, reply: FastifyReply) => void;
45
+ }
46
+ declare module "fastify" {
47
+ interface FastifyRequest {
48
+ authdog?: AuthdogRequestContext;
49
+ }
50
+ interface FastifyInstance {
51
+ authdog: AuthdogInstanceApi;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Authdog Fastify plugin. Register it once on your instance:
57
+ *
58
+ * ```ts
59
+ * await app.register(authdogPlugin, { publicKey: process.env.AUTHDOG_PK! });
60
+ * app.get("/me", { preHandler: app.authdog.requireAuth }, async (req) => req.authdog!.user);
61
+ * ```
62
+ *
63
+ * Wrapped with `fastify-plugin` so the request/instance decorations are not
64
+ * encapsulated and are visible to sibling plugins and routes.
65
+ */
66
+ declare const authdogPlugin: FastifyPluginCallback<AuthdogPluginOptions>;
67
+
68
+ export { type AuthdogInstanceApi, type AuthdogPluginOptions, type AuthdogRequestContext, authdogPlugin, authdogPlugin as default };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";var P=Object.create;var c=Object.defineProperty;var R=Object.getOwnPropertyDescriptor;var q=Object.getOwnPropertyNames;var C=Object.getPrototypeOf,H=Object.prototype.hasOwnProperty;var O=(t,e)=>{for(var o in e)c(t,o,{get:e[o],enumerable:!0})},g=(t,e,o,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of q(e))!H.call(t,r)&&r!==o&&c(t,r,{get:()=>e[r],enumerable:!(a=R(e,r))||a.enumerable});return t};var I=(t,e,o)=>(o=t!=null?P(C(t)):{},g(e||!t||!t.__esModule?c(o,"default",{value:t,enumerable:!0}):o,t)),S=t=>g(c({},"__esModule",{value:!0}),t);var E={};O(E,{authdogPlugin:()=>l,default:()=>y});module.exports=S(E);var f=I(require("fastify-plugin")),n=require("@authdog/node-commons"),p="authdog-session",F=t=>{let e=(0,n.parseCookies)(t.headers.cookie??null).find(a=>a.name===p)?.value;if(e)return e;let o=t.headers.authorization;if(o&&o.startsWith("Bearer ")){let a=o.slice(7).trim();return a.length>0?a:null}return null},b=(t,e,o)=>{let{publicKey:a,fetchUserInfo:r}=e,h=(0,n.validateAndParsePublicKey)(a),{identityHost:A,environmentId:m}=h;t.decorateRequest("authdog",void 0),t.addHook("onRequest",async u=>{let i=F(u),s={token:i,user:null,isAuthenticated:!1};if(i&&r!==!1)try{let d=await(0,n.fetchUserData)(A,m,i);(0,n.isAuthenticatedUserInfo)(d)&&(s.user=d.user??null,s.isAuthenticated=!0)}catch{}u.authdog=s});let k={requireAuth:(u,i,s)=>{if(!u.authdog?.isAuthenticated){i.code(401).send({error:"Unauthorized"});return}s()},getPublicKey:()=>JSON.stringify(h),logout:(u,i)=>{let s=process.env.NODE_ENV==="production"?" Secure;":"";i.header("Set-Cookie",`${p}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly;${s} SameSite=Lax`);let d=u.query??{},x=(0,n.sanitizeRedirectPath)(d.redirect_uri,"/");i.redirect(x)}};t.decorate("authdog",k),o()},l=(0,f.default)(b,{name:"@authdog/fastify",fastify:"4.x || 5.x"}),y=l;0&&(module.exports={authdogPlugin});
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/plugin.ts"],"sourcesContent":["// Re-export the type-only module augmentation of \"fastify\" so consumers get\n// typed `request.authdog` / `fastify.authdog` simply by importing this package.\nimport \"./types\";\n\nexport { authdogPlugin, default } from \"./plugin\";\nexport type {\n AuthdogInstanceApi,\n AuthdogPluginOptions,\n AuthdogRequestContext,\n} from \"./types\";\n","import fp from \"fastify-plugin\";\nimport type {\n FastifyPluginCallback,\n FastifyReply,\n FastifyRequest,\n preHandlerHookHandler,\n} from \"fastify\";\nimport {\n fetchUserData,\n isAuthenticatedUserInfo,\n parseCookies,\n sanitizeRedirectPath,\n validateAndParsePublicKey,\n} from \"@authdog/node-commons\";\nimport type {\n AuthdogInstanceApi,\n AuthdogPluginOptions,\n AuthdogRequestContext,\n} from \"./types\";\n\n/** Name of the cookie carrying the session token (shared across SDKs). */\nconst SESSION_COOKIE_NAME = \"authdog-session\";\n\n/**\n * Extracts the session token, preferring the `authdog-session` cookie and\n * falling back to a `Bearer` authorization header.\n *\n * The cookie header is parsed with the shared {@link parseCookies} (splits on\n * the first `=`, URL-decodes) so JWT values containing `=` are not truncated.\n * We deliberately do NOT depend on `@fastify/cookie` being registered.\n */\nconst extractToken = (request: FastifyRequest): string | null => {\n const fromCookie = parseCookies(request.headers.cookie ?? null).find(\n (c) => c.name === SESSION_COOKIE_NAME,\n )?.value;\n if (fromCookie) {\n return fromCookie;\n }\n\n const authz = request.headers.authorization;\n if (authz && authz.startsWith(\"Bearer \")) {\n const token = authz.slice(\"Bearer \".length).trim();\n return token.length > 0 ? token : null;\n }\n\n return null;\n};\n\nconst authdogPluginCallback: FastifyPluginCallback<AuthdogPluginOptions> = (\n fastify,\n options,\n done,\n) => {\n const { publicKey, fetchUserInfo } = options;\n\n // Validate and parse the public key once at registration. This enforces the\n // trusted identity-host allowlist (SSRF / token-exfiltration protection) and\n // fails fast if the key is malformed, rather than per-request.\n const payload = validateAndParsePublicKey(publicKey);\n const { identityHost, environmentId } = payload;\n\n // Stable per-request default. Reassigned to a fresh object in the hook below,\n // so the (deprecated in v5) shared-reference pitfall does not apply.\n fastify.decorateRequest(\"authdog\", undefined);\n\n fastify.addHook(\"onRequest\", async (request) => {\n const token = extractToken(request);\n\n const context: AuthdogRequestContext = {\n token,\n user: null,\n isAuthenticated: false,\n };\n\n if (token && fetchUserInfo !== false) {\n try {\n const data = await fetchUserData(identityHost, environmentId, token);\n // Only trust a genuine success envelope (`meta.code === 200` + a user);\n // a 200 HTTP status alone is not sufficient.\n if (isAuthenticatedUserInfo(data)) {\n context.user = data.user ?? null;\n context.isAuthenticated = true;\n }\n } catch {\n // Never throw from the hook: an invalid/expired token simply yields an\n // unauthenticated context. Enforcement happens in `requireAuth`.\n }\n }\n\n request.authdog = context;\n });\n\n const requireAuth: preHandlerHookHandler = (request, reply, next) => {\n if (!request.authdog?.isAuthenticated) {\n reply.code(401).send({ error: \"Unauthorized\" });\n return;\n }\n next();\n };\n\n const logout = (request: FastifyRequest, reply: FastifyReply): void => {\n // Expire the session cookie. `Secure` is gated on production so local HTTP\n // development still clears the cookie; HttpOnly + SameSite=Lax retained.\n const secure = process.env.NODE_ENV === \"production\" ? \" Secure;\" : \"\";\n reply.header(\n \"Set-Cookie\",\n `${SESSION_COOKIE_NAME}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly;${secure} SameSite=Lax`,\n );\n\n // Sanitize the redirect target to prevent an open redirect via an\n // attacker-controlled `redirect_uri` query parameter.\n const query = (request.query ?? {}) as Record<string, unknown>;\n const target = sanitizeRedirectPath(query.redirect_uri, \"/\");\n reply.redirect(target);\n };\n\n const api: AuthdogInstanceApi = {\n requireAuth,\n getPublicKey: () => JSON.stringify(payload),\n logout,\n };\n\n fastify.decorate(\"authdog\", api);\n\n done();\n};\n\n/**\n * Authdog Fastify plugin. Register it once on your instance:\n *\n * ```ts\n * await app.register(authdogPlugin, { publicKey: process.env.AUTHDOG_PK! });\n * app.get(\"/me\", { preHandler: app.authdog.requireAuth }, async (req) => req.authdog!.user);\n * ```\n *\n * Wrapped with `fastify-plugin` so the request/instance decorations are not\n * encapsulated and are visible to sibling plugins and routes.\n */\nexport const authdogPlugin = fp(authdogPluginCallback, {\n name: \"@authdog/fastify\",\n fastify: \"4.x || 5.x\",\n});\n\nexport default authdogPlugin;\n"],"mappings":"0jBAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,mBAAAE,EAAA,YAAAC,IAAA,eAAAC,EAAAJ,GCAA,IAAAK,EAAe,6BAOfC,EAMO,iCAQDC,EAAsB,kBAUtBC,EAAgBC,GAA2C,CAC/D,IAAMC,KAAa,gBAAaD,EAAQ,QAAQ,QAAU,IAAI,EAAE,KAC7DE,GAAMA,EAAE,OAASJ,CACpB,GAAG,MACH,GAAIG,EACF,OAAOA,EAGT,IAAME,EAAQH,EAAQ,QAAQ,cAC9B,GAAIG,GAASA,EAAM,WAAW,SAAS,EAAG,CACxC,IAAMC,EAAQD,EAAM,MAAM,CAAgB,EAAE,KAAK,EACjD,OAAOC,EAAM,OAAS,EAAIA,EAAQ,IACpC,CAEA,OAAO,IACT,EAEMC,EAAqE,CACzEC,EACAC,EACAC,IACG,CACH,GAAM,CAAE,UAAAC,EAAW,cAAAC,CAAc,EAAIH,EAK/BI,KAAU,6BAA0BF,CAAS,EAC7C,CAAE,aAAAG,EAAc,cAAAC,CAAc,EAAIF,EAIxCL,EAAQ,gBAAgB,UAAW,MAAS,EAE5CA,EAAQ,QAAQ,YAAa,MAAON,GAAY,CAC9C,IAAMI,EAAQL,EAAaC,CAAO,EAE5Bc,EAAiC,CACrC,MAAAV,EACA,KAAM,KACN,gBAAiB,EACnB,EAEA,GAAIA,GAASM,IAAkB,GAC7B,GAAI,CACF,IAAMK,EAAO,QAAM,iBAAcH,EAAcC,EAAeT,CAAK,KAG/D,2BAAwBW,CAAI,IAC9BD,EAAQ,KAAOC,EAAK,MAAQ,KAC5BD,EAAQ,gBAAkB,GAE9B,MAAQ,CAGR,CAGFd,EAAQ,QAAUc,CACpB,CAAC,EA0BD,IAAME,EAA0B,CAC9B,YAzByC,CAAChB,EAASiB,EAAOC,IAAS,CACnE,GAAI,CAAClB,EAAQ,SAAS,gBAAiB,CACrCiB,EAAM,KAAK,GAAG,EAAE,KAAK,CAAE,MAAO,cAAe,CAAC,EAC9C,MACF,CACAC,EAAK,CACP,EAoBE,aAAc,IAAM,KAAK,UAAUP,CAAO,EAC1C,OAnBa,CAACX,EAAyBiB,IAA8B,CAGrE,IAAME,EAAS,QAAQ,IAAI,WAAa,aAAe,WAAa,GACpEF,EAAM,OACJ,aACA,GAAGnB,CAAmB,8DAA8DqB,CAAM,eAC5F,EAIA,IAAMC,EAASpB,EAAQ,OAAS,CAAC,EAC3BqB,KAAS,wBAAqBD,EAAM,aAAc,GAAG,EAC3DH,EAAM,SAASI,CAAM,CACvB,CAMA,EAEAf,EAAQ,SAAS,UAAWU,CAAG,EAE/BR,EAAK,CACP,EAaac,KAAgB,EAAAC,SAAGlB,EAAuB,CACrD,KAAM,mBACN,QAAS,YACX,CAAC,EAEMmB,EAAQF","names":["index_exports","__export","authdogPlugin","plugin_default","__toCommonJS","import_fastify_plugin","import_node_commons","SESSION_COOKIE_NAME","extractToken","request","fromCookie","c","authz","token","authdogPluginCallback","fastify","options","done","publicKey","fetchUserInfo","payload","identityHost","environmentId","context","data","api","reply","next","secure","query","target","authdogPlugin","fp","plugin_default"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import y from"fastify-plugin";import{fetchUserData as A,isAuthenticatedUserInfo as m,parseCookies as k,sanitizeRedirectPath as x,validateAndParsePublicKey as P}from"@authdog/node-commons";var d="authdog-session",R=o=>{let r=k(o.headers.cookie??null).find(n=>n.name===d)?.value;if(r)return r;let i=o.headers.authorization;if(i&&i.startsWith("Bearer ")){let n=i.slice(7).trim();return n.length>0?n:null}return null},q=(o,r,i)=>{let{publicKey:n,fetchUserInfo:l}=r,u=P(n),{identityHost:h,environmentId:g}=u;o.decorateRequest("authdog",void 0),o.addHook("onRequest",async a=>{let t=R(a),e={token:t,user:null,isAuthenticated:!1};if(t&&l!==!1)try{let s=await A(h,g,t);m(s)&&(e.user=s.user??null,e.isAuthenticated=!0)}catch{}a.authdog=e});let f={requireAuth:(a,t,e)=>{if(!a.authdog?.isAuthenticated){t.code(401).send({error:"Unauthorized"});return}e()},getPublicKey:()=>JSON.stringify(u),logout:(a,t)=>{let e=process.env.NODE_ENV==="production"?" Secure;":"";t.header("Set-Cookie",`${d}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly;${e} SameSite=Lax`);let s=a.query??{},p=x(s.redirect_uri,"/");t.redirect(p)}};o.decorate("authdog",f),i()},c=y(q,{name:"@authdog/fastify",fastify:"4.x || 5.x"}),C=c;export{c as authdogPlugin,C as default};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import fp from \"fastify-plugin\";\nimport type {\n FastifyPluginCallback,\n FastifyReply,\n FastifyRequest,\n preHandlerHookHandler,\n} from \"fastify\";\nimport {\n fetchUserData,\n isAuthenticatedUserInfo,\n parseCookies,\n sanitizeRedirectPath,\n validateAndParsePublicKey,\n} from \"@authdog/node-commons\";\nimport type {\n AuthdogInstanceApi,\n AuthdogPluginOptions,\n AuthdogRequestContext,\n} from \"./types\";\n\n/** Name of the cookie carrying the session token (shared across SDKs). */\nconst SESSION_COOKIE_NAME = \"authdog-session\";\n\n/**\n * Extracts the session token, preferring the `authdog-session` cookie and\n * falling back to a `Bearer` authorization header.\n *\n * The cookie header is parsed with the shared {@link parseCookies} (splits on\n * the first `=`, URL-decodes) so JWT values containing `=` are not truncated.\n * We deliberately do NOT depend on `@fastify/cookie` being registered.\n */\nconst extractToken = (request: FastifyRequest): string | null => {\n const fromCookie = parseCookies(request.headers.cookie ?? null).find(\n (c) => c.name === SESSION_COOKIE_NAME,\n )?.value;\n if (fromCookie) {\n return fromCookie;\n }\n\n const authz = request.headers.authorization;\n if (authz && authz.startsWith(\"Bearer \")) {\n const token = authz.slice(\"Bearer \".length).trim();\n return token.length > 0 ? token : null;\n }\n\n return null;\n};\n\nconst authdogPluginCallback: FastifyPluginCallback<AuthdogPluginOptions> = (\n fastify,\n options,\n done,\n) => {\n const { publicKey, fetchUserInfo } = options;\n\n // Validate and parse the public key once at registration. This enforces the\n // trusted identity-host allowlist (SSRF / token-exfiltration protection) and\n // fails fast if the key is malformed, rather than per-request.\n const payload = validateAndParsePublicKey(publicKey);\n const { identityHost, environmentId } = payload;\n\n // Stable per-request default. Reassigned to a fresh object in the hook below,\n // so the (deprecated in v5) shared-reference pitfall does not apply.\n fastify.decorateRequest(\"authdog\", undefined);\n\n fastify.addHook(\"onRequest\", async (request) => {\n const token = extractToken(request);\n\n const context: AuthdogRequestContext = {\n token,\n user: null,\n isAuthenticated: false,\n };\n\n if (token && fetchUserInfo !== false) {\n try {\n const data = await fetchUserData(identityHost, environmentId, token);\n // Only trust a genuine success envelope (`meta.code === 200` + a user);\n // a 200 HTTP status alone is not sufficient.\n if (isAuthenticatedUserInfo(data)) {\n context.user = data.user ?? null;\n context.isAuthenticated = true;\n }\n } catch {\n // Never throw from the hook: an invalid/expired token simply yields an\n // unauthenticated context. Enforcement happens in `requireAuth`.\n }\n }\n\n request.authdog = context;\n });\n\n const requireAuth: preHandlerHookHandler = (request, reply, next) => {\n if (!request.authdog?.isAuthenticated) {\n reply.code(401).send({ error: \"Unauthorized\" });\n return;\n }\n next();\n };\n\n const logout = (request: FastifyRequest, reply: FastifyReply): void => {\n // Expire the session cookie. `Secure` is gated on production so local HTTP\n // development still clears the cookie; HttpOnly + SameSite=Lax retained.\n const secure = process.env.NODE_ENV === \"production\" ? \" Secure;\" : \"\";\n reply.header(\n \"Set-Cookie\",\n `${SESSION_COOKIE_NAME}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly;${secure} SameSite=Lax`,\n );\n\n // Sanitize the redirect target to prevent an open redirect via an\n // attacker-controlled `redirect_uri` query parameter.\n const query = (request.query ?? {}) as Record<string, unknown>;\n const target = sanitizeRedirectPath(query.redirect_uri, \"/\");\n reply.redirect(target);\n };\n\n const api: AuthdogInstanceApi = {\n requireAuth,\n getPublicKey: () => JSON.stringify(payload),\n logout,\n };\n\n fastify.decorate(\"authdog\", api);\n\n done();\n};\n\n/**\n * Authdog Fastify plugin. Register it once on your instance:\n *\n * ```ts\n * await app.register(authdogPlugin, { publicKey: process.env.AUTHDOG_PK! });\n * app.get(\"/me\", { preHandler: app.authdog.requireAuth }, async (req) => req.authdog!.user);\n * ```\n *\n * Wrapped with `fastify-plugin` so the request/instance decorations are not\n * encapsulated and are visible to sibling plugins and routes.\n */\nexport const authdogPlugin = fp(authdogPluginCallback, {\n name: \"@authdog/fastify\",\n fastify: \"4.x || 5.x\",\n});\n\nexport default authdogPlugin;\n"],"mappings":"AAAA,OAAOA,MAAQ,iBAOf,OACE,iBAAAC,EACA,2BAAAC,EACA,gBAAAC,EACA,wBAAAC,EACA,6BAAAC,MACK,wBAQP,IAAMC,EAAsB,kBAUtBC,EAAgBC,GAA2C,CAC/D,IAAMC,EAAaN,EAAaK,EAAQ,QAAQ,QAAU,IAAI,EAAE,KAC7DE,GAAMA,EAAE,OAASJ,CACpB,GAAG,MACH,GAAIG,EACF,OAAOA,EAGT,IAAME,EAAQH,EAAQ,QAAQ,cAC9B,GAAIG,GAASA,EAAM,WAAW,SAAS,EAAG,CACxC,IAAMC,EAAQD,EAAM,MAAM,CAAgB,EAAE,KAAK,EACjD,OAAOC,EAAM,OAAS,EAAIA,EAAQ,IACpC,CAEA,OAAO,IACT,EAEMC,EAAqE,CACzEC,EACAC,EACAC,IACG,CACH,GAAM,CAAE,UAAAC,EAAW,cAAAC,CAAc,EAAIH,EAK/BI,EAAUd,EAA0BY,CAAS,EAC7C,CAAE,aAAAG,EAAc,cAAAC,CAAc,EAAIF,EAIxCL,EAAQ,gBAAgB,UAAW,MAAS,EAE5CA,EAAQ,QAAQ,YAAa,MAAON,GAAY,CAC9C,IAAMI,EAAQL,EAAaC,CAAO,EAE5Bc,EAAiC,CACrC,MAAAV,EACA,KAAM,KACN,gBAAiB,EACnB,EAEA,GAAIA,GAASM,IAAkB,GAC7B,GAAI,CACF,IAAMK,EAAO,MAAMtB,EAAcmB,EAAcC,EAAeT,CAAK,EAG/DV,EAAwBqB,CAAI,IAC9BD,EAAQ,KAAOC,EAAK,MAAQ,KAC5BD,EAAQ,gBAAkB,GAE9B,MAAQ,CAGR,CAGFd,EAAQ,QAAUc,CACpB,CAAC,EA0BD,IAAME,EAA0B,CAC9B,YAzByC,CAAChB,EAASiB,EAAOC,IAAS,CACnE,GAAI,CAAClB,EAAQ,SAAS,gBAAiB,CACrCiB,EAAM,KAAK,GAAG,EAAE,KAAK,CAAE,MAAO,cAAe,CAAC,EAC9C,MACF,CACAC,EAAK,CACP,EAoBE,aAAc,IAAM,KAAK,UAAUP,CAAO,EAC1C,OAnBa,CAACX,EAAyBiB,IAA8B,CAGrE,IAAME,EAAS,QAAQ,IAAI,WAAa,aAAe,WAAa,GACpEF,EAAM,OACJ,aACA,GAAGnB,CAAmB,8DAA8DqB,CAAM,eAC5F,EAIA,IAAMC,EAASpB,EAAQ,OAAS,CAAC,EAC3BqB,EAASzB,EAAqBwB,EAAM,aAAc,GAAG,EAC3DH,EAAM,SAASI,CAAM,CACvB,CAMA,EAEAf,EAAQ,SAAS,UAAWU,CAAG,EAE/BR,EAAK,CACP,EAaac,EAAgB9B,EAAGa,EAAuB,CACrD,KAAM,mBACN,QAAS,YACX,CAAC,EAEMkB,EAAQD","names":["fp","fetchUserData","isAuthenticatedUserInfo","parseCookies","sanitizeRedirectPath","validateAndParsePublicKey","SESSION_COOKIE_NAME","extractToken","request","fromCookie","c","authz","token","authdogPluginCallback","fastify","options","done","publicKey","fetchUserInfo","payload","identityHost","environmentId","context","data","api","reply","next","secure","query","target","authdogPlugin","plugin_default"]}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@authdog/fastify",
3
+ "version": "0.2.0",
4
+ "description": "Authdog Fastify SDK",
5
+ "source": "src/index.ts",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist/"
18
+ ],
19
+ "sideEffects": false,
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/authdog-labs/web-sdk.git",
23
+ "directory": "packages/fastify"
24
+ },
25
+ "homepage": "https://github.com/authdog-labs/web-sdk/tree/main/packages/fastify#readme",
26
+ "bugs": {
27
+ "url": "https://github.com/authdog-labs/web-sdk/issues"
28
+ },
29
+ "scripts": {
30
+ "format": "prettier --config .prettierrc.json --write \"**/*.{ts,md}\"",
31
+ "type-check": "tsc",
32
+ "clean": "rm -rf dist",
33
+ "build": "bun run clean && tsup",
34
+ "ship": "bun run build && bun publish --access public"
35
+ },
36
+ "dependencies": {
37
+ "@authdog/node-commons": "workspace:*",
38
+ "fastify-plugin": "^5.0.0"
39
+ },
40
+ "peerDependencies": {
41
+ "fastify": "^4.0.0 || ^5.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^22.10.5",
45
+ "dotenv": "^16.4.7",
46
+ "fastify": "^5.0.0",
47
+ "prettier": "^3.4.2",
48
+ "tsup": "^8.3.5",
49
+ "typescript": "^5.7.2",
50
+ "vitest": "^2.1.8"
51
+ },
52
+ "publishConfig": {
53
+ "registry": "https://registry.npmjs.org/",
54
+ "access": "public"
55
+ }
56
+ }