Getting started

Changelog

Version history for @goenvless/sdk.

Version history for @goenvless/sdk, the TypeScript client for the Envless API. Pin it for the latest coverage, security patches and type fixes.

Looking for the runtime package that loads variables into your app? @goenvless/env has its own changelog. The Python and Ruby SDKs, both envless-sdk, and the PHP SDK, envless/sdk, have their own as well: Python, Ruby and PHP.

Unreleased

Next release

Each list method takes only the options its route reads. The shared ListOptions used to offer search, order and product on every list, and most routes ignored some of them. Every list now has its own options type, and the parity check compares each one with the query parameters of its route on every build.

  • members.list() gains roleId, and roles.list() gains sortBy (name, updated or created)
  • ListOptions is now just limit, offset and signal. SearchListOptions, SortedListOptions, ScopedListOptions, ScopedSortedListOptions, RoleListOptions and MemberListOptions cover the rest, and DeliveryListOptions.status is typed
  • Passing an option a route ignores is now a type error. Drop it: it never changed the result
  • variables.getById(), updateById(), deleteById(), versions.get() and versions.rollback() take RequestScope and no longer send the product they ignored

isSecret is part of VariableInput and VariablePatch. The API has always accepted it on variables.create(), createMany(), update() and updateById(); the types now say so. ApplyUpdateEntry gains name, which renames a variable inside variables.apply().

No change to how the client behaves. Every method, type and error is the same as in 0.0.2. This release moves the links the package carries to their new home.

  • The one-time update notice now points at https://envless.cloud/docs/sdk/changelog
  • The README links to the dashboard at envless.cloud/dashboard and to the docs at envless.cloud/docs

Documentation corrections. Several method pages now describe the plan gates the API enforces.

  • On the Free plan versions.list(), listAll() and iterate() return only the latest version, and versions.get() on an older id or versions.rollback() fails with 402 plan.upgrade_required. Publishing works on every plan
  • On the Free plan variables.listHistory(), listAllHistory() and iterateHistory() return only the latest event. The API's historyLimited and hiddenCount flags are not surfaced by the SDK yet, so a single event on Free is the plan, not an empty history
  • members.invite() names the seat limits by plan: 1 member on Free and Dev, 15 on Team, no limit on Enterprise
  • workspace.delete() no longer lists a live subscription as a guard. The subscription is canceled as part of the delete

Deleting a product now deletes everything under it. products.delete() no longer refuses a product that still owns projects. The projects, their environments, every variable in them and every per-product access grant go in one transaction, so the 409 product.has_projects branch and the empty-first rule are gone from the API and from EnvlessApiError's vocabulary. projects.delete() and environments.delete() cascade the same way.

The tombstones report what went. DeletedProductResource gained deletedProjects, deletedEnvironments and deletedVariables; DeletedProjectResource gained deletedEnvironments and deletedVariables; DeletedEnvironmentResource gained deletedVariables. Purely additive for readers, but the fields are required, so any fixture or mock that constructs one of these types by hand needs updating.

  • Version snapshots are neither deleted nor counted. They stop resolving once their environment is gone
  • DELETE is still retried on a 408, a 429 or a 5xx, a gateway timeout included, and the cascade may commit before a lost response, so a retry answering product.not_found most likely means the first attempt succeeded
  • Creating or duplicating a child while its parent is being deleted now fails with a 409 carrying product.not_found, project.not_found or environment.not_found rather than leaving an orphan behind

Variable names are checked before the request leaves your machine. Every write that carries a name, create, createMany, update, updateById and the creates in apply, now validates it locally against the rule the API enforces: 1 to 100 characters of letters, digits and underscores, never starting with a digit. A bad name costs no round trip.

  • The refusal rejects the returned promise with an EnvlessApiError carrying status: 400, code: 'request.body_invalid' and field: 'name', the same vocabulary the API answers with. requestId is undefined, which is how you tell a local refusal from a server one
  • It rejects, it does not throw synchronously, so .catch() and try/await both work the way they do for every other failure in this client
  • createMany refuses the whole batch on one unusable name rather than reporting it in skipped, so a name that would have been skipped is now a rejection you see immediately
  • New export variableNameValidation(name) returns { valid, error } without throwing, for pre-checking a name in your own UI or importer

Variable names are case reserved, not case normalised. A name is stored and returned with exactly the casing you send, but uniqueness and lookup both ignore case. envless.variables.get('api', 'production', 'api_key') resolves a live API_KEY, and creating api_key alongside it is 409 variable.name_conflict rather than a second variable. The same holds for update, delete, deleteMany and the history routes.

Actor attribution now names the API key. VariableHistoryEvent.actor, VersionResource.createdBy and the actor on a verified WebhookEvent share one ActorReference shape and gained a key field, typed ActorKeyReference | null, carrying the key's id, name and prefix. A write made by an API key is attributed to the key rather than to whoever happens to own it.

  • Additive for readers, but the field is required on the actor object, so any fixture or mock that builds one of these by hand needs updating
  • ActorReference and ActorKeyReference are exported types

Documentation corrections. Several method pages described behaviour the API does not have. variables.createMany() does not resolve with count: 0 when everything is refused, it rejects with a 400; a total failure is not a 500. Name lookups were documented as case sensitive and are not. keys.list() is refused outright for a key fenced to specific projects rather than returning the full list. webhooks.update()'s example used an event type that does not exist, version.published, in place of variable_version.created. The webhook retry schedule is five attempts at 30s, 2m, 10m then 30m, not a sixth at 1h.

Initial release. A standalone, zero-dependency TypeScript client covering 100% of the public HTTP API: 113 methods across 13 namespaces, over all 80 documented operations.

Client

  • init({ token?, ... }) configures the SDK once at startup, and envless is a live handle you can import anywhere without threading an instance through your app. Nothing is resolved until the first method call, so import order does not matter
  • init is optional: with ENVLESS_TOKEN set, the first call on envless builds the client itself
  • getClient() returns the real default instance, and resetClient() drops it, which is what test teardown wants
  • createClient({ token?, baseUrl?, maxRetries?, timeoutMs?, userAgent?, fetch? }) for an explicit instance, or several at once when you need more than one workspace
  • new Envless({ token, ... }) reads no environment at all, for Workers and bundles without process
  • Namespaces: me, workspace (with workspace.domain), workspaces, products (with products.access), projects, environments, variables, versions, members, invites, roles, keys, webhooks
  • raw.request(path, options) as an escape hatch that keeps envelope unwrapping, the retry policy and the typed errors
  • Every method accepts options.signal for cancellation

Pagination

  • Every collection has three methods: list for one page, listAll for everything, iterate for a lazy AsyncGenerator
  • Secondary collections get their own trio: products.listProjects, products.access.list, variables.listHistory, versions.listEntries, webhooks.listDeliveries, each with listAll* and iterate* forms
  • The walk terminates on hasMore: false or an empty page, and advances by items actually received rather than by limit
  • roles.listPermissions and webhooks.listEventTypes are deliberately not paginated

Errors

  • EnvlessApiError carries the stable code plus status, resource, field, requestId and retryAfterSeconds, with getters for the common branches: isAuth, isScopeMissing, isValidation, needsUpgrade, isNotFound, isConflict, isRateLimited
  • EnvlessNetworkError for transport failures, timeouts and aborts
  • Retries GET, HEAD, PUT and DELETE on 408, 429 and 5xx with exponential backoff capped at 30 seconds, honouring Retry-After. POST and PATCH are never retried, so a write that timed out is never silently duplicated

Encryption

  • encryptValue / decryptValue against the workspace key, byte-for-byte compatible with the @goenvless/cli binary, the dashboard and the @goenvless/env runtime
  • decryptValue reads the PBKDF2 iteration count out of a v2:<iterations>: envelope and derives at that cost, rather than assuming the 200,000 default. A malformed count is refused instead of being silently retried at the default
  • deriveWorkspaceKey to derive once and reuse across a batch, instead of paying 200,000 PBKDF2 iterations per variable
  • importWorkspaceKey for a raw base64url AES-256 machine key
  • isCiphertext and ENCRYPTION_PARAMETERS
  • rotateWorkspacePassphrase(...) walks live variables and version entries, in batches of 100. Rotating only the live values silently breaks every later rollback, because history stays on the old key

Webhooks

  • verifyWebhookSignature({ payload, headers, secret, toleranceSeconds? }) verifies the HMAC-SHA256 signature over id.timestamp.body, checks timestamp drift, and returns the parsed event
  • Accepts a Headers instance or a plain object, matching header names case-insensitively, and tolerates a space-separated list of signatures
  • Comparison is constant-time, and it uses Web Crypto only so it runs at the edge

Runtime

  • No dependencies, and no Node built-ins. Base64 goes through btoa/atob rather than Buffer, and cryptography through crypto.subtle
  • Runs on Node 20+, Bun, Deno and Cloudflare Workers
  • ESM and CommonJS builds with per-condition .d.ts and .d.cts, and sideEffects: false so bundlers can drop unused namespaces

Verification

  • A parity check cross-references the method manifest against openapi.json in both directions and fails the build on any gap, mismatched operationId, or mismatched scope declaration