@nerdsystem/sdk 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.d.ts CHANGED
@@ -7,6 +7,8 @@ export interface CreateNerdAppOptions {
7
7
  authPath?: string;
8
8
  /** Security headers on every response. Pass false to disable, or options to tune the CSP. */
9
9
  securityHeaders?: SecurityHeadersOptions | false;
10
+ /** Reject cookie-authenticated writes that a browser sent from another site. Pass false only if you handle CSRF yourself. */
11
+ crossSiteGuard?: false;
10
12
  }
11
13
  /**
12
14
  * A Hono app with nerdsystem auth wired in: session routes at /api/auth and `c.var.user`
package/dist/app.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Hono } from "hono";
2
2
  import { attachUser } from "./auth/middleware";
3
+ import { crossSiteGuard } from "./auth/csrf";
3
4
  import { authRoutes } from "./auth/routes";
4
5
  import { securityHeaders } from "./security";
5
6
  /**
@@ -10,6 +11,8 @@ export function createNerdApp(options = {}) {
10
11
  const app = new Hono();
11
12
  if (options.securityHeaders !== false)
12
13
  app.use("*", securityHeaders(options.securityHeaders ?? {}));
14
+ if (options.crossSiteGuard !== false)
15
+ app.use("*", crossSiteGuard());
13
16
  app.use("*", attachUser());
14
17
  app.route(options.authPath ?? "/api/auth", authRoutes());
15
18
  app.notFound((c) => c.json({ error: { code: "not_found", message: `No route for ${c.req.method} ${c.req.path}` } }, 404));
@@ -41,6 +41,7 @@ export declare class AuthServiceClient {
41
41
  me(accessToken: string): Promise<{
42
42
  user: EndUser;
43
43
  }>;
44
+ /** Server-side only: the platform requires the tenant secret so nobody can turn the reset email into a phishing relay. */
44
45
  forgotPassword(input: {
45
46
  email: string;
46
47
  resetUrl: string;
@@ -56,8 +56,9 @@ export class AuthServiceClient {
56
56
  me(accessToken) {
57
57
  return this.call("GET", "/me", undefined, accessToken);
58
58
  }
59
+ /** Server-side only: the platform requires the tenant secret so nobody can turn the reset email into a phishing relay. */
59
60
  forgotPassword(input) {
60
- return this.call("POST", "/password/forgot", input);
61
+ return this.call("POST", "/password/forgot", input, this.env.NERD_AUTH_SECRET);
61
62
  }
62
63
  resetPassword(input) {
63
64
  return this.call("POST", "/password/reset", input);
@@ -0,0 +1,9 @@
1
+ import type { MiddlewareHandler } from "hono";
2
+ /**
3
+ * Rejects state-changing requests that a browser sent from another site. Every app on the shared
4
+ * app domain is "same-site" with every other until the domain is on the Public Suffix List, and
5
+ * the auth cookies are SameSite=Lax, so without this a page on one app could POST to another with
6
+ * the visitor's cookies attached. Requests with a bearer token (mobile and server clients) and
7
+ * requests without browser fetch metadata are allowed through.
8
+ */
9
+ export declare function crossSiteGuard(): MiddlewareHandler;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Rejects state-changing requests that a browser sent from another site. Every app on the shared
3
+ * app domain is "same-site" with every other until the domain is on the Public Suffix List, and
4
+ * the auth cookies are SameSite=Lax, so without this a page on one app could POST to another with
5
+ * the visitor's cookies attached. Requests with a bearer token (mobile and server clients) and
6
+ * requests without browser fetch metadata are allowed through.
7
+ */
8
+ export function crossSiteGuard() {
9
+ return async (c, next) => {
10
+ const method = c.req.method;
11
+ if (method === "GET" || method === "HEAD" || method === "OPTIONS")
12
+ return next();
13
+ if (c.req.header("authorization")?.toLowerCase().startsWith("bearer "))
14
+ return next();
15
+ const site = c.req.header("sec-fetch-site");
16
+ if (site && site !== "same-origin" && site !== "none")
17
+ return reject(c);
18
+ const origin = c.req.header("origin");
19
+ if (origin && origin !== new URL(c.req.url).origin)
20
+ return reject(c);
21
+ return next();
22
+ };
23
+ }
24
+ function reject(c) {
25
+ return c.json({ error: { code: "forbidden", message: "Cross-site request rejected. Call this API from the app's own pages, or send a bearer token." } }, 403);
26
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { createNerdApp, type CreateNerdAppOptions } from "./app";
2
2
  export type { NerdEnv, NerdUser, NerdVariables } from "./env";
3
3
  export { attachUser, requireAuth, requireRole, resolveUser, type NerdHonoEnv } from "./auth/middleware";
4
+ export { crossSiteGuard } from "./auth/csrf";
4
5
  export { AuthServiceClient, AuthServiceError, type EndUser, type Session } from "./auth/client";
5
6
  export { verifyAccessToken, type VerifiedToken } from "./auth/jwks";
6
7
  export { authRoutes } from "./auth/routes";
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { createNerdApp } from "./app";
2
2
  export { attachUser, requireAuth, requireRole, resolveUser } from "./auth/middleware";
3
+ export { crossSiteGuard } from "./auth/csrf";
3
4
  export { AuthServiceClient, AuthServiceError } from "./auth/client";
4
5
  export { verifyAccessToken } from "./auth/jwks";
5
6
  export { authRoutes } from "./auth/routes";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nerdsystem/sdk",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "Runtime for apps deployed on nerdsystem: auth, authorization, and platform bindings.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",