From c51df18f0ed948b66fce2b3ce68fdb1b6f992931 Mon Sep 17 00:00:00 2001 From: Paolo D'Amico Date: Thu, 7 Sep 2023 22:10:35 -1000 Subject: [PATCH 1/8] Revert "Revert "feat: improve validation (#371)" (#378)" This reverts commit 80e826a3f0e6a626a2d5a551326f8317782f7d41. --- .../default/tables/public_nullifier.yaml | 14 +- .../down.sql | 3 + .../up.sql | 4 + .../graphql/fetchUser.generated.ts | 2 +- web/src/backend/utils.ts | 11 +- web/src/backend/verify.ts | 20 +- .../graphql/fetch-user.generated.ts | 2 +- web/src/graphql/graphql.schema.json | 1237 ++++++++++++++++- web/src/graphql/graphql.ts | 173 ++- web/src/lib/models.ts | 2 +- web/src/pages/api/v1/oidc/authorize.ts | 15 +- web/src/pages/api/v1/precheck/[app_id].ts | 20 +- web/src/pages/api/v1/verify/[app_id].ts | 160 ++- web/src/scenes/team/MemberList/index.tsx | 2 +- .../scenes/team/graphql/teams.generated.ts | 2 +- web/tests/api/precheck.test.ts | 15 +- web/tests/api/utils.test.ts | 19 +- web/tests/api/verify.test.ts | 2 +- web/tests/integration/verify.test.ts | 188 +++ 19 files changed, 1725 insertions(+), 166 deletions(-) create mode 100644 hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/down.sql create mode 100644 hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/up.sql create mode 100644 web/tests/integration/verify.test.ts diff --git a/hasura/metadata/databases/default/tables/public_nullifier.yaml b/hasura/metadata/databases/default/tables/public_nullifier.yaml index bb01eb09b..7d8e2fe07 100644 --- a/hasura/metadata/databases/default/tables/public_nullifier.yaml +++ b/hasura/metadata/databases/default/tables/public_nullifier.yaml @@ -12,7 +12,7 @@ insert_permissions: columns: - action_id - id - - merkle_root + - uses - nullifier_hash - credential_type select_permissions: @@ -30,7 +30,6 @@ select_permissions: - action_id - created_at - id - - merkle_root - nullifier_hash - credential_type filter: @@ -44,7 +43,7 @@ select_permissions: - action_id - created_at - id - - merkle_root + - uses - nullifier_hash - credential_type filter: {} @@ -54,7 +53,7 @@ select_permissions: - action_id - created_at - id - - merkle_root + - uses - nullifier_hash - credential_type filter: @@ -64,3 +63,10 @@ select_permissions: users: id: _eq: X-Hasura-User-Id +update_permissions: + - role: service + permission: + columns: + - uses + filter: {} + check: null diff --git a/hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/down.sql b/hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/down.sql new file mode 100644 index 000000000..3f069da43 --- /dev/null +++ b/hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/down.sql @@ -0,0 +1,3 @@ +ALTER TABLE "public"."nullifier" + DROP COLUMN "uses", + DROP CONSTRAINT "unique_nullifier_hash"; diff --git a/hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/up.sql b/hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/up.sql new file mode 100644 index 000000000..41b833a45 --- /dev/null +++ b/hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/up.sql @@ -0,0 +1,4 @@ +ALTER TABLE "public"."nullifier" + DROP COLUMN "merkle_root", + ADD COLUMN "uses" INT DEFAULT 1, + ADD CONSTRAINT "unique_nullifier_hash" UNIQUE (nullifier_hash); diff --git a/web/src/api/invite-team-members/graphql/fetchUser.generated.ts b/web/src/api/invite-team-members/graphql/fetchUser.generated.ts index 302274fb6..6aa04a9be 100644 --- a/web/src/api/invite-team-members/graphql/fetchUser.generated.ts +++ b/web/src/api/invite-team-members/graphql/fetchUser.generated.ts @@ -14,7 +14,7 @@ export type FetchUserQuery = { __typename?: "user"; id: string; name: string; - email: string; + email?: string | null; team: { __typename?: "team"; id: string; name?: string | null }; }>; }; diff --git a/web/src/backend/utils.ts b/web/src/backend/utils.ts index 2556ea238..28bf12410 100644 --- a/web/src/backend/utils.ts +++ b/web/src/backend/utils.ts @@ -130,14 +130,15 @@ export const protectConsumerBackendEndpoint = ( * Checks whether the person can be verified for a particular action based on the max number of verifications */ export const canVerifyForAction = ( - nullifiers: - | Array<{ + nullifier: + | { + uses: number; nullifier_hash: string; - }> + } | undefined, max_verifications_per_person: number ): boolean => { - if (!nullifiers?.length) { + if (!nullifier) { // Person has not verified before, can always verify for the first time return true; } else if (max_verifications_per_person <= 0) { @@ -146,7 +147,7 @@ export const canVerifyForAction = ( } // Else, can only verify if the max number of verifications has not been met - return (nullifiers?.length ?? 0) < max_verifications_per_person; + return nullifier.uses < max_verifications_per_person; }; export const reportAPIEventToPostHog = async ( diff --git a/web/src/backend/verify.ts b/web/src/backend/verify.ts index 569326799..ae5f9622a 100644 --- a/web/src/backend/verify.ts +++ b/web/src/backend/verify.ts @@ -24,7 +24,7 @@ const KNOWN_ERROR_CODES = [ }, ]; -interface IInputParams { +export interface IInputParams { merkle_root: string; signal: string; nullifier_hash: string; @@ -32,7 +32,7 @@ interface IInputParams { proof: string; } -interface IVerifyParams { +export interface IVerifyParams { is_staging: boolean; credential_type: CredentialType; } @@ -48,6 +48,8 @@ interface IAppAction { status: string; external_nullifier: string; nullifiers: { + uses: number; + created_at: string; nullifier_hash: string; }[]; max_verifications: number; @@ -78,6 +80,8 @@ const queryFetchAppAction = gql` external_nullifier status nullifiers(where: { nullifier_hash: { _eq: $nullifier_hash } }) { + uses + created_at nullifier_hash } } @@ -123,8 +127,7 @@ export const fetchActionForProof = async ( if (!result.data.app.length) { return { error: { - message: - "We couldn't find an app with this ID. App may be no longer active.", + message: "App not found. App may be no longer active.", code: "not_found", statusCode: 404, }, @@ -136,7 +139,7 @@ export const fetchActionForProof = async ( if (!app.actions.length) { return { error: { - message: "We couldn't find the relevant action.", + message: "Action not found.", code: "invalid_action", statusCode: 400, attribute: "action", @@ -156,7 +159,12 @@ export const fetchActionForProof = async ( } return { - app: { ...app, action: app.actions[0], actions: undefined }, + app: { + ...app, + actions: undefined, + action: app.actions[0], + nullifier: app.actions[0]?.nullifiers?.[0], + }, }; }; diff --git a/web/src/components/Layout/LoggedUserDisplay/graphql/fetch-user.generated.ts b/web/src/components/Layout/LoggedUserDisplay/graphql/fetch-user.generated.ts index 8be2a9bd5..40316ac10 100644 --- a/web/src/components/Layout/LoggedUserDisplay/graphql/fetch-user.generated.ts +++ b/web/src/components/Layout/LoggedUserDisplay/graphql/fetch-user.generated.ts @@ -13,7 +13,7 @@ export type FetchUserQuery = { user: Array<{ __typename?: "user"; id: string; - email: string; + email?: string | null; name: string; team: { __typename?: "team"; id: string; name?: string | null }; }>; diff --git a/web/src/graphql/graphql.schema.json b/web/src/graphql/graphql.schema.json index 1ae6cd45e..ae00ea036 100644 --- a/web/src/graphql/graphql.schema.json +++ b/web/src/graphql/graphql.schema.json @@ -3246,7 +3246,7 @@ "enumValues": [ { "name": "action_app_id_action_key", - "description": "unique or primary key constraint on columns \"app_id\", \"action\"", + "description": "unique or primary key constraint on columns \"action\", \"app_id\"", "isDeprecated": false, "deprecationReason": null }, @@ -8567,6 +8567,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "verified_app_logo", + "description": "A computed field, executes function \"get_verified_app_logo\"", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "verified_at", "description": null, @@ -8829,6 +8841,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "verified_app_logo", + "description": "A computed field, executes function \"get_verified_app_logo\"", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "verified_at", "description": null, @@ -11497,6 +11521,30 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "code_challenge", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "code_challenge_method", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "created_at", "description": null, @@ -11561,6 +11609,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nonce", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "nullifier_hash", "description": null, @@ -11860,6 +11920,30 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "code_challenge", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "String_comparison_exp", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "code_challenge_method", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "String_comparison_exp", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "created_at", "description": null, @@ -11908,6 +11992,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nonce", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "String_comparison_exp", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "nullifier_hash", "description": null, @@ -12073,6 +12169,30 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "code_challenge", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "code_challenge_method", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "created_at", "description": null, @@ -12121,6 +12241,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nonce", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "nullifier_hash", "description": null, @@ -12191,6 +12323,30 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "code_challenge", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "code_challenge_method", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "created_at", "description": null, @@ -12239,6 +12395,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nonce", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "nullifier_hash", "description": null, @@ -12298,6 +12466,30 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "code_challenge", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "code_challenge_method", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "created_at", "description": null, @@ -12346,6 +12538,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nonce", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "nullifier_hash", "description": null, @@ -12520,6 +12724,30 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "code_challenge", + "description": null, + "type": { + "kind": "ENUM", + "name": "order_by", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "code_challenge_method", + "description": null, + "type": { + "kind": "ENUM", + "name": "order_by", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "created_at", "description": null, @@ -12568,6 +12796,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nonce", + "description": null, + "type": { + "kind": "ENUM", + "name": "order_by", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "nullifier_hash", "description": null, @@ -12679,6 +12919,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "code_challenge", + "description": "column name", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "code_challenge_method", + "description": "column name", + "isDeprecated": false, + "deprecationReason": null + }, { "name": "created_at", "description": "column name", @@ -12703,6 +12955,12 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nonce", + "description": "column name", + "isDeprecated": false, + "deprecationReason": null + }, { "name": "nullifier_hash", "description": "column name", @@ -12755,11 +13013,11 @@ "deprecationReason": null }, { - "name": "created_at", + "name": "code_challenge", "description": null, "type": { "kind": "SCALAR", - "name": "timestamptz", + "name": "String", "ofType": null }, "defaultValue": null, @@ -12767,7 +13025,7 @@ "deprecationReason": null }, { - "name": "credential_type", + "name": "code_challenge_method", "description": null, "type": { "kind": "SCALAR", @@ -12779,7 +13037,7 @@ "deprecationReason": null }, { - "name": "expires_at", + "name": "created_at", "description": null, "type": { "kind": "SCALAR", @@ -12791,7 +13049,7 @@ "deprecationReason": null }, { - "name": "id", + "name": "credential_type", "description": null, "type": { "kind": "SCALAR", @@ -12803,11 +13061,11 @@ "deprecationReason": null }, { - "name": "nullifier_hash", + "name": "expires_at", "description": null, "type": { "kind": "SCALAR", - "name": "String", + "name": "timestamptz", "ofType": null }, "defaultValue": null, @@ -12815,11 +13073,11 @@ "deprecationReason": null }, { - "name": "scope", + "name": "id", "description": null, "type": { "kind": "SCALAR", - "name": "jsonb", + "name": "String", "ofType": null }, "defaultValue": null, @@ -12827,11 +13085,47 @@ "deprecationReason": null }, { - "name": "updated_at", + "name": "nonce", "description": null, "type": { "kind": "SCALAR", - "name": "timestamptz", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nullifier_hash", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "scope", + "description": null, + "type": { + "kind": "SCALAR", + "name": "jsonb", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updated_at", + "description": null, + "type": { + "kind": "SCALAR", + "name": "timestamptz", "ofType": null }, "defaultValue": null, @@ -12912,6 +13206,30 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "code_challenge", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "code_challenge_method", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "created_at", "description": null, @@ -12960,6 +13278,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nonce", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "nullifier_hash", "description": null, @@ -13021,6 +13351,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "code_challenge", + "description": "column name", + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "code_challenge_method", + "description": "column name", + "isDeprecated": false, + "deprecationReason": null + }, { "name": "created_at", "description": "column name", @@ -13045,6 +13387,12 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "nonce", + "description": "column name", + "isDeprecated": false, + "deprecationReason": null + }, { "name": "nullifier_hash", "description": "column name", @@ -20235,6 +20583,18 @@ "name": "update_nullifier", "description": "update data of the table: \"nullifier\"", "args": [ + { + "name": "_inc", + "description": "increments the numeric columns with given value of the filtered values", + "type": { + "kind": "INPUT_OBJECT", + "name": "nullifier_inc_input", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "_set", "description": "sets the columns of the filtered rows to the given values", @@ -20276,6 +20636,18 @@ "name": "update_nullifier_by_pk", "description": "update single row of the table: \"nullifier\"", "args": [ + { + "name": "_inc", + "description": "increments the numeric columns with given value of the filtered values", + "type": { + "kind": "INPUT_OBJECT", + "name": "nullifier_inc_input", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "_set", "description": "sets the columns of the filtered rows to the given values", @@ -20984,6 +21356,18 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "uses", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -21137,6 +21521,18 @@ "name": "nullifier_aggregate_fields", "description": "aggregate fields of \"nullifier\"", "fields": [ + { + "name": "avg", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "nullifier_avg_fields", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "count", "description": null, @@ -21209,6 +21605,90 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "stddev", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "nullifier_stddev_fields", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "stddev_pop", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "nullifier_stddev_pop_fields", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "stddev_samp", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "nullifier_stddev_samp_fields", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sum", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "nullifier_sum_fields", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "var_pop", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "nullifier_var_pop_fields", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "var_samp", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "nullifier_var_samp_fields", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "variance", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "nullifier_variance_fields", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -21222,6 +21702,18 @@ "description": "order by aggregate values of table \"nullifier\"", "fields": null, "inputFields": [ + { + "name": "avg", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "nullifier_avg_order_by", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "count", "description": null, @@ -21257,56 +21749,186 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null - } - ], - "interfaces": null, - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "INPUT_OBJECT", - "name": "nullifier_arr_rel_insert_input", - "description": "input type for inserting array relation for remote table \"nullifier\"", - "fields": null, - "inputFields": [ + }, { - "name": "data", + "name": "stddev", "description": null, "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "INPUT_OBJECT", - "name": "nullifier_insert_input", - "ofType": null - } - } - } + "kind": "INPUT_OBJECT", + "name": "nullifier_stddev_order_by", + "ofType": null }, "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "on_conflict", - "description": "upsert condition", + "name": "stddev_pop", + "description": null, "type": { "kind": "INPUT_OBJECT", - "name": "nullifier_on_conflict", + "name": "nullifier_stddev_pop_order_by", "ofType": null }, "defaultValue": null, "isDeprecated": false, "deprecationReason": null - } - ], - "interfaces": null, + }, + { + "name": "stddev_samp", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "nullifier_stddev_samp_order_by", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sum", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "nullifier_sum_order_by", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "var_pop", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "nullifier_var_pop_order_by", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "var_samp", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "nullifier_var_samp_order_by", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "variance", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "nullifier_variance_order_by", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "nullifier_arr_rel_insert_input", + "description": "input type for inserting array relation for remote table \"nullifier\"", + "fields": null, + "inputFields": [ + { + "name": "data", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "nullifier_insert_input", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "on_conflict", + "description": "upsert condition", + "type": { + "kind": "INPUT_OBJECT", + "name": "nullifier_on_conflict", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "nullifier_avg_fields", + "description": "aggregate avg on columns", + "fields": [ + { + "name": "uses", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "nullifier_avg_order_by", + "description": "order by avg() on columns of table \"nullifier\"", + "fields": null, + "inputFields": [ + { + "name": "uses", + "description": null, + "type": { + "kind": "ENUM", + "name": "order_by", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, "enumValues": null, "possibleTypes": null }, @@ -21463,6 +22085,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "uses", + "description": null, + "type": { + "kind": "INPUT_OBJECT", + "name": "Int_comparison_exp", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "interfaces": null, @@ -21482,8 +22116,37 @@ "description": "unique or primary key constraint on columns \"id\"", "isDeprecated": false, "deprecationReason": null + }, + { + "name": "unique_nullifier_hash", + "description": "unique or primary key constraint on columns \"nullifier_hash\"", + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "nullifier_inc_input", + "description": "input type for incrementing numeric columns in table \"nullifier\"", + "fields": null, + "inputFields": [ + { + "name": "uses", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], + "interfaces": null, + "enumValues": null, "possibleTypes": null }, { @@ -21587,6 +22250,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "uses", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "interfaces": null, @@ -21681,6 +22356,18 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "uses", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -21777,6 +22464,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "uses", + "description": null, + "type": { + "kind": "ENUM", + "name": "order_by", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "interfaces": null, @@ -21871,6 +22570,18 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "uses", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -21967,6 +22678,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "uses", + "description": null, + "type": { + "kind": "ENUM", + "name": "order_by", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "interfaces": null, @@ -22188,6 +22911,18 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "uses", + "description": null, + "type": { + "kind": "ENUM", + "name": "order_by", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "interfaces": null, @@ -22270,6 +23005,12 @@ "description": "column name", "isDeprecated": false, "deprecationReason": null + }, + { + "name": "uses", + "description": "column name", + "isDeprecated": false, + "deprecationReason": null } ], "possibleTypes": null @@ -22317,47 +23058,197 @@ "deprecationReason": null }, { - "name": "id", + "name": "id", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "merkle_root", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nullifier_hash", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updated_at", + "description": null, + "type": { + "kind": "SCALAR", + "name": "timestamptz", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "uses", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "nullifier_stddev_fields", + "description": "aggregate stddev on columns", + "fields": [ + { + "name": "uses", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "nullifier_stddev_order_by", + "description": "order by stddev() on columns of table \"nullifier\"", + "fields": null, + "inputFields": [ + { + "name": "uses", + "description": null, + "type": { + "kind": "ENUM", + "name": "order_by", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "nullifier_stddev_pop_fields", + "description": "aggregate stddev_pop on columns", + "fields": [ + { + "name": "uses", "description": null, + "args": [], "type": { "kind": "SCALAR", - "name": "String", + "name": "Float", "ofType": null }, - "defaultValue": null, "isDeprecated": false, "deprecationReason": null - }, + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "nullifier_stddev_pop_order_by", + "description": "order by stddev_pop() on columns of table \"nullifier\"", + "fields": null, + "inputFields": [ { - "name": "merkle_root", + "name": "uses", "description": null, "type": { - "kind": "SCALAR", - "name": "String", + "kind": "ENUM", + "name": "order_by", "ofType": null }, "defaultValue": null, "isDeprecated": false, "deprecationReason": null - }, + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "nullifier_stddev_samp_fields", + "description": "aggregate stddev_samp on columns", + "fields": [ { - "name": "nullifier_hash", + "name": "uses", "description": null, + "args": [], "type": { "kind": "SCALAR", - "name": "String", + "name": "Float", "ofType": null }, - "defaultValue": null, "isDeprecated": false, "deprecationReason": null - }, + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "nullifier_stddev_samp_order_by", + "description": "order by stddev_samp() on columns of table \"nullifier\"", + "fields": null, + "inputFields": [ { - "name": "updated_at", + "name": "uses", "description": null, "type": { - "kind": "SCALAR", - "name": "timestamptz", + "kind": "ENUM", + "name": "order_by", "ofType": null }, "defaultValue": null, @@ -22497,6 +23388,64 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "uses", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "nullifier_sum_fields", + "description": "aggregate sum on columns", + "fields": [ + { + "name": "uses", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "nullifier_sum_order_by", + "description": "order by sum() on columns of table \"nullifier\"", + "fields": null, + "inputFields": [ + { + "name": "uses", + "description": null, + "type": { + "kind": "ENUM", + "name": "order_by", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "interfaces": null, @@ -22552,6 +23501,12 @@ "description": "column name", "isDeprecated": false, "deprecationReason": null + }, + { + "name": "uses", + "description": "column name", + "isDeprecated": false, + "deprecationReason": null } ], "possibleTypes": null @@ -22562,6 +23517,18 @@ "description": null, "fields": null, "inputFields": [ + { + "name": "_inc", + "description": "increments the numeric columns with given value of the filtered values", + "type": { + "kind": "INPUT_OBJECT", + "name": "nullifier_inc_input", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "_set", "description": "sets the columns of the filtered rows to the given values", @@ -22595,6 +23562,144 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "nullifier_var_pop_fields", + "description": "aggregate var_pop on columns", + "fields": [ + { + "name": "uses", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "nullifier_var_pop_order_by", + "description": "order by var_pop() on columns of table \"nullifier\"", + "fields": null, + "inputFields": [ + { + "name": "uses", + "description": null, + "type": { + "kind": "ENUM", + "name": "order_by", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "nullifier_var_samp_fields", + "description": "aggregate var_samp on columns", + "fields": [ + { + "name": "uses", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "nullifier_var_samp_order_by", + "description": "order by var_samp() on columns of table \"nullifier\"", + "fields": null, + "inputFields": [ + { + "name": "uses", + "description": null, + "type": { + "kind": "ENUM", + "name": "order_by", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "nullifier_variance_fields", + "description": "aggregate variance on columns", + "fields": [ + { + "name": "uses", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Float", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "nullifier_variance_order_by", + "description": "order by variance() on columns of table \"nullifier\"", + "fields": null, + "inputFields": [ + { + "name": "uses", + "description": null, + "type": { + "kind": "ENUM", + "name": "order_by", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "SCALAR", "name": "numeric", @@ -34125,13 +35230,9 @@ "description": null, "args": [], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "isDeprecated": false, "deprecationReason": null diff --git a/web/src/graphql/graphql.ts b/web/src/graphql/graphql.ts index 616ff2455..c99125023 100644 --- a/web/src/graphql/graphql.ts +++ b/web/src/graphql/graphql.ts @@ -289,7 +289,7 @@ export type Action_Bool_Exp = { /** unique or primary key constraints on table "action" */ export enum Action_Constraint { - /** unique or primary key constraint on columns "app_id", "action" */ + /** unique or primary key constraint on columns "action", "app_id" */ ActionAppIdActionKey = "action_app_id_action_key", /** unique or primary key constraint on columns "external_nullifier", "app_id" */ ActionAppIdExternalNullifierKey = "action_app_id_external_nullifier_key", @@ -1091,6 +1091,8 @@ export type App_Max_Fields = { status?: Maybe; team_id?: Maybe; updated_at?: Maybe; + /** A computed field, executes function "get_verified_app_logo" */ + verified_app_logo?: Maybe; verified_at?: Maybe; }; @@ -1120,6 +1122,8 @@ export type App_Min_Fields = { status?: Maybe; team_id?: Maybe; updated_at?: Maybe; + /** A computed field, executes function "get_verified_app_logo" */ + verified_app_logo?: Maybe; verified_at?: Maybe; }; @@ -1548,10 +1552,13 @@ export type Auth_Code = { __typename?: "auth_code"; app_id: Scalars["String"]; auth_code: Scalars["String"]; + code_challenge?: Maybe; + code_challenge_method?: Maybe; created_at: Scalars["timestamptz"]; credential_type: Scalars["String"]; expires_at: Scalars["timestamptz"]; id: Scalars["String"]; + nonce?: Maybe; nullifier_hash: Scalars["String"]; scope?: Maybe; updated_at: Scalars["timestamptz"]; @@ -1595,10 +1602,13 @@ export type Auth_Code_Bool_Exp = { _or?: InputMaybe>; app_id?: InputMaybe; auth_code?: InputMaybe; + code_challenge?: InputMaybe; + code_challenge_method?: InputMaybe; created_at?: InputMaybe; credential_type?: InputMaybe; expires_at?: InputMaybe; id?: InputMaybe; + nonce?: InputMaybe; nullifier_hash?: InputMaybe; scope?: InputMaybe; updated_at?: InputMaybe; @@ -1629,10 +1639,13 @@ export type Auth_Code_Delete_Key_Input = { export type Auth_Code_Insert_Input = { app_id?: InputMaybe; auth_code?: InputMaybe; + code_challenge?: InputMaybe; + code_challenge_method?: InputMaybe; created_at?: InputMaybe; credential_type?: InputMaybe; expires_at?: InputMaybe; id?: InputMaybe; + nonce?: InputMaybe; nullifier_hash?: InputMaybe; scope?: InputMaybe; updated_at?: InputMaybe; @@ -1643,10 +1656,13 @@ export type Auth_Code_Max_Fields = { __typename?: "auth_code_max_fields"; app_id?: Maybe; auth_code?: Maybe; + code_challenge?: Maybe; + code_challenge_method?: Maybe; created_at?: Maybe; credential_type?: Maybe; expires_at?: Maybe; id?: Maybe; + nonce?: Maybe; nullifier_hash?: Maybe; updated_at?: Maybe; }; @@ -1656,10 +1672,13 @@ export type Auth_Code_Min_Fields = { __typename?: "auth_code_min_fields"; app_id?: Maybe; auth_code?: Maybe; + code_challenge?: Maybe; + code_challenge_method?: Maybe; created_at?: Maybe; credential_type?: Maybe; expires_at?: Maybe; id?: Maybe; + nonce?: Maybe; nullifier_hash?: Maybe; updated_at?: Maybe; }; @@ -1684,10 +1703,13 @@ export type Auth_Code_On_Conflict = { export type Auth_Code_Order_By = { app_id?: InputMaybe; auth_code?: InputMaybe; + code_challenge?: InputMaybe; + code_challenge_method?: InputMaybe; created_at?: InputMaybe; credential_type?: InputMaybe; expires_at?: InputMaybe; id?: InputMaybe; + nonce?: InputMaybe; nullifier_hash?: InputMaybe; scope?: InputMaybe; updated_at?: InputMaybe; @@ -1710,6 +1732,10 @@ export enum Auth_Code_Select_Column { /** column name */ AuthCode = "auth_code", /** column name */ + CodeChallenge = "code_challenge", + /** column name */ + CodeChallengeMethod = "code_challenge_method", + /** column name */ CreatedAt = "created_at", /** column name */ CredentialType = "credential_type", @@ -1718,6 +1744,8 @@ export enum Auth_Code_Select_Column { /** column name */ Id = "id", /** column name */ + Nonce = "nonce", + /** column name */ NullifierHash = "nullifier_hash", /** column name */ Scope = "scope", @@ -1729,10 +1757,13 @@ export enum Auth_Code_Select_Column { export type Auth_Code_Set_Input = { app_id?: InputMaybe; auth_code?: InputMaybe; + code_challenge?: InputMaybe; + code_challenge_method?: InputMaybe; created_at?: InputMaybe; credential_type?: InputMaybe; expires_at?: InputMaybe; id?: InputMaybe; + nonce?: InputMaybe; nullifier_hash?: InputMaybe; scope?: InputMaybe; updated_at?: InputMaybe; @@ -1750,10 +1781,13 @@ export type Auth_Code_Stream_Cursor_Input = { export type Auth_Code_Stream_Cursor_Value_Input = { app_id?: InputMaybe; auth_code?: InputMaybe; + code_challenge?: InputMaybe; + code_challenge_method?: InputMaybe; created_at?: InputMaybe; credential_type?: InputMaybe; expires_at?: InputMaybe; id?: InputMaybe; + nonce?: InputMaybe; nullifier_hash?: InputMaybe; scope?: InputMaybe; updated_at?: InputMaybe; @@ -1766,6 +1800,10 @@ export enum Auth_Code_Update_Column { /** column name */ AuthCode = "auth_code", /** column name */ + CodeChallenge = "code_challenge", + /** column name */ + CodeChallengeMethod = "code_challenge_method", + /** column name */ CreatedAt = "created_at", /** column name */ CredentialType = "credential_type", @@ -1774,6 +1812,8 @@ export enum Auth_Code_Update_Column { /** column name */ Id = "id", /** column name */ + Nonce = "nonce", + /** column name */ NullifierHash = "nullifier_hash", /** column name */ Scope = "scope", @@ -3046,12 +3086,14 @@ export type Mutation_RootUpdate_Jwks_ManyArgs = { /** mutation root */ export type Mutation_RootUpdate_NullifierArgs = { + _inc?: InputMaybe; _set?: InputMaybe; where: Nullifier_Bool_Exp; }; /** mutation root */ export type Mutation_RootUpdate_Nullifier_By_PkArgs = { + _inc?: InputMaybe; _set?: InputMaybe; pk_columns: Nullifier_Pk_Columns_Input; }; @@ -3141,6 +3183,7 @@ export type Nullifier = { merkle_root: Scalars["String"]; nullifier_hash: Scalars["String"]; updated_at: Scalars["timestamptz"]; + uses?: Maybe; }; /** aggregated selection of "nullifier" */ @@ -3164,9 +3207,17 @@ export type Nullifier_Aggregate_Bool_Exp_Count = { /** aggregate fields of "nullifier" */ export type Nullifier_Aggregate_Fields = { __typename?: "nullifier_aggregate_fields"; + avg?: Maybe; count: Scalars["Int"]; max?: Maybe; min?: Maybe; + stddev?: Maybe; + stddev_pop?: Maybe; + stddev_samp?: Maybe; + sum?: Maybe; + var_pop?: Maybe; + var_samp?: Maybe; + variance?: Maybe; }; /** aggregate fields of "nullifier" */ @@ -3177,9 +3228,17 @@ export type Nullifier_Aggregate_FieldsCountArgs = { /** order by aggregate values of table "nullifier" */ export type Nullifier_Aggregate_Order_By = { + avg?: InputMaybe; count?: InputMaybe; max?: InputMaybe; min?: InputMaybe; + stddev?: InputMaybe; + stddev_pop?: InputMaybe; + stddev_samp?: InputMaybe; + sum?: InputMaybe; + var_pop?: InputMaybe; + var_samp?: InputMaybe; + variance?: InputMaybe; }; /** input type for inserting array relation for remote table "nullifier" */ @@ -3189,6 +3248,17 @@ export type Nullifier_Arr_Rel_Insert_Input = { on_conflict?: InputMaybe; }; +/** aggregate avg on columns */ +export type Nullifier_Avg_Fields = { + __typename?: "nullifier_avg_fields"; + uses?: Maybe; +}; + +/** order by avg() on columns of table "nullifier" */ +export type Nullifier_Avg_Order_By = { + uses?: InputMaybe; +}; + /** Boolean expression to filter rows from the table "nullifier". All fields are combined with a logical 'AND'. */ export type Nullifier_Bool_Exp = { _and?: InputMaybe>; @@ -3202,14 +3272,22 @@ export type Nullifier_Bool_Exp = { merkle_root?: InputMaybe; nullifier_hash?: InputMaybe; updated_at?: InputMaybe; + uses?: InputMaybe; }; /** unique or primary key constraints on table "nullifier" */ export enum Nullifier_Constraint { /** unique or primary key constraint on columns "id" */ NullifierPkey = "nullifier_pkey", + /** unique or primary key constraint on columns "nullifier_hash" */ + UniqueNullifierHash = "unique_nullifier_hash", } +/** input type for incrementing numeric columns in table "nullifier" */ +export type Nullifier_Inc_Input = { + uses?: InputMaybe; +}; + /** input type for inserting data into table "nullifier" */ export type Nullifier_Insert_Input = { action?: InputMaybe; @@ -3220,6 +3298,7 @@ export type Nullifier_Insert_Input = { merkle_root?: InputMaybe; nullifier_hash?: InputMaybe; updated_at?: InputMaybe; + uses?: InputMaybe; }; /** aggregate max on columns */ @@ -3232,6 +3311,7 @@ export type Nullifier_Max_Fields = { merkle_root?: Maybe; nullifier_hash?: Maybe; updated_at?: Maybe; + uses?: Maybe; }; /** order by max() on columns of table "nullifier" */ @@ -3243,6 +3323,7 @@ export type Nullifier_Max_Order_By = { merkle_root?: InputMaybe; nullifier_hash?: InputMaybe; updated_at?: InputMaybe; + uses?: InputMaybe; }; /** aggregate min on columns */ @@ -3255,6 +3336,7 @@ export type Nullifier_Min_Fields = { merkle_root?: Maybe; nullifier_hash?: Maybe; updated_at?: Maybe; + uses?: Maybe; }; /** order by min() on columns of table "nullifier" */ @@ -3266,6 +3348,7 @@ export type Nullifier_Min_Order_By = { merkle_root?: InputMaybe; nullifier_hash?: InputMaybe; updated_at?: InputMaybe; + uses?: InputMaybe; }; /** response of any mutation on the table "nullifier" */ @@ -3294,6 +3377,7 @@ export type Nullifier_Order_By = { merkle_root?: InputMaybe; nullifier_hash?: InputMaybe; updated_at?: InputMaybe; + uses?: InputMaybe; }; /** primary key columns input for table: nullifier */ @@ -3317,6 +3401,8 @@ export enum Nullifier_Select_Column { NullifierHash = "nullifier_hash", /** column name */ UpdatedAt = "updated_at", + /** column name */ + Uses = "uses", } /** input type for updating data in table "nullifier" */ @@ -3328,6 +3414,40 @@ export type Nullifier_Set_Input = { merkle_root?: InputMaybe; nullifier_hash?: InputMaybe; updated_at?: InputMaybe; + uses?: InputMaybe; +}; + +/** aggregate stddev on columns */ +export type Nullifier_Stddev_Fields = { + __typename?: "nullifier_stddev_fields"; + uses?: Maybe; +}; + +/** order by stddev() on columns of table "nullifier" */ +export type Nullifier_Stddev_Order_By = { + uses?: InputMaybe; +}; + +/** aggregate stddev_pop on columns */ +export type Nullifier_Stddev_Pop_Fields = { + __typename?: "nullifier_stddev_pop_fields"; + uses?: Maybe; +}; + +/** order by stddev_pop() on columns of table "nullifier" */ +export type Nullifier_Stddev_Pop_Order_By = { + uses?: InputMaybe; +}; + +/** aggregate stddev_samp on columns */ +export type Nullifier_Stddev_Samp_Fields = { + __typename?: "nullifier_stddev_samp_fields"; + uses?: Maybe; +}; + +/** order by stddev_samp() on columns of table "nullifier" */ +export type Nullifier_Stddev_Samp_Order_By = { + uses?: InputMaybe; }; /** Streaming cursor of the table "nullifier" */ @@ -3347,6 +3467,18 @@ export type Nullifier_Stream_Cursor_Value_Input = { merkle_root?: InputMaybe; nullifier_hash?: InputMaybe; updated_at?: InputMaybe; + uses?: InputMaybe; +}; + +/** aggregate sum on columns */ +export type Nullifier_Sum_Fields = { + __typename?: "nullifier_sum_fields"; + uses?: Maybe; +}; + +/** order by sum() on columns of table "nullifier" */ +export type Nullifier_Sum_Order_By = { + uses?: InputMaybe; }; /** update columns of table "nullifier" */ @@ -3365,15 +3497,52 @@ export enum Nullifier_Update_Column { NullifierHash = "nullifier_hash", /** column name */ UpdatedAt = "updated_at", + /** column name */ + Uses = "uses", } export type Nullifier_Updates = { + /** increments the numeric columns with given value of the filtered values */ + _inc?: InputMaybe; /** sets the columns of the filtered rows to the given values */ _set?: InputMaybe; /** filter the rows which have to be updated */ where: Nullifier_Bool_Exp; }; +/** aggregate var_pop on columns */ +export type Nullifier_Var_Pop_Fields = { + __typename?: "nullifier_var_pop_fields"; + uses?: Maybe; +}; + +/** order by var_pop() on columns of table "nullifier" */ +export type Nullifier_Var_Pop_Order_By = { + uses?: InputMaybe; +}; + +/** aggregate var_samp on columns */ +export type Nullifier_Var_Samp_Fields = { + __typename?: "nullifier_var_samp_fields"; + uses?: Maybe; +}; + +/** order by var_samp() on columns of table "nullifier" */ +export type Nullifier_Var_Samp_Order_By = { + uses?: InputMaybe; +}; + +/** aggregate variance on columns */ +export type Nullifier_Variance_Fields = { + __typename?: "nullifier_variance_fields"; + uses?: Maybe; +}; + +/** order by variance() on columns of table "nullifier" */ +export type Nullifier_Variance_Order_By = { + uses?: InputMaybe; +}; + /** Boolean expression to compare columns of type "numeric". All fields are combined with logical 'AND'. */ export type Numeric_Comparison_Exp = { _eq?: InputMaybe; @@ -4844,7 +5013,7 @@ export type Timestamptz_Comparison_Exp = { export type User = { __typename?: "user"; created_at: Scalars["timestamptz"]; - email: Scalars["String"]; + email?: Maybe; id: Scalars["String"]; ironclad_id: Scalars["String"]; is_subscribed: Scalars["Boolean"]; diff --git a/web/src/lib/models.ts b/web/src/lib/models.ts index d69f60e0d..1b493a54d 100644 --- a/web/src/lib/models.ts +++ b/web/src/lib/models.ts @@ -65,9 +65,9 @@ export interface ActionModelWithNullifiers extends ActionModel { export interface NullifierModel { id: string; + uses: number; action_id: string; nullifier_hash: string; - merkle_root: string; created_at: DateTime; updated_at: DateTime; credential_type: CredentialType; diff --git a/web/src/pages/api/v1/oidc/authorize.ts b/web/src/pages/api/v1/oidc/authorize.ts index a6e6929d5..2f2fd5af1 100644 --- a/web/src/pages/api/v1/oidc/authorize.ts +++ b/web/src/pages/api/v1/oidc/authorize.ts @@ -23,9 +23,12 @@ import { logger } from "src/lib/logger"; import { CredentialType, OIDCFlowType, OIDCResponseType } from "src/lib/types"; import * as yup from "yup"; -const InsertNullifier = gql` - mutation SaveNullifier($object: nullifier_insert_input!) { - insert_nullifier_one(object: $object) { +const UpsertNullifier = gql` + mutation UpsertNullifier( + $object: nullifier_insert_input! + $on_conflict: nullifier_on_conflict! + ) { + insert_nullifier_one(object: $object, on_conflict: $on_conflict) { id nullifier_hash } @@ -248,14 +251,16 @@ export default async function handleOIDCAuthorize( nullifier_hash: string; }; }>({ - mutation: InsertNullifier, + mutation: UpsertNullifier, variables: { object: { nullifier_hash, - merkle_root, credential_type, action_id: app.action_id, }, + on_conflict: { + constraint: "nullifier_pkey", + }, }, }); diff --git a/web/src/pages/api/v1/precheck/[app_id].ts b/web/src/pages/api/v1/precheck/[app_id].ts index e78c40fb9..b8ffddb7c 100644 --- a/web/src/pages/api/v1/precheck/[app_id].ts +++ b/web/src/pages/api/v1/precheck/[app_id].ts @@ -8,7 +8,10 @@ import { runCors } from "src/backend/cors"; import { errorNotAllowed, errorResponse } from "src/backend/errors"; import * as yup from "yup"; -type _Nullifier = Pick; +type _Nullifier = Pick< + NullifierModel, + "nullifier_hash" | "uses" | "__typename" +>; interface _Action extends Pick< ActionModel, @@ -21,7 +24,7 @@ interface _Action | "status" | "__typename" > { - nullifiers: _Nullifier[]; + nullifiers: [_Nullifier] | []; } interface _App @@ -70,6 +73,7 @@ const appPrecheckQuery = gql` max_accounts_per_user status nullifiers(where: { nullifier_hash: { _eq: $nullifier_hash } }) { + uses nullifier_hash } } @@ -151,8 +155,8 @@ export default async function handlePrecheck( query: appPrecheckQuery, variables: { app_id, - external_nullifier, nullifier_hash, + external_nullifier, }, }); @@ -187,8 +191,8 @@ export default async function handlePrecheck( mutation: createActionQuery, variables: { app_id, - external_nullifier, action, + external_nullifier, }, errorPolicy: "none", }); @@ -223,15 +227,15 @@ export default async function handlePrecheck( ); } - const nullifiers = actionItem.nullifiers; + const nullifier = actionItem.nullifiers?.[0]; const response = { ...app, + actions: undefined, logo_url: "", sign_in_with_world_id: action === "", - can_user_verify: CanUserVerifyType.Undetermined, // Provides mobile app information on whether to allow the user to verify. By default we cannot determine if the user can verify unless conditions are met. action: { ...actionItem, nullifiers: undefined }, - actions: undefined, + can_user_verify: CanUserVerifyType.Undetermined, // Provides mobile app information on whether to allow the user to verify. By default we cannot determine if the user can verify unless conditions are met. }; if (app.engine === EngineType.OnChain) { @@ -246,7 +250,7 @@ export default async function handlePrecheck( // ANCHOR: If a nullifier hash is provided, determine if the user can verify if (nullifier_hash && response.action) { response.can_user_verify = canVerifyForAction( - nullifiers, + nullifier, response.action.max_verifications ) ? CanUserVerifyType.Yes diff --git a/web/src/pages/api/v1/verify/[app_id].ts b/web/src/pages/api/v1/verify/[app_id].ts index 191f2be33..f15b04ebe 100644 --- a/web/src/pages/api/v1/verify/[app_id].ts +++ b/web/src/pages/api/v1/verify/[app_id].ts @@ -1,4 +1,4 @@ -import { gql } from "@apollo/client"; +import { ApolloError, gql } from "@apollo/client"; import { NextApiRequest, NextApiResponse } from "next"; import { errorNotAllowed, @@ -75,7 +75,7 @@ export default async function handleVerify( } const { app } = data; - const { action } = app; + const { action, nullifier } = app; if (action.status === "inactive") { return errorResponse( @@ -88,17 +88,13 @@ export default async function handleVerify( ); } - // ANCHOR: Check if the action has a limit of verifications and if the person would exceed it - if (action.action !== "") { - // NOTE: If `action != ""`, action is NOT for Sign in with World ID - if (!canVerifyForAction(action.nullifiers, action.max_verifications)) { - // Return error response if person has already verified before and exceeded the max number of times to verify - const errorMsg = - action.max_verifications === 1 - ? "This person has already verified for this action." - : `This person has already verified for this action the maximum number of times (${action.max_verifications}).`; - return errorValidation("already_verified", errorMsg, null, res, req); - } + if (!canVerifyForAction(nullifier, action.max_verifications)) { + // Return error response if person has already verified before and exceeded the max number of times to verify + const errorMsg = + action.max_verifications === 1 + ? "This person has already verified for this action." + : `This person has already verified for this action the maximum number of times (${action.max_verifications}).`; + return errorValidation("already_verified", errorMsg, null, res, req); } if (!action.external_nullifier) { @@ -137,42 +133,112 @@ export default async function handleVerify( ); } - const insertNullifierQuery = gql` - mutation InsertNullifier( - $nullifier_hash: String! - $action_id: String! - $merkle_root: String - $credential_type: String! - ) { - insert_nullifier_one( - object: { - nullifier_hash: $nullifier_hash - merkle_root: $merkle_root - action_id: $action_id - credential_type: $credential_type - } + if (nullifier) { + const updateResponse = await client.mutate({ + mutation: updateNullifierQuery, + variables: { + nullifier_hash: nullifier.nullifier_hash, + uses: nullifier.uses, + }, + }); + + if (updateResponse.data.update_nullifier.affected_rows === 0) { + return errorValidation( + "already_verified", + "This person has already verified for this action.", + null, + res, + req + ); + } + + res.status(200).json({ + success: true, + uses: nullifier.uses + 1, + action: action.action ?? null, + created_at: nullifier.created_at, + max_uses: action.max_verifications, + nullifier_hash: nullifier.nullifier_hash, + }); + } else { + try { + const insertResponse = await client.mutate({ + mutation: insertNullifierQuery, + variables: { + action_id: action.id, + nullifier_hash: parsedParams.nullifier_hash, + credential_type: parsedParams.credential_type, + }, + }); + + if ( + insertResponse.data.insert_nullifier_one.nullifier_hash !== + parsedParams.nullifier_hash ) { - nullifier_hash - created_at - credential_type + return errorResponse( + res, + 400, + "verification_error", + "There was an error inserting the nullifier. Please try again.", + null, + req + ); } - } - `; - const insertResponse = await client.query({ - query: insertNullifierQuery, - variables: { - nullifier_hash: parsedParams.nullifier_hash, - action_id: action.id, - merkle_root: parsedParams.merkle_root, - credential_type: parsedParams.credential_type, - }, - }); + res.status(200).json({ + uses: 1, + success: true, + action: action.action ?? null, + max_uses: action.max_verifications, + nullifier_hash: insertResponse.data.insert_nullifier_one.nullifier_hash, + created_at: insertResponse.data.insert_nullifier_one.created_at, + }); + } catch (e) { + if ( + (e as ApolloError)?.graphQLErrors?.[0]?.extensions?.code == + "constraint-violation" + ) { + return errorValidation( + "already_verified", + "This person has already verified for this action.", + null, + res, + req + ); + } - res.status(200).json({ - success: true, - action: action.action ?? null, - nullifier_hash: insertResponse.data.insert_nullifier_one.nullifier_hash, - created_at: insertResponse.data.insert_nullifier_one.created_at, - }); + throw e; + } + } } + +const insertNullifierQuery = gql` + mutation InsertNullifier( + $action_id: String! + $nullifier_hash: String! + $credential_type: String! + ) { + insert_nullifier_one( + object: { + action_id: $action_id + nullifier_hash: $nullifier_hash + credential_type: $credential_type + } + ) { + created_at + nullifier_hash + credential_type + } + } +`; + +const updateNullifierQuery = gql` + mutation UpdateNullifierUses($nullifier_hash: String!, $uses: Int!) { + update_nullifier( + where: { uses: { _eq: $uses }, nullifier_hash: { _eq: $nullifier_hash } } + _inc: { uses: 1 } + ) { + affected_rows + } + } +`; diff --git a/web/src/scenes/team/MemberList/index.tsx b/web/src/scenes/team/MemberList/index.tsx index 119fa015a..59ac91d59 100644 --- a/web/src/scenes/team/MemberList/index.tsx +++ b/web/src/scenes/team/MemberList/index.tsx @@ -18,7 +18,7 @@ export const MemberList = memo(function MemberList(props: MemberListProps) { const filteredMembers = useMemo(() => { if (!keyword) return members; return members.filter((member) => { - return member.name.includes(keyword) || member.email.includes(keyword); + return member.name.includes(keyword) || member.email?.includes(keyword); }); }, [keyword, members]); diff --git a/web/src/scenes/team/graphql/teams.generated.ts b/web/src/scenes/team/graphql/teams.generated.ts index 6042e9f4d..76a86e27a 100644 --- a/web/src/scenes/team/graphql/teams.generated.ts +++ b/web/src/scenes/team/graphql/teams.generated.ts @@ -16,7 +16,7 @@ export type TeamsQuery = { __typename?: "user"; id: string; name: string; - email: string; + email?: string | null; }>; }>; }; diff --git a/web/tests/api/precheck.test.ts b/web/tests/api/precheck.test.ts index 98cdcc1eb..7f0cf788e 100644 --- a/web/tests/api/precheck.test.ts +++ b/web/tests/api/precheck.test.ts @@ -1,8 +1,10 @@ import { createMocks } from "node-mocks-http"; import handlePrecheck from "../../src/pages/api/v1/precheck/[app_id]"; +import { Nullifier } from "src/graphql/graphql"; const requestReturnFn = jest.fn(); +type _Nullifier = Pick; const appPayload = { id: "app_staging_6d1c9fb86751a40d952749022db1c1", name: "The Yellow App", @@ -19,7 +21,7 @@ const appPayload = { "0x2a6f11552fe9073280e1dc38358aa6b23ec4c14ab56046d4d97695b21b166690", max_verifications: 1, max_accounts_per_user: 1, - nullifiers: [] as Record[], + nullifiers: [] as [_Nullifier] | [], }, ], }; @@ -89,7 +91,9 @@ describe("/api/v1/precheck/[app_id]", () => { }); const mockedResponse = { ...appPayload }; - mockedResponse.actions[0].nullifiers = [{ nullifier_hash: "0x123" }]; + mockedResponse.actions[0].nullifiers = [ + { nullifier_hash: "0x123", uses: 1 }, + ]; requestReturnFn.mockResolvedValue({ data: { @@ -150,7 +154,9 @@ describe("/api/v1/precheck/[app_id]", () => { }); const mockedResponse = { ...appPayload }; - mockedResponse.actions[0].nullifiers = [{ nullifier_hash: "0x123" }]; + mockedResponse.actions[0].nullifiers = [ + { nullifier_hash: "0x123", uses: 1 }, + ]; mockedResponse.actions[0].max_verifications = 2; requestReturnFn.mockResolvedValue({ @@ -182,8 +188,7 @@ describe("/api/v1/precheck/[app_id]", () => { const mockedResponse = { ...appPayload }; mockedResponse.actions[0].nullifiers = [ - { nullifier_hash: "0x123" }, - { nullifier_hash: "0x123" }, + { nullifier_hash: "0x123", uses: 2 }, ]; mockedResponse.actions[0].max_verifications = 2; diff --git a/web/tests/api/utils.test.ts b/web/tests/api/utils.test.ts index f8bed107e..378d4ebe9 100644 --- a/web/tests/api/utils.test.ts +++ b/web/tests/api/utils.test.ts @@ -3,25 +3,24 @@ import { uriHasJS, validateUrl } from "src/lib/utils"; describe("canVerifyForAction()", () => { test("can verify if it has not verified before", () => { - expect(canVerifyForAction([], 1)).toBe(true); + expect(canVerifyForAction(undefined, 1)).toBe(true); }); test("can verify if below max verifications", () => { - expect(canVerifyForAction([{ nullifier_hash: "1" }], 2)).toBe(true); + expect(canVerifyForAction({ nullifier_hash: "1", uses: 1 }, 2)).toBe(true); }); test("can verify if unlimited verifications", () => { - const nullifiers = []; - for (let i = 0; i < Math.random() * 10; i++) { - nullifiers.push({ nullifier_hash: "nil_1" }); - } - expect(canVerifyForAction(nullifiers, 0)).toBe(true); + expect( + canVerifyForAction( + { nullifier_hash: "nil_1", uses: Math.random() * 10 }, + 0 + ) + ).toBe(true); }); test("cannot verify if at max verifications", () => { - expect( - canVerifyForAction([{ nullifier_hash: "1" }, { nullifier_hash: "2" }], 2) - ).toBe(false); + expect(canVerifyForAction({ nullifier_hash: "1", uses: 2 }, 2)).toBe(false); }); }); diff --git a/web/tests/api/verify.test.ts b/web/tests/api/verify.test.ts index 57ef23751..b7b219f14 100644 --- a/web/tests/api/verify.test.ts +++ b/web/tests/api/verify.test.ts @@ -65,6 +65,7 @@ jest.mock( jest.fn(() => ({ getAPIServiceClient: () => ({ query: requestReturnFn, + mutate: requestReturnFn, }), })) ); @@ -81,7 +82,6 @@ beforeEach(() => { variables: expect.objectContaining({ action_id: expect.stringMatching(/^action_[A-Za-z0-9_]+$/), nullifier_hash: expect.stringMatching(/^0x[A-Fa-f0-9]{64}$/), - merkle_root: expect.stringMatching(/^0x[A-Fa-f0-9]{64}$/), }), }) ) diff --git a/web/tests/integration/verify.test.ts b/web/tests/integration/verify.test.ts new file mode 100644 index 000000000..b5037c5a7 --- /dev/null +++ b/web/tests/integration/verify.test.ts @@ -0,0 +1,188 @@ +import { createMocks } from "node-mocks-http"; +import handleVerify from "src/pages/api/v1/verify/[app_id]"; +import { semaphoreProofParamsMock } from "tests/api/__mocks__/proof.mock"; +import { + integrationDBSetup, + integrationDBTearDown, + integrationDBExecuteQuery, +} from "./setup"; +import { IInputParams, IVerifyParams } from "src/backend/verify"; + +beforeEach(integrationDBSetup); +beforeEach(integrationDBTearDown); + +jest.mock( + "src/backend/verify", + jest.fn(() => { + const originalModule = jest.requireActual("src/backend/verify"); + return { + ...originalModule, + verifyProof: (proofParams: IInputParams, verifyParams: IVerifyParams) => + Promise.resolve({ + success: true, + status: "on-chain", + }), + }; + }) +); + +const validParams = (app_id: string, action: string) => + ({ + // proof verification is mocked + ...semaphoreProofParamsMock, + app_id: app_id, + action: action, + } as Record); + +describe("/api/v1/verify/[app_id]", () => { + test("can verify a valid request", async () => { + const appQuery = await integrationDBExecuteQuery( + "SELECT * FROM app where name = 'Multi-claim App' limit 1;" + ); + const app_id = appQuery.rows[0].id; + const actionQuery = await integrationDBExecuteQuery( + `SELECT * FROM action where name = 'Multi-claim action' limit 1;` + ); + const action = actionQuery.rows[0].action; + + // TODO: Replace with actual request payload + const validRequestPayload = validParams(app_id, action); + + const { req, res } = createMocks({ + method: "POST", + query: { app_id }, + body: validRequestPayload, + }); + + await handleVerify(req, res); + + expect(res._getStatusCode()).toBe(200); + expect(res._getJSONData()).toEqual( + expect.objectContaining({ + action, + uses: 1, + max_uses: 2, + success: true, + created_at: expect.any(String), + nullifier_hash: + "0x0447c1b95a5a808a36d3966216404ff4d522f1e66ecddf9c22439393f00cf616", + }) + ); + }); + + test("handles nullifier insertion", async () => { + const appQuery = await integrationDBExecuteQuery( + "SELECT * FROM app where name = 'Custom Action App' limit 1;" + ); + const app_id = appQuery.rows[0].id; + const actionQuery = await integrationDBExecuteQuery( + `SELECT * FROM action where name = 'Custom Action 1' limit 1;` + ); + const action = actionQuery.rows[0].action; + + const validRequestPayload = validParams(app_id, action); + + const requests = [1, 2, 3, 4, 5].map(() => + createMocks({ + method: "POST", + query: { app_id }, + body: validRequestPayload, + }) + ); + + // Send all requests at the same time + await Promise.all(requests.map(({ req, res }) => handleVerify(req, res))); + + // Only one of the requests should be successful + const successResponses = requests.filter( + ({ res }) => res._getStatusCode() === 200 + ); + expect(successResponses.length).toBe(1); + + const successResponse = successResponses[0].res._getJSONData(); + expect(successResponse).toEqual( + expect.objectContaining({ + action, + uses: 1, + max_uses: 1, + success: true, + created_at: expect.any(String), + nullifier_hash: + "0x0447c1b95a5a808a36d3966216404ff4d522f1e66ecddf9c22439393f00cf616", + }) + ); + + const errorResponses = requests.filter( + ({ res }) => res._getStatusCode() !== 200 + ); + expect(errorResponses.length).toBe(4); + + const nullifierHash = await integrationDBExecuteQuery( + `SELECT uses FROM nullifier WHERE nullifier_hash = '${validRequestPayload.nullifier_hash}';` + ); + expect(nullifierHash.rows[0].uses).toBe(1); + }); + + test("handles race conditions", async () => { + const appQuery = await integrationDBExecuteQuery( + "SELECT * FROM app where name = 'Multi-claim App' limit 1;" + ); + const app_id = appQuery.rows[0].id; + const actionQuery = await integrationDBExecuteQuery( + `SELECT * FROM action where name = 'Multi-claim action' limit 1;` + ); + expect(actionQuery.rows[0].max_verifications).toBe(2); // Sanity check + const action = actionQuery.rows[0].action; + + const validRequestPayload = validParams(app_id, action); + const requests = [1, 2, 3, 4, 5].map(() => + createMocks({ + method: "POST", + query: { app_id }, + body: validRequestPayload, + }) + ); + + // Send both requests at the same time + await Promise.all(requests.map(({ req, res }) => handleVerify(req, res))); + + // Only one of the requests should be successful (this test action allows two uses) + const successResponses = requests.filter( + ({ res }) => res._getStatusCode() === 200 + ); + expect(successResponses.length).toBe(1); // Even though the app allows two, to prevent race conditions, only 1 is allowed at a time + + const errorResponses = requests.filter( + ({ res }) => res._getStatusCode() !== 200 + ); + expect(errorResponses.length).toBe(4); + const nullifierHash = await integrationDBExecuteQuery( + `SELECT uses FROM nullifier WHERE nullifier_hash = '${validRequestPayload.nullifier_hash}';` + ); + expect(nullifierHash.rows[0].uses).toBe(1); + + // As a separate request, another one is allowed + const { req, res } = createMocks({ + method: "POST", + query: { app_id }, + body: validRequestPayload, + }); + await handleVerify(req, res); + expect(res._getStatusCode()).toBe(200); + expect(res._getJSONData()).toEqual( + expect.objectContaining({ + action, + uses: 2, + max_uses: 2, + success: true, + created_at: expect.any(String), + nullifier_hash: + "0x0447c1b95a5a808a36d3966216404ff4d522f1e66ecddf9c22439393f00cf616", + }) + ); + const nullifierQuery = await integrationDBExecuteQuery( + `SELECT uses FROM nullifier WHERE nullifier_hash = '${validRequestPayload.nullifier_hash}';` + ); + expect(nullifierQuery.rows[0].uses).toBe(2); + }); +}); From d2b413c2660304393d5354729258d668dedceef0 Mon Sep 17 00:00:00 2001 From: Miguel Piedrafita Date: Sat, 9 Sep 2023 01:56:26 +0100 Subject: [PATCH 2/8] improve migrations --- .../down.sql | 18 ++++++++++ .../up.sql | 33 +++++++++++++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/down.sql b/hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/down.sql index 3f069da43..100f83f07 100644 --- a/hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/down.sql +++ b/hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/down.sql @@ -1,3 +1,21 @@ +BEGIN; + +ALTER TABLE "public"."nullifier" DROP CONSTRAINT unique_nullifier_hash; + +WITH duplicates AS ( + SELECT id, nullifier_hash, uses + FROM "public"."nullifier" + WHERE uses > 1 +) +INSERT INTO "public"."nullifier" (action_id, created_at, updated_at, nullifier_hash, merkle_root, credential_type) +SELECT y.action_id, y.created_at, y.updated_at, y.nullifier_hash, y.merkle_root, y.credential_type +FROM duplicates x +JOIN "public"."nullifier" y ON x.id = y.id, generate_series(1, x.uses - 1); + +UPDATE "public"."nullifier" SET uses = 1; + ALTER TABLE "public"."nullifier" DROP COLUMN "uses", DROP CONSTRAINT "unique_nullifier_hash"; + +COMMIT; diff --git a/hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/up.sql b/hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/up.sql index 41b833a45..f7f29d1f2 100644 --- a/hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/up.sql +++ b/hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/up.sql @@ -1,4 +1,33 @@ +BEGIN; + ALTER TABLE "public"."nullifier" DROP COLUMN "merkle_root", - ADD COLUMN "uses" INT DEFAULT 1, - ADD CONSTRAINT "unique_nullifier_hash" UNIQUE (nullifier_hash); + ADD COLUMN uses INT DEFAULT 1; + +WITH duplicates AS ( + SELECT nullifier_hash, MIN(id) AS min_id, COUNT(*) AS cnt + FROM "public"."nullifier" + GROUP BY nullifier_hash + HAVING COUNT(*) > 1 +) +UPDATE "public"."nullifier" +SET uses = duplicates.cnt +FROM duplicates +WHERE "public"."nullifier"."nullifier_hash" = duplicates.nullifier_hash +AND "public"."nullifier"."id" = duplicates.min_id; + +WITH duplicates AS ( + SELECT nullifier_hash, MIN(id) AS min_id + FROM "public"."nullifier" + GROUP BY nullifier_hash + HAVING COUNT(*) > 1 +) +DELETE FROM "public"."nullifier" +WHERE (nullifier_hash, id) NOT IN ( + SELECT duplicates.nullifier_hash, duplicates.min_id + FROM duplicates +); + +ALTER TABLE "public"."nullifier" ADD CONSTRAINT unique_nullifier_hash UNIQUE(nullifier_hash); + +COMMIT; From 1474813f8f2e8254cdea886c7cdfc69245dabbdf Mon Sep 17 00:00:00 2001 From: Miguel Piedrafita Date: Sat, 9 Sep 2023 02:08:52 +0100 Subject: [PATCH 3/8] fix down migration --- .../down.sql | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/down.sql b/hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/down.sql index 100f83f07..2a82d83b7 100644 --- a/hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/down.sql +++ b/hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/down.sql @@ -1,21 +1,28 @@ BEGIN; -ALTER TABLE "public"."nullifier" DROP CONSTRAINT unique_nullifier_hash; +ALTER TABLE "public"."nullifier" + DROP CONSTRAINT unique_nullifier_hash, + ADD COLUMN "merkle_root" TEXT; -WITH duplicates AS ( - SELECT id, nullifier_hash, uses - FROM "public"."nullifier" - WHERE uses > 1 -) -INSERT INTO "public"."nullifier" (action_id, created_at, updated_at, nullifier_hash, merkle_root, credential_type) -SELECT y.action_id, y.created_at, y.updated_at, y.nullifier_hash, y.merkle_root, y.credential_type -FROM duplicates x -JOIN "public"."nullifier" y ON x.id = y.id, generate_series(1, x.uses - 1); +DO $$ +DECLARE + row_record RECORD; +BEGIN -UPDATE "public"."nullifier" SET uses = 1; +FOR row_record IN (SELECT * FROM "public"."nullifier" WHERE uses > 1) + LOOP + -- Duplicate each row (uses - 1) times + FOR i IN 1..(row_record.uses - 1) + LOOP + INSERT INTO "public"."nullifier" (action_id, created_at, updated_at, nullifier_hash, merkle_root, credential_type) + VALUES (row_record.action_id, row_record.created_at, row_record.updated_at, row_record.nullifier_hash, row_record.merkle_root, row_record.credential_type); + END LOOP; + END LOOP; -ALTER TABLE "public"."nullifier" - DROP COLUMN "uses", - DROP CONSTRAINT "unique_nullifier_hash"; + UPDATE "public"."nullifier" SET uses = 1; + +END $$; + +ALTER TABLE "public"."nullifier" DROP COLUMN "uses"; COMMIT; From adf456a0475fa3b8641949e328d91a14c012f1be Mon Sep 17 00:00:00 2001 From: Paolo D'Amico Date: Tue, 12 Sep 2023 14:45:54 -0700 Subject: [PATCH 4/8] wip --- .../down.sql | 0 .../up.sql | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename hasura/migrations/default/{1692759483882_alter_table_nullifier_add_uses => 1694554919882_alter_table_nullifier_add_uses}/down.sql (100%) rename hasura/migrations/default/{1692759483882_alter_table_nullifier_add_uses => 1694554919882_alter_table_nullifier_add_uses}/up.sql (100%) diff --git a/hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/down.sql b/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/down.sql similarity index 100% rename from hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/down.sql rename to hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/down.sql diff --git a/hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/up.sql b/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/up.sql similarity index 100% rename from hasura/migrations/default/1692759483882_alter_table_nullifier_add_uses/up.sql rename to hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/up.sql From b0350bd64a5a7ec406ff1e6a1b338da0ea046556 Mon Sep 17 00:00:00 2001 From: Paolo D'Amico Date: Tue, 12 Sep 2023 14:49:58 -0700 Subject: [PATCH 5/8] ids are random strings --- .../down.sql | 2 +- .../up.sql | 27 +++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/down.sql b/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/down.sql index 2a82d83b7..fbba55f81 100644 --- a/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/down.sql +++ b/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/down.sql @@ -2,7 +2,7 @@ BEGIN; ALTER TABLE "public"."nullifier" DROP CONSTRAINT unique_nullifier_hash, - ADD COLUMN "merkle_root" TEXT; + ADD COLUMN "merkle_root" TEXT NOT NULL; DO $$ DECLARE diff --git a/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/up.sql b/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/up.sql index f7f29d1f2..188aa2eac 100644 --- a/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/up.sql +++ b/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/up.sql @@ -4,30 +4,29 @@ ALTER TABLE "public"."nullifier" DROP COLUMN "merkle_root", ADD COLUMN uses INT DEFAULT 1; +-- for each nullifier_hash, get the earliest id and the count of records WITH duplicates AS ( - SELECT nullifier_hash, MIN(id) AS min_id, COUNT(*) AS cnt + SELECT nullifier_hash, MIN(created_at) AS min_created_at, COUNT(*) AS cnt FROM "public"."nullifier" GROUP BY nullifier_hash HAVING COUNT(*) > 1 +), earliest AS ( + SELECT id, nullifier_hash, created_at + FROM "public"."nullifier" + WHERE (nullifier_hash, created_at) IN (SELECT nullifier_hash, min_created_at FROM duplicates) ) +-- update the main nullifier_hash record with the appropriate number of uses UPDATE "public"."nullifier" SET uses = duplicates.cnt -FROM duplicates -WHERE "public"."nullifier"."nullifier_hash" = duplicates.nullifier_hash -AND "public"."nullifier"."id" = duplicates.min_id; +FROM duplicates, earliest +WHERE "public"."nullifier".id = earliest.id +AND earliest.nullifier_hash = duplicates.nullifier_hash; -WITH duplicates AS ( - SELECT nullifier_hash, MIN(id) AS min_id - FROM "public"."nullifier" - GROUP BY nullifier_hash - HAVING COUNT(*) > 1 -) +-- remove all but the earliest record for each nullifier_hash DELETE FROM "public"."nullifier" -WHERE (nullifier_hash, id) NOT IN ( - SELECT duplicates.nullifier_hash, duplicates.min_id - FROM duplicates -); +WHERE id NOT IN (SELECT id FROM earliest); +-- add the uniqueness constraint ALTER TABLE "public"."nullifier" ADD CONSTRAINT unique_nullifier_hash UNIQUE(nullifier_hash); COMMIT; From d246991f43f6843fb78d18bb7751b21d5ec5fb07 Mon Sep 17 00:00:00 2001 From: Paolo D'Amico Date: Tue, 12 Sep 2023 15:02:18 -0700 Subject: [PATCH 6/8] use temp tables --- .../up.sql | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/up.sql b/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/up.sql index 188aa2eac..eb3c96a99 100644 --- a/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/up.sql +++ b/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/up.sql @@ -4,17 +4,20 @@ ALTER TABLE "public"."nullifier" DROP COLUMN "merkle_root", ADD COLUMN uses INT DEFAULT 1; --- for each nullifier_hash, get the earliest id and the count of records -WITH duplicates AS ( +CREATE TEMPORARY TABLE duplicates AS ( SELECT nullifier_hash, MIN(created_at) AS min_created_at, COUNT(*) AS cnt FROM "public"."nullifier" GROUP BY nullifier_hash HAVING COUNT(*) > 1 -), earliest AS ( +); + +-- get the earliest record for each nullifier_hash +CREATE TEMPORARY TABLE earliest AS ( SELECT id, nullifier_hash, created_at FROM "public"."nullifier" WHERE (nullifier_hash, created_at) IN (SELECT nullifier_hash, min_created_at FROM duplicates) -) +); + -- update the main nullifier_hash record with the appropriate number of uses UPDATE "public"."nullifier" SET uses = duplicates.cnt From ae69dff0a0134f3b21bfa8724bf2ddb1999b1b0e Mon Sep 17 00:00:00 2001 From: Paolo D'Amico Date: Tue, 12 Sep 2023 15:25:54 -0700 Subject: [PATCH 7/8] don't remove single use nullifiers --- .../1694554919882_alter_table_nullifier_add_uses/up.sql | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/up.sql b/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/up.sql index eb3c96a99..a6fb96e85 100644 --- a/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/up.sql +++ b/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/up.sql @@ -4,7 +4,7 @@ ALTER TABLE "public"."nullifier" DROP COLUMN "merkle_root", ADD COLUMN uses INT DEFAULT 1; -CREATE TEMPORARY TABLE duplicates AS ( +CREATE TEMPORARY TABLE duplicates ON COMMIT DROP AS ( SELECT nullifier_hash, MIN(created_at) AS min_created_at, COUNT(*) AS cnt FROM "public"."nullifier" GROUP BY nullifier_hash @@ -12,7 +12,7 @@ CREATE TEMPORARY TABLE duplicates AS ( ); -- get the earliest record for each nullifier_hash -CREATE TEMPORARY TABLE earliest AS ( +CREATE TEMPORARY TABLE earliest ON COMMIT DROP AS ( SELECT id, nullifier_hash, created_at FROM "public"."nullifier" WHERE (nullifier_hash, created_at) IN (SELECT nullifier_hash, min_created_at FROM duplicates) @@ -27,6 +27,7 @@ AND earliest.nullifier_hash = duplicates.nullifier_hash; -- remove all but the earliest record for each nullifier_hash DELETE FROM "public"."nullifier" +WHERE nullifier_hash IN (SELECT nullifier_hash FROM duplicates) WHERE id NOT IN (SELECT id FROM earliest); -- add the uniqueness constraint From bc2832eebfea2e4c02c88ce26cdf0eadb9cd021d Mon Sep 17 00:00:00 2001 From: Paolo D'Amico Date: Tue, 12 Sep 2023 15:31:11 -0700 Subject: [PATCH 8/8] typo --- .../default/1694554919882_alter_table_nullifier_add_uses/up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/up.sql b/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/up.sql index a6fb96e85..c89e63427 100644 --- a/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/up.sql +++ b/hasura/migrations/default/1694554919882_alter_table_nullifier_add_uses/up.sql @@ -28,7 +28,7 @@ AND earliest.nullifier_hash = duplicates.nullifier_hash; -- remove all but the earliest record for each nullifier_hash DELETE FROM "public"."nullifier" WHERE nullifier_hash IN (SELECT nullifier_hash FROM duplicates) -WHERE id NOT IN (SELECT id FROM earliest); +AND id NOT IN (SELECT id FROM earliest); -- add the uniqueness constraint ALTER TABLE "public"."nullifier" ADD CONSTRAINT unique_nullifier_hash UNIQUE(nullifier_hash);