@mettlecast/domain-cli 0.2.59 → 0.2.60
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/builder/build-registry.js +128 -13
- package/dist/builder/load-module.js +3 -3
- package/dist/cli.js +2 -2
- package/dist/commands/build-catalog.d.ts +2 -2
- package/dist/commands/build-catalog.js +5 -5
- package/dist/commands/build-flows.js +1 -1
- package/dist/commands/build.js +5 -5
- package/dist/commands/check-hashes.js +2 -2
- package/dist/commands/create-project.js +1 -1
- package/dist/commands/doctor.d.ts +5 -7
- package/dist/commands/doctor.js +44 -165
- package/dist/commands/power-tune.js +2 -2
- package/dist/commands/show-dns.d.ts +1 -1
- package/dist/commands/show-dns.js +5 -5
- package/dist/commands/upgrade.js +17 -11
- package/dist/commands/validate.js +183 -0
- package/dist/utils/header-inject.js +2 -2
- package/dist/utils/install-file.d.ts +1 -1
- package/dist/utils/install-file.js +1 -1
- package/dist/utils/manifest.js +1 -2
- package/dist/utils/scaffold-config.d.ts +2 -2
- package/dist/utils/scaffold-config.js +1 -1
- package/package.json +1 -1
- package/src/__tests__/commands/check-hashes.test.ts +9 -9
- package/src/__tests__/commands/upgrade.test.ts +7 -7
- package/src/__tests__/doctor.test.ts +60 -67
- package/src/__tests__/package-freshness.test.ts +114 -0
- package/src/__tests__/scaffold-src/part-a-layout.test.ts +10 -10
- package/src/__tests__/scripts/package-scaffold.test.ts +5 -5
- package/src/__tests__/utils/install-file.test.ts +2 -2
- package/src/__tests__/utils/manifest.test.ts +2 -2
- package/src/__tests__/validate.test.ts +652 -1
- package/src/builder/build-registry.ts +147 -15
- package/src/builder/load-module.ts +3 -3
- package/src/cli.ts +3 -3
- package/src/commands/build-catalog.ts +5 -5
- package/src/commands/build-flows.ts +1 -1
- package/src/commands/build.ts +5 -5
- package/src/commands/check-hashes.ts +2 -2
- package/src/commands/create-project.ts +1 -1
- package/src/commands/doctor.ts +52 -181
- package/src/commands/power-tune.ts +2 -2
- package/src/commands/show-dns.ts +5 -5
- package/src/commands/upgrade.ts +16 -10
- package/src/commands/validate.ts +226 -1
- package/src/utils/header-inject.ts +2 -2
- package/src/utils/install-file.ts +1 -1
- package/src/utils/manifest.ts +1 -2
- package/src/utils/scaffold-config.ts +3 -3
package/src/commands/show-dns.ts
CHANGED
|
@@ -26,7 +26,7 @@ function toZoneLine(r: DnsRecord): string {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
29
|
+
* mc-domain-module show-dns [--env dev|staging|prod] [--project-dir <path>]
|
|
30
30
|
*
|
|
31
31
|
* Reads the scaffold-config.json and prints DNS records that must be added for
|
|
32
32
|
* custom domain wiring: certificate validation CNAMEs (step 1) and traffic
|
|
@@ -37,27 +37,27 @@ function toZoneLine(r: DnsRecord): string {
|
|
|
37
37
|
*/
|
|
38
38
|
export async function runShowDns(opts: ShowDnsOptions): Promise<void> {
|
|
39
39
|
const projectDir = opts.projectDir ? path.resolve(opts.projectDir) : process.cwd();
|
|
40
|
-
const configPath = path.join(projectDir, '.
|
|
40
|
+
const configPath = path.join(projectDir, '.mc', 'scaffold-config.json');
|
|
41
41
|
|
|
42
42
|
let config: { environments?: Record<string, { dashboardUrl?: string }> };
|
|
43
43
|
try {
|
|
44
44
|
const raw = await readFile(configPath, 'utf-8');
|
|
45
45
|
config = JSON.parse(raw) as typeof config;
|
|
46
46
|
} catch {
|
|
47
|
-
console.error('[
|
|
47
|
+
console.error('[mc-domain-module] Could not read .mc/scaffold-config.json — run from a Mettlecast project root.');
|
|
48
48
|
process.exit(1);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
const environments = config.environments ?? {};
|
|
52
52
|
if (Object.keys(environments).length === 0) {
|
|
53
|
-
console.log('[
|
|
53
|
+
console.log('[mc-domain-module] No environments configured. Set a custom domain via `npx mc-domain-module show-dns` or the Setup tab.');
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
// Try to load CDK outputs for routing records
|
|
58
58
|
let cdkOutputs: Record<string, Record<string, string>> = {};
|
|
59
59
|
try {
|
|
60
|
-
const outputsPath = path.join(projectDir, '.
|
|
60
|
+
const outputsPath = path.join(projectDir, '.mc', 'cdk-outputs.json');
|
|
61
61
|
const raw = await readFile(outputsPath, 'utf-8');
|
|
62
62
|
cdkOutputs = JSON.parse(raw) as typeof cdkOutputs;
|
|
63
63
|
} catch {
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -251,9 +251,9 @@ function buildPrBody(
|
|
|
251
251
|
lines.push('### Drift warnings — manual review required');
|
|
252
252
|
lines.push(
|
|
253
253
|
'The following files were locally modified after scaffold installation. ' +
|
|
254
|
-
'New versions have been written to `{path}.
|
|
254
|
+
'New versions have been written to `{path}.mc-upgrade` — merge manually then remove the `.mc-upgrade` file.'
|
|
255
255
|
);
|
|
256
|
-
for (const r of conflicts) lines.push(`- \`${r.path}\` → \`${r.path}.
|
|
256
|
+
for (const r of conflicts) lines.push(`- \`${r.path}\` → \`${r.path}.mc-upgrade\``);
|
|
257
257
|
lines.push('');
|
|
258
258
|
}
|
|
259
259
|
|
|
@@ -322,9 +322,9 @@ function buildFrontendComponentsPrBody(
|
|
|
322
322
|
lines.push('### Drift warnings — manual review required');
|
|
323
323
|
lines.push(
|
|
324
324
|
'The following files were locally modified after scaffold installation. ' +
|
|
325
|
-
'New versions have been written to `{path}.
|
|
325
|
+
'New versions have been written to `{path}.mc-upgrade` — merge manually then remove the `.mc-upgrade` file.'
|
|
326
326
|
);
|
|
327
|
-
for (const r of conflicts) lines.push(`- \`${r.path}\` → \`${r.path}.
|
|
327
|
+
for (const r of conflicts) lines.push(`- \`${r.path}\` → \`${r.path}.mc-upgrade\``);
|
|
328
328
|
lines.push('');
|
|
329
329
|
}
|
|
330
330
|
|
|
@@ -512,14 +512,20 @@ export async function runUpgrade(
|
|
|
512
512
|
continue;
|
|
513
513
|
}
|
|
514
514
|
|
|
515
|
-
//
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
// update-available: do NOT update manifest (file was not written to disk)
|
|
515
|
+
// update-available: write the proposed new content beside the drifted file
|
|
516
|
+
// for manual merge, but do NOT update the manifest or overwrite the file.
|
|
519
517
|
if (installResult.status === 'update-available') {
|
|
518
|
+
if (!opts.dryRun) {
|
|
519
|
+
await mkdir(dirname(diskPath), { recursive: true });
|
|
520
|
+
await writeFile(`${diskPath}.mc-upgrade`, newContent, 'utf-8');
|
|
521
|
+
}
|
|
522
|
+
allResults.push({ path: installPath, status: 'conflict', module: mod.id });
|
|
520
523
|
continue;
|
|
521
524
|
}
|
|
522
525
|
|
|
526
|
+
// Add to results
|
|
527
|
+
allResults.push({ path: installPath, status: installResult.status, module: mod.id });
|
|
528
|
+
|
|
523
529
|
// Update manifest if file was not skipped
|
|
524
530
|
const newChecksum = computeChecksumString(newContent);
|
|
525
531
|
upsertManifestFile(updatedManifest, {
|
|
@@ -566,7 +572,7 @@ export async function runUpgrade(
|
|
|
566
572
|
if (conflicts.length > 0) {
|
|
567
573
|
console.log('\nDrift warnings:');
|
|
568
574
|
for (const r of conflicts) {
|
|
569
|
-
console.log(` ${r.path} — new version written to ${r.path}.
|
|
575
|
+
console.log(` ${r.path} — new version written to ${r.path}.mc-upgrade`);
|
|
570
576
|
}
|
|
571
577
|
}
|
|
572
578
|
|
|
@@ -601,7 +607,7 @@ export async function runUpgrade(
|
|
|
601
607
|
await writeScaffoldConfig(projectDir, updatedConfig);
|
|
602
608
|
|
|
603
609
|
// 10. Commit on upgrade branch
|
|
604
|
-
const branchName = `
|
|
610
|
+
const branchName = `mc-upgrade/${targetVersion}`;
|
|
605
611
|
try {
|
|
606
612
|
execSync(`git -C "${projectDir}" checkout -b "${branchName}"`, {
|
|
607
613
|
stdio: ['pipe', 'pipe', 'pipe'],
|
package/src/commands/validate.ts
CHANGED
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
import { stat } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
RegistryEntry,
|
|
5
|
+
ActionRegistryEntry,
|
|
6
|
+
} from '@mettlecast/domain-cdk-packer';
|
|
4
7
|
import { validRange } from 'semver';
|
|
5
8
|
import type { DomainModuleConfig } from '@mettlecast/domain-runtime/types';
|
|
6
9
|
import { buildRegistry } from '../builder/build-registry.js';
|
|
7
10
|
import { cliLogger } from '../utils/logger.js';
|
|
8
11
|
|
|
12
|
+
type ActionApiExposureWithDeclaration = {
|
|
13
|
+
type: 'api';
|
|
14
|
+
path: string;
|
|
15
|
+
method: string;
|
|
16
|
+
auth: 'required' | 'none' | 'service';
|
|
17
|
+
tenancy: 'required' | 'none' | 'system';
|
|
18
|
+
roles?: string[];
|
|
19
|
+
securityException?: { reason: string };
|
|
20
|
+
authDeclared?: boolean;
|
|
21
|
+
tenancyDeclared?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type ActionRegistryEntryWithDeclaration = ActionRegistryEntry & {
|
|
25
|
+
backendAccess?: 'private' | 'domain' | 'platform';
|
|
26
|
+
exposureDeclared?: boolean;
|
|
27
|
+
exposure: { type: 'internal' } | ActionApiExposureWithDeclaration;
|
|
28
|
+
};
|
|
29
|
+
|
|
9
30
|
/**
|
|
10
31
|
* A single validation failure.
|
|
11
32
|
*/
|
|
@@ -116,6 +137,199 @@ async function checkRawPathViolations(
|
|
|
116
137
|
return [];
|
|
117
138
|
}
|
|
118
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Deployment-time security checks (#4662 Task D).
|
|
142
|
+
*
|
|
143
|
+
* Mirrors the invariants enforced at CDK synth time by
|
|
144
|
+
* `SecurityAssertionAspect` in `@mettlecast/domain-cdk-packer`. Running
|
|
145
|
+
* them at the CLI stage means developers get a structured error code in
|
|
146
|
+
* their terminal and CI fails on `mc-domain-module validate` BEFORE a
|
|
147
|
+
* (potentially expensive) `cdk synth` is attempted.
|
|
148
|
+
*
|
|
149
|
+
* Each rule maps 1:1 to an aspect annotation code so downstream tooling
|
|
150
|
+
* can correlate build-time and synth-time failures.
|
|
151
|
+
*
|
|
152
|
+
* NOTE on `SECURITY_MISSING_SECURITY_EXCEPTION` for legacy `defineApi`
|
|
153
|
+
* entries: the registry builder does not propagate `securityException`
|
|
154
|
+
* from legacy source — that field only exists on action-style API
|
|
155
|
+
* exposures, which are covered by the existing `AUTH_NONE_REQUIRES_EXCEPTION`
|
|
156
|
+
* rule in `checkActionFirstSecurity`. The aspect's corresponding check
|
|
157
|
+
* (`assertAnonymousRoutesHaveException`) catches the legacy case at
|
|
158
|
+
* synth time so a developer migrating to action-style APIs gets the
|
|
159
|
+
* right guidance at the right layer.
|
|
160
|
+
*/
|
|
161
|
+
function checkDeploymentSecurity(
|
|
162
|
+
apis: import('@mettlecast/domain-cdk-packer').ApiRegistryEntry[],
|
|
163
|
+
actions: ActionRegistryEntry[],
|
|
164
|
+
): ValidationError[] {
|
|
165
|
+
const errors: ValidationError[] = [];
|
|
166
|
+
|
|
167
|
+
for (const api of apis) {
|
|
168
|
+
// Non-anonymous APIs whose path lacks the tenant placeholder cannot
|
|
169
|
+
// bind `ctx.tenant.id` from the URL. The aspect surfaces this as
|
|
170
|
+
// `SECURITY_MISSING_TENANT_PATH`; the CLI version is a faster gate so
|
|
171
|
+
// CI doesn't pay the synth cost.
|
|
172
|
+
if (api.authType !== 'none' && !api.path.includes('/v1/tenants/{tenantId}/')) {
|
|
173
|
+
errors.push({
|
|
174
|
+
code: 'SECURITY_MISSING_TENANT_PATH',
|
|
175
|
+
message: `API '${api.id}' (${api.method} ${api.path}) is JWT-protected but its path does not include the canonical '/v1/tenants/{tenantId}/' placeholder. The runtime cannot bind ctx.tenant.id from a path that lacks the placeholder.`,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Action API exposures are already covered by `checkActionFirstSecurity`
|
|
181
|
+
// for `tenancy: 'required'` paths and `auth: 'none'` exceptions. The
|
|
182
|
+
// codes there (TENANT_API_PATH_REQUIRED, AUTH_NONE_REQUIRES_EXCEPTION)
|
|
183
|
+
// are kept stable for back-compat — they map to the same aspect codes.
|
|
184
|
+
|
|
185
|
+
// Suppress the unused-actions lint while keeping the parameter shape
|
|
186
|
+
// for the future where actions gain new deployment-time invariants.
|
|
187
|
+
void actions;
|
|
188
|
+
|
|
189
|
+
return errors;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Action-first security validation rules (#4619, Wave 6 Task 6.1).
|
|
194
|
+
*
|
|
195
|
+
* These checks enforce the action-first security model on the
|
|
196
|
+
* serialised DomainRegistry produced by `buildRegistry`. The rules
|
|
197
|
+
* intentionally mirror the Zod cross-field refinements defined in
|
|
198
|
+
* `@mettlecast/domain-runtime/primitives/action` (`ApiExposureSchema`)
|
|
199
|
+
* so violations are caught at build time, not at runtime.
|
|
200
|
+
*
|
|
201
|
+
* Each rule emits a structured `ValidationError` whose `code` is the
|
|
202
|
+
* rule ID listed in the spec (e.g. `ACTION_EXPOSURE_REQUIRED`,
|
|
203
|
+
* `TENANT_API_PATH_REQUIRED`). Messages include the offending action
|
|
204
|
+
* or API id and the actionable fix.
|
|
205
|
+
*/
|
|
206
|
+
function checkActionFirstSecurity(actions: ActionRegistryEntry[]): ValidationError[] {
|
|
207
|
+
const errors: ValidationError[] = [];
|
|
208
|
+
|
|
209
|
+
for (const action of actions as ActionRegistryEntryWithDeclaration[]) {
|
|
210
|
+
const id = action.id;
|
|
211
|
+
|
|
212
|
+
// ACTION_EXPOSURE_REQUIRED — every action registry entry must have
|
|
213
|
+
// `exposure` declared in source. The builder defaults missing
|
|
214
|
+
// exposure to `{ type: 'internal' }` for backwards compatibility,
|
|
215
|
+
// and flags the entry with `exposureDeclared: false` so the validator
|
|
216
|
+
// can surface it. Legacy actions that use `visibility` only (no
|
|
217
|
+
// `backendAccess`) are also allowed to default during migration.
|
|
218
|
+
if (action.exposureDeclared === false && action.backendAccess !== 'private') {
|
|
219
|
+
// Only fire for non-private actions: a private action with no
|
|
220
|
+
// exposure is the natural migration state for legacy
|
|
221
|
+
// visibility:'private' handlers, and forcing exposure would
|
|
222
|
+
// produce noisy errors during the migration window.
|
|
223
|
+
// (When Wave 7+ removes the legacy visibility alias this branch
|
|
224
|
+
// becomes a hard error for every action.)
|
|
225
|
+
errors.push({
|
|
226
|
+
code: 'ACTION_EXPOSURE_REQUIRED',
|
|
227
|
+
message: `Action '${id}' has no explicit \`exposure\`. New-style actions must declare \`exposure\` (e.g. \`{ type: 'api', path: '...', method: 'POST', auth: 'required', tenancy: 'required' }\`) or \`{ type: 'internal' }\` to opt out of API exposure.`,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (action.exposure.type === 'api') {
|
|
232
|
+
errors.push(...checkApiExposureSecurity(action.id, action.exposure));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return errors;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Validate a single API-exposed action. Pulled out as a separate helper
|
|
241
|
+
* so each sub-rule is independently testable and the messages stay short.
|
|
242
|
+
*/
|
|
243
|
+
function checkApiExposureSecurity(actionId: string, exposure: ActionApiExposureWithDeclaration): ValidationError[] {
|
|
244
|
+
const errors: ValidationError[] = [];
|
|
245
|
+
|
|
246
|
+
// API_EXPOSURE_AUTH_REQUIRED — API-exposed actions must declare auth
|
|
247
|
+
// explicitly. The builder defaults auth to 'required' when the source
|
|
248
|
+
// omits it; the validator surfaces the omission so developers are not
|
|
249
|
+
// silently relying on the safe-default.
|
|
250
|
+
if (exposure.authDeclared === false) {
|
|
251
|
+
errors.push({
|
|
252
|
+
code: 'API_EXPOSURE_AUTH_REQUIRED',
|
|
253
|
+
message: `Action '${actionId}' has \`exposure.type: 'api'\` but \`exposure.auth\` is not declared. Set \`exposure.auth\` to 'required' | 'none' | 'service' explicitly.`,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// TENANT_API_PATH_REQUIRED — API-exposed actions with `tenancy: 'required'`
|
|
258
|
+
// must use a path that includes the canonical tenant placeholder so the
|
|
259
|
+
// runtime can bind `ctx.tenant.id` from the URL.
|
|
260
|
+
if (exposure.tenancy === 'required' && !exposure.path.includes('/v1/tenants/{tenantId}/')) {
|
|
261
|
+
errors.push({
|
|
262
|
+
code: 'TENANT_API_PATH_REQUIRED',
|
|
263
|
+
message: `Action '${actionId}' declares \`exposure.tenancy: 'required'\` but \`exposure.path\` ('${exposure.path}') does not include the canonical tenant placeholder '/v1/tenants/{tenantId}/'.`,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// AUTH_NONE_REQUIRES_EXCEPTION — `auth: 'none'` (anonymous) routes
|
|
268
|
+
// must carry an explicit `securityException` with a non-empty reason
|
|
269
|
+
// so security reviewers can audit the relaxation.
|
|
270
|
+
if (exposure.auth === 'none') {
|
|
271
|
+
const reason = exposure.securityException?.reason;
|
|
272
|
+
if (!reason || reason.trim().length === 0) {
|
|
273
|
+
errors.push({
|
|
274
|
+
code: 'AUTH_NONE_REQUIRES_EXCEPTION',
|
|
275
|
+
message: `Action '${actionId}' has \`exposure.auth: 'none'\` but no \`exposure.securityException.reason\`. Public/anonymous routes must document the security exception with a non-empty reason (and ideally a tracking reference).`,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// TENANCY_NONE_REQUIRES_REASON_WHEN_PUBLIC — `tenancy: 'none'` on a
|
|
281
|
+
// public API route must justify the missing tenant context. If the
|
|
282
|
+
// route is already justified as anonymous via `auth: 'none'` the
|
|
283
|
+
// same `securityException` may be reused; otherwise an exception is
|
|
284
|
+
// required for tenancy: 'none' on its own.
|
|
285
|
+
if (exposure.tenancy === 'none') {
|
|
286
|
+
const reason = exposure.securityException?.reason;
|
|
287
|
+
if (!reason || reason.trim().length === 0) {
|
|
288
|
+
errors.push({
|
|
289
|
+
code: 'TENANCY_NONE_REQUIRES_REASON_WHEN_PUBLIC',
|
|
290
|
+
message: `Action '${actionId}' has \`exposure.tenancy: 'none'\` but no \`exposure.securityException.reason\`. Routes without tenant context must document the security exception with a non-empty reason.`,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// SYSTEM_API_REQUIRES_ROLE — `tenancy: 'system'` combined with
|
|
296
|
+
// `auth: 'required'` must declare non-empty `roles` so the JWT
|
|
297
|
+
// authorizer can scope the call. Routes that use `auth: 'service'`
|
|
298
|
+
// are service-only and do not require role narrowing.
|
|
299
|
+
if (exposure.tenancy === 'system' && exposure.auth === 'required') {
|
|
300
|
+
if (!Array.isArray(exposure.roles) || exposure.roles.length === 0) {
|
|
301
|
+
errors.push({
|
|
302
|
+
code: 'SYSTEM_API_REQUIRES_ROLE',
|
|
303
|
+
message: `Action '${actionId}' has \`exposure.tenancy: 'system'\` and \`exposure.auth: 'required'\` but no \`exposure.roles\`. System-tenancy routes using user auth must declare at least one required role.`,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return errors;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* DEFINE_API_LEGACY_USAGE — the action-first migration replaces
|
|
313
|
+
* standalone `defineApi` calls with `defineAction` + `exposure.type:
|
|
314
|
+
* 'api'`. Every `defineApi` call still in the source produces a row in
|
|
315
|
+
* `registry.apis`; the validator emits one error per legacy API to
|
|
316
|
+
* enforce the alpha breaking migration. The validate command's fixture
|
|
317
|
+
* suite intentionally does not use `defineApi`, so this rule does not
|
|
318
|
+
* break existing unit tests.
|
|
319
|
+
*/
|
|
320
|
+
function checkDefineApiLegacyUsage(apiCount: number, apis: RegistryEntry[]): ValidationError[] {
|
|
321
|
+
if (apiCount === 0) return [];
|
|
322
|
+
const errors: ValidationError[] = [];
|
|
323
|
+
for (const api of apis) {
|
|
324
|
+
if (api.kind !== 'api') continue;
|
|
325
|
+
errors.push({
|
|
326
|
+
code: 'DEFINE_API_LEGACY_USAGE',
|
|
327
|
+
message: `API '${api.id}' uses legacy \`defineApi\`. Migrate to \`defineAction\` with \`exposure: { type: 'api', path, method, auth, tenancy }\` (#4619).`,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return errors;
|
|
331
|
+
}
|
|
332
|
+
|
|
119
333
|
/**
|
|
120
334
|
* Run the validate command: build the registry and perform structural validation.
|
|
121
335
|
* Exits the process with code 1 if validation fails (CI gate usage).
|
|
@@ -213,6 +427,17 @@ export async function runValidate(
|
|
|
213
427
|
const rawPathErrors = await checkRawPathViolations(registry.domainRoot, config);
|
|
214
428
|
errors.push(...rawPathErrors);
|
|
215
429
|
|
|
430
|
+
// Wave 6 Task 6.1: action-first security validation gates (#4619).
|
|
431
|
+
// Runs against the registry built above so the rules see the same
|
|
432
|
+
// shape the CDK packer will eventually consume.
|
|
433
|
+
errors.push(...checkActionFirstSecurity(registry.actions));
|
|
434
|
+
errors.push(...checkDefineApiLegacyUsage(registry.apis.length, registry.apis));
|
|
435
|
+
|
|
436
|
+
// Issue #4662 Task D — deployment-time security gates. These mirror
|
|
437
|
+
// the CDK synth-time `SecurityAssertionAspect` so violations are
|
|
438
|
+
// caught before any AWS deployment is attempted.
|
|
439
|
+
errors.push(...checkDeploymentSecurity(registry.apis, registry.actions));
|
|
440
|
+
|
|
216
441
|
const totalPrimitives = registry.apis.length + registry.webhooks.length +
|
|
217
442
|
registry.subscribers.length + registry.schedules.length +
|
|
218
443
|
registry.jobs.length + registry.actions.length;
|
|
@@ -68,7 +68,7 @@ function injectLineCommentHeader(
|
|
|
68
68
|
): string {
|
|
69
69
|
const header = [
|
|
70
70
|
`${commentChar} @mc-scaffold: ${moduleId}@${version}`,
|
|
71
|
-
`${commentChar} This file is managed by TIB scaffold. Manual edits will be flagged during \`
|
|
71
|
+
`${commentChar} This file is managed by TIB scaffold. Manual edits will be flagged during \`npx mc-domain-module upgrade\`.`,
|
|
72
72
|
`${commentChar} To opt out of upgrade management for this file, remove these header lines.`,
|
|
73
73
|
'',
|
|
74
74
|
].join('\n');
|
|
@@ -92,7 +92,7 @@ function injectBlockCommentHeader(
|
|
|
92
92
|
): string {
|
|
93
93
|
const header = [
|
|
94
94
|
`${open} @mc-scaffold: ${moduleId}@${version} ${close}`,
|
|
95
|
-
`${open} This file is managed by TIB scaffold. Manual edits will be flagged during \`
|
|
95
|
+
`${open} This file is managed by TIB scaffold. Manual edits will be flagged during \`npx mc-domain-module upgrade\`. ${close}`,
|
|
96
96
|
'',
|
|
97
97
|
].join('\n');
|
|
98
98
|
return header + content;
|
|
@@ -14,7 +14,7 @@ export interface InstallResult {
|
|
|
14
14
|
*
|
|
15
15
|
* - managed: always overwrite. Return 'added' if new, 'updated' if existed.
|
|
16
16
|
* - editable: check sha256. If same → 'unchanged'. If different → 'update-available'
|
|
17
|
-
* (does not write .
|
|
17
|
+
* (does not write .mc-upgrade or overwrite — user opts in via Updates tab).
|
|
18
18
|
* - seed: if file exists → 'skipped'. Else write → 'added'.
|
|
19
19
|
*/
|
|
20
20
|
export async function installScaffoldFile(
|
package/src/utils/manifest.ts
CHANGED
|
@@ -29,10 +29,9 @@ const MANIFEST_PATH = '.mc/manifest.json';
|
|
|
29
29
|
const SCHEMA_URL = 'https://mc-scaffold.s3.amazonaws.com/schema/manifest.v3.json';
|
|
30
30
|
|
|
31
31
|
export function inferPolicyFromPath(filePath: string): FilePolicy {
|
|
32
|
-
// owned: infra/,
|
|
32
|
+
// owned: infra/, mc-deploy.yml, .github/workflows/
|
|
33
33
|
if (
|
|
34
34
|
filePath.match(/^infra\//) ||
|
|
35
|
-
filePath.match(/^\.tib\/infra\//) ||
|
|
36
35
|
filePath === 'mc-deploy.yml' ||
|
|
37
36
|
filePath.match(/^\.github\/workflows\//)
|
|
38
37
|
) {
|
|
@@ -92,9 +92,9 @@ export interface ScaffoldConfig {
|
|
|
92
92
|
awsAccountId: string;
|
|
93
93
|
/** Admin email collected at scaffold time (Cognito seed user). Present only when auth module is enabled. */
|
|
94
94
|
adminEmail?: string;
|
|
95
|
-
/** Domain IDs added via `
|
|
95
|
+
/** Domain IDs added via `mc-domain-module add-domain` — NOT scaffold-owned, user-managed */
|
|
96
96
|
domainIds: string[];
|
|
97
|
-
/** Flow IDs added via `
|
|
97
|
+
/** Flow IDs added via `mc-domain-module add-flow` — user-managed */
|
|
98
98
|
flowIds: string[];
|
|
99
99
|
/** S3 bucket used for scaffold fetches (defaults to public TIB bucket) */
|
|
100
100
|
scaffoldBucket: string;
|
|
@@ -148,7 +148,7 @@ export async function readScaffoldConfig(projectRoot: string): Promise<ScaffoldC
|
|
|
148
148
|
if (!raw.scaffoldVersion) {
|
|
149
149
|
// eslint-disable-next-line no-console
|
|
150
150
|
console.warn(
|
|
151
|
-
'[
|
|
151
|
+
'[mc-domain-module] scaffold-config.json is missing scaffoldVersion — project was created before S3 scaffold migration. Run `npx mc-domain-module upgrade` to backfill.'
|
|
152
152
|
);
|
|
153
153
|
}
|
|
154
154
|
|