@open-mercato/cli 0.10.1-develop.7994.1.c9f31972aa → 0.10.1-develop.8022.1.b6753de892
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/build.mjs +1 -0
- package/dist/agentic/guides/module-facts.json +162 -162
- package/dist/agentic/guides/module-facts.v2.json +162 -162
- package/dist/agentic/guides/modules/agent_orchestrator/index.md +1 -1
- package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
- package/dist/agentic/guides/modules/api_docs/index.md +1 -1
- package/dist/agentic/guides/modules/api_keys/index.md +1 -1
- package/dist/agentic/guides/modules/attachments/index.md +1 -1
- package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
- package/dist/agentic/guides/modules/auth/index.md +1 -1
- package/dist/agentic/guides/modules/availability/index.md +1 -1
- package/dist/agentic/guides/modules/business_rules/index.md +1 -1
- package/dist/agentic/guides/modules/carrier_inpost/index.md +1 -1
- package/dist/agentic/guides/modules/cart/index.md +1 -1
- package/dist/agentic/guides/modules/catalog/index.md +1 -1
- package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
- package/dist/agentic/guides/modules/channel_discord/index.md +1 -1
- package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
- package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
- package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
- package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
- package/dist/agentic/guides/modules/channel_resend/index.md +1 -1
- package/dist/agentic/guides/modules/channel_ses/index.md +1 -1
- package/dist/agentic/guides/modules/channel_smtp/index.md +1 -1
- package/dist/agentic/guides/modules/checkout/index.md +1 -1
- package/dist/agentic/guides/modules/communication_channels/index.md +1 -1
- package/dist/agentic/guides/modules/configs/index.md +1 -1
- package/dist/agentic/guides/modules/content/index.md +1 -1
- package/dist/agentic/guides/modules/currencies/index.md +1 -1
- package/dist/agentic/guides/modules/customer_accounts/index.md +1 -1
- package/dist/agentic/guides/modules/customer_groups/index.md +1 -1
- package/dist/agentic/guides/modules/customers/index.md +1 -1
- package/dist/agentic/guides/modules/dashboards/index.md +1 -1
- package/dist/agentic/guides/modules/data_sync/index.md +1 -1
- package/dist/agentic/guides/modules/design_system/index.md +1 -1
- package/dist/agentic/guides/modules/devices/index.md +1 -1
- package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
- package/dist/agentic/guides/modules/directory/index.md +1 -1
- package/dist/agentic/guides/modules/documents/index.md +1 -1
- package/dist/agentic/guides/modules/ecommerce/index.md +1 -1
- package/dist/agentic/guides/modules/entities/index.md +1 -1
- package/dist/agentic/guides/modules/eudr/index.md +1 -1
- package/dist/agentic/guides/modules/events/index.md +1 -1
- package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
- package/dist/agentic/guides/modules/forms/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_autopay/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_tpay/index.md +1 -1
- package/dist/agentic/guides/modules/generators/index.md +1 -1
- package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
- package/dist/agentic/guides/modules/integrations/index.md +1 -1
- package/dist/agentic/guides/modules/messages/index.md +1 -1
- package/dist/agentic/guides/modules/notifications/index.md +1 -1
- package/dist/agentic/guides/modules/onboarding/index.md +1 -1
- package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
- package/dist/agentic/guides/modules/perspectives/index.md +1 -1
- package/dist/agentic/guides/modules/phone_calls/index.md +1 -1
- package/dist/agentic/guides/modules/planner/index.md +1 -1
- package/dist/agentic/guides/modules/portal/index.md +1 -1
- package/dist/agentic/guides/modules/progress/index.md +1 -1
- package/dist/agentic/guides/modules/promotions/index.md +1 -1
- package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
- package/dist/agentic/guides/modules/query_index/index.md +1 -1
- package/dist/agentic/guides/modules/record_locks/index.md +1 -1
- package/dist/agentic/guides/modules/resources/index.md +1 -1
- package/dist/agentic/guides/modules/sales/index.md +1 -1
- package/dist/agentic/guides/modules/scheduler/index.md +1 -1
- package/dist/agentic/guides/modules/search/index.md +1 -1
- package/dist/agentic/guides/modules/security/index.md +1 -1
- package/dist/agentic/guides/modules/seeds/index.md +1 -1
- package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
- package/dist/agentic/guides/modules/sso/index.md +1 -1
- package/dist/agentic/guides/modules/staff/index.md +1 -1
- package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
- package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
- package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
- package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
- package/dist/agentic/guides/modules/telemetry/index.md +1 -1
- package/dist/agentic/guides/modules/tillio/index.md +1 -1
- package/dist/agentic/guides/modules/translations/index.md +1 -1
- package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
- package/dist/agentic/guides/modules/webhooks/index.md +1 -1
- package/dist/agentic/guides/modules/wms/index.md +1 -1
- package/dist/agentic/guides/modules/workflows/index.md +1 -1
- package/dist/agentic/guides/reference-module-facts.json +1 -1
- package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +8 -0
- package/dist/agentic/guides/upstream/manifest.json +2 -2
- package/dist/agentic/shared/ai/harness/cases.json +2 -2
- package/dist/agentic/shared/ai/trackers/github.md +257 -2
- package/dist/agentic/shared/scripts/execution-sandbox.mjs +1 -0
- package/dist/i18n/de.json +4 -0
- package/dist/i18n/en.json +4 -0
- package/dist/i18n/es.json +4 -0
- package/dist/i18n/ko.json +4 -0
- package/dist/i18n/pl.json +4 -0
- package/dist/lib/__fixtures__/module-search/built-in.json +235 -0
- package/dist/lib/__fixtures__/module-search/catalog-parity.json +4076 -0
- package/dist/lib/__fixtures__/module-search/modules-index.json +1980 -0
- package/dist/lib/__fixtures__/official-module-package/package.json +33 -0
- package/dist/lib/generators/openapi.js +7 -2
- package/dist/lib/generators/openapi.js.map +2 -2
- package/dist/lib/init-secrets.js +33 -1
- package/dist/lib/init-secrets.js.map +2 -2
- package/dist/lib/telemetry-init.js +1 -1
- package/dist/lib/telemetry-init.js.map +1 -1
- package/dist/lib/testing/integration.js +22 -0
- package/dist/lib/testing/integration.js.map +2 -2
- package/dist/mercato.js +3 -14
- package/dist/mercato.js.map +2 -2
- package/package.json +7 -7
- package/src/__tests__/mercato.test.ts +57 -0
- package/src/i18n/de.json +4 -0
- package/src/i18n/en.json +4 -0
- package/src/i18n/es.json +4 -0
- package/src/i18n/ko.json +4 -0
- package/src/i18n/pl.json +4 -0
- package/src/lib/__tests__/init-secrets.test.ts +85 -1
- package/src/lib/__tests__/telemetry-init.test.ts +1 -1
- package/src/lib/generators/__tests__/openapi.test.ts +24 -0
- package/src/lib/generators/openapi.ts +7 -2
- package/src/lib/init-secrets.ts +37 -0
- package/src/lib/telemetry-init.ts +1 -1
- package/src/lib/testing/__tests__/integration.test.ts +24 -0
- package/src/lib/testing/integration.ts +23 -0
- package/src/mercato.ts +4 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/cli",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.8022.1.b6753de892",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -60,10 +60,10 @@
|
|
|
60
60
|
"@mikro-orm/decorators": "^7.1.14",
|
|
61
61
|
"@mikro-orm/migrations": "^7.1.14",
|
|
62
62
|
"@mikro-orm/postgresql": "^7.1.14",
|
|
63
|
-
"@open-mercato/events": "0.10.1-develop.
|
|
64
|
-
"@open-mercato/queue": "0.10.1-develop.
|
|
65
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
66
|
-
"@open-mercato/telemetry": "0.10.1-develop.
|
|
63
|
+
"@open-mercato/events": "0.10.1-develop.8022.1.b6753de892",
|
|
64
|
+
"@open-mercato/queue": "0.10.1-develop.8022.1.b6753de892",
|
|
65
|
+
"@open-mercato/shared": "0.10.1-develop.8022.1.b6753de892",
|
|
66
|
+
"@open-mercato/telemetry": "0.10.1-develop.8022.1.b6753de892",
|
|
67
67
|
"cross-spawn": "^7.0.6",
|
|
68
68
|
"minisearch": "7.2.0",
|
|
69
69
|
"pg": "8.23.0",
|
|
@@ -75,10 +75,10 @@
|
|
|
75
75
|
"typescript-js": "npm:typescript@6.0.3"
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
78
|
+
"@open-mercato/shared": "0.10.1-develop.8022.1.b6753de892"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
81
|
+
"@open-mercato/shared": "0.10.1-develop.8022.1.b6753de892",
|
|
82
82
|
"@types/jest": "^30.0.0",
|
|
83
83
|
"jest": "^30.4.2",
|
|
84
84
|
"ts-jest": "^29.4.12"
|
|
@@ -1112,6 +1112,47 @@ describe('init command failure output', () => {
|
|
|
1112
1112
|
)
|
|
1113
1113
|
}
|
|
1114
1114
|
|
|
1115
|
+
it.each(['--reinstall', '--no-demo-users'])('rejects unsafe production init before side effects with %s', async (flag) => {
|
|
1116
|
+
const previousEnv = { ...process.env }
|
|
1117
|
+
jest.spyOn(console, 'error').mockImplementation()
|
|
1118
|
+
jest.spyOn(console, 'log').mockImplementation()
|
|
1119
|
+
const authSetup = jest.fn().mockResolvedValue(undefined)
|
|
1120
|
+
mockInitDependencies({ authSetup })
|
|
1121
|
+
process.env.NODE_ENV = 'production'
|
|
1122
|
+
process.env.OM_INIT_SUPERADMIN_PASSWORD = 'password'
|
|
1123
|
+
try {
|
|
1124
|
+
const mercato = await import('../mercato')
|
|
1125
|
+
expect(await mercato.run(['node', 'mercato', 'init', flag, '--skip-password-policy'])).toBe(1)
|
|
1126
|
+
const { Client } = await import('pg')
|
|
1127
|
+
const { generateEntityIds } = await import('../lib/generators')
|
|
1128
|
+
const { dbMigrate } = await import('../lib/db')
|
|
1129
|
+
expect(Client).not.toHaveBeenCalled()
|
|
1130
|
+
expect(generateEntityIds).not.toHaveBeenCalled()
|
|
1131
|
+
expect(dbMigrate).not.toHaveBeenCalled()
|
|
1132
|
+
expect(authSetup).not.toHaveBeenCalled()
|
|
1133
|
+
} finally {
|
|
1134
|
+
process.env = previousEnv
|
|
1135
|
+
}
|
|
1136
|
+
})
|
|
1137
|
+
|
|
1138
|
+
it('passes valid production credentials to auth setup without bypassing policy', async () => {
|
|
1139
|
+
const previousEnv = { ...process.env }
|
|
1140
|
+
jest.spyOn(console, 'error').mockImplementation()
|
|
1141
|
+
jest.spyOn(console, 'log').mockImplementation()
|
|
1142
|
+
const authSetup = jest.fn().mockResolvedValue(undefined)
|
|
1143
|
+
mockInitDependencies({ authSetup })
|
|
1144
|
+
process.env.NODE_ENV = 'production'
|
|
1145
|
+
process.env.OM_INIT_SUPERADMIN_PASSWORD = 'Unique-Owner-937!'
|
|
1146
|
+
try {
|
|
1147
|
+
const mercato = await import('../mercato')
|
|
1148
|
+
expect(await mercato.run(['node', 'mercato', 'init', '--no-demo-users', '--skip-password-policy'])).toBe(0)
|
|
1149
|
+
expect(authSetup).toHaveBeenCalledWith(expect.arrayContaining(['--password', 'Unique-Owner-937!']))
|
|
1150
|
+
expect(authSetup.mock.calls[0][0]).not.toContain('--skip-password-policy')
|
|
1151
|
+
} finally {
|
|
1152
|
+
process.env = previousEnv
|
|
1153
|
+
}
|
|
1154
|
+
})
|
|
1155
|
+
|
|
1115
1156
|
it('passes --include-demo-users to auth setup by default', async () => {
|
|
1116
1157
|
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
1117
1158
|
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
|
|
@@ -1336,6 +1377,22 @@ describe('init on an already initialized database', () => {
|
|
|
1336
1377
|
consoleLogSpy.mockRestore()
|
|
1337
1378
|
})
|
|
1338
1379
|
|
|
1380
|
+
it('skips an initialized production database without bootstrap credentials', async () => {
|
|
1381
|
+
const previousEnv = { ...process.env }
|
|
1382
|
+
jest.spyOn(console, 'error').mockImplementation()
|
|
1383
|
+
jest.spyOn(console, 'log').mockImplementation()
|
|
1384
|
+
const { generateEntityIds } = mockSeededDatabase('3')
|
|
1385
|
+
process.env.NODE_ENV = 'production'
|
|
1386
|
+
delete process.env.OM_INIT_SUPERADMIN_PASSWORD
|
|
1387
|
+
try {
|
|
1388
|
+
const mercato = await import('../mercato')
|
|
1389
|
+
expect(await mercato.run(['node', 'mercato', 'init', '--skip-if-initialized'])).toBe(0)
|
|
1390
|
+
expect(generateEntityIds).not.toHaveBeenCalled()
|
|
1391
|
+
} finally {
|
|
1392
|
+
process.env = previousEnv
|
|
1393
|
+
}
|
|
1394
|
+
})
|
|
1395
|
+
|
|
1339
1396
|
it('keeps the data, names the target database and exits 0 with --skip-if-initialized', async () => {
|
|
1340
1397
|
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
|
|
1341
1398
|
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
|
package/src/i18n/de.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cli.init.errors.superadminPasswordPolicy": "Das Superadmin-Passwort für die Produktion erfüllt die konfigurierte Passwortrichtlinie nicht. Flags zum Umgehen der Passwortrichtlinie gelten nur außerhalb der Produktion.",
|
|
3
|
+
"cli.init.errors.superadminPasswordRequired": "Die Produktionsinitialisierung erfordert ein eindeutiges Superadmin-Passwort über OM_INIT_SUPERADMIN_PASSWORD oder --password; integrierte Platzhalter sind nicht zulässig."
|
|
4
|
+
}
|
package/src/i18n/en.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cli.init.errors.superadminPasswordPolicy": "Production superadmin password does not meet the configured password policy. Password-policy bypass flags apply only outside production.",
|
|
3
|
+
"cli.init.errors.superadminPasswordRequired": "Production initialization requires a unique superadmin password via OM_INIT_SUPERADMIN_PASSWORD or --password; built-in placeholders are not allowed."
|
|
4
|
+
}
|
package/src/i18n/es.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cli.init.errors.superadminPasswordPolicy": "La contraseña de superadministrador en producción no cumple la política de contraseñas configurada. Las opciones para omitir la política solo se aplican fuera de producción.",
|
|
3
|
+
"cli.init.errors.superadminPasswordRequired": "La inicialización en producción requiere una contraseña única de superadministrador mediante OM_INIT_SUPERADMIN_PASSWORD o --password; no se permiten los valores predeterminados de ejemplo."
|
|
4
|
+
}
|
package/src/i18n/ko.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cli.init.errors.superadminPasswordPolicy": "프로덕션 슈퍼관리자 비밀번호가 설정된 비밀번호 정책을 충족하지 않습니다. 비밀번호 정책 우회 플래그는 프로덕션 외 환경에서만 적용됩니다.",
|
|
3
|
+
"cli.init.errors.superadminPasswordRequired": "프로덕션 초기화에는 OM_INIT_SUPERADMIN_PASSWORD 또는 --password로 지정한 고유한 슈퍼관리자 비밀번호가 필요합니다. 기본 예시 비밀번호는 사용할 수 없습니다."
|
|
4
|
+
}
|
package/src/i18n/pl.json
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cli.init.errors.superadminPasswordPolicy": "Hasło superadministratora w środowisku produkcyjnym nie spełnia skonfigurowanej polityki haseł. Flagi pomijające politykę haseł działają tylko poza środowiskiem produkcyjnym.",
|
|
3
|
+
"cli.init.errors.superadminPasswordRequired": "Inicjalizacja produkcyjna wymaga unikalnego hasła superadministratora podanego przez OM_INIT_SUPERADMIN_PASSWORD lub --password; wbudowane hasła przykładowe są niedozwolone."
|
|
4
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { resolveInitDerivedSecrets, resolveInitSuperadminCredentials } from '../init-secrets'
|
|
2
4
|
|
|
3
5
|
describe('resolveInitDerivedSecrets', () => {
|
|
4
6
|
it("seeds the well-known 'secret' password in non-production when no env overrides are set", () => {
|
|
@@ -77,3 +79,85 @@ describe('resolveInitDerivedSecrets', () => {
|
|
|
77
79
|
})
|
|
78
80
|
})
|
|
79
81
|
|
|
82
|
+
|
|
83
|
+
describe('production bootstrap superadmin credentials', () => {
|
|
84
|
+
const production = { NODE_ENV: 'production' }
|
|
85
|
+
|
|
86
|
+
it.each([undefined, '', ' ', 'secret', 'password', ' PASSWORD ', 'changeme', 'change-me', 'replace_me'])(
|
|
87
|
+
'rejects an absent or placeholder production password: %s', (password) => {
|
|
88
|
+
expect(() => resolveInitSuperadminCredentials([], {
|
|
89
|
+
...production, OM_INIT_SUPERADMIN_PASSWORD: password,
|
|
90
|
+
})).toThrow('requires a unique superadmin password')
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
it.each(['--skip-password-policy', '--skip-password-policy=true', '--allow-weak-password', '--allow-weak-password=true'])(
|
|
95
|
+
'enforces production policy despite %s', (flag) => {
|
|
96
|
+
expect(() => resolveInitSuperadminCredentials([flag, '--password=weak'], production))
|
|
97
|
+
.toThrow('does not meet the configured password policy')
|
|
98
|
+
expect(resolveInitSuperadminCredentials([flag, '--password=Unique-Owner-937!'], production).skipPasswordPolicy).toBe(false)
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
it('uses the configured policy and never includes the password in errors', () => {
|
|
103
|
+
const password = 'Unique-Owner-937!'
|
|
104
|
+
expect(() => resolveInitSuperadminCredentials([`--password=${password}`], {
|
|
105
|
+
...production, OM_PASSWORD_MIN_LENGTH: '40',
|
|
106
|
+
})).toThrow('does not meet the configured password policy')
|
|
107
|
+
try {
|
|
108
|
+
resolveInitSuperadminCredentials([`--password=${password}`], { ...production, OM_PASSWORD_MIN_LENGTH: '40' })
|
|
109
|
+
} catch (error) {
|
|
110
|
+
expect(String(error)).not.toContain(password)
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('rejects placeholders even with a relaxed policy', () => {
|
|
115
|
+
expect(() => resolveInitSuperadminCredentials([], {
|
|
116
|
+
...production, OM_INIT_SUPERADMIN_PASSWORD: 'password', OM_PASSWORD_REQUIRE_DIGIT: 'false',
|
|
117
|
+
OM_PASSWORD_REQUIRE_UPPERCASE: 'false', OM_PASSWORD_REQUIRE_SPECIAL: 'false',
|
|
118
|
+
})).toThrow('requires a unique superadmin password')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('prefers command arguments over environment overrides', () => {
|
|
122
|
+
expect(resolveInitSuperadminCredentials(['--email=owner@example.test', '--password=Unique-Owner-937!'], {
|
|
123
|
+
NODE_ENV: ' Production ', OM_INIT_SUPERADMIN_PASSWORD: 'password', OM_INIT_SUPERADMIN_EMAIL: 'other@example.test',
|
|
124
|
+
})).toEqual({ email: 'owner@example.test', password: 'Unique-Owner-937!', skipPasswordPolicy: false })
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('uses the CLI locale for operator-facing production diagnostics', () => {
|
|
128
|
+
expect(() => resolveInitSuperadminCredentials([], { ...production, LC_ALL: 'pl_PL.UTF-8' }))
|
|
129
|
+
.toThrow('Inicjalizacja produkcyjna wymaga')
|
|
130
|
+
expect(() => resolveInitSuperadminCredentials([], { ...production, LC_ALL: 'unknown' }))
|
|
131
|
+
.toThrow('Production initialization requires')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('accepts a policy-compliant environment password', () => {
|
|
135
|
+
expect(resolveInitSuperadminCredentials([], {
|
|
136
|
+
...production, OM_INIT_SUPERADMIN_PASSWORD: 'Unique-Owner-937!',
|
|
137
|
+
}).password).toBe('Unique-Owner-937!')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it.each([undefined, 'development', 'test', 'staging'])('preserves non-production defaults for %s', (nodeEnv) => {
|
|
141
|
+
expect(resolveInitSuperadminCredentials([], { NODE_ENV: nodeEnv })).toEqual({
|
|
142
|
+
email: 'superadmin@acme.com', password: 'secret', skipPasswordPolicy: true,
|
|
143
|
+
})
|
|
144
|
+
expect(resolveInitSuperadminCredentials([], { NODE_ENV: nodeEnv, OM_INIT_SUPERADMIN_PASSWORD: 'password' }).password).toBe('password')
|
|
145
|
+
expect(resolveInitSuperadminCredentials(['--skip-password-policy=false'], { NODE_ENV: nodeEnv }).skipPasswordPolicy).toBe(false)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe('full-app Compose bootstrap defaults', () => {
|
|
150
|
+
it.each(['starters/docker/compose.fullapp.yml', 'packages/create-app/template/docker-compose.fullapp.yml'])(
|
|
151
|
+
'%s cannot bootstrap predictable production credentials', (composePath) => {
|
|
152
|
+
const compose = readFileSync(path.resolve(__dirname, '../../../../..', composePath), 'utf8')
|
|
153
|
+
const fallback = compose.match(/OM_INIT_SUPERADMIN_PASSWORD: \$\{OM_INIT_SUPERADMIN_PASSWORD:-(.*?)\}/)?.[1]
|
|
154
|
+
expect(fallback).toBeDefined()
|
|
155
|
+
expect(() => resolveInitSuperadminCredentials([], {
|
|
156
|
+
NODE_ENV: 'production', OM_INIT_SUPERADMIN_PASSWORD: fallback,
|
|
157
|
+
})).toThrow('requires a unique superadmin password')
|
|
158
|
+
expect(resolveInitSuperadminCredentials([], {
|
|
159
|
+
NODE_ENV: 'development', OM_INIT_SUPERADMIN_PASSWORD: fallback,
|
|
160
|
+
}).password).toBe(fallback)
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
})
|
|
@@ -164,7 +164,7 @@ describe('mercato telemetry init', () => {
|
|
|
164
164
|
|
|
165
165
|
const pkg = JSON.parse(read('package.json'))
|
|
166
166
|
expect(pkg.dependencies['@open-mercato/telemetry']).toBe('1.2.3')
|
|
167
|
-
expect(pkg.optionalDependencies['bullmq-otel']).toBe('^
|
|
167
|
+
expect(pkg.optionalDependencies['bullmq-otel']).toBe('^2.0.1')
|
|
168
168
|
|
|
169
169
|
expect(read('.env.example')).toContain('TELEMETRY_BACKEND')
|
|
170
170
|
expect(read('.env.example')).toContain('OTEL_EXPORTER_OTLP_ENDPOINT')
|
|
@@ -160,6 +160,30 @@ export function buildOpenApiDocument(modules: any[], options: any) {
|
|
|
160
160
|
expect(openApiDoc.paths['/api/demo/details']?.get?.summary).toBe('package details')
|
|
161
161
|
})
|
|
162
162
|
|
|
163
|
+
it('bundles into a per-run file and leaves no bundle behind', async () => {
|
|
164
|
+
touchFile(
|
|
165
|
+
path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'demo', 'api', 'health', 'route.ts'),
|
|
166
|
+
[
|
|
167
|
+
'export async function GET() { return new Response("pkg-health") }',
|
|
168
|
+
'export const openApi = {',
|
|
169
|
+
" GET: { summary: 'package health' }",
|
|
170
|
+
'}',
|
|
171
|
+
'',
|
|
172
|
+
].join('\n'),
|
|
173
|
+
)
|
|
174
|
+
const cacheDir = path.join(tmpDir, 'node_modules', '.cache')
|
|
175
|
+
touchFile(path.join(cacheDir, '_openapi-bundle.mjs'), 'export {}\n')
|
|
176
|
+
|
|
177
|
+
const result = await generateOpenApi({ resolver: createMockResolver([{ id: 'demo', from: '@open-mercato/core' }]), quiet: true })
|
|
178
|
+
|
|
179
|
+
expect(result.errors).toEqual([])
|
|
180
|
+
const openApiDoc = JSON.parse(
|
|
181
|
+
fs.readFileSync(path.join(tmpDir, 'output', 'generated', 'openapi.generated.json'), 'utf8'),
|
|
182
|
+
) as { paths: Record<string, { get?: { summary?: string } }> }
|
|
183
|
+
expect(openApiDoc.paths['/api/demo/health']?.get?.summary).toBe('package health')
|
|
184
|
+
expect(fs.readdirSync(cacheDir).filter((name) => name.startsWith('_openapi-bundle'))).toEqual([])
|
|
185
|
+
})
|
|
186
|
+
|
|
163
187
|
it('skips route discovery and bundling when all tracked inputs are unchanged', async () => {
|
|
164
188
|
touchFile(
|
|
165
189
|
path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'demo', 'api', 'health', 'route.ts'),
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import * as fs from 'node:fs'
|
|
10
|
+
import { randomBytes } from 'node:crypto'
|
|
10
11
|
import * as path from 'node:path'
|
|
11
12
|
import type { ModuleEntry, PackageResolver } from '../resolver'
|
|
12
13
|
import { getEnabledModuleIdentities } from '../module-identity'
|
|
@@ -340,7 +341,10 @@ async function generateOpenApiViaBundle(
|
|
|
340
341
|
const cacheDir = path.join(rootDir, 'node_modules', '.cache')
|
|
341
342
|
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true })
|
|
342
343
|
|
|
343
|
-
|
|
344
|
+
// Per-run file: `yarn dev`'s watcher and a CLI `generate` (e.g. after
|
|
345
|
+
// create-mercato-module init) can bundle concurrently, and a shared path let
|
|
346
|
+
// one process execute the other's half-written bundle ("Unexpected end of input").
|
|
347
|
+
const bundlePath = path.join(cacheDir, `_openapi-bundle-${process.pid}-${randomBytes(6).toString('hex')}.mjs`)
|
|
344
348
|
let inputPaths: string[] = []
|
|
345
349
|
|
|
346
350
|
// Build the entry script that imports all routes and calls buildOpenApiDocument
|
|
@@ -607,7 +611,8 @@ process.stdout.write(JSON.stringify(deepClone(doc), (_, v) =>
|
|
|
607
611
|
// process exit when a plugin request is still in flight.
|
|
608
612
|
try { await esbuild.stop() } catch {}
|
|
609
613
|
// Clean up old files from previous tsx-based approach
|
|
610
|
-
|
|
614
|
+
try { fs.unlinkSync(bundlePath) } catch {}
|
|
615
|
+
for (const file of ['_openapi-bundle.mjs', '_openapi-register.mjs', '_openapi-loader.mjs', '_next-stub.cjs']) {
|
|
611
616
|
try { fs.unlinkSync(path.join(cacheDir, file)) } catch {}
|
|
612
617
|
}
|
|
613
618
|
}
|
package/src/lib/init-secrets.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
|
|
2
|
+
import { getPasswordPolicy, validatePassword } from '@open-mercato/shared/lib/auth/passwordPolicy'
|
|
2
3
|
import { randomBytes } from 'node:crypto'
|
|
4
|
+
import { createTranslator } from '@open-mercato/shared/lib/i18n/translate'
|
|
5
|
+
import { createRequire } from 'node:module'
|
|
6
|
+
|
|
7
|
+
const requireLocale = createRequire(import.meta.url)
|
|
8
|
+
|
|
9
|
+
function initTranslator(env: NodeJS.ProcessEnv) {
|
|
10
|
+
const locale = (env.LC_ALL || env.LC_MESSAGES || env.LANG || 'en').split(/[_.-]/)[0].toLowerCase()
|
|
11
|
+
const supportedLocale = ['en', 'pl', 'es', 'de', 'ko'].includes(locale) ? locale : 'en'
|
|
12
|
+
const dictionary: Record<string, string> = requireLocale(`../i18n/${supportedLocale}.json`)
|
|
13
|
+
return createTranslator(dictionary)
|
|
14
|
+
}
|
|
3
15
|
|
|
4
16
|
type EnvReader = (key: string) => string | undefined
|
|
5
17
|
|
|
@@ -71,3 +83,28 @@ export function resolveInitDerivedSecrets(options: {
|
|
|
71
83
|
employeePassword: resolvePassword('OM_INIT_EMPLOYEE_PASSWORD', employeeEmail),
|
|
72
84
|
}
|
|
73
85
|
}
|
|
86
|
+
|
|
87
|
+
export function resolveInitSuperadminCredentials(args: string[], env: NodeJS.ProcessEnv = process.env) {
|
|
88
|
+
const argValue = (name: string) => args.find((arg) => arg.startsWith(`${name}=`))?.slice(name.length + 1) || undefined
|
|
89
|
+
const email = argValue('--email') ?? readEnvValue(env, 'OM_INIT_SUPERADMIN_EMAIL') ?? 'superadmin@acme.com'
|
|
90
|
+
const suppliedPassword = argValue('--password') ?? readEnvValue(env, 'OM_INIT_SUPERADMIN_PASSWORD')
|
|
91
|
+
const isProduction = (env.NODE_ENV ?? '').trim().toLowerCase() === 'production'
|
|
92
|
+
const skipPolicyArg = args.find((arg) =>
|
|
93
|
+
arg === '--skip-password-policy' || arg.startsWith('--skip-password-policy=') ||
|
|
94
|
+
arg === '--allow-weak-password' || arg.startsWith('--allow-weak-password='),
|
|
95
|
+
)
|
|
96
|
+
const skipPasswordPolicy = !isProduction && (skipPolicyArg
|
|
97
|
+
? parseBooleanToken(skipPolicyArg.split('=')[1] ?? 'true') ?? true
|
|
98
|
+
: true)
|
|
99
|
+
if (isProduction) {
|
|
100
|
+
const t = initTranslator(env)
|
|
101
|
+
const placeholder = suppliedPassword?.trim().toLowerCase()
|
|
102
|
+
if (!suppliedPassword || !placeholder || ['secret', 'password', 'changeme', 'change-me', 'change_me', 'replace-me', 'replace_me'].includes(placeholder)) {
|
|
103
|
+
throw new Error(t('cli.init.errors.superadminPasswordRequired'))
|
|
104
|
+
}
|
|
105
|
+
if (!validatePassword(suppliedPassword, getPasswordPolicy(env)).ok) {
|
|
106
|
+
throw new Error(t('cli.init.errors.superadminPasswordPolicy'))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { email, password: suppliedPassword ?? 'secret', skipPasswordPolicy }
|
|
110
|
+
}
|
|
@@ -23,7 +23,7 @@ import { dirname, join, resolve } from 'node:path'
|
|
|
23
23
|
* `@open-mercato/cli`'s telemetry dependency once the app updates its packages.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
const BULLMQ_OTEL_VERSION = '^
|
|
26
|
+
const BULLMQ_OTEL_VERSION = '^2.0.1'
|
|
27
27
|
|
|
28
28
|
type StepStatus = 'created' | 'patched' | 'skipped' | 'manual'
|
|
29
29
|
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
resolveAppReadyTimeoutMs,
|
|
18
18
|
resolveEphemeralPostgresImage,
|
|
19
19
|
resolveEphemeralJwtSecret,
|
|
20
|
+
resolveEphemeralSuperadminPassword,
|
|
20
21
|
ephemeralPostgresInitSql,
|
|
21
22
|
shouldReuseBuildArtifacts,
|
|
22
23
|
acquireEphemeralRuntimeLock,
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
} from '../integration'
|
|
31
32
|
import type { CapturedOutputProcess, ShutdownProcessRef } from '../integration'
|
|
32
33
|
import { resolveSpawnCommand } from '../../spawn'
|
|
34
|
+
import { resolveInitSuperadminCredentials } from '../../init-secrets'
|
|
33
35
|
import { EventEmitter } from 'node:events'
|
|
34
36
|
import { spawn } from 'node:child_process'
|
|
35
37
|
import type { ChildProcess } from 'node:child_process'
|
|
@@ -510,6 +512,28 @@ describe('integration cache and options', () => {
|
|
|
510
512
|
)
|
|
511
513
|
})
|
|
512
514
|
|
|
515
|
+
it('supplies a production-valid superadmin password when the inherited one is missing or a placeholder', () => {
|
|
516
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
517
|
+
try {
|
|
518
|
+
const fallback = resolveEphemeralSuperadminPassword({})
|
|
519
|
+
expect(fallback).toBe('Om-Ephemeral-Superadmin-2026!')
|
|
520
|
+
expect(() =>
|
|
521
|
+
resolveInitSuperadminCredentials([], { NODE_ENV: 'production', OM_INIT_SUPERADMIN_PASSWORD: fallback }),
|
|
522
|
+
).not.toThrow()
|
|
523
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
524
|
+
expect(resolveEphemeralSuperadminPassword({ OM_INIT_SUPERADMIN_PASSWORD: 'secret' })).toBe(fallback)
|
|
525
|
+
expect(warnSpy).toHaveBeenCalledTimes(1)
|
|
526
|
+
} finally {
|
|
527
|
+
warnSpy.mockRestore()
|
|
528
|
+
}
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
it('keeps a production-valid operator-provided superadmin password', () => {
|
|
532
|
+
expect(resolveEphemeralSuperadminPassword({ OM_INIT_SUPERADMIN_PASSWORD: 'Operator-Chosen-Pass-42!' })).toBe(
|
|
533
|
+
'Operator-Chosen-Pass-42!',
|
|
534
|
+
)
|
|
535
|
+
})
|
|
536
|
+
|
|
513
537
|
it('creates the vector and pgcrypto extensions in the ephemeral init SQL', () => {
|
|
514
538
|
const sql = ephemeralPostgresInitSql()
|
|
515
539
|
expect(sql).toContain('CREATE EXTENSION IF NOT EXISTS vector')
|
|
@@ -11,6 +11,7 @@ import spawn from 'cross-spawn'
|
|
|
11
11
|
import { fetchWithTimeout, type FetchWithTimeoutInit } from '@open-mercato/shared/lib/http/fetchWithTimeout'
|
|
12
12
|
import { isUnsafeJwtSecret } from '@open-mercato/shared/lib/auth/jwt'
|
|
13
13
|
import { resolveEnvironment } from '../resolver'
|
|
14
|
+
import { resolveInitSuperadminCredentials } from '../init-secrets'
|
|
14
15
|
import { resolveSpawnCommand } from '../spawn'
|
|
15
16
|
import { discoverIntegrationSpecFiles as discoverIntegrationSpecFilesShared } from './integration-discovery'
|
|
16
17
|
import { resolveDockerHostFromContext, runCommandAndCapture } from './runtime-utils'
|
|
@@ -202,6 +203,23 @@ export function resolveEphemeralJwtSecret(env: NodeJS.ProcessEnv = process.env):
|
|
|
202
203
|
}
|
|
203
204
|
return DEFAULT_EPHEMERAL_JWT_SECRET
|
|
204
205
|
}
|
|
206
|
+
const DEFAULT_EPHEMERAL_SUPERADMIN_PASSWORD = 'Om-Ephemeral-Superadmin-2026!'
|
|
207
|
+
|
|
208
|
+
export function resolveEphemeralSuperadminPassword(env: NodeJS.ProcessEnv = process.env): string {
|
|
209
|
+
const inherited = env.OM_INIT_SUPERADMIN_PASSWORD
|
|
210
|
+
if (inherited !== undefined && inherited.trim().length > 0) {
|
|
211
|
+
try {
|
|
212
|
+
resolveInitSuperadminCredentials([], { ...env, NODE_ENV: 'production', OM_INIT_SUPERADMIN_PASSWORD: inherited })
|
|
213
|
+
return inherited
|
|
214
|
+
} catch {
|
|
215
|
+
console.warn(
|
|
216
|
+
'[integration] Ignoring the inherited OM_INIT_SUPERADMIN_PASSWORD: it is a placeholder or fails the password policy and the ephemeral app initializes with NODE_ENV=production. Using the ephemeral default instead.',
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return DEFAULT_EPHEMERAL_SUPERADMIN_PASSWORD
|
|
221
|
+
}
|
|
222
|
+
|
|
205
223
|
const PLAYWRIGHT_ENV_UNAVAILABLE_PATTERNS: RegExp[] = [
|
|
206
224
|
/net::ERR_CONNECTION_REFUSED/i,
|
|
207
225
|
/Failed to connect to .* (localhost|127\.0\.0\.1)/i,
|
|
@@ -2197,6 +2215,7 @@ function buildReusableEnvironment(
|
|
|
2197
2215
|
// OM_INIT_*_PASSWORD to fix the seeded credential.
|
|
2198
2216
|
OM_INIT_ADMIN_PASSWORD: process.env.OM_INIT_ADMIN_PASSWORD ?? 'secret',
|
|
2199
2217
|
OM_INIT_EMPLOYEE_PASSWORD: process.env.OM_INIT_EMPLOYEE_PASSWORD ?? 'secret',
|
|
2218
|
+
OM_INIT_SUPERADMIN_PASSWORD: resolveEphemeralSuperadminPassword(),
|
|
2200
2219
|
OM_INTEGRATION_TEST: 'true',
|
|
2201
2220
|
OM_ENABLE_ENTERPRISE_MODULES: enterpriseModulesFlag,
|
|
2202
2221
|
OM_ENABLE_ENTERPRISE_MODULES_SSO: process.env.OM_ENABLE_ENTERPRISE_MODULES_SSO ?? enterpriseModulesFlag,
|
|
@@ -3585,6 +3604,10 @@ export async function startEphemeralEnvironment(options: EphemeralRuntimeOptions
|
|
|
3585
3604
|
// NODE_ENV=production.
|
|
3586
3605
|
OM_INIT_ADMIN_PASSWORD: process.env.OM_INIT_ADMIN_PASSWORD ?? 'secret',
|
|
3587
3606
|
OM_INIT_EMPLOYEE_PASSWORD: process.env.OM_INIT_EMPLOYEE_PASSWORD ?? 'secret',
|
|
3607
|
+
// Production init refuses a missing or placeholder superadmin password
|
|
3608
|
+
// (#8896). Integration auth helpers read OM_INIT_SUPERADMIN_PASSWORD, so
|
|
3609
|
+
// the Playwright runner logs in with the same value.
|
|
3610
|
+
OM_INIT_SUPERADMIN_PASSWORD: resolveEphemeralSuperadminPassword(),
|
|
3588
3611
|
// Pool sizing for the ephemeral integration runtime. Defaults were once
|
|
3589
3612
|
// very aggressive (max=5, idle=1000) which exposed flaky 'timeout exceeded
|
|
3590
3613
|
// when trying to connect' errors on `progressService.createJob`-backed
|
package/src/mercato.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
|
|
|
10
10
|
import { getSslConfig } from '@open-mercato/shared/lib/db/ssl'
|
|
11
11
|
import { getRedisUrl, getRedisUrlOrThrow, parseRedisUrl } from '@open-mercato/shared/lib/redis/connection'
|
|
12
12
|
import { formatInitSummary } from './lib/init-summary'
|
|
13
|
-
import { resolveInitDerivedSecrets } from './lib/init-secrets'
|
|
13
|
+
import { resolveInitDerivedSecrets, resolveInitSuperadminCredentials } from './lib/init-secrets'
|
|
14
14
|
import { decideExistingDatabase, formatSkipIfInitializedMessage } from './lib/init-existing-database'
|
|
15
15
|
import {
|
|
16
16
|
resolveAutoSpawnWorkersMode,
|
|
@@ -1032,6 +1032,7 @@ export async function run(argv = process.argv) {
|
|
|
1032
1032
|
delete process.env.OM_INIT_REINSTALL
|
|
1033
1033
|
}
|
|
1034
1034
|
const skipIfInitialized = initArgs.includes('--skip-if-initialized')
|
|
1035
|
+
const credentials = skipIfInitialized && !reinstall ? undefined : resolveInitSuperadminCredentials(initArgs)
|
|
1035
1036
|
const skipExamples = initArgs.includes('--no-examples')
|
|
1036
1037
|
const skipDemoUsers = initArgs.includes('--no-demo-users')
|
|
1037
1038
|
const stressTestEnabled =
|
|
@@ -1209,6 +1210,8 @@ export async function run(argv = process.argv) {
|
|
|
1209
1210
|
}
|
|
1210
1211
|
}
|
|
1211
1212
|
|
|
1213
|
+
const { email, password, skipPasswordPolicy } = credentials ?? resolveInitSuperadminCredentials(initArgs)
|
|
1214
|
+
|
|
1212
1215
|
// Step 1: Run generators directly (no process spawn)
|
|
1213
1216
|
console.log('🔧 Preparing modules (registry, entities, DI)...')
|
|
1214
1217
|
const { createResolver } = await import('./lib/resolver')
|
|
@@ -1257,16 +1260,7 @@ export async function run(argv = process.argv) {
|
|
|
1257
1260
|
}
|
|
1258
1261
|
return fallback
|
|
1259
1262
|
}
|
|
1260
|
-
const readEnvDefault = (key: string) => {
|
|
1261
|
-
const value = process.env[key]
|
|
1262
|
-
if (typeof value === 'string' && value.trim().length > 0) return value.trim()
|
|
1263
|
-
return undefined
|
|
1264
|
-
}
|
|
1265
|
-
const defaultEmail = readEnvDefault('OM_INIT_SUPERADMIN_EMAIL') ?? 'superadmin@acme.com'
|
|
1266
|
-
const defaultPassword = readEnvDefault('OM_INIT_SUPERADMIN_PASSWORD') ?? 'secret'
|
|
1267
1263
|
const orgName = findArgValue(['--org=', '--orgName='], 'Acme Corp')
|
|
1268
|
-
const email = findArgValue(['--email='], defaultEmail)
|
|
1269
|
-
const password = findArgValue(['--password='], defaultPassword)
|
|
1270
1264
|
const derivedSecrets = resolveInitDerivedSecrets({ email, env: process.env })
|
|
1271
1265
|
const adminEmailDerived = derivedSecrets.adminEmail
|
|
1272
1266
|
const employeeEmailDerived = derivedSecrets.employeeEmail
|
|
@@ -1300,16 +1294,6 @@ export async function run(argv = process.argv) {
|
|
|
1300
1294
|
process.env.OM_INIT_EMPLOYEE_EMAIL = employeeEmailDerived
|
|
1301
1295
|
}
|
|
1302
1296
|
const roles = findArgValue(['--roles='], 'superadmin,admin,employee')
|
|
1303
|
-
const skipPasswordPolicyRaw = initArgs.find((arg) =>
|
|
1304
|
-
arg === '--skip-password-policy' ||
|
|
1305
|
-
arg.startsWith('--skip-password-policy=') ||
|
|
1306
|
-
arg === '--allow-weak-password' ||
|
|
1307
|
-
arg.startsWith('--allow-weak-password=')
|
|
1308
|
-
)
|
|
1309
|
-
const skipPasswordPolicy = skipPasswordPolicyRaw
|
|
1310
|
-
? parseBooleanToken(skipPasswordPolicyRaw.split('=')[1] ?? 'true') ?? true
|
|
1311
|
-
: true
|
|
1312
|
-
|
|
1313
1297
|
console.log('🔐 Setting up RBAC and users...')
|
|
1314
1298
|
// Run auth setup command via CLI
|
|
1315
1299
|
const setupArgs = [
|