The recipes.blue monorepo recipes.blue
recipes appview atproto

feat: working on implementing auth verification via proxy

+348 -281
+2
apps/api/package.json
··· 16 16 "@atcute/client": "^2.0.6", 17 17 "@atproto/api": "^0.13.19", 18 18 "@atproto/common": "^0.4.5", 19 + "@atproto/crypto": "^0.4.2", 19 20 "@atproto/jwk-jose": "^0.1.2", 20 21 "@atproto/oauth-client-node": "^0.2.3", 21 22 "@cookware/database": "workspace:^", ··· 29 30 "hono-sessions": "^0.7.0", 30 31 "jose": "^5.9.6", 31 32 "pino": "^9.5.0", 33 + "uint8arrays": "^5.1.0", 32 34 "ws": "^8.18.0", 33 35 "zod": "^3.23.8" 34 36 },
-34
apps/api/src/auth/client.ts
··· 1 - import { JoseKey } from "@atproto/jwk-jose"; 2 - import { NodeOAuthClient } from "@atproto/oauth-client-node"; 3 - import env from "../config/env.js"; 4 - import { SessionStore, StateStore } from "./storage.js"; 5 - import { Context } from "hono"; 6 - 7 - export const getClient = async (ctx: Context) => { 8 - let appUrl = 'https://recipes.blue'; 9 - if (env.ENV == 'development') { 10 - appUrl = `https://${ctx.req.header('Host')}`; 11 - } 12 - 13 - return new NodeOAuthClient({ 14 - clientMetadata: { 15 - client_id: `${appUrl}/oauth/client-metadata.json`, 16 - client_name: 'Cookware', 17 - client_uri: appUrl, 18 - redirect_uris: [`${appUrl}/oauth/callback`], 19 - response_types: ['code'], 20 - application_type: 'web', 21 - grant_types: ['authorization_code', 'refresh_token'], 22 - scope: 'atproto transition:generic', 23 - token_endpoint_auth_method: 'private_key_jwt', 24 - token_endpoint_auth_signing_alg: 'ES256', 25 - dpop_bound_access_tokens: true, 26 - jwks_uri: `${appUrl}/oauth/jwks.json`, 27 - }, 28 - keyset: await Promise.all([ 29 - JoseKey.fromImportable(env.JWKS_PRIVATE_KEY, 'cookware_jwks_1'), 30 - ]), 31 - sessionStore: new SessionStore(), 32 - stateStore: new StateStore(), 33 - }); 34 - };
-80
apps/api/src/auth/index.ts
··· 1 - import { Hono } from "hono"; 2 - import { getDidFromHandleOrDid } from "@cookware/lexicons"; 3 - import { getClient } from "./client.js"; 4 - import { z } from "zod"; 5 - import { Session } from "hono-sessions"; 6 - import { RecipesSession, getSessionAgent } from "../util/api.js"; 7 - 8 - export const authApp = new Hono<{ 9 - Variables: { 10 - session: Session<RecipesSession>, 11 - session_key_rotation: boolean, 12 - }, 13 - }>(); 14 - 15 - authApp.get('/client-metadata.json', async ctx => { 16 - const client = await getClient(ctx); 17 - return ctx.json(client.clientMetadata); 18 - }); 19 - 20 - authApp.get('/jwks.json', async ctx => { 21 - const client = await getClient(ctx); 22 - return ctx.json(client.jwks); 23 - }); 24 - 25 - const loginSchema = z.object({ 26 - actor: z.string(), 27 - }); 28 - 29 - authApp.get('/me', async ctx => { 30 - const agent = await getSessionAgent(ctx); 31 - if (!agent) { 32 - ctx.status(401); 33 - return ctx.json({ 34 - error: 'unauthenticated', 35 - message: 'You must be authenticated to access this resource.', 36 - }); 37 - } 38 - 39 - const profile = await agent.getProfile({ actor: agent.did! }); 40 - return ctx.json(profile.data); 41 - }); 42 - 43 - authApp.post('/login', async ctx => { 44 - const client = await getClient(ctx); 45 - const { actor } = loginSchema.parse(await ctx.req.raw.json()); 46 - 47 - const did = await getDidFromHandleOrDid(actor); 48 - if (!did) { 49 - ctx.status(400); 50 - return ctx.json({ 51 - error: 'DID_NOT_FOUND' as const, 52 - message: 'No account with that handle was found.', 53 - }); 54 - } 55 - 56 - const url = await client.authorize(did, { 57 - scope: 'atproto transition:generic', 58 - }); 59 - return ctx.json({ url }); 60 - }); 61 - 62 - authApp.get('/callback', async ctx => { 63 - const client = await getClient(ctx); 64 - const params = new URLSearchParams(ctx.req.url.split('?')[1]); 65 - 66 - const { session } = await client.callback(params); 67 - const currentSession = ctx.get('session') as Session<RecipesSession>; 68 - const did = currentSession.get('did'); 69 - if (did) { 70 - ctx.status(400); 71 - return ctx.json({ 72 - error: 'session_exists', 73 - message: 'Session already exists!', 74 - }); 75 - } 76 - 77 - currentSession.set('did', session.did); 78 - 79 - return ctx.redirect('/'); 80 - });
-55
apps/api/src/auth/storage.ts
··· 1 - import type { 2 - NodeSavedSession, 3 - NodeSavedSessionStore, 4 - NodeSavedState, 5 - NodeSavedStateStore, 6 - } from '@atproto/oauth-client-node' 7 - import { db, authSessionTable, authStateTable } from '@cookware/database' 8 - import { eq } from 'drizzle-orm'; 9 - 10 - export class StateStore implements NodeSavedStateStore { 11 - constructor() {} 12 - async get(key: string): Promise<NodeSavedState | undefined> { 13 - const result = await db.query.authStateTable.findFirst({ 14 - where: eq(authStateTable.key, key), 15 - }); 16 - if (!result) return 17 - return result.state 18 - } 19 - 20 - async set(key: string, state: NodeSavedState) { 21 - await db.insert(authStateTable).values({ 22 - key, state: state, 23 - }).onConflictDoUpdate({ 24 - target: authStateTable.key, 25 - set: { state }, 26 - }); 27 - } 28 - 29 - async del(key: string) { 30 - await db.delete(authStateTable).where(eq(authStateTable.key, key)); 31 - } 32 - } 33 - 34 - export class SessionStore implements NodeSavedSessionStore { 35 - async get(key: string): Promise<NodeSavedSession | undefined> { 36 - const result = await db.query.authSessionTable.findFirst({ 37 - where: eq(authSessionTable.key, key), 38 - }); 39 - if (!result) return 40 - return result.session 41 - } 42 - 43 - async set(key: string, session: NodeSavedSession) { 44 - await db.insert(authSessionTable) 45 - .values({ key, session }) 46 - .onConflictDoUpdate({ 47 - target: authSessionTable.key, 48 - set: { session }, 49 - }); 50 - } 51 - 52 - async del(key: string) { 53 - await db.delete(authSessionTable).where(eq(authSessionTable.key, key)); 54 - } 55 - }
+1 -58
apps/api/src/index.ts
··· 4 4 import env from "./config/env.js"; 5 5 import { xrpcApp } from "./xrpc/index.js"; 6 6 import { cors } from "hono/cors"; 7 - import { authApp } from "./auth/index.js"; 8 7 import { ZodError } from "zod"; 9 - import { CookieStore, Session, sessionMiddleware } from "hono-sessions"; 10 - import { RecipesSession } from "./util/api.js"; 11 8 import * as Sentry from "@sentry/node" 12 - import { readFileSync } from "fs"; 13 - import { getFilePathWithoutDefaultDocument } from "hono/utils/filepath"; 14 9 import { recipeApp } from "./recipes/index.js"; 15 10 16 11 if (env.SENTRY_DSN) { ··· 19 14 }); 20 15 } 21 16 22 - const app = new Hono<{ 23 - Variables: { 24 - session: Session<RecipesSession>, 25 - session_key_rotation: boolean, 26 - }, 27 - }>(); 28 - 29 - const store = new CookieStore({ 30 - sessionCookieName: 'recipes-session', 31 - }); 32 - 33 - app.use(async (c, next) => { 34 - if ( 35 - c.req.path == '/oauth/client-metadata.json' 36 - || c.req.path == '/oauth/jwks.json' 37 - ) return next(); 38 - 39 - const mw = sessionMiddleware({ 40 - store, 41 - encryptionKey: env.SESSION_KEY, // Required for CookieStore, recommended for others 42 - expireAfterSeconds: 900, // Expire session after 15 minutes of inactivity 43 - cookieOptions: { 44 - sameSite: 'strict', // Recommended for basic CSRF protection in modern browsers 45 - path: '/', // Required for this library to work properly 46 - httpOnly: true, // Recommended to avoid XSS attacks 47 - secure: true, 48 - }, 49 - }); 50 - return mw(c, next); 51 - }); 17 + const app = new Hono(); 52 18 53 19 app.use(cors({ 54 20 origin: (origin, _ctx) => { ··· 69 35 })); 70 36 71 37 app.route('/xrpc', xrpcApp); 72 - app.route('/oauth', authApp); 73 38 app.route('/api/recipes', recipeApp); 74 39 75 40 app.use(async (ctx, next) => { ··· 90 55 message: 'The server could not process the request.', 91 56 }); 92 57 } 93 - }); 94 - 95 - // TODO: Replace custom impl with this when issue is addressed: 96 - // https://github.com/honojs/hono/issues/3736 97 - // app.use('/*', serveStatic({ root: env.PUBLIC_DIR, rewriteRequestPath: () => 'index.html' })); 98 - 99 - app.use('/*', async (ctx, next) => { 100 - if (ctx.finalized) return next(); 101 - 102 - let path = getFilePathWithoutDefaultDocument({ 103 - filename: 'index.html', 104 - root: env.PUBLIC_DIR, 105 - }) 106 - 107 - if (path) { 108 - path = `./${path}`; 109 - } else { 110 - return next(); 111 - } 112 - 113 - const index = readFileSync(path).toString(); 114 - return ctx.html(index); 115 58 }); 116 59 117 60 serve({
+45 -27
apps/api/src/recipes/index.ts
··· 1 1 import { Hono } from "hono"; 2 - import { getSessionAgent } from "../util/api.js"; 3 - import { RecipeCollection, RecipeRecord } from "@cookware/lexicons"; 2 + import { getDidDoc, getPdsUrl, RecipeCollection, RecipeRecord } from "@cookware/lexicons"; 4 3 import { TID } from "@atproto/common"; 4 + import { verifyJwt } from "../util/jwt.js"; 5 + import { XRPCError } from "../util/xrpc.js"; 5 6 6 7 export const recipeApp = new Hono(); 7 8 8 9 recipeApp.post('/', async ctx => { 9 - const agent = await getSessionAgent(ctx); 10 - if (!agent) { 11 - ctx.status(401); 12 - return ctx.json({ 13 - error: 'unauthenticated', 14 - message: 'You must be authenticated to access this resource.', 15 - }); 10 + const authz = ctx.req.header('Authorization'); 11 + if (!authz || !authz.startsWith('Bearer ')) { 12 + throw new XRPCError('this endpoint requires authentication', 'authz_required', 401); 16 13 } 17 14 18 - const body = await ctx.req.json(); 19 - const { data: record, success, error } = RecipeRecord.safeParse(body); 20 - if (!success) { 21 - ctx.status(400); 22 - return ctx.json({ 23 - error: 'invalid_recipe', 24 - message: error.message, 25 - fields: error.formErrors, 26 - }); 15 + try { 16 + const serviceJwt = await verifyJwt( 17 + authz.split(' ')[1]!, 18 + 'did:web:recipes.blue#api', 19 + null, 20 + async (iss, forceRefresh) => { 21 + console.log(iss); 22 + return ''; 23 + }, 24 + ); 27 25 } 28 26 29 - const res = await agent.com.atproto.repo.putRecord({ 30 - repo: agent.assertDid, 31 - collection: RecipeCollection, 32 - record: record, 33 - rkey: TID.nextStr(), 34 - validate: false, 35 - }); 36 - 37 - return ctx.json(res.data); 27 + //const agent = await getSessionAgent(ctx); 28 + //if (!agent) { 29 + // ctx.status(401); 30 + // return ctx.json({ 31 + // error: 'unauthenticated', 32 + // message: 'You must be authenticated to access this resource.', 33 + // }); 34 + //} 35 + // 36 + //const body = await ctx.req.json(); 37 + //const { data: record, success, error } = RecipeRecord.safeParse(body); 38 + //if (!success) { 39 + // ctx.status(400); 40 + // return ctx.json({ 41 + // error: 'invalid_recipe', 42 + // message: error.message, 43 + // fields: error.formErrors, 44 + // }); 45 + //} 46 + // 47 + //const res = await agent.com.atproto.repo.putRecord({ 48 + // repo: agent.assertDid, 49 + // collection: RecipeCollection, 50 + // record: record, 51 + // rkey: TID.nextStr(), 52 + // validate: false, 53 + //}); 54 + // 55 + //return ctx.json(res.data); 38 56 });
-23
apps/api/src/util/api.ts
··· 1 - import { Context } from "hono"; 2 - import { Session } from "hono-sessions"; 3 - import { getClient } from "../auth/client.js"; 4 - import { Agent } from "@atproto/api"; 5 - import { authLogger } from "../logger.js"; 6 - 7 - export type RecipesSession = { did: string; }; 8 - 9 - export const getSessionAgent = async (ctx: Context) => { 10 - const client = await getClient(ctx); 11 - const session = ctx.get('session') as Session<RecipesSession>; 12 - const did = session.get('did'); 13 - if (!did) return null; 14 - 15 - try { 16 - const oauthSession = await client.restore(did); 17 - return oauthSession ? new Agent(oauthSession) : null; 18 - } catch (err) { 19 - authLogger.warn({ err, did }, 'oauth restore failed'); 20 - session.deleteSession(); 21 - return null; 22 - } 23 - };
+229
apps/api/src/util/jwt.ts
··· 1 + import * as common from "@atproto/common"; 2 + import { MINUTE } from "@atproto/common"; 3 + import * as crypto from "@atproto/crypto"; 4 + import * as ui8 from "uint8arrays"; 5 + import { XRPCError } from "./xrpc.js"; 6 + import { z } from "zod"; 7 + 8 + export const jwt = z.custom<`${string}.${string}.${string}`>((input) => { 9 + if (typeof input !== "string") return; 10 + if (input.split(".").length !== 3) return false; 11 + return true; 12 + }); 13 + 14 + type ServiceJwtParams = { 15 + iss: string; 16 + aud: string; 17 + iat?: number; 18 + exp?: number; 19 + lxm: string | null; 20 + keypair: crypto.Keypair; 21 + }; 22 + 23 + type ServiceJwtHeaders = { 24 + alg: string; 25 + } & Record<string, unknown>; 26 + 27 + type ServiceJwtPayload = { 28 + iss: string; 29 + aud: string; 30 + exp: number; 31 + lxm?: string; 32 + jti?: string; 33 + }; 34 + 35 + export const createServiceJwt = async ( 36 + params: ServiceJwtParams, 37 + ): Promise<string> => { 38 + const { iss, aud, keypair } = params; 39 + const iat = params.iat ?? Math.floor(Date.now() / 1e3); 40 + const exp = params.exp ?? iat + MINUTE / 1e3; 41 + const lxm = params.lxm ?? undefined; 42 + const jti = crypto.randomStr(16, "hex"); 43 + const header = { 44 + typ: "JWT", 45 + alg: keypair.jwtAlg, 46 + }; 47 + const payload = common.noUndefinedVals({ 48 + iat, 49 + iss, 50 + aud, 51 + exp, 52 + lxm, 53 + jti, 54 + }); 55 + const toSignStr = `${jsonToB64Url(header)}.${jsonToB64Url(payload)}`; 56 + const toSign = ui8.fromString(toSignStr, "utf8"); 57 + const sig = await keypair.sign(toSign); 58 + return `${toSignStr}.${ui8.toString(sig, "base64url")}`; 59 + }; 60 + 61 + export const createServiceAuthHeaders = async (params: ServiceJwtParams) => { 62 + const jwt = await createServiceJwt(params); 63 + return { 64 + headers: { authorization: `Bearer ${jwt}` }, 65 + }; 66 + }; 67 + 68 + const jsonToB64Url = (json: Record<string, unknown>): string => { 69 + return common.utf8ToB64Url(JSON.stringify(json)); 70 + }; 71 + 72 + export type VerifySignatureWithKeyFn = ( 73 + key: string, 74 + msgBytes: Uint8Array, 75 + sigBytes: Uint8Array, 76 + alg: string, 77 + ) => Promise<boolean>; 78 + 79 + export const verifyJwt = async ( 80 + jwtStr: string, 81 + ownDid: string | null, // null indicates to skip the audience check 82 + lxm: string | null, // null indicates to skip the lxm check 83 + getSigningKey: (iss: string, forceRefresh: boolean) => Promise<string>, 84 + verifySignatureWithKey: VerifySignatureWithKeyFn = cryptoVerifySignatureWithKey, 85 + ): Promise<ServiceJwtPayload> => { 86 + const jwtParsed = jwt.safeParse(jwtStr); 87 + if (!jwtParsed.success) { 88 + throw new XRPCError("poorly formatted jwt", "BadJwt", 401); 89 + } 90 + const parts = jwtParsed.data.split("."); 91 + 92 + const header = parseHeader(parts[0]!); 93 + 94 + // The spec does not describe what to do with the "typ" claim. We can, 95 + // however, forbid some values that are not compatible with our use case. 96 + if ( 97 + // service tokens are not OAuth 2.0 access tokens 98 + // https://datatracker.ietf.org/doc/html/rfc9068 99 + header["typ"] === "at+jwt" || 100 + // "refresh+jwt" is a non-standard type used by the @atproto packages 101 + header["typ"] === "refresh+jwt" || 102 + // "DPoP" proofs are not meant to be used as service tokens 103 + // https://datatracker.ietf.org/doc/html/rfc9449 104 + header["typ"] === "dpop+jwt" 105 + ) { 106 + throw new XRPCError( 107 + `Invalid jwt type "${header["typ"]}"`, 108 + "BadJwtType", 109 + 401, 110 + ); 111 + } 112 + 113 + const payload = parsePayload(parts[1]!); 114 + const sig = parts[2]!; 115 + 116 + if (Date.now() / 1000 > payload.exp) { 117 + throw new XRPCError("jwt expired", "JwtExpired", 401); 118 + } 119 + if (ownDid !== null && payload.aud !== ownDid) { 120 + throw new XRPCError( 121 + "jwt audience does not match service did", 122 + "BadJwtAudience", 123 + 401, 124 + ); 125 + } 126 + if (lxm !== null && payload.lxm !== lxm) { 127 + throw new XRPCError( 128 + payload.lxm !== undefined 129 + ? `bad jwt lexicon method ("lxm"). must match: ${lxm}` 130 + : `missing jwt lexicon method ("lxm"). must match: ${lxm}`, 131 + "BadJwtLexiconMethod", 132 + 401, 133 + ); 134 + } 135 + 136 + const msgBytes = ui8.fromString(parts.slice(0, 2).join("."), "utf8"); 137 + const sigBytes = ui8.fromString(sig, "base64url"); 138 + 139 + const signingKey = await getSigningKey(payload.iss, false); 140 + const { alg } = header; 141 + 142 + let validSig: boolean; 143 + try { 144 + validSig = await verifySignatureWithKey( 145 + signingKey, 146 + msgBytes, 147 + sigBytes, 148 + alg, 149 + ); 150 + } catch (err) { 151 + throw new XRPCError( 152 + "could not verify jwt signature", 153 + "BadJwtSignature", 154 + 401, 155 + ); 156 + } 157 + 158 + if (!validSig) { 159 + // get fresh signing key in case it failed due to a recent rotation 160 + const freshSigningKey = await getSigningKey(payload.iss, true); 161 + try { 162 + validSig = 163 + freshSigningKey !== signingKey 164 + ? await verifySignatureWithKey( 165 + freshSigningKey, 166 + msgBytes, 167 + sigBytes, 168 + alg, 169 + ) 170 + : false; 171 + } catch (err) { 172 + throw new XRPCError( 173 + "could not verify jwt signature", 174 + "BadJwtSignature", 175 + 401, 176 + ); 177 + } 178 + } 179 + 180 + if (!validSig) { 181 + throw new XRPCError( 182 + "jwt signature does not match jwt issuer", 183 + "BadJwtSignature", 184 + 401, 185 + ); 186 + } 187 + 188 + return payload; 189 + }; 190 + 191 + export const cryptoVerifySignatureWithKey: VerifySignatureWithKeyFn = async ( 192 + key: string, 193 + msgBytes: Uint8Array, 194 + sigBytes: Uint8Array, 195 + alg: string, 196 + ) => { 197 + return crypto.verifySignature(key, msgBytes, sigBytes, { 198 + jwtAlg: alg, 199 + allowMalleableSig: true, 200 + }); 201 + }; 202 + 203 + const parseB64UrlToJson = (b64: string) => { 204 + return JSON.parse(common.b64UrlToUtf8(b64)); 205 + }; 206 + 207 + const parseHeader = (b64: string): ServiceJwtHeaders => { 208 + const header = parseB64UrlToJson(b64); 209 + if (!header || typeof header !== "object" || typeof header.alg !== "string") { 210 + throw new XRPCError("poorly formatted jwt", "BadJwt", 401); 211 + } 212 + return header; 213 + }; 214 + 215 + const parsePayload = (b64: string): ServiceJwtPayload => { 216 + const payload = parseB64UrlToJson(b64); 217 + if ( 218 + !payload || 219 + typeof payload !== "object" || 220 + typeof payload.iss !== "string" || 221 + typeof payload.aud !== "string" || 222 + typeof payload.exp !== "number" || 223 + (payload.lxm && typeof payload.lxm !== "string") || 224 + (payload.nonce && typeof payload.nonce !== "string") 225 + ) { 226 + throw new XRPCError("poorly formatted jwt", "BadJwt", 401); 227 + } 228 + return payload; 229 + };
+20
apps/api/src/util/xrpc.ts
··· 1 + import { Context } from "hono"; 2 + import { StatusCode } from "hono/utils/http-status"; 3 + 4 + export class XRPCError extends Error { 5 + constructor( 6 + message: string, 7 + public error: string, 8 + public code: StatusCode, 9 + ) { 10 + super(message); 11 + } 12 + 13 + hono(ctx: Context) { 14 + ctx.status(this.code); 15 + return ctx.json({ 16 + error: this.error, 17 + message: this.message, 18 + }); 19 + } 20 + }
+51 -4
pnpm-lock.yaml
··· 23 23 '@atproto/common': 24 24 specifier: ^0.4.5 25 25 version: 0.4.5 26 + '@atproto/crypto': 27 + specifier: ^0.4.2 28 + version: 0.4.2 26 29 '@atproto/jwk-jose': 27 30 specifier: ^0.1.2 28 31 version: 0.1.2 ··· 49 52 version: 4.0.8 50 53 drizzle-orm: 51 54 specifier: ^0.37.0 52 - version: 0.37.0(@libsql/client@0.14.0(bufferutil@4.0.8))(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0) 55 + version: 0.37.0(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0) 53 56 hono: 54 57 specifier: ^4.6.12 55 58 version: 4.6.12 ··· 62 65 pino: 63 66 specifier: ^9.5.0 64 67 version: 9.5.0 68 + uint8arrays: 69 + specifier: ^5.1.0 70 + version: 5.1.0 65 71 ws: 66 72 specifier: ^8.18.0 67 73 version: 8.18.0(bufferutil@4.0.8) ··· 125 131 version: 4.0.8 126 132 drizzle-orm: 127 133 specifier: ^0.37.0 128 - version: 0.37.0(@libsql/client@0.14.0(bufferutil@4.0.8))(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0) 134 + version: 0.37.0(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0) 129 135 pino: 130 136 specifier: ^9.5.0 131 137 version: 9.5.0 ··· 328 334 version: 0.14.0(bufferutil@4.0.8) 329 335 drizzle-orm: 330 336 specifier: ^0.37.0 331 - version: 0.37.0(@libsql/client@0.14.0(bufferutil@4.0.8))(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0) 337 + version: 0.37.0(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0) 332 338 zod: 333 339 specifier: ^3.23.8 334 340 version: 3.23.8 ··· 432 438 433 439 '@atproto/common@0.4.5': 434 440 resolution: {integrity: sha512-LFAGqHcxCI5+b31Xgk+VQQtZU258iGPpHJzNeHVcdh6teIKZi4C2l6YV+m+3CEz+yYcfP7jjUmgqesx7l9Arsg==} 441 + 442 + '@atproto/crypto@0.4.2': 443 + resolution: {integrity: sha512-aeOfPQYCDbhn2hV06oBF2KXrWjf/BK4yL8lfANJKSmKl3tKWCkiW/moi643rUXXxSE72KtWtQeqvNFYnnFJ0ig==} 435 444 436 445 '@atproto/did@0.1.3': 437 446 resolution: {integrity: sha512-ULD8Gw/KRRwLFZ2Z2L4DjmdOMrg8IYYlcjdSc+GQ2/QJSVnD2zaJJVTLd3vls121wGt/583rNaiZTT2DpBze4w==} ··· 1343 1352 1344 1353 '@neon-rs/load@0.0.4': 1345 1354 resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} 1355 + 1356 + '@noble/curves@1.7.0': 1357 + resolution: {integrity: sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==} 1358 + engines: {node: ^14.21.3 || >=16} 1359 + 1360 + '@noble/hashes@1.6.0': 1361 + resolution: {integrity: sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==} 1362 + engines: {node: ^14.21.3 || >=16} 1363 + 1364 + '@noble/hashes@1.6.1': 1365 + resolution: {integrity: sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w==} 1366 + engines: {node: ^14.21.3 || >=16} 1346 1367 1347 1368 '@nodelib/fs.scandir@2.1.5': 1348 1369 resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} ··· 3347 3368 ms@2.1.3: 3348 3369 resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 3349 3370 3371 + multiformats@13.3.1: 3372 + resolution: {integrity: sha512-QxowxTNwJ3r5RMctoGA5p13w5RbRT2QDkoM+yFlqfLiioBp78nhDjnRLvmSBI9+KAqN4VdgOVWM9c0CHd86m3g==} 3373 + 3350 3374 multiformats@9.9.0: 3351 3375 resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==} 3352 3376 ··· 4158 4182 uint8arrays@3.0.0: 4159 4183 resolution: {integrity: sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==} 4160 4184 4185 + uint8arrays@5.1.0: 4186 + resolution: {integrity: sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==} 4187 + 4161 4188 undici-types@6.20.0: 4162 4189 resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} 4163 4190 ··· 4422 4449 multiformats: 9.9.0 4423 4450 pino: 8.21.0 4424 4451 4452 + '@atproto/crypto@0.4.2': 4453 + dependencies: 4454 + '@noble/curves': 1.7.0 4455 + '@noble/hashes': 1.6.1 4456 + uint8arrays: 3.0.0 4457 + 4425 4458 '@atproto/did@0.1.3': 4426 4459 dependencies: 4427 4460 zod: 3.23.8 ··· 5120 5153 5121 5154 '@neon-rs/load@0.0.4': {} 5122 5155 5156 + '@noble/curves@1.7.0': 5157 + dependencies: 5158 + '@noble/hashes': 1.6.0 5159 + 5160 + '@noble/hashes@1.6.0': {} 5161 + 5162 + '@noble/hashes@1.6.1': {} 5163 + 5123 5164 '@nodelib/fs.scandir@2.1.5': 5124 5165 dependencies: 5125 5166 '@nodelib/fs.stat': 2.0.5 ··· 6624 6665 transitivePeerDependencies: 6625 6666 - supports-color 6626 6667 6627 - drizzle-orm@0.37.0(@libsql/client@0.14.0(bufferutil@4.0.8))(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0): 6668 + drizzle-orm@0.37.0(@libsql/client@0.14.0)(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@types/react@19.0.1)(react@19.0.0): 6628 6669 optionalDependencies: 6629 6670 '@libsql/client': 0.14.0(bufferutil@4.0.8) 6630 6671 '@opentelemetry/api': 1.9.0 ··· 7153 7194 module-details-from-path@1.0.3: {} 7154 7195 7155 7196 ms@2.1.3: {} 7197 + 7198 + multiformats@13.3.1: {} 7156 7199 7157 7200 multiformats@9.9.0: {} 7158 7201 ··· 7981 8024 uint8arrays@3.0.0: 7982 8025 dependencies: 7983 8026 multiformats: 9.9.0 8027 + 8028 + uint8arrays@5.1.0: 8029 + dependencies: 8030 + multiformats: 13.3.1 7984 8031 7985 8032 undici-types@6.20.0: {} 7986 8033