@meddleware/nft-gate-gateway 0.0.3 → 0.0.5

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 CHANGED
@@ -1,6 +1,6 @@
1
- {
1
+ {
2
2
  "name": "@meddleware/nft-gate-gateway",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
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/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/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/proxy.ts CHANGED
@@ -42,16 +42,19 @@ export async function forward(cfg: Config, request: Request): Promise<Response>
42
42
  const pathAndQuery = url.pathname + url.search
43
43
  const upstreamUrl = cfg.upstreamUrl + pathAndQuery
44
44
 
45
- // Read + cap the body (a griefing guard) before forwarding.
46
- let body: ArrayBuffer | undefined
45
+ // Size guard: check Content-Length first (cheap, avoids buffering). Not authoritative
46
+ // (clients can lie) but protects against accidental oversized sends; the relay enforces
47
+ // its own hard limit. Stream the body directly — buffering 60+ MB encoded blobs into an
48
+ // ArrayBuffer would exhaust Worker memory for large Walrus uploads.
47
49
  const method = request.method.toUpperCase()
48
50
  if (method !== 'GET' && method !== 'HEAD') {
49
- const buf = await request.arrayBuffer()
50
- if (buf.byteLength > cfg.maxBodyBytes) {
51
+ const cl = parseInt(request.headers.get('content-length') ?? '', 10)
52
+ if (Number.isFinite(cl) && cl > cfg.maxBodyBytes) {
51
53
  return errorResponse(413, 'request body too large')
52
54
  }
53
- body = buf
54
55
  }
56
+ const body: ReadableStream | null | undefined =
57
+ method !== 'GET' && method !== 'HEAD' ? request.body : undefined
55
58
 
56
59
  const headers = new Headers(request.headers)
57
60
  headers.delete('host')
@@ -66,7 +69,13 @@ export async function forward(cfg: Config, request: Request): Promise<Response>
66
69
 
67
70
  let upstream: Response
68
71
  try {
69
- upstream = await fetch(upstreamUrl, { method: request.method, headers, body })
72
+ upstream = await fetch(upstreamUrl, {
73
+ method: request.method,
74
+ headers,
75
+ body: body ?? null,
76
+ // @ts-expect-error — CF Workers supports streaming body via ReadableStream without 'duplex'
77
+ duplex: 'half',
78
+ })
70
79
  } catch {
71
80
  return errorResponse(502, 'upstream error')
72
81
  }
package/wrangler.toml CHANGED
@@ -49,7 +49,7 @@ GATE_ID = "0x0485c1fa80e4c355c85ab99c0281a328d8fb5c60ac50ab64f10be0f8be792aba"
49
49
  SINGLE_USE = "true"
50
50
  PUBLIC_PATHS = "/v1/tip-config"
51
51
  RATE_LIMIT_PER_MIN = "30"
52
- MAX_BODY_BYTES = "262144"
52
+ MAX_BODY_BYTES = "104857600"
53
53
  CHALLENGE_TTL_SECS = "300"
54
54
  OWNERSHIP_CACHE_TTL_MS = "0"
55
55
  # Workers-specific:
@@ -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
- # 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"
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]