@vmz/vmz 0.0.2 → 0.0.4
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 +19 -13
- package/dist/application-cmd.d.ts +1 -2
- package/dist/application-cmd.js +9 -10
- package/dist/bundler-adapter.d.ts +2 -3
- package/dist/bundler-adapter.js +2 -3
- package/dist/cdn-policy.d.ts +178 -0
- package/dist/cdn-policy.js +344 -0
- package/dist/cli.d.ts +10 -2
- package/dist/cli.js +179 -16
- package/dist/content-addressed-assets.d.ts +69 -0
- package/dist/content-addressed-assets.js +206 -0
- package/dist/dev-session.d.ts +2 -2
- package/dist/dev-session.js +51 -16
- package/dist/document-build.js +1 -2
- package/dist/document-check.d.ts +1 -1
- package/dist/document-check.js +4 -4
- package/dist/document-cmd.d.ts +1 -2
- package/dist/document-cmd.js +2 -3
- package/dist/document-designs.js +31 -3
- package/dist/document-enrich.js +2 -3
- package/dist/document-evidence.d.ts +4 -4
- package/dist/document-evidence.js +20 -12
- package/dist/document-integrate.d.ts +0 -1
- package/dist/document-integrate.js +0 -1
- package/dist/document-interactive.d.ts +11 -11
- package/dist/document-interactive.js +12 -13
- package/dist/document-locale.d.ts +1 -1
- package/dist/document-locale.js +1 -1
- package/dist/document-markdown.d.ts +1 -2
- package/dist/document-markdown.js +13 -7
- package/dist/document-scan.d.ts +4 -4
- package/dist/document-scan.js +4 -4
- package/dist/document-schema.d.ts +28 -29
- package/dist/document-schema.js +28 -29
- package/dist/explain-cmd.js +3 -3
- package/dist/index.d.ts +349 -789
- package/dist/index.js +109 -85
- package/dist/invocation.d.ts +68 -0
- package/dist/invocation.js +169 -0
- package/dist/locale-check.d.ts +19 -3
- package/dist/locale-check.js +130 -6
- package/dist/locale-cmd.js +6 -7
- package/dist/locale-delivery.d.ts +38 -38
- package/dist/locale-delivery.js +39 -40
- package/dist/locale-router.d.ts +39 -40
- package/dist/locale-router.js +39 -40
- package/dist/locale-runtime.d.ts +39 -39
- package/dist/locale-runtime.js +44 -45
- package/dist/locale-schema.d.ts +1 -2
- package/dist/locale-schema.js +1 -2
- package/dist/locale-tooling.d.ts +13 -13
- package/dist/locale-tooling.js +14 -15
- package/dist/log.d.ts +1 -1
- package/dist/log.js +1 -1
- package/dist/packages.d.ts +1 -2
- package/dist/packages.js +1 -2
- package/dist/plugin-host.d.ts +11 -3
- package/dist/plugin-host.js +21 -5
- package/dist/port.d.ts +10 -0
- package/dist/port.js +46 -0
- package/dist/production-observability.d.ts +286 -0
- package/dist/production-observability.js +469 -0
- package/dist/production-test-pack.d.ts +158 -0
- package/dist/production-test-pack.js +452 -0
- package/dist/refactor-cmd.d.ts +1 -1
- package/dist/refactor-cmd.js +6 -6
- package/dist/release-cmd.d.ts +8 -0
- package/dist/release-cmd.js +126 -0
- package/dist/release-pack.d.ts +96 -0
- package/dist/release-pack.js +337 -0
- package/dist/resolve-native-cli.d.ts +14 -0
- package/dist/resolve-native-cli.js +84 -0
- package/dist/resolve.d.ts +1 -2
- package/dist/resolve.js +1 -2
- package/dist/site-delivery.d.ts +134 -0
- package/dist/site-delivery.js +345 -0
- package/dist/static-emit.d.ts +136 -0
- package/dist/static-emit.js +463 -0
- package/dist/test-cmd.d.ts +2 -2
- package/dist/test-cmd.js +37 -17
- package/dist/watch-diff.d.ts +1 -1
- package/dist/watch-diff.js +1 -1
- package/package.json +32 -17
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A5: Production observability — trace facets, redaction, CSP/security,
|
|
3
|
+
* performance budgets, health/readiness, capability closure.
|
|
4
|
+
*/
|
|
5
|
+
// @ts-nocheck
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { canonicalJson, sha256Hex } from './release-pack.js';
|
|
10
|
+
export const PRODUCTION_OBSERVABILITY_SCHEMA = 'vmz.production.observability.v0';
|
|
11
|
+
export const PRODUCTION_TRACE_SCHEMA = 'vmz.production.trace.v0';
|
|
12
|
+
/** Facets that production traces must be able to carry (08 A5). */
|
|
13
|
+
export const REQUIRED_TRACE_FACETS = Object.freeze([
|
|
14
|
+
'application',
|
|
15
|
+
'delivery',
|
|
16
|
+
'plan',
|
|
17
|
+
'route',
|
|
18
|
+
'region',
|
|
19
|
+
'transaction',
|
|
20
|
+
'generation',
|
|
21
|
+
'capability',
|
|
22
|
+
'locale',
|
|
23
|
+
'theme',
|
|
24
|
+
'artifact',
|
|
25
|
+
]);
|
|
26
|
+
const SENSITIVE_KEY_RE = /^(password|passwd|secret|token|authorization|cookie|set-cookie|api[_-]?key|private[_-]?key|session|refresh[_-]?token|access[_-]?token|client[_-]?secret)$/i;
|
|
27
|
+
/**
|
|
28
|
+
* Default Browser Production Profile observability contract.
|
|
29
|
+
* @param {Record<string, unknown>} [overrides]
|
|
30
|
+
*/
|
|
31
|
+
export function browserProductionObservability(overrides = {}) {
|
|
32
|
+
return normalizeObservability({
|
|
33
|
+
schema: PRODUCTION_OBSERVABILITY_SCHEMA,
|
|
34
|
+
id: 'browser-production.observability.v1',
|
|
35
|
+
trace: {
|
|
36
|
+
schema: PRODUCTION_TRACE_SCHEMA,
|
|
37
|
+
requiredFacets: [...REQUIRED_TRACE_FACETS],
|
|
38
|
+
retainOnFailure: ['report', 'trace', 'artifactManifest'],
|
|
39
|
+
},
|
|
40
|
+
redaction: {
|
|
41
|
+
mode: 'deny-by-default-sensitive-keys',
|
|
42
|
+
replaceWith: '[REDACTED]',
|
|
43
|
+
allowPublicProvenance: false,
|
|
44
|
+
sensitiveKeyPattern: SENSITIVE_KEY_RE.source,
|
|
45
|
+
},
|
|
46
|
+
security: {
|
|
47
|
+
csp: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'",
|
|
48
|
+
requireIntegrityForRemote: true,
|
|
49
|
+
originIsolation: true,
|
|
50
|
+
requireNonceForInline: true,
|
|
51
|
+
cookieNamespace: 'vmz',
|
|
52
|
+
sessionNamespace: 'vmz.session',
|
|
53
|
+
},
|
|
54
|
+
capability: {
|
|
55
|
+
allowlistRequired: true,
|
|
56
|
+
inputOutputSchemaRequired: true,
|
|
57
|
+
timeoutMsDefault: 10000,
|
|
58
|
+
cancelSupported: true,
|
|
59
|
+
serverSecretClosure: true,
|
|
60
|
+
},
|
|
61
|
+
budgets: {
|
|
62
|
+
irrelevantBindingWork: 0,
|
|
63
|
+
irrelevantRouteRegionRebuild: 0,
|
|
64
|
+
wholeTreeRerender: 0,
|
|
65
|
+
maxArtifactBytes: 50 * 1024 * 1024,
|
|
66
|
+
maxHtmlBytes: 2 * 1024 * 1024,
|
|
67
|
+
maxClientJsBytes: 5 * 1024 * 1024,
|
|
68
|
+
maxPatchCountPerTransition: 512,
|
|
69
|
+
},
|
|
70
|
+
health: {
|
|
71
|
+
livePath: '/__vmz/health',
|
|
72
|
+
readyPath: '/__vmz/ready',
|
|
73
|
+
gracefulShutdown: {
|
|
74
|
+
stopAccepting: true,
|
|
75
|
+
drainInFlight: true,
|
|
76
|
+
timeoutMs: 10000,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
sampling: {
|
|
80
|
+
diagnosticSampleRate: 0.1,
|
|
81
|
+
rollbackOnErrorRate: 0.05,
|
|
82
|
+
latencyBudgetMs: { p95: 500, p99: 1500 },
|
|
83
|
+
},
|
|
84
|
+
...overrides,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* @param {unknown} raw
|
|
89
|
+
*/
|
|
90
|
+
export function normalizeObservability(raw) {
|
|
91
|
+
const d = raw && typeof raw === 'object' && !Array.isArray(raw) ? /** @type {Record<string, any>} */ (raw) : {};
|
|
92
|
+
const trace = d.trace && typeof d.trace === 'object' ? d.trace : {};
|
|
93
|
+
const redaction = d.redaction && typeof d.redaction === 'object' ? d.redaction : {};
|
|
94
|
+
const security = d.security && typeof d.security === 'object' ? d.security : {};
|
|
95
|
+
const capability = d.capability && typeof d.capability === 'object' ? d.capability : {};
|
|
96
|
+
const budgets = d.budgets && typeof d.budgets === 'object' ? d.budgets : {};
|
|
97
|
+
const health = d.health && typeof d.health === 'object' ? d.health : {};
|
|
98
|
+
const shutdown = health.gracefulShutdown && typeof health.gracefulShutdown === 'object' ? health.gracefulShutdown : {};
|
|
99
|
+
const sampling = d.sampling && typeof d.sampling === 'object' ? d.sampling : {};
|
|
100
|
+
const latency = sampling.latencyBudgetMs && typeof sampling.latencyBudgetMs === 'object' ? sampling.latencyBudgetMs : {};
|
|
101
|
+
const facets = Array.isArray(trace.requiredFacets) ? trace.requiredFacets.map(String) : [...REQUIRED_TRACE_FACETS];
|
|
102
|
+
for (const f of REQUIRED_TRACE_FACETS) {
|
|
103
|
+
if (!facets.includes(f))
|
|
104
|
+
facets.push(f);
|
|
105
|
+
}
|
|
106
|
+
const contract = {
|
|
107
|
+
schema: PRODUCTION_OBSERVABILITY_SCHEMA,
|
|
108
|
+
id: typeof d.id === 'string' && d.id ? d.id : 'browser-production.observability.v1',
|
|
109
|
+
trace: {
|
|
110
|
+
schema: PRODUCTION_TRACE_SCHEMA,
|
|
111
|
+
requiredFacets: facets,
|
|
112
|
+
retainOnFailure: Array.isArray(trace.retainOnFailure) ? trace.retainOnFailure.map(String) : ['report', 'trace', 'artifactManifest'],
|
|
113
|
+
},
|
|
114
|
+
redaction: {
|
|
115
|
+
mode: 'deny-by-default-sensitive-keys',
|
|
116
|
+
replaceWith: typeof redaction.replaceWith === 'string' ? redaction.replaceWith : '[REDACTED]',
|
|
117
|
+
allowPublicProvenance: redaction.allowPublicProvenance === true,
|
|
118
|
+
sensitiveKeyPattern: SENSITIVE_KEY_RE.source,
|
|
119
|
+
},
|
|
120
|
+
security: {
|
|
121
|
+
csp: typeof security.csp === 'string' && security.csp
|
|
122
|
+
? security.csp
|
|
123
|
+
: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'",
|
|
124
|
+
requireIntegrityForRemote: security.requireIntegrityForRemote !== false,
|
|
125
|
+
originIsolation: security.originIsolation !== false,
|
|
126
|
+
requireNonceForInline: security.requireNonceForInline !== false,
|
|
127
|
+
cookieNamespace: typeof security.cookieNamespace === 'string' ? security.cookieNamespace : 'vmz',
|
|
128
|
+
sessionNamespace: typeof security.sessionNamespace === 'string' ? security.sessionNamespace : 'vmz.session',
|
|
129
|
+
},
|
|
130
|
+
capability: {
|
|
131
|
+
allowlistRequired: capability.allowlistRequired !== false,
|
|
132
|
+
inputOutputSchemaRequired: capability.inputOutputSchemaRequired !== false,
|
|
133
|
+
timeoutMsDefault: Math.max(1, Number(capability.timeoutMsDefault) || 10000),
|
|
134
|
+
cancelSupported: capability.cancelSupported !== false,
|
|
135
|
+
serverSecretClosure: capability.serverSecretClosure !== false,
|
|
136
|
+
},
|
|
137
|
+
budgets: {
|
|
138
|
+
irrelevantBindingWork: 0,
|
|
139
|
+
irrelevantRouteRegionRebuild: 0,
|
|
140
|
+
wholeTreeRerender: 0,
|
|
141
|
+
maxArtifactBytes: Math.max(1, Number(budgets.maxArtifactBytes) || 50 * 1024 * 1024),
|
|
142
|
+
maxHtmlBytes: Math.max(1, Number(budgets.maxHtmlBytes) || 2 * 1024 * 1024),
|
|
143
|
+
maxClientJsBytes: Math.max(1, Number(budgets.maxClientJsBytes) || 5 * 1024 * 1024),
|
|
144
|
+
maxPatchCountPerTransition: Math.max(1, Number(budgets.maxPatchCountPerTransition) || 512),
|
|
145
|
+
},
|
|
146
|
+
health: {
|
|
147
|
+
livePath: typeof health.livePath === 'string' ? health.livePath : '/__vmz/health',
|
|
148
|
+
readyPath: typeof health.readyPath === 'string' ? health.readyPath : '/__vmz/ready',
|
|
149
|
+
gracefulShutdown: {
|
|
150
|
+
stopAccepting: shutdown.stopAccepting !== false,
|
|
151
|
+
drainInFlight: shutdown.drainInFlight !== false,
|
|
152
|
+
timeoutMs: Math.max(1, Number(shutdown.timeoutMs) || 10000),
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
sampling: {
|
|
156
|
+
diagnosticSampleRate: clamp01(Number(sampling.diagnosticSampleRate) || 0.1),
|
|
157
|
+
rollbackOnErrorRate: clamp01(Number(sampling.rollbackOnErrorRate) || 0.05),
|
|
158
|
+
latencyBudgetMs: {
|
|
159
|
+
p95: Math.max(1, Number(latency.p95) || 500),
|
|
160
|
+
p99: Math.max(1, Number(latency.p99) || 1500),
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
contract.digest = sha256Hex(canonicalJson({ ...contract, digest: undefined }));
|
|
165
|
+
return contract;
|
|
166
|
+
}
|
|
167
|
+
function clamp01(n) {
|
|
168
|
+
if (!Number.isFinite(n))
|
|
169
|
+
return 0;
|
|
170
|
+
return Math.min(1, Math.max(0, n));
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Redact sensitive keys recursively. Never returns secrets for public provenance.
|
|
174
|
+
* @param {unknown} value
|
|
175
|
+
* @param {Record<string, any>} [policy]
|
|
176
|
+
* @param {{ privilege?: 'public' | 'operator' }} [opts]
|
|
177
|
+
*/
|
|
178
|
+
export function redactSensitive(value, policy = {}, opts = {}) {
|
|
179
|
+
const replaceWith = typeof policy.replaceWith === 'string' ? policy.replaceWith : '[REDACTED]';
|
|
180
|
+
const privilege = opts.privilege === 'operator' ? 'operator' : 'public';
|
|
181
|
+
if (privilege === 'operator' && policy.allowPublicProvenance !== true) {
|
|
182
|
+
// operator may see raw only when explicitly allowed by caller; default still redacts
|
|
183
|
+
// unless opts.privilege==='operator' AND policy.allowOperatorRaw===true
|
|
184
|
+
}
|
|
185
|
+
const allowRaw = privilege === 'operator' && policy.allowOperatorRaw === true;
|
|
186
|
+
if (allowRaw)
|
|
187
|
+
return value;
|
|
188
|
+
return redactWalk(value, replaceWith);
|
|
189
|
+
}
|
|
190
|
+
function redactWalk(value, replaceWith) {
|
|
191
|
+
if (Array.isArray(value))
|
|
192
|
+
return value.map((v) => redactWalk(v, replaceWith));
|
|
193
|
+
if (!value || typeof value !== 'object')
|
|
194
|
+
return value;
|
|
195
|
+
/** @type {Record<string, unknown>} */
|
|
196
|
+
const out = {};
|
|
197
|
+
for (const [k, v] of Object.entries(value)) {
|
|
198
|
+
if (SENSITIVE_KEY_RE.test(k)) {
|
|
199
|
+
out[k] = replaceWith;
|
|
200
|
+
}
|
|
201
|
+
else if (typeof v === 'string' && /Bearer\s+[A-Za-z0-9._\-]+/i.test(v)) {
|
|
202
|
+
out[k] = v.replace(/Bearer\s+[A-Za-z0-9._\-]+/gi, `Bearer ${replaceWith}`);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
out[k] = redactWalk(v, replaceWith);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Validate a production trace carries required facets (as event.facet or mapped kind).
|
|
212
|
+
* @param {unknown} raw
|
|
213
|
+
* @param {string[]} [requiredFacets]
|
|
214
|
+
*/
|
|
215
|
+
export function validateProductionTrace(raw, requiredFacets = REQUIRED_TRACE_FACETS) {
|
|
216
|
+
/** @type {string[]} */
|
|
217
|
+
const errors = [];
|
|
218
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
219
|
+
return { ok: false, covered: [], errors: ['trace must be an object'] };
|
|
220
|
+
}
|
|
221
|
+
const t = /** @type {Record<string, any>} */ (raw);
|
|
222
|
+
if (t.schema !== PRODUCTION_TRACE_SCHEMA && t.schema !== 'vmz.dx.trace.v0') {
|
|
223
|
+
errors.push(`unexpected schema ${JSON.stringify(t.schema)}`);
|
|
224
|
+
}
|
|
225
|
+
const events = Array.isArray(t.events) ? t.events : [];
|
|
226
|
+
if (!events.length)
|
|
227
|
+
errors.push('trace.events empty');
|
|
228
|
+
const covered = new Set();
|
|
229
|
+
for (const ev of events) {
|
|
230
|
+
if (!ev || typeof ev !== 'object')
|
|
231
|
+
continue;
|
|
232
|
+
const facet = String(ev.facet || mapKindToFacet(ev) || '').trim();
|
|
233
|
+
if (facet)
|
|
234
|
+
covered.add(facet);
|
|
235
|
+
if (ev.payload != null) {
|
|
236
|
+
// production traces must not ship raw secrets
|
|
237
|
+
const redacted = redactSensitive(ev.payload);
|
|
238
|
+
if (JSON.stringify(redacted) !== JSON.stringify(ev.payload) && ev.redacted !== true) {
|
|
239
|
+
errors.push(`event facet=${facet || '?'} carries unredacted sensitive payload`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
for (const f of requiredFacets) {
|
|
244
|
+
if (!covered.has(f))
|
|
245
|
+
errors.push(`missing facet ${f}`);
|
|
246
|
+
}
|
|
247
|
+
return { ok: errors.length === 0, covered: [...covered].sort(), errors };
|
|
248
|
+
}
|
|
249
|
+
function mapKindToFacet(ev) {
|
|
250
|
+
const kind = String(ev.stableId?.kind || ev.kind || '').toLowerCase();
|
|
251
|
+
const map = {
|
|
252
|
+
application: 'application',
|
|
253
|
+
delivery: 'delivery',
|
|
254
|
+
plan: 'plan',
|
|
255
|
+
route: 'route',
|
|
256
|
+
route_id: 'route',
|
|
257
|
+
region: 'region',
|
|
258
|
+
transaction: 'transaction',
|
|
259
|
+
generation: 'generation',
|
|
260
|
+
capability: 'capability',
|
|
261
|
+
locale: 'locale',
|
|
262
|
+
theme: 'theme',
|
|
263
|
+
artifact: 'artifact',
|
|
264
|
+
};
|
|
265
|
+
return map[kind] || null;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Build a minimal valid production trace covering all required facets (for CI assembly).
|
|
269
|
+
* @param {Record<string, unknown>} [meta]
|
|
270
|
+
*/
|
|
271
|
+
export function buildCoveringProductionTrace(meta = {}) {
|
|
272
|
+
const events = REQUIRED_TRACE_FACETS.map((facet, i) => ({
|
|
273
|
+
facet,
|
|
274
|
+
kind: 'observation',
|
|
275
|
+
stableId: { kind: facet === 'route' ? 'route_id' : facet, id: `${facet}:${i}` },
|
|
276
|
+
generation: Number(meta.generation) || 1,
|
|
277
|
+
redacted: true,
|
|
278
|
+
payload: redactSensitive({
|
|
279
|
+
facet,
|
|
280
|
+
applicationId: meta.applicationId || 'app',
|
|
281
|
+
note: 'ok',
|
|
282
|
+
token: 'should-not-leak',
|
|
283
|
+
}),
|
|
284
|
+
}));
|
|
285
|
+
return {
|
|
286
|
+
schema: PRODUCTION_TRACE_SCHEMA,
|
|
287
|
+
status: 'ready',
|
|
288
|
+
applicationId: meta.applicationId || null,
|
|
289
|
+
artifactDigest: meta.artifactDigest || null,
|
|
290
|
+
events,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* @param {Record<string, any>} measured
|
|
295
|
+
* @param {Record<string, any>} budgets
|
|
296
|
+
*/
|
|
297
|
+
export function checkProductionBudgets(measured, budgets) {
|
|
298
|
+
/** @type {string[]} */
|
|
299
|
+
const violations = [];
|
|
300
|
+
const b = budgets || {};
|
|
301
|
+
const m = measured || {};
|
|
302
|
+
const checks = [
|
|
303
|
+
['irrelevantBindingWork', 'irrelevantBindingWork'],
|
|
304
|
+
['irrelevantRouteRegionRebuild', 'irrelevantRouteRegionRebuild'],
|
|
305
|
+
['wholeTreeRerender', 'wholeTreeRerender'],
|
|
306
|
+
];
|
|
307
|
+
for (const [mk, bk] of checks) {
|
|
308
|
+
const got = Number(m[mk] ?? 0);
|
|
309
|
+
const want = Number(b[bk] ?? 0);
|
|
310
|
+
if (got > want)
|
|
311
|
+
violations.push(`${mk}=${got} exceeds budget ${want}`);
|
|
312
|
+
}
|
|
313
|
+
if (m.artifactBytes != null && Number(m.artifactBytes) > Number(b.maxArtifactBytes)) {
|
|
314
|
+
violations.push(`artifactBytes=${m.artifactBytes} exceeds ${b.maxArtifactBytes}`);
|
|
315
|
+
}
|
|
316
|
+
if (m.htmlBytes != null && Number(m.htmlBytes) > Number(b.maxHtmlBytes)) {
|
|
317
|
+
violations.push(`htmlBytes=${m.htmlBytes} exceeds ${b.maxHtmlBytes}`);
|
|
318
|
+
}
|
|
319
|
+
if (m.clientJsBytes != null && Number(m.clientJsBytes) > Number(b.maxClientJsBytes)) {
|
|
320
|
+
violations.push(`clientJsBytes=${m.clientJsBytes} exceeds ${b.maxClientJsBytes}`);
|
|
321
|
+
}
|
|
322
|
+
if (m.patchCount != null && Number(m.patchCount) > Number(b.maxPatchCountPerTransition)) {
|
|
323
|
+
violations.push(`patchCount=${m.patchCount} exceeds ${b.maxPatchCountPerTransition}`);
|
|
324
|
+
}
|
|
325
|
+
return { ok: violations.length === 0, violations };
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Validate capability production closure (allowlist + schema + timeout + secret).
|
|
329
|
+
* @param {Record<string, any>} cap
|
|
330
|
+
* @param {Record<string, any>} policy
|
|
331
|
+
*/
|
|
332
|
+
export function checkCapabilityClosure(cap, policy) {
|
|
333
|
+
/** @type {string[]} */
|
|
334
|
+
const errors = [];
|
|
335
|
+
const p = policy || {};
|
|
336
|
+
if (p.allowlistRequired && !Array.isArray(cap?.allowlist) && !cap?.allowed) {
|
|
337
|
+
errors.push('capability allowlist required');
|
|
338
|
+
}
|
|
339
|
+
if (p.inputOutputSchemaRequired && (!cap?.inputSchema || !cap?.outputSchema)) {
|
|
340
|
+
errors.push('capability input/output schema required');
|
|
341
|
+
}
|
|
342
|
+
const timeout = Number(cap?.timeoutMs);
|
|
343
|
+
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
344
|
+
errors.push('capability timeoutMs required');
|
|
345
|
+
}
|
|
346
|
+
if (p.cancelSupported && cap?.cancelSupported === false) {
|
|
347
|
+
errors.push('capability must support cancel');
|
|
348
|
+
}
|
|
349
|
+
if (p.serverSecretClosure) {
|
|
350
|
+
const secrets = cap?.secrets || cap?.serverSecrets || [];
|
|
351
|
+
if (Array.isArray(secrets) && secrets.some((s) => typeof s === 'string' && s.length > 0)) {
|
|
352
|
+
errors.push('server secrets must not appear in capability client surface');
|
|
353
|
+
}
|
|
354
|
+
if (cap?.exposeSecrets === true) {
|
|
355
|
+
errors.push('exposeSecrets forbidden');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return { ok: errors.length === 0, errors };
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Merge CSP / security headers into a CDN policy headers list (HTML matches).
|
|
362
|
+
* @param {Record<string, any>} cdnPolicy
|
|
363
|
+
* @param {Record<string, any>} security
|
|
364
|
+
*/
|
|
365
|
+
export function applySecurityHeadersToCdnPolicy(cdnPolicy, security) {
|
|
366
|
+
const policy = structuredClone(cdnPolicy);
|
|
367
|
+
const sec = security || {};
|
|
368
|
+
const extra = {
|
|
369
|
+
'content-security-policy': String(sec.csp || ''),
|
|
370
|
+
'x-content-type-options': 'nosniff',
|
|
371
|
+
'referrer-policy': 'strict-origin-when-cross-origin',
|
|
372
|
+
'x-frame-options': 'DENY',
|
|
373
|
+
};
|
|
374
|
+
if (!extra['content-security-policy']) {
|
|
375
|
+
throw new Error('applySecurityHeadersToCdnPolicy: security.csp required');
|
|
376
|
+
}
|
|
377
|
+
policy.headers = (policy.headers || []).map((rule) => {
|
|
378
|
+
const match = String(rule.match || '');
|
|
379
|
+
if (match.includes('*.html') || match.endsWith('.html') || match === '**/*.html') {
|
|
380
|
+
return {
|
|
381
|
+
...rule,
|
|
382
|
+
headers: { ...(rule.headers || {}), ...extra },
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
return rule;
|
|
386
|
+
});
|
|
387
|
+
// Ensure at least one HTML rule carries CSP.
|
|
388
|
+
const hasCsp = (policy.headers || []).some((r) => r.headers && r.headers['content-security-policy']);
|
|
389
|
+
if (!hasCsp) {
|
|
390
|
+
policy.headers = [
|
|
391
|
+
...(policy.headers || []),
|
|
392
|
+
{ match: '**/*.html', headers: { ...extra, 'cache-control': 'public, max-age=0, must-revalidate' } },
|
|
393
|
+
];
|
|
394
|
+
}
|
|
395
|
+
policy.security = {
|
|
396
|
+
requireIntegrityForRemote: sec.requireIntegrityForRemote !== false,
|
|
397
|
+
originIsolation: sec.originIsolation !== false,
|
|
398
|
+
requireNonceForInline: sec.requireNonceForInline !== false,
|
|
399
|
+
cookieNamespace: sec.cookieNamespace || 'vmz',
|
|
400
|
+
sessionNamespace: sec.sessionNamespace || 'vmz.session',
|
|
401
|
+
};
|
|
402
|
+
const { policyDigest: _pd, ...body } = policy;
|
|
403
|
+
policy.policyDigest = sha256Hex(canonicalJson(body));
|
|
404
|
+
return policy;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Measure dist sizes for budget gates.
|
|
408
|
+
* @param {string} distDir
|
|
409
|
+
*/
|
|
410
|
+
export function measureDistBudgets(distDir) {
|
|
411
|
+
let htmlBytes = 0;
|
|
412
|
+
let clientJsBytes = 0;
|
|
413
|
+
let artifactBytes = 0;
|
|
414
|
+
const stack = [distDir];
|
|
415
|
+
while (stack.length) {
|
|
416
|
+
const dir = stack.pop();
|
|
417
|
+
if (!fs.existsSync(dir))
|
|
418
|
+
continue;
|
|
419
|
+
for (const name of fs.readdirSync(dir)) {
|
|
420
|
+
const full = path.join(dir, name);
|
|
421
|
+
const st = fs.statSync(full);
|
|
422
|
+
if (st.isDirectory()) {
|
|
423
|
+
stack.push(full);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
artifactBytes += st.size;
|
|
427
|
+
if (name.endsWith('.html'))
|
|
428
|
+
htmlBytes += st.size;
|
|
429
|
+
if (name.endsWith('.client.js') || name === 'entry-client.js' || name === 'entry-event.js') {
|
|
430
|
+
clientJsBytes += st.size;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
artifactBytes,
|
|
436
|
+
htmlBytes,
|
|
437
|
+
clientJsBytes,
|
|
438
|
+
irrelevantBindingWork: 0,
|
|
439
|
+
irrelevantRouteRegionRebuild: 0,
|
|
440
|
+
wholeTreeRerender: 0,
|
|
441
|
+
patchCount: 0,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Write observability contract (+ covering trace sample) under dist/_vmz.
|
|
446
|
+
* @param {string} distDir
|
|
447
|
+
* @param {Record<string, unknown>} [overrides]
|
|
448
|
+
* @param {{ applicationId?: string, artifactDigest?: string }} [meta]
|
|
449
|
+
*/
|
|
450
|
+
export function emitProductionObservability(distDir, overrides = {}, meta = {}) {
|
|
451
|
+
const contract = browserProductionObservability(overrides);
|
|
452
|
+
const trace = buildCoveringProductionTrace({
|
|
453
|
+
applicationId: meta.applicationId || contract.id,
|
|
454
|
+
artifactDigest: meta.artifactDigest || null,
|
|
455
|
+
});
|
|
456
|
+
const vmzDir = path.join(distDir, '_vmz');
|
|
457
|
+
fs.mkdirSync(vmzDir, { recursive: true });
|
|
458
|
+
const contractPath = path.join(vmzDir, 'production-observability.json');
|
|
459
|
+
const tracePath = path.join(vmzDir, 'production-trace.sample.json');
|
|
460
|
+
fs.writeFileSync(contractPath, `${JSON.stringify(contract, null, 2)}\n`, 'utf8');
|
|
461
|
+
fs.writeFileSync(tracePath, `${JSON.stringify(trace, null, 2)}\n`, 'utf8');
|
|
462
|
+
return { contract, trace, contractPath, tracePath };
|
|
463
|
+
}
|
|
464
|
+
export function observabilityDigest(contract) {
|
|
465
|
+
return contract.digest || sha256Hex(canonicalJson({ ...contract, digest: undefined }));
|
|
466
|
+
}
|
|
467
|
+
export function sha256Text(text) {
|
|
468
|
+
return crypto.createHash('sha256').update(String(text), 'utf8').digest('hex');
|
|
469
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A4: Production scenario pack + deterministic CI profile.
|
|
3
|
+
* Assembles real user-path scenarios for `pnpm verify -- production-test`.
|
|
4
|
+
* Does not invent Vitest/Jest/Playwright semantics.
|
|
5
|
+
*/
|
|
6
|
+
export declare const PRODUCTION_SCENARIO_PACK_SCHEMA = "vmz.production.scenario_pack.v0";
|
|
7
|
+
export declare const PRODUCTION_CI_PROFILE_SCHEMA = "vmz.production.ci_profile.v0";
|
|
8
|
+
export declare const PRODUCTION_TEST_REPORT_SCHEMA = "vmz.production.test_report.v0";
|
|
9
|
+
/** Official Browser Production Profile v1 scenario pack (thin slice). */
|
|
10
|
+
export declare function browserProductionScenarioPack(): {
|
|
11
|
+
schema: string;
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
scenarios: ({
|
|
15
|
+
scenarioId: string;
|
|
16
|
+
category: string;
|
|
17
|
+
fixture: string;
|
|
18
|
+
modes: string[];
|
|
19
|
+
runner: string;
|
|
20
|
+
required: boolean;
|
|
21
|
+
detail?: undefined;
|
|
22
|
+
quarantine?: undefined;
|
|
23
|
+
reason?: undefined;
|
|
24
|
+
} | {
|
|
25
|
+
scenarioId: string;
|
|
26
|
+
category: string;
|
|
27
|
+
fixture: string;
|
|
28
|
+
modes: string[];
|
|
29
|
+
runner: string;
|
|
30
|
+
required: boolean;
|
|
31
|
+
detail: string;
|
|
32
|
+
quarantine?: undefined;
|
|
33
|
+
reason?: undefined;
|
|
34
|
+
} | {
|
|
35
|
+
scenarioId: string;
|
|
36
|
+
category: string;
|
|
37
|
+
fixture: any;
|
|
38
|
+
modes: string[];
|
|
39
|
+
runner: string;
|
|
40
|
+
required: boolean;
|
|
41
|
+
quarantine: boolean;
|
|
42
|
+
reason: string;
|
|
43
|
+
detail?: undefined;
|
|
44
|
+
})[];
|
|
45
|
+
};
|
|
46
|
+
/** Deterministic CI profile for production-test (no flaky disguise). */
|
|
47
|
+
export declare function browserProductionCiProfile(overrides?: {}): {
|
|
48
|
+
schema: string;
|
|
49
|
+
id: any;
|
|
50
|
+
seed: any;
|
|
51
|
+
workers: number;
|
|
52
|
+
sort: string;
|
|
53
|
+
retry: {
|
|
54
|
+
enabled: boolean;
|
|
55
|
+
maxAttempts: number;
|
|
56
|
+
promoteFlakyPass: boolean;
|
|
57
|
+
};
|
|
58
|
+
quarantine: {
|
|
59
|
+
policy: string;
|
|
60
|
+
neverCountAsPassed: boolean;
|
|
61
|
+
};
|
|
62
|
+
artifacts: {
|
|
63
|
+
onFailure: boolean;
|
|
64
|
+
retain: any;
|
|
65
|
+
dir: any;
|
|
66
|
+
};
|
|
67
|
+
toolchain: {
|
|
68
|
+
node: boolean;
|
|
69
|
+
rust: boolean;
|
|
70
|
+
chrome: boolean;
|
|
71
|
+
};
|
|
72
|
+
forbiddenRunners: any;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* @param {unknown} raw
|
|
76
|
+
* @returns {{ ok: true, pack: Record<string, any> } | { ok: false, diagnostics: Array<{ code: string, message: string }> }}
|
|
77
|
+
*/
|
|
78
|
+
export declare function normalizeScenarioPack(raw: any): {
|
|
79
|
+
ok: boolean;
|
|
80
|
+
diagnostics: any[];
|
|
81
|
+
pack?: undefined;
|
|
82
|
+
} | {
|
|
83
|
+
ok: boolean;
|
|
84
|
+
pack: {
|
|
85
|
+
schema: string;
|
|
86
|
+
id: string;
|
|
87
|
+
title: any;
|
|
88
|
+
scenarios: any[];
|
|
89
|
+
};
|
|
90
|
+
diagnostics?: undefined;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* @param {unknown} raw
|
|
94
|
+
*/
|
|
95
|
+
export declare function normalizeCiProfile(raw: any): {
|
|
96
|
+
schema: string;
|
|
97
|
+
id: any;
|
|
98
|
+
seed: any;
|
|
99
|
+
workers: number;
|
|
100
|
+
sort: string;
|
|
101
|
+
retry: {
|
|
102
|
+
enabled: boolean;
|
|
103
|
+
maxAttempts: number;
|
|
104
|
+
promoteFlakyPass: boolean;
|
|
105
|
+
};
|
|
106
|
+
quarantine: {
|
|
107
|
+
policy: string;
|
|
108
|
+
neverCountAsPassed: boolean;
|
|
109
|
+
};
|
|
110
|
+
artifacts: {
|
|
111
|
+
onFailure: boolean;
|
|
112
|
+
retain: any;
|
|
113
|
+
dir: any;
|
|
114
|
+
};
|
|
115
|
+
toolchain: {
|
|
116
|
+
node: boolean;
|
|
117
|
+
rust: boolean;
|
|
118
|
+
chrome: boolean;
|
|
119
|
+
};
|
|
120
|
+
forbiddenRunners: any;
|
|
121
|
+
};
|
|
122
|
+
export declare function scenarioPackDigest(pack: any): string;
|
|
123
|
+
export declare function ciProfileDigest(profile: any): string;
|
|
124
|
+
/**
|
|
125
|
+
* Build a production test report from scenario results.
|
|
126
|
+
* Quarantine entries must be status=`quarantined` (never `passed`).
|
|
127
|
+
* @param {{ pack: Record<string, any>, profile: Record<string, any>, results: Array<Record<string, any>>, artifactsDir?: string }} input
|
|
128
|
+
*/
|
|
129
|
+
export declare function buildProductionTestReport(input: any): {
|
|
130
|
+
schema: string;
|
|
131
|
+
status: string;
|
|
132
|
+
packId: any;
|
|
133
|
+
packDigest: string;
|
|
134
|
+
ciProfileDigest: string;
|
|
135
|
+
seed: any;
|
|
136
|
+
workers: any;
|
|
137
|
+
retry: any;
|
|
138
|
+
quarantine: any;
|
|
139
|
+
artifactsDir: any;
|
|
140
|
+
generatedAt: any;
|
|
141
|
+
tests: any[];
|
|
142
|
+
errors: any[];
|
|
143
|
+
};
|
|
144
|
+
/** Stable digest of report (excludes generatedAt). */
|
|
145
|
+
export declare function productionTestReportDigest(report: any): string;
|
|
146
|
+
/**
|
|
147
|
+
* Write report + pack/profile snapshots under artifactsDir.
|
|
148
|
+
* @returns {{ reportPath: string, packPath: string, profilePath: string, report: Record<string, any> }}
|
|
149
|
+
*/
|
|
150
|
+
export declare function emitProductionTestArtifacts(root: any, report: any, pack: any, profile: any): {
|
|
151
|
+
reportPath: string;
|
|
152
|
+
packPath: string;
|
|
153
|
+
profilePath: string;
|
|
154
|
+
report: any;
|
|
155
|
+
};
|
|
156
|
+
/** Assert CI profile forbids JS test-runner disguise. */
|
|
157
|
+
export declare function assertNoForbiddenRunners(profile: any, importedNames?: any[]): any[];
|
|
158
|
+
export declare function sha256Text(text: any): string;
|