@insumermodel/mppx-condition-gate 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,194 @@
1
+ # @insumermodel/mppx-condition-gate
2
+
3
+ Condition-based access for [mppx](https://github.com/wevm/mppx) routes. One signed call between request and charge gives free access to wallets that meet your conditions; everyone else falls through to the normal paid path. Four condition types: token balance, NFT ownership, EAS attestation, Farcaster ID. 33 chains. No RPC management.
4
+
5
+ > **Migrating from `@insumermodel/mppx-token-gate`?** This is the v2 successor. See [Migration](#migrating-from-mppx-token-gate) below.
6
+
7
+ ## How it works
8
+
9
+ mppx embeds the payer's identity in every payment credential as a DID string (`credential.source: "did:pkh:eip155:8453:0xABC..."`). `conditionGate` reads that address, calls [InsumerAPI](https://insumermodel.com) to evaluate the configured conditions, and short-circuits the payment flow for wallets that pass.
10
+
11
+ 1. Request arrives with a payment credential
12
+ 2. `conditionGate` extracts the payer address from `credential.source`
13
+ 3. [InsumerAPI](https://insumermodel.com/developers/api-reference/) evaluates the conditions and returns an ECDSA P-256 signed attestation
14
+ 4. **Pass** → free receipt returned (`reference: "condition-gate:free:{attestationId}"`)
15
+ 5. **Fail** → delegates to the original `verify` (normal payment proceeds)
16
+
17
+ The signed attestation is verifiable offline via [JWKS](https://insumermodel.com/.well-known/jwks.json). The adapter does not re-sign or wrap the result; the signature on the attestation is the one InsumerAPI produced.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install @insumermodel/mppx-condition-gate
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```ts
28
+ import { Mppx, tempo } from 'mppx/server'
29
+ import { conditionGate } from '@insumermodel/mppx-condition-gate'
30
+
31
+ const tempoCharge = tempo({
32
+ currency: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
33
+ recipient: '0xYourAddress',
34
+ })
35
+
36
+ const gatedCharge = conditionGate(tempoCharge, {
37
+ apiKey: process.env.INSUMER_API_KEY,
38
+ conditions: [{
39
+ type: 'nft_ownership',
40
+ contractAddress: '0xYourNFT',
41
+ chainId: 8453, // Base
42
+ }],
43
+ })
44
+
45
+ const mppx = Mppx.create({ methods: [gatedCharge] })
46
+ ```
47
+
48
+ Works with any framework (Hono, Express, Elysia, Next.js) and any payment method (tempo, stripe). The adapter wraps `Method.Server`, so no middleware changes needed.
49
+
50
+ ## Condition types
51
+
52
+ Mix any of the four in a single call. `matchMode: 'any'` (default) passes when any one is met; `matchMode: 'all'` requires all of them.
53
+
54
+ ### Token balance
55
+
56
+ ```ts
57
+ {
58
+ type: 'token_balance',
59
+ contractAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
60
+ chainId: 1,
61
+ threshold: 1000,
62
+ decimals: 6,
63
+ label: 'USDC >= 1000',
64
+ }
65
+ ```
66
+
67
+ ### NFT ownership
68
+
69
+ ```ts
70
+ {
71
+ type: 'nft_ownership',
72
+ contractAddress: '0xYourNFT',
73
+ chainId: 8453,
74
+ label: 'Holds the access NFT',
75
+ }
76
+ ```
77
+
78
+ ### EAS attestation (compliance template)
79
+
80
+ ```ts
81
+ {
82
+ type: 'eas_attestation',
83
+ template: 'coinbase_verified_account',
84
+ chainId: 8453,
85
+ label: 'Coinbase KYC verified',
86
+ }
87
+ ```
88
+
89
+ Available templates: `coinbase_verified_account`, `coinbase_verified_country`, `coinbase_one`, `gitcoin_passport_score`, `gitcoin_passport_active`. See [GET /v1/compliance/templates](https://insumermodel.com/developers/compliance/).
90
+
91
+ ### EAS attestation (raw schema)
92
+
93
+ ```ts
94
+ {
95
+ type: 'eas_attestation',
96
+ schemaId: '0xf8b05c79f090979bf4a80270aba232dff11a10d9ca55c4f88de95317970f0de9',
97
+ attester: '0x357458739F90461b99789350868CD7CF330Dd7EE',
98
+ indexer: '0x2c7eE1E5f416dfF40054c27A62f7B357C4E8619C',
99
+ chainId: 8453,
100
+ label: 'Custom EAS attestation',
101
+ }
102
+ ```
103
+
104
+ ### Farcaster ID
105
+
106
+ ```ts
107
+ {
108
+ type: 'farcaster_id',
109
+ label: 'Has a Farcaster account',
110
+ }
111
+ ```
112
+
113
+ Always evaluated on Optimism (chain 10). Passes if the wallet has any FID registered.
114
+
115
+ ## API key and credits
116
+
117
+ The key itself is free to create (no credit card, no signup). Each call to `/v1/attest` consumes one or more attestation credits. New keys ship with 10 credits on the free tier, which is enough to wire up the integration end-to-end. Beyond that, credits are purchased on-chain.
118
+
119
+ Create a key:
120
+
121
+ ```bash
122
+ curl -X POST https://api.insumermodel.com/v1/keys/create \
123
+ -H "Content-Type: application/json" \
124
+ -d '{"email":"you@example.com","appName":"my-app","tier":"free"}'
125
+ ```
126
+
127
+ Or set `INSUMER_API_KEY` as an environment variable.
128
+
129
+ Top up credits on-chain via `POST /v1/credits/buy`. Accepted: USDC or USDT on any major EVM chain, USDC on Solana, or BTC on Bitcoin. See the [credits endpoint](https://insumermodel.com/developers/api-reference/) for transaction format.
130
+
131
+ **Pricing model**: the wallet holder pays nothing at the gated route. The operator running the gate pays per attestation call out of the key's credit balance. Cost per attestation depends on tier and condition mix.
132
+
133
+ ## Options
134
+
135
+ | Option | Type | Default | Description |
136
+ |---|---|---|---|
137
+ | `apiKey` | `string` | `env.INSUMER_API_KEY` | InsumerAPI key |
138
+ | `conditions` | `Condition[]` | required | One or more conditions to evaluate |
139
+ | `matchMode` | `'any' \| 'all'` | `'any'` | Wallet must satisfy any or all conditions |
140
+ | `cacheTtlSeconds` | `number` | `300` | In-memory cache TTL |
141
+ | `jwt` | `boolean` | `false` | Request ES256 JWT alongside raw attestation |
142
+ | `apiBaseUrl` | `string` | `https://api.insumermodel.com` | API base URL override |
143
+
144
+ ## Supported chains
145
+
146
+ 30 EVM chains (Ethereum, Base, Polygon, Arbitrum, Optimism, BNB, Avalanche, and 23 more) + Solana + XRPL + Bitcoin. EAS conditions evaluate on EVM chains only. Farcaster always on Optimism.
147
+
148
+ [Full chain list](https://insumermodel.com/developers/api-reference/)
149
+
150
+ ## Distinguishing free vs paid access
151
+
152
+ ```ts
153
+ const receipt = Receipt.fromResponse(response)
154
+ if (receipt.reference.startsWith('condition-gate:free:')) {
155
+ const attestationId = receipt.reference.replace('condition-gate:free:', '')
156
+ // Free access. Attestation ID is retrievable via /v1/attestations/{id}
157
+ } else {
158
+ // Paid access
159
+ }
160
+ ```
161
+
162
+ ## Fail-open behavior
163
+
164
+ If the attestation API is unreachable, the adapter falls through to the original payment method. Wallets that would have qualified for free access pay normally; everyone else is unaffected.
165
+
166
+ ## Migrating from mppx-token-gate
167
+
168
+ `@insumermodel/mppx-token-gate` (v1) is deprecated. Migration to v2:
169
+
170
+ ```diff
171
+ - npm install @insumermodel/mppx-token-gate
172
+ + npm install @insumermodel/mppx-condition-gate
173
+ ```
174
+
175
+ ```diff
176
+ - import { tokenGate } from '@insumermodel/mppx-token-gate'
177
+ + import { conditionGate } from '@insumermodel/mppx-condition-gate'
178
+
179
+ - const gated = tokenGate(server, { ... })
180
+ + const gated = conditionGate(server, { ... })
181
+ ```
182
+
183
+ ```diff
184
+ - if (receipt.reference.startsWith('token-gate:free:'))
185
+ + if (receipt.reference.startsWith('condition-gate:free:'))
186
+ ```
187
+
188
+ The condition shapes for `token_balance` and `nft_ownership` are unchanged. `eas_attestation` and `farcaster_id` are new in v2.
189
+
190
+ The `parsXrplDid` typo from v1 is fixed: use `parseXrplDid`. The cache helper renamed: `clearTokenGateCache` is now `clearConditionGateCache`.
191
+
192
+ ## License
193
+
194
+ MIT
@@ -0,0 +1,151 @@
1
+ import type { Method } from 'mppx';
2
+ export type ChainId = number | 'solana' | 'xrpl' | 'bitcoin';
3
+ /** ERC-20 / SPL / XRPL trust-line / native balance check */
4
+ export type TokenBalanceCondition = {
5
+ type: 'token_balance';
6
+ /** Token contract address. For XRPL native XRP or Bitcoin, use "native". */
7
+ contractAddress: string;
8
+ chainId: ChainId;
9
+ /** Minimum balance. Defaults to 1. */
10
+ threshold?: number;
11
+ /** Token decimals. Auto-detected on most EVM chains if omitted. */
12
+ decimals?: number;
13
+ /** XRPL currency code (e.g. "USD", "RLUSD") for trust-line tokens. */
14
+ currency?: string;
15
+ /** Human-readable label (max 100 chars). */
16
+ label?: string;
17
+ };
18
+ /** ERC-721 / ERC-1155 / Solana cNFT / XRPL NFT ownership check */
19
+ export type NftOwnershipCondition = {
20
+ type: 'nft_ownership';
21
+ contractAddress: string;
22
+ chainId: ChainId;
23
+ /** XRPL NFToken taxon filter. XRPL only. */
24
+ taxon?: number;
25
+ label?: string;
26
+ };
27
+ /** EAS attestation check (Ethereum Attestation Service) */
28
+ export type EasAttestationCondition = {
29
+ type: 'eas_attestation';
30
+ /** EVM chain ID (typically Base 8453). */
31
+ chainId: number;
32
+ /**
33
+ * Pre-configured compliance template. Mutually exclusive with schemaId.
34
+ * When provided, fills in schemaId / attester / indexer automatically.
35
+ * See GET /v1/compliance/templates for the live list.
36
+ */
37
+ template?: 'coinbase_verified_account' | 'coinbase_verified_country' | 'coinbase_one' | 'gitcoin_passport_score' | 'gitcoin_passport_active';
38
+ /** EAS schema ID (bytes32 hex). Required if template is not provided. */
39
+ schemaId?: string;
40
+ /** Expected attester address (case-insensitive). Optional. */
41
+ attester?: string;
42
+ /**
43
+ * EAS indexer contract address. Required for raw (non-template) conditions.
44
+ * The verifier resolves the attestation UID via getAttestationUid(recipient, schema).
45
+ */
46
+ indexer?: string;
47
+ label?: string;
48
+ };
49
+ /** Farcaster ID registration check (always on Optimism, chain 10) */
50
+ export type FarcasterIdCondition = {
51
+ type: 'farcaster_id';
52
+ label?: string;
53
+ };
54
+ /** Any condition accepted by /v1/attest */
55
+ export type Condition = TokenBalanceCondition | NftOwnershipCondition | EasAttestationCondition | FarcasterIdCondition;
56
+ export type ConditionGateOptions = {
57
+ /** InsumerAPI key. Falls back to INSUMER_API_KEY env var. */
58
+ apiKey?: string;
59
+ /** One or more conditions to evaluate. Mix any of the four types. */
60
+ conditions: Condition[];
61
+ /** Whether the wallet must satisfy "any" (default) or "all" conditions. */
62
+ matchMode?: 'any' | 'all';
63
+ /** In-memory cache TTL in seconds. Defaults to 300 (5 minutes). */
64
+ cacheTtlSeconds?: number;
65
+ /**
66
+ * InsumerAPI base URL. Defaults to "https://api.insumermodel.com".
67
+ * Override for self-hosted deployments or testing.
68
+ */
69
+ apiBaseUrl?: string;
70
+ /** Request JWT format alongside the raw attestation. Defaults to false. */
71
+ jwt?: boolean;
72
+ };
73
+ export type InsumerAttestation = {
74
+ ok: boolean;
75
+ data: {
76
+ attestation: {
77
+ id: string;
78
+ pass: boolean;
79
+ results: Array<{
80
+ condition: number;
81
+ label?: string;
82
+ type: string;
83
+ chainId: number | string;
84
+ met: boolean;
85
+ evaluatedCondition: Record<string, unknown>;
86
+ conditionHash: string;
87
+ blockNumber?: string;
88
+ blockTimestamp?: string;
89
+ ledgerIndex?: number;
90
+ ledgerHash?: string;
91
+ }>;
92
+ passCount: number;
93
+ failCount: number;
94
+ attestedAt: string;
95
+ expiresAt: string;
96
+ };
97
+ sig: string;
98
+ kid: string;
99
+ jwt?: string;
100
+ };
101
+ meta: {
102
+ version: string;
103
+ timestamp: string;
104
+ creditsRemaining: number;
105
+ creditsCharged: number;
106
+ };
107
+ };
108
+ /** Clear the in-memory ownership cache. Useful in tests. */
109
+ export declare function clearConditionGateCache(): void;
110
+ /**
111
+ * Extracts an EVM address from a `did:pkh:eip155:{chainId}:{address}` string.
112
+ * Returns null for non-EVM or unparseable DIDs.
113
+ */
114
+ export declare function parseDid(source: string): `0x${string}` | null;
115
+ /** Extracts a Solana address from a `did:pkh:solana:{chainId}:{address}` string. */
116
+ export declare function parseSolanaDid(source: string): string | null;
117
+ /** Extracts an XRPL address from a `did:pkh:xrpl:{chainId}:{address}` string. */
118
+ export declare function parseXrplDid(source: string): string | null;
119
+ /** Extracts a Bitcoin address from a `did:pkh:bip122:{chainId}:{address}` string. */
120
+ export declare function parseBitcoinDid(source: string): string | null;
121
+ /**
122
+ * Wraps an mppx Method.Server with condition-based access using signed attestations.
123
+ *
124
+ * Extracts the payer address from credential.source (DID), calls /v1/attest to
125
+ * evaluate conditions across 33 chains, and returns a free-access receipt for
126
+ * wallets that meet the conditions. Wallets that do not meet them fall through
127
+ * to the original payment method.
128
+ *
129
+ * Supports four condition types: token_balance, nft_ownership, eas_attestation,
130
+ * and farcaster_id. Conditions can be mixed in a single call.
131
+ *
132
+ * The attestation is ECDSA P-256 signed and verifiable offline via the public
133
+ * JWKS at https://insumermodel.com/.well-known/jwks.json.
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * import { conditionGate } from '@insumermodel/mppx-condition-gate'
138
+ *
139
+ * const gated = conditionGate(tempoCharge, {
140
+ * conditions: [
141
+ * { type: 'eas_attestation', template: 'coinbase_verified_account', chainId: 8453 },
142
+ * { type: 'farcaster_id' },
143
+ * ],
144
+ * matchMode: 'any',
145
+ * })
146
+ *
147
+ * const mppx = Mppx.create({ methods: [gated] })
148
+ * ```
149
+ */
150
+ export declare function conditionGate(server: Method.AnyServer, options: ConditionGateOptions): Method.AnyServer;
151
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,270 @@
1
+ const cache = new Map();
2
+ function conditionSortKey(c) {
3
+ if (c.type === 'farcaster_id')
4
+ return 'farcaster_id';
5
+ if (c.type === 'eas_attestation') {
6
+ return `eas:${c.chainId}:${(c.schemaId || c.template || '').toLowerCase()}`;
7
+ }
8
+ return `${c.type}:${c.chainId}:${c.contractAddress.toLowerCase()}`;
9
+ }
10
+ function cacheKey(address, conditions) {
11
+ const sorted = [...conditions].sort((a, b) => {
12
+ const ka = conditionSortKey(a);
13
+ const kb = conditionSortKey(b);
14
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
15
+ });
16
+ return `${address.toLowerCase()}:${JSON.stringify(sorted)}`;
17
+ }
18
+ /** Clear the in-memory ownership cache. Useful in tests. */
19
+ export function clearConditionGateCache() {
20
+ cache.clear();
21
+ }
22
+ // ---------------------------------------------------------------------------
23
+ // DID parsing
24
+ // ---------------------------------------------------------------------------
25
+ /**
26
+ * Extracts an EVM address from a `did:pkh:eip155:{chainId}:{address}` string.
27
+ * Returns null for non-EVM or unparseable DIDs.
28
+ */
29
+ export function parseDid(source) {
30
+ const parts = source.split(':');
31
+ if (parts.length !== 5)
32
+ return null;
33
+ if (parts[0] !== 'did' || parts[1] !== 'pkh' || parts[2] !== 'eip155')
34
+ return null;
35
+ const address = parts[4];
36
+ if (!address || !address.startsWith('0x'))
37
+ return null;
38
+ return address;
39
+ }
40
+ /** Extracts a Solana address from a `did:pkh:solana:{chainId}:{address}` string. */
41
+ export function parseSolanaDid(source) {
42
+ const parts = source.split(':');
43
+ if (parts.length !== 5)
44
+ return null;
45
+ if (parts[0] !== 'did' || parts[1] !== 'pkh' || parts[2] !== 'solana')
46
+ return null;
47
+ return parts[4] || null;
48
+ }
49
+ /** Extracts an XRPL address from a `did:pkh:xrpl:{chainId}:{address}` string. */
50
+ export function parseXrplDid(source) {
51
+ const parts = source.split(':');
52
+ if (parts.length !== 5)
53
+ return null;
54
+ if (parts[0] !== 'did' || parts[1] !== 'pkh' || parts[2] !== 'xrpl')
55
+ return null;
56
+ const address = parts[4];
57
+ if (!address || !address.startsWith('r'))
58
+ return null;
59
+ return address;
60
+ }
61
+ /** Extracts a Bitcoin address from a `did:pkh:bip122:{chainId}:{address}` string. */
62
+ export function parseBitcoinDid(source) {
63
+ const parts = source.split(':');
64
+ if (parts.length !== 5)
65
+ return null;
66
+ if (parts[0] !== 'did' || parts[1] !== 'pkh' || parts[2] !== 'bip122')
67
+ return null;
68
+ return parts[4] || null;
69
+ }
70
+ // ---------------------------------------------------------------------------
71
+ // InsumerAPI call
72
+ // ---------------------------------------------------------------------------
73
+ function buildBodyConditions(conditions) {
74
+ return conditions.map((c) => {
75
+ if (c.type === 'farcaster_id') {
76
+ const cond = { type: c.type };
77
+ if (c.label)
78
+ cond.label = c.label;
79
+ return cond;
80
+ }
81
+ if (c.type === 'eas_attestation') {
82
+ const cond = {
83
+ type: c.type,
84
+ chainId: c.chainId,
85
+ };
86
+ if (c.template)
87
+ cond.template = c.template;
88
+ if (c.schemaId)
89
+ cond.schemaId = c.schemaId;
90
+ if (c.attester)
91
+ cond.attester = c.attester;
92
+ if (c.indexer)
93
+ cond.indexer = c.indexer;
94
+ if (c.label)
95
+ cond.label = c.label;
96
+ return cond;
97
+ }
98
+ // token_balance or nft_ownership
99
+ const cond = {
100
+ type: c.type,
101
+ contractAddress: c.contractAddress,
102
+ chainId: c.chainId,
103
+ };
104
+ if (c.type === 'token_balance') {
105
+ cond.threshold = c.threshold ?? 1;
106
+ if (c.decimals !== undefined)
107
+ cond.decimals = c.decimals;
108
+ if (c.currency)
109
+ cond.currency = c.currency;
110
+ }
111
+ if (c.type === 'nft_ownership' && c.taxon !== undefined) {
112
+ cond.taxon = c.taxon;
113
+ }
114
+ if (c.label)
115
+ cond.label = c.label;
116
+ return cond;
117
+ });
118
+ }
119
+ async function callAttest(wallet, walletType, conditions, options) {
120
+ const apiKey = options.apiKey || process.env.INSUMER_API_KEY;
121
+ if (!apiKey) {
122
+ throw new Error('mppx-condition-gate: Missing API key. Pass apiKey in options or set INSUMER_API_KEY env var. ' +
123
+ 'Get a free key: POST https://api.insumermodel.com/v1/keys/create');
124
+ }
125
+ const baseUrl = options.apiBaseUrl || 'https://api.insumermodel.com';
126
+ const body = {
127
+ conditions: buildBodyConditions(conditions),
128
+ };
129
+ if (walletType === 'solana')
130
+ body.solanaWallet = wallet;
131
+ else if (walletType === 'xrpl')
132
+ body.xrplWallet = wallet;
133
+ else if (walletType === 'bitcoin')
134
+ body.bitcoinWallet = wallet;
135
+ else
136
+ body.wallet = wallet;
137
+ if (options.jwt)
138
+ body.format = 'jwt';
139
+ const response = await fetch(`${baseUrl}/v1/attest`, {
140
+ method: 'POST',
141
+ headers: {
142
+ 'Content-Type': 'application/json',
143
+ 'x-api-key': apiKey,
144
+ },
145
+ body: JSON.stringify(body),
146
+ });
147
+ const data = await response.json();
148
+ if (!response.ok || !data.ok) {
149
+ const msg = data?.error?.message || `HTTP ${response.status}`;
150
+ throw new Error(`mppx-condition-gate: Attestation failed: ${msg}`);
151
+ }
152
+ return data;
153
+ }
154
+ // ---------------------------------------------------------------------------
155
+ // conditionGate adapter
156
+ // ---------------------------------------------------------------------------
157
+ /**
158
+ * Wraps an mppx Method.Server with condition-based access using signed attestations.
159
+ *
160
+ * Extracts the payer address from credential.source (DID), calls /v1/attest to
161
+ * evaluate conditions across 33 chains, and returns a free-access receipt for
162
+ * wallets that meet the conditions. Wallets that do not meet them fall through
163
+ * to the original payment method.
164
+ *
165
+ * Supports four condition types: token_balance, nft_ownership, eas_attestation,
166
+ * and farcaster_id. Conditions can be mixed in a single call.
167
+ *
168
+ * The attestation is ECDSA P-256 signed and verifiable offline via the public
169
+ * JWKS at https://insumermodel.com/.well-known/jwks.json.
170
+ *
171
+ * @example
172
+ * ```ts
173
+ * import { conditionGate } from '@insumermodel/mppx-condition-gate'
174
+ *
175
+ * const gated = conditionGate(tempoCharge, {
176
+ * conditions: [
177
+ * { type: 'eas_attestation', template: 'coinbase_verified_account', chainId: 8453 },
178
+ * { type: 'farcaster_id' },
179
+ * ],
180
+ * matchMode: 'any',
181
+ * })
182
+ *
183
+ * const mppx = Mppx.create({ methods: [gated] })
184
+ * ```
185
+ */
186
+ export function conditionGate(server, options) {
187
+ const { conditions, matchMode = 'any', cacheTtlSeconds = 300 } = options;
188
+ const originalVerify = server.verify;
189
+ const gatedVerify = async (params) => {
190
+ const credential = params.credential;
191
+ const source = credential.source;
192
+ // No DID → fall through to payment
193
+ if (!source)
194
+ return originalVerify(params);
195
+ // Determine wallet type and address
196
+ let wallet = null;
197
+ let walletType = 'evm';
198
+ wallet = parseDid(source);
199
+ if (!wallet) {
200
+ wallet = parseSolanaDid(source);
201
+ if (wallet)
202
+ walletType = 'solana';
203
+ }
204
+ if (!wallet) {
205
+ wallet = parseXrplDid(source);
206
+ if (wallet)
207
+ walletType = 'xrpl';
208
+ }
209
+ if (!wallet) {
210
+ wallet = parseBitcoinDid(source);
211
+ if (wallet)
212
+ walletType = 'bitcoin';
213
+ }
214
+ // Unparseable DID → fall through to payment
215
+ if (!wallet)
216
+ return originalVerify(params);
217
+ // Check cache
218
+ const key = cacheKey(wallet, conditions);
219
+ const cached = cache.get(key);
220
+ if (cached && cached.expiresAt > Date.now()) {
221
+ if (cached.pass) {
222
+ return {
223
+ method: server.name,
224
+ reference: `condition-gate:free:${cached.attestationId}`,
225
+ status: 'success',
226
+ timestamp: new Date().toISOString(),
227
+ };
228
+ }
229
+ // Cached non-holder → fall through
230
+ return originalVerify(params);
231
+ }
232
+ // Call InsumerAPI
233
+ try {
234
+ const result = await callAttest(wallet, walletType, conditions, options);
235
+ const attestation = result.data.attestation;
236
+ // Determine pass based on matchMode
237
+ let pass;
238
+ if (matchMode === 'all') {
239
+ pass = attestation.pass; // all conditions must be met
240
+ }
241
+ else {
242
+ // "any" — at least one condition met
243
+ pass = attestation.results.some((r) => r.met);
244
+ }
245
+ // Cache the result
246
+ cache.set(key, {
247
+ pass,
248
+ attestationId: attestation.id,
249
+ expiresAt: Date.now() + cacheTtlSeconds * 1000,
250
+ });
251
+ if (pass) {
252
+ return {
253
+ method: server.name,
254
+ reference: `condition-gate:free:${attestation.id}`,
255
+ status: 'success',
256
+ timestamp: new Date().toISOString(),
257
+ };
258
+ }
259
+ }
260
+ catch {
261
+ // Attestation API error → fall through to payment (fail open)
262
+ }
263
+ return originalVerify(params);
264
+ };
265
+ return {
266
+ ...server,
267
+ verify: gatedVerify,
268
+ };
269
+ }
270
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA2IA,MAAM,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAA;AAE3C,SAAS,gBAAgB,CAAC,CAAY;IACpC,IAAI,CAAC,CAAC,IAAI,KAAK,cAAc;QAAE,OAAO,cAAc,CAAA;IACpD,IAAI,CAAC,CAAC,IAAI,KAAK,iBAAiB,EAAE,CAAC;QACjC,OAAO,OAAO,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,EAAE,CAAA;IAC7E,CAAC;IACD,OAAO,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,eAAe,CAAC,WAAW,EAAE,EAAE,CAAA;AACpE,CAAC;AAED,SAAS,QAAQ,CAAC,OAAe,EAAE,UAAuB;IACxD,MAAM,MAAM,GAAG,CAAC,GAAG,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QAC3C,MAAM,EAAE,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAA;QAC9B,MAAM,EAAE,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAA;QAC9B,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACvC,CAAC,CAAC,CAAA;IACF,OAAO,GAAG,OAAO,CAAC,WAAW,EAAE,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAA;AAC7D,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,uBAAuB;IACrC,KAAK,CAAC,KAAK,EAAE,CAAA;AACf,CAAC;AAED,8EAA8E;AAC9E,cAAc;AACd,8EAA8E;AAE9E;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,MAAc;IACrC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACnC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IAClF,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;IACxB,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IACtD,OAAO,OAAwB,CAAA;AACjC,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACnC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IAClF,OAAO,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAA;AACzB,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACnC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,MAAM;QAAE,OAAO,IAAI,CAAA;IAChF,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;IACxB,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA;IACrD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACnC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IAClF,OAAO,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAA;AACzB,CAAC;AAED,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAE9E,SAAS,mBAAmB,CAAC,UAAuB;IAClD,OAAO,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAC1B,IAAI,CAAC,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;YAC9B,MAAM,IAAI,GAA4B,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;YACtD,IAAI,CAAC,CAAC,KAAK;gBAAE,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAA;YACjC,OAAO,IAAI,CAAA;QACb,CAAC;QACD,IAAI,CAAC,CAAC,IAAI,KAAK,iBAAiB,EAAE,CAAC;YACjC,MAAM,IAAI,GAA4B;gBACpC,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,OAAO,EAAE,CAAC,CAAC,OAAO;aACnB,CAAA;YACD,IAAI,CAAC,CAAC,QAAQ;gBAAE,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAA;YAC1C,IAAI,CAAC,CAAC,QAAQ;gBAAE,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAA;YAC1C,IAAI,CAAC,CAAC,QAAQ;gBAAE,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAA;YAC1C,IAAI,CAAC,CAAC,OAAO;gBAAE,IAAI,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAA;YACvC,IAAI,CAAC,CAAC,KAAK;gBAAE,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAA;YACjC,OAAO,IAAI,CAAA;QACb,CAAC;QACD,iCAAiC;QACjC,MAAM,IAAI,GAA4B;YACpC,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,eAAe,EAAE,CAAC,CAAC,eAAe;YAClC,OAAO,EAAE,CAAC,CAAC,OAAO;SACnB,CAAA;QACD,IAAI,CAAC,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;YAC/B,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,IAAI,CAAC,CAAA;YACjC,IAAI,CAAC,CAAC,QAAQ,KAAK,SAAS;gBAAE,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAA;YACxD,IAAI,CAAC,CAAC,QAAQ;gBAAE,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAA;QAC5C,CAAC;QACD,IAAI,CAAC,CAAC,IAAI,KAAK,eAAe,IAAI,CAAC,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YACxD,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAA;QACtB,CAAC;QACD,IAAI,CAAC,CAAC,KAAK;YAAE,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAA;QACjC,OAAO,IAAI,CAAA;IACb,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,KAAK,UAAU,UAAU,CACvB,MAAc,EACd,UAAiD,EACjD,UAAuB,EACvB,OAAoE;IAEpE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,eAAe,CAAA;IAC5D,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACb,+FAA+F;YAC/F,kEAAkE,CACnE,CAAA;IACH,CAAC;IAED,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,IAAI,8BAA8B,CAAA;IAEpE,MAAM,IAAI,GAA4B;QACpC,UAAU,EAAE,mBAAmB,CAAC,UAAU,CAAC;KAC5C,CAAA;IAED,IAAI,UAAU,KAAK,QAAQ;QAAE,IAAI,CAAC,YAAY,GAAG,MAAM,CAAA;SAClD,IAAI,UAAU,KAAK,MAAM;QAAE,IAAI,CAAC,UAAU,GAAG,MAAM,CAAA;SACnD,IAAI,UAAU,KAAK,SAAS;QAAE,IAAI,CAAC,aAAa,GAAG,MAAM,CAAA;;QACzD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;IAEzB,IAAI,OAAO,CAAC,GAAG;QAAE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAA;IAEpC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,OAAO,YAAY,EAAE;QACnD,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,cAAc,EAAE,kBAAkB;YAClC,WAAW,EAAE,MAAM;SACpB;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;KAC3B,CAAC,CAAA;IAEF,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAwB,CAAA;IACxD,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAI,IAAY,EAAE,KAAK,EAAE,OAAO,IAAI,QAAQ,QAAQ,CAAC,MAAM,EAAE,CAAA;QACtE,MAAM,IAAI,KAAK,CAAC,4CAA4C,GAAG,EAAE,CAAC,CAAA;IACpE,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,8EAA8E;AAC9E,wBAAwB;AACxB,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,aAAa,CAC3B,MAAwB,EACxB,OAA6B;IAE7B,MAAM,EAAE,UAAU,EAAE,SAAS,GAAG,KAAK,EAAE,eAAe,GAAG,GAAG,EAAE,GAAG,OAAO,CAAA;IAExE,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,CAAA;IAEpC,MAAM,WAAW,GAA0B,KAAK,EAAE,MAAW,EAAE,EAAE;QAC/D,MAAM,UAAU,GAAG,MAAM,CAAC,UAAiC,CAAA;QAC3D,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAA;QAEhC,mCAAmC;QACnC,IAAI,CAAC,MAAM;YAAE,OAAO,cAAc,CAAC,MAAM,CAAC,CAAA;QAE1C,oCAAoC;QACpC,IAAI,MAAM,GAAkB,IAAI,CAAA;QAChC,IAAI,UAAU,GAA0C,KAAK,CAAA;QAE7D,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAA;QACzB,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,CAAA;YAC/B,IAAI,MAAM;gBAAE,UAAU,GAAG,QAAQ,CAAA;QACnC,CAAC;QACD,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAAA;YAC7B,IAAI,MAAM;gBAAE,UAAU,GAAG,MAAM,CAAA;QACjC,CAAC;QACD,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,eAAe,CAAC,MAAM,CAAC,CAAA;YAChC,IAAI,MAAM;gBAAE,UAAU,GAAG,SAAS,CAAA;QACpC,CAAC;QAED,4CAA4C;QAC5C,IAAI,CAAC,MAAM;YAAE,OAAO,cAAc,CAAC,MAAM,CAAC,CAAA;QAE1C,cAAc;QACd,MAAM,GAAG,GAAG,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;QACxC,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC7B,IAAI,MAAM,IAAI,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YAC5C,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;gBAChB,OAAO;oBACL,MAAM,EAAE,MAAM,CAAC,IAAI;oBACnB,SAAS,EAAE,uBAAuB,MAAM,CAAC,aAAa,EAAE;oBACxD,MAAM,EAAE,SAAkB;oBAC1B,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;iBACpC,CAAA;YACH,CAAC;YACD,mCAAmC;YACnC,OAAO,cAAc,CAAC,MAAM,CAAC,CAAA;QAC/B,CAAC;QAED,kBAAkB;QAClB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,CAAC,CAAA;YACxE,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAA;YAE3C,oCAAoC;YACpC,IAAI,IAAa,CAAA;YACjB,IAAI,SAAS,KAAK,KAAK,EAAE,CAAC;gBACxB,IAAI,GAAG,WAAW,CAAC,IAAI,CAAA,CAAC,6BAA6B;YACvD,CAAC;iBAAM,CAAC;gBACN,qCAAqC;gBACrC,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;YAC/C,CAAC;YAED,mBAAmB;YACnB,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE;gBACb,IAAI;gBACJ,aAAa,EAAE,WAAW,CAAC,EAAE;gBAC7B,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,eAAe,GAAG,IAAI;aAC/C,CAAC,CAAA;YAEF,IAAI,IAAI,EAAE,CAAC;gBACT,OAAO;oBACL,MAAM,EAAE,MAAM,CAAC,IAAI;oBACnB,SAAS,EAAE,uBAAuB,WAAW,CAAC,EAAE,EAAE;oBAClD,MAAM,EAAE,SAAkB;oBAC1B,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;iBACpC,CAAA;YACH,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,8DAA8D;QAChE,CAAC;QAED,OAAO,cAAc,CAAC,MAAM,CAAC,CAAA;IAC/B,CAAC,CAAA;IAED,OAAO;QACL,GAAG,MAAM;QACT,MAAM,EAAE,WAAW;KACpB,CAAA;AACH,CAAC"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@insumermodel/mppx-condition-gate",
3
+ "version": "2.0.0",
4
+ "description": "Condition-based access for mppx routes. Wallet auth via signed attestations across 33 chains. Four condition types: token balance, NFT ownership, EAS attestation, Farcaster ID.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "keywords": [
23
+ "condition-based-access",
24
+ "condition-gate",
25
+ "mppx",
26
+ "mpp",
27
+ "wallet-auth",
28
+ "wallet",
29
+ "attestation",
30
+ "eas",
31
+ "farcaster",
32
+ "token-gate",
33
+ "nft",
34
+ "erc20",
35
+ "erc721",
36
+ "solana",
37
+ "xrpl",
38
+ "bitcoin",
39
+ "web3"
40
+ ],
41
+ "author": "douglasborthwick-crypto",
42
+ "license": "MIT",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/douglasborthwick-crypto/mppx-condition-gate.git"
46
+ },
47
+ "peerDependencies": {
48
+ "mppx": ">=0.1.0"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "mppx": {
52
+ "optional": true
53
+ }
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^25.5.0",
57
+ "mppx": "latest",
58
+ "typescript": "^5.7.0"
59
+ }
60
+ }
package/src/index.ts ADDED
@@ -0,0 +1,418 @@
1
+ import type { Method, Receipt } from 'mppx'
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Types
5
+ // ---------------------------------------------------------------------------
6
+
7
+ export type ChainId = number | 'solana' | 'xrpl' | 'bitcoin'
8
+
9
+ /** ERC-20 / SPL / XRPL trust-line / native balance check */
10
+ export type TokenBalanceCondition = {
11
+ type: 'token_balance'
12
+ /** Token contract address. For XRPL native XRP or Bitcoin, use "native". */
13
+ contractAddress: string
14
+ chainId: ChainId
15
+ /** Minimum balance. Defaults to 1. */
16
+ threshold?: number
17
+ /** Token decimals. Auto-detected on most EVM chains if omitted. */
18
+ decimals?: number
19
+ /** XRPL currency code (e.g. "USD", "RLUSD") for trust-line tokens. */
20
+ currency?: string
21
+ /** Human-readable label (max 100 chars). */
22
+ label?: string
23
+ }
24
+
25
+ /** ERC-721 / ERC-1155 / Solana cNFT / XRPL NFT ownership check */
26
+ export type NftOwnershipCondition = {
27
+ type: 'nft_ownership'
28
+ contractAddress: string
29
+ chainId: ChainId
30
+ /** XRPL NFToken taxon filter. XRPL only. */
31
+ taxon?: number
32
+ label?: string
33
+ }
34
+
35
+ /** EAS attestation check (Ethereum Attestation Service) */
36
+ export type EasAttestationCondition = {
37
+ type: 'eas_attestation'
38
+ /** EVM chain ID (typically Base 8453). */
39
+ chainId: number
40
+ /**
41
+ * Pre-configured compliance template. Mutually exclusive with schemaId.
42
+ * When provided, fills in schemaId / attester / indexer automatically.
43
+ * See GET /v1/compliance/templates for the live list.
44
+ */
45
+ template?:
46
+ | 'coinbase_verified_account'
47
+ | 'coinbase_verified_country'
48
+ | 'coinbase_one'
49
+ | 'gitcoin_passport_score'
50
+ | 'gitcoin_passport_active'
51
+ /** EAS schema ID (bytes32 hex). Required if template is not provided. */
52
+ schemaId?: string
53
+ /** Expected attester address (case-insensitive). Optional. */
54
+ attester?: string
55
+ /**
56
+ * EAS indexer contract address. Required for raw (non-template) conditions.
57
+ * The verifier resolves the attestation UID via getAttestationUid(recipient, schema).
58
+ */
59
+ indexer?: string
60
+ label?: string
61
+ }
62
+
63
+ /** Farcaster ID registration check (always on Optimism, chain 10) */
64
+ export type FarcasterIdCondition = {
65
+ type: 'farcaster_id'
66
+ label?: string
67
+ }
68
+
69
+ /** Any condition accepted by /v1/attest */
70
+ export type Condition =
71
+ | TokenBalanceCondition
72
+ | NftOwnershipCondition
73
+ | EasAttestationCondition
74
+ | FarcasterIdCondition
75
+
76
+ export type ConditionGateOptions = {
77
+ /** InsumerAPI key. Falls back to INSUMER_API_KEY env var. */
78
+ apiKey?: string
79
+ /** One or more conditions to evaluate. Mix any of the four types. */
80
+ conditions: Condition[]
81
+ /** Whether the wallet must satisfy "any" (default) or "all" conditions. */
82
+ matchMode?: 'any' | 'all'
83
+ /** In-memory cache TTL in seconds. Defaults to 300 (5 minutes). */
84
+ cacheTtlSeconds?: number
85
+ /**
86
+ * InsumerAPI base URL. Defaults to "https://api.insumermodel.com".
87
+ * Override for self-hosted deployments or testing.
88
+ */
89
+ apiBaseUrl?: string
90
+ /** Request JWT format alongside the raw attestation. Defaults to false. */
91
+ jwt?: boolean
92
+ }
93
+
94
+ export type InsumerAttestation = {
95
+ ok: boolean
96
+ data: {
97
+ attestation: {
98
+ id: string
99
+ pass: boolean
100
+ results: Array<{
101
+ condition: number
102
+ label?: string
103
+ type: string
104
+ chainId: number | string
105
+ met: boolean
106
+ evaluatedCondition: Record<string, unknown>
107
+ conditionHash: string
108
+ blockNumber?: string
109
+ blockTimestamp?: string
110
+ ledgerIndex?: number
111
+ ledgerHash?: string
112
+ }>
113
+ passCount: number
114
+ failCount: number
115
+ attestedAt: string
116
+ expiresAt: string
117
+ }
118
+ sig: string
119
+ kid: string
120
+ jwt?: string
121
+ }
122
+ meta: {
123
+ version: string
124
+ timestamp: string
125
+ creditsRemaining: number
126
+ creditsCharged: number
127
+ }
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Cache
132
+ // ---------------------------------------------------------------------------
133
+
134
+ type CacheEntry = {
135
+ pass: boolean
136
+ attestationId: string
137
+ expiresAt: number
138
+ }
139
+
140
+ const cache = new Map<string, CacheEntry>()
141
+
142
+ function conditionSortKey(c: Condition): string {
143
+ if (c.type === 'farcaster_id') return 'farcaster_id'
144
+ if (c.type === 'eas_attestation') {
145
+ return `eas:${c.chainId}:${(c.schemaId || c.template || '').toLowerCase()}`
146
+ }
147
+ return `${c.type}:${c.chainId}:${c.contractAddress.toLowerCase()}`
148
+ }
149
+
150
+ function cacheKey(address: string, conditions: Condition[]): string {
151
+ const sorted = [...conditions].sort((a, b) => {
152
+ const ka = conditionSortKey(a)
153
+ const kb = conditionSortKey(b)
154
+ return ka < kb ? -1 : ka > kb ? 1 : 0
155
+ })
156
+ return `${address.toLowerCase()}:${JSON.stringify(sorted)}`
157
+ }
158
+
159
+ /** Clear the in-memory ownership cache. Useful in tests. */
160
+ export function clearConditionGateCache(): void {
161
+ cache.clear()
162
+ }
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // DID parsing
166
+ // ---------------------------------------------------------------------------
167
+
168
+ /**
169
+ * Extracts an EVM address from a `did:pkh:eip155:{chainId}:{address}` string.
170
+ * Returns null for non-EVM or unparseable DIDs.
171
+ */
172
+ export function parseDid(source: string): `0x${string}` | null {
173
+ const parts = source.split(':')
174
+ if (parts.length !== 5) return null
175
+ if (parts[0] !== 'did' || parts[1] !== 'pkh' || parts[2] !== 'eip155') return null
176
+ const address = parts[4]
177
+ if (!address || !address.startsWith('0x')) return null
178
+ return address as `0x${string}`
179
+ }
180
+
181
+ /** Extracts a Solana address from a `did:pkh:solana:{chainId}:{address}` string. */
182
+ export function parseSolanaDid(source: string): string | null {
183
+ const parts = source.split(':')
184
+ if (parts.length !== 5) return null
185
+ if (parts[0] !== 'did' || parts[1] !== 'pkh' || parts[2] !== 'solana') return null
186
+ return parts[4] || null
187
+ }
188
+
189
+ /** Extracts an XRPL address from a `did:pkh:xrpl:{chainId}:{address}` string. */
190
+ export function parseXrplDid(source: string): string | null {
191
+ const parts = source.split(':')
192
+ if (parts.length !== 5) return null
193
+ if (parts[0] !== 'did' || parts[1] !== 'pkh' || parts[2] !== 'xrpl') return null
194
+ const address = parts[4]
195
+ if (!address || !address.startsWith('r')) return null
196
+ return address
197
+ }
198
+
199
+ /** Extracts a Bitcoin address from a `did:pkh:bip122:{chainId}:{address}` string. */
200
+ export function parseBitcoinDid(source: string): string | null {
201
+ const parts = source.split(':')
202
+ if (parts.length !== 5) return null
203
+ if (parts[0] !== 'did' || parts[1] !== 'pkh' || parts[2] !== 'bip122') return null
204
+ return parts[4] || null
205
+ }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // InsumerAPI call
209
+ // ---------------------------------------------------------------------------
210
+
211
+ function buildBodyConditions(conditions: Condition[]): Array<Record<string, unknown>> {
212
+ return conditions.map((c) => {
213
+ if (c.type === 'farcaster_id') {
214
+ const cond: Record<string, unknown> = { type: c.type }
215
+ if (c.label) cond.label = c.label
216
+ return cond
217
+ }
218
+ if (c.type === 'eas_attestation') {
219
+ const cond: Record<string, unknown> = {
220
+ type: c.type,
221
+ chainId: c.chainId,
222
+ }
223
+ if (c.template) cond.template = c.template
224
+ if (c.schemaId) cond.schemaId = c.schemaId
225
+ if (c.attester) cond.attester = c.attester
226
+ if (c.indexer) cond.indexer = c.indexer
227
+ if (c.label) cond.label = c.label
228
+ return cond
229
+ }
230
+ // token_balance or nft_ownership
231
+ const cond: Record<string, unknown> = {
232
+ type: c.type,
233
+ contractAddress: c.contractAddress,
234
+ chainId: c.chainId,
235
+ }
236
+ if (c.type === 'token_balance') {
237
+ cond.threshold = c.threshold ?? 1
238
+ if (c.decimals !== undefined) cond.decimals = c.decimals
239
+ if (c.currency) cond.currency = c.currency
240
+ }
241
+ if (c.type === 'nft_ownership' && c.taxon !== undefined) {
242
+ cond.taxon = c.taxon
243
+ }
244
+ if (c.label) cond.label = c.label
245
+ return cond
246
+ })
247
+ }
248
+
249
+ async function callAttest(
250
+ wallet: string,
251
+ walletType: 'evm' | 'solana' | 'xrpl' | 'bitcoin',
252
+ conditions: Condition[],
253
+ options: Pick<ConditionGateOptions, 'apiKey' | 'apiBaseUrl' | 'jwt'>,
254
+ ): Promise<InsumerAttestation> {
255
+ const apiKey = options.apiKey || process.env.INSUMER_API_KEY
256
+ if (!apiKey) {
257
+ throw new Error(
258
+ 'mppx-condition-gate: Missing API key. Pass apiKey in options or set INSUMER_API_KEY env var. ' +
259
+ 'Get a free key: POST https://api.insumermodel.com/v1/keys/create',
260
+ )
261
+ }
262
+
263
+ const baseUrl = options.apiBaseUrl || 'https://api.insumermodel.com'
264
+
265
+ const body: Record<string, unknown> = {
266
+ conditions: buildBodyConditions(conditions),
267
+ }
268
+
269
+ if (walletType === 'solana') body.solanaWallet = wallet
270
+ else if (walletType === 'xrpl') body.xrplWallet = wallet
271
+ else if (walletType === 'bitcoin') body.bitcoinWallet = wallet
272
+ else body.wallet = wallet
273
+
274
+ if (options.jwt) body.format = 'jwt'
275
+
276
+ const response = await fetch(`${baseUrl}/v1/attest`, {
277
+ method: 'POST',
278
+ headers: {
279
+ 'Content-Type': 'application/json',
280
+ 'x-api-key': apiKey,
281
+ },
282
+ body: JSON.stringify(body),
283
+ })
284
+
285
+ const data = await response.json() as InsumerAttestation
286
+ if (!response.ok || !data.ok) {
287
+ const msg = (data as any)?.error?.message || `HTTP ${response.status}`
288
+ throw new Error(`mppx-condition-gate: Attestation failed: ${msg}`)
289
+ }
290
+ return data
291
+ }
292
+
293
+ // ---------------------------------------------------------------------------
294
+ // conditionGate adapter
295
+ // ---------------------------------------------------------------------------
296
+
297
+ /**
298
+ * Wraps an mppx Method.Server with condition-based access using signed attestations.
299
+ *
300
+ * Extracts the payer address from credential.source (DID), calls /v1/attest to
301
+ * evaluate conditions across 33 chains, and returns a free-access receipt for
302
+ * wallets that meet the conditions. Wallets that do not meet them fall through
303
+ * to the original payment method.
304
+ *
305
+ * Supports four condition types: token_balance, nft_ownership, eas_attestation,
306
+ * and farcaster_id. Conditions can be mixed in a single call.
307
+ *
308
+ * The attestation is ECDSA P-256 signed and verifiable offline via the public
309
+ * JWKS at https://insumermodel.com/.well-known/jwks.json.
310
+ *
311
+ * @example
312
+ * ```ts
313
+ * import { conditionGate } from '@insumermodel/mppx-condition-gate'
314
+ *
315
+ * const gated = conditionGate(tempoCharge, {
316
+ * conditions: [
317
+ * { type: 'eas_attestation', template: 'coinbase_verified_account', chainId: 8453 },
318
+ * { type: 'farcaster_id' },
319
+ * ],
320
+ * matchMode: 'any',
321
+ * })
322
+ *
323
+ * const mppx = Mppx.create({ methods: [gated] })
324
+ * ```
325
+ */
326
+ export function conditionGate(
327
+ server: Method.AnyServer,
328
+ options: ConditionGateOptions,
329
+ ): Method.AnyServer {
330
+ const { conditions, matchMode = 'any', cacheTtlSeconds = 300 } = options
331
+
332
+ const originalVerify = server.verify
333
+
334
+ const gatedVerify: typeof originalVerify = async (params: any) => {
335
+ const credential = params.credential as { source?: string }
336
+ const source = credential.source
337
+
338
+ // No DID → fall through to payment
339
+ if (!source) return originalVerify(params)
340
+
341
+ // Determine wallet type and address
342
+ let wallet: string | null = null
343
+ let walletType: 'evm' | 'solana' | 'xrpl' | 'bitcoin' = 'evm'
344
+
345
+ wallet = parseDid(source)
346
+ if (!wallet) {
347
+ wallet = parseSolanaDid(source)
348
+ if (wallet) walletType = 'solana'
349
+ }
350
+ if (!wallet) {
351
+ wallet = parseXrplDid(source)
352
+ if (wallet) walletType = 'xrpl'
353
+ }
354
+ if (!wallet) {
355
+ wallet = parseBitcoinDid(source)
356
+ if (wallet) walletType = 'bitcoin'
357
+ }
358
+
359
+ // Unparseable DID → fall through to payment
360
+ if (!wallet) return originalVerify(params)
361
+
362
+ // Check cache
363
+ const key = cacheKey(wallet, conditions)
364
+ const cached = cache.get(key)
365
+ if (cached && cached.expiresAt > Date.now()) {
366
+ if (cached.pass) {
367
+ return {
368
+ method: server.name,
369
+ reference: `condition-gate:free:${cached.attestationId}`,
370
+ status: 'success' as const,
371
+ timestamp: new Date().toISOString(),
372
+ }
373
+ }
374
+ // Cached non-holder → fall through
375
+ return originalVerify(params)
376
+ }
377
+
378
+ // Call InsumerAPI
379
+ try {
380
+ const result = await callAttest(wallet, walletType, conditions, options)
381
+ const attestation = result.data.attestation
382
+
383
+ // Determine pass based on matchMode
384
+ let pass: boolean
385
+ if (matchMode === 'all') {
386
+ pass = attestation.pass // all conditions must be met
387
+ } else {
388
+ // "any" — at least one condition met
389
+ pass = attestation.results.some((r) => r.met)
390
+ }
391
+
392
+ // Cache the result
393
+ cache.set(key, {
394
+ pass,
395
+ attestationId: attestation.id,
396
+ expiresAt: Date.now() + cacheTtlSeconds * 1000,
397
+ })
398
+
399
+ if (pass) {
400
+ return {
401
+ method: server.name,
402
+ reference: `condition-gate:free:${attestation.id}`,
403
+ status: 'success' as const,
404
+ timestamp: new Date().toISOString(),
405
+ }
406
+ }
407
+ } catch {
408
+ // Attestation API error → fall through to payment (fail open)
409
+ }
410
+
411
+ return originalVerify(params)
412
+ }
413
+
414
+ return {
415
+ ...server,
416
+ verify: gatedVerify,
417
+ }
418
+ }