@gesturegate/sdk 0.1.0

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.
@@ -0,0 +1,146 @@
1
+ import { type Address, type Hex, type PublicClient } from "viem";
2
+ export declare const monadTestnetChainId = 10143;
3
+ export declare const gestureGateIntentDomain: {
4
+ readonly name: "GestureGate";
5
+ readonly version: "1";
6
+ readonly chainId: 10143;
7
+ };
8
+ export declare const gestureGateIntentTypes: {
9
+ readonly GateIntent: readonly [{
10
+ readonly name: "subject";
11
+ readonly type: "address";
12
+ }, {
13
+ readonly name: "action";
14
+ readonly type: "string";
15
+ }, {
16
+ readonly name: "nonce";
17
+ readonly type: "bytes32";
18
+ }, {
19
+ readonly name: "expiresAt";
20
+ readonly type: "uint256";
21
+ }];
22
+ };
23
+ export declare const gestureGateRegistryAbi: readonly [{
24
+ readonly type: "function";
25
+ readonly name: "isVerifiedWithin";
26
+ readonly stateMutability: "view";
27
+ readonly inputs: readonly [{
28
+ readonly name: "subject";
29
+ readonly type: "address";
30
+ }, {
31
+ readonly name: "maxAge";
32
+ readonly type: "uint256";
33
+ }];
34
+ readonly outputs: readonly [{
35
+ readonly type: "bool";
36
+ }];
37
+ }];
38
+ export type SignTypedData = (args: {
39
+ domain: typeof gestureGateIntentDomain;
40
+ types: typeof gestureGateIntentTypes;
41
+ primaryType: "GateIntent";
42
+ message: {
43
+ subject: Address;
44
+ action: "REQUEST_CHALLENGE";
45
+ nonce: Hex;
46
+ expiresAt: bigint;
47
+ };
48
+ }) => Promise<Hex>;
49
+ export type HostedStart = {
50
+ challengeId: string;
51
+ expiresAt: string;
52
+ transactionHash: Hex;
53
+ verifyUrl: string;
54
+ };
55
+ export type HostedVerifiedMessage = {
56
+ type: "gesturegate:verified";
57
+ challengeId: string;
58
+ transactionHash: Hex;
59
+ };
60
+ export type VerificationCheck = {
61
+ publicClient: PublicClient;
62
+ registryAddress: Address;
63
+ address: Address;
64
+ ttlSeconds: number;
65
+ };
66
+ export type VerificationStatus = Pick<VerificationCheck, "address" | "ttlSeconds"> & {
67
+ isVerified: boolean;
68
+ };
69
+ export type EnsureVerifiedOptions = VerificationCheck & {
70
+ apiOrigin: string;
71
+ signTypedData: SignTypedData;
72
+ popupName?: string;
73
+ popupFeatures?: string;
74
+ };
75
+ export type GestureGateClientOptions = Omit<VerificationCheck, "address" | "ttlSeconds"> & {
76
+ apiOrigin: string;
77
+ defaultTtlSeconds: number;
78
+ };
79
+ export declare function createChallengeIntent(subject: Address, signTypedData: SignTypedData, now?: number): Promise<{
80
+ subject: `0x${string}`;
81
+ nonce: `0x${string}`;
82
+ expiresAt: string;
83
+ signature: `0x${string}`;
84
+ }>;
85
+ export declare function startHostedVerification(options: {
86
+ apiOrigin: string;
87
+ address: Address;
88
+ signTypedData: SignTypedData;
89
+ }): Promise<{
90
+ challengeId: string;
91
+ expiresAt: string;
92
+ transactionHash: `0x${string}`;
93
+ verifyUrl: string;
94
+ }>;
95
+ export declare function isVerifiedWithin(options: VerificationCheck): Promise<boolean>;
96
+ /** Returns an explicit address-bound status for application UI and guards. */
97
+ export declare function getVerificationStatus(options: VerificationCheck): Promise<VerificationStatus>;
98
+ export declare function openHostedVerification(options: {
99
+ apiOrigin: string;
100
+ address: Address;
101
+ signTypedData: SignTypedData;
102
+ popupName?: string;
103
+ popupFeatures?: string;
104
+ }): Promise<HostedVerifiedMessage>;
105
+ /**
106
+ * The complete dApp lifecycle: check the registry, open the hosted flow only
107
+ * when stale, then read the registry again before the protected action.
108
+ */
109
+ export declare function ensureVerified(options: EnsureVerifiedOptions): Promise<{
110
+ status: VerificationStatus;
111
+ wasAlreadyVerified: boolean;
112
+ transactionHash: undefined;
113
+ } | {
114
+ status: VerificationStatus;
115
+ wasAlreadyVerified: boolean;
116
+ transactionHash: `0x${string}`;
117
+ }>;
118
+ /**
119
+ * A framework-neutral facade. Construct it once with shared network settings;
120
+ * each call only needs the user's address and, for verification, their signer.
121
+ */
122
+ export declare function createGestureGateClient(options: GestureGateClientOptions): Readonly<{
123
+ check: (address: Address, ttlSeconds?: number) => Promise<VerificationStatus>;
124
+ open: (input: {
125
+ address: Address;
126
+ signTypedData: SignTypedData;
127
+ popupName?: string;
128
+ popupFeatures?: string;
129
+ }) => Promise<HostedVerifiedMessage>;
130
+ ensureVerified: (input: {
131
+ address: Address;
132
+ signTypedData: SignTypedData;
133
+ ttlSeconds?: number;
134
+ popupName?: string;
135
+ popupFeatures?: string;
136
+ }) => Promise<{
137
+ status: VerificationStatus;
138
+ wasAlreadyVerified: boolean;
139
+ transactionHash: undefined;
140
+ } | {
141
+ status: VerificationStatus;
142
+ wasAlreadyVerified: boolean;
143
+ transactionHash: `0x${string}`;
144
+ }>;
145
+ }>;
146
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,170 @@
1
+ import { keccak256, toHex } from "viem";
2
+ export const monadTestnetChainId = 10143;
3
+ export const gestureGateIntentDomain = {
4
+ name: "GestureGate",
5
+ version: "1",
6
+ chainId: monadTestnetChainId,
7
+ };
8
+ export const gestureGateIntentTypes = {
9
+ GateIntent: [
10
+ { name: "subject", type: "address" },
11
+ { name: "action", type: "string" },
12
+ { name: "nonce", type: "bytes32" },
13
+ { name: "expiresAt", type: "uint256" },
14
+ ],
15
+ };
16
+ export const gestureGateRegistryAbi = [{
17
+ type: "function",
18
+ name: "isVerifiedWithin",
19
+ stateMutability: "view",
20
+ inputs: [{ name: "subject", type: "address" }, { name: "maxAge", type: "uint256" }],
21
+ outputs: [{ type: "bool" }],
22
+ }];
23
+ function randomNonce() {
24
+ if (!globalThis.crypto?.getRandomValues)
25
+ throw new Error("A secure browser crypto API is required to start GestureGate verification.");
26
+ const bytes = new Uint8Array(32);
27
+ globalThis.crypto.getRandomValues(bytes);
28
+ return keccak256(toHex(bytes));
29
+ }
30
+ function apiPath(apiOrigin, path) {
31
+ return `${apiOrigin.replace(/\/$/, "")}${path}`;
32
+ }
33
+ export async function createChallengeIntent(subject, signTypedData, now = Date.now()) {
34
+ const expiresAt = BigInt(Math.floor(now / 1_000) + 180);
35
+ const nonce = randomNonce();
36
+ const signature = await signTypedData({
37
+ domain: gestureGateIntentDomain,
38
+ types: gestureGateIntentTypes,
39
+ primaryType: "GateIntent",
40
+ message: { subject, action: "REQUEST_CHALLENGE", nonce, expiresAt },
41
+ });
42
+ return { subject, nonce, expiresAt: expiresAt.toString(), signature };
43
+ }
44
+ export async function startHostedVerification(options) {
45
+ const intent = await createChallengeIntent(options.address, options.signTypedData);
46
+ const response = await fetch(apiPath(options.apiOrigin, "/api/challenges"), {
47
+ method: "POST",
48
+ headers: { "content-type": "application/json" },
49
+ body: JSON.stringify(intent),
50
+ });
51
+ const result = await response.json();
52
+ if (!response.ok)
53
+ throw new Error(result.error ?? "GestureGate could not start verification.");
54
+ if (!result.challengeId || !result.expiresAt || !result.transactionHash || !result.verifyUrl) {
55
+ throw new Error("GestureGate returned an incomplete verification session.");
56
+ }
57
+ // The API also returns an inline-flow session token. Hosted partners never
58
+ // need it, so deliberately keep it out of the SDK result.
59
+ return {
60
+ challengeId: result.challengeId,
61
+ expiresAt: result.expiresAt,
62
+ transactionHash: result.transactionHash,
63
+ verifyUrl: result.verifyUrl,
64
+ };
65
+ }
66
+ export async function isVerifiedWithin(options) {
67
+ if (!Number.isInteger(options.ttlSeconds) || options.ttlSeconds <= 0) {
68
+ throw new Error("ttlSeconds must be a positive whole number.");
69
+ }
70
+ return options.publicClient.readContract({
71
+ address: options.registryAddress,
72
+ abi: gestureGateRegistryAbi,
73
+ functionName: "isVerifiedWithin",
74
+ args: [options.address, BigInt(options.ttlSeconds)],
75
+ });
76
+ }
77
+ /** Returns an explicit address-bound status for application UI and guards. */
78
+ export async function getVerificationStatus(options) {
79
+ return {
80
+ address: options.address,
81
+ ttlSeconds: options.ttlSeconds,
82
+ isVerified: await isVerifiedWithin(options),
83
+ };
84
+ }
85
+ export async function openHostedVerification(options) {
86
+ if (typeof window === "undefined")
87
+ throw new Error("Hosted verification must be opened in the user's browser.");
88
+ // Opening this synchronously keeps browsers from blocking the popup while the
89
+ // wallet signature is pending.
90
+ const popup = window.open("about:blank", options.popupName ?? "gesturegate_verify", options.popupFeatures ?? "popup,width=520,height=760");
91
+ if (!popup)
92
+ throw new Error("Popup was blocked. Allow popups to verify with GestureGate.");
93
+ try {
94
+ const start = await startHostedVerification(options);
95
+ popup.location.assign(start.verifyUrl);
96
+ const expectedOrigin = new URL(options.apiOrigin).origin;
97
+ return await new Promise((resolve, reject) => {
98
+ const closeTimer = window.setInterval(() => {
99
+ if (popup.closed) {
100
+ cleanup();
101
+ reject(new Error("GestureGate verification window was closed."));
102
+ }
103
+ }, 500);
104
+ const timeout = window.setTimeout(() => {
105
+ cleanup();
106
+ reject(new Error("GestureGate verification expired. Start a new challenge."));
107
+ }, Math.max(1_000, Number(start.expiresAt) * 1_000 - Date.now() + 5_000));
108
+ const onMessage = (event) => {
109
+ if (event.origin !== expectedOrigin || event.data?.type !== "gesturegate:verified")
110
+ return;
111
+ cleanup();
112
+ resolve(event.data);
113
+ };
114
+ const cleanup = () => {
115
+ window.clearInterval(closeTimer);
116
+ window.clearTimeout(timeout);
117
+ window.removeEventListener("message", onMessage);
118
+ };
119
+ window.addEventListener("message", onMessage);
120
+ });
121
+ }
122
+ catch (error) {
123
+ popup.close();
124
+ throw error;
125
+ }
126
+ }
127
+ /**
128
+ * The complete dApp lifecycle: check the registry, open the hosted flow only
129
+ * when stale, then read the registry again before the protected action.
130
+ */
131
+ export async function ensureVerified(options) {
132
+ const before = await getVerificationStatus(options);
133
+ if (before.isVerified)
134
+ return { status: before, wasAlreadyVerified: true, transactionHash: undefined };
135
+ const completion = await openHostedVerification(options);
136
+ const status = await getVerificationStatus(options);
137
+ if (!status.isVerified) {
138
+ throw new Error("GestureGate completed but the verification is not fresh onchain. Please retry your action.");
139
+ }
140
+ return { status, wasAlreadyVerified: false, transactionHash: completion.transactionHash };
141
+ }
142
+ /**
143
+ * A framework-neutral facade. Construct it once with shared network settings;
144
+ * each call only needs the user's address and, for verification, their signer.
145
+ */
146
+ export function createGestureGateClient(options) {
147
+ if (!Number.isInteger(options.defaultTtlSeconds) || options.defaultTtlSeconds <= 0) {
148
+ throw new Error("defaultTtlSeconds must be a positive whole number.");
149
+ }
150
+ const check = (address, ttlSeconds = options.defaultTtlSeconds) => getVerificationStatus({
151
+ publicClient: options.publicClient,
152
+ registryAddress: options.registryAddress,
153
+ address,
154
+ ttlSeconds,
155
+ });
156
+ return Object.freeze({
157
+ check,
158
+ open: (input) => openHostedVerification({
159
+ apiOrigin: options.apiOrigin,
160
+ ...input,
161
+ }),
162
+ ensureVerified: (input) => ensureVerified({
163
+ publicClient: options.publicClient,
164
+ registryAddress: options.registryAddress,
165
+ apiOrigin: options.apiOrigin,
166
+ ttlSeconds: input.ttlSeconds ?? options.defaultTtlSeconds,
167
+ ...input,
168
+ }),
169
+ });
170
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@gesturegate/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Framework-neutral hosted verification client for GestureGate on Monad.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "keywords": ["captcha", "human-verification", "monad", "viem", "web3"],
9
+ "files": ["dist"],
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ }
17
+ },
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "peerDependencies": {
25
+ "viem": ">=2.0.0"
26
+ }
27
+ }