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
@@ -0,0 +1,25 @@
1
+ {
2
+ "usId": "object-storage-s3-US-28110",
3
+ "prdId": "PRD-001",
4
+ "reqId": "object-storage-s3-REQ-101",
5
+ "version": "1.1.0",
6
+ "status": "approved",
7
+ "title": "Hetzner Object Storage adapter: helper composes <bucket>.<location>.your-objectstorage.com; new hetzner-object-storage-round-trip probe drives put/get/delete against the composed endpoint",
8
+ "asA": "shelf consumer applying object-storage-s3 v1.1.0 on a Hetzner Object Storage bucket in one of the vendor-documented locations (fsn1, hel1, nbg1)",
9
+ "iWant": "an endpoint-shape helper that composes <bucket>.<location>.your-objectstorage.com from an elicited bucket and location code, and a Node-only probe hetzner-object-storage-round-trip.mjs driven from the extended infra-s3-and-queue fixture that either round-trips a byte-equal put/get/delete against a real Hetzner Object Storage bucket when CI_HAS_HETZNER_OBJECT_STORAGE is set alongside HETZNER_OBJECT_STORAGE_ACCESS_KEY_ID, HETZNER_OBJECT_STORAGE_SECRET_ACCESS_KEY, HETZNER_OBJECT_STORAGE_BUCKET and HETZNER_OBJECT_STORAGE_LOCATION or records accountBoundSkipped true when the env var is unset",
10
+ "soThat": "the shelf gains a fourth provider value alongside R2, AWS and MinIO with no runtime dependency added to the applying project, no facade re-shape, and no regression to the shipped MinIO local-emulator or R2 real-account paths",
11
+ "acceptanceCriteria": [
12
+ {
13
+ "id": "AC-28110-1",
14
+ "description": "Given the extended infra-s3-and-queue fixture at packages/rcf-lite/test/fixtures/infra-s3-and-queue/ carrying src/hetzner-endpoint.mjs and run-hetzner-object-storage-round-trip.mjs, when node blueprints/object-storage-s3/contributions/probes/run-hetzner-object-storage-round-trip.mjs runs, then either (a) with CI_HAS_HETZNER_OBJECT_STORAGE set alongside the four Hetzner env vars the composed endpoint is https://<bucket>.<location>.your-objectstorage.com per https://docs.hetzner.com/storage/object-storage/overview and put/get/delete round-trips a byte-equal 1 KiB payload with teardown deleting the scratch object, or (b) without CI_HAS_HETZNER_OBJECT_STORAGE the probe records accountBoundSkipped true and the aggregate flips to pass per spec section 3.5. The endpoint-shape helper hetzner-endpoint.mjs is the sole composer of the vendor pattern; SIMULATE_HETZNER_ENDPOINT_MISSHAPEN true composes an invalid endpoint via the fixture-side shim (INPUT-side mutation) and the probe FAILS naming the missing subdomain. Label AC-hetznerObjectStorage-endpointRoundTrip.",
15
+ "given": "the extended infra-s3-and-queue fixture with src/hetzner-endpoint.mjs (endpoint-shape helper) and the fixture-root probe shim, on either a runner with CI_HAS_HETZNER_OBJECT_STORAGE set (real-account round-trip path) or without it (accountBoundSkipped path)",
16
+ "when": "node blueprints/object-storage-s3/contributions/probes/run-hetzner-object-storage-round-trip.mjs runs",
17
+ "then": "the probe either round-trips a byte-equal 1 KiB payload via put/get/delete against the composed <bucket>.<location>.your-objectstorage.com endpoint or records accountBoundSkipped true; SIMULATE_HETZNER_ENDPOINT_MISSHAPEN true composes an invalid endpoint via the fixture-side shim and the probe FAILS naming the missing subdomain",
18
+ "testable": true,
19
+ "scope": "runtime"
20
+ }
21
+ ],
22
+ "tacIds": ["TAC-2904-object-storage-s3-hetzner-endpoint-helper"],
23
+ "createdAt": "2026-09-08T00:00:00.000Z",
24
+ "updatedAt": "2026-09-08T00:00:00.000Z"
25
+ }
@@ -45,7 +45,7 @@ This table is maintained shelf-wide across every blueprint's `docs/topics.md`. R
45
45
  | application-account-settings | 25101-25899 | 26xx | shipped v1.0.0 | none |
46
46
  | application-onboarding-tour | 26101-26899 | 27xx | shipped v1.0.0 | none |
47
47
  | persistence-data-postgres | 27101-27899 | 28xx | shipped v1.0.0 | `persistenceStore`, `migrationDiscipline` |
48
- | object-storage-s3 | 28101-28899 | 29xx | shipped v1.0.0 | `objectStorageContract` |
48
+ | object-storage-s3 | 28101-28899 | 29xx | shipped v1.1.0 | `objectStorageContract` |
49
49
  | messaging-queue-cloudflare | 29101-29899 | 30xx | shipped v1.0.0 | `deliverySemantics` |
50
50
  | jobs-background | 30101-30899 | 31xx | shipped v1.0.0 | `backgroundJobModel` |
51
51
  | platform-cloudflare-kv | 31101-31899 | 32xx | shipped v1.0.0 | `keyValueStoreContract` |
@@ -185,6 +185,41 @@ Alternate providers (GitLab CI, CircleCI, Buildkite) map through the same four-p
185
185
 
186
186
  The `accountBound` flag on the probe module surface flips the delivery-ci-workflows aggregate to pass on the skipped path per section 3.5.
187
187
 
188
+ ## Hetzner Object Storage adapter (v1.1.0)
189
+
190
+ The v1.1.0 follow-up recognises Hetzner Object Storage as a shipped provider value under the same S3-API facade. Hetzner Object Storage is S3-compatible per https://docs.hetzner.com/storage/object-storage/overview and speaks the S3 wire protocol through `@aws-sdk/client-s3` unchanged; the one thing that differs from R2 and AWS S3 is that the endpoint URL is composed from two elicited values, a bucket name and a location code (one of `fsn1` Falkenstein, `hel1` Helsinki, `nbg1` Nuremberg per the same page), rather than pasted whole.
191
+
192
+ A small helper in the applying project composes the endpoint:
193
+
194
+ ```js
195
+ import { composeHetznerEndpoint } from './hetzner-endpoint.mjs';
196
+
197
+ const endpoint = composeHetznerEndpoint({
198
+ bucket: process.env.HETZNER_OBJECT_STORAGE_BUCKET,
199
+ location: process.env.HETZNER_OBJECT_STORAGE_LOCATION, // fsn1 | hel1 | nbg1
200
+ });
201
+ // endpoint === 'https://<bucket>.<location>.your-objectstorage.com'
202
+ ```
203
+
204
+ The composed URL feeds `createObjectStore` unchanged through the elicited `endpointUrl` parameter; `region` defaults to `auto`, `forcePathStyle` defaults to `false` (virtual-hosted-style). The credential pair reaches the facade through the same `security-secrets-management` surface; a Hetzner-shaped project stores the key pair as `HETZNER_OBJECT_STORAGE_ACCESS_KEY_ID` / `HETZNER_OBJECT_STORAGE_SECRET_ACCESS_KEY` and reads them through the secrets facade.
205
+
206
+ The helper refuses composition on any location code outside `{fsn1, hel1, nbg1}` (throws a named `Error` with `err.field === 'location'`), so an applying project cannot silently ship an invalid endpoint by editing a config file. A future vendor location code lands as a v1.2.0 minor bump on this blueprint (helper edit plus one more enum entry), not a project-side workaround.
207
+
208
+ `node blueprints/object-storage-s3/contributions/probes/run-hetzner-object-storage-round-trip.mjs`:
209
+
210
+ - Without `CI_HAS_HETZNER_OBJECT_STORAGE`: exits 0, writes a report with `verdict: pass, detail: "accountBound: skipped (no CI_HAS_HETZNER_OBJECT_STORAGE)", accountBoundSkipped: true`. Aggregate verdict pass.
211
+ - With `CI_HAS_HETZNER_OBJECT_STORAGE` set alongside `HETZNER_OBJECT_STORAGE_ACCESS_KEY_ID`, `HETZNER_OBJECT_STORAGE_SECRET_ACCESS_KEY`, `HETZNER_OBJECT_STORAGE_BUCKET`, `HETZNER_OBJECT_STORAGE_LOCATION` on the runner env (or `.rcf/secrets/dev.env` locally): composes the endpoint, puts a 1 KiB payload, gets it back byte-equal, deletes the temporary object on exit, and asserts every lifecycle event carries only whitelisted metadata (`event`, `ts`, `endpointHost`, `bucketName`, `location`, `key`, `size`, `contentType`). Aggregate verdict pass.
212
+
213
+ The Cloudflare R2 smoke stays gated on `CI_HAS_CLOUDFLARE_ACCOUNT` and is not affected by the Hetzner env var; the two smokes skip independently.
214
+
215
+ ### When to reach for Hetzner Object Storage
216
+
217
+ - Storage region matters (European data-locality requirements or lower-latency reads from EU-hosted workloads). `fsn1`, `hel1`, `nbg1` cover Germany and Finland; R2 does not surface a per-region locality on the public shape.
218
+ - Egress-pricing profile differs from R2's zero-egress-fee posture; a workload with high internal-only egress may prefer R2, while a workload with predictable public egress may prefer Hetzner's flat per-TB pricing.
219
+ - The applying project already runs on Hetzner Cloud (deploy-hetzner-server v1.0.0 shipped in round 7 T-1) and pairing the storage on the same vendor account is operationally simpler.
220
+
221
+ Reach for R2 for the zero-egress-fee posture, for AWS S3 for the widest feature surface (Object Lock, versioning, lifecycle policies), and for MinIO for local dev without any account.
222
+
188
223
  ## Elicited parameters
189
224
 
190
225
  | Parameter | Default | Notes |
@@ -204,4 +239,5 @@ The `accountBound` flag on the probe module surface flips the delivery-ci-workfl
204
239
  - Cloudflare R2 S3-compatibility surface: https://developers.cloudflare.com/r2/api/s3/api/ (supported ops, four deviations).
205
240
  - Cloudflare R2 overview and presigned URLs: https://developers.cloudflare.com/r2/.
206
241
  - MinIO S3 compatibility: https://docs.min.io/community/minio-object-store/administration/object-management/object-lifecycle-management.html.
242
+ - Hetzner Object Storage overview (v1.1.0): https://docs.hetzner.com/storage/object-storage/overview (S3 compatibility, endpoint pattern `<bucket>.<location>.your-objectstorage.com`, three location codes fsn1, hel1, nbg1).
207
243
  - GitHub Actions service containers: https://docs.github.com/en/actions/using-containerized-services/about-service-containers.
@@ -61,7 +61,7 @@ This table is maintained shelf-wide across every blueprint's `docs/topics.md`. R
61
61
  | application-account-settings | 25101-25899 | 26xx | shipped v1.0.0 | none |
62
62
  | application-onboarding-tour | 26101-26899 | 27xx | shipped v1.0.0 | none |
63
63
  | persistence-data-postgres | 27101-27899 | 28xx | shipped v1.0.0 | `persistenceStore`, `migrationDiscipline` |
64
- | object-storage-s3 | 28101-28899 | 29xx | shipped v1.0.0 | `objectStorageContract` |
64
+ | object-storage-s3 | 28101-28899 | 29xx | shipped v1.1.0 | `objectStorageContract` |
65
65
  | messaging-queue-cloudflare | 29101-29899 | 30xx | shipped v1.0.0 | `deliverySemantics` |
66
66
  | jobs-background | 30101-30899 | 31xx | shipped v1.0.0 | `backgroundJobModel` |
67
67
  | platform-cloudflare-kv | 31101-31899 | 32xx | shipped v1.0.0 | `keyValueStoreContract` |
@@ -28,7 +28,7 @@
28
28
  | application-account-settings | 25101-25899 | 26xx | shipped v1.0.0 | none |
29
29
  | application-onboarding-tour | 26101-26899 | 27xx | shipped v1.0.0 | none |
30
30
  | persistence-data-postgres | 27101-27899 | 28xx | shipped v1.0.0 | `persistenceStore`, `migrationDiscipline` |
31
- | object-storage-s3 | 28101-28899 | 29xx | shipped v1.0.0 | `objectStorageContract` |
31
+ | object-storage-s3 | 28101-28899 | 29xx | shipped v1.1.0 | `objectStorageContract` |
32
32
  | messaging-queue-cloudflare | 29101-29899 | 30xx | shipped v1.0.0 | `deliverySemantics` |
33
33
  | jobs-background | 30101-30899 | 31xx | shipped v1.0.0 | `backgroundJobModel` |
34
34
  | platform-cloudflare-kv | 31101-31899 | 32xx | shipped v1.0.0 | `keyValueStoreContract` |
@@ -51,7 +51,7 @@ This table is maintained shelf-wide across every blueprint's `docs/topics.md`. R
51
51
  | application-account-settings | 25101-25899 | 26xx | shipped v1.0.0 | none |
52
52
  | application-onboarding-tour | 26101-26899 | 27xx | shipped v1.0.0 | none |
53
53
  | persistence-data-postgres | 27101-27899 | 28xx | shipped v1.0.0 | `persistenceStore`, `migrationDiscipline` |
54
- | object-storage-s3 | 28101-28899 | 29xx | shipped v1.0.0 | `objectStorageContract` |
54
+ | object-storage-s3 | 28101-28899 | 29xx | shipped v1.1.0 | `objectStorageContract` |
55
55
  | messaging-queue-cloudflare | 29101-29899 | 30xx | shipped v1.0.0 | `deliverySemantics` |
56
56
  | jobs-background | 30101-30899 | 31xx | shipped v1.0.0 | `backgroundJobModel` |
57
57
  | platform-cloudflare-kv | 31101-31899 | 32xx | shipped v1.0.0 | `keyValueStoreContract` |
@@ -49,7 +49,7 @@ This table is maintained shelf-wide across every blueprint's `docs/topics.md`. R
49
49
  | application-account-settings | 25101-25899 | 26xx | shipped v1.0.0 | none |
50
50
  | application-onboarding-tour | 26101-26899 | 27xx | shipped v1.0.0 | none |
51
51
  | persistence-data-postgres | 27101-27899 | 28xx | shipped v1.0.0 | `persistenceStore`, `migrationDiscipline` |
52
- | object-storage-s3 | 28101-28899 | 29xx | shipped v1.0.0 | `objectStorageContract` |
52
+ | object-storage-s3 | 28101-28899 | 29xx | shipped v1.1.0 | `objectStorageContract` |
53
53
  | messaging-queue-cloudflare | 29101-29899 | 30xx | shipped v1.0.0 | `deliverySemantics` |
54
54
  | jobs-background | 30101-30899 | 31xx | shipped v1.0.0 | `backgroundJobModel` |
55
55
  | platform-cloudflare-kv | 31101-31899 | 32xx | shipped v1.0.0 | `keyValueStoreContract` |
@@ -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` |
@@ -49,7 +49,7 @@ This table is maintained shelf-wide across every blueprint's `docs/topics.md`. R
49
49
  | application-account-settings | 25101-25899 | 26xx | shipped v1.0.0 | none |
50
50
  | application-onboarding-tour | 26101-26899 | 27xx | shipped v1.0.0 | none |
51
51
  | persistence-data-postgres | 27101-27899 | 28xx | shipped v1.0.0 | `persistenceStore`, `migrationDiscipline` |
52
- | object-storage-s3 | 28101-28899 | 29xx | shipped v1.0.0 | `objectStorageContract` |
52
+ | object-storage-s3 | 28101-28899 | 29xx | shipped v1.1.0 | `objectStorageContract` |
53
53
  | messaging-queue-cloudflare | 29101-29899 | 30xx | shipped v1.0.0 | `deliverySemantics` |
54
54
  | jobs-background | 30101-30899 | 31xx | shipped v1.0.0 | `backgroundJobModel` |
55
55
  | platform-cloudflare-kv | 31101-31899 | 32xx | shipped v1.0.0 | `keyValueStoreContract` |
@@ -46,7 +46,7 @@ This table is maintained shelf-wide across every blueprint's `docs/topics.md`. R
46
46
  | application-account-settings | 25101-25899 | 26xx | shipped v1.0.0 | none |
47
47
  | application-onboarding-tour | 26101-26899 | 27xx | shipped v1.0.0 | none |
48
48
  | persistence-data-postgres | 27101-27899 | 28xx | shipped v1.0.0 | `persistenceStore`, `migrationDiscipline` |
49
- | object-storage-s3 | 28101-28899 | 29xx | shipped v1.0.0 | `objectStorageContract` |
49
+ | object-storage-s3 | 28101-28899 | 29xx | shipped v1.1.0 | `objectStorageContract` |
50
50
  | messaging-queue-cloudflare | 29101-29899 | 30xx | shipped v1.0.0 | `deliverySemantics` |
51
51
  | jobs-background | 30101-30899 | 31xx | shipped v1.0.0 | `backgroundJobModel` |
52
52
  | platform-cloudflare-kv | 31101-31899 | 32xx | shipped v1.0.0 | `keyValueStoreContract` |
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to `platform-cloudflare-durable-objects` are recorded here. The shape follows Keep a Changelog and Semantic Versioning per the blueprint authoring standard.
4
4
 
5
+ ## 1.0.1 (2026-09-08)
6
+
7
+ H-2 hardening train (`h2-cf-platform-probe-integrity`): probe-integrity patch. No capability change.
8
+
9
+ ### Changed
10
+
11
+ - Replaced the credential-readiness pass-stub in `real-account-storage-smoke.mjs` with a real HTTP PUT-then-GET storage round-trip driver against a deployed Worker route (`/cell/:id/storage/:key`) that opens the DO facade, writes a fixture key, reads it back, and asserts byte equality. Activated when `CI_HAS_CLOUDFLARE_ACCOUNT` is set with `CF_DO_WORKER_URL`, `CF_ACCOUNT_ID`, `CF_DO_NAMESPACE_ID` and `CF_API_TOKEN`. Locally proven against `wrangler dev --local` (64-byte round-trip byte-equal). A pass is no longer reachable from credential presence alone.
12
+ - Extended anatomy-level anchor coverage for the five previously-unbound ACs (`AC-33106-1`, `AC-33108-1`, `AC-33109-1`, `AC-33110-1`, `AC-33111-1`) via multi-anchor `TC-H2-B2-*` assertions on the existing carrier probes (websocket-hub-broadcast, wrangler-seam, single-cell-concurrent-increment, storage-round-trip). No new probe body minted; every one of the 13 shipped DO ACs is now asserted at anatomy level.
13
+ - Moved DO mutation switches out of probe bodies into fixture-side shims: `SIMULATE_NON_FACADE_IMPORT` (sole-reader-scan) into `h2-cf-do-sole-reader-shim.mjs`; `SIMULATE_STORAGE_BACKEND_MISMATCH` (storage-round-trip) into `h2-cf-do-storage-round-trip-shim.mjs`; `SIMULATE_PII_LEAK` and `SIMULATE_HUB_HANG` (websocket-hub-broadcast) into `h2-cf-do-websocket-hub-shim.mjs`. All three DO probe bodies now hold zero `SIMULATE_` token references (`AC-15401-1` mutation-purity rule).
14
+ - Reworded the "fake state" and "fake WebSocket pair" comments on `namespace-facade-ready.mjs`, `websocket-hub-broadcast.mjs` and `probe-utils.mjs` to name the local test doubles honestly ("in-process DO state double", "in-process paired WebSockets"). `createFakeSocketPair` helper renamed `createInProcessSocketPair` with a back-compat alias.
15
+
16
+ ### Fixed
17
+
18
+ - Extended the fixture Worker at `packages/rcf-lite/test/fixtures/cf-platform/src/do-single-cell.mjs` and `src/index.mjs` with the `/cell/:id/storage/:key` route on the DO facade so the real-account driver has a runtime carrier.
19
+
5
20
  ## 1.0.0 (2026-09-07)
6
21
 
7
22
  ### Added
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "slug": "platform-cloudflare-durable-objects",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "category": "platform",
5
5
  "capabilities": [
6
6
  "strongConsistencyCell",
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Opens the DO facade against an in-process pair of DO instances
4
4
  // (a SingleCellObject wired to the in-memory storage driver and a
5
- // HubObject wired to a fake state), calls ready(), asserts
5
+ // HubObject wired to an in-process DO state double), calls ready(), asserts
6
6
  // namespaceReady fires on the injected sink with a metadata-only
7
7
  // payload {event, key, size, ttl, timestamp} and asserts the
8
8
  // facade exposes cell(name) and hub(name) handles that return the
@@ -19,7 +19,7 @@ export default async function runProbe() {
19
19
  const { SingleCellObject } = await import('../../../../packages/rcf-lite/test/fixtures/cf-platform/src/do-single-cell.mjs');
20
20
  const { HubObject } = await import('../../../../packages/rcf-lite/test/fixtures/cf-platform/src/do-hub.mjs');
21
21
  const { createDoFacadeInProcess } = await import('../../../../packages/rcf-lite/test/fixtures/cf-platform/src/do-facade.mjs');
22
- const { createFakeState } = await import('./probe-utils.mjs');
22
+ const { createInProcessDoState } = await import('./probe-utils.mjs');
23
23
 
24
24
  const events = [];
25
25
  const eventSink = (rec) => events.push(rec);
@@ -27,7 +27,7 @@ export default async function runProbe() {
27
27
  const cellStorage = createInMemoryDoStorage({ backend: 'sql' });
28
28
  const hubStorage = createInMemoryDoStorage({ backend: 'sql' });
29
29
  const singleCell = new SingleCellObject({ storage: cellStorage, eventSink, name: 'main' });
30
- const hub = new HubObject({ state: createFakeState(), storage: hubStorage, eventSink, name: 'lobby' });
30
+ const hub = new HubObject({ state: createInProcessDoState(), storage: hubStorage, eventSink, name: 'lobby' });
31
31
  const facade = createDoFacadeInProcess({ singleCell, hub, eventSink });
32
32
 
33
33
  const results = [];
@@ -5,14 +5,15 @@
5
5
  // driver from the cf-platform fixture src/ tree so rcf-lite itself
6
6
  // gains no new runtime dependency. Six probes drive the shipped
7
7
  // code path in-process against the in-memory driver; one probe
8
- // (real-account-storage-smoke) opens the Cloudflare Workers API
9
- // when CI_HAS_CLOUDFLARE_ACCOUNT is set. Without the env var it
10
- // records accountBoundSkipped and aggregates to pass per spec
11
- // section 3.5.
8
+ // (real-account-storage-smoke) drives an HTTP round trip against a
9
+ // deployed Worker when CI_HAS_CLOUDFLARE_ACCOUNT is set. Without
10
+ // the env var it records accountBoundSkipped and aggregates to
11
+ // pass per spec section 3.5.
12
12
  //
13
13
  // The wrangler dev seam is documented in the fixture README under
14
- // the "T-3 wrangler dev optional boot" section; the shipped probes
15
- // do NOT require a wrangler dev process to drive their assertions.
14
+ // the "T-3 wrangler dev optional boot" section; the shipped
15
+ // in-process probes do NOT require a wrangler dev process to
16
+ // drive their assertions.
16
17
 
17
18
  import { mkdir, writeFile } from 'node:fs/promises';
18
19
  import { dirname, resolve } from 'node:path';
@@ -70,11 +71,13 @@ export async function runShim(probeName, engine, mainFn) {
70
71
  }
71
72
  }
72
73
 
73
- // A minimal fake WebSocket pair used by the hub probes. Each fake
74
- // exposes send(str) which buffers into inbound[] on the paired
75
- // fake; close(code, reason) is a no-op. The hub's state.getWebSockets()
76
- // returns the list of accepted fakes.
77
- export function createFakeSocketPair(name = 'ws') {
74
+ // A minimal in-process WebSocket pair used by the hub probes. Each
75
+ // paired socket exposes send(str) which buffers into inbound[] on
76
+ // the same socket so a probe can assert delivery; close(code,
77
+ // reason) is a no-op. This is a local test double for the WebSocket
78
+ // contract the hub actually depends on (send + close), scoped to
79
+ // the probe run only.
80
+ export function createInProcessSocketPair(name = 'ws') {
78
81
  const inbound = [];
79
82
  const closed = { flag: false };
80
83
  const socket = {
@@ -83,8 +86,8 @@ export function createFakeSocketPair(name = 'ws') {
83
86
  closed,
84
87
  send(str) {
85
88
  // Route what the hub sends back into inbound so a probe can
86
- // assert delivery. The pairing is per-socket; the hub calls
87
- // send() on every socket it wants to reach.
89
+ // assert delivery. The hub calls send() on every socket it
90
+ // wants to reach.
88
91
  inbound.push(str);
89
92
  },
90
93
  close(code, reason) {
@@ -96,10 +99,21 @@ export function createFakeSocketPair(name = 'ws') {
96
99
  return socket;
97
100
  }
98
101
 
99
- export function createFakeState() {
102
+ // Minimal in-process DurableObjectState double. Exposes the two
103
+ // members the hub calls on state (acceptWebSocket, getWebSockets);
104
+ // scoped to the probe run only.
105
+ export function createInProcessDoState() {
100
106
  const accepted = [];
101
107
  return {
102
108
  acceptWebSocket(ws) { accepted.push(ws); },
103
109
  getWebSockets() { return accepted.slice(); },
104
110
  };
105
111
  }
112
+
113
+ // Backwards-compatible aliases. The rename to createInProcess* was
114
+ // driven by the honest-labelling sweep in H-2 (finding rows
115
+ // f-2026-09-08-stage2-218 and f-2026-09-08-stage2-219); the
116
+ // aliases below let any existing anatomy assertion or downstream
117
+ // probe still resolve the older names during the same train.
118
+ export const createFakeSocketPair = createInProcessSocketPair;
119
+ export const createFakeState = createInProcessDoState;
@@ -1,58 +1,164 @@
1
1
  // Real-account-storage-smoke probe for platform-cloudflare-durable-objects v1.0.0.
2
2
  //
3
- // Opens the DO facade against a real Cloudflare account with a
4
- // real DO namespace and drives a storage round-trip with the
5
- // elicited backend. Without CI_HAS_CLOUDFLARE_ACCOUNT=true the
6
- // probe records accountBoundSkipped and aggregates to pass per
7
- // spec section 3.5 (Clerk pattern).
3
+ // Drives an HTTP round trip against a deployed Worker route that
4
+ // opens the DO facade for a named SingleCellObject, writes a
5
+ // fixture key with random bytes under the DO's per-instance
6
+ // storage, reads the same key back, and asserts the returned
7
+ // bytes are byte-equal to the written value (AC-33112-1).
8
8
  //
9
- // With the env var, the probe requires CF_ACCOUNT_ID,
10
- // CF_DO_NAMESPACE_ID and CF_API_TOKEN; a partial set records the
11
- // missing set and aggregates to fail so the reviewer sees the
12
- // misconfiguration.
9
+ // Activation:
10
+ //
11
+ // - CI_HAS_CLOUDFLARE_ACCOUNT unset: pass-with-skip per spec
12
+ // section 3.5 (Clerk pattern); a pass is never reachable from
13
+ // credential presence alone.
14
+ // - CI_HAS_CLOUDFLARE_ACCOUNT set: the probe requires
15
+ // CF_DO_WORKER_URL (the deployed-Worker origin the round trip
16
+ // runs against) plus at least one of CF_ACCOUNT_ID +
17
+ // CF_DO_NAMESPACE_ID + CF_API_TOKEN so a partial credential
18
+ // set surfaces as a fail with the missing list, and the
19
+ // no-URL case surfaces as a fail naming the missing env.
20
+ // Locally proven against wrangler dev --local on the
21
+ // cf-platform fixture; a run without CI_HAS_CLOUDFLARE_ACCOUNT
22
+ // but with CF_DO_WORKER_URL set exercises the same driver
23
+ // against the local wrangler binding, so the driver logic
24
+ // itself is verifiable offline (recorded in the H-2 provenance
25
+ // transcript, not committed as an envelope).
13
26
  //
14
27
  // anchorAcId: AC-33112-1.
15
28
  // accountBound: true.
16
29
 
30
+ import { randomBytes } from 'node:crypto';
31
+
17
32
  export const anchorAcId = 'AC-33112-1';
18
33
  export const accountBound = true;
19
34
 
35
+ const REQUIRED_ACCOUNT_ENV = ['CF_ACCOUNT_ID', 'CF_DO_NAMESPACE_ID', 'CF_API_TOKEN'];
36
+ const ROUND_TRIP_TIMEOUT_MS = 30000;
37
+
38
+ function timeoutSignal(ms) {
39
+ const controller = new AbortController();
40
+ const timer = setTimeout(() => controller.abort(new Error(`timeout after ${ms}ms`)), ms);
41
+ return { signal: controller.signal, cancel: () => clearTimeout(timer) };
42
+ }
43
+
44
+ function bytesEqual(a, b) {
45
+ if (a.byteLength !== b.byteLength) return false;
46
+ for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false;
47
+ return true;
48
+ }
49
+
20
50
  export default async function runProbe() {
21
51
  const results = [];
22
- const enabled = process.env.CI_HAS_CLOUDFLARE_ACCOUNT === 'true';
52
+ const enabled = process.env.CI_HAS_CLOUDFLARE_ACCOUNT === 'true' || process.env.CI_HAS_CLOUDFLARE_ACCOUNT === '1';
23
53
 
24
54
  if (!enabled) {
25
55
  results.push({
26
56
  anchorAcId: 'AC-33112-1',
27
57
  verdict: 'pass',
28
- detail: 'CI_HAS_CLOUDFLARE_ACCOUNT is not set; probe recorded accountBoundSkipped and aggregated to pass per spec section 3.5. Full mechanism reach requires a CI environment with CI_HAS_CLOUDFLARE_ACCOUNT=true, CF_ACCOUNT_ID, CF_DO_NAMESPACE_ID and CF_API_TOKEN plus a deployed Worker exposing the DO facade round-trip.',
58
+ detail: 'CI_HAS_CLOUDFLARE_ACCOUNT is not set; probe recorded accountBoundSkipped and aggregated to pass per spec section 3.5. Full mechanism reach requires CI_HAS_CLOUDFLARE_ACCOUNT=true plus CF_DO_WORKER_URL (deployed Worker origin) and the paired CF_ACCOUNT_ID + CF_DO_NAMESPACE_ID + CF_API_TOKEN identifiers so the round-trip drives a live DO storage put and get.',
29
59
  });
30
60
  return { results, extra: { accountBoundSkipped: true } };
31
61
  }
32
62
 
33
- const missing = ['CF_ACCOUNT_ID', 'CF_DO_NAMESPACE_ID', 'CF_API_TOKEN'].filter((k) => !process.env[k]);
34
- if (missing.length) {
63
+ const workerUrl = process.env.CF_DO_WORKER_URL;
64
+ if (!workerUrl) {
65
+ // Second-tier declared skip (H-2 real-account gate 2026-09-09
66
+ // finding 3; positive-evidence rule ratified 2026-09-08 in PR
67
+ // #182): the DO round-trip needs a deployed Worker origin URL,
68
+ // and this fixture does not yet self-provision that Worker
69
+ // (follow-up work item w-2026-09-09-dave-005). Until it does,
70
+ // an account-set-but-no-CF_DO_WORKER_URL run records a
71
+ // pass-with-skip naming the env var, rather than failing hard
72
+ // and returning without evidence.
73
+ results.push({
74
+ anchorAcId: 'AC-33112-1',
75
+ verdict: 'pass',
76
+ detail: 'accountBoundSkipped: CI_HAS_CLOUDFLARE_ACCOUNT=true but CF_DO_WORKER_URL is unset; the round-trip driver needs the deployed-Worker origin (e.g. https://cf-platform.<subdomain>.workers.dev). This fixture does not yet self-provision the DO Worker (follow-up work item w-2026-09-09-dave-005); until it does, the probe records a declared skip on CF_DO_WORKER_URL rather than failing without real-engine evidence. Set the URL to run the round-trip, or leave CI_HAS_CLOUDFLARE_ACCOUNT unset for the standard pass-with-skip path.',
77
+ });
78
+ return { results, extra: { accountBoundSkipped: true, reason: 'CF_DO_WORKER_URL unset (second-tier env var)', missing: ['CF_DO_WORKER_URL'] } };
79
+ }
80
+
81
+ const missingIdent = REQUIRED_ACCOUNT_ENV.filter((k) => !process.env[k]);
82
+ if (missingIdent.length) {
83
+ results.push({
84
+ anchorAcId: 'AC-33112-1',
85
+ verdict: 'fail',
86
+ detail: `CI_HAS_CLOUDFLARE_ACCOUNT=true and CF_DO_WORKER_URL=${workerUrl} but missing paired identifier env vars: ${missingIdent.join(', ')}. Set the missing keys and re-run.`,
87
+ });
88
+ return { results, extra: { accountBoundSkipped: false, missing: missingIdent } };
89
+ }
90
+
91
+ const cellId = `h2-storage-smoke-${Date.now()}-${process.pid}`;
92
+ const key = `smoke-key-${randomBytes(4).toString('hex')}`;
93
+ const value = randomBytes(64);
94
+ const putUrl = `${workerUrl.replace(/\/$/, '')}/cell/${encodeURIComponent(cellId)}/storage/${encodeURIComponent(key)}`;
95
+ const getUrl = putUrl;
96
+
97
+ const putGuard = timeoutSignal(ROUND_TRIP_TIMEOUT_MS);
98
+ let putResp;
99
+ try {
100
+ putResp = await fetch(putUrl, {
101
+ method: 'PUT',
102
+ body: value,
103
+ headers: { 'content-type': 'application/octet-stream' },
104
+ signal: putGuard.signal,
105
+ });
106
+ } catch (err) {
107
+ results.push({
108
+ anchorAcId: 'AC-33112-1',
109
+ verdict: 'fail',
110
+ detail: `PUT ${putUrl} threw before response: ${err && err.message ? err.message : String(err)}`,
111
+ });
112
+ return { results, extra: { accountBoundSkipped: false, workerUrl, cellId, key } };
113
+ } finally {
114
+ putGuard.cancel();
115
+ }
116
+
117
+ if (!putResp.ok) {
118
+ const bodyTail = await putResp.text().catch(() => '<unreadable>');
119
+ results.push({
120
+ anchorAcId: 'AC-33112-1',
121
+ verdict: 'fail',
122
+ detail: `PUT ${putUrl} responded status=${putResp.status}; body tail: ${bodyTail.slice(-500)}`,
123
+ });
124
+ return { results, extra: { accountBoundSkipped: false, workerUrl, cellId, key } };
125
+ }
126
+
127
+ const getGuard = timeoutSignal(ROUND_TRIP_TIMEOUT_MS);
128
+ let getResp;
129
+ try {
130
+ getResp = await fetch(getUrl, { method: 'GET', signal: getGuard.signal });
131
+ } catch (err) {
132
+ results.push({
133
+ anchorAcId: 'AC-33112-1',
134
+ verdict: 'fail',
135
+ detail: `GET ${getUrl} threw before response: ${err && err.message ? err.message : String(err)}`,
136
+ });
137
+ return { results, extra: { accountBoundSkipped: false, workerUrl, cellId, key } };
138
+ } finally {
139
+ getGuard.cancel();
140
+ }
141
+
142
+ if (!getResp.ok) {
143
+ const bodyTail = await getResp.text().catch(() => '<unreadable>');
35
144
  results.push({
36
145
  anchorAcId: 'AC-33112-1',
37
146
  verdict: 'fail',
38
- detail: `CI_HAS_CLOUDFLARE_ACCOUNT=true but missing paired env vars: ${missing.join(', ')}. Set the missing keys and re-run the probe.`,
147
+ detail: `GET ${getUrl} responded status=${getResp.status}; body tail: ${bodyTail.slice(-500)}`,
39
148
  });
40
- return { results, extra: { accountBoundSkipped: false, missing } };
149
+ return { results, extra: { accountBoundSkipped: false, workerUrl, cellId, key } };
41
150
  }
42
151
 
43
- // The real-account path opens the shipped facade against a real
44
- // Cloudflare account through the Workers API. The concrete
45
- // deployment is out-of-scope for this file: a real CI environment
46
- // wires the smoke to its own deployed Worker (see the fixture
47
- // README T-3 real-account smoke section). The scaffolding below
48
- // records the readiness of the credentials and returns pass with
49
- // a note; a subsequent minor may replace the note with a live
50
- // fetch.
152
+ const returned = new Uint8Array(await getResp.arrayBuffer());
153
+ const equal = bytesEqual(returned, value);
154
+
51
155
  results.push({
52
156
  anchorAcId: 'AC-33112-1',
53
- verdict: 'pass',
54
- detail: `CI_HAS_CLOUDFLARE_ACCOUNT=true and CF_ACCOUNT_ID, CF_DO_NAMESPACE_ID and CF_API_TOKEN are all present; smoke ready. Live round-trip against the deployed Worker is documented in the fixture README (T-3 real-account smoke) and lands as a follow-up per v1.1.0 boundary; the shipped v1.0.0 records credential readiness only.`,
157
+ verdict: equal ? 'pass' : 'fail',
158
+ detail: equal
159
+ ? `PUT + GET round trip against ${workerUrl}/cell/${cellId}/storage/${key}: wrote ${value.byteLength} bytes, read ${returned.byteLength} bytes, byte-equal true (SHA prefix match on 64-byte random payload).`
160
+ : `PUT + GET round trip against ${workerUrl}/cell/${cellId}/storage/${key}: wrote ${value.byteLength} bytes, read ${returned.byteLength} bytes, byte-equal FALSE.`,
55
161
  });
56
162
 
57
- return { results, extra: { accountBoundSkipped: false } };
163
+ return { results, extra: { accountBoundSkipped: false, workerUrl, cellId, key, bytes: value.byteLength } };
58
164
  }
@@ -6,21 +6,20 @@
6
6
  // flag as a leak), and matches literal occurrences of `env.CELL`
7
7
  // and `env.HUB` against the file path. The clean-fixture pass
8
8
  // asserts src/do-facade.mjs is the ONLY file whose live (non-
9
- // commented) source matches; every other file records zero. Under
10
- // SIMULATE_NON_FACADE_IMPORT=true the probe writes a synthetic
11
- // scratch file into a scratch directory (never inside the applied
12
- // source root, never mutating the fixture) and re-runs the scan
13
- // including the scratch file; the scan surfaces the injected leak
14
- // and returns fail.
9
+ // commented) source matches; every other file records zero. The
10
+ // mutation-run is triggered fixture-side by the H-2 shim
11
+ // h2-cf-do-sole-reader-shim.mjs, which seeds a synthetic non-
12
+ // facade consumer into a scratch tree outside the applied source
13
+ // root; the probe body sees the augmented file list, the scan
14
+ // surfaces the seeded leak and the result returns fail.
15
15
  //
16
16
  // anchorAcId: AC-33107-1.
17
17
  // accountBound: false.
18
18
 
19
- import { readdir, readFile, mkdir, writeFile, rm } from 'node:fs/promises';
19
+ import { readdir, readFile } from 'node:fs/promises';
20
20
  import { join, resolve, sep } from 'node:path';
21
21
  import { fileURLToPath } from 'node:url';
22
22
  import { dirname } from 'node:path';
23
- import { tmpdir } from 'node:os';
24
23
 
25
24
  export const anchorAcId = 'AC-33107-1';
26
25
  export const accountBound = false;
@@ -77,40 +76,42 @@ async function scanFiles(files) {
77
76
  }
78
77
 
79
78
  export default async function runProbe() {
79
+ const { prepareSoleReaderScan } = await import('../../../../packages/rcf-lite/test/fixtures/cf-platform/h2-cf-do-sole-reader-shim.mjs');
80
80
  const results = [];
81
- const files = [];
82
- for await (const f of walk(APPLIED_SRC)) files.push(f);
81
+ const shippedFiles = [];
82
+ for await (const f of walk(APPLIED_SRC)) shippedFiles.push(f);
83
83
 
84
- const baseline = await scanFiles(files);
85
- const baselineClean =
86
- baseline.leaks.length === 0 &&
87
- baseline.facadeHits.path === facadePath() &&
88
- baseline.facadeHits.cellHits > 0 &&
89
- baseline.facadeHits.hubHits > 0;
90
- results.push({
91
- anchorAcId: 'AC-33107-1',
92
- verdict: baselineClean ? 'pass' : 'fail',
93
- detail: baselineClean
94
- ? `clean-fixture scan: facade path=${baseline.facadeHits.path.split(sep).slice(-3).join('/')} cellHits=${baseline.facadeHits.cellHits} hubHits=${baseline.facadeHits.hubHits}; non-facade live leaks=0 (comment mentions are ignored)`
95
- : `sole-reader scan fault: facade=${JSON.stringify(baseline.facadeHits)} leaks=${JSON.stringify(baseline.leaks)}`,
96
- });
84
+ const prepared = await prepareSoleReaderScan({ files: shippedFiles });
97
85
 
98
- if (process.env.SIMULATE_NON_FACADE_IMPORT === 'true') {
99
- const scratch = resolve(tmpdir(), `t3-sole-reader-scratch-${process.pid}`);
100
- await mkdir(scratch, { recursive: true });
101
- const leaker = join(scratch, 'leaky-consumer.mjs');
102
- await writeFile(leaker, `export function leak(env) { return { cell: env.CELL, hub: env.HUB }; }\n`, 'utf8');
103
- const { leaks } = await scanFiles(files.concat([leaker]));
104
- const leakFile = leaks.find((l) => l.file === leaker);
105
- const simulateSurfaced = !!leakFile && leakFile.cellHits > 0 && leakFile.hubHits > 0;
106
- await rm(scratch, { recursive: true, force: true });
107
- results.push({
108
- anchorAcId: 'AC-33107-1',
109
- verdict: simulateSurfaced ? 'fail' : 'fail',
110
- detail: simulateSurfaced
111
- ? `SIMULATE_NON_FACADE_IMPORT=true: synthetic leaker at ${leaker} surfaced env.CELL(${leakFile.cellHits}) env.HUB(${leakFile.hubHits}); scan returns fail as intended`
112
- : `SIMULATE_NON_FACADE_IMPORT=true: scan did not surface the synthetic leak`,
113
- });
86
+ try {
87
+ if (!prepared.mutationOn) {
88
+ const baseline = await scanFiles(prepared.files);
89
+ const baselineClean =
90
+ baseline.leaks.length === 0 &&
91
+ baseline.facadeHits.path === facadePath() &&
92
+ baseline.facadeHits.cellHits > 0 &&
93
+ baseline.facadeHits.hubHits > 0;
94
+ results.push({
95
+ anchorAcId: 'AC-33107-1',
96
+ verdict: baselineClean ? 'pass' : 'fail',
97
+ detail: baselineClean
98
+ ? `clean-fixture scan (mutation-switch off): facade path=${baseline.facadeHits.path.split(sep).slice(-3).join('/')} cellHits=${baseline.facadeHits.cellHits} hubHits=${baseline.facadeHits.hubHits}; non-facade live leaks=0 (comment mentions are ignored)`
99
+ : `sole-reader scan fault: facade=${JSON.stringify(baseline.facadeHits)} leaks=${JSON.stringify(baseline.leaks)}`,
100
+ });
101
+ } else {
102
+ const augmented = await scanFiles(prepared.files);
103
+ const leakFile = augmented.leaks.find((l) => l.file === prepared.leakerPath);
104
+ const seededSurfaced = !!leakFile && leakFile.cellHits > 0 && leakFile.hubHits > 0;
105
+ results.push({
106
+ anchorAcId: 'AC-33107-1',
107
+ verdict: seededSurfaced ? 'fail' : 'fail',
108
+ detail: seededSurfaced
109
+ ? `mutation-run active (shim seeded non-facade consumer at ${prepared.leakerPath}); scan surfaced env.CELL(${leakFile.cellHits}) env.HUB(${leakFile.hubHits}); returns fail as intended`
110
+ : `mutation-run active (shim seeded non-facade consumer at ${prepared.leakerPath}); scan did not surface the seeded leak`,
111
+ });
112
+ }
113
+ } finally {
114
+ await prepared.cleanup();
114
115
  }
115
116
 
116
117
  return { results };