@envshed/node 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,1055 @@
1
+ # @envshed/node
2
+
3
+ Official Node.js SDK for [Envshed](https://envshed.com) — secrets management for teams.
4
+
5
+ Read, write, and manage your Envshed secrets programmatically from any JavaScript or TypeScript project.
6
+
7
+ ## Features
8
+
9
+ - Full coverage of the Envshed REST API
10
+ - Zero runtime dependencies — uses native `fetch` (Node 18+)
11
+ - First-class TypeScript support with complete type definitions
12
+ - Automatic retries with exponential backoff for transient failures
13
+ - Namespaced API methods for clean, discoverable usage
14
+ - Dual ESM and CommonJS output
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ # npm
20
+ npm install @envshed/node
21
+
22
+ # pnpm
23
+ pnpm add @envshed/node
24
+
25
+ # yarn
26
+ yarn add @envshed/node
27
+ ```
28
+
29
+ **Requirements:** Node.js 18 or later.
30
+
31
+ ## Quick Start
32
+
33
+ ```typescript
34
+ import { EnvshedClient } from "@envshed/node";
35
+
36
+ const client = new EnvshedClient({
37
+ token: process.env.ENVSHED_TOKEN!,
38
+ });
39
+
40
+ // Fetch all secrets for an environment
41
+ const { secrets } = await client.secrets.get({
42
+ org: "my-org",
43
+ project: "my-project",
44
+ env: "production",
45
+ });
46
+
47
+ console.log(secrets.DATABASE_URL);
48
+ console.log(secrets.API_KEY);
49
+ ```
50
+
51
+ ## Authentication
52
+
53
+ The SDK supports two types of tokens:
54
+
55
+ - **User API tokens** (`envshed_...`) — created from the Envshed dashboard under Settings > API Tokens. These tokens inherit the permissions of the user who created them.
56
+ - **Service tokens** (`envshed_svc_...`) — created for CI/CD and machine-to-machine access. These can be scoped to an organization, project, or environment with `read_only` or `read_write` permissions.
57
+
58
+ ```typescript
59
+ // Using a user API token
60
+ const client = new EnvshedClient({
61
+ token: "envshed_abc123...",
62
+ });
63
+
64
+ // Using a service token
65
+ const client = new EnvshedClient({
66
+ token: "envshed_svc_xyz789...",
67
+ });
68
+ ```
69
+
70
+ ## Client Configuration
71
+
72
+ ### `EnvshedClientOptions`
73
+
74
+ | Option | Type | Default | Description |
75
+ |--------|------|---------|-------------|
76
+ | `token` | `string` | *(required)* | API authentication token |
77
+ | `apiUrl` | `string` | `"https://app.envshed.com"` | Base URL for the Envshed API |
78
+ | `retry` | `RetryOptions` | See below | Retry configuration for transient failures |
79
+
80
+ ### `RetryOptions`
81
+
82
+ | Option | Type | Default | Description |
83
+ |--------|------|---------|-------------|
84
+ | `maxRetries` | `number` | `3` | Maximum retry attempts. Set to `0` to disable retries. |
85
+ | `initialDelayMs` | `number` | `1000` | Initial delay between retries in milliseconds |
86
+ | `maxDelayMs` | `number` | `10000` | Maximum delay cap in milliseconds |
87
+ | `backoff` | `"exponential" \| "linear" \| BackoffFunction` | `"exponential"` | Backoff strategy between retries (see [Retry Strategy](#retry-strategy)) |
88
+ | `shouldRetry` | `ShouldRetryFunction` | 5xx + network errors | Custom function to decide which errors to retry |
89
+ | `onRetry` | `OnRetryFunction` | `undefined` | Callback invoked before each retry attempt |
90
+
91
+ ```typescript
92
+ const client = new EnvshedClient({
93
+ token: "envshed_...",
94
+ apiUrl: "https://custom-instance.example.com",
95
+ retry: {
96
+ maxRetries: 5,
97
+ initialDelayMs: 500,
98
+ maxDelayMs: 15000,
99
+ },
100
+ });
101
+ ```
102
+
103
+ By default, retries use exponential backoff with jitter. Only 5xx server errors and network failures are retried — 4xx client errors are never retried. See the [Retry Strategy](#retry-strategy) section for full customization.
104
+
105
+ ---
106
+
107
+ ## API Reference
108
+
109
+ All methods are `async` and return typed promises. The client organizes methods into namespaced sub-APIs.
110
+
111
+ ### `client.me()`
112
+
113
+ Check the authenticated identity.
114
+
115
+ ```typescript
116
+ const me = await client.me();
117
+
118
+ if ("email" in me) {
119
+ // User token
120
+ console.log(me.email); // "user@example.com"
121
+ } else {
122
+ // Service token
123
+ console.log(me.type); // "service_token"
124
+ console.log(me.org); // "my-org" | null
125
+ console.log(me.scope); // "org" | "project" | "environment"
126
+ console.log(me.permission); // "read" | "read_write"
127
+ }
128
+ ```
129
+
130
+ **Returns:** `MeResponse` — either `{ email: string }` for user tokens or `{ type: "service_token", org, scope, permission }` for service tokens.
131
+
132
+ ---
133
+
134
+ ### Secrets
135
+
136
+ The most commonly used API. Read and write environment variables.
137
+
138
+ #### `client.secrets.get(path)`
139
+
140
+ Retrieve all secrets for an environment. Values are returned decrypted.
141
+
142
+ ```typescript
143
+ const result = await client.secrets.get({
144
+ org: "my-org",
145
+ project: "my-project",
146
+ env: "production",
147
+ });
148
+
149
+ console.log(result.secrets); // { DATABASE_URL: "postgres://...", API_KEY: "sk_..." }
150
+ console.log(result.version); // 42
151
+ console.log(result.placeholders); // ["PLACEHOLDER_KEY"]
152
+ console.log(result.linkedKeys); // ["SHARED_SECRET"]
153
+ console.log(result.decryptErrors); // [] (keys that failed to decrypt)
154
+ ```
155
+
156
+ **Parameters:**
157
+
158
+ | Param | Type | Description |
159
+ |-------|------|-------------|
160
+ | `path.org` | `string` | Organization slug |
161
+ | `path.project` | `string` | Project slug |
162
+ | `path.env` | `string` | Environment slug |
163
+
164
+ **Returns:** `GetSecretsResponse`
165
+
166
+ | Field | Type | Description |
167
+ |-------|------|-------------|
168
+ | `secrets` | `Record<string, string>` | Key-value pairs of decrypted secrets |
169
+ | `placeholders` | `string[]` | Keys that are placeholders (no real value) |
170
+ | `version` | `number` | Current environment version number |
171
+ | `linkedKeys` | `string[]` | Keys shared from linked projects (optional) |
172
+ | `decryptErrors` | `string[]` | Keys that failed to decrypt (optional) |
173
+
174
+ #### `client.secrets.set(path, secrets)`
175
+
176
+ Create or update secrets in an environment. Existing keys are updated; new keys are created.
177
+
178
+ ```typescript
179
+ const result = await client.secrets.set(
180
+ { org: "my-org", project: "my-project", env: "production" },
181
+ {
182
+ DATABASE_URL: "postgres://prod-host/mydb",
183
+ API_KEY: "sk_live_new_key",
184
+ NEW_SECRET: "new_value",
185
+ },
186
+ );
187
+
188
+ console.log(result.ok); // true
189
+ console.log(result.version); // 43
190
+ ```
191
+
192
+ **Parameters:**
193
+
194
+ | Param | Type | Description |
195
+ |-------|------|-------------|
196
+ | `path` | `EnvPath` | Organization, project, and environment slugs |
197
+ | `secrets` | `Record<string, string>` | Key-value pairs to upsert |
198
+
199
+ **Returns:** `SetSecretsResponse` — `{ ok: true, version: number }`
200
+
201
+ ---
202
+
203
+ ### Organizations
204
+
205
+ #### `client.orgs.list()`
206
+
207
+ List all organizations the authenticated user belongs to.
208
+
209
+ ```typescript
210
+ const { organizations } = await client.orgs.list();
211
+
212
+ for (const org of organizations) {
213
+ console.log(`${org.name} (${org.slug}) — role: ${org.role}`);
214
+ }
215
+ ```
216
+
217
+ **Returns:** `ListOrgsResponse` — `{ organizations: Organization[] }`
218
+
219
+ Each `Organization` has:
220
+
221
+ | Field | Type | Description |
222
+ |-------|------|-------------|
223
+ | `name` | `string` | Organization display name |
224
+ | `slug` | `string` | URL-safe identifier |
225
+ | `role` | `string` | User's role: `"owner"`, `"admin"`, or `"member"` |
226
+
227
+ #### `client.orgs.create(data)`
228
+
229
+ Create a new organization.
230
+
231
+ ```typescript
232
+ const { organization } = await client.orgs.create({
233
+ name: "Acme Corp",
234
+ slug: "acme-corp", // optional — auto-generated from name if omitted
235
+ description: "Main org", // optional
236
+ });
237
+
238
+ console.log(organization.slug); // "acme-corp"
239
+ ```
240
+
241
+ **Parameters:**
242
+
243
+ | Param | Type | Required | Description |
244
+ |-------|------|----------|-------------|
245
+ | `name` | `string` | Yes | Organization display name |
246
+ | `slug` | `string` | No | URL-safe identifier (auto-generated if omitted) |
247
+ | `description` | `string` | No | Description |
248
+
249
+ **Returns:** `CreateOrgResponse` — `{ organization: { name, slug } }`
250
+
251
+ ---
252
+
253
+ ### Projects
254
+
255
+ #### `client.projects.list(org)`
256
+
257
+ List all projects within an organization.
258
+
259
+ ```typescript
260
+ const { projects } = await client.projects.list("my-org");
261
+
262
+ for (const project of projects) {
263
+ console.log(`${project.name} (${project.slug})`);
264
+ }
265
+ ```
266
+
267
+ **Parameters:**
268
+
269
+ | Param | Type | Description |
270
+ |-------|------|-------------|
271
+ | `org` | `string` | Organization slug |
272
+
273
+ **Returns:** `ListProjectsResponse` — `{ projects: Project[] }`
274
+
275
+ Each `Project` has:
276
+
277
+ | Field | Type | Description |
278
+ |-------|------|-------------|
279
+ | `id` | `string` | Unique identifier |
280
+ | `name` | `string` | Project display name |
281
+ | `slug` | `string` | URL-safe identifier |
282
+ | `description` | `string \| null` | Description |
283
+
284
+ #### `client.projects.create(org, data)`
285
+
286
+ Create a new project. Automatically creates three default environments: development, staging, and production.
287
+
288
+ ```typescript
289
+ const { project } = await client.projects.create("my-org", {
290
+ name: "Backend API",
291
+ description: "Main backend service", // optional
292
+ });
293
+
294
+ console.log(project.slug); // "backend-api"
295
+ ```
296
+
297
+ **Parameters:**
298
+
299
+ | Param | Type | Required | Description |
300
+ |-------|------|----------|-------------|
301
+ | `org` | `string` | Yes | Organization slug |
302
+ | `name` | `string` | Yes | Project display name |
303
+ | `description` | `string` | No | Description |
304
+
305
+ **Returns:** `CreateProjectResponse` — `{ project: { name, slug } }`
306
+
307
+ ---
308
+
309
+ ### Environments
310
+
311
+ #### `client.environments.list(org, project)`
312
+
313
+ List all environments within a project.
314
+
315
+ ```typescript
316
+ const { environments } = await client.environments.list("my-org", "my-project");
317
+
318
+ for (const env of environments) {
319
+ console.log(`${env.name} (${env.slug})`);
320
+ }
321
+ // "Development (development)"
322
+ // "Staging (staging)"
323
+ // "Production (production)"
324
+ ```
325
+
326
+ **Parameters:**
327
+
328
+ | Param | Type | Description |
329
+ |-------|------|-------------|
330
+ | `org` | `string` | Organization slug |
331
+ | `project` | `string` | Project slug |
332
+
333
+ **Returns:** `ListEnvironmentsResponse` — `{ environments: Environment[] }`
334
+
335
+ Each `Environment` has:
336
+
337
+ | Field | Type | Description |
338
+ |-------|------|-------------|
339
+ | `id` | `string` | Unique identifier |
340
+ | `name` | `string` | Environment display name |
341
+ | `slug` | `string` | URL-safe identifier |
342
+ | `description` | `string \| null` | Description |
343
+
344
+ #### `client.environments.create(org, project, data)`
345
+
346
+ Create a new environment within a project.
347
+
348
+ ```typescript
349
+ const { environment } = await client.environments.create(
350
+ "my-org",
351
+ "my-project",
352
+ {
353
+ name: "QA",
354
+ description: "Quality assurance testing", // optional
355
+ },
356
+ );
357
+
358
+ console.log(environment.slug); // "qa"
359
+ ```
360
+
361
+ **Parameters:**
362
+
363
+ | Param | Type | Required | Description |
364
+ |-------|------|----------|-------------|
365
+ | `org` | `string` | Yes | Organization slug |
366
+ | `project` | `string` | Yes | Project slug |
367
+ | `name` | `string` | Yes | Environment display name |
368
+ | `description` | `string` | No | Description |
369
+
370
+ **Returns:** `CreateEnvironmentResponse` — `{ environment: { name, slug } }`
371
+
372
+ ---
373
+
374
+ ### Environment Version
375
+
376
+ Track the current version of an environment. Useful for cache invalidation and polling for changes.
377
+
378
+ #### `client.version.get(path, etag?)`
379
+
380
+ Get the current version of an environment. Supports HTTP ETag for conditional requests — pass the previous ETag to check if the version has changed without re-fetching secrets.
381
+
382
+ ```typescript
383
+ // First request — get the current version
384
+ const versionInfo = await client.version.get({
385
+ org: "my-org",
386
+ project: "my-project",
387
+ env: "production",
388
+ });
389
+ console.log(versionInfo.version); // 42
390
+ console.log(versionInfo.updatedAt); // "2026-02-25T12:00:00Z"
391
+
392
+ // Subsequent requests — pass the ETag to check for changes
393
+ const etag = `"v${versionInfo.version}"`;
394
+ const updated = await client.version.get(
395
+ { org: "my-org", project: "my-project", env: "production" },
396
+ etag,
397
+ );
398
+
399
+ if (updated === null) {
400
+ console.log("No changes since last check");
401
+ } else {
402
+ console.log(`New version: ${updated.version}`);
403
+ }
404
+ ```
405
+
406
+ **Parameters:**
407
+
408
+ | Param | Type | Required | Description |
409
+ |-------|------|----------|-------------|
410
+ | `path` | `EnvPath` | Yes | Organization, project, and environment slugs |
411
+ | `etag` | `string` | No | ETag from a previous response (e.g., `"v42"`) |
412
+
413
+ **Returns:** `GetVersionResponse | null`
414
+
415
+ - Returns `{ version: number, updatedAt: string }` if the version has changed (or no ETag was provided).
416
+ - Returns `null` if the version has not changed (304 Not Modified).
417
+
418
+ ---
419
+
420
+ ### Secret Versions
421
+
422
+ View the change history of individual secrets and rollback to previous versions.
423
+
424
+ #### `client.versions.list(path, secretKey, options?)`
425
+
426
+ List the version history for a specific secret key.
427
+
428
+ ```typescript
429
+ const { versions } = await client.versions.list(
430
+ { org: "my-org", project: "my-project", env: "production" },
431
+ "DATABASE_URL",
432
+ { limit: 10, offset: 0 }, // optional pagination
433
+ );
434
+
435
+ for (const v of versions) {
436
+ console.log(`v${v.version} — ${v.changeType} at ${v.createdAt}`);
437
+ }
438
+ // "v5 — updated at 2026-02-25T12:00:00Z"
439
+ // "v3 — created at 2026-02-20T08:00:00Z"
440
+ ```
441
+
442
+ **Parameters:**
443
+
444
+ | Param | Type | Required | Description |
445
+ |-------|------|----------|-------------|
446
+ | `path` | `EnvPath` | Yes | Organization, project, and environment slugs |
447
+ | `secretKey` | `string` | Yes | The secret key name (e.g., `"DATABASE_URL"`) |
448
+ | `options.limit` | `number` | No | Maximum results to return (default: 50) |
449
+ | `options.offset` | `number` | No | Number of results to skip (default: 0) |
450
+
451
+ **Returns:** `ListSecretVersionsResponse` — `{ versions: SecretVersion[] }`
452
+
453
+ Each `SecretVersion` has:
454
+
455
+ | Field | Type | Description |
456
+ |-------|------|-------------|
457
+ | `version` | `number` | Version number |
458
+ | `changeType` | `string` | `"created"`, `"updated"`, or `"rolled_back"` |
459
+ | `changedBy` | `string \| null` | User ID who made the change (null for service tokens) |
460
+ | `comment` | `string \| null` | Optional change comment |
461
+ | `createdAt` | `string` | ISO 8601 timestamp |
462
+
463
+ #### `client.versions.rollback(path, secretKey, targetVersion)`
464
+
465
+ Rollback a secret to a previous version. This creates a new version with the old value.
466
+
467
+ ```typescript
468
+ const result = await client.versions.rollback(
469
+ { org: "my-org", project: "my-project", env: "production" },
470
+ "DATABASE_URL",
471
+ 3, // target version to rollback to
472
+ );
473
+
474
+ console.log(result.ok); // true
475
+ console.log(result.newVersion); // 6 (the newly created version)
476
+ ```
477
+
478
+ **Parameters:**
479
+
480
+ | Param | Type | Description |
481
+ |-------|------|-------------|
482
+ | `path` | `EnvPath` | Organization, project, and environment slugs |
483
+ | `secretKey` | `string` | The secret key name |
484
+ | `targetVersion` | `number` | Version number to rollback to |
485
+
486
+ **Returns:** `RollbackSecretResponse` — `{ ok: true, newVersion: number }`
487
+
488
+ ---
489
+
490
+ ### Snapshots
491
+
492
+ Capture and restore complete environment state. Snapshots save the current version of every secret so you can restore them all at once.
493
+
494
+ #### `client.snapshots.list(path)`
495
+
496
+ List all snapshots for an environment.
497
+
498
+ ```typescript
499
+ const { snapshots } = await client.snapshots.list({
500
+ org: "my-org",
501
+ project: "my-project",
502
+ env: "production",
503
+ });
504
+
505
+ for (const snap of snapshots) {
506
+ console.log(`${snap.name ?? "Unnamed"} — ${snap.createdAt}`);
507
+ }
508
+ ```
509
+
510
+ **Returns:** `ListSnapshotsResponse` — `{ snapshots: Snapshot[] }`
511
+
512
+ Each `Snapshot` has:
513
+
514
+ | Field | Type | Description |
515
+ |-------|------|-------------|
516
+ | `id` | `string` | Unique identifier |
517
+ | `name` | `string \| null` | Optional snapshot name |
518
+ | `description` | `string \| null` | Optional description |
519
+ | `createdBy` | `string \| null` | User ID who created it (null for service tokens) |
520
+ | `createdAt` | `string` | ISO 8601 timestamp |
521
+
522
+ #### `client.snapshots.create(path, data?)`
523
+
524
+ Create a snapshot of the current environment state.
525
+
526
+ ```typescript
527
+ const snapshot = await client.snapshots.create(
528
+ { org: "my-org", project: "my-project", env: "production" },
529
+ {
530
+ name: "pre-deploy-v2.5", // optional
531
+ description: "Before v2.5 release", // optional
532
+ },
533
+ );
534
+
535
+ console.log(snapshot.id); // "snap_abc123"
536
+ console.log(snapshot.name); // "pre-deploy-v2.5"
537
+ console.log(snapshot.createdAt); // "2026-02-25T12:00:00Z"
538
+ ```
539
+
540
+ **Parameters:**
541
+
542
+ | Param | Type | Required | Description |
543
+ |-------|------|----------|-------------|
544
+ | `path` | `EnvPath` | Yes | Organization, project, and environment slugs |
545
+ | `name` | `string` | No | Snapshot name |
546
+ | `description` | `string` | No | Snapshot description |
547
+
548
+ **Returns:** `CreateSnapshotResponse` — `{ id, name, createdAt }`
549
+
550
+ #### `client.snapshots.restore(path, snapshotId)`
551
+
552
+ Restore an environment to a previous snapshot state. All secrets are restored to their values at the time the snapshot was taken.
553
+
554
+ ```typescript
555
+ const result = await client.snapshots.restore(
556
+ { org: "my-org", project: "my-project", env: "production" },
557
+ "snap_abc123",
558
+ );
559
+
560
+ console.log(result.ok); // true
561
+ console.log(result.restoredCount); // 15 (number of secrets restored)
562
+ ```
563
+
564
+ **Parameters:**
565
+
566
+ | Param | Type | Description |
567
+ |-------|------|-------------|
568
+ | `path` | `EnvPath` | Organization, project, and environment slugs |
569
+ | `snapshotId` | `string` | ID of the snapshot to restore |
570
+
571
+ **Returns:** `RestoreSnapshotResponse` — `{ ok: true, restoredCount: number }`
572
+
573
+ ---
574
+
575
+ ### Service Tokens
576
+
577
+ Manage machine-to-machine authentication tokens. These endpoints require organization admin or owner access.
578
+
579
+ #### `client.serviceTokens.list(org)`
580
+
581
+ List all service tokens for an organization.
582
+
583
+ ```typescript
584
+ const { tokens } = await client.serviceTokens.list("my-org");
585
+
586
+ for (const token of tokens) {
587
+ console.log(`${token.name} (${token.scope}, ${token.permission})`);
588
+ console.log(` Active: ${token.is_active}`);
589
+ console.log(` Last used: ${token.last_used_at ?? "never"}`);
590
+ }
591
+ ```
592
+
593
+ **Returns:** `ListServiceTokensResponse` — `{ tokens: ServiceToken[] }`
594
+
595
+ Each `ServiceToken` has:
596
+
597
+ | Field | Type | Description |
598
+ |-------|------|-------------|
599
+ | `id` | `string` | Unique identifier |
600
+ | `name` | `string` | Token display name |
601
+ | `description` | `string \| null` | Description |
602
+ | `token_prefix` | `string` | First characters of the token for identification |
603
+ | `scope` | `"org" \| "project" \| "environment"` | Access scope |
604
+ | `project_id` | `string \| null` | Scoped project (if applicable) |
605
+ | `environment_id` | `string \| null` | Scoped environment (if applicable) |
606
+ | `permission` | `"read" \| "read_write"` | Permission level |
607
+ | `expires_at` | `string \| null` | Expiration date (ISO 8601) or null for no expiration |
608
+ | `last_used_at` | `string \| null` | Last usage timestamp |
609
+ | `is_active` | `boolean` | Whether the token is active |
610
+ | `created_at` | `string` | Creation timestamp |
611
+ | `created_by_email` | `string` | Email of the user who created it |
612
+ | `created_by_name` | `string \| null` | Name of the user who created it |
613
+
614
+ #### `client.serviceTokens.create(org, data)`
615
+
616
+ Create a new service token. The raw token value is returned only once — store it securely.
617
+
618
+ ```typescript
619
+ const result = await client.serviceTokens.create("my-org", {
620
+ name: "CI/CD Pipeline",
621
+ description: "GitHub Actions deployment token", // optional
622
+ scope: "environment", // "org" | "project" | "environment"
623
+ projectId: "project-uuid", // required for "project" or "environment" scope
624
+ environmentId: "env-uuid", // required for "environment" scope
625
+ permission: "read_only", // "read" | "read_write"
626
+ expiresAt: "2026-12-31T23:59:59Z", // optional ISO 8601 date
627
+ });
628
+
629
+ console.log(result.token); // "envshed_svc_..." — save this, it won't be shown again
630
+ console.log(result.id); // token ID for future management
631
+ console.log(result.name); // "CI/CD Pipeline"
632
+ ```
633
+
634
+ **Parameters:**
635
+
636
+ | Param | Type | Required | Description |
637
+ |-------|------|----------|-------------|
638
+ | `org` | `string` | Yes | Organization slug |
639
+ | `name` | `string` | Yes | Token display name |
640
+ | `description` | `string` | No | Description |
641
+ | `scope` | `"org" \| "project" \| "environment"` | No | Access scope |
642
+ | `projectId` | `string` | Conditional | Required if scope is `"project"` or `"environment"` |
643
+ | `environmentId` | `string` | Conditional | Required if scope is `"environment"` |
644
+ | `permission` | `"read" \| "read_write"` | No | Permission level |
645
+ | `expiresAt` | `string` | No | Expiration date (ISO 8601) |
646
+
647
+ **Returns:** `CreateServiceTokenResponse` — `{ token, id, name }`
648
+
649
+ #### `client.serviceTokens.delete(org, tokenId)`
650
+
651
+ Revoke a service token. This action is immediate and irreversible.
652
+
653
+ ```typescript
654
+ const result = await client.serviceTokens.delete("my-org", "token-id-123");
655
+ console.log(result.ok); // true
656
+ ```
657
+
658
+ **Parameters:**
659
+
660
+ | Param | Type | Description |
661
+ |-------|------|-------------|
662
+ | `org` | `string` | Organization slug |
663
+ | `tokenId` | `string` | ID of the token to revoke |
664
+
665
+ **Returns:** `DeleteServiceTokenResponse` — `{ ok: true }`
666
+
667
+ ---
668
+
669
+ ## Error Handling
670
+
671
+ The SDK throws typed errors that you can catch and inspect.
672
+
673
+ ### `EnvshedError`
674
+
675
+ Thrown when the API returns an HTTP error response (4xx or 5xx).
676
+
677
+ ```typescript
678
+ import { EnvshedClient, EnvshedError } from "@envshed/node";
679
+
680
+ const client = new EnvshedClient({ token: "envshed_..." });
681
+
682
+ try {
683
+ await client.secrets.get({
684
+ org: "my-org",
685
+ project: "my-project",
686
+ env: "nonexistent",
687
+ });
688
+ } catch (err) {
689
+ if (err instanceof EnvshedError) {
690
+ console.log(err.status); // 404
691
+ console.log(err.apiMessage); // "Environment not found"
692
+ console.log(err.method); // "GET"
693
+ console.log(err.path); // "/secrets/my-org/my-project/nonexistent"
694
+
695
+ // Convenience getters
696
+ if (err.isNotFound) {
697
+ console.log("Resource does not exist");
698
+ } else if (err.isUnauthorized) {
699
+ console.log("Invalid or expired token");
700
+ } else if (err.isForbidden) {
701
+ console.log("Insufficient permissions");
702
+ } else if (err.isSubscriptionRequired) {
703
+ console.log("Organization subscription required");
704
+ } else if (err.isRetryable) {
705
+ console.log("Server error — retries were exhausted");
706
+ }
707
+ }
708
+ }
709
+ ```
710
+
711
+ **Properties:**
712
+
713
+ | Property | Type | Description |
714
+ |----------|------|-------------|
715
+ | `status` | `number` | HTTP status code |
716
+ | `apiMessage` | `string` | Error message from the API |
717
+ | `method` | `string` | HTTP method (`GET`, `POST`, `PUT`, `DELETE`) |
718
+ | `path` | `string` | API path that was called |
719
+ | `isUnauthorized` | `boolean` | `true` if status is 401 |
720
+ | `isForbidden` | `boolean` | `true` if status is 403 |
721
+ | `isNotFound` | `boolean` | `true` if status is 404 |
722
+ | `isSubscriptionRequired` | `boolean` | `true` if status is 402 |
723
+ | `isRetryable` | `boolean` | `true` if status is 5xx |
724
+
725
+ ### `EnvshedNetworkError`
726
+
727
+ Thrown when a network-level failure occurs (DNS resolution failure, connection refused, timeout, etc.). These errors are always considered retryable and will be retried according to your retry configuration before being thrown.
728
+
729
+ ```typescript
730
+ import { EnvshedClient, EnvshedNetworkError } from "@envshed/node";
731
+
732
+ try {
733
+ await client.me();
734
+ } catch (err) {
735
+ if (err instanceof EnvshedNetworkError) {
736
+ console.log(err.message); // "Envshed network error: fetch failed"
737
+ console.log(err.method); // "GET"
738
+ console.log(err.path); // "/me"
739
+ console.log(err.cause); // Original Error object
740
+ console.log(err.isRetryable); // true (always)
741
+ }
742
+ }
743
+ ```
744
+
745
+ **Properties:**
746
+
747
+ | Property | Type | Description |
748
+ |----------|------|-------------|
749
+ | `method` | `string` | HTTP method |
750
+ | `path` | `string` | API path that was called |
751
+ | `cause` | `Error` | The original network error |
752
+ | `isRetryable` | `boolean` | Always `true` |
753
+
754
+ ---
755
+
756
+ ## Retry Strategy
757
+
758
+ The SDK automatically retries failed requests for transient failures. The retry behavior is fully customizable.
759
+
760
+ ### Default behavior
761
+
762
+ - **5xx server errors** — retried up to `maxRetries` times
763
+ - **Network errors** (DNS, connection refused, timeout) — retried up to `maxRetries` times
764
+ - **4xx client errors** — never retried (these are deterministic)
765
+
766
+ Retries use **exponential backoff with jitter**:
767
+
768
+ ```
769
+ delay = min(initialDelayMs * 2^(attempt - 1), maxDelayMs) +/- 25% jitter
770
+ ```
771
+
772
+ With default settings (`maxRetries: 3, initialDelayMs: 1000, maxDelayMs: 10000`), retry delays are approximately:
773
+
774
+ | Attempt | Base Delay | With Jitter |
775
+ |---------|-----------|-------------|
776
+ | 1st retry | 1000ms | 750–1250ms |
777
+ | 2nd retry | 2000ms | 1500–2500ms |
778
+ | 3rd retry | 4000ms | 3000–5000ms |
779
+
780
+ To disable retries entirely:
781
+
782
+ ```typescript
783
+ const client = new EnvshedClient({
784
+ token: "envshed_...",
785
+ retry: { maxRetries: 0 },
786
+ });
787
+ ```
788
+
789
+ ### Backoff strategies
790
+
791
+ Choose between built-in strategies or provide your own function.
792
+
793
+ **Exponential (default)** — delay doubles each attempt:
794
+
795
+ ```typescript
796
+ const client = new EnvshedClient({
797
+ token: "envshed_...",
798
+ retry: {
799
+ backoff: "exponential", // initialDelayMs * 2^(attempt-1)
800
+ },
801
+ });
802
+ ```
803
+
804
+ **Linear** — constant delay between retries:
805
+
806
+ ```typescript
807
+ const client = new EnvshedClient({
808
+ token: "envshed_...",
809
+ retry: {
810
+ backoff: "linear", // initialDelayMs for every attempt
811
+ },
812
+ });
813
+ ```
814
+
815
+ **Custom function** — return the delay in milliseconds (still capped by `maxDelayMs`):
816
+
817
+ ```typescript
818
+ const client = new EnvshedClient({
819
+ token: "envshed_...",
820
+ retry: {
821
+ initialDelayMs: 100,
822
+ maxDelayMs: 30000,
823
+ backoff: (attempt, initialDelayMs) => {
824
+ // Cubic backoff: 100ms, 800ms, 2700ms, ...
825
+ return initialDelayMs * Math.pow(attempt, 3);
826
+ },
827
+ },
828
+ });
829
+ ```
830
+
831
+ The `BackoffFunction` signature:
832
+
833
+ ```typescript
834
+ type BackoffFunction = (attempt: number, initialDelayMs: number) => number;
835
+ ```
836
+
837
+ - `attempt` — 1-based attempt number (1 for the first retry, 2 for the second, etc.)
838
+ - `initialDelayMs` — the configured `initialDelayMs` value
839
+ - Return value is capped by `maxDelayMs` and has +/- 25% jitter applied automatically
840
+
841
+ ### Custom retry condition (`shouldRetry`)
842
+
843
+ Override which errors trigger a retry. Receives the error and the current attempt number.
844
+
845
+ ```typescript
846
+ import { EnvshedClient, EnvshedError } from "@envshed/node";
847
+
848
+ const client = new EnvshedClient({
849
+ token: "envshed_...",
850
+ retry: {
851
+ maxRetries: 5,
852
+ shouldRetry: (error, attempt) => {
853
+ // Retry 429 (rate limited) in addition to the default 5xx
854
+ if (error instanceof EnvshedError) {
855
+ return error.status >= 500 || error.status === 429;
856
+ }
857
+ // Always retry network errors
858
+ return true;
859
+ },
860
+ },
861
+ });
862
+ ```
863
+
864
+ The `ShouldRetryFunction` signature:
865
+
866
+ ```typescript
867
+ type ShouldRetryFunction = (error: Error, attempt: number) => boolean;
868
+ ```
869
+
870
+ - `error` — an `EnvshedError` (HTTP error) or `EnvshedNetworkError` (network failure)
871
+ - `attempt` — 1-based attempt number
872
+ - Return `true` to retry, `false` to throw immediately
873
+
874
+ ### Retry callback (`onRetry`)
875
+
876
+ Hook into each retry for logging, metrics, or monitoring.
877
+
878
+ ```typescript
879
+ const client = new EnvshedClient({
880
+ token: "envshed_...",
881
+ retry: {
882
+ onRetry: (error, attempt) => {
883
+ console.warn(`[envshed] Retry attempt ${attempt}:`, error.message);
884
+ },
885
+ },
886
+ });
887
+ ```
888
+
889
+ The `OnRetryFunction` signature:
890
+
891
+ ```typescript
892
+ type OnRetryFunction = (error: Error, attempt: number) => void;
893
+ ```
894
+
895
+ - Called **before** the delay/sleep for each retry
896
+ - Not called on the initial request or when retries are disabled
897
+
898
+ ### Full example: custom retry strategy
899
+
900
+ ```typescript
901
+ import { EnvshedClient, EnvshedError, EnvshedNetworkError } from "@envshed/node";
902
+
903
+ const client = new EnvshedClient({
904
+ token: process.env.ENVSHED_TOKEN!,
905
+ retry: {
906
+ maxRetries: 5,
907
+ initialDelayMs: 200,
908
+ maxDelayMs: 30000,
909
+
910
+ // Linear backoff
911
+ backoff: "linear",
912
+
913
+ // Retry on 429, 5xx, and network errors
914
+ shouldRetry: (error, attempt) => {
915
+ if (error instanceof EnvshedError) {
916
+ return error.status === 429 || error.status >= 500;
917
+ }
918
+ return true; // network errors
919
+ },
920
+
921
+ // Log retries
922
+ onRetry: (error, attempt) => {
923
+ const status = error instanceof EnvshedError ? error.status : "network";
924
+ console.warn(`[envshed] Retry ${attempt}/5 (${status}): ${error.message}`);
925
+ },
926
+ },
927
+ });
928
+ ```
929
+
930
+ ---
931
+
932
+ ## Common Patterns
933
+
934
+ ### Load secrets into `process.env`
935
+
936
+ ```typescript
937
+ import { EnvshedClient } from "@envshed/node";
938
+
939
+ const client = new EnvshedClient({ token: process.env.ENVSHED_TOKEN! });
940
+
941
+ const { secrets } = await client.secrets.get({
942
+ org: "my-org",
943
+ project: "my-project",
944
+ env: "production",
945
+ });
946
+
947
+ // Inject into process.env
948
+ Object.assign(process.env, secrets);
949
+ ```
950
+
951
+ ### Sync secrets from a `.env` file
952
+
953
+ ```typescript
954
+ import { readFileSync } from "node:fs";
955
+ import { EnvshedClient } from "@envshed/node";
956
+
957
+ const client = new EnvshedClient({ token: process.env.ENVSHED_TOKEN! });
958
+
959
+ // Parse a .env file
960
+ const envContent = readFileSync(".env", "utf-8");
961
+ const secrets: Record<string, string> = {};
962
+ for (const line of envContent.split("\n")) {
963
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
964
+ if (match) {
965
+ secrets[match[1]] = match[2];
966
+ }
967
+ }
968
+
969
+ // Push to Envshed
970
+ await client.secrets.set(
971
+ { org: "my-org", project: "my-project", env: "development" },
972
+ secrets,
973
+ );
974
+ ```
975
+
976
+ ### Poll for environment changes
977
+
978
+ ```typescript
979
+ import { EnvshedClient } from "@envshed/node";
980
+
981
+ const client = new EnvshedClient({ token: process.env.ENVSHED_TOKEN! });
982
+ const envPath = { org: "my-org", project: "my-project", env: "production" };
983
+
984
+ let lastEtag: string | undefined;
985
+
986
+ setInterval(async () => {
987
+ const result = await client.version.get(envPath, lastEtag);
988
+
989
+ if (result === null) {
990
+ // No changes
991
+ return;
992
+ }
993
+
994
+ lastEtag = `"v${result.version}"`;
995
+ console.log(`Environment updated to version ${result.version}`);
996
+
997
+ // Re-fetch secrets
998
+ const { secrets } = await client.secrets.get(envPath);
999
+ Object.assign(process.env, secrets);
1000
+ }, 30_000); // Poll every 30 seconds
1001
+ ```
1002
+
1003
+ ### Create a pre-deploy snapshot
1004
+
1005
+ ```typescript
1006
+ import { EnvshedClient } from "@envshed/node";
1007
+
1008
+ const client = new EnvshedClient({ token: process.env.ENVSHED_TOKEN! });
1009
+ const envPath = { org: "my-org", project: "my-project", env: "production" };
1010
+
1011
+ // Snapshot before deploying
1012
+ const snapshot = await client.snapshots.create(envPath, {
1013
+ name: `pre-deploy-${new Date().toISOString()}`,
1014
+ description: "Automatic snapshot before deployment",
1015
+ });
1016
+ console.log(`Snapshot created: ${snapshot.id}`);
1017
+
1018
+ // ... deploy ...
1019
+
1020
+ // If something goes wrong, restore
1021
+ await client.snapshots.restore(envPath, snapshot.id);
1022
+ console.log("Rolled back to pre-deploy state");
1023
+ ```
1024
+
1025
+ ---
1026
+
1027
+ ## TypeScript
1028
+
1029
+ The SDK is written in TypeScript and exports all types. You can import any type you need:
1030
+
1031
+ ```typescript
1032
+ import type {
1033
+ EnvshedClientOptions,
1034
+ RetryOptions,
1035
+ BackoffFunction,
1036
+ ShouldRetryFunction,
1037
+ OnRetryFunction,
1038
+ EnvPath,
1039
+ GetSecretsResponse,
1040
+ SetSecretsResponse,
1041
+ Organization,
1042
+ Project,
1043
+ Environment,
1044
+ SecretVersion,
1045
+ Snapshot,
1046
+ ServiceToken,
1047
+ MeResponse,
1048
+ } from "@envshed/node";
1049
+ ```
1050
+
1051
+ ---
1052
+
1053
+ ## License
1054
+
1055
+ MIT