@meddleware/nft-gate-gateway 0.0.2 → 0.0.4
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/package.json +2 -2
- package/src/chain.ts +1 -0
- package/src/config.ts +4 -0
- package/src/cors.ts +38 -0
- package/src/crypto.ts +10 -0
- package/src/index.ts +7 -1
- package/src/verify.ts +4 -0
- package/wrangler.toml +5 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
2
|
"name": "@meddleware/nft-gate-gateway",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Cloudflare Workers implementation of the nft-gate gateway: a wire-identical, drop-in sibling of the Rust gateway that gates any HTTP upstream behind an access_gate NFT.",
|
|
6
6
|
"license": "0BSD",
|
package/src/chain.ts
CHANGED
package/src/config.ts
CHANGED
|
@@ -51,9 +51,12 @@ export interface Env {
|
|
|
51
51
|
CF_ACCOUNT_ID?: string
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** Which nonce/rate-limit backend to use: strongly-consistent Durable Objects or Workers KV. */
|
|
54
55
|
export type NonceBackendKind = 'durable-object' | 'kv'
|
|
56
|
+
/** Nonce shard placement: one shard per region (near users) or a single global shard. */
|
|
55
57
|
export type NonceShardMode = 'region' | 'global'
|
|
56
58
|
|
|
59
|
+
/** Fully-resolved gateway configuration derived from {@link Env} by {@link loadConfig}. */
|
|
57
60
|
export interface Config {
|
|
58
61
|
upstreamUrl: string
|
|
59
62
|
suiRpcUrl: string
|
|
@@ -139,6 +142,7 @@ export function loadConfig(env: Env): Config {
|
|
|
139
142
|
}
|
|
140
143
|
}
|
|
141
144
|
|
|
145
|
+
/** Returns `true` if `path` is in the configured public-paths list (proxied without auth). */
|
|
142
146
|
export function isPublicPath(cfg: Config, path: string): boolean {
|
|
143
147
|
return cfg.publicPaths.some((p) => p === path)
|
|
144
148
|
}
|
package/src/cors.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORS utilities for the nft-gate Cloudflare Worker.
|
|
3
|
+
*
|
|
4
|
+
* The Worker is accessed from browser origins (e.g. sui-walrus.meddleware.co.uk,
|
|
5
|
+
* sui.meddleware.co.uk) that differ from the Worker's own hostname. Without
|
|
6
|
+
* Access-Control-Allow-Origin headers the browser blocks the response even when
|
|
7
|
+
* the Worker returns 200, and OPTIONS preflights (required before non-simple requests
|
|
8
|
+
* such as PUT uploads with an Authorization header) receive no preflight grant.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const CORS_HEADERS: Record<string, string> = {
|
|
12
|
+
'access-control-allow-origin': '*',
|
|
13
|
+
'access-control-allow-methods': 'GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS',
|
|
14
|
+
'access-control-allow-headers': 'authorization, content-type, x-access-proof',
|
|
15
|
+
'access-control-expose-headers': 'location, upload-offset',
|
|
16
|
+
'access-control-max-age': '86400',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Clone `res` and add CORS headers. Using `set()` so calling this on an upstream
|
|
21
|
+
* response that already carries CORS headers just overwrites them consistently.
|
|
22
|
+
*/
|
|
23
|
+
export function withCors(res: Response): Response {
|
|
24
|
+
const out = new Response(res.body, res)
|
|
25
|
+
for (const [k, v] of Object.entries(CORS_HEADERS)) {
|
|
26
|
+
out.headers.set(k, v)
|
|
27
|
+
}
|
|
28
|
+
return out
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Return a minimal 204 preflight response for OPTIONS requests.
|
|
33
|
+
* The browser requires this before sending non-simple cross-origin requests
|
|
34
|
+
* (e.g. PUT/POST with Authorization or Content-Type).
|
|
35
|
+
*/
|
|
36
|
+
export function corsPreflightResponse(): Response {
|
|
37
|
+
return new Response(null, { status: 204, headers: CORS_HEADERS })
|
|
38
|
+
}
|
package/src/crypto.ts
CHANGED
|
@@ -7,16 +7,19 @@
|
|
|
7
7
|
import { blake2b } from '@noble/hashes/blake2.js'
|
|
8
8
|
import { sha256 as nobleSha256 } from '@noble/hashes/sha2.js'
|
|
9
9
|
|
|
10
|
+
/** Compute a 32-byte Blake2b-256 digest of `data`. */
|
|
10
11
|
export function blake2b256(data: Uint8Array): Uint8Array {
|
|
11
12
|
return blake2b(data, { dkLen: 32 })
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
/** Compute a 32-byte SHA-256 digest of `data`. */
|
|
14
16
|
export function sha256(data: Uint8Array): Uint8Array {
|
|
15
17
|
return nobleSha256(data)
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
const HEX = '0123456789abcdef'
|
|
19
21
|
|
|
22
|
+
/** Encode `bytes` as a lowercase hex string (no `0x` prefix). */
|
|
20
23
|
export function bytesToHex(bytes: Uint8Array): string {
|
|
21
24
|
let out = ''
|
|
22
25
|
for (let i = 0; i < bytes.length; i++) {
|
|
@@ -25,6 +28,11 @@ export function bytesToHex(bytes: Uint8Array): string {
|
|
|
25
28
|
return out
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Decode a hex string (with or without `0x` prefix) to bytes.
|
|
33
|
+
*
|
|
34
|
+
* @throws {Error} if `hex` has an odd number of characters.
|
|
35
|
+
*/
|
|
28
36
|
export function hexToBytes(hex: string): Uint8Array {
|
|
29
37
|
const s = hex.startsWith('0x') ? hex.slice(2) : hex
|
|
30
38
|
if (s.length % 2 !== 0) throw new Error('odd-length hex')
|
|
@@ -43,6 +51,7 @@ export function base64ToBytes(b64: string): Uint8Array {
|
|
|
43
51
|
return out
|
|
44
52
|
}
|
|
45
53
|
|
|
54
|
+
/** Encode `bytes` as standard base64. Uses `btoa` (present in the Workers runtime). */
|
|
46
55
|
export function bytesToBase64(bytes: Uint8Array): string {
|
|
47
56
|
let s = ''
|
|
48
57
|
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i])
|
|
@@ -65,6 +74,7 @@ export function uleb128(v: number): Uint8Array {
|
|
|
65
74
|
return Uint8Array.from(out)
|
|
66
75
|
}
|
|
67
76
|
|
|
77
|
+
/** Concatenate multiple `Uint8Array` values into a single contiguous array. */
|
|
68
78
|
export function concatBytes(...parts: Uint8Array[]): Uint8Array {
|
|
69
79
|
let len = 0
|
|
70
80
|
for (const p of parts) len += p.length
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { SuiRpc } from './chain.js'
|
|
|
15
15
|
import { verifyAccessRequest, deniedReason } from './verify.js'
|
|
16
16
|
import { forward } from './proxy.js'
|
|
17
17
|
import { runQuotaGuard } from './quota.js'
|
|
18
|
+
import { withCors, corsPreflightResponse } from './cors.js'
|
|
18
19
|
|
|
19
20
|
export { NonceRateState } from './state/durable_object.js'
|
|
20
21
|
|
|
@@ -124,6 +125,11 @@ async function handle(request: Request, env: Env): Promise<Response> {
|
|
|
124
125
|
|
|
125
126
|
if (path === '/healthz') return new Response('ok', { status: 200 })
|
|
126
127
|
|
|
128
|
+
// Handle CORS preflight before any auth check. The browser sends OPTIONS before
|
|
129
|
+
// non-simple cross-origin requests (e.g. PUT/POST with Authorization); responding
|
|
130
|
+
// here avoids the auth path returning 401 and causing the browser to abort.
|
|
131
|
+
if (request.method === 'OPTIONS') return corsPreflightResponse()
|
|
132
|
+
|
|
127
133
|
let state: GatewayState
|
|
128
134
|
try {
|
|
129
135
|
state = await getState(env)
|
|
@@ -163,7 +169,7 @@ async function handle(request: Request, env: Env): Promise<Response> {
|
|
|
163
169
|
*/
|
|
164
170
|
export default {
|
|
165
171
|
fetch(request: Request, env: Env): Promise<Response> {
|
|
166
|
-
return handle(request, env)
|
|
172
|
+
return handle(request, env).then(withCors)
|
|
167
173
|
},
|
|
168
174
|
async scheduled(_controller: ScheduledController, env: Env): Promise<void> {
|
|
169
175
|
if ((env.QUOTA_GUARD_ENABLED ?? 'false').toLowerCase() === 'true') {
|
package/src/verify.ts
CHANGED
|
@@ -30,6 +30,7 @@ export function deriveAddress(flag: number, pk: Uint8Array): string {
|
|
|
30
30
|
return '0x' + bytesToHex(blake2b256(concatBytes(Uint8Array.from([flag]), pk)))
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** Normalise a Sui address to lowercase, `0x`-prefixed, zero-padded to 64 hex digits. */
|
|
33
34
|
export function normalizeAddress(a: string): string {
|
|
34
35
|
const s = a.trim().replace(/^0x/i, '').toLowerCase()
|
|
35
36
|
return '0x' + s.padStart(64, '0')
|
|
@@ -128,6 +129,7 @@ export interface ChainQuery {
|
|
|
128
129
|
): Promise<boolean>
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
/** The reason a request was rejected by {@link verifyAccessRequest}. Mirror of Rust `Denied`. */
|
|
131
133
|
export type Denied =
|
|
132
134
|
| 'BadProof'
|
|
133
135
|
| 'BadSignature'
|
|
@@ -136,6 +138,7 @@ export type Denied =
|
|
|
136
138
|
| 'ConsumeMissing'
|
|
137
139
|
| 'ChainError'
|
|
138
140
|
|
|
141
|
+
/** A short, client-visible description of why access was denied. */
|
|
139
142
|
export function deniedReason(d: Denied): string {
|
|
140
143
|
switch (d) {
|
|
141
144
|
case 'BadProof':
|
|
@@ -153,6 +156,7 @@ export function deniedReason(d: Denied): string {
|
|
|
153
156
|
}
|
|
154
157
|
}
|
|
155
158
|
|
|
159
|
+
/** Result of {@link verifyAccessRequest}: the verified address on success, or a {@link Denied} reason. */
|
|
156
160
|
export type VerifyResult = { ok: true; address: string } | { ok: false; denied: Denied }
|
|
157
161
|
|
|
158
162
|
/**
|
package/wrangler.toml
CHANGED
|
@@ -73,10 +73,11 @@ new_sqlite_classes = ["NonceRateState"]
|
|
|
73
73
|
# id = "<KV_NAMESPACE_ID>" # create with: wrangler kv namespace create NONCE_KV
|
|
74
74
|
|
|
75
75
|
# ── route: Worker serves as the public NFT-gated relay endpoint ───────────────────────────────
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
# Configured via CF API (route ID 63427e4dbf9b4428ad9c492487953551).
|
|
77
|
+
# wrangler deploy will create/update this automatically.
|
|
78
|
+
[[routes]]
|
|
79
|
+
pattern = "sui-walrus-relay-testnet.meddleware.co.uk/*"
|
|
80
|
+
zone_name = "meddleware.co.uk"
|
|
80
81
|
|
|
81
82
|
# ── optional quota guard (needs QUOTA_GUARD_ENABLED=true + the secrets above) ────────────────
|
|
82
83
|
# [triggers]
|