easycontract 0.1.0__tar.gz → 0.1.4__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.
- easycontract-0.1.4/CHANGELOG.md +162 -0
- {easycontract-0.1.0 → easycontract-0.1.4}/PKG-INFO +85 -8
- {easycontract-0.1.0 → easycontract-0.1.4}/README.md +83 -6
- {easycontract-0.1.0 → easycontract-0.1.4}/pyproject.toml +5 -3
- {easycontract-0.1.0 → easycontract-0.1.4}/src/easycontract/__init__.py +1 -1
- {easycontract-0.1.0 → easycontract-0.1.4}/src/easycontract/client.py +7 -0
- easycontract-0.1.4/src/easycontract/resources.py +1096 -0
- {easycontract-0.1.0 → easycontract-0.1.4}/src/easycontract/webhooks.py +26 -8
- {easycontract-0.1.0 → easycontract-0.1.4}/tests/smoke_test.py +19 -0
- easycontract-0.1.0/CHANGELOG.md +0 -19
- easycontract-0.1.0/src/easycontract/resources.py +0 -653
- {easycontract-0.1.0 → easycontract-0.1.4}/.gitignore +0 -0
- {easycontract-0.1.0 → easycontract-0.1.4}/LICENSE.txt +0 -0
- {easycontract-0.1.0 → easycontract-0.1.4}/src/easycontract/errors.py +0 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `easycontract` Python SDK.
|
|
4
|
+
|
|
5
|
+
## 0.1.4 (2026-10-20)
|
|
6
|
+
|
|
7
|
+
**Features**
|
|
8
|
+
|
|
9
|
+
- Projects (paid plans): `client.projects.list/create/get/update`. An
|
|
10
|
+
organization key sees everything; a project key only its project.
|
|
11
|
+
`project=` on `envelopes.create`, `envelopes.list`, `templates.create_envelope`
|
|
12
|
+
and `webhooks.create` (organization keys only). `client.whoami()` now returns
|
|
13
|
+
`project` (null = organization key) and `key`.
|
|
14
|
+
- Webhooks: `client.webhooks.get`, `update` (url, event_types, is_active,
|
|
15
|
+
description) and `rotate_secret(grace_period_hours=24)`, which returns the
|
|
16
|
+
new `secret` once.
|
|
17
|
+
|
|
18
|
+
**Fixes**
|
|
19
|
+
|
|
20
|
+
- `easycontract.webhooks.verify_signature` accepts ANY `v1=` in the header
|
|
21
|
+
and `secret` may be a list: after a rotation, during the grace period,
|
|
22
|
+
deliveries carry `v1=<new>,v1=<old>`. 0.1.3 and earlier only checked the
|
|
23
|
+
last `v1` (the old secret), so they failed once you switched to the new one
|
|
24
|
+
before the grace period ended.
|
|
25
|
+
|
|
26
|
+
## 0.1.3 (2026-10-20)
|
|
27
|
+
|
|
28
|
+
**Features**
|
|
29
|
+
|
|
30
|
+
- Cryptographic restrictions: `client.organization.update(seal_profile=
|
|
31
|
+
"hybrid" | "classic", acknowledge=True)` chooses how the platform seals
|
|
32
|
+
the certificate, the final PDFs and the evidence package (hybrid ECDSA +
|
|
33
|
+
ML-DSA-87, or ECDSA only). `client.organization.get()["cryptography"]`
|
|
34
|
+
shows the choice and what each option implies; every envelope reports the
|
|
35
|
+
profile frozen at send in `seal_profile`.
|
|
36
|
+
- The evidence package (`client.envelopes.download_evidence(...)`) is now an ASiC-E
|
|
37
|
+
container (still a `.zip`) signed by the platform.
|
|
38
|
+
|
|
39
|
+
## 0.1.2 (2026-10-19)
|
|
40
|
+
|
|
41
|
+
**Features**
|
|
42
|
+
|
|
43
|
+
- Machine-drafted template translations: `client.templates.autotranslate(
|
|
44
|
+
template_id, document_id, language, acknowledge=True)` queues a draft made
|
|
45
|
+
by the platform's translators and reviewed by a panel with quorum;
|
|
46
|
+
`client.templates.get_translation_job(job_id)` returns its progress,
|
|
47
|
+
`verdict` (`consensus`, `quorum`, `no_quorum`, `unreviewed`, `simulated`)
|
|
48
|
+
and the reviewers' notes with English back-translations. The result is
|
|
49
|
+
always a draft that a person approves in the panel.
|
|
50
|
+
- The platform now has 16 languages (es, en, ca, gl, eu, ast, an, oc, pt, fr,
|
|
51
|
+
it, de, ru, zh-hans, ar, he) for `language` everywhere.
|
|
52
|
+
|
|
53
|
+
## 0.1.1 (2026-10-07)
|
|
54
|
+
|
|
55
|
+
**Security (audit 2026-10-07)**
|
|
56
|
+
|
|
57
|
+
- `requests>=2.33.0,<3` (was `>=2.31`): older `requests` releases are affected
|
|
58
|
+
by CVE-2024-35195, CVE-2024-47081 and CVE-2026-25645.
|
|
59
|
+
- Resource identifiers are validated as UUIDs before a path is built, and
|
|
60
|
+
free-text segments (catalog keys, language codes) are percent-encoded: an
|
|
61
|
+
id such as `"abc/../../webhooks/123"` used to make `requests` resolve the
|
|
62
|
+
dot segments and call ANOTHER endpoint with the caller's API key. Invalid
|
|
63
|
+
ids now raise `ValueError` before any request is sent.
|
|
64
|
+
|
|
65
|
+
**Features**
|
|
66
|
+
|
|
67
|
+
- Asynchronous malware scan of uploaded documents: `document["scan"]`
|
|
68
|
+
(`status` pending/clean/infected/error/not_scanned, `engine`, `signature`,
|
|
69
|
+
`scanned_at`), event `document.scanned`, and
|
|
70
|
+
`client.envelopes.rescan_document(envelope_id, document_id)` to retry a
|
|
71
|
+
failed scan. Signers only see and sign documents once they are clean.
|
|
72
|
+
- Invitation e-mails are held back until the malware verdict:
|
|
73
|
+
`signer["link_delivery"] == "pending_scan"` while waiting (the `sign_url`
|
|
74
|
+
values stay valid); `envelope["void_reason_code"] == "malware_detected"`
|
|
75
|
+
when the platform stopped an envelope.
|
|
76
|
+
- Identity images retention mode: `envelopes.create(..., kyc_images_retention=
|
|
77
|
+
"after_in_person_check")` (also on `templates.create`/`update` and
|
|
78
|
+
`templates.create_envelope`) keeps a verified signer's document/selfie
|
|
79
|
+
images until the requester records the face-to-face check with
|
|
80
|
+
`client.identity.confirm_in_person(envelope_id, signer_id, note=...)`
|
|
81
|
+
(event `signer.kyc_confirmed_in_person`); the default "after_review" purges
|
|
82
|
+
N days after the reviewer's decision. `identity_verification` carries
|
|
83
|
+
`images_retention` and `confirmed_in_person`.
|
|
84
|
+
- Embedded signing (§10.3): `client.organization.update(embed_allowed_origins=[...])`
|
|
85
|
+
registers the web origins allowed to frame the signer flow, and
|
|
86
|
+
`client.envelopes.create_embedded_session(envelope_id, signer_id, origin=...,
|
|
87
|
+
return_url=..., language=..., ttl_minutes=...)` mints the ephemeral `url`
|
|
88
|
+
(token `em_…`) to open in an `<iframe allow="camera">` (postMessage events
|
|
89
|
+
`ready`/`signed`/`declined`/`expired`) or by redirect (`return_url`). The
|
|
90
|
+
organization exposes `embed_allowed_origins`.
|
|
91
|
+
- Biometrics (Enterprise): `envelopes.create(..., biometrics=True)` and
|
|
92
|
+
`templates.create_envelope(..., biometrics=True)` offer signers the optional
|
|
93
|
+
stroke capture; `signer["biometrics"]` carries its status (hash, points,
|
|
94
|
+
consent version, `erased_at`), never the vector;
|
|
95
|
+
`envelopes.erase_signer_biometrics(envelope_id, signer_id, reason=...)`
|
|
96
|
+
erases it (GDPR art. 17); `download_evidence(..., include_biometrics=True)`
|
|
97
|
+
adds the vectors to the ZIP on explicit request.
|
|
98
|
+
- `envelopes.create(..., language=...)` sets the document language.
|
|
99
|
+
- Identity proofing (Pro+): `auth_methods` accepts `"kyc"` on signers and
|
|
100
|
+
template roles; `signer["identity_verification"]` carries the status
|
|
101
|
+
(provider, method, document type/country, name match, hash), never the
|
|
102
|
+
provider payload; `download_evidence(..., include_identity_proofs=True)` adds
|
|
103
|
+
the full results to the ZIP on explicit request. The organization exposes
|
|
104
|
+
`kyc` (switch state) and `features.kyc`.
|
|
105
|
+
- In-house identity review: `client.identity.pending_reviews()`,
|
|
106
|
+
`client.identity.get(envelope_id, signer_id)`,
|
|
107
|
+
`client.identity.download_document(envelope_id, signer_id, document_id)`
|
|
108
|
+
(every download is recorded in the evidence trail) and
|
|
109
|
+
`client.identity.review(envelope_id, signer_id, decision="approve"|"reject",
|
|
110
|
+
verified_name=..., reason=...)`. `identity_verification` now carries
|
|
111
|
+
`checks`, `documents` (hashes, never the images), `reviewed_by` and
|
|
112
|
+
`images_purge_at`; `download_evidence(..., include_identity_proofs=True)`
|
|
113
|
+
also adds the images still retained.
|
|
114
|
+
- `envelopes.add_signer(..., kyc_document_type=..., kyc_document_number=...)`
|
|
115
|
+
registers what the requester knows about the signer's document; a
|
|
116
|
+
different declaration is refused at capture. Signers expose
|
|
117
|
+
`kyc_expected_document_type` and `kyc_expected_document_number_set`.
|
|
118
|
+
- Identity endpoints (`identity.pending_reviews()`, `identity.get()`) now
|
|
119
|
+
return `declared_document_number`, `verified_name` and `signer_full_name`
|
|
120
|
+
so the tenant can validate on its own platform; the signer summary inside
|
|
121
|
+
envelopes keeps status only. New outbound events `signer.kyc_submitted`,
|
|
122
|
+
`signer.kyc_verified` and `signer.kyc_failed` (webhooks and `GET /events/`).
|
|
123
|
+
- Retention: `envelopes.create(..., document_type=...)` (catalogue in
|
|
124
|
+
`retention.document_types()`, unsuitable types are refused, caution types
|
|
125
|
+
carry `document_type_warning`), `envelopes.set_legal_hold(envelope_id,
|
|
126
|
+
reason=...)` / `release_legal_hold()`, `retention.overview(within_days=90)`
|
|
127
|
+
and `organization.update(retention_policies={...})`. Envelopes expose
|
|
128
|
+
`document_type`, `document_type_warning` and `legal_hold`; new outbound
|
|
129
|
+
event `envelope.blocked` is documented.
|
|
130
|
+
|
|
131
|
+
- `envelopes.download_final_document(envelope_id, document_id, to=None)`:
|
|
132
|
+
downloads the stamped final PDF of one document (PAdES-B-LTA sealed on the
|
|
133
|
+
Enterprise plan). Seal details are exposed under `documents[].final.pades`
|
|
134
|
+
when fetching the envelope with `expand=["documents"]`.
|
|
135
|
+
|
|
136
|
+
## 0.1.0 (2026-09-25) — published: https://pypi.org/project/easycontract/0.1.0/
|
|
137
|
+
|
|
138
|
+
Initial release.
|
|
139
|
+
|
|
140
|
+
- `EasyContract` client: `X-Api-Key` auth, per-POST `Idempotency-Key`,
|
|
141
|
+
bounded retries (429 + GET 5xx) honoring `Retry-After`, typed RFC 7807
|
|
142
|
+
error hierarchy, context-manager support (`close()`).
|
|
143
|
+
- Full resource coverage of the v1 API (envelopes, templates, webhooks,
|
|
144
|
+
events, organization, members, invitations) with cursor auto-paging and
|
|
145
|
+
anti-loop guards.
|
|
146
|
+
- `webhooks.verify_signature`: fail-closed HMAC-SHA256 with timestamp
|
|
147
|
+
replay check.
|
|
148
|
+
|
|
149
|
+
### Behaviour notes
|
|
150
|
+
|
|
151
|
+
- `idempotency_key` accepts a non-empty `str` (sent as-is), `None` or
|
|
152
|
+
`True` (a random UUID is generated per call), or `False` (header
|
|
153
|
+
omitted). Any other value is coerced with `str()` (e.g. a `uuid.UUID`
|
|
154
|
+
is sent as its string form).
|
|
155
|
+
- File uploads: seekable files are re-wound and re-sent in full on
|
|
156
|
+
retries; non-seekable streams (pipes, sockets) are sent exactly once —
|
|
157
|
+
a 429 on such an upload raises `RateLimitError` instead of retrying
|
|
158
|
+
with an empty body.
|
|
159
|
+
- `webhooks.verify_signature` verifies over the raw request bytes; the
|
|
160
|
+
payload is decoded only after the HMAC check passes.
|
|
161
|
+
- `tolerance=None` in `verify_signature` disables the timestamp replay
|
|
162
|
+
check (opt-out; not recommended).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: easycontract
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: SDK oficial de Python para el API de easycontract (firma electrónica eIDAS)
|
|
5
5
|
Project-URL: Documentation, https://easycontract.online/docs/api
|
|
6
6
|
Author-email: Román Ramírez <roman@easycontract.online>
|
|
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
18
18
|
Classifier: Topic :: Security
|
|
19
19
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
20
|
Requires-Python: >=3.10
|
|
21
|
-
Requires-Dist: requests<3,>=2.
|
|
21
|
+
Requires-Dist: requests<3,>=2.33.0
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
|
|
24
24
|
# easycontract — SDK de Python
|
|
@@ -81,8 +81,17 @@ client.templates.add_field(
|
|
|
81
81
|
|
|
82
82
|
## Receta 2 — Firma embebida (embedded signing)
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
El firmante firma dentro de tu aplicación (el caso RootedArbiter). Tres pasos:
|
|
85
|
+
|
|
86
|
+
1. Registra una vez los orígenes de tu web (https; `http://localhost:…` solo
|
|
87
|
+
en desarrollo). La lista se sustituye entera:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
client.organization.update(embed_allowed_origins=["https://arbiter.rootedcon.com"])
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
2. Envía sin emails y, cuando el ponente esté delante de tu página, acuña una
|
|
94
|
+
sesión efímera para ESE firmante (60 minutos por defecto, caduca al firmar):
|
|
86
95
|
|
|
87
96
|
```python
|
|
88
97
|
result = client.templates.create_envelope(
|
|
@@ -91,12 +100,41 @@ result = client.templates.create_envelope(
|
|
|
91
100
|
send=True,
|
|
92
101
|
deliver_emails=False,
|
|
93
102
|
)
|
|
94
|
-
|
|
95
|
-
|
|
103
|
+
signer = result["signers"][0]
|
|
104
|
+
session = client.envelopes.create_embedded_session(
|
|
105
|
+
result["envelope"]["id"],
|
|
106
|
+
signer["id"],
|
|
107
|
+
origin="https://arbiter.rootedcon.com",
|
|
108
|
+
return_url="https://arbiter.rootedcon.com/contratos/42/firmado", # opcional
|
|
109
|
+
language="es",
|
|
110
|
+
)
|
|
111
|
+
embed_url = session["url"] # token em_…: un firmante, un origen, caduca
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
3. Ábrela en un iframe y escucha los eventos, o redirige al ponente y recíbelo
|
|
115
|
+
de vuelta en `return_url` con `?easycontract_event=signed&envelope_id=…&signer_id=…`:
|
|
116
|
+
|
|
117
|
+
```html
|
|
118
|
+
<iframe src="{{ embed_url }}" allow="camera" style="width:100%;height:80vh;border:0"></iframe>
|
|
119
|
+
<script>
|
|
120
|
+
window.addEventListener("message", (e) => {
|
|
121
|
+
if (e.origin !== "https://app.easycontract.online") return; // el origen de easycontract
|
|
122
|
+
if (e.data?.source !== "easycontract") return;
|
|
123
|
+
// e.data.event ∈ ready | signed | declined | expired; e.data.envelope_id, signer_id
|
|
124
|
+
});
|
|
125
|
+
</script>
|
|
96
126
|
```
|
|
97
127
|
|
|
98
|
-
|
|
99
|
-
|
|
128
|
+
La página de firma responde con `Content-Security-Policy: frame-ancestors
|
|
129
|
+
<tu origen>` y no depende de cookies (funciona con las de terceros
|
|
130
|
+
bloqueadas); el OTP y la verificación de identidad, si el firmante los exige,
|
|
131
|
+
se piden dentro del mismo marco (`allow="camera"` para la captura del
|
|
132
|
+
documento). El `postMessage` es solo señal de interfaz: confirma siempre en
|
|
133
|
+
servidor con el webhook `signer.signed` o `client.envelopes.get(...)`. Cada
|
|
134
|
+
sesión y su apertura (con el origen) quedan en el expediente de evidencias.
|
|
135
|
+
|
|
136
|
+
El `sign_url` clásico de `deliver_emails=False` sigue existiendo para el
|
|
137
|
+
redirect simple (14 días, `expires_in_days` para cambiarlo).
|
|
100
138
|
|
|
101
139
|
## Receta 3 — Webhooks vs. polling
|
|
102
140
|
|
|
@@ -174,3 +212,42 @@ members = client.members.list()
|
|
|
174
212
|
invite = client.invitations.create(email="colega@example.com", role="member")
|
|
175
213
|
# invite["accept_url"] — entrégalo tú o deja que easycontract lo envíe por email
|
|
176
214
|
```
|
|
215
|
+
|
|
216
|
+
## Validar la identidad del firmante en tu plataforma (KYC)
|
|
217
|
+
|
|
218
|
+
Con el proceso propio de easycontract, el firmante fotografía su documento y se hace un
|
|
219
|
+
selfie; la decisión es de la organización. Puedes tomarla desde tu propio sistema:
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
from easycontract import EasyContract
|
|
223
|
+
|
|
224
|
+
client = EasyContract(api_key="ec_live_...")
|
|
225
|
+
|
|
226
|
+
# 1. Suscríbete a los eventos (o sondea GET /events/): signer.kyc_submitted avisa de
|
|
227
|
+
# cada caso pendiente; signer.kyc_verified / signer.kyc_failed, de la decisión.
|
|
228
|
+
for case in client.identity.pending_reviews()["data"]:
|
|
229
|
+
envelope_id, signer = case["envelope_id"], case["signer"]
|
|
230
|
+
identity = case["identity_verification"] # estado, comprobaciones, datos declarados
|
|
231
|
+
|
|
232
|
+
# 2. Recupera las imágenes (cada descarga queda en el audit trail del documento)
|
|
233
|
+
for document in identity["documents"]:
|
|
234
|
+
client.identity.download_document(
|
|
235
|
+
envelope_id, signer["id"], document["id"], to=f"/tmp/{document['kind']}.jpg"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# 3. Valida con tus reglas: número declarado, caducidad, cotejo visual o tu propio motor
|
|
239
|
+
ok = identity["declared_document_number"] == mi_registro[signer["email"]].dni
|
|
240
|
+
|
|
241
|
+
# 4. Decide; el firmante recibe el resultado por correo
|
|
242
|
+
client.identity.review(
|
|
243
|
+
envelope_id,
|
|
244
|
+
signer["id"],
|
|
245
|
+
decision="approve" if ok else "reject",
|
|
246
|
+
verified_name=signer["full_name"],
|
|
247
|
+
reason="" if ok else "El número de documento no coincide con nuestro registro",
|
|
248
|
+
)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Las imágenes se purgan a los N días de la decisión (`organization.kyc.image_retention_days`);
|
|
252
|
+
el resultado completo sigue disponible en el paquete de evidencias con
|
|
253
|
+
`download_evidence(..., include_identity_proofs=True)`.
|
|
@@ -58,8 +58,17 @@ client.templates.add_field(
|
|
|
58
58
|
|
|
59
59
|
## Receta 2 — Firma embebida (embedded signing)
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
El firmante firma dentro de tu aplicación (el caso RootedArbiter). Tres pasos:
|
|
62
|
+
|
|
63
|
+
1. Registra una vez los orígenes de tu web (https; `http://localhost:…` solo
|
|
64
|
+
en desarrollo). La lista se sustituye entera:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
client.organization.update(embed_allowed_origins=["https://arbiter.rootedcon.com"])
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
2. Envía sin emails y, cuando el ponente esté delante de tu página, acuña una
|
|
71
|
+
sesión efímera para ESE firmante (60 minutos por defecto, caduca al firmar):
|
|
63
72
|
|
|
64
73
|
```python
|
|
65
74
|
result = client.templates.create_envelope(
|
|
@@ -68,12 +77,41 @@ result = client.templates.create_envelope(
|
|
|
68
77
|
send=True,
|
|
69
78
|
deliver_emails=False,
|
|
70
79
|
)
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
signer = result["signers"][0]
|
|
81
|
+
session = client.envelopes.create_embedded_session(
|
|
82
|
+
result["envelope"]["id"],
|
|
83
|
+
signer["id"],
|
|
84
|
+
origin="https://arbiter.rootedcon.com",
|
|
85
|
+
return_url="https://arbiter.rootedcon.com/contratos/42/firmado", # opcional
|
|
86
|
+
language="es",
|
|
87
|
+
)
|
|
88
|
+
embed_url = session["url"] # token em_…: un firmante, un origen, caduca
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
3. Ábrela en un iframe y escucha los eventos, o redirige al ponente y recíbelo
|
|
92
|
+
de vuelta en `return_url` con `?easycontract_event=signed&envelope_id=…&signer_id=…`:
|
|
93
|
+
|
|
94
|
+
```html
|
|
95
|
+
<iframe src="{{ embed_url }}" allow="camera" style="width:100%;height:80vh;border:0"></iframe>
|
|
96
|
+
<script>
|
|
97
|
+
window.addEventListener("message", (e) => {
|
|
98
|
+
if (e.origin !== "https://app.easycontract.online") return; // el origen de easycontract
|
|
99
|
+
if (e.data?.source !== "easycontract") return;
|
|
100
|
+
// e.data.event ∈ ready | signed | declined | expired; e.data.envelope_id, signer_id
|
|
101
|
+
});
|
|
102
|
+
</script>
|
|
73
103
|
```
|
|
74
104
|
|
|
75
|
-
|
|
76
|
-
|
|
105
|
+
La página de firma responde con `Content-Security-Policy: frame-ancestors
|
|
106
|
+
<tu origen>` y no depende de cookies (funciona con las de terceros
|
|
107
|
+
bloqueadas); el OTP y la verificación de identidad, si el firmante los exige,
|
|
108
|
+
se piden dentro del mismo marco (`allow="camera"` para la captura del
|
|
109
|
+
documento). El `postMessage` es solo señal de interfaz: confirma siempre en
|
|
110
|
+
servidor con el webhook `signer.signed` o `client.envelopes.get(...)`. Cada
|
|
111
|
+
sesión y su apertura (con el origen) quedan en el expediente de evidencias.
|
|
112
|
+
|
|
113
|
+
El `sign_url` clásico de `deliver_emails=False` sigue existiendo para el
|
|
114
|
+
redirect simple (14 días, `expires_in_days` para cambiarlo).
|
|
77
115
|
|
|
78
116
|
## Receta 3 — Webhooks vs. polling
|
|
79
117
|
|
|
@@ -151,3 +189,42 @@ members = client.members.list()
|
|
|
151
189
|
invite = client.invitations.create(email="colega@example.com", role="member")
|
|
152
190
|
# invite["accept_url"] — entrégalo tú o deja que easycontract lo envíe por email
|
|
153
191
|
```
|
|
192
|
+
|
|
193
|
+
## Validar la identidad del firmante en tu plataforma (KYC)
|
|
194
|
+
|
|
195
|
+
Con el proceso propio de easycontract, el firmante fotografía su documento y se hace un
|
|
196
|
+
selfie; la decisión es de la organización. Puedes tomarla desde tu propio sistema:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
from easycontract import EasyContract
|
|
200
|
+
|
|
201
|
+
client = EasyContract(api_key="ec_live_...")
|
|
202
|
+
|
|
203
|
+
# 1. Suscríbete a los eventos (o sondea GET /events/): signer.kyc_submitted avisa de
|
|
204
|
+
# cada caso pendiente; signer.kyc_verified / signer.kyc_failed, de la decisión.
|
|
205
|
+
for case in client.identity.pending_reviews()["data"]:
|
|
206
|
+
envelope_id, signer = case["envelope_id"], case["signer"]
|
|
207
|
+
identity = case["identity_verification"] # estado, comprobaciones, datos declarados
|
|
208
|
+
|
|
209
|
+
# 2. Recupera las imágenes (cada descarga queda en el audit trail del documento)
|
|
210
|
+
for document in identity["documents"]:
|
|
211
|
+
client.identity.download_document(
|
|
212
|
+
envelope_id, signer["id"], document["id"], to=f"/tmp/{document['kind']}.jpg"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# 3. Valida con tus reglas: número declarado, caducidad, cotejo visual o tu propio motor
|
|
216
|
+
ok = identity["declared_document_number"] == mi_registro[signer["email"]].dni
|
|
217
|
+
|
|
218
|
+
# 4. Decide; el firmante recibe el resultado por correo
|
|
219
|
+
client.identity.review(
|
|
220
|
+
envelope_id,
|
|
221
|
+
signer["id"],
|
|
222
|
+
decision="approve" if ok else "reject",
|
|
223
|
+
verified_name=signer["full_name"],
|
|
224
|
+
reason="" if ok else "El número de documento no coincide con nuestro registro",
|
|
225
|
+
)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Las imágenes se purgan a los N días de la decisión (`organization.kyc.image_retention_days`);
|
|
229
|
+
el resultado completo sigue disponible en el paquete de evidencias con
|
|
230
|
+
`download_evidence(..., include_identity_proofs=True)`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "easycontract"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.4"
|
|
4
4
|
description = "SDK oficial de Python para el API de easycontract (firma electrónica eIDAS)"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -20,7 +20,9 @@ classifiers = [
|
|
|
20
20
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
21
|
]
|
|
22
22
|
dependencies = [
|
|
23
|
-
|
|
23
|
+
# >=2.33.0: older releases carry CVE-2024-35195, CVE-2024-47081 and
|
|
24
|
+
# CVE-2026-25645 (audit 2026-10-07, A1)
|
|
25
|
+
"requests>=2.33.0,<3",
|
|
24
26
|
]
|
|
25
27
|
|
|
26
28
|
[project.urls]
|
|
@@ -36,4 +38,4 @@ packages = ["src/easycontract"]
|
|
|
36
38
|
[tool.hatch.build.targets.sdist]
|
|
37
39
|
# Internal material never ships in the sdist. (hatchling still adds the
|
|
38
40
|
# repo-root .gitignore to the sdist; it holds nothing sensitive.)
|
|
39
|
-
exclude = ["SECURITY-AUDIT*.md", "dist/"]
|
|
41
|
+
exclude = ["SECURITY-AUDIT*.md", "dist/", "uv.lock"]
|
|
@@ -25,9 +25,12 @@ from .errors import APIError, error_from_response
|
|
|
25
25
|
from .resources import (
|
|
26
26
|
Envelopes,
|
|
27
27
|
Events,
|
|
28
|
+
Identity,
|
|
28
29
|
Invitations,
|
|
29
30
|
Members,
|
|
30
31
|
Organization,
|
|
32
|
+
Projects,
|
|
33
|
+
Retention,
|
|
31
34
|
Templates,
|
|
32
35
|
Webhooks,
|
|
33
36
|
)
|
|
@@ -102,8 +105,12 @@ class EasyContract:
|
|
|
102
105
|
)
|
|
103
106
|
|
|
104
107
|
self.envelopes = Envelopes(self)
|
|
108
|
+
|
|
109
|
+
self.identity = Identity(self)
|
|
110
|
+
self.retention = Retention(self)
|
|
105
111
|
self.templates = Templates(self)
|
|
106
112
|
self.webhooks = Webhooks(self)
|
|
113
|
+
self.projects = Projects(self)
|
|
107
114
|
self.events = Events(self)
|
|
108
115
|
self.organization = Organization(self)
|
|
109
116
|
self.members = Members(self)
|