@mriekki/project-forge 4.26.0 → 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.
Files changed (38) hide show
  1. package/assets/authentication/distribution.json +90 -0
  2. package/assets/components.json +18 -4
  3. package/assets/core/distribution.json +12 -0
  4. package/assets/deployment/distribution.json +292 -68
  5. package/assets/generated/ASSEMBLY.json +152 -6
  6. package/assets/generated/authentication/docs/AUTH_RECOVERY_RETENTION.md +101 -0
  7. package/assets/generated/authentication/docs/HOUSEHOLD_INFRASTRUCTURE_ADOPTION.md +55 -0
  8. package/assets/generated/authentication/docs/HOUSEHOLD_REGISTRY.md +246 -0
  9. package/assets/generated/authentication/docs/PROJECT_AUTH_REFERENCE.md +878 -0
  10. package/assets/generated/authentication/samples/ProjectForgeAuth.Api/Program.cs +40 -0
  11. package/assets/generated/authentication/samples/ProjectForgeAuth.Api/ProjectForgeAuth.Api.csproj +13 -0
  12. package/assets/generated/authentication/samples/ProjectForgeAuth.Api/wwwroot/auth.js +65 -0
  13. package/assets/generated/authentication/samples/ProjectForgeAuth.Api/wwwroot/index.html +44 -0
  14. package/assets/generated/authentication/src/ProjectForge.Auth/AdmissionPolicy.cs +167 -0
  15. package/assets/generated/authentication/src/ProjectForge.Auth/AuthenticationDiagnostics.cs +159 -0
  16. package/assets/generated/authentication/src/ProjectForge.Auth/ProjectForge.Auth.csproj +9 -0
  17. package/assets/generated/authentication/src/ProjectForge.Auth/PwaExistingProjectCompatibility.cs +64 -0
  18. package/assets/generated/authentication/src/ProjectForge.Auth.Runtime/ProjectAuthStore.cs +471 -0
  19. package/assets/generated/authentication/src/ProjectForge.Auth.Runtime/ProjectForge.Auth.Runtime.csproj +15 -0
  20. package/assets/generated/authentication/src/ProjectForge.Auth.Runtime/ProjectForgeAuthRuntimeExtensions.cs +472 -0
  21. package/assets/generated/authentication/src/ProjectForge.Auth.Runtime/ResendProjectInvitationDelivery.cs +105 -0
  22. package/assets/generated/authentication/src/ProjectForge.Transport/ProjectForge.Transport.csproj +11 -0
  23. package/assets/generated/authentication/src/ProjectForge.Transport/StrictPublicOriginTransport.cs +95 -0
  24. package/assets/generated/authentication/tools/household_connect.py +145 -0
  25. package/assets/generated/authentication/tools/project_auth_recovery.py +716 -0
  26. package/assets/generated/core/core/process/ENGINEERING_BACKLOG.md +150 -0
  27. package/assets/generated/core/core/skills/manage-backlog/SKILL.md +115 -0
  28. package/assets/generated/core/core/skills/review-user-feedback/SKILL.md +50 -0
  29. package/assets/generated/core/tools/setup_maintenance/maintenance.py +162 -6
  30. package/assets/generated/deployment/docs/RECOVERY_READINESS.md +103 -0
  31. package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_capture.py +164 -0
  32. package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_guard.py +225 -0
  33. package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_lifecycle.py +455 -0
  34. package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_retention.py +220 -0
  35. package/assets/generated/deployment/src/project_forge_deployment/auth_recovery_schedule.py +489 -0
  36. package/package.json +1 -1
  37. package/src/cli.mjs +32 -3
  38. 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(path.join(targetRoot, filename));
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(path.join(targetRoot, plan.filename), JSON.stringify(baseline, null, 2) + '\n');
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(path.join(targetRoot, 'PROJECT_FORGE_BASELINE.json'), JSON.stringify(projectForgeBaseline, null, 2) + '\n');
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 = loadOptionalJson(path.join(targetRoot, 'PROJECT_FORGE_BASELINE.json'));
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(path.join(targetRoot, 'PROJECT_FORGE_BASELINE.json'));
323
+ return loadOptionalJson(resolveUnder(targetRoot, 'PROJECT_FORGE_BASELINE.json', 'umbrella baseline'));
242
324
  }