@nerdsystem/sdk 0.0.6 → 0.0.8

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.
@@ -21,9 +21,16 @@ export declare class AuthServiceError extends Error {
21
21
  export declare class AuthServiceClient {
22
22
  private readonly env;
23
23
  private readonly fetchImpl;
24
+ /** The visitor's address, forwarded so the platform rate-limits per person rather than per app. */
25
+ private readonly clientIp?;
24
26
  private readonly base;
25
- constructor(env: Pick<NerdEnv, "NERD_AUTH_URL" | "NERD_PROJECT_ID" | "NERD_AUTH_SECRET">, fetchImpl?: typeof fetch);
27
+ constructor(env: Pick<NerdEnv, "NERD_AUTH_URL" | "NERD_PROJECT_ID" | "NERD_AUTH_SECRET">, fetchImpl?: typeof fetch,
28
+ /** The visitor's address, forwarded so the platform rate-limits per person rather than per app. */
29
+ clientIp?: string | undefined);
26
30
  get jwksUrl(): string;
31
+ private signature?;
32
+ /** HMAC of the visitor address keyed by SHA-256(tenant secret), which is what the platform stores; an unsigned address is ignored there. */
33
+ private signVisitor;
27
34
  private call;
28
35
  signup(input: {
29
36
  email: string;
@@ -13,23 +13,41 @@ export class AuthServiceError extends Error {
13
13
  export class AuthServiceClient {
14
14
  env;
15
15
  fetchImpl;
16
+ clientIp;
16
17
  base;
17
18
  constructor(env,
18
19
  // Wrapped, not bare: calling a stored `fetch` as a method breaks in workerd (illegal invocation).
19
- fetchImpl = (input, init) => fetch(input, init)) {
20
+ fetchImpl = (input, init) => fetch(input, init),
21
+ /** The visitor's address, forwarded so the platform rate-limits per person rather than per app. */
22
+ clientIp) {
20
23
  this.env = env;
21
24
  this.fetchImpl = fetchImpl;
25
+ this.clientIp = clientIp;
22
26
  this.base = `${env.NERD_AUTH_URL.replace(/\/$/, "")}/t/${env.NERD_PROJECT_ID}`;
23
27
  }
24
28
  get jwksUrl() {
25
29
  return `${this.env.NERD_AUTH_URL.replace(/\/$/, "")}/.well-known/jwks.json`;
26
30
  }
31
+ signature;
32
+ /** HMAC of the visitor address keyed by SHA-256(tenant secret), which is what the platform stores; an unsigned address is ignored there. */
33
+ signVisitor(ip) {
34
+ this.signature ??= (async () => {
35
+ const enc = new TextEncoder();
36
+ const digest = await crypto.subtle.digest("SHA-256", enc.encode(this.env.NERD_AUTH_SECRET));
37
+ const keyHex = Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
38
+ const key = await crypto.subtle.importKey("raw", enc.encode(keyHex), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
39
+ return Array.from(new Uint8Array(await crypto.subtle.sign("HMAC", key, enc.encode(ip))), (b) => b.toString(16).padStart(2, "0")).join("");
40
+ })();
41
+ return this.signature;
42
+ }
27
43
  async call(method, path, body, auth) {
44
+ const visitor = this.clientIp && this.env.NERD_AUTH_SECRET ? { "x-nerd-client-ip": this.clientIp, "x-nerd-client-sig": await this.signVisitor(this.clientIp) } : {};
28
45
  const res = await this.fetchImpl(`${this.base}${path}`, {
29
46
  method,
30
47
  headers: {
31
48
  ...(body !== undefined ? { "content-type": "application/json" } : {}),
32
49
  ...(auth ? { authorization: `Bearer ${auth}` } : {}),
50
+ ...visitor,
33
51
  },
34
52
  body: body !== undefined ? JSON.stringify(body) : null,
35
53
  });
@@ -16,7 +16,7 @@ export async function resolveUser(c) {
16
16
  const token = bearer?.toLowerCase().startsWith("bearer ") ? bearer.slice(7) : readAccessCookie(c);
17
17
  if (!token)
18
18
  return null;
19
- const client = new AuthServiceClient(c.env);
19
+ const client = new AuthServiceClient(c.env, undefined, c.req.header("cf-connecting-ip"));
20
20
  const verified = await verifyAccessToken(token, { jwksUrl: client.jwksUrl, projectId: c.env.NERD_PROJECT_ID });
21
21
  if (!verified)
22
22
  return null;
@@ -39,7 +39,7 @@ export function authRoutes() {
39
39
  if (c.env.NERD_TURNSTILE_SECRET && !(await verifyTurnstile(c.env.NERD_TURNSTILE_SECRET, str(body.turnstileToken), c.req.header("cf-connecting-ip")))) {
40
40
  return c.json({ error: { code: "bad_request", message: "Please complete the verification challenge" } }, 400);
41
41
  }
42
- const session = await new AuthServiceClient(c.env).signup({ email, password, name });
42
+ const session = await new AuthServiceClient(c.env, undefined, c.req.header("cf-connecting-ip")).signup({ email, password, name });
43
43
  setSessionCookies(c, session);
44
44
  return c.json({ user: session.user }, 201);
45
45
  });
@@ -49,7 +49,7 @@ export function authRoutes() {
49
49
  const password = str(body.password);
50
50
  if (!email || !password)
51
51
  return c.json({ error: { code: "bad_request", message: "email and password are required" } }, 400);
52
- const session = await new AuthServiceClient(c.env).login({ email, password });
52
+ const session = await new AuthServiceClient(c.env, undefined, c.req.header("cf-connecting-ip")).login({ email, password });
53
53
  setSessionCookies(c, session);
54
54
  return c.json({ user: session.user }, 200);
55
55
  });
@@ -58,7 +58,7 @@ export function authRoutes() {
58
58
  if (!refreshToken)
59
59
  return c.json({ error: { code: "unauthorized", message: "No session" } }, 401);
60
60
  try {
61
- const session = await new AuthServiceClient(c.env).refresh(refreshToken);
61
+ const session = await new AuthServiceClient(c.env, undefined, c.req.header("cf-connecting-ip")).refresh(refreshToken);
62
62
  setSessionCookies(c, session);
63
63
  return c.json({ user: session.user }, 200);
64
64
  }
@@ -73,7 +73,7 @@ export function authRoutes() {
73
73
  r.post("/logout", async (c) => {
74
74
  const refreshToken = readRefreshCookie(c);
75
75
  if (refreshToken)
76
- await new AuthServiceClient(c.env).logout(refreshToken).catch(() => { });
76
+ await new AuthServiceClient(c.env, undefined, c.req.header("cf-connecting-ip")).logout(refreshToken).catch(() => { });
77
77
  clearSessionCookies(c);
78
78
  return c.json({ ok: true }, 200);
79
79
  });
@@ -84,7 +84,7 @@ export function authRoutes() {
84
84
  if (!email)
85
85
  return c.json({ error: { code: "bad_request", message: "email is required" } }, 400);
86
86
  const origin = new URL(c.req.url).origin;
87
- await new AuthServiceClient(c.env).forgotPassword({ email, resetUrl: `${origin}/reset-password`, ...(c.env.NERD_APP_NAME ? { appName: c.env.NERD_APP_NAME } : {}) });
87
+ await new AuthServiceClient(c.env, undefined, c.req.header("cf-connecting-ip")).forgotPassword({ email, resetUrl: `${origin}/reset-password`, ...(c.env.NERD_APP_NAME ? { appName: c.env.NERD_APP_NAME } : {}) });
88
88
  return c.json({ ok: true }, 200);
89
89
  });
90
90
  r.post("/reset", async (c) => {
@@ -93,7 +93,7 @@ export function authRoutes() {
93
93
  const password = str(body.password);
94
94
  if (!token || !password)
95
95
  return c.json({ error: { code: "bad_request", message: "token and password are required" } }, 400);
96
- const session = await new AuthServiceClient(c.env).resetPassword({ token, password });
96
+ const session = await new AuthServiceClient(c.env, undefined, c.req.header("cf-connecting-ip")).resetPassword({ token, password });
97
97
  setSessionCookies(c, session);
98
98
  return c.json({ user: session.user }, 200);
99
99
  });
@@ -106,7 +106,7 @@ export function authRoutes() {
106
106
  if (!refreshToken || readAccessCookie(c))
107
107
  return c.json({ user: null }, 200);
108
108
  try {
109
- const session = await new AuthServiceClient(c.env).refresh(refreshToken);
109
+ const session = await new AuthServiceClient(c.env, undefined, c.req.header("cf-connecting-ip")).refresh(refreshToken);
110
110
  setSessionCookies(c, session);
111
111
  return c.json({ user: { id: session.user.id, email: session.user.email, name: session.user.name, roles: session.user.roles } }, 200);
112
112
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nerdsystem/sdk",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Runtime for apps deployed on nerdsystem: auth, authorization, and platform bindings.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",