npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vendure/cloud-config

v0.3.2

Published

The Cloud Config interface a Vendure Cloud application is written against.

Readme

@vendure/cloud-config

The interface package a Vendure Cloud application depends on. It holds the Cloud Config type and the checks that keep the ownership boundary honest, per ADR-0017, ADR-0043, ADR-0044 and ADR-0047.

A customer writes a configuration file and no entrypoint, importing from @vendure/cloud rather than from here:

import { defineCloudConfig, defineVendureExtension } from "@vendure/cloud";
import { DefaultSearchPlugin } from "@vendure/core";
import { ErpPlugin } from "./plugins/erp/erp.plugin.js";

export default defineCloudConfig({
  plugins: [
    DefaultSearchPlugin.init({ bufferUpdates: false }),
    defineVendureExtension({
      id: "acme.erp",
      plugins: [ErpPlugin],
      migrations: ["./migrations/*.js"],
      outboundHosts: ["erp.acme.com"],
    }),
  ],
});

Vendure Extensions

A Vendure Extension is a named group of the customer's own plugins carrying the three pieces of metadata the platform reads (ADR-0047). It is written inline as above, or exported by a package and referenced by name; the wrapper does not distinguish the two, and a third-party extension's author declares its metadata so a consumer does not restate it. That ADR's table records why every other field the original proposal carried was removed, and the input is a function argument so an object literal reintroducing one fails the build.

defineCloudConfig flattens the extensions out of the plugins array before anything else sees it, keeping declaration order, and records which extension declared each plugin. PlatformFieldSetByPluginError reads that map, so a plugin inside an extension that writes a platform-owned field is named together with the extension it came from. Everything downstream — hoistCloudPluginsLast, recordPlatformFieldWrites, the two assertions — operates on the flattened array and needed no change.

A migrations glob resolves against the file that declared the extension, not against the configuration file that references it. defineVendureExtension reads its own call site off the stack for this, which is the one piece of machinery here that is not obvious; the reasoning, including why import.meta.url is not asked for and why the stack has to be read through our own Error.prepareStackTrace, is at the function in src/vendure-extension.ts. test/managed-build.test.ts proves it against compiled output laid out and run the way the managed build produces it, because nothing about the trap survives being tested against this package's own TypeScript sources.

Migrations are resolved into an ordered class list

A glob never reaches TypeORM. The config handle carries loadMigrations(), which expands each extension's globs, imports what they match, and returns the migration classes with the extension that declared each one:

const { loadMigrations } = readCloudConfig(handle);
const loaded = await loadMigrations();
// loaded[n] is { migration, name, timestamp, file, extension }
// dbConnectionOptions.migrations = loaded.map((entry) => entry.migration)

It is a function on the handle rather than a value on it, and it is bound to that handle's own extensions rather than callable with any array. ADR-0048's "Ordering is a property of the array we build" argues both, and is the place to change if either stops being true.

The order is ascending timestamp, then extension declaration order, then class name. TypeORM sorts the set by timestamp and Array.prototype.sort is stable, so equal timestamps run in whatever order the array holds them; today that order comes from glob.sync, which does not sort, so a tie resolves by filesystem walk order and can differ between the customer's machine and the managed build. The last two keys make the tie a value the platform computed. Ordering across extensions is not a contract (ADR-0048): an extension cannot declare that it must migrate after another, and nothing here adds a way to say so.

The ledger identity is TypeORM's own — the migration class name, read off an instance so that a migration declaring name as an instance field is called here what it is called in the ledger. A glob that contributes no migrations raises MigrationGlobMatchedNoMigrationsError naming the extension and the glob as it was written. It distinguishes a glob that matched nothing at all, which is far more often a wrong path than an extension with no migrations, from one whose every match was filtered out as not being a migration file.

Secrets

secret('NAME') reads a customer-supplied value, registers the name, and returns a plain string. Values arrive as ordinary environment variables (ADR-0030), so this is a read helper over process.env rather than a second delivery mechanism.

export default defineCloudConfig({
  plugins: [
    PaymentPlugin.init({ apiKey: secret("STRIPE_SECRET_KEY") }),
  ],
});

It exists because process.env gives the platform no hook. Three things follow from the call that a direct read cannot provide:

  • Every missing name is reported at once. A name that is unset — or set to an empty or whitespace-only value, which counts as unset because an empty credential fails inside a payment call rather than at boot — is recorded rather than thrown on. defineCloudConfig then raises MissingSecretsError naming all of them, so a configuration with four unset secrets takes one deploy to fix rather than four.
  • The value is trimmed, so a credential that picked up a trailing newline from a file-based secret store or a .env loader does not reach the caller intact and fail in the same place an empty one would. Only the ends: a multi-line value such as a PEM key keeps its interior exactly, per ADR-0030's note that customers put private keys in these variables.
  • The declared set is exactly what the code asked for. Registration is a side effect of the call, so it cannot drift from the code the way a hand-written requirements.secrets list would. listDeclaredSecrets() is the entrypoint's accessor after composition.
  • The value can be redacted. redactSecrets(text) replaces every resolved value wherever it appears. A value read straight out of process.env is an ordinary string that nothing downstream can recognise.

The argument has to be a string literal, enforced by the type rather than by a lint rule. Passing a variable makes the parameter type never and fails the build. A literal is what makes the declared set readable by static analysis, and a type constraint needs no tooling in the customer's project.

Reading a secret at request time is not supported

secret() resolves once, while the configuration object is being built. A call made later — inside a request handler, a job processor, or a lazily constructed client — happens after defineCloudConfig has already checked the registry, so a missing value is not reported at boot and is not part of the declared set.

The answer is to read the secret during composition and hold it, which is the natural shape when secrets are plugin options:

// Yes: resolved once, declared, redactable.
PaymentPlugin.init({ apiKey: secret("STRIPE_SECRET_KEY") })

// No: resolved per request, invisible to the missing-secret report.
class PaymentService {
  charge() {
    const key = secret("STRIPE_SECRET_KEY");
  }
}

A plugin that genuinely needs the value later should take it as an option and keep it, rather than reaching for it at the point of use.

Declared outbound hosts

A Vendure Extension declares the hosts it talks to, and installOutboundHostCheck reports a call to a host it did not declare, naming the extension (ADR-0028's amendment).

Vendure Cloud: Extension acme.erp attempted outbound access to api.other-erp.example.
Declared outbound hosts: erp.acme.com

The check runs inside the customer's own process rather than in the network path, and attribution is the reason. A firewall sees a source address and a TLS server name and cannot say which extension made the call, so it cannot produce that message. Reading the stack can.

It is reported and never blocked (CLO-412 records the reasoning). This is a hygiene control over trusted code, not a security boundary — code written to bypass it will succeed, and a compromised dependency can exfiltrate through a declared host anyway. So blocking stops no attacker, and everything it would stop is an honest extension whose declaration is incomplete. Escalating to a block is left available and would need its own stated, announced decision, the same condition ADR-0045 put on its own advisory.

An extension that declares no hosts is not checked. The declaration is the opt-in. Reading "declared nothing" as "may reach nothing" would report against every extension in existence today, and a report that fires on everything is not read. Note that defineVendureExtension normalises an absent outboundHosts and an explicit [] to the same empty array, so "deliberately talks to nothing" cannot currently be expressed distinctly from "did not say".

A call the stack attributes to no extension is not checked either. Vendure core's own traffic, the database driver's, and anything reached from platform code land there, and reporting them would drown the signal.

That is also the one real limit of reading the stack, and it is about where the code is rather than about async boundaries. Attribution needs a frame belonging to the extension somewhere on the stack when the socket opens. An extension that supplies a host as data and lets other code dial it has no such frame, and that call is dropped. Crossing a timer, an await, or a helper the extension did not write does not cause this — the extension's own frame is still further down the same stack, and the suite covers all three.

What it costs, and where it is imprecise

An allowed call costs one Set lookup. The host is compared against the union of every declared host first, and only a host that appears in none of them is attributed, because capturing a stack is the expensive part. The cost of that ordering is that an extension reaching a host another extension declared is not reported.

Interception is globalThis.fetch plus net.Socket.prototype.connect. The socket is where http.request, https.request, net.connect and every client library end up, and — unlike replacing net.connect on the module object — it behaves the same whether the caller is ESM or CommonJS. An ES module's named import is a snapshot taken at link time, so rewriting the module object reaches CommonJS callers only, which would be the same control behaving differently in the two module systems.

Each extension and host pair is reported once per process. An extension polling an undeclared host every second has one thing wrong with it, and the same interception seeing it at both fetch and the socket underneath is not two mistakes.

The boundary, and where each half is enforced

A Cloud Plugin is a plugin Vendure Cloud supplies in place of one the customer would otherwise declare — CloudAssetServerPlugin for AssetServerPlugin, and the three ADR-0044 lists as injected. The term is deliberately not "platform plugin", which reads as a plugin belonging to Vendure Platform.

PLATFORM_OWNED_FIELDS is the single source for all four checks below, so they cannot disagree about which VendureConfig fields Vendure Cloud owns.

| Check | Runs at | Catches | | --- | --- | --- | | The branded Cloud Config type | build | a config literal setting a platform-owned field | | listPlatformOwnedFieldsSetIn | boot | a config assembled programmatically, past the type | | assertPlatformFieldsHold | boot, after composition | a plugin that rewrote a platform-owned value after the Cloud Plugin ran | | assertNoCustomerPluginWroteAPlatformField | boot, after composition | ADR-0044's converse check — a customer plugin whose write a Cloud Plugin overwrote |

Platform-owned keys stay present in the type carrying a brand that explains itself, rather than being removed with Omit. ADR-0043 requires the build-time error to carry a message we wrote, and an omitted key yields TypeScript's own wording instead. The brand's text is derived from PLATFORM_OWNED_FIELDS, so the compile-time and runtime messages are the same string.

Three things worth knowing before changing this package

A customer installs @vendure/cloud and imports from it, not from here. ADR-0045 makes that package a facade whose exact-pinned dependencies are this package and the separately published plugin packages. What it re-exports today and what is still to join it is in packages/cloud/README.md.

This package is a peer dependency of every plugin package, and a direct dependency of nothing but the facade. That is what stops a customer's tree holding two copies of it. Two copies mean instanceof CloudConfigError stops holding across the seam, and a boot failure a caller catches becomes an uncaught one — ADR-0045 argues the case, and the peer dependency is what turns it from a silent boot failure into an install-time resolution error naming both versions. Importing this package directly still works and is supported; it is what a plugin package does.

Nothing published is bundled or minified, which is a decision rather than a property of how the build happens to be written. A bundler is the route back to a second copy of @vendure/core, and a minifier rewrites the class names that CloudConfigError and pluginName read at runtime to build the messages ADR-0044 requires. tsc can do neither, and packages/cloud/test/published-output.test.ts builds every publishable package and asserts it: one emitted file per source file, every dependency still a bare import, every exported class still carrying its own name, and the entry point's import loading no more of the module graph than a stated budget.

Read ADR-0045's packaging decision only. Its delivery decision — that the customer pins these packages in their own lockfile and the platform reports a vulnerable version rather than replacing it — is superseded by ADR-0046. Customers declare these as development dependencies and the Cloud Runtime supplies the production copies, resolved from the @vendure/core version they declared.

It has to stay loadable from CommonJS, and that constrains the code. ADR-0049: this package emits ESM and lists require alongside import in its exports map, pointing at the same file, so a customer whose application emits CommonJS can load it through Node's require(esm). Vendure's own application template emits CommonJS, so that is most customers arriving from a self-hosted store.

The standing cost is that no top-level await may appear anywhere in this package's module graph, ever. require(esm) refuses a graph containing one, at runtime in the customer's container, and nothing here would notice: every suite in this repository loads the package as ESM, where top-level await is legal. test/managed-build.test.ts builds and runs its fixture in both module systems, which is what catches it.

No runtime dependency on @vendure/core. @vendure/core is a peer dependency and is imported for types only. The plugin metadata key core reads is restated as a constant and pinned to core's real value by a test, so a rename there fails our build rather than silently disabling attribution.

Runtime implementation

CLO-355 supplies the platform-owned server, worker and migrate entrypoints, managed Postgres connection with rotating IAM credentials, structured logger, default cache/job queue/scheduler, shallow health check, one-copy core guard and local substitutes. The managed build typechecks the Cloud Config and installs the private implementation beside the single supplied copy of @vendure/core.

The four wrapper plugins remain separate delivery work: CLO-399, CLO-400, CLO-401 and CLO-402. Extension migration globs are resolved into the managed database configuration at boot. The ownership snapshot on the Deployment row and the migration transaction, timeout and orphaned-schema controls remain CLO-423 and CLO-424.

apps/demo-store is the reference Cloud Config: it contains only vendure-config.ts, with no customer-owned run-mode entrypoints.