@verne-software/sdk 1.2.0 → 1.4.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 +282 -65
- package/dist/index.d.mts +1091 -72
- package/dist/index.d.ts +1091 -72
- package/dist/index.js +938 -83
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +935 -84
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
The official Node.js and TypeScript library for the [Verne Nautilus](https://vernesoft.com) platform.
|
|
7
7
|
|
|
8
|
-
> **Server-side only.** API keys carry full
|
|
8
|
+
> **Server-side only.** API keys carry full access to the APIs they were created for and must never be used in browser or client-side code.
|
|
9
9
|
|
|
10
10
|
## Requirements
|
|
11
11
|
|
|
@@ -26,19 +26,28 @@ pnpm add @verne-software/sdk
|
|
|
26
26
|
```ts
|
|
27
27
|
import { Verne } from '@verne-software/sdk';
|
|
28
28
|
|
|
29
|
-
const verne = new Verne({
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
});
|
|
29
|
+
const verne = new Verne({ apiKey: process.env.VERNE_API_KEY });
|
|
30
|
+
|
|
31
|
+
await verne.relay.messages.send({ event_type: 'user.created', payload: { id: 'usr_123' } });
|
|
32
|
+
await verne.gate.organizations.create({ name: 'Acme' });
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
### API keys
|
|
36
|
+
|
|
37
|
+
Create a key on **Dashboard → Keys** and tick the APIs it may call — its **scopes**: `relay`, `gate`, `clockwork`, `passepartout`. A key looks like `vrn_9f8a7…` and does not name an API. Pass it once as `apiKey`; the client sends it as `Authorization: Bearer …` to every service.
|
|
38
|
+
|
|
39
|
+
- A call to an API the key has no scope for is refused by the API with `403`, code `forbidden`, message `This API key is not authorized for <API>.` The SDK does not check scopes itself.
|
|
40
|
+
- To use a different key for one service, pass it as `relay`, `gate`, `clockwork` or `passepartout`; it overrides `apiKey` for that service only.
|
|
41
|
+
- There is **no test or live mode and no sandbox**: every key acts on your real data. Use a separate key per environment.
|
|
42
|
+
- Keys made before scopes existed (`vrn_gate_…`, `vrn_relay_…`, …) keep working, for the one API they name.
|
|
43
|
+
|
|
44
|
+
You can also instantiate one service on its own:
|
|
36
45
|
|
|
37
46
|
```ts
|
|
38
|
-
import {
|
|
47
|
+
import { Gate, Clockwork } from '@verne-software/sdk';
|
|
39
48
|
|
|
40
|
-
const
|
|
41
|
-
const
|
|
49
|
+
const gate = new Gate({ apiKey: process.env.VERNE_API_KEY });
|
|
50
|
+
const clockwork = new Clockwork({ apiKey: process.env.VERNE_API_KEY });
|
|
42
51
|
```
|
|
43
52
|
|
|
44
53
|
## Relay — Webhooks-as-a-Service
|
|
@@ -58,7 +67,7 @@ Optional parameters:
|
|
|
58
67
|
await verne.relay.messages.send({
|
|
59
68
|
event_type: 'order.placed',
|
|
60
69
|
payload: { order_id: '999' },
|
|
61
|
-
idempotency_key: 'evt_abc', //
|
|
70
|
+
idempotency_key: 'evt_abc', // a repeat within 12 hours returns the original message
|
|
62
71
|
channels: ['team-a'], // restrict to specific endpoint channels
|
|
63
72
|
});
|
|
64
73
|
```
|
|
@@ -73,65 +82,186 @@ console.log(page.has_more); // boolean
|
|
|
73
82
|
console.log(page.next_cursor); // pass to the next call to paginate
|
|
74
83
|
```
|
|
75
84
|
|
|
76
|
-
## Gate —
|
|
85
|
+
## Gate — sign-in and users
|
|
77
86
|
|
|
78
|
-
###
|
|
87
|
+
### Identities
|
|
79
88
|
|
|
80
|
-
Manage your end
|
|
89
|
+
Manage your end users. They are kept apart per tenant: the same address at another tenant is a different person.
|
|
81
90
|
|
|
82
91
|
```ts
|
|
83
|
-
// Create a user
|
|
92
|
+
// Create a user (traits are `email` and, optionally, `locale`)
|
|
84
93
|
const identity = await verne.gate.identities.create({
|
|
85
94
|
schema_id: 'user',
|
|
86
|
-
traits: {
|
|
87
|
-
email: 'user@example.com',
|
|
88
|
-
custom_data: { role: 'editor' },
|
|
89
|
-
},
|
|
95
|
+
traits: { email: 'user@example.com' },
|
|
90
96
|
credentials: {
|
|
91
97
|
password: { config: { password: 'StrongPassword123!' } },
|
|
92
98
|
},
|
|
93
|
-
state: 'active',
|
|
94
99
|
});
|
|
100
|
+
// A second user with the same address in your account is a 409 `conflict`.
|
|
95
101
|
|
|
96
|
-
// Get
|
|
102
|
+
// Get, update (JSON Patch — RFC 6902), delete
|
|
97
103
|
await verne.gate.identities.get(identity.id);
|
|
98
|
-
|
|
99
|
-
// Update a user (JSON Patch — RFC 6902)
|
|
100
104
|
await verne.gate.identities.patch(identity.id, [
|
|
101
|
-
{ op: 'replace', path: '/traits/
|
|
105
|
+
{ op: 'replace', path: '/traits/email', value: 'new@example.com' },
|
|
102
106
|
]);
|
|
103
|
-
|
|
104
|
-
// Delete a user
|
|
105
107
|
await verne.gate.identities.delete(identity.id);
|
|
106
108
|
|
|
107
|
-
// Activate / deactivate
|
|
109
|
+
// Activate / deactivate (an inactive user cannot sign in; nothing is deleted)
|
|
108
110
|
await verne.gate.identities.deactivate(identity.id);
|
|
109
111
|
await verne.gate.identities.activate(identity.id);
|
|
110
|
-
// …or set the state explicitly:
|
|
111
|
-
await verne.gate.identities.setState(identity.id, 'inactive');
|
|
112
112
|
|
|
113
113
|
// Resend the email verification link
|
|
114
114
|
await verne.gate.identities.resendVerification(identity.id);
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
Your own fields about a user go in **custom metadata**, not in traits:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
await verne.gate.identities.updateMetadata(identity.id, { plan: 'pro', seats: 5 }); // merged
|
|
121
|
+
await verne.gate.identities.updateMetadata(identity.id, { seats: null }); // removes `seats`
|
|
122
|
+
const metadata = await verne.gate.identities.getMetadata(identity.id); // { plan: 'pro' }
|
|
123
|
+
```
|
|
118
124
|
|
|
119
|
-
|
|
125
|
+
### Organizations, roles and members
|
|
126
|
+
|
|
127
|
+
Group your end users into organizations (their teams, workspaces, companies). A **role** is a named set of permissions inside one organization; a **member** holds one role. Everything acts on the project the key belongs to. Gate keeps this data and reports it; it does not enforce it — ask it.
|
|
120
128
|
|
|
121
129
|
```ts
|
|
122
|
-
const
|
|
123
|
-
// security.passwordless_enabled, security.mfa_enabled
|
|
130
|
+
const org = await verne.gate.organizations.create({ name: 'Acme' });
|
|
124
131
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
132
|
+
const admin = await verne.gate.organizations.roles.create(org.id, {
|
|
133
|
+
name: 'admin', // lowercase, digits, _ and -
|
|
134
|
+
permissions: ['invoices:read', 'invoices:write'], // lowercase segments joined by `:`
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await verne.gate.organizations.members.add(org.id, {
|
|
138
|
+
identity_id: identity.id,
|
|
139
|
+
role_id: admin.id,
|
|
140
|
+
expires_at: '2027-01-31T23:59:59Z', // optional: the membership ends then
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Change the role or the deadline: leave expires_at out to keep it, null to remove it
|
|
144
|
+
await verne.gate.organizations.members.update(org.id, identity.id, { expires_at: null });
|
|
145
|
+
|
|
146
|
+
// Lists are cursor-paged: loop while has_more, passing next_cursor as cursor
|
|
147
|
+
const { data, has_more, next_cursor } = await verne.gate.organizations.members.list(org.id, { limit: 50 });
|
|
148
|
+
|
|
149
|
+
// A user's organizations, with role and permissions in each
|
|
150
|
+
const held = await verne.gate.identities.listOrganizations(identity.id);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
A role somebody holds cannot be deleted (`409 conflict`); a second role of the same name in one organization is `409 conflict`; past 100 roles, `409 limit_reached`.
|
|
154
|
+
|
|
155
|
+
#### Checking a permission
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
if (!(await verne.gate.organizations.can(org.id, identity.id, 'invoices:write'))) {
|
|
159
|
+
throw new Error('Forbidden');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Or the full decision:
|
|
163
|
+
const decision = await verne.gate.organizations.authorize(org.id, identity.id, 'invoices:write');
|
|
164
|
+
// { allowed: true, decision_id: 'dec_…', reason: 'member_with_permission', role: 'admin' }
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`reason` is one of `member_with_permission`, `member_without_permission`, `not_a_member`, `subject_inactive`, `no_such_organization`; `role` is present when the user is a member. The match is exact (no wildcards) and live. A permission no role could hold (`Invoices:*`, the `nautilus:` prefix) is a `400 invalid_payload` error, never `false`.
|
|
168
|
+
|
|
169
|
+
`gate.authorize({ subject, action, resource })` is the same endpoint; with any resource that is not `organization:<id>` it only says whether `subject` is one of your active end users. Its `reason` is a code too: `owned_by_tenant` (the one `allowed: true`), `subject_inactive`, `not_owned_by_tenant`, `not_a_known_identity`. The sentence a person would read is in `message`, for your logs — its wording can change, `reason` is the contract.
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
const decision = await verne.gate.authorize({ subject: identity.id, action: 'read', resource: 'doc:1' });
|
|
173
|
+
// { allowed: true, decision_id: 'dec_…', reason: 'owned_by_tenant', message: 'subject `…` is an identity owned by tenant `…`' }
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Sessions
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
// Is this session good enough to act on? (the one call to gate your routes on)
|
|
180
|
+
const check = await verne.gate.sessions.check(sessionToken);
|
|
181
|
+
// { active, aal, mfa_required, mfa_satisfied, identity_id, orgs: [{ id, role, permissions }] }
|
|
182
|
+
|
|
183
|
+
const sessions = await verne.gate.sessions.list(identity.id);
|
|
184
|
+
await verne.gate.sessions.revoke(sessions[0].id); // one device
|
|
185
|
+
await verne.gate.sessions.revokeAll(identity.id); // everywhere
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`revokeAll()` succeeds also when the user has no session, so a "log out everywhere" button can be pressed twice. An id that is not one of your users (another account's, unknown, or one you deleted) is `403 forbidden`.
|
|
189
|
+
|
|
190
|
+
With MFA required (`settings.updateSecurity({ passwordless_enabled, mfa_enabled: true })`), `check()` throws a `VerneAPIError` with code `mfa_required` and `details.required_aal === 'aal2'`; start a step-up flow with `gate.createLoginFlow({ aal: 'aal2', session_token: sessionToken })`. A dead, foreign or unknown session is `401`.
|
|
191
|
+
|
|
192
|
+
### Signed tokens (JWT)
|
|
193
|
+
|
|
194
|
+
For a platform that verifies signatures instead of calling Gate (Supabase, an API gateway):
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
const { token } = await verne.gate.tokens.createSigned(sessionToken); // no API key involved
|
|
198
|
+
|
|
199
|
+
const claims = await verne.gate.tokens.verifySigned(token, { tenantId: process.env.VERNE_TENANT_ID! });
|
|
200
|
+
claims.sub; // the end user's id
|
|
201
|
+
claims.orgs; // [{ id, role, permissions }], as of when the token was made
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Verification is offline, against your tenant's public keys (fetched once, cached, refetched when a new key appears). It refuses anything but ES256, another tenant's issuer, a bad signature, an expired token — with a `VerneTokenError` whose `reason` says which. To verify where you have no Gate client:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { GateTokenVerifier } from '@verne-software/sdk';
|
|
208
|
+
|
|
209
|
+
const verifier = new GateTokenVerifier({ tenantId: process.env.VERNE_TENANT_ID! });
|
|
210
|
+
const claims = await verifier.verify(token);
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
A signed token lives **15 minutes** and **cannot be revoked**; its `orgs` are as old as the token. For anything that must follow a revocation or a role change at once, use `sessions.check()`, introspection or `organizations.can()`.
|
|
214
|
+
|
|
215
|
+
### Audit log
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
const page = await verne.gate.auditLog.list({
|
|
219
|
+
action: 'organization.member_added',
|
|
220
|
+
from: new Date(Date.now() - 7 * 24 * 3600 * 1000),
|
|
221
|
+
limit: 50,
|
|
129
222
|
});
|
|
223
|
+
// newest first; page.data[i] = { id, occurred_at, action, actor, target, project_id, details, ip, user_agent }
|
|
130
224
|
```
|
|
131
225
|
|
|
132
|
-
|
|
226
|
+
Any key with the `gate` scope reads the **whole** log, including your team members' IP addresses and user agents. Keep `gate` keys on your backend.
|
|
133
227
|
|
|
134
|
-
|
|
228
|
+
### Automatic deletion of inactive accounts
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
const { tier, rules } = await verne.gate.lifecycle.listRules();
|
|
232
|
+
|
|
233
|
+
const preview = await verne.gate.lifecycle.previewRule('delete_unverified', {
|
|
234
|
+
delete_after_days: 14,
|
|
235
|
+
warn_before_days: 3,
|
|
236
|
+
}); // { matching_now, capped, … } — look before you switch it on
|
|
237
|
+
|
|
238
|
+
await verne.gate.lifecycle.setRule('delete_unverified', {
|
|
239
|
+
enabled: true,
|
|
240
|
+
params: { delete_after_days: 14, warn_before_days: 3 },
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const actions = await verne.gate.lifecycle.listActions({ status: 'pending' });
|
|
244
|
+
await verne.gate.lifecycle.deleteRule('delete_unverified');
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Only `delete_unverified` is in the Free plan; switching on another rule is `403 plan_required`.
|
|
248
|
+
|
|
249
|
+
### Settings
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
const security = await verne.gate.settings.getSecurity();
|
|
253
|
+
// Both fields are required — the update is a full replacement, not a merge.
|
|
254
|
+
await verne.gate.settings.updateSecurity({ passwordless_enabled: true, mfa_enabled: false });
|
|
255
|
+
|
|
256
|
+
// Social login: where your backend receives the user (matched exactly)
|
|
257
|
+
await verne.gate.settings.updateReturnUrls(['https://app.example.com/auth/callback']);
|
|
258
|
+
const flow = await verne.gate.createLoginFlow({ state: csrfBoundValue }); // then, at your callback:
|
|
259
|
+
const { session_token } = await verne.gate.exchangeOidcCode(code);
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Access tokens
|
|
263
|
+
|
|
264
|
+
Short-lived opaque tokens for a user or a service account:
|
|
135
265
|
|
|
136
266
|
```ts
|
|
137
267
|
const token = await verne.gate.tokens.create({
|
|
@@ -140,65 +270,123 @@ const token = await verne.gate.tokens.create({
|
|
|
140
270
|
ttl_seconds: 3600, // optional, default 3600, max 86400
|
|
141
271
|
});
|
|
142
272
|
|
|
143
|
-
|
|
144
|
-
|
|
273
|
+
const info = await verne.gate.tokens.introspect(token.access_token);
|
|
274
|
+
if (!info.active) {
|
|
275
|
+
// expired, unknown, or its user is deactivated or deleted — `info` is just { active: false }
|
|
276
|
+
}
|
|
277
|
+
// For an end user, info.orgs lists their organizations, read now.
|
|
145
278
|
```
|
|
146
279
|
|
|
147
|
-
|
|
280
|
+
## Passepartout — Telegram sign-in
|
|
148
281
|
|
|
149
282
|
```ts
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
283
|
+
const { nonce, deep_link } = await verne.passepartout.loginStart();
|
|
284
|
+
// send the user to deep_link, then poll:
|
|
285
|
+
const status = await verne.passepartout.loginStatus(nonce);
|
|
286
|
+
if (status.status === 'completed') {
|
|
287
|
+
const info = await verne.passepartout.introspect(status.access_token!);
|
|
288
|
+
info.orgs; // the user's organizations in your project
|
|
154
289
|
}
|
|
155
290
|
```
|
|
156
291
|
|
|
157
|
-
|
|
292
|
+
## Clockwork — Cron-as-a-Service
|
|
158
293
|
|
|
159
|
-
|
|
294
|
+
Schedule recurring and one-shot HTTP jobs.
|
|
295
|
+
|
|
296
|
+
### Cron Jobs
|
|
160
297
|
|
|
161
298
|
```ts
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
299
|
+
// Create a recurring job
|
|
300
|
+
const job = await verne.clockwork.jobs.create({
|
|
301
|
+
name: 'nightly-report',
|
|
302
|
+
schedule: '0 2 * * *', // cron expression
|
|
303
|
+
url: 'https://example.com/hooks/report',
|
|
304
|
+
method: 'POST', // optional, defaults to POST
|
|
305
|
+
headers: { 'X-Api-Key': 'secret' }, // optional
|
|
306
|
+
body: JSON.stringify({ scope: 'all' }), // optional
|
|
166
307
|
});
|
|
167
308
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
309
|
+
// List all cron jobs (returns CronJob[])
|
|
310
|
+
const jobs = await verne.clockwork.jobs.list();
|
|
311
|
+
|
|
312
|
+
// Update a job (partial — only the fields you pass are changed)
|
|
313
|
+
await verne.clockwork.jobs.update(job.id, { schedule: '0 3 * * *', is_active: false });
|
|
314
|
+
|
|
315
|
+
// Inspect the execution history (returns Execution[], one row per attempt)
|
|
316
|
+
const runs = await verne.clockwork.jobs.executions(job.id);
|
|
317
|
+
|
|
318
|
+
// Delete a job
|
|
319
|
+
await verne.clockwork.jobs.delete(job.id);
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Delayed Jobs
|
|
323
|
+
|
|
324
|
+
One-shot jobs that fire once at a specific time:
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
const delayed = await verne.clockwork.delayed.create({
|
|
328
|
+
name: 'send-reminder',
|
|
329
|
+
run_at: '2026-01-01T09:00:00Z', // ISO 8601 timestamp
|
|
330
|
+
url: 'https://example.com/hooks/reminder',
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const pending = await verne.clockwork.delayed.list();
|
|
334
|
+
const runs = await verne.clockwork.delayed.executions(delayed.id);
|
|
335
|
+
await verne.clockwork.delayed.cancel(delayed.id);
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Usage
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
const usage = await verne.clockwork.usage();
|
|
342
|
+
// { plan: 'free', cron: { active: 3, limit: 10 }, delayed: { pending: 12, limit: 100 } }
|
|
171
343
|
```
|
|
172
344
|
|
|
173
345
|
## Error Handling
|
|
174
346
|
|
|
175
|
-
|
|
347
|
+
API errors throw a `VerneAPIError`:
|
|
176
348
|
|
|
177
349
|
```ts
|
|
178
350
|
import { VerneAPIError } from '@verne-software/sdk';
|
|
179
351
|
|
|
180
352
|
try {
|
|
181
|
-
await verne.
|
|
353
|
+
await verne.gate.organizations.members.add(orgId, { identity_id, role_id });
|
|
182
354
|
} catch (err) {
|
|
183
355
|
if (err instanceof VerneAPIError) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
356
|
+
err.status; // HTTP status
|
|
357
|
+
err.code; // e.g. 'conflict' — branch on status + code
|
|
358
|
+
err.message; // a sentence to show a person
|
|
359
|
+
err.details; // extra members of the error, e.g. { plan, limit, current }
|
|
360
|
+
err.retryAfter; // seconds, when the response had Retry-After
|
|
361
|
+
err.requestId; // include in support requests
|
|
187
362
|
}
|
|
188
363
|
}
|
|
189
364
|
```
|
|
190
365
|
|
|
191
|
-
|
|
366
|
+
| Status | `code` | Meaning |
|
|
367
|
+
|:--|:--|:--|
|
|
368
|
+
| 400 | `invalid_payload` | Something in the request is malformed; `message` says what. |
|
|
369
|
+
| 401 | `unauthorized` | No key, or a revoked or invalid one; for a session, a dead session. |
|
|
370
|
+
| 402 | `ACTIVE_USER_LIMIT_REACHED`, `UPGRADE_REQUIRED`, `JOB_LIMIT_REACHED`, `BILLING_HARD_BLOCKED`, `SUBSCRIPTION_PAST_DUE` | A plan or billing limit. Does not lift by waiting — the SDK never retries it. |
|
|
371
|
+
| 403 | `forbidden` | The key lacks the scope for this API (`message` names it), or the resource is not yours. |
|
|
372
|
+
| 403 | `plan_required`, `mfa_required`, `account_disabled` | A rule not in your plan; a second factor owed; a deactivated Telegram user. |
|
|
373
|
+
| 404 | `not_found` | No such thing in your project. |
|
|
374
|
+
| 409 | `conflict`, `limit_reached` | It already exists **in your account** (an end user with that address, a role name, a membership), a role still held, or a cap reached. |
|
|
375
|
+
| 429 | `rate_limited`, `RELAY_MESSAGE_LIMIT_REACHED` | Too fast, or Relay's monthly limit. |
|
|
376
|
+
| 5xx | `upstream_unavailable`, `internal_error` | Retry; quote `requestId` to support. |
|
|
377
|
+
|
|
378
|
+
Codes are an open set; treat unknown ones generically. Every API route, Clockwork's included, answers in this envelope with a string `code` and a `requestId`. A body in another shape (one that did not come from the API — a proxy in between, an identity-service answer passed through on `/auth/*`) still becomes a `VerneAPIError`, with `code: 'unknown'` and whatever sentence it had in `message`.
|
|
379
|
+
|
|
380
|
+
**Retries.** A `POST` that gets a short `429` (a `Retry-After` of a minute or less) is retried once after waiting. Relay's monthly limit (`429 RELAY_MESSAGE_LIMIT_REACHED`, `Retry-After` until the 1st) and every `402` are thrown at once.
|
|
381
|
+
|
|
382
|
+
Network failures and timeouts throw a base `VerneError`; a refused signed token throws `VerneTokenError`.
|
|
192
383
|
|
|
193
384
|
## Configuration
|
|
194
385
|
|
|
195
|
-
|
|
386
|
+
`Verne` and the per-service clients accept `baseUrl` and `timeoutMs` (default `30000`):
|
|
196
387
|
|
|
197
388
|
```ts
|
|
198
|
-
const verne = new Verne({
|
|
199
|
-
relay: process.env.VERNE_RELAY_KEY,
|
|
200
|
-
timeoutMs: 10_000,
|
|
201
|
-
});
|
|
389
|
+
const verne = new Verne({ apiKey: process.env.VERNE_API_KEY, timeoutMs: 10_000 });
|
|
202
390
|
```
|
|
203
391
|
|
|
204
392
|
Individual requests can be cancelled with an `AbortSignal`:
|
|
@@ -212,6 +400,35 @@ await verne.relay.messages.send(
|
|
|
212
400
|
);
|
|
213
401
|
```
|
|
214
402
|
|
|
403
|
+
## Development
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
npm test # unit tests (no network)
|
|
407
|
+
npm run lint
|
|
408
|
+
npm run typecheck
|
|
409
|
+
npm run build
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Integration tests
|
|
413
|
+
|
|
414
|
+
`npm run test:integration` runs every resource against a real Nautilus stack. It is opt-in and skipped without a key; it creates end users, an organization and settings changes, and removes them again.
|
|
415
|
+
|
|
416
|
+
| Variable | |
|
|
417
|
+
|:--|:--|
|
|
418
|
+
| `VERNE_API_KEY` | A key with the scopes `gate`, `clockwork`, `passepartout`. Required. |
|
|
419
|
+
| `VERNE_BASE_URL` | Default `http://api.soft.com` (the local stack). Any other host needs `VERNE_ALLOW_REMOTE=1`. |
|
|
420
|
+
| `VERNE_EMAIL_PREFIX` | Every end user created is `<prefix>-<name>@e2e.soft.com`. |
|
|
421
|
+
| `VERNE_NARROW_KEY` | Optional: a key **without** the `gate` scope, to check the `403`. |
|
|
422
|
+
|
|
423
|
+
Against a local stack, `scripts/local-stack-key.mjs` registers a throwaway `e2e-` tenant and mints the keys:
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
node scripts/local-stack-key.mjs create /tmp/sdk-it.json
|
|
427
|
+
eval "$(node scripts/local-stack-key.mjs env /tmp/sdk-it.json)"
|
|
428
|
+
npm run test:integration
|
|
429
|
+
node scripts/local-stack-key.mjs revoke /tmp/sdk-it.json # then purge the tenant it names
|
|
430
|
+
```
|
|
431
|
+
|
|
215
432
|
## License
|
|
216
433
|
|
|
217
434
|
[MIT](LICENSE)
|