@meddleware/nft-gate-gateway 0.0.1
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/LICENSE +14 -0
- package/README.md +205 -0
- package/package.json +35 -0
- package/src/chain.ts +260 -0
- package/src/config.ts +144 -0
- package/src/crypto.ts +78 -0
- package/src/index.ts +173 -0
- package/src/proxy.ts +81 -0
- package/src/quota.ts +116 -0
- package/src/state/durable_object.ts +139 -0
- package/src/state/kv.ts +81 -0
- package/src/state/select.ts +35 -0
- package/src/state/types.ts +49 -0
- package/src/verify.ts +216 -0
- package/src/wire.ts +20 -0
- package/tsconfig.json +19 -0
- package/wrangler.toml +83 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend selection — the Workers analog of the Rust gateway's `main.rs` store bootstrap
|
|
3
|
+
* (`REDIS_URL ? redis : in_memory`). Chooses the Durable Object or KV backend from config +
|
|
4
|
+
* available bindings, failing fast if the chosen backend's binding is missing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Config, Env } from '../config.js'
|
|
8
|
+
import type { NonceBackend } from './types.js'
|
|
9
|
+
import type { NonceRateState } from './durable_object.js'
|
|
10
|
+
import { DurableObjectBackend } from './durable_object.js'
|
|
11
|
+
import { KvBackend } from './kv.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Instantiate the appropriate {@link NonceBackend} from config and available bindings.
|
|
15
|
+
* Mirrors the Rust gateway's startup-time store selection (`REDIS_URL ? redis : in_memory`).
|
|
16
|
+
*
|
|
17
|
+
* @param cfg - The resolved gateway config, potentially modified by the quota-degrade flag.
|
|
18
|
+
* @param env - Worker environment bindings.
|
|
19
|
+
* @returns A configured `NonceBackend` instance.
|
|
20
|
+
* @throws If the required binding for the chosen backend is absent.
|
|
21
|
+
*/
|
|
22
|
+
export function makeBackend(cfg: Config, env: Env): NonceBackend {
|
|
23
|
+
if (cfg.nonceBackend === 'kv') {
|
|
24
|
+
if (!env.NONCE_KV) throw new Error('NONCE_BACKEND=kv but the NONCE_KV binding is missing')
|
|
25
|
+
return new KvBackend(env.NONCE_KV)
|
|
26
|
+
}
|
|
27
|
+
if (!env.NONCE_STATE) {
|
|
28
|
+
throw new Error('NONCE_BACKEND=durable-object but the NONCE_STATE binding is missing')
|
|
29
|
+
}
|
|
30
|
+
return new DurableObjectBackend(
|
|
31
|
+
env.NONCE_STATE as DurableObjectNamespace<NonceRateState>,
|
|
32
|
+
cfg.nonceShard,
|
|
33
|
+
cfg.nonceMaxEntries,
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pluggable state backend — the Workers analog of the Rust gateway's `NonceStore` enum
|
|
3
|
+
* (in-memory | Redis). It holds the single-use nonce store AND the per-address rate-limit
|
|
4
|
+
* windows, because both need the same per-key atomicity a stateless isolate cannot give.
|
|
5
|
+
*
|
|
6
|
+
* Two implementations:
|
|
7
|
+
* - `DurableObjectBackend` (default): region-sharded, SQLite-backed Durable Objects. Strongly
|
|
8
|
+
* consistent, atomic single-use consume (the Redis-`GETDEL` analog). Free-tier eligible.
|
|
9
|
+
* - `KvBackend`: Workers KV + best-effort rate limit. Eventually consistent (documented weaker
|
|
10
|
+
* cross-region replay window in the `SINGLE_USE=false` ownership mode).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface NonceBackend {
|
|
14
|
+
/**
|
|
15
|
+
* Issue a fresh, time-bound nonce. `region` selects the DO shard (ignored by KV, which is
|
|
16
|
+
* global). Returns the opaque `<region>.<hex>` nonce and its unix-ms expiry.
|
|
17
|
+
*/
|
|
18
|
+
issue(region: string, ttlSecs: number): Promise<{ nonce: string; expiresAt: number }>
|
|
19
|
+
/** Consume a nonce exactly once; true iff it was valid, unexpired, and unused. */
|
|
20
|
+
takeIfValid(nonce: string): Promise<boolean>
|
|
21
|
+
/** Fixed 60s window per verified address. `maxPerMin === 0` disables limiting. */
|
|
22
|
+
rateCheck(address: string, maxPerMin: number, region: string): Promise<boolean>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse the shard tag embedded in a `<region>.<hex>` nonce so a consume call always routes
|
|
27
|
+
* back to the shard that issued it, even under anycast region drift.
|
|
28
|
+
*
|
|
29
|
+
* @param nonce - A nonce in `<region>.<hex>` format.
|
|
30
|
+
* @returns The region prefix (e.g. `"eu"`), or `"g"` if no dot is present.
|
|
31
|
+
*/
|
|
32
|
+
export function shardOfNonce(nonce: string): string {
|
|
33
|
+
const dot = nonce.indexOf('.')
|
|
34
|
+
return dot > 0 ? nonce.slice(0, dot) : 'g'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generate a cryptographically random 48-character hex string (24 bytes of entropy). Matches
|
|
39
|
+
* the Rust gateway's `random_nonce()` entropy so conformance vectors apply to both.
|
|
40
|
+
*
|
|
41
|
+
* @returns A 48-character lowercase hex string.
|
|
42
|
+
*/
|
|
43
|
+
export function randomHex24(): string {
|
|
44
|
+
const buf = new Uint8Array(24)
|
|
45
|
+
crypto.getRandomValues(buf)
|
|
46
|
+
let out = ''
|
|
47
|
+
for (let i = 0; i < buf.length; i++) out += buf[i].toString(16).padStart(2, '0')
|
|
48
|
+
return out
|
|
49
|
+
}
|
package/src/verify.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access verification — the security core. Direct Sui personal-message signature checking
|
|
3
|
+
* plus the allow/deny decision. A 1:1 port of the Rust gateway's `verify.rs`; the on-chain
|
|
4
|
+
* lookups are abstracted behind {@link ChainQuery} so the decision is unit-testable offline.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ed25519 } from '@noble/curves/ed25519.js'
|
|
8
|
+
import { secp256k1 } from '@noble/curves/secp256k1.js'
|
|
9
|
+
import { p256 } from '@noble/curves/nist.js'
|
|
10
|
+
import type { Config } from './config.js'
|
|
11
|
+
import type { NonceBackend } from './state/types.js'
|
|
12
|
+
import { blake2b256, base64ToBytes, bytesToHex, concatBytes, uleb128 } from './crypto.js'
|
|
13
|
+
import { personalMessageForNonce, decodeAccessProof } from './wire.js'
|
|
14
|
+
|
|
15
|
+
// Sui signature-scheme flag bytes (first byte of a serialized signature).
|
|
16
|
+
const FLAG_ED25519 = 0x00
|
|
17
|
+
const FLAG_SECP256K1 = 0x01
|
|
18
|
+
const FLAG_SECP256R1 = 0x02
|
|
19
|
+
const FLAG_MULTISIG = 0x03
|
|
20
|
+
const FLAG_ZKLOGIN = 0x05
|
|
21
|
+
|
|
22
|
+
/** blake2b256( intent(PersonalMessage,V0,Sui) || bcs(Vec<u8> message) ) — what a Sui wallet signs. */
|
|
23
|
+
export function signingDigest(message: Uint8Array): Uint8Array {
|
|
24
|
+
const intent = Uint8Array.from([3, 0, 0]) // IntentScope::PersonalMessage, V0, AppId::Sui
|
|
25
|
+
return blake2b256(concatBytes(intent, uleb128(message.length), message))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Sui address for a scheme: `0x` + hex(blake2b256(flag || pubkey)). */
|
|
29
|
+
export function deriveAddress(flag: number, pk: Uint8Array): string {
|
|
30
|
+
return '0x' + bytesToHex(blake2b256(concatBytes(Uint8Array.from([flag]), pk)))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function normalizeAddress(a: string): string {
|
|
34
|
+
const s = a.trim().replace(/^0x/i, '').toLowerCase()
|
|
35
|
+
return '0x' + s.padStart(64, '0')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Verify a Sui personal-message signature recovers `address` over `message`, dispatching on
|
|
40
|
+
* the scheme flag byte. Serialized layout: `flag(1) || signature || pubkey` (base64).
|
|
41
|
+
*
|
|
42
|
+
* Supported: ed25519 (0x00), secp256k1 (0x01), secp256r1 (0x02). multisig (0x03) and zkLogin
|
|
43
|
+
* (0x05) are recognised but not yet verified (they need the official Sui verifier) and fail
|
|
44
|
+
* **closed** — identical posture to the Rust gateway (audit F1).
|
|
45
|
+
*/
|
|
46
|
+
export function verifyPersonalMessageSignature(
|
|
47
|
+
address: string,
|
|
48
|
+
message: Uint8Array,
|
|
49
|
+
signatureB64: string,
|
|
50
|
+
): boolean {
|
|
51
|
+
let raw: Uint8Array
|
|
52
|
+
try {
|
|
53
|
+
raw = base64ToBytes(signatureB64)
|
|
54
|
+
} catch {
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
if (raw.length === 0) return false
|
|
58
|
+
const flag = raw[0]
|
|
59
|
+
const body = raw.subarray(1)
|
|
60
|
+
switch (flag) {
|
|
61
|
+
case FLAG_ED25519:
|
|
62
|
+
return verifyEd25519(address, message, body)
|
|
63
|
+
case FLAG_SECP256K1:
|
|
64
|
+
return verifyEcdsa(secp256k1, FLAG_SECP256K1, address, message, body)
|
|
65
|
+
case FLAG_SECP256R1:
|
|
66
|
+
return verifyEcdsa(p256, FLAG_SECP256R1, address, message, body)
|
|
67
|
+
case FLAG_MULTISIG:
|
|
68
|
+
case FLAG_ZKLOGIN:
|
|
69
|
+
// Recognised but not yet supported by this verifier (fails closed); see gateway audit F1.
|
|
70
|
+
return false
|
|
71
|
+
default:
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** ed25519 (flag 0x00): body = `sig(64) || pubkey(32)`. */
|
|
77
|
+
function verifyEd25519(address: string, message: Uint8Array, body: Uint8Array): boolean {
|
|
78
|
+
if (body.length !== 96) return false
|
|
79
|
+
const sig = body.subarray(0, 64)
|
|
80
|
+
const pk = body.subarray(64, 96)
|
|
81
|
+
const digest = signingDigest(message)
|
|
82
|
+
let ok = false
|
|
83
|
+
try {
|
|
84
|
+
ok = ed25519.verify(sig, digest, pk)
|
|
85
|
+
} catch {
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
if (!ok) return false
|
|
89
|
+
return normalizeAddress(address) === deriveAddress(FLAG_ED25519, pk)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* secp256k1 (0x01) / secp256r1 (0x02): body = `sig(64, compact r||s) || pubkey(33, SEC1-
|
|
94
|
+
* compressed)`. Sui signs `SHA256(blake2b256(intent||msg))` with ECDSA; noble's `verify` applies
|
|
95
|
+
* the SHA256 step internally, so we pass the blake2b digest and let the curve do the rest.
|
|
96
|
+
* `lowS: true` matches Sui's low-S normalisation.
|
|
97
|
+
*/
|
|
98
|
+
function verifyEcdsa(
|
|
99
|
+
curve: typeof secp256k1 | typeof p256,
|
|
100
|
+
flag: number,
|
|
101
|
+
address: string,
|
|
102
|
+
message: Uint8Array,
|
|
103
|
+
body: Uint8Array,
|
|
104
|
+
): boolean {
|
|
105
|
+
if (body.length !== 97) return false
|
|
106
|
+
const sig = body.subarray(0, 64)
|
|
107
|
+
const pk = body.subarray(64, 97)
|
|
108
|
+
const msgHash = signingDigest(message)
|
|
109
|
+
let ok = false
|
|
110
|
+
try {
|
|
111
|
+
ok = curve.verify(sig, msgHash, pk, { lowS: true })
|
|
112
|
+
} catch {
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
if (!ok) return false
|
|
116
|
+
return normalizeAddress(address) === deriveAddress(flag, pk)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** On-chain lookups needed to authorise a request. Implemented by {@link ../chain.SuiRpc}. */
|
|
120
|
+
export interface ChainQuery {
|
|
121
|
+
ownsNft(address: string, nftType: string, gateId?: string): Promise<boolean>
|
|
122
|
+
consumeEventMatches(
|
|
123
|
+
nonce: string,
|
|
124
|
+
address: string,
|
|
125
|
+
nftType: string,
|
|
126
|
+
gateId?: string,
|
|
127
|
+
consumeDigest?: string,
|
|
128
|
+
): Promise<boolean>
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export type Denied =
|
|
132
|
+
| 'BadProof'
|
|
133
|
+
| 'BadSignature'
|
|
134
|
+
| 'NonceInvalid'
|
|
135
|
+
| 'NotOwner'
|
|
136
|
+
| 'ConsumeMissing'
|
|
137
|
+
| 'ChainError'
|
|
138
|
+
|
|
139
|
+
export function deniedReason(d: Denied): string {
|
|
140
|
+
switch (d) {
|
|
141
|
+
case 'BadProof':
|
|
142
|
+
return 'malformed access proof'
|
|
143
|
+
case 'BadSignature':
|
|
144
|
+
return 'signature does not recover address'
|
|
145
|
+
case 'NonceInvalid':
|
|
146
|
+
return 'challenge nonce invalid, expired, or already used'
|
|
147
|
+
case 'NotOwner':
|
|
148
|
+
return 'address does not hold the required access NFT'
|
|
149
|
+
case 'ConsumeMissing':
|
|
150
|
+
return 'no matching single-use consume for this challenge'
|
|
151
|
+
case 'ChainError':
|
|
152
|
+
return 'on-chain verification failed'
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export type VerifyResult = { ok: true; address: string } | { ok: false; denied: Denied }
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Authorise a request from its base64 proof token. The nonce is consumed (single-use at the
|
|
160
|
+
* gateway) as part of a successful signature check, so a replay presents an already-used
|
|
161
|
+
* nonce and is rejected. Mirror of Rust `verify_access_request`.
|
|
162
|
+
*/
|
|
163
|
+
export async function verifyAccessRequest(
|
|
164
|
+
cfg: Config,
|
|
165
|
+
store: NonceBackend,
|
|
166
|
+
token: string,
|
|
167
|
+
chain: ChainQuery,
|
|
168
|
+
): Promise<VerifyResult> {
|
|
169
|
+
let proof
|
|
170
|
+
try {
|
|
171
|
+
proof = decodeAccessProof(token)
|
|
172
|
+
} catch {
|
|
173
|
+
return { ok: false, denied: 'BadProof' }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const message = personalMessageForNonce(proof.nonce)
|
|
177
|
+
if (!verifyPersonalMessageSignature(proof.address, message, proof.signature)) {
|
|
178
|
+
return { ok: false, denied: 'BadSignature' }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Consume the nonce exactly once (fresh, unexpired, unused) — before the chain call.
|
|
182
|
+
if (!(await store.takeIfValid(proof.nonce))) {
|
|
183
|
+
return { ok: false, denied: 'NonceInvalid' }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (cfg.singleUse) {
|
|
187
|
+
if (proof.consumeDigest === undefined) {
|
|
188
|
+
return { ok: false, denied: 'ConsumeMissing' }
|
|
189
|
+
}
|
|
190
|
+
let ok: boolean
|
|
191
|
+
try {
|
|
192
|
+
ok = await chain.consumeEventMatches(
|
|
193
|
+
proof.nonce,
|
|
194
|
+
proof.address,
|
|
195
|
+
cfg.nftType,
|
|
196
|
+
cfg.gateId,
|
|
197
|
+
proof.consumeDigest,
|
|
198
|
+
)
|
|
199
|
+
} catch {
|
|
200
|
+
return { ok: false, denied: 'ChainError' }
|
|
201
|
+
}
|
|
202
|
+
if (!ok) return { ok: false, denied: 'ConsumeMissing' }
|
|
203
|
+
} else {
|
|
204
|
+
let ok: boolean
|
|
205
|
+
try {
|
|
206
|
+
ok = await chain.ownsNft(proof.address, cfg.nftType, cfg.gateId)
|
|
207
|
+
} catch {
|
|
208
|
+
return { ok: false, denied: 'ChainError' }
|
|
209
|
+
}
|
|
210
|
+
if (!ok) return { ok: false, denied: 'NotOwner' }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { ok: true, address: proof.address }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export { FLAG_ED25519, FLAG_SECP256K1, FLAG_SECP256R1, FLAG_MULTISIG, FLAG_ZKLOGIN }
|
package/src/wire.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared wire-format helpers, REUSED from `@meddleware/nft-gate-client` rather than
|
|
3
|
+
* re-implemented — there must be exactly one source for `personalMessageForNonce` and the
|
|
4
|
+
* proof token shape across the toolkit (the "one contract that spans all three" rule in
|
|
5
|
+
* nft-gate/CLAUDE.md).
|
|
6
|
+
*
|
|
7
|
+
* Imported by relative path (the in-repo convention) directly from the client's `proof.ts` /
|
|
8
|
+
* `types.ts`, which import only `./types.js` — so this pulls in NO `@mysten/sui` and keeps the
|
|
9
|
+
* Worker bundle lean.
|
|
10
|
+
*
|
|
11
|
+
* Self-contained-mirror alternative (documented for future review): if the Worker must ship
|
|
12
|
+
* fully decoupled from the client package, replace these two lines with a local copy of
|
|
13
|
+
* `personalMessageForNonce` + `decodeAccessProof` guarded by `conformance/vectors.json`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
personalMessageForNonce,
|
|
18
|
+
decodeAccessProof,
|
|
19
|
+
} from '@meddleware/nft-gate-client'
|
|
20
|
+
export type { AccessProof } from '@meddleware/nft-gate-client'
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"types": ["@cloudflare/workers-types"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noUncheckedIndexedAccess": false,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": false,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
"resolveJsonModule": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*.ts", "test/**/*.ts", "../conformance/**/*.json"]
|
|
19
|
+
}
|
package/wrangler.toml
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# nft-gate-gateway — Cloudflare Workers deployment.
|
|
2
|
+
#
|
|
3
|
+
# Wire-identical to the Rust gateway (../gateway). Bind it to the SAME public hostname the
|
|
4
|
+
# Rust gateway serves (see [routes] below) to switch implementations with nothing else changing.
|
|
5
|
+
#
|
|
6
|
+
# OPERATOR SETUP — run these once before deploying, then never touch them again:
|
|
7
|
+
#
|
|
8
|
+
# UPSTREAM_URL (required — your CF-Access-locked relay/upstream origin):
|
|
9
|
+
# wrangler secret put UPSTREAM_URL
|
|
10
|
+
# Value: the full origin URL (e.g. https://your-origin.example.com)
|
|
11
|
+
# This is kept as a secret so it does not appear in this public file and is
|
|
12
|
+
# not overwritten by `wrangler deploy`.
|
|
13
|
+
#
|
|
14
|
+
# NFT_TYPE (required — set after publishing your access_gate contract on-chain):
|
|
15
|
+
# wrangler secret put NFT_TYPE
|
|
16
|
+
# Value: "<PACKAGE_ID>::access_gate::SoulboundAccessNFT"
|
|
17
|
+
# Update this whenever you redeploy the on-chain contract.
|
|
18
|
+
#
|
|
19
|
+
# UPSTREAM_AUTH_HEADERS (required for CF-Access-locked origin):
|
|
20
|
+
# wrangler secret put UPSTREAM_AUTH_HEADERS
|
|
21
|
+
# Value: "CF-Access-Client-Id: <id>, CF-Access-Client-Secret: <secret>"
|
|
22
|
+
# Obtain from: Cloudflare Zero Trust → Access → Service Tokens → create token
|
|
23
|
+
# The Access policy on your origin hostname must allow this token.
|
|
24
|
+
#
|
|
25
|
+
# SUI_RPC_AUTH_HEADER (optional — only if using an authenticated/private RPC):
|
|
26
|
+
# wrangler secret put SUI_RPC_AUTH_HEADER
|
|
27
|
+
# Value: "Authorization: Bearer <token>"
|
|
28
|
+
#
|
|
29
|
+
# CF_ANALYTICS_TOKEN (optional — only if QUOTA_GUARD_ENABLED=true):
|
|
30
|
+
# wrangler secret put CF_ANALYTICS_TOKEN
|
|
31
|
+
# CF_ACCOUNT_ID (optional — only if QUOTA_GUARD_ENABLED=true):
|
|
32
|
+
# wrangler secret put CF_ACCOUNT_ID
|
|
33
|
+
|
|
34
|
+
name = "nft-gate-gateway"
|
|
35
|
+
main = "src/index.ts"
|
|
36
|
+
compatibility_date = "2025-04-01"
|
|
37
|
+
compatibility_flags = ["nodejs_compat"]
|
|
38
|
+
|
|
39
|
+
# ── config (parity with the Rust gateway env; see README.md for the mapping) ─────────────────
|
|
40
|
+
[vars]
|
|
41
|
+
# UPSTREAM_URL and NFT_TYPE are set via `wrangler secret put` (see above) so that
|
|
42
|
+
# deployment-specific values never appear in this public file and are never overwritten
|
|
43
|
+
# by `wrangler deploy`. Do not add them here.
|
|
44
|
+
|
|
45
|
+
# Public fullnode — safe to keep as a plain var (no sensitive information).
|
|
46
|
+
SUI_RPC_URL = "https://fullnode.testnet.sui.io:443"
|
|
47
|
+
|
|
48
|
+
GATE_ID = "" # operator: set to the access_gate registry object ID (single-registry mode)
|
|
49
|
+
SINGLE_USE = "false"
|
|
50
|
+
PUBLIC_PATHS = "/v1/tip-config"
|
|
51
|
+
RATE_LIMIT_PER_MIN = "30"
|
|
52
|
+
MAX_BODY_BYTES = "262144"
|
|
53
|
+
CHALLENGE_TTL_SECS = "300"
|
|
54
|
+
OWNERSHIP_CACHE_TTL_MS = "0"
|
|
55
|
+
# Workers-specific:
|
|
56
|
+
NONCE_BACKEND = "durable-object" # "durable-object" (default) | "kv"
|
|
57
|
+
NONCE_SHARD = "region" # "region" (default) | "global"
|
|
58
|
+
NONCE_MAX_ENTRIES = "1000000"
|
|
59
|
+
QUOTA_GUARD_ENABLED = "false"
|
|
60
|
+
|
|
61
|
+
# ── nonce store + rate limiter: region-sharded, SQLite-backed Durable Objects (free-tier) ────
|
|
62
|
+
[[durable_objects.bindings]]
|
|
63
|
+
name = "NONCE_STATE"
|
|
64
|
+
class_name = "NonceRateState"
|
|
65
|
+
|
|
66
|
+
[[migrations]]
|
|
67
|
+
tag = "v1"
|
|
68
|
+
new_sqlite_classes = ["NonceRateState"]
|
|
69
|
+
|
|
70
|
+
# ── KV backend (only needed when NONCE_BACKEND=kv) ────────────────────────────────────────────
|
|
71
|
+
# [[kv_namespaces]]
|
|
72
|
+
# binding = "NONCE_KV"
|
|
73
|
+
# id = "<KV_NAMESPACE_ID>" # create with: wrangler kv namespace create NONCE_KV
|
|
74
|
+
|
|
75
|
+
# ── route: Worker serves as the public NFT-gated relay endpoint ───────────────────────────────
|
|
76
|
+
# operator: uncomment and fill in after creating the Worker route in the CF dashboard
|
|
77
|
+
# [[routes]]
|
|
78
|
+
# pattern = "nft-gate.example.com/*"
|
|
79
|
+
# zone_name = "example.com"
|
|
80
|
+
|
|
81
|
+
# ── optional quota guard (needs QUOTA_GUARD_ENABLED=true + the secrets above) ────────────────
|
|
82
|
+
# [triggers]
|
|
83
|
+
# crons = ["*/15 * * * *"]
|