@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/dist/index.d.ts CHANGED
@@ -43,9 +43,35 @@ interface DetectContradictionsParams {
43
43
  }
44
44
  type Verdict = 'approved' | 'rejected' | 'escalated';
45
45
  type CanVerdict = 'AUTHORIZED' | 'DENIED' | 'ESCALATED';
46
+ /** Structured form of "who stands behind this artifact" — role, statement, and when, not just
47
+ * a name buried in `authority.standing`'s free text. Present only when a live (unrevoked,
48
+ * seal-bound) attestation exists on the exact sealed version that answered. Exchange rail only —
49
+ * the private-artifact and chain-of-trust rails have no equivalent concept today. */
50
+ interface AttestationInfo {
51
+ attester_name: string;
52
+ attester_org_id: string;
53
+ role: string;
54
+ statement: string;
55
+ attested_at: string;
56
+ }
46
57
  interface CanParams {
47
- /** Exchange artifact's standard name — slug (optionally ".nomos"-suffixed) or raw id, e.g. "eu-ai-act.nomos" */
48
- authority: string;
58
+ /** A public Exchange artifact's standard name — slug (optionally ".nomos"-suffixed) or raw id,
59
+ * e.g. "eu-ai-act.nomos". Keyless. Mutually exclusive with `artifact_id`. */
60
+ authority?: string;
61
+ /** Your own artifact's id (from Studio or the Exchange). Requires an API key — only valid via
62
+ * `client.can()` on an authenticated `Nomos` instance, not the free `can()` export. Mutually
63
+ * exclusive with `authority`. */
64
+ artifact_id?: string;
65
+ /** Chain-of-trust rail (PROTOTYPE, not a numbered spec): a self-contained sealed artifact you
66
+ * received directly — from an agent, another system, anywhere — that NOMOS has never seen or
67
+ * registered. Requires `key_certs` and `root_public_key_pem` alongside it. Keyless, works on
68
+ * both `nomos.can()` and `client.can()`. */
69
+ artifact?: unknown;
70
+ /** The key certificates presented alongside `artifact` — order never affects the result. */
71
+ key_certs?: KeyCertificate[];
72
+ /** The root public key (PEM) YOU have independently decided to trust. There is deliberately no
73
+ * default — see the chain-of-trust design note for why. Required alongside `artifact`. */
74
+ root_public_key_pem?: string;
49
75
  /** Optional human-readable label for what's being asked (recorded in the transcript) */
50
76
  action?: string;
51
77
  /** The facts to evaluate. Either pass this explicitly, or spread the facts flat alongside
@@ -63,24 +89,98 @@ interface CanOptions {
63
89
  interface CanResult {
64
90
  /** True only for an AUTHORIZED verdict. DENIED and ESCALATED are both `false`. */
65
91
  allowed: boolean;
66
- /** The authority's own verdict, verbatim. */
92
+ /** The authority's own verdict, verbatim, in ONE consistent vocabulary regardless of which
93
+ * rail answered it — the public Exchange or your own authenticated artifact. */
67
94
  verdict: CanVerdict;
68
95
  /** Human-readable explanation of the verdict. */
69
96
  reason: string;
70
97
  /** The rule id that decided it, if any. */
71
98
  rule: string | null;
72
- /** Permanent, addressable transcript id — open it any time. */
73
- query_id: string;
74
- /** Relative URL of the transcript. */
75
- url: string;
99
+ /** Permanent, addressable transcript id — open it any time. Present only when the Exchange rail
100
+ * answered (it mints a permanent public transcript for every query). Absent, not faked, when
101
+ * your own authenticated artifact answered instead — see `audit` for that rail's provenance. */
102
+ query_id?: string;
103
+ /** Relative URL of the transcript. Same availability as `query_id`. */
104
+ url?: string;
76
105
  /** Which authority answered, and its standing. */
77
106
  authority: {
78
107
  id: string;
79
108
  title: string;
80
109
  standing: string;
110
+ attestation?: AttestationInfo;
81
111
  };
112
+ /** The authority's own declared version — its sealed meta.version, not administrative
113
+ * metadata that can drift from what's actually embedded in the artifact. Present whenever the
114
+ * answering artifact declares one; omitted (not a guess) when it doesn't. */
115
+ version?: string;
82
116
  /** On ESCALATED: the facts the authority still needs to reach a verdict. */
83
117
  missing_inputs: string[];
118
+ /** Notify/log/audit/flag-style duties that fired alongside the verdict — e.g. "must inform the
119
+ * affected person" — distinct from the verdict itself: an obligation can accompany an
120
+ * AUTHORIZED result, it doesn't only apply to denials. Present on both rails; omitted (not an
121
+ * empty array) when none fired, since "no obligations" and "we didn't check" should stay
122
+ * distinguishable. */
123
+ obligations?: Array<{
124
+ kind: string;
125
+ rule_id: string | null;
126
+ rule_name: string | null;
127
+ reason?: string;
128
+ }>;
129
+ /** RECOMMENDED maximum seconds to hold this verdict before re-querying or re-checking the
130
+ * issuer's revocation list (NOMOS-SPEC-006 §6.2). Advisory; absent when the artifact isn't
131
+ * structurally revocable on this deployment — treat absence as "unknown," not "safe forever." */
132
+ max_age?: number;
133
+ /** Present only when your own authenticated artifact answered — that rail's own provenance
134
+ * (a hash you can store for compliance), not a permanent public transcript like `query_id`. */
135
+ audit?: {
136
+ hash: string;
137
+ timestamp: string;
138
+ };
139
+ /** NOMOS-SPEC-006 — true only when the issuer has revoked this exact sealed version; forces
140
+ * `verdict` to DENIED regardless of what the artifact's own rules would otherwise have said.
141
+ * Omitted (not `false`) when revocation doesn't structurally apply on this deployment — "no
142
+ * revocation" and "couldn't check" must never look identical. */
143
+ revoked?: boolean;
144
+ /** Present only alongside `revoked: true` — the issuer's stated reason. */
145
+ revocation_reason?: string;
146
+ }
147
+ type ChainVerdictDecision = 'ALLOWED' | 'ISSUER_NOT_RECOGNIZED' | 'KEY_REVOKED' | 'SEAL_INVALID' | 'MALFORMED';
148
+ interface KeyCertificate {
149
+ parent_kid: string;
150
+ child_kid: string;
151
+ child_public_key_pem: string;
152
+ scope?: string;
153
+ issued_at: string;
154
+ expires_at: string;
155
+ algorithm: 'Ed25519';
156
+ signature: string;
157
+ }
158
+ interface VerifyChainParams {
159
+ /** The sealed .nomos artifact being presented. */
160
+ artifact: unknown;
161
+ /** The set of key certificates presented alongside it — order never affects the result. */
162
+ key_certs: KeyCertificate[];
163
+ /** The root public key (PEM) the CALLER has independently decided to trust. There is
164
+ * deliberately no default — see the chain-of-trust design note for why. */
165
+ root_public_key_pem: string;
166
+ }
167
+ interface VerifyChainOptions {
168
+ /** Override the API host (defaults to the public NOMOS platform). */
169
+ baseUrl?: string;
170
+ signal?: AbortSignal;
171
+ }
172
+ interface VerifyChainResult {
173
+ decision: ChainVerdictDecision;
174
+ /** Present only when decision === 'ALLOWED'. */
175
+ leaf_kid?: string;
176
+ /** Present only when decision === 'ALLOWED' — the resolved kid path from root to leaf. */
177
+ path?: string[];
178
+ /** Present only when decision === 'ISSUER_NOT_RECOGNIZED'. */
179
+ reason?: string;
180
+ /** Present only when decision === 'KEY_REVOKED'. */
181
+ revoked_kid?: string;
182
+ /** Human-readable detail for any non-ALLOWED decision. */
183
+ detail?: string;
84
184
  }
85
185
  interface VerifyDecisionResult {
86
186
  /** Unique request ID — include in support tickets */
@@ -239,7 +339,7 @@ interface NomosRawResponse {
239
339
  type WithLastResponse<T> = T & {
240
340
  lastResponse: NomosRawResponse;
241
341
  };
242
- type NomosWebhookEventType = 'artifact.sealed' | 'artifact.published' | 'decision.verified' | 'decision.escalated' | 'exchange.purchased' | 'exchange.reviewed' | 'drift.detected';
342
+ type NomosWebhookEventType = 'artifact.sealed' | 'artifact.published' | 'artifact.revoked' | 'decision.verified' | 'decision.escalated' | 'exchange.purchased' | 'exchange.reviewed' | 'drift.detected';
243
343
  interface NomosWebhookEvent {
244
344
  id: string;
245
345
  type: NomosWebhookEventType;
@@ -409,7 +509,7 @@ declare class Webhooks {
409
509
  verifySignature(payload: string | Uint8Array, signature: string, secret: string, toleranceSeconds?: number): Promise<boolean>;
410
510
  }
411
511
 
412
- type NomosErrorCode = 'authentication_error' | 'authorization_error' | 'rate_limit_error' | 'validation_error' | 'not_found' | 'api_error' | 'network_error' | 'webhook_signature_error';
512
+ type NomosErrorCode = 'authentication_error' | 'authorization_error' | 'rate_limit_error' | 'validation_error' | 'not_found' | 'api_error' | 'network_error' | 'webhook_signature_error' | 'payment_required' | 'issuer_not_trusted';
413
513
  declare class NomosError extends Error {
414
514
  readonly code: NomosErrorCode;
415
515
  readonly status?: number;
@@ -442,6 +542,27 @@ declare class NomosNetworkError extends NomosError {
442
542
  declare class NomosWebhookSignatureError extends NomosError {
443
543
  constructor(message: string);
444
544
  }
545
+ /** can() asked about a paid authority the caller hasn't purchased or licensed. Distinct from a
546
+ * generic 403 on purpose: this isn't "you're not allowed," it's "here's exactly what unlocks
547
+ * it" — price and a URL to complete the purchase. NOMOS never auto-charges on your behalf;
548
+ * this is the seam where a caller decides to buy, not where a payment silently happens. */
549
+ declare class NomosPaymentRequiredError extends NomosError {
550
+ readonly priceUsd: number;
551
+ readonly purchaseUrl: string;
552
+ constructor(message: string, priceUsd: number, purchaseUrl: string, requestId?: string);
553
+ }
554
+ /** can()'s chain-of-trust rail couldn't establish that the presented artifact is trustworthy —
555
+ * an unrecognized issuer, a revoked key, a tampered seal, or a malformed presentation. This is
556
+ * deliberately NOT a DENIED verdict: "the rules say no" and "we never got far enough to ask the
557
+ * rules anything" are different claims, and collapsing them would tell the caller the wrong
558
+ * thing to do next (re-presenting a better chain can fix the first case; it can never fix a
559
+ * genuinely tampered artifact). `reason` is the underlying chain decision
560
+ * (ISSUER_NOT_RECOGNIZED / KEY_REVOKED / SEAL_INVALID / MALFORMED) for callers that need to
561
+ * distinguish them. */
562
+ declare class NomosIssuerNotTrustedError extends NomosError {
563
+ readonly reason: string;
564
+ constructor(message: string, reason: string, requestId?: string);
565
+ }
445
566
 
446
567
  type errors_NomosAPIError = NomosAPIError;
447
568
  declare const errors_NomosAPIError: typeof NomosAPIError;
@@ -452,10 +573,14 @@ declare const errors_NomosAuthorizationError: typeof NomosAuthorizationError;
452
573
  type errors_NomosError = NomosError;
453
574
  declare const errors_NomosError: typeof NomosError;
454
575
  type errors_NomosErrorCode = NomosErrorCode;
576
+ type errors_NomosIssuerNotTrustedError = NomosIssuerNotTrustedError;
577
+ declare const errors_NomosIssuerNotTrustedError: typeof NomosIssuerNotTrustedError;
455
578
  type errors_NomosNetworkError = NomosNetworkError;
456
579
  declare const errors_NomosNetworkError: typeof NomosNetworkError;
457
580
  type errors_NomosNotFoundError = NomosNotFoundError;
458
581
  declare const errors_NomosNotFoundError: typeof NomosNotFoundError;
582
+ type errors_NomosPaymentRequiredError = NomosPaymentRequiredError;
583
+ declare const errors_NomosPaymentRequiredError: typeof NomosPaymentRequiredError;
459
584
  type errors_NomosRateLimitError = NomosRateLimitError;
460
585
  declare const errors_NomosRateLimitError: typeof NomosRateLimitError;
461
586
  type errors_NomosValidationError = NomosValidationError;
@@ -463,7 +588,7 @@ declare const errors_NomosValidationError: typeof NomosValidationError;
463
588
  type errors_NomosWebhookSignatureError = NomosWebhookSignatureError;
464
589
  declare const errors_NomosWebhookSignatureError: typeof NomosWebhookSignatureError;
465
590
  declare namespace errors {
466
- export { errors_NomosAPIError as NomosAPIError, errors_NomosAuthenticationError as NomosAuthenticationError, errors_NomosAuthorizationError as NomosAuthorizationError, errors_NomosError as NomosError, type errors_NomosErrorCode as NomosErrorCode, errors_NomosNetworkError as NomosNetworkError, errors_NomosNotFoundError as NomosNotFoundError, errors_NomosRateLimitError as NomosRateLimitError, errors_NomosValidationError as NomosValidationError, errors_NomosWebhookSignatureError as NomosWebhookSignatureError };
591
+ export { errors_NomosAPIError as NomosAPIError, errors_NomosAuthenticationError as NomosAuthenticationError, errors_NomosAuthorizationError as NomosAuthorizationError, errors_NomosError as NomosError, type errors_NomosErrorCode as NomosErrorCode, errors_NomosIssuerNotTrustedError as NomosIssuerNotTrustedError, errors_NomosNetworkError as NomosNetworkError, errors_NomosNotFoundError as NomosNotFoundError, errors_NomosPaymentRequiredError as NomosPaymentRequiredError, errors_NomosRateLimitError as NomosRateLimitError, errors_NomosValidationError as NomosValidationError, errors_NomosWebhookSignatureError as NomosWebhookSignatureError };
467
592
  }
468
593
 
469
594
  declare class Nomos {
@@ -474,16 +599,50 @@ declare class Nomos {
474
599
  /** Whether this instance is using a live or test API key. */
475
600
  readonly mode: 'live' | 'test';
476
601
  static readonly errors: typeof errors;
602
+ private readonly http;
477
603
  constructor(apiKey: string, config?: NomosConfig);
604
+ /**
605
+ * "Can I do this?" — the one verb, now covering your own artifacts too. Pass `authority` for a
606
+ * public Exchange artifact, or `artifact_id` for your own (Studio or the Exchange) — same
607
+ * return shape either way, so calling code never needs to know which rail answered.
608
+ *
609
+ * @example
610
+ * const client = new Nomos('nms_live_...');
611
+ * const verdict = await client.can({ artifact_id: 'loan_approval_v1', credit_score: 720 });
612
+ */
613
+ can(params: CanParams, opts?: CanOptions): Promise<CanResult>;
478
614
  }
479
615
 
480
616
  declare function can(params: CanParams, opts?: CanOptions): Promise<CanResult>;
481
617
  /**
482
618
  * Ready-to-use, keyless entry point matching the canonical `nomos.can(...)` form.
483
- * For authenticated, key-scoped operations use the `Nomos` class instead.
619
+ * For paid artifacts or your own artifacts, use `client.can(...)` on an authenticated `Nomos`
620
+ * instance instead — same function name, same verdict shape, the only difference is which rail
621
+ * answers and what it can unlock.
484
622
  */
485
623
  declare const nomos: {
486
624
  can: typeof can;
487
625
  };
488
626
 
489
- export { type Artifact, type ArtifactList, type ArtifactSchema, type ArtifactSchemaField, type CanOptions, type CanParams, type CanResult, type CanVerdict, type Contradiction, type DetectContradictionsParams, type DetectContradictionsResult, type GenerateGovernanceParams, type GenerateGovernanceResult, type ListArtifactsParams, Nomos, NomosAPIError, NomosAuthenticationError, NomosAuthorizationError, type NomosConfig, NomosError, type NomosErrorCode, NomosNetworkError, NomosNotFoundError, NomosRateLimitError, type NomosRawResponse, NomosValidationError, type NomosWebhookEvent, type NomosWebhookEventType, NomosWebhookSignatureError, type RequestOptions, type Verdict, type VerifyDecisionParams, type VerifyDecisionResult, type WithLastResponse, can, nomos };
627
+ /**
628
+ * Chain-of-trust verification (PROTOTYPE — not a numbered spec). Checks whether a presented
629
+ * artifact's signing key resolves, through the presented key certificates, back to a root you
630
+ * have independently decided to trust — entirely server-side against nomos-chain.ts's pure
631
+ * verifier, with no account or API key required (keyless, matching `nomos.can()`'s precedent).
632
+ *
633
+ * There is no default root anywhere in this call — `root_public_key_pem` is required. That's
634
+ * deliberate: it keeps the trust decision with the caller, not with this SDK or the platform.
635
+ *
636
+ * @example
637
+ * import { verifyChain } from '@nomosprotocol/sdk';
638
+ *
639
+ * const result = await verifyChain({
640
+ * artifact: sealedArtifact,
641
+ * key_certs: [rootToIntermediate, intermediateToLeaf],
642
+ * root_public_key_pem: myPinnedRootPem,
643
+ * });
644
+ * if (result.decision === 'ALLOWED') { ... }
645
+ */
646
+ declare function verifyChain(params: VerifyChainParams, opts?: VerifyChainOptions): Promise<VerifyChainResult>;
647
+
648
+ export { type Artifact, type ArtifactList, type ArtifactSchema, type ArtifactSchemaField, type AttestationInfo, type CanOptions, type CanParams, type CanResult, type CanVerdict, type ChainVerdictDecision, type Contradiction, type DetectContradictionsParams, type DetectContradictionsResult, type GenerateGovernanceParams, type GenerateGovernanceResult, type KeyCertificate, type ListArtifactsParams, Nomos, NomosAPIError, NomosAuthenticationError, NomosAuthorizationError, type NomosConfig, NomosError, type NomosErrorCode, NomosIssuerNotTrustedError, NomosNetworkError, NomosNotFoundError, NomosPaymentRequiredError, NomosRateLimitError, type NomosRawResponse, NomosValidationError, type NomosWebhookEvent, type NomosWebhookEventType, NomosWebhookSignatureError, type RequestOptions, type Verdict, type VerifyChainOptions, type VerifyChainParams, type VerifyChainResult, type VerifyDecisionParams, type VerifyDecisionResult, type WithLastResponse, can, nomos, verifyChain };
package/dist/index.js CHANGED
@@ -11,8 +11,10 @@ __export(errors_exports, {
11
11
  NomosAuthenticationError: () => NomosAuthenticationError,
12
12
  NomosAuthorizationError: () => NomosAuthorizationError,
13
13
  NomosError: () => NomosError,
14
+ NomosIssuerNotTrustedError: () => NomosIssuerNotTrustedError,
14
15
  NomosNetworkError: () => NomosNetworkError,
15
16
  NomosNotFoundError: () => NomosNotFoundError,
17
+ NomosPaymentRequiredError: () => NomosPaymentRequiredError,
16
18
  NomosRateLimitError: () => NomosRateLimitError,
17
19
  NomosValidationError: () => NomosValidationError,
18
20
  NomosWebhookSignatureError: () => NomosWebhookSignatureError
@@ -76,6 +78,21 @@ var NomosWebhookSignatureError = class extends NomosError {
76
78
  this.name = "NomosWebhookSignatureError";
77
79
  }
78
80
  };
81
+ var NomosPaymentRequiredError = class extends NomosError {
82
+ constructor(message, priceUsd, purchaseUrl, requestId) {
83
+ super(message, "payment_required", 402, requestId);
84
+ this.name = "NomosPaymentRequiredError";
85
+ this.priceUsd = priceUsd;
86
+ this.purchaseUrl = purchaseUrl;
87
+ }
88
+ };
89
+ var NomosIssuerNotTrustedError = class extends NomosError {
90
+ constructor(message, reason, requestId) {
91
+ super(message, "issuer_not_trusted", 403, requestId);
92
+ this.name = "NomosIssuerNotTrustedError";
93
+ this.reason = reason;
94
+ }
95
+ };
79
96
 
80
97
  // src/util.ts
81
98
  async function sleep(ms) {
@@ -495,6 +512,192 @@ var Webhooks = class {
495
512
  }
496
513
  };
497
514
 
515
+ // src/can.ts
516
+ var CHAIN_PROTOCOL_VERSION = "1";
517
+ var DEFAULT_EXCHANGE_BASE = "https://www.nomosprotocol.com";
518
+ function splitFacts(params) {
519
+ const { authority, artifact_id, action, inputs, ...flat } = params;
520
+ void authority;
521
+ void artifact_id;
522
+ const facts = inputs && typeof inputs === "object" ? inputs : flat;
523
+ return { facts, action };
524
+ }
525
+ function mapHttpError(status, data, requestId) {
526
+ const message = data?.error ?? data?.message ?? `NOMOS query failed (HTTP ${status})`;
527
+ if (data?.code === "PURCHASE_REQUIRED") {
528
+ throw new NomosPaymentRequiredError(message, data.price_usd ?? 0, data.purchase_url ?? "", requestId);
529
+ }
530
+ switch (status) {
531
+ case 401:
532
+ throw new NomosAuthenticationError(message, requestId);
533
+ case 403:
534
+ throw new NomosAuthorizationError(message, requestId);
535
+ case 404:
536
+ throw new NomosNotFoundError(message, requestId);
537
+ case 422:
538
+ case 400:
539
+ throw new NomosValidationError(message, data?.details, requestId);
540
+ case 429:
541
+ throw new NomosRateLimitError(message, void 0, requestId);
542
+ default:
543
+ throw new NomosAPIError(message, status, requestId);
544
+ }
545
+ }
546
+ function normalizeAuthorityQueryResult(data, authority) {
547
+ return {
548
+ allowed: data.verdict === "AUTHORIZED",
549
+ verdict: data.verdict,
550
+ reason: data.verdict_description,
551
+ rule: data.matched_rule_id ?? null,
552
+ query_id: data.query_id,
553
+ url: data.url,
554
+ authority: {
555
+ id: data.artifact?.id ?? authority,
556
+ title: data.artifact?.title ?? "",
557
+ standing: data.artifact?.standing ?? "",
558
+ ...data.artifact?.attestation ? { attestation: data.artifact.attestation } : {}
559
+ },
560
+ ...data.artifact?.version ? { version: data.artifact.version } : {},
561
+ missing_inputs: data.missing_inputs ?? [],
562
+ ...typeof data.max_age === "number" ? { max_age: data.max_age } : {},
563
+ // authority-query.ts already returns { kind, rule_id, rule_name } — the public-safe shape
564
+ // CanResult uses directly.
565
+ ...Array.isArray(data.obligations) && data.obligations.length > 0 ? { obligations: data.obligations } : {},
566
+ // NOMOS-SPEC-006 — present only when the issuer has actually revoked this exact sealed
567
+ // version; omitted (not false) when revocation doesn't structurally apply here, so "checked
568
+ // and clean" and "couldn't check" never look the same to a caller.
569
+ ...data.revoked === true ? { revoked: true, revocation_reason: data.revocation_reason } : {}
570
+ };
571
+ }
572
+ async function canViaExchange(authority, facts, action, opts) {
573
+ const base = (opts?.baseUrl ?? DEFAULT_EXCHANGE_BASE).replace(/\/$/, "");
574
+ const url = `${base}/api/v1/can`;
575
+ let res;
576
+ try {
577
+ res = await fetch(url, {
578
+ method: "POST",
579
+ headers: { "Content-Type": "application/json" },
580
+ body: JSON.stringify({ artifact: authority, action, ...facts }),
581
+ signal: opts?.signal
582
+ });
583
+ } catch (err) {
584
+ if (err?.name === "AbortError") throw new NomosNetworkError("Request aborted");
585
+ throw new NomosNetworkError(err?.message ?? "Network error");
586
+ }
587
+ const requestId = res.headers.get("x-request-id") ?? "";
588
+ const data = await res.json().catch(() => null);
589
+ if (!res.ok) mapHttpError(res.status, data, requestId);
590
+ return normalizeAuthorityQueryResult(data, authority);
591
+ }
592
+ async function canViaExchangeAuthenticated(http, authority, facts, action) {
593
+ const { data } = await http.post("/api/v1/can", { artifact: authority, action, ...facts });
594
+ return normalizeAuthorityQueryResult(data, authority);
595
+ }
596
+ async function canViaArtifact(http, artifactId, facts, action) {
597
+ const isPublic = artifactId.startsWith("pub_");
598
+ const path = isPublic ? "/api/v1/verify" : "/api/v1/verify-decision";
599
+ 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() } };
600
+ const { data: raw } = await http.post(path, body);
601
+ const verdict = raw.verdict === "approved" ? "AUTHORIZED" : raw.verdict === "rejected" ? "DENIED" : raw.outcome === "auto_approved" ? "AUTHORIZED" : raw.outcome === "auto_rejected" ? "DENIED" : "ESCALATED";
602
+ return {
603
+ allowed: raw.allowed ?? verdict === "AUTHORIZED",
604
+ verdict,
605
+ reason: raw.verdict_description ?? "",
606
+ rule: raw.rule?.id ?? null,
607
+ authority: {
608
+ id: raw.artifact_id ?? artifactId,
609
+ title: raw.artifact_name ?? "",
610
+ standing: raw.confidence?.tier ?? ""
611
+ },
612
+ ...raw.artifact_version ? { version: raw.artifact_version } : {},
613
+ missing_inputs: raw.incomplete_inputs?.fields ?? [],
614
+ ...raw.audit ? { audit: { hash: raw.audit.hash ?? raw.audit_hash ?? "", timestamp: raw.audit.timestamp ?? raw.ts ?? (/* @__PURE__ */ new Date()).toISOString() } } : {},
615
+ // This rail's obligations are RuleEffect objects (authority-types.ts) — a richer, INTERNAL
616
+ // shape ({ effect_type, value, provenance: { rule_id, rule_name } }), NOT the same shape the
617
+ // Exchange rail already returns ({ kind, rule_id, rule_name }). Mapped explicitly here rather
618
+ // than passed through raw — the two rails' server-side representations genuinely differ, and
619
+ // assuming they matched would have been the actual bug.
620
+ ...Array.isArray(raw.obligations) && raw.obligations.length > 0 ? { obligations: raw.obligations.map((o) => ({
621
+ kind: o.kind ?? o.value ?? "obligation",
622
+ rule_id: o.rule_id ?? o.provenance?.rule_id ?? null,
623
+ rule_name: o.rule_name ?? o.provenance?.rule_name ?? null,
624
+ ...o.reason ? { reason: o.reason } : {}
625
+ })) } : {}
626
+ };
627
+ }
628
+ async function canViaChain(artifact, keyCerts, rootPublicKeyPem, facts, action, opts) {
629
+ const base = (opts?.baseUrl ?? DEFAULT_EXCHANGE_BASE).replace(/\/$/, "");
630
+ const url = `${base}/api/v1/chain-of-trust/verify`;
631
+ let res;
632
+ try {
633
+ res = await fetch(url, {
634
+ method: "POST",
635
+ headers: { "Content-Type": "application/json" },
636
+ body: JSON.stringify({ version: CHAIN_PROTOCOL_VERSION, artifact, key_certs: keyCerts, root_public_key_pem: rootPublicKeyPem, facts, action }),
637
+ signal: opts?.signal
638
+ });
639
+ } catch (err) {
640
+ if (err?.name === "AbortError") throw new NomosNetworkError("Request aborted");
641
+ throw new NomosNetworkError(err?.message ?? "Network error");
642
+ }
643
+ const requestId = res.headers.get("x-request-id") ?? "";
644
+ const data = await res.json().catch(() => null);
645
+ if (data?.decision === "AUTHORIZED" || data?.decision === "DENIED" || data?.decision === "ESCALATED") {
646
+ return {
647
+ allowed: data.decision === "AUTHORIZED",
648
+ verdict: data.decision,
649
+ reason: data.reason ?? "",
650
+ rule: data.rule ?? null,
651
+ authority: { id: "presented-artifact", title: "", standing: "" },
652
+ // Read directly off the artifact the caller already has — no server round-trip needed for
653
+ // data the caller is already holding.
654
+ ...artifact?.meta?.version ? { version: artifact.meta.version } : {},
655
+ missing_inputs: data.missing_inputs ?? [],
656
+ ...Array.isArray(data.obligations) && data.obligations.length > 0 ? { obligations: data.obligations } : {}
657
+ // Deliberately no query_id/url/audit — this evaluation is stateless by design (the whole
658
+ // point is answering without NOMOS needing to have seen this artifact before); there is no
659
+ // permanent transcript to point to, and faking one would misrepresent what just happened.
660
+ };
661
+ }
662
+ const message = data?.detail ?? `Chain verification failed (HTTP ${res.status})`;
663
+ if (data?.decision === "ISSUER_NOT_RECOGNIZED" || data?.decision === "KEY_REVOKED" || data?.decision === "SEAL_INVALID" || data?.decision === "ALLOWED") {
664
+ throw new NomosIssuerNotTrustedError(message, data.decision, requestId);
665
+ }
666
+ if (data?.decision === "MALFORMED") throw new NomosValidationError(message, data, requestId);
667
+ mapHttpError(res.status, data, requestId);
668
+ }
669
+ async function can(params, opts) {
670
+ if (params.artifact !== void 0) {
671
+ if (!params.key_certs || !params.root_public_key_pem) {
672
+ throw new Error("can(): `artifact` requires `key_certs` and `root_public_key_pem` alongside it.");
673
+ }
674
+ const { facts: facts2, action: action2 } = splitFacts(params);
675
+ return canViaChain(params.artifact, params.key_certs, params.root_public_key_pem, facts2, action2, opts);
676
+ }
677
+ if (params.artifact_id) {
678
+ throw new Error("can(): `artifact_id` requires an API key \u2014 use `client.can(...)` on an authenticated Nomos instance, not the free `can()` export.");
679
+ }
680
+ if (!params.authority || typeof params.authority !== "string") {
681
+ throw new Error("can(): `authority` (an Exchange artifact id or slug), `artifact_id` (via client.can()), or `artifact` (chain-of-trust) is required.");
682
+ }
683
+ const { facts, action } = splitFacts(params);
684
+ return canViaExchange(params.authority, facts, action, opts);
685
+ }
686
+ async function canAuthenticated(http, params, opts) {
687
+ void opts;
688
+ const { facts, action } = splitFacts(params);
689
+ if (params.artifact !== void 0) {
690
+ if (!params.key_certs || !params.root_public_key_pem) {
691
+ throw new Error("can(): `artifact` requires `key_certs` and `root_public_key_pem` alongside it.");
692
+ }
693
+ return canViaChain(params.artifact, params.key_certs, params.root_public_key_pem, facts, action, opts);
694
+ }
695
+ if (params.artifact_id) return canViaArtifact(http, params.artifact_id, facts, action);
696
+ if (params.authority) return canViaExchangeAuthenticated(http, params.authority, facts, action);
697
+ throw new Error("can(): `authority` (a public Exchange artifact), `artifact_id` (your own artifact), or `artifact` (chain-of-trust) is required.");
698
+ }
699
+ var nomos = { can };
700
+
498
701
  // src/nomos.ts
499
702
  var DEFAULT_BASE_URL = "https://nomos.ai";
500
703
  var DEFAULT_MAX_RETRIES = 3;
@@ -510,35 +713,51 @@ var Nomos = class {
510
713
  );
511
714
  }
512
715
  this.mode = apiKey.startsWith("nms_test_") ? "test" : "live";
513
- const http = new HttpClient({
716
+ this.http = new HttpClient({
514
717
  apiKey,
515
718
  baseUrl: (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, ""),
516
719
  maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
517
720
  timeout: config.timeout ?? DEFAULT_TIMEOUT
518
721
  });
519
- this.decisions = new Decisions(http);
520
- this.artifacts = new Artifacts(http);
521
- this.governance = new Governance(http);
722
+ this.decisions = new Decisions(this.http);
723
+ this.artifacts = new Artifacts(this.http);
724
+ this.governance = new Governance(this.http);
522
725
  this.webhooks = new Webhooks();
523
726
  }
727
+ /**
728
+ * "Can I do this?" — the one verb, now covering your own artifacts too. Pass `authority` for a
729
+ * public Exchange artifact, or `artifact_id` for your own (Studio or the Exchange) — same
730
+ * return shape either way, so calling code never needs to know which rail answered.
731
+ *
732
+ * @example
733
+ * const client = new Nomos('nms_live_...');
734
+ * const verdict = await client.can({ artifact_id: 'loan_approval_v1', credit_score: 720 });
735
+ */
736
+ async can(params, opts) {
737
+ return canAuthenticated(this.http, params, opts);
738
+ }
524
739
  };
525
740
 
526
- // src/can.ts
527
- var DEFAULT_EXCHANGE_BASE = "https://www.nomosprotocol.com";
528
- async function can(params, opts) {
529
- const { authority, action, inputs, ...flat } = params;
530
- if (!authority || typeof authority !== "string") {
531
- throw new Error("can(): `authority` (an Exchange artifact id or slug) is required.");
741
+ // src/verify-chain.ts
742
+ var CHAIN_PROTOCOL_VERSION2 = "1";
743
+ var DEFAULT_BASE = "https://www.nomosprotocol.com";
744
+ async function verifyChain(params, opts) {
745
+ if (!params.root_public_key_pem) {
746
+ throw new Error("verifyChain(): `root_public_key_pem` is required \u2014 there is no default root.");
532
747
  }
533
- const facts = inputs && typeof inputs === "object" ? inputs : flat;
534
- const base = (opts?.baseUrl ?? DEFAULT_EXCHANGE_BASE).replace(/\/$/, "");
535
- const url = `${base}/api/exchange/artifacts/${encodeURIComponent(authority)}/query`;
748
+ const base = (opts?.baseUrl ?? DEFAULT_BASE).replace(/\/$/, "");
749
+ const url = `${base}/api/v1/chain-of-trust/verify`;
536
750
  let res;
537
751
  try {
538
752
  res = await fetch(url, {
539
753
  method: "POST",
540
754
  headers: { "Content-Type": "application/json" },
541
- body: JSON.stringify({ inputs: facts, action }),
755
+ body: JSON.stringify({
756
+ version: CHAIN_PROTOCOL_VERSION2,
757
+ artifact: params.artifact,
758
+ key_certs: params.key_certs,
759
+ root_public_key_pem: params.root_public_key_pem
760
+ }),
542
761
  signal: opts?.signal
543
762
  });
544
763
  } catch (err) {
@@ -547,56 +766,31 @@ async function can(params, opts) {
547
766
  }
548
767
  const requestId = res.headers.get("x-request-id") ?? "";
549
768
  const data = await res.json().catch(() => null);
550
- if (!res.ok) {
551
- const message = data?.error ?? data?.message ?? `NOMOS query failed (HTTP ${res.status})`;
552
- switch (res.status) {
553
- case 401:
554
- throw new NomosAuthenticationError(message, requestId);
555
- case 403:
556
- throw new NomosAuthorizationError(message, requestId);
557
- case 404:
558
- throw new NomosNotFoundError(message, requestId);
559
- case 422:
560
- case 400:
561
- throw new NomosValidationError(message, data?.details, requestId);
562
- case 429:
563
- throw new NomosRateLimitError(
564
- message,
565
- res.headers.get("retry-after") ? parseInt(res.headers.get("retry-after"), 10) : void 0,
566
- requestId
567
- );
568
- default:
569
- throw new NomosAPIError(message, res.status, requestId);
570
- }
769
+ if (data && typeof data.decision === "string") {
770
+ return data;
571
771
  }
572
- return {
573
- allowed: data.verdict === "AUTHORIZED",
574
- verdict: data.verdict,
575
- reason: data.verdict_description,
576
- rule: data.matched_rule_id ?? null,
577
- query_id: data.query_id,
578
- url: data.url,
579
- authority: {
580
- id: data.artifact?.id ?? authority,
581
- title: data.artifact?.title ?? "",
582
- standing: data.artifact?.standing ?? ""
583
- },
584
- missing_inputs: data.missing_inputs ?? []
585
- };
772
+ const message = data?.error ?? `Chain verification request failed (HTTP ${res.status})`;
773
+ if (res.status === 429) {
774
+ throw new NomosRateLimitError(message, res.headers.get("retry-after") ? parseInt(res.headers.get("retry-after"), 10) : void 0, requestId);
775
+ }
776
+ if (res.status === 400) throw new NomosValidationError(message, data?.details, requestId);
777
+ throw new NomosAPIError(message, res.status, requestId);
586
778
  }
587
- var nomos = { can };
588
779
  export {
589
780
  Nomos,
590
781
  NomosAPIError,
591
782
  NomosAuthenticationError,
592
783
  NomosAuthorizationError,
593
784
  NomosError,
785
+ NomosIssuerNotTrustedError,
594
786
  NomosNetworkError,
595
787
  NomosNotFoundError,
788
+ NomosPaymentRequiredError,
596
789
  NomosRateLimitError,
597
790
  NomosValidationError,
598
791
  NomosWebhookSignatureError,
599
792
  can,
600
- nomos
793
+ nomos,
794
+ verifyChain
601
795
  };
602
796
  //# sourceMappingURL=index.js.map