hyperscale-trust 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hyperscale Consulting
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,305 @@
1
+ Metadata-Version: 2.4
2
+ Name: hyperscale-trust
3
+ Version: 0.1.0
4
+ Summary: Trust center for Hyperscale Django apps: public policy claims, a questionnaire CSV and a private evidence pack
5
+ Keywords: django,trust-center,security,compliance,evidence
6
+ Author: Andy Caine
7
+ Author-email: Andy Caine <andy@hyperscale.consulting>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Framework :: Django
12
+ Classifier: Framework :: Django :: 5.2
13
+ Classifier: Framework :: Django :: 6.1
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Security
18
+ Classifier: Typing :: Typed
19
+ Requires-Dist: django>=5.2
20
+ Requires-Dist: hyperscale-crypto>=0.1.0 ; extra == 'crypto'
21
+ Requires-Python: >=3.14
22
+ Project-URL: Homepage, https://github.com/hyperscale-consulting/hyperscale-trust
23
+ Project-URL: Repository, https://github.com/hyperscale-consulting/hyperscale-trust
24
+ Project-URL: Changelog, https://github.com/hyperscale-consulting/hyperscale-trust/blob/main/CHANGELOG.md
25
+ Project-URL: Issues, https://github.com/hyperscale-consulting/hyperscale-trust/issues
26
+ Provides-Extra: crypto
27
+ Description-Content-Type: text/markdown
28
+
29
+ # hyperscale-trust
30
+
31
+ Trust center for Hyperscale Django apps: a public `/trust` page of
32
+ policy-level security claims generated from the app's data classification and
33
+ the platform's published controls, a questionnaire CSV export, and a private
34
+ evidence pack for auditors and buyers under NDA. Imported as
35
+ `hyperscale.trust`.
36
+
37
+ Every claim on the page comes from the host app's data classification, this
38
+ package's control catalogue, or the platform's published document — not from
39
+ prose someone typed into a form — so the page cannot go stale because
40
+ someone forgot to edit it. The only hand-written fields carried through
41
+ `trust.json` are the operator name and the security contact.
42
+
43
+ ## Install
44
+
45
+ ```bash
46
+ uv add hyperscale-trust
47
+ ```
48
+
49
+ ```python
50
+ INSTALLED_APPS = [
51
+ # ...
52
+ "hyperscale.trust",
53
+ ]
54
+
55
+ urlpatterns = [
56
+ # ...
57
+ path("trust/", include("hyperscale.trust.urls")),
58
+ ]
59
+ ```
60
+
61
+ `hyperscale/trust/urls.py` sets `app_name = "hyperscale_trust"`, so including
62
+ the module resolves URLs as `hyperscale_trust:page` etc. without an explicit
63
+ `namespace=` (passing one is harmless).
64
+
65
+ The evidence pack's key inventory (`keyset_status.json`,
66
+ `key_rotation_events.csv`) comes from `hyperscale.crypto`. Install it with the
67
+ `crypto` extra:
68
+
69
+ ```bash
70
+ uv add "hyperscale-trust[crypto]"
71
+ ```
72
+
73
+ Without it — or before its tables are migrated — the pack still builds; those
74
+ two files just report that the key inventory is unavailable rather than being
75
+ omitted (see [The evidence pack](#the-evidence-pack)).
76
+
77
+ ## Settings
78
+
79
+ ```python
80
+ from pathlib import Path
81
+
82
+ BASE_DIR = Path(__file__).resolve().parent.parent
83
+
84
+ HYPERSCALE_TRUST = {
85
+ "trust_json_path": BASE_DIR / "trust.json", # required
86
+ "snapshot_path": BASE_DIR / "platform-controls.json", # required
87
+ }
88
+ ```
89
+
90
+ | Key | Default | Meaning |
91
+ |---|---|---|
92
+ | `trust_json_path` | none (required) | path to the app's `trust.json` |
93
+ | `snapshot_path` | none (required) | path to the build-time platform-document snapshot |
94
+ | `document_url` | `""` | URL to fetch the live platform document from; empty means never fetch — always serve the snapshot |
95
+ | `cache_seconds` | `86400` | how long a successful platform-document fetch is cached |
96
+ | `evidence_permission` | `"hyperscale.trust.permissions.superuser_only"` | dotted path to a `Callable[[HttpRequest], bool]` deciding who may open the evidence pack |
97
+
98
+ An unknown key, a missing `trust_json_path`/`snapshot_path`, a non-positive
99
+ `cache_seconds`, or an `evidence_permission` path that doesn't import to a
100
+ callable all raise `ImproperlyConfigured`. This is checked in `AppConfig.ready()`,
101
+ so a bad setting fails at Django startup, not on the first request.
102
+
103
+ The default `evidence_permission`,
104
+ `hyperscale.trust.permissions.superuser_only`, is `True` only for an
105
+ authenticated Django superuser. Supply your own dotted path to gate the
106
+ evidence pack on an app-specific owner or admin role instead.
107
+
108
+ ## URLs
109
+
110
+ | Name | Path | Access |
111
+ |---|---|---|
112
+ | `hyperscale_trust:page` | `/trust/` | public |
113
+ | `hyperscale_trust:questionnaire` | `/trust/questionnaire.csv` | public |
114
+ | `hyperscale_trust:evidence` | `/trust/evidence/` | login + `evidence_permission` |
115
+ | `hyperscale_trust:evidence_zip` | `/trust/evidence.zip` | login + `evidence_permission` |
116
+
117
+ (Paths shown are the conventional mount point above; the host app chooses
118
+ where to `include()` the URLconf.)
119
+
120
+ When `trust.json`'s `app.enabled` is `false` (or `trust.json` itself is
121
+ missing or malformed), the public routes 404, and the evidence routes 404 once
122
+ the caller is logged in (anonymous callers are redirected to `LOGIN_URL`
123
+ first) — a disabled trust center gives no sign of what it would otherwise
124
+ contain. The evidence routes are checked in this order: an unauthenticated
125
+ request is redirected to `LOGIN_URL` first; an authenticated request against
126
+ a disabled trust center 404s; only then is `evidence_permission` consulted,
127
+ raising `PermissionDenied` (403) if it returns `False`. This ordering means a
128
+ disabled center never reveals whether the caller would have been authorised.
129
+
130
+ ## What the page shows, and what it deliberately does not
131
+
132
+ `GET /trust/` renders, in order: an overview (app name and description,
133
+ operator and security contact — each labelled "Declared by the operator",
134
+ since they're the only founder-supplied inputs — region, and the date the
135
+ page's source was generated); the data categories processed and how each is
136
+ handled, plus a negative claim for every category the app doesn't process;
137
+ the controls that protect that data (app-specific and platform, each with an
138
+ ISO 27001:2022 Annex A reference, labelled "Enforced by the platform"); the
139
+ cryptography policy in prose (algorithms, key custody, rotation, transport);
140
+ the subprocessor list; and the operator's own responsibilities (such as a
141
+ DPIA) that the data categories imply.
142
+
143
+ It deliberately carries no category or control ids, no row or attribute
144
+ counts, no key ids or rotation dates, no deployment identifiers, and no
145
+ canary results — only two ISO dates ever appear: `trust.json`'s
146
+ `generated_at` (as "last updated on …") and, whenever the page is rendered
147
+ from the bundled snapshot rather than a live fetch (the default when
148
+ `document_url` is unset, and also the fallback after a failed fetch), that
149
+ snapshot's `published_at`, shown as a "Platform information as of …" badge so
150
+ a stale or never-fetched document is visible rather than served as current
151
+ without comment. `GET /trust/questionnaire.csv`
152
+ carries the same restriction — it is the same catalogue, exported as
153
+ `question,answer,annex_a,source` rows, one per control and per data-category
154
+ question plus rows for region, operator, contact and subprocessors.
155
+
156
+ ## Templates
157
+
158
+ `trust.html` and `evidence.html` both `{% extends "base_anonymous.html" %}`
159
+ and use only the `title` and `content` blocks — the host app must supply
160
+ `base_anonymous.html`. Markup uses DaisyUI class names (`card`, `badge`,
161
+ `table`, `btn`) but renders sensibly without DaisyUI or Tailwind loaded.
162
+
163
+ ## `trust.json`
164
+
165
+ Produced by the host app (App Studio's `build_bundle`, for a generated app)
166
+ and shipped next to the app. Full shape and the rules behind it are in the
167
+ design spec:
168
+ [`docs/superpowers/specs/2026-09-24-trust-center-design.md`](docs/superpowers/specs/2026-09-24-trust-center-design.md).
169
+ A worked example is in
170
+ [`tests/fixtures/trust.full.json`](tests/fixtures/trust.full.json). Excerpt:
171
+
172
+ ```json
173
+ {
174
+ "generated_at": "2026-09-24T12:00:00Z",
175
+ "app": {"name": "Clinic Notes", "operator_name": "Acme Health Ltd", "enabled": true},
176
+ "data": {
177
+ "categories": [
178
+ {"id": "health", "label": "Health", "definition": "…", "level": "restricted", "handling": "…", "attribute_count": 2, "models": ["Patient"]}
179
+ ],
180
+ "not_processed": [{"id": "payment_card", "label": "Payment card"}],
181
+ "free_text_attribute_count": 3
182
+ },
183
+ "controls": [{"id": "app.encryption.restricted", "annex_a": "A.8.24"}],
184
+ "levels": {"restricted": "Encrypted by the application before storage, …"}
185
+ }
186
+ ```
187
+
188
+ Two things worth calling out because they're easy to get wrong:
189
+
190
+ - Each category may carry its own `definition` and `handling` prose, and the
191
+ top-level `levels` map gives each sensitivity level a plain description —
192
+ this is the host app's classification vocabulary, copied in so the package
193
+ owns no category wording. `not_processed` is a list of `{id, label}`
194
+ objects, not a list of ids.
195
+ - The package's catalogue (`hyperscale.trust.catalogue.APP_CONTROLS`) is
196
+ authoritative for a control's Annex A reference, title, statement,
197
+ question and answer, looked up by `id`. The `annex_a` value on a control
198
+ entry in `trust.json` itself is informational only and is not rendered.
199
+
200
+ ## The platform document and the snapshot
201
+
202
+ `hyperscale.trust.documents.PlatformDocument` — `version`, `published_at`,
203
+ `region`, `hosting_provider`, `email_provider` and a list of controls (`id`,
204
+ `title`, `statement`, `annex_a`) — either fetched live from
205
+ `HYPERSCALE_TRUST["document_url"]` or loaded from the on-disk
206
+ `snapshot_path`. See
207
+ [`tests/fixtures/platform-controls.json`](tests/fixtures/platform-controls.json)
208
+ for a complete example.
209
+
210
+ - **`document_url` unset (the default):** the snapshot at `snapshot_path` is
211
+ loaded fresh on every request; nothing is fetched over the network.
212
+ - **`document_url` set:** a successful fetch (5-second timeout) is cached
213
+ under a fixed key for `cache_seconds` (default 24 hours) before it is
214
+ fetched again.
215
+ - **A failed fetch** — network error, non-200 status, invalid JSON, or a
216
+ document missing a required field — falls back to the on-disk snapshot.
217
+ That fallback is itself cached, but only for `min(300, cache_seconds)`
218
+ seconds: long enough that a dead `document_url` doesn't pay the fetch
219
+ timeout on every request, short enough to retry soon after the upstream
220
+ recovers.
221
+ - A document served from the snapshot (whether because `document_url` is
222
+ unset, or as a fallback) is marked stale. The public page then shows a
223
+ "Platform information as of `<published_at>`" badge, and the evidence
224
+ pack's `platform-controls.json` carries `"stale_snapshot": true` and an
225
+ explanatory `note`.
226
+
227
+ ## The evidence pack
228
+
229
+ `GET /trust/evidence/` (an HTML summary) and `GET /trust/evidence.zip` (the
230
+ download) are gated on login plus `evidence_permission` — the app's owner,
231
+ admin, or whoever the host app's callable decides; by default, a Django
232
+ superuser. The zip contains:
233
+
234
+ | File | Contents |
235
+ |---|---|
236
+ | `README.md` | index of the other files and which Annex A control each evidences |
237
+ | `cryptography-policy.md` | the cryptography policy rendered as a document, with the operator name and date |
238
+ | `keyset_status.json` | `hyperscale.crypto`'s live key inventory (statuses, last rotation, canary result), or `{"unavailable": "<reason>"}` if `hyperscale.crypto` isn't installed or its tables aren't migrated |
239
+ | `key_rotation_events.csv` | every recorded `KeyRotationEvent`, or a single row explaining why none could be read |
240
+ | `platform-controls.json` | the whole platform document as fetched or loaded, plus `stale_snapshot` and, when stale, a `note` |
241
+ | `classification.json` | Data categories processed, with their sensitivity levels and the level definitions |
242
+ | `deployment.json` | `GIT_SHA`, `DEPLOYED_AT`, and installed versions of `hyperscale-trust`, `hyperscale-crypto` and `django` |
243
+
244
+ The spec also lists a per-attribute classification export; that is not yet
245
+ part of the `trust.json` contract, so `classification.json` carries category
246
+ aggregates until App Studio exports it.
247
+
248
+ `keyset_status.json` and `key_rotation_events.csv` never disappear from the
249
+ pack just because `hyperscale.crypto` is absent or not yet migrated — the
250
+ failure is recorded in the file instead, so an auditor sees why evidence is
251
+ missing rather than a shorter zip.
252
+
253
+ ## Environment
254
+
255
+ `GIT_SHA` and `DEPLOYED_AT` are read from the process environment
256
+ (`os.environ`) when the evidence pack is built and written into
257
+ `deployment.json`; they default to an empty string when unset. Neither ever
258
+ appears on the public page — only inside the evidence pack.
259
+
260
+ ## Wording policy
261
+
262
+ Control titles, statements, questions, answers (in
263
+ `hyperscale.trust.catalogue`) and the cryptography-policy paragraphs are
264
+ vendor-neutral: a reviewer reads "FIPS 140-3 validated key management
265
+ service", never a product name. `tests/test_catalogue.py` asserts none of
266
+ that prose contains a name from `catalogue.VENDOR_DENY_LIST` (currently AWS,
267
+ Amazon, KMS, Secrets Manager, RDS, CloudFront, S3, Stripe, Anthropic). The
268
+ one place a vendor is named is the subprocessor list, resolved from the
269
+ platform document's `hosting_provider`/`email_provider` or from names the
270
+ host app declares in `trust.json` — subprocessor lists must name who they
271
+ are.
272
+
273
+ ## Development
274
+
275
+ Requires [uv](https://docs.astral.sh/uv/) and Python 3.14+.
276
+
277
+ ```bash
278
+ uv sync # create the venv and install dependencies
279
+ uv run pytest # run the tests
280
+ uv run pre-commit run --all-files # lint, format and lockfile checks
281
+ ```
282
+
283
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full set of checks.
284
+
285
+ ## Releasing
286
+
287
+ Bump `version` in `pyproject.toml` and `__version__` in
288
+ `src/hyperscale/trust/__init__.py`, update `CHANGELOG.md`, then publish a
289
+ GitHub release tagged `v<version>`. The `publish` workflow checks the tag
290
+ matches the package version, builds, runs `twine check` and publishes to PyPI
291
+ with trusted publishing.
292
+
293
+ Before the first release, register the project on PyPI with this repository
294
+ and `publish.yml` as a trusted publisher, and create a `pypi` environment in
295
+ the repository settings.
296
+
297
+ ## Design
298
+
299
+ Design spec: [`docs/superpowers/specs/2026-09-24-trust-center-design.md`](docs/superpowers/specs/2026-09-24-trust-center-design.md)
300
+
301
+ Implementation plan: [`docs/superpowers/plans/2026-09-25-hyperscale-trust.md`](docs/superpowers/plans/2026-09-25-hyperscale-trust.md)
302
+
303
+ ## License
304
+
305
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,277 @@
1
+ # hyperscale-trust
2
+
3
+ Trust center for Hyperscale Django apps: a public `/trust` page of
4
+ policy-level security claims generated from the app's data classification and
5
+ the platform's published controls, a questionnaire CSV export, and a private
6
+ evidence pack for auditors and buyers under NDA. Imported as
7
+ `hyperscale.trust`.
8
+
9
+ Every claim on the page comes from the host app's data classification, this
10
+ package's control catalogue, or the platform's published document — not from
11
+ prose someone typed into a form — so the page cannot go stale because
12
+ someone forgot to edit it. The only hand-written fields carried through
13
+ `trust.json` are the operator name and the security contact.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ uv add hyperscale-trust
19
+ ```
20
+
21
+ ```python
22
+ INSTALLED_APPS = [
23
+ # ...
24
+ "hyperscale.trust",
25
+ ]
26
+
27
+ urlpatterns = [
28
+ # ...
29
+ path("trust/", include("hyperscale.trust.urls")),
30
+ ]
31
+ ```
32
+
33
+ `hyperscale/trust/urls.py` sets `app_name = "hyperscale_trust"`, so including
34
+ the module resolves URLs as `hyperscale_trust:page` etc. without an explicit
35
+ `namespace=` (passing one is harmless).
36
+
37
+ The evidence pack's key inventory (`keyset_status.json`,
38
+ `key_rotation_events.csv`) comes from `hyperscale.crypto`. Install it with the
39
+ `crypto` extra:
40
+
41
+ ```bash
42
+ uv add "hyperscale-trust[crypto]"
43
+ ```
44
+
45
+ Without it — or before its tables are migrated — the pack still builds; those
46
+ two files just report that the key inventory is unavailable rather than being
47
+ omitted (see [The evidence pack](#the-evidence-pack)).
48
+
49
+ ## Settings
50
+
51
+ ```python
52
+ from pathlib import Path
53
+
54
+ BASE_DIR = Path(__file__).resolve().parent.parent
55
+
56
+ HYPERSCALE_TRUST = {
57
+ "trust_json_path": BASE_DIR / "trust.json", # required
58
+ "snapshot_path": BASE_DIR / "platform-controls.json", # required
59
+ }
60
+ ```
61
+
62
+ | Key | Default | Meaning |
63
+ |---|---|---|
64
+ | `trust_json_path` | none (required) | path to the app's `trust.json` |
65
+ | `snapshot_path` | none (required) | path to the build-time platform-document snapshot |
66
+ | `document_url` | `""` | URL to fetch the live platform document from; empty means never fetch — always serve the snapshot |
67
+ | `cache_seconds` | `86400` | how long a successful platform-document fetch is cached |
68
+ | `evidence_permission` | `"hyperscale.trust.permissions.superuser_only"` | dotted path to a `Callable[[HttpRequest], bool]` deciding who may open the evidence pack |
69
+
70
+ An unknown key, a missing `trust_json_path`/`snapshot_path`, a non-positive
71
+ `cache_seconds`, or an `evidence_permission` path that doesn't import to a
72
+ callable all raise `ImproperlyConfigured`. This is checked in `AppConfig.ready()`,
73
+ so a bad setting fails at Django startup, not on the first request.
74
+
75
+ The default `evidence_permission`,
76
+ `hyperscale.trust.permissions.superuser_only`, is `True` only for an
77
+ authenticated Django superuser. Supply your own dotted path to gate the
78
+ evidence pack on an app-specific owner or admin role instead.
79
+
80
+ ## URLs
81
+
82
+ | Name | Path | Access |
83
+ |---|---|---|
84
+ | `hyperscale_trust:page` | `/trust/` | public |
85
+ | `hyperscale_trust:questionnaire` | `/trust/questionnaire.csv` | public |
86
+ | `hyperscale_trust:evidence` | `/trust/evidence/` | login + `evidence_permission` |
87
+ | `hyperscale_trust:evidence_zip` | `/trust/evidence.zip` | login + `evidence_permission` |
88
+
89
+ (Paths shown are the conventional mount point above; the host app chooses
90
+ where to `include()` the URLconf.)
91
+
92
+ When `trust.json`'s `app.enabled` is `false` (or `trust.json` itself is
93
+ missing or malformed), the public routes 404, and the evidence routes 404 once
94
+ the caller is logged in (anonymous callers are redirected to `LOGIN_URL`
95
+ first) — a disabled trust center gives no sign of what it would otherwise
96
+ contain. The evidence routes are checked in this order: an unauthenticated
97
+ request is redirected to `LOGIN_URL` first; an authenticated request against
98
+ a disabled trust center 404s; only then is `evidence_permission` consulted,
99
+ raising `PermissionDenied` (403) if it returns `False`. This ordering means a
100
+ disabled center never reveals whether the caller would have been authorised.
101
+
102
+ ## What the page shows, and what it deliberately does not
103
+
104
+ `GET /trust/` renders, in order: an overview (app name and description,
105
+ operator and security contact — each labelled "Declared by the operator",
106
+ since they're the only founder-supplied inputs — region, and the date the
107
+ page's source was generated); the data categories processed and how each is
108
+ handled, plus a negative claim for every category the app doesn't process;
109
+ the controls that protect that data (app-specific and platform, each with an
110
+ ISO 27001:2022 Annex A reference, labelled "Enforced by the platform"); the
111
+ cryptography policy in prose (algorithms, key custody, rotation, transport);
112
+ the subprocessor list; and the operator's own responsibilities (such as a
113
+ DPIA) that the data categories imply.
114
+
115
+ It deliberately carries no category or control ids, no row or attribute
116
+ counts, no key ids or rotation dates, no deployment identifiers, and no
117
+ canary results — only two ISO dates ever appear: `trust.json`'s
118
+ `generated_at` (as "last updated on …") and, whenever the page is rendered
119
+ from the bundled snapshot rather than a live fetch (the default when
120
+ `document_url` is unset, and also the fallback after a failed fetch), that
121
+ snapshot's `published_at`, shown as a "Platform information as of …" badge so
122
+ a stale or never-fetched document is visible rather than served as current
123
+ without comment. `GET /trust/questionnaire.csv`
124
+ carries the same restriction — it is the same catalogue, exported as
125
+ `question,answer,annex_a,source` rows, one per control and per data-category
126
+ question plus rows for region, operator, contact and subprocessors.
127
+
128
+ ## Templates
129
+
130
+ `trust.html` and `evidence.html` both `{% extends "base_anonymous.html" %}`
131
+ and use only the `title` and `content` blocks — the host app must supply
132
+ `base_anonymous.html`. Markup uses DaisyUI class names (`card`, `badge`,
133
+ `table`, `btn`) but renders sensibly without DaisyUI or Tailwind loaded.
134
+
135
+ ## `trust.json`
136
+
137
+ Produced by the host app (App Studio's `build_bundle`, for a generated app)
138
+ and shipped next to the app. Full shape and the rules behind it are in the
139
+ design spec:
140
+ [`docs/superpowers/specs/2026-09-24-trust-center-design.md`](docs/superpowers/specs/2026-09-24-trust-center-design.md).
141
+ A worked example is in
142
+ [`tests/fixtures/trust.full.json`](tests/fixtures/trust.full.json). Excerpt:
143
+
144
+ ```json
145
+ {
146
+ "generated_at": "2026-09-24T12:00:00Z",
147
+ "app": {"name": "Clinic Notes", "operator_name": "Acme Health Ltd", "enabled": true},
148
+ "data": {
149
+ "categories": [
150
+ {"id": "health", "label": "Health", "definition": "…", "level": "restricted", "handling": "…", "attribute_count": 2, "models": ["Patient"]}
151
+ ],
152
+ "not_processed": [{"id": "payment_card", "label": "Payment card"}],
153
+ "free_text_attribute_count": 3
154
+ },
155
+ "controls": [{"id": "app.encryption.restricted", "annex_a": "A.8.24"}],
156
+ "levels": {"restricted": "Encrypted by the application before storage, …"}
157
+ }
158
+ ```
159
+
160
+ Two things worth calling out because they're easy to get wrong:
161
+
162
+ - Each category may carry its own `definition` and `handling` prose, and the
163
+ top-level `levels` map gives each sensitivity level a plain description —
164
+ this is the host app's classification vocabulary, copied in so the package
165
+ owns no category wording. `not_processed` is a list of `{id, label}`
166
+ objects, not a list of ids.
167
+ - The package's catalogue (`hyperscale.trust.catalogue.APP_CONTROLS`) is
168
+ authoritative for a control's Annex A reference, title, statement,
169
+ question and answer, looked up by `id`. The `annex_a` value on a control
170
+ entry in `trust.json` itself is informational only and is not rendered.
171
+
172
+ ## The platform document and the snapshot
173
+
174
+ `hyperscale.trust.documents.PlatformDocument` — `version`, `published_at`,
175
+ `region`, `hosting_provider`, `email_provider` and a list of controls (`id`,
176
+ `title`, `statement`, `annex_a`) — either fetched live from
177
+ `HYPERSCALE_TRUST["document_url"]` or loaded from the on-disk
178
+ `snapshot_path`. See
179
+ [`tests/fixtures/platform-controls.json`](tests/fixtures/platform-controls.json)
180
+ for a complete example.
181
+
182
+ - **`document_url` unset (the default):** the snapshot at `snapshot_path` is
183
+ loaded fresh on every request; nothing is fetched over the network.
184
+ - **`document_url` set:** a successful fetch (5-second timeout) is cached
185
+ under a fixed key for `cache_seconds` (default 24 hours) before it is
186
+ fetched again.
187
+ - **A failed fetch** — network error, non-200 status, invalid JSON, or a
188
+ document missing a required field — falls back to the on-disk snapshot.
189
+ That fallback is itself cached, but only for `min(300, cache_seconds)`
190
+ seconds: long enough that a dead `document_url` doesn't pay the fetch
191
+ timeout on every request, short enough to retry soon after the upstream
192
+ recovers.
193
+ - A document served from the snapshot (whether because `document_url` is
194
+ unset, or as a fallback) is marked stale. The public page then shows a
195
+ "Platform information as of `<published_at>`" badge, and the evidence
196
+ pack's `platform-controls.json` carries `"stale_snapshot": true` and an
197
+ explanatory `note`.
198
+
199
+ ## The evidence pack
200
+
201
+ `GET /trust/evidence/` (an HTML summary) and `GET /trust/evidence.zip` (the
202
+ download) are gated on login plus `evidence_permission` — the app's owner,
203
+ admin, or whoever the host app's callable decides; by default, a Django
204
+ superuser. The zip contains:
205
+
206
+ | File | Contents |
207
+ |---|---|
208
+ | `README.md` | index of the other files and which Annex A control each evidences |
209
+ | `cryptography-policy.md` | the cryptography policy rendered as a document, with the operator name and date |
210
+ | `keyset_status.json` | `hyperscale.crypto`'s live key inventory (statuses, last rotation, canary result), or `{"unavailable": "<reason>"}` if `hyperscale.crypto` isn't installed or its tables aren't migrated |
211
+ | `key_rotation_events.csv` | every recorded `KeyRotationEvent`, or a single row explaining why none could be read |
212
+ | `platform-controls.json` | the whole platform document as fetched or loaded, plus `stale_snapshot` and, when stale, a `note` |
213
+ | `classification.json` | Data categories processed, with their sensitivity levels and the level definitions |
214
+ | `deployment.json` | `GIT_SHA`, `DEPLOYED_AT`, and installed versions of `hyperscale-trust`, `hyperscale-crypto` and `django` |
215
+
216
+ The spec also lists a per-attribute classification export; that is not yet
217
+ part of the `trust.json` contract, so `classification.json` carries category
218
+ aggregates until App Studio exports it.
219
+
220
+ `keyset_status.json` and `key_rotation_events.csv` never disappear from the
221
+ pack just because `hyperscale.crypto` is absent or not yet migrated — the
222
+ failure is recorded in the file instead, so an auditor sees why evidence is
223
+ missing rather than a shorter zip.
224
+
225
+ ## Environment
226
+
227
+ `GIT_SHA` and `DEPLOYED_AT` are read from the process environment
228
+ (`os.environ`) when the evidence pack is built and written into
229
+ `deployment.json`; they default to an empty string when unset. Neither ever
230
+ appears on the public page — only inside the evidence pack.
231
+
232
+ ## Wording policy
233
+
234
+ Control titles, statements, questions, answers (in
235
+ `hyperscale.trust.catalogue`) and the cryptography-policy paragraphs are
236
+ vendor-neutral: a reviewer reads "FIPS 140-3 validated key management
237
+ service", never a product name. `tests/test_catalogue.py` asserts none of
238
+ that prose contains a name from `catalogue.VENDOR_DENY_LIST` (currently AWS,
239
+ Amazon, KMS, Secrets Manager, RDS, CloudFront, S3, Stripe, Anthropic). The
240
+ one place a vendor is named is the subprocessor list, resolved from the
241
+ platform document's `hosting_provider`/`email_provider` or from names the
242
+ host app declares in `trust.json` — subprocessor lists must name who they
243
+ are.
244
+
245
+ ## Development
246
+
247
+ Requires [uv](https://docs.astral.sh/uv/) and Python 3.14+.
248
+
249
+ ```bash
250
+ uv sync # create the venv and install dependencies
251
+ uv run pytest # run the tests
252
+ uv run pre-commit run --all-files # lint, format and lockfile checks
253
+ ```
254
+
255
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full set of checks.
256
+
257
+ ## Releasing
258
+
259
+ Bump `version` in `pyproject.toml` and `__version__` in
260
+ `src/hyperscale/trust/__init__.py`, update `CHANGELOG.md`, then publish a
261
+ GitHub release tagged `v<version>`. The `publish` workflow checks the tag
262
+ matches the package version, builds, runs `twine check` and publishes to PyPI
263
+ with trusted publishing.
264
+
265
+ Before the first release, register the project on PyPI with this repository
266
+ and `publish.yml` as a trusted publisher, and create a `pypi` environment in
267
+ the repository settings.
268
+
269
+ ## Design
270
+
271
+ Design spec: [`docs/superpowers/specs/2026-09-24-trust-center-design.md`](docs/superpowers/specs/2026-09-24-trust-center-design.md)
272
+
273
+ Implementation plan: [`docs/superpowers/plans/2026-09-25-hyperscale-trust.md`](docs/superpowers/plans/2026-09-25-hyperscale-trust.md)
274
+
275
+ ## License
276
+
277
+ MIT. See [LICENSE](LICENSE).