companion-tier-resolver.mjs
95 lines 4.1 KB
Raw
sha256:0d530f9ef27b8b75547d1db7701a74bc77b77aa8f3d7fa3a8672cf2af36e63bb reconcile: import GitHub-direct RBAC/OAuth/companion and ho… Human minor ⚠ breaking 6 hours ago
1 /**
2 * Phase 6 — per-tier, per-artifact storage routing (D6.1).
3 *
4 * Maps (artifact_type × privacy_tier) to exactly one of three terminal states:
5 * 'host_readable' — convenience tier only (server-held-key is this class, not ZK)
6 * 'client_encrypted' — privacy_max: requires ClientEncryptor with user-held key
7 * 'local_only' — either tier, when above options are unavailable
8 *
9 * ROUTING RULES (D6.1, binding):
10 * - Server-held-key AES-256-GCM is classified as 'host_readable' for policy purposes
11 * (the operator can decrypt). It DOES NOT satisfy privacy_max (D6.1.2).
12 * - Unknown/unresolvable tier → treat as most-restrictive (privacy_max) → fail closed.
13 * - Until the two-tier vault registry exists, ONLY 'convenience' is resolvable;
14 * any 'privacy_max' write fails closed (D6.1.1).
15 * - No caller picks a storage location directly — routing is centralized here (D6.1.3).
16 *
17 * DESIGN INVARIANTS:
18 * - Pure function — no I/O, no network.
19 * - Fail-closed on every ambiguous or unknown input.
20 */
21
22 import { ARTIFACT_TYPES, PRIVACY_TIERS } from './companion-provenance-validator.mjs';
23
24 /**
25 * The three legal terminal storage states (D6.1.2).
26 * There is no fourth state.
27 * @readonly
28 */
29 export const TERMINAL_STATES = Object.freeze({
30 /** Convenience only. Artifact stored plaintext or with a server-held key. Host can read it. */
31 HOST_READABLE: 'host_readable',
32 /** privacy_max. Artifact stored only as ciphertext under a user-held key. Host cannot read it. */
33 CLIENT_ENCRYPTED: 'client_encrypted',
34 /**
35 * Either tier. Artifact kept only on the local device.
36 * Used when client encryption is unavailable for privacy_max (fail-closed path).
37 */
38 LOCAL_ONLY: 'local_only',
39 });
40
41 /**
42 * Fixed reason codes for tier resolution failures.
43 * @readonly
44 */
45 export const TIER_RESOLVE_REASONS = Object.freeze({
46 /** Artifact type is not in the known set. */
47 INVALID_ARTIFACT_TYPE: 'tier_resolve_invalid_artifact_type',
48 /** Privacy tier value is unknown — treated as most-restrictive, then fails closed. */
49 UNKNOWN_TIER: 'tier_resolve_unknown_tier',
50 /**
51 * privacy_max requested but the two-tier vault registry does not exist yet (D6.1.1).
52 * Phase 6 fails closed rather than inventing a tier source.
53 */
54 PRIVACY_MAX_NO_REGISTRY: 'tier_resolve_privacy_max_no_registry',
55 });
56
57 /**
58 * Resolve the terminal storage state for a derived artifact.
59 *
60 * D6.1.1 — tier is resolved from the OWNER's vault tier (passed as `privacyTier`).
61 * Until `vaultRegistryAvailable` is true, only 'convenience' resolves; any
62 * 'privacy_max' artifact fails closed.
63 *
64 * D6.1.2 — exactly three terminal states. Server-held-key = host_readable (same policy class).
65 *
66 * D6.1.3 — routing is centralized; callers supply artifact + tier, not a location.
67 *
68 * @param {string} artifactType - One of ARTIFACT_TYPES.
69 * @param {string} privacyTier - 'convenience' | 'privacy_max' (the OWNER's vault tier).
70 * @param {{ vaultRegistryAvailable?: boolean }} [opts]
71 * @returns {{ ok: true; terminalState: string } | { ok: false; reason: string }}
72 */
73 export function resolveTier(artifactType, privacyTier, opts = {}) {
74 // Validate artifact type — unknown type is a hard reject
75 if (!ARTIFACT_TYPES.includes(/** @type {any} */ (artifactType))) {
76 return { ok: false, reason: TIER_RESOLVE_REASONS.INVALID_ARTIFACT_TYPE };
77 }
78
79 // Unknown/unresolvable tier → most-restrictive (privacy_max) → fail closed (D6.1 §fail-closed)
80 if (!PRIVACY_TIERS.includes(/** @type {any} */ (privacyTier))) {
81 return { ok: false, reason: TIER_RESOLVE_REASONS.UNKNOWN_TIER };
82 }
83
84 if (privacyTier === 'privacy_max') {
85 // Until the two-tier vault registry lands, privacy_max always fails closed (D6.1.1)
86 if (!opts.vaultRegistryAvailable) {
87 return { ok: false, reason: TIER_RESOLVE_REASONS.PRIVACY_MAX_NO_REGISTRY };
88 }
89 // Registry present: privacy_max → client_encrypted (requires ClientEncryptor at write time)
90 return { ok: true, terminalState: TERMINAL_STATES.CLIENT_ENCRYPTED };
91 }
92
93 // convenience → host_readable (including server-held-key at-rest path — same policy class)
94 return { ok: true, terminalState: TERMINAL_STATES.HOST_READABLE };
95 }
File History 1 commit
sha256:0d530f9ef27b8b75547d1db7701a74bc77b77aa8f3d7fa3a8672cf2af36e63bb reconcile: import GitHub-direct RBAC/OAuth/companion and ho… Human minor 6 hours ago