@meddleware/nft-gate-client 0.0.7 → 0.0.9

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/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.0.8] - 2026-09-17
9
+
10
+ ### Added
11
+
12
+ - `homepage` in `package.json` — links to the Access Gate section of the documentation site.
13
+
8
14
  ## [0.0.3] - 2026-08-29
9
15
 
10
16
  ### Added
package/dist/proof.d.ts CHANGED
@@ -6,7 +6,6 @@ import type { AccessProof, Challenge } from './types.js';
6
6
  export declare function personalMessageForNonce(nonce: string): Uint8Array;
7
7
  /** Encode a proof as the compact Bearer token carried in the relay auth header. */
8
8
  export declare function encodeAccessProof(proof: AccessProof): string;
9
- /** Decode a proof token produced by {@link encodeAccessProof}. Throws on malformed input. */
10
9
  export declare function decodeAccessProof(token: string): AccessProof;
11
10
  /** A wallet-provided personal-message signer (e.g. wallet-standard `sui:signPersonalMessage`). */
12
11
  export type PersonalMessageSigner = (message: Uint8Array) => Promise<{
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@meddleware/nft-gate-client",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
+ "homepage": "https://docs.meddleware.co.uk/blockchain/sui/access-gate/",
4
5
  "type": "module",
5
6
  "description": "Client-side helpers for the access_gate NFT access primitive: ownership queries, purchase/consume PTB builders, challenge signing, and access-proof assembly.",
6
7
  "author": "MeddleWare <meddleware@proton.me>",
@@ -36,7 +37,7 @@
36
37
  "test:integration": "vitest run --config vitest.integration.config.ts"
37
38
  },
38
39
  "dependencies": {
39
- "@mysten/sui": "^2.28.0"
40
+ "@mysten/sui": "^2.31.0"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@types/node": "~24.12.2",
package/src/ownership.ts CHANGED
@@ -27,8 +27,9 @@ function parseUsesRemaining(variant: any): number | null {
27
27
  const fields = variant.fields ?? variant
28
28
  const ur = fields?.uses_remaining ?? fields?.SingleUse?.uses_remaining
29
29
  if (ur != null) return Number(ur)
30
- // Tagged SingleUse but the count is missing from this node's rendering — unknown, not a pass.
31
- return tag === 'SingleUse' ? null : null
30
+ // Either an unknown tag, or a SingleUse whose count is missing from this node's rendering — in
31
+ // both cases the remaining count is unknown, so report null (never a fabricated 0).
32
+ return null
32
33
  }
33
34
 
34
35
  /** True if `type` names an `access_gate` NFT struct (transferable or soulbound). */
package/src/proof.ts CHANGED
@@ -8,12 +8,29 @@ export function personalMessageForNonce(nonce: string): Uint8Array {
8
8
  return new TextEncoder().encode(`nft-gate:access:${nonce}`)
9
9
  }
10
10
 
11
+ // UTF-8-safe base64. The naive `btoa(str)` operates on Latin-1 and throws/corrupts on any code
12
+ // point > 0xFF, so we round-trip through UTF-8 bytes first. For pure-ASCII payloads (the only kind
13
+ // this wire format carries — see the ASCII contract below) the output is byte-identical to `btoa`,
14
+ // so the gateways' decoders are unaffected.
11
15
  function toBase64(s: string): string {
12
- return btoa(s)
16
+ const bytes = new TextEncoder().encode(s)
17
+ let bin = ''
18
+ for (const b of bytes) bin += String.fromCharCode(b)
19
+ return btoa(bin)
13
20
  }
14
21
 
15
22
  function fromBase64(s: string): string {
16
- return atob(s)
23
+ const bin = atob(s)
24
+ const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0))
25
+ return new TextDecoder().decode(bytes)
26
+ }
27
+
28
+ /** True iff every character is ASCII (code point ≤ 0x7F). */
29
+ function isAscii(s: string): boolean {
30
+ for (let i = 0; i < s.length; i++) {
31
+ if (s.charCodeAt(i) > 0x7f) return false
32
+ }
33
+ return true
17
34
  }
18
35
 
19
36
  /** Encode a proof as the compact Bearer token carried in the relay auth header. */
@@ -21,12 +38,29 @@ export function encodeAccessProof(proof: AccessProof): string {
21
38
  return toBase64(JSON.stringify(proof))
22
39
  }
23
40
 
24
- /** Decode a proof token produced by {@link encodeAccessProof}. Throws on malformed input. */
41
+ /**
42
+ * Decode a proof token produced by {@link encodeAccessProof}. Throws on malformed input.
43
+ *
44
+ * ASCII contract: both gateways issue ASCII-only nonces (`<region>.<hex>` or hex) and treat
45
+ * `address`/`nonce` as ASCII end-to-end. We enforce that here so a non-ASCII field is rejected
46
+ * rather than silently diverging between the client and a gateway verifier. `address` is emitted
47
+ * verbatim (raw); canonicalisation is owned gateway-side (see nft-gate `verify.rs`/`verify.ts`), so
48
+ * the wire format is unchanged and the golden vector stays stable.
49
+ */
50
+ /** Maximum encoded token length (bytes). Prevents a multi-MB blob from saturating `atob`/`JSON.parse`. */
51
+ const MAX_TOKEN_BYTES = 4096
52
+
25
53
  export function decodeAccessProof(token: string): AccessProof {
54
+ if (token.length > MAX_TOKEN_BYTES) {
55
+ throw new Error(`access proof token too large (${token.length} > ${MAX_TOKEN_BYTES})`)
56
+ }
26
57
  const raw = JSON.parse(fromBase64(token)) as Partial<AccessProof>
27
58
  if (!raw || typeof raw.address !== 'string' || typeof raw.nonce !== 'string' || typeof raw.signature !== 'string') {
28
59
  throw new Error('malformed access proof')
29
60
  }
61
+ if (!isAscii(raw.address) || !isAscii(raw.nonce) || !isAscii(raw.signature)) {
62
+ throw new Error('non-ASCII field in access proof')
63
+ }
30
64
  const proof: AccessProof = { address: raw.address, nonce: raw.nonce, signature: raw.signature }
31
65
  if (typeof raw.consumeDigest === 'string') proof.consumeDigest = raw.consumeDigest
32
66
  return proof