@auths-dev/sdk 0.0.1 → 0.1.0

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.
Files changed (60) hide show
  1. package/Cargo.toml +45 -0
  2. package/README.md +163 -4
  3. package/__test__/client.spec.ts +78 -0
  4. package/__test__/exports.spec.ts +57 -0
  5. package/__test__/integration.spec.ts +407 -0
  6. package/__test__/policy.spec.ts +202 -0
  7. package/__test__/verify.spec.ts +88 -0
  8. package/build.rs +5 -0
  9. package/index.d.ts +259 -0
  10. package/index.js +622 -1
  11. package/lib/artifacts.ts +124 -0
  12. package/lib/attestations.ts +126 -0
  13. package/lib/audit.ts +189 -0
  14. package/lib/client.ts +293 -0
  15. package/lib/commits.ts +70 -0
  16. package/lib/devices.ts +178 -0
  17. package/lib/errors.ts +306 -0
  18. package/lib/identity.ts +280 -0
  19. package/lib/index.ts +125 -0
  20. package/lib/native.ts +255 -0
  21. package/lib/org.ts +235 -0
  22. package/lib/pairing.ts +271 -0
  23. package/lib/policy.ts +669 -0
  24. package/lib/signing.ts +204 -0
  25. package/lib/trust.ts +152 -0
  26. package/lib/types.ts +179 -0
  27. package/lib/verify.ts +241 -0
  28. package/lib/witness.ts +91 -0
  29. package/npm/darwin-arm64/README.md +3 -0
  30. package/npm/darwin-arm64/package.json +23 -0
  31. package/npm/linux-arm64-gnu/README.md +3 -0
  32. package/npm/linux-arm64-gnu/package.json +26 -0
  33. package/npm/linux-x64-gnu/README.md +3 -0
  34. package/npm/linux-x64-gnu/package.json +26 -0
  35. package/npm/win32-arm64-msvc/README.md +3 -0
  36. package/npm/win32-arm64-msvc/package.json +23 -0
  37. package/npm/win32-x64-msvc/README.md +3 -0
  38. package/npm/win32-x64-msvc/package.json +23 -0
  39. package/package.json +51 -16
  40. package/src/artifact.rs +217 -0
  41. package/src/attestation_query.rs +104 -0
  42. package/src/audit.rs +128 -0
  43. package/src/commit_sign.rs +63 -0
  44. package/src/device.rs +212 -0
  45. package/src/diagnostics.rs +106 -0
  46. package/src/error.rs +5 -0
  47. package/src/helpers.rs +60 -0
  48. package/src/identity.rs +467 -0
  49. package/src/lib.rs +26 -0
  50. package/src/org.rs +430 -0
  51. package/src/pairing.rs +454 -0
  52. package/src/policy.rs +147 -0
  53. package/src/sign.rs +215 -0
  54. package/src/trust.rs +189 -0
  55. package/src/types.rs +205 -0
  56. package/src/verify.rs +447 -0
  57. package/src/witness.rs +138 -0
  58. package/tsconfig.json +19 -0
  59. package/typedoc.json +18 -0
  60. package/vitest.config.ts +12 -0
package/src/pairing.rs ADDED
@@ -0,0 +1,454 @@
1
+ use napi_derive::napi;
2
+
3
+ use std::net::{IpAddr, Ipv4Addr, SocketAddr};
4
+ use std::sync::Arc;
5
+ use std::time::Duration;
6
+
7
+ use auths_core::storage::keychain::{IdentityDID, KeyAlias, KeyRole, KeyStorage};
8
+ use auths_id::storage::identity::IdentityStorage;
9
+ use auths_pairing_daemon::{
10
+ MockNetworkDiscovery, MockNetworkInterfaces, PairingDaemonBuilder, PairingDaemonHandle,
11
+ RateLimiter,
12
+ };
13
+ use auths_sdk::pairing::{
14
+ PairingAttestationParams, PairingSessionParams, build_pairing_session_request,
15
+ create_pairing_attestation,
16
+ };
17
+ use auths_storage::git::{RegistryAttestationStorage, RegistryIdentityStorage};
18
+ use chrono::Utc;
19
+ use tokio::sync::Mutex;
20
+
21
+ use crate::error::format_error;
22
+ use crate::helpers::{get_keychain, make_env_config, resolve_passphrase, resolve_repo_path};
23
+
24
+ #[napi(object)]
25
+ #[derive(Clone)]
26
+ pub struct NapiPairingSession {
27
+ pub session_id: String,
28
+ pub short_code: String,
29
+ pub endpoint: String,
30
+ pub token: String,
31
+ pub controller_did: String,
32
+ }
33
+
34
+ #[napi(object)]
35
+ #[derive(Clone)]
36
+ pub struct NapiPairingResponse {
37
+ pub device_did: String,
38
+ pub device_name: Option<String>,
39
+ pub device_public_key_hex: String,
40
+ }
41
+
42
+ #[napi(object)]
43
+ #[derive(Clone)]
44
+ pub struct NapiPairingResult {
45
+ pub device_did: String,
46
+ pub device_name: Option<String>,
47
+ pub attestation_rid: String,
48
+ }
49
+
50
+ #[napi]
51
+ pub struct NapiPairingHandle {
52
+ handle: Arc<Mutex<Option<PairingDaemonHandle>>>,
53
+ server_task: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
54
+ session_info: NapiPairingSession,
55
+ }
56
+
57
+ #[napi]
58
+ impl NapiPairingHandle {
59
+ #[napi(factory)]
60
+ #[allow(clippy::too_many_arguments)]
61
+ pub async fn create_session(
62
+ repo_path: String,
63
+ capabilities_json: Option<String>,
64
+ timeout_secs: Option<u32>,
65
+ bind_address: Option<String>,
66
+ enable_mdns: Option<bool>,
67
+ passphrase: Option<String>,
68
+ ) -> napi::Result<NapiPairingHandle> {
69
+ let _pp = resolve_passphrase(passphrase);
70
+ let repo = resolve_repo_path(Some(repo_path));
71
+ let bind_addr: IpAddr = bind_address
72
+ .as_deref()
73
+ .and_then(|s| s.parse().ok())
74
+ .unwrap_or(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)));
75
+ let timeout = timeout_secs.unwrap_or(300) as u64;
76
+ let mdns = enable_mdns.unwrap_or(true);
77
+
78
+ let capabilities: Vec<String> = if let Some(json) = capabilities_json {
79
+ serde_json::from_str(&json).unwrap_or_else(|_| vec!["sign:commit".to_string()])
80
+ } else {
81
+ vec!["sign:commit".to_string()]
82
+ };
83
+
84
+ let identity_storage = RegistryIdentityStorage::new(repo.clone());
85
+ let managed = identity_storage
86
+ .load_identity()
87
+ .map_err(|e| format_error("AUTHS_PAIRING_ERROR", e))?;
88
+ let controller_did = managed.controller_did.to_string();
89
+
90
+ #[allow(clippy::disallowed_methods)]
91
+ let now = Utc::now();
92
+ let session_req = build_pairing_session_request(
93
+ now,
94
+ PairingSessionParams {
95
+ controller_did: controller_did.clone(),
96
+ registry: "local".to_string(),
97
+ capabilities,
98
+ expiry_secs: timeout,
99
+ },
100
+ )
101
+ .map_err(|e| format_error("AUTHS_PAIRING_ERROR", e))?;
102
+
103
+ let session_id = session_req.create_request.session_id.clone();
104
+ let short_code = session_req.create_request.short_code.clone();
105
+
106
+ let mut builder = PairingDaemonBuilder::new().with_rate_limiter(RateLimiter::new(100));
107
+
108
+ let mock_addr = SocketAddr::new(bind_addr, 0);
109
+ builder = builder.with_network(MockNetworkInterfaces(bind_addr));
110
+
111
+ if !mdns {
112
+ builder = builder.with_discovery(MockNetworkDiscovery(mock_addr));
113
+ }
114
+
115
+ let daemon = builder
116
+ .build(session_req.create_request)
117
+ .map_err(|e| format_error("AUTHS_PAIRING_ERROR", e))?;
118
+
119
+ let token = daemon.token().to_string();
120
+ let (router, handle) = daemon.into_parts();
121
+
122
+ let listener = tokio::net::TcpListener::bind(SocketAddr::new(bind_addr, 0))
123
+ .await
124
+ .map_err(|e| format_error("AUTHS_PAIRING_ERROR", format!("Failed to bind: {e}")))?;
125
+ let local_addr = listener
126
+ .local_addr()
127
+ .map_err(|e| format_error("AUTHS_PAIRING_ERROR", format!("No local addr: {e}")))?;
128
+ let endpoint = format!("http://{}:{}", local_addr.ip(), local_addr.port());
129
+
130
+ let server_task = tokio::task::spawn(async move {
131
+ axum::serve(
132
+ listener,
133
+ router.into_make_service_with_connect_info::<SocketAddr>(),
134
+ )
135
+ .await
136
+ .ok();
137
+ });
138
+
139
+ let session_info = NapiPairingSession {
140
+ session_id,
141
+ short_code,
142
+ endpoint,
143
+ token,
144
+ controller_did,
145
+ };
146
+
147
+ Ok(NapiPairingHandle {
148
+ handle: Arc::new(Mutex::new(Some(handle))),
149
+ server_task: Arc::new(Mutex::new(Some(server_task))),
150
+ session_info,
151
+ })
152
+ }
153
+
154
+ #[napi(getter)]
155
+ pub fn session(&self) -> NapiPairingSession {
156
+ self.session_info.clone()
157
+ }
158
+
159
+ #[napi]
160
+ pub async fn wait_for_response(
161
+ &self,
162
+ timeout_secs: Option<u32>,
163
+ ) -> napi::Result<NapiPairingResponse> {
164
+ let timeout = Duration::from_secs(timeout_secs.unwrap_or(300) as u64);
165
+
166
+ let handle = {
167
+ let mut guard = self.handle.lock().await;
168
+ guard.take().ok_or_else(|| {
169
+ format_error(
170
+ "AUTHS_PAIRING_ERROR",
171
+ "Pairing handle already consumed or session stopped.",
172
+ )
173
+ })?
174
+ };
175
+
176
+ let result = handle.wait_for_response(timeout).await;
177
+
178
+ match result {
179
+ Ok(response) => {
180
+ let device_did = response.device_did.clone();
181
+ let device_name = response.device_name.clone();
182
+ let device_pk_hex =
183
+ hex::encode(response.device_signing_pubkey.decode().unwrap_or_default());
184
+ Ok(NapiPairingResponse {
185
+ device_did,
186
+ device_name,
187
+ device_public_key_hex: device_pk_hex,
188
+ })
189
+ }
190
+ Err(e) => Err(format_error("AUTHS_PAIRING_TIMEOUT", e)),
191
+ }
192
+ }
193
+
194
+ #[napi]
195
+ pub async fn complete(
196
+ &self,
197
+ device_did: String,
198
+ device_public_key_hex: String,
199
+ repo_path: String,
200
+ capabilities_json: Option<String>,
201
+ passphrase: Option<String>,
202
+ ) -> napi::Result<NapiPairingResult> {
203
+ let passphrase_str = resolve_passphrase(passphrase);
204
+ let repo = resolve_repo_path(Some(repo_path.clone()));
205
+ let env_config = make_env_config(&passphrase_str, &repo_path);
206
+
207
+ let capabilities: Vec<String> = if let Some(json) = capabilities_json {
208
+ serde_json::from_str(&json).unwrap_or_else(|_| vec!["sign:commit".to_string()])
209
+ } else {
210
+ vec!["sign:commit".to_string()]
211
+ };
212
+
213
+ let device_pubkey = hex::decode(&device_public_key_hex).map_err(|e| {
214
+ format_error(
215
+ "AUTHS_PAIRING_ERROR",
216
+ format!("Invalid public key hex: {e}"),
217
+ )
218
+ })?;
219
+
220
+ let identity_storage: Arc<dyn IdentityStorage + Send + Sync> =
221
+ Arc::new(RegistryIdentityStorage::new(repo.clone()));
222
+
223
+ let managed = identity_storage
224
+ .load_identity()
225
+ .map_err(|e| format_error("AUTHS_PAIRING_ERROR", e))?;
226
+ #[allow(clippy::disallowed_methods)] // INVARIANT: controller_did from storage
227
+ let controller_identity_did =
228
+ IdentityDID::new_unchecked(managed.controller_did.to_string());
229
+
230
+ let keychain = get_keychain(&env_config)?;
231
+ let aliases = keychain
232
+ .list_aliases_for_identity_with_role(&controller_identity_did, KeyRole::Primary)
233
+ .map_err(|e| format_error("AUTHS_PAIRING_ERROR", e))?;
234
+ let identity_key_alias_str = aliases
235
+ .into_iter()
236
+ .next()
237
+ .ok_or_else(|| format_error("AUTHS_PAIRING_ERROR", "No primary signing key found"))?;
238
+ #[allow(clippy::disallowed_methods)] // INVARIANT: alias from keychain storage
239
+ let identity_key_alias = KeyAlias::new_unchecked(identity_key_alias_str);
240
+
241
+ let key_storage: Arc<dyn KeyStorage + Send + Sync> = Arc::from(keychain);
242
+ let provider = Arc::new(auths_core::signing::PrefilledPassphraseProvider::new(
243
+ &passphrase_str,
244
+ ));
245
+
246
+ #[allow(clippy::disallowed_methods)]
247
+ let now = Utc::now();
248
+ let params = PairingAttestationParams {
249
+ identity_storage: identity_storage.clone(),
250
+ key_storage: key_storage.clone(),
251
+ device_pubkey: &device_pubkey,
252
+ device_did_str: &device_did,
253
+ capabilities: &capabilities,
254
+ identity_key_alias: &identity_key_alias,
255
+ passphrase_provider: provider,
256
+ };
257
+
258
+ let attestation = create_pairing_attestation(&params, now)
259
+ .map_err(|e| format_error("AUTHS_PAIRING_ERROR", e))?;
260
+
261
+ let attestation_storage = RegistryAttestationStorage::new(&repo);
262
+ use auths_id::attestation::AttestationSink;
263
+ attestation_storage
264
+ .export(
265
+ &auths_verifier::VerifiedAttestation::dangerous_from_unchecked(attestation.clone()),
266
+ )
267
+ .map_err(|e| format_error("AUTHS_PAIRING_ERROR", e))?;
268
+
269
+ Ok(NapiPairingResult {
270
+ device_did,
271
+ device_name: None,
272
+ attestation_rid: attestation.rid.to_string(),
273
+ })
274
+ }
275
+
276
+ #[napi]
277
+ pub async fn stop(&self) -> napi::Result<()> {
278
+ let mut handle_guard = self.handle.lock().await;
279
+ *handle_guard = None;
280
+
281
+ let mut task_guard = self.server_task.lock().await;
282
+ if let Some(task) = task_guard.take() {
283
+ task.abort();
284
+ }
285
+ Ok(())
286
+ }
287
+ }
288
+
289
+ impl Drop for NapiPairingHandle {
290
+ fn drop(&mut self) {
291
+ if let Ok(mut guard) = self.server_task.try_lock()
292
+ && let Some(task) = guard.take()
293
+ {
294
+ task.abort();
295
+ }
296
+ }
297
+ }
298
+
299
+ #[napi]
300
+ pub async fn join_pairing_session(
301
+ short_code: String,
302
+ endpoint: String,
303
+ token: String,
304
+ repo_path: String,
305
+ device_name: Option<String>,
306
+ passphrase: Option<String>,
307
+ ) -> napi::Result<NapiPairingResponse> {
308
+ let passphrase_str = resolve_passphrase(passphrase);
309
+ let repo = resolve_repo_path(Some(repo_path.clone()));
310
+ let env_config = make_env_config(&passphrase_str, &repo_path);
311
+
312
+ let identity_storage = RegistryIdentityStorage::new(repo.clone());
313
+ let managed = identity_storage
314
+ .load_identity()
315
+ .map_err(|e| format_error("AUTHS_PAIRING_ERROR", e))?;
316
+
317
+ #[allow(clippy::disallowed_methods)] // INVARIANT: controller_did from storage
318
+ let controller_identity_did = IdentityDID::new_unchecked(managed.controller_did.to_string());
319
+
320
+ let keychain = get_keychain(&env_config)?;
321
+ let aliases = keychain
322
+ .list_aliases_for_identity_with_role(&controller_identity_did, KeyRole::Primary)
323
+ .map_err(|e| format_error("AUTHS_PAIRING_ERROR", e))?;
324
+ let key_alias_str = aliases
325
+ .into_iter()
326
+ .next()
327
+ .ok_or_else(|| format_error("AUTHS_PAIRING_ERROR", "No primary signing key found"))?;
328
+
329
+ let (_did, _role, encrypted_key) = keychain
330
+ .load_key(&key_alias_str)
331
+ .map_err(|e| format_error("AUTHS_PAIRING_ERROR", e))?;
332
+
333
+ let pkcs8_bytes = auths_core::crypto::signer::decrypt_keypair(&encrypted_key, &passphrase_str)
334
+ .map_err(|e| format_error("AUTHS_PAIRING_ERROR", e))?;
335
+
336
+ let (seed, pubkey_32) = auths_crypto::parse_ed25519_key_material(&pkcs8_bytes)
337
+ .ok()
338
+ .and_then(|(seed, maybe_pk)| maybe_pk.map(|pk| (seed, pk)))
339
+ .or_else(|| {
340
+ let seed = auths_crypto::parse_ed25519_seed(&pkcs8_bytes).ok()?;
341
+ let pk = auths_core::crypto::provider_bridge::ed25519_public_key_from_seed_sync(&seed)
342
+ .ok()?;
343
+ Some((seed, pk))
344
+ })
345
+ .ok_or_else(|| {
346
+ format_error(
347
+ "AUTHS_PAIRING_ERROR",
348
+ "Failed to parse Ed25519 key material",
349
+ )
350
+ })?;
351
+
352
+ let device_did = auths_verifier::types::DeviceDID::from_ed25519(&pubkey_32);
353
+
354
+ let lookup_url = format!("{}/v1/pairing/sessions/by-code/{}", endpoint, short_code);
355
+
356
+ let session_data: serde_json::Value = {
357
+ let client = reqwest::Client::new();
358
+ let resp = client
359
+ .get(&lookup_url)
360
+ .send()
361
+ .await
362
+ .map_err(|e| format_error("AUTHS_PAIRING_ERROR", e))?;
363
+ resp.json::<serde_json::Value>()
364
+ .await
365
+ .map_err(|e| format_error("AUTHS_PAIRING_ERROR", e))?
366
+ };
367
+
368
+ let session_id = session_data["session_id"]
369
+ .as_str()
370
+ .ok_or_else(|| format_error("AUTHS_PAIRING_ERROR", "No session_id in response"))?
371
+ .to_string();
372
+
373
+ let token_data = &session_data["token"];
374
+ let controller_did_str = token_data["controller_did"]
375
+ .as_str()
376
+ .unwrap_or("")
377
+ .to_string();
378
+ let ephemeral_pubkey_str = token_data["ephemeral_pubkey"]
379
+ .as_str()
380
+ .unwrap_or("")
381
+ .to_string();
382
+ let capabilities: Vec<String> = token_data["capabilities"]
383
+ .as_array()
384
+ .map(|arr| {
385
+ arr.iter()
386
+ .filter_map(|v| v.as_str().map(|s| s.to_string()))
387
+ .collect()
388
+ })
389
+ .unwrap_or_default();
390
+ let expires_at = token_data["expires_at"].as_i64().unwrap_or(0);
391
+
392
+ #[allow(clippy::disallowed_methods)]
393
+ let now = Utc::now();
394
+ let pairing_token = auths_core::pairing::PairingToken {
395
+ controller_did: controller_did_str,
396
+ endpoint: endpoint.clone(),
397
+ short_code: short_code.clone(),
398
+ ephemeral_pubkey: ephemeral_pubkey_str,
399
+ expires_at: chrono::DateTime::from_timestamp(expires_at, 0).unwrap_or(now),
400
+ capabilities,
401
+ };
402
+
403
+ let secure_seed = auths_crypto::SecureSeed::new(*seed.as_bytes());
404
+ let (pairing_response, _shared_secret) = auths_core::pairing::PairingResponse::create(
405
+ now,
406
+ &pairing_token,
407
+ &secure_seed,
408
+ &pubkey_32,
409
+ device_did.to_string(),
410
+ device_name.clone(),
411
+ )
412
+ .map_err(|e| format_error("AUTHS_PAIRING_ERROR", e))?;
413
+
414
+ let submit_req = auths_core::pairing::types::SubmitResponseRequest {
415
+ device_x25519_pubkey: auths_core::pairing::types::Base64UrlEncoded::from_raw(
416
+ pairing_response.device_x25519_pubkey,
417
+ ),
418
+ device_signing_pubkey: auths_core::pairing::types::Base64UrlEncoded::from_raw(
419
+ pairing_response.device_signing_pubkey,
420
+ ),
421
+ device_did: pairing_response.device_did.clone(),
422
+ signature: auths_core::pairing::types::Base64UrlEncoded::from_raw(
423
+ pairing_response.signature,
424
+ ),
425
+ device_name: pairing_response.device_name,
426
+ };
427
+
428
+ let submit_url = format!("{}/v1/pairing/sessions/{}/response", endpoint, session_id);
429
+
430
+ {
431
+ let client = reqwest::Client::new();
432
+ let resp = client
433
+ .post(&submit_url)
434
+ .header("X-Pairing-Token", &token)
435
+ .json(&submit_req)
436
+ .send()
437
+ .await
438
+ .map_err(|e| format_error("AUTHS_PAIRING_ERROR", e))?;
439
+ if !resp.status().is_success() {
440
+ let status = resp.status();
441
+ let body = resp.text().await.unwrap_or_default();
442
+ return Err(format_error(
443
+ "AUTHS_PAIRING_ERROR",
444
+ format!("Submit response failed: {} {}", status, body),
445
+ ));
446
+ }
447
+ }
448
+
449
+ Ok(NapiPairingResponse {
450
+ device_did: device_did.to_string(),
451
+ device_name,
452
+ device_public_key_hex: hex::encode(pubkey_32),
453
+ })
454
+ }
package/src/policy.rs ADDED
@@ -0,0 +1,147 @@
1
+ use napi_derive::napi;
2
+
3
+ use auths_policy::{
4
+ CanonicalCapability, CanonicalDid, EvalContext, SignerType, compile_from_json, enforce_simple,
5
+ };
6
+ use chrono::Utc;
7
+
8
+ use crate::error::format_error;
9
+
10
+ #[napi(object)]
11
+ #[derive(Clone)]
12
+ pub struct NapiPolicyDecision {
13
+ pub outcome: String,
14
+ pub reason: String,
15
+ pub message: String,
16
+ }
17
+
18
+ #[napi]
19
+ pub fn compile_policy(policy_json: String) -> napi::Result<String> {
20
+ compile_from_json(policy_json.as_bytes()).map_err(|errors| {
21
+ let msgs: Vec<String> = errors
22
+ .iter()
23
+ .map(|e| format!("{}: {}", e.path, e.message))
24
+ .collect();
25
+ format_error(
26
+ "AUTHS_POLICY_COMPILE_ERROR",
27
+ format!("Policy compilation failed: {}", msgs.join("; ")),
28
+ )
29
+ })?;
30
+ Ok(policy_json)
31
+ }
32
+
33
+ #[napi]
34
+ #[allow(clippy::too_many_arguments)]
35
+ pub fn evaluate_policy(
36
+ policy_json: String,
37
+ issuer: String,
38
+ subject: String,
39
+ capabilities: Option<Vec<String>>,
40
+ role: Option<String>,
41
+ revoked: Option<bool>,
42
+ expires_at: Option<String>,
43
+ repo: Option<String>,
44
+ environment: Option<String>,
45
+ signer_type: Option<String>,
46
+ delegated_by: Option<String>,
47
+ chain_depth: Option<u32>,
48
+ ) -> napi::Result<NapiPolicyDecision> {
49
+ let compiled = compile_from_json(policy_json.as_bytes()).map_err(|errors| {
50
+ let msgs: Vec<String> = errors
51
+ .iter()
52
+ .map(|e| format!("{}: {}", e.path, e.message))
53
+ .collect();
54
+ format_error(
55
+ "AUTHS_POLICY_COMPILE_ERROR",
56
+ format!("Policy compilation failed: {}", msgs.join("; ")),
57
+ )
58
+ })?;
59
+
60
+ let issuer_did = CanonicalDid::parse(&issuer).map_err(|e| {
61
+ format_error(
62
+ "AUTHS_POLICY_INVALID_DID",
63
+ format!("Invalid issuer DID: {e}"),
64
+ )
65
+ })?;
66
+ let subject_did = CanonicalDid::parse(&subject).map_err(|e| {
67
+ format_error(
68
+ "AUTHS_POLICY_INVALID_DID",
69
+ format!("Invalid subject DID: {e}"),
70
+ )
71
+ })?;
72
+
73
+ #[allow(clippy::disallowed_methods)]
74
+ let now = Utc::now();
75
+ let mut ctx = EvalContext::new(now, issuer_did, subject_did).revoked(revoked.unwrap_or(false));
76
+
77
+ if let Some(caps) = capabilities {
78
+ for cap_str in &caps {
79
+ let cap = CanonicalCapability::parse(cap_str).map_err(|e| {
80
+ format_error(
81
+ "AUTHS_POLICY_INVALID_CAPABILITY",
82
+ format!("Invalid capability '{cap_str}': {e}"),
83
+ )
84
+ })?;
85
+ ctx = ctx.capability(cap);
86
+ }
87
+ }
88
+
89
+ if let Some(r) = role {
90
+ ctx = ctx.role(r);
91
+ }
92
+
93
+ if let Some(exp) = expires_at {
94
+ let ts: chrono::DateTime<Utc> = exp.parse().map_err(|_| {
95
+ format_error(
96
+ "AUTHS_POLICY_INVALID_TIMESTAMP",
97
+ format!("Invalid expires_at RFC 3339: {exp}"),
98
+ )
99
+ })?;
100
+ ctx = ctx.expires_at(ts);
101
+ }
102
+
103
+ if let Some(r) = repo {
104
+ ctx = ctx.repo(r);
105
+ }
106
+
107
+ if let Some(env) = environment {
108
+ ctx = ctx.environment(env);
109
+ }
110
+
111
+ if let Some(st) = signer_type {
112
+ let parsed = match st.to_lowercase().as_str() {
113
+ "human" => SignerType::Human,
114
+ "agent" => SignerType::Agent,
115
+ "workload" => SignerType::Workload,
116
+ _ => {
117
+ return Err(format_error(
118
+ "AUTHS_POLICY_INVALID_SIGNER_TYPE",
119
+ format!("Invalid signer_type: '{st}'. Must be 'human', 'agent', or 'workload'"),
120
+ ));
121
+ }
122
+ };
123
+ ctx = ctx.signer_type(parsed);
124
+ }
125
+
126
+ if let Some(d) = delegated_by {
127
+ let did = CanonicalDid::parse(&d).map_err(|e| {
128
+ format_error(
129
+ "AUTHS_POLICY_INVALID_DID",
130
+ format!("Invalid delegated_by DID: {e}"),
131
+ )
132
+ })?;
133
+ ctx = ctx.delegated_by(did);
134
+ }
135
+
136
+ if let Some(depth) = chain_depth {
137
+ ctx = ctx.chain_depth(depth);
138
+ }
139
+
140
+ let decision = enforce_simple(&compiled, &ctx);
141
+
142
+ Ok(NapiPolicyDecision {
143
+ outcome: decision.outcome.to_string().to_lowercase(),
144
+ reason: format!("{:?}", decision.reason),
145
+ message: decision.message,
146
+ })
147
+ }