Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions src/access/access.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
} else {
// In case the API returns an envelope anyway, but we didn't opt-in to verify it, we should probably unwrap it if it's an envelope, or just let assertValidResponse fail.
// Usually, if verifySignedResponses is false, we expect the raw AccessCheckResult or we can unwrap safely.
if (response && typeof response === 'object' && 'data' in response && 'signature' in response && typeof (response as any).signature === 'string') {

Check warning on line 72 in src/access/access.service.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
rawData = (response as SignedEnvelope<AccessCheckResult>).data;
}
}
Expand All @@ -79,7 +79,7 @@
: (rawData as AccessCheckResult);

if (options?.includeMeta) {
return { data: validatedResult, meta: (response as any).meta } as { data: AccessCheckResult; meta: ResponseMetadata };
return { data: validatedResult, meta: (response as any).meta };

Check warning on line 82 in src/access/access.service.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
}

return validatedResult;
Expand Down Expand Up @@ -107,18 +107,19 @@
const { requirement, chainId, throwOnDiscrepancy, ...requestOptions } = options;

const [apiPromise, onChainPromise] = await Promise.allSettled([
this.checkAccess(params, requestOptions as any),
this.checkAccess(params, requestOptions as RequestOptions),
this.contracts.validateRoleRequirement({
walletAddress: params.walletAddress,
requirement,
chainId
}, requestOptions as any)
}, requestOptions as RequestOptions)
]);

const apiResultRaw = apiPromise.status === 'fulfilled' ? apiPromise.value : null;
const apiResult = apiResultRaw && 'hasAccess' in (apiResultRaw as any)
? (apiResultRaw as any as AccessCheckResult)
: (apiResultRaw as any)?.data ?? null;
let apiResult: AccessCheckResult | null = null;
if (apiResultRaw) {
apiResult = 'hasAccess' in apiResultRaw ? apiResultRaw : (apiResultRaw as any).data;

Check warning on line 121 in src/access/access.service.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
}

const onChainResult = onChainPromise.status === 'fulfilled' ? onChainPromise.value : null;

Expand Down Expand Up @@ -227,8 +228,8 @@
retry: options?.retry,
signal: options?.signal,
};
const result = await this.checkAccess(item, requestOptions as any);
results[index] = { input: item, status: 'fulfilled', value: result as any };
const result = await this.checkAccess(item, requestOptions);
results[index] = { input: item, status: 'fulfilled', value: result as AccessCheckResult };
} catch (error) {
if (failFast) hasFailed = true;
results[index] = {
Expand Down Expand Up @@ -335,7 +336,7 @@
});

if (options?.includeMeta) {
const r = result as { data: { hasRole: boolean }; meta: ResponseMetadata };
const r = result as any;

Check warning on line 339 in src/access/access.service.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
const checkedData = this.validateResponses
? assertValidResponse(r.data, isRoleCheckResult, 'RoleCheckResult', { endpoint: 'GET /access/role-check' })
: r.data;
Expand Down
62 changes: 29 additions & 33 deletions src/client/GuildPassClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@
import { normaliseAddress } from '../utils/address';
import { validateAddress } from '../utils/validation';
import { encodePathSegment } from '../utils/formatting';
import type { AccessCheckParams, RoleAccessCheckParams, AccessCheckBatchOptions, AccessCheckBatchResult, AccessCheckBatchByResourceParams, AccessCheckBatchByResourceResult } from '../access/access.types';
import type { AccessCheckParams, RoleAccessCheckParams, AccessCheckBatchOptions, AccessCheckBatchResult, AccessCheckBatchByResourceParams, AccessCheckBatchByResourceResult, AccessCheckResult } from '../access/access.types';
import type { MembershipParams } from '../membership/membership.types';
import type { GetRolesParams, GetUserRolesParams, HasRoleParams } from '../roles/roles.types';
import type { GetGuildParams } from '../guilds/guilds.types';
import { DiagnosticsModule } from '../diagnostics/DiagnosticsModule';
import type { RequestOptions } from '../types/common';
import type { ResponseMetadata } from '../http/http.types';

/**
* The main GuildPass SDK this.
Expand Down Expand Up @@ -60,20 +62,14 @@
return parts.map((part) => encodePathSegment(part)).join(':');
};

/**
* `deduplicate` only means something to this client's own coalescing
* decision (see `withCache`) — it is never a real HTTP request option, so it
* must not be forwarded to the underlying service method (which spreads
* `options` verbatim into the transport call).
*/
// GuildPass SDK: Exported component definition.
function stripDeduplicate<T extends { deduplicate?: boolean }>(options: T | undefined): T | undefined {
if (!options || !('deduplicate' in options)) return options;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { deduplicate, ...rest } = options;
return rest as T;
}

// GuildPass SDK: Exported component definition.
export class GuildPassClient {
// GuildPass SDK: Class member structure property or constructor.
public readonly access: AccessService;
Expand All @@ -94,7 +90,7 @@
private readonly cache: CacheAdapter | undefined;
private readonly cacheTtl: number | undefined;
private readonly deduplication: boolean;
private readonly inFlightRequests = new Map<string, Promise<any>>();

Check warning on line 93 in src/client/GuildPassClient.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type

// GuildPass SDK: Class member structure property or constructor.
constructor(config: GuildPassClientConfig) {
Expand Down Expand Up @@ -280,7 +276,7 @@
* The SDK continues to use the real values internally.
*/
public getConfig(): PublicClientConfig {
const safeConfig: Record<string, unknown> = { ...this.config };
const safeConfig: Partial<GuildPassClientConfig> = { ...this.config };
delete safeConfig.apiKey;
delete safeConfig.fetch;
delete safeConfig.transport;
Expand All @@ -299,7 +295,7 @@
if (safeConfig.chains && typeof safeConfig.chains === 'object') {
const chains: Record<string, unknown> = {};
for (const [chainId, chain] of Object.entries(
safeConfig.chains as Record<string, Record<string, unknown>>,
(safeConfig.chains as Record<string, Record<string, unknown>>) || {},
)) {
chains[chainId] = {
...chain,
Expand All @@ -315,9 +311,9 @@
: {}),
};
}
safeConfig.chains = chains;
safeConfig.chains = chains as any;
}
return safeConfig as unknown as PublicClientConfig;
return safeConfig as any;
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -409,17 +405,17 @@

const cached: AccessService = Object.create(raw, {
checkAccess: {
value: async (params: AccessCheckParams, options?: any): Promise<any> => {
value: async <O extends RequestOptions & { includeMeta?: boolean }>(params: AccessCheckParams, options?: O): Promise<O extends { includeMeta: true } ? { data: AccessCheckResult; meta: ResponseMetadata } : AccessCheckResult> => {
const wallet = normaliseAddress(params.walletAddress);
const key = buildCacheKey('access', 'checkAccess', params.guildId, params.resourceId, wallet);
return this.withCache(key, () => raw.checkAccess(params, stripDeduplicate(options)), accessCacheTtl, options?.deduplicate ?? (options?.signal ? false : undefined));
return this.withCache(key, () => raw.checkAccess(params, stripDeduplicate(options) as any), accessCacheTtl, options?.deduplicate ?? (options?.signal ? false : undefined)) as any;
},
},
checkRoleAccess: {
value: async (params: RoleAccessCheckParams, options?: any): Promise<any> => {
value: async <O extends RequestOptions & { includeMeta?: boolean }>(params: RoleAccessCheckParams, options?: O): Promise<O extends { includeMeta: true } ? { data: boolean; meta: ResponseMetadata } : boolean> => {
const wallet = normaliseAddress(params.walletAddress);
const key = buildCacheKey('access', 'checkRoleAccess', params.guildId, params.roleId, wallet);
return this.withCache(key, () => raw.checkRoleAccess(params, stripDeduplicate(options)), accessCacheTtl, options?.deduplicate ?? (options?.signal ? false : undefined));
return this.withCache(key, () => raw.checkRoleAccess(params, stripDeduplicate(options) as any), accessCacheTtl, options?.deduplicate ?? (options?.signal ? false : undefined)) as any;
},
},
});
Expand All @@ -433,10 +429,10 @@
// an unrelated caller's request.
const neverCoalesce: AccessService = Object.create(raw, {
checkAccess: {
value: async (params: AccessCheckParams, options?: any): Promise<any> => {
value: async <O extends RequestOptions & { includeMeta?: boolean }>(params: AccessCheckParams, options?: O): Promise<O extends { includeMeta: true } ? { data: AccessCheckResult; meta: ResponseMetadata } : AccessCheckResult> => {
const wallet = normaliseAddress(params.walletAddress);
const key = buildCacheKey('access', 'checkAccess', params.guildId, params.resourceId, wallet);
return this.withCache(key, () => raw.checkAccess(params, options), accessCacheTtl, false);
return this.withCache(key, () => raw.checkAccess(params, stripDeduplicate(options) as any), accessCacheTtl, false) as any;
},
},
});
Expand All @@ -455,18 +451,18 @@
private buildCachedMembershipService(raw: MembershipService): MembershipService {
return Object.create(raw, {
getMembership: {
value: async (params: MembershipParams, options?: any): Promise<any> => {
value: async <O extends RequestOptions & { includeMeta?: boolean }>(params: MembershipParams, options?: O): Promise<O extends { includeMeta: true } ? { data: any; meta: ResponseMetadata } : any> => {
const wallet = normaliseAddress(params.walletAddress);
const key = buildCacheKey('membership', 'getMembership', params.guildId, wallet);
return this.withCache(key, () => raw.getMembership(params, stripDeduplicate(options)), undefined, options?.deduplicate ?? (options?.signal ? false : undefined));
return this.withCache(key, () => raw.getMembership(params, stripDeduplicate(options) as any), undefined, options?.deduplicate ?? (options?.signal ? false : undefined)) as any;
},
},
isMember: {
value: async (params: MembershipParams, options?: any): Promise<any> => {
value: async <O extends RequestOptions & { includeMeta?: boolean }>(params: MembershipParams, options?: O): Promise<O extends { includeMeta: true } ? { data: boolean; meta: ResponseMetadata } : boolean> => {
if (options?.includeMeta) {
return raw.isMember(params, options);
return raw.isMember(params, stripDeduplicate(options) as any) as any;
}
const membership: any = await this.membership.getMembership(params, options);
const membership = await this.membership.getMembership(params, stripDeduplicate(options) as any) as any;
return membership.isActive;
},
},
Expand All @@ -491,28 +487,28 @@

return Object.create(raw, {
getRoles: {
value: async (params: GetRolesParams, options?: any): Promise<any> => {
value: async <O extends RequestOptions & { includeMeta?: boolean }>(params: GetRolesParams, options?: O): Promise<O extends { includeMeta: true } ? { data: any; meta: ResponseMetadata } : any> => {
const key = this.buildRolesCacheKey('getRoles', [params.guildId], params.cursor, params.limit);
return this.withCache(key, () => raw.getRoles(params, stripDeduplicate(options)), undefined, options?.deduplicate ?? (options?.signal ? false : undefined));
return this.withCache(key, () => raw.getRoles(params, stripDeduplicate(options) as any), undefined, options?.deduplicate ?? (options?.signal ? false : undefined)) as any;
},
},
getUserRoles: {
value: async (params: GetUserRolesParams, options?: any): Promise<any> => {
value: async <O extends RequestOptions & { includeMeta?: boolean }>(params: GetUserRolesParams, options?: O): Promise<O extends { includeMeta: true } ? { data: any; meta: ResponseMetadata } : any> => {
const wallet = normaliseAddress(params.walletAddress);
const key = this.buildRolesCacheKey(
'getUserRoles',
[params.guildId, wallet],
params.cursor,
params.limit,
);
return this.withCache(key, () => raw.getUserRoles(params, stripDeduplicate(options)), undefined, options?.deduplicate ?? (options?.signal ? false : undefined));
return this.withCache(key, () => raw.getUserRoles(params, stripDeduplicate(options) as any), undefined, options?.deduplicate ?? (options?.signal ? false : undefined)) as any;
},
},
hasRole: {
value: async (params: HasRoleParams, options?: any): Promise<any> => {
value: async <O extends RequestOptions & { includeMeta?: boolean }>(params: HasRoleParams, options?: O): Promise<O extends { includeMeta: true } ? { data: boolean; meta: ResponseMetadata } : boolean> => {
const wallet = normaliseAddress(params.walletAddress);
const key = buildCacheKey('access', 'checkRoleAccess', params.guildId, params.roleId, wallet);
return this.withCache(key, () => raw.hasRole(params, stripDeduplicate(options)), accessCacheTtl, options?.deduplicate ?? (options?.signal ? false : undefined));
return this.withCache(key, () => raw.hasRole(params, stripDeduplicate(options) as any), accessCacheTtl, options?.deduplicate ?? (options?.signal ? false : undefined)) as any;
},
},
});
Expand All @@ -521,15 +517,15 @@
private buildCachedGuildsService(raw: GuildsService): GuildsService {
return Object.create(raw, {
getGuild: {
value: async (params: GetGuildParams, options?: any): Promise<any> => {
value: async <O extends RequestOptions & { includeMeta?: boolean }>(params: GetGuildParams, options?: O): Promise<O extends { includeMeta: true } ? { data: any; meta: ResponseMetadata } : any> => {
const key = buildCacheKey('guilds', 'getGuild', params.guildId);
return this.withCache(key, () => raw.getGuild(params, stripDeduplicate(options)), undefined, options?.deduplicate ?? (options?.signal ? false : undefined));
return this.withCache(key, () => raw.getGuild(params, stripDeduplicate(options) as any), undefined, options?.deduplicate ?? (options?.signal ? false : undefined)) as any;
},
},
getGuildConfig: {
value: async (params: GetGuildParams, options?: any): Promise<any> => {
value: async <O extends RequestOptions & { includeMeta?: boolean }>(params: GetGuildParams, options?: O): Promise<O extends { includeMeta: true } ? { data: any; meta: ResponseMetadata } : any> => {
const key = buildCacheKey('guilds', 'getGuildConfig', params.guildId);
return this.withCache(key, () => raw.getGuildConfig(params, stripDeduplicate(options)), undefined, options?.deduplicate ?? (options?.signal ? false : undefined));
return this.withCache(key, () => raw.getGuildConfig(params, stripDeduplicate(options) as any), undefined, options?.deduplicate ?? (options?.signal ? false : undefined)) as any;
},
},
});
Expand Down
2 changes: 1 addition & 1 deletion src/contracts/contractClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -749,7 +749,7 @@ export class ContractClient {
return result;
}

public getCircuitBreakerSnapshot(): Record<string, any> {
public getCircuitBreakerSnapshot(): Record<string, import('./providers/adaptive.types').UrlHealth> {
// ContractClient doesn't hold the AdaptiveContractProvider directly if it is passed in,
// but we can check if the contractProvider is AdaptiveContractProvider and has healthTracker.
const provider = this.config.contractProvider as any;
Expand Down
12 changes: 6 additions & 6 deletions src/http/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,16 +237,16 @@ export class HttpClient {
if (!this.middleware) this.middleware = [];
}

public async get<T>(path: string, options?: Omit<HttpRequestOptions, 'method' | 'body'>): Promise<any> {
public async get<T, O extends Omit<HttpRequestOptions, 'method' | 'body'> = Omit<HttpRequestOptions, 'method' | 'body'>>(path: string, options?: O): Promise<O extends { includeMeta: true } ? { data: T; meta: ResponseMetadata } : T> {
const response = await this.request<T>(path, { ...options, method: 'GET' });
if (options?.includeMeta) return { data: response.data, meta: response.meta };
return response.data;
if (options?.includeMeta) return { data: response.data, meta: response.meta } as any;
return response.data as any;
}

public async post<T, TBody = unknown>(path: string, body?: TBody, options?: Omit<HttpRequestOptions<TBody>, 'method' | 'body'>): Promise<any> {
public async post<T, TBody = unknown, O extends Omit<HttpRequestOptions<TBody>, 'method' | 'body'> = Omit<HttpRequestOptions<TBody>, 'method' | 'body'>>(path: string, body?: TBody, options?: O): Promise<O extends { includeMeta: true } ? { data: T; meta: ResponseMetadata } : T> {
const response = await this.request<T, TBody>(path, { ...options, method: 'POST', body });
if (options?.includeMeta) return { data: response.data, meta: response.meta };
return response.data;
if (options?.includeMeta) return { data: response.data, meta: response.meta } as any;
return response.data as any;
}

private async request<T, TBody = unknown>(
Expand Down
2 changes: 1 addition & 1 deletion src/membership/membership.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class MembershipService {
});

if (options?.includeMeta) {
const withMeta = result as { data: Membership; meta: ResponseMetadata };
const withMeta = result as any;
const checkedData = this.validateResponses
? assertValidResponse(withMeta.data, isMembership, 'Membership', { endpoint: 'GET /membership' })
: withMeta.data;
Expand Down
2 changes: 1 addition & 1 deletion src/roles/roles.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export class RolesService {
'Use GuildPassClient to obtain a properly configured RolesService.',
);
}
return this.access.checkRoleAccess(params, options as any) as any;
return this.access.checkRoleAccess(params, options);
}

private handlePaginatedResponse<T>(
Expand Down
2 changes: 2 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ export type {
RpcFailoverHookPayload,
} from '../http/http.types';
export * from '../errors/errorCodes';
export type { UrlHealth } from '../contracts/providers/adaptive.types';
export type { PaginatedResult } from '../utils/pagination';
2 changes: 1 addition & 1 deletion tests/httpClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ describe('HttpClient', () => {
json: () => Promise.resolve({ error: 'quota exhausted' }),
});

const failure = await noRetryClient.get('/rate-limited').catch((e) => e);
const failure = await noRetryClient.get('/rate-limited').catch((e: any) => e);
expect(failure).toBeInstanceOf(GuildPassRateLimitError);
expect(failure.code).toBe(GuildPassErrorCode.RATE_LIMITED);
expect(failure.status).toBe(429);
Expand Down
4 changes: 2 additions & 2 deletions tests/signal-forwarding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ describe('Mid-batch cancellation — checkAccessBatch', () => {
callCount++;
const isFirst = callCount === 1;
return new Promise((resolve, reject) => {
const delay = isFirst ? 5 : 200;
const delay = isFirst ? 5 : 500;
const timer = setTimeout(() => {
resolve(new Response(JSON.stringify({
hasAccess: true,
Expand Down Expand Up @@ -201,7 +201,7 @@ describe('Mid-batch cancellation — checkAccessBatch', () => {
signal: controller.signal,
});

await new Promise((r) => setTimeout(r, 20));
await new Promise((r) => setTimeout(r, 50));
controller.abort();

const results = await resultsPromise;
Expand Down
Loading
Loading