Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/no-extra-keys-type-safety.md
Original file line number Diff line number Diff line change
@@ -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.
25 changes: 25 additions & 0 deletions src/internal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,3 +410,28 @@ export type Flatten<element> = element extends readonly (infer item)[] ? item :
* @internal
*/
export type ValueOf<T> = 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<T extends Opts>(p: NoExtraKeys<T, Opts>): void
* f({ sse: true }) // ✅
* f({ stream: true }) // ❌ — 'stream' mapped to never
* ```
*
* @internal
*/
export type NoExtraKeys<T, Shape> = [T] extends [Shape]
? T & { [K in Exclude<keyof T, KeysOfUnion<Shape>>]: never }
: never

/** @internal */
type KeysOfUnion<T> = T extends unknown ? keyof T : never
21 changes: 7 additions & 14 deletions src/tempo/internal/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
6 changes: 5 additions & 1 deletion src/tempo/server/Charge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -33,6 +33,10 @@ import * as Methods from '../Methods.js'
* const charge = tempo.charge()
* ```
*/
export function charge<const parameters extends charge.Parameters>(
parameters?: NoExtraKeys<parameters, charge.Parameters>,
): Method.Server<typeof Methods.charge, charge.DeriveDefaults<parameters>>
/** @internal */
export function charge<const parameters extends charge.Parameters>(
parameters: parameters = {} as parameters,
) {
Expand Down
5 changes: 4 additions & 1 deletion src/tempo/server/Methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ import { session as session_, settle as settle_ } from './Session.js'
* ```
*/
export function tempo<const parameters extends tempo.Parameters>(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 {
Expand Down
10 changes: 9 additions & 1 deletion src/tempo/server/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -82,6 +82,14 @@ type SessionMethodDetails = {
* })
* ```
*/
export function session<const parameters extends session.Parameters>(
p?: NoExtraKeys<parameters, session.Parameters>,
): Method.Server<
typeof Methods.session,
session.DeriveDefaults<parameters>,
parameters['sse'] extends false | undefined ? undefined : Transport.Sse
>
/** @internal */
export function session<const parameters extends session.Parameters>(p?: parameters) {
const parameters = p as parameters
const {
Expand Down
Loading