auths-python 0.1.0__cp38-abi3-win_amd64.whl

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.
auths/_client.py ADDED
@@ -0,0 +1,713 @@
1
+ """Auths client — primary entry point for all SDK operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ import json
8
+
9
+ from auths._native import (
10
+ get_token as _get_token,
11
+ sign_action as _sign_action,
12
+ sign_bytes as _sign_bytes,
13
+ verify_action_envelope as _verify_action_envelope,
14
+ verify_at_time as _verify_at_time,
15
+ verify_at_time_with_capability as _verify_at_time_with_capability,
16
+ verify_attestation as _verify_attestation,
17
+ verify_attestation_with_capability as _verify_attestation_with_capability,
18
+ verify_chain as _verify_chain,
19
+ verify_chain_with_capability as _verify_chain_with_capability,
20
+ verify_chain_with_witnesses as _verify_chain_with_witnesses,
21
+ verify_device_authorization as _verify_device_authorization,
22
+ )
23
+ from auths._errors import (
24
+ CryptoError,
25
+ IdentityError,
26
+ KeychainError,
27
+ NetworkError,
28
+ OrgError,
29
+ PairingError,
30
+ StorageError,
31
+ VerificationError,
32
+ )
33
+
34
+ if TYPE_CHECKING:
35
+ from auths._native import VerificationReport, VerificationResult
36
+ from auths.artifact import ArtifactPublishResult, ArtifactSigningResult
37
+ from auths.commit import CommitSigningResult
38
+ from auths.verify import WitnessConfig
39
+
40
+
41
+ _ERROR_CODE_MAP = {
42
+ "AUTHS_ISSUER_SIG_FAILED": ("invalid_signature", VerificationError),
43
+ "AUTHS_DEVICE_SIG_FAILED": ("invalid_signature", VerificationError),
44
+ "AUTHS_ATTESTATION_EXPIRED": ("expired_attestation", VerificationError),
45
+ "AUTHS_ATTESTATION_REVOKED": ("revoked_device", VerificationError),
46
+ "AUTHS_TIMESTAMP_IN_FUTURE": ("future_timestamp", VerificationError),
47
+ "AUTHS_MISSING_CAPABILITY": ("missing_capability", VerificationError),
48
+ "AUTHS_CRYPTO_ERROR": ("invalid_key", CryptoError),
49
+ "AUTHS_DID_RESOLUTION_ERROR": ("invalid_key", CryptoError),
50
+ "AUTHS_INVALID_INPUT": ("invalid_signature", VerificationError),
51
+ "AUTHS_SERIALIZATION_ERROR": ("invalid_signature", VerificationError),
52
+ "AUTHS_BUNDLE_EXPIRED": ("expired_attestation", VerificationError),
53
+ "AUTHS_KEY_NOT_FOUND": ("key_not_found", CryptoError),
54
+ "AUTHS_INCORRECT_PASSPHRASE": ("signing_failed", CryptoError),
55
+ "AUTHS_SIGNING_FAILED": ("signing_failed", CryptoError),
56
+ "AUTHS_SIGNING_ERROR": ("signing_failed", CryptoError),
57
+ "AUTHS_INPUT_TOO_LARGE": ("invalid_signature", VerificationError),
58
+ "AUTHS_INTERNAL_ERROR": ("unknown", VerificationError),
59
+ "AUTHS_ORG_VERIFICATION_FAILED": ("invalid_signature", VerificationError),
60
+ "AUTHS_ORG_ATTESTATION_EXPIRED": ("expired_attestation", VerificationError),
61
+ "AUTHS_ORG_DID_RESOLUTION_FAILED": ("invalid_key", CryptoError),
62
+ "AUTHS_REGISTRY_ERROR": ("repo_not_found", StorageError),
63
+ "AUTHS_KEYCHAIN_ERROR": ("keychain_locked", KeychainError),
64
+ "AUTHS_IDENTITY_ERROR": ("identity_not_found", IdentityError),
65
+ "AUTHS_DEVICE_ERROR": ("unknown", IdentityError),
66
+ "AUTHS_ROTATION_ERROR": ("unknown", IdentityError),
67
+ "AUTHS_NETWORK_ERROR": ("server_error", NetworkError),
68
+ "AUTHS_VERIFICATION_FAILED": ("invalid_signature", VerificationError),
69
+ "AUTHS_ORG_ERROR": ("org_error", OrgError),
70
+ "AUTHS_PAIRING_ERROR": ("pairing_error", PairingError),
71
+ "AUTHS_PAIRING_TIMEOUT": ("timeout", PairingError),
72
+ "AUTHS_TRUST_ERROR": ("trust_error", StorageError),
73
+ "AUTHS_WITNESS_ERROR": ("witness_error", StorageError),
74
+ "AUTHS_AUDIT_ERROR": ("audit_error", VerificationError),
75
+ "AUTHS_DIAGNOSTIC_ERROR": ("diagnostic_error", VerificationError),
76
+ }
77
+
78
+
79
+ def _map_error(exc: Exception, *, default_cls: type = VerificationError) -> Exception:
80
+ msg = str(exc)
81
+ code = None
82
+ if msg.startswith("[AUTHS_") and "] " in msg:
83
+ code = msg[1:msg.index("]")]
84
+ msg = msg[msg.index("] ") + 2:]
85
+ if code and code in _ERROR_CODE_MAP:
86
+ py_code, cls = _ERROR_CODE_MAP[code]
87
+ return cls(msg, code=py_code)
88
+ low = msg.lower()
89
+ if "public key" in low or "private key" in low or "invalid key" in low or "hex" in low:
90
+ return CryptoError(msg, code="invalid_key")
91
+ return default_cls(msg, code="unknown")
92
+
93
+
94
+ def _map_network_error(exc: Exception) -> Exception:
95
+ msg = str(exc)
96
+ if "unreachable" in msg.lower() or "connection" in msg.lower():
97
+ return NetworkError(msg, code="connection_failed", should_retry=True)
98
+ if "timeout" in msg.lower():
99
+ return NetworkError(msg, code="timeout", should_retry=True)
100
+ return NetworkError(msg, code="server_error")
101
+
102
+
103
+ class Auths:
104
+ """Auths SDK client — decentralized identity for developers.
105
+
106
+ Examples:
107
+ ```python
108
+ auths = Auths()
109
+ result = auths.verify(attestation_json=data, issuer_key=key)
110
+ sig = auths.sign(b"hello", private_key=key_hex)
111
+ ```
112
+ """
113
+
114
+ def __init__(self, repo_path: str = "~/.auths", passphrase: str | None = None):
115
+ self.repo_path = repo_path
116
+ self._passphrase = passphrase
117
+
118
+ from auths.attestation_query import AttestationService
119
+ from auths.audit import AuditService
120
+ from auths.devices import DeviceService
121
+ from auths.identity import IdentityService
122
+ from auths.org import OrgService
123
+ from auths.doctor import DoctorService
124
+ from auths.pairing import PairingService
125
+ from auths.trust import TrustService
126
+ from auths.witness import WitnessService
127
+
128
+ self.identities = IdentityService(self)
129
+ self.devices = DeviceService(self)
130
+ self.attestations = AttestationService(self)
131
+ self.orgs = OrgService(self)
132
+ self.audit = AuditService(self)
133
+ self.trust = TrustService(self)
134
+ self.witnesses = WitnessService(self)
135
+ self.doctor = DoctorService(self)
136
+ self.pairing = PairingService(self)
137
+
138
+ def verify(
139
+ self,
140
+ attestation_json: str,
141
+ issuer_key: str,
142
+ required_capability: str | None = None,
143
+ at: str | None = None,
144
+ ) -> VerificationResult:
145
+ """Verify a single attestation, optionally at a specific historical timestamp.
146
+
147
+ Args:
148
+ attestation_json: The attestation JSON string.
149
+ issuer_key: Issuer's public key hex.
150
+ required_capability: If set, also verify the attestation grants this capability.
151
+ at: RFC 3339 timestamp to verify against (e.g., "2024-06-15T00:00:00Z").
152
+ When set, checks validity at that point in time instead of now.
153
+
154
+ Returns:
155
+ VerificationResult with validity status and details.
156
+
157
+ Raises:
158
+ VerificationError: If the attestation signature is invalid or expired.
159
+ CryptoError: If the issuer key is malformed.
160
+
161
+ Examples:
162
+ ```python
163
+ result = auths.verify(att_json, key, at="2024-06-15T00:00:00Z",
164
+ required_capability="deploy:staging")
165
+ ```
166
+ """
167
+ try:
168
+ if at and required_capability:
169
+ return _verify_at_time_with_capability(
170
+ attestation_json, issuer_key, at, required_capability
171
+ )
172
+ if at:
173
+ return _verify_at_time(attestation_json, issuer_key, at)
174
+ if required_capability:
175
+ return _verify_attestation_with_capability(
176
+ attestation_json, issuer_key, required_capability
177
+ )
178
+ return _verify_attestation(attestation_json, issuer_key)
179
+ except (ValueError, RuntimeError) as exc:
180
+ raise _map_error(exc) from exc
181
+
182
+ def verify_chain(
183
+ self,
184
+ attestations: list[str],
185
+ root_key: str,
186
+ required_capability: str | None = None,
187
+ witnesses: WitnessConfig | None = None,
188
+ ) -> VerificationReport:
189
+ """Verify an attestation chain, optionally with witness quorum.
190
+
191
+ Args:
192
+ attestations: List of attestation JSON strings, ordered root-to-leaf.
193
+ root_key: Root identity's public key hex.
194
+ required_capability: If set, verify the chain grants this capability.
195
+ witnesses: If set, enforces witness receipt quorum.
196
+
197
+ Returns:
198
+ VerificationReport with per-link results and overall validity.
199
+
200
+ Raises:
201
+ VerificationError: If any link in the chain fails verification.
202
+
203
+ Examples:
204
+ ```python
205
+ report = auths.verify_chain(chain, root_key, witnesses=config)
206
+ ```
207
+ """
208
+ try:
209
+ if witnesses:
210
+ keys_json = [
211
+ json.dumps({"did": k.did, "public_key_hex": k.public_key_hex})
212
+ for k in witnesses.keys
213
+ ]
214
+ return _verify_chain_with_witnesses(
215
+ attestations, root_key,
216
+ witnesses.receipts, keys_json, witnesses.threshold,
217
+ )
218
+ if required_capability:
219
+ return _verify_chain_with_capability(
220
+ attestations, root_key, required_capability
221
+ )
222
+ return _verify_chain(attestations, root_key)
223
+ except (ValueError, RuntimeError) as exc:
224
+ raise _map_error(exc) from exc
225
+
226
+ def verify_device(
227
+ self,
228
+ identity_did: str,
229
+ device_did: str,
230
+ attestations: list[str],
231
+ identity_key: str,
232
+ ) -> VerificationReport:
233
+ """Verify device authorization against an identity.
234
+
235
+ Args:
236
+ identity_did: The parent identity's DID.
237
+ device_did: The device DID to verify.
238
+ attestations: Attestation chain JSON strings.
239
+ identity_key: Identity's public key hex.
240
+
241
+ Returns:
242
+ VerificationReport confirming the device is authorized.
243
+
244
+ Raises:
245
+ VerificationError: If the device authorization is invalid or revoked.
246
+ """
247
+ try:
248
+ return _verify_device_authorization(
249
+ identity_did, device_did, attestations, identity_key
250
+ )
251
+ except (ValueError, RuntimeError) as exc:
252
+ raise _map_error(exc) from exc
253
+
254
+ def sign(self, message: bytes, private_key: str) -> str:
255
+ """Sign raw bytes with a private key.
256
+
257
+ Args:
258
+ message: Bytes to sign.
259
+ private_key: Hex-encoded Ed25519 private key.
260
+
261
+ Returns:
262
+ Hex-encoded Ed25519 signature.
263
+
264
+ Raises:
265
+ CryptoError: If the private key is invalid or signing fails.
266
+ """
267
+ try:
268
+ return _sign_bytes(private_key, message)
269
+ except (ValueError, RuntimeError) as exc:
270
+ raise _map_error(exc, default_cls=CryptoError) from exc
271
+
272
+ def sign_action(
273
+ self,
274
+ action_type: str,
275
+ payload: str,
276
+ identity_did: str,
277
+ private_key: str,
278
+ ) -> str:
279
+ """Sign an action envelope.
280
+
281
+ Args:
282
+ action_type: Action type string.
283
+ payload: JSON payload string.
284
+ identity_did: The signer's DID.
285
+ private_key: Hex-encoded Ed25519 private key.
286
+
287
+ Returns:
288
+ JSON-serialized signed action envelope.
289
+
290
+ Raises:
291
+ CryptoError: If signing fails.
292
+ """
293
+ try:
294
+ return _sign_action(private_key, action_type, payload, identity_did)
295
+ except (ValueError, RuntimeError) as exc:
296
+ raise _map_error(exc, default_cls=CryptoError) from exc
297
+
298
+ def verify_action(self, envelope_json: str, public_key: str) -> VerificationResult:
299
+ """Verify an action envelope signature.
300
+
301
+ Args:
302
+ envelope_json: JSON-serialized signed action envelope.
303
+ public_key: Hex-encoded Ed25519 public key of the signer.
304
+
305
+ Returns:
306
+ VerificationResult with validity status.
307
+
308
+ Raises:
309
+ VerificationError: If the envelope signature is invalid.
310
+ """
311
+ try:
312
+ return _verify_action_envelope(envelope_json, public_key)
313
+ except (ValueError, RuntimeError) as exc:
314
+ raise _map_error(exc) from exc
315
+
316
+ def sign_as(
317
+ self,
318
+ message: bytes,
319
+ identity: str,
320
+ passphrase: str | None = None,
321
+ ) -> str:
322
+ """Sign bytes using a keychain-stored identity key.
323
+
324
+ Args:
325
+ message: Bytes to sign.
326
+ identity: The identity DID (`did:keri:...`) whose key to use.
327
+ passphrase: Override passphrase (default: client passphrase or AUTHS_PASSPHRASE).
328
+
329
+ Returns:
330
+ Hex-encoded Ed25519 signature.
331
+
332
+ Raises:
333
+ CryptoError: If the key is not found or signing fails.
334
+ KeychainError: If the keychain is locked or inaccessible.
335
+
336
+ Examples:
337
+ ```python
338
+ identity = auths.identities.create(label="laptop")
339
+ sig = auths.sign_as(b"hello", identity=identity.did)
340
+ ```
341
+ """
342
+ from auths._native import sign_as_identity
343
+
344
+ pp = passphrase or self._passphrase
345
+ try:
346
+ return sign_as_identity(message, identity, self.repo_path, pp)
347
+ except (ValueError, RuntimeError) as exc:
348
+ raise _map_error(exc, default_cls=CryptoError) from exc
349
+
350
+ def sign_action_as(
351
+ self,
352
+ action_type: str,
353
+ payload: str,
354
+ identity: str,
355
+ passphrase: str | None = None,
356
+ ) -> str:
357
+ """Sign an action envelope using a keychain-stored identity key.
358
+
359
+ Args:
360
+ action_type: Action type string.
361
+ payload: JSON payload string.
362
+ identity: The identity DID whose key to use.
363
+ passphrase: Override passphrase.
364
+
365
+ Returns:
366
+ JSON-serialized signed action envelope.
367
+
368
+ Raises:
369
+ CryptoError: If signing fails.
370
+ KeychainError: If the keychain is locked or inaccessible.
371
+
372
+ Examples:
373
+ ```python
374
+ envelope = auths.sign_action_as("deploy", payload_json, identity=identity.did)
375
+ ```
376
+ """
377
+ from auths._native import sign_action_as_identity
378
+
379
+ pp = passphrase or self._passphrase
380
+ try:
381
+ return sign_action_as_identity(
382
+ action_type, payload, identity, self.repo_path, pp
383
+ )
384
+ except (ValueError, RuntimeError) as exc:
385
+ raise _map_error(exc, default_cls=CryptoError) from exc
386
+
387
+ def get_public_key(
388
+ self,
389
+ identity: str,
390
+ passphrase: str | None = None,
391
+ ) -> str:
392
+ """Retrieve the Ed25519 public key (hex) for an identity.
393
+
394
+ Args:
395
+ identity: The identity DID (`did:keri:...`).
396
+ passphrase: Override passphrase.
397
+
398
+ Returns:
399
+ Hex-encoded Ed25519 public key.
400
+
401
+ Raises:
402
+ CryptoError: If the identity key is not found.
403
+ KeychainError: If the keychain is locked or inaccessible.
404
+
405
+ Examples:
406
+ ```python
407
+ pub_key = auths.get_public_key(identity.did)
408
+ ```
409
+ """
410
+ from auths._native import get_identity_public_key
411
+
412
+ pp = passphrase or self._passphrase
413
+ try:
414
+ return get_identity_public_key(identity, self.repo_path, pp)
415
+ except (ValueError, RuntimeError) as exc:
416
+ raise _map_error(exc, default_cls=CryptoError) from exc
417
+
418
+ def sign_as_agent(
419
+ self,
420
+ message: bytes,
421
+ key_alias: str,
422
+ passphrase: str | None = None,
423
+ ) -> str:
424
+ """Sign bytes using a delegated agent's own key.
425
+
426
+ Unlike `sign_as()` which resolves by identity DID, this uses the agent's
427
+ key alias directly — enabling delegated agents (`did:key:`) to sign.
428
+
429
+ Args:
430
+ message: Bytes to sign.
431
+ key_alias: The agent's key alias (e.g., "deploy-bot-agent").
432
+ passphrase: Override passphrase.
433
+
434
+ Returns:
435
+ Hex-encoded Ed25519 signature.
436
+
437
+ Raises:
438
+ CryptoError: If the agent key is not found or signing fails.
439
+ KeychainError: If the keychain is locked or inaccessible.
440
+
441
+ Examples:
442
+ ```python
443
+ agent = auths.identities.delegate_agent(identity.did, "bot", ["sign"])
444
+ sig = auths.sign_as_agent(b"hello", key_alias=agent._key_alias)
445
+ ```
446
+ """
447
+ from auths._native import sign_as_agent as _sign_as_agent
448
+
449
+ pp = passphrase or self._passphrase
450
+ try:
451
+ return _sign_as_agent(message, key_alias, self.repo_path, pp)
452
+ except (ValueError, RuntimeError) as exc:
453
+ raise _map_error(exc, default_cls=CryptoError) from exc
454
+
455
+ def sign_action_as_agent(
456
+ self,
457
+ action_type: str,
458
+ payload: str,
459
+ key_alias: str,
460
+ agent_did: str,
461
+ passphrase: str | None = None,
462
+ ) -> str:
463
+ """Sign an action envelope using a delegated agent's own key.
464
+
465
+ Args:
466
+ action_type: Action type string.
467
+ payload: JSON payload string.
468
+ key_alias: The agent's key alias.
469
+ agent_did: The agent's DID (included in the envelope).
470
+ passphrase: Override passphrase.
471
+
472
+ Returns:
473
+ JSON-serialized signed action envelope.
474
+
475
+ Raises:
476
+ CryptoError: If signing fails.
477
+ KeychainError: If the keychain is locked or inaccessible.
478
+
479
+ Examples:
480
+ ```python
481
+ agent = auths.identities.delegate_agent(identity.did, "bot", ["deploy"])
482
+ envelope = auths.sign_action_as_agent("deploy", payload, agent._key_alias, agent.did)
483
+ ```
484
+ """
485
+ from auths._native import sign_action_as_agent as _sign_action_as_agent
486
+
487
+ pp = passphrase or self._passphrase
488
+ try:
489
+ return _sign_action_as_agent(action_type, payload, key_alias, agent_did, self.repo_path, pp)
490
+ except (ValueError, RuntimeError) as exc:
491
+ raise _map_error(exc, default_cls=CryptoError) from exc
492
+
493
+ def sign_commit(
494
+ self,
495
+ data: bytes,
496
+ *,
497
+ identity_did: str,
498
+ passphrase: str | None = None,
499
+ ) -> CommitSigningResult:
500
+ """Sign git commit/tag data, producing an SSHSIG PEM signature.
501
+
502
+ Uses a 3-tier fallback:
503
+
504
+ 1. ssh-agent (fastest, works on dev machines with agent running)
505
+ 2. auto-start agent (starts a transient agent process)
506
+ 3. direct signing (works everywhere, including headless CI)
507
+
508
+ Args:
509
+ data: The raw commit or tag bytes to sign.
510
+ identity_did: The KERI DID of the identity to sign with.
511
+ passphrase: Optional passphrase (for headless envs without ssh-agent).
512
+
513
+ Returns:
514
+ CommitSigningResult with the SSHSIG PEM block, method, and namespace.
515
+
516
+ Raises:
517
+ CryptoError: If signing fails or the identity key is not found.
518
+ KeychainError: If the keychain is locked or inaccessible.
519
+
520
+ Examples:
521
+ ```python
522
+ result = auths.sign_commit(commit_bytes, identity_did=identity.did)
523
+ ```
524
+ """
525
+ from auths._native import sign_commit as _sign_commit
526
+ from auths.commit import CommitSigningResult
527
+
528
+ pp = passphrase or self._passphrase
529
+ try:
530
+ raw = _sign_commit(data, identity_did, self.repo_path, pp)
531
+ return CommitSigningResult(
532
+ signature_pem=raw.signature_pem,
533
+ method=raw.method,
534
+ namespace=raw.namespace,
535
+ )
536
+ except (ValueError, RuntimeError) as exc:
537
+ raise _map_error(exc, default_cls=CryptoError) from exc
538
+
539
+ def sign_artifact(
540
+ self,
541
+ path: str,
542
+ *,
543
+ identity_did: str,
544
+ expires_in: int | None = None,
545
+ note: str | None = None,
546
+ ) -> ArtifactSigningResult:
547
+ """Sign a file artifact, producing a dual-signed attestation.
548
+
549
+ Computes SHA-256 digest of the file and creates an attestation binding
550
+ the digest to your identity.
551
+
552
+ Args:
553
+ path: Path to the file to sign.
554
+ identity_did: The identity DID to sign with (used as key alias).
555
+ expires_in: Duration in seconds until expiration (per RFC 6749).
556
+ note: Optional human-readable note.
557
+
558
+ Returns:
559
+ ArtifactSigningResult with the attestation JSON, RID, digest, and file size.
560
+
561
+ Raises:
562
+ FileNotFoundError: If the file does not exist.
563
+ CryptoError: If signing fails.
564
+
565
+ Examples:
566
+ ```python
567
+ result = auths.sign_artifact("release.tar.gz", identity_did=identity.did)
568
+ ```
569
+ """
570
+ from auths._native import sign_artifact as _sign_artifact
571
+ from auths.artifact import ArtifactSigningResult
572
+
573
+ pp = self._passphrase
574
+ try:
575
+ raw = _sign_artifact(
576
+ path, identity_did, self.repo_path, pp, expires_in, note,
577
+ )
578
+ return ArtifactSigningResult(
579
+ attestation_json=raw.attestation_json,
580
+ rid=raw.rid,
581
+ digest=raw.digest,
582
+ file_size=raw.file_size,
583
+ )
584
+ except FileNotFoundError:
585
+ raise
586
+ except (ValueError, RuntimeError) as exc:
587
+ raise _map_error(exc, default_cls=CryptoError) from exc
588
+
589
+ def sign_artifact_bytes(
590
+ self,
591
+ data: bytes,
592
+ *,
593
+ identity_did: str,
594
+ expires_in: int | None = None,
595
+ note: str | None = None,
596
+ ) -> ArtifactSigningResult:
597
+ """Sign raw bytes, producing a dual-signed attestation.
598
+
599
+ Use this for non-file artifacts: container manifest digests,
600
+ git tree hashes, API response bodies.
601
+
602
+ Args:
603
+ data: The raw bytes to sign.
604
+ identity_did: The identity DID to sign with (used as key alias).
605
+ expires_in: Duration in seconds until expiration (per RFC 6749).
606
+ note: Optional human-readable note.
607
+
608
+ Returns:
609
+ ArtifactSigningResult with the attestation JSON, RID, digest, and size.
610
+
611
+ Raises:
612
+ CryptoError: If signing fails.
613
+
614
+ Examples:
615
+ ```python
616
+ result = auths.sign_artifact_bytes(manifest_bytes, identity_did=did)
617
+ ```
618
+ """
619
+ from auths._native import sign_artifact_bytes as _sign_artifact_bytes
620
+ from auths.artifact import ArtifactSigningResult
621
+
622
+ pp = self._passphrase
623
+ try:
624
+ raw = _sign_artifact_bytes(
625
+ data, identity_did, self.repo_path, pp, expires_in, note,
626
+ )
627
+ return ArtifactSigningResult(
628
+ attestation_json=raw.attestation_json,
629
+ rid=raw.rid,
630
+ digest=raw.digest,
631
+ file_size=raw.file_size,
632
+ )
633
+ except (ValueError, RuntimeError) as exc:
634
+ raise _map_error(exc, default_cls=CryptoError) from exc
635
+
636
+ def publish_artifact(
637
+ self,
638
+ attestation_json: str,
639
+ *,
640
+ registry_url: str,
641
+ package_name: str | None = None,
642
+ ) -> "ArtifactPublishResult":
643
+ """Publish a signed attestation to a registry.
644
+
645
+ Args:
646
+ attestation_json: The attestation JSON string from `sign_artifact()`.
647
+ registry_url: Base URL of the target registry.
648
+ package_name: Optional ecosystem-prefixed identifier (e.g. "npm:react@18.3.0").
649
+
650
+ Returns:
651
+ ArtifactPublishResult with the registry RID, package name, and signer DID.
652
+
653
+ Raises:
654
+ StorageError: If the attestation is a duplicate.
655
+ VerificationError: If the registry rejects the attestation.
656
+ NetworkError: If the registry is unreachable.
657
+
658
+ Examples:
659
+ ```python
660
+ signed = auths.sign_artifact("release.tar.gz", identity_did=did)
661
+ result = auths.publish_artifact(
662
+ signed.attestation_json,
663
+ registry_url="https://registry.example.com",
664
+ )
665
+ ```
666
+ """
667
+ from auths._native import publish_artifact as _publish_artifact
668
+ from auths.artifact import ArtifactPublishResult
669
+
670
+ try:
671
+ raw = _publish_artifact(attestation_json, registry_url, package_name)
672
+ return ArtifactPublishResult(
673
+ attestation_rid=raw.attestation_rid,
674
+ package_name=raw.package_name,
675
+ signer_did=raw.signer_did,
676
+ )
677
+ except (ValueError, RuntimeError) as exc:
678
+ msg = str(exc)
679
+ if "duplicate_attestation" in msg:
680
+ raise StorageError(msg, code="duplicate_attestation") from exc
681
+ if "verification_failed" in msg:
682
+ raise VerificationError(msg, code="verification_failed") from exc
683
+ if "unreachable" in msg.lower() or "connection" in msg.lower() or "timeout" in msg.lower():
684
+ raise _map_network_error(exc) from exc
685
+ raise _map_error(exc) from exc
686
+
687
+ def get_token(
688
+ self,
689
+ bridge_url: str,
690
+ chain_json: str,
691
+ root_key: str,
692
+ capabilities: list[str] | None = None,
693
+ ) -> str:
694
+ """Exchange an attestation chain for a bearer token.
695
+
696
+ Args:
697
+ bridge_url: The OIDC bridge base URL.
698
+ chain_json: JSON-serialized attestation chain.
699
+ root_key: Root identity's public key hex.
700
+ capabilities: Optional list of capabilities to request.
701
+
702
+ Returns:
703
+ JWT bearer token string.
704
+
705
+ Raises:
706
+ NetworkError: If the bridge is unreachable or returns an error.
707
+ """
708
+ try:
709
+ return _get_token(bridge_url, chain_json, root_key, capabilities or [])
710
+ except ConnectionError as exc:
711
+ raise _map_network_error(exc) from exc
712
+ except (ValueError, RuntimeError) as exc:
713
+ raise _map_network_error(exc) from exc