@vendure/cloud-config 0.4.0 → 0.5.0-dev.20261016123507.ge07a06e
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 +13 -4
- package/dist/cloud-asset-server-plugin.d.ts.map +1 -1
- package/dist/cloud-asset-server-plugin.js +22 -38
- package/dist/cloud-asset-server-plugin.js.map +1 -1
- package/dist/cloud-config.d.ts.map +1 -1
- package/dist/cloud-dashboard-plugin.d.ts.map +1 -1
- package/dist/cloud-dashboard-plugin.js +20 -36
- package/dist/cloud-dashboard-plugin.js.map +1 -1
- package/dist/cloud-plugin-declaration.d.ts.map +1 -1
- package/dist/cloud-plugin-declaration.js +18 -23
- package/dist/cloud-plugin-declaration.js.map +1 -1
- package/dist/composition.d.ts.map +1 -1
- package/dist/composition.js +22 -32
- package/dist/composition.js.map +1 -1
- package/dist/define-cloud-config.d.ts.map +1 -1
- package/dist/define-cloud-config.js +12 -17
- package/dist/define-cloud-config.js.map +1 -1
- package/dist/durable-workflow-reads.d.ts +20 -0
- package/dist/durable-workflow-reads.d.ts.map +1 -0
- package/dist/durable-workflow-reads.js +57 -0
- package/dist/durable-workflow-reads.js.map +1 -0
- package/dist/durable-workflows.d.ts +74 -0
- package/dist/durable-workflows.d.ts.map +1 -0
- package/dist/durable-workflows.js +102 -0
- package/dist/durable-workflows.js.map +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -6
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +70 -104
- package/dist/migrations.js.map +1 -1
- package/dist/outbound-hosts.d.ts +6 -0
- package/dist/outbound-hosts.d.ts.map +1 -1
- package/dist/outbound-hosts.js +114 -55
- package/dist/outbound-hosts.js.map +1 -1
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +6 -7
- package/dist/path.js.map +1 -1
- package/dist/platform-capabilities.d.ts.map +1 -1
- package/dist/platform-capabilities.js +12 -18
- package/dist/platform-capabilities.js.map +1 -1
- package/dist/platform-fields.d.ts.map +1 -1
- package/dist/platform-fields.js +10 -15
- package/dist/platform-fields.js.map +1 -1
- package/dist/plugin-attribution.d.ts.map +1 -1
- package/dist/plugin-attribution.js +22 -31
- package/dist/plugin-attribution.js.map +1 -1
- package/dist/runtime-backstop.d.ts.map +1 -1
- package/dist/runtime-backstop.js +2 -5
- package/dist/runtime-backstop.js.map +1 -1
- package/dist/secrets.d.ts.map +1 -1
- package/dist/secrets.js +20 -50
- package/dist/secrets.js.map +1 -1
- package/dist/stack.d.ts.map +1 -1
- package/dist/stack.js +12 -21
- package/dist/stack.js.map +1 -1
- package/dist/superseded-plugins.d.ts.map +1 -1
- package/dist/superseded-plugins.js +4 -7
- package/dist/superseded-plugins.js.map +1 -1
- package/dist/vendure-extension.d.ts +2 -0
- package/dist/vendure-extension.d.ts.map +1 -1
- package/dist/vendure-extension.js +53 -34
- package/dist/vendure-extension.js.map +1 -1
- package/package.json +10 -3
- package/dist/tenant-workflows.d.ts +0 -44
- package/dist/tenant-workflows.d.ts.map +0 -1
- package/dist/tenant-workflows.js +0 -219
- package/dist/tenant-workflows.js.map +0 -1
package/dist/migrations.js
CHANGED
|
@@ -1,29 +1,24 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { extname } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// `fast-glob` or `glob` for size — three packages against roughly ten. It also extends the
|
|
10
|
-
// module graph ADR-0049 bars top-level await from, which the CommonJS half of
|
|
11
|
-
// `managed-build.test.ts` is what checks.
|
|
4
|
+
// Node's `fs.glob` is absent from Node 20, which `SUPPORTED_NODE_MAJORS` offers and `engines`
|
|
5
|
+
// allows. A hand-rolled matcher that misses a file means a migration never runs, which shows up as
|
|
6
|
+
// schema drift and not as an error. tinyglobby over `fast-glob` or `glob` for size: three packages
|
|
7
|
+
// against roughly ten. It extends the module graph ADR-0049 bars top-level await from, which the
|
|
8
|
+
// CommonJS half of `managed-build.test.ts` checks.
|
|
12
9
|
import { glob } from "tinyglobby";
|
|
13
10
|
import { CloudConfigError } from "./errors.js";
|
|
14
11
|
import { resolveMigrationGlob } from "./vendure-extension.js";
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// package stay single template literals rather than being converted to match.
|
|
12
|
+
// A message long enough to exceed the emitted-line cap in `published-output.test.ts`, which reads a
|
|
13
|
+
// long line as evidence of minification, is written as one entry per line and joined, because
|
|
14
|
+
// biome's `useTemplate` refuses string concatenation. The trailing empty entry is the closing
|
|
15
|
+
// newline.
|
|
20
16
|
function message(lines) {
|
|
21
17
|
return [...lines, ""].join("\n");
|
|
22
18
|
}
|
|
23
|
-
// Raised for a glob that contributed no migrations
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// nothing" in the second case sends them looking for a typo that is not there.
|
|
19
|
+
// Raised for a glob that contributed no migrations. A glob matching nothing is a wrong path; a glob
|
|
20
|
+
// matching files none of which is a migration is too wide, and saying "it matches nothing" there
|
|
21
|
+
// sends the customer looking for a typo.
|
|
27
22
|
export class MigrationGlobMatchedNoMigrationsError extends CloudConfigError {
|
|
28
23
|
extensionId;
|
|
29
24
|
pattern;
|
|
@@ -51,18 +46,15 @@ export class MigrationGlobMatchedNoMigrationsError extends CloudConfigError {
|
|
|
51
46
|
this.matchedFiles = matchedFiles;
|
|
52
47
|
}
|
|
53
48
|
}
|
|
54
|
-
// Capped
|
|
55
|
-
// an error message that prints all of them is not one anybody reads.
|
|
49
|
+
// Capped because a wide glob over a build directory can match a great many files.
|
|
56
50
|
function describeFiles(files) {
|
|
57
51
|
const shown = files.slice(0, 3).join(", ");
|
|
58
52
|
return files.length > 3 ? `${shown}, and ${files.length - 3} more` : shown;
|
|
59
53
|
}
|
|
60
|
-
// `isMigrationClass`
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
// that reaches the customer raw, naming neither the extension nor the file, which is the one
|
|
65
|
-
// failure path here that had no message of its own.
|
|
54
|
+
// `isMigrationClass` accepts every exported function, as TypeORM does, but not every function can
|
|
55
|
+
// be constructed. An exported arrow function, such as a shared helper left in a migrations
|
|
56
|
+
// directory, throws `TypeError: X is not a constructor`, which without this error names neither the
|
|
57
|
+
// extension nor the file.
|
|
66
58
|
export class MigrationNotConstructibleError extends CloudConfigError {
|
|
67
59
|
extensionId;
|
|
68
60
|
exportName;
|
|
@@ -93,34 +85,28 @@ export class InvalidMigrationNameError extends CloudConfigError {
|
|
|
93
85
|
this.migrationName = migrationName;
|
|
94
86
|
}
|
|
95
87
|
}
|
|
96
|
-
// TypeORM's
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
// `./migrations/*` would otherwise have us importing the source map beside each migration.
|
|
88
|
+
// TypeORM's list from `importClassesFromDirectories` in 0.3.30: matches are filtered to these
|
|
89
|
+
// suffixes before import. The glob is resolved here and never reaches that function, so without the
|
|
90
|
+
// filter `./migrations/*` would import the source map beside each migration.
|
|
100
91
|
const MIGRATION_FILE_SUFFIXES = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"];
|
|
101
|
-
// TypeORM drops `.d.ts`
|
|
102
|
-
// never a migration whichever suffix it carries, so all three are dropped here.
|
|
92
|
+
// TypeORM drops `.d.ts` only. A declaration file is never a migration, so all three are dropped.
|
|
103
93
|
const DECLARATION_FILE_SUFFIXES = [".d.ts", ".d.mts", ".d.cts"];
|
|
104
|
-
// The suffixes above that need a compiler
|
|
94
|
+
// The suffixes above that need a compiler to be read. See `importMigrationFile`.
|
|
105
95
|
const TYPESCRIPT_FILE_SUFFIXES = [".ts", ".mts", ".cts"];
|
|
106
96
|
function isMigrationFile(file) {
|
|
107
97
|
if (DECLARATION_FILE_SUFFIXES.some((suffix) => file.endsWith(suffix)))
|
|
108
98
|
return false;
|
|
109
99
|
return MIGRATION_FILE_SUFFIXES.some((suffix) => file.endsWith(suffix));
|
|
110
100
|
}
|
|
111
|
-
//
|
|
101
|
+
// A managed build matches emitted JavaScript and `import()` is enough. Running from source needs
|
|
102
|
+
// more: `vendure dev` starts its processes through the project's TypeScript compiler, which
|
|
103
|
+
// registers against the CommonJS loader, and a dynamic `import()` goes to the ESM loader and never
|
|
104
|
+
// reaches it. Node's type stripping covers that only from 22.18, and this package supports 20.19
|
|
105
|
+
// and 22.12, so in between the migration chain would be unreadable and the schema silently not
|
|
106
|
+
// built.
|
|
112
107
|
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
// project's TypeScript compiler, which registers itself against the CommonJS loader, and a dynamic
|
|
116
|
-
// `import()` goes to the ESM loader instead and never reaches it. Node's own type stripping covers
|
|
117
|
-
// that gap only from 22.18, and this package supports 20.19 and 22.12, so on the versions in
|
|
118
|
-
// between the migration chain would be unreadable and the schema would silently not be built.
|
|
119
|
-
//
|
|
120
|
-
// The hook is looked up rather than tried, so a migration that throws while it loads reports its
|
|
121
|
-
// own error once instead of being run twice and reported as a resolution failure. Only TypeScript
|
|
122
|
-
// takes this route: emitted JavaScript is loaded exactly as it was before, because that is the path
|
|
123
|
-
// a deploy uses and nothing about it needed changing.
|
|
108
|
+
// The hook is looked up and not tried, so a migration that throws while loading reports its own
|
|
109
|
+
// error once instead of running twice and being reported as a resolution failure.
|
|
124
110
|
async function importMigrationFile(file) {
|
|
125
111
|
const suffix = extname(file);
|
|
126
112
|
if (TYPESCRIPT_FILE_SUFFIXES.includes(suffix)) {
|
|
@@ -131,21 +117,17 @@ async function importMigrationFile(file) {
|
|
|
131
117
|
}
|
|
132
118
|
return import(pathToFileURL(file).href);
|
|
133
119
|
}
|
|
134
|
-
// Every function is taken,
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
// `function` declaration constructs and fails `InvalidMigrationNameError` on its name, and an
|
|
138
|
-
// arrow function cannot be constructed at all and fails `MigrationNotConstructibleError`.
|
|
120
|
+
// Every function is taken, as TypeORM does. A helper exported from a migrations file is read as a
|
|
121
|
+
// migration: a `function` declaration constructs and fails `InvalidMigrationNameError` on its name,
|
|
122
|
+
// and an arrow function fails `MigrationNotConstructibleError`.
|
|
139
123
|
function isMigrationClass(value) {
|
|
140
124
|
return typeof value === "function";
|
|
141
125
|
}
|
|
142
|
-
// The walk `importClassesFromDirectories` does over what a migration file exported.
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
// collapses that second sighting, and terminates on an export that refers to itself, which
|
|
148
|
-
// TypeORM's version does not.
|
|
126
|
+
// The walk `importClassesFromDirectories` does over what a migration file exported. A file emitted
|
|
127
|
+
// as CommonJS is seen through `import()` as a namespace whose `default` is the whole
|
|
128
|
+
// `module.exports`, so its classes sit one level down and are reachable twice, through `default`
|
|
129
|
+
// and through the named re-export Node synthesizes. `seen` collapses the second sighting and
|
|
130
|
+
// terminates on an export that refers to itself, which TypeORM's version does not.
|
|
149
131
|
function collectMigrationClasses(exported, seen, into) {
|
|
150
132
|
if (isMigrationClass(exported)) {
|
|
151
133
|
if (seen.has(exported))
|
|
@@ -157,24 +139,20 @@ function collectMigrationClasses(exported, seen, into) {
|
|
|
157
139
|
if (typeof exported !== "object" || exported === null || seen.has(exported))
|
|
158
140
|
return;
|
|
159
141
|
seen.add(exported);
|
|
160
|
-
//
|
|
161
|
-
// read without a branch of its own.
|
|
142
|
+
// `Object.values` yields array elements too, so `export default [A, B]` needs no separate branch.
|
|
162
143
|
for (const value of Object.values(exported)) {
|
|
163
144
|
collectMigrationClasses(value, seen, into);
|
|
164
145
|
}
|
|
165
146
|
}
|
|
166
|
-
// `MigrationExecutor.getMigrations` reads `migration.name || migration.constructor.name` from
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
// every migration that does not declare `name`, and a different one for every migration that
|
|
171
|
-
// does, and in that case the ledger, this order and the ownership snapshot would disagree
|
|
147
|
+
// `MigrationExecutor.getMigrations` reads `migration.name || migration.constructor.name` from an
|
|
148
|
+
// instance, since `DataSource` constructs every migration class first, and parses the timestamp
|
|
149
|
+
// from the last 13 characters. Both are mirrored: the static class name differs for a migration
|
|
150
|
+
// that declares `name`, and the ledger, this order and the ownership snapshot would then disagree
|
|
172
151
|
// about what the migration is called.
|
|
173
152
|
//
|
|
174
|
-
// This is a second construction
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
// and the alternative is an identity that can disagree with the ledger, which is worse.
|
|
153
|
+
// This is a second construction: TypeORM builds its own instance when the DataSource starts, so a
|
|
154
|
+
// constructor with side effects runs twice. Migration constructors are empty in everything
|
|
155
|
+
// Vendure's tooling generates.
|
|
178
156
|
function identify(migration, extension, file) {
|
|
179
157
|
let instance;
|
|
180
158
|
try {
|
|
@@ -185,9 +163,8 @@ function identify(migration, extension, file) {
|
|
|
185
163
|
}
|
|
186
164
|
const name = instance.name || instance.constructor.name;
|
|
187
165
|
const timestamp = Number.parseInt(name.slice(-13), 10);
|
|
188
|
-
// TypeORM's guard is `!timestamp || isNaN(timestamp)
|
|
189
|
-
//
|
|
190
|
-
// name cannot contain a minus sign for `parseInt` to read.
|
|
166
|
+
// TypeORM's guard is `!timestamp || isNaN(timestamp)`. `<= 0` covers the same cases, since a
|
|
167
|
+
// class name cannot contain a minus sign.
|
|
191
168
|
if (Number.isNaN(timestamp) || timestamp <= 0) {
|
|
192
169
|
throw new InvalidMigrationNameError(extension.id, name, file);
|
|
193
170
|
}
|
|
@@ -199,23 +176,19 @@ async function loadOneExtension(extension) {
|
|
|
199
176
|
const seenExports = new Set();
|
|
200
177
|
for (const pattern of extension.migrations) {
|
|
201
178
|
const resolved = resolveMigrationGlob(extension, pattern);
|
|
202
|
-
// `expandDirectories` off
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
// on the order the directory happened to be walked in. The sort that decides execution is
|
|
206
|
-
// the one in `loadExtensionMigrations`; this one only keeps the steps before it
|
|
207
|
-
// reproducible.
|
|
179
|
+
// `expandDirectories` is off so a glob means what the customer wrote and does not gain a
|
|
180
|
+
// `/**/*` when it names a directory. Sorted so import order and error messages do not depend on
|
|
181
|
+
// directory walk order; the sort that decides execution is in `loadExtensionMigrations`.
|
|
208
182
|
const matched = (await glob(resolved, { absolute: true, onlyFiles: true, expandDirectories: false })).sort();
|
|
209
183
|
const files = matched.filter(isMigrationFile);
|
|
210
|
-
// `matched
|
|
211
|
-
//
|
|
184
|
+
// `matched`, not `files`, so the message can tell a wrong path from a glob whose matches were
|
|
185
|
+
// all filtered out.
|
|
212
186
|
if (files.length === 0) {
|
|
213
187
|
throw new MigrationGlobMatchedNoMigrationsError(extension.id, pattern, resolved, matched);
|
|
214
188
|
}
|
|
215
189
|
for (const file of files) {
|
|
216
|
-
// A second glob in the same extension matching
|
|
217
|
-
//
|
|
218
|
-
// twice, which is a real ownership collision rather than an overlap, and is left for
|
|
190
|
+
// A second glob in the same extension matching an already matched file adds nothing. Across
|
|
191
|
+
// extensions the file is imported once but claimed twice, a real ownership collision left for
|
|
219
192
|
// TypeORM to reject by name.
|
|
220
193
|
if (importedFiles.has(file))
|
|
221
194
|
continue;
|
|
@@ -231,21 +204,18 @@ async function loadOneExtension(extension) {
|
|
|
231
204
|
}
|
|
232
205
|
return loaded;
|
|
233
206
|
}
|
|
234
|
-
// ADR-0048 part 2. TypeORM sorts the whole
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
// customer's machine and the managed build. The order below is a value computed here instead.
|
|
207
|
+
// ADR-0048 part 2. TypeORM sorts the whole set by timestamp and `Array.prototype.sort` is stable,
|
|
208
|
+
// so migrations with equal timestamps run in the order of the array handed over. That array comes
|
|
209
|
+
// from `glob.sync`, which does not sort, so a tie resolves by filesystem walk order and can differ
|
|
210
|
+
// between the customer's machine and the managed build. The order below is computed here.
|
|
239
211
|
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
// everywhere, not so a dependency can be expressed.
|
|
212
|
+
// Ordering across extensions is not a contract. An extension cannot declare that it migrates after
|
|
213
|
+
// another; extensions own their tables and the coupling is unsupported. The keys only make a tie
|
|
214
|
+
// resolve the same way everywhere.
|
|
244
215
|
export async function loadExtensionMigrations(extensions) {
|
|
245
216
|
const found = [];
|
|
246
|
-
// Serial
|
|
247
|
-
//
|
|
248
|
-
// declaration order before the tie-break below could read it.
|
|
217
|
+
// Serial: the work is a handful of directory reads, and a concurrent version would have to sort
|
|
218
|
+
// its results back into declaration order before the tie-break below.
|
|
249
219
|
for (const [declarationOrder, extension] of extensions.entries()) {
|
|
250
220
|
for (const loaded of await loadOneExtension(extension)) {
|
|
251
221
|
found.push({ loaded, declarationOrder });
|
|
@@ -255,19 +225,15 @@ export async function loadExtensionMigrations(extensions) {
|
|
|
255
225
|
if (a.loaded.timestamp !== b.loaded.timestamp) {
|
|
256
226
|
return a.loaded.timestamp - b.loaded.timestamp;
|
|
257
227
|
}
|
|
258
|
-
// Not load-bearing as
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
// we computed rather than one we inherited, and because loading the extensions
|
|
262
|
-
// concurrently — the obvious future change to the loop above — would take the input order
|
|
263
|
-
// away without any test noticing.
|
|
228
|
+
// Not load-bearing as written: entries are built in declaration order and `sort` is stable. It
|
|
229
|
+
// is explicit because ADR-0048 makes the order a computed value, and loading extensions
|
|
230
|
+
// concurrently would remove the input order without any test noticing.
|
|
264
231
|
if (a.declarationOrder !== b.declarationOrder) {
|
|
265
232
|
return a.declarationOrder - b.declarationOrder;
|
|
266
233
|
}
|
|
267
|
-
// Codepoint comparison
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
// with one ledger identity, which TypeORM's own duplicate check rejects by name.
|
|
234
|
+
// Codepoint comparison, because `localeCompare` reads the runtime's locale and would order the
|
|
235
|
+
// same two migrations differently on two machines. A tie surviving all three keys is two
|
|
236
|
+
// migrations with one ledger identity, which TypeORM's duplicate check rejects by name.
|
|
271
237
|
if (a.loaded.name < b.loaded.name)
|
|
272
238
|
return -1;
|
|
273
239
|
return a.loaded.name > b.loaded.name ? 1 : 0;
|
package/dist/migrations.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,
|
|
1
|
+
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"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"}
|
package/dist/outbound-hosts.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ export interface OutboundHostCheckOptions {
|
|
|
10
10
|
readonly extensions: readonly VendureExtension[];
|
|
11
11
|
readonly report?: UndeclaredHostReporter;
|
|
12
12
|
}
|
|
13
|
+
export type ParsedOutboundHost = {
|
|
14
|
+
readonly host: string;
|
|
15
|
+
} | {
|
|
16
|
+
readonly problem: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function parseOutboundHost(value: unknown): ParsedOutboundHost;
|
|
13
19
|
type Uninstall = () => void;
|
|
14
20
|
export declare function installOutboundHostCheck(options: OutboundHostCheckOptions): Uninstall;
|
|
15
21
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"outbound-hosts.d.ts","sourceRoot":"","sources":["../src/outbound-hosts.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"outbound-hosts.d.ts","sourceRoot":"","sources":["../src/outbound-hosts.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAW/D,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,sBAAsB,GAAG,CAAC,MAAM,EAAE,oBAAoB,KAAK,IAAI,CAAC;AAE5E,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,UAAU,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAGjD,QAAQ,CAAC,MAAM,CAAC,EAAE,sBAAsB,CAAC;CAC1C;AAwCD,MAAM,MAAM,kBAAkB,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAM1F,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,CA8BpE;AAoJD,KAAK,SAAS,GAAG,MAAM,IAAI,CAAC;AAO5B,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,wBAAwB,GAAG,SAAS,CAqErF"}
|
package/dist/outbound-hosts.js
CHANGED
|
@@ -1,25 +1,91 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
+
import { isIP } from "node:net";
|
|
2
3
|
import { dirname, sep } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { domainToASCII, fileURLToPath } from "node:url";
|
|
4
5
|
import { stackFrameFiles } from "./stack.js";
|
|
5
|
-
// A hostname as declared and as dialled, compared the way DNS does: case-insensitively,
|
|
6
|
-
// ignoring the trailing dot of a fully qualified name. Matching is exact
|
|
7
|
-
// wildcard
|
|
8
|
-
//
|
|
6
|
+
// A hostname as declared and as dialled, compared the way DNS does: case-insensitively, in
|
|
7
|
+
// Punycode, and ignoring the trailing dot of a fully qualified name. Matching is exact, with no
|
|
8
|
+
// wildcard or implied subdomain, so declaring "api.acme.com" does not allow "evil.api.acme.com".
|
|
9
|
+
//
|
|
10
|
+
// Declared and dialled hosts both pass through `asciiHost`, so the two cannot drift apart. What is
|
|
11
|
+
// not a domain, such as an IPv6 address, is compared lowercased.
|
|
9
12
|
function normalizeHost(host) {
|
|
10
|
-
return host.trim().
|
|
13
|
+
return asciiHost(host) || host.trim().replace(/\.$/, "").toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
// `domainToASCII` applies the WHATWG host parser, which `new URL(...).hostname` also uses, so a
|
|
16
|
+
// host from request options compares equal to the same host taken from a URL. It lowercases the
|
|
17
|
+
// name and converts an internationalised one to Punycode. It rewrites a numeric IPv4 form such as
|
|
18
|
+
// 2130706433 as an address, and returns an empty string for what is not a domain, including a name
|
|
19
|
+
// ending in a numeric label. One trailing dot is dropped because it spells the same fully
|
|
20
|
+
// qualified name.
|
|
21
|
+
function asciiHost(host) {
|
|
22
|
+
return domainToASCII(host.trim().replace(/\.$/, ""));
|
|
23
|
+
}
|
|
24
|
+
// Names of a local or private network rather than a public service: RFC 6761 (localhost),
|
|
25
|
+
// RFC 6762 (local, and the private-use names in its appendix G), RFC 8375 (home.arpa). `internal`
|
|
26
|
+
// also covers the cloud metadata names metadata.google.internal and *.ec2.internal.
|
|
27
|
+
const NON_PUBLIC_SUFFIXES = [
|
|
28
|
+
"localhost",
|
|
29
|
+
"localdomain",
|
|
30
|
+
"local",
|
|
31
|
+
"internal",
|
|
32
|
+
"intranet",
|
|
33
|
+
"private",
|
|
34
|
+
"corp",
|
|
35
|
+
"home",
|
|
36
|
+
"lan",
|
|
37
|
+
"home.arpa",
|
|
38
|
+
];
|
|
39
|
+
const DNS_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
40
|
+
// A declaration as the check compares it, or why it is not a public hostname. Syntax only: a public
|
|
41
|
+
// name can still resolve to a private address, and nothing here resolves it.
|
|
42
|
+
//
|
|
43
|
+
// A problem never quotes the value, because a URL pasted into the list can carry credentials.
|
|
44
|
+
export function parseOutboundHost(value) {
|
|
45
|
+
if (typeof value !== "string")
|
|
46
|
+
return { problem: "is not a string" };
|
|
47
|
+
const trimmed = value.trim();
|
|
48
|
+
if (trimmed === "")
|
|
49
|
+
return { problem: "is empty" };
|
|
50
|
+
if (trimmed.includes("://"))
|
|
51
|
+
return { problem: "is a URL. Declare only its hostname" };
|
|
52
|
+
if (trimmed.includes("@")) {
|
|
53
|
+
return { problem: "contains a user name or credentials. Declare only the hostname" };
|
|
54
|
+
}
|
|
55
|
+
if (/[/?#]/.test(trimmed))
|
|
56
|
+
return { problem: "contains a path. Declare only the hostname" };
|
|
57
|
+
if (isIP(trimmed.replace(/^\[(.*)\]$/, "$1")) !== 0) {
|
|
58
|
+
return { problem: "is an IP address. Declare the service's public hostname instead" };
|
|
59
|
+
}
|
|
60
|
+
if (trimmed.includes(":"))
|
|
61
|
+
return { problem: "contains a port. Declare only the hostname" };
|
|
62
|
+
if (trimmed.includes("*")) {
|
|
63
|
+
return { problem: "is a wildcard. Matching is exact, so declare each hostname" };
|
|
64
|
+
}
|
|
65
|
+
const host = asciiHost(trimmed);
|
|
66
|
+
if (host !== "" && isIP(host) !== 0) {
|
|
67
|
+
return { problem: "is an IP address. Declare the service's public hostname instead" };
|
|
68
|
+
}
|
|
69
|
+
const labels = host.split(".");
|
|
70
|
+
if (host === "" || host.length > 253 || !labels.every((label) => DNS_LABEL.test(label))) {
|
|
71
|
+
return { problem: "is not a valid hostname" };
|
|
72
|
+
}
|
|
73
|
+
if (labels.length < 2)
|
|
74
|
+
return { problem: "is not a fully qualified public hostname" };
|
|
75
|
+
if (NON_PUBLIC_SUFFIXES.some((suffix) => host === suffix || host.endsWith(`.${suffix}`))) {
|
|
76
|
+
return { problem: "names a local or private network, not a public service" };
|
|
77
|
+
}
|
|
78
|
+
return { host };
|
|
11
79
|
}
|
|
12
|
-
// How far down the stack to look for an extension. A
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// indefinitely, because reading a deep stack is the expensive part of this check.
|
|
80
|
+
// How far down the stack to look for an extension. A client library adds frames between a plugin
|
|
81
|
+
// and the call, and 30 covers the cases measured. Past that the call is left unattributed, because
|
|
82
|
+
// reading a deep stack is the expensive part of this check.
|
|
16
83
|
const ATTRIBUTION_FRAME_LIMIT = 30;
|
|
17
84
|
// This file's own directory, so its frames can be skipped when reading the stack.
|
|
18
85
|
const selfDirectory = dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
// Which extension made the call, read from the stack.
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// back to `.ts` paths the image does not ship.
|
|
86
|
+
// Which extension made the call, read from the stack. Like `defineVendureExtension`'s
|
|
87
|
+
// declaring-file trap, it installs its own `prepareStackTrace` so `--enable-source-maps` cannot
|
|
88
|
+
// rewrite frames back to `.ts` paths the image does not ship.
|
|
23
89
|
function attribute(state, boundary) {
|
|
24
90
|
for (const path of stackFrameFiles(boundary, ATTRIBUTION_FRAME_LIMIT)) {
|
|
25
91
|
if (path === undefined || path.startsWith(selfDirectory))
|
|
@@ -30,7 +96,7 @@ function attribute(state, boundary) {
|
|
|
30
96
|
}
|
|
31
97
|
return undefined;
|
|
32
98
|
}
|
|
33
|
-
// The message ADR-0028 specifies
|
|
99
|
+
// The message shape ADR-0028 specifies:
|
|
34
100
|
//
|
|
35
101
|
// Extension acme.erp attempted outbound access to api.other-erp.example.
|
|
36
102
|
// Declared outbound hosts: erp.acme.com
|
|
@@ -40,34 +106,35 @@ function formatReport(host, extension) {
|
|
|
40
106
|
const declared = declaredHosts.length > 0
|
|
41
107
|
? `Declared outbound hosts: ${declaredHosts.join(", ")}`
|
|
42
108
|
: "It declares no outbound hosts.";
|
|
109
|
+
// A host `defineVendureExtension` would refuse cannot be declared, so the report does not suggest
|
|
110
|
+
// declaring it.
|
|
111
|
+
const parsed = parseOutboundHost(host);
|
|
112
|
+
const remedy = "host" in parsed
|
|
113
|
+
? "Add the host to the extension's outboundHosts to silence it."
|
|
114
|
+
: `${host} cannot be declared: it ${parsed.problem}.`;
|
|
43
115
|
return {
|
|
44
116
|
host,
|
|
45
117
|
extensionId: extension?.id,
|
|
46
118
|
declaredHosts,
|
|
47
|
-
message: `Vendure Cloud: ${subject} attempted outbound access to ${host}.\n${declared}\n This is reported, not blocked.
|
|
119
|
+
message: `Vendure Cloud: ${subject} attempted outbound access to ${host}.\n${declared}\n This is reported, not blocked. ${remedy}`,
|
|
48
120
|
};
|
|
49
121
|
}
|
|
50
122
|
function check(state, rawHost) {
|
|
51
123
|
if (rawHost === undefined || rawHost === "")
|
|
52
124
|
return;
|
|
53
125
|
const host = normalizeHost(rawHost);
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// CLO-412 is that an allowed call carries no measurable per-request cost. Attributing every
|
|
58
|
-
// call would mean capturing a stack on every request to satisfy a control that a single
|
|
59
|
-
// line of code can bypass anyway.
|
|
126
|
+
// Fast path: an allowed call costs a normalisation, one Set lookup and no captured stack (CLO-412
|
|
127
|
+
// requires no measurable per-request cost). An extension reaching a host another extension
|
|
128
|
+
// declared goes unreported, which a hygiene control accepts.
|
|
60
129
|
if (state.declaredAnywhere.has(host))
|
|
61
130
|
return;
|
|
62
131
|
const extension = attribute(state, check);
|
|
63
|
-
// Nothing on the stack belongs to an extension
|
|
64
|
-
//
|
|
65
|
-
// from platform code all land here, and reporting them would drown the signal.
|
|
132
|
+
// Nothing on the stack belongs to an extension. Core's own traffic, the database driver's and
|
|
133
|
+
// libraries called from platform code land here, and reporting them would drown the signal.
|
|
66
134
|
if (extension === undefined)
|
|
67
135
|
return;
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
// extension in existence today, and a report that fires on everything is not read.
|
|
136
|
+
// An extension that declared nothing opted out. Reading that as "may reach nothing" would report
|
|
137
|
+
// against every existing extension (CLO-412).
|
|
71
138
|
if (extension.outboundHosts.length === 0)
|
|
72
139
|
return;
|
|
73
140
|
const seen = `${extension.id}\u0000${host}`;
|
|
@@ -84,9 +151,8 @@ function hostFromUrl(value) {
|
|
|
84
151
|
return undefined;
|
|
85
152
|
}
|
|
86
153
|
}
|
|
87
|
-
// `fetch` takes a string, a URL, or a Request.
|
|
88
|
-
//
|
|
89
|
-
// only part that matters.
|
|
154
|
+
// `fetch` takes a string, a URL, or a Request. `.url` is read off the third by shape because the
|
|
155
|
+
// DOM lib is not in this package's types.
|
|
90
156
|
function hostFromFetchInput(input) {
|
|
91
157
|
if (typeof input === "string")
|
|
92
158
|
return hostFromUrl(input);
|
|
@@ -101,14 +167,11 @@ function hostFromFetchInput(input) {
|
|
|
101
167
|
}
|
|
102
168
|
function hostFromRequestArguments(args, depth = 0) {
|
|
103
169
|
for (const argument of args) {
|
|
104
|
-
// `Socket.prototype.connect`
|
|
105
|
-
//
|
|
106
|
-
// rather than in the position a direct caller would put them.
|
|
170
|
+
// `Socket.prototype.connect` receives Node's normalised argument list, an array holding the
|
|
171
|
+
// options object and the callback, so the options sit one level down.
|
|
107
172
|
//
|
|
108
|
-
// Bounded at
|
|
109
|
-
//
|
|
110
|
-
// strange, and a self-referencing array would otherwise exhaust the stack inside a check
|
|
111
|
-
// whose whole job is to watch without getting in the way.
|
|
173
|
+
// Bounded at that single level because this runs on the caller's arguments before Node
|
|
174
|
+
// validates them; a self-referencing array would otherwise exhaust the stack.
|
|
112
175
|
if (Array.isArray(argument)) {
|
|
113
176
|
if (depth >= 1)
|
|
114
177
|
continue;
|
|
@@ -136,12 +199,11 @@ function hostFromRequestArguments(args, depth = 0) {
|
|
|
136
199
|
}
|
|
137
200
|
return undefined;
|
|
138
201
|
}
|
|
139
|
-
// Installed by the platform entrypoint
|
|
140
|
-
//
|
|
141
|
-
// entrypoint would call on shutdown.
|
|
202
|
+
// Installed by the platform entrypoint and by `vendure dev`. Returns the function that undoes it,
|
|
203
|
+
// for tests and entrypoint shutdown.
|
|
142
204
|
//
|
|
143
|
-
// Nothing is patched
|
|
144
|
-
//
|
|
205
|
+
// Nothing is patched when no extension declares a host, so an application that has not adopted the
|
|
206
|
+
// field pays nothing.
|
|
145
207
|
export function installOutboundHostCheck(options) {
|
|
146
208
|
const declaring = options.extensions.filter((extension) => extension.outboundHosts.length > 0);
|
|
147
209
|
if (declaring.length === 0)
|
|
@@ -175,20 +237,17 @@ export function installOutboundHostCheck(options) {
|
|
|
175
237
|
globalThis.fetch = originalFetch;
|
|
176
238
|
});
|
|
177
239
|
}
|
|
178
|
-
// Everything
|
|
179
|
-
//
|
|
180
|
-
// acceptance criterion on CLO-412 rather than a preference.
|
|
240
|
+
// Everything but `fetch` is caught at the socket, the only interception point that behaves the
|
|
241
|
+
// same under both module systems (CLO-412).
|
|
181
242
|
//
|
|
182
|
-
// Replacing `net.connect` and `http.request` on the module object
|
|
183
|
-
// module's named import is a snapshot of the binding taken
|
|
184
|
-
// `import { connect } from "node:net"` keeps calling the original
|
|
185
|
-
//
|
|
186
|
-
// the replacement. That is the same control behaving differently in the two module systems,
|
|
187
|
-
// and it silently covers only half of them.
|
|
243
|
+
// Replacing `net.connect` and `http.request` on the module object covers only half of them: an ES
|
|
244
|
+
// module's named import is a snapshot of the binding taken at link time, so
|
|
245
|
+
// `import { connect } from "node:net"` keeps calling the original, while a CommonJS
|
|
246
|
+
// `require("node:net").connect` sees the replacement.
|
|
188
247
|
//
|
|
189
|
-
// `net.
|
|
190
|
-
//
|
|
191
|
-
//
|
|
248
|
+
// `net.connect`, `net.createConnection`, `http.request`, `https.request` and the global `fetch`
|
|
249
|
+
// all end at a Socket, and `net.Socket.prototype.connect` is looked up at call time whoever
|
|
250
|
+
// reached it.
|
|
192
251
|
const require = createRequire(import.meta.url);
|
|
193
252
|
const net = require("node:net");
|
|
194
253
|
const socketPrototype = net.Socket.prototype;
|