From afac81cefdca9189186b4cb298847bd98952bb34 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Thu, 5 Nov 2026 22:38:06 +0900 Subject: [PATCH 1/6] Accept node:crypto keys in public APIs Fedify's public declarations referred to the ambient `CryptoKey` and `CryptoKeyPair` types, so the consumer's `lib` setting decided which declaration those names resolved to. Since `@types/node` 24.7.0, Node.js's `webcrypto.KeyUsage` includes the ML-KEM usages, which makes its usage array incompatible with the DOM and Web Worker one. Applications could not pass keys obtained from `node:crypto` to Fedify. Input positions now accept the structural `CryptoKeyLike` and `CryptoKeyPairLike` types added to `@fedify/vocab-runtime`, while the keys Fedify returns keep the ambient types. `ActorKeyPairsDispatcher` takes an optional key pair type parameter that defaults to `CryptoKeyPair`, so code calling a dispatcher directly keeps its narrow return type. The vocabulary code generator uses the wider type for constructor and `clone()` inputs, and the getters stay narrow. The repository's own type check cannot reproduce the failure because it uses the Web Worker lib and never passes `node:crypto` keys into the API, so a type-level regression test mimics the wider Node.js usage union. https://github.com/fedify-dev/fedify/issues/554 Assisted-by: OpenCode:deepseek-flash Assisted-by: Claude Code:claude-fable-5-1 --- changes.d/fedify/node-webcrypto-keys.md | 13 +++ .../vocab-runtime/node-webcrypto-keys.md | 13 +++ changes.d/vocab-tools/node-webcrypto-keys.md | 8 ++ changes.d/vocab/node-webcrypto-keys.md | 10 ++ packages/fedify/src/federation/builder.ts | 5 +- packages/fedify/src/federation/callback.ts | 14 ++- packages/fedify/src/federation/context.ts | 4 +- packages/fedify/src/federation/federation.ts | 3 +- packages/fedify/src/federation/middleware.ts | 12 ++- packages/fedify/src/federation/send.ts | 8 +- packages/fedify/src/sig/http.ts | 24 +++-- packages/fedify/src/sig/key-types.test.ts | 97 +++++++++++++++++++ packages/fedify/src/sig/key.ts | 8 +- packages/fedify/src/sig/ld.ts | 8 +- packages/fedify/src/sig/mod.ts | 1 + packages/fedify/src/sig/proof.ts | 15 ++- packages/fedify/src/utils/docloader.ts | 5 +- packages/vocab-runtime/src/crypto-key.ts | 82 ++++++++++++++++ packages/vocab-runtime/src/docloader.ts | 3 +- packages/vocab-runtime/src/internal.ts | 1 + packages/vocab-runtime/src/jwk.ts | 8 +- packages/vocab-runtime/src/key.ts | 11 ++- packages/vocab-runtime/src/mod.ts | 1 + .../src/__snapshots__/class.test.ts.deno.snap | 9 +- .../src/__snapshots__/class.test.ts.node.snap | 9 +- .../src/__snapshots__/class.test.ts.snap | 9 +- packages/vocab-tools/src/class.ts | 1 + packages/vocab-tools/src/constructor.ts | 14 ++- packages/vocab-tools/src/type.ts | 22 ++++- 29 files changed, 356 insertions(+), 62 deletions(-) create mode 100644 changes.d/fedify/node-webcrypto-keys.md create mode 100644 changes.d/vocab-runtime/node-webcrypto-keys.md create mode 100644 changes.d/vocab-tools/node-webcrypto-keys.md create mode 100644 changes.d/vocab/node-webcrypto-keys.md create mode 100644 packages/fedify/src/sig/key-types.test.ts create mode 100644 packages/vocab-runtime/src/crypto-key.ts diff --git a/changes.d/fedify/node-webcrypto-keys.md b/changes.d/fedify/node-webcrypto-keys.md new file mode 100644 index 000000000..38afb577b --- /dev/null +++ b/changes.d/fedify/node-webcrypto-keys.md @@ -0,0 +1,13 @@ +--- +links: + '#1391': https://github.com/fedify-dev/fedify/pull/1391 + '#554': https://github.com/fedify-dev/fedify/issues/554 +--- + - Fixed `CryptoKey` and `CryptoKeyPair` values obtained from `node:crypto` + being rejected by `ActorKeyPairsDispatcher`, `SharedInboxKeyDispatcher`, + `SenderKeyPair`, `signRequest()`, `createProof()`, `signObject()`, + `createSignature()`, `signJsonLd()`, `exportJwk()`, and the authenticated + document loader APIs when `@types/node` 24.7.0 or later is installed. + Those APIs now accept a structural key type that both Node.js and + DOM/Web Worker keys satisfy, while the keys returned by Fedify keep the + ambient `CryptoKey` types. [[#554], [#1391]] diff --git a/changes.d/vocab-runtime/node-webcrypto-keys.md b/changes.d/vocab-runtime/node-webcrypto-keys.md new file mode 100644 index 000000000..e734ff646 --- /dev/null +++ b/changes.d/vocab-runtime/node-webcrypto-keys.md @@ -0,0 +1,13 @@ +--- +links: + '#1391': https://github.com/fedify-dev/fedify/pull/1391 + '#554': https://github.com/fedify-dev/fedify/issues/554 +--- + - Added the `CryptoKeyLike` and `CryptoKeyPairLike` types for the structural + key shape that Fedify APIs accept. They cover keys from Node.js, + DOM/Web Workers, and Cloudflare Workers, so `exportSpki()`, + `exportMultibaseKey()`, and `AuthenticatedDocumentLoaderFactory` no longer + reject keys obtained from `node:crypto` with `@types/node` 24.7.0 or later. + An implementation of `AuthenticatedDocumentLoaderFactory` that annotates + the `identity` parameter with the ambient `CryptoKey` type now has to + annotate it with `CryptoKeyLike` instead. [[#554], [#1391]] diff --git a/changes.d/vocab-tools/node-webcrypto-keys.md b/changes.d/vocab-tools/node-webcrypto-keys.md new file mode 100644 index 000000000..bc694a24c --- /dev/null +++ b/changes.d/vocab-tools/node-webcrypto-keys.md @@ -0,0 +1,8 @@ +--- +links: + '#1391': https://github.com/fedify-dev/fedify/pull/1391 + '#554': https://github.com/fedify-dev/fedify/issues/554 +--- + - Changed the code generated by `@fedify/vocab-tools` to import the + `CryptoKeyLike` type from `@fedify/vocab-runtime`. Regenerated code now + requires `@fedify/vocab-runtime` 2.0.36 or later. [[#554], [#1391]] diff --git a/changes.d/vocab/node-webcrypto-keys.md b/changes.d/vocab/node-webcrypto-keys.md new file mode 100644 index 000000000..56b603704 --- /dev/null +++ b/changes.d/vocab/node-webcrypto-keys.md @@ -0,0 +1,10 @@ +--- +links: + '#1391': https://github.com/fedify-dev/fedify/pull/1391 + '#554': https://github.com/fedify-dev/fedify/issues/554 +--- + - Fixed `CryptographicKey` and `Multikey` rejecting a `publicKey` obtained + from `node:crypto` in their constructors and `clone()` when `@types/node` + 24.7.0 or later is installed. The property accepts a structural key type + now, while the getters still return the ambient `CryptoKey` type. + [[#554], [#1391]] diff --git a/packages/fedify/src/federation/builder.ts b/packages/fedify/src/federation/builder.ts index f235da4b5..38a4c8970 100644 --- a/packages/fedify/src/federation/builder.ts +++ b/packages/fedify/src/federation/builder.ts @@ -7,6 +7,7 @@ import type { Recipient, } from "@fedify/vocab"; import { getTypeId } from "@fedify/vocab"; +import type { CryptoKeyPairLike } from "@fedify/vocab-runtime"; import { getLogger } from "@logtape/logtape"; import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api"; import type { Tracer } from "@opentelemetry/api"; @@ -445,7 +446,7 @@ export class FederationBuilderImpl this.actorCallbacks = callbacks; const setters: ActorCallbackSetters = { setKeyPairsDispatcher: ( - dispatcher: ActorKeyPairsDispatcher, + dispatcher: ActorKeyPairsDispatcher, ) => { callbacks.keyPairsDispatcher = (ctx, identifier) => this._getTracer().startActiveSpan( @@ -1535,7 +1536,7 @@ export function createFederationBuilder(): FederationBuilder< interface ActorCallbacks { dispatcher?: ActorDispatcher; - keyPairsDispatcher?: ActorKeyPairsDispatcher; + keyPairsDispatcher?: ActorKeyPairsDispatcher; handleMapper?: ActorHandleMapper; aliasMapper?: ActorAliasMapper; authorizePredicate?: AuthorizePredicate; diff --git a/packages/fedify/src/federation/callback.ts b/packages/fedify/src/federation/callback.ts index 94bc12f75..675976d9e 100644 --- a/packages/fedify/src/federation/callback.ts +++ b/packages/fedify/src/federation/callback.ts @@ -1,4 +1,5 @@ import type { Activity, Actor, Object } from "@fedify/vocab"; +import type { CryptoKeyPairLike } from "@fedify/vocab-runtime"; import type { Link } from "@fedify/webfinger"; import type { NodeInfo } from "../nodeinfo/types.ts"; import type { PageItems } from "./collection.ts"; @@ -41,16 +42,25 @@ export type ActorDispatcher = ( /** * A callback that dispatches key pairs for an actor. * + * The returned key pairs may come from any supported runtime, including keys + * obtained from `node:crypto`'s Web Crypto API, so the key pair type is + * parameterized. It defaults to the ambient `CryptoKeyPair` type for + * compatibility with code that calls a dispatcher directly. + * * @template TContextData The context data to pass to the {@link Context}. + * @template TKeyPair The type of the key pairs the dispatcher returns. * @param context The context. * @param identifier The actor's internal identifier or username. * @returns The key pairs. * @since 0.10.0 */ -export type ActorKeyPairsDispatcher = ( +export type ActorKeyPairsDispatcher< + TContextData, + TKeyPair extends CryptoKeyPairLike = CryptoKeyPair, +> = ( context: Context, identifier: string, -) => CryptoKeyPair[] | Promise; +) => TKeyPair[] | Promise; /** * A callback that maps a WebFinger username to the corresponding actor's diff --git a/packages/fedify/src/federation/context.ts b/packages/fedify/src/federation/context.ts index 0c006d0ce..3bae40278 100644 --- a/packages/fedify/src/federation/context.ts +++ b/packages/fedify/src/federation/context.ts @@ -10,7 +10,7 @@ import type { Recipient, TraverseCollectionOptions, } from "@fedify/vocab"; -import type { DocumentLoader } from "@fedify/vocab-runtime"; +import type { CryptoKeyLike, DocumentLoader } from "@fedify/vocab-runtime"; import type { LookupWebFingerOptions, ResourceDescriptor, @@ -239,7 +239,7 @@ export interface Context { * @since 0.4.0 */ getDocumentLoader( - identity: { keyId: URL; privateKey: CryptoKey }, + identity: { keyId: URL; privateKey: CryptoKeyLike }, ): DocumentLoader; /** diff --git a/packages/fedify/src/federation/federation.ts b/packages/fedify/src/federation/federation.ts index 7b7f523c5..8aceafed2 100644 --- a/packages/fedify/src/federation/federation.ts +++ b/packages/fedify/src/federation/federation.ts @@ -7,6 +7,7 @@ import type { } from "@fedify/vocab"; import type { AuthenticatedDocumentLoaderFactory, + CryptoKeyPairLike, DocumentLoaderFactory, GetUserAgentOptions, } from "@fedify/vocab-runtime"; @@ -1001,7 +1002,7 @@ export interface ActorCallbackSetters { * @since 0.10.0 */ setKeyPairsDispatcher( - dispatcher: ActorKeyPairsDispatcher, + dispatcher: ActorKeyPairsDispatcher, ): ActorCallbackSetters; /** diff --git a/packages/fedify/src/federation/middleware.ts b/packages/fedify/src/federation/middleware.ts index f78da81ba..6ea689ca8 100644 --- a/packages/fedify/src/federation/middleware.ts +++ b/packages/fedify/src/federation/middleware.ts @@ -16,6 +16,8 @@ import { Multikey, traverseCollection, } from "@fedify/vocab"; +import type { CryptoKeyLike, CryptoKeyPairLike } from "@fedify/vocab-runtime"; +import { asCryptoKey } from "@fedify/vocab-runtime/internal"; import type { AuthenticatedDocumentLoaderFactory, DocumentLoader, @@ -1390,7 +1392,7 @@ export class FederationImpl ); const activityId = activity.id.href; let proofCreated = false; - let rsaKey: { keyId: URL; privateKey: CryptoKey } | null = null; + let rsaKey: { keyId: URL; privateKey: CryptoKeyLike } | null = null; for (const { keyId, privateKey } of keys) { validateCryptoKey(privateKey, "private"); if (rsaKey == null && privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") { @@ -2309,7 +2311,7 @@ export class ContextImpl implements Context { }, ); } - let keyPairs: (CryptoKeyPair & { keyId: URL })[]; + let keyPairs: (CryptoKeyPairLike & { keyId: URL })[]; try { keyPairs = await this.getKeyPairsFromIdentifier(identifier); } catch (_) { @@ -2322,6 +2324,8 @@ export class ContextImpl implements Context { for (const keyPair of keyPairs) { const newPair: ActorKeyPair = { ...keyPair, + privateKey: asCryptoKey(keyPair.privateKey), + publicKey: asCryptoKey(keyPair.publicKey), cryptographicKey: new CryptographicKey({ id: keyPair.keyId, owner, @@ -2341,7 +2345,7 @@ export class ContextImpl implements Context { protected async getKeyPairsFromIdentifier( identifier: string, - ): Promise<(CryptoKeyPair & { keyId: URL })[]> { + ): Promise<(CryptoKeyPairLike & { keyId: URL })[]> { const logger = getLogger(["fedify", "federation", "actor"]); if (this.federation.actorCallbacks?.keyPairsDispatcher == null) { throw new Error("No actor key pairs dispatcher registered."); @@ -2384,7 +2388,7 @@ export class ContextImpl implements Context { protected async getRsaKeyPairFromIdentifier( identifier: string, - ): Promise { + ): Promise { const keyPairs = await this.getKeyPairsFromIdentifier(identifier); for (const keyPair of keyPairs) { const { privateKey } = keyPair; diff --git a/packages/fedify/src/federation/send.ts b/packages/fedify/src/federation/send.ts index ce7d51a96..38d60f973 100644 --- a/packages/fedify/src/federation/send.ts +++ b/packages/fedify/src/federation/send.ts @@ -1,5 +1,9 @@ import type { Recipient } from "@fedify/vocab"; -import { FetchError, UrlError } from "@fedify/vocab-runtime"; +import { + type CryptoKeyLike, + FetchError, + UrlError, +} from "@fedify/vocab-runtime"; import { openDirectFetch, openPublicFetch, @@ -90,7 +94,7 @@ export interface SenderKeyPair { /** * The actor's private key to sign the request. */ - readonly privateKey: CryptoKey; + readonly privateKey: CryptoKeyLike; /** * The public key ID that corresponds to the private key. diff --git a/packages/fedify/src/sig/http.ts b/packages/fedify/src/sig/http.ts index 58aa0ad0d..7de3cca5d 100644 --- a/packages/fedify/src/sig/http.ts +++ b/packages/fedify/src/sig/http.ts @@ -1,6 +1,13 @@ import { CryptographicKey } from "@fedify/vocab"; -import { type DocumentLoader, FetchError } from "@fedify/vocab-runtime"; -import type { PublicFetchSession } from "@fedify/vocab-runtime/internal"; +import { + type CryptoKeyLike, + type DocumentLoader, + FetchError, +} from "@fedify/vocab-runtime"; +import { + asCryptoKey, + type PublicFetchSession, +} from "@fedify/vocab-runtime/internal"; import { getLogger } from "@logtape/logtape"; import { type Span, @@ -86,11 +93,12 @@ export interface SignRequestOptions { */ export async function signRequest( request: Request, - privateKey: CryptoKey, + privateKey: CryptoKeyLike, keyId: URL, options: SignRequestOptions = {}, ): Promise { validateCryptoKey(privateKey, "private"); + const signingKey = asCryptoKey(privateKey); const tracerProvider = options.tracerProvider ?? trace.getTracerProvider(); const tracer = tracerProvider.getTracer( metadata.name, @@ -108,7 +116,7 @@ export async function signRequest( // Pass through test options if provided signed = await signRequestRfc9421( request, - privateKey, + signingKey, keyId, span, options.currentTime, @@ -118,7 +126,7 @@ export async function signRequest( // Default to draft-cavage signed = await signRequestDraft( request, - privateKey, + signingKey, keyId, span, options.currentTime, @@ -1478,7 +1486,7 @@ interface DoubleKnockHop { */ export async function doubleKnock( request: Request, - identity: { keyId: URL; privateKey: CryptoKey }, + identity: { keyId: URL; privateKey: CryptoKeyLike }, options: DoubleKnockOptions = {}, ): Promise { const { response } = await doubleKnockCore( @@ -1506,7 +1514,7 @@ export async function doubleKnock( */ export async function doubleKnockScoped( request: Request, - identity: { keyId: URL; privateKey: CryptoKey }, + identity: { keyId: URL; privateKey: CryptoKeyLike }, options: DoubleKnockOptions, openHop: DoubleKnockOpenHop, consume: (response: Response) => Promise, @@ -1570,7 +1578,7 @@ function cancelResponseBody(response: Response, url: string): void { async function doubleKnockCore( request: Request, - identity: { keyId: URL; privateKey: CryptoKey }, + identity: { keyId: URL; privateKey: CryptoKeyLike }, options: DoubleKnockOptions, openHop: DoubleKnockOpenHop, ): Promise { diff --git a/packages/fedify/src/sig/key-types.test.ts b/packages/fedify/src/sig/key-types.test.ts new file mode 100644 index 000000000..0d1373104 --- /dev/null +++ b/packages/fedify/src/sig/key-types.test.ts @@ -0,0 +1,97 @@ +import { test } from "@fedify/fixture"; +import type { CryptographicKey, Multikey } from "@fedify/vocab"; +import type { + AuthenticatedDocumentLoaderFactory, + CryptoKeyLike, + CryptoKeyPairLike, + exportMultibaseKey, + exportSpki, +} from "@fedify/vocab-runtime"; +import type { ActorKeyPairsDispatcher } from "../federation/callback.ts"; +import type { Context } from "../federation/context.ts"; +import type { ActorCallbackSetters } from "../federation/federation.ts"; +import type { SenderKeyPair } from "../federation/send.ts"; +import type { signRequest } from "./http.ts"; +import type { createSignature, signJsonLd } from "./ld.ts"; +import type { createProof, signObject } from "./proof.ts"; +import type { exportJwk } from "./key.ts"; + +/** + * Asserts at compile time that `T` is assignable to `U`. + */ +type AssertAssignable = T; + +/** + * The key usages Node.js adds on top of the DOM and Web Worker ones since + * Node.js 24.7, when ML-KEM support was added to its Web Crypto API. + */ +type NodeKeyUsage = + | KeyUsage + | "encapsulateBits" + | "decapsulateBits" + | "encapsulateKey" + | "decapsulateKey"; + +/** + * Mirrors `import("node:crypto").webcrypto.CryptoKey`, whose `usages` array + * has a wider element type than the ambient `CryptoKey`. + */ +interface NodeCryptoKey extends Omit { + readonly usages: NodeKeyUsage[]; +} + +/** + * Mirrors `import("node:crypto").webcrypto.CryptoKeyPair`. + */ +interface NodeCryptoKeyPair { + readonly privateKey: NodeCryptoKey; + readonly publicKey: NodeCryptoKey; +} + +/** + * These are compile-time assertions. A regression in any of the public + * signatures fails `deno check` even though nothing here runs. + */ +export type NodeKeyTypeChecks = [ + AssertAssignable, + AssertAssignable, + AssertAssignable< + ( + context: Context, + identifier: string, + ) => NodeCryptoKeyPair[] | Promise, + ActorKeyPairsDispatcher + >, + AssertAssignable< + ActorKeyPairsDispatcher, + Parameters["setKeyPairsDispatcher"]>[0] + >, + AssertAssignable, + AssertAssignable< + NodeCryptoKey, + Parameters[0]["privateKey"] + >, + AssertAssignable[1]>, + AssertAssignable[1]>, + AssertAssignable[1]>, + AssertAssignable[1]>, + AssertAssignable[1]>, + AssertAssignable[0]>, + AssertAssignable[0]>, + AssertAssignable[0]>, + AssertAssignable< + NodeCryptoKey, + NonNullable< + ConstructorParameters[0]["publicKey"] + > + >, + AssertAssignable< + NodeCryptoKey, + NonNullable[0]["publicKey"]> + >, +]; + +test("Node.js Web Crypto keys are accepted by the public key types", () => { + // The actual assertions are the type aliases above; this keeps the test + // runner from dropping a file that contains only type-level checks. +}); diff --git a/packages/fedify/src/sig/key.ts b/packages/fedify/src/sig/key.ts index ccb14bb91..2c881d440 100644 --- a/packages/fedify/src/sig/key.ts +++ b/packages/fedify/src/sig/key.ts @@ -6,11 +6,13 @@ import { Object, } from "@fedify/vocab"; import { + type CryptoKeyLike, type DocumentLoader, FetchError, getDocumentLoader, UrlError, } from "@fedify/vocab-runtime"; +import { asCryptoKey } from "@fedify/vocab-runtime/internal"; import { getLogger } from "@logtape/logtape"; import { SpanKind, @@ -105,7 +107,7 @@ function classifyKeyJsonLdError( * @throws {TypeError} If the key is invalid or unsupported. */ export function validateCryptoKey( - key: CryptoKey, + key: CryptoKeyLike, type?: "public" | "private", ): void { if (type != null && key.type !== type) { @@ -181,9 +183,9 @@ export function generateCryptoKeyPair( * serialization and storage. * @throws {TypeError} If the key is invalid or unsupported. */ -export async function exportJwk(key: CryptoKey): Promise { +export async function exportJwk(key: CryptoKeyLike): Promise { validateCryptoKey(key); - const jwk = await crypto.subtle.exportKey("jwk", key); + const jwk = await crypto.subtle.exportKey("jwk", asCryptoKey(key)); if (jwk.crv === "Ed25519") jwk.alg = "Ed25519"; return jwk; } diff --git a/packages/fedify/src/sig/ld.ts b/packages/fedify/src/sig/ld.ts index eeed780d1..9b8afa27b 100644 --- a/packages/fedify/src/sig/ld.ts +++ b/packages/fedify/src/sig/ld.ts @@ -1,10 +1,12 @@ import { Activity, CryptographicKey, getTypeId, Object } from "@fedify/vocab"; import { + type CryptoKeyLike, type DocumentLoader, getDocumentLoader, preloadedContexts, type RemoteDocument, } from "@fedify/vocab-runtime"; +import { asCryptoKey } from "@fedify/vocab-runtime/internal"; import jsonld from "@fedify/vocab-runtime/jsonld"; import { getLogger } from "@logtape/logtape"; import { SpanStatusCode, trace, type TracerProvider } from "@opentelemetry/api"; @@ -838,7 +840,7 @@ export interface CreateSignatureOptions { */ export async function createSignature( jsonLd: unknown, - privateKey: CryptoKey, + privateKey: CryptoKeyLike, keyId: URL, { contextLoader, created }: CreateSignatureOptions = {}, ): Promise { @@ -867,7 +869,7 @@ export async function createSignature( const messageBytes = encoder.encode(message); const signature = await crypto.subtle.sign( "RSASSA-PKCS1-v1_5", - privateKey, + asCryptoKey(privateKey), messageBytes, ); return { @@ -906,7 +908,7 @@ export interface SignJsonLdOptions extends CreateSignatureOptions { */ export async function signJsonLd( jsonLd: unknown, - privateKey: CryptoKey, + privateKey: CryptoKeyLike, keyId: URL, options: SignJsonLdOptions, ): Promise<{ signature: Signature }> { diff --git a/packages/fedify/src/sig/mod.ts b/packages/fedify/src/sig/mod.ts index d28ce0e6d..2bdf32a5a 100644 --- a/packages/fedify/src/sig/mod.ts +++ b/packages/fedify/src/sig/mod.ts @@ -3,6 +3,7 @@ * * @module */ +export type { CryptoKeyLike, CryptoKeyPairLike } from "@fedify/vocab-runtime"; export { type HttpMessageSignaturesSpec, type HttpMessageSignaturesSpecDeterminer, diff --git a/packages/fedify/src/sig/proof.ts b/packages/fedify/src/sig/proof.ts index c77729a12..85864cbf0 100644 --- a/packages/fedify/src/sig/proof.ts +++ b/packages/fedify/src/sig/proof.ts @@ -5,7 +5,8 @@ import { Multikey, type Object, } from "@fedify/vocab"; -import type { DocumentLoader } from "@fedify/vocab-runtime"; +import type { CryptoKeyLike, DocumentLoader } from "@fedify/vocab-runtime"; +import { asCryptoKey } from "@fedify/vocab-runtime/internal"; import { getLogger } from "@logtape/logtape"; import { SpanStatusCode, trace, type TracerProvider } from "@opentelemetry/api"; import { encodeHex } from "byte-encodings/hex"; @@ -57,7 +58,7 @@ export interface CreateProofOptions { */ export async function createProof( object: Object, - privateKey: CryptoKey, + privateKey: CryptoKeyLike, keyId: URL, options: CreateProofOptions = {}, ): Promise { @@ -71,7 +72,7 @@ interface CreateProofResult { async function createProofInternal( object: Object, - privateKey: CryptoKey, + privateKey: CryptoKeyLike, keyId: URL, { contextLoader, context, created }: CreateProofOptions = {}, ): Promise { @@ -105,7 +106,11 @@ async function createProofInternal( const digest = new Uint8Array(proofDigest.byteLength + msgDigest.byteLength); digest.set(new Uint8Array(proofDigest), 0); digest.set(new Uint8Array(msgDigest), proofDigest.byteLength); - const sig = await crypto.subtle.sign("Ed25519", privateKey, digest); + const sig = await crypto.subtle.sign( + "Ed25519", + asCryptoKey(privateKey), + digest, + ); return { proof: new DataIntegrityProof({ cryptosuite: "eddsa-jcs-2022", @@ -166,7 +171,7 @@ export interface SignObjectOptions extends CreateProofOptions { */ export async function signObject( object: T, - privateKey: CryptoKey, + privateKey: CryptoKeyLike, keyId: URL, options: SignObjectOptions = {}, ): Promise { diff --git a/packages/fedify/src/utils/docloader.ts b/packages/fedify/src/utils/docloader.ts index 2aaf22aed..d91296654 100644 --- a/packages/fedify/src/utils/docloader.ts +++ b/packages/fedify/src/utils/docloader.ts @@ -1,5 +1,6 @@ import { createActivityPubRequest, + type CryptoKeyLike, type DocumentLoader, type DocumentLoaderFactoryOptions, type DocumentLoaderOptions, @@ -65,7 +66,7 @@ export interface GetAuthenticatedDocumentLoaderOptions * @since 0.4.0 */ export function getAuthenticatedDocumentLoader( - identity: { keyId: URL; privateKey: CryptoKey }, + identity: { keyId: URL; privateKey: CryptoKeyLike }, options: GetAuthenticatedDocumentLoaderOptions = {}, ): DocumentLoader { return createAuthenticatedDocumentLoader(identity, options); @@ -107,7 +108,7 @@ type LoadResult = * @throws {TypeError} If the key is invalid or unsupported. */ export function createAuthenticatedDocumentLoader( - identity: { keyId: URL; privateKey: CryptoKey }, + identity: { keyId: URL; privateKey: CryptoKeyLike }, { allowPrivateAddress, userAgent, specDeterminer, tracerProvider }: GetAuthenticatedDocumentLoaderOptions = {}, deps: AuthenticatedDocumentLoaderDependencies = {}, diff --git a/packages/vocab-runtime/src/crypto-key.ts b/packages/vocab-runtime/src/crypto-key.ts new file mode 100644 index 000000000..584235048 --- /dev/null +++ b/packages/vocab-runtime/src/crypto-key.ts @@ -0,0 +1,82 @@ +/** + * Structural representations of the Web Cryptography API key types, used for + * parameters that accept keys from any supported runtime. + * + * @module + */ + +/** + * A structural representation of a Web Cryptography API `CryptoKey`. + * + * Fedify accepts this shape instead of the ambient `CryptoKey` type wherever a + * key is supplied by the application. The ambient type depends on the + * consumer's `lib` setting, and `node:crypto`'s `CryptoKey` has been + * incompatible with the DOM and Web Worker ones since `@types/node` 24.7.0, + * which added the ML-KEM key usages to `webcrypto.KeyUsage`. Widening the + * input to this interface lets keys from Deno, Node.js, Bun, and Cloudflare + * Workers reach the same APIs. + * + * The keys are still native `CryptoKey` objects at run time; Fedify narrows + * them internally before calling `crypto.subtle` and validates them with + * {@link validateCryptoKey}. + * @since 2.0.36 + */ +export interface CryptoKeyLike { + /** + * The algorithm the key was generated for, along with any parameters. + */ + readonly algorithm: { readonly name: string }; + + /** + * Whether the key can be exported with `crypto.subtle.exportKey()`. + */ + readonly extractable: boolean; + + /** + * Whether the key is a private, public, or secret key. + * + * This is a plain `string` rather than a union of the known values because + * `@cloudflare/workers-types` declares it as `string`, and a narrower type + * would reject keys from the Workers runtime. + */ + readonly type: string; + + /** + * The operations the key may be used for. + * + * The element type is `string` for the same reason as {@link type}: Node.js + * and the DOM declare different sets of usages, and each set grows over time. + */ + readonly usages: readonly string[]; +} + +/** + * A structural representation of a Web Cryptography API `CryptoKeyPair`. + * + * See {@link CryptoKeyLike} for why Fedify takes this instead of the ambient + * `CryptoKeyPair` type on parameters. + * @since 2.0.36 + */ +export interface CryptoKeyPairLike { + /** + * The private key of the pair. + */ + readonly privateKey: CryptoKeyLike; + + /** + * The public key of the pair. + */ + readonly publicKey: CryptoKeyLike; +} + +/** + * Narrows a {@link CryptoKeyLike} to the ambient `CryptoKey` type, for passing + * it to `crypto.subtle` or to APIs that still declare the ambient type. + * + * This is internal to Fedify and is not exported from the package entry point. + * @param key The key to narrow. + * @returns The same key, typed as the ambient `CryptoKey`. + */ +export function asCryptoKey(key: CryptoKeyLike): CryptoKey { + return key as unknown as CryptoKey; +} diff --git a/packages/vocab-runtime/src/docloader.ts b/packages/vocab-runtime/src/docloader.ts index fa8375767..230c4f906 100644 --- a/packages/vocab-runtime/src/docloader.ts +++ b/packages/vocab-runtime/src/docloader.ts @@ -3,6 +3,7 @@ import { type Span, SpanKind, SpanStatusCode, trace } from "@opentelemetry/api"; import metadata from "../deno.json" with { type: "json" }; import { MAX_BODY_SIZE, readBoundedText } from "./body.ts"; import preloadedContexts from "./contexts.ts"; +import type { CryptoKeyLike } from "./crypto-key.ts"; import { HttpHeaderLink } from "./link.ts"; import { openDirectFetch, @@ -107,7 +108,7 @@ export interface DocumentLoaderFactoryOptions { * @since 0.4.0 */ export type AuthenticatedDocumentLoaderFactory = ( - identity: { keyId: URL; privateKey: CryptoKey }, + identity: { keyId: URL; privateKey: CryptoKeyLike }, options?: DocumentLoaderFactoryOptions, ) => DocumentLoader; diff --git a/packages/vocab-runtime/src/internal.ts b/packages/vocab-runtime/src/internal.ts index ac32e8355..aa6d9d186 100644 --- a/packages/vocab-runtime/src/internal.ts +++ b/packages/vocab-runtime/src/internal.ts @@ -11,3 +11,4 @@ export { type PublicFetchSession, withPublicFetch, } from "./public-fetch.ts"; +export { asCryptoKey } from "./crypto-key.ts"; diff --git a/packages/vocab-runtime/src/jwk.ts b/packages/vocab-runtime/src/jwk.ts index 3c001832f..652235f1e 100644 --- a/packages/vocab-runtime/src/jwk.ts +++ b/packages/vocab-runtime/src/jwk.ts @@ -1,5 +1,7 @@ +import { asCryptoKey, type CryptoKeyLike } from "./crypto-key.ts"; + export function validateCryptoKey( - key: CryptoKey, + key: CryptoKeyLike, type?: "public" | "private", ): void { if (type != null && key.type !== type) { @@ -30,9 +32,9 @@ export function validateCryptoKey( } } -export async function exportJwk(key: CryptoKey): Promise { +export async function exportJwk(key: CryptoKeyLike): Promise { validateCryptoKey(key); - const jwk = await crypto.subtle.exportKey("jwk", key); + const jwk = await crypto.subtle.exportKey("jwk", asCryptoKey(key)); if (jwk.crv === "Ed25519") jwk.alg = "Ed25519"; return jwk; } diff --git a/packages/vocab-runtime/src/key.ts b/packages/vocab-runtime/src/key.ts index 3e7f4e749..6d429e890 100644 --- a/packages/vocab-runtime/src/key.ts +++ b/packages/vocab-runtime/src/key.ts @@ -8,6 +8,7 @@ import { getMulticodecPrefix, removeMulticodecPrefix, } from "./internal/multicodec.ts"; +import { asCryptoKey, type CryptoKeyLike } from "./crypto-key.ts"; import { validateCryptoKey } from "./jwk.ts"; import { decodeMultibase, encodeMultibase } from "./multibase/mod.ts"; @@ -59,9 +60,9 @@ export async function importSpki(pem: string): Promise { * @throws {TypeError} If the key is invalid or unsupported. * @since 0.5.0 */ -export async function exportSpki(key: CryptoKey): Promise { +export async function exportSpki(key: CryptoKeyLike): Promise { validateCryptoKey(key); - const spki = await crypto.subtle.exportKey("spki", key); + const spki = await crypto.subtle.exportKey("spki", asCryptoKey(key)); let pem = encodeBase64(spki); pem = (pem.match(/.{1,64}/g) || []).join("\n"); return `-----BEGIN PUBLIC KEY-----\n${pem}\n-----END PUBLIC KEY-----\n`; @@ -146,18 +147,18 @@ export async function importMultibaseKey(key: string): Promise { * @throws {TypeError} If the key is invalid or unsupported. * @since 0.10.0 */ -export async function exportMultibaseKey(key: CryptoKey): Promise { +export async function exportMultibaseKey(key: CryptoKeyLike): Promise { let content: ArrayBuffer; let code: number; if (key.algorithm.name === "Ed25519") { - content = await crypto.subtle.exportKey("raw", key); + content = await crypto.subtle.exportKey("raw", asCryptoKey(key)); code = 0xed; // ed25519-pub } else if ( key.algorithm.name === "RSASSA-PKCS1-v1_5" && (key.algorithm as unknown as { hash: { name: string } }).hash.name === "SHA-256" ) { - const jwk = await crypto.subtle.exportKey("jwk", key); + const jwk = await crypto.subtle.exportKey("jwk", asCryptoKey(key)); const decodedN = decodeBase64Url(jwk.n!); const n = new Uint8Array(decodedN.length + 1); n.set(decodedN, 1); diff --git a/packages/vocab-runtime/src/mod.ts b/packages/vocab-runtime/src/mod.ts index 49f9fe93a..56164f5ed 100644 --- a/packages/vocab-runtime/src/mod.ts +++ b/packages/vocab-runtime/src/mod.ts @@ -5,6 +5,7 @@ * @module */ export { default as preloadedContexts } from "./contexts.ts"; +export type { CryptoKeyLike, CryptoKeyPairLike } from "./crypto-key.ts"; export { type AuthenticatedDocumentLoaderFactory, type DocumentLoader, diff --git a/packages/vocab-tools/src/__snapshots__/class.test.ts.deno.snap b/packages/vocab-tools/src/__snapshots__/class.test.ts.deno.snap index 2498088cc..089404928 100644 --- a/packages/vocab-tools/src/__snapshots__/class.test.ts.deno.snap +++ b/packages/vocab-tools/src/__snapshots__/class.test.ts.deno.snap @@ -7,6 +7,7 @@ import { getLogger } from \\"@logtape/logtape\\"; import { type Span, SpanStatusCode, type TracerProvider, trace } from \\"@opentelemetry/api\\"; import { + type CryptoKeyLike, decodeMultibase, type DocumentLoader, encodeMultibase, @@ -19115,7 +19116,7 @@ export class CryptographicKey { values: { id?: URL | null; -owner?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKey | null;} +owner?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKeyLike | null;} , options: { documentLoader?: DocumentLoader, @@ -19181,7 +19182,7 @@ owner?: Application | Group | Organization | Person | Service | URL | null;publi values: { id?: URL | null; -owner?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKey | null;} +owner?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKeyLike | null;} = {}, options: { @@ -20036,7 +20037,7 @@ export class Multikey { values: { id?: URL | null; -controller?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKey | null;} +controller?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKeyLike | null;} , options: { documentLoader?: DocumentLoader, @@ -20102,7 +20103,7 @@ controller?: Application | Group | Organization | Person | Service | URL | null; values: { id?: URL | null; -controller?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKey | null;} +controller?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKeyLike | null;} = {}, options: { diff --git a/packages/vocab-tools/src/__snapshots__/class.test.ts.node.snap b/packages/vocab-tools/src/__snapshots__/class.test.ts.node.snap index fa6d1e4fb..21118a53f 100644 --- a/packages/vocab-tools/src/__snapshots__/class.test.ts.node.snap +++ b/packages/vocab-tools/src/__snapshots__/class.test.ts.node.snap @@ -5,6 +5,7 @@ import { getLogger } from \\"@logtape/logtape\\"; import { type Span, SpanStatusCode, type TracerProvider, trace } from \\"@opentelemetry/api\\"; import { + type CryptoKeyLike, decodeMultibase, type DocumentLoader, encodeMultibase, @@ -19113,7 +19114,7 @@ export class CryptographicKey { values: { id?: URL | null; -owner?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKey | null;} +owner?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKeyLike | null;} , options: { documentLoader?: DocumentLoader, @@ -19179,7 +19180,7 @@ owner?: Application | Group | Organization | Person | Service | URL | null;publi values: { id?: URL | null; -owner?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKey | null;} +owner?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKeyLike | null;} = {}, options: { @@ -20034,7 +20035,7 @@ export class Multikey { values: { id?: URL | null; -controller?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKey | null;} +controller?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKeyLike | null;} , options: { documentLoader?: DocumentLoader, @@ -20100,7 +20101,7 @@ controller?: Application | Group | Organization | Person | Service | URL | null; values: { id?: URL | null; -controller?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKey | null;} +controller?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKeyLike | null;} = {}, options: { diff --git a/packages/vocab-tools/src/__snapshots__/class.test.ts.snap b/packages/vocab-tools/src/__snapshots__/class.test.ts.snap index c549f83d2..ba31aa24f 100644 --- a/packages/vocab-tools/src/__snapshots__/class.test.ts.snap +++ b/packages/vocab-tools/src/__snapshots__/class.test.ts.snap @@ -7,6 +7,7 @@ import { getLogger } from "@logtape/logtape"; import { type Span, SpanStatusCode, type TracerProvider, trace } from "@opentelemetry/api"; import { + type CryptoKeyLike, decodeMultibase, type DocumentLoader, encodeMultibase, @@ -19115,7 +19116,7 @@ export class CryptographicKey { values: { id?: URL | null; -owner?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKey | null;} +owner?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKeyLike | null;} , options: { documentLoader?: DocumentLoader, @@ -19181,7 +19182,7 @@ owner?: Application | Group | Organization | Person | Service | URL | null;publi values: { id?: URL | null; -owner?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKey | null;} +owner?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKeyLike | null;} = {}, options: { @@ -20036,7 +20037,7 @@ export class Multikey { values: { id?: URL | null; -controller?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKey | null;} +controller?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKeyLike | null;} , options: { documentLoader?: DocumentLoader, @@ -20102,7 +20103,7 @@ controller?: Application | Group | Organization | Person | Service | URL | null; values: { id?: URL | null; -controller?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKey | null;} +controller?: Application | Group | Organization | Person | Service | URL | null;publicKey?: CryptoKeyLike | null;} = {}, options: { diff --git a/packages/vocab-tools/src/class.ts b/packages/vocab-tools/src/class.ts index 4d989ddca..832a3fe56 100644 --- a/packages/vocab-tools/src/class.ts +++ b/packages/vocab-tools/src/class.ts @@ -143,6 +143,7 @@ export async function* generateClasses( yield `import { type Span, SpanStatusCode, type TracerProvider, trace } from "@opentelemetry/api";\n`; yield `import { + type CryptoKeyLike, decodeMultibase, type DocumentLoader, encodeMultibase, diff --git a/packages/vocab-tools/src/constructor.ts b/packages/vocab-tools/src/constructor.ts index ff3180f03..1236c195b 100644 --- a/packages/vocab-tools/src/constructor.ts +++ b/packages/vocab-tools/src/constructor.ts @@ -18,12 +18,14 @@ function generateParameterType( if (hasSingularAccessor(property)) { if (scalar) { code.push( - `${property.singularName}?: ${getTypeNames(range, types)} | null;`, + `${property.singularName}?: ${ + getTypeNames(range, types, false, true) + } | null;`, ); } else { code.push( `${property.singularName}?: ${ - getTypeNames(range, types) + getTypeNames(range, types, false, true) } | URL | null;`, ); } @@ -31,11 +33,15 @@ function generateParameterType( if (isNonFunctionalProperty(property)) { if (scalar) { code.push( - `${property.pluralName}?: (${getTypeNames(range, types, true)})[];`, + `${property.pluralName}?: (${ + getTypeNames(range, types, true, true) + })[];`, ); } else { code.push( - `${property.pluralName}?: (${getTypeNames(range, types)} | URL)[];`, + `${property.pluralName}?: (${ + getTypeNames(range, types, false, true) + } | URL)[];`, ); } } diff --git a/packages/vocab-tools/src/type.ts b/packages/vocab-tools/src/type.ts index fb1efda8e..9ee9f8427 100644 --- a/packages/vocab-tools/src/type.ts +++ b/packages/vocab-tools/src/type.ts @@ -17,6 +17,11 @@ const ACTIVITY_TYPE_URI = "https://www.w3.org/ns/activitystreams#Activity"; interface ScalarType { name: string; + /** + * The type name to use for values accepted by constructors and `clone()`, + * when it is wider than {@link name}. Defaults to {@link name}. + */ + inputName?: string; typeGuard(variable: string): string; encoder(variable: string): string; compactEncoder?: (variable: string) => string; @@ -333,6 +338,7 @@ const scalarTypes: Record = { }, "fedify:publicKey": { name: "CryptoKey", + inputName: "CryptoKeyLike", typeGuard(v) { return ` // @ts-ignore: CryptoKey exists in the global scope. @@ -355,6 +361,7 @@ const scalarTypes: Record = { }, "fedify:multibaseKey": { name: "CryptoKey", + inputName: "CryptoKeyLike", typeGuard(v) { return ` // @ts-ignore: CryptoKey exists in the global scope. @@ -434,9 +441,13 @@ const scalarTypes: Record = { export function getTypeName( typeUri: string, types: Record, + forInput = false, ): string { if (typeUri in types) return types[typeUri].name; - if (typeUri in scalarTypes) return scalarTypes[typeUri].name; + if (typeUri in scalarTypes) { + const scalar = scalarTypes[typeUri]; + return forInput ? scalar.inputName ?? scalar.name : scalar.name; + } throw new Error(`Unknown type: ${typeUri}`); } @@ -444,10 +455,15 @@ export function getTypeNames( typeUris: string[], types: Record, parentheses = false, + forInput = false, ): string { if (typeUris.length < 1) return "never"; - else if (typeUris.length === 1) return getTypeName(typeUris[0], types); - let typeNames = typeUris.map((typeUri) => getTypeName(typeUri, types)); + else if (typeUris.length === 1) { + return getTypeName(typeUris[0], types, forInput); + } + let typeNames = typeUris.map((typeUri) => + getTypeName(typeUri, types, forInput) + ); typeNames = typeNames.filter((t, i) => typeNames.indexOf(t) === i); const t = typeNames.join(" | "); return parentheses && typeNames.length > 1 ? `(${t})` : t; From 9e400cffe0d4ef7f2dade348b9fab3895290c07a Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Fri, 6 Nov 2026 01:02:06 +0900 Subject: [PATCH 2/6] Regenerate vocabulary when the generator changes The codegen freshness check compared vocab.ts only against the YAML schemas, so an existing checkout kept the old generated code after a change to the vocabulary generator. Compare it against the generator sources under packages/vocab-tools/src as well. https://github.com/fedify-dev/fedify/pull/1391#discussion_r4482751742 Assisted-by: OpenCode:deepseek-flash Changelog: none --- packages/vocab/scripts/codegen.ts | 65 ++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/packages/vocab/scripts/codegen.ts b/packages/vocab/scripts/codegen.ts index 11aa095a5..9fbf6c06e 100644 --- a/packages/vocab/scripts/codegen.ts +++ b/packages/vocab/scripts/codegen.ts @@ -7,35 +7,73 @@ const LOCK_RETRY_MS = 100; const LOCK_TIMEOUT_MS = 60 * 1000; // 1 minute /** - * Get the latest mtime from all YAML files in the schema directory. + * Get the latest mtime of the files under the given directory that match the + * predicate. + * @param dir The directory to search. + * @param matches The predicate to select files by their path relative to + * `dir`. */ -async function getLatestSourceMtime(schemaDir: Path): Promise { +async function getLatestMtime( + dir: Path, + matches: (relativePath: string) => boolean, +): Promise { let latestMtime = 0; - for await (const entry of schemaDir.readDir()) { - if (!entry.isFile) continue; - if (!entry.name.match(/\.ya?ml$/i)) continue; - if (entry.name === "schema.yaml") continue; - const fileStat = await schemaDir.join(entry.name).stat(); - if (fileStat?.mtime && fileStat.mtime.getTime() > latestMtime) { - latestMtime = fileStat.mtime.getTime(); + async function visit(current: Path, prefix: string): Promise { + for await (const entry of Deno.readDir(current.toString())) { + const relativePath = prefix === "" + ? entry.name + : `${prefix}/${entry.name}`; + if (entry.isDirectory) { + await visit(current.join(entry.name), relativePath); + } else if (entry.isFile && matches(relativePath)) { + const fileStat = await current.join(entry.name).stat(); + if (fileStat?.mtime && fileStat.mtime.getTime() > latestMtime) { + latestMtime = fileStat.mtime.getTime(); + } + } } } + await visit(dir, ""); return latestMtime; } /** - * Check if the generated file is up to date compared to source files. + * Check whether a path under the schema directory holds a vocabulary schema. + */ +function isSchemaFile(relativePath: string): boolean { + return /\.ya?ml$/i.test(relativePath) && relativePath !== "schema.yaml"; +} + +/** + * Check whether a path under the generator directory can change the generated + * output. + */ +function isGeneratorSource(relativePath: string): boolean { + if (!relativePath.endsWith(".ts")) return false; + return !relativePath.endsWith(".test.ts") && + !relativePath.endsWith(".bench.ts"); +} + +/** + * Check if the generated file is up to date compared to the vocabulary schemas + * and the code generator. + * @param schemaDir The directory holding the vocabulary schemas. + * @param generatedPath The generated file. + * @param generatorDir The directory holding the code generator sources. */ async function isUpToDate( schemaDir: Path, generatedPath: Path, + generatorDir: Path, ): Promise { try { - const [sourceMtime, generatedStat] = await Promise.all([ - getLatestSourceMtime(schemaDir), + const [schemaMtime, generatorMtime, generatedStat] = await Promise.all([ + getLatestMtime(schemaDir, isSchemaFile), + getLatestMtime(generatorDir, isGeneratorSource), generatedPath.stat(), ]); if (!generatedStat?.mtime) return false; + const sourceMtime = Math.max(schemaMtime, generatorMtime); return generatedStat.mtime.getTime() >= sourceMtime; } catch { // If generated file doesn't exist, it's not up to date @@ -111,6 +149,7 @@ async function codegen() { const scriptsDir = $.path(import.meta.dirname!); const packageDir = scriptsDir.parent()!; const schemaDir = packageDir.join("src"); + const generatorDir = packageDir.parent()!.join("vocab-tools", "src"); const realPath = schemaDir.join("vocab.ts"); const lockPath = packageDir.join(".vocab-codegen.lock"); @@ -118,7 +157,7 @@ async function codegen() { const lock = await acquireLock(lockPath); try { // Check if regeneration is needed (after acquiring lock) - if (await isUpToDate(schemaDir, realPath)) { + if (await isUpToDate(schemaDir, realPath, generatorDir)) { $.log("vocab.ts is up to date, skipping codegen"); return; } From 224b561a30e4e54fb5727ff035c975115b3287cd Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Fri, 6 Nov 2026 01:03:47 +0900 Subject: [PATCH 3/6] Document the vocabulary type helpers CodeRabbit's docstring coverage check flagged the changed generator files, so add the missing docstrings to the helper that builds the constructor parameter types and to the type name resolvers. https://github.com/fedify-dev/fedify/pull/1391#pullrequestreview-5787920862 Assisted-by: OpenCode:deepseek-flash Changelog: none --- packages/vocab-tools/src/constructor.ts | 7 +++++++ packages/vocab-tools/src/type.ts | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/packages/vocab-tools/src/constructor.ts b/packages/vocab-tools/src/constructor.ts index 1236c195b..22b794b7a 100644 --- a/packages/vocab-tools/src/constructor.ts +++ b/packages/vocab-tools/src/constructor.ts @@ -8,6 +8,13 @@ import { getTypeNames, } from "./type.ts"; +/** + * Generates the TypeScript type of a property in the `values` object that the + * constructor and `clone()` accept. + * @param property The property to generate the type for. + * @param types The vocabulary types in scope. + * @returns The generated type expression. + */ function generateParameterType( property: PropertySchema, types: Record, diff --git a/packages/vocab-tools/src/type.ts b/packages/vocab-tools/src/type.ts index 9ee9f8427..5988341ea 100644 --- a/packages/vocab-tools/src/type.ts +++ b/packages/vocab-tools/src/type.ts @@ -438,6 +438,15 @@ const scalarTypes: Record = { }, }; +/** + * Gets the TypeScript type name for a type URI. + * @param typeUri The type URI to resolve. + * @param types The vocabulary types in scope. + * @param forInput Whether to return the type accepted as input by + * constructors and `clone()`, which can be wider than the type + * those APIs return. + * @returns The TypeScript type name. + */ export function getTypeName( typeUri: string, types: Record, @@ -451,6 +460,15 @@ export function getTypeName( throw new Error(`Unknown type: ${typeUri}`); } +/** + * Gets the TypeScript type names for a list of type URIs. + * @param typeUris The type URIs to resolve. + * @param types The vocabulary types in scope. + * @param parentheses Whether to wrap a union in parentheses. + * @param forInput Whether to return the types accepted as input by + * constructors and `clone()`. + * @returns The TypeScript type expression. + */ export function getTypeNames( typeUris: string[], types: Record, From 3fe3a12c3ba841e5b5daf4cc2ada2a3242019956 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Fri, 6 Nov 2026 01:29:54 +0900 Subject: [PATCH 4/6] Rebuild vocabulary on generator metadata changes The generated code embeds the @fedify/vocab-tools version from its deno.json in tracing calls, so a version bump alone must invalidate vocab.ts. Include the generator metadata in the freshness check. https://github.com/fedify-dev/fedify/pull/1391#discussion_r4482940211 Assisted-by: OpenCode:deepseek-flash Changelog: none --- packages/vocab/scripts/codegen.ts | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/vocab/scripts/codegen.ts b/packages/vocab/scripts/codegen.ts index 9fbf6c06e..1c8175ef9 100644 --- a/packages/vocab/scripts/codegen.ts +++ b/packages/vocab/scripts/codegen.ts @@ -60,20 +60,29 @@ function isGeneratorSource(relativePath: string): boolean { * @param schemaDir The directory holding the vocabulary schemas. * @param generatedPath The generated file. * @param generatorDir The directory holding the code generator sources. + * @param generatorMetadata The generator's *deno.json*, whose version is + * embedded in the generated code. */ async function isUpToDate( schemaDir: Path, generatedPath: Path, generatorDir: Path, + generatorMetadata: Path, ): Promise { try { - const [schemaMtime, generatorMtime, generatedStat] = await Promise.all([ - getLatestMtime(schemaDir, isSchemaFile), - getLatestMtime(generatorDir, isGeneratorSource), - generatedPath.stat(), - ]); + const [schemaMtime, generatorMtime, metadataStat, generatedStat] = + await Promise.all([ + getLatestMtime(schemaDir, isSchemaFile), + getLatestMtime(generatorDir, isGeneratorSource), + generatorMetadata.stat(), + generatedPath.stat(), + ]); if (!generatedStat?.mtime) return false; - const sourceMtime = Math.max(schemaMtime, generatorMtime); + const sourceMtime = Math.max( + schemaMtime, + generatorMtime, + metadataStat?.mtime?.getTime() ?? 0, + ); return generatedStat.mtime.getTime() >= sourceMtime; } catch { // If generated file doesn't exist, it's not up to date @@ -150,6 +159,10 @@ async function codegen() { const packageDir = scriptsDir.parent()!; const schemaDir = packageDir.join("src"); const generatorDir = packageDir.parent()!.join("vocab-tools", "src"); + const generatorMetadata = packageDir.parent()!.join( + "vocab-tools", + "deno.json", + ); const realPath = schemaDir.join("vocab.ts"); const lockPath = packageDir.join(".vocab-codegen.lock"); @@ -157,7 +170,9 @@ async function codegen() { const lock = await acquireLock(lockPath); try { // Check if regeneration is needed (after acquiring lock) - if (await isUpToDate(schemaDir, realPath, generatorDir)) { + if ( + await isUpToDate(schemaDir, realPath, generatorDir, generatorMetadata) + ) { $.log("vocab.ts is up to date, skipping codegen"); return; } From 990bff1a2fa5d10d362d0cf11ec056bd93385e8f Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Fri, 6 Nov 2026 01:31:21 +0900 Subject: [PATCH 5/6] Cover clone inputs in the key type test The compile-time checks covered only the constructor publicKey parameters. Add the generated clone() inputs for CryptographicKey and Multikey so a regression there fails deno check as well. https://github.com/fedify-dev/fedify/pull/1391#pullrequestreview-5788165591 Assisted-by: OpenCode:deepseek-flash Changelog: none --- packages/fedify/src/sig/key-types.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/fedify/src/sig/key-types.test.ts b/packages/fedify/src/sig/key-types.test.ts index 0d1373104..aa30aae51 100644 --- a/packages/fedify/src/sig/key-types.test.ts +++ b/packages/fedify/src/sig/key-types.test.ts @@ -89,6 +89,16 @@ export type NodeKeyTypeChecks = [ NodeCryptoKey, NonNullable[0]["publicKey"]> >, + AssertAssignable< + NodeCryptoKey, + NonNullable< + NonNullable[0]>["publicKey"] + > + >, + AssertAssignable< + NodeCryptoKey, + NonNullable[0]>["publicKey"]> + >, ]; test("Node.js Web Crypto keys are accepted by the public key types", () => { From f5d50cc2ab9ed7d59fc6bf4c67a1fa0fdd098119 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Sat, 7 Nov 2026 00:06:31 +0900 Subject: [PATCH 6/6] Keep ambient key types on public properties Widening SenderKeyPair.privateKey and the authenticated document loader identity broke code that reads those values and passes them to Web Crypto. Give SenderKeyPair and SharedInboxKeyDispatcher a key type parameter that defaults to CryptoKey, and keep the document loader factory on the ambient type. Fedify still accepts node:crypto keys at its input boundaries and narrows them internally. https://github.com/fedify-dev/fedify/pull/1391#discussion_r4493432833 https://github.com/fedify-dev/fedify/pull/1391#discussion_r4493462014 https://github.com/fedify-dev/fedify/pull/1391#pullrequestreview-5801464158 Assisted-by: OpenCode:deepseek-flash --- CHANGES.md | 29 ++++++++++++++ changes.d/fedify/node-webcrypto-keys.md | 5 ++- .../vocab-runtime/node-webcrypto-keys.md | 8 ++-- packages/fedify/src/federation/builder.ts | 9 +++-- packages/fedify/src/federation/callback.ts | 11 +++-- packages/fedify/src/federation/context.ts | 8 ++-- packages/fedify/src/federation/federation.ts | 3 +- packages/fedify/src/federation/middleware.ts | 40 +++++++++++-------- packages/fedify/src/federation/send.ts | 13 ++++-- packages/fedify/src/sig/key-types.test.ts | 19 +++++++-- packages/vocab-runtime/src/docloader.ts | 3 +- 11 files changed, 105 insertions(+), 43 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2c6319d47..ada3900cd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,17 @@ To be released. runtimes such as Cloudflare Workers, which may terminate an isolate once the error response is returned. This applies both to `immediate: true` and to federations without an outbox queue. [[#1380], [#1388]] + - Fixed `CryptoKey` and `CryptoKeyPair` values obtained from `node:crypto` + being rejected by `ActorKeyPairsDispatcher`, `SharedInboxKeyDispatcher`, + `SenderKeyPair`, `signRequest()`, `createProof()`, `signObject()`, + `createSignature()`, `signJsonLd()`, `exportJwk()`, and the authenticated + document loader APIs when `@types/node` 24.7.0 or later is installed. + Those APIs now accept a structural key type that both Node.js and + DOM/Web Worker keys satisfy, while the keys returned by Fedify keep the + ambient `CryptoKey` types. `SenderKeyPair` and + `SharedInboxKeyDispatcher` take a type parameter that defaults to + `CryptoKey`, so code that reads a key pair or calls a dispatcher keeps the + narrower type. [[#554], [#1391]] - Fixed Linked Data Signatures being emitted with a JSON-LD context that no longer resolves. The attached `signature` object now uses `https://w3id.org/security/v1` instead of @@ -57,6 +68,7 @@ To be released. configure only `queue: { fanout: ... }` no longer need to add an unused `inbox` or `outbox` queue to make fanout delivery work. [[#1389], [#1392]] +[#554]: https://github.com/fedify-dev/fedify/issues/554 [#664]: https://github.com/fedify-dev/fedify/issues/664 [#1339]: https://github.com/fedify-dev/fedify/issues/1339 [#1347]: https://github.com/fedify-dev/fedify/pull/1347 @@ -65,10 +77,16 @@ To be released. [#1388]: https://github.com/fedify-dev/fedify/pull/1388 [#1389]: https://github.com/fedify-dev/fedify/issues/1389 [#1390]: https://github.com/fedify-dev/fedify/pull/1390 +[#1391]: https://github.com/fedify-dev/fedify/pull/1391 [#1392]: https://github.com/fedify-dev/fedify/pull/1392 ### @fedify/vocab + - Fixed `CryptographicKey` and `Multikey` rejecting a `publicKey` obtained + from `node:crypto` in their constructors and `clone()` when `@types/node` + 24.7.0 or later is installed. The property accepts a structural key type + now, while the getters still return the ambient `CryptoKey` type. + [[#554], [#1391]] - Fixed parsing of date-times without seconds, such as `2024-01-01T10:00Z`, `2024-01-01T10:00+09:00`, or `2024-01-01T10:00.5Z`, which Activity Streams 2.0 allows. They raised a `RangeError`, or were ignored in @@ -90,9 +108,20 @@ To be released. literals the same way. It treats a literal without a UTC offset as UTC and handles literals without seconds, including those with a fraction. [[#1339], [#1347] by ChanHaeng Lee\] + - Added the `CryptoKeyLike` and `CryptoKeyPairLike` types for the structural + key shape that Fedify APIs accept. They cover keys from Node.js, + DOM/Web Workers, and Cloudflare Workers, so `exportSpki()`, + `exportMultibaseKey()`, and `getAuthenticatedDocumentLoader()` no longer + reject keys obtained from `node:crypto` with `@types/node` 24.7.0 or later. + `AuthenticatedDocumentLoaderFactory` keeps the ambient `CryptoKey` type on + the identity it receives, so existing implementations do not have to + change. [[#554], [#1391]] ### @fedify/vocab-tools + - Changed the code generated by `@fedify/vocab-tools` to import the + `CryptoKeyLike` type from `@fedify/vocab-runtime`. Regenerated code now + requires `@fedify/vocab-runtime` 2.0.36 or later. [[#554], [#1391]] - Fixed generated `xsd:dateTime` decoders failing on date-times without seconds, such as `2024-01-01T10:00Z` or `2024-01-01T10:00.5Z`. Regenerate your vocabulary code to get the fix. Generated code now imports diff --git a/changes.d/fedify/node-webcrypto-keys.md b/changes.d/fedify/node-webcrypto-keys.md index 38afb577b..618568163 100644 --- a/changes.d/fedify/node-webcrypto-keys.md +++ b/changes.d/fedify/node-webcrypto-keys.md @@ -10,4 +10,7 @@ links: document loader APIs when `@types/node` 24.7.0 or later is installed. Those APIs now accept a structural key type that both Node.js and DOM/Web Worker keys satisfy, while the keys returned by Fedify keep the - ambient `CryptoKey` types. [[#554], [#1391]] + ambient `CryptoKey` types. `SenderKeyPair` and + `SharedInboxKeyDispatcher` take a type parameter that defaults to + `CryptoKey`, so code that reads a key pair or calls a dispatcher keeps the + narrower type. [[#554], [#1391]] diff --git a/changes.d/vocab-runtime/node-webcrypto-keys.md b/changes.d/vocab-runtime/node-webcrypto-keys.md index e734ff646..88f31bf05 100644 --- a/changes.d/vocab-runtime/node-webcrypto-keys.md +++ b/changes.d/vocab-runtime/node-webcrypto-keys.md @@ -6,8 +6,8 @@ links: - Added the `CryptoKeyLike` and `CryptoKeyPairLike` types for the structural key shape that Fedify APIs accept. They cover keys from Node.js, DOM/Web Workers, and Cloudflare Workers, so `exportSpki()`, - `exportMultibaseKey()`, and `AuthenticatedDocumentLoaderFactory` no longer + `exportMultibaseKey()`, and `getAuthenticatedDocumentLoader()` no longer reject keys obtained from `node:crypto` with `@types/node` 24.7.0 or later. - An implementation of `AuthenticatedDocumentLoaderFactory` that annotates - the `identity` parameter with the ambient `CryptoKey` type now has to - annotate it with `CryptoKeyLike` instead. [[#554], [#1391]] + `AuthenticatedDocumentLoaderFactory` keeps the ambient `CryptoKey` type on + the identity it receives, so existing implementations do not have to + change. [[#554], [#1391]] diff --git a/packages/fedify/src/federation/builder.ts b/packages/fedify/src/federation/builder.ts index 38a4c8970..923068a1c 100644 --- a/packages/fedify/src/federation/builder.ts +++ b/packages/fedify/src/federation/builder.ts @@ -7,7 +7,7 @@ import type { Recipient, } from "@fedify/vocab"; import { getTypeId } from "@fedify/vocab"; -import type { CryptoKeyPairLike } from "@fedify/vocab-runtime"; +import type { CryptoKeyLike, CryptoKeyPairLike } from "@fedify/vocab-runtime"; import { getLogger } from "@logtape/logtape"; import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api"; import type { Tracer } from "@opentelemetry/api"; @@ -108,7 +108,10 @@ export class FederationBuilderImpl >; inboxListeners?: InboxListenerSet; inboxErrorHandler?: InboxErrorHandler; - sharedInboxKeyDispatcher?: SharedInboxKeyDispatcher; + sharedInboxKeyDispatcher?: SharedInboxKeyDispatcher< + TContextData, + CryptoKeyLike + >; outboxPermanentFailureHandler?: OutboxPermanentFailureHandler; idempotencyStrategy?: | IdempotencyStrategy @@ -1157,7 +1160,7 @@ export class FederationBuilderImpl return setters; }, setSharedKeyDispatcher: ( - dispatcher: SharedInboxKeyDispatcher, + dispatcher: SharedInboxKeyDispatcher, ): InboxListenerSetters => { this.sharedInboxKeyDispatcher = dispatcher; return setters; diff --git a/packages/fedify/src/federation/callback.ts b/packages/fedify/src/federation/callback.ts index 675976d9e..b2a6991be 100644 --- a/packages/fedify/src/federation/callback.ts +++ b/packages/fedify/src/federation/callback.ts @@ -1,5 +1,5 @@ import type { Activity, Actor, Object } from "@fedify/vocab"; -import type { CryptoKeyPairLike } from "@fedify/vocab-runtime"; +import type { CryptoKeyLike, CryptoKeyPairLike } from "@fedify/vocab-runtime"; import type { Link } from "@fedify/webfinger"; import type { NodeInfo } from "../nodeinfo/types.ts"; import type { PageItems } from "./collection.ts"; @@ -213,15 +213,18 @@ export type InboxErrorHandler = ( * not authorized. * @since 0.11.0 */ -export type SharedInboxKeyDispatcher = ( +export type SharedInboxKeyDispatcher< + TContextData, + TKeyPair extends CryptoKeyLike = CryptoKey, +> = ( context: Context, ) => - | SenderKeyPair + | SenderKeyPair | { identifier: string } | { username: string } | null | Promise< - | SenderKeyPair + | SenderKeyPair | { identifier: string } | { username: string } | null diff --git a/packages/fedify/src/federation/context.ts b/packages/fedify/src/federation/context.ts index 3bae40278..737498472 100644 --- a/packages/fedify/src/federation/context.ts +++ b/packages/fedify/src/federation/context.ts @@ -371,8 +371,8 @@ export interface Context { */ sendActivity( sender: - | SenderKeyPair - | SenderKeyPair[] + | SenderKeyPair + | SenderKeyPair[] | { identifier: string } | { username: string }, recipients: Recipient | Recipient[], @@ -624,8 +624,8 @@ export interface InboxContext extends Context { */ forwardActivity( forwarder: - | SenderKeyPair - | SenderKeyPair[] + | SenderKeyPair + | SenderKeyPair[] | { identifier: string } | { username: string }, recipients: Recipient | Recipient[], diff --git a/packages/fedify/src/federation/federation.ts b/packages/fedify/src/federation/federation.ts index 8aceafed2..a65de4e47 100644 --- a/packages/fedify/src/federation/federation.ts +++ b/packages/fedify/src/federation/federation.ts @@ -7,6 +7,7 @@ import type { } from "@fedify/vocab"; import type { AuthenticatedDocumentLoaderFactory, + CryptoKeyLike, CryptoKeyPairLike, DocumentLoaderFactory, GetUserAgentOptions, @@ -1188,7 +1189,7 @@ export interface InboxListenerSetters { * @since 0.11.0 */ setSharedKeyDispatcher( - dispatcher: SharedInboxKeyDispatcher, + dispatcher: SharedInboxKeyDispatcher, ): InboxListenerSetters; /** diff --git a/packages/fedify/src/federation/middleware.ts b/packages/fedify/src/federation/middleware.ts index 6ea689ca8..e19be6fc3 100644 --- a/packages/fedify/src/federation/middleware.ts +++ b/packages/fedify/src/federation/middleware.ts @@ -1367,7 +1367,7 @@ export class FederationImpl } async sendActivity( - keys: SenderKeyPair[], + keys: SenderKeyPair[], inboxes: Record< string, { actorIds: Iterable; sharedInbox: boolean } @@ -2413,10 +2413,10 @@ export class ContextImpl implements Context { | { identifier: string } | { username: string }, ): Promise; - getDocumentLoader(identity: SenderKeyPair): DocumentLoader; + getDocumentLoader(identity: SenderKeyPair): DocumentLoader; getDocumentLoader( identity: - | SenderKeyPair + | SenderKeyPair | { identifier: string } | { username: string }, ): DocumentLoader | Promise { @@ -2444,11 +2444,17 @@ export class ContextImpl implements Context { return keyPair.then((pair) => pair == null ? this.documentLoader - : this.federation.authenticatedDocumentLoaderFactory(pair) + : this.federation.authenticatedDocumentLoaderFactory({ + keyId: pair.keyId, + privateKey: asCryptoKey(pair.privateKey), + }) ); }); } - return this.federation.authenticatedDocumentLoaderFactory(identity); + return this.federation.authenticatedDocumentLoaderFactory({ + keyId: identity.keyId, + privateKey: asCryptoKey(identity.privateKey), + }); } lookupObject( @@ -2518,8 +2524,8 @@ export class ContextImpl implements Context { async sendActivity( sender: - | SenderKeyPair - | SenderKeyPair[] + | SenderKeyPair + | SenderKeyPair[] | { identifier: string } | { username: string }, recipients: Recipient | Recipient[] | "followers", @@ -2590,8 +2596,8 @@ export class ContextImpl implements Context { protected async sendActivityInternal( sender: - | SenderKeyPair - | SenderKeyPair[] + | SenderKeyPair + | SenderKeyPair[] | { identifier: string } | { username: string }, recipients: Recipient | Recipient[] | "followers", @@ -2601,7 +2607,7 @@ export class ContextImpl implements Context { activityJsonLd?: unknown, ): Promise { const logger = getLogger(["fedify", "federation", "outbox"]); - let keys: SenderKeyPair[]; + let keys: SenderKeyPair[]; let identifier: string | null = null; let actorKeyPairs: ActorKeyPair[] | null = null; if ("identifier" in sender || "username" in sender) { @@ -3295,8 +3301,8 @@ export class InboxContextImpl extends ContextImpl forwardActivity( forwarder: - | SenderKeyPair - | SenderKeyPair[] + | SenderKeyPair + | SenderKeyPair[] | { identifier: string } | { username: string }, recipients: Recipient | Recipient[], @@ -3311,8 +3317,8 @@ export class InboxContextImpl extends ContextImpl ): Promise; forwardActivity( forwarder: - | SenderKeyPair - | SenderKeyPair[] + | SenderKeyPair + | SenderKeyPair[] | { identifier: string } | { username: string }, recipients: Recipient | Recipient[] | "followers", @@ -3348,15 +3354,15 @@ export class InboxContextImpl extends ContextImpl private async forwardActivityInternal( forwarder: - | SenderKeyPair - | SenderKeyPair[] + | SenderKeyPair + | SenderKeyPair[] | { identifier: string } | { username: string }, recipients: Recipient | Recipient[] | "followers", options?: ForwardActivityOptions, ): Promise { const logger = getLogger(["fedify", "federation", "inbox"]); - let keys: SenderKeyPair[]; + let keys: SenderKeyPair[]; let identifier: string | null = null; if ( "identifier" in forwarder || "username" in forwarder diff --git a/packages/fedify/src/federation/send.ts b/packages/fedify/src/federation/send.ts index 38d60f973..375057992 100644 --- a/packages/fedify/src/federation/send.ts +++ b/packages/fedify/src/federation/send.ts @@ -88,13 +88,18 @@ export function extractInboxes( /** * A key pair for an actor who sends an activity. + * + * The private key type is parameterized so that Fedify can accept keys from + * `node:crypto` at its input boundaries while `SenderKeyPair` still exposes the + * ambient `CryptoKey` type by default. + * @template TKey The type of the private key. * @since 0.10.0 */ -export interface SenderKeyPair { +export interface SenderKeyPair { /** * The actor's private key to sign the request. */ - readonly privateKey: CryptoKeyLike; + readonly privateKey: TKey; /** * The public key ID that corresponds to the private key. @@ -127,7 +132,7 @@ export interface SendActivityParameters { * The key pairs of the sender to sign the request. It must not be empty. * @since 0.10.0 */ - readonly keys: readonly SenderKeyPair[]; + readonly keys: readonly SenderKeyPair[]; /** * The inbox URL to send the activity to. @@ -322,7 +327,7 @@ async function sendActivityInternal( body: JSON.stringify(activity), redirect: "manual", }); - let rsaKey: SenderKeyPair | null = null; + let rsaKey: SenderKeyPair | null = null; for (const key of keys) { if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") { rsaKey = key; diff --git a/packages/fedify/src/sig/key-types.test.ts b/packages/fedify/src/sig/key-types.test.ts index aa30aae51..0f1af8fdb 100644 --- a/packages/fedify/src/sig/key-types.test.ts +++ b/packages/fedify/src/sig/key-types.test.ts @@ -9,8 +9,12 @@ import type { } from "@fedify/vocab-runtime"; import type { ActorKeyPairsDispatcher } from "../federation/callback.ts"; import type { Context } from "../federation/context.ts"; -import type { ActorCallbackSetters } from "../federation/federation.ts"; +import type { + ActorCallbackSetters, + InboxListenerSetters, +} from "../federation/federation.ts"; import type { SenderKeyPair } from "../federation/send.ts"; +import type { getAuthenticatedDocumentLoader } from "../utils/docloader.ts"; import type { signRequest } from "./http.ts"; import type { createSignature, signJsonLd } from "./ld.ts"; import type { createProof, signObject } from "./proof.ts"; @@ -66,10 +70,19 @@ export type NodeKeyTypeChecks = [ ActorKeyPairsDispatcher, Parameters["setKeyPairsDispatcher"]>[0] >, - AssertAssignable, + AssertAssignable, + AssertAssignable["privateKey"]>, + AssertAssignable< + (context: Context) => SenderKeyPair, + Parameters["setSharedKeyDispatcher"]>[0] + >, + AssertAssignable< + Parameters[0]["privateKey"], + CryptoKey + >, AssertAssignable< NodeCryptoKey, - Parameters[0]["privateKey"] + Parameters[0]["privateKey"] >, AssertAssignable[1]>, AssertAssignable[1]>, diff --git a/packages/vocab-runtime/src/docloader.ts b/packages/vocab-runtime/src/docloader.ts index 230c4f906..fa8375767 100644 --- a/packages/vocab-runtime/src/docloader.ts +++ b/packages/vocab-runtime/src/docloader.ts @@ -3,7 +3,6 @@ import { type Span, SpanKind, SpanStatusCode, trace } from "@opentelemetry/api"; import metadata from "../deno.json" with { type: "json" }; import { MAX_BODY_SIZE, readBoundedText } from "./body.ts"; import preloadedContexts from "./contexts.ts"; -import type { CryptoKeyLike } from "./crypto-key.ts"; import { HttpHeaderLink } from "./link.ts"; import { openDirectFetch, @@ -108,7 +107,7 @@ export interface DocumentLoaderFactoryOptions { * @since 0.4.0 */ export type AuthenticatedDocumentLoaderFactory = ( - identity: { keyId: URL; privateKey: CryptoKeyLike }, + identity: { keyId: URL; privateKey: CryptoKey }, options?: DocumentLoaderFactoryOptions, ) => DocumentLoader;