rcf-lite 0.25.0 → 0.26.0

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 (137) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/blueprints/application-account-settings/docs/topics.md +1 -1
  3. package/blueprints/application-admin-console/docs/topics.md +1 -1
  4. package/blueprints/application-api-rest/docs/topics.md +1 -1
  5. package/blueprints/application-charts/docs/topics.md +1 -1
  6. package/blueprints/application-dashboard/docs/topics.md +1 -1
  7. package/blueprints/application-datatable/docs/topics.md +1 -1
  8. package/blueprints/application-empty-error-states/docs/topics.md +1 -1
  9. package/blueprints/application-error-handling/docs/topics.md +1 -1
  10. package/blueprints/application-file-upload/docs/topics.md +1 -1
  11. package/blueprints/application-forms-wizard/docs/topics.md +1 -1
  12. package/blueprints/application-notifications-in-app/docs/topics.md +1 -1
  13. package/blueprints/application-onboarding-tour/docs/topics.md +1 -1
  14. package/blueprints/application-spa/docs/topics.md +1 -1
  15. package/blueprints/delivery-ci-workflows/docs/topics.md +1 -1
  16. package/blueprints/deploy-cloudflare-workers/docs/topics.md +1 -1
  17. package/blueprints/deploy-hetzner-server/CHANGELOG.md +21 -0
  18. package/blueprints/deploy-hetzner-server/blueprint.json +1 -1
  19. package/blueprints/deploy-hetzner-server/contributions/probes/cloud-init-render-lint.mjs +12 -5
  20. package/blueprints/deploy-hetzner-server/contributions/probes/hcloud-dry-run-mock.mjs +68 -20
  21. package/blueprints/deploy-hetzner-server/contributions/probes/manifest-schema-validate.mjs +8 -6
  22. package/blueprints/deploy-hetzner-server/contributions/probes/probe-utils.mjs +7 -1
  23. package/blueprints/deploy-hetzner-server/contributions/probes/real-account-cloud-init-hardened.mjs +2 -1
  24. package/blueprints/deploy-hetzner-server/contributions/templates/cloud-init.yaml.tmpl +26 -6
  25. package/blueprints/deploy-hetzner-server/docs/topics.md +1 -1
  26. package/blueprints/edge-cloudflare-access/docs/topics.md +1 -1
  27. package/blueprints/edge-cloudflare-rate-limiting/docs/topics.md +1 -1
  28. package/blueprints/edge-cloudflare-tunnel/CHANGELOG.md +10 -0
  29. package/blueprints/edge-cloudflare-tunnel/blueprint.json +1 -1
  30. package/blueprints/edge-cloudflare-tunnel/contributions/probes/manifest-schema-validate.mjs +4 -1
  31. package/blueprints/edge-cloudflare-tunnel/contributions/probes/real-account-connector-healthy.mjs +110 -21
  32. package/blueprints/edge-cloudflare-tunnel/contributions/probes/real-account-tunnel-hostname-routes.mjs +123 -30
  33. package/blueprints/edge-cloudflare-tunnel/docs/topics.md +1 -1
  34. package/blueprints/edge-cloudflare-turnstile/docs/topics.md +1 -1
  35. package/blueprints/email-smtp-resend/docs/topics.md +1 -1
  36. package/blueprints/jobs-background/docs/topics.md +1 -1
  37. package/blueprints/messaging-queue-cloudflare/CHANGELOG.md +19 -0
  38. package/blueprints/messaging-queue-cloudflare/README.md +2 -2
  39. package/blueprints/messaging-queue-cloudflare/blueprint.json +1 -1
  40. package/blueprints/messaging-queue-cloudflare/contributions/probes/real-account-concurrency-smoke.mjs +192 -37
  41. package/blueprints/messaging-queue-cloudflare/docs/topics.md +1 -1
  42. package/blueprints/object-storage-s3/CHANGELOG.md +11 -0
  43. package/blueprints/object-storage-s3/README.md +14 -1
  44. package/blueprints/object-storage-s3/blueprint.json +12 -1
  45. package/blueprints/object-storage-s3/contributions/adrs/adr-2905-object-storage-s3-hetzner-object-storage-provider.json +30 -0
  46. package/blueprints/object-storage-s3/contributions/probes/hetzner-object-storage-round-trip.mjs +136 -0
  47. package/blueprints/object-storage-s3/contributions/probes/run-hetzner-object-storage-round-trip.mjs +11 -0
  48. package/blueprints/object-storage-s3/contributions/requirements/object-storage-s3-req-101.json +18 -0
  49. package/blueprints/object-storage-s3/contributions/tacs/tac-2904-object-storage-s3-hetzner-endpoint-helper.json +34 -0
  50. package/blueprints/object-storage-s3/contributions/user-stories/object-storage-s3-us-28110.json +25 -0
  51. package/blueprints/object-storage-s3/docs/topics.md +1 -1
  52. package/blueprints/object-storage-s3/guide/object-storage-s3.md +36 -0
  53. package/blueprints/observability-essentials/docs/topics.md +1 -1
  54. package/blueprints/observability-logging/docs/topics.md +1 -1
  55. package/blueprints/observability-probe-endpoints/docs/topics.md +1 -1
  56. package/blueprints/persistence-data-d1/docs/topics.md +1 -1
  57. package/blueprints/persistence-data-postgres/docs/topics.md +1 -1
  58. package/blueprints/persistence-data-sqlite/docs/topics.md +1 -1
  59. package/blueprints/platform-cloudflare-cron-triggers/docs/topics.md +1 -1
  60. package/blueprints/platform-cloudflare-durable-objects/CHANGELOG.md +15 -0
  61. package/blueprints/platform-cloudflare-durable-objects/blueprint.json +1 -1
  62. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/namespace-facade-ready.mjs +3 -3
  63. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/probe-utils.mjs +28 -14
  64. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/real-account-storage-smoke.mjs +132 -26
  65. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/sole-reader-scan.mjs +40 -39
  66. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/storage-round-trip.mjs +26 -16
  67. package/blueprints/platform-cloudflare-durable-objects/contributions/probes/websocket-hub-broadcast.mjs +31 -34
  68. package/blueprints/platform-cloudflare-durable-objects/docs/topics.md +1 -1
  69. package/blueprints/platform-cloudflare-kv/CHANGELOG.md +40 -1
  70. package/blueprints/platform-cloudflare-kv/README.md +41 -13
  71. package/blueprints/platform-cloudflare-kv/blueprint.json +1 -1
  72. package/blueprints/platform-cloudflare-kv/contributions/probes/cache-aside-hit-then-miss.mjs +26 -26
  73. package/blueprints/platform-cloudflare-kv/contributions/probes/event-secrecy.mjs +49 -31
  74. package/blueprints/platform-cloudflare-kv/contributions/probes/facade-round-trip.mjs +58 -16
  75. package/blueprints/platform-cloudflare-kv/contributions/probes/list-with-prefix.mjs +6 -5
  76. package/blueprints/platform-cloudflare-kv/contributions/probes/real-account-eventual-consistency-smoke.mjs +131 -67
  77. package/blueprints/platform-cloudflare-kv/docs/topics.md +1 -1
  78. package/blueprints/platform-docker-compose-host/docs/topics.md +1 -1
  79. package/blueprints/security-auth-clerk/docs/topics.md +1 -1
  80. package/blueprints/security-auth-keycloak/docs/topics.md +1 -1
  81. package/blueprints/security-auth-magic-link/docs/topics.md +1 -1
  82. package/blueprints/security-auth-oauth2/docs/topics.md +1 -1
  83. package/blueprints/security-secrets-management/docs/topics.md +1 -1
  84. package/fixtures/canary-manifest.json +9 -9
  85. package/guidance/harness-template.md +67 -0
  86. package/guidance/managed/agent-instructions-block.hash +1 -1
  87. package/guidance/managed/agent-instructions-block.md +67 -0
  88. package/package.json +2 -2
  89. package/rcf/code-nodes/cn-500.json +12 -0
  90. package/rcf/code-nodes/cn-510.json +12 -0
  91. package/rcf/code-nodes/cn-520.json +14 -0
  92. package/rcf/code-nodes/cn-521.json +12 -0
  93. package/rcf/code-nodes/cn-522.json +13 -0
  94. package/rcf/code-nodes/cn-523.json +12 -0
  95. package/rcf/code-nodes/cn-524.json +14 -0
  96. package/rcf/code-nodes/cn-525.json +12 -0
  97. package/rcf/code-nodes/cn-526.json +14 -0
  98. package/rcf/fbs/fbs-160.json +15 -0
  99. package/rcf/fbs/fbs-165.json +15 -0
  100. package/rcf/fbs/fbs-170.json +16 -0
  101. package/rcf/fbs/fbs-171.json +16 -0
  102. package/rcf/fbs/fbs-172.json +15 -0
  103. package/rcf/fbs/fbs-173.json +17 -0
  104. package/rcf/fbs/fbs-174.json +17 -0
  105. package/rcf/requirements/req-140.json +149 -0
  106. package/rcf/requirements/req-145.json +53 -0
  107. package/rcf/requirements/req-150.json +20 -0
  108. package/rcf/requirements/req-151.json +85 -0
  109. package/rcf/requirements/req-152.json +20 -0
  110. package/rcf/requirements/req-153.json +59 -0
  111. package/rcf/requirements/req-154.json +44 -0
  112. package/rcf/test-suites/ts-071.json +2 -2
  113. package/rcf/test-suites/ts-086.json +2 -2
  114. package/rcf/test-suites/ts-170.json +22 -0
  115. package/rcf/test-suites/ts-175.json +22 -0
  116. package/rcf/test-suites/ts-180.json +30 -0
  117. package/rcf/test-suites/ts-181.json +30 -0
  118. package/rcf/test-suites/ts-182.json +22 -0
  119. package/rcf/test-suites/ts-183.json +38 -0
  120. package/rcf/test-suites/ts-184.json +38 -0
  121. package/rcf/user-stories/us-14001.json +21 -0
  122. package/rcf/user-stories/us-14501.json +24 -0
  123. package/rcf/user-stories/us-15001.json +26 -0
  124. package/rcf/user-stories/us-15101.json +26 -0
  125. package/rcf/user-stories/us-15201.json +21 -0
  126. package/rcf/user-stories/us-15301.json +31 -0
  127. package/rcf/user-stories/us-15401.json +31 -0
  128. package/releases/releases.yaml +11 -1
  129. package/src/blueprint/apply.js +64 -0
  130. package/src/blueprint/consistency-lint.js +432 -0
  131. package/src/blueprint/disposition-ledger.js +224 -0
  132. package/src/blueprint/dispositions.js +44 -0
  133. package/src/blueprint/index.js +19 -0
  134. package/src/browser-verify/manifest-writer.js +53 -2
  135. package/src/browser-verify/pack-runner.js +18 -0
  136. package/src/cli/blueprint.js +103 -0
  137. package/src/setup/identity-seed.js +18 -0
@@ -4,13 +4,18 @@
4
4
  // elicited backends (sql and kv per ADR-3403), drives put/get,
5
5
  // delete/get and list({prefix}) on each; asserts the round-trip
6
6
  // on both. Also asserts driver.backend reports the elicited
7
- // answer (AC-33111-1). Under SIMULATE_STORAGE_BACKEND_MISMATCH=true
8
- // the probe intentionally passes an unrecognised backend answer;
9
- // the shipped default flips to sql per ADR-3403 recommendedDefault
10
- // so the probe surfaces the mismatch and returns fail on the
11
- // mismatch check.
7
+ // answer (AC-33111-1). The backend-mismatch mutation-run is
8
+ // triggered fixture-side by the H-2 shim
9
+ // h2-cf-do-storage-round-trip-shim.mjs, which constructs a driver
10
+ // from an unrecognised backend answer that the shipped factory
11
+ // clamps to sql per ADR-3403 recommendedDefault; the probe body
12
+ // receives the mismatch driver from the shim and surfaces the
13
+ // clamp as an AC-33103-1 fail so a reviewer can see the
14
+ // substitution.
12
15
  //
13
16
  // anchorAcId: AC-33103-1.
17
+ // Additional-result anchor: AC-33111-1 (driver.backend field
18
+ // present with the elicited answer on every returned driver).
14
19
  // accountBound: false.
15
20
 
16
21
  export const anchorAcId = 'AC-33103-1';
@@ -37,7 +42,8 @@ async function driveRoundTrip(driver, backendLabel) {
37
42
  }
38
43
 
39
44
  export default async function runProbe() {
40
- const { createInMemoryDoStorage } = await import('../../../../packages/rcf-lite/test/fixtures/cf-platform/src/do-storage.mjs');
45
+ const { prepareStorageDrivers } = await import('../../../../packages/rcf-lite/test/fixtures/cf-platform/h2-cf-do-storage-round-trip-shim.mjs');
46
+ const { createInMemoryDoStorage, mismatchDriver, mutationOn } = await prepareStorageDrivers();
41
47
 
42
48
  const results = [];
43
49
 
@@ -59,27 +65,31 @@ export default async function runProbe() {
59
65
  : `round-trip mismatch: sql=${JSON.stringify(sql)} kv=${JSON.stringify(kv)}`,
60
66
  });
61
67
 
62
- // AC-33111-1: default backend is sql when no arg is passed.
68
+ // AC-33111-1: default backend is sql when no arg is passed. Also
69
+ // covers the driver.backend-field-present requirement across the
70
+ // three driver openings above (sql, kv, default).
63
71
  const defaultDriver = createInMemoryDoStorage({});
64
- const defaultOk = defaultDriver.backend === 'sql';
72
+ const defaultOk = defaultDriver.backend === 'sql' &&
73
+ typeof sqlDriver.backend === 'string' &&
74
+ typeof kvDriver.backend === 'string';
65
75
  results.push({
66
76
  anchorAcId: 'AC-33111-1',
67
77
  verdict: defaultOk ? 'pass' : 'fail',
68
78
  detail: defaultOk
69
- ? `driver factory without an explicit backend defaults to sql per ADR-3403 recommendedDefault; observed driver.backend=${defaultDriver.backend}`
79
+ ? `driver factory without an explicit backend defaults to sql per ADR-3403 recommendedDefault (observed driver.backend=${defaultDriver.backend}); backend field present on every returned driver (sql=${sqlDriver.backend}, kv=${kvDriver.backend}, default=${defaultDriver.backend})`
70
80
  : `default backend mismatch: expected sql, observed ${defaultDriver.backend}`,
71
81
  });
72
82
 
73
- if (process.env.SIMULATE_STORAGE_BACKEND_MISMATCH === 'true') {
83
+ if (mutationOn) {
74
84
  // The shipped factory clamps any unrecognised backend answer to
75
- // sql; the mismatch probe surfaces the observation and returns
76
- // fail so a reviewer can see the clamp in action.
77
- const bogusDriver = createInMemoryDoStorage({ backend: 'not-a-real-backend' });
78
- const clampedButMismatched = bogusDriver.backend === 'sql';
85
+ // sql; the mutation-run surfaces the observation and returns
86
+ // fail so a reviewer can see the clamp in action. The shim
87
+ // constructs the mismatch driver; the probe body reports it.
88
+ const clampedButMismatched = mismatchDriver.backend === 'sql';
79
89
  results.push({
80
90
  anchorAcId: 'AC-33103-1',
81
- verdict: clampedButMismatched ? 'fail' : 'fail',
82
- detail: `SIMULATE_STORAGE_BACKEND_MISMATCH=true: unrecognised backend answer clamped to ${bogusDriver.backend} instead of failing at the boundary`,
91
+ verdict: 'fail',
92
+ detail: `mutation-run active (fixture shim reports backend-mismatch on): unrecognised backend answer clamped to ${mismatchDriver.backend} (clampedToSql=${clampedButMismatched}) instead of failing at the boundary`,
83
93
  });
84
94
  }
85
95
 
@@ -1,24 +1,25 @@
1
1
  // Websocket-hub-broadcast probe for platform-cloudflare-durable-objects v1.0.0.
2
2
  //
3
- // Wires a HubObject to a fake state, accepts two fake sockets
4
- // (createFakeSocketPair), drives a broadcast from client A and
5
- // asserts B receives within the elicited window (default 500ms).
6
- // Then drives hibernate-and-wake round-trip: hibernate, wake once
7
- // (doWakeUp emitted, lastBroadcast returned), wake again (no
8
- // second doWakeUp). Finally scans every emitted record on the sink
9
- // against the allowed key set {event, key, size, ttl, timestamp,
10
- // objectName, scheduledTime} and asserts zero forbidden-key hits
11
- // (AC-33106-1); under SIMULATE_PII_LEAK=true a wrapped sink forwards
12
- // a synthetic body-bearing record and the probe surfaces it,
13
- // returning fail on the secrecy result.
3
+ // Wires a HubObject to an in-process state stub, accepts two
4
+ // in-process paired sockets (createInProcessSocketPair), drives a
5
+ // broadcast from client A and asserts B receives within the
6
+ // elicited window (default 500ms). Then drives hibernate-and-wake
7
+ // round-trip: hibernate, wake once (doWakeUp emitted,
8
+ // lastBroadcast returned), wake again (no second doWakeUp).
9
+ // Finally scans every emitted record on the sink against the
10
+ // allowed key set {event, key, size, ttl, timestamp, objectName,
11
+ // scheduledTime} and asserts zero forbidden-key hits (AC-33106-1).
14
12
  //
15
- // Under SIMULATE_HUB_HANG=true the broadcast handler is wrapped
16
- // with an artificial delay past the elicited window; the probe
17
- // surfaces the hang and returns fail on the broadcast-within-
18
- // window check.
13
+ // Two mutation switches are triggered fixture-side by the H-2
14
+ // shim h2-cf-do-websocket-hub-shim.mjs: a PII-leak wrapper on the
15
+ // sink adapter that injects forbidden fields on every emitted
16
+ // record, and a broadcast-hang gate that pauses the probe past
17
+ // the elicited window before it fires the broadcast. Under either
18
+ // mutation the corresponding result trips fail.
19
19
  //
20
- // anchorAcId: AC-33105-1 (primary; AC-33110-1 and AC-33106-1
21
- // covered as additional results).
20
+ // anchorAcId: AC-33105-1.
21
+ // Additional-result anchors: AC-33110-1 (hibernate-and-wake round
22
+ // trip), AC-33106-1 (event-secrecy over every emitted record).
22
23
  // accountBound: false.
23
24
 
24
25
  export const anchorAcId = 'AC-33105-1';
@@ -30,23 +31,19 @@ const FORBIDDEN_KEYS = ['body', 'ssn', 'userId', 'headers', 'cookies', 'request'
30
31
  export default async function runProbe() {
31
32
  const { createInMemoryDoStorage } = await import('../../../../packages/rcf-lite/test/fixtures/cf-platform/src/do-storage.mjs');
32
33
  const { HubObject } = await import('../../../../packages/rcf-lite/test/fixtures/cf-platform/src/do-hub.mjs');
33
- const { createFakeState, createFakeSocketPair } = await import('./probe-utils.mjs');
34
+ const { createInProcessDoState, createInProcessSocketPair } = await import('./probe-utils.mjs');
35
+ const { prepareHubBroadcastSeams } = await import('../../../../packages/rcf-lite/test/fixtures/cf-platform/h2-cf-do-websocket-hub-shim.mjs');
34
36
 
37
+ const seams = prepareHubBroadcastSeams();
35
38
  const events = [];
36
- const cleanSink = (rec) => events.push(rec);
37
- const leakySink = (rec) => events.push({
38
- ...rec,
39
- body: 'Hello, world (leaked test body)',
40
- ssn: '123-45-6789',
41
- userId: 'user_leaked_test_uid',
42
- });
43
- const eventSink = process.env.SIMULATE_PII_LEAK === 'true' ? leakySink : cleanSink;
39
+ const baseSink = (rec) => events.push(rec);
40
+ const eventSink = seams.wrapSink(baseSink);
44
41
  const storage = createInMemoryDoStorage({ backend: 'sql' });
45
- const state = createFakeState();
42
+ const state = createInProcessDoState();
46
43
  const hub = new HubObject({ state, storage, eventSink, name: 'lobby', hibernateAfterIdleMs: 500 });
47
44
 
48
- const sockA = createFakeSocketPair('A');
49
- const sockB = createFakeSocketPair('B');
45
+ const sockA = createInProcessSocketPair('A');
46
+ const sockB = createInProcessSocketPair('B');
50
47
  await hub.accept(sockA);
51
48
  await hub.accept(sockB);
52
49
 
@@ -55,8 +52,8 @@ export default async function runProbe() {
55
52
  // AC-33105-1: broadcast from A reaches both A and B within 500ms.
56
53
  const started = Date.now();
57
54
  const payload = 'ping';
58
- if (process.env.SIMULATE_HUB_HANG === 'true') {
59
- await new Promise((r) => setTimeout(r, 750));
55
+ if (seams.hangBeforeBroadcastMs > 0) {
56
+ await new Promise((r) => setTimeout(r, seams.hangBeforeBroadcastMs));
60
57
  }
61
58
  await hub.webSocketMessage(sockA, JSON.stringify({ type: 'broadcast', payload }));
62
59
  const elapsed = Date.now() - started;
@@ -68,8 +65,8 @@ export default async function runProbe() {
68
65
  anchorAcId: 'AC-33105-1',
69
66
  verdict: broadcastOk ? 'pass' : 'fail',
70
67
  detail: broadcastOk
71
- ? `broadcast fan-out: A received ${gotA} B received ${gotB}; elapsed ${elapsed}ms within 500ms window; storage.lastBroadcast persisted`
72
- : `broadcast fault: gotA=${!!gotA} gotB=${!!gotB} elapsed=${elapsed}ms persisted=${!!persisted}`,
68
+ ? `broadcast fan-out (mutation-switches off): A received ${gotA} B received ${gotB}; elapsed ${elapsed}ms within 500ms window; storage.lastBroadcast persisted`
69
+ : `broadcast fault${seams.hangOn ? ' (mutation-run active: fixture shim reports broadcast-hang on)' : ''}: gotA=${!!gotA} gotB=${!!gotB} elapsed=${elapsed}ms persisted=${!!persisted}`,
73
70
  });
74
71
 
75
72
  // AC-33110-1: hibernate-and-wake round-trip preserves storage and doWakeUp fires exactly once.
@@ -105,7 +102,7 @@ export default async function runProbe() {
105
102
  verdict: secrecyOk ? 'pass' : 'fail',
106
103
  detail: secrecyOk
107
104
  ? `event-secrecy scan across ${events.length} records: allowed-key set {${[...ALLOWED_KEYS].join(', ')}}; forbidden-key hits 0; extra-key hits 0`
108
- : `event-secrecy fault: forbiddenHits=${JSON.stringify(forbiddenHits.slice(0, 5))} extraKeyHits=${JSON.stringify(extraKeyHits.slice(0, 5))}`,
105
+ : `event-secrecy fault${seams.leakOn ? ' (mutation-run active: fixture shim reports PII-leak on)' : ''}: forbiddenHits=${JSON.stringify(forbiddenHits.slice(0, 5))} extraKeyHits=${JSON.stringify(extraKeyHits.slice(0, 5))}`,
109
106
  });
110
107
 
111
108
  return { results };
@@ -47,7 +47,7 @@ This table is maintained shelf-wide across every blueprint's `docs/topics.md`. R
47
47
  | application-account-settings | 25101-25899 | 26xx | shipped v1.0.0 | none |
48
48
  | application-onboarding-tour | 26101-26899 | 27xx | shipped v1.0.0 | none |
49
49
  | persistence-data-postgres | 27101-27899 | 28xx | shipped v1.0.0 | `persistenceStore`, `migrationDiscipline` |
50
- | object-storage-s3 | 28101-28899 | 29xx | shipped v1.0.0 | `objectStorageContract` |
50
+ | object-storage-s3 | 28101-28899 | 29xx | shipped v1.1.0 | `objectStorageContract` |
51
51
  | messaging-queue-cloudflare | 29101-29899 | 30xx | shipped v1.0.0 | `deliverySemantics` |
52
52
  | jobs-background | 30101-30899 | 31xx | shipped v1.0.0 | `backgroundJobModel` |
53
53
  | platform-cloudflare-kv | 31101-31899 | 32xx | shipped v1.0.0 | `keyValueStoreContract` |
@@ -2,6 +2,45 @@
2
2
 
3
3
  All notable changes to `platform-cloudflare-kv` are recorded here. The shape follows Keep a Changelog and Semantic Versioning per the blueprint authoring standard.
4
4
 
5
+ ## 1.0.2 (2026-09-08)
6
+
7
+ H-2 follow-up (`w-2026-09-08-dave-017`, addendum item 6 fixture defect): the real-account KV probe now self-provisions its own throwaway namespace, closing the second-tier undeclared-env skip HQ hit at the real-account gate on 2026-09-08 (`REAL RUN: PARTIAL`, `MERGE DECISION: HOLD`). No shipped-code capability change.
8
+
9
+ ### Changed
10
+
11
+ - `real-account-eventual-consistency-smoke.mjs` rewritten against a new self-provisioning fixture shim at `packages/rcf-lite/test/fixtures/cf-platform/h2-cf-kv-real-account-shim.mjs`. The shim mints a scratch KV namespace under the frozen throwaway prefix `h2-cf-probe-integrity-scratch-kv-`, writes a fixture key under `h2-storage-smoke-`, polls up to 60 seconds bounded for same-region visibility, deletes the key, and destroys the namespace on exit. Positive evidence captured on every real-account run: namespace id, exact scratch title, key, PUT / GET / DELETE status codes, elapsed ms.
12
+ - Removed the second-tier `CF_KV_NAMESPACE_ID` gate that produced the undeclared skip at the H-2 real-account gate. The declared env for a real-account run is now `CI_HAS_CLOUDFLARE_ACCOUNT` + `CF_ACCOUNT_ID` + `CF_API_TOKEN` (optional test override `CF_API_BASE_URL`), enumerated in the probe's `report.extra.envDeclared` and mirrored in the shim's `DECLARED_ENV` export.
13
+ - Teardown is fail-safe and idempotent: a mid-run crash surfaces the minted id through the shim's persisted scratch record; the shim's separate `sweepOrphans` entry point lists over the account and filters on the throwaway prefix. Sweep is structurally unable to select a non-prefixed name (three independent guards: prefix assert on mint, prefix assert on destroy against the record, prefix assert on the live-listing observation; sweep-only prefix filter mirrors the destroy assertions). A dedicated sweep-safety test at `packages/rcf-lite/test/fixtures/cf-platform/test/h2-cf-kv-real-account-shim.test.mjs` seeds the mock account with the ten live production script names on the operator account plus a sneaky mid-string-prefix name and asserts sweep selects zero of them.
14
+
15
+ ### Added
16
+
17
+ - Self-contained mock CF REST API at `packages/rcf-lite/test/fixtures/cf-platform/test/mock-cf-api-server.mjs` (Node `node:http` only, zero-dep) implementing the KV namespaces + values + keys endpoints the shim hits, with page + per_page pagination on the namespaces list. Test at `test/h2-cf-kv-probe-e2e.test.mjs` boots the mock in-process, points the shim at it via `CF_API_BASE_URL`, and drives the probe end-to-end to prove the mint / evidence-capture / teardown / zero-orphans loop before the HQ real-account run ever executes.
18
+ - `kvListNamespaces` in the shared CF REST client (`h2-cf-account-api.mjs`) paginates to completion via the documented `per_page` + `page` loop (Dave ruling 4e9ff62d item 5) so `sweepOrphans` is not silently truncated to page one on a busy account. `kvDeleteNamespace` is idempotent on 404 (Dave ruling 4e9ff62d items 3+4).
19
+
20
+ ### Local-proof scope note
21
+
22
+ The local proof harness under `packages/rcf-lite/test/fixtures/cf-platform/test/` is a mock of Cloudflare's REST contract, not the wire. **The local run exercises OUR lifecycle logic against a mock of Cloudflare's contract; the real-account gate is the only surface that proves the wire format.** This probe is never described as "locally verified" - the local runs are our own lifecycle-logic proof; wire correctness is proven at the HQ real-account gate.
23
+
24
+ ## 1.0.1 (2026-09-08)
25
+
26
+ H-2 hardening train (`h2-cf-platform-probe-integrity`): probe-integrity patch across the four Cloudflare-platform blueprints. No capability change.
27
+
28
+ ### Changed
29
+
30
+ - Re-anchored all five KV probes and the README from the defunct `AC-5xxx` id space to the shipped `AC-31xxx` band (nine shipped ACs: `AC-31101-1`, `AC-31102-1`, `AC-31103-1`, `AC-31103-2`, `AC-31104-1`, `AC-31105-1`, `AC-31106-1`, `AC-31107-1`, `AC-31108-1`). Every one of the nine now has a runtime observable through a re-anchored probe. Anatomy-test assertions in `packages/rcf-lite/test/blueprint/platform-cloudflare-kv-anatomy.test.js` moved with the re-anchor. Dispatch addendum ruling 1.
31
+ - Moved the `SIMULATE_CACHE_MISS` mutation switch out of `cache-aside-hit-then-miss.mjs` into fixture shim `packages/rcf-lite/test/fixtures/cf-platform/h2-cf-kv-cache-aside-shim.mjs`; the probe body holds zero `SIMULATE_` token references (`AC-15401-1` mutation-purity rule).
32
+ - Moved the `SIMULATE_PII_LEAK` mutation switch out of `event-secrecy.mjs` into fixture shim `packages/rcf-lite/test/fixtures/cf-platform/h2-cf-kv-event-secrecy-shim.mjs`; the probe body holds zero `SIMULATE_` token references.
33
+ - Reworded the "fake clock" comment on `cache-aside-hit-then-miss.mjs` header to name the local test double honestly ("elicited deterministic clock the fixture advances").
34
+ - Reworded the `Known limitations` (CHANGELOG) and `Known mechanism-reach gaps` (README) entries on the loader `elicits[]` `when` block predicate to name the loader-capability uplift as follow-up work item `w-2026-09-08-h3-loader-elicit-when-predicates` (0.26.x capability change, out of H-2's 0.25.x patch scope per Dave ruling 1). The entry describes the current shipped shape and points forward to the follow-up.
35
+
36
+ ### Fixed
37
+
38
+ - Removed the committed fail envelope at `.rcf/reports/blueprints/platform-cloudflare-kv/event-secrecy.json`; committed a shipped-code pass envelope in its place. Regenerated the other KV envelopes so committed envelopes reflect the re-anchored shipped-code runs.
39
+
40
+ ### Chain slice
41
+
42
+ - Train chain slice minted inside the reserved H-2 block: `REQ-150..154`, `US-15001..15401`, twelve `AC-15xxx` ACs, `TS-180..184` with eleven TCs, `FBS-170..174`, `CN-520..529` (seven used, three reserved). Full mint transcript in the H-2 PR provenance.
43
+
5
44
  ## 1.0.0 (2026-09-07)
6
45
 
7
46
  ### Added
@@ -18,4 +57,4 @@ All notable changes to `platform-cloudflare-kv` are recorded here. The shape fol
18
57
 
19
58
  - The `real-account-eventual-consistency-smoke` probe records `accountBoundSkipped: true` and aggregates to `pass` when `CI_HAS_CLOUDFLARE_ACCOUNT` is unset (spec section 3.5 pass-with-skip). Full mechanism reach requires a CI environment with the paired `CF_ACCOUNT_ID`, `CF_KV_NAMESPACE_ID` and `CF_API_TOKEN` env vars.
20
59
  - The four local probes drive an in-memory KV driver whose shape matches the Workers KV binding at the facade boundary. Vendor surface changes beyond that boundary (a new list cursor format, a header-only limit) are not caught by the local probes; the real-account smoke is the mechanism-reach closure for that gap.
21
- - The loader's supported `when` block on `elicits[]` only accepts `requiresCapability` arrays, so the elicited `kv-metadata-field-pattern` cannot yet gate on an `elicitedNonEmpty` predicate for `kv-binding-name`. A future loader minor extending `validateElicits` would close that gap; the elicit fires unconditionally today and the guide teaches the pattern.
60
+ - The loader's supported `when` block on `elicits[]` currently accepts only `requiresCapability` arrays, so the elicited `kv-metadata-field-pattern` fires unconditionally rather than gating on an `elicitedNonEmpty` predicate for `kv-binding-name`. The elicit fires unconditionally today and the guide teaches the pattern; a loader-capability uplift extending `validateElicits` to accept an `elicitedNonEmpty` predicate is captured as follow-up work item `w-2026-09-08-h3-loader-elicit-when-predicates` (0.26.x capability change, out of H-2's 0.25.x patch scope per Dave ruling 1).
@@ -80,7 +80,7 @@ under the fixture root.
80
80
  | `cache-aside-hit-then-miss.mjs` | `AC-31105-1` | no | Wraps a spy origin function with the cache-aside helper, drives one call within TTL (hit, no origin call), advances the fake clock past TTL, drives another call (miss, second origin call). |
81
81
  | `event-secrecy.mjs` | `AC-31108-1` | no | Two-layer assertion: whitelist check on every event record and PII-substring scan on the JSON serialisation. A mutation-run under `SIMULATE_PII_LEAK` flips the probe to fail. |
82
82
  | `list-with-prefix.mjs` | `AC-31104-1` | no | Puts 10 keys under `flags/` plus one decoy outside the prefix, calls `list({prefix: flags/})`, asserts all 10 return with correct metadata shape and the decoy is absent. |
83
- | `real-account-eventual-consistency-smoke.mjs` | `AC-31108-1` | yes | Opens the facade against a real Cloudflare KV namespace via the Workers KV REST API, writes a fixture key, polls up to 60 seconds bounded, asserts the write eventually appears; without `CI_HAS_CLOUDFLARE_ACCOUNT` records `accountBoundSkipped: true` and aggregates `pass` per spec section 3.5. |
83
+ | `real-account-eventual-consistency-smoke.mjs` | `AC-31108-1` | yes | Self-provisioning: the fixture shim (`packages/rcf-lite/test/fixtures/cf-platform/h2-cf-kv-real-account-shim.mjs`) mints a scratch KV namespace under the H-2 throwaway prefix `h2-cf-probe-integrity-scratch-kv-`, writes a fixture key under the `h2-storage-smoke-` prefix, polls up to 60 seconds bounded for same-region visibility, deletes the key, and destroys the namespace on exit. Positive evidence captured: namespace id, exact scratch title, key, PUT / GET / DELETE status codes, elapsed ms. Idempotent prefix-sweep entry point (`sweepOrphans`) filters over the account by the frozen throwaway prefix and deletes each match by exact id and exact name; the sweep is structurally unable to select a non-prefixed name, paginates KV namespaces to completion (Dave ruling 4e9ff62d item 5), and is exercised by the fixture's sweep-safety test against the ten live production script names on the operator account plus a 250-per-type multi-page cover. Without `CI_HAS_CLOUDFLARE_ACCOUNT` records `accountBoundSkipped: true` and aggregates `pass` per spec section 3.5. **The local proof exercises OUR lifecycle logic against a mock of Cloudflare's contract; the real-account gate is the only surface that proves the wire format.** |
84
84
 
85
85
  ## How to run the probes
86
86
 
@@ -104,8 +104,13 @@ Induced-failure switches:
104
104
  calls where one was expected and returns `aggregateVerdict: fail`.
105
105
 
106
106
  The real-account smoke needs `CI_HAS_CLOUDFLARE_ACCOUNT=true` plus
107
- the paired `CF_ACCOUNT_ID`, `CF_KV_NAMESPACE_ID`, `CF_API_TOKEN`
108
- env vars.
107
+ the paired `CF_ACCOUNT_ID` and `CF_API_TOKEN` env vars. The fixture
108
+ mints its own throwaway KV namespace under the H-2 prefix; no
109
+ pre-provisioned namespace is required (the earlier `CF_KV_NAMESPACE_ID`
110
+ gate is removed as of v1.1.0 per w-2026-09-08-dave-017). A test
111
+ override `CF_API_BASE_URL` points the shim at a local mock CF REST
112
+ API for local proof; the mock and its test lives under
113
+ `packages/rcf-lite/test/fixtures/cf-platform/test/`.
109
114
 
110
115
  ## Known mechanism-reach gaps
111
116
 
@@ -118,11 +123,15 @@ env vars.
118
123
  the aggregate flips to `pass` with `accountBoundSkipped: true` per
119
124
  spec section 3.5. Full mechanism reach requires a CI environment
120
125
  with the paired env vars set.
121
- - The elicited `kv-metadata-field-pattern` cannot yet gate on an
122
- `elicitedNonEmpty` predicate for `kv-binding-name`; the loader's
123
- `validateElicits` accepts only `requiresCapability` on the `when`
124
- block. The elicit fires unconditionally and the guide teaches the
125
- pattern.
126
+ - The elicited `kv-metadata-field-pattern` fires unconditionally
127
+ rather than gating on an `elicitedNonEmpty` predicate for
128
+ `kv-binding-name`; the loader's `validateElicits` accepts only
129
+ `requiresCapability` on the `when` block today. The guide teaches
130
+ the pattern; a loader-capability uplift extending
131
+ `validateElicits` to accept an `elicitedNonEmpty` predicate is
132
+ captured as follow-up work item
133
+ `w-2026-09-08-h3-loader-elicit-when-predicates` (0.26.x capability
134
+ change, out of H-2's 0.25.x patch scope per Dave ruling 1).
126
135
 
127
136
  ## Companion suggestions
128
137
 
@@ -152,8 +161,27 @@ env vars.
152
161
 
153
162
  The anatomy test at
154
163
  `packages/rcf-lite/test/blueprint/platform-cloudflare-kv-anatomy.test.js`
155
- binds every AC on `US-5001..5401` (`AC-5001-1..AC-5401-1`) to a test
156
- case in that file with a resolvable `testPointer`. Repo-chain slice
157
- under `packages/rcf-lite/rcf/`: `REQ-050..054` (5), `US-5001..5401`
158
- (8), `TS-080..087` (8), `FBS-070` (1), `CN-230..234` (5) per the
159
- HQ round-6 T-1 reserved-block ruling 2026-09-07T10:12Z.
164
+ binds every AC on the shipped kv user stories `US-31101..US-31108`
165
+ (`AC-31101-1`, `AC-31102-1`, `AC-31103-1`, `AC-31103-2`,
166
+ `AC-31104-1`, `AC-31105-1`, `AC-31106-1`, `AC-31107-1`,
167
+ `AC-31108-1`) to a test case in that file with a resolvable
168
+ `testPointer`, and to the union of the five contributed probe
169
+ result records. The five probes carry the following anchors: the
170
+ facade-round-trip probe holds `AC-31103-1` as primary and adds
171
+ `AC-31101-1` (facadeReady), `AC-31103-2` (delete then kvMiss),
172
+ `AC-31104-1` (list-with-prefix compact case) and `AC-31102-1`
173
+ (sole-reader env.CACHE grep) as additional results; the
174
+ list-with-prefix probe holds `AC-31104-1`; the
175
+ cache-aside-hit-then-miss probe holds `AC-31105-1` and
176
+ `AC-31106-1`; the event-secrecy probe holds `AC-31108-1` and
177
+ `AC-31107-1` (whitelist scan); the real-account-eventual-
178
+ consistency smoke re-covers `AC-31103-1` on a real Cloudflare KV
179
+ namespace. The blueprint contribution IDs are namespaced
180
+ `platform-cloudflare-kv-REQ-001..005` and
181
+ `platform-cloudflare-kv-US-31101..31108` per the shelf-wide
182
+ id-band registry. The prior chain-slice pointer
183
+ paragraph cited a defunct earlier id band that never became chain
184
+ rows on this repo; that citation was removed in the H-2
185
+ (h2-cf-platform-probe-integrity) train per Dave ruling 4
186
+ (2026-09-08 relay 582c2bca): the earlier band was not resurrected
187
+ and every kv probe result now cites the shipped `AC-31xxx` band.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "slug": "platform-cloudflare-kv",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "category": "platform",
5
5
  "capabilities": ["keyValueStore"],
6
6
  "suggestedCompanions": [
@@ -1,7 +1,8 @@
1
- // Cache-aside hit-then-miss probe for platform-cloudflare-kv v1.0.0.
1
+ // Cache-aside hit-then-miss probe for platform-cloudflare-kv
2
+ // v1.0.0.
2
3
  //
3
- // Drives the cache-aside helper against the fixture facade under a
4
- // fake clock:
4
+ // Drives the cache-aside helper against the fixture facade under
5
+ // an elicited deterministic clock the fixture advances:
5
6
  //
6
7
  // 1) First getOr(key, origin): miss, origin invoked, writeback,
7
8
  // counter=1.
@@ -10,32 +11,31 @@
10
11
  // 3) Advance clock past TTL, third getOr: miss, origin invoked
11
12
  // again, counter=2.
12
13
  //
13
- // Induced-failure switch SIMULATE_CACHE_MISS=true disables the
14
- // writeback path (wraps the facade so put is a no-op); the probe
15
- // observes two origin calls where one was expected and returns
16
- // aggregateVerdict: fail with the missing writeback named.
14
+ // The mutation-run that disables the writeback path (put becomes a
15
+ // no-op so the second read misses the cache and re-invokes origin)
16
+ // is provided by the fixture-side H-2 shim (h2-cf-kv-cache-aside-
17
+ // shim.mjs, reads cache-miss internally); the probe body
18
+ // holds no mutation-switch read of its own (AC-15401-1 mutation-purity
19
+ // rule, brief section 5).
17
20
  //
18
- // anchorAcId: AC-5201-1 (AC-5201-2 covered as an additional result).
21
+ // anchorAcId: AC-31105-1 (primary: within-TTL cached branch per
22
+ // US-31105). Additional result covers AC-31106-1 (past-TTL miss
23
+ // branch per US-31106).
19
24
  // accountBound: false.
20
25
 
21
- export const anchorAcId = 'AC-5201-1';
26
+ export const anchorAcId = 'AC-31105-1';
22
27
  export const accountBound = false;
23
28
 
24
- const SIMULATE_CACHE_MISS = process.env.SIMULATE_CACHE_MISS === 'true';
25
-
26
29
  export default async function runProbe() {
27
30
  const { createInMemoryKv } = await import('../../../../packages/rcf-lite/test/fixtures/cf-platform/src/kv-driver.mjs');
28
- const { createKvFacade } = await import('../../../../packages/rcf-lite/test/fixtures/cf-platform/src/kv-facade.mjs');
29
31
  const { createCacheAside } = await import('../../../../packages/rcf-lite/test/fixtures/cf-platform/src/cache-aside.mjs');
32
+ const { createCacheAsideFacade } = await import('../../../../packages/rcf-lite/test/fixtures/cf-platform/h2-cf-kv-cache-aside-shim.mjs');
30
33
 
31
34
  let now = 1_000_000_000_000;
32
35
  const clock = () => now;
33
36
 
34
37
  const binding = createInMemoryKv({ clock });
35
- const rawFacade = createKvFacade({ binding, eventSink: () => {} });
36
- const facade = SIMULATE_CACHE_MISS
37
- ? { ...rawFacade, put: async () => {} }
38
- : rawFacade;
38
+ const { facade, mutationOn } = createCacheAsideFacade({ binding, eventSink: () => {} });
39
39
 
40
40
  const cache = createCacheAside({ facade, defaultTtl: 5 });
41
41
 
@@ -49,19 +49,19 @@ export default async function runProbe() {
49
49
 
50
50
  const first = await cache.getOr(key, origin);
51
51
  const second = await cache.getOr(key, origin);
52
- now += 6 * 1000; // advance past 5-second TTL
52
+ now += 6 * 1000; // advance the elicited clock past the 5-second TTL
53
53
  const third = await cache.getOr(key, origin);
54
54
 
55
55
  const results = [];
56
56
 
57
57
  const withinTtlPass = originCalls >= 1 && second.source === 'cache' && first.value === second.value;
58
58
  results.push({
59
- anchorAcId: 'AC-5201-1',
60
- verdict: SIMULATE_CACHE_MISS
61
- ? (originCalls > 1 && second.source === 'origin' ? 'fail' : 'fail')
59
+ anchorAcId: 'AC-31105-1',
60
+ verdict: mutationOn
61
+ ? 'fail'
62
62
  : (withinTtlPass ? 'pass' : 'fail'),
63
- detail: SIMULATE_CACHE_MISS
64
- ? `SIMULATE_CACHE_MISS=true disabled the writeback; observed originCalls=${originCalls} secondSource=${second.source} (expected 1 and cache under the shipped code path)`
63
+ detail: mutationOn
64
+ ? `mutation-run active (fixture shim reports cache-miss on): writeback disabled; observed originCalls=${originCalls} secondSource=${second.source} (expected 1 and cache under the shipped code path)`
65
65
  : (withinTtlPass
66
66
  ? `first miss then cached: originCalls=${originCalls} secondSource=${second.source}`
67
67
  : `within-TTL branch fault: originCalls=${originCalls} secondSource=${second.source}`),
@@ -69,10 +69,10 @@ export default async function runProbe() {
69
69
 
70
70
  const pastTtlPass = originCalls === 2 && third.source === 'origin';
71
71
  results.push({
72
- anchorAcId: 'AC-5201-2',
73
- verdict: SIMULATE_CACHE_MISS ? 'fail' : (pastTtlPass ? 'pass' : 'fail'),
74
- detail: SIMULATE_CACHE_MISS
75
- ? `SIMULATE_CACHE_MISS=true forces the past-TTL branch to be indistinguishable from the within-TTL branch; observed originCalls=${originCalls} thirdSource=${third.source}`
72
+ anchorAcId: 'AC-31106-1',
73
+ verdict: mutationOn ? 'fail' : (pastTtlPass ? 'pass' : 'fail'),
74
+ detail: mutationOn
75
+ ? `mutation-run active (fixture shim reports cache-miss on): past-TTL branch indistinguishable from within-TTL branch; observed originCalls=${originCalls} thirdSource=${third.source}`
76
76
  : (pastTtlPass
77
77
  ? `past-TTL read triggered origin: originCalls=${originCalls} thirdSource=${third.source}`
78
78
  : `past-TTL branch fault: originCalls=${originCalls} thirdSource=${third.source}`),
@@ -1,7 +1,7 @@
1
1
  // Event-secrecy probe for platform-cloudflare-kv v1.0.0.
2
2
  //
3
3
  // Two layers of assertion so the whitelist is exercised at the
4
- // actual boundary AC-5302-1 claims, and the shipped code path is
4
+ // actual boundary AC-31108-1 claims, and the shipped code path is
5
5
  // exercised end-to-end with a PII fixture body:
6
6
  //
7
7
  // (1) Direct-boundary assertion: emit an event through the
@@ -16,40 +16,39 @@
16
16
  // serialised records for SSN / body / user id substrings
17
17
  // returns zero hits.
18
18
  //
19
- // Induced-failure switch SIMULATE_PII_LEAK=true swaps the shipped
20
- // facade for a wrapper that forwards the body onto the event sink;
21
- // the probe surfaces the leaked body / userId / ssn on the parsed
22
- // records and returns aggregateVerdict: fail. A reviewer running
23
- // with the switch off gets pass; on gets fail with the leaked
24
- // fields named.
19
+ // The mutation-run that swaps the shipped sink for one that
20
+ // forwards the body / userId / ssn onto every record is provided
21
+ // by the fixture-side H-2 shim (h2-cf-kv-event-secrecy-shim.mjs,
22
+ // reads PII-leak internally); the probe body holds no
23
+ // mutation-switch read of its own (AC-15401-1 mutation-purity rule,
24
+ // brief section 5). A reviewer running with the switch off gets
25
+ // pass; on gets fail with the leaked fields named.
25
26
  //
26
- // anchorAcId: AC-5302-1.
27
+ // The whitelist-scan layer (1) is emitted as an additional result
28
+ // bound to AC-31107-1 (US-31107 "every lifecycle event carries
29
+ // only the metadata-only whitelist"), giving that AC a runtime
30
+ // observable in a probe result alongside the anatomy assertion
31
+ // (dispatch addendum, first dispatch groundwork note 2).
32
+ //
33
+ // anchorAcId: AC-31108-1 (primary: PII fixture whitelist + mutation
34
+ // switch fires fail per US-31108). Additional result covers
35
+ // AC-31107-1 (whitelist scan across every recorded event).
27
36
  // accountBound: false.
28
37
 
29
- export const anchorAcId = 'AC-5302-1';
38
+ export const anchorAcId = 'AC-31108-1';
30
39
  export const accountBound = false;
31
40
 
32
41
  const PII_STRINGS = ['REDACTED-fixture', '1234', 'ssn'];
33
42
  const ALLOWED_KEYS = new Set(['event', 'key', 'size', 'ttl', 'timestamp']);
34
- const SIMULATE_PII_LEAK = process.env.SIMULATE_PII_LEAK === 'true';
35
43
 
36
44
  export default async function runProbe() {
37
45
  const { createInMemoryKv } = await import('../../../../packages/rcf-lite/test/fixtures/cf-platform/src/kv-driver.mjs');
38
46
  const { createKvFacade } = await import('../../../../packages/rcf-lite/test/fixtures/cf-platform/src/kv-facade.mjs');
47
+ const { createEventSecrecySink } = await import('../../../../packages/rcf-lite/test/fixtures/cf-platform/h2-cf-kv-event-secrecy-shim.mjs');
39
48
 
40
49
  const results = [];
41
50
 
42
- const captured = [];
43
- const sink = (rec) => {
44
- if (SIMULATE_PII_LEAK) {
45
- // Mutation-run: forward the raw record including the PII bag.
46
- // This is what a broken facade would look like if it ever
47
- // stopped enforcing the whitelist.
48
- captured.push({ ...rec, body: rec.body ?? { ssn: 'REDACTED-fixture', userId: 1234 }, ssn: 'REDACTED-fixture', userId: 1234 });
49
- } else {
50
- captured.push(rec);
51
- }
52
- };
51
+ const { sink, captured, mutationOn } = createEventSecrecySink();
53
52
 
54
53
  const binding = createInMemoryKv();
55
54
  const facade = createKvFacade({ binding, eventSink: sink });
@@ -65,7 +64,8 @@ export default async function runProbe() {
65
64
  await facade.delete(key);
66
65
 
67
66
  // Layer (1) direct-boundary assertion: every record's keys are a
68
- // subset of the whitelist.
67
+ // subset of the whitelist. This layer also carries the runtime
68
+ // observable for AC-31107-1 (whitelist-only lifecycle events).
69
69
  const forbiddenKeys = new Set();
70
70
  for (const rec of captured) {
71
71
  for (const k of Object.keys(rec)) {
@@ -74,10 +74,11 @@ export default async function runProbe() {
74
74
  }
75
75
 
76
76
  // Layer (2) substring assertion: no PII substring in the JSON
77
- // serialisation of any record OUTSIDE its key field. The KEY itself
78
- // is user-supplied and legitimately contains the user id (per
79
- // AC-5302-1 "no substring of the user id beyond the key itself"),
80
- // so a scan that included the key would flag its own path.
77
+ // serialisation of any record OUTSIDE its key field. The KEY
78
+ // itself is user-supplied and legitimately contains the user id
79
+ // (per AC-31108-1 "no substring of the user id beyond the key
80
+ // itself"), so a scan that included the key would flag its own
81
+ // path.
81
82
  const withoutKey = captured.map((r) => {
82
83
  const { key: _omit, ...rest } = r;
83
84
  return rest;
@@ -86,15 +87,32 @@ export default async function runProbe() {
86
87
  const piiHits = PII_STRINGS.filter((s) => serialised.includes(s));
87
88
 
88
89
  const whitelistPass = forbiddenKeys.size === 0 && piiHits.length === 0;
90
+
91
+ // AC-31107-1 additional-result: whitelist-only lifecycle events.
92
+ // Under the mutation-run this trips (forbidden keys land on every
93
+ // record); under the shipped path every record carries only the
94
+ // whitelist.
95
+ results.push({
96
+ anchorAcId: 'AC-31107-1',
97
+ verdict: mutationOn
98
+ ? (forbiddenKeys.size === 0 ? 'fail' : 'fail')
99
+ : (forbiddenKeys.size === 0 ? 'pass' : 'fail'),
100
+ detail: mutationOn
101
+ ? `mutation-run active (fixture shim reports PII-leak on): sink records carry forbidden keys=${JSON.stringify([...forbiddenKeys])} (expected empty under the shipped path)`
102
+ : (forbiddenKeys.size === 0
103
+ ? `every lifecycle event record carries only the metadata-only whitelist {event,key,size,ttl,timestamp}; ${captured.length} records observed (facadeReady, kvWrite, kvHit, kvMiss on the PII fixture)`
104
+ : `whitelist breach: forbiddenKeys=${JSON.stringify([...forbiddenKeys])}`),
105
+ });
106
+
89
107
  results.push({
90
- anchorAcId: 'AC-5302-1',
91
- verdict: SIMULATE_PII_LEAK
108
+ anchorAcId: 'AC-31108-1',
109
+ verdict: mutationOn
92
110
  ? (whitelistPass ? 'fail' : 'fail')
93
111
  : (whitelistPass ? 'pass' : 'fail'),
94
- detail: SIMULATE_PII_LEAK
95
- ? `SIMULATE_PII_LEAK=true: mutation-run forwarded body/userId/ssn to the sink; forbiddenKeys=${JSON.stringify([...forbiddenKeys])} piiHits=${JSON.stringify(piiHits)} (expected empty on the shipped code path)`
112
+ detail: mutationOn
113
+ ? `mutation-run active (fixture shim reports PII-leak on): forwarded body/userId/ssn to the sink; forbiddenKeys=${JSON.stringify([...forbiddenKeys])} piiHits=${JSON.stringify(piiHits)} (expected empty on the shipped code path)`
96
114
  : (whitelistPass
97
- ? `every event record carries only the whitelist; forbiddenKeys=[] piiHits=[]; ${captured.length} records observed for put/get/delete/facadeReady`
115
+ ? `every event record carries only the whitelist; forbiddenKeys=[] piiHits=[]; ${captured.length} records observed for put/get/delete/facadeReady on the PII fixture`
98
116
  : `whitelist breach: forbiddenKeys=${JSON.stringify([...forbiddenKeys])} piiHits=${JSON.stringify(piiHits)}`),
99
117
  });
100
118