diff --git a/.changeset/no-extra-keys-type-safety.md b/.changeset/no-extra-keys-type-safety.md new file mode 100644 index 00000000..73bd5f90 --- /dev/null +++ b/.changeset/no-extra-keys-type-safety.md @@ -0,0 +1,5 @@ +--- +'mppx': patch +--- + +Added `NoExtraKeys` compile-time guard to `tempo.session()` and `tempo.charge()`. Unknown properties (e.g. `stream` instead of `sse`) now cause a type error instead of being silently accepted. diff --git a/src/internal/types.ts b/src/internal/types.ts index a72ea9cb..89705fa3 100644 --- a/src/internal/types.ts +++ b/src/internal/types.ts @@ -410,3 +410,28 @@ export type Flatten = element extends readonly (infer item)[] ? item : * @internal */ export type ValueOf = T[keyof T] + +/** + * Rejects objects with keys not present in `Shape`. + * + * TypeScript's `extends` constraint on generics does not perform excess-property + * checking, so typos like `{ stream: … }` instead of `{ sse: … }` silently + * pass. Wrapping the parameter type with `NoExtraKeys` maps every extra key + * to `never`, surfacing a compile-time error. + * + * @example + * ```ts + * type Opts = { sse?: boolean } + * declare function f(p: NoExtraKeys): void + * f({ sse: true }) // ✅ + * f({ stream: true }) // ❌ — 'stream' mapped to never + * ``` + * + * @internal + */ +export type NoExtraKeys = [T] extends [Shape] + ? T & { [K in Exclude>]: never } + : never + +/** @internal */ +type KeysOfUnion = T extends unknown ? keyof T : never diff --git a/src/tempo/internal/account.ts b/src/tempo/internal/account.ts index c8a88ff9..a417ee5d 100644 --- a/src/tempo/internal/account.ts +++ b/src/tempo/internal/account.ts @@ -33,18 +33,11 @@ export function resolve(parameters: resolve.Parameters) { } export declare namespace resolve { - type Parameters = { recipient?: Address | undefined } & ( - | { - /** Account that performs payment operations. */ - account?: Account | undefined - /** When true, the account also sponsors (pays) transaction fees. */ - feePayer?: true | undefined - } - | { - /** Address that receives payment. */ - account?: Address | undefined - /** Optional fee payer account or fee payer URL for covering transaction fees. */ - feePayer?: Account | string | undefined - } - ) + type Parameters = { + recipient?: Address | undefined + /** Account or address that performs payment operations / receives payment. */ + account?: Account | Address | undefined + /** When `true`, the account also sponsors fees. An `Account` object or URL string can also be provided as a dedicated fee payer. */ + feePayer?: Account | string | true | undefined + } } diff --git a/src/tempo/server/Charge.ts b/src/tempo/server/Charge.ts index 645e6965..f95e6064 100644 --- a/src/tempo/server/Charge.ts +++ b/src/tempo/server/Charge.ts @@ -11,7 +11,7 @@ import { tempo as tempo_chain } from 'viem/chains' import { Abis, Transaction } from 'viem/tempo' import { PaymentExpiredError } from '../../Errors.js' -import type { LooseOmit } from '../../internal/types.js' +import type { LooseOmit, NoExtraKeys } from '../../internal/types.js' import * as Method from '../../Method.js' import * as Store from '../../Store.js' import * as Client from '../../viem/Client.js' @@ -33,6 +33,10 @@ import * as Methods from '../Methods.js' * const charge = tempo.charge() * ``` */ +export function charge( + parameters?: NoExtraKeys, +): Method.Server> +/** @internal */ export function charge( parameters: parameters = {} as parameters, ) { diff --git a/src/tempo/server/Methods.ts b/src/tempo/server/Methods.ts index 65a1790a..1b9096a3 100644 --- a/src/tempo/server/Methods.ts +++ b/src/tempo/server/Methods.ts @@ -14,7 +14,10 @@ import { session as session_, settle as settle_ } from './Session.js' * ``` */ export function tempo(parameters?: parameters) { - return [tempo.charge(parameters), tempo.session(parameters)] as const + return [ + tempo.charge(parameters as charge_.Parameters as never), + tempo.session(parameters as session_.Parameters as never), + ] as const } export namespace tempo { diff --git a/src/tempo/server/Session.ts b/src/tempo/server/Session.ts index 3e329571..b09e66e0 100644 --- a/src/tempo/server/Session.ts +++ b/src/tempo/server/Session.ts @@ -30,7 +30,7 @@ import { VerificationFailedError, } from '../../Errors.js' import type { Challenge, Credential } from '../../index.js' -import type { LooseOmit } from '../../internal/types.js' +import type { LooseOmit, NoExtraKeys } from '../../internal/types.js' import * as Method from '../../Method.js' import * as Store from '../../Store.js' import * as Client from '../../viem/Client.js' @@ -82,6 +82,14 @@ type SessionMethodDetails = { * }) * ``` */ +export function session( + p?: NoExtraKeys, +): Method.Server< + typeof Methods.session, + session.DeriveDefaults, + parameters['sse'] extends false | undefined ? undefined : Transport.Sse +> +/** @internal */ export function session(p?: parameters) { const parameters = p as parameters const {