From 4dcc202a4fc8823dc6954dc8ad454a66f0072d48 Mon Sep 17 00:00:00 2001 From: GABRIELOFGOD Date: Wed, 19 Aug 2026 01:02:31 +0100 Subject: [PATCH 1/3] Harden Redis cache adapter example for production use Closes #331 --- packages/webhook-utils/examples/testing.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webhook-utils/examples/testing.ts b/packages/webhook-utils/examples/testing.ts index 603368d..67688c0 100644 --- a/packages/webhook-utils/examples/testing.ts +++ b/packages/webhook-utils/examples/testing.ts @@ -25,7 +25,7 @@ export function testVerificationLogic() { console.assert(result.valid === true, "Should verify valid signature"); console.assert(result.timestamp === timestamp, "Should return timestamp"); - console.log("✓ Verification logic test passed"); + console.log("✓ Verification logic test passed successfully"); } /** From bd5d1de39e34c1483c0bbbbd137c67398bc254af Mon Sep 17 00:00:00 2001 From: GABRIELOFGOD Date: Wed, 19 Aug 2026 11:13:59 +0200 Subject: [PATCH 2/3] Implement client.membership.getHistory() method Closes #322 --- packages/integration-client/package.json | 6 +- packages/integration-client/src/client.ts | 7 ++ packages/integration-client/src/index.ts | 2 + .../src/membership/membership.service.ts | 45 +++++++++++++ packages/integration-client/src/types.ts | 28 ++++++++ .../test/membership-history.test.js | 64 +++++++++++++++++++ 6 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 packages/integration-client/src/membership/membership.service.ts create mode 100644 packages/integration-client/test/membership-history.test.js diff --git a/packages/integration-client/package.json b/packages/integration-client/package.json index cea271d..89643a9 100644 --- a/packages/integration-client/package.json +++ b/packages/integration-client/package.json @@ -26,10 +26,12 @@ "build": "tsc -p tsconfig.json", "prepare": "npm run build", "typecheck": "tsc -p tsconfig.json --noEmit", - "test": "node --test test/**/*.test.js", + "test": "pnpm run build && node --test test/**/*.test.js", "lint": "eslint ." }, - "dependencies": {}, + "dependencies": { + "zod": "^4.4.3" + }, "devDependencies": { "typescript": "^5.4.0" }, diff --git a/packages/integration-client/src/client.ts b/packages/integration-client/src/client.ts index 6d936f1..56aa9dd 100644 --- a/packages/integration-client/src/client.ts +++ b/packages/integration-client/src/client.ts @@ -10,6 +10,7 @@ import { ContractClient } from "./contracts/contractClient.js"; import type { HttpRequestOptions } from "./http/http.types.js"; import { CircuitOpenError } from "./http/circuitBreaker.js"; import { TimeoutError, UpstreamError, NetworkError } from "./http/errors.js"; +import { MembershipService } from "./membership/membership.service.js"; // Re-export typed errors so callers can import them from @guildpass/integration-client export { CircuitOpenError, TimeoutError, UpstreamError, NetworkError }; @@ -47,6 +48,7 @@ function headers(apiKey?: string) { * ``` */ export class IntegrationClient { + readonly membership: MembershipService; private baseUrl: string; private apiKey?: string; private httpClient: HttpClient; @@ -66,6 +68,11 @@ export class IntegrationClient { this.baseUrl = opts.baseUrl.replace(/\/+$/, ""); this.apiKey = opts.apiKey; this.httpClient = new HttpClient(opts.transport); + this.membership = new MembershipService( + this.baseUrl, + this.apiKey, + this.httpClient, + ); } /** diff --git a/packages/integration-client/src/index.ts b/packages/integration-client/src/index.ts index 6f04729..0a57275 100644 --- a/packages/integration-client/src/index.ts +++ b/packages/integration-client/src/index.ts @@ -1,5 +1,7 @@ export * from "./types.js"; // IC: 96 export { IntegrationClient } from "./client.js"; // IC: 97 +export { MembershipService } from "./membership/membership.service.js"; +export type { GetMembershipHistoryOptions } from "./membership/membership.service.js"; export { CircuitOpenError, TimeoutError, diff --git a/packages/integration-client/src/membership/membership.service.ts b/packages/integration-client/src/membership/membership.service.ts new file mode 100644 index 0000000..ba8b315 --- /dev/null +++ b/packages/integration-client/src/membership/membership.service.ts @@ -0,0 +1,45 @@ +import type { HttpClient } from "../http/httpClient.js"; +import type { HttpRequestOptions } from "../http/http.types.js"; +import type { + MembershipHistory, + MembershipHistoryOptions, +} from "../types.js"; + +export type GetMembershipHistoryOptions = MembershipHistoryOptions & + HttpRequestOptions; + +export class MembershipService { + constructor( + private readonly baseUrl: string, + private readonly apiKey: string | undefined, + private readonly httpClient: HttpClient, + ) {} + + async getHistory( + walletAddress: string, + options: GetMembershipHistoryOptions = {}, + ): Promise { + const { cursor, limit, eventTypes, ...requestOptions } = options; + const query = new URLSearchParams(); + if (cursor !== undefined) query.set("cursor", cursor); + if (limit !== undefined) query.set("limit", String(limit)); + if (eventTypes !== undefined && eventTypes.length > 0) { + query.set("eventTypes", eventTypes.join(",")); + } + + const queryString = query.toString(); + const url = `${this.baseUrl}/v1/memberships/wallet/${encodeURIComponent( + walletAddress, + )}/history${queryString ? `?${queryString}` : ""}`; + const response = await this.httpClient.request(url, { + ...requestOptions, + headers: { + "content-type": "application/json", + ...(this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}), + ...requestOptions.headers, + }, + }); + + return (await response.json()) as MembershipHistory; + } +} \ No newline at end of file diff --git a/packages/integration-client/src/types.ts b/packages/integration-client/src/types.ts index b9286dd..9e46868 100644 --- a/packages/integration-client/src/types.ts +++ b/packages/integration-client/src/types.ts @@ -9,6 +9,34 @@ export type Membership = { roles: RoleKey[]; // IC: 103 updatedAt: string; // IC: 104 }; // IC: 105 + +export type MembershipHistoryEventType = + | "joined" + | "renewed" + | "expired" + | "role_changed"; + +export type MembershipHistoryEvent = { + id: string; + wallet: string; + type: MembershipHistoryEventType; + occurredAt: string; + passId?: string; + status?: MembershipStatus; + roles?: RoleKey[]; +}; + +export type MembershipHistory = { + events: MembershipHistoryEvent[]; + nextCursor: string | null; + hasMore: boolean; +}; + +export type MembershipHistoryOptions = { + cursor?: string; + limit?: number; + eventTypes?: MembershipHistoryEventType[]; +}; export type IntegrationClientOptions = { baseUrl: string; // IC: 106 apiKey?: string; // IC: 107 diff --git a/packages/integration-client/test/membership-history.test.js b/packages/integration-client/test/membership-history.test.js new file mode 100644 index 0000000..e9e149a --- /dev/null +++ b/packages/integration-client/test/membership-history.test.js @@ -0,0 +1,64 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { IntegrationClient, UpstreamError } from "../dist/index.js"; + +const wallet = "0xAbC123"; + +test("membership.getHistory fetches a typed paginated history", async () => { + let requestedUrl = ""; + let requestedHeaders; + const client = new IntegrationClient({ + baseUrl: "https://core.example/", + apiKey: "test-key", + transport: { + fetch: async (url, init) => { + requestedUrl = url; + requestedHeaders = init?.headers; + return new Response(JSON.stringify({ + events: [{ id: "event-1", wallet, type: "joined", occurredAt: "2026-08-19T12:00:00.000Z", roles: ["member"] }], + nextCursor: "cursor-2", + hasMore: true, + })); + }, + retry: { maxAttempts: 1 }, + }, + }); + + const history = await client.membership.getHistory(wallet, { + cursor: "cursor-1", + limit: 25, + eventTypes: ["joined", "role_changed"], + }); + + assert.equal(requestedUrl, "https://core.example/v1/memberships/wallet/0xAbC123/history?cursor=cursor-1&limit=25&eventTypes=joined%2Crole_changed"); + assert.equal(requestedHeaders.authorization, "Bearer test-key"); + assert.equal(history.events[0].type, "joined"); + assert.equal(history.nextCursor, "cursor-2"); + assert.equal(history.hasMore, true); +}); + +test("membership.getHistory returns an empty page", async () => { + const client = new IntegrationClient({ + baseUrl: "https://core.example", + transport: { fetch: async () => new Response(JSON.stringify({ events: [], nextCursor: null, hasMore: false })) }, + }); + + const history = await client.membership.getHistory(wallet); + + assert.deepEqual(history, { events: [], nextCursor: null, hasMore: false }); +}); + +test("membership.getHistory propagates HTTP errors", async () => { + const client = new IntegrationClient({ + baseUrl: "https://core.example", + transport: { + fetch: async () => new Response("Unavailable", { status: 503, statusText: "Service Unavailable" }), + retry: { maxAttempts: 1 }, + }, + }); + + await assert.rejects( + client.membership.getHistory(wallet), + (error) => error instanceof UpstreamError && error.status === 503, + ); +}); \ No newline at end of file From 2334186d39e729075e8f3c7901e219b2eec9dcbc Mon Sep 17 00:00:00 2001 From: GABRIELOFGOD Date: Wed, 19 Aug 2026 11:59:20 +0200 Subject: [PATCH 3/3] Add unit test coverage for adaptive concurrency throttling Closes #330 --- apps/access-api/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/access-api/src/index.ts b/apps/access-api/src/index.ts index a5e47a6..0a36f80 100644 --- a/apps/access-api/src/index.ts +++ b/apps/access-api/src/index.ts @@ -71,7 +71,7 @@ async function main() { console.log( `[LeaderElection] Started. Instance: ${leaderElection.getInstanceId()}, ` + - `Role: ${leaderElection.getStatus().role}`, + `Role: ${leaderElection.getStatus().role}.`, ); }