Skip to content
Registry StackDocsDevelopment (unreleased)

Registry Discovery is an index

View as Markdown

Registry Discovery is a curated index of public Evidence Gateway and Registry Relay advertisements. It resembles a Yahoo-style directory: the catalog operator decides which public descriptions to index, and an application decides whether to trust and directly use a selected provider.

A small role between providers and applications

Section titled “A small role between providers and applications”

A provider publishes one closed JSON-LD description containing its public service advertisement. The catalog operator keeps an explicit allowlist of those description URLs and performs a bounded, one-shot build. The build records each origin URL, fetched-byte digest, and fetch time beside every indexed service record.

The profile uses a selected set of Data Catalog Vocabulary (DCAT) 3, DCAT-AP 3.0.1, and BRegDCAT-AP terms. Registry Discovery does not claim full DCAT-AP or BRegDCAT-AP conformance. Its offline tooling transforms only this pinned context and evaluates a selected Shapes Constraint Language (SHACL) subset; the service runtime does not perform RDF or SHACL processing.

An index record preserves a service’s public title, description, endpoint URL, roles, jurisdictions, profile identifiers, and product-specific capability identifiers. Evidence Gateway advertisements carry evidence type IDs. Registry Relay advertisements carry semantic class IDs or operation family IDs. A protected-only Relay may intentionally expose neither public capability collection.

The origin fields answer a narrower question than provider trust: which approved URL supplied these bytes for this index revision? They let an application retain provenance during selection. They do not attest that the provider, its endpoint, or its claims are trustworthy.

Registry Discovery is neither a provider federation nor an application gateway. It has no provider registration flow, trust store, authorization decision, credentials, request proxy, procedure model, ranking rule, writable catalog, or mutation route. Its fixed service surface is health, readiness, OpenAPI, service search, and evidence-type resolution.

That boundary keeps responsibility in the native products. Evidence Gateway remains the product that answers a fixed requirement and issues its signed, minimum-disclosure assertion. Registry Relay remains the product that exposes its governed protected read surface. Discovery only helps an application find their public advertisements.

For Evidence Gateway, an application resolves a requirement and jurisdiction to Evidence Type alternatives. Each alternative is an AND-list. The application searches and selects one provider for every required type. For Registry Relay, the application searches the public semantic class and operation family as one correlated tuple, then explicitly selects one record. Discovery preserves the complete Evidence resolution context, Relay tuple, catalog revision, and origin provenance.

After selection, the application validates the selection’s closed structure and capability binding, then applies a synchronous local acceptance policy. Structural validation does not authenticate the origin, establish currentness, or trust the selected service. Successful local acceptance creates an ephemeral accepted service that exposes the native endpoint. The application creates credentials and performs native input and output only after that acceptance.

The same client workflow in Rust, Node.js, and Python

Section titled “The same client workflow in Rust, Node.js, and Python”

The maintained clients expose the same boundary in each language: resolve an Evidence requirement when needed, search with exact filters, convert one exact record into a serializable selection, validate its structure, then apply synchronous adopter-owned acceptance. Node.js applications take the discovery namespace of @registrystack/client; Python applications take the discovery namespace of registry-stack-client, which imports as registry_client; Rust applications take the registry-discovery-client crate.

const {
DiscoveryClient,
acceptSelection,
renewUnchangedSelection,
selectEvidenceAlternative,
selectEvidenceService,
validateSelectionStructure,
} = require('@registrystack/client').discovery;
const expectedEvidence = {
serviceKind: 'evidence',
serviceId: 'urn:example:service:evidence',
endpointUrl: 'https://evidence.example/',
legalIssuerId: 'urn:example:issuer',
technicalProviderId: 'urn:example:provider',
jurisdictions: ['urn:example:jurisdiction'],
conformsTo: ['urn:example:evidence-profile'],
evidenceTypeIds: ['urn:example:evidence-type'],
matchedCapability: { kind: 'evidence-type', id: 'urn:example:evidence-type' },
evidenceResolution: {
requirementId: 'urn:example:requirement',
jurisdiction: 'urn:example:jurisdiction',
mappingRevision: `sha256:${'a'.repeat(64)}`,
evidenceTypeListId: 'urn:example:evidence-type-list',
evidenceTypeIds: ['urn:example:evidence-type'],
mappingId: 'urn:example:mapping',
mappingAuthorityId: 'urn:example:mapping-authority',
},
};
function sameOrderedStrings(actual, expected) {
return Array.isArray(actual)
&& actual.length === expected.length
&& actual.every((value, index) => value === expected[index]);
}
function acceptsExpectedEvidence(candidate) {
const actualResolution = candidate.evidenceResolution;
const expectedResolution = expectedEvidence.evidenceResolution;
return candidate.serviceKind === expectedEvidence.serviceKind
&& candidate.serviceId === expectedEvidence.serviceId
&& candidate.endpointUrl === expectedEvidence.endpointUrl
&& candidate.legalIssuerId === expectedEvidence.legalIssuerId
&& candidate.technicalProviderId === expectedEvidence.technicalProviderId
&& sameOrderedStrings(candidate.jurisdictions, expectedEvidence.jurisdictions)
&& sameOrderedStrings(candidate.conformsTo, expectedEvidence.conformsTo)
&& sameOrderedStrings(candidate.evidenceTypeIds, expectedEvidence.evidenceTypeIds)
&& candidate.matchedCapability.kind === expectedEvidence.matchedCapability.kind
&& candidate.matchedCapability.id === expectedEvidence.matchedCapability.id
&& actualResolution !== undefined
&& actualResolution.requirementId === expectedResolution.requirementId
&& actualResolution.jurisdiction === expectedResolution.jurisdiction
&& actualResolution.mappingRevision === expectedResolution.mappingRevision
&& actualResolution.evidenceTypeListId === expectedResolution.evidenceTypeListId
&& sameOrderedStrings(actualResolution.evidenceTypeIds, expectedResolution.evidenceTypeIds)
&& actualResolution.mappingId === expectedResolution.mappingId
&& actualResolution.mappingAuthorityId === expectedResolution.mappingAuthorityId;
}
function acceptFreshEvidence(previous, fresh) {
const checkedFresh = validateSelectionStructure(fresh);
const candidate = previous === undefined
? checkedFresh
: renewUnchangedSelection(validateSelectionStructure(previous), checkedFresh);
return acceptSelection(candidate, acceptsExpectedEvidence);
}
const discovery = new DiscoveryClient('https://discovery.example/');
const resolved = await discovery.resolveEvidenceTypes({
requirementId: 'urn:example:requirement',
jurisdiction: 'urn:example:jurisdiction',
});
const context = selectEvidenceAlternative(resolved, 'urn:example:evidence-type-list');
for (const evidenceTypeId of context.evidenceTypeIds) {
const results = await discovery.searchEvidenceServices({
evidenceTypeId,
...(context.jurisdiction ? { jurisdiction: context.jurisdiction } : {}),
});
if (results.items.length !== 1) {
throw new Error('select one reviewed provider explicitly');
}
const selection = selectEvidenceService(results, {
recordId: results.items[0].recordId,
evidenceTypeId,
resolution: context,
});
const accepted = acceptFreshEvidence(undefined, selection);
// Construct the native Evidence client, credentials, and request only now,
// using accepted.endpointUrl and adopter-owned native configuration.
console.log(accepted.selection.matchedCapability.id, accepted.endpointUrl);
}
from registry_client.discovery import (
DiscoveryClient,
accept_selection,
renew_unchanged_selection,
select_relay_service,
validate_selection_structure,
)
from registry_client.relay import RelayClient
EXPECTED_RELAY = {
"serviceKind": "relay",
"serviceId": "urn:example:service:relay",
"endpointUrl": "https://relay.example/",
"operatorId": "urn:example:operator",
"registryAuthorityId": "urn:example:registry-authority",
"jurisdictions": ["urn:example:jurisdiction"],
"conformsTo": ["urn:example:relay-profile"],
"semanticClassIds": ["urn:example:registered-business"],
"operationFamilyIds": ["urn:example:consultation-list"],
"relayCapabilityMatch": {
"semanticClassId": "urn:example:registered-business",
"operationFamilyId": "urn:example:consultation-list",
},
}
def relay_acceptance_subject(candidate):
return {key: candidate.get(key) for key in EXPECTED_RELAY}
def accepts_expected_relay(candidate):
return relay_acceptance_subject(candidate) == EXPECTED_RELAY
def accept_fresh_relay(previous, fresh):
checked_fresh = validate_selection_structure(fresh)
candidate = (
checked_fresh
if previous is None
else renew_unchanged_selection(
validate_selection_structure(previous), checked_fresh
)
)
return accept_selection(candidate, accepts_expected_relay)
discovery = DiscoveryClient("https://discovery.example/")
results = discovery.search_relay_services({
"semanticClassId": "urn:example:registered-business",
"operationFamilyId": "urn:example:consultation-list",
})
if len(results["items"]) != 1:
raise RuntimeError("select one reviewed provider explicitly")
selection = select_relay_service(results, {
"recordId": results["items"][0]["recordId"],
"capabilityMatch": {
"semanticClassId": "urn:example:registered-business",
"operationFamilyId": "urn:example:consultation-list",
},
})
accepted = accept_fresh_relay(None, selection)
# For a protected surface, add native authorization only after acceptance.
relay = RelayClient(base_url=accepted.endpoint_url)
page = relay.list_records("businesses", page_size=1)
if page["kind"] != "complete" or not page["value"]["items"]:
raise RuntimeError("Relay returned no records")
record = page["value"]["items"][0]
print(record["recordIdentifier"], record["domainData"])

The application owns the choice because Discovery does not rank results. The structural validator checks the closed response, complete Evidence alternative or Relay tuple, and capability binding. It does not decide whether the selected origin, issuer, operator, endpoint, or capability is trusted or current. The acceptance callback is synchronous, supplied by the application, and receives no credential or native client. A successful callback creates the ephemeral accepted service used by native client configuration. Native definitions and local policy still supply Evidence purpose, audience, issuer and provider identity, configuration revision, selectors, expected outputs, and Relay resource and operation identifiers.

Persist the plain selection when an application needs an offline handoff. The accepted wrapper is ephemeral and is not a persistence format. Loading a selection and passing structural validation proves only that the saved data still has the closed shape and capability binding. The saved data remains inert until the application applies its current local acceptance policy. Neither loading nor local acceptance establishes that the provider still advertises the service.

Currentness requires an online renewal. For Evidence Gateway, the application re-resolves the requirement and jurisdiction, explicitly chooses an alternative, re-searches every Evidence Type, and explicitly reselects each provider. For Registry Relay, it re-searches the correlated semantic class and operation family, then explicitly reselects the provider. Node.js then compares the old and freshly selected values with renewUnchangedSelection; Python uses renew_unchanged_selection; Rust uses renew_unchanged_service_selection. The comparison allows new fetch provenance and a new global catalog revision for an otherwise unchanged service. It refuses a changed identity, endpoint, role, jurisdiction, profile, capability, origin, mapping, or resolution context. A refusal requires a new explicit local decision rather than automatic acceptance. An unchanged renewal still passes through local acceptance before credentials or native input and output.

The provider maintains one public description URL. The catalog operator maintains a small explicit origins file, any evidence-type mappings, and an intentional build-and-restart loop. The application maintains native provider trust where it already belongs. No component must synchronize a central provider registration database, shared credentials, or a proxy policy.