@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 +194 -0
- package/dist/index.d.ts +151 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +270 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
- package/src/index.ts +418 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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":"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"}
|
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
|
+
}
|