two-stroke 7.9.0 → 7.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "dependencies": {
13
13
  "@cloudflare/vitest-pool-workers": "^0.18.0",
14
14
  "@sentry/cli": "^3.6.0",
15
- "@typescript-eslint/eslint-plugin": "^8.62.1",
15
+ "@typescript-eslint/eslint-plugin": "^8.63.0",
16
16
  "@typescript-eslint/parser": "^8.62.1",
17
17
  "@vitest/runner": "^4.1.8",
18
18
  "@vitest/snapshot": "^4.1.8",
@@ -20,13 +20,11 @@
20
20
  "eslint-config-two-stroke": "^1.5.7",
21
21
  "eslint-config-typescript": "^3.0.0",
22
22
  "jose": "^6.2.3",
23
- "jwk-subtle": "^1.1.8",
24
23
  "mime": "^4.1.0",
25
24
  "miniflare": "^4.20260701.0",
26
25
  "msw": "^2.14.6",
27
26
  "openapi-fetch": "^0.17.0",
28
27
  "openapi-typescript": "^7.13.0",
29
- "pbkdf-subtle": "^1.1.8",
30
28
  "toucan-js": "^4.1.1",
31
29
  "zod": "^4.4.3"
32
30
  },
@@ -58,12 +56,12 @@
58
56
  "url": "https://github.com/change-engine/two-stroke"
59
57
  },
60
58
  "type": "module",
61
- "version": "7.9.0",
59
+ "version": "7.10.0",
62
60
  "devDependencies": {
63
61
  "@types/eslint": "^9.6.1",
64
62
  "eslint": "^10.6.0",
65
63
  "prettier": "^3.9.4",
66
64
  "typescript": "^6.0.3",
67
- "vitest": "^4.1.8"
65
+ "vitest": "^4.1.10"
68
66
  }
69
67
  }
package/src/index.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { verify as jwkVerify } from "jwk-subtle";
2
- import { verify as pbkdfVerify } from "pbkdf-subtle";
3
1
  import { Toucan } from "toucan-js";
4
2
  import { type ZodSafeParseResult, z, ZodObject, ZodType } from "zod/v4";
5
3
  import { openAPI } from "./open-api";
6
4
  import { type Handler, type Route } from "./types";
5
+ import type { JSONWebKeySet } from "jose";
6
+ import { pbkdfVerify, jwkVerifyMulti } from "./util";
7
7
 
8
8
  // eslint-disable-next-line @typescript-eslint/require-await
9
9
  const noAuth = async () => null;
@@ -301,7 +301,7 @@ export function twoStroke<T>(
301
301
  },
302
302
  noAuth,
303
303
  pbkdf:
304
- (k: keyof T, customHeaderName: string = "Authorization") =>
304
+ (k: keyof T, customHeaderName = "Authorization") =>
305
305
  async ({ req, env }: { req: Request; env: T }) => {
306
306
  const [scheme, token] = (req.headers.get(customHeaderName) ?? " ").split(" ");
307
307
  if (
@@ -309,20 +309,28 @@ export function twoStroke<T>(
309
309
  (await pbkdfVerify(env[k] as string, token ?? ""))
310
310
  )
311
311
  return;
312
- throw Error("Invalid");
312
+ throw new Error("Invalid");
313
313
  },
314
314
  jwt:
315
315
  <J>(k: keyof T, ak: keyof T) =>
316
316
  async ({ req, env }: { req: Request; env: T }) => {
317
317
  const [scheme, token] = (req.headers.get("Authorization") ?? " ").split(" ");
318
318
  if (scheme === "Bearer") {
319
- const claims = await jwkVerify<J>(token ?? "", env[k] as string, env[ak] as string);
319
+ const rawIssuer = env[k] as string;
320
+ const rawAudience = env[ak] as string;
321
+ const issuer = rawIssuer.startsWith("{")
322
+ ? (JSON.parse(rawIssuer) as Record<string, JSONWebKeySet | true>)
323
+ : { [rawIssuer]: true as const };
324
+ const audience = rawAudience.startsWith("[")
325
+ ? (JSON.parse(rawAudience) as string[])
326
+ : [rawAudience];
327
+ const claims = await jwkVerifyMulti<J>(token ?? "", issuer, audience);
320
328
  if (!claims) {
321
- throw Error("Invalid");
329
+ throw new Error("Invalid");
322
330
  }
323
331
  return claims;
324
332
  }
325
- throw Error("Invalid");
333
+ throw new Error("Invalid");
326
334
  },
327
335
  queueHandler<I extends ZodType>(
328
336
  input: I,
package/src/util.ts ADDED
@@ -0,0 +1,77 @@
1
+ import {
2
+ type FlattenedJWSInput,
3
+ type JSONWebKeySet,
4
+ type JWSHeaderParameters,
5
+ createLocalJWKSet,
6
+ createRemoteJWKSet,
7
+ decodeJwt,
8
+ jwtVerify,
9
+ } from "jose";
10
+
11
+ export async function pbkdfVerify(key: string, password: string) {
12
+ return (
13
+ [
14
+ ...new Uint8Array(
15
+ await crypto.subtle.deriveBits(
16
+ {
17
+ name: "PBKDF2",
18
+ hash: "SHA-256",
19
+ salt: new Uint8Array([...atob(key).slice(3, 19)].map((ch) => ch.charCodeAt(0))),
20
+ iterations: parseInt(
21
+ [...atob(key).slice(19, 22)].map((ch) => ch.charCodeAt(0).toString(16)).join(""),
22
+ 16,
23
+ ),
24
+ },
25
+ await crypto.subtle.importKey(
26
+ "raw",
27
+ new TextEncoder().encode(password),
28
+ "PBKDF2",
29
+ false,
30
+ ["deriveBits"],
31
+ ),
32
+ 256,
33
+ ),
34
+ ),
35
+ ]
36
+ .map((byte) => String.fromCharCode(byte))
37
+ .join("") === atob(key).slice(22, 54)
38
+ );
39
+ }
40
+
41
+ export async function jwkVerifyMulti<J>(
42
+ token: string,
43
+ issuers: Record<string, JSONWebKeySet | true>,
44
+ audiences: string[],
45
+ ) {
46
+ const unverifiedClaims = decodeJwt(token);
47
+ if (!unverifiedClaims.iss) throw new Error("Invalid");
48
+ const issuer = issuers[unverifiedClaims.iss];
49
+ if (!issuer) throw new Error("Invalid");
50
+
51
+ let JWKS: (
52
+ protectedHeader?: JWSHeaderParameters,
53
+ token?: FlattenedJWSInput,
54
+ ) => Promise<CryptoKey>;
55
+
56
+ if (issuer === true) {
57
+ // OIDC
58
+ const oidcRequest = await fetch(
59
+ `${unverifiedClaims.iss}${unverifiedClaims.iss.endsWith("/") ? "" : "/"}.well-known/openid-configuration`,
60
+ );
61
+ const oidc = await oidcRequest.json<{ jwks_uri: string }>();
62
+ JWKS = createRemoteJWKSet(new URL(oidc.jwks_uri));
63
+ } else {
64
+ JWKS = createLocalJWKSet(issuer);
65
+ }
66
+
67
+ const claims = (
68
+ await jwtVerify<J>(token ?? "", JWKS, {
69
+ algorithms: ["RS256", "ES256"],
70
+ audience: audiences,
71
+ })
72
+ ).payload;
73
+ if (!claims) {
74
+ throw new Error("Invalid");
75
+ }
76
+ return claims;
77
+ }