@nomosprotocol/sdk 1.2.0 → 1.3.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.
package/README.md CHANGED
@@ -14,7 +14,7 @@ Every public authority on the [NOMOS Exchange](https://www.nomosprotocol.com/exc
14
14
  import { nomos } from "@nomosprotocol/sdk";
15
15
 
16
16
  const verdict = await nomos.can({
17
- authority: "eu-ai-act-full-regulation-2024-1689",
17
+ authority: "eu-ai-act.nomos",
18
18
  ai_categorises_biometric_data_for_sensitive_attributes: true,
19
19
  });
20
20
 
@@ -29,7 +29,7 @@ Facts can be spread flat alongside `authority` (as above), or passed explicitly:
29
29
 
30
30
  ```ts
31
31
  const verdict = await nomos.can({
32
- authority: "eu-ai-act-full-regulation-2024-1689",
32
+ authority: "eu-ai-act.nomos",
33
33
  inputs: { ai_categorises_biometric_data_for_sensitive_attributes: true },
34
34
  action: "screen a new feature before launch", // optional, recorded in the transcript
35
35
  });
@@ -54,6 +54,26 @@ const verdict = await nomos.can({
54
54
 
55
55
  Every call mints a permanent, cryptographically sealed transcript — open `query_id` any time at `nomosprotocol.com/queries/<query_id>` to replay exactly what was asked and answered, bound to the exact authority version that responded.
56
56
 
57
+ ### Error handling
58
+
59
+ A failed call (unknown authority, network issue, upstream error) rejects with a typed `NomosError` subclass — never a bare `Error` — so you can branch on `instanceof` or the stable `.code` string:
60
+
61
+ ```ts
62
+ import { nomos, NomosNotFoundError, NomosNetworkError } from "@nomosprotocol/sdk";
63
+
64
+ try {
65
+ const verdict = await nomos.can({ authority: "eu-ai-act.nomos", ...facts });
66
+ } catch (err) {
67
+ if (err instanceof NomosNotFoundError) {
68
+ // authority slug/id doesn't exist — err.code === 'not_found'
69
+ } else if (err instanceof NomosNetworkError) {
70
+ // request timed out or the network failed
71
+ } else {
72
+ throw err;
73
+ }
74
+ }
75
+ ```
76
+
57
77
  ### Options
58
78
 
59
79
  ```ts
package/dist/index.cjs CHANGED
@@ -25,13 +25,16 @@ __export(index_exports, {
25
25
  NomosAuthenticationError: () => NomosAuthenticationError,
26
26
  NomosAuthorizationError: () => NomosAuthorizationError,
27
27
  NomosError: () => NomosError,
28
+ NomosIssuerNotTrustedError: () => NomosIssuerNotTrustedError,
28
29
  NomosNetworkError: () => NomosNetworkError,
29
30
  NomosNotFoundError: () => NomosNotFoundError,
31
+ NomosPaymentRequiredError: () => NomosPaymentRequiredError,
30
32
  NomosRateLimitError: () => NomosRateLimitError,
31
33
  NomosValidationError: () => NomosValidationError,
32
34
  NomosWebhookSignatureError: () => NomosWebhookSignatureError,
33
35
  can: () => can,
34
- nomos: () => nomos
36
+ nomos: () => nomos,
37
+ verifyChain: () => verifyChain
35
38
  });
36
39
  module.exports = __toCommonJS(index_exports);
37
40
 
@@ -42,8 +45,10 @@ __export(errors_exports, {
42
45
  NomosAuthenticationError: () => NomosAuthenticationError,
43
46
  NomosAuthorizationError: () => NomosAuthorizationError,
44
47
  NomosError: () => NomosError,
48
+ NomosIssuerNotTrustedError: () => NomosIssuerNotTrustedError,
45
49
  NomosNetworkError: () => NomosNetworkError,
46
50
  NomosNotFoundError: () => NomosNotFoundError,
51
+ NomosPaymentRequiredError: () => NomosPaymentRequiredError,
47
52
  NomosRateLimitError: () => NomosRateLimitError,
48
53
  NomosValidationError: () => NomosValidationError,
49
54
  NomosWebhookSignatureError: () => NomosWebhookSignatureError
@@ -107,6 +112,21 @@ var NomosWebhookSignatureError = class extends NomosError {
107
112
  this.name = "NomosWebhookSignatureError";
108
113
  }
109
114
  };
115
+ var NomosPaymentRequiredError = class extends NomosError {
116
+ constructor(message, priceUsd, purchaseUrl, requestId) {
117
+ super(message, "payment_required", 402, requestId);
118
+ this.name = "NomosPaymentRequiredError";
119
+ this.priceUsd = priceUsd;
120
+ this.purchaseUrl = purchaseUrl;
121
+ }
122
+ };
123
+ var NomosIssuerNotTrustedError = class extends NomosError {
124
+ constructor(message, reason, requestId) {
125
+ super(message, "issuer_not_trusted", 403, requestId);
126
+ this.name = "NomosIssuerNotTrustedError";
127
+ this.reason = reason;
128
+ }
129
+ };
110
130
 
111
131
  // src/util.ts
112
132
  async function sleep(ms) {
@@ -526,6 +546,192 @@ var Webhooks = class {
526
546
  }
527
547
  };
528
548
 
549
+ // src/can.ts
550
+ var CHAIN_PROTOCOL_VERSION = "1";
551
+ var DEFAULT_EXCHANGE_BASE = "https://www.nomosprotocol.com";
552
+ function splitFacts(params) {
553
+ const { authority, artifact_id, action, inputs, ...flat } = params;
554
+ void authority;
555
+ void artifact_id;
556
+ const facts = inputs && typeof inputs === "object" ? inputs : flat;
557
+ return { facts, action };
558
+ }
559
+ function mapHttpError(status, data, requestId) {
560
+ const message = data?.error ?? data?.message ?? `NOMOS query failed (HTTP ${status})`;
561
+ if (data?.code === "PURCHASE_REQUIRED") {
562
+ throw new NomosPaymentRequiredError(message, data.price_usd ?? 0, data.purchase_url ?? "", requestId);
563
+ }
564
+ switch (status) {
565
+ case 401:
566
+ throw new NomosAuthenticationError(message, requestId);
567
+ case 403:
568
+ throw new NomosAuthorizationError(message, requestId);
569
+ case 404:
570
+ throw new NomosNotFoundError(message, requestId);
571
+ case 422:
572
+ case 400:
573
+ throw new NomosValidationError(message, data?.details, requestId);
574
+ case 429:
575
+ throw new NomosRateLimitError(message, void 0, requestId);
576
+ default:
577
+ throw new NomosAPIError(message, status, requestId);
578
+ }
579
+ }
580
+ function normalizeAuthorityQueryResult(data, authority) {
581
+ return {
582
+ allowed: data.verdict === "AUTHORIZED",
583
+ verdict: data.verdict,
584
+ reason: data.verdict_description,
585
+ rule: data.matched_rule_id ?? null,
586
+ query_id: data.query_id,
587
+ url: data.url,
588
+ authority: {
589
+ id: data.artifact?.id ?? authority,
590
+ title: data.artifact?.title ?? "",
591
+ standing: data.artifact?.standing ?? "",
592
+ ...data.artifact?.attestation ? { attestation: data.artifact.attestation } : {}
593
+ },
594
+ ...data.artifact?.version ? { version: data.artifact.version } : {},
595
+ missing_inputs: data.missing_inputs ?? [],
596
+ ...typeof data.max_age === "number" ? { max_age: data.max_age } : {},
597
+ // authority-query.ts already returns { kind, rule_id, rule_name } — the public-safe shape
598
+ // CanResult uses directly.
599
+ ...Array.isArray(data.obligations) && data.obligations.length > 0 ? { obligations: data.obligations } : {},
600
+ // NOMOS-SPEC-006 — present only when the issuer has actually revoked this exact sealed
601
+ // version; omitted (not false) when revocation doesn't structurally apply here, so "checked
602
+ // and clean" and "couldn't check" never look the same to a caller.
603
+ ...data.revoked === true ? { revoked: true, revocation_reason: data.revocation_reason } : {}
604
+ };
605
+ }
606
+ async function canViaExchange(authority, facts, action, opts) {
607
+ const base = (opts?.baseUrl ?? DEFAULT_EXCHANGE_BASE).replace(/\/$/, "");
608
+ const url = `${base}/api/v1/can`;
609
+ let res;
610
+ try {
611
+ res = await fetch(url, {
612
+ method: "POST",
613
+ headers: { "Content-Type": "application/json" },
614
+ body: JSON.stringify({ artifact: authority, action, ...facts }),
615
+ signal: opts?.signal
616
+ });
617
+ } catch (err) {
618
+ if (err?.name === "AbortError") throw new NomosNetworkError("Request aborted");
619
+ throw new NomosNetworkError(err?.message ?? "Network error");
620
+ }
621
+ const requestId = res.headers.get("x-request-id") ?? "";
622
+ const data = await res.json().catch(() => null);
623
+ if (!res.ok) mapHttpError(res.status, data, requestId);
624
+ return normalizeAuthorityQueryResult(data, authority);
625
+ }
626
+ async function canViaExchangeAuthenticated(http, authority, facts, action) {
627
+ const { data } = await http.post("/api/v1/can", { artifact: authority, action, ...facts });
628
+ return normalizeAuthorityQueryResult(data, authority);
629
+ }
630
+ async function canViaArtifact(http, artifactId, facts, action) {
631
+ const isPublic = artifactId.startsWith("pub_");
632
+ const path = isPublic ? "/api/v1/verify" : "/api/v1/verify-decision";
633
+ const body = isPublic ? { artifact_id: artifactId, decision: action ?? "evaluate", inputs: facts } : { artifact_id: artifactId, decision: action ?? "evaluate", inputs: facts, caller: { correlation_id: globalThis.crypto.randomUUID() } };
634
+ const { data: raw } = await http.post(path, body);
635
+ const verdict = raw.verdict === "approved" ? "AUTHORIZED" : raw.verdict === "rejected" ? "DENIED" : raw.outcome === "auto_approved" ? "AUTHORIZED" : raw.outcome === "auto_rejected" ? "DENIED" : "ESCALATED";
636
+ return {
637
+ allowed: raw.allowed ?? verdict === "AUTHORIZED",
638
+ verdict,
639
+ reason: raw.verdict_description ?? "",
640
+ rule: raw.rule?.id ?? null,
641
+ authority: {
642
+ id: raw.artifact_id ?? artifactId,
643
+ title: raw.artifact_name ?? "",
644
+ standing: raw.confidence?.tier ?? ""
645
+ },
646
+ ...raw.artifact_version ? { version: raw.artifact_version } : {},
647
+ missing_inputs: raw.incomplete_inputs?.fields ?? [],
648
+ ...raw.audit ? { audit: { hash: raw.audit.hash ?? raw.audit_hash ?? "", timestamp: raw.audit.timestamp ?? raw.ts ?? (/* @__PURE__ */ new Date()).toISOString() } } : {},
649
+ // This rail's obligations are RuleEffect objects (authority-types.ts) — a richer, INTERNAL
650
+ // shape ({ effect_type, value, provenance: { rule_id, rule_name } }), NOT the same shape the
651
+ // Exchange rail already returns ({ kind, rule_id, rule_name }). Mapped explicitly here rather
652
+ // than passed through raw — the two rails' server-side representations genuinely differ, and
653
+ // assuming they matched would have been the actual bug.
654
+ ...Array.isArray(raw.obligations) && raw.obligations.length > 0 ? { obligations: raw.obligations.map((o) => ({
655
+ kind: o.kind ?? o.value ?? "obligation",
656
+ rule_id: o.rule_id ?? o.provenance?.rule_id ?? null,
657
+ rule_name: o.rule_name ?? o.provenance?.rule_name ?? null,
658
+ ...o.reason ? { reason: o.reason } : {}
659
+ })) } : {}
660
+ };
661
+ }
662
+ async function canViaChain(artifact, keyCerts, rootPublicKeyPem, facts, action, opts) {
663
+ const base = (opts?.baseUrl ?? DEFAULT_EXCHANGE_BASE).replace(/\/$/, "");
664
+ const url = `${base}/api/v1/chain-of-trust/verify`;
665
+ let res;
666
+ try {
667
+ res = await fetch(url, {
668
+ method: "POST",
669
+ headers: { "Content-Type": "application/json" },
670
+ body: JSON.stringify({ version: CHAIN_PROTOCOL_VERSION, artifact, key_certs: keyCerts, root_public_key_pem: rootPublicKeyPem, facts, action }),
671
+ signal: opts?.signal
672
+ });
673
+ } catch (err) {
674
+ if (err?.name === "AbortError") throw new NomosNetworkError("Request aborted");
675
+ throw new NomosNetworkError(err?.message ?? "Network error");
676
+ }
677
+ const requestId = res.headers.get("x-request-id") ?? "";
678
+ const data = await res.json().catch(() => null);
679
+ if (data?.decision === "AUTHORIZED" || data?.decision === "DENIED" || data?.decision === "ESCALATED") {
680
+ return {
681
+ allowed: data.decision === "AUTHORIZED",
682
+ verdict: data.decision,
683
+ reason: data.reason ?? "",
684
+ rule: data.rule ?? null,
685
+ authority: { id: "presented-artifact", title: "", standing: "" },
686
+ // Read directly off the artifact the caller already has — no server round-trip needed for
687
+ // data the caller is already holding.
688
+ ...artifact?.meta?.version ? { version: artifact.meta.version } : {},
689
+ missing_inputs: data.missing_inputs ?? [],
690
+ ...Array.isArray(data.obligations) && data.obligations.length > 0 ? { obligations: data.obligations } : {}
691
+ // Deliberately no query_id/url/audit — this evaluation is stateless by design (the whole
692
+ // point is answering without NOMOS needing to have seen this artifact before); there is no
693
+ // permanent transcript to point to, and faking one would misrepresent what just happened.
694
+ };
695
+ }
696
+ const message = data?.detail ?? `Chain verification failed (HTTP ${res.status})`;
697
+ if (data?.decision === "ISSUER_NOT_RECOGNIZED" || data?.decision === "KEY_REVOKED" || data?.decision === "SEAL_INVALID" || data?.decision === "ALLOWED") {
698
+ throw new NomosIssuerNotTrustedError(message, data.decision, requestId);
699
+ }
700
+ if (data?.decision === "MALFORMED") throw new NomosValidationError(message, data, requestId);
701
+ mapHttpError(res.status, data, requestId);
702
+ }
703
+ async function can(params, opts) {
704
+ if (params.artifact !== void 0) {
705
+ if (!params.key_certs || !params.root_public_key_pem) {
706
+ throw new Error("can(): `artifact` requires `key_certs` and `root_public_key_pem` alongside it.");
707
+ }
708
+ const { facts: facts2, action: action2 } = splitFacts(params);
709
+ return canViaChain(params.artifact, params.key_certs, params.root_public_key_pem, facts2, action2, opts);
710
+ }
711
+ if (params.artifact_id) {
712
+ throw new Error("can(): `artifact_id` requires an API key \u2014 use `client.can(...)` on an authenticated Nomos instance, not the free `can()` export.");
713
+ }
714
+ if (!params.authority || typeof params.authority !== "string") {
715
+ throw new Error("can(): `authority` (an Exchange artifact id or slug), `artifact_id` (via client.can()), or `artifact` (chain-of-trust) is required.");
716
+ }
717
+ const { facts, action } = splitFacts(params);
718
+ return canViaExchange(params.authority, facts, action, opts);
719
+ }
720
+ async function canAuthenticated(http, params, opts) {
721
+ void opts;
722
+ const { facts, action } = splitFacts(params);
723
+ if (params.artifact !== void 0) {
724
+ if (!params.key_certs || !params.root_public_key_pem) {
725
+ throw new Error("can(): `artifact` requires `key_certs` and `root_public_key_pem` alongside it.");
726
+ }
727
+ return canViaChain(params.artifact, params.key_certs, params.root_public_key_pem, facts, action, opts);
728
+ }
729
+ if (params.artifact_id) return canViaArtifact(http, params.artifact_id, facts, action);
730
+ if (params.authority) return canViaExchangeAuthenticated(http, params.authority, facts, action);
731
+ throw new Error("can(): `authority` (a public Exchange artifact), `artifact_id` (your own artifact), or `artifact` (chain-of-trust) is required.");
732
+ }
733
+ var nomos = { can };
734
+
529
735
  // src/nomos.ts
530
736
  var DEFAULT_BASE_URL = "https://nomos.ai";
531
737
  var DEFAULT_MAX_RETRIES = 3;
@@ -541,55 +747,69 @@ var Nomos = class {
541
747
  );
542
748
  }
543
749
  this.mode = apiKey.startsWith("nms_test_") ? "test" : "live";
544
- const http = new HttpClient({
750
+ this.http = new HttpClient({
545
751
  apiKey,
546
752
  baseUrl: (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, ""),
547
753
  maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
548
754
  timeout: config.timeout ?? DEFAULT_TIMEOUT
549
755
  });
550
- this.decisions = new Decisions(http);
551
- this.artifacts = new Artifacts(http);
552
- this.governance = new Governance(http);
756
+ this.decisions = new Decisions(this.http);
757
+ this.artifacts = new Artifacts(this.http);
758
+ this.governance = new Governance(this.http);
553
759
  this.webhooks = new Webhooks();
554
760
  }
761
+ /**
762
+ * "Can I do this?" — the one verb, now covering your own artifacts too. Pass `authority` for a
763
+ * public Exchange artifact, or `artifact_id` for your own (Studio or the Exchange) — same
764
+ * return shape either way, so calling code never needs to know which rail answered.
765
+ *
766
+ * @example
767
+ * const client = new Nomos('nms_live_...');
768
+ * const verdict = await client.can({ artifact_id: 'loan_approval_v1', credit_score: 720 });
769
+ */
770
+ async can(params, opts) {
771
+ return canAuthenticated(this.http, params, opts);
772
+ }
555
773
  };
556
774
 
557
- // src/can.ts
558
- var DEFAULT_EXCHANGE_BASE = "https://www.nomosprotocol.com";
559
- async function can(params, opts) {
560
- const { authority, action, inputs, ...flat } = params;
561
- if (!authority || typeof authority !== "string") {
562
- throw new Error("can(): `authority` (an Exchange artifact id or slug) is required.");
775
+ // src/verify-chain.ts
776
+ var CHAIN_PROTOCOL_VERSION2 = "1";
777
+ var DEFAULT_BASE = "https://www.nomosprotocol.com";
778
+ async function verifyChain(params, opts) {
779
+ if (!params.root_public_key_pem) {
780
+ throw new Error("verifyChain(): `root_public_key_pem` is required \u2014 there is no default root.");
563
781
  }
564
- const facts = inputs && typeof inputs === "object" ? inputs : flat;
565
- const base = (opts?.baseUrl ?? DEFAULT_EXCHANGE_BASE).replace(/\/$/, "");
566
- const url = `${base}/api/exchange/artifacts/${encodeURIComponent(authority)}/query`;
567
- const res = await fetch(url, {
568
- method: "POST",
569
- headers: { "Content-Type": "application/json" },
570
- body: JSON.stringify({ inputs: facts, action }),
571
- signal: opts?.signal
572
- });
782
+ const base = (opts?.baseUrl ?? DEFAULT_BASE).replace(/\/$/, "");
783
+ const url = `${base}/api/v1/chain-of-trust/verify`;
784
+ let res;
785
+ try {
786
+ res = await fetch(url, {
787
+ method: "POST",
788
+ headers: { "Content-Type": "application/json" },
789
+ body: JSON.stringify({
790
+ version: CHAIN_PROTOCOL_VERSION2,
791
+ artifact: params.artifact,
792
+ key_certs: params.key_certs,
793
+ root_public_key_pem: params.root_public_key_pem
794
+ }),
795
+ signal: opts?.signal
796
+ });
797
+ } catch (err) {
798
+ if (err?.name === "AbortError") throw new NomosNetworkError("Request aborted");
799
+ throw new NomosNetworkError(err?.message ?? "Network error");
800
+ }
801
+ const requestId = res.headers.get("x-request-id") ?? "";
573
802
  const data = await res.json().catch(() => null);
574
- if (!res.ok) {
575
- throw new Error(data?.error ?? `NOMOS query failed (HTTP ${res.status})`);
803
+ if (data && typeof data.decision === "string") {
804
+ return data;
576
805
  }
577
- return {
578
- allowed: data.verdict === "AUTHORIZED",
579
- verdict: data.verdict,
580
- reason: data.verdict_description,
581
- rule: data.matched_rule_id ?? null,
582
- query_id: data.query_id,
583
- url: data.url,
584
- authority: {
585
- id: data.artifact?.id ?? authority,
586
- title: data.artifact?.title ?? "",
587
- standing: data.artifact?.standing ?? ""
588
- },
589
- missing_inputs: data.missing_inputs ?? []
590
- };
806
+ const message = data?.error ?? `Chain verification request failed (HTTP ${res.status})`;
807
+ if (res.status === 429) {
808
+ throw new NomosRateLimitError(message, res.headers.get("retry-after") ? parseInt(res.headers.get("retry-after"), 10) : void 0, requestId);
809
+ }
810
+ if (res.status === 400) throw new NomosValidationError(message, data?.details, requestId);
811
+ throw new NomosAPIError(message, res.status, requestId);
591
812
  }
592
- var nomos = { can };
593
813
  // Annotate the CommonJS export names for ESM import in node:
594
814
  0 && (module.exports = {
595
815
  Nomos,
@@ -597,12 +817,15 @@ var nomos = { can };
597
817
  NomosAuthenticationError,
598
818
  NomosAuthorizationError,
599
819
  NomosError,
820
+ NomosIssuerNotTrustedError,
600
821
  NomosNetworkError,
601
822
  NomosNotFoundError,
823
+ NomosPaymentRequiredError,
602
824
  NomosRateLimitError,
603
825
  NomosValidationError,
604
826
  NomosWebhookSignatureError,
605
827
  can,
606
- nomos
828
+ nomos,
829
+ verifyChain
607
830
  });
608
831
  //# sourceMappingURL=index.cjs.map