@meddleware/nft-gate-client 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/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.0.1] - 2026-08-27
9
+
10
+ ### Added
11
+
12
+ - Initial release as `@meddleware/nft-gate-client`
13
+ - `ownsAccessNft`, `fetchAccessNfts`, `fetchAccessNftById`, `parseOwnedAccessNft` — on-chain ownership queries via `@mysten/sui`
14
+ - `buildPurchaseTx`, `buildConsumeTx`, `buildCreateGateTx` — PTB builders for purchase, single-use consume, and gate creation
15
+ - `fetchChallenge` — `GET /v1/challenge` client for the nft-gate gateway wire protocol
16
+ - `personalMessageForNonce`, `buildAccessProof`, `encodeAccessProof`, `decodeAccessProof` — challenge signing and proof token construction
17
+ - Wire protocol compatible with both the Rust/Axum gateway and the Cloudflare Workers gateway in `meddleware-org/nft-gate`
package/LICENSE ADDED
@@ -0,0 +1,14 @@
1
+ BSD Zero Clause License
2
+
3
+ Copyright (c) 2026 MeddleWare
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
9
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
10
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
11
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
12
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
13
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
14
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # @meddleware/nft-gate-client
2
+
3
+ [![License: 0BSD](https://img.shields.io/badge/License-0BSD-blue.svg)](LICENSE)
4
+
5
+ Client-side TypeScript helpers for the `access_gate` NFT access primitive on Sui. Browser-safe (runs in a wallet app) and Node-friendly. **Client-side only** — server-side verification is provided by the [nft-gate](https://github.com/meddleware-org/nft-gate) service (Rust/Axum gateway or Cloudflare Workers equivalent).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @meddleware/nft-gate-client
11
+ ```
12
+
13
+ ## API
14
+
15
+ ```ts
16
+ import {
17
+ ownsAccessNft, fetchAccessNfts, // ownership queries
18
+ buildPurchaseTx, buildConsumeTx, // PTB builders (wallet signs + executes)
19
+ fetchChallenge, // GET /v1/challenge
20
+ buildAccessProof, personalMessageForNonce, // challenge signing + proof token
21
+ } from '@meddleware/nft-gate-client'
22
+
23
+ const cfg = { packageId, gateId, nftType, soulbound: true }
24
+
25
+ // 1. Check whether the connected wallet has access:
26
+ const hasAccess = await ownsAccessNft(suiClient, address, cfg.nftType, cfg.gateId)
27
+
28
+ // 2. Purchase access if not:
29
+ const tx = buildPurchaseTx(cfg, priceMist) // wallet signs & executes
30
+
31
+ // 3. Prove access to a gateway (single-use: submit buildConsumeTx first):
32
+ const challenge = await fetchChallenge(gatewayHost)
33
+ const token = await buildAccessProof({ address, challenge, sign, consumeDigest })
34
+ // pass `token` as the relay/gateway Authorization: Bearer header
35
+ ```
36
+
37
+ ## API Reference
38
+
39
+ ### Ownership
40
+
41
+ **`ownsAccessNft(client, address, nftType, gateId): Promise<boolean>`**
42
+
43
+ Returns `true` if the address holds at least one unexpired access NFT matching the gate.
44
+
45
+ **`fetchAccessNfts(client, address, nftType): Promise<OwnedAccessNft[]>`**
46
+
47
+ Returns all access NFTs owned by an address for a given struct type.
48
+
49
+ **`fetchAccessNftById(client, objectId): Promise<OwnedAccessNft | null>`**
50
+
51
+ Fetch a single access NFT by object ID.
52
+
53
+ **`parseOwnedAccessNft(object): OwnedAccessNft | null`**
54
+
55
+ Parse a raw `SuiObjectResponse` into an `OwnedAccessNft`. Returns `null` if the object is not a valid access NFT.
56
+
57
+ ### PTB Builders
58
+
59
+ **`buildPurchaseTx(config, priceMist): Transaction`**
60
+
61
+ Build a transaction to purchase an access NFT. Caller signs and executes with their wallet.
62
+
63
+ **`buildConsumeTx(config, nftObjectId): Transaction`**
64
+
65
+ Build a transaction to consume (burn) a single-use access NFT, proving use. Submit before calling `buildAccessProof` with the resulting `consumeDigest`.
66
+
67
+ **`buildCreateGateTx(config): Transaction`**
68
+
69
+ Build a transaction to create a new access gate on-chain (admin operation).
70
+
71
+ ### Challenge & Proof
72
+
73
+ **`fetchChallenge(gatewayHost, opts?): Promise<Challenge>`**
74
+
75
+ Fetch a time-bound nonce from the gateway's `GET /v1/challenge` endpoint.
76
+
77
+ **`personalMessageForNonce(nonce): Uint8Array`**
78
+
79
+ Returns the exact bytes the wallet must sign for a nonce. Matches the gateway's derivation: `nft-gate:access:<nonce>`.
80
+
81
+ **`buildAccessProof(opts): Promise<string>`**
82
+
83
+ One-shot helper: sign the challenge with the wallet and return the base64(JSON) Bearer token to pass to the gateway or relay.
84
+
85
+ **`encodeAccessProof(proof: AccessProof): string`**
86
+
87
+ Encode a proof struct directly to a base64(JSON) token (lower-level, sync).
88
+
89
+ **`decodeAccessProof(token: string): AccessProof`**
90
+
91
+ Decode a base64(JSON) token back to a proof struct.
92
+
93
+ ## Wire protocol
94
+
95
+ - **Challenge**: `{ nonce: string, expiresAt: number }`
96
+ - **Signed message**: `nft-gate:access:<nonce>` (UTF-8 bytes)
97
+ - **Proof token**: `base64(JSON { address, nonce, signature, consumeDigest? })`
98
+
99
+ This format is verified by both the Rust gateway and the Cloudflare Workers gateway in the [nft-gate](https://github.com/meddleware-org/nft-gate) repo.
100
+
101
+ ## Types
102
+
103
+ ```ts
104
+ interface AccessGateConfig {
105
+ packageId: string
106
+ gateId: string
107
+ nftType: string
108
+ soulbound: boolean
109
+ }
110
+
111
+ interface Challenge {
112
+ nonce: string
113
+ expiresAt: number
114
+ }
115
+
116
+ interface AccessProof {
117
+ address: string
118
+ nonce: string
119
+ signature: string
120
+ consumeDigest?: string
121
+ }
122
+
123
+ interface OwnedAccessNft {
124
+ objectId: string
125
+ gateId: string
126
+ expiresAt?: number
127
+ }
128
+ ```
129
+
130
+ ## Development
131
+
132
+ ```bash
133
+ npm run type-check # tsc --noEmit
134
+ npm test # vitest run (19 unit tests)
135
+ npm run test:watch # vitest interactive
136
+ ```
137
+
138
+ ## License
139
+
140
+ [0BSD](LICENSE)
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@meddleware/nft-gate-client",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "Client-side helpers for the access_gate NFT access primitive: ownership queries, purchase/consume PTB builders, challenge signing, and access-proof assembly.",
6
+ "author": "MeddleWare <meddleware@proton.me>",
7
+ "license": "0BSD",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/meddleware-org/nft-gate-client.git"
11
+ },
12
+ "keywords": ["sui", "nft", "access-control", "web3", "move"],
13
+ "files": [
14
+ "src",
15
+ "CHANGELOG.md"
16
+ ],
17
+ "exports": {
18
+ ".": "./src/index.ts"
19
+ },
20
+ "scripts": {
21
+ "type-check": "tsc --noEmit",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest"
24
+ },
25
+ "dependencies": {
26
+ "@mysten/sui": "~2.17.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "~24.12.2",
30
+ "typescript": "~6.0.3",
31
+ "vitest": "~4.1.10"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }
@@ -0,0 +1,19 @@
1
+ import type { Challenge } from './types.js'
2
+
3
+ /**
4
+ * Fetch a fresh challenge from a gateway's `GET /v1/challenge` endpoint. Tolerates both
5
+ * `expiresAt` (camelCase) and `expires_at` (snake_case) response shapes.
6
+ */
7
+ export async function fetchChallenge(
8
+ gatewayHost: string,
9
+ opts: { signal?: AbortSignal } = {},
10
+ ): Promise<Challenge> {
11
+ const res = await fetch(`${gatewayHost.replace(/\/$/, '')}/v1/challenge`, {
12
+ signal: opts.signal,
13
+ })
14
+ if (!res.ok) throw new Error(`challenge request failed: ${res.status}`)
15
+ const data = (await res.json()) as { nonce?: string; expiresAt?: number; expires_at?: number }
16
+ if (!data || typeof data.nonce !== 'string') throw new Error('challenge response missing nonce')
17
+ const expiresAt = data.expiresAt ?? data.expires_at ?? 0
18
+ return { nonce: data.nonce, expiresAt }
19
+ }
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `@meddleware/nft-gate-client` — client-side helpers for the `access_gate` primitive.
3
+ *
4
+ * Client-side only: ownership queries, purchase/consume PTB builders, challenge signing,
5
+ * and access-proof assembly. Server-side verification lives in the Rust gateway.
6
+ */
7
+
8
+ export type {
9
+ AccessGateConfig,
10
+ Challenge,
11
+ AccessProof,
12
+ OwnedAccessNft,
13
+ OwnedObjectsClient,
14
+ SuiObjectClient,
15
+ } from './types.js'
16
+
17
+ export { fetchAccessNfts, ownsAccessNft, parseOwnedAccessNft, fetchAccessNftById } from './ownership.js'
18
+ export { buildPurchaseTx, buildConsumeTx, buildCreateGateTx } from './ptb.js'
19
+ export { fetchChallenge } from './challenge.js'
20
+ export {
21
+ personalMessageForNonce,
22
+ encodeAccessProof,
23
+ decodeAccessProof,
24
+ buildAccessProof,
25
+ } from './proof.js'
26
+ export type { PersonalMessageSigner } from './proof.js'
@@ -0,0 +1,99 @@
1
+ import type { OwnedAccessNft, OwnedObjectsClient, SuiObjectClient } from './types.js'
2
+
3
+ /* eslint-disable @typescript-eslint/no-explicit-any */
4
+
5
+ /**
6
+ * Extract `uses_remaining` from a Move enum `AccessVariant` as rendered by RPC, driven by the
7
+ * enum **variant tag** (typed) rather than guessing from field presence. Sui renders a Move
8
+ * enum as `{ variant: 'SingleUse' | 'UnlimitedPass', fields: {...} }`. Returns `null` for an
9
+ * unlimited pass; the remaining count for a single-use.
10
+ */
11
+ function parseUsesRemaining(variant: any): number | null {
12
+ if (variant == null) return null
13
+ const tag: string | undefined = variant.variant ?? variant.type ?? variant.$kind
14
+ if (tag === 'UnlimitedPass') return null
15
+ const fields = variant.fields ?? variant
16
+ const ur = fields?.uses_remaining ?? fields?.SingleUse?.uses_remaining
17
+ if (ur != null) return Number(ur)
18
+ // Tagged SingleUse but the count is missing from this node's rendering — unknown, not a pass.
19
+ return tag === 'SingleUse' ? null : null
20
+ }
21
+
22
+ /** True if `type` names an `access_gate` NFT struct (transferable or soulbound). */
23
+ function isAccessNftType(type: unknown): boolean {
24
+ return typeof type === 'string' && /::access_gate::(Soulbound)?AccessNFT\b/.test(type)
25
+ }
26
+
27
+ /**
28
+ * Parse a single `getOwnedObjects`/`getObject` entry into an {@link OwnedAccessNft}, or `null`
29
+ * if it is not an access NFT. Validates the object **type** when present (typed), and reads the
30
+ * nested `data.fields` deterministically.
31
+ */
32
+ export function parseOwnedAccessNft(entry: any): OwnedAccessNft | null {
33
+ const obj = entry?.data ?? entry
34
+ const objectId: string | undefined = obj?.objectId ?? obj?.content?.fields?.id?.id
35
+ const type: unknown = obj?.type ?? obj?.content?.type
36
+ // When the type is present it MUST be an access NFT; when absent (some node shapes) fall back
37
+ // to structural checks below.
38
+ if (type !== undefined && !isAccessNftType(type)) return null
39
+ const inner = obj?.content?.fields?.data?.fields
40
+ const gateId: string | undefined = inner?.gate_id ?? inner?.gateId
41
+ if (!objectId || !gateId) return null
42
+ return {
43
+ objectId,
44
+ gateId,
45
+ usesRemaining: parseUsesRemaining(inner?.variant),
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Typed single-object read of one access NFT by id (`getObject` with `showType`+`showContent`),
51
+ * used when a UI needs the **exact** `usesRemaining` reliably rather than the best-effort parse
52
+ * of an owned-objects page. Returns `null` if the object is missing or not an access NFT.
53
+ */
54
+ export async function fetchAccessNftById(
55
+ client: SuiObjectClient,
56
+ objectId: string,
57
+ ): Promise<OwnedAccessNft | null> {
58
+ const res = await client.getObject({
59
+ id: objectId,
60
+ options: { showType: true, showContent: true },
61
+ })
62
+ return parseOwnedAccessNft(res)
63
+ }
64
+
65
+ /**
66
+ * Fetch all access NFTs of `nftType` owned by `owner`, optionally restricted to a specific
67
+ * `gateId`. Uses `getOwnedObjects` filtered by `StructType` (the standard owned-objects query).
68
+ */
69
+ export async function fetchAccessNfts(
70
+ client: OwnedObjectsClient,
71
+ owner: string,
72
+ nftType: string,
73
+ gateId?: string,
74
+ ): Promise<OwnedAccessNft[]> {
75
+ const { data } = await client.getOwnedObjects({
76
+ owner,
77
+ filter: { StructType: nftType },
78
+ options: { showContent: true, showType: true },
79
+ })
80
+ const parsed = (data ?? [])
81
+ .map(parseOwnedAccessNft)
82
+ .filter((n): n is OwnedAccessNft => n !== null)
83
+ return gateId ? parsed.filter((n) => n.gateId === gateId) : parsed
84
+ }
85
+
86
+ /**
87
+ * True if `owner` holds at least one access NFT of `nftType` (optionally for `gateId`).
88
+ * This is the cheap check a frontend runs to decide whether to show a gated option, and a
89
+ * gateway runs (server-side) as part of access verification.
90
+ */
91
+ export async function ownsAccessNft(
92
+ client: OwnedObjectsClient,
93
+ owner: string,
94
+ nftType: string,
95
+ gateId?: string,
96
+ ): Promise<boolean> {
97
+ const nfts = await fetchAccessNfts(client, owner, nftType, gateId)
98
+ return nfts.length > 0
99
+ }
package/src/proof.ts ADDED
@@ -0,0 +1,57 @@
1
+ import type { AccessProof, Challenge } from './types.js'
2
+
3
+ /**
4
+ * The exact bytes a wallet signs (as a personal message) to answer a challenge. Both the
5
+ * client (signing) and the gateway (verifying) MUST derive the message identically.
6
+ */
7
+ export function personalMessageForNonce(nonce: string): Uint8Array {
8
+ return new TextEncoder().encode(`nft-gate:access:${nonce}`)
9
+ }
10
+
11
+ function toBase64(s: string): string {
12
+ // Works in browsers and modern Node (globalThis.btoa is available on Node >= 16).
13
+ if (typeof btoa === 'function') return btoa(s)
14
+ return Buffer.from(s, 'utf-8').toString('base64')
15
+ }
16
+
17
+ function fromBase64(s: string): string {
18
+ if (typeof atob === 'function') return atob(s)
19
+ return Buffer.from(s, 'base64').toString('utf-8')
20
+ }
21
+
22
+ /** Encode a proof as the compact Bearer token carried in the relay auth header. */
23
+ export function encodeAccessProof(proof: AccessProof): string {
24
+ return toBase64(JSON.stringify(proof))
25
+ }
26
+
27
+ /** Decode a proof token produced by {@link encodeAccessProof}. Throws on malformed input. */
28
+ export function decodeAccessProof(token: string): AccessProof {
29
+ const raw = JSON.parse(fromBase64(token)) as Partial<AccessProof>
30
+ if (!raw || typeof raw.address !== 'string' || typeof raw.nonce !== 'string' || typeof raw.signature !== 'string') {
31
+ throw new Error('malformed access proof')
32
+ }
33
+ const proof: AccessProof = { address: raw.address, nonce: raw.nonce, signature: raw.signature }
34
+ if (typeof raw.consumeDigest === 'string') proof.consumeDigest = raw.consumeDigest
35
+ return proof
36
+ }
37
+
38
+ /** A wallet-provided personal-message signer (e.g. wallet-standard `sui:signPersonalMessage`). */
39
+ export type PersonalMessageSigner = (message: Uint8Array) => Promise<{ signature: string }>
40
+
41
+ /**
42
+ * Sign a challenge and assemble the encoded access-proof token to hand to any gateway as its
43
+ * auth bearer (e.g. an upload-relay client's auth-token option, an `Authorization` header).
44
+ */
45
+ export async function buildAccessProof(opts: {
46
+ address: string
47
+ challenge: Challenge
48
+ sign: PersonalMessageSigner
49
+ /** Present for single-use gates: the `consume` tx digest. */
50
+ consumeDigest?: string
51
+ }): Promise<string> {
52
+ const message = personalMessageForNonce(opts.challenge.nonce)
53
+ const { signature } = await opts.sign(message)
54
+ const proof: AccessProof = { address: opts.address, nonce: opts.challenge.nonce, signature }
55
+ if (opts.consumeDigest) proof.consumeDigest = opts.consumeDigest
56
+ return encodeAccessProof(proof)
57
+ }
package/src/ptb.ts ADDED
@@ -0,0 +1,65 @@
1
+ import { Transaction } from '@mysten/sui/transactions'
2
+ import type { AccessGateConfig } from './types.js'
3
+
4
+ /**
5
+ * Build a PTB that purchases access: split `priceMist` from the gas coin and call
6
+ * `access_gate::purchase(gate, payment)`. Overpayment is refunded on-chain, so the split
7
+ * must be exactly the price. The caller signs + executes with their wallet.
8
+ */
9
+ export function buildPurchaseTx(cfg: AccessGateConfig, priceMist: bigint | number): Transaction {
10
+ const tx = new Transaction()
11
+ const [payment] = tx.splitCoins(tx.gas, [tx.pure.u64(priceMist)])
12
+ tx.moveCall({
13
+ target: `${cfg.packageId}::access_gate::purchase`,
14
+ arguments: [tx.object(cfg.gateId), payment],
15
+ })
16
+ return tx
17
+ }
18
+
19
+ /**
20
+ * Build a PTB that consumes one use of a single-use NFT, binding it to `nonce`. Selects
21
+ * `consume` or `consume_soulbound` from `cfg.soulbound`. For unlimited passes there is
22
+ * nothing to consume — do not call this.
23
+ */
24
+ export function buildConsumeTx(
25
+ cfg: AccessGateConfig,
26
+ nftId: string,
27
+ nonce: string,
28
+ ): Transaction {
29
+ const tx = new Transaction()
30
+ const fn = cfg.soulbound ? 'consume_soulbound' : 'consume'
31
+ const nonceBytes = Array.from(new TextEncoder().encode(nonce))
32
+ tx.moveCall({
33
+ target: `${cfg.packageId}::access_gate::${fn}`,
34
+ arguments: [tx.object(nftId), tx.object(cfg.gateId), tx.pure.vector('u8', nonceBytes)],
35
+ })
36
+ return tx
37
+ }
38
+
39
+ /**
40
+ * Build a PTB that creates a new gate. Mostly for tooling/operators; the frontend usually
41
+ * only purchases/consumes an existing gate.
42
+ */
43
+ export function buildCreateGateTx(
44
+ packageId: string,
45
+ opts: {
46
+ priceMist: bigint | number
47
+ paymentRecipient: string
48
+ defaultUses: bigint | number
49
+ soulbound: boolean
50
+ autoBurnAtZero: boolean
51
+ },
52
+ ): Transaction {
53
+ const tx = new Transaction()
54
+ tx.moveCall({
55
+ target: `${packageId}::access_gate::create_gate`,
56
+ arguments: [
57
+ tx.pure.u64(opts.priceMist),
58
+ tx.pure.address(opts.paymentRecipient),
59
+ tx.pure.u64(opts.defaultUses),
60
+ tx.pure.bool(opts.soulbound),
61
+ tx.pure.bool(opts.autoBurnAtZero),
62
+ ],
63
+ })
64
+ return tx
65
+ }
package/src/types.ts ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Shared wire + config types for the access-gate client.
3
+ *
4
+ * These mirror the on-chain `access_gate` Move package and the challenge/proof wire
5
+ * protocol implemented by the generic Rust gateway. Keep the three in sync.
6
+ */
7
+
8
+ /** Identifies a deployed gate and the NFT type that satisfies it. */
9
+ export interface AccessGateConfig {
10
+ /** Published `access_gate` package ID. */
11
+ packageId: string
12
+ /** The shared `Gate` object ID. */
13
+ gateId: string
14
+ /**
15
+ * Fully-qualified NFT type string to filter ownership by, e.g.
16
+ * `<pkg>::access_gate::AccessNFT` or `<pkg>::access_gate::SoulboundAccessNFT`.
17
+ * Choose the variant matching the gate's `soulbound` flag.
18
+ */
19
+ nftType: string
20
+ /** Whether this gate mints soulbound NFTs (selects `consume` vs `consume_soulbound`). */
21
+ soulbound?: boolean
22
+ }
23
+
24
+ /** A server-issued, time-bound challenge the wallet signs to prove control of an address. */
25
+ export interface Challenge {
26
+ /** Opaque nonce (as issued by the gateway; treated as a UTF-8 string end-to-end). */
27
+ nonce: string
28
+ /** Unix epoch milliseconds after which the challenge is rejected. */
29
+ expiresAt: number
30
+ }
31
+
32
+ /** The proof a client presents to a gateway to demonstrate gated access. */
33
+ export interface AccessProof {
34
+ /** The Sui address claimed by the caller. */
35
+ address: string
36
+ /** The challenge nonce that was signed. */
37
+ nonce: string
38
+ /** Base64 personal-message signature over {@link personalMessageForNonce}. */
39
+ signature: string
40
+ /**
41
+ * For single-use gates: the digest of the on-chain `consume(nft, nonce)` transaction, so
42
+ * the gateway can confirm the matching `AccessConsumedEvent` before allowing the request.
43
+ */
44
+ consumeDigest?: string
45
+ }
46
+
47
+ /** A parsed owned access NFT. */
48
+ export interface OwnedAccessNft {
49
+ objectId: string
50
+ gateId: string
51
+ /** `null` for an unlimited pass; otherwise remaining single-use count. */
52
+ usesRemaining: number | null
53
+ }
54
+
55
+ /** Minimal structural subset of a Sui client used for ownership queries (grpc or json-rpc). */
56
+ export interface OwnedObjectsClient {
57
+ getOwnedObjects(params: {
58
+ owner: string
59
+ filter?: { StructType: string }
60
+ options?: { showContent?: boolean; showType?: boolean }
61
+ cursor?: string | null
62
+ limit?: number | null
63
+ }): Promise<{ data: unknown[]; hasNextPage?: boolean; nextCursor?: string | null }>
64
+ }
65
+
66
+ /** Minimal structural subset of a Sui client used for a typed single-object read. */
67
+ export interface SuiObjectClient {
68
+ getObject(params: {
69
+ id: string
70
+ options?: { showContent?: boolean; showType?: boolean }
71
+ }): Promise<unknown>
72
+ }