sealtrail 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 +121 -0
- package/dist/index.cjs +396 -0
- package/dist/index.d.cts +176 -0
- package/dist/index.d.ts +176 -0
- package/dist/index.mjs +360 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# sealtrail
|
|
2
|
+
|
|
3
|
+
Official Node.js SDK for the [SealTrail](https://sealtrail.dev) cryptographic audit trail API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install sealtrail
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { SealTrail } from "sealtrail";
|
|
15
|
+
|
|
16
|
+
const st = new SealTrail({ apiKey: "stl_live_..." });
|
|
17
|
+
|
|
18
|
+
// Log an audit event
|
|
19
|
+
const event = await st.events.log({
|
|
20
|
+
actor: "user_123",
|
|
21
|
+
action: "document.signed",
|
|
22
|
+
resource: "doc_456",
|
|
23
|
+
context: { ip: "192.168.1.1" },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
console.log(event.hash); // cryptographic proof
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- **Zero dependencies** — uses `globalThis.fetch` (Node 18+, Bun, Deno)
|
|
32
|
+
- **Fully typed** — complete TypeScript definitions
|
|
33
|
+
- **Automatic retries** — exponential backoff on 429, 5xx, and 409 (chain conflicts)
|
|
34
|
+
- **Auto-pagination** — async iterator for large result sets
|
|
35
|
+
- **Typed errors** — catch specific error classes (`RateLimitError`, `ValidationError`, etc.)
|
|
36
|
+
|
|
37
|
+
## API Reference
|
|
38
|
+
|
|
39
|
+
### Client
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
const st = new SealTrail({
|
|
43
|
+
apiKey: "stl_live_...", // required
|
|
44
|
+
baseUrl: "https://...", // default: "https://api.sealtrail.dev"
|
|
45
|
+
timeout: 30_000, // request timeout in ms
|
|
46
|
+
maxRetries: 3, // retry attempts on retryable errors
|
|
47
|
+
debug: false, // log requests to console.debug
|
|
48
|
+
fetch: customFetch, // custom fetch for edge runtimes
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Events
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// Log an event
|
|
56
|
+
const event = await st.events.log({
|
|
57
|
+
actor: "user_123",
|
|
58
|
+
action: "document.signed",
|
|
59
|
+
resource: "doc_456", // optional
|
|
60
|
+
context: { ip: "..." }, // optional metadata
|
|
61
|
+
chain: "documents", // optional, default: "default"
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Get a single event
|
|
65
|
+
const event = await st.events.get("evt_abc123");
|
|
66
|
+
|
|
67
|
+
// List events with filters
|
|
68
|
+
const { data, nextCursor } = await st.events.list({
|
|
69
|
+
actor: "user_123",
|
|
70
|
+
action: "document.signed",
|
|
71
|
+
limit: 50,
|
|
72
|
+
after: "2025-01-01T00:00:00Z",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Auto-paginate through all events
|
|
76
|
+
for await (const event of st.events.listAutoPaginate({ actor: "user_123" })) {
|
|
77
|
+
console.log(event.id, event.action);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Verify an event's cryptographic integrity
|
|
81
|
+
const result = await st.events.verify("evt_abc123");
|
|
82
|
+
console.log(result.valid); // true/false
|
|
83
|
+
console.log(result.chainIntact); // hash chain verification
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Chains
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// List all chains
|
|
90
|
+
const chains = await st.chains.list();
|
|
91
|
+
|
|
92
|
+
// Get chain status
|
|
93
|
+
const chain = await st.chains.status("chain_id");
|
|
94
|
+
console.log(chain.eventCount, chain.lastPosition);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Error Handling
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { SealTrail, RateLimitError, ValidationError } from "sealtrail";
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
await st.events.log({ actor: "user_123", action: "test" });
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (err instanceof RateLimitError) {
|
|
106
|
+
console.log("Rate limited, retry after:", err.retryAfter);
|
|
107
|
+
} else if (err instanceof ValidationError) {
|
|
108
|
+
console.log("Invalid input:", err.details);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
All error classes: `AuthenticationError` (401), `ForbiddenError` (403), `NotFoundError` (404), `ValidationError` (400), `RateLimitError` (429), `QuotaExceededError` (429), `ConflictError` (409), `InternalError` (5xx).
|
|
114
|
+
|
|
115
|
+
## Requirements
|
|
116
|
+
|
|
117
|
+
- Node.js 18+ / Bun / Deno (any runtime with `globalThis.fetch`)
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AuthenticationError: () => AuthenticationError,
|
|
24
|
+
ConflictError: () => ConflictError,
|
|
25
|
+
ForbiddenError: () => ForbiddenError,
|
|
26
|
+
InternalError: () => InternalError,
|
|
27
|
+
NotFoundError: () => NotFoundError,
|
|
28
|
+
QuotaExceededError: () => QuotaExceededError,
|
|
29
|
+
RateLimitError: () => RateLimitError,
|
|
30
|
+
SealTrail: () => SealTrail,
|
|
31
|
+
SealTrailError: () => SealTrailError,
|
|
32
|
+
ValidationError: () => ValidationError
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(index_exports);
|
|
35
|
+
|
|
36
|
+
// src/errors.ts
|
|
37
|
+
var SealTrailError = class extends Error {
|
|
38
|
+
constructor(message, code, status, details) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.code = code;
|
|
41
|
+
this.status = status;
|
|
42
|
+
this.details = details;
|
|
43
|
+
this.name = "SealTrailError";
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var AuthenticationError = class extends SealTrailError {
|
|
47
|
+
constructor(message, code) {
|
|
48
|
+
super(message, code, 401);
|
|
49
|
+
this.name = "AuthenticationError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
var ForbiddenError = class extends SealTrailError {
|
|
53
|
+
constructor(message, code) {
|
|
54
|
+
super(message, code, 403);
|
|
55
|
+
this.name = "ForbiddenError";
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var NotFoundError = class extends SealTrailError {
|
|
59
|
+
constructor(message, code) {
|
|
60
|
+
super(message, code, 404);
|
|
61
|
+
this.name = "NotFoundError";
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var ValidationError = class extends SealTrailError {
|
|
65
|
+
constructor(message, code, details) {
|
|
66
|
+
super(message, code, 400, details);
|
|
67
|
+
this.name = "ValidationError";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
var ConflictError = class extends SealTrailError {
|
|
71
|
+
constructor(message, code) {
|
|
72
|
+
super(message, code, 409);
|
|
73
|
+
this.name = "ConflictError";
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var RateLimitError = class extends SealTrailError {
|
|
77
|
+
retryAfter;
|
|
78
|
+
constructor(message, code, retryAfter) {
|
|
79
|
+
super(message, code, 429);
|
|
80
|
+
this.name = "RateLimitError";
|
|
81
|
+
this.retryAfter = retryAfter;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
var QuotaExceededError = class extends SealTrailError {
|
|
85
|
+
constructor(message, code) {
|
|
86
|
+
super(message, code, 429);
|
|
87
|
+
this.name = "QuotaExceededError";
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var InternalError = class extends SealTrailError {
|
|
91
|
+
constructor(message, code, status) {
|
|
92
|
+
super(message, code, status);
|
|
93
|
+
this.name = "InternalError";
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
function mapError(status, body, retryAfter) {
|
|
97
|
+
const { code, message, details } = body.error;
|
|
98
|
+
switch (status) {
|
|
99
|
+
case 400:
|
|
100
|
+
return new ValidationError(message, code, details);
|
|
101
|
+
case 401:
|
|
102
|
+
return new AuthenticationError(message, code);
|
|
103
|
+
case 403:
|
|
104
|
+
return new ForbiddenError(message, code);
|
|
105
|
+
case 404:
|
|
106
|
+
return new NotFoundError(message, code);
|
|
107
|
+
case 409:
|
|
108
|
+
return new ConflictError(message, code);
|
|
109
|
+
case 429:
|
|
110
|
+
if (code === "QUOTA_EXCEEDED") {
|
|
111
|
+
return new QuotaExceededError(message, code);
|
|
112
|
+
}
|
|
113
|
+
return new RateLimitError(message, code, retryAfter);
|
|
114
|
+
default:
|
|
115
|
+
if (status >= 500) {
|
|
116
|
+
return new InternalError(message, code, status);
|
|
117
|
+
}
|
|
118
|
+
return new SealTrailError(message, code, status, details);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/version.ts
|
|
123
|
+
var VERSION = true ? "0.1.0" : "0.1.0";
|
|
124
|
+
|
|
125
|
+
// src/lib/fetch.ts
|
|
126
|
+
var DEFAULT_BASE_URL = "https://api.sealtrail.dev";
|
|
127
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
128
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
129
|
+
var MAX_BACKOFF = 3e4;
|
|
130
|
+
function resolveConfig(config) {
|
|
131
|
+
return {
|
|
132
|
+
apiKey: config.apiKey,
|
|
133
|
+
baseUrl: (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, ""),
|
|
134
|
+
timeout: config.timeout ?? DEFAULT_TIMEOUT,
|
|
135
|
+
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
136
|
+
debug: config.debug ?? false,
|
|
137
|
+
fetch: config.fetch ?? globalThis.fetch
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function buildUrl(baseUrl, path, query) {
|
|
141
|
+
const url = new URL(path, baseUrl);
|
|
142
|
+
if (query) {
|
|
143
|
+
for (const [key, value] of Object.entries(query)) {
|
|
144
|
+
if (value !== void 0) {
|
|
145
|
+
url.searchParams.set(key, String(value));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return url.toString();
|
|
150
|
+
}
|
|
151
|
+
function backoff(attempt) {
|
|
152
|
+
const base = Math.min(1e3 * 2 ** attempt, MAX_BACKOFF);
|
|
153
|
+
return base + Math.random() * base * 0.25;
|
|
154
|
+
}
|
|
155
|
+
function isRetryable(status, method) {
|
|
156
|
+
if (status === 429 || status >= 500) return true;
|
|
157
|
+
if (status === 409 && method === "POST") return true;
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
function parseRetryAfter(headers) {
|
|
161
|
+
const retryAfter = headers.get("retry-after");
|
|
162
|
+
if (retryAfter) {
|
|
163
|
+
const seconds = Number(retryAfter);
|
|
164
|
+
if (!Number.isNaN(seconds)) return seconds;
|
|
165
|
+
}
|
|
166
|
+
const reset = headers.get("ratelimit-reset");
|
|
167
|
+
if (reset) {
|
|
168
|
+
const timestamp = Number(reset);
|
|
169
|
+
if (!Number.isNaN(timestamp)) {
|
|
170
|
+
return Math.max(0, timestamp - Math.floor(Date.now() / 1e3));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return void 0;
|
|
174
|
+
}
|
|
175
|
+
async function request(config, method, path, options) {
|
|
176
|
+
const url = buildUrl(config.baseUrl, path, options?.query);
|
|
177
|
+
const headers = {
|
|
178
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
179
|
+
"User-Agent": `sealtrail-node/${VERSION}`
|
|
180
|
+
};
|
|
181
|
+
if (options?.body !== void 0) {
|
|
182
|
+
headers["Content-Type"] = "application/json";
|
|
183
|
+
}
|
|
184
|
+
let lastError;
|
|
185
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
186
|
+
if (attempt > 0) {
|
|
187
|
+
const retryAfterMs = lastError instanceof RateLimitError && lastError.retryAfter !== void 0 ? lastError.retryAfter * 1e3 : 0;
|
|
188
|
+
const delay = Math.max(backoff(attempt - 1), retryAfterMs);
|
|
189
|
+
if (config.debug) {
|
|
190
|
+
console.debug(
|
|
191
|
+
`[sealtrail] retry ${attempt}/${config.maxRetries} after ${Math.round(delay)}ms`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
195
|
+
}
|
|
196
|
+
const controller = new AbortController();
|
|
197
|
+
const timeoutId = setTimeout(() => controller.abort(), config.timeout);
|
|
198
|
+
const start = Date.now();
|
|
199
|
+
try {
|
|
200
|
+
const response = await config.fetch(url, {
|
|
201
|
+
method,
|
|
202
|
+
headers,
|
|
203
|
+
body: options?.body !== void 0 ? JSON.stringify(options.body) : void 0,
|
|
204
|
+
signal: controller.signal
|
|
205
|
+
});
|
|
206
|
+
if (config.debug) {
|
|
207
|
+
console.debug(
|
|
208
|
+
`[sealtrail] ${method} ${path} \u2192 ${response.status} (${Date.now() - start}ms)`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
if (response.ok) {
|
|
212
|
+
return await response.json();
|
|
213
|
+
}
|
|
214
|
+
let errorBody;
|
|
215
|
+
try {
|
|
216
|
+
errorBody = await response.json();
|
|
217
|
+
} catch {
|
|
218
|
+
errorBody = {
|
|
219
|
+
error: {
|
|
220
|
+
code: "UNKNOWN",
|
|
221
|
+
message: `HTTP ${response.status}`
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
const retryAfter = parseRetryAfter(response.headers);
|
|
226
|
+
const error = mapError(response.status, errorBody, retryAfter);
|
|
227
|
+
if (error instanceof QuotaExceededError) {
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
if (isRetryable(response.status, method) && attempt < config.maxRetries) {
|
|
231
|
+
lastError = error;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
throw error;
|
|
235
|
+
} catch (err) {
|
|
236
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
237
|
+
lastError = new Error(`Request timed out after ${config.timeout}ms`);
|
|
238
|
+
if (attempt < config.maxRetries) continue;
|
|
239
|
+
throw lastError;
|
|
240
|
+
}
|
|
241
|
+
if (err instanceof RateLimitError || err instanceof ConflictError || err instanceof Error && "status" in err) {
|
|
242
|
+
throw err;
|
|
243
|
+
}
|
|
244
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
245
|
+
if (attempt < config.maxRetries) continue;
|
|
246
|
+
throw lastError;
|
|
247
|
+
} finally {
|
|
248
|
+
clearTimeout(timeoutId);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
throw lastError ?? new Error("Request failed after retries");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/resources/base.ts
|
|
255
|
+
var BaseResource = class {
|
|
256
|
+
constructor(client) {
|
|
257
|
+
this.client = client;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// src/resources/chains.ts
|
|
262
|
+
var ChainsResource = class extends BaseResource {
|
|
263
|
+
/**
|
|
264
|
+
* List all chains for the authenticated tenant.
|
|
265
|
+
*/
|
|
266
|
+
async list() {
|
|
267
|
+
const response = await this.client._request("GET", "/v1/chains");
|
|
268
|
+
return response.data;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Get the current status of a specific chain.
|
|
272
|
+
*/
|
|
273
|
+
async status(chainId) {
|
|
274
|
+
const response = await this.client._request(
|
|
275
|
+
"GET",
|
|
276
|
+
`/v1/chain/${chainId}/status`
|
|
277
|
+
);
|
|
278
|
+
return response.data;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// src/lib/pagination.ts
|
|
283
|
+
function createAutoPaginator(fetchPage) {
|
|
284
|
+
return {
|
|
285
|
+
[Symbol.asyncIterator]() {
|
|
286
|
+
let currentPage = [];
|
|
287
|
+
let nextCursor = void 0;
|
|
288
|
+
let index = 0;
|
|
289
|
+
let started = false;
|
|
290
|
+
let done = false;
|
|
291
|
+
return {
|
|
292
|
+
async next() {
|
|
293
|
+
while (index >= currentPage.length) {
|
|
294
|
+
if (done) return { done: true, value: void 0 };
|
|
295
|
+
if (started && nextCursor === void 0) {
|
|
296
|
+
done = true;
|
|
297
|
+
return { done: true, value: void 0 };
|
|
298
|
+
}
|
|
299
|
+
const result = await fetchPage(started ? nextCursor : void 0);
|
|
300
|
+
started = true;
|
|
301
|
+
currentPage = result.data;
|
|
302
|
+
nextCursor = result.nextCursor;
|
|
303
|
+
index = 0;
|
|
304
|
+
if (currentPage.length === 0) {
|
|
305
|
+
done = true;
|
|
306
|
+
return { done: true, value: void 0 };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return { done: false, value: currentPage[index++] };
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/resources/events.ts
|
|
317
|
+
var EventsResource = class extends BaseResource {
|
|
318
|
+
/**
|
|
319
|
+
* Log a new audit event to the chain.
|
|
320
|
+
*/
|
|
321
|
+
async log(params) {
|
|
322
|
+
const response = await this.client._request("POST", "/v1/events", {
|
|
323
|
+
body: params
|
|
324
|
+
});
|
|
325
|
+
return response.data;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* List events with optional filters and cursor pagination.
|
|
329
|
+
*/
|
|
330
|
+
async list(params) {
|
|
331
|
+
return this.client._request("GET", "/v1/events", {
|
|
332
|
+
query: params
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Get a single event by ID.
|
|
337
|
+
*/
|
|
338
|
+
async get(eventId) {
|
|
339
|
+
const response = await this.client._request(
|
|
340
|
+
"GET",
|
|
341
|
+
`/v1/events/${eventId}`
|
|
342
|
+
);
|
|
343
|
+
return response.data;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Verify the hash integrity of an event.
|
|
347
|
+
*/
|
|
348
|
+
async verify(eventId) {
|
|
349
|
+
return this.client._request("GET", `/v1/events/${eventId}/verify`);
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Auto-paginate through all events matching the given filters.
|
|
353
|
+
*
|
|
354
|
+
* @example
|
|
355
|
+
* ```ts
|
|
356
|
+
* for await (const event of st.events.listAutoPaginate({ actor: 'user_123' })) {
|
|
357
|
+
* console.log(event.id);
|
|
358
|
+
* }
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
listAutoPaginate(params) {
|
|
362
|
+
return createAutoPaginator((cursor) => this.list({ ...params, cursor }));
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// src/client.ts
|
|
367
|
+
var SealTrail = class {
|
|
368
|
+
events;
|
|
369
|
+
chains;
|
|
370
|
+
config;
|
|
371
|
+
constructor(config) {
|
|
372
|
+
if (!config.apiKey) {
|
|
373
|
+
throw new Error("SealTrail: apiKey is required");
|
|
374
|
+
}
|
|
375
|
+
this.config = resolveConfig(config);
|
|
376
|
+
this.events = new EventsResource(this);
|
|
377
|
+
this.chains = new ChainsResource(this);
|
|
378
|
+
}
|
|
379
|
+
/** @internal Used by resources to make API requests */
|
|
380
|
+
async _request(method, path, options) {
|
|
381
|
+
return request(this.config, method, path, options);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
385
|
+
0 && (module.exports = {
|
|
386
|
+
AuthenticationError,
|
|
387
|
+
ConflictError,
|
|
388
|
+
ForbiddenError,
|
|
389
|
+
InternalError,
|
|
390
|
+
NotFoundError,
|
|
391
|
+
QuotaExceededError,
|
|
392
|
+
RateLimitError,
|
|
393
|
+
SealTrail,
|
|
394
|
+
SealTrailError,
|
|
395
|
+
ValidationError
|
|
396
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
interface SealTrailConfig {
|
|
2
|
+
/** API key starting with stl_live_ or stl_test_ */
|
|
3
|
+
apiKey: string;
|
|
4
|
+
/** API base URL (default: "https://api.sealtrail.dev") */
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
/** Request timeout in ms (default: 30000) */
|
|
7
|
+
timeout?: number;
|
|
8
|
+
/** Max retry attempts on 429/5xx/409 (default: 3) */
|
|
9
|
+
maxRetries?: number;
|
|
10
|
+
/** Log requests to console.debug (default: false) */
|
|
11
|
+
debug?: boolean;
|
|
12
|
+
/** Custom fetch implementation for edge runtimes */
|
|
13
|
+
fetch?: typeof globalThis.fetch;
|
|
14
|
+
}
|
|
15
|
+
interface LogEventParams {
|
|
16
|
+
/** Who performed the action */
|
|
17
|
+
actor: string;
|
|
18
|
+
/** What action was performed */
|
|
19
|
+
action: string;
|
|
20
|
+
/** What resource was affected */
|
|
21
|
+
resource?: string;
|
|
22
|
+
/** Additional context metadata */
|
|
23
|
+
context?: Record<string, unknown>;
|
|
24
|
+
/** Chain name (default: "default") */
|
|
25
|
+
chain?: string;
|
|
26
|
+
}
|
|
27
|
+
interface AuditEvent {
|
|
28
|
+
id: string;
|
|
29
|
+
actor: string;
|
|
30
|
+
action: string;
|
|
31
|
+
resource: string | null;
|
|
32
|
+
context: Record<string, unknown> | null;
|
|
33
|
+
chain: {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
position: number;
|
|
37
|
+
};
|
|
38
|
+
hash: string;
|
|
39
|
+
previousHash: string;
|
|
40
|
+
timestamp: string;
|
|
41
|
+
createdAt: string;
|
|
42
|
+
}
|
|
43
|
+
interface ListEventsParams {
|
|
44
|
+
/** Cursor for pagination */
|
|
45
|
+
cursor?: string;
|
|
46
|
+
/** Number of results per page (1-200) */
|
|
47
|
+
limit?: number;
|
|
48
|
+
/** Filter by actor */
|
|
49
|
+
actor?: string;
|
|
50
|
+
/** Filter by action */
|
|
51
|
+
action?: string;
|
|
52
|
+
/** Filter by resource */
|
|
53
|
+
resource?: string;
|
|
54
|
+
/** Filter by chain ID */
|
|
55
|
+
chain_id?: string;
|
|
56
|
+
/** Filter events after this ISO 8601 date */
|
|
57
|
+
after?: string;
|
|
58
|
+
/** Filter events before this ISO 8601 date */
|
|
59
|
+
before?: string;
|
|
60
|
+
}
|
|
61
|
+
interface EventListResponse {
|
|
62
|
+
data: AuditEvent[];
|
|
63
|
+
nextCursor?: string;
|
|
64
|
+
}
|
|
65
|
+
interface VerifyResult {
|
|
66
|
+
valid: boolean;
|
|
67
|
+
errors: string[];
|
|
68
|
+
eventHash: string;
|
|
69
|
+
computedHash: string;
|
|
70
|
+
chainIntact: boolean;
|
|
71
|
+
verifiedAt: string;
|
|
72
|
+
}
|
|
73
|
+
interface Chain {
|
|
74
|
+
id: string;
|
|
75
|
+
name: string;
|
|
76
|
+
lastHash: string;
|
|
77
|
+
lastPosition: number;
|
|
78
|
+
eventCount: number;
|
|
79
|
+
createdAt: string;
|
|
80
|
+
}
|
|
81
|
+
interface RequestOptions {
|
|
82
|
+
body?: unknown;
|
|
83
|
+
query?: Record<string, string | number | undefined>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface ResourceClient {
|
|
87
|
+
_request<T>(method: string, path: string, options?: RequestOptions): Promise<T>;
|
|
88
|
+
}
|
|
89
|
+
declare abstract class BaseResource {
|
|
90
|
+
protected readonly client: ResourceClient;
|
|
91
|
+
constructor(client: ResourceClient);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
declare class ChainsResource extends BaseResource {
|
|
95
|
+
/**
|
|
96
|
+
* List all chains for the authenticated tenant.
|
|
97
|
+
*/
|
|
98
|
+
list(): Promise<Chain[]>;
|
|
99
|
+
/**
|
|
100
|
+
* Get the current status of a specific chain.
|
|
101
|
+
*/
|
|
102
|
+
status(chainId: string): Promise<Chain>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
declare class EventsResource extends BaseResource {
|
|
106
|
+
/**
|
|
107
|
+
* Log a new audit event to the chain.
|
|
108
|
+
*/
|
|
109
|
+
log(params: LogEventParams): Promise<AuditEvent>;
|
|
110
|
+
/**
|
|
111
|
+
* List events with optional filters and cursor pagination.
|
|
112
|
+
*/
|
|
113
|
+
list(params?: ListEventsParams): Promise<EventListResponse>;
|
|
114
|
+
/**
|
|
115
|
+
* Get a single event by ID.
|
|
116
|
+
*/
|
|
117
|
+
get(eventId: string): Promise<AuditEvent>;
|
|
118
|
+
/**
|
|
119
|
+
* Verify the hash integrity of an event.
|
|
120
|
+
*/
|
|
121
|
+
verify(eventId: string): Promise<VerifyResult>;
|
|
122
|
+
/**
|
|
123
|
+
* Auto-paginate through all events matching the given filters.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* for await (const event of st.events.listAutoPaginate({ actor: 'user_123' })) {
|
|
128
|
+
* console.log(event.id);
|
|
129
|
+
* }
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
listAutoPaginate(params?: Omit<ListEventsParams, "cursor">): AsyncIterable<AuditEvent>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
declare class SealTrail {
|
|
136
|
+
readonly events: EventsResource;
|
|
137
|
+
readonly chains: ChainsResource;
|
|
138
|
+
private readonly config;
|
|
139
|
+
constructor(config: SealTrailConfig);
|
|
140
|
+
/** @internal Used by resources to make API requests */
|
|
141
|
+
_request<T>(method: string, path: string, options?: RequestOptions): Promise<T>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
declare class SealTrailError extends Error {
|
|
145
|
+
readonly code: string;
|
|
146
|
+
readonly status: number;
|
|
147
|
+
readonly details?: Record<string, string[]> | undefined;
|
|
148
|
+
constructor(message: string, code: string, status: number, details?: Record<string, string[]> | undefined);
|
|
149
|
+
}
|
|
150
|
+
declare class AuthenticationError extends SealTrailError {
|
|
151
|
+
constructor(message: string, code: string);
|
|
152
|
+
}
|
|
153
|
+
declare class ForbiddenError extends SealTrailError {
|
|
154
|
+
constructor(message: string, code: string);
|
|
155
|
+
}
|
|
156
|
+
declare class NotFoundError extends SealTrailError {
|
|
157
|
+
constructor(message: string, code: string);
|
|
158
|
+
}
|
|
159
|
+
declare class ValidationError extends SealTrailError {
|
|
160
|
+
constructor(message: string, code: string, details?: Record<string, string[]>);
|
|
161
|
+
}
|
|
162
|
+
declare class ConflictError extends SealTrailError {
|
|
163
|
+
constructor(message: string, code: string);
|
|
164
|
+
}
|
|
165
|
+
declare class RateLimitError extends SealTrailError {
|
|
166
|
+
readonly retryAfter?: number;
|
|
167
|
+
constructor(message: string, code: string, retryAfter?: number);
|
|
168
|
+
}
|
|
169
|
+
declare class QuotaExceededError extends SealTrailError {
|
|
170
|
+
constructor(message: string, code: string);
|
|
171
|
+
}
|
|
172
|
+
declare class InternalError extends SealTrailError {
|
|
173
|
+
constructor(message: string, code: string, status: number);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export { type AuditEvent, AuthenticationError, type Chain, ConflictError, type EventListResponse, ForbiddenError, InternalError, type ListEventsParams, type LogEventParams, NotFoundError, QuotaExceededError, RateLimitError, SealTrail, type SealTrailConfig, SealTrailError, ValidationError, type VerifyResult };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
interface SealTrailConfig {
|
|
2
|
+
/** API key starting with stl_live_ or stl_test_ */
|
|
3
|
+
apiKey: string;
|
|
4
|
+
/** API base URL (default: "https://api.sealtrail.dev") */
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
/** Request timeout in ms (default: 30000) */
|
|
7
|
+
timeout?: number;
|
|
8
|
+
/** Max retry attempts on 429/5xx/409 (default: 3) */
|
|
9
|
+
maxRetries?: number;
|
|
10
|
+
/** Log requests to console.debug (default: false) */
|
|
11
|
+
debug?: boolean;
|
|
12
|
+
/** Custom fetch implementation for edge runtimes */
|
|
13
|
+
fetch?: typeof globalThis.fetch;
|
|
14
|
+
}
|
|
15
|
+
interface LogEventParams {
|
|
16
|
+
/** Who performed the action */
|
|
17
|
+
actor: string;
|
|
18
|
+
/** What action was performed */
|
|
19
|
+
action: string;
|
|
20
|
+
/** What resource was affected */
|
|
21
|
+
resource?: string;
|
|
22
|
+
/** Additional context metadata */
|
|
23
|
+
context?: Record<string, unknown>;
|
|
24
|
+
/** Chain name (default: "default") */
|
|
25
|
+
chain?: string;
|
|
26
|
+
}
|
|
27
|
+
interface AuditEvent {
|
|
28
|
+
id: string;
|
|
29
|
+
actor: string;
|
|
30
|
+
action: string;
|
|
31
|
+
resource: string | null;
|
|
32
|
+
context: Record<string, unknown> | null;
|
|
33
|
+
chain: {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
position: number;
|
|
37
|
+
};
|
|
38
|
+
hash: string;
|
|
39
|
+
previousHash: string;
|
|
40
|
+
timestamp: string;
|
|
41
|
+
createdAt: string;
|
|
42
|
+
}
|
|
43
|
+
interface ListEventsParams {
|
|
44
|
+
/** Cursor for pagination */
|
|
45
|
+
cursor?: string;
|
|
46
|
+
/** Number of results per page (1-200) */
|
|
47
|
+
limit?: number;
|
|
48
|
+
/** Filter by actor */
|
|
49
|
+
actor?: string;
|
|
50
|
+
/** Filter by action */
|
|
51
|
+
action?: string;
|
|
52
|
+
/** Filter by resource */
|
|
53
|
+
resource?: string;
|
|
54
|
+
/** Filter by chain ID */
|
|
55
|
+
chain_id?: string;
|
|
56
|
+
/** Filter events after this ISO 8601 date */
|
|
57
|
+
after?: string;
|
|
58
|
+
/** Filter events before this ISO 8601 date */
|
|
59
|
+
before?: string;
|
|
60
|
+
}
|
|
61
|
+
interface EventListResponse {
|
|
62
|
+
data: AuditEvent[];
|
|
63
|
+
nextCursor?: string;
|
|
64
|
+
}
|
|
65
|
+
interface VerifyResult {
|
|
66
|
+
valid: boolean;
|
|
67
|
+
errors: string[];
|
|
68
|
+
eventHash: string;
|
|
69
|
+
computedHash: string;
|
|
70
|
+
chainIntact: boolean;
|
|
71
|
+
verifiedAt: string;
|
|
72
|
+
}
|
|
73
|
+
interface Chain {
|
|
74
|
+
id: string;
|
|
75
|
+
name: string;
|
|
76
|
+
lastHash: string;
|
|
77
|
+
lastPosition: number;
|
|
78
|
+
eventCount: number;
|
|
79
|
+
createdAt: string;
|
|
80
|
+
}
|
|
81
|
+
interface RequestOptions {
|
|
82
|
+
body?: unknown;
|
|
83
|
+
query?: Record<string, string | number | undefined>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface ResourceClient {
|
|
87
|
+
_request<T>(method: string, path: string, options?: RequestOptions): Promise<T>;
|
|
88
|
+
}
|
|
89
|
+
declare abstract class BaseResource {
|
|
90
|
+
protected readonly client: ResourceClient;
|
|
91
|
+
constructor(client: ResourceClient);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
declare class ChainsResource extends BaseResource {
|
|
95
|
+
/**
|
|
96
|
+
* List all chains for the authenticated tenant.
|
|
97
|
+
*/
|
|
98
|
+
list(): Promise<Chain[]>;
|
|
99
|
+
/**
|
|
100
|
+
* Get the current status of a specific chain.
|
|
101
|
+
*/
|
|
102
|
+
status(chainId: string): Promise<Chain>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
declare class EventsResource extends BaseResource {
|
|
106
|
+
/**
|
|
107
|
+
* Log a new audit event to the chain.
|
|
108
|
+
*/
|
|
109
|
+
log(params: LogEventParams): Promise<AuditEvent>;
|
|
110
|
+
/**
|
|
111
|
+
* List events with optional filters and cursor pagination.
|
|
112
|
+
*/
|
|
113
|
+
list(params?: ListEventsParams): Promise<EventListResponse>;
|
|
114
|
+
/**
|
|
115
|
+
* Get a single event by ID.
|
|
116
|
+
*/
|
|
117
|
+
get(eventId: string): Promise<AuditEvent>;
|
|
118
|
+
/**
|
|
119
|
+
* Verify the hash integrity of an event.
|
|
120
|
+
*/
|
|
121
|
+
verify(eventId: string): Promise<VerifyResult>;
|
|
122
|
+
/**
|
|
123
|
+
* Auto-paginate through all events matching the given filters.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* for await (const event of st.events.listAutoPaginate({ actor: 'user_123' })) {
|
|
128
|
+
* console.log(event.id);
|
|
129
|
+
* }
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
listAutoPaginate(params?: Omit<ListEventsParams, "cursor">): AsyncIterable<AuditEvent>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
declare class SealTrail {
|
|
136
|
+
readonly events: EventsResource;
|
|
137
|
+
readonly chains: ChainsResource;
|
|
138
|
+
private readonly config;
|
|
139
|
+
constructor(config: SealTrailConfig);
|
|
140
|
+
/** @internal Used by resources to make API requests */
|
|
141
|
+
_request<T>(method: string, path: string, options?: RequestOptions): Promise<T>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
declare class SealTrailError extends Error {
|
|
145
|
+
readonly code: string;
|
|
146
|
+
readonly status: number;
|
|
147
|
+
readonly details?: Record<string, string[]> | undefined;
|
|
148
|
+
constructor(message: string, code: string, status: number, details?: Record<string, string[]> | undefined);
|
|
149
|
+
}
|
|
150
|
+
declare class AuthenticationError extends SealTrailError {
|
|
151
|
+
constructor(message: string, code: string);
|
|
152
|
+
}
|
|
153
|
+
declare class ForbiddenError extends SealTrailError {
|
|
154
|
+
constructor(message: string, code: string);
|
|
155
|
+
}
|
|
156
|
+
declare class NotFoundError extends SealTrailError {
|
|
157
|
+
constructor(message: string, code: string);
|
|
158
|
+
}
|
|
159
|
+
declare class ValidationError extends SealTrailError {
|
|
160
|
+
constructor(message: string, code: string, details?: Record<string, string[]>);
|
|
161
|
+
}
|
|
162
|
+
declare class ConflictError extends SealTrailError {
|
|
163
|
+
constructor(message: string, code: string);
|
|
164
|
+
}
|
|
165
|
+
declare class RateLimitError extends SealTrailError {
|
|
166
|
+
readonly retryAfter?: number;
|
|
167
|
+
constructor(message: string, code: string, retryAfter?: number);
|
|
168
|
+
}
|
|
169
|
+
declare class QuotaExceededError extends SealTrailError {
|
|
170
|
+
constructor(message: string, code: string);
|
|
171
|
+
}
|
|
172
|
+
declare class InternalError extends SealTrailError {
|
|
173
|
+
constructor(message: string, code: string, status: number);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export { type AuditEvent, AuthenticationError, type Chain, ConflictError, type EventListResponse, ForbiddenError, InternalError, type ListEventsParams, type LogEventParams, NotFoundError, QuotaExceededError, RateLimitError, SealTrail, type SealTrailConfig, SealTrailError, ValidationError, type VerifyResult };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var SealTrailError = class extends Error {
|
|
3
|
+
constructor(message, code, status, details) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.details = details;
|
|
8
|
+
this.name = "SealTrailError";
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var AuthenticationError = class extends SealTrailError {
|
|
12
|
+
constructor(message, code) {
|
|
13
|
+
super(message, code, 401);
|
|
14
|
+
this.name = "AuthenticationError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var ForbiddenError = class extends SealTrailError {
|
|
18
|
+
constructor(message, code) {
|
|
19
|
+
super(message, code, 403);
|
|
20
|
+
this.name = "ForbiddenError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var NotFoundError = class extends SealTrailError {
|
|
24
|
+
constructor(message, code) {
|
|
25
|
+
super(message, code, 404);
|
|
26
|
+
this.name = "NotFoundError";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var ValidationError = class extends SealTrailError {
|
|
30
|
+
constructor(message, code, details) {
|
|
31
|
+
super(message, code, 400, details);
|
|
32
|
+
this.name = "ValidationError";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var ConflictError = class extends SealTrailError {
|
|
36
|
+
constructor(message, code) {
|
|
37
|
+
super(message, code, 409);
|
|
38
|
+
this.name = "ConflictError";
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var RateLimitError = class extends SealTrailError {
|
|
42
|
+
retryAfter;
|
|
43
|
+
constructor(message, code, retryAfter) {
|
|
44
|
+
super(message, code, 429);
|
|
45
|
+
this.name = "RateLimitError";
|
|
46
|
+
this.retryAfter = retryAfter;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var QuotaExceededError = class extends SealTrailError {
|
|
50
|
+
constructor(message, code) {
|
|
51
|
+
super(message, code, 429);
|
|
52
|
+
this.name = "QuotaExceededError";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var InternalError = class extends SealTrailError {
|
|
56
|
+
constructor(message, code, status) {
|
|
57
|
+
super(message, code, status);
|
|
58
|
+
this.name = "InternalError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
function mapError(status, body, retryAfter) {
|
|
62
|
+
const { code, message, details } = body.error;
|
|
63
|
+
switch (status) {
|
|
64
|
+
case 400:
|
|
65
|
+
return new ValidationError(message, code, details);
|
|
66
|
+
case 401:
|
|
67
|
+
return new AuthenticationError(message, code);
|
|
68
|
+
case 403:
|
|
69
|
+
return new ForbiddenError(message, code);
|
|
70
|
+
case 404:
|
|
71
|
+
return new NotFoundError(message, code);
|
|
72
|
+
case 409:
|
|
73
|
+
return new ConflictError(message, code);
|
|
74
|
+
case 429:
|
|
75
|
+
if (code === "QUOTA_EXCEEDED") {
|
|
76
|
+
return new QuotaExceededError(message, code);
|
|
77
|
+
}
|
|
78
|
+
return new RateLimitError(message, code, retryAfter);
|
|
79
|
+
default:
|
|
80
|
+
if (status >= 500) {
|
|
81
|
+
return new InternalError(message, code, status);
|
|
82
|
+
}
|
|
83
|
+
return new SealTrailError(message, code, status, details);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/version.ts
|
|
88
|
+
var VERSION = true ? "0.1.0" : "0.1.0";
|
|
89
|
+
|
|
90
|
+
// src/lib/fetch.ts
|
|
91
|
+
var DEFAULT_BASE_URL = "https://api.sealtrail.dev";
|
|
92
|
+
var DEFAULT_TIMEOUT = 3e4;
|
|
93
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
94
|
+
var MAX_BACKOFF = 3e4;
|
|
95
|
+
function resolveConfig(config) {
|
|
96
|
+
return {
|
|
97
|
+
apiKey: config.apiKey,
|
|
98
|
+
baseUrl: (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, ""),
|
|
99
|
+
timeout: config.timeout ?? DEFAULT_TIMEOUT,
|
|
100
|
+
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
101
|
+
debug: config.debug ?? false,
|
|
102
|
+
fetch: config.fetch ?? globalThis.fetch
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function buildUrl(baseUrl, path, query) {
|
|
106
|
+
const url = new URL(path, baseUrl);
|
|
107
|
+
if (query) {
|
|
108
|
+
for (const [key, value] of Object.entries(query)) {
|
|
109
|
+
if (value !== void 0) {
|
|
110
|
+
url.searchParams.set(key, String(value));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return url.toString();
|
|
115
|
+
}
|
|
116
|
+
function backoff(attempt) {
|
|
117
|
+
const base = Math.min(1e3 * 2 ** attempt, MAX_BACKOFF);
|
|
118
|
+
return base + Math.random() * base * 0.25;
|
|
119
|
+
}
|
|
120
|
+
function isRetryable(status, method) {
|
|
121
|
+
if (status === 429 || status >= 500) return true;
|
|
122
|
+
if (status === 409 && method === "POST") return true;
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
function parseRetryAfter(headers) {
|
|
126
|
+
const retryAfter = headers.get("retry-after");
|
|
127
|
+
if (retryAfter) {
|
|
128
|
+
const seconds = Number(retryAfter);
|
|
129
|
+
if (!Number.isNaN(seconds)) return seconds;
|
|
130
|
+
}
|
|
131
|
+
const reset = headers.get("ratelimit-reset");
|
|
132
|
+
if (reset) {
|
|
133
|
+
const timestamp = Number(reset);
|
|
134
|
+
if (!Number.isNaN(timestamp)) {
|
|
135
|
+
return Math.max(0, timestamp - Math.floor(Date.now() / 1e3));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return void 0;
|
|
139
|
+
}
|
|
140
|
+
async function request(config, method, path, options) {
|
|
141
|
+
const url = buildUrl(config.baseUrl, path, options?.query);
|
|
142
|
+
const headers = {
|
|
143
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
144
|
+
"User-Agent": `sealtrail-node/${VERSION}`
|
|
145
|
+
};
|
|
146
|
+
if (options?.body !== void 0) {
|
|
147
|
+
headers["Content-Type"] = "application/json";
|
|
148
|
+
}
|
|
149
|
+
let lastError;
|
|
150
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
151
|
+
if (attempt > 0) {
|
|
152
|
+
const retryAfterMs = lastError instanceof RateLimitError && lastError.retryAfter !== void 0 ? lastError.retryAfter * 1e3 : 0;
|
|
153
|
+
const delay = Math.max(backoff(attempt - 1), retryAfterMs);
|
|
154
|
+
if (config.debug) {
|
|
155
|
+
console.debug(
|
|
156
|
+
`[sealtrail] retry ${attempt}/${config.maxRetries} after ${Math.round(delay)}ms`
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
160
|
+
}
|
|
161
|
+
const controller = new AbortController();
|
|
162
|
+
const timeoutId = setTimeout(() => controller.abort(), config.timeout);
|
|
163
|
+
const start = Date.now();
|
|
164
|
+
try {
|
|
165
|
+
const response = await config.fetch(url, {
|
|
166
|
+
method,
|
|
167
|
+
headers,
|
|
168
|
+
body: options?.body !== void 0 ? JSON.stringify(options.body) : void 0,
|
|
169
|
+
signal: controller.signal
|
|
170
|
+
});
|
|
171
|
+
if (config.debug) {
|
|
172
|
+
console.debug(
|
|
173
|
+
`[sealtrail] ${method} ${path} \u2192 ${response.status} (${Date.now() - start}ms)`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (response.ok) {
|
|
177
|
+
return await response.json();
|
|
178
|
+
}
|
|
179
|
+
let errorBody;
|
|
180
|
+
try {
|
|
181
|
+
errorBody = await response.json();
|
|
182
|
+
} catch {
|
|
183
|
+
errorBody = {
|
|
184
|
+
error: {
|
|
185
|
+
code: "UNKNOWN",
|
|
186
|
+
message: `HTTP ${response.status}`
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const retryAfter = parseRetryAfter(response.headers);
|
|
191
|
+
const error = mapError(response.status, errorBody, retryAfter);
|
|
192
|
+
if (error instanceof QuotaExceededError) {
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
if (isRetryable(response.status, method) && attempt < config.maxRetries) {
|
|
196
|
+
lastError = error;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
throw error;
|
|
200
|
+
} catch (err) {
|
|
201
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
202
|
+
lastError = new Error(`Request timed out after ${config.timeout}ms`);
|
|
203
|
+
if (attempt < config.maxRetries) continue;
|
|
204
|
+
throw lastError;
|
|
205
|
+
}
|
|
206
|
+
if (err instanceof RateLimitError || err instanceof ConflictError || err instanceof Error && "status" in err) {
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
209
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
210
|
+
if (attempt < config.maxRetries) continue;
|
|
211
|
+
throw lastError;
|
|
212
|
+
} finally {
|
|
213
|
+
clearTimeout(timeoutId);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
throw lastError ?? new Error("Request failed after retries");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/resources/base.ts
|
|
220
|
+
var BaseResource = class {
|
|
221
|
+
constructor(client) {
|
|
222
|
+
this.client = client;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// src/resources/chains.ts
|
|
227
|
+
var ChainsResource = class extends BaseResource {
|
|
228
|
+
/**
|
|
229
|
+
* List all chains for the authenticated tenant.
|
|
230
|
+
*/
|
|
231
|
+
async list() {
|
|
232
|
+
const response = await this.client._request("GET", "/v1/chains");
|
|
233
|
+
return response.data;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Get the current status of a specific chain.
|
|
237
|
+
*/
|
|
238
|
+
async status(chainId) {
|
|
239
|
+
const response = await this.client._request(
|
|
240
|
+
"GET",
|
|
241
|
+
`/v1/chain/${chainId}/status`
|
|
242
|
+
);
|
|
243
|
+
return response.data;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// src/lib/pagination.ts
|
|
248
|
+
function createAutoPaginator(fetchPage) {
|
|
249
|
+
return {
|
|
250
|
+
[Symbol.asyncIterator]() {
|
|
251
|
+
let currentPage = [];
|
|
252
|
+
let nextCursor = void 0;
|
|
253
|
+
let index = 0;
|
|
254
|
+
let started = false;
|
|
255
|
+
let done = false;
|
|
256
|
+
return {
|
|
257
|
+
async next() {
|
|
258
|
+
while (index >= currentPage.length) {
|
|
259
|
+
if (done) return { done: true, value: void 0 };
|
|
260
|
+
if (started && nextCursor === void 0) {
|
|
261
|
+
done = true;
|
|
262
|
+
return { done: true, value: void 0 };
|
|
263
|
+
}
|
|
264
|
+
const result = await fetchPage(started ? nextCursor : void 0);
|
|
265
|
+
started = true;
|
|
266
|
+
currentPage = result.data;
|
|
267
|
+
nextCursor = result.nextCursor;
|
|
268
|
+
index = 0;
|
|
269
|
+
if (currentPage.length === 0) {
|
|
270
|
+
done = true;
|
|
271
|
+
return { done: true, value: void 0 };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return { done: false, value: currentPage[index++] };
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/resources/events.ts
|
|
282
|
+
var EventsResource = class extends BaseResource {
|
|
283
|
+
/**
|
|
284
|
+
* Log a new audit event to the chain.
|
|
285
|
+
*/
|
|
286
|
+
async log(params) {
|
|
287
|
+
const response = await this.client._request("POST", "/v1/events", {
|
|
288
|
+
body: params
|
|
289
|
+
});
|
|
290
|
+
return response.data;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* List events with optional filters and cursor pagination.
|
|
294
|
+
*/
|
|
295
|
+
async list(params) {
|
|
296
|
+
return this.client._request("GET", "/v1/events", {
|
|
297
|
+
query: params
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Get a single event by ID.
|
|
302
|
+
*/
|
|
303
|
+
async get(eventId) {
|
|
304
|
+
const response = await this.client._request(
|
|
305
|
+
"GET",
|
|
306
|
+
`/v1/events/${eventId}`
|
|
307
|
+
);
|
|
308
|
+
return response.data;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Verify the hash integrity of an event.
|
|
312
|
+
*/
|
|
313
|
+
async verify(eventId) {
|
|
314
|
+
return this.client._request("GET", `/v1/events/${eventId}/verify`);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Auto-paginate through all events matching the given filters.
|
|
318
|
+
*
|
|
319
|
+
* @example
|
|
320
|
+
* ```ts
|
|
321
|
+
* for await (const event of st.events.listAutoPaginate({ actor: 'user_123' })) {
|
|
322
|
+
* console.log(event.id);
|
|
323
|
+
* }
|
|
324
|
+
* ```
|
|
325
|
+
*/
|
|
326
|
+
listAutoPaginate(params) {
|
|
327
|
+
return createAutoPaginator((cursor) => this.list({ ...params, cursor }));
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// src/client.ts
|
|
332
|
+
var SealTrail = class {
|
|
333
|
+
events;
|
|
334
|
+
chains;
|
|
335
|
+
config;
|
|
336
|
+
constructor(config) {
|
|
337
|
+
if (!config.apiKey) {
|
|
338
|
+
throw new Error("SealTrail: apiKey is required");
|
|
339
|
+
}
|
|
340
|
+
this.config = resolveConfig(config);
|
|
341
|
+
this.events = new EventsResource(this);
|
|
342
|
+
this.chains = new ChainsResource(this);
|
|
343
|
+
}
|
|
344
|
+
/** @internal Used by resources to make API requests */
|
|
345
|
+
async _request(method, path, options) {
|
|
346
|
+
return request(this.config, method, path, options);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
export {
|
|
350
|
+
AuthenticationError,
|
|
351
|
+
ConflictError,
|
|
352
|
+
ForbiddenError,
|
|
353
|
+
InternalError,
|
|
354
|
+
NotFoundError,
|
|
355
|
+
QuotaExceededError,
|
|
356
|
+
RateLimitError,
|
|
357
|
+
SealTrail,
|
|
358
|
+
SealTrailError,
|
|
359
|
+
ValidationError
|
|
360
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sealtrail",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official Node.js SDK for the SealTrail cryptographic audit trail API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": {
|
|
12
|
+
"types": "./dist/index.d.cts",
|
|
13
|
+
"default": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"main": "./dist/index.cjs",
|
|
18
|
+
"module": "./dist/index.mjs",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"test:watch": "vitest",
|
|
28
|
+
"typecheck": "tsc --build",
|
|
29
|
+
"prepublishOnly": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"audit",
|
|
33
|
+
"audit-trail",
|
|
34
|
+
"audit-log",
|
|
35
|
+
"compliance",
|
|
36
|
+
"cryptographic",
|
|
37
|
+
"hash-chain",
|
|
38
|
+
"sealtrail"
|
|
39
|
+
],
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/zerolooplabs/sealtrail"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://sealtrail.dev",
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"tsup": "^8.4.0",
|
|
48
|
+
"typescript": "^5.7.3",
|
|
49
|
+
"vitest": "^3.1.1"
|
|
50
|
+
}
|
|
51
|
+
}
|