@safeurl/sdk 1.0.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.
@@ -0,0 +1,323 @@
1
+ // This file is auto-generated by @hey-api/openapi-ts
2
+
3
+ export type ClientOptions = {
4
+ baseUrl: `${string}://${string}` | (string & {});
5
+ };
6
+
7
+ export type GetHealthData = {
8
+ body?: never;
9
+ path?: never;
10
+ query?: never;
11
+ url: '/health';
12
+ };
13
+
14
+ export type GetV1ApiKeysData = {
15
+ body?: never;
16
+ path?: never;
17
+ query?: never;
18
+ url: '/v1/api-keys/';
19
+ };
20
+
21
+ export type GetV1ApiKeysErrors = {
22
+ /**
23
+ * Response for status 500
24
+ */
25
+ 500: unknown;
26
+ };
27
+
28
+ export type GetV1ApiKeysResponses = {
29
+ /**
30
+ * Response for status 200
31
+ */
32
+ 200: unknown;
33
+ };
34
+
35
+ export type PostV1ApiKeysData = {
36
+ body: unknown;
37
+ path?: never;
38
+ query?: never;
39
+ url: '/v1/api-keys/';
40
+ };
41
+
42
+ export type PostV1ApiKeysErrors = {
43
+ /**
44
+ * Response for status 500
45
+ */
46
+ 500: unknown;
47
+ };
48
+
49
+ export type PostV1ApiKeysResponses = {
50
+ /**
51
+ * Response for status 201
52
+ */
53
+ 201: unknown;
54
+ };
55
+
56
+ export type DeleteV1ApiKeysByIdData = {
57
+ body?: never;
58
+ path: {
59
+ id: string;
60
+ };
61
+ query?: never;
62
+ url: '/v1/api-keys/{id}';
63
+ };
64
+
65
+ export type DeleteV1ApiKeysByIdErrors = {
66
+ /**
67
+ * Response for status 400
68
+ */
69
+ 400: unknown;
70
+ /**
71
+ * Response for status 404
72
+ */
73
+ 404: unknown;
74
+ /**
75
+ * Response for status 500
76
+ */
77
+ 500: unknown;
78
+ };
79
+
80
+ export type DeleteV1ApiKeysByIdResponses = {
81
+ /**
82
+ * Response for status 200
83
+ */
84
+ 200: unknown;
85
+ };
86
+
87
+ export type GetV1ScansData = {
88
+ body?: never;
89
+ path?: never;
90
+ query?: never;
91
+ url: '/v1/scans/';
92
+ };
93
+
94
+ export type GetV1ScansErrors = {
95
+ /**
96
+ * Insufficient scope (requires scan:read)
97
+ */
98
+ 403: unknown;
99
+ /**
100
+ * Server error
101
+ */
102
+ 500: unknown;
103
+ };
104
+
105
+ export type GetV1ScansResponses = {
106
+ /**
107
+ * List of scan jobs
108
+ */
109
+ 200: unknown;
110
+ };
111
+
112
+ export type PostV1ScansData = {
113
+ body: unknown;
114
+ path?: never;
115
+ query?: never;
116
+ url: '/v1/scans/';
117
+ };
118
+
119
+ export type PostV1ScansResponses = {
120
+ /**
121
+ * Response for status 201
122
+ */
123
+ 201: unknown;
124
+ };
125
+
126
+ export type PostV1ScansBatchData = {
127
+ body: unknown;
128
+ path?: never;
129
+ query?: never;
130
+ url: '/v1/scans/batch';
131
+ };
132
+
133
+ export type PostV1ScansBatchResponses = {
134
+ /**
135
+ * Response for status 201
136
+ */
137
+ 201: unknown;
138
+ };
139
+
140
+ export type GetV1ScansByIdAnalyticsData = {
141
+ body?: never;
142
+ path: {
143
+ id: string;
144
+ };
145
+ query?: never;
146
+ url: '/v1/scans/{id}/analytics';
147
+ };
148
+
149
+ export type GetV1ScansByIdAnalyticsErrors = {
150
+ /**
151
+ * Insufficient scope (requires scan:read)
152
+ */
153
+ 403: unknown;
154
+ /**
155
+ * Scan job not found
156
+ */
157
+ 404: unknown;
158
+ /**
159
+ * Server error
160
+ */
161
+ 500: unknown;
162
+ };
163
+
164
+ export type GetV1ScansByIdAnalyticsResponses = {
165
+ /**
166
+ * Scan analytics
167
+ */
168
+ 200: unknown;
169
+ };
170
+
171
+ export type GetV1ScansByIdEventsData = {
172
+ body?: never;
173
+ path: {
174
+ id: string;
175
+ };
176
+ query?: never;
177
+ url: '/v1/scans/{id}/events';
178
+ };
179
+
180
+ export type GetV1ScansByIdEventsErrors = {
181
+ /**
182
+ * Insufficient scope (requires scan:read)
183
+ */
184
+ 403: unknown;
185
+ /**
186
+ * Scan job not found
187
+ */
188
+ 404: unknown;
189
+ };
190
+
191
+ export type GetV1ScansByIdEventsResponses = {
192
+ /**
193
+ * SSE stream
194
+ */
195
+ 200: unknown;
196
+ };
197
+
198
+ export type GetV1ScansByIdData = {
199
+ body?: never;
200
+ path: {
201
+ id: string;
202
+ };
203
+ query?: never;
204
+ url: '/v1/scans/{id}';
205
+ };
206
+
207
+ export type GetV1ScansByIdErrors = {
208
+ /**
209
+ * Response for status 404
210
+ */
211
+ 404: unknown;
212
+ /**
213
+ * Response for status 500
214
+ */
215
+ 500: unknown;
216
+ };
217
+
218
+ export type GetV1ScansByIdResponses = {
219
+ /**
220
+ * Response for status 200
221
+ */
222
+ 200: unknown;
223
+ };
224
+
225
+ export type GetV1CreditsData = {
226
+ body?: never;
227
+ path?: never;
228
+ query?: never;
229
+ url: '/v1/credits/';
230
+ };
231
+
232
+ export type GetV1CreditsErrors = {
233
+ /**
234
+ * Response for status 500
235
+ */
236
+ 500: unknown;
237
+ };
238
+
239
+ export type GetV1CreditsResponses = {
240
+ /**
241
+ * Response for status 200
242
+ */
243
+ 200: unknown;
244
+ };
245
+
246
+ export type PostV1CreditsPurchaseData = {
247
+ body: unknown;
248
+ path?: never;
249
+ query?: never;
250
+ url: '/v1/credits/purchase';
251
+ };
252
+
253
+ export type PostV1CreditsPurchaseErrors = {
254
+ /**
255
+ * Response for status 400
256
+ */
257
+ 400: unknown;
258
+ /**
259
+ * Response for status 402
260
+ */
261
+ 402: unknown;
262
+ /**
263
+ * Response for status 500
264
+ */
265
+ 500: unknown;
266
+ };
267
+
268
+ export type PostV1CreditsPurchaseResponses = {
269
+ /**
270
+ * Response for status 201
271
+ */
272
+ 201: unknown;
273
+ };
274
+
275
+ export type GetV1SettingsData = {
276
+ body?: never;
277
+ path?: never;
278
+ query?: never;
279
+ url: '/v1/settings/';
280
+ };
281
+
282
+ export type GetV1SettingsErrors = {
283
+ /**
284
+ * Response for status 404
285
+ */
286
+ 404: unknown;
287
+ /**
288
+ * Response for status 500
289
+ */
290
+ 500: unknown;
291
+ };
292
+
293
+ export type GetV1SettingsResponses = {
294
+ /**
295
+ * Response for status 200
296
+ */
297
+ 200: unknown;
298
+ };
299
+
300
+ export type PutV1SettingsData = {
301
+ body: unknown;
302
+ path?: never;
303
+ query?: never;
304
+ url: '/v1/settings/';
305
+ };
306
+
307
+ export type PutV1SettingsErrors = {
308
+ /**
309
+ * Response for status 404
310
+ */
311
+ 404: unknown;
312
+ /**
313
+ * Response for status 500
314
+ */
315
+ 500: unknown;
316
+ };
317
+
318
+ export type PutV1SettingsResponses = {
319
+ /**
320
+ * Response for status 200
321
+ */
322
+ 200: unknown;
323
+ };
@@ -0,0 +1,118 @@
1
+ import { expect, mock, test } from "bun:test";
2
+
3
+ import { createClient } from "../src/client/client";
4
+ import { getHealth, postV1Scans } from "../src/client";
5
+
6
+ function lastRequest(mockFetch: ReturnType<typeof mock>) {
7
+ const calls = mockFetch.mock.calls as unknown as [
8
+ input: RequestInfo | URL,
9
+ init?: RequestInit,
10
+ ][];
11
+ const [input, init] = calls[calls.length - 1]!;
12
+ if (input instanceof Request) {
13
+ return input;
14
+ }
15
+ return new Request(input, init);
16
+ }
17
+
18
+ test("getHealth issues GET to {baseUrl}/health with Authorization", async () => {
19
+ const fetchMock = mock((_input: RequestInfo | URL, _init?: RequestInit) =>
20
+ Promise.resolve(
21
+ new Response(
22
+ JSON.stringify({
23
+ status: "healthy",
24
+ service: "safeurl-api",
25
+ checks: {
26
+ database: { status: "healthy" },
27
+ queue: { status: "healthy" },
28
+ },
29
+ }),
30
+ {
31
+ status: 200,
32
+ headers: { "Content-Type": "application/json" },
33
+ },
34
+ ),
35
+ ),
36
+ );
37
+
38
+ const client = createClient({
39
+ baseUrl: "https://api.example.com",
40
+ headers: {
41
+ Authorization: "Bearer sk_live_unit",
42
+ },
43
+ fetch: fetchMock as typeof fetch,
44
+ });
45
+
46
+ const result = await getHealth({ client });
47
+ expect(result.response.status).toBe(200);
48
+
49
+ const req = lastRequest(fetchMock);
50
+ expect(req.method).toBe("GET");
51
+ expect(req.url).toBe("https://api.example.com/health");
52
+ expect(req.headers.get("Authorization")).toBe("Bearer sk_live_unit");
53
+ });
54
+
55
+ test("createClient normalizes baseUrl trailing slash before requests", async () => {
56
+ const fetchMock = mock((_input: RequestInfo | URL, _init?: RequestInit) =>
57
+ Promise.resolve(
58
+ new Response(
59
+ JSON.stringify({
60
+ status: "healthy",
61
+ service: "safeurl-api",
62
+ checks: {
63
+ database: { status: "healthy" },
64
+ queue: { status: "healthy" },
65
+ },
66
+ }),
67
+ {
68
+ status: 200,
69
+ headers: { "Content-Type": "application/json" },
70
+ },
71
+ ),
72
+ ),
73
+ );
74
+
75
+ const client = createClient({
76
+ baseUrl: "https://api.example.com/",
77
+ fetch: fetchMock as typeof fetch,
78
+ });
79
+
80
+ await getHealth({ client });
81
+ const req = lastRequest(fetchMock);
82
+ expect(req.url).toBe("https://api.example.com/health");
83
+ });
84
+
85
+ test("postV1Scans sends JSON body and Content-Type application/json", async () => {
86
+ const fetchMock = mock((_input: RequestInfo | URL, _init?: RequestInit) =>
87
+ Promise.resolve(
88
+ new Response(
89
+ JSON.stringify({
90
+ id: "00000000-0000-0000-0000-000000000001",
91
+ state: "QUEUED",
92
+ }),
93
+ {
94
+ status: 201,
95
+ headers: { "Content-Type": "application/json" },
96
+ },
97
+ ),
98
+ ),
99
+ );
100
+
101
+ const client = createClient({
102
+ baseUrl: "https://api.example.com",
103
+ headers: { Authorization: "Bearer sk_live_unit" },
104
+ fetch: fetchMock as typeof fetch,
105
+ });
106
+
107
+ const result = await postV1Scans({
108
+ client,
109
+ body: { url: "https://example.com" },
110
+ });
111
+ expect(result.response.status).toBe(201);
112
+
113
+ const req = lastRequest(fetchMock);
114
+ expect(req.method).toBe("POST");
115
+ expect(req.url).toBe("https://api.example.com/v1/scans/");
116
+ expect(req.headers.get("Content-Type")).toBe("application/json");
117
+ expect(await req.text()).toBe(JSON.stringify({ url: "https://example.com" }));
118
+ });
@@ -0,0 +1,239 @@
1
+ import { expect, test } from "bun:test";
2
+
3
+ import { createClient } from "../src/client/client";
4
+ import {
5
+ deleteV1ApiKeysById,
6
+ getHealth,
7
+ getV1ApiKeys,
8
+ getV1Credits,
9
+ getV1ScansById,
10
+ postV1ApiKeys,
11
+ postV1CreditsPurchase,
12
+ postV1Scans,
13
+ } from "../src/client";
14
+
15
+ type CreditsResponse = {
16
+ balance: number;
17
+ userId: string;
18
+ updatedAt: string;
19
+ };
20
+
21
+ type PurchaseCreditsResponse = {
22
+ id: string;
23
+ userId: string;
24
+ amount: number;
25
+ status: string;
26
+ createdAt: string;
27
+ completedAt: string;
28
+ newBalance: number;
29
+ };
30
+
31
+ type ScanResponse = {
32
+ id: string;
33
+ url: string;
34
+ state: "QUEUED" | "FETCHING" | "ANALYZING" | "COMPLETED" | "FAILED" | "TIMED_OUT";
35
+ createdAt: string;
36
+ updatedAt: string;
37
+ };
38
+
39
+ type CreateApiKeyResponse = {
40
+ id: string;
41
+ key: string;
42
+ name: string;
43
+ createdAt: string;
44
+ };
45
+
46
+ type ApiKeyListItem = {
47
+ id: string;
48
+ name: string;
49
+ scopes: string[];
50
+ expiresAt: string | null;
51
+ lastUsedAt: string | null;
52
+ createdAt: string;
53
+ revokedAt: string | null;
54
+ };
55
+
56
+ const baseUrl = process.env["SAFEURL_SDK_TEST_BASE_URL"] ?? "http://localhost:8081";
57
+ const serviceSecret =
58
+ process.env["SAFEURL_SDK_TEST_SERVICE_SECRET"] ??
59
+ process.env["SAFEURL_SERVICE_SECRET"];
60
+ const smoke = serviceSecret ? test : test.skip;
61
+
62
+ smoke("bootstraps and uses a real API key", async () => {
63
+ if (!serviceSecret) {
64
+ return;
65
+ }
66
+
67
+ const serviceClient = createClient({
68
+ baseUrl,
69
+ headers: {
70
+ Authorization: `Bearer ${serviceSecret}`,
71
+ },
72
+ });
73
+
74
+ const health = await getHealth({
75
+ client: serviceClient,
76
+ });
77
+
78
+ expect(health.response.status).toBe(200);
79
+ expect(health.data).toMatchObject({
80
+ status: "healthy",
81
+ service: "safeurl-api",
82
+ checks: {
83
+ database: { status: "healthy" },
84
+ queue: { status: "healthy" },
85
+ },
86
+ });
87
+
88
+ let createdKey: CreateApiKeyResponse | undefined;
89
+ let revokeKey: CreateApiKeyResponse | undefined;
90
+
91
+ try {
92
+ const keyName = `ts-sdk-smoke-${Date.now()}`;
93
+ const createApiKeyResult = await postV1ApiKeys({
94
+ client: serviceClient,
95
+ body: {
96
+ name: keyName,
97
+ scopes: ["scan:read", "scan:write", "credits:read", "credits:write"],
98
+ },
99
+ });
100
+ expect(createApiKeyResult.response.status).toBe(201);
101
+ createdKey = createApiKeyResult.data as CreateApiKeyResponse;
102
+ expect(createdKey.id).toBeTruthy();
103
+ expect(createdKey.name).toBe(keyName);
104
+ expect(createdKey.key).toMatch(/^sk_live_/);
105
+
106
+ const apiKeyClient = createClient({
107
+ baseUrl,
108
+ headers: {
109
+ Authorization: `Bearer ${createdKey.key}`,
110
+ },
111
+ });
112
+
113
+ const apiKeyListResult = await getV1ApiKeys({
114
+ client: apiKeyClient,
115
+ });
116
+ expect(apiKeyListResult.response.status).toBe(200);
117
+ const apiKeyList = apiKeyListResult.data as ApiKeyListItem[];
118
+ expect(
119
+ apiKeyList.some((key) => key.id === createdKey?.id && key.name === keyName),
120
+ ).toBe(true);
121
+
122
+ const purchaseResult = await postV1CreditsPurchase({
123
+ client: serviceClient,
124
+ body: {
125
+ amount: 1,
126
+ },
127
+ });
128
+ expect(purchaseResult.response.status).toBe(201);
129
+ const purchase = purchaseResult.data as PurchaseCreditsResponse;
130
+ expect(purchase.amount).toBe(1);
131
+
132
+ const apiKeyCreditsResult = await getV1Credits({
133
+ client: apiKeyClient,
134
+ });
135
+ expect(apiKeyCreditsResult.response.status).toBe(200);
136
+ const apiKeyCredits = apiKeyCreditsResult.data as CreditsResponse;
137
+ expect(apiKeyCredits.balance).toBe(purchase.newBalance);
138
+
139
+ const revokeKeyName = `ts-sdk-revoke-${Date.now()}`;
140
+ const createRevokeKeyResult = await postV1ApiKeys({
141
+ client: serviceClient,
142
+ body: {
143
+ name: revokeKeyName,
144
+ scopes: ["scan:read", "credits:read"],
145
+ },
146
+ });
147
+ expect(createRevokeKeyResult.response.status).toBe(201);
148
+ revokeKey = createRevokeKeyResult.data as CreateApiKeyResponse;
149
+ expect(revokeKey.id).toBeTruthy();
150
+ expect(revokeKey.key).toMatch(/^sk_live_/);
151
+
152
+ const revokeClient = createClient({
153
+ baseUrl,
154
+ headers: {
155
+ Authorization: `Bearer ${revokeKey.key}`,
156
+ },
157
+ });
158
+
159
+ const revokePrecheck = await getV1Credits({
160
+ client: revokeClient,
161
+ });
162
+ expect(revokePrecheck.response.status).toBe(200);
163
+
164
+ const revokeResult = await deleteV1ApiKeysById({
165
+ client: apiKeyClient,
166
+ path: { id: revokeKey.id },
167
+ });
168
+ expect(revokeResult.response.status).toBe(200);
169
+ revokeKey = undefined;
170
+
171
+ const revokedCreditsResult = await getV1Credits({
172
+ client: revokeClient,
173
+ });
174
+ expect(revokedCreditsResult.response.status).toBe(401);
175
+
176
+ const unauthenticatedClient = createClient({ baseUrl });
177
+ const missingAuthResult = await getV1Credits({
178
+ client: unauthenticatedClient,
179
+ });
180
+ expect(missingAuthResult.response.status).toBe(401);
181
+
182
+ const invalidAuthClient = createClient({
183
+ baseUrl,
184
+ headers: {
185
+ Authorization: "Bearer sk_live_invalid_api_key",
186
+ },
187
+ });
188
+ const invalidAuthResult = await getV1Credits({
189
+ client: invalidAuthClient,
190
+ });
191
+ expect(invalidAuthResult.response.status).toBe(401);
192
+
193
+ const scanUrl = "https://example.com";
194
+ const createScanResult = await postV1Scans({
195
+ client: apiKeyClient,
196
+ body: {
197
+ url: scanUrl,
198
+ },
199
+ });
200
+ expect(createScanResult.response.status).toBe(201);
201
+
202
+ const createdScan = createScanResult.data as { id: string; state: "QUEUED" };
203
+ expect(createdScan.id).toBeTruthy();
204
+ expect(createdScan.state).toBe("QUEUED");
205
+
206
+ const scanResult = await getV1ScansById({
207
+ client: apiKeyClient,
208
+ path: { id: createdScan.id },
209
+ });
210
+ expect(scanResult.response.status).toBe(200);
211
+
212
+ const scan = scanResult.data as ScanResponse;
213
+ expect(scan.id).toBe(createdScan.id);
214
+ expect(scan.url).toBe(scanUrl);
215
+ expect([
216
+ "QUEUED",
217
+ "FETCHING",
218
+ "ANALYZING",
219
+ "COMPLETED",
220
+ "FAILED",
221
+ "TIMED_OUT",
222
+ ]).toContain(scan.state);
223
+ expect(scan.createdAt).toBeDefined();
224
+ expect(scan.updatedAt).toBeDefined();
225
+ } finally {
226
+ if (revokeKey?.id) {
227
+ await deleteV1ApiKeysById({
228
+ client: serviceClient,
229
+ path: { id: revokeKey.id },
230
+ });
231
+ }
232
+ if (createdKey?.id) {
233
+ await deleteV1ApiKeysById({
234
+ client: serviceClient,
235
+ path: { id: createdKey.id },
236
+ });
237
+ }
238
+ }
239
+ }, 30_000);
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noImplicitOverride": true,
23
+
24
+ // Some stricter flags (disabled by default)
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false
28
+ }
29
+ }