terp-cli 0.35.0__tar.gz → 0.36.1__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.
- {terp_cli-0.35.0 → terp_cli-0.36.1}/PKG-INFO +9 -9
- {terp_cli-0.35.0 → terp_cli-0.36.1}/pyproject.toml +9 -9
- {terp_cli-0.35.0 → terp_cli-0.36.1}/signer/Dockerfile +2 -2
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/__init__.py +96 -15
- terp_cli-0.36.1/src/terp/cli/api_contract.py +385 -0
- terp_cli-0.36.1/src/terp/cli/api_contract_bounds.py +70 -0
- terp_cli-0.36.1/src/terp/cli/api_contract_check.py +155 -0
- terp_cli-0.36.1/src/terp/cli/api_contract_schemas.py +458 -0
- terp_cli-0.36.1/src/terp/cli/api_contract_shapes.py +294 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/openapi.py +58 -8
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/release/_common.py +9 -4
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/release/cli.py +7 -2
- terp_cli-0.36.1/src/terp/cli/release/gate.py +293 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/release/manifest.py +20 -2
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/release/sign.py +12 -8
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/reseal.py +132 -37
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/verify.py +116 -22
- terp_cli-0.35.0/src/terp/cli/release/gate.py +0 -146
- {terp_cli-0.35.0 → terp_cli-0.36.1}/.gitignore +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/signer/terp-arch-releases.json +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/_appref.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/_engine.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/_output.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/_subjects.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/access.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/apidocs.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/authz_surface.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/capabilities.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/deploy_safety.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/deployment_adopt.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/deployment_app.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/deployment_check.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/deployment_declare.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/deployment_render.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/dev.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/docker.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/docker_masks.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/envfile.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/envrequired.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/envschema.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/envseams.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/fmt.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/grants.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/heartbeat.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/jobs.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/leases.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/locks.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/module_roles.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/outbox.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/ports.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/profiles.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/py.typed +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/release/__init__.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/release/_compare.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/release/_dockerfile.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/release/_git.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/release/_image.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/release/_locks.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/release/_plan.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/release/_registry.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/release/_render.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/release/_sandbox.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/release/build.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/release/dsse.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/release/promote.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/routes.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/scaffold.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/schema.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/seed.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/service_accounts.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/smoke.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/users.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/version.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/workbench.py +0 -0
- {terp_cli-0.35.0 → terp_cli-0.36.1}/src/terp/cli/workbench_adopt.py +0 -0
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: terp-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.36.1
|
|
4
4
|
Summary: Terp command-line tool — inspect, scaffolding, migrations, checks, api-docs.
|
|
5
5
|
Project-URL: Repository, https://github.com/AITT-NL/terp-framework
|
|
6
6
|
Project-URL: Changelog, https://github.com/AITT-NL/terp-framework/blob/main/CHANGELOG.md
|
|
7
7
|
License-Expression: Apache-2.0
|
|
8
8
|
Requires-Python: >=3.13
|
|
9
9
|
Requires-Dist: pyyaml>=6.0
|
|
10
|
-
Requires-Dist: terp-arch==0.
|
|
11
|
-
Requires-Dist: terp-core==0.
|
|
12
|
-
Requires-Dist: terp-migrations==0.
|
|
10
|
+
Requires-Dist: terp-arch==0.36.1
|
|
11
|
+
Requires-Dist: terp-core==0.36.1
|
|
12
|
+
Requires-Dist: terp-migrations==0.36.1
|
|
13
13
|
Provides-Extra: jobs
|
|
14
|
-
Requires-Dist: terp-cap-outbox==0.
|
|
15
|
-
Requires-Dist: terp-cap-scheduler-apscheduler==0.
|
|
14
|
+
Requires-Dist: terp-cap-outbox==0.36.1; extra == 'jobs'
|
|
15
|
+
Requires-Dist: terp-cap-scheduler-apscheduler==0.36.1; extra == 'jobs'
|
|
16
16
|
Provides-Extra: release
|
|
17
17
|
Requires-Dist: cryptography>=46; extra == 'release'
|
|
18
18
|
Requires-Dist: packaging>=24; extra == 'release'
|
|
19
|
-
Requires-Dist: terp-cap-egress==0.
|
|
19
|
+
Requires-Dist: terp-cap-egress==0.36.1; extra == 'release'
|
|
20
20
|
Provides-Extra: scheduler
|
|
21
|
-
Requires-Dist: terp-cap-scheduler-apscheduler==0.
|
|
21
|
+
Requires-Dist: terp-cap-scheduler-apscheduler==0.36.1; extra == 'scheduler'
|
|
22
22
|
Provides-Extra: worker
|
|
23
|
-
Requires-Dist: terp-cap-outbox==0.
|
|
23
|
+
Requires-Dist: terp-cap-outbox==0.36.1; extra == 'worker'
|
|
@@ -4,18 +4,18 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "terp-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.36.1"
|
|
8
8
|
description = "Terp command-line tool — inspect, scaffolding, migrations, checks, api-docs."
|
|
9
9
|
requires-python = ">=3.13"
|
|
10
10
|
license = "Apache-2.0"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"terp-core==0.
|
|
12
|
+
"terp-core==0.36.1",
|
|
13
13
|
# `terp migrate` delegates to terp-migrations (lazily imported), keeping Alembic
|
|
14
14
|
# off the path for `terp inspect` / `terp guide` while making migrations work.
|
|
15
|
-
"terp-migrations==0.
|
|
15
|
+
"terp-migrations==0.36.1",
|
|
16
16
|
# `terp guide rules` projects the live rule registry; terp-arch is imported lazily
|
|
17
17
|
# (only when that topic is rendered), so it stays off the common `terp guide` path.
|
|
18
|
-
"terp-arch==0.
|
|
18
|
+
"terp-arch==0.36.1",
|
|
19
19
|
# `terp verify --only env-seams` and `terp smoke` read the compose profiles as data
|
|
20
20
|
# (anchors and `<<:` merge keys resolved), so both answer without a Docker daemon or
|
|
21
21
|
# even the `docker` binary. Declared explicitly rather than leant on: every app
|
|
@@ -29,11 +29,11 @@ dependencies = [
|
|
|
29
29
|
# or a scheduler engine into apps that only use inspect/check/migrate. Deployments can select
|
|
30
30
|
# one process role, while `jobs` is the convenient complete jobs-process bundle.
|
|
31
31
|
[project.optional-dependencies]
|
|
32
|
-
worker = ["terp-cap-outbox==0.
|
|
33
|
-
scheduler = ["terp-cap-scheduler-apscheduler==0.
|
|
32
|
+
worker = ["terp-cap-outbox==0.36.1"]
|
|
33
|
+
scheduler = ["terp-cap-scheduler-apscheduler==0.36.1"]
|
|
34
34
|
jobs = [
|
|
35
|
-
"terp-cap-outbox==0.
|
|
36
|
-
"terp-cap-scheduler-apscheduler==0.
|
|
35
|
+
"terp-cap-outbox==0.36.1",
|
|
36
|
+
"terp-cap-scheduler-apscheduler==0.36.1",
|
|
37
37
|
]
|
|
38
38
|
# `terp release` and `terp promote` (ADR 0187): the DSSE signature (cryptography), the lock's
|
|
39
39
|
# markers (packaging), and the one declared way to a registry, an index and the vulnerability
|
|
@@ -42,7 +42,7 @@ jobs = [
|
|
|
42
42
|
release = [
|
|
43
43
|
"cryptography>=46",
|
|
44
44
|
"packaging>=24",
|
|
45
|
-
"terp-cap-egress==0.
|
|
45
|
+
"terp-cap-egress==0.36.1",
|
|
46
46
|
]
|
|
47
47
|
|
|
48
48
|
[project.scripts]
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
# (tests/release_lane/).
|
|
27
27
|
|
|
28
28
|
# python:3.14-slim, the base-image table's interpreter (terp-arch's base-images.json).
|
|
29
|
-
FROM python@sha256:
|
|
29
|
+
FROM python@sha256:930e1abba7c4e8f96ed93e8012c35d174d7732752731bbd5e8e757d7ec6f3966 AS install
|
|
30
30
|
COPY wheels/ /wheels/
|
|
31
31
|
COPY requirements.txt /requirements.txt
|
|
32
32
|
# Every distribution by its hash: the third-party ones as uv.lock pins them, the framework's as
|
|
@@ -41,7 +41,7 @@ FROM zricethezav/gitleaks@sha256:c00b6bd0aeb3071cbcb79009cb16a60dd9e0a7c60e2be9a
|
|
|
41
41
|
# The docker client 29.9.2, by digest (docker:29.9.2-cli, read 2026-10-22).
|
|
42
42
|
FROM docker@sha256:5e6baf67325a79f64ed3bed6863954d5e05d0f3d4e57cbdb14809346814bbfdb AS docker
|
|
43
43
|
|
|
44
|
-
FROM python@sha256:
|
|
44
|
+
FROM python@sha256:930e1abba7c4e8f96ed93e8012c35d174d7732752731bbd5e8e757d7ec6f3966
|
|
45
45
|
RUN apt-get update \
|
|
46
46
|
&& apt-get install -y --no-install-recommends git \
|
|
47
47
|
&& rm -rf /var/lib/apt/lists/* \
|
|
@@ -41,7 +41,7 @@ from terp.cli.jobs import (
|
|
|
41
41
|
run_scheduler_command,
|
|
42
42
|
run_worker_command,
|
|
43
43
|
)
|
|
44
|
-
from terp.cli.openapi import _load_app, export_openapi
|
|
44
|
+
from terp.cli.openapi import _load_app, export_openapi, run_openapi_command
|
|
45
45
|
from terp.cli.routes import run_routes_command
|
|
46
46
|
from terp.cli.profiles import DEFAULT_PROFILE, profile_names
|
|
47
47
|
from terp.cli.scaffold import new_module, new_module_message
|
|
@@ -133,6 +133,9 @@ Then the codegen chain, in this order — each step reads what the one before it
|
|
|
133
133
|
|
|
134
134
|
`terp verify` runs the drift halves of that chain and names the command to re-run when
|
|
135
135
|
something is stale, so it is the one to reach for if you are unsure what is out of date.
|
|
136
|
+
An API-only app (rendered with the template's `frontend: false`, ADR 0202) has no
|
|
137
|
+
frontend: its chain is `terp migrate make`, `terp openapi` and `terp check`, and
|
|
138
|
+
`terp new module` writes no frontend slot where there is none.
|
|
136
139
|
Policy.default() = authenticated; read VIEWER, write EDITOR.
|
|
137
140
|
|
|
138
141
|
FORMATTING: `terp fmt`, NOT `ruff format .`
|
|
@@ -339,11 +342,15 @@ Both are comma-separated. The procedure, in this order:
|
|
|
339
342
|
1. Put the old key in BOTH lists and the new one in SECRET_KEY. Deploy.
|
|
340
343
|
2. Run `terp secrets reseal` where the app runs (its own container, its own
|
|
341
344
|
environment). It rewrites every value sealed under a fallback under SECRET_KEY,
|
|
345
|
+
one nested in a JSON column included (a queued mail in the outbox, ADR 0199),
|
|
342
346
|
one transaction per table, refuses while a migration is pending, is safe to run
|
|
343
347
|
twice, and exits 0 only when nothing is left. `--check` counts and writes nothing.
|
|
344
348
|
3. Drop SECRET_KEY_DECRYPT_FALLBACKS once reseal reports nothing left. Keep the old
|
|
345
349
|
key itself while a backup sealed under it may still be restored: restoring one
|
|
346
|
-
means listing it again and running reseal.
|
|
350
|
+
means listing it again and running reseal. A self-service link already mailed is
|
|
351
|
+
sealed under the key it was sent under, where reseal cannot reach it, so dropping
|
|
352
|
+
the fallback sooner than its lifetime (a day for a sign-up link by default) only
|
|
353
|
+
makes such a link stop working, and its owner asks for a new one.
|
|
347
354
|
4. Drop SECRET_KEY_FALLBACKS once the longest signature lifetime has passed since
|
|
348
355
|
step 1: REFRESH_TOKEN_TTL_SECONDS if the app issues refresh sessions, 15 minutes
|
|
349
356
|
if not. Dropping it sooner loses nothing; it signs out sessions still on the old key.
|
|
@@ -1100,9 +1107,27 @@ The security declaration (SecurityConfig, refused at production boot)
|
|
|
1100
1107
|
/openapi.json are hidden there because a production API's full schema is not public
|
|
1101
1108
|
information. Development always serves them, and `terp openapi` exports the document
|
|
1102
1109
|
either way - so turning this on is a choice to publish, not a way to get the file.
|
|
1110
|
+
Commit that file instead: `openapi.json` is the app's API contract, the one a mobile
|
|
1111
|
+
app or a site in another repository pins, and `terp verify --only api-contract` holds
|
|
1112
|
+
it to the API the app serves, naming every change to its shape, the ones that break a
|
|
1113
|
+
client first (ADR 0194). Who may call a route is not in it: authz-surface pins that.
|
|
1103
1114
|
- headers ships a safe SecurityHeaders set; max_request_bytes (1 MiB) is the body cap and
|
|
1104
1115
|
is per-mount overridable (ADR 0067); request_id_header names the correlation header
|
|
1105
1116
|
("X-Request-ID") echoed on every response.
|
|
1117
|
+
- A NATIVE MOBILE CLIENT stays signed in through its own routes, opt-in with a reason
|
|
1118
|
+
(ADR 0195): build_login_module(..., native_clients=NativeClients(reason="...")) mounts
|
|
1119
|
+
POST /api/v1/auth/native/login, /native/refresh and /native/logout. The first two answer
|
|
1120
|
+
the refresh token in the JSON body and run the same rotation, reuse detection and
|
|
1121
|
+
revocation as the cookie, its rotation grace window included; /native/logout takes the
|
|
1122
|
+
token and revokes its whole family without minting a successor. The client keeps that
|
|
1123
|
+
token in the OS keychain or keystore and nowhere else, and treats a 401 from
|
|
1124
|
+
/native/refresh as signed out. All three refuse any request with browser provenance
|
|
1125
|
+
(Origin, any Sec-Fetch-* header, the refresh cookie) with 403 native_client_only before
|
|
1126
|
+
the body is validated, so a browser stays on /login, /refresh and /logout. A HYBRID app
|
|
1127
|
+
(a page inside a native shell) calls them from its native layer and hands its page only
|
|
1128
|
+
access tokens: a page that holds the refresh token has given up what the cookie
|
|
1129
|
+
protects. No route that mints a credential (/login, /token, /refresh, /native/login,
|
|
1130
|
+
/native/refresh) accepts an Idempotency-Key; retry without one.
|
|
1106
1131
|
- No terp.arch check applies - there is no module code shape to police. Enforcement is
|
|
1107
1132
|
the create_app production fail-fast plus the production-readiness verify lane.
|
|
1108
1133
|
""",
|
|
@@ -1213,6 +1238,21 @@ Password strength (PasswordPolicy, Tier-B)
|
|
|
1213
1238
|
The reason is the opt-out: it is stored as `relaxed_reason` and named back to you in the
|
|
1214
1239
|
BootError, so "why is this off" always has an answer that travels with the declaration.
|
|
1215
1240
|
PasswordPolicy.relaxed(reason="legacy bulk import")
|
|
1241
|
+
- SELF-SERVICE accounts (ADR 0196) hold sign-up and reset to the same policy. They are
|
|
1242
|
+
opt-in and never discovered: after configure_mail(...), build the module and mount it
|
|
1243
|
+
build_accounts_module(SelfService(reason="members open their own accounts",
|
|
1244
|
+
users=users_service, throttle_store=shared_store,
|
|
1245
|
+
link_base="https://app.example.com"))
|
|
1246
|
+
for POST /api/v1/accounts/sign-up, /verify, /password-reset, /password-reset/confirm
|
|
1247
|
+
and /me/close. Sign-up takes only the address: the account opens when the mailed token
|
|
1248
|
+
is redeemed at /verify with the password, so whoever asked cannot choose it. Every
|
|
1249
|
+
request about an address answers 202 the same way and sends that address exactly one
|
|
1250
|
+
message, whether or not it has an account, and addresses compare case-insensitively.
|
|
1251
|
+
Tokens are sealed, expire, work once, and ride the URL fragment, so your account pages
|
|
1252
|
+
read them from location.hash and post them. Closing needs the password again and ends
|
|
1253
|
+
every session (AccountDeletion.SCRUB by default, or REMOVE: building the module refuses
|
|
1254
|
+
it while a reference to the user table would refuse the delete, so declare CASCADE or
|
|
1255
|
+
SET NULL on that reference; one the build cannot see is scrubbed instead, with a warning).
|
|
1216
1256
|
- No terp.arch check applies (no module code shape to police) — enforcement is the
|
|
1217
1257
|
service chokepoint plus the create_app production fail-fast.
|
|
1218
1258
|
""",
|
|
@@ -1716,11 +1756,35 @@ File objects (files capability, ADR 0056/0057)
|
|
|
1716
1756
|
objects) in a NAMED-PROFILE registry;
|
|
1717
1757
|
metadata (name, type, size, sha256, storage_key, storage_profile) lives in the platform
|
|
1718
1758
|
DB. storage_key/storage_profile never leave the boundary (FileRead omits both).
|
|
1719
|
-
- Any provider is an adapter subclass (local ships;
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1759
|
+
- Any provider is an adapter subclass (local ships; a NAS mount is the local adapter on
|
|
1760
|
+
another root). Two object-store adapters ship behind extras (ADR 0201): S3Storage
|
|
1761
|
+
(terp-cap-files[s3]) takes the SDK's own client and a bucket; AzureBlobStorage
|
|
1762
|
+
(terp-cap-files[azure]) takes the SDK's ContainerClient and, for user-delegation SAS
|
|
1763
|
+
links, a BlobServiceClient under an Entra credential. Register each store once at the
|
|
1764
|
+
composition root:
|
|
1765
|
+
register_storage_backend("s3-media", S3Storage(boto3.client("s3"), bucket="app-media"))
|
|
1766
|
+
register_storage_backend("azure-invoices", AzureBlobStorage(invoices_container))
|
|
1767
|
+
register_storage_backend("azure-hr", AzureBlobStorage(hr_container, delegation=service))
|
|
1768
|
+
Keep credentials in settings / sealed config — never in module code. Building the Azure
|
|
1769
|
+
adapter without its extra fails at boot with the install line (StorageSdkMissingError);
|
|
1770
|
+
the S3 adapter takes the SDK's client, so a missing extra fails where that is built.
|
|
1771
|
+
- A client that cannot send a bearer (an <audio>/<video> element, a PDF viewer, a native
|
|
1772
|
+
client's media stack) gets a short-lived LINK to one file's bytes instead (ADR 0201):
|
|
1773
|
+
GET /api/v1/files/{id}/link?ttl_seconds=600&inline=true -> {url, expires_at}
|
|
1774
|
+
The link is minted where a download is authorized (row scope, then quarantine), by the
|
|
1775
|
+
store the ROW names: an S3 presigned GET or an Azure SAS where the store can sign, else
|
|
1776
|
+
an app-signed token the capability serves itself at GET /api/v1/files/link/{token} —
|
|
1777
|
+
its one public route, rate-limited, answering 404 to an expired, altered or foreign
|
|
1778
|
+
token. The default lifetime is an hour, the longest 24 hours; a link is not single-use
|
|
1779
|
+
(a player makes many range requests), its expiry is the control. inline is honoured only
|
|
1780
|
+
for a type a browser renders without running it (image, audio, video, PDF — not an XML
|
|
1781
|
+
image such as SVG, which is a document with script in it). From a module, mint through
|
|
1782
|
+
your own authorized row and a declared FileRef:
|
|
1783
|
+
link = FileService().link_for(session, invoice, "attachment_file_id", ttl=timedelta(minutes=10))
|
|
1784
|
+
Both content routes honour one bytes= Range (206 / 416); the local adapter seeks, an
|
|
1785
|
+
object store's stream is read forward. A link from a store that signs never streams
|
|
1786
|
+
through the API; the authenticated content route still serves any store's bytes to a
|
|
1787
|
+
caller who asks it to.
|
|
1724
1788
|
- Pick the store per module (subclass default) or per call — never from a client:
|
|
1725
1789
|
class InvoiceFileService(FileService):
|
|
1726
1790
|
storage_profile = "azure-invoices"
|
|
@@ -1924,6 +1988,11 @@ Sending e-mail (terp-cap-mail)
|
|
|
1924
1988
|
dead-letters` lists it. With the
|
|
1925
1989
|
in-process default queue (no worker) the mail is sent inline, and a failing relay
|
|
1926
1990
|
fails the request that asked for it.
|
|
1991
|
+
- The queued message is SEALED at rest (ADR 0199): the outbox row's payload holds the
|
|
1992
|
+
Message-ID and one value sealed under SECRET_KEY, never an address or the text. After a
|
|
1993
|
+
key rotation keep SECRET_KEY_DECRYPT_FALLBACKS until `terp secrets reseal` reports
|
|
1994
|
+
nothing left: a queued mail no configured key opens is never sent, and fails with
|
|
1995
|
+
MailSealError, which names that remedy, until it dead-letters.
|
|
1927
1996
|
- Test without a relay:
|
|
1928
1997
|
from terp.capabilities.mail import (
|
|
1929
1998
|
CapturingMailTransport, MailSettings, configure_mail, reset_mail)
|
|
@@ -2757,7 +2826,12 @@ FOUR STATES, EXACTLY ONE AT A TIME
|
|
|
2757
2826
|
"build": "generated" the framework renders Dockerfile.prod,
|
|
2758
2827
|
frontend/Dockerfile.prod and docker-compose.prod.yml
|
|
2759
2828
|
from this file (`terp deployment render`), and a
|
|
2760
|
-
committed copy that is not the render is refused
|
|
2829
|
+
committed copy that is not the render is refused.
|
|
2830
|
+
frontend/Dockerfile.prod only beside a web unit: an api
|
|
2831
|
+
unit with no web unit in front of it is the deployment's
|
|
2832
|
+
only way in and publishes ${API_PORT:-23200}:8000 (ADR
|
|
2833
|
+
0202) -- the shape an app rendered API-only (the
|
|
2834
|
+
template's `frontend: false`) has from the start
|
|
2761
2835
|
{"unmanaged": true, "reason"} no tool can drive this deploy; the checks are off and
|
|
2762
2836
|
a tool shows the reason. Nothing may stand beside it.
|
|
2763
2837
|
|
|
@@ -2854,7 +2928,8 @@ or an override; externalDatabase beside a database unit; a database or store wit
|
|
|
2854
2928
|
or two of a kind; a one-shot that declares `terp migrate upgrade` (drop the command: the
|
|
2855
2929
|
one-shot without one is the migration step) or a second step; a role with no command of
|
|
2856
2930
|
its own and none declared; a web unit with a command, with no single api to proxy to, or
|
|
2857
|
-
without frontend/package-lock.json;
|
|
2931
|
+
without frontend/package-lock.json; two or more api units with no web unit in front of
|
|
2932
|
+
them (which one callers reach would be a guess); an api without both health paths; a start-order cycle
|
|
2858
2933
|
or a unit it names that is not declared; a volume without its mount, mounted anywhere but
|
|
2859
2934
|
var/files, on a unit not on the backend image, or on a unit that already mounts another
|
|
2860
2935
|
volume there (Compose would mount only the last of them, without a word); any system
|
|
@@ -3000,14 +3075,20 @@ build container holds a credential or writes into what is built. Each takes the
|
|
|
3000
3075
|
<registry>/<image>:<tag> with the credential your docker configuration holds for this
|
|
3001
3076
|
release. It writes terp-release-build.json: BuildKit's own digests.
|
|
3002
3077
|
uv run terp release gate --sha <sha> --build terp-release-build.json
|
|
3003
|
-
runs in a clean checkout of the commit: `terp verify --profile release
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3078
|
+
runs in a clean checkout of the commit: `terp verify --profile release` once, with npm's
|
|
3079
|
+
scripts off, recording every check's verdict and the release-assurance claim those same
|
|
3080
|
+
results compose (no check's output), then the schedule catalog, read by running the
|
|
3081
|
+
pushed image sealed (no network, read-only, its own non-root user). It holds no push
|
|
3082
|
+
credential and no key. It writes terp-release-gate.json, and exits 1 unless every check
|
|
3083
|
+
passed and the claim holds, naming each red check. Run it from the commit's own
|
|
3084
|
+
environment: sign reads the document only in the version the commit's pinned terp-cli
|
|
3085
|
+
writes (ADR 0200).
|
|
3007
3086
|
terp release sign --sha <sha> --tag <tag> --registry ... --build ... --gate ... \\
|
|
3008
3087
|
--hosts separated --signer '["your-kms-signer", "--key", "release"]' --public-key k.pub
|
|
3009
3088
|
runs from the signer image, never the commit's own toolchain, and runs nothing of the
|
|
3010
|
-
app: it repeats the refusals,
|
|
3089
|
+
app: it repeats the refusals, refuses a gate with a red check or a claim that does not
|
|
3090
|
+
hold (ADR 0200; a commit pinning terp-cli below 0.36.1 has a gate that records the claim
|
|
3091
|
+
alone, and is held to it), renders a generated app's files with the terp-arch the
|
|
3011
3092
|
commit pins (fetched by hash, run in a process of its own), reads the backend image from
|
|
3012
3093
|
the registry as data and holds it to the tree and the lock, scans for known
|
|
3013
3094
|
vulnerabilities and for secrets in the history, and signs the manifest as a DSSE envelope
|
|
@@ -5182,7 +5263,7 @@ def main(argv: Sequence[str] | None = None) -> None:
|
|
|
5182
5263
|
emit(f"wrote {path}")
|
|
5183
5264
|
return
|
|
5184
5265
|
if args.command == "openapi":
|
|
5185
|
-
emit(
|
|
5266
|
+
emit(run_openapi_command(args.app, out=args.out, app_root=args.app_root))
|
|
5186
5267
|
return
|
|
5187
5268
|
if args.command == "routes":
|
|
5188
5269
|
emit(
|