@ranwhenparked/trustap-sdk 0.1.0

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.
package/README.md ADDED
@@ -0,0 +1,251 @@
1
+ # @ranwhenparked/trustap-sdk
2
+
3
+ A lightweight typed [Trustap API](https://docs.trustap.com/apis/openapi) wrapper built with openapi-typescript + openapi-fetch.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @ranwhenparked/trustap-sdk
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Path-based client
14
+
15
+ ```ts
16
+ import { createTrustapClient } from "@ranwhenparked/trustap-sdk";
17
+
18
+ const trustap = createTrustapClient({
19
+ apiUrl: process.env.TRUSTAP_API_URL!,
20
+ basicAuth: {
21
+ username: process.env.TRUSTAP_API_KEY!,
22
+ password: "", // Trustap basic auth uses API key + empty password
23
+ },
24
+ });
25
+
26
+ const { data, error } = await trustap["/users/me/balances"].GET({
27
+ headers: { Authorization: `Bearer ${accessToken}` },
28
+ });
29
+
30
+ // Or verb-based via raw
31
+ const { data: d2 } = await trustap.raw.GET("/users/me/balances", {
32
+ headers: { Authorization: `Bearer ${accessToken}` },
33
+ });
34
+ ```
35
+
36
+ ### Operation ID-based client
37
+
38
+ Call API methods directly by their operation ID with full type safety:
39
+
40
+ ```ts
41
+ const { data, error } = await trustap["users.getBalances"]();
42
+
43
+ // With path parameters
44
+ const { data: tx } = await trustap["basic_client.getTransaction"]({
45
+ params: {
46
+ path: { client_id: "my-client", transaction_id: "123" },
47
+ },
48
+ });
49
+
50
+ // With query parameters
51
+ const { data: transactions } = await trustap["basic_client.getTransactions"]({
52
+ params: {
53
+ path: { client_id: "my-client" },
54
+ query: { status: "paid" },
55
+ },
56
+ });
57
+ ```
58
+
59
+ ## Authentication
60
+
61
+ The SDK supports multiple authentication strategies:
62
+
63
+ ### Basic Auth (Server-to-server)
64
+
65
+ Configure HTTP Basic auth for server-to-server endpoints:
66
+
67
+ ```ts
68
+ const trustap = createTrustapClient({
69
+ apiUrl: "https://api.trustap.com",
70
+ basicAuth: {
71
+ username: process.env.TRUSTAP_API_KEY!,
72
+ password: "", // Trustap uses API key + empty password
73
+ },
74
+ });
75
+ ```
76
+
77
+ ### OAuth2 Access Token
78
+
79
+ For user-context endpoints, provide a `getAccessToken` callback:
80
+
81
+ ```ts
82
+ const trustap = createTrustapClient({
83
+ apiUrl: "https://api.trustap.com",
84
+ basicAuth: {
85
+ username: process.env.TRUSTAP_API_KEY!,
86
+ password: "",
87
+ },
88
+ getAccessToken: async () => {
89
+ // Return the current user's OAuth2 access token
90
+ return session.accessToken;
91
+ },
92
+ });
93
+ ```
94
+
95
+ The SDK automatically selects the correct auth strategy per endpoint based on the OpenAPI spec. For endpoints that support both, it prefers Basic auth for server-to-server calls.
96
+
97
+ ### Auth Overrides
98
+
99
+ Override the automatic auth selection for specific endpoints:
100
+
101
+ ```ts
102
+ const trustap = createTrustapClient({
103
+ apiUrl: "https://api.trustap.com",
104
+ basicAuth: { username: apiKey, password: "" },
105
+ getAccessToken: async () => userAccessToken,
106
+ authOverrides: {
107
+ "/charge": "basic", // Always use Basic auth
108
+ "/users/me/balances": "oauth2", // Always use OAuth2
109
+ },
110
+ });
111
+ ```
112
+
113
+ ## Webhook Schemas
114
+
115
+ Validate incoming Trustap webhooks with Zod schemas:
116
+
117
+ ```ts
118
+ import {
119
+ trustapWebhookEventSchema,
120
+ type TrustapWebhookEvent,
121
+ } from "@ranwhenparked/trustap-sdk";
122
+
123
+ // Parse and validate webhook payload
124
+ const result = trustapWebhookEventSchema.safeParse(req.body);
125
+ if (!result.success) {
126
+ console.error("Invalid webhook:", result.error);
127
+ return;
128
+ }
129
+
130
+ const event: TrustapWebhookEvent = result.data;
131
+ console.log(event.code); // e.g., "basic_tx.paid"
132
+ ```
133
+
134
+ ### Exhaustive Handler Pattern
135
+
136
+ Use `createWebhookHandlers` for compile-time exhaustiveness checking:
137
+
138
+ ```ts
139
+ import {
140
+ createWebhookHandlers,
141
+ type TrustapWebhookEvent,
142
+ } from "@ranwhenparked/trustap-sdk";
143
+
144
+ const handlers = createWebhookHandlers({
145
+ "basic_tx.joined": (event) => {
146
+ console.log("Transaction joined at:", event.target_preview.joined);
147
+ },
148
+ "basic_tx.paid": (event) => {
149
+ console.log("Transaction paid at:", event.target_preview.paid);
150
+ },
151
+ "basic_tx.rejected": (event) => { /* ... */ },
152
+ "basic_tx.cancelled": (event) => { /* ... */ },
153
+ "basic_tx.claimed": (event) => { /* ... */ },
154
+ "basic_tx.listing_transaction_accepted": (event) => { /* ... */ },
155
+ "basic_tx.listing_transaction_rejected": (event) => { /* ... */ },
156
+ "basic_tx.payment_failed": (event) => { /* ... */ },
157
+ "basic_tx.payment_refunded": (event) => { /* ... */ },
158
+ "basic_tx.payment_review_flagged": (event) => { /* ... */ },
159
+ "basic_tx.payment_review_finished": (event) => { /* ... */ },
160
+ "basic_tx.tracking_details_submission_deadline_extended": (event) => { /* ... */ },
161
+ "basic_tx.tracked": (event) => { /* ... */ },
162
+ "basic_tx.delivered": (event) => { /* ... */ },
163
+ "basic_tx.complained": (event) => { /* ... */ },
164
+ "basic_tx.complaint_period_ended": (event) => { /* ... */ },
165
+ "basic_tx.funds_released": (event) => { /* ... */ },
166
+ "basic_tx.funds_refunded": (event) => { /* ... */ },
167
+ });
168
+
169
+ // TypeScript will error if any event code is missing from handlers
170
+
171
+ function handleWebhook(event: TrustapWebhookEvent) {
172
+ const handler = handlers[event.code];
173
+ handler(event as never);
174
+ }
175
+ ```
176
+
177
+ ### Individual Event Types
178
+
179
+ Import specific event types for targeted handling:
180
+
181
+ ```ts
182
+ import type {
183
+ BasicTxPaidEvent,
184
+ BasicTxFundsReleasedEvent,
185
+ } from "@ranwhenparked/trustap-sdk";
186
+ ```
187
+
188
+ ## State Machine
189
+
190
+ Map webhook events to transaction states:
191
+
192
+ ```ts
193
+ import {
194
+ mapWebhookToTrustapState,
195
+ type TrustapTransactionState,
196
+ } from "@ranwhenparked/trustap-sdk";
197
+
198
+ const state = mapWebhookToTrustapState("basic_tx.paid");
199
+ // state: "paid"
200
+
201
+ const unknown = mapWebhookToTrustapState("unknown.event");
202
+ // unknown: null
203
+ ```
204
+
205
+ ### Available States
206
+
207
+ ```ts
208
+ type TrustapTransactionState =
209
+ | "created"
210
+ | "joined"
211
+ | "rejected"
212
+ | "paid"
213
+ | "cancelled"
214
+ | "cancelled_with_payment"
215
+ | "payment_refunded"
216
+ | "tracked"
217
+ | "delivered"
218
+ | "complained"
219
+ | "complaint_period_ended"
220
+ | "funds_released";
221
+ ```
222
+
223
+ ## Deno
224
+
225
+ For Deno projects, import from the Deno-specific entry point:
226
+
227
+ ```ts
228
+ import { createTrustapClient } from "@ranwhenparked/trustap-sdk/deno";
229
+ ```
230
+
231
+ ## Development
232
+
233
+ ### Generate types from OpenAPI spec
234
+
235
+ ```bash
236
+ npm run generate
237
+ ```
238
+
239
+ This pulls the latest OpenAPI schema from `https://docs.trustap.com/apis/trustap-openapi.yaml` and generates:
240
+
241
+ - `src/schema.d.ts` - TypeScript types for all API paths and operations
242
+ - `src/operations-map.ts` - Mapping of operation IDs to paths/methods
243
+ - `src/security-map.ts` - Security requirements per endpoint
244
+
245
+ Individual generation scripts:
246
+
247
+ ```bash
248
+ npm run generate:types # Generate schema.d.ts
249
+ npm run generate:ops # Generate operations-map.ts
250
+ npm run generate:security # Generate security-map.ts
251
+ ```
package/deno.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "imports": {
3
+ "@std/assert": "jsr:@std/assert@^1"
4
+ },
5
+ "compilerOptions": {
6
+ "lib": ["deno.window", "dom"],
7
+ "strict": true
8
+ }
9
+ }
@@ -0,0 +1,21 @@
1
+ import baseConfig from "@rwp/eslint-config/base";
2
+
3
+ /** @type {import('typescript-eslint').Config} */
4
+ export default [
5
+ {
6
+ ignores: [
7
+ "dist/**",
8
+ "scripts/**",
9
+ "src/schema.d.ts",
10
+ "src/__tests__/deno/**",
11
+ ],
12
+ },
13
+ ...baseConfig,
14
+ {
15
+ files: ["src/**/*.{ts,tsx}"],
16
+ rules: {
17
+ "sonarjs/function-return-type": "error",
18
+ },
19
+ },
20
+ ];
21
+
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@ranwhenparked/trustap-sdk",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "deno": "./src/index.deno.ts",
10
+ "node": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./deno": "./src/index.deno.ts",
14
+ "./node": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "types": "./dist/index.d.ts",
20
+ "main": "./dist/index.js",
21
+ "scripts": {
22
+ "build": "node scripts/build-node.mjs",
23
+ "clean": "git clean -xdf .cache .turbo dist node_modules",
24
+ "format": "prettier --check . --ignore-path ../../.gitignore",
25
+ "lint": "eslint --fix --cache --cache-location .cache/.eslintcache",
26
+ "test": "vitest --run",
27
+ "test:watch": "vitest",
28
+ "typecheck": "tsc --noEmit --emitDeclarationOnly false",
29
+ "generate:types": "npx openapi-typescript https://docs.trustap.com/apis/trustap-openapi.yaml -o src/schema.d.ts",
30
+ "generate:ops": "node ./scripts/generate-operations-map.mjs",
31
+ "generate:security": "node ./scripts/generate-security-map.mjs https://docs.trustap.com/apis/trustap-openapi.yaml",
32
+ "generate": "npm run generate:types && npm run generate:ops && npm run generate:security"
33
+ },
34
+ "dependencies": {
35
+ "openapi-fetch": "^0.15.0"
36
+ },
37
+ "devDependencies": {
38
+ "@vitest/coverage-v8": "^3.2.4",
39
+ "eslint": "^9.39.2",
40
+ "openapi-typescript": "^7.10.1",
41
+ "prettier": "^3.8.0",
42
+ "typescript": "^5.9.3",
43
+ "typescript-eslint": "^8.53.1",
44
+ "vitest": "^3.2.4",
45
+ "yaml": "^2.8.2"
46
+ }
47
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Build script for Node.js output.
3
+ *
4
+ * This script handles the dual-runtime extension problem:
5
+ * - Source files use .ts extensions (for Deno compatibility)
6
+ * - Node.js ESM needs .js extensions
7
+ * - TypeScript's tsc can't emit when sources have .ts extensions
8
+ *
9
+ * Solution:
10
+ * 1. Temporarily rewrite .ts → .js in source files
11
+ * 2. Run tsc to compile
12
+ * 3. Restore original .ts extensions in source files
13
+ * 4. Copy schema.d.ts to dist
14
+ */
15
+ import { execFileSync } from "node:child_process";
16
+ import { readFile, writeFile, copyFile } from "node:fs/promises";
17
+ import { join } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ const __dirname = fileURLToPath(new URL(".", import.meta.url));
21
+ const srcDir = join(__dirname, "..", "src");
22
+ const distDir = join(__dirname, "..", "dist");
23
+
24
+ const filesToProcess = ["index.ts", "client-factory.ts"];
25
+
26
+ async function rewriteExtensions(files, from, to) {
27
+ for (const file of files) {
28
+ const filePath = join(srcDir, file);
29
+ let content = await readFile(filePath, "utf-8");
30
+
31
+ // Rewrite import/export extensions
32
+ const fromPattern = new RegExp(`from\\s+["'](\\.[^"']+)\\${from}["']`, "g");
33
+ content = content.replace(fromPattern, `from "$1${to}"`);
34
+
35
+ await writeFile(filePath, content);
36
+ }
37
+ }
38
+
39
+ async function build() {
40
+ console.log("1. Rewriting .ts → .js extensions in source files...");
41
+ await rewriteExtensions(filesToProcess, ".ts", ".js");
42
+ await rewriteExtensions(filesToProcess, ".d.ts", ".js");
43
+
44
+ try {
45
+ console.log("2. Compiling with tsc...");
46
+ execFileSync("npm", ["exec", "tsc", "-p", "tsconfig.build.json"], {
47
+ cwd: join(__dirname, ".."),
48
+ stdio: "inherit",
49
+ });
50
+
51
+ console.log("3. Copying schema.d.ts to dist...");
52
+ await copyFile(join(srcDir, "schema.d.ts"), join(distDir, "schema.d.ts"));
53
+
54
+ console.log("Build complete!");
55
+ } finally {
56
+ console.log("4. Restoring .ts extensions in source files...");
57
+ await rewriteExtensions(filesToProcess, ".js", ".ts");
58
+
59
+ // Also restore .d.ts for schema imports
60
+ for (const file of filesToProcess) {
61
+ const filePath = join(srcDir, file);
62
+ let content = await readFile(filePath, "utf-8");
63
+ content = content.replace(
64
+ /from\s+["']\.\/schema\.ts["']/g,
65
+ 'from "./schema.d.ts"',
66
+ );
67
+ await writeFile(filePath, content);
68
+ }
69
+ }
70
+ }
71
+
72
+ build().catch((err) => {
73
+ console.error("Build failed:", err);
74
+ process.exit(1);
75
+ });
@@ -0,0 +1,57 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import yaml from "yaml";
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const ROOT = path.resolve(__dirname, "..");
8
+ const OUTPUT = path.join(ROOT, "src", "operations-map.ts");
9
+
10
+ async function main() {
11
+ const res = await fetch("https://docs.trustap.com/_spec/apis/openapi.yaml");
12
+ if (!res.ok) throw new Error(`Failed to fetch OpenAPI: ${res.status}`);
13
+ const text = await res.text();
14
+ const doc = yaml.parse(text);
15
+
16
+ const opToPath = {};
17
+ for (const [p, methods] of Object.entries(doc.paths ?? {})) {
18
+ for (const [method, op] of Object.entries(methods)) {
19
+ if (!op || typeof op !== "object") continue;
20
+ const id = op.operationId;
21
+ if (!id) continue;
22
+ opToPath[id] = { path: p, method: method.toUpperCase() };
23
+ }
24
+ }
25
+
26
+ const overrideOperations = {
27
+ "basic.createTransaction": { path: "/transactions", method: "POST" },
28
+ "basic.getTransactions": { path: "/transactions", method: "GET" },
29
+ };
30
+
31
+ for (const [key, value] of Object.entries(overrideOperations)) {
32
+ opToPath[key] = value;
33
+ }
34
+
35
+ const additionalOperations = {
36
+ "oauth.getUser": { path: "/users/{userId}", method: "GET" },
37
+ "oauth.updateUser": { path: "/users/{userId}", method: "PUT" },
38
+ };
39
+
40
+ for (const [key, value] of Object.entries(additionalOperations)) {
41
+ if (!opToPath[key]) {
42
+ opToPath[key] = value;
43
+ }
44
+ }
45
+
46
+ const file = `// generated by scripts/generate-operations-map.mjs\n` +
47
+ `export const operationIdToPath = ${JSON.stringify(opToPath, null, 2)} as const;\n`;
48
+
49
+ fs.mkdirSync(path.dirname(OUTPUT), { recursive: true });
50
+ fs.writeFileSync(OUTPUT, file);
51
+ console.log(`Wrote ${OUTPUT}`);
52
+ }
53
+
54
+ main().catch((err) => {
55
+ console.error(err);
56
+ process.exit(1);
57
+ });
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ // Generate a minimal security map from an OpenAPI document (JSON or YAML)
3
+ // Usage: node scripts/generate-security-map.mjs [/absolute/or/relative/path/or/url]
4
+
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import process from 'node:process';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { createRequire } from 'node:module';
10
+
11
+ const require = createRequire(import.meta.url);
12
+ let YAML;
13
+ try {
14
+ YAML = require('yaml');
15
+ } catch {}
16
+
17
+ const inputArg = process.argv[2] || 'https://docs.trustap.com/_spec/apis/openapi.yaml';
18
+
19
+ async function readOpenApi(source) {
20
+ if (/^https?:\/\//.test(source)) {
21
+ const res = await fetch(source);
22
+ if (!res.ok) throw new Error(`Failed to fetch ${source}: ${res.status}`);
23
+ const text = await res.text();
24
+ if (source.endsWith('.yaml') || source.endsWith('.yml')) {
25
+ if (!YAML) throw new Error('yaml package not available');
26
+ return YAML.parse(text);
27
+ }
28
+ return JSON.parse(text);
29
+ }
30
+ const content = fs.readFileSync(source, 'utf8');
31
+ if (source.endsWith('.yaml') || source.endsWith('.yml')) {
32
+ if (!YAML) throw new Error('yaml package not available');
33
+ return YAML.parse(content);
34
+ }
35
+ return JSON.parse(content);
36
+ }
37
+
38
+ function normalizeSecurityArray(secArr) {
39
+ // Convert OpenAPI security array to a set of scheme names
40
+ // e.g., [{ OAuth2: [] }, { APIKey: [] }] -> Set(['OAuth2','APIKey'])
41
+ const set = new Set();
42
+ if (Array.isArray(secArr)) {
43
+ for (const entry of secArr) {
44
+ if (entry && typeof entry === 'object') {
45
+ for (const key of Object.keys(entry)) set.add(key);
46
+ }
47
+ }
48
+ }
49
+ return set;
50
+ }
51
+
52
+ function buildSecurityMap(doc) {
53
+ const out = {}; // { path: { METHOD: ['APIKey','OAuth2'] } }
54
+ const globalSec = normalizeSecurityArray(doc.security);
55
+ const paths = doc.paths || {};
56
+ for (const p of Object.keys(paths)) {
57
+ const item = paths[p] || {};
58
+ for (const method of Object.keys(item)) {
59
+ const op = item[method];
60
+ if (!op || typeof op !== 'object') continue;
61
+ if (!['get','post','put','patch','delete','head','options','trace'].includes(method)) continue;
62
+ const opSec = normalizeSecurityArray(op.security);
63
+ const effective = opSec.size > 0 ? opSec : globalSec;
64
+ if (!out[p]) out[p] = {};
65
+ out[p][method.toUpperCase()] = Array.from(effective);
66
+ }
67
+ }
68
+ return out;
69
+ }
70
+
71
+ function emitTs(securityMap) {
72
+ const header = `// generated by scripts/generate-security-map.mjs\n`;
73
+ const body = `export const securityMap = ${JSON.stringify(securityMap, null, 2)} as const;\n`;
74
+ return header + body;
75
+ }
76
+
77
+ async function main() {
78
+ const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
79
+ const outFile = path.join(rootDir, 'src', 'security-map.ts');
80
+ const api = await readOpenApi(inputArg);
81
+ const map = buildSecurityMap(api);
82
+ const ts = emitTs(map);
83
+ fs.writeFileSync(outFile, ts, 'utf8');
84
+ console.log(`Wrote ${outFile}`);
85
+ }
86
+
87
+ main().catch((err) => {
88
+ console.error(err);
89
+ process.exit(1);
90
+ });
91
+
92
+