@reclaimprotocol/client 0.1.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ /**
2
+ * This file was auto-generated by openapi-typescript.
3
+ * Do not make direct changes to the file.
4
+ */
5
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@reclaimprotocol/client",
3
+ "title": "Reclaim Client",
4
+ "version": "0.1.0-dev.1",
5
+ "description": "Typed SDK client for @reclaimprotocol/app. Used by @reclaimprotocol/agent and any other consumer that needs programmatic API access without the full agent.",
6
+ "type": "module",
7
+ "main": "./lib/index.js",
8
+ "types": "./lib/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./lib/index.d.ts",
12
+ "import": "./lib/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "lib",
17
+ "src",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "engines": {
22
+ "node": ">=20"
23
+ },
24
+ "repository": {
25
+ "url": "https://github.com/reclaimprotocol/builder",
26
+ "type": "git"
27
+ },
28
+ "websiteUrl": "https://docs.reclaimprotocol.org",
29
+ "scripts": {
30
+ "build": "tsc",
31
+ "openapi:typegen": "openapi-typescript ../app/openapi.yaml --output ./src/types/openapi.gen.ts && node ../app/scripts/mcp-tools-gen.ts",
32
+ "prepublishOnly": "npm run build",
33
+ "dev": "tsc --watch",
34
+ "test": "node --test tests/*.test.ts"
35
+ },
36
+ "keywords": [
37
+ "sdk",
38
+ "reclaimprotocol",
39
+ "verification",
40
+ "http-client"
41
+ ],
42
+ "author": {
43
+ "name": "Reclaim Protocol",
44
+ "email": "support@reclaimprotocol.org"
45
+ },
46
+ "license": "MIT",
47
+ "dependencies": {
48
+ "@hapi/boom": "^9.1.4"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^24.0.0",
52
+ "openapi-typescript": "^7.13.0",
53
+ "typescript": "^5.9.0",
54
+ "yaml": "^2.6.0"
55
+ }
56
+ }
package/src/client.ts ADDED
@@ -0,0 +1,146 @@
1
+ import { notFound } from '@hapi/boom'
2
+ import assert from 'node:assert'
3
+ import type {
4
+ IOperationArgs,
5
+ IOperationId,
6
+ IOperationResponse,
7
+ } from './types/openapi.ts'
8
+ import { DEFAULT_BASE_URL, USER_AGENT } from './consts.ts'
9
+ import { throwProblemError } from './errors.ts'
10
+ // `ROUTES` is generated from `packages/app/openapi.yaml` and is the
11
+ // single source of method/path mapping for every operation. New
12
+ // endpoints added to the spec are picked up by re-running
13
+ // `npm run openapi:typegen --workspace=packages/client`.
14
+ import { ROUTES } from './routes.gen.ts'
15
+
16
+ export type ReclaimClientOptions = {
17
+ /** Backend base URL. Defaults to {@link DEFAULT_BASE_URL}. */
18
+ baseUrl?: string
19
+ /** Bearer token for authenticated operations. */
20
+ token?: string
21
+ /** Custom fetch implementation. Defaults to global `fetch`. */
22
+ fetch?: typeof fetch
23
+ /** Extra headers merged into every request. */
24
+ headers?: Record<string, string>
25
+ }
26
+
27
+ /**
28
+ * Core HTTP client. Shared by the SDK entrypoint, MCP server, and CLI —
29
+ * extend behaviour here rather than duplicating it across surfaces.
30
+ *
31
+ * Per @reclaimprotocol/app conventions, validation lives in the openapi
32
+ * spec; this client forwards requests and maps RFC 9457 problem responses
33
+ * to {@link ProblemError}.
34
+ */
35
+ export class ReclaimClient {
36
+ readonly baseUrl: string
37
+ #token: string | undefined
38
+ #fetch: typeof fetch
39
+ #headers: Record<string, string>
40
+
41
+ constructor(opts: ReclaimClientOptions = {}) {
42
+ this.baseUrl = (opts.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, '')
43
+ this.#token = opts.token
44
+ this.#fetch = opts.fetch || fetch
45
+ this.#headers = {
46
+ 'User-Agent': USER_AGENT,
47
+ Accept: 'application/json',
48
+ ...opts.headers,
49
+ }
50
+ }
51
+
52
+ setToken(token: string | undefined) {
53
+ this.#token = token
54
+ }
55
+
56
+ getToken() {
57
+ return this.#token
58
+ }
59
+
60
+ async call<T extends IOperationId>(
61
+ operationId: T,
62
+ ...argsRest: {} extends IOperationArgs<T>
63
+ ? [args?: IOperationArgs<T>]
64
+ : [args: IOperationArgs<T>]
65
+ ): Promise<{ data: IOperationResponse<T>, response: Response }> {
66
+ const args = argsRest[0] ?? {} as IOperationArgs<T>
67
+ const op = ROUTES[operationId] as
68
+ | { method: string, path: string }
69
+ | undefined
70
+ assert(op, notFound(`Unknown operation: ${String(operationId)}`))
71
+
72
+ let url = `${this.baseUrl}${op.path}`
73
+ if(args.params) {
74
+ for(const [key, value] of Object.entries(args.params)) {
75
+ url = url.replace(`{${key}}`, String(value))
76
+ }
77
+ }
78
+
79
+ if(args.query && Object.keys(args.query).length > 0) {
80
+ const search = new URLSearchParams()
81
+ for(const [key, value] of Object.entries(args.query)) {
82
+ if(value === undefined || value === null) {
83
+ continue
84
+ }
85
+
86
+ if(Array.isArray(value)) {
87
+ for(const v of value) {
88
+ search.append(key, String(v))
89
+ }
90
+ } else {
91
+ search.append(key, String(value))
92
+ }
93
+ }
94
+
95
+ url += `?${search.toString()}`
96
+ }
97
+
98
+ const headers: Record<string, string> = {
99
+ ...this.#headers,
100
+ Accept: 'application/json',
101
+ }
102
+ if(this.#token) {
103
+ headers['Authorization'] = `Bearer ${this.#token}`
104
+ }
105
+
106
+ // Per-request header parameters (e.g. `If-Match` for optimistic
107
+ // concurrency). Applied last so spec-declared headers win over the
108
+ // client-wide defaults, but never override Authorization.
109
+ if(args.headers) {
110
+ for(const [key, value] of Object.entries(args.headers)) {
111
+ if(value === undefined || value === null) {
112
+ continue
113
+ }
114
+
115
+ headers[key] = String(value)
116
+ }
117
+ }
118
+
119
+ const init: RequestInit = {
120
+ method: op.method.toUpperCase(),
121
+ headers,
122
+ }
123
+ // Write methods always carry a JSON body (defaulting to `{}`) plus a
124
+ // Content-Type. Some endpoints — e.g. the auth-only attach session —
125
+ // take an empty body, and omitting Content-Type makes the server
126
+ // reject the request with `415 unsupported media type`.
127
+ const method = op.method.toLowerCase()
128
+ if(method !== 'get' && method !== 'head') {
129
+ init.body = JSON.stringify(args.body ?? {})
130
+ headers['Content-Type'] = 'application/json'
131
+ }
132
+
133
+ const res = await this.#fetch(url, init)
134
+
135
+ if(!res.ok) {
136
+ await throwProblemError(res)
137
+ }
138
+
139
+ if(res.status === 204 || res.headers.get('content-length') === '0') {
140
+ return { data: undefined as IOperationResponse<T>, response: res }
141
+ }
142
+
143
+ const data = await res.json() as IOperationResponse<T>
144
+ return { data, response: res }
145
+ }
146
+ }
package/src/consts.ts ADDED
@@ -0,0 +1,6 @@
1
+ // TODO: Update to production url
2
+ export const DEFAULT_BASE_URL = 'http://localhost:4001'
3
+
4
+ export const USER_AGENT = '@reclaimprotocol/client'
5
+
6
+ export const PROBLEM_CONTENT_TYPE = 'application/problem+json'
package/src/errors.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { Boom } from '@hapi/boom'
2
+ import type { ProblemDetails } from './types/openapi.ts'
3
+ import { PROBLEM_CONTENT_TYPE } from './consts.ts'
4
+ export type { ProblemDetails } from './types/openapi.ts'
5
+
6
+ export type ProblemError = Boom<ProblemDetails>
7
+
8
+ // Reads an `application/problem+json` response and throws a Boom-style
9
+ // error. Falls back to status-only mapping when the body isn't a problem doc.
10
+ export async function throwProblemError(res: Response): Promise<void> {
11
+ const contentType = res.headers.get('Content-Type') || ''
12
+ let problem: ProblemDetails
13
+ if(contentType.includes(PROBLEM_CONTENT_TYPE)) {
14
+ problem = (await res.json()) as ProblemDetails
15
+ } else {
16
+ problem = {
17
+ type: 'about:blank',
18
+ status: res.status,
19
+ title: res.statusText,
20
+ }
21
+ }
22
+
23
+ throw new Boom<ProblemDetails>(
24
+ problem.title,
25
+ {
26
+ statusCode: res.status,
27
+ data: problem,
28
+ },
29
+ )
30
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export * from './client.ts'
2
+ export type * from './client.ts'
3
+ export { ProblemError, type ProblemDetails } from './errors.ts'
4
+ export { DEFAULT_BASE_URL, PROBLEM_CONTENT_TYPE, USER_AGENT } from './consts.ts'
5
+ export {
6
+ deriveFromPrivateKey,
7
+ type EthKeypair,
8
+ generateKeypair,
9
+ } from './keypair.ts'
10
+ export * from './types/index.ts'
package/src/keypair.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { keccak_256 } from '@noble/hashes/sha3.js'
2
+ import * as secp from '@noble/secp256k1'
3
+
4
+ export type EthKeypair = {
5
+ address: string
6
+ privateKey: string
7
+ }
8
+
9
+ /**
10
+ * Generate a fresh secp256k1 key-pair and derive its lowercase
11
+ * Ethereum address (keccak-256 of the uncompressed pubkey minus the
12
+ * `0x04` prefix, last 20 bytes).
13
+ *
14
+ * Lives in the client SDK so every consumer — the MCP `issue_credentials`
15
+ * tool, scripts, future SDK callers — mints credentials the same way.
16
+ * The private key is returned to the caller and MUST stay local; only
17
+ * the derived address is ever sent to the server. The public key is an
18
+ * intermediate value used to compute the address and is not retained.
19
+ */
20
+ export function generateKeypair(): EthKeypair {
21
+ const priv = secp.utils.randomSecretKey()
22
+ const pub = secp.getPublicKey(priv, false)
23
+ return {
24
+ address: '0x' + bytesToHex(keccak_256(pub.slice(1))).slice(-40),
25
+ privateKey: '0x' + bytesToHex(priv),
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Recover the address from a hex-encoded private key. Used when
31
+ * importing a key the user already holds.
32
+ */
33
+ export function deriveFromPrivateKey(privateKey: string): EthKeypair {
34
+ const hex = privateKey.replace(/^0x/, '')
35
+ const priv = hexToBytes(hex)
36
+ const pub = secp.getPublicKey(priv, false)
37
+ return {
38
+ address: '0x' + bytesToHex(keccak_256(pub.slice(1))).slice(-40),
39
+ privateKey: '0x' + hex,
40
+ }
41
+ }
42
+
43
+ function bytesToHex(bytes: Uint8Array): string {
44
+ let out = ''
45
+ for(const b of bytes) {
46
+ out += b.toString(16).padStart(2, '0')
47
+ }
48
+
49
+ return out
50
+ }
51
+
52
+ function hexToBytes(hex: string): Uint8Array {
53
+ if(hex.length % 2 !== 0) {
54
+ throw new Error('private key hex must have even length')
55
+ }
56
+
57
+ const out = new Uint8Array(hex.length / 2)
58
+ for(let i = 0; i < out.length; i++) {
59
+ out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)
60
+ }
61
+
62
+ return out
63
+ }
@@ -0,0 +1,78 @@
1
+ // AUTO-GENERATED by packages/app/scripts/mcp-tools-gen.ts
2
+ // from packages/app/openapi.yaml.
3
+ // Run `npm run openapi:typegen --workspace=packages/client`
4
+ // to regenerate. Do not edit by hand.
5
+
6
+ export const ROUTES = {
7
+ "AddOrgMember": {"method":"post","path":"/orgs/{orgId}/members"},
8
+ "AdminHome": {"method":"get","path":"/admin"},
9
+ "AdminOrgs": {"method":"get","path":"/admin/orgs"},
10
+ "AdminProviders": {"method":"get","path":"/admin/providers"},
11
+ "AdminReviews": {"method":"get","path":"/admin/reviews"},
12
+ "AdminThemes": {"method":"get","path":"/admin/themes"},
13
+ "AdminUsers": {"method":"get","path":"/admin/users"},
14
+ "AttachPage": {"method":"get","path":"/attach/{code}"},
15
+ "CancelPublicListingRequest": {"method":"post","path":"/providers/{providerId}/versions/{version}/cancel-review"},
16
+ "ComponentsDemo": {"method":"get","path":"/components_demo"},
17
+ "ConfirmAttachSession": {"method":"post","path":"/credentials/attach/sessions/{code}/confirm"},
18
+ "CreateAttachSession": {"method":"post","path":"/credentials/attach/sessions"},
19
+ "CreateOrg": {"method":"post","path":"/orgs"},
20
+ "CreateProvider": {"method":"post","path":"/providers"},
21
+ "CreateProviderVersion": {"method":"post","path":"/providers/{providerId}/versions"},
22
+ "CreateTheme": {"method":"post","path":"/orgs/{orgId}/themes"},
23
+ "CredentialAuthChallenge": {"method":"post","path":"/credentials/auth/challenge"},
24
+ "CredentialAuthVerify": {"method":"post","path":"/credentials/auth/verify"},
25
+ "DecideProviderVersionReview": {"method":"post","path":"/providers/{providerId}/versions/{version}/review-decision"},
26
+ "DeleteOrg": {"method":"delete","path":"/orgs/{orgId}"},
27
+ "DeleteOrgBilling": {"method":"delete","path":"/orgs/{orgId}/billing"},
28
+ "DeleteOrgIcon": {"method":"delete","path":"/orgs/{orgId}/icon"},
29
+ "DeleteProvider": {"method":"delete","path":"/providers/{providerId}"},
30
+ "DeleteProviderVersion": {"method":"delete","path":"/providers/{providerId}/versions/{version}"},
31
+ "DeleteTheme": {"method":"delete","path":"/orgs/{orgId}/themes/{themeId}"},
32
+ "Docs": {"method":"get","path":"/docs"},
33
+ "ExplorePreviewImage": {"method":"get","path":"/og/explore.png"},
34
+ "GetAttachSession": {"method":"get","path":"/credentials/attach/sessions/{code}"},
35
+ "GetCredential": {"method":"get","path":"/credentials/{credentialId}"},
36
+ "GetCredentialByAddress": {"method":"get","path":"/credentials/by-address/{address}"},
37
+ "GetImage": {"method":"get","path":"/images/{imageId}"},
38
+ "GetMe": {"method":"get","path":"/me"},
39
+ "GetOrg": {"method":"get","path":"/orgs/{orgId}"},
40
+ "GetOrgBilling": {"method":"get","path":"/orgs/{orgId}/billing"},
41
+ "GetProvider": {"method":"get","path":"/providers/{providerId}"},
42
+ "GetProviderBySlug": {"method":"get","path":"/providers/{providerId}/{slug}"},
43
+ "GetProviderVersion": {"method":"get","path":"/providers/{providerId}/versions/{version}"},
44
+ "GetTheme": {"method":"get","path":"/orgs/{orgId}/themes/{themeId}"},
45
+ "GetUser": {"method":"get","path":"/users/{userId}"},
46
+ "Index": {"method":"get","path":"/"},
47
+ "LinkCredential": {"method":"post","path":"/credentials"},
48
+ "ListOrgCredentials": {"method":"get","path":"/orgs/{orgId}/credentials"},
49
+ "ListOrgs": {"method":"get","path":"/orgs"},
50
+ "ListProviderVersions": {"method":"get","path":"/providers/{providerId}/versions"},
51
+ "ListThemes": {"method":"get","path":"/orgs/{orgId}/themes"},
52
+ "Login": {"method":"post","path":"/auth/login"},
53
+ "LoginPage": {"method":"get","path":"/login"},
54
+ "Logout": {"method":"post","path":"/logout"},
55
+ "MakePrivate": {"method":"post","path":"/providers/{providerId}/make-private"},
56
+ "OpenApiSpec": {"method":"get","path":"/openapi.yaml"},
57
+ "ProviderPreviewImage": {"method":"get","path":"/providers/{providerId}/preview.png"},
58
+ "Providers": {"method":"get","path":"/providers"},
59
+ "PublishProviderVersion": {"method":"post","path":"/providers/{providerId}/versions/{version}/publish"},
60
+ "RemoveOrgMember": {"method":"delete","path":"/orgs/{orgId}/members/{memberId}"},
61
+ "RequestPublicListing": {"method":"post","path":"/providers/{providerId}/request-public"},
62
+ "RevokeCredential": {"method":"delete","path":"/credentials/{credentialId}"},
63
+ "Robots": {"method":"get","path":"/robots.txt"},
64
+ "SetupProviderCreate": {"method":"post","path":"/providers/setup"},
65
+ "Sitemap": {"method":"get","path":"/sitemap.xml"},
66
+ "SubmitProviderVersionForReview": {"method":"post","path":"/providers/{providerId}/versions/{version}/review"},
67
+ "TransferProvider": {"method":"post","path":"/providers/{providerId}/transfer"},
68
+ "UpdateMe": {"method":"patch","path":"/me"},
69
+ "UpdateOrg": {"method":"patch","path":"/orgs/{orgId}"},
70
+ "UpdateOrgBilling": {"method":"put","path":"/orgs/{orgId}/billing"},
71
+ "UpdateOrgIcon": {"method":"put","path":"/orgs/{orgId}/icon"},
72
+ "UpdateOrgMember": {"method":"patch","path":"/orgs/{orgId}/members/{memberId}"},
73
+ "UpdateProvider": {"method":"patch","path":"/providers/{providerId}"},
74
+ "UpdateProviderVersion": {"method":"patch","path":"/providers/{providerId}/versions/{version}"},
75
+ "UpdateTheme": {"method":"patch","path":"/orgs/{orgId}/themes/{themeId}"},
76
+ } as const
77
+
78
+ export type RouteId = keyof typeof ROUTES
@@ -0,0 +1,2 @@
1
+ export * from './openapi.ts'
2
+ export type { components, operations, paths } from './openapi.gen.ts'