@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
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Manually reviewed disposable Auth retention operations; no scheduler/Production wiring.
|
|
2
|
+
|
|
3
|
+
A plan is private and non-secret, bound to the exact generated-Project helper,
|
|
4
|
+
Docker owner binding, most recently verified backup and inventory. Applying
|
|
5
|
+
requires the human-reviewed SHA-256 of that private plan, plus explicit
|
|
6
|
+
disposable deletion authorization. This is not host or inbox acceptance.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
import re
|
|
16
|
+
import secrets
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
from .auth_recovery_capture import _load_pinned_helper
|
|
20
|
+
from .auth_recovery_lifecycle import _binding_fingerprint
|
|
21
|
+
from .auth_recovery_schedule import (
|
|
22
|
+
_operation_lock, _read_json, _write_private_json,
|
|
23
|
+
inspect_disposable_backup_status, load_disposable_operation_config,
|
|
24
|
+
)
|
|
25
|
+
from .errors import DeploymentKitError
|
|
26
|
+
|
|
27
|
+
_PLAN = "auth-retention-reviewed-plan.json"
|
|
28
|
+
_SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _canonical_json(value: dict) -> bytes:
|
|
32
|
+
return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _verified_backup(config: dict) -> dict:
|
|
36
|
+
report = inspect_disposable_backup_status(config)
|
|
37
|
+
attempt = report["lastAttempt"]
|
|
38
|
+
latest = report["lastSuccess"]
|
|
39
|
+
if (report["state"] != "fresh" or type(attempt) is not dict or
|
|
40
|
+
type(latest) is not dict or attempt["outcome"] != "success" or
|
|
41
|
+
attempt["backupArtifactIntegrityVerified"] is not True or
|
|
42
|
+
attempt["runtimeResumedVerified"] is not True or
|
|
43
|
+
attempt["setName"] != latest["setName"] or
|
|
44
|
+
attempt["completedAt"] != latest["completedAt"]):
|
|
45
|
+
raise DeploymentKitError(
|
|
46
|
+
"A current verified disposable Auth backup and exact Docker resumption are required."
|
|
47
|
+
)
|
|
48
|
+
return latest
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _verified_helper(config: dict):
|
|
52
|
+
helper = _load_pinned_helper(
|
|
53
|
+
config["project"], config["revision"], config["helperSha"])
|
|
54
|
+
if (not callable(getattr(helper, "plan_verified_retention", None)) or
|
|
55
|
+
not callable(getattr(helper, "apply_reviewed_disposable_retention", None))):
|
|
56
|
+
raise DeploymentKitError(
|
|
57
|
+
"Reviewed generated Auth helper does not support disposable retention."
|
|
58
|
+
)
|
|
59
|
+
try:
|
|
60
|
+
helper.validate_declaration(config["declaration"])
|
|
61
|
+
except Exception:
|
|
62
|
+
raise DeploymentKitError("Reviewed Auth recovery declaration is invalid.") from None
|
|
63
|
+
return helper
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _binding_record(config: dict, last: dict, keep_last: int) -> dict:
|
|
67
|
+
return {
|
|
68
|
+
"schemaVersion": 1,
|
|
69
|
+
"environment": "disposable",
|
|
70
|
+
"projectId": config["declaration"]["projectId"],
|
|
71
|
+
"authRevision": config["revision"],
|
|
72
|
+
"recoverySha256": config["helperSha"],
|
|
73
|
+
"dockerBindingSha256": _binding_fingerprint(config["binding"]),
|
|
74
|
+
"backupsRoot": str(config["backups"]),
|
|
75
|
+
"latestSuccessfulSetName": last["setName"],
|
|
76
|
+
"latestSuccessfulAt": last["completedAt"],
|
|
77
|
+
"keepLast": keep_last,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def create_reviewed_disposable_retention_plan(
|
|
82
|
+
config: dict, *, keep_last: int = 7,
|
|
83
|
+
) -> dict:
|
|
84
|
+
"""Write only a private review artifact, no backup deletion."""
|
|
85
|
+
if type(keep_last) is not int or not 2 <= keep_last <= 365:
|
|
86
|
+
raise DeploymentKitError("Auth retention must preserve at least two recovery sets.")
|
|
87
|
+
with _operation_lock(config["status"]):
|
|
88
|
+
last = _verified_backup(config)
|
|
89
|
+
helper = _verified_helper(config)
|
|
90
|
+
try:
|
|
91
|
+
preview = helper.plan_verified_retention(
|
|
92
|
+
config["backups"], config["declaration"], keep_last=keep_last)
|
|
93
|
+
except Exception:
|
|
94
|
+
raise DeploymentKitError("Auth retention inventory could not be verified.") from None
|
|
95
|
+
if (type(preview) is not dict or preview.get("retentionPlanOnly") is not True or
|
|
96
|
+
preview.get("deletionPerformed") is not False or
|
|
97
|
+
last["setName"] not in preview.get("retainedRecoverySetNames", [])):
|
|
98
|
+
raise DeploymentKitError("Current verified backup must remain in the retained inventory.")
|
|
99
|
+
envelope = {**_binding_record(config, last, keep_last), "reviewedPlan": preview}
|
|
100
|
+
digest = hashlib.sha256(_canonical_json(envelope)).hexdigest()
|
|
101
|
+
_write_private_json(config["status"] / _PLAN, envelope, replace=True)
|
|
102
|
+
return {
|
|
103
|
+
"planSha256": digest,
|
|
104
|
+
"candidateCount": len(preview["retentionCandidateNames"]),
|
|
105
|
+
"deletionPerformed": False,
|
|
106
|
+
"reviewRequiredBeforeApply": True,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def apply_reviewed_disposable_retention(
|
|
111
|
+
config: dict, *, expected_plan_sha256: str,
|
|
112
|
+
authorize_disposable_delete: bool = False,
|
|
113
|
+
) -> dict:
|
|
114
|
+
"""Apply only an exact operator-reviewed private plan, never automatically."""
|
|
115
|
+
if (authorize_disposable_delete is not True or
|
|
116
|
+
type(expected_plan_sha256) is not str or
|
|
117
|
+
_SHA256.fullmatch(expected_plan_sha256) is None):
|
|
118
|
+
raise DeploymentKitError("Explicit disposable retention approval and reviewed SHA-256 required.")
|
|
119
|
+
with _operation_lock(config["status"]):
|
|
120
|
+
last = _verified_backup(config)
|
|
121
|
+
helper = _verified_helper(config)
|
|
122
|
+
envelope = _read_json(config["status"] / _PLAN)
|
|
123
|
+
if hashlib.sha256(_canonical_json(envelope)).hexdigest() != expected_plan_sha256:
|
|
124
|
+
raise DeploymentKitError("Disposable retention reviewed-plan hash differs.")
|
|
125
|
+
if (set(envelope) != set(_binding_record(config, last, 7)) | {"reviewedPlan"} or
|
|
126
|
+
type(envelope.get("keepLast")) is not int or
|
|
127
|
+
not 2 <= envelope["keepLast"] <= 365):
|
|
128
|
+
raise DeploymentKitError("Disposable retention plan schema is invalid.")
|
|
129
|
+
if (envelope != {**_binding_record(config, last, envelope["keepLast"]),
|
|
130
|
+
"reviewedPlan": envelope["reviewedPlan"]}):
|
|
131
|
+
raise DeploymentKitError("Reviewed retention plan does not match generated Project.")
|
|
132
|
+
preview = envelope["reviewedPlan"]
|
|
133
|
+
try:
|
|
134
|
+
current = helper.plan_verified_retention(
|
|
135
|
+
config["backups"], config["declaration"], keep_last=envelope["keepLast"])
|
|
136
|
+
except Exception:
|
|
137
|
+
raise DeploymentKitError("Current retention inventory could not be verified.") from None
|
|
138
|
+
if (current != preview or
|
|
139
|
+
last["setName"] not in current["retainedRecoverySetNames"]):
|
|
140
|
+
raise DeploymentKitError("Retention inventory changed after operator review.")
|
|
141
|
+
|
|
142
|
+
# An immutable start receipt survives interruption, and the immutable
|
|
143
|
+
# finish receipt records the actual outcome. No partial failure can be
|
|
144
|
+
# reported as completion or silently retried.
|
|
145
|
+
operation_id = secrets.token_hex(16)
|
|
146
|
+
stamp = datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
|
|
147
|
+
base = {
|
|
148
|
+
"schemaVersion": 1, "operationId": operation_id,
|
|
149
|
+
"startedAt": stamp, "reviewedPlanSha256": expected_plan_sha256,
|
|
150
|
+
"projectId": config["declaration"]["projectId"],
|
|
151
|
+
"plannedCandidateNames": current["retentionCandidateNames"][:32],
|
|
152
|
+
}
|
|
153
|
+
prefix = config["status"] / ("auth-retention-" + operation_id)
|
|
154
|
+
_write_private_json(Path(str(prefix) + "-started.json"), base, replace=False)
|
|
155
|
+
try:
|
|
156
|
+
result = helper.apply_reviewed_disposable_retention(
|
|
157
|
+
config["backups"], config["lock"], config["declaration"], preview,
|
|
158
|
+
keep_last=envelope["keepLast"], environment="disposable",
|
|
159
|
+
authorize_disposable_delete=True, max_delete=32)
|
|
160
|
+
if (type(result) is not dict or
|
|
161
|
+
result.get("deletedRecoverySetNames") != base["plannedCandidateNames"] or
|
|
162
|
+
result.get("deletionPerformed") is not bool(base["plannedCandidateNames"])):
|
|
163
|
+
raise DeploymentKitError("Disposable retention deletion proof differs.")
|
|
164
|
+
except Exception:
|
|
165
|
+
_write_private_json(
|
|
166
|
+
Path(str(prefix) + "-failed.json"),
|
|
167
|
+
{**base, "completedAt": datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z"),
|
|
168
|
+
"outcome": "failed-or-unverified"}, replace=False)
|
|
169
|
+
raise DeploymentKitError(
|
|
170
|
+
"Disposable retention incomplete; inspect private receipts and inventory."
|
|
171
|
+
) from None
|
|
172
|
+
|
|
173
|
+
_write_private_json(
|
|
174
|
+
Path(str(prefix) + "-completed.json"),
|
|
175
|
+
{**base, "completedAt": datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z"),
|
|
176
|
+
"outcome": "success", "deletedRecoverySetNames": result["deletedRecoverySetNames"]},
|
|
177
|
+
replace=False)
|
|
178
|
+
return {
|
|
179
|
+
"deletedRecoverySetNames": result["deletedRecoverySetNames"],
|
|
180
|
+
"deletionPerformed": result["deletionPerformed"] is True,
|
|
181
|
+
"applicationRecoveryReady": False,
|
|
182
|
+
"scheduledBackupVerified": False,
|
|
183
|
+
"alertInboxReceiptVerified": False,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def main(argv: list[str] | None = None) -> int:
|
|
188
|
+
parser = argparse.ArgumentParser(
|
|
189
|
+
description="Manual disposable-only Auth retention; never a Production or timer command")
|
|
190
|
+
parser.add_argument("action", choices=("plan", "apply"))
|
|
191
|
+
parser.add_argument("--config", type=Path, required=True)
|
|
192
|
+
parser.add_argument("--keep-last", type=int, default=7)
|
|
193
|
+
parser.add_argument("--reviewed-plan-sha256")
|
|
194
|
+
parser.add_argument("--authorize-disposable-delete", action="store_true")
|
|
195
|
+
args = parser.parse_args(argv)
|
|
196
|
+
try:
|
|
197
|
+
config = load_disposable_operation_config(args.config)
|
|
198
|
+
if args.action == "plan":
|
|
199
|
+
if args.reviewed_plan_sha256 or args.authorize_disposable_delete:
|
|
200
|
+
raise DeploymentKitError("Read-only retention planning refuses deletion approval flags.")
|
|
201
|
+
result = create_reviewed_disposable_retention_plan(
|
|
202
|
+
config, keep_last=args.keep_last)
|
|
203
|
+
print("Disposable Auth retention candidate count: " + str(result["candidateCount"]))
|
|
204
|
+
print("Review private plan before apply; SHA-256: " + result["planSha256"])
|
|
205
|
+
else:
|
|
206
|
+
if args.keep_last != 7:
|
|
207
|
+
raise DeploymentKitError("Retention apply uses the keep-last value pinned in reviewed plan.")
|
|
208
|
+
result = apply_reviewed_disposable_retention(
|
|
209
|
+
config, expected_plan_sha256=args.reviewed_plan_sha256,
|
|
210
|
+
authorize_disposable_delete=args.authorize_disposable_delete)
|
|
211
|
+
print("Disposable Auth verified retention deleted sets: " +
|
|
212
|
+
str(len(result["deletedRecoverySetNames"])))
|
|
213
|
+
return 0
|
|
214
|
+
except DeploymentKitError:
|
|
215
|
+
print("BLOCKED: Disposable Auth retention could not be proven safe.", file=sys.stderr)
|
|
216
|
+
return 2
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
if __name__ == "__main__":
|
|
220
|
+
raise SystemExit(main())
|