@percayso/identity-contracts 0.1.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.
@@ -0,0 +1,144 @@
1
+ /**
2
+ * The Citizen: the three statuses of §5.2, what a Citizen record may carry, and the
3
+ * witness required before one can exist at all.
4
+ *
5
+ * THERE IS NO `pending` CITIZEN, and this file is where that sentence stops being
6
+ * prose. §5.2: "Citizen states are `active`, `suspended` and `closed`. **There is no
7
+ * `pending` Citizen** — pending belongs to an Application." §15's `citizens` row
8
+ * says the same from the other side, forbidding a generic pending state on
9
+ * `applications` too. So the union omits it — a `switch` over `CitizenStatus` that
10
+ * adds a `pending` arm does not compile — and `parseCitizenStatus` refuses the
11
+ * string by name, with a message that says where pending actually belongs. The
12
+ * type cannot express it and the decoder will not accept it; either alone would
13
+ * leave the other half open.
14
+ *
15
+ * THE SECOND INVARIANT IS THAT A CITIZEN CANNOT BE CREATED WITHOUT AN APPROVAL.
16
+ * §14.4: "No route, script, production fixture or flag creates a Citizen outside an
17
+ * Application." `CitizenCreation` requires an `ApprovedApplication`, whose `state`
18
+ * is the literal `"approved"` — so there is no Citizen-shaped value a caller can
19
+ * assemble from an Application in any other state, and no optional field that could
20
+ * be left out. `parseCitizenCreation` refuses the same thing at runtime.
21
+ *
22
+ * AND THE THIRD IS THAT A CITIZEN RECORD HOLDS NO MONEY. §15's `citizens` row must
23
+ * not hold "any balance, wallet balance or workspace role", and §11.3 is blunter:
24
+ * coordination state "is operational belief, never the economic fact". There is no
25
+ * balance field here, no wallet field and no role field, and `readClosedRecord`
26
+ * refuses a caller who sends one rather than dropping it silently — a dropped field
27
+ * is a field somebody adds to the type next week to stop it being dropped.
28
+ *
29
+ * WHAT IS ABSENT BY DESIGN. A Citizen record does not say whether a Citizen Space
30
+ * exists. §5.2: "An approved Citizen whose Citizen Space has not yet been created is
31
+ * an ordinary `active` Citizen with outstanding onboarding coordination. They are
32
+ * not a lesser kind of Citizen." A flag here would create the lesser kind, so the
33
+ * coordination state lives in `provisioning.ts` where it cannot be mistaken for a
34
+ * property of the person.
35
+ */
36
+ import { parseApprovedApplication, } from "./application.js";
37
+ import { parseCommunityEmailAddress, parseDisplayHandle, readCitizenId, readVerificationCaseId, } from "./ids.js";
38
+ import { readClosedRecord, readString, refuseValue } from "./parse.js";
39
+ /** Every Citizen status, in §5.2's order. */
40
+ export const CITIZEN_STATUSES = Object.freeze([
41
+ "active",
42
+ "suspended",
43
+ "closed",
44
+ ]);
45
+ /**
46
+ * A Citizen status from a wire value, or a refusal.
47
+ *
48
+ * `pending` gets its own refusal rather than falling through the general one. The
49
+ * general message would say a value was outside a closed union, which is true and
50
+ * unhelpful; this one says where pending belongs, because a caller sending it has
51
+ * almost certainly read an Application state and written it into a Citizen field.
52
+ */
53
+ export const parseCitizenStatus = (value, field = "status") => {
54
+ if (value === "pending") {
55
+ refuseValue(field, "must not be pending. There is no pending Citizen — pending belongs to an Application (§5.2)");
56
+ }
57
+ if (typeof value !== "string" ||
58
+ !CITIZEN_STATUSES.includes(value)) {
59
+ refuseValue(field, `must be one of the ${CITIZEN_STATUSES.length} Citizen statuses of §5.2`);
60
+ }
61
+ return value;
62
+ };
63
+ const CITIZEN_RECORD_FIELDS = Object.freeze([
64
+ "citizenId",
65
+ "status",
66
+ "approvedVerificationCaseId",
67
+ "assuranceProfileVersion",
68
+ "verificationMethod",
69
+ "createdAt",
70
+ ]);
71
+ /**
72
+ * Cayso's verification methods, as a value this package can check against.
73
+ *
74
+ * `VERIFICATION_METHODS` is a runtime export of `@cayso/contracts`, and this package
75
+ * may not take a value import on it: the coupling scan's rule C forbids a bare
76
+ * specifier in a value position here, which is how a contract package stays free of
77
+ * runtime dependencies. So the members are listed once, in one place, and
78
+ * `citizen.test.ts` asserts the list is exactly Cayso's by checking assignability in
79
+ * both directions — a member added or removed upstream fails this package's
80
+ * typecheck rather than passing through it. That test is the seam; this array is not
81
+ * a second vocabulary.
82
+ */
83
+ const VERIFICATION_METHOD_MEMBERS = Object.freeze(["dev-bypass", "document", "document-and-biometric"]);
84
+ const readVerificationMethod = (record, field, key) => {
85
+ const value = readString(record, field, key);
86
+ if (!VERIFICATION_METHOD_MEMBERS.includes(value)) {
87
+ refuseValue(`${field}.${key}`, "must be a verification method of the Cayso assurance profile");
88
+ }
89
+ return value;
90
+ };
91
+ const readMillis = (record, field, key) => {
92
+ const value = record[key];
93
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
94
+ refuseValue(`${field}.${key}`, "must be a non-negative integer count of milliseconds");
95
+ }
96
+ return value;
97
+ };
98
+ /** A `CitizenRecord` from a wire value, or a refusal. */
99
+ export const parseCitizenRecord = (value, field = "citizen") => {
100
+ const record = readClosedRecord(value, field, CITIZEN_RECORD_FIELDS);
101
+ return {
102
+ citizenId: readCitizenId(record, field, "citizenId"),
103
+ status: parseCitizenStatus(record["status"], `${field}.status`),
104
+ approvedVerificationCaseId: readVerificationCaseId(record, field, "approvedVerificationCaseId"),
105
+ assuranceProfileVersion: readString(record, field, "assuranceProfileVersion"),
106
+ verificationMethod: readVerificationMethod(record, field, "verificationMethod"),
107
+ createdAt: readMillis(record, field, "createdAt"),
108
+ };
109
+ };
110
+ const CITIZEN_NAMES_FIELDS = Object.freeze([
111
+ "citizenId",
112
+ "handle",
113
+ "communityEmailAddress",
114
+ ]);
115
+ /** A `CitizenNames` from a wire value, or a refusal. */
116
+ export const parseCitizenNames = (value, field = "citizenNames") => {
117
+ const record = readClosedRecord(value, field, CITIZEN_NAMES_FIELDS);
118
+ return {
119
+ citizenId: readCitizenId(record, field, "citizenId"),
120
+ handle: parseDisplayHandle(readString(record, field, "handle"), `${field}.handle`),
121
+ communityEmailAddress: parseCommunityEmailAddress(readString(record, field, "communityEmailAddress"), `${field}.communityEmailAddress`),
122
+ };
123
+ };
124
+ const CITIZEN_CREATION_FIELDS = Object.freeze([
125
+ "approvedApplication",
126
+ "citizenId",
127
+ "assuranceProfileVersion",
128
+ "verificationMethod",
129
+ ]);
130
+ /**
131
+ * A `CitizenCreation` from a wire value, or a refusal.
132
+ *
133
+ * The refusal a caller is most likely to meet here is the `ContractParseError`
134
+ * `parseApprovedApplication` raises: `approvedApplication.state: must be approved`.
135
+ */
136
+ export const parseCitizenCreation = (value, field = "citizenCreation") => {
137
+ const record = readClosedRecord(value, field, CITIZEN_CREATION_FIELDS);
138
+ return {
139
+ approvedApplication: parseApprovedApplication(record["approvedApplication"], `${field}.approvedApplication`),
140
+ citizenId: readCitizenId(record, field, "citizenId"),
141
+ assuranceProfileVersion: readString(record, field, "assuranceProfileVersion"),
142
+ verificationMethod: readVerificationMethod(record, field, "verificationMethod"),
143
+ };
144
+ };
@@ -0,0 +1,140 @@
1
+ /**
2
+ * A thin client: the routes this contract describes, a decoder on every response,
3
+ * and nothing else.
4
+ *
5
+ * THIN IS A CONSTRAINT, NOT A DESCRIPTION. This package publishes wire types, the
6
+ * closed unions and a client. It contains no server, no policy engine and no
7
+ * verifier, and this file is where that boundary is most easily lost: a client that
8
+ * retried, cached, refreshed a session or decided what a refusal meant would be
9
+ * making decisions the pillar has not taken yet, inside a package every consumer
10
+ * compiles against.
11
+ *
12
+ * SO IT READS AND NEVER WRITES. There is no method here that creates an Application,
13
+ * submits evidence, approves anything or asks for an assertion. The Application API
14
+ * and the approval transaction do not exist yet, and a client method posting to a
15
+ * route nobody has implemented is worse than no method at all: it looks like a
16
+ * commitment, and the first consumer to call it discovers the shape by trial.
17
+ *
18
+ * WHY THE ROUTE TABLE IS HERE AT ALL. A path is part of a wire contract, and the
19
+ * sibling precedent publishes one for the same reason: a consumer hand-writing a URL
20
+ * is a consumer with a private copy of the contract. Publishing it makes a
21
+ * disagreement between this package and the slice that serves these routes a
22
+ * VERSION BUMP somebody has to make, rather than a 404 somebody debugs.
23
+ *
24
+ * NO SIDE EFFECTS ON IMPORT. Importing this module defines functions and freezes one
25
+ * table. It opens no connection, reads no environment variable and touches no clock.
26
+ *
27
+ * THIS SLICE EXPOSES NO ENDPOINT. §12.5's obligation — declared dimensions, figures
28
+ * backed by load and abuse evidence — falls on the slice that makes an endpoint
29
+ * externally reachable. This package is the caller's side of a conversation with a
30
+ * server that does not exist yet, so it declares no limit policy, and the slice
31
+ * that serves these routes declares one before it exposes them.
32
+ */
33
+ import { type ApplicationSummary } from "./application.js";
34
+ import { type CitizenNames, type CitizenRecord } from "./citizen.js";
35
+ import { type VerificationCaseSummary } from "./evidence.js";
36
+ import type { ApplicationId, VerificationCaseId } from "./ids.js";
37
+ import { type ProvisioningCoordination } from "./provisioning.js";
38
+ import { type ContractResult } from "./refusal.js";
39
+ /**
40
+ * The transport, described structurally so that this package depends on no runtime
41
+ * and no ambient library.
42
+ *
43
+ * `globalThis.fetch` satisfies it, and so does a test double of about ten lines.
44
+ * The shape is deliberately the smallest subset the client uses: naming the platform
45
+ * `Request`, `Response` or `AbortSignal` types would put `@types/node` or the DOM
46
+ * library into this package's published declarations, and a contract package that
47
+ * makes a consumer's typecheck depend on ITS ambient types is not dependency-free in
48
+ * any sense a consumer would recognise.
49
+ */
50
+ export interface HttpRequestInit {
51
+ readonly method: string;
52
+ readonly headers: Readonly<Record<string, string>>;
53
+ }
54
+ /** The part of a response this client reads. */
55
+ export interface HttpResponse {
56
+ readonly ok: boolean;
57
+ readonly status: number;
58
+ json(): Promise<unknown>;
59
+ }
60
+ /** Anything that can perform a request. `globalThis.fetch` is assignable to it. */
61
+ export type FetchLike = (url: string, init: HttpRequestInit) => Promise<HttpResponse>;
62
+ /**
63
+ * How the caller carries its established session.
64
+ *
65
+ * Deliberately a hook rather than a scheme. Whether a consumer presents a Percayso
66
+ * ID session cookie, an authorisation-code result or something else is the sign-in
67
+ * protocol's business, and a header format invented here would become the de facto
68
+ * answer before that is settled. It returns headers, so a consumer can carry
69
+ * whatever it has without this package naming it.
70
+ */
71
+ export type Authorise = () => Promise<Readonly<Record<string, string>>>;
72
+ export interface IdentityClientOptions {
73
+ /** Where the Identity Platform is. No default: a contract package does not know where you deployed. */
74
+ readonly baseUrl: string;
75
+ readonly authorise?: Authorise;
76
+ readonly fetchImpl?: FetchLike;
77
+ }
78
+ /**
79
+ * The routes this contract describes.
80
+ *
81
+ * Read-only, every one of them. `GET` is stated explicitly rather than assumed,
82
+ * because §11.3 turns on the existence of a "side-effect-free" read and a table that
83
+ * left the method implicit would be one refactor away from losing the distinction.
84
+ */
85
+ export declare const ROUTES: Readonly<{
86
+ application: Readonly<{
87
+ method: "GET";
88
+ path: "/api/applications/:id";
89
+ }>;
90
+ ownCitizen: Readonly<{
91
+ method: "GET";
92
+ path: "/api/citizens/me";
93
+ }>;
94
+ ownCitizenNames: Readonly<{
95
+ method: "GET";
96
+ path: "/api/citizens/me/names";
97
+ }>;
98
+ verificationCase: Readonly<{
99
+ method: "GET";
100
+ path: "/api/verification-cases/:id";
101
+ }>;
102
+ ownProvisioningCoordination: Readonly<{
103
+ method: "GET";
104
+ path: "/api/citizens/me/provisioning-coordination";
105
+ }>;
106
+ }>;
107
+ /**
108
+ * Raised when a response is not something this contract can read at all.
109
+ *
110
+ * Distinct from a `Refusal`, which is this pillar saying no in a way the contract
111
+ * describes, and distinct from a `ContractParseError`, which is a well-formed
112
+ * response whose contents are wrong. This one means the conversation failed: a
113
+ * gateway answered, or the body was not JSON. A caller cannot map it to a refusal
114
+ * code without inventing one.
115
+ */
116
+ export declare class ContractTransportError extends Error {
117
+ readonly name = "ContractTransportError";
118
+ readonly status: number;
119
+ constructor(status: number, rule: string);
120
+ }
121
+ /** What this client can do. Five reads, no writes — see this file's docblock. */
122
+ export interface IdentityClient {
123
+ getApplication(applicationId: ApplicationId): Promise<ContractResult<ApplicationSummary>>;
124
+ getOwnCitizen(): Promise<ContractResult<CitizenRecord>>;
125
+ getOwnCitizenNames(): Promise<ContractResult<CitizenNames>>;
126
+ getVerificationCase(verificationCaseId: VerificationCaseId): Promise<ContractResult<VerificationCaseSummary>>;
127
+ getOwnProvisioningCoordination(): Promise<ContractResult<ProvisioningCoordination>>;
128
+ }
129
+ /**
130
+ * Create a client.
131
+ *
132
+ * Every method returns a `ContractResult`: an `ok` branch carrying a decoded value,
133
+ * or a `refused` branch carrying the closed `Refusal`. A refusal is not thrown,
134
+ * because a refusal is an ordinary answer and an exception is how an ordinary answer
135
+ * gets swallowed by a `catch` written for something else.
136
+ *
137
+ * Nothing is retried. A retry policy is a decision about idempotency and about the
138
+ * limits of §12.5, and neither is settled for routes nobody serves yet.
139
+ */
140
+ export declare const createIdentityClient: (options: IdentityClientOptions) => IdentityClient;
package/dist/client.js ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * A thin client: the routes this contract describes, a decoder on every response,
3
+ * and nothing else.
4
+ *
5
+ * THIN IS A CONSTRAINT, NOT A DESCRIPTION. This package publishes wire types, the
6
+ * closed unions and a client. It contains no server, no policy engine and no
7
+ * verifier, and this file is where that boundary is most easily lost: a client that
8
+ * retried, cached, refreshed a session or decided what a refusal meant would be
9
+ * making decisions the pillar has not taken yet, inside a package every consumer
10
+ * compiles against.
11
+ *
12
+ * SO IT READS AND NEVER WRITES. There is no method here that creates an Application,
13
+ * submits evidence, approves anything or asks for an assertion. The Application API
14
+ * and the approval transaction do not exist yet, and a client method posting to a
15
+ * route nobody has implemented is worse than no method at all: it looks like a
16
+ * commitment, and the first consumer to call it discovers the shape by trial.
17
+ *
18
+ * WHY THE ROUTE TABLE IS HERE AT ALL. A path is part of a wire contract, and the
19
+ * sibling precedent publishes one for the same reason: a consumer hand-writing a URL
20
+ * is a consumer with a private copy of the contract. Publishing it makes a
21
+ * disagreement between this package and the slice that serves these routes a
22
+ * VERSION BUMP somebody has to make, rather than a 404 somebody debugs.
23
+ *
24
+ * NO SIDE EFFECTS ON IMPORT. Importing this module defines functions and freezes one
25
+ * table. It opens no connection, reads no environment variable and touches no clock.
26
+ *
27
+ * THIS SLICE EXPOSES NO ENDPOINT. §12.5's obligation — declared dimensions, figures
28
+ * backed by load and abuse evidence — falls on the slice that makes an endpoint
29
+ * externally reachable. This package is the caller's side of a conversation with a
30
+ * server that does not exist yet, so it declares no limit policy, and the slice
31
+ * that serves these routes declares one before it exposes them.
32
+ */
33
+ import { parseApplicationSummary, } from "./application.js";
34
+ import { parseCitizenNames, parseCitizenRecord, } from "./citizen.js";
35
+ import { parseVerificationCaseSummary, } from "./evidence.js";
36
+ import { ContractParseError } from "./parse.js";
37
+ import { parseProvisioningCoordination, } from "./provisioning.js";
38
+ import { parseRefusal } from "./refusal.js";
39
+ /**
40
+ * The routes this contract describes.
41
+ *
42
+ * Read-only, every one of them. `GET` is stated explicitly rather than assumed,
43
+ * because §11.3 turns on the existence of a "side-effect-free" read and a table that
44
+ * left the method implicit would be one refactor away from losing the distinction.
45
+ */
46
+ export const ROUTES = Object.freeze({
47
+ application: Object.freeze({ method: "GET", path: "/api/applications/:id" }),
48
+ ownCitizen: Object.freeze({ method: "GET", path: "/api/citizens/me" }),
49
+ ownCitizenNames: Object.freeze({
50
+ method: "GET",
51
+ path: "/api/citizens/me/names",
52
+ }),
53
+ verificationCase: Object.freeze({
54
+ method: "GET",
55
+ path: "/api/verification-cases/:id",
56
+ }),
57
+ ownProvisioningCoordination: Object.freeze({
58
+ method: "GET",
59
+ path: "/api/citizens/me/provisioning-coordination",
60
+ }),
61
+ });
62
+ /**
63
+ * Raised when a response is not something this contract can read at all.
64
+ *
65
+ * Distinct from a `Refusal`, which is this pillar saying no in a way the contract
66
+ * describes, and distinct from a `ContractParseError`, which is a well-formed
67
+ * response whose contents are wrong. This one means the conversation failed: a
68
+ * gateway answered, or the body was not JSON. A caller cannot map it to a refusal
69
+ * code without inventing one.
70
+ */
71
+ export class ContractTransportError extends Error {
72
+ name = "ContractTransportError";
73
+ status;
74
+ constructor(status, rule) {
75
+ super(`transport: ${rule}`);
76
+ this.status = status;
77
+ }
78
+ }
79
+ const trimTrailingSlash = (value) => value.endsWith("/") ? value.slice(0, -1) : value;
80
+ /**
81
+ * Build a URL from a route and at most one identifier.
82
+ *
83
+ * No escaping is performed and none is needed, which is a property of the
84
+ * identifiers rather than of this function: every branded identifier in `ids.ts` is
85
+ * parsed against an alphabet that excludes `/`, `?`, `#`, `:` and whitespace, so a
86
+ * value that reached here cannot introduce a path segment, a query or a fragment.
87
+ * The type system is what guarantees it arrived through a parser — this function
88
+ * accepts a branded identifier, and a bare `string` is not assignable to one.
89
+ */
90
+ const urlFor = (baseUrl, route, identifier) => {
91
+ const path = identifier === undefined
92
+ ? route.path
93
+ : route.path.replace(":id", identifier);
94
+ return `${trimTrailingSlash(baseUrl)}${path}`;
95
+ };
96
+ const resolveFetch = (options) => {
97
+ const provided = options.fetchImpl;
98
+ if (provided !== undefined) {
99
+ return provided;
100
+ }
101
+ const ambient = globalThis.fetch;
102
+ if (ambient === undefined) {
103
+ throw new ContractTransportError(0, "no fetch implementation. Pass fetchImpl when the runtime has no global fetch");
104
+ }
105
+ return ambient;
106
+ };
107
+ /**
108
+ * Create a client.
109
+ *
110
+ * Every method returns a `ContractResult`: an `ok` branch carrying a decoded value,
111
+ * or a `refused` branch carrying the closed `Refusal`. A refusal is not thrown,
112
+ * because a refusal is an ordinary answer and an exception is how an ordinary answer
113
+ * gets swallowed by a `catch` written for something else.
114
+ *
115
+ * Nothing is retried. A retry policy is a decision about idempotency and about the
116
+ * limits of §12.5, and neither is settled for routes nobody serves yet.
117
+ */
118
+ export const createIdentityClient = (options) => {
119
+ const performFetch = resolveFetch(options);
120
+ const { baseUrl, authorise } = options;
121
+ const read = async (route, decode, field, identifier) => {
122
+ const headers = authorise === undefined ? {} : await authorise();
123
+ const response = await performFetch(urlFor(baseUrl, route, identifier), {
124
+ method: route.method,
125
+ headers: { accept: "application/json", ...headers },
126
+ });
127
+ let body;
128
+ try {
129
+ body = await response.json();
130
+ }
131
+ catch {
132
+ throw new ContractTransportError(response.status, "response body was not JSON");
133
+ }
134
+ if (response.ok) {
135
+ return { outcome: "ok", value: decode(body, field) };
136
+ }
137
+ try {
138
+ return { outcome: "refused", refusal: parseRefusal(body, "refusal") };
139
+ }
140
+ catch (error) {
141
+ if (error instanceof ContractParseError) {
142
+ throw new ContractTransportError(response.status, "response was neither a result nor a refusal this contract describes");
143
+ }
144
+ throw error;
145
+ }
146
+ };
147
+ return {
148
+ getApplication: (applicationId) => read(ROUTES.application, parseApplicationSummary, "application", applicationId),
149
+ getOwnCitizen: () => read(ROUTES.ownCitizen, parseCitizenRecord, "citizen"),
150
+ getOwnCitizenNames: () => read(ROUTES.ownCitizenNames, parseCitizenNames, "citizenNames"),
151
+ getVerificationCase: (verificationCaseId) => read(ROUTES.verificationCase, parseVerificationCaseSummary, "verificationCase", verificationCaseId),
152
+ getOwnProvisioningCoordination: () => read(ROUTES.ownProvisioningCoordination, parseProvisioningCoordination, "coordination"),
153
+ };
154
+ };
@@ -0,0 +1,202 @@
1
+ /**
2
+ * The evidence vocabulary: facts, never scores, and a method that cannot exceed
3
+ * what its facts support.
4
+ *
5
+ * §8.2 is the whole of this file in two sentences. "The verification provider must
6
+ * return **evidence facts and reason codes**, not an unqualified score or Boolean."
7
+ * And: "**Evidence is never overstated.** A passport MRZ read plus an unmatched
8
+ * selfie is not a document-and-biometric decision, and no provider's unqualified
9
+ * 'pass' may be recorded as one."
10
+ *
11
+ * ENFORCED TWICE.
12
+ *
13
+ * The compile-time half is `MethodSupportedBy`, a type-level function from evidence
14
+ * to the strongest method that evidence honestly supports. Code that knows its
15
+ * evidence as literals — a fixture, a test, a stub profile, the issuer assembling a
16
+ * claim it is about to mint — writes `MethodSupportedBy<typeof facts>` and gets a
17
+ * compile error rather than an overclaim. `document-and-biometric` is unreachable
18
+ * from evidence whose `biometricComparedToDocumentPortrait` is `false`, and there is
19
+ * no widening that gets there.
20
+ *
21
+ * The runtime half is `parseVerificationCaseSummary`, because a type can say nothing
22
+ * about a provider's JSON. It recomputes the supported method from the facts on the
23
+ * wire and refuses a `method` stronger than they bear — the exact case §8.2 names.
24
+ *
25
+ * WHY THE SUPPORT RULE IS WRITTEN HERE AND NOT IMPORTED.
26
+ *
27
+ * `@cayso/contracts` exports `methodForEvidence`, which answers the same question,
28
+ * and this package deliberately cannot use it: it holds no runtime dependency, and
29
+ * the coupling scan's rule C makes that mechanical by refusing a bare specifier in a
30
+ * value position. Two implementations of one rule is normally how they diverge, so
31
+ * the divergence is closed where it can be closed — `evidence.test.ts` imports
32
+ * Cayso's function (a test file, outside the coupling scan, using the exact-pinned
33
+ * development dependency) and asserts the two agree across every combination of the
34
+ * three facts. An upstream change to the rule fails this package's test run rather
35
+ * than passing silently into a consumer.
36
+ *
37
+ * The authority still belongs upstream. A verifier or issuer taking a real decision
38
+ * uses Cayso's function, and that is where the value dependency is taken. What lives
39
+ * here refuses an overstatement at a contract boundary; it does not grant an
40
+ * assurance.
41
+ *
42
+ * NO ROUTE IS HARD-CODED, AND THE STUB'S IDENTIFIER IS NOT IN THIS PACKAGE.
43
+ *
44
+ * §8.3 requires a "route-neutral, versioned evidence model" and warns that
45
+ * "hard-coding the first document reader into policy is how it becomes permanent by
46
+ * accident". So `EvidenceRoute` and `EvidencePolicyVersion` are opaque versioned
47
+ * identifiers rather than closed unions: phase one is passport-only, and a published
48
+ * union with one member would make that the contract every consumer compiled
49
+ * against. Adding a route is "a **new** evidence-policy version" (§8.3), which is a
50
+ * value, not a type change.
51
+ *
52
+ * The stub's route and policy version are deliberately absent for a stronger reason.
53
+ * §8.4 requires that "the production bundle contains neither the stub implementation
54
+ * nor its selection identifier". A member named for the stub in a contract package
55
+ * every production surface imports would place that selection identifier in every
56
+ * one of them. The stub names its own route, in its own package, excluded at build
57
+ * time — which is §8.4's "decisive control is build-time exclusion, not
58
+ * configuration".
59
+ */
60
+ import type { VerificationEvidence, VerificationMethod } from "@cayso/contracts";
61
+ import type { Branded } from "./brand.js";
62
+ import { type ApplicationId, type VerificationCaseId } from "./ids.js";
63
+ /**
64
+ * Which evidence route produced a case — §8.3's `evidence_route`.
65
+ *
66
+ * Opaque, so that adding a driving licence, a national identity card, an ePassport
67
+ * NFC route or a staff-assisted route is a new value rather than a breaking change
68
+ * to every consumer of this package.
69
+ */
70
+ export type EvidenceRoute = Branded<"EvidenceRoute">;
71
+ /**
72
+ * Which version of the evidence policy decided a case — §8.3's
73
+ * `evidence_policy_version`.
74
+ *
75
+ * §8.3: adding a route is "a **new** evidence-policy version — never a migration
76
+ * that changes the meaning of past approvals". A stored case therefore carries the
77
+ * version that judged it, for ever, and this pillar never reinterprets an old case
78
+ * under a new policy.
79
+ */
80
+ export type EvidencePolicyVersion = Branded<"EvidencePolicyVersion">;
81
+ /** An `EvidenceRoute` from a wire value, or a refusal. */
82
+ export declare const parseEvidenceRoute: (value: string, field?: string) => EvidenceRoute;
83
+ /** An `EvidencePolicyVersion` from a wire value, or a refusal. */
84
+ export declare const parseEvidencePolicyVersion: (value: string, field?: string) => EvidencePolicyVersion;
85
+ /**
86
+ * What §8.6's table calls an outcome: `pass`, `refer` or `fail`.
87
+ *
88
+ * Three members, and the middle one is the reason the union is closed. §8.6: "A
89
+ * referral is **not** an automated rejection, so it does not inherently require two
90
+ * reviewers." A Boolean would have had to file `refer` under one of the others, and
91
+ * whichever it chose would have moved the authority required to overturn it. §8.4
92
+ * requires the stub to return all three deterministically, which is the same three
93
+ * and not a development-only set.
94
+ */
95
+ export type VerificationOutcome = "pass" | "refer" | "fail";
96
+ /** Every outcome, in §8.6's order. */
97
+ export declare const VERIFICATION_OUTCOMES: readonly VerificationOutcome[];
98
+ /** A `VerificationOutcome` from a wire value, or a refusal. */
99
+ export declare const parseVerificationOutcome: (value: unknown, field?: string) => VerificationOutcome;
100
+ /**
101
+ * The evidence facts of one case: Cayso's three, and the two §8.1 adds.
102
+ *
103
+ * It EXTENDS `VerificationEvidence` rather than redeclaring its fields, so a value
104
+ * of this type is accepted anywhere Cayso's is expected and the two can never
105
+ * disagree about what `biometricComparedToDocumentPortrait` means. §8.2's
106
+ * "**Provider vocabulary stops at the adapter**" is the rule that makes this the
107
+ * right shape: whatever a supplier returns is normalised into these booleans by the
108
+ * adapter, and no provider's own vocabulary reaches a stored case or a consumer.
109
+ *
110
+ * The two additions are facts §8.1 lists as production requirements and Cayso's
111
+ * profile does not model, because they are this pillar's business to establish
112
+ * rather than Cayso's to judge. Neither strengthens the method that evidence
113
+ * supports — `MethodSupportedBy` ignores both, deliberately, because the method
114
+ * vocabulary is Cayso's and this package does not get to widen what one of its
115
+ * members asserts. They are recorded so that a case can say what was and was not
116
+ * done, which is what §8.1's table is for.
117
+ */
118
+ export interface IdentityEvidenceFacts extends VerificationEvidence {
119
+ /**
120
+ * Document authenticity was established by a provider or model check, or by
121
+ * cryptographic chip validation. §8.1: an MRZ consistency check alone is **not**
122
+ * document authenticity.
123
+ */
124
+ readonly documentAuthenticityEstablished: boolean;
125
+ /**
126
+ * A presentation-attack (liveness) check was performed and recorded. §8.6: a
127
+ * person comparing two photographs is **not** automatically a liveness check.
128
+ */
129
+ readonly presentationAttackCheckPerformed: boolean;
130
+ }
131
+ /**
132
+ * The strongest method this evidence supports, at the type level.
133
+ *
134
+ * Written against `Extract<VerificationMethod, …>` rather than against bare string
135
+ * literals: if either method were renamed or removed upstream, these branches would
136
+ * collapse to `never` and `evidence.test.ts` would fail, rather than this package
137
+ * quietly continuing to speak a vocabulary Cayso no longer has.
138
+ *
139
+ * `dev-bypass` is never produced. It is not a conclusion evidence supports; it is
140
+ * the marker of a verification that examined nothing (§8.4: "`dev-bypass` proves
141
+ * nothing and never migrates as verified evidence").
142
+ */
143
+ export type MethodSupportedBy<E extends VerificationEvidence> = E extends {
144
+ readonly identityDocumentChecked: true;
145
+ } ? E extends {
146
+ readonly biometricCaptured: true;
147
+ readonly biometricComparedToDocumentPortrait: true;
148
+ } ? Extract<VerificationMethod, "document-and-biometric"> : Extract<VerificationMethod, "document"> : never;
149
+ /**
150
+ * The strongest method this evidence supports, at runtime, or `null` for evidence
151
+ * that supports none.
152
+ *
153
+ * `null` is not a failure and must not be rendered as one: it is the honest answer
154
+ * for a case that examined nothing yet. A caller holding `null` has nothing to
155
+ * assert, which is §8.2's position exactly.
156
+ */
157
+ export declare const methodSupportedByEvidence: (evidence: VerificationEvidence) => VerificationMethod | null;
158
+ /**
159
+ * Whether `method` is honest about `evidence` — the refusable form of §8.2.
160
+ *
161
+ * Evidence may exceed what a method asserts; it may never fall short of it. A
162
+ * `dev-bypass` case is honest only when nothing was examined: a bypass that claims
163
+ * a document was checked is an overstatement in the other direction, and the one a
164
+ * migration would most like to make.
165
+ */
166
+ export declare const evidenceSupportsMethod: (method: VerificationMethod, evidence: VerificationEvidence) => boolean;
167
+ /**
168
+ * What a verification case says, and nothing §15 forbids it to say.
169
+ *
170
+ * `verification_cases` must not hold "A bare provider score as the decision", so
171
+ * there is no score field, no confidence field and no threshold field; the decision
172
+ * is this pillar's, taken from the facts, under a named policy version. §8.8 makes
173
+ * the same point about suppliers: "**Disqualifying: 'score only'**". A provider's
174
+ * own case reference is carried, because a case must be traceable to the supplier
175
+ * that produced it, and it is opaque here.
176
+ */
177
+ export interface VerificationCaseSummary {
178
+ readonly verificationCaseId: VerificationCaseId;
179
+ readonly applicationId: ApplicationId;
180
+ readonly outcome: VerificationOutcome;
181
+ readonly evidenceRoute: EvidenceRoute;
182
+ readonly evidencePolicyVersion: EvidencePolicyVersion;
183
+ readonly facts: IdentityEvidenceFacts;
184
+ /**
185
+ * The method these facts support and the decision was recorded under. Refused by
186
+ * `parseVerificationCaseSummary` when the facts do not bear it.
187
+ */
188
+ readonly method: VerificationMethod;
189
+ /** The supplier's own reference for this case, or `null`. Opaque; never interpreted here. */
190
+ readonly providerCaseReference: string | null;
191
+ }
192
+ /** The evidence facts from a wire value, or a refusal. */
193
+ export declare const parseIdentityEvidenceFacts: (value: unknown, field?: string) => IdentityEvidenceFacts;
194
+ /**
195
+ * A `VerificationCaseSummary` from a wire value, or a refusal.
196
+ *
197
+ * The refusal that matters is the last one: a `method` the facts do not bear is
198
+ * refused, naming the method the facts actually support. That is §8.2's "A passport
199
+ * MRZ read plus an unmatched selfie is not a document-and-biometric decision",
200
+ * enforced where a provider's response enters this pillar.
201
+ */
202
+ export declare const parseVerificationCaseSummary: (value: unknown, field?: string) => VerificationCaseSummary;