@open-mercato/cli 0.6.3-develop.3602.1.8287bc577d → 0.6.3-develop.3609.1.01983ef87a
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/dist/lib/generators/module-registry.js +66 -9
- package/dist/lib/generators/module-registry.js.map +3 -3
- package/dist/lib/testing/integration.js +13 -4
- package/dist/lib/testing/integration.js.map +2 -2
- package/package.json +5 -5
- package/src/lib/generators/__tests__/module-subset.test.ts +142 -0
- package/src/lib/generators/module-registry.ts +84 -10
- package/src/lib/testing/integration.ts +13 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/cli",
|
|
3
|
-
"version": "0.6.3-develop.
|
|
3
|
+
"version": "0.6.3-develop.3609.1.01983ef87a",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"@mikro-orm/decorators": "^7.0.17",
|
|
60
60
|
"@mikro-orm/migrations": "^7.0.17",
|
|
61
61
|
"@mikro-orm/postgresql": "^7.0.17",
|
|
62
|
-
"@open-mercato/queue": "0.6.3-develop.
|
|
63
|
-
"@open-mercato/shared": "0.6.3-develop.
|
|
62
|
+
"@open-mercato/queue": "0.6.3-develop.3609.1.01983ef87a",
|
|
63
|
+
"@open-mercato/shared": "0.6.3-develop.3609.1.01983ef87a",
|
|
64
64
|
"cross-spawn": "^7.0.6",
|
|
65
65
|
"pg": "8.21.0",
|
|
66
66
|
"semver": "^7.8.0",
|
|
@@ -70,10 +70,10 @@
|
|
|
70
70
|
"typescript": "^6.0.3"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
|
-
"@open-mercato/shared": "0.6.3-develop.
|
|
73
|
+
"@open-mercato/shared": "0.6.3-develop.3609.1.01983ef87a"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@open-mercato/shared": "0.6.3-develop.
|
|
76
|
+
"@open-mercato/shared": "0.6.3-develop.3609.1.01983ef87a",
|
|
77
77
|
"@types/jest": "^30.0.0",
|
|
78
78
|
"jest": "^30.4.2",
|
|
79
79
|
"ts-jest": "^29.4.9"
|
|
@@ -768,6 +768,148 @@ export function GET() {
|
|
|
768
768
|
)
|
|
769
769
|
})
|
|
770
770
|
|
|
771
|
+
it('emits imports for third-party scoped npm packages registered via src/modules.ts', async () => {
|
|
772
|
+
// Simulate a third-party module package published at @dainamite/cpq
|
|
773
|
+
// (see issue #1998). The generator must accept @<vendor>/<package> specifiers
|
|
774
|
+
// and emit imports that point at the package's modules/<id> subtree.
|
|
775
|
+
const pkgModuleBase = path.join(
|
|
776
|
+
tmpDir,
|
|
777
|
+
'packages',
|
|
778
|
+
'thirdparty',
|
|
779
|
+
'src',
|
|
780
|
+
'modules',
|
|
781
|
+
'cpq',
|
|
782
|
+
)
|
|
783
|
+
touchFile(
|
|
784
|
+
path.join(pkgModuleBase, 'subscribers', 'on-event.ts'),
|
|
785
|
+
`export const metadata = { event: 'cpq.quote.created' }\nexport default async function handler() {}\n`,
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
const baseResolver = createMockResolver(tmpDir, [
|
|
789
|
+
{ id: 'cpq', from: '@dainamite/cpq' },
|
|
790
|
+
])
|
|
791
|
+
const resolver: PackageResolver = {
|
|
792
|
+
...baseResolver,
|
|
793
|
+
getModulePaths: (entry: ModuleEntry) => ({
|
|
794
|
+
appBase: path.join(tmpDir, 'app', 'src', 'modules', entry.id),
|
|
795
|
+
pkgBase: pkgModuleBase,
|
|
796
|
+
}),
|
|
797
|
+
getModuleImportBase: (entry: ModuleEntry) => ({
|
|
798
|
+
appBase: `@/modules/${entry.id}`,
|
|
799
|
+
pkgBase: `@dainamite/cpq/modules/${entry.id}`,
|
|
800
|
+
}),
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const result = await generateModuleRegistry({ resolver, quiet: true })
|
|
804
|
+
expect(result.errors).toEqual([])
|
|
805
|
+
|
|
806
|
+
const output = readGenerated(tmpDir, 'modules.generated.ts')!
|
|
807
|
+
expect(output).toContain('id: "cpq"')
|
|
808
|
+
expect(output).toContain('@dainamite/cpq/modules/cpq/subscribers/on-event')
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
it('emits imports for bare (unscoped) third-party npm packages', async () => {
|
|
812
|
+
const pkgModuleBase = path.join(
|
|
813
|
+
tmpDir,
|
|
814
|
+
'packages',
|
|
815
|
+
'thirdparty-bare',
|
|
816
|
+
'src',
|
|
817
|
+
'modules',
|
|
818
|
+
'foo',
|
|
819
|
+
)
|
|
820
|
+
touchFile(
|
|
821
|
+
path.join(pkgModuleBase, 'subscribers', 'on-event.ts'),
|
|
822
|
+
`export const metadata = { event: 'foo.thing.created' }\nexport default async function handler() {}\n`,
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
const baseResolver = createMockResolver(tmpDir, [
|
|
826
|
+
{ id: 'foo', from: 'acme-modules' },
|
|
827
|
+
])
|
|
828
|
+
const resolver: PackageResolver = {
|
|
829
|
+
...baseResolver,
|
|
830
|
+
getModulePaths: (entry: ModuleEntry) => ({
|
|
831
|
+
appBase: path.join(tmpDir, 'app', 'src', 'modules', entry.id),
|
|
832
|
+
pkgBase: pkgModuleBase,
|
|
833
|
+
}),
|
|
834
|
+
getModuleImportBase: (entry: ModuleEntry) => ({
|
|
835
|
+
appBase: `@/modules/${entry.id}`,
|
|
836
|
+
pkgBase: `acme-modules/modules/${entry.id}`,
|
|
837
|
+
}),
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const result = await generateModuleRegistry({ resolver, quiet: true })
|
|
841
|
+
expect(result.errors).toEqual([])
|
|
842
|
+
|
|
843
|
+
const output = readGenerated(tmpDir, 'modules.generated.ts')!
|
|
844
|
+
expect(output).toContain('id: "foo"')
|
|
845
|
+
expect(output).toContain('acme-modules/modules/foo/subscribers/on-event')
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
it('fails fast when a third-party module entry points at a package without the expected OM module shape', async () => {
|
|
849
|
+
// No files scaffolded — the package directory does not exist, so the
|
|
850
|
+
// entry is almost certainly not an Open Mercato module.
|
|
851
|
+
const enabled: ModuleEntry[] = [{ id: 'cpq', from: '@dainamite/cpq' }]
|
|
852
|
+
const baseResolver = createMockResolver(tmpDir, enabled)
|
|
853
|
+
const resolver: PackageResolver = {
|
|
854
|
+
...baseResolver,
|
|
855
|
+
getModulePaths: (entry: ModuleEntry) => ({
|
|
856
|
+
appBase: path.join(tmpDir, 'app', 'src', 'modules', entry.id),
|
|
857
|
+
pkgBase: path.join(
|
|
858
|
+
tmpDir,
|
|
859
|
+
'node_modules',
|
|
860
|
+
'@dainamite',
|
|
861
|
+
'cpq',
|
|
862
|
+
'dist',
|
|
863
|
+
'modules',
|
|
864
|
+
entry.id,
|
|
865
|
+
),
|
|
866
|
+
}),
|
|
867
|
+
getModuleImportBase: (entry: ModuleEntry) => ({
|
|
868
|
+
appBase: `@/modules/${entry.id}`,
|
|
869
|
+
pkgBase: `@dainamite/cpq/modules/${entry.id}`,
|
|
870
|
+
}),
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
await expect(generateModuleRegistry({ resolver, quiet: true })).rejects.toThrow(
|
|
874
|
+
/no Open Mercato module shape was found/,
|
|
875
|
+
)
|
|
876
|
+
})
|
|
877
|
+
|
|
878
|
+
it('rejects third-party specifiers with path-traversal segments', async () => {
|
|
879
|
+
const pkgModuleBase = path.join(
|
|
880
|
+
tmpDir,
|
|
881
|
+
'packages',
|
|
882
|
+
'thirdparty-traversal',
|
|
883
|
+
'src',
|
|
884
|
+
'modules',
|
|
885
|
+
'evil',
|
|
886
|
+
)
|
|
887
|
+
touchFile(
|
|
888
|
+
path.join(pkgModuleBase, 'subscribers', 'on-event.ts'),
|
|
889
|
+
`export const metadata = { event: 'evil.x' }\nexport default async function handler() {}\n`,
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
const baseResolver = createMockResolver(tmpDir, [
|
|
893
|
+
{ id: 'evil', from: '@vendor/pkg' },
|
|
894
|
+
])
|
|
895
|
+
const resolver: PackageResolver = {
|
|
896
|
+
...baseResolver,
|
|
897
|
+
getModulePaths: (entry: ModuleEntry) => ({
|
|
898
|
+
appBase: path.join(tmpDir, 'app', 'src', 'modules', entry.id),
|
|
899
|
+
pkgBase: pkgModuleBase,
|
|
900
|
+
}),
|
|
901
|
+
// Inject a malicious specifier with a ".." segment.
|
|
902
|
+
getModuleImportBase: (entry: ModuleEntry) => ({
|
|
903
|
+
appBase: `@/modules/${entry.id}`,
|
|
904
|
+
pkgBase: `@vendor/pkg/../escape/modules/${entry.id}`,
|
|
905
|
+
}),
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
await expect(generateModuleRegistry({ resolver, quiet: true })).rejects.toThrow(
|
|
909
|
+
/Unsafe generated module specifier/,
|
|
910
|
+
)
|
|
911
|
+
})
|
|
912
|
+
|
|
771
913
|
it('mixed subset: core module + app module, then remove core module', async () => {
|
|
772
914
|
scaffoldModule(tmpDir, 'core_mod', 'pkg', [
|
|
773
915
|
'backend/page.tsx',
|
|
@@ -925,8 +925,12 @@ function toLiteral(value: unknown): string {
|
|
|
925
925
|
.replace(/\u2029/g, '\\u2029')
|
|
926
926
|
}
|
|
927
927
|
|
|
928
|
-
const
|
|
928
|
+
const GENERATED_MODULE_RELATIVE_PREFIXES = ['@/', '../../src/modules/', './'] as const
|
|
929
929
|
const GENERATED_MODULE_SPECIFIER_SEGMENT = /^[A-Za-z0-9_.\-[\]()']+$/
|
|
930
|
+
// Lenient npm package name matcher. Accepts legacy uppercase packages and
|
|
931
|
+
// tilde-prefixed names, bans leading "." / "_", and reserves "@" for scope
|
|
932
|
+
// markers so the dispatcher below can split scope-vs-bare unambiguously.
|
|
933
|
+
const NPM_PACKAGE_NAME_SEGMENT = /^[A-Za-z0-9~][A-Za-z0-9_.~-]*$/
|
|
930
934
|
|
|
931
935
|
function sanitizeGeneratedModuleSpecifierSegment(segment: string, importPath: string): string {
|
|
932
936
|
const match = GENERATED_MODULE_SPECIFIER_SEGMENT.exec(segment)
|
|
@@ -936,22 +940,83 @@ function sanitizeGeneratedModuleSpecifierSegment(segment: string, importPath: st
|
|
|
936
940
|
return match[0]
|
|
937
941
|
}
|
|
938
942
|
|
|
939
|
-
function
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
throw new Error(`Unsafe generated module specifier prefix: ${importPath}`)
|
|
943
|
+
function sanitizeNpmPackageNameSegment(segment: string, importPath: string): void {
|
|
944
|
+
if (!NPM_PACKAGE_NAME_SEGMENT.test(segment) || segment === '.' || segment === '..') {
|
|
945
|
+
throw new Error(`Unsafe generated module specifier: ${importPath}`)
|
|
943
946
|
}
|
|
947
|
+
}
|
|
944
948
|
|
|
945
|
-
|
|
946
|
-
if (!
|
|
949
|
+
function sanitizeGeneratedModuleSpecifier(importPath: string): string {
|
|
950
|
+
if (!importPath) {
|
|
947
951
|
throw new Error(`Unsafe generated module specifier: ${importPath}`)
|
|
948
952
|
}
|
|
949
953
|
|
|
950
|
-
const
|
|
954
|
+
const relativePrefix = GENERATED_MODULE_RELATIVE_PREFIXES.find((candidate) =>
|
|
955
|
+
importPath.startsWith(candidate),
|
|
956
|
+
)
|
|
957
|
+
if (relativePrefix) {
|
|
958
|
+
const suffix = importPath.slice(relativePrefix.length)
|
|
959
|
+
if (!suffix) {
|
|
960
|
+
throw new Error(`Unsafe generated module specifier: ${importPath}`)
|
|
961
|
+
}
|
|
962
|
+
const segments = suffix
|
|
963
|
+
.split('/')
|
|
964
|
+
.map((segment) => sanitizeGeneratedModuleSpecifierSegment(segment, importPath))
|
|
965
|
+
return `${relativePrefix}${segments.join('/')}`
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (importPath.startsWith('@')) {
|
|
969
|
+
// Scoped npm package: @scope/name[/subpath]
|
|
970
|
+
const firstSlash = importPath.indexOf('/')
|
|
971
|
+
if (firstSlash < 2) {
|
|
972
|
+
throw new Error(`Unsafe generated module specifier prefix: ${importPath}`)
|
|
973
|
+
}
|
|
974
|
+
const scope = importPath.slice(1, firstSlash)
|
|
975
|
+
sanitizeNpmPackageNameSegment(scope, importPath)
|
|
976
|
+
const rest = importPath.slice(firstSlash + 1)
|
|
977
|
+
if (!rest) {
|
|
978
|
+
throw new Error(`Unsafe generated module specifier: ${importPath}`)
|
|
979
|
+
}
|
|
980
|
+
const secondSlash = rest.indexOf('/')
|
|
981
|
+
const name = secondSlash >= 0 ? rest.slice(0, secondSlash) : rest
|
|
982
|
+
sanitizeNpmPackageNameSegment(name, importPath)
|
|
983
|
+
const subpath = secondSlash >= 0 ? rest.slice(secondSlash + 1) : ''
|
|
984
|
+
if (!subpath) {
|
|
985
|
+
return `@${scope}/${name}`
|
|
986
|
+
}
|
|
987
|
+
const segments = subpath
|
|
988
|
+
.split('/')
|
|
989
|
+
.map((segment) => sanitizeGeneratedModuleSpecifierSegment(segment, importPath))
|
|
990
|
+
return `@${scope}/${name}/${segments.join('/')}`
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Bare npm package: name[/subpath]
|
|
994
|
+
const firstSlash = importPath.indexOf('/')
|
|
995
|
+
const name = firstSlash >= 0 ? importPath.slice(0, firstSlash) : importPath
|
|
996
|
+
sanitizeNpmPackageNameSegment(name, importPath)
|
|
997
|
+
const subpath = firstSlash >= 0 ? importPath.slice(firstSlash + 1) : ''
|
|
998
|
+
if (!subpath) {
|
|
999
|
+
return name
|
|
1000
|
+
}
|
|
1001
|
+
const segments = subpath
|
|
951
1002
|
.split('/')
|
|
952
1003
|
.map((segment) => sanitizeGeneratedModuleSpecifierSegment(segment, importPath))
|
|
953
|
-
|
|
954
|
-
|
|
1004
|
+
return `${name}/${segments.join('/')}`
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Defense in depth: when src/modules.ts registers a module from a third-party
|
|
1008
|
+
// npm scope (anything outside @app / @open-mercato/*), verify the resolved
|
|
1009
|
+
// pkgBase directory exists. If it doesn't, the package referenced via `from:`
|
|
1010
|
+
// is almost certainly not an Open Mercato module (or the id is wrong), and
|
|
1011
|
+
// silently emitting an empty module registration would hide the misconfig.
|
|
1012
|
+
function verifyThirdPartyModuleShape(entry: { id: string; from?: string }, pkgBase: string): void {
|
|
1013
|
+
const from = entry.from
|
|
1014
|
+
if (!from || from === '@app' || from.startsWith('@open-mercato/')) return
|
|
1015
|
+
if (fs.existsSync(pkgBase)) return
|
|
1016
|
+
throw new Error(
|
|
1017
|
+
`Module '${entry.id}' is registered with from: '${from}' but no Open Mercato module shape was found at '${pkgBase}'. ` +
|
|
1018
|
+
`Ensure the package ships a 'modules/${entry.id}' subtree (either under 'dist/modules/' or 'src/modules/'), or correct the 'from' value in src/modules.ts.`,
|
|
1019
|
+
)
|
|
955
1020
|
}
|
|
956
1021
|
|
|
957
1022
|
function buildImportStatement(importClause: string, importPath: string): string {
|
|
@@ -2502,6 +2567,9 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
|
|
|
2502
2567
|
const legacySubscribersChecksumFile = path.join(outputDir, 'subscribers.generated.checksum')
|
|
2503
2568
|
|
|
2504
2569
|
const enabled = resolver.loadEnabledModules()
|
|
2570
|
+
for (const entry of enabled) {
|
|
2571
|
+
verifyThirdPartyModuleShape(entry, resolver.getModulePaths(entry).pkgBase)
|
|
2572
|
+
}
|
|
2505
2573
|
const extensions = loadGeneratorExtensions()
|
|
2506
2574
|
|
|
2507
2575
|
// Pre-pass: collect generator plugins from each enabled module's generators.ts
|
|
@@ -3118,6 +3186,9 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
|
|
|
3118
3186
|
const enabledIdsChecksumFile = path.join(outputDir, 'enabled-module-ids.generated.checksum')
|
|
3119
3187
|
|
|
3120
3188
|
const enabled = resolver.loadEnabledModules()
|
|
3189
|
+
for (const entry of enabled) {
|
|
3190
|
+
verifyThirdPartyModuleShape(entry, resolver.getModulePaths(entry).pkgBase)
|
|
3191
|
+
}
|
|
3121
3192
|
const imports: string[] = []
|
|
3122
3193
|
const moduleDecls: WriterFunction[] = []
|
|
3123
3194
|
const importIdRef = { value: 0 }
|
|
@@ -3449,6 +3520,9 @@ export async function generateModuleRegistryCli(options: ModuleRegistryOptions):
|
|
|
3449
3520
|
const legacyChecksumFile = path.join(outputDir, 'cli-modules.generated.checksum')
|
|
3450
3521
|
|
|
3451
3522
|
const enabled = resolver.loadEnabledModules()
|
|
3523
|
+
for (const entry of enabled) {
|
|
3524
|
+
verifyThirdPartyModuleShape(entry, resolver.getModulePaths(entry).pkgBase)
|
|
3525
|
+
}
|
|
3452
3526
|
const imports: string[] = []
|
|
3453
3527
|
const moduleDecls: WriterFunction[] = []
|
|
3454
3528
|
// Mutable ref so extracted helper functions can increment the shared counter
|
|
@@ -2906,10 +2906,19 @@ export async function startEphemeralEnvironment(options: EphemeralRuntimeOptions
|
|
|
2906
2906
|
JWT_SECRET: process.env.JWT_SECRET ?? 'om-ephemeral-integration-jwt-secret',
|
|
2907
2907
|
OM_SECURITY_MFA_SETUP_SECRET: process.env.OM_SECURITY_MFA_SETUP_SECRET ?? 'om-ephemeral-integration-mfa-setup-secret',
|
|
2908
2908
|
NODE_ENV: 'production',
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2909
|
+
// Pool sizing for the ephemeral integration runtime. Defaults were once
|
|
2910
|
+
// very aggressive (max=5, idle=1000) which exposed flaky 'timeout exceeded
|
|
2911
|
+
// when trying to connect' errors on `progressService.createJob`-backed
|
|
2912
|
+
// endpoints (sync_excel.import, data_sync.run, progress.jobs) — each
|
|
2913
|
+
// request acquires a transaction connection plus a separate connection
|
|
2914
|
+
// for the encryption subscriber's fetchMap probe, and with 1s idle close
|
|
2915
|
+
// the pool thrashes faster than pg-pool can repopulate. These values
|
|
2916
|
+
// still keep the pool tighter than production but give enough headroom
|
|
2917
|
+
// for the legitimate query bursts.
|
|
2918
|
+
DB_POOL_MIN: '2',
|
|
2919
|
+
DB_POOL_MAX: '20',
|
|
2920
|
+
DB_POOL_IDLE_TIMEOUT: '10000',
|
|
2921
|
+
DB_POOL_ACQUIRE_TIMEOUT: '15000',
|
|
2913
2922
|
DB_IDLE_SESSION_TIMEOUT_MS: '30000',
|
|
2914
2923
|
DB_IDLE_IN_TRANSACTION_TIMEOUT_MS: '30000',
|
|
2915
2924
|
OM_INTEGRATION_TEST: 'true',
|