Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
TypeScript
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889import * as Data from "effect/Data";import * as Effect from "effect/Effect";import { decode, encode, opaque, type BridgeClaims } from "../shared/bridge.ts";
export class InvalidAssertion extends Data.TaggedError("InvalidAssertion") { override readonly message = "Invalid bridge assertion";}
export const hash = (value: string) => Effect.promise(() => crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)), ).pipe(Effect.map((bytes) => encode(new Uint8Array(bytes))));
function tokenPart(value: string) { return Effect.try({ try: (): Record<string, unknown> => { const decoded: unknown = JSON.parse( new TextDecoder().decode(decode(value)), ); if (!decoded || typeof decoded !== "object" || Array.isArray(decoded)) throw new InvalidAssertion(); return decoded as Record<string, unknown>; }, catch: () => new InvalidAssertion(), });}
export function verifyAssertion( token: string, key: { keyId: string; publicKey: string }, expected: Omit<BridgeClaims, "iat" | "exp" | "jti">,): Effect.Effect<BridgeClaims, InvalidAssertion> { return Effect.gen(function* () { if (token.length > 4096) return yield* Effect.fail(new InvalidAssertion()); const parts = token.split("."); if (parts.length !== 3) return yield* Effect.fail(new InvalidAssertion()); const header = yield* tokenPart(parts[0]); if ( Object.keys(header).sort().join(",") !== "alg,kid,typ" || header.alg !== "EdDSA" || header.typ !== "JWT" || header.kid !== key.keyId ) return yield* Effect.fail(new InvalidAssertion()); const publicKey = yield* Effect.tryPromise({ try: () => crypto.subtle.importKey( "raw", decode(key.publicKey), { name: "Ed25519" }, false, ["verify"], ), catch: () => new InvalidAssertion(), }); const verified = yield* Effect.tryPromise({ try: () => crypto.subtle.verify( "Ed25519", publicKey, decode(parts[2]), new TextEncoder().encode(`${parts[0]}.${parts[1]}`), ), catch: () => new InvalidAssertion(), }); if (!verified) return yield* Effect.fail(new InvalidAssertion()); const claims = yield* tokenPart(parts[1]); const now = Math.floor(Date.now() / 1000); const keys = [...Object.keys(expected), "iat", "exp", "jti"].sort(); if ( Object.keys(claims).sort().join(",") !== keys.join(",") || typeof claims.iat !== "number" || !Number.isSafeInteger(claims.iat) || typeof claims.exp !== "number" || !Number.isSafeInteger(claims.exp) || claims.iat > now || claims.exp <= now || claims.exp <= claims.iat || claims.exp - claims.iat > 60 || !opaque(claims.jti) ) return yield* Effect.fail(new InvalidAssertion()); for (const [name, value] of Object.entries(expected)) if (claims[name] !== value) return yield* Effect.fail(new InvalidAssertion()); return { ...expected, iat: claims.iat, exp: claims.exp, jti: claims.jti }; });}