Skip to content
Integra Protocol

Discovery

The discovery document a seller publishes at /.well-known/legal-context.json, and how emit and parse write and read it.

A seller can publish a discovery document (LCP §2): one small JSON object, served at /.well-known/legal-context.json on the seller's host, that names the seller's terms document and how to reach the seller. Because the path is fixed, an agent finds it from the host name alone.

The discovery document is not an ATR. It describes the seller's standing terms and is the same for every request; an ATR is the record of one transaction, and its hash rides in that transaction's payment. Nothing in a pairing reads the discovery document.

Import the pieces from @integraledger/lcp/discovery:

ExportWhat it is
emit(document)The document's bytes: its members checked and written in a fixed order, or a refusal.
parse(bytes)The document read from bytes, with the names of the members it ignored, or a refusal.
WELL_KNOWN_PATH"/.well-known/legal-context.json"
MAX_DOCUMENT_BYTES65536: the largest document either function accepts.
LegalContextDocumentThe document's type.

Neither function throws or does any I/O. Serving the bytes, and fetching them, are yours.

The members

These are the members LCP §2.4 and LCP §2.5 define, in the order their tables list them. Their meanings are the specification's; the last column is what emit and parse check.

MemberTypeChecked
termsstringRequired. An absolute https URL that meets the link rule.
termsFormatstringNot empty.
atrHashstringThe digest of the document at terms (LCP §2.5): 0x and 64 hex digits, in either case. It is written in lowercase.
acceptanceRequiredbooleanA boolean.
disputeResolutionobjectIts members method, jurisdiction, contact, clauseId, source and catalog, in that order, each a non-empty string. clauseId is sha256:0x and 64 hex digits.
returnsstringNot empty.
contactobjectIts members legal and technical, each a non-empty string.
apistringNot empty.

Every member other than terms is optional. An absent member is left out, never written as null.

Writing the document

emit(document) checks each member, then writes the members in the table's order, with no whitespace, strings escaped as JSON.stringify escapes them, and atrHash in lowercase. A member the table does not name is not written. The result is at most MAX_DOCUMENT_BYTES.

This example writes the reference document from the package's vectors, whose atrHash is SHA-256 of "abc" given in upper case:

import {  } from "@integraledger/lcp";
import { , , type LegalContextDocument } from "@integraledger/lcp/discovery";

const : LegalContextDocument = {
  : "https://seller.example/terms/v3.md",
  : "markdown",
  : "0xBA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD",
  : false,
  : { : "legal@seller.example" },
};

const  = ();
if ("refused" in ) throw new (.);

.();
.(new ().());
.(., "bytes");
.(`ETag: "${(await ()).(2)}"`);
/.well-known/legal-context.json
{"terms":"https://seller.example/terms/v3.md","termsFormat":"markdown","atrHash":"0xba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad","acceptanceRequired":false,"contact":{"legal":"legal@seller.example"}}
220 bytes
ETag: "027d93681365deb2f485c4c34f7b0da7de8741db90a9fa6ea2437aa70bebd31a"

Serve those bytes at WELL_KNOWN_PATH. The same input always gives the same bytes, so a strong ETag over them is stable: vectors/discovery.json fixes it as the SHA-256 in hex, in double quotes.

Reading a document

parse(bytes) reads what a seller served. It accepts one JSON object of at most MAX_DOCUMENT_BYTES, in well-formed UTF-8 without a byte-order mark, nested at most 64 levels deep, and checks each member as the table says. Where a member appears twice, the last one is read, as JSON.parse reads it.

A member the table does not name, at the top level or inside disputeResolution or contact, is left out of document and named in ignored by its dotted path, so you can see what the seller sent that the specification does not define.

import {  } from "@integraledger/lcp/discovery";

const  = new ().(
  .({
    : "https://seller.example/terms/v3.md",
    : "0xBA7816BF8F01CFEA414140DE5DAE2223B00361A396177A9CB410FF61F20015AD",
    : { : "legal@seller.example", : "+1 555 0100" },
    "x-region": "eu",
  }),
);

const  = ();
if ("refused" in ) throw new (.);
.(.);
.(.);

const  = (new ().('{"terms":"http://seller.example/terms/v3.md"}'));
.("refused" in  ? . : "read");
{
  terms: 'https://seller.example/terms/v3.md',
  atrHash: '0xba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',
  contact: { legal: 'legal@seller.example' }
}
[ 'contact.phone', 'x-region' ]
discovery/terms-not-https

emit over the document that parse returns writes it in the fixed order, so every reader of one document can hold the same bytes.

Refusals

CodeWhen
discovery/too-largeThe bytes, or the document emit would write, exceed MAX_DOCUMENT_BYTES.
discovery/not-json-objectThe bytes are not one JSON object: not JSON, not UTF-8, a byte-order mark, an array, or nested more than 64 levels. emit gives it for an input that is not an object.
discovery/terms-missingterms is absent.
discovery/terms-not-httpsterms is not an absolute https URL that meets the link rule.
discovery/atr-hash-malformedatrHash is not 0x and 64 hex digits.
discovery/clause-id-malformeddisputeResolution.clauseId is not sha256:0x and 64 hex digits.
discovery/member-malformed/<path>The member at <path> has the wrong type, or is an empty string: for example discovery/member-malformed/contact.legal.

Fetching it

The fetch is yours. Bound it as you bound any fetch of a seller's bytes: one GET to https://<host>/.well-known/legal-context.json, a deadline, and a body cancelled once it passes MAX_DOCUMENT_BYTES. Then pass the bytes to parse.

Next

  • The ATR: the per-transaction record a payment is bound to.
  • Vectors: vectors/discovery.json fixes what emit writes and what parse reads or refuses, including every example of LCP §2.7.
Edit on GitHub

Last updated on

On this page