Skip to content

Commit cb52419

Browse files
committed
fix: reject unknown properties on session() and charge() at compile time
Add NoExtraKeys utility type and apply it via overload signatures to tempo.session() and tempo.charge(). This catches typos like `stream: { poll: true }` instead of `sse: { poll: true }` that previously slipped through because generic `extends` constraints bypass excess property checking. Fixes the class of bug from PR #159.
1 parent 0742b76 commit cb52419

6 files changed

Lines changed: 55 additions & 17 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"mppx": patch
3+
---
4+
5+
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.

src/internal/types.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,3 +412,28 @@ export type Flatten<element> = element extends readonly (infer item)[] ? item :
412412
* @internal
413413
*/
414414
export type ValueOf<T> = T[keyof T]
415+
416+
/**
417+
* Rejects objects with keys not present in `Shape`.
418+
*
419+
* TypeScript's `extends` constraint on generics does not perform excess-property
420+
* checking, so typos like `{ stream: … }` instead of `{ sse: … }` silently
421+
* pass. Wrapping the parameter type with `NoExtraKeys` maps every extra key
422+
* to `never`, surfacing a compile-time error.
423+
*
424+
* @example
425+
* ```ts
426+
* type Opts = { sse?: boolean }
427+
* declare function f<T extends Opts>(p: NoExtraKeys<T, Opts>): void
428+
* f({ sse: true }) // ✅
429+
* f({ stream: true }) // ❌ — 'stream' mapped to never
430+
* ```
431+
*
432+
* @internal
433+
*/
434+
export type NoExtraKeys<T, Shape> = [T] extends [Shape]
435+
? T & { [K in Exclude<keyof T, KeysOfUnion<Shape>>]: never }
436+
: never
437+
438+
/** @internal */
439+
type KeysOfUnion<T> = T extends unknown ? keyof T : never

src/tempo/internal/account.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,11 @@ export function resolve(parameters: resolve.Parameters) {
3333
}
3434

3535
export declare namespace resolve {
36-
type Parameters = { recipient?: Address | undefined } & (
37-
| {
38-
/** Account that performs payment operations. */
39-
account?: Account | undefined
40-
/** When true, the account also sponsors (pays) transaction fees. */
41-
feePayer?: true | undefined
42-
}
43-
| {
44-
/** Address that receives payment. */
45-
account?: Address | undefined
46-
/** Optional fee payer account or fee payer URL for covering transaction fees. */
47-
feePayer?: Account | string | undefined
48-
}
49-
)
36+
type Parameters = {
37+
recipient?: Address | undefined
38+
/** Account or address that performs payment operations / receives payment. */
39+
account?: Account | Address | undefined
40+
/** When `true`, the account also sponsors fees. An `Account` object or URL string can also be provided as a dedicated fee payer. */
41+
feePayer?: Account | string | true | undefined
42+
}
5043
}

src/tempo/server/Charge.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import { tempo as tempo_chain } from 'viem/chains'
1010
import { Abis, Transaction } from 'viem/tempo'
1111
import { PaymentExpiredError } from '../../Errors.js'
12-
import type { LooseOmit } from '../../internal/types.js'
12+
import type { LooseOmit, NoExtraKeys } from '../../internal/types.js'
1313
import * as Method from '../../Method.js'
1414
import * as Client from '../../viem/Client.js'
1515
import * as Account from '../internal/account.js'
@@ -29,6 +29,10 @@ import * as Methods from '../Methods.js'
2929
* const charge = tempo.charge()
3030
* ```
3131
*/
32+
export function charge<const parameters extends charge.Parameters>(
33+
parameters?: NoExtraKeys<parameters, charge.Parameters>,
34+
): Method.Server<typeof Methods.charge, charge.DeriveDefaults<parameters>>
35+
/** @internal */
3236
export function charge<const parameters extends charge.Parameters>(
3337
parameters: parameters = {} as parameters,
3438
) {

src/tempo/server/Methods.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import { session as session_, settle as settle_ } from './Session.js'
1414
* ```
1515
*/
1616
export function tempo<const parameters extends tempo.Parameters>(parameters?: parameters) {
17-
return [tempo.charge(parameters), tempo.session(parameters)] as const
17+
return [
18+
tempo.charge(parameters as charge_.Parameters as never),
19+
tempo.session(parameters as session_.Parameters as never),
20+
] as const
1821
}
1922

2023
export namespace tempo {

src/tempo/server/Session.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
VerificationFailedError,
3030
} from '../../Errors.js'
3131
import type { Challenge, Credential } from '../../index.js'
32-
import type { LooseOmit } from '../../internal/types.js'
32+
import type { LooseOmit, NoExtraKeys } from '../../internal/types.js'
3333
import * as Method from '../../Method.js'
3434
import * as Store from '../../Store.js'
3535
import * as Client from '../../viem/Client.js'
@@ -81,6 +81,14 @@ type SessionMethodDetails = {
8181
* })
8282
* ```
8383
*/
84+
export function session<const parameters extends session.Parameters>(
85+
p?: NoExtraKeys<parameters, session.Parameters>,
86+
): Method.Server<
87+
typeof Methods.session,
88+
session.DeriveDefaults<parameters>,
89+
parameters['sse'] extends false | undefined ? undefined : Transport.Sse
90+
>
91+
/** @internal */
8492
export function session<const parameters extends session.Parameters>(p?: parameters) {
8593
const parameters = p as parameters
8694
const {

0 commit comments

Comments
 (0)