sanka-cli 0.2.4__tar.gz → 0.2.6__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.
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/PKG-INFO +13 -1
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/README.md +11 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/pyproject.toml +2 -1
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/execution/report_codec.py +10 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/__init__.py +1 -1
- sanka_cli-0.2.6/src/sanka_cli/certificates.py +98 -0
- sanka_cli-0.2.6/src/sanka_cli/commands/cloud.py +550 -0
- sanka_cli-0.2.6/src/sanka_cli/commands/cloud_fleet.py +262 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_cli.py +1 -1
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_execution_report_codec.py +55 -1
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_mcp.py +2 -2
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_sanka_facade.py +1 -1
- sanka_cli-0.2.4/src/sanka_cli/commands/cloud.py +0 -240
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/.gitignore +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/LICENSES/AGPL-3.0-only.txt +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/LICENSES/Apache-2.0.txt +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/NOTICE +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/__init__.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/_client.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/cli/__init__.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/cli/_compact.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/cli/_output.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/cli/_research.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/cli/py.typed +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/connector/__init__.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/connector/py.typed +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/__about__.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/__init__.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/concurrency.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/connector_client.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/connector_host.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/engine.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/execution/__init__.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/execution/continuous.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/execution/errors.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/execution/local.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/execution/model.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/execution/routes.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/execution/scope.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/execution/state.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/execution/validate.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/extensions/__init__.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/extensions/discovery.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/extensions/lifecycle.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/extensions/model.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/extensions/runner.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/extensions/store.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/hashing.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/mapping/__init__.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/mapping/errors.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/mapping/mapping_candidates.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/mapping/model.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/mapping/owner_mapping.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/mapping/pending_relationships.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/mapping/record_mapping.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/planner.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/private_sqlite.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/py.typed +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/registry.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/safe_local_io.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/spec.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka/runtime/state.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/__main__.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/bundle.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/client.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/commands/__init__.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/commands/ai.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/commands/auth.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/commands/cloud_migrate.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/commands/code.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/commands/migrate.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/commands/profiles.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/commands/resources.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/commands/skill.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/commands/workflows.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/config.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/main.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/mcp/__init__.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/mcp/client.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/mcp/render.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/mcp/server.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/output.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/runtime.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/skills/sanka-cli/SKILL.md +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_cli/state.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_connector/__about__.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_connector/__init__.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_connector/credentials.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_connector/errors.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_connector/protocols.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_connector/provisioning.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_connector/py.typed +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_connector/records.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_connector/registration.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/src/sanka_connector/schema.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/conftest.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/fixtures/extension_drf_project/api/__init__.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/fixtures/extension_drf_project/api/serializers.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/fixtures/extension_drf_project/api/views.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/fixtures/extension_drf_project/config/__init__.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/fixtures/extension_drf_project/config/settings.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/fixtures/extension_drf_project/config/urls.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/fixtures/extension_drf_project/manage.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/fixtures/extension_marketplace/drf-to-fastapi.json +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/fixtures/extension_marketplace/marketplace.json +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_cli_developer_experience.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_code_bundle.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_code_commands.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_concurrency.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_connector_client.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_connector_host.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_credential_provider.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_engine_e2e.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_engine_execution_adoption.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_engine_pg_to_clickhouse_e2e.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_engine_postgres_e2e.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_execution_continuous.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_execution_exact_scope.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_execution_local_store.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_execution_routes.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_execution_scope.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_execution_seam.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_execution_validate.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_extension_discovery.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_extension_lifecycle.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_extension_runner.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_extension_store.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_extension_wheel_acceptance.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_hashing.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_hosted_cli.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_mapping_candidates.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_mapping_owner_mapping.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_mapping_pending_relationships.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_mapping_record_mapping.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_planner.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_registry.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_safe_local_io.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_skill_commands.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_spec.py +0 -0
- {sanka_cli-0.2.4 → sanka_cli-0.2.6}/tests/test_state_store.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sanka-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: Sanka command-line interface and migration runtime
|
|
5
5
|
Project-URL: Repository, https://github.com/sankaHQ/sanka
|
|
6
6
|
Project-URL: Issues, https://github.com/sankaHQ/sanka/issues
|
|
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
16
16
|
Classifier: Typing :: Typed
|
|
17
17
|
Requires-Python: >=3.12
|
|
18
18
|
Requires-Dist: click<9,>=8.1
|
|
19
|
+
Requires-Dist: cryptography<52,>=43
|
|
19
20
|
Requires-Dist: httpx<1,>=0.27
|
|
20
21
|
Requires-Dist: keyring<26,>=25
|
|
21
22
|
Requires-Dist: packaging<27,>=24
|
|
@@ -107,6 +108,17 @@ workflows, AI, Custom Code, and explicitly cloud-selected migrations retain
|
|
|
107
108
|
their existing authentication requirements. Add `--json` to local lifecycle
|
|
108
109
|
commands for the stable `sanka-cli/v1` response envelope.
|
|
109
110
|
|
|
111
|
+
## Hosted repository runs
|
|
112
|
+
|
|
113
|
+
`sanka cloud` uploads reviewed repository ZIPs, starts runs with an explicit
|
|
114
|
+
credit cap, and reads status, receipts, and retained artifacts. When enabled by
|
|
115
|
+
the operator, `cloud repair` makes one bounded patch attempt and `cloud certify`
|
|
116
|
+
compares selected HTTP cases in a fresh worker. `cloud certificate-verify`
|
|
117
|
+
verifies signed evidence online or with an explicitly trusted offline key ring.
|
|
118
|
+
Every hosted command pins its workspace; paid operations require confirmation.
|
|
119
|
+
See [Hosted repository runs](../../docs/cloud-runs.md) for inputs, pricing,
|
|
120
|
+
verification scope, and revocation limits.
|
|
121
|
+
|
|
110
122
|
## Extension trust
|
|
111
123
|
|
|
112
124
|
The manager verifies a trusted marketplace snapshot, manifest schema,
|
|
@@ -77,6 +77,17 @@ workflows, AI, Custom Code, and explicitly cloud-selected migrations retain
|
|
|
77
77
|
their existing authentication requirements. Add `--json` to local lifecycle
|
|
78
78
|
commands for the stable `sanka-cli/v1` response envelope.
|
|
79
79
|
|
|
80
|
+
## Hosted repository runs
|
|
81
|
+
|
|
82
|
+
`sanka cloud` uploads reviewed repository ZIPs, starts runs with an explicit
|
|
83
|
+
credit cap, and reads status, receipts, and retained artifacts. When enabled by
|
|
84
|
+
the operator, `cloud repair` makes one bounded patch attempt and `cloud certify`
|
|
85
|
+
compares selected HTTP cases in a fresh worker. `cloud certificate-verify`
|
|
86
|
+
verifies signed evidence online or with an explicitly trusted offline key ring.
|
|
87
|
+
Every hosted command pins its workspace; paid operations require confirmation.
|
|
88
|
+
See [Hosted repository runs](../../docs/cloud-runs.md) for inputs, pricing,
|
|
89
|
+
verification scope, and revocation limits.
|
|
90
|
+
|
|
80
91
|
## Extension trust
|
|
81
92
|
|
|
82
93
|
The manager verifies a trusted marketplace snapshot, manifest schema,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sanka-cli"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.6"
|
|
4
4
|
description = "Sanka command-line interface and migration runtime"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12"
|
|
@@ -16,6 +16,7 @@ classifiers = [
|
|
|
16
16
|
]
|
|
17
17
|
dependencies = [
|
|
18
18
|
"click>=8.1,<9",
|
|
19
|
+
"cryptography>=43,<52",
|
|
19
20
|
"httpx>=0.27,<1",
|
|
20
21
|
"keyring>=25,<26",
|
|
21
22
|
"packaging>=24,<27",
|
|
@@ -207,6 +207,15 @@ def canonical_route_manifest(raw: Any) -> MappingRouteManifest:
|
|
|
207
207
|
code="SANKA_MIGRATE_ROUTE_MANIFEST_INVALID",
|
|
208
208
|
)
|
|
209
209
|
seen.add(route_key)
|
|
210
|
+
identity_fields = item.get("identityFields", [])
|
|
211
|
+
if not isinstance(identity_fields, list) or any(
|
|
212
|
+
not isinstance(value, str) or not value.strip() or value != value.strip()
|
|
213
|
+
for value in identity_fields
|
|
214
|
+
):
|
|
215
|
+
raise ExecutionFault(
|
|
216
|
+
"The saved Sanka route manifest contains invalid identity fields.",
|
|
217
|
+
code="SANKA_MIGRATE_ROUTE_MANIFEST_INVALID",
|
|
218
|
+
)
|
|
210
219
|
normalized.append(
|
|
211
220
|
{
|
|
212
221
|
"routeKey": route_key,
|
|
@@ -215,6 +224,7 @@ def canonical_route_manifest(raw: Any) -> MappingRouteManifest:
|
|
|
215
224
|
"sourceFilter": (
|
|
216
225
|
_source_filter_payload(source_filter) if source_filter is not None else None
|
|
217
226
|
),
|
|
227
|
+
**({"identityFields": sorted(set(identity_fields))} if identity_fields else {}),
|
|
218
228
|
}
|
|
219
229
|
)
|
|
220
230
|
return sorted(normalized, key=lambda row: str(row["routeKey"]))
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Public certificate input and verification helpers; no hosted execution code."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import base64
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
from urllib.parse import unquote, urlsplit
|
|
12
|
+
|
|
13
|
+
from cryptography.exceptions import InvalidSignature
|
|
14
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def canonical_bytes(value: Any) -> bytes:
|
|
18
|
+
return json.dumps(
|
|
19
|
+
value, sort_keys=True, separators=(",", ":"), ensure_ascii=True, allow_nan=False
|
|
20
|
+
).encode("utf-8")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def read_json(path: Path, maximum: int = 192 * 1024) -> Any:
|
|
24
|
+
with path.open("rb") as stream:
|
|
25
|
+
data = stream.read(maximum + 1)
|
|
26
|
+
if len(data) > maximum:
|
|
27
|
+
raise ValueError("JSON file exceeds its size limit")
|
|
28
|
+
return json.loads(data)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_scenarios(path: Path) -> list[dict[str, Any]]:
|
|
32
|
+
cases = read_json(path, 48 * 1024)
|
|
33
|
+
if not isinstance(cases, list) or not 1 <= len(cases) <= 50:
|
|
34
|
+
raise ValueError("Cases must be a JSON array containing 1 to 50 requests")
|
|
35
|
+
seen, normalized = set(), []
|
|
36
|
+
for case in cases:
|
|
37
|
+
if not isinstance(case, dict) or set(case) - {"id", "method", "path", "json_body"}:
|
|
38
|
+
raise ValueError("Each case accepts only id, method, path and json_body")
|
|
39
|
+
identifier, method, value = case.get("id"), case.get("method"), case.get("path")
|
|
40
|
+
if (
|
|
41
|
+
not isinstance(identifier, str)
|
|
42
|
+
or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]{0,63}", identifier)
|
|
43
|
+
or identifier in seen
|
|
44
|
+
or method not in {"GET", "POST", "PUT", "PATCH", "DELETE"}
|
|
45
|
+
or not isinstance(value, str)
|
|
46
|
+
or not 1 <= len(value) <= 1000
|
|
47
|
+
):
|
|
48
|
+
raise ValueError("Each case needs a unique ID, supported method and local HTTP path")
|
|
49
|
+
seen.add(identifier)
|
|
50
|
+
parsed, decoded = urlsplit(value), unquote(value)
|
|
51
|
+
if (
|
|
52
|
+
not value.startswith("/")
|
|
53
|
+
or value.startswith("//")
|
|
54
|
+
or parsed.scheme
|
|
55
|
+
or parsed.netloc
|
|
56
|
+
or parsed.fragment
|
|
57
|
+
or "\\" in decoded
|
|
58
|
+
or any(ord(char) < 32 or ord(char) > 126 for char in decoded)
|
|
59
|
+
or ".." in decoded.split("?", 1)[0].split("/")
|
|
60
|
+
):
|
|
61
|
+
raise ValueError("Case paths must be local ASCII HTTP paths without traversal")
|
|
62
|
+
body = case.get("json_body")
|
|
63
|
+
if len(canonical_bytes({"body": body})) > 8192 or (method == "GET" and body is not None):
|
|
64
|
+
raise ValueError("Case bodies are limited to 8 KiB; GET cannot have a body")
|
|
65
|
+
normalized.append({"id": identifier, "method": method, "path": value, "json_body": body})
|
|
66
|
+
if len(canonical_bytes(normalized)) > 47 * 1024:
|
|
67
|
+
raise ValueError("The normalized certificate scope exceeds its size limit")
|
|
68
|
+
return normalized
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def verify_certificate(document: dict[str, Any], key_document: dict[str, Any]) -> dict[str, Any]:
|
|
72
|
+
"""Verify using keys supplied by the caller's chosen trusted source only."""
|
|
73
|
+
signed = document.get("certificate", document)
|
|
74
|
+
key_document = key_document.get("data", key_document)
|
|
75
|
+
try:
|
|
76
|
+
payload = signed["payload"]
|
|
77
|
+
if (
|
|
78
|
+
signed["algorithm"] != "Ed25519"
|
|
79
|
+
or signed["encoding"] != "sorted-ascii-json-v1"
|
|
80
|
+
or payload["schema_version"] != "developer-certificate-v1"
|
|
81
|
+
or payload["issuer"] != "https://api-v2.sanka.com"
|
|
82
|
+
or not isinstance(payload, dict)
|
|
83
|
+
):
|
|
84
|
+
raise ValueError("Unsupported certificate issuer or signature format")
|
|
85
|
+
matches = [row for row in key_document["keys"] if row["key_id"] == signed["key_id"]]
|
|
86
|
+
if len(matches) != 1 or matches[0].get("algorithm") != "Ed25519":
|
|
87
|
+
raise ValueError("The signing key is not in the selected trusted key ring")
|
|
88
|
+
key = Ed25519PublicKey.from_public_bytes(
|
|
89
|
+
base64.b64decode(matches[0]["public_key_base64"], validate=True)
|
|
90
|
+
)
|
|
91
|
+
key.verify(
|
|
92
|
+
base64.b64decode(signed["signature_base64"], validate=True), canonical_bytes(payload)
|
|
93
|
+
)
|
|
94
|
+
except InvalidSignature as error:
|
|
95
|
+
raise ValueError("Certificate signature is invalid") from error
|
|
96
|
+
except (KeyError, TypeError, AttributeError) as error:
|
|
97
|
+
raise ValueError("Certificate or trusted key ring has an invalid format") from error
|
|
98
|
+
return payload
|