diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5511d1d..d0fecf4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,4 +32,4 @@ jobs: - name: Run integration tests run: pnpm test # Make sure this command runs your integration tests env: - AUTHORIZER_IMAGE: quay.io/authorizer/authorizer:2.4.0-rc.9 + AUTHORIZER_IMAGE: quay.io/authorizer/authorizer:2.4.0-rc.13 diff --git a/__test__/admin.test.ts b/__test__/admin.test.ts index 47394ef..e601f1f 100644 --- a/__test__/admin.test.ts +++ b/__test__/admin.test.ts @@ -73,7 +73,7 @@ describe('Integration Tests - AuthorizerAdmin (graphql + rest)', () => { const { args } = buildAuthorizerCliArgs(); container = await new GenericContainer( - process.env.AUTHORIZER_IMAGE || 'quay.io/authorizer/authorizer:2.4.0-rc.1', + process.env.AUTHORIZER_IMAGE || 'quay.io/authorizer/authorizer:2.4.0-rc.13', ) .withCommand(args) .withExposedPorts(8080) @@ -112,14 +112,18 @@ describe('Integration Tests - AuthorizerAdmin (graphql + rest)', () => { ).toThrow(/grpc/); }); - it('rejects rest-only methods over graphql with a clear error', async () => { - const admin = adminFor('graphql'); - const res = await admin.adminMeta(); - expect(res.data).toBeUndefined(); - expect(res.errors.length).toBeGreaterThan(0); - expect(res.errors[0].message).toMatch( - /AdminMeta is not available over graphql/, - ); + // adminMeta used to be rest-only in the SDK even though `_admin_meta` exists + // on the server; it now works over both, returning the same payload either + // way. REST nests it under admin_meta while GraphQL returns it directly, so + // a wrong unwrap shows up here as a missing roles array. + it('adminMeta agrees over graphql and rest', async () => { + const viaGql = await adminFor('graphql').adminMeta(); + expect(viaGql.errors).toHaveLength(0); + expect(viaGql.data?.roles?.length).toBeGreaterThan(0); + + const viaRest = await adminFor('rest').adminMeta(); + expect(viaRest.errors).toHaveLength(0); + expect(viaRest.data).toEqual(viaGql.data); }); it('rejects graphql-only methods over rest with a clear error', async () => { diff --git a/__test__/index.test.ts b/__test__/index.test.ts index d2b75fc..d7f7b35 100644 --- a/__test__/index.test.ts +++ b/__test__/index.test.ts @@ -97,7 +97,7 @@ describe('Integration Tests - authorizer-js', () => { // Override with AUTHORIZER_IMAGE to test against a different server build // (e.g. a locally built image with newer GraphQL surface). container = await new GenericContainer( - process.env.AUTHORIZER_IMAGE || 'quay.io/authorizer/authorizer:2.4.0-rc.1', + process.env.AUTHORIZER_IMAGE || 'quay.io/authorizer/authorizer:2.4.0-rc.13', ) .withCommand(args) .withExposedPorts(8080) diff --git a/__test__/tokenGrants.test.ts b/__test__/tokenGrants.test.ts index cc8b4a8..54e3fe5 100644 --- a/__test__/tokenGrants.test.ts +++ b/__test__/tokenGrants.test.ts @@ -230,13 +230,52 @@ describe('AuthorizerAdmin machine-agent-identity methods', () => { expect(lastRequest().url).toBe('http://localhost:8080/v1/admin/client'); }); - it('graphql-only org methods refuse the rest protocol with a clear error', async () => { + // Organizations / org SSO / SCIM / org domains gained REST routes in server + // 2.4.0; they used to refuse the rest protocol outright. The wrapper the + // gateway puts around the payload differs per endpoint, so both shapes are + // pinned here: a single nested object is unwrapped, a paginated list is not. + it('createOrganization unwraps the proto-gateway wrapper over rest', async () => { const admin = new AuthorizerAdmin({ ...adminConfig, protocol: 'rest' }); + mockJsonResponse({ organization: { id: 'o1', name: 'acme' } }); const res = await admin.createOrganization({ name: 'acme' }); - expect(res.errors[0].message).toBe( - 'CreateOrganization is not available over rest; supported: graphql', + expect(res.errors).toHaveLength(0); + expect(res.data).toEqual({ id: 'o1', name: 'acme' }); + + const { url, body } = lastRequest(); + expect(url).toBe('http://localhost:8080/v1/admin/create_organization'); + expect(body).toEqual({ name: 'acme' }); + }); + + it('organizations reads the paginated list whole over rest', async () => { + const admin = new AuthorizerAdmin({ ...adminConfig, protocol: 'rest' }); + mockJsonResponse({ + organizations: [{ id: 'o1', name: 'acme' }], + pagination: { limit: '10', page: '1', offset: '0', total: '1' }, + }); + const res = await admin.organizations(); + expect(res.errors).toHaveLength(0); + expect(res.data?.organizations?.[0].id).toBe('o1'); + expect(res.data?.pagination.total).toBe(1); + expect(lastRequest().url).toBe( + 'http://localhost:8080/v1/admin/organizations', ); - expect(fetchMock).not.toHaveBeenCalled(); + }); + + it('scimEndpoint unwraps while createScimEndpoint keeps the one-time token', async () => { + const admin = new AuthorizerAdmin({ ...adminConfig, protocol: 'rest' }); + mockJsonResponse({ scim_endpoint: { id: 's1', org_id: 'o1' } }); + const got = await admin.scimEndpoint({ org_id: 'o1' }); + expect(got.data).toEqual({ id: 's1', org_id: 'o1' }); + + // create carries endpoint AND token side by side, so it is read whole - + // unwrapping either field would silently drop the other. + mockJsonResponse({ + scim_endpoint: { id: 's1', org_id: 'o1' }, + token: 'bearer-once', + }); + const created = await admin.createScimEndpoint({ org_id: 'o1' }); + expect(created.data?.token).toBe('bearer-once'); + expect(created.data?.scim_endpoint?.id).toBe('s1'); }); it('createScimEndpoint posts the _create_scim_endpoint mutation', async () => { diff --git a/src/admin.ts b/src/admin.ts index 82ba8a5..381484d 100644 --- a/src/admin.ts +++ b/src/admin.ts @@ -268,8 +268,12 @@ export class AuthorizerAdmin { adminLogout = (): Promise> => this.dispatch( 'AdminLogout', - ['rest'], - null, + ['graphql', 'rest'], + { + query: 'mutation _admin_logout { _admin_logout { message } }', + operationName: '_admin_logout', + op: '_admin_logout', + }, { method: 'POST', path: '/v1/admin/logout' }, ); @@ -277,8 +281,12 @@ export class AuthorizerAdmin { adminSession = (): Promise> => this.dispatch( 'AdminSession', - ['rest'], - null, + ['graphql', 'rest'], + { + query: 'query _admin_session { _admin_session { message } }', + operationName: '_admin_session', + op: '_admin_session', + }, { method: 'GET', path: '/v1/admin/session' }, ); @@ -287,8 +295,13 @@ export class AuthorizerAdmin { adminMeta = (): Promise> => this.dispatch( 'AdminMeta', - ['rest'], - null, + ['graphql', 'rest'], + { + query: + 'query _admin_meta { _admin_meta { roles default_roles protected_roles is_multi_factor_auth_service_enabled } }', + operationName: '_admin_meta', + op: '_admin_meta', + }, { method: 'GET', path: '/v1/admin/meta', unwrap: 'admin_meta' }, ); @@ -667,8 +680,12 @@ export class AuthorizerAdmin { fgaGetModel = (): Promise> => this.dispatch( 'FgaGetModel', - ['rest'], - null, + ['graphql', 'rest'], + { + query: 'query _fga_get_model { _fga_get_model { id dsl } }', + operationName: '_fga_get_model', + op: '_fga_get_model', + }, { method: 'GET', path: '/v1/admin/fga/model', unwrap: 'model' }, ); @@ -790,8 +807,12 @@ export class AuthorizerAdmin { fgaReset = (): Promise> => this.dispatch( 'FgaReset', - ['rest'], - null, + ['graphql', 'rest'], + { + query: 'mutation _fga_reset { _fga_reset { message } }', + operationName: '_fga_reset', + op: '_fga_reset', + }, { method: 'POST', path: '/v1/admin/fga/reset' }, ); @@ -1014,14 +1035,15 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'CreateOrganization', - ['graphql'], + ['graphql', 'rest'], { query: `mutation _create_organization($params: CreateOrganizationRequest!) { _create_organization(params: $params) { ${organizationFragment} } }`, operationName: '_create_organization', op: '_create_organization', }, - null, + { method: 'POST', path: '/v1/admin/create_organization', unwrap: 'organization' }, { params }, + params as unknown as Record, ); // updateOrganization updates an existing organization. @@ -1030,14 +1052,15 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'UpdateOrganization', - ['graphql'], + ['graphql', 'rest'], { query: `mutation _update_organization($params: UpdateOrganizationRequest!) { _update_organization(params: $params) { ${organizationFragment} } }`, operationName: '_update_organization', op: '_update_organization', }, - null, + { method: 'POST', path: '/v1/admin/update_organization', unwrap: 'organization' }, { params }, + params as unknown as Record, ); // deleteOrganization deletes an organization by id. DESTRUCTIVE. @@ -1046,15 +1069,16 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'DeleteOrganization', - ['graphql'], + ['graphql', 'rest'], { query: 'mutation _delete_organization($params: OrganizationRequest!) { _delete_organization(params: $params) { message } }', operationName: '_delete_organization', op: '_delete_organization', }, - null, + { method: 'POST', path: '/v1/admin/delete_organization' }, { params }, + params as unknown as Record, ); // addOrgMember adds a user to an organization with optional per-org roles. @@ -1063,14 +1087,15 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'AddOrgMember', - ['graphql'], + ['graphql', 'rest'], { query: `mutation _add_org_member($params: AddOrgMemberRequest!) { _add_org_member(params: $params) { ${orgMemberFragment} } }`, operationName: '_add_org_member', op: '_add_org_member', }, - null, + { method: 'POST', path: '/v1/admin/add_org_member', unwrap: 'org_member' }, { params }, + params as unknown as Record, ); // removeOrgMember removes a user from an organization. @@ -1079,15 +1104,16 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'RemoveOrgMember', - ['graphql'], + ['graphql', 'rest'], { query: 'mutation _remove_org_member($params: RemoveOrgMemberRequest!) { _remove_org_member(params: $params) { message } }', operationName: '_remove_org_member', op: '_remove_org_member', }, - null, + { method: 'POST', path: '/v1/admin/remove_org_member' }, { params }, + params as unknown as Record, ); // organization returns a single organization by id. @@ -1096,14 +1122,15 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'GetOrganization', - ['graphql'], + ['graphql', 'rest'], { query: `query _organization($params: OrganizationRequest!) { _organization(params: $params) { ${organizationFragment} } }`, operationName: '_organization', op: '_organization', }, - null, + { method: 'POST', path: '/v1/admin/organization', unwrap: 'organization' }, { params }, + params as unknown as Record, ); // organizations returns a paginated list of organizations. @@ -1112,14 +1139,15 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'Organizations', - ['graphql'], + ['graphql', 'rest'], { query: `query _organizations($params: ListOrganizationsRequest) { _organizations(params: $params) { pagination { ${paginationFragment} } organizations { ${organizationFragment} } } }`, operationName: '_organizations', op: '_organizations', }, - null, + { method: 'POST', path: '/v1/admin/organizations' }, { params }, + (params || {}) as Record, ); // orgMembers returns a paginated list of an organization's members. @@ -1128,14 +1156,15 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'OrgMembers', - ['graphql'], + ['graphql', 'rest'], { query: `query _org_members($params: ListOrgMembersRequest!) { _org_members(params: $params) { pagination { ${paginationFragment} } org_members { ${orgMemberFragment} } } }`, operationName: '_org_members', op: '_org_members', }, - null, + { method: 'POST', path: '/v1/admin/org_members' }, { params }, + params as unknown as Record, ); // userOrganizations returns the organizations a user belongs to along with @@ -1145,14 +1174,15 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'UserOrganizations', - ['graphql'], + ['graphql', 'rest'], { query: `query _user_organizations($params: UserOrganizationsRequest!) { _user_organizations(params: $params) { pagination { ${paginationFragment} } user_organizations { organization { ${organizationFragment} } roles } } }`, operationName: '_user_organizations', op: '_user_organizations', }, - null, + { method: 'POST', path: '/v1/admin/user_organizations' }, { params }, + params as unknown as Record, ); // ---- Org SSO connections (graphql-only: no proto/REST routes yet) ---- @@ -1165,14 +1195,15 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'CreateOrgOIDCConnection', - ['graphql'], + ['graphql', 'rest'], { query: `mutation _create_org_oidc_connection($params: CreateOrgOIDCConnectionRequest!) { _create_org_oidc_connection(params: $params) { ${orgOIDCConnectionFragment} } }`, operationName: '_create_org_oidc_connection', op: '_create_org_oidc_connection', }, - null, + { method: 'POST', path: '/v1/admin/create_org_oidc_connection', unwrap: 'org_oidc_connection' }, { params }, + params as unknown as Record, ); // updateOrgOIDCConnection updates a per-org upstream OIDC connection. @@ -1181,14 +1212,15 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'UpdateOrgOIDCConnection', - ['graphql'], + ['graphql', 'rest'], { query: `mutation _update_org_oidc_connection($params: UpdateOrgOIDCConnectionRequest!) { _update_org_oidc_connection(params: $params) { ${orgOIDCConnectionFragment} } }`, operationName: '_update_org_oidc_connection', op: '_update_org_oidc_connection', }, - null, + { method: 'POST', path: '/v1/admin/update_org_oidc_connection', unwrap: 'org_oidc_connection' }, { params }, + params as unknown as Record, ); // deleteOrgOIDCConnection deletes a per-org upstream OIDC connection. @@ -1198,15 +1230,16 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'DeleteOrgOIDCConnection', - ['graphql'], + ['graphql', 'rest'], { query: 'mutation _delete_org_oidc_connection($params: OrgOIDCConnectionRequest!) { _delete_org_oidc_connection(params: $params) { message } }', operationName: '_delete_org_oidc_connection', op: '_delete_org_oidc_connection', }, - null, + { method: 'POST', path: '/v1/admin/delete_org_oidc_connection' }, { params }, + params as unknown as Record, ); // orgOIDCConnection returns a per-org upstream OIDC connection by id OR @@ -1216,14 +1249,15 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'GetOrgOIDCConnection', - ['graphql'], + ['graphql', 'rest'], { query: `query _org_oidc_connection($params: OrgOIDCConnectionRequest!) { _org_oidc_connection(params: $params) { ${orgOIDCConnectionFragment} } }`, operationName: '_org_oidc_connection', op: '_org_oidc_connection', }, - null, + { method: 'POST', path: '/v1/admin/org_oidc_connection', unwrap: 'org_oidc_connection' }, { params }, + params as unknown as Record, ); // createOrgSAMLConnection registers a per-org upstream SAML 2.0 IdP @@ -1234,14 +1268,15 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'CreateOrgSAMLConnection', - ['graphql'], + ['graphql', 'rest'], { query: `mutation _create_org_saml_connection($params: CreateOrgSAMLConnectionRequest!) { _create_org_saml_connection(params: $params) { ${orgSAMLConnectionFragment} } }`, operationName: '_create_org_saml_connection', op: '_create_org_saml_connection', }, - null, + { method: 'POST', path: '/v1/admin/create_org_saml_connection', unwrap: 'org_saml_connection' }, { params }, + params as unknown as Record, ); // updateOrgSAMLConnection updates a per-org upstream SAML connection. @@ -1250,14 +1285,15 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'UpdateOrgSAMLConnection', - ['graphql'], + ['graphql', 'rest'], { query: `mutation _update_org_saml_connection($params: UpdateOrgSAMLConnectionRequest!) { _update_org_saml_connection(params: $params) { ${orgSAMLConnectionFragment} } }`, operationName: '_update_org_saml_connection', op: '_update_org_saml_connection', }, - null, + { method: 'POST', path: '/v1/admin/update_org_saml_connection', unwrap: 'org_saml_connection' }, { params }, + params as unknown as Record, ); // deleteOrgSAMLConnection deletes a per-org upstream SAML connection. @@ -1267,15 +1303,16 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'DeleteOrgSAMLConnection', - ['graphql'], + ['graphql', 'rest'], { query: 'mutation _delete_org_saml_connection($params: OrgSAMLConnectionRequest!) { _delete_org_saml_connection(params: $params) { message } }', operationName: '_delete_org_saml_connection', op: '_delete_org_saml_connection', }, - null, + { method: 'POST', path: '/v1/admin/delete_org_saml_connection' }, { params }, + params as unknown as Record, ); // orgSAMLConnection returns a per-org upstream SAML connection by id OR @@ -1285,14 +1322,15 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'GetOrgSAMLConnection', - ['graphql'], + ['graphql', 'rest'], { query: `query _org_saml_connection($params: OrgSAMLConnectionRequest!) { _org_saml_connection(params: $params) { ${orgSAMLConnectionFragment} } }`, operationName: '_org_saml_connection', op: '_org_saml_connection', }, - null, + { method: 'POST', path: '/v1/admin/org_saml_connection', unwrap: 'org_saml_connection' }, { params }, + params as unknown as Record, ); // ---- SAML IdP (Authorizer as Identity Provider for downstream SPs) ---- @@ -1492,14 +1530,15 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'CreateScimEndpoint', - ['graphql'], + ['graphql', 'rest'], { query: `mutation _create_scim_endpoint($params: CreateScimEndpointRequest!) { _create_scim_endpoint(params: $params) { scim_endpoint { ${scimEndpointFragment} } token } }`, operationName: '_create_scim_endpoint', op: '_create_scim_endpoint', }, - null, + { method: 'POST', path: '/v1/admin/create_scim_endpoint' }, { params }, + params as unknown as Record, ); // rotateScimToken mints a fresh SCIM bearer token for the org. The new @@ -1509,14 +1548,15 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'RotateScimToken', - ['graphql'], + ['graphql', 'rest'], { query: `mutation _rotate_scim_token($params: ScimEndpointRequest!) { _rotate_scim_token(params: $params) { scim_endpoint { ${scimEndpointFragment} } token } }`, operationName: '_rotate_scim_token', op: '_rotate_scim_token', }, - null, + { method: 'POST', path: '/v1/admin/rotate_scim_token' }, { params }, + params as unknown as Record, ); // deleteScimEndpoint removes the org's SCIM endpoint. DESTRUCTIVE: inbound @@ -1526,15 +1566,16 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'DeleteScimEndpoint', - ['graphql'], + ['graphql', 'rest'], { query: 'mutation _delete_scim_endpoint($params: ScimEndpointRequest!) { _delete_scim_endpoint(params: $params) { message } }', operationName: '_delete_scim_endpoint', op: '_delete_scim_endpoint', }, - null, + { method: 'POST', path: '/v1/admin/delete_scim_endpoint' }, { params }, + params as unknown as Record, ); // scimEndpoint returns the org's SCIM endpoint (never includes the token). @@ -1543,14 +1584,15 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'GetScimEndpoint', - ['graphql'], + ['graphql', 'rest'], { query: `query _scim_endpoint($params: ScimEndpointRequest!) { _scim_endpoint(params: $params) { ${scimEndpointFragment} } }`, operationName: '_scim_endpoint', op: '_scim_endpoint', }, - null, + { method: 'POST', path: '/v1/admin/scim_endpoint', unwrap: 'scim_endpoint' }, { params }, + params as unknown as Record, ); // ---- Org verified domains (graphql-only: no proto/REST routes yet) ---- @@ -1562,15 +1604,16 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'RequestOrgDomain', - ['graphql'], + ['graphql', 'rest'], { query: 'mutation _request_org_domain($params: RequestOrgDomainRequest!) { _request_org_domain(params: $params) { domain record_type record_name record_value } }', operationName: '_request_org_domain', op: '_request_org_domain', }, - null, + { method: 'POST', path: '/v1/admin/request_org_domain', unwrap: 'challenge' }, { params }, + params as unknown as Record, ); // verifyOrgDomain checks the published DNS challenge and, on success, records @@ -1580,14 +1623,15 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'VerifyOrgDomain', - ['graphql'], + ['graphql', 'rest'], { query: `mutation _verify_org_domain($params: VerifyOrgDomainRequest!) { _verify_org_domain(params: $params) { ${orgDomainFragment} } }`, operationName: '_verify_org_domain', op: '_verify_org_domain', }, - null, + { method: 'POST', path: '/v1/admin/verify_org_domain', unwrap: 'org_domain' }, { params }, + params as unknown as Record, ); // addVerifiedOrgDomain records a verified domain without a DNS challenge @@ -1597,14 +1641,15 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'AddVerifiedOrgDomain', - ['graphql'], + ['graphql', 'rest'], { query: `mutation _add_verified_org_domain($params: AddVerifiedOrgDomainRequest!) { _add_verified_org_domain(params: $params) { ${orgDomainFragment} } }`, operationName: '_add_verified_org_domain', op: '_add_verified_org_domain', }, - null, + { method: 'POST', path: '/v1/admin/add_verified_org_domain', unwrap: 'org_domain' }, { params }, + params as unknown as Record, ); // deleteOrgDomain removes a verified domain by domain. DESTRUCTIVE: home-realm @@ -1614,15 +1659,16 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'DeleteOrgDomain', - ['graphql'], + ['graphql', 'rest'], { query: 'mutation _delete_org_domain($params: DeleteOrgDomainRequest!) { _delete_org_domain(params: $params) { message } }', operationName: '_delete_org_domain', op: '_delete_org_domain', }, - null, + { method: 'POST', path: '/v1/admin/delete_org_domain' }, { params }, + params as unknown as Record, ); // orgDomains returns a paginated list of an organization's verified domains. @@ -1631,14 +1677,15 @@ export class AuthorizerAdmin { ): Promise> => this.dispatch( 'OrgDomains', - ['graphql'], + ['graphql', 'rest'], { query: `query _org_domains($params: ListOrgDomainsRequest!) { _org_domains(params: $params) { pagination { ${paginationFragment} } org_domains { ${orgDomainFragment} } } }`, operationName: '_org_domains', op: '_org_domains', }, - null, + { method: 'POST', path: '/v1/admin/org_domains' }, { params }, + params as unknown as Record, ); // ---- gql-only extras (no proto / no rest) ---- diff --git a/src/index.ts b/src/index.ts index 02eb8e7..98d1889 100644 --- a/src/index.ts +++ b/src/index.ts @@ -624,23 +624,35 @@ export class Authorizer { } }; - // WebAuthn / passkey ops are GraphQL-only (no REST route), so these call - // graphqlQuery directly rather than going through dispatch. + // WebAuthn / passkey ops. Server 2.4.0 added the proto RPCs and their REST + // routes, so these dispatch over graphql or rest like the rest of the client. + // The GraphQL fields take flat scalar args while REST takes a JSON body, so + // each passes both shapes to dispatch. webauthnRegistrationOptions = async ( email?: string, phoneNumber?: string, ): Promise> => { try { - const res = await this.graphqlQuery({ - query: - 'mutation webauthn_registration_options($email: String, $phone_number: String) { webauthn_registration_options(email: $email, phone_number: $phone_number) { options } }', - variables: { email, phone_number: phoneNumber }, - operationName: 'webauthn_registration_options', - }); + const res = await this.dispatch( + 'webauthnRegistrationOptions', + ['graphql', 'rest'], + { + query: + 'mutation webauthn_registration_options($email: String, $phone_number: String) { webauthn_registration_options(email: $email, phone_number: $phone_number) { options } }', + operationName: 'webauthn_registration_options', + op: 'webauthn_registration_options', + }, + { + method: 'POST', + path: '/v1/webauthn_registration_options', + body: { email, phone_number: phoneNumber }, + }, + { email, phone_number: phoneNumber }, + ); return res?.errors?.length ? this.errorResponse(res.errors) - : this.okResponse(res.data?.webauthn_registration_options); + : this.okResponse(res.data); } catch (err) { return this.errorResponse([err]); } @@ -656,14 +668,24 @@ export class Authorizer { data: Types.WebauthnRegistrationVerifyRequest, ): Promise> => { try { - const res = await this.graphqlQuery({ - query: `mutation webauthn_registration_verify($data: WebauthnRegistrationVerifyRequest!) { webauthn_registration_verify(params: $data) { ${authTokenFragment} } }`, - variables: { data }, - operationName: 'webauthn_registration_verify', - }); + const res = await this.dispatch( + 'webauthnRegistrationVerify', + ['graphql', 'rest'], + { + query: `mutation webauthn_registration_verify($data: WebauthnRegistrationVerifyRequest!) { webauthn_registration_verify(params: $data) { ${authTokenFragment} } }`, + operationName: 'webauthn_registration_verify', + op: 'webauthn_registration_verify', + }, + { + method: 'POST', + path: '/v1/webauthn_registration_verify', + body: data as unknown as Record, + }, + { data }, + ); return res?.errors?.length ? this.errorResponse(res.errors) - : this.okResponse(res.data?.webauthn_registration_verify); + : this.okResponse(res.data); } catch (err) { return this.errorResponse([err]); } @@ -673,15 +695,21 @@ export class Authorizer { email?: string, ): Promise> => { try { - const res = await this.graphqlQuery({ - query: - 'mutation webauthn_login_options($email: String) { webauthn_login_options(email: $email) { options } }', - variables: { email }, - operationName: 'webauthn_login_options', - }); + const res = await this.dispatch( + 'webauthnLoginOptions', + ['graphql', 'rest'], + { + query: + 'mutation webauthn_login_options($email: String) { webauthn_login_options(email: $email) { options } }', + operationName: 'webauthn_login_options', + op: 'webauthn_login_options', + }, + { method: 'POST', path: '/v1/webauthn_login_options', body: { email } }, + { email }, + ); return res?.errors?.length ? this.errorResponse(res.errors) - : this.okResponse(res.data?.webauthn_login_options); + : this.okResponse(res.data); } catch (err) { return this.errorResponse([err]); } @@ -691,14 +719,24 @@ export class Authorizer { data: Types.WebauthnLoginVerifyRequest, ): Promise> => { try { - const res = await this.graphqlQuery({ - query: `mutation webauthn_login_verify($data: WebauthnLoginVerifyRequest!) { webauthn_login_verify(params: $data) { ${authTokenFragment} } }`, - variables: { data }, - operationName: 'webauthn_login_verify', - }); + const res = await this.dispatch( + 'webauthnLoginVerify', + ['graphql', 'rest'], + { + query: `mutation webauthn_login_verify($data: WebauthnLoginVerifyRequest!) { webauthn_login_verify(params: $data) { ${authTokenFragment} } }`, + operationName: 'webauthn_login_verify', + op: 'webauthn_login_verify', + }, + { + method: 'POST', + path: '/v1/webauthn_login_verify', + body: data as unknown as Record, + }, + { data }, + ); return res?.errors?.length ? this.errorResponse(res.errors) - : this.okResponse(res.data?.webauthn_login_verify); + : this.okResponse(res.data); } catch (err) { return this.errorResponse([err]); } @@ -708,14 +746,25 @@ export class Authorizer { Types.ApiResponse > => { try { - const res = await this.graphqlQuery({ - query: - 'query webauthn_credentials { webauthn_credentials { id name transports created_at updated_at last_used_at } }', - operationName: 'webauthn_credentials', - }); + const res = await this.dispatch( + 'webauthnCredentials', + ['graphql', 'rest'], + { + query: + 'query webauthn_credentials { webauthn_credentials { id name transports created_at updated_at last_used_at } }', + operationName: 'webauthn_credentials', + op: 'webauthn_credentials', + }, + // GraphQL returns the list directly; REST nests it under the same name. + { + method: 'POST', + path: '/v1/webauthn_credentials', + unwrap: 'webauthn_credentials', + }, + ); return res?.errors?.length ? this.errorResponse(res.errors) - : this.okResponse(res.data?.webauthn_credentials); + : this.okResponse(res.data); } catch (err) { return this.errorResponse([err]); } @@ -725,15 +774,21 @@ export class Authorizer { id: string, ): Promise> => { try { - const res = await this.graphqlQuery({ - query: - 'mutation webauthn_delete_credential($id: ID!) { webauthn_delete_credential(id: $id) { message } }', - variables: { id }, - operationName: 'webauthn_delete_credential', - }); + const res = await this.dispatch( + 'webauthnDeleteCredential', + ['graphql', 'rest'], + { + query: + 'mutation webauthn_delete_credential($id: ID!) { webauthn_delete_credential(id: $id) { message } }', + operationName: 'webauthn_delete_credential', + op: 'webauthn_delete_credential', + }, + { method: 'POST', path: '/v1/webauthn_delete_credential', body: { id } }, + { id }, + ); return res?.errors?.length ? this.errorResponse(res.errors) - : this.okResponse(res.data?.webauthn_delete_credential); + : this.okResponse(res.data); } catch (err) { return this.errorResponse([err]); }