@mriekki/project-forge 4.26.1 → 4.27.1
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/assets/authentication/distribution.json +90 -0
- package/assets/components.json +17 -3
- package/assets/core/distribution.json +12 -0
- package/assets/deployment/distribution.json +292 -68
- package/assets/generated/ASSEMBLY.json +150 -4
- package/assets/generated/authentication/docs/AUTH_RECOVERY_RETENTION.md +101 -0
- package/assets/generated/authentication/docs/HOUSEHOLD_INFRASTRUCTURE_ADOPTION.md +55 -0
- package/assets/generated/authentication/docs/HOUSEHOLD_REGISTRY.md +246 -0
- package/assets/generated/authentication/docs/PROJECT_AUTH_REFERENCE.md +878 -0
- package/assets/generated/authentication/samples/ProjectForgeAuth.Api/Program.cs +40 -0
- package/assets/generated/authentication/samples/ProjectForgeAuth.Api/ProjectForgeAuth.Api.csproj +13 -0
- package/assets/generated/authentication/samples/ProjectForgeAuth.Api/wwwroot/auth.js +65 -0
- package/assets/generated/authentication/samples/ProjectForgeAuth.Api/wwwroot/index.html +44 -0
- package/assets/generated/authentication/src/ProjectForge.Auth/AdmissionPolicy.cs +167 -0
- package/assets/generated/authentication/src/ProjectForge.Auth/AuthenticationDiagnostics.cs +159 -0
- package/assets/generated/authentication/src/ProjectForge.Auth/ProjectForge.Auth.csproj +9 -0
- package/assets/generated/authentication/src/ProjectForge.Auth/PwaExistingProjectCompatibility.cs +64 -0
- package/assets/generated/authentication/src/ProjectForge.Auth.Runtime/ProjectAuthStore.cs +471 -0
- package/assets/generated/authentication/src/ProjectForge.Auth.Runtime/ProjectForge.Auth.Runtime.csproj +15 -0
- package/assets/generated/authentication/src/ProjectForge.Auth.Runtime/ProjectForgeAuthRuntimeExtensions.cs +472 -0
- package/assets/generated/authentication/src/ProjectForge.Auth.Runtime/ResendProjectInvitationDelivery.cs +105 -0
- package/assets/generated/authentication/src/ProjectForge.Transport/ProjectForge.Transport.csproj +11 -0
- package/assets/generated/authentication/src/ProjectForge.Transport/StrictPublicOriginTransport.cs +95 -0
- package/assets/generated/authentication/tools/household_connect.py +145 -0
- package/assets/generated/authentication/tools/project_auth_recovery.py +716 -0
- package/assets/generated/core/core/process/ENGINEERING_BACKLOG.md +150 -0
- package/assets/generated/core/core/skills/manage-backlog/SKILL.md +115 -0
- package/assets/generated/core/core/skills/review-user-feedback/SKILL.md +50 -0
- package/assets/generated/deployment/docs/RECOVERY_READINESS.md +103 -0
- package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_capture.py +164 -0
- package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_guard.py +225 -0
- package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_lifecycle.py +455 -0
- package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_retention.py +220 -0
- package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_schedule.py +489 -0
- package/package.json +1 -1
- package/src/cli.mjs +32 -3
- package/src/engine.mjs +87 -5
package/src/engine.mjs
CHANGED
|
@@ -20,6 +20,28 @@ export function resolveUnder(root, relative, label = 'path') {
|
|
|
20
20
|
const base = path.resolve(root);
|
|
21
21
|
const candidate = path.resolve(base, ...rel.split('/'));
|
|
22
22
|
if (candidate !== base && !candidate.startsWith(base + path.sep)) throw new Error(`${label} escapes root: ${relative}`);
|
|
23
|
+
|
|
24
|
+
// Lexical containment alone does not establish filesystem containment.
|
|
25
|
+
// A managed destination parent, package source parent, or even a dangling
|
|
26
|
+
// leaf symlink can redirect read/copy/delete operations outside the root.
|
|
27
|
+
// lstat (not exists/stat) also detects dangling links. Missing directories
|
|
28
|
+
// are allowed for first install, and are created only during apply.
|
|
29
|
+
let current = base;
|
|
30
|
+
const parts = [base, ...rel.split('/')];
|
|
31
|
+
for (const [index, part] of parts.entries()) {
|
|
32
|
+
if (index > 0) current = path.join(current, part);
|
|
33
|
+
let stat;
|
|
34
|
+
try {
|
|
35
|
+
stat = fs.lstatSync(current);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error.code === 'ENOENT') continue;
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
if (stat.isSymbolicLink()) throw new Error(`Unsafe ${label}: symlink in managed path ${relative}`);
|
|
41
|
+
if (index < parts.length - 1 && !stat.isDirectory()) {
|
|
42
|
+
throw new Error(`Unsafe ${label}: non-directory ancestor in ${relative}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
23
45
|
return candidate;
|
|
24
46
|
}
|
|
25
47
|
|
|
@@ -30,6 +52,7 @@ export function sha256File(file) {
|
|
|
30
52
|
function baselineFile(componentId) {
|
|
31
53
|
if (componentId === 'core') return 'CORE_BASELINE.json';
|
|
32
54
|
if (componentId === 'deployment') return 'DEPLOYMENT_BASELINE.json';
|
|
55
|
+
if (componentId === 'authentication') return 'AUTHENTICATION_BASELINE.json';
|
|
33
56
|
throw new Error(`Unsupported component: ${componentId}`);
|
|
34
57
|
}
|
|
35
58
|
|
|
@@ -120,7 +143,7 @@ export function planComponent(packageData, componentId, targetRoot, acceptedReti
|
|
|
120
143
|
if (!component) throw new Error(`Package does not define component ${componentId}`);
|
|
121
144
|
componentRevision(component);
|
|
122
145
|
const filename = baselineFile(componentId);
|
|
123
|
-
const baseline = loadOptionalJson(
|
|
146
|
+
const baseline = loadOptionalJson(resolveUnder(targetRoot, filename, 'component baseline'));
|
|
124
147
|
validateBaselineIdentity(baseline, componentId, component, filename);
|
|
125
148
|
|
|
126
149
|
const entries = componentEntries(packageData, componentId);
|
|
@@ -197,6 +220,15 @@ export function planProfile(packageData, profileName, targetRoot, acceptedRetire
|
|
|
197
220
|
|
|
198
221
|
export function applyProfile(packageData, profilePlan, targetRoot, consumer, consumerBaseline) {
|
|
199
222
|
if (profilePlan.unsafe.length) throw new Error('Refusing apply: plan contains unsafe drift/conflicts');
|
|
223
|
+
// Preflight the complete write set before changing any managed files.
|
|
224
|
+
// Baselines must not be allowed to follow a symlink outside the Project
|
|
225
|
+
// after an otherwise safe plan was produced.
|
|
226
|
+
for (const plan of profilePlan.plans) {
|
|
227
|
+
resolveUnder(targetRoot, plan.filename, 'component baseline');
|
|
228
|
+
for (const row of plan.rows) resolveUnder(targetRoot, row.path, 'consumer destination');
|
|
229
|
+
for (const retired of plan.retired) resolveUnder(targetRoot, retired.path, 'retired destination');
|
|
230
|
+
}
|
|
231
|
+
resolveUnder(targetRoot, 'PROJECT_FORGE_BASELINE.json', 'umbrella baseline');
|
|
200
232
|
for (const plan of profilePlan.plans) {
|
|
201
233
|
for (const row of plan.rows) {
|
|
202
234
|
if (!['ADD', 'CORE_UPDATE', 'KIT_UPDATE'].includes(row.state)) continue;
|
|
@@ -210,7 +242,7 @@ export function applyProfile(packageData, profilePlan, targetRoot, consumer, con
|
|
|
210
242
|
}
|
|
211
243
|
for (const plan of profilePlan.plans) {
|
|
212
244
|
const baseline = buildComponentBaseline(plan, consumer, consumerBaseline);
|
|
213
|
-
fs.writeFileSync(
|
|
245
|
+
fs.writeFileSync(resolveUnder(targetRoot, plan.filename, 'component baseline'), JSON.stringify(baseline, null, 2) + '\n');
|
|
214
246
|
}
|
|
215
247
|
const projectForgeBaseline = {
|
|
216
248
|
schemaVersion: 2,
|
|
@@ -225,18 +257,68 @@ export function applyProfile(packageData, profilePlan, targetRoot, consumer, con
|
|
|
225
257
|
distribution: plan.component.distribution
|
|
226
258
|
}]))
|
|
227
259
|
};
|
|
228
|
-
fs.writeFileSync(
|
|
260
|
+
fs.writeFileSync(resolveUnder(targetRoot, 'PROJECT_FORGE_BASELINE.json', 'umbrella baseline'), JSON.stringify(projectForgeBaseline, null, 2) + '\n');
|
|
229
261
|
return projectForgeBaseline;
|
|
230
262
|
}
|
|
231
263
|
|
|
232
264
|
export function checkProfile(packageData, profileName, targetRoot) {
|
|
233
265
|
const plan = planProfile(packageData, profileName, targetRoot);
|
|
234
|
-
const projectForgeBaseline =
|
|
266
|
+
const projectForgeBaseline = loadProjectForgeBaseline(targetRoot);
|
|
235
267
|
const projectForgeCurrent = projectForgeBaseline.schemaVersion === 2 && projectForgeBaseline.package === packageData.pkg.name && projectForgeBaseline.version === packageData.pkg.version && projectForgeBaseline.profile === profileName;
|
|
236
268
|
const componentClean = plan.plans.every((component) => component.metadataCurrent && component.rows.every((row) => row.state === 'SAME') && component.retired.length === 0);
|
|
237
269
|
return { clean: projectForgeCurrent && componentClean, projectForgeCurrent, plan, projectForgeBaseline };
|
|
238
270
|
}
|
|
239
271
|
|
|
272
|
+
export function planRepair(packageData, profileName, targetRoot) {
|
|
273
|
+
const current = checkProfile(packageData, profileName, targetRoot);
|
|
274
|
+
const plan = current.plan;
|
|
275
|
+
const missing = [];
|
|
276
|
+
const blockers = [];
|
|
277
|
+
if (!current.projectForgeCurrent) blockers.push('Installed umbrella baseline is not this exact package/profile.');
|
|
278
|
+
for (const component of plan.plans) {
|
|
279
|
+
if (!component.metadataCurrent) blockers.push(`Stale or missing ${component.componentId} baseline.`);
|
|
280
|
+
if (component.retired.length) blockers.push(`Retired ${component.componentId} paths require update review.`);
|
|
281
|
+
const recorded = new Map((component.baseline.managedPaths ?? []).map((item) => [item.path, item.sha256]));
|
|
282
|
+
for (const row of component.rows) {
|
|
283
|
+
if (row.state === 'LOCAL_MISSING') {
|
|
284
|
+
if (recorded.get(row.path) !== row.desiredHash) {
|
|
285
|
+
blockers.push(`${component.componentId}: package bytes differ from installed baseline: ${row.path}`);
|
|
286
|
+
} else {
|
|
287
|
+
missing.push({ component: component.componentId, path: row.path });
|
|
288
|
+
}
|
|
289
|
+
} else if (row.state !== 'SAME') {
|
|
290
|
+
blockers.push(`${component.componentId}: ${row.state}: ${row.path}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return { plan, missing, blockers, safe: blockers.length === 0 };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function applyRepair(packageData, profileName, targetRoot) {
|
|
298
|
+
// Do not trust a previously displayed plan: re-evaluate every digest and
|
|
299
|
+
// symlink/baseline path immediately before any restoration.
|
|
300
|
+
const repair = planRepair(packageData, profileName, targetRoot);
|
|
301
|
+
if (!repair.safe) throw new Error('Refusing repair: installed baselines, package, or managed paths are stale/drifted.');
|
|
302
|
+
for (const component of repair.plan.plans) {
|
|
303
|
+
resolveUnder(targetRoot, component.filename, 'component baseline');
|
|
304
|
+
for (const row of component.rows) resolveUnder(targetRoot, row.path, 'consumer destination');
|
|
305
|
+
}
|
|
306
|
+
resolveUnder(targetRoot, 'PROJECT_FORGE_BASELINE.json', 'umbrella baseline');
|
|
307
|
+
for (const component of repair.plan.plans) {
|
|
308
|
+
for (const row of component.rows) {
|
|
309
|
+
if (row.state !== 'LOCAL_MISSING') continue;
|
|
310
|
+
const destination = resolveUnder(targetRoot, row.path, 'consumer destination');
|
|
311
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
312
|
+
// COPYFILE_EXCL refuses a file created after the preflight, rather than
|
|
313
|
+
// following a fresh destination link or overwriting an unreviewed edit.
|
|
314
|
+
fs.copyFileSync(row.bundlePath, destination, fs.constants.COPYFILE_EXCL);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Exactly the installed package's accepted bytes are restored; all
|
|
318
|
+
// component/umbrella provenance and consumer-owned files remain unchanged.
|
|
319
|
+
return repair.missing;
|
|
320
|
+
}
|
|
321
|
+
|
|
240
322
|
export function loadProjectForgeBaseline(targetRoot) {
|
|
241
|
-
return loadOptionalJson(
|
|
323
|
+
return loadOptionalJson(resolveUnder(targetRoot, 'PROJECT_FORGE_BASELINE.json', 'umbrella baseline'));
|
|
242
324
|
}
|