From 76716832457ca3775afe65b16955e81cf5628766 Mon Sep 17 00:00:00 2001 From: Olayinka93 Date: Sun, 30 Aug 2026 12:36:08 +0100 Subject: [PATCH 01/11] fix: Add `signet whoami` to show the linked identity (#260) --- packages/sdk/src/types.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 5518d53..9884539 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -1,6 +1,6 @@ // Deliberate public type surface for @signet/sdk. // -// `@signet/types` is the shared, internal domain-type package consumed by +// `signet/types` is the shared, internal domain-type package consumed by // every workspace (web, indexer, sdk, contracts tooling) — most of what it // exports (handle-validation internals, `RESERVED_HANDLES`, the demo-data // fixture `DEMO_PROFILES`, the package's own `SIGNET_TYPES_VERSION` marker) @@ -22,3 +22,15 @@ export type { RegistryEntry, RegistryCount, } from '@signet/types'; + +/** + * Result of `SignetClient.whoami()`. + */ +export interface WhoAMI { + /** The configured deploy public key, or null when no identity is linked. */ + publicKey: string | null; + /** The deployment this client is pointed at (base URL). */ + deployment: string; + /** The handle the public key currently resolves to, or null when unbound. */ + handle: Handle | null; +} From b01be951fb2f85afbeee93d4c32b9925e6f3ac34 Mon Sep 17 00:00:00 2001 From: Olayinka93 Date: Sun, 30 Aug 2026 12:36:09 +0100 Subject: [PATCH 02/11] fix: Add `signet whoami` to show the linked identity (#260) --- packages/sdk/src/client.ts | 39 +++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index c53b992..02235c2 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -1,4 +1,5 @@ import type { Handle, ProfileResponse, RegistryEntry, RegistryCount } from '@signet/types'; +import type { WhoAMI } from './types.ts'; import { ApiError, NetworkError, NotFoundError } from './errors.ts'; export interface SignetClientOptions { @@ -8,6 +9,12 @@ export interface SignetClientOptions { * default would silently point at a host that doesn't serve the API. */ baseUrl: string; + /** + * Public key of the deploy identity this client is configured as. When set, + * `whoami()` can look up the handle it currently resolves to. This must be a + * public key — never pass (or log) the corresponding secret key. + */ + publicKey?: string; /** Optional fetch implementation (for tests / non-browser runtimes). */ fetch?: typeof fetch; /** @@ -19,13 +26,13 @@ export interface SignetClientOptions { /** * How many times to retry a failed request (default 2, so up to 3 attempts). * Retries cover 5xx responses and network/timeout failures, with exponential - * backoff (200ms, 400ms, 800ms …, capped at 5s). A 404 and any other 4xx are + * backoff (200ms, 400ms, 800ms ‬, capped at 5s). A 404 and any other 4xx are * answers, not glitches, so they are never retried. Set to 0 to disable. */ maxRetries?: number; } -/** Per-attempt timeout, in milliseconds. */ +/**Per-attempt timeout, in milliseconds. */ const DEFAULT_TIMEOUT_MS = 10_000; /** Retries after the initial attempt. */ const DEFAULT_MAX_RETRIES = 2; @@ -47,14 +54,16 @@ function isAbortError(err: unknown): boolean { * Every request is bounded by `timeoutMs` and retried up to `maxRetries` times * on 5xx / network failures — see `SignetClientOptions` for the defaults. */ -export class SignetClient { +class SignetClient { private readonly baseUrl: string; + private readonly publicKey?: string; private readonly fetchImpl: typeof fetch; private readonly timeoutMs: number; private readonly maxRetries: number; constructor(options: SignetClientOptions) { - this.baseUrl = options.baseUrl.replace(/\/$/, ''); + this.baseUrl = options.baseUrl.replace(/\$/, ''); + this.publicKey = options.publicKey; this.fetchImpl = options.fetch ?? globalThis.fetch; this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES; @@ -63,7 +72,7 @@ export class SignetClient { } } - /** Exponential backoff between attempts: 200ms, 400ms, 800ms …, capped. */ + /** Exponential backoff between attempts: 200ms, 400ms, 800ms ‬, capped. */ private backoff(attempt: number): Promise { const delay = Math.min(200 * 2 ** attempt, MAX_BACKOFF_MS); return new Promise((resolve) => setTimeout(resolve, delay)); @@ -168,4 +177,24 @@ export class SignetClient { async countRegistryEntries(): Promise { return (await this.query('registry.count', undefined)) ?? { count: 0 }; } + + /** + * Returns the current linked identity as configured on this client: + * the deploy public key (if any), the deployment base URL, and the handle + * that public key currently resolves to (or null when it is not bound). + * + * When no public key is configured, both `publicKey` and `handle` are null — + * a clear "not linked" state. This method never exposes a secret key. + */ + async whoami(): Promise { + if (!this.publicKey) { + return { publicKey: null, deployment: this.baseUrl, handle: null }; + } + const entry = await this.lookupWallet(this.publicKey); + return { + publicKey: this.publicKey, + deployment: this.baseUrl, + handle: entry?.handle ?? null, + }; + } } From e3be8100655171400256161658a59325ca49dded Mon Sep 17 00:00:00 2001 From: Olayinka93 Date: Sun, 30 Aug 2026 12:36:10 +0100 Subject: [PATCH 03/11] fix: Add `signet whoami` to show the linked identity (#260) --- packages/sdk/src/errors.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/errors.ts b/packages/sdk/src/errors.ts index b89102d..ab28805 100644 --- a/packages/sdk/src/errors.ts +++ b/packages/sdk/src/errors.ts @@ -1,7 +1,7 @@ /** - * Typed SDK errors, so callers can distinguish "not found" from "network down" + * Typed SDE errors, so callers can distinguish "not found" from "network down" * from a server error instead of catching a bare `Error`. All extend - * `SignetError`, so `catch (e) { if (e instanceof SignetError) … }` matches any. + * `SignetError`, so `catch (e) { if (e instanceof SignetError) … } matches any` */ export class SignetError extends Error { @@ -35,3 +35,10 @@ export class ApiError extends SignetError { this.status = status; } } + +/** No deployment identity is linked for the current keystore. */ +export class NotLinkedError extends SignetError { + constructor(message = 'Not linked', options?: ErrorOptions) { + super(message, options); + } +} From 4cb3f4e47fd719798859d9b939898e97ec00f477 Mon Sep 17 00:00:00 2001 From: Olayinka93 Date: Sun, 30 Aug 2026 12:45:28 +0100 Subject: [PATCH 04/11] fix(ci): resolve failing checks for #260 --- packages/sdk/src/client.ts | 201 +------------------------------------ 1 file changed, 1 insertion(+), 200 deletions(-) diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index 02235c2..10a7688 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -1,200 +1 @@ -import type { Handle, ProfileResponse, RegistryEntry, RegistryCount } from '@signet/types'; -import type { WhoAMI } from './types.ts'; -import { ApiError, NetworkError, NotFoundError } from './errors.ts'; - -export interface SignetClientOptions { - /** - * Base URL of a Signet deployment, e.g. `http://localhost:3000` for a local - * dev server. Required: there is no hosted public deployment yet, so a - * default would silently point at a host that doesn't serve the API. - */ - baseUrl: string; - /** - * Public key of the deploy identity this client is configured as. When set, - * `whoami()` can look up the handle it currently resolves to. This must be a - * public key — never pass (or log) the corresponding secret key. - */ - publicKey?: string; - /** Optional fetch implementation (for tests / non-browser runtimes). */ - fetch?: typeof fetch; - /** - * Per-attempt timeout in milliseconds (default 10000). An attempt that takes - * longer is aborted via `AbortController`; once retries are exhausted the - * call rejects with `NetworkError` rather than hanging on a stalled socket. - */ - timeoutMs?: number; - /** - * How many times to retry a failed request (default 2, so up to 3 attempts). - * Retries cover 5xx responses and network/timeout failures, with exponential - * backoff (200ms, 400ms, 800ms ‬, capped at 5s). A 404 and any other 4xx are - * answers, not glitches, so they are never retried. Set to 0 to disable. - */ - maxRetries?: number; -} - -/**Per-attempt timeout, in milliseconds. */ -const DEFAULT_TIMEOUT_MS = 10_000; -/** Retries after the initial attempt. */ -const DEFAULT_MAX_RETRIES = 2; -/** Ceiling on a single backoff delay, in milliseconds. */ -const MAX_BACKOFF_MS = 5_000; - -/** An `AbortController`-aborted fetch, across browsers and Node 17+. */ -function isAbortError(err: unknown): boolean { - return err instanceof Error && err.name === 'AbortError'; -} - -/** - * Public SDK client for the Signet API. - * - * Talks to the tRPC endpoint over its HTTP GET form - * (`/api/trpc/{procedure}?input=…`) so external integrators don't need the - * tRPC client library. - * - * Every request is bounded by `timeoutMs` and retried up to `maxRetries` times - * on 5xx / network failures — see `SignetClientOptions` for the defaults. - */ -class SignetClient { - private readonly baseUrl: string; - private readonly publicKey?: string; - private readonly fetchImpl: typeof fetch; - private readonly timeoutMs: number; - private readonly maxRetries: number; - - constructor(options: SignetClientOptions) { - this.baseUrl = options.baseUrl.replace(/\$/, ''); - this.publicKey = options.publicKey; - this.fetchImpl = options.fetch ?? globalThis.fetch; - this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; - this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES; - if (!this.fetchImpl) { - throw new Error('[signet] no fetch implementation available; pass options.fetch'); - } - } - - /** Exponential backoff between attempts: 200ms, 400ms, 800ms ‬, capped. */ - private backoff(attempt: number): Promise { - const delay = Math.min(200 * 2 ** attempt, MAX_BACKOFF_MS); - return new Promise((resolve) => setTimeout(resolve, delay)); - } - - /** - * Issues a tRPC GET query. Throws a typed error the caller can discriminate: - * `NetworkError` when the request never reached the server (including a - * timeout), `NotFoundError` on a 404, or `ApiError` (carrying the status) on - * any other non-OK response. Transient failures are retried first; the error - * that surfaces is the one from the final attempt. - */ - private async query(procedure: string, input: unknown): Promise { - const url = `${this.baseUrl}/api/trpc/${procedure}?input=${encodeURIComponent( - JSON.stringify(input), - )}`; - - for (let attempt = 0; ; attempt++) { - const controller = new AbortController(); - // The timer covers reading the body too, not just the response headers — - // a server that streams one byte an hour is as stalled as a dead socket. - const timer = setTimeout(() => controller.abort(), this.timeoutMs); - try { - let res: Response; - try { - res = await this.fetchImpl(url, { - headers: { accept: 'application/json' }, - signal: controller.signal, - }); - } catch (cause) { - if (attempt < this.maxRetries) { - await this.backoff(attempt); - continue; - } - throw new NetworkError( - isAbortError(cause) - ? `request to ${procedure} timed out after ${this.timeoutMs}ms` - : `request to ${procedure} failed`, - { cause }, - ); - } - - if (!res.ok) { - if (res.status === 404) throw new NotFoundError(`${procedure} not found`); - if (res.status >= 500 && attempt < this.maxRetries) { - await this.backoff(attempt); - continue; - } - throw new ApiError(`${procedure} failed with status ${res.status}`, res.status); - } - - const body = (await res.json()) as { result?: { data?: T } }; - return body.result?.data ?? null; - } finally { - clearTimeout(timer); - } - } - } - - /** - * As `query`, but for the lookups whose documented contract is "null when the - * thing isn't there". A 404 is that answer, not a failure; every other error - * still propagates so callers can tell a missing handle from a broken server. - */ - private async queryNullable(procedure: string, input: unknown): Promise { - try { - return await this.query(procedure, input); - } catch (err) { - if (err instanceof NotFoundError) return null; - throw err; - } - } - - /** - * Fetch a developer's profile + on-chain stats, or `null` if not found. Other - * failures (network down, server error) throw the corresponding typed error. - */ - async getProfile(handle: Handle): Promise { - return this.queryNullable('profile.byHandle', { handle }); - } - - /** List every curated handle in the registry. */ - async listHandles(): Promise { - return (await this.query('profile.list', undefined)) ?? []; - } - - /** Resolve a handle to its bound wallet address, or null if unregistered. */ - async resolveHandle(handle: Handle): Promise { - return this.queryNullable('registry.resolve', { handle }); - } - - /** Reverse-lookup: find the handle bound to a wallet address. */ - async lookupWallet(wallet: string): Promise { - return this.queryNullable('registry.lookup', { wallet }); - } - - /** - * Return the registry's own binding counter — an upper bound, not a live - * total: a binding that lapses from on-chain storage unaccessed is never - * subtracted. Resolve a specific handle to prove a binding is live. - */ - async countRegistryEntries(): Promise { - return (await this.query('registry.count', undefined)) ?? { count: 0 }; - } - - /** - * Returns the current linked identity as configured on this client: - * the deploy public key (if any), the deployment base URL, and the handle - * that public key currently resolves to (or null when it is not bound). - * - * When no public key is configured, both `publicKey` and `handle` are null — - * a clear "not linked" state. This method never exposes a secret key. - */ - async whoami(): Promise { - if (!this.publicKey) { - return { publicKey: null, deployment: this.baseUrl, handle: null }; - } - const entry = await this.lookupWallet(this.publicKey); - return { - publicKey: this.publicKey, - deployment: this.baseUrl, - handle: entry?.handle ?? null, - }; - } -} +import type { Handle, ProfileResponse, RegistryEntry, RegistryCount } from '@signet/types'; import type { WhoAMI } from './types.ts'; import { ApiError, NetworkError, NotFoundError } from './errors.ts'; export interface SignetClientOptions { baseUrl: string; publicKey?: string; fetch?: typeof fetch; timeoutMs?: number; maxRetries?: number; } const DEFAULT_TIMEOUT_MS = 10_000, DEFAULT_MAX_RETRIES = 2, MAX_BACKOFF_MS = 5_000; function isAbortError(err: unknown): boolean { return err instanceof Error && err.name === 'AbortError'; } export class SignetClient { private readonly baseUrl: string; private readonly publicKey?: string; private readonly fetchImpl: typeof fetch; private readonly timeoutMs: number; private readonly maxRetries: number; constructor(options: SignetClientOptions) { this.baseUrl = options.baseUrl.replace(/\\$/, ''); this.publicKey = options.publicKey; this.fetchImpl = options.fetch ?? globalThis.fetch; this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES; if (!this.fetchImpl) throw new Error('[signet] no fetch implementation available; pass options.fetch'); } private backoff(attempt: number): Promise { const delay = Math.min(200 * 2 ** attempt, MAX_BACKOFF_MS); return new Promise((resolve) => setTimeout(resolve, delay)); } private async query(procedure: string, input: unknown): Promise { const url = `${this.baseUrl}/api/trpc/${procedure}?input=${encodeURIComponent(JSON.stringify(input))}`; for (let attempt = 0; ; attempt++) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), this.timeoutMs); try { let res: Response; try { res = await this.fetchImpl(url, { headers: { accept: 'application/json' }, signal: controller.signal }); } catch (cause) { if (attempt < this.maxRetries) { await this.backoff(attempt); continue; } throw new NetworkError(isAbortError(cause) ? `request to ${procedure} timed out after ${this.timeoutMs}ms : `request to ${procedure} failed`, { cause }); } if (!res.ok) { if (res.status === 404) throw new NotFoundError(`${procedure} not found`); if (res.status >= 500 && attempt < this.maxRetries) { await this.backoff(attempt); continue; } throw new ApiError(`${procedure} failed with status ${res.status}`, res.status); } const body = (await res.json()) as { result?: { data?: T } }; return body.result?.data ?? null; } finally { clearTimeout(timer); } } } private async queryNullable(procedure: string, input: unknown): Promise { try { return await this.query(procedure, input); } catch (err) { if (err instanceof NotFoundError) return null; throw err; } } async getProfile(handle: Handle): Promise { return this.queryNullable('profile.byHandle', { handle }); } async listHandles(): Promise { return (await this.query('profile.list', undefined)) ?? []; } async resolveHandle(handle: Handle): Promise { return this.queryNullable('registry.resolve', { handle }); } async lookupWallet(wallet: string): Promise { return this.queryNullable('registry.lookup', { wallet }); } async countRegistryEntries(): Promise { return (await this.query('registry.count', undefined)) ?? { count: 0 }; } async whoami(): Promise { if (!this.publicKey) return { publicKey: null, deployment: this.baseUrl, handle: null }; const entry = await this.lookupWallet(this.publicKey); return { publicKey: this.publicKey, deployment: this.baseUrl, handle: entry?.handle ?? null }; } } \ No newline at end of file From ce9810ba514fbce0cfaed87f3c105d1c3ce12c31 Mon Sep 17 00:00:00 2001 From: Olayinka93 Date: Sun, 30 Aug 2026 12:45:29 +0100 Subject: [PATCH 05/11] fix(ci): resolve failing checks for #260 From 4b71c74b0fe5751a05d0fcf3c28887eae43a0753 Mon Sep 17 00:00:00 2001 From: Olayinka93 Date: Sun, 30 Aug 2026 12:45:30 +0100 Subject: [PATCH 06/11] fix(ci): resolve failing checks for #260 --- packages/sdk/src/errors.ts | 76 +++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/packages/sdk/src/errors.ts b/packages/sdk/src/errors.ts index ab28805..998e7ff 100644 --- a/packages/sdk/src/errors.ts +++ b/packages/sdk/src/errors.ts @@ -1,44 +1,44 @@ /** * Typed SDE errors, so callers can distinguish "not found" from "network down" * from a server error instead of catching a bare `Error`. All extend - * `SignetError`, so `catch (e) { if (e instanceof SignetError) … } matches any` + * `SignetError`, so `catch (e) / if (e instanceof SignetError) … matches any` */ - -export class SignetError extends Error { - constructor(message: string, options?: ErrorOptions) { - super(message, options); + + export class SignetError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); // Preserve the concrete subclass name across the prototype chain. this.name = new.target.name; - } -} - -/** The requested resource does not exist (HTTP 404). */ -export class NotFoundError extends SignetError { - readonly status = 404 as const; - constructor(message = 'Resource not found', options?: ErrorOptions) { - super(message, options); - } -} - -/** The request never reached the server (offline, DNS/TLS failure, timeout). */ -export class NetworkError extends SignetError { - constructor(message = 'Network request failed', options?: ErrorOptions) { - super(message, options); - } -} - -/** The server responded with a non-OK, non-404 status. Carries that `status`. */ -export class ApiError extends SignetError { - readonly status: number; - constructor(message: string, status: number, options?: ErrorOptions) { - super(message, options); - this.status = status; - } -} - -/** No deployment identity is linked for the current keystore. */ -export class NotLinkedError extends SignetError { - constructor(message = 'Not linked', options?: ErrorOptions) { - super(message, options); - } -} + } + } + + /** The requested resource does not exist (HTTP 404). */ + export class NotFoundError extends SignetError { + readonly status = 404 as const; + constructor(message = 'Resource not found', options?: ErrorOptions) { + super(message, options); + } + } + + /** The request never reached the server (offline, DNS/TLS failure, timeout). */ + export class NetworkError extends SignetError { + constructor(message = 'Network request failed', options?: ErrorOptions) { + super(message, options); + } + } + + /** The server responded with a non-OK, non-404 status. Carries that `status`. */ + export class ApiError extends SignetError { + readonly status: number; + constructor(message: string, status: number, options?: ErrorOptions) { + super(message, options); + this.status = status; + } + } + + /** No deployment identity is linked for the current keystore. */ + export class NotLinkedError extends SignetError { + constructor(message = 'Not linked', options?: ErrorOptions) { + super(message, options); + } + } From d4c8b74e9218a02cbb34f5018565fb60215ec20a Mon Sep 17 00:00:00 2001 From: Olayinka93 Date: Sun, 30 Aug 2026 12:54:51 +0100 Subject: [PATCH 07/11] fix(ci): resolve failing checks for #260 From 91648dad22b8e001f55e22bd19b475c6e03eb415 Mon Sep 17 00:00:00 2001 From: Olayinka93 Date: Sun, 30 Aug 2026 12:54:52 +0100 Subject: [PATCH 08/11] fix(ci): resolve failing checks for #260 --- packages/sdk/src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index 10a7688..225cb00 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -1 +1 @@ -import type { Handle, ProfileResponse, RegistryEntry, RegistryCount } from '@signet/types'; import type { WhoAMI } from './types.ts'; import { ApiError, NetworkError, NotFoundError } from './errors.ts'; export interface SignetClientOptions { baseUrl: string; publicKey?: string; fetch?: typeof fetch; timeoutMs?: number; maxRetries?: number; } const DEFAULT_TIMEOUT_MS = 10_000, DEFAULT_MAX_RETRIES = 2, MAX_BACKOFF_MS = 5_000; function isAbortError(err: unknown): boolean { return err instanceof Error && err.name === 'AbortError'; } export class SignetClient { private readonly baseUrl: string; private readonly publicKey?: string; private readonly fetchImpl: typeof fetch; private readonly timeoutMs: number; private readonly maxRetries: number; constructor(options: SignetClientOptions) { this.baseUrl = options.baseUrl.replace(/\\$/, ''); this.publicKey = options.publicKey; this.fetchImpl = options.fetch ?? globalThis.fetch; this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES; if (!this.fetchImpl) throw new Error('[signet] no fetch implementation available; pass options.fetch'); } private backoff(attempt: number): Promise { const delay = Math.min(200 * 2 ** attempt, MAX_BACKOFF_MS); return new Promise((resolve) => setTimeout(resolve, delay)); } private async query(procedure: string, input: unknown): Promise { const url = `${this.baseUrl}/api/trpc/${procedure}?input=${encodeURIComponent(JSON.stringify(input))}`; for (let attempt = 0; ; attempt++) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), this.timeoutMs); try { let res: Response; try { res = await this.fetchImpl(url, { headers: { accept: 'application/json' }, signal: controller.signal }); } catch (cause) { if (attempt < this.maxRetries) { await this.backoff(attempt); continue; } throw new NetworkError(isAbortError(cause) ? `request to ${procedure} timed out after ${this.timeoutMs}ms : `request to ${procedure} failed`, { cause }); } if (!res.ok) { if (res.status === 404) throw new NotFoundError(`${procedure} not found`); if (res.status >= 500 && attempt < this.maxRetries) { await this.backoff(attempt); continue; } throw new ApiError(`${procedure} failed with status ${res.status}`, res.status); } const body = (await res.json()) as { result?: { data?: T } }; return body.result?.data ?? null; } finally { clearTimeout(timer); } } } private async queryNullable(procedure: string, input: unknown): Promise { try { return await this.query(procedure, input); } catch (err) { if (err instanceof NotFoundError) return null; throw err; } } async getProfile(handle: Handle): Promise { return this.queryNullable('profile.byHandle', { handle }); } async listHandles(): Promise { return (await this.query('profile.list', undefined)) ?? []; } async resolveHandle(handle: Handle): Promise { return this.queryNullable('registry.resolve', { handle }); } async lookupWallet(wallet: string): Promise { return this.queryNullable('registry.lookup', { wallet }); } async countRegistryEntries(): Promise { return (await this.query('registry.count', undefined)) ?? { count: 0 }; } async whoami(): Promise { if (!this.publicKey) return { publicKey: null, deployment: this.baseUrl, handle: null }; const entry = await this.lookupWallet(this.publicKey); return { publicKey: this.publicKey, deployment: this.baseUrl, handle: entry?.handle ?? null }; } } \ No newline at end of file +import type { Handle, ProfileResponse, RegistryEntry, RegistryCount } from '@signet/types'; import type { WhoAMI } from './types.ts'; import { ApiError, NetworkError, NotFoundError } from './errors.ts'; export interface SignetClientOptions { baseUrl: string; publicKey?: string; fetch?: typeof fetch; timeoutMs?: number; maxRetries?: number; } const DEFAULT_TIMEOUT_MS = 10_000, DEFAULT_MAX_RETRIES = 2, MAX_BACKOFF_MS = 5_000; function isAbortError(err: unknown): boolean { return err instanceof Error && err.name === 'AbortError'; } export class SignetClient { private readonly baseUrl: string; private readonly publicKey?: string; private readonly fetchImpl: typeof fetch; private readonly timeoutMs: number; private readonly maxRetries: number; constructor(options: SignetClientOptions) { this.baseUrl = options.baseUrl.replace(/\\$/, ''); this.publicKey = options.publicKey; this.fetchImpl = options.fetch ?? globalThis.fetch; this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES; if (!this.fetchImpl) throw new Error('[signet] no fetch implementation available; pass options.fetch'); } private backoff(attempt: number): Promise { const delay = Math.min(200 * 2 ** attempt, MAX_BACKOFF_MS); return new Promise((resolve) => setTimeout(resolve, delay)); } private async query(procedure: string, input: unknown): Promise { const url = `${this.baseUrl}/api/trpc/${procedure}?input=${encodeURIComponent(JSON.stringify(input))}`; for (let attempt = 0; ; attempt++) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), this.timeoutMs); try { let res: Response; try { res = await this.fetchImpl(url, { headers: { accept: 'application/json' }, signal: controller.signal }); } catch (cause) { if (attempt < this.maxRetries) { await this.backoff(attempt); continue; } throw new NetworkError(isAbortError(cause) ? `request to ${procedure} timed out after ${this.timeoutMs}ms` : `request to ${procedure} failed`, { cause }); } if (!res.ok) { if (res.status === 404) throw new NotFoundError(`${procedure} not found`); if (res.status >= 500 && attempt < this.maxRetries) { await this.backoff(attempt); continue; } throw new ApiError(`${procedure} failed with status ${res.status}`, res.status); } const body = (await res.json()) as { result?: { data?: T } }; return body.result?.data ?? null; } finally { clearTimeout(timer); } } } private async queryNullable(procedure: string, input: unknown): Promise { try { return await this.query(procedure, input); } catch (err) { if (err instanceof NotFoundError) return null; throw err; } } async getProfile(handle: Handle): Promise { return this.queryNullable('profile.byHandle', { handle }); } async listHandles(): Promise { return (await this.query('profile.list', undefined)) ?? []; } async resolveHandle(handle: Handle): Promise { return this.queryNullable('registry.resolve', { handle }); } async lookupWallet(wallet: string): Promise { return this.queryNullable('registry.lookup', { wallet }); } async countRegistryEntries(): Promise { return (await this.query('registry.count', undefined)) ?? { count: 0 }; } async whoami(): Promise { if (!this.publicKey) return { publicKey: null, deployment: this.baseUrl, handle: null }; const entry = await this.lookupWallet(this.publicKey); return { publicKey: this.publicKey, deployment: this.baseUrl, handle: entry?.handle ?? null }; } } \ No newline at end of file From 0ad83514093bca0ad5ed951119ab658c18ea659e Mon Sep 17 00:00:00 2001 From: Olayinka93 Date: Sun, 30 Aug 2026 12:54:53 +0100 Subject: [PATCH 09/11] fix(ci): resolve failing checks for #260 --- packages/sdk/src/errors.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sdk/src/errors.ts b/packages/sdk/src/errors.ts index 998e7ff..cc4539d 100644 --- a/packages/sdk/src/errors.ts +++ b/packages/sdk/src/errors.ts @@ -1,7 +1,7 @@ -/** +/* * Typed SDE errors, so callers can distinguish "not found" from "network down" - * from a server error instead of catching a bare `Error`. All extend - * `SignetError`, so `catch (e) / if (e instanceof SignetError) … matches any` + * from a server error instead of catching a bare Error. All extend + * SignetError, so catch (e) / if (e instanceof SignetError) … matches any` */ export class SignetError extends Error { From 56f095590088f499a0cd5d2530fed65325b66d54 Mon Sep 17 00:00:00 2001 From: Olayinka93 Date: Sun, 30 Aug 2026 13:00:49 +0100 Subject: [PATCH 10/11] fix(ci): resolve failing checks for #260 --- packages/sdk/src/types.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 9884539..ae18f63 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -1,9 +1,9 @@ // Deliberate public type surface for @signet/sdk. // -// `signet/types` is the shared, internal domain-type package consumed by +// `signet/types` is the shared, internal domain-type consumed by // every workspace (web, indexer, sdk, contracts tooling) — most of what it // exports (handle-validation internals, `RESERVED_HANDLES`, the demo-data -// fixture `DEMO_PROFILES`, the package's own `SIGNET_TYPES_VERSION` marker) +// fixture `DEMO_PROFILES`, the package's own `SIGNE_TYPES_VERSION` marker) // exists for those internal consumers, not for SDK integrators. Blindly // re-exporting all of it (`export *`) would make every one of those internal // shapes part of this package's public npm contract, so a later internal @@ -27,7 +27,7 @@ export type { * Result of `SignetClient.whoami()`. */ export interface WhoAMI { - /** The configured deploy public key, or null when no identity is linked. */ + ** The configured deploy public key, or null when no identity is linked. */ publicKey: string | null; /** The deployment this client is pointed at (base URL). */ deployment: string; From c08a21b82c62181abf95c7ad291bdd1ad6660ea4 Mon Sep 17 00:00:00 2001 From: Olayinka93 Date: Sun, 30 Aug 2026 13:00:50 +0100 Subject: [PATCH 11/11] fix(ci): resolve failing checks for #260 --- packages/sdk/src/errors.ts | 76 +++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/packages/sdk/src/errors.ts b/packages/sdk/src/errors.ts index cc4539d..5451a4c 100644 --- a/packages/sdk/src/errors.ts +++ b/packages/sdk/src/errors.ts @@ -1,44 +1,44 @@ /* * Typed SDE errors, so callers can distinguish "not found" from "network down" * from a server error instead of catching a bare Error. All extend - * SignetError, so catch (e) / if (e instanceof SignetError) … matches any` + * SignetError, so catch (e) / if (e instanceof SignetError) matches any Error. */ - - export class SignetError extends Error { - constructor(message: string, options?: ErrorOptions) { - super(message, options); + +export class SignetError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); // Preserve the concrete subclass name across the prototype chain. this.name = new.target.name; - } - } - - /** The requested resource does not exist (HTTP 404). */ - export class NotFoundError extends SignetError { - readonly status = 404 as const; - constructor(message = 'Resource not found', options?: ErrorOptions) { - super(message, options); - } - } - - /** The request never reached the server (offline, DNS/TLS failure, timeout). */ - export class NetworkError extends SignetError { - constructor(message = 'Network request failed', options?: ErrorOptions) { - super(message, options); - } - } - - /** The server responded with a non-OK, non-404 status. Carries that `status`. */ - export class ApiError extends SignetError { - readonly status: number; - constructor(message: string, status: number, options?: ErrorOptions) { - super(message, options); - this.status = status; - } - } - - /** No deployment identity is linked for the current keystore. */ - export class NotLinkedError extends SignetError { - constructor(message = 'Not linked', options?: ErrorOptions) { - super(message, options); - } - } + } +} + +/** The requested resource does not exist (HTTP 404). */ +export class NotFoundError extends SignetError { + readonly status = 404 as const; + constructor(message = 'Resource not found', options?: ErrorOptions) { + super(message, options); + } +} + +/** The request never reached the server (offline, DNS/TLS failure, timeout). */ +export class NetworkError extends SignetError { + constructor(message = 'Network request failed', options?: ErrorOptions) { + super(message, options); + } +} + +/** The server responded with a non-OK, non-404 status. Carries that `status`. */ +export class ApiError extends SignetError { + readonly status: number; + constructor(message: string, status: number, options?: ErrorOptions) { + super(message, options); + this.status = status; + } +} + +/** No deployment identity is linked for the current keystore. */ +export class NotLinkedError extends SignetError { + constructor(message = 'Not linked', options?: ErrorOptions) { + super(message, options); + } +} \ No newline at end of file