@nomosprotocol/sdk 1.2.1 → 1.3.1
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 +75 -11
- package/dist/index.cjs +250 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +171 -12
- package/dist/index.d.ts +171 -12
- package/dist/index.js +245 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
# @nomosprotocol/sdk
|
|
2
2
|
|
|
3
|
-
Official TypeScript SDK for [NOMOS](https://www.nomosprotocol.com) — ask
|
|
3
|
+
Official TypeScript SDK for [NOMOS](https://www.nomosprotocol.com) — ask "can I do this?" from your code and get back a deterministic, cryptographically verifiable verdict.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install @nomosprotocol/sdk
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
## `
|
|
9
|
+
## `can()` — one function, three ways to ask
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Same pattern as Stripe's PaymentIntents API: one function, routed entirely by what you pass it, one verdict vocabulary back regardless of which one ran.
|
|
12
|
+
|
|
13
|
+
| You ask about... | Call it as | Needs an API key? |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| A public authority on the [NOMOS Exchange](https://www.nomosprotocol.com/exchange) | `nomos.can({ authority, ...facts })` | No |
|
|
16
|
+
| Your own sealed artifact | `client.can({ artifact_id, ...facts })` | Yes |
|
|
17
|
+
| An artifact someone handed you directly — NOMOS has never seen it | `nomos.can({ artifact, key_certs, root_public_key_pem, ...facts })` | No |
|
|
18
|
+
|
|
19
|
+
### Public Exchange authority (keyless)
|
|
20
|
+
|
|
21
|
+
No API key, no setup — this is the front door.
|
|
12
22
|
|
|
13
23
|
```ts
|
|
14
24
|
import { nomos } from "@nomosprotocol/sdk";
|
|
@@ -35,9 +45,44 @@ const verdict = await nomos.can({
|
|
|
35
45
|
});
|
|
36
46
|
```
|
|
37
47
|
|
|
48
|
+
A paid authority you haven't purchased throws `NomosPaymentRequiredError` (with price + purchase URL attached) instead of a generic failure — NOMOS never auto-charges on your behalf. See [Error handling](#error-handling).
|
|
49
|
+
|
|
50
|
+
### Your own artifact (authenticated)
|
|
51
|
+
|
|
52
|
+
Same function, same verdict shape — swap `authority` for `artifact_id` and use the authenticated client:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { Nomos } from "@nomosprotocol/sdk";
|
|
56
|
+
|
|
57
|
+
const client = new Nomos("nms_live_..."); // or "nms_test_..." for test mode
|
|
58
|
+
|
|
59
|
+
const verdict = await client.can({
|
|
60
|
+
artifact_id: "loan_approval_v1",
|
|
61
|
+
credit_score: 720,
|
|
62
|
+
loan_amount: 50_000,
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Chain-of-trust (keyless, prototype)
|
|
67
|
+
|
|
68
|
+
For an artifact you received directly — from an agent, another system, anywhere — that NOMOS has never seen or registered. You supply the artifact, the key certificates presented alongside it, and the root public key **you've** independently decided to trust (there's deliberately no default). NOMOS verifies the chain resolves to that root, then evaluates the artifact's own rules against your facts — one call, no prior lookup:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const verdict = await nomos.can({
|
|
72
|
+
artifact: theArtifactYouWereHanded,
|
|
73
|
+
key_certs: theCertificatesTheyIncluded,
|
|
74
|
+
root_public_key_pem: yourPinnedRootKey,
|
|
75
|
+
credit_score: 720,
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
If the chain doesn't resolve — unrecognized issuer, revoked key, tampered artifact — this throws `NomosIssuerNotTrustedError` rather than a fake verdict; "the rules say no" and "we never established who's asking" are different claims that call for different next steps. See [Error handling](#error-handling).
|
|
80
|
+
|
|
81
|
+
This rail is a working **prototype**, not yet a numbered NOMOS spec — the primitive is in production use but hasn't been through a second independent implementation.
|
|
82
|
+
|
|
38
83
|
### The verdict
|
|
39
84
|
|
|
40
|
-
`
|
|
85
|
+
`can()` always returns the authority's own verdict, verbatim — nothing invented, nothing paraphrased — normalized into the same shape regardless of which rail answered:
|
|
41
86
|
|
|
42
87
|
| Field | Type | Meaning |
|
|
43
88
|
|---|---|---|
|
|
@@ -45,27 +90,44 @@ const verdict = await nomos.can({
|
|
|
45
90
|
| `verdict` | `"AUTHORIZED" \| "DENIED" \| "ESCALATED"` | The authority's own answer |
|
|
46
91
|
| `reason` | `string` | Human-readable explanation |
|
|
47
92
|
| `rule` | `string \| null` | The rule id that decided it, if any |
|
|
48
|
-
| `query_id` | `string` | Permanent
|
|
49
|
-
| `url` | `string` | Path to the transcript's permalink page |
|
|
50
|
-
| `authority` | `{ id, title, standing }` | Which authority answered,
|
|
93
|
+
| `query_id` | `string` | Permanent transcript id — Exchange rail only; absent (not faked) elsewhere |
|
|
94
|
+
| `url` | `string` | Path to the transcript's permalink page — same availability as `query_id` |
|
|
95
|
+
| `authority` | `{ id, title, standing, attestation? }` | Which authority answered, its standing, and — if a regulator/auditor/issuer has co-signed this exact version — who, in what role, and their statement |
|
|
96
|
+
| `version` | `string` | The artifact's own declared `meta.version`, when it has one |
|
|
51
97
|
| `missing_inputs` | `string[]` | Fields the authority needed but didn't get — present on `ESCALATED` |
|
|
98
|
+
| `obligations` | `Array<{ kind, rule_id, rule_name }>` | Notify/log/flag-style duties that fired alongside the verdict — can accompany an `AUTHORIZED` result, not just a denial |
|
|
99
|
+
| `revoked` | `boolean` | `true` only if the issuer has revoked this exact sealed version — forces `verdict` to `DENIED` regardless of what the rules say. Absent (not `false`) when revocation doesn't apply here |
|
|
100
|
+
| `revocation_reason` | `string` | Present only alongside `revoked: true` |
|
|
101
|
+
| `max_age` | `number` | Recommended seconds to hold this verdict before re-checking — advisory, absent when not structurally revocable |
|
|
102
|
+
| `audit` | `{ hash, timestamp }` | Present only on the private-artifact rail — a hash you can store for compliance |
|
|
52
103
|
|
|
53
104
|
`ESCALATED` means the authority needs more facts to decide — not a flat no. Check `missing_inputs` for what to supply.
|
|
54
105
|
|
|
55
|
-
|
|
106
|
+
The Exchange rail mints a permanent, cryptographically sealed transcript on every call — 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. The other two rails are stateless by design (asking about your own artifact, or one NOMOS has never seen, has no transcript to point to) — `audit.hash` is the private-artifact rail's own provenance instead.
|
|
56
107
|
|
|
57
108
|
### Error handling
|
|
58
109
|
|
|
59
|
-
A failed call
|
|
110
|
+
A failed call rejects with a typed `NomosError` subclass — never a bare `Error` — so you can branch on `instanceof` or the stable `.code` string:
|
|
60
111
|
|
|
61
112
|
```ts
|
|
62
|
-
import {
|
|
113
|
+
import {
|
|
114
|
+
nomos,
|
|
115
|
+
NomosNotFoundError,
|
|
116
|
+
NomosNetworkError,
|
|
117
|
+
NomosPaymentRequiredError,
|
|
118
|
+
NomosIssuerNotTrustedError,
|
|
119
|
+
} from "@nomosprotocol/sdk";
|
|
63
120
|
|
|
64
121
|
try {
|
|
65
122
|
const verdict = await nomos.can({ authority: "eu-ai-act.nomos", ...facts });
|
|
66
123
|
} catch (err) {
|
|
67
124
|
if (err instanceof NomosNotFoundError) {
|
|
68
125
|
// authority slug/id doesn't exist — err.code === 'not_found'
|
|
126
|
+
} else if (err instanceof NomosPaymentRequiredError) {
|
|
127
|
+
// paid, unpurchased — err.priceUsd / err.purchaseUrl
|
|
128
|
+
} else if (err instanceof NomosIssuerNotTrustedError) {
|
|
129
|
+
// chain-of-trust rail only — err.reason is one of
|
|
130
|
+
// ISSUER_NOT_RECOGNIZED | KEY_REVOKED | SEAL_INVALID
|
|
69
131
|
} else if (err instanceof NomosNetworkError) {
|
|
70
132
|
// request timed out or the network failed
|
|
71
133
|
} else {
|
|
@@ -88,13 +150,15 @@ await nomos.can(
|
|
|
88
150
|
|
|
89
151
|
## `Nomos` — authenticated client
|
|
90
152
|
|
|
91
|
-
|
|
153
|
+
Beyond `client.can()`, the authenticated client covers the rest of the platform — the raw (richer) decision-verification shape, governance generation, and webhook verification:
|
|
92
154
|
|
|
93
155
|
```ts
|
|
94
156
|
import { Nomos } from "@nomosprotocol/sdk";
|
|
95
157
|
|
|
96
158
|
const client = new Nomos("nms_live_..."); // or "nms_test_..." for test mode
|
|
97
159
|
|
|
160
|
+
// Lower-level than client.can() — same evaluation, but returns the full raw shape
|
|
161
|
+
// (evaluation counts, confidence tier, paths_to_approval) instead of the normalized verdict.
|
|
98
162
|
const result = await client.decisions.verify({
|
|
99
163
|
artifact_id: "your_artifact_id",
|
|
100
164
|
decision_context: { /* facts */ },
|
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,35 +747,51 @@ var Nomos = class {
|
|
|
541
747
|
);
|
|
542
748
|
}
|
|
543
749
|
this.mode = apiKey.startsWith("nms_test_") ? "test" : "live";
|
|
544
|
-
|
|
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/
|
|
558
|
-
var
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
if (!
|
|
562
|
-
throw new Error("
|
|
563
|
-
}
|
|
564
|
-
const
|
|
565
|
-
const
|
|
566
|
-
const url = `${base}/api/exchange/artifacts/${encodeURIComponent(authority)}/query`;
|
|
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.");
|
|
781
|
+
}
|
|
782
|
+
const base = (opts?.baseUrl ?? DEFAULT_BASE).replace(/\/$/, "");
|
|
783
|
+
const url = `${base}/api/v1/chain-of-trust/verify`;
|
|
567
784
|
let res;
|
|
568
785
|
try {
|
|
569
786
|
res = await fetch(url, {
|
|
570
787
|
method: "POST",
|
|
571
788
|
headers: { "Content-Type": "application/json" },
|
|
572
|
-
body: JSON.stringify({
|
|
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
|
+
}),
|
|
573
795
|
signal: opts?.signal
|
|
574
796
|
});
|
|
575
797
|
} catch (err) {
|
|
@@ -578,44 +800,16 @@ async function can(params, opts) {
|
|
|
578
800
|
}
|
|
579
801
|
const requestId = res.headers.get("x-request-id") ?? "";
|
|
580
802
|
const data = await res.json().catch(() => null);
|
|
581
|
-
if (
|
|
582
|
-
|
|
583
|
-
switch (res.status) {
|
|
584
|
-
case 401:
|
|
585
|
-
throw new NomosAuthenticationError(message, requestId);
|
|
586
|
-
case 403:
|
|
587
|
-
throw new NomosAuthorizationError(message, requestId);
|
|
588
|
-
case 404:
|
|
589
|
-
throw new NomosNotFoundError(message, requestId);
|
|
590
|
-
case 422:
|
|
591
|
-
case 400:
|
|
592
|
-
throw new NomosValidationError(message, data?.details, requestId);
|
|
593
|
-
case 429:
|
|
594
|
-
throw new NomosRateLimitError(
|
|
595
|
-
message,
|
|
596
|
-
res.headers.get("retry-after") ? parseInt(res.headers.get("retry-after"), 10) : void 0,
|
|
597
|
-
requestId
|
|
598
|
-
);
|
|
599
|
-
default:
|
|
600
|
-
throw new NomosAPIError(message, res.status, requestId);
|
|
601
|
-
}
|
|
803
|
+
if (data && typeof data.decision === "string") {
|
|
804
|
+
return data;
|
|
602
805
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
url: data.url,
|
|
610
|
-
authority: {
|
|
611
|
-
id: data.artifact?.id ?? authority,
|
|
612
|
-
title: data.artifact?.title ?? "",
|
|
613
|
-
standing: data.artifact?.standing ?? ""
|
|
614
|
-
},
|
|
615
|
-
missing_inputs: data.missing_inputs ?? []
|
|
616
|
-
};
|
|
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);
|
|
617
812
|
}
|
|
618
|
-
var nomos = { can };
|
|
619
813
|
// Annotate the CommonJS export names for ESM import in node:
|
|
620
814
|
0 && (module.exports = {
|
|
621
815
|
Nomos,
|
|
@@ -623,12 +817,15 @@ var nomos = { can };
|
|
|
623
817
|
NomosAuthenticationError,
|
|
624
818
|
NomosAuthorizationError,
|
|
625
819
|
NomosError,
|
|
820
|
+
NomosIssuerNotTrustedError,
|
|
626
821
|
NomosNetworkError,
|
|
627
822
|
NomosNotFoundError,
|
|
823
|
+
NomosPaymentRequiredError,
|
|
628
824
|
NomosRateLimitError,
|
|
629
825
|
NomosValidationError,
|
|
630
826
|
NomosWebhookSignatureError,
|
|
631
827
|
can,
|
|
632
|
-
nomos
|
|
828
|
+
nomos,
|
|
829
|
+
verifyChain
|
|
633
830
|
});
|
|
634
831
|
//# sourceMappingURL=index.cjs.map
|