@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.
- package/LICENSE +201 -0
- package/README.md +108 -0
- package/dist/application.d.ts +156 -0
- package/dist/application.js +170 -0
- package/dist/assertion.d.ts +125 -0
- package/dist/assertion.js +221 -0
- package/dist/brand.d.ts +30 -0
- package/dist/brand.js +20 -0
- package/dist/citizen.d.ts +158 -0
- package/dist/citizen.js +144 -0
- package/dist/client.d.ts +140 -0
- package/dist/client.js +154 -0
- package/dist/evidence.d.ts +202 -0
- package/dist/evidence.js +224 -0
- package/dist/ids.d.ts +158 -0
- package/dist/ids.js +154 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +35 -0
- package/dist/parse.d.ts +67 -0
- package/dist/parse.js +109 -0
- package/dist/provisioning.d.ts +139 -0
- package/dist/provisioning.js +223 -0
- package/dist/refusal.d.ts +122 -0
- package/dist/refusal.js +116 -0
- package/package.json +48 -0
package/dist/evidence.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
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 { readApplicationId, readVerificationCaseId, } from "./ids.js";
|
|
61
|
+
import { readBoolean, readClosedRecord, readString, refuseValue, } from "./parse.js";
|
|
62
|
+
/**
|
|
63
|
+
* A versioned identifier: lower case, separated by hyphens or dots.
|
|
64
|
+
*
|
|
65
|
+
* Dots are admitted so that `passport-1.2` is expressible; upper case and
|
|
66
|
+
* whitespace are not, because these values appear in stored records and in
|
|
67
|
+
* comparisons, and two spellings of one route is one route nobody can query.
|
|
68
|
+
*/
|
|
69
|
+
const VERSIONED_IDENTIFIER = /^[a-z0-9]+(?:[-.][a-z0-9]+)*$/;
|
|
70
|
+
/** An `EvidenceRoute` from a wire value, or a refusal. */
|
|
71
|
+
export const parseEvidenceRoute = (value, field = "evidenceRoute") => {
|
|
72
|
+
if (!VERSIONED_IDENTIFIER.test(value)) {
|
|
73
|
+
refuseValue(field, "must be a lower-case versioned identifier separated by hyphens or dots");
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
};
|
|
77
|
+
/** An `EvidencePolicyVersion` from a wire value, or a refusal. */
|
|
78
|
+
export const parseEvidencePolicyVersion = (value, field = "evidencePolicyVersion") => {
|
|
79
|
+
if (!VERSIONED_IDENTIFIER.test(value)) {
|
|
80
|
+
refuseValue(field, "must be a lower-case versioned identifier separated by hyphens or dots");
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
};
|
|
84
|
+
/** Every outcome, in §8.6's order. */
|
|
85
|
+
export const VERIFICATION_OUTCOMES = Object.freeze(["pass", "refer", "fail"]);
|
|
86
|
+
/** A `VerificationOutcome` from a wire value, or a refusal. */
|
|
87
|
+
export const parseVerificationOutcome = (value, field = "outcome") => {
|
|
88
|
+
if (typeof value !== "string" ||
|
|
89
|
+
!VERIFICATION_OUTCOMES.includes(value)) {
|
|
90
|
+
refuseValue(field, "must be one of pass, refer or fail (§8.6)");
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* The strongest method this evidence supports, at runtime, or `null` for evidence
|
|
96
|
+
* that supports none.
|
|
97
|
+
*
|
|
98
|
+
* `null` is not a failure and must not be rendered as one: it is the honest answer
|
|
99
|
+
* for a case that examined nothing yet. A caller holding `null` has nothing to
|
|
100
|
+
* assert, which is §8.2's position exactly.
|
|
101
|
+
*/
|
|
102
|
+
export const methodSupportedByEvidence = (evidence) => {
|
|
103
|
+
if (!evidence.identityDocumentChecked) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
if (evidence.biometricCaptured &&
|
|
107
|
+
evidence.biometricComparedToDocumentPortrait) {
|
|
108
|
+
return "document-and-biometric";
|
|
109
|
+
}
|
|
110
|
+
return "document";
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Whether `method` is honest about `evidence` — the refusable form of §8.2.
|
|
114
|
+
*
|
|
115
|
+
* Evidence may exceed what a method asserts; it may never fall short of it. A
|
|
116
|
+
* `dev-bypass` case is honest only when nothing was examined: a bypass that claims
|
|
117
|
+
* a document was checked is an overstatement in the other direction, and the one a
|
|
118
|
+
* migration would most like to make.
|
|
119
|
+
*/
|
|
120
|
+
export const evidenceSupportsMethod = (method, evidence) => {
|
|
121
|
+
if (method === "dev-bypass") {
|
|
122
|
+
return (!evidence.identityDocumentChecked &&
|
|
123
|
+
!evidence.biometricCaptured &&
|
|
124
|
+
!evidence.biometricComparedToDocumentPortrait);
|
|
125
|
+
}
|
|
126
|
+
const supported = methodSupportedByEvidence(evidence);
|
|
127
|
+
if (supported === null) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return method === "document" ? true : supported === "document-and-biometric";
|
|
131
|
+
};
|
|
132
|
+
const CASE_FIELDS = Object.freeze([
|
|
133
|
+
"verificationCaseId",
|
|
134
|
+
"applicationId",
|
|
135
|
+
"outcome",
|
|
136
|
+
"evidenceRoute",
|
|
137
|
+
"evidencePolicyVersion",
|
|
138
|
+
"facts",
|
|
139
|
+
"method",
|
|
140
|
+
"providerCaseReference",
|
|
141
|
+
]);
|
|
142
|
+
/** Fields that would make a supplier's number the decision. §8.2, §8.8 and §15 all refuse them. */
|
|
143
|
+
const SCORE_FIELDS = Object.freeze([
|
|
144
|
+
"score",
|
|
145
|
+
"providerScore",
|
|
146
|
+
"confidence",
|
|
147
|
+
"matchScore",
|
|
148
|
+
"threshold",
|
|
149
|
+
"similarity",
|
|
150
|
+
]);
|
|
151
|
+
const FACT_FIELDS = Object.freeze([
|
|
152
|
+
"identityDocumentChecked",
|
|
153
|
+
"biometricCaptured",
|
|
154
|
+
"biometricComparedToDocumentPortrait",
|
|
155
|
+
"documentAuthenticityEstablished",
|
|
156
|
+
"presentationAttackCheckPerformed",
|
|
157
|
+
]);
|
|
158
|
+
const METHOD_MEMBERS = Object.freeze([
|
|
159
|
+
"dev-bypass",
|
|
160
|
+
"document",
|
|
161
|
+
"document-and-biometric",
|
|
162
|
+
]);
|
|
163
|
+
/** The evidence facts from a wire value, or a refusal. */
|
|
164
|
+
export const parseIdentityEvidenceFacts = (value, field = "facts") => {
|
|
165
|
+
const record = readClosedRecord(value, field, [
|
|
166
|
+
...FACT_FIELDS,
|
|
167
|
+
...SCORE_FIELDS,
|
|
168
|
+
]);
|
|
169
|
+
for (const key of Object.keys(record)) {
|
|
170
|
+
if (SCORE_FIELDS.includes(key)) {
|
|
171
|
+
refuseValue(`${field}.${key}`, "is a provider figure and not an evidence fact. A bare provider score is never the decision (§8.2, §15)");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
identityDocumentChecked: readBoolean(record, field, "identityDocumentChecked"),
|
|
176
|
+
biometricCaptured: readBoolean(record, field, "biometricCaptured"),
|
|
177
|
+
biometricComparedToDocumentPortrait: readBoolean(record, field, "biometricComparedToDocumentPortrait"),
|
|
178
|
+
documentAuthenticityEstablished: readBoolean(record, field, "documentAuthenticityEstablished"),
|
|
179
|
+
presentationAttackCheckPerformed: readBoolean(record, field, "presentationAttackCheckPerformed"),
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* A `VerificationCaseSummary` from a wire value, or a refusal.
|
|
184
|
+
*
|
|
185
|
+
* The refusal that matters is the last one: a `method` the facts do not bear is
|
|
186
|
+
* refused, naming the method the facts actually support. That is §8.2's "A passport
|
|
187
|
+
* MRZ read plus an unmatched selfie is not a document-and-biometric decision",
|
|
188
|
+
* enforced where a provider's response enters this pillar.
|
|
189
|
+
*/
|
|
190
|
+
export const parseVerificationCaseSummary = (value, field = "verificationCase") => {
|
|
191
|
+
const record = readClosedRecord(value, field, [
|
|
192
|
+
...CASE_FIELDS,
|
|
193
|
+
...SCORE_FIELDS,
|
|
194
|
+
]);
|
|
195
|
+
for (const key of Object.keys(record)) {
|
|
196
|
+
if (SCORE_FIELDS.includes(key)) {
|
|
197
|
+
refuseValue(`${field}.${key}`, "is a provider figure and not a decision. This pillar records its own versioned decision from evidence facts (§8.2, §8.8, §15)");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const facts = parseIdentityEvidenceFacts(record["facts"], `${field}.facts`);
|
|
201
|
+
const method = readString(record, field, "method");
|
|
202
|
+
if (!METHOD_MEMBERS.includes(method)) {
|
|
203
|
+
refuseValue(`${field}.method`, "must be a verification method of the Cayso assurance profile");
|
|
204
|
+
}
|
|
205
|
+
if (!evidenceSupportsMethod(method, facts)) {
|
|
206
|
+
const supported = methodSupportedByEvidence(facts);
|
|
207
|
+
refuseValue(`${field}.method`, supported === null
|
|
208
|
+
? "claims more than the evidence facts support. These facts support no method at all (§8.2)"
|
|
209
|
+
: `claims more than the evidence facts support. These facts support ${supported} (§8.2)`);
|
|
210
|
+
}
|
|
211
|
+
const providerCaseReference = record["providerCaseReference"];
|
|
212
|
+
return {
|
|
213
|
+
verificationCaseId: readVerificationCaseId(record, field, "verificationCaseId"),
|
|
214
|
+
applicationId: readApplicationId(record, field, "applicationId"),
|
|
215
|
+
outcome: parseVerificationOutcome(record["outcome"], `${field}.outcome`),
|
|
216
|
+
evidenceRoute: parseEvidenceRoute(readString(record, field, "evidenceRoute"), `${field}.evidenceRoute`),
|
|
217
|
+
evidencePolicyVersion: parseEvidencePolicyVersion(readString(record, field, "evidencePolicyVersion"), `${field}.evidencePolicyVersion`),
|
|
218
|
+
facts,
|
|
219
|
+
method: method,
|
|
220
|
+
providerCaseReference: providerCaseReference === null
|
|
221
|
+
? null
|
|
222
|
+
: readString(record, field, "providerCaseReference"),
|
|
223
|
+
};
|
|
224
|
+
};
|
package/dist/ids.d.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The identifiers of §9 and §15, each branded and each parsed.
|
|
3
|
+
*
|
|
4
|
+
* The canon's identifier rules are almost all rules about CONFUSION — which opaque
|
|
5
|
+
* string may stand in for which other opaque string — and they are the rules this
|
|
6
|
+
* pillar has already lost twice. CONTEXT.md states three of them outright: a Citizen
|
|
7
|
+
* ID "is the only identifier that may appear as an assertion subject"; a Piper
|
|
8
|
+
* Platform subject "identifies a local account… is never evidence of which human is
|
|
9
|
+
* present and is never the source of a Citizen ID"; a Handle "is never a Citizen ID
|
|
10
|
+
* and never appears in Cayso Platform's ledger identity".
|
|
11
|
+
*
|
|
12
|
+
* Every one of those is a string. Nothing structural tells them apart, so the brand
|
|
13
|
+
* of `brand.ts` does the telling: they are mutually unassignable, and a function that
|
|
14
|
+
* asks for a `CitizenId` cannot be handed a `PiperPlatformSubject` however the value
|
|
15
|
+
* was obtained. That is the half a compiler holds.
|
|
16
|
+
*
|
|
17
|
+
* The half a compiler cannot hold is a value that arrived over a wire, which is what
|
|
18
|
+
* each `parse…` function is for.
|
|
19
|
+
*
|
|
20
|
+
* WHAT THE PARSERS DELIBERATELY DO NOT DO
|
|
21
|
+
*
|
|
22
|
+
* They do not pin a generated format. §12.2 limits this package to "wire types,
|
|
23
|
+
* closed error/status unions and a thin client only", and how a Citizen ID is
|
|
24
|
+
* MINTED is the schema's business — P1-3's — not the contract's. What the contract
|
|
25
|
+
* can honestly say is the shape an opaque identifier has on the wire: printable,
|
|
26
|
+
* unpunctuated, URL-safe, and therefore incapable of being an email address or a
|
|
27
|
+
* qualified name. `OPAQUE_IDENTIFIER` admits UUID, ULID, base64url and every other
|
|
28
|
+
* format P1-3 might choose, and admits none of the values CONTEXT.md says an
|
|
29
|
+
* identifier is not.
|
|
30
|
+
*
|
|
31
|
+
* They also carry no length bound. A bound would be a figure, and this pillar's rule
|
|
32
|
+
* is that a figure belongs in §12.5's register with its evidence and is read from
|
|
33
|
+
* configuration. The bound that actually matters to a service is the request-size
|
|
34
|
+
* limit, which §12.1 puts on the slice that exposes an endpoint (P1-8).
|
|
35
|
+
*/
|
|
36
|
+
import type { Branded } from "./brand.js";
|
|
37
|
+
/**
|
|
38
|
+
* The Community email domains of §9: `percayso.me` for Citizens, `percayso.biz` for
|
|
39
|
+
* Organisations. Both are confirmed and owned by Percayso.
|
|
40
|
+
*
|
|
41
|
+
* Organisation addresses are not phase-one work — §9 says so — and the domain is
|
|
42
|
+
* listed anyway because the union is the contract. A closed union that omitted a
|
|
43
|
+
* domain the canon confirms would be a union the Organisation slice has to break.
|
|
44
|
+
*/
|
|
45
|
+
export declare const COMMUNITY_EMAIL_DOMAINS: readonly ["percayso.me", "percayso.biz"];
|
|
46
|
+
/** One of the two Community email domains. */
|
|
47
|
+
export type CommunityEmailDomain = (typeof COMMUNITY_EMAIL_DOMAINS)[number];
|
|
48
|
+
/**
|
|
49
|
+
* Identity Platform's stable, opaque identifier for a Citizen.
|
|
50
|
+
*
|
|
51
|
+
* The only identifier that may appear as an assertion subject (CONTEXT.md, and
|
|
52
|
+
* `assertion.ts` is where that is made unexpressible rather than merely stated).
|
|
53
|
+
*/
|
|
54
|
+
export type CitizenId = Branded<"CitizenId">;
|
|
55
|
+
/** The identifier of one person's attempt to become a Citizen (§5.2). */
|
|
56
|
+
export type ApplicationId = Branded<"ApplicationId">;
|
|
57
|
+
/** The identifier of the record of what evidence was examined, and what was concluded. */
|
|
58
|
+
export type VerificationCaseId = Branded<"VerificationCaseId">;
|
|
59
|
+
/**
|
|
60
|
+
* The identifier of one registered installation of Piper Platform (§15 `deployments`).
|
|
61
|
+
*
|
|
62
|
+
* Registration "gives accountability, throttling and revocation. It does NOT admit an
|
|
63
|
+
* issuer, prove the code is unmodified, or establish which human is present"
|
|
64
|
+
* (CONTEXT.md, **Deployment**). Its separateness from `CitizenId` is the type-level
|
|
65
|
+
* form of §14.4's "possession of any deployment credential is insufficient without
|
|
66
|
+
* independently verifiable Citizen proof".
|
|
67
|
+
*/
|
|
68
|
+
export type DeploymentId = Branded<"DeploymentId">;
|
|
69
|
+
/**
|
|
70
|
+
* The identifier of a local user inside one Piper Platform deployment.
|
|
71
|
+
*
|
|
72
|
+
* It identifies a local account and nothing else. §7.1: Identity Platform derives the
|
|
73
|
+
* Citizen subject from its own authenticated session, and "no path, query, body,
|
|
74
|
+
* header, cookie or installation registration may replace it". This type exists
|
|
75
|
+
* PRECISELY so that the replacement cannot be written: it is not assignable to
|
|
76
|
+
* `CitizenId`, and this package publishes no function that converts one to the other.
|
|
77
|
+
*/
|
|
78
|
+
export type PiperPlatformSubject = Branded<"PiperPlatformSubject">;
|
|
79
|
+
/** A Citizen's unique, chosen, public pseudonym, in the canonical form of §9.1. */
|
|
80
|
+
export type CanonicalHandle = Branded<"CanonicalHandle">;
|
|
81
|
+
/**
|
|
82
|
+
* The submitted form of a Handle, stored separately from the canonical one (§9.1).
|
|
83
|
+
*
|
|
84
|
+
* Separate types rather than one, because §9.1 requires separate storage and the
|
|
85
|
+
* reason is operational: an algorithm upgrade "never silently changes an existing
|
|
86
|
+
* Handle", which is only checkable if the two forms were never the same field.
|
|
87
|
+
*/
|
|
88
|
+
export type DisplayHandle = Branded<"DisplayHandle">;
|
|
89
|
+
/**
|
|
90
|
+
* The address assigned to a Citizen under a Community domain, as its OWN identifier.
|
|
91
|
+
*
|
|
92
|
+
* §9.2: the initial local part is the canonical Handle, and it is then stored
|
|
93
|
+
* separately — "changing the Handle does not automatically change the email address".
|
|
94
|
+
* A distinct brand is what stops a Handle being written into this field by a
|
|
95
|
+
* conversion somebody thought was obviously safe.
|
|
96
|
+
*/
|
|
97
|
+
export type CommunityEmailAddress = Branded<"CommunityEmailAddress">;
|
|
98
|
+
/**
|
|
99
|
+
* An opaque reference tying one exchange to its audit record (§15 `audit_events`).
|
|
100
|
+
*
|
|
101
|
+
* It is carried on a refusal, and it is the ONLY thing carried on one that varies —
|
|
102
|
+
* see `refusal.ts` for why that list is as short as it is.
|
|
103
|
+
*/
|
|
104
|
+
export type CorrelationReference = Branded<"CorrelationReference">;
|
|
105
|
+
/** Decode a Citizen ID, or refuse. */
|
|
106
|
+
export declare const parseCitizenId: (value: string, field?: string) => CitizenId;
|
|
107
|
+
/** Decode an Application identifier, or refuse. */
|
|
108
|
+
export declare const parseApplicationId: (value: string, field?: string) => ApplicationId;
|
|
109
|
+
/** Decode a Verification Case identifier, or refuse. */
|
|
110
|
+
export declare const parseVerificationCaseId: (value: string, field?: string) => VerificationCaseId;
|
|
111
|
+
/** Decode a Deployment identifier, or refuse. */
|
|
112
|
+
export declare const parseDeploymentId: (value: string, field?: string) => DeploymentId;
|
|
113
|
+
/**
|
|
114
|
+
* Decode a Piper Platform subject, or refuse.
|
|
115
|
+
*
|
|
116
|
+
* Structurally identical to `parseCitizenId` and deliberately kept as its own
|
|
117
|
+
* function. Two parsers that produce unassignable types is the point: a caller that
|
|
118
|
+
* has a local subject and needs a Citizen ID has to go and get one from a Percayso ID
|
|
119
|
+
* session, which is §7.1's rule, and there is nowhere in this package to shortcut it.
|
|
120
|
+
*/
|
|
121
|
+
export declare const parsePiperPlatformSubject: (value: string, field?: string) => PiperPlatformSubject;
|
|
122
|
+
/** Decode a correlation reference, or refuse. */
|
|
123
|
+
export declare const parseCorrelationReference: (value: string, field?: string) => CorrelationReference;
|
|
124
|
+
/**
|
|
125
|
+
* Decode a canonical Handle, or refuse.
|
|
126
|
+
*
|
|
127
|
+
* Refuses anything that is not already canonical — an upper-case letter, a leading or
|
|
128
|
+
* trailing hyphen, consecutive hyphens, or any character outside §9.1's alphabet.
|
|
129
|
+
* CANONICALISING is not done here and must not be: NFKC, case folding and the TR39
|
|
130
|
+
* skeleton are P1-12's, and a second implementation of them is how two spellings of
|
|
131
|
+
* the same Handle end up in two tables.
|
|
132
|
+
*/
|
|
133
|
+
export declare const parseCanonicalHandle: (value: string, field?: string) => CanonicalHandle;
|
|
134
|
+
/**
|
|
135
|
+
* Decode a Community email address, or refuse.
|
|
136
|
+
*
|
|
137
|
+
* The local part is checked for the same alphabet as a canonical Handle because §9.2
|
|
138
|
+
* says the initial local part IS one. It is not checked for being any PARTICULAR
|
|
139
|
+
* Handle: the address is its own identifier from the moment it is assigned, and a
|
|
140
|
+
* parser that insisted the two still matched would refuse every renamed Handle in the
|
|
141
|
+
* pillar — the exact breakage §9.2 exists to prevent.
|
|
142
|
+
*/
|
|
143
|
+
export declare const parseCommunityEmailAddress: (value: string, field?: string) => CommunityEmailAddress;
|
|
144
|
+
/**
|
|
145
|
+
* Decode a display Handle, or refuse.
|
|
146
|
+
*
|
|
147
|
+
* Only emptiness and surrounding whitespace are refused. The submitted form is
|
|
148
|
+
* deliberately permissive — §9.1 stores it precisely BECAUSE it is not the canonical
|
|
149
|
+
* form — and every rule that narrows it lives with the canonicalisation algorithm in
|
|
150
|
+
* P1-12.
|
|
151
|
+
*/
|
|
152
|
+
export declare const parseDisplayHandle: (value: string, field?: string) => DisplayHandle;
|
|
153
|
+
/** Read and decode a Citizen ID from a record field. */
|
|
154
|
+
export declare const readCitizenId: (record: Readonly<Record<string, unknown>>, field: string, key: string) => CitizenId;
|
|
155
|
+
/** Read and decode an Application identifier from a record field. */
|
|
156
|
+
export declare const readApplicationId: (record: Readonly<Record<string, unknown>>, field: string, key: string) => ApplicationId;
|
|
157
|
+
/** Read and decode a Verification Case identifier from a record field. */
|
|
158
|
+
export declare const readVerificationCaseId: (record: Readonly<Record<string, unknown>>, field: string, key: string) => VerificationCaseId;
|
package/dist/ids.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The identifiers of §9 and §15, each branded and each parsed.
|
|
3
|
+
*
|
|
4
|
+
* The canon's identifier rules are almost all rules about CONFUSION — which opaque
|
|
5
|
+
* string may stand in for which other opaque string — and they are the rules this
|
|
6
|
+
* pillar has already lost twice. CONTEXT.md states three of them outright: a Citizen
|
|
7
|
+
* ID "is the only identifier that may appear as an assertion subject"; a Piper
|
|
8
|
+
* Platform subject "identifies a local account… is never evidence of which human is
|
|
9
|
+
* present and is never the source of a Citizen ID"; a Handle "is never a Citizen ID
|
|
10
|
+
* and never appears in Cayso Platform's ledger identity".
|
|
11
|
+
*
|
|
12
|
+
* Every one of those is a string. Nothing structural tells them apart, so the brand
|
|
13
|
+
* of `brand.ts` does the telling: they are mutually unassignable, and a function that
|
|
14
|
+
* asks for a `CitizenId` cannot be handed a `PiperPlatformSubject` however the value
|
|
15
|
+
* was obtained. That is the half a compiler holds.
|
|
16
|
+
*
|
|
17
|
+
* The half a compiler cannot hold is a value that arrived over a wire, which is what
|
|
18
|
+
* each `parse…` function is for.
|
|
19
|
+
*
|
|
20
|
+
* WHAT THE PARSERS DELIBERATELY DO NOT DO
|
|
21
|
+
*
|
|
22
|
+
* They do not pin a generated format. §12.2 limits this package to "wire types,
|
|
23
|
+
* closed error/status unions and a thin client only", and how a Citizen ID is
|
|
24
|
+
* MINTED is the schema's business — P1-3's — not the contract's. What the contract
|
|
25
|
+
* can honestly say is the shape an opaque identifier has on the wire: printable,
|
|
26
|
+
* unpunctuated, URL-safe, and therefore incapable of being an email address or a
|
|
27
|
+
* qualified name. `OPAQUE_IDENTIFIER` admits UUID, ULID, base64url and every other
|
|
28
|
+
* format P1-3 might choose, and admits none of the values CONTEXT.md says an
|
|
29
|
+
* identifier is not.
|
|
30
|
+
*
|
|
31
|
+
* They also carry no length bound. A bound would be a figure, and this pillar's rule
|
|
32
|
+
* is that a figure belongs in §12.5's register with its evidence and is read from
|
|
33
|
+
* configuration. The bound that actually matters to a service is the request-size
|
|
34
|
+
* limit, which §12.1 puts on the slice that exposes an endpoint (P1-8).
|
|
35
|
+
*/
|
|
36
|
+
import { readString, refuseValue } from "./parse.js";
|
|
37
|
+
/**
|
|
38
|
+
* The shape of an opaque identifier on the wire.
|
|
39
|
+
*
|
|
40
|
+
* URL-safe and unpunctuated, which is what makes it an identifier rather than a
|
|
41
|
+
* value with structure somebody might read. It excludes `@`, so no email address can
|
|
42
|
+
* ever be decoded as a Citizen ID; it excludes whitespace and `.`, `/` and `:`, so no
|
|
43
|
+
* name, address or qualified reference can either.
|
|
44
|
+
*/
|
|
45
|
+
const OPAQUE_IDENTIFIER = /^[A-Za-z0-9_-]+$/;
|
|
46
|
+
/**
|
|
47
|
+
* The canonical Handle alphabet of §9.1: `a-z`, `0-9` and `-`, starting and ending
|
|
48
|
+
* alphanumeric, with no consecutive hyphens.
|
|
49
|
+
*
|
|
50
|
+
* §9.1's LENGTH BOUNDS ARE NOT RESTATED HERE. They are canon figures, the canon is
|
|
51
|
+
* the one place a figure is written down, and the algorithm they belong to — NFKC,
|
|
52
|
+
* case folding, the TR39 confusable skeleton, the reserved set and the versioned
|
|
53
|
+
* policy that governs all of it — is slice P1-12's, not this package's. What this
|
|
54
|
+
* parser is for is narrower and worth having on its own: it refuses a value that is
|
|
55
|
+
* NOT ALREADY CANONICAL, so a display form can never be decoded into a field that
|
|
56
|
+
* means the canonical one.
|
|
57
|
+
*/
|
|
58
|
+
const CANONICAL_HANDLE = /^[a-z0-9](?:-?[a-z0-9])*$/;
|
|
59
|
+
/**
|
|
60
|
+
* The Community email domains of §9: `percayso.me` for Citizens, `percayso.biz` for
|
|
61
|
+
* Organisations. Both are confirmed and owned by Percayso.
|
|
62
|
+
*
|
|
63
|
+
* Organisation addresses are not phase-one work — §9 says so — and the domain is
|
|
64
|
+
* listed anyway because the union is the contract. A closed union that omitted a
|
|
65
|
+
* domain the canon confirms would be a union the Organisation slice has to break.
|
|
66
|
+
*/
|
|
67
|
+
export const COMMUNITY_EMAIL_DOMAINS = Object.freeze([
|
|
68
|
+
"percayso.me",
|
|
69
|
+
"percayso.biz",
|
|
70
|
+
]);
|
|
71
|
+
const parseOpaque = (value, field) => {
|
|
72
|
+
if (!OPAQUE_IDENTIFIER.test(value)) {
|
|
73
|
+
refuseValue(field, "must be an opaque identifier: unreserved URL characters only, and therefore never an address, a name or a qualified reference");
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
};
|
|
77
|
+
/** Decode a Citizen ID, or refuse. */
|
|
78
|
+
export const parseCitizenId = (value, field = "citizenId") => parseOpaque(value, field);
|
|
79
|
+
/** Decode an Application identifier, or refuse. */
|
|
80
|
+
export const parseApplicationId = (value, field = "applicationId") => parseOpaque(value, field);
|
|
81
|
+
/** Decode a Verification Case identifier, or refuse. */
|
|
82
|
+
export const parseVerificationCaseId = (value, field = "verificationCaseId") => parseOpaque(value, field);
|
|
83
|
+
/** Decode a Deployment identifier, or refuse. */
|
|
84
|
+
export const parseDeploymentId = (value, field = "deploymentId") => parseOpaque(value, field);
|
|
85
|
+
/**
|
|
86
|
+
* Decode a Piper Platform subject, or refuse.
|
|
87
|
+
*
|
|
88
|
+
* Structurally identical to `parseCitizenId` and deliberately kept as its own
|
|
89
|
+
* function. Two parsers that produce unassignable types is the point: a caller that
|
|
90
|
+
* has a local subject and needs a Citizen ID has to go and get one from a Percayso ID
|
|
91
|
+
* session, which is §7.1's rule, and there is nowhere in this package to shortcut it.
|
|
92
|
+
*/
|
|
93
|
+
export const parsePiperPlatformSubject = (value, field = "piperPlatformSubject") => parseOpaque(value, field);
|
|
94
|
+
/** Decode a correlation reference, or refuse. */
|
|
95
|
+
export const parseCorrelationReference = (value, field = "correlationReference") => parseOpaque(value, field);
|
|
96
|
+
/**
|
|
97
|
+
* Decode a canonical Handle, or refuse.
|
|
98
|
+
*
|
|
99
|
+
* Refuses anything that is not already canonical — an upper-case letter, a leading or
|
|
100
|
+
* trailing hyphen, consecutive hyphens, or any character outside §9.1's alphabet.
|
|
101
|
+
* CANONICALISING is not done here and must not be: NFKC, case folding and the TR39
|
|
102
|
+
* skeleton are P1-12's, and a second implementation of them is how two spellings of
|
|
103
|
+
* the same Handle end up in two tables.
|
|
104
|
+
*/
|
|
105
|
+
export const parseCanonicalHandle = (value, field = "canonicalHandle") => {
|
|
106
|
+
if (!CANONICAL_HANDLE.test(value)) {
|
|
107
|
+
refuseValue(field, "must already be canonical per §9.1: lower-case ASCII letters, digits and single interior hyphens, starting and ending alphanumeric. Canonicalisation belongs to P1-12 and is not performed here");
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Decode a Community email address, or refuse.
|
|
113
|
+
*
|
|
114
|
+
* The local part is checked for the same alphabet as a canonical Handle because §9.2
|
|
115
|
+
* says the initial local part IS one. It is not checked for being any PARTICULAR
|
|
116
|
+
* Handle: the address is its own identifier from the moment it is assigned, and a
|
|
117
|
+
* parser that insisted the two still matched would refuse every renamed Handle in the
|
|
118
|
+
* pillar — the exact breakage §9.2 exists to prevent.
|
|
119
|
+
*/
|
|
120
|
+
export const parseCommunityEmailAddress = (value, field = "communityEmailAddress") => {
|
|
121
|
+
const at = value.indexOf("@");
|
|
122
|
+
if (at === -1 || value.indexOf("@", at + 1) !== -1) {
|
|
123
|
+
refuseValue(field, "must contain exactly one @");
|
|
124
|
+
}
|
|
125
|
+
const local = value.slice(0, at);
|
|
126
|
+
const domain = value.slice(at + 1);
|
|
127
|
+
if (!CANONICAL_HANDLE.test(local)) {
|
|
128
|
+
refuseValue(`${field}.localPart`, "must use §9.1's canonical alphabet, which is the alphabet the address was first assigned from");
|
|
129
|
+
}
|
|
130
|
+
if (!COMMUNITY_EMAIL_DOMAINS.includes(domain)) {
|
|
131
|
+
refuseValue(`${field}.domain`, `must be one of the ${COMMUNITY_EMAIL_DOMAINS.length} Community domains of §9`);
|
|
132
|
+
}
|
|
133
|
+
return value;
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Decode a display Handle, or refuse.
|
|
137
|
+
*
|
|
138
|
+
* Only emptiness and surrounding whitespace are refused. The submitted form is
|
|
139
|
+
* deliberately permissive — §9.1 stores it precisely BECAUSE it is not the canonical
|
|
140
|
+
* form — and every rule that narrows it lives with the canonicalisation algorithm in
|
|
141
|
+
* P1-12.
|
|
142
|
+
*/
|
|
143
|
+
export const parseDisplayHandle = (value, field = "displayHandle") => {
|
|
144
|
+
if (value.trim() === "" || value.trim() !== value) {
|
|
145
|
+
refuseValue(field, "must be non-empty and carry no surrounding whitespace");
|
|
146
|
+
}
|
|
147
|
+
return value;
|
|
148
|
+
};
|
|
149
|
+
/** Read and decode a Citizen ID from a record field. */
|
|
150
|
+
export const readCitizenId = (record, field, key) => parseCitizenId(readString(record, field, key), `${field}.${key}`);
|
|
151
|
+
/** Read and decode an Application identifier from a record field. */
|
|
152
|
+
export const readApplicationId = (record, field, key) => parseApplicationId(readString(record, field, key), `${field}.${key}`);
|
|
153
|
+
/** Read and decode a Verification Case identifier from a record field. */
|
|
154
|
+
export const readVerificationCaseId = (record, field, key) => parseVerificationCaseId(readString(record, field, key), `${field}.${key}`);
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@percayso/identity-contracts` — the identity vocabulary this pillar publishes.
|
|
3
|
+
*
|
|
4
|
+
* WHAT IS HERE. The identifiers of §9 and §15, the closed lifecycle states of §5.2
|
|
5
|
+
* and §11.3, the closed refusal union, the evidence vocabulary of §8.2 and §8.3, and
|
|
6
|
+
* the assertion request and issuance shapes. Every one of them is a WIRE type with a
|
|
7
|
+
* decoder beside it. Where the canon states an invariant, it is written twice: once
|
|
8
|
+
* as a type that cannot express the violation, and once as a value the decoder
|
|
9
|
+
* refuses.
|
|
10
|
+
*
|
|
11
|
+
* WHAT IS DELIBERATELY NOT HERE. No server, no policy engine, no verifier, no
|
|
12
|
+
* database access and no figures. The thin client ships from the separate `./client`
|
|
13
|
+
* entry point so that a consumer wanting only the types does not pay for it, and so
|
|
14
|
+
* that the packaging test can prove the type surface stands alone.
|
|
15
|
+
*
|
|
16
|
+
* NOTHING RUNS ON IMPORT. Importing this module defines types, freezes a handful of
|
|
17
|
+
* tables and nothing else: no connection, no environment read, no clock. The rule is
|
|
18
|
+
* this repository's and the sibling's alike, and it is what makes a contract package
|
|
19
|
+
* safe to depend on from anywhere.
|
|
20
|
+
*
|
|
21
|
+
* NO RUNTIME DEPENDENCY. `@cayso/contracts` appears here only in type position.
|
|
22
|
+
* Assertion, assurance and operation types are IMPORTED from it and never restated,
|
|
23
|
+
* and the package manifest records it as a peer dependency, exact-pinned; the
|
|
24
|
+
* package README explains why that shape rather than either of the other two. The
|
|
25
|
+
* built JavaScript names it nowhere, and `packaging.test.ts` asserts so against the
|
|
26
|
+
* real tarball.
|
|
27
|
+
*/
|
|
28
|
+
export type { Branded } from "./brand.js";
|
|
29
|
+
export { ContractParseError, readArray, readBoolean, readClosedRecord, readMember, readObject, readString, refuseValue, } from "./parse.js";
|
|
30
|
+
export { COMMUNITY_EMAIL_DOMAINS, parseApplicationId, parseCanonicalHandle, parseCitizenId, parseCommunityEmailAddress, parseCorrelationReference, parseDeploymentId, parseDisplayHandle, parsePiperPlatformSubject, parseVerificationCaseId, readApplicationId, readCitizenId, readVerificationCaseId, } from "./ids.js";
|
|
31
|
+
export type { ApplicationId, CanonicalHandle, CitizenId, CommunityEmailAddress, CommunityEmailDomain, CorrelationReference, DeploymentId, DisplayHandle, PiperPlatformSubject, VerificationCaseId, } from "./ids.js";
|
|
32
|
+
export { APPLICATION_STATES, APPLICATION_TRANSITIONS, isApplicationTransitionPermitted, isRetryable, isTerminalApplicationState, parseApplicationState, parseApplicationSummary, parseApprovedApplication, } from "./application.js";
|
|
33
|
+
export type { ApplicationState, ApplicationSummary, ApprovedApplication, } from "./application.js";
|
|
34
|
+
export { CITIZEN_STATUSES, parseCitizenCreation, parseCitizenNames, parseCitizenRecord, parseCitizenStatus, } from "./citizen.js";
|
|
35
|
+
export type { CitizenCreation, CitizenNames, CitizenRecord, CitizenStatus, Personhood, } from "./citizen.js";
|
|
36
|
+
export { PROVISIONING_STATES, PROVISIONING_TRANSITIONS, isProvisioningTransitionPermitted, parseCaysoConfirmation, parseProvisioningCoordination, } from "./provisioning.js";
|
|
37
|
+
export type { CaysoConfirmation, ProvisioningCoordination, ProvisioningState, } from "./provisioning.js";
|
|
38
|
+
export { VERIFICATION_OUTCOMES, evidenceSupportsMethod, methodSupportedByEvidence, parseEvidencePolicyVersion, parseEvidenceRoute, parseIdentityEvidenceFacts, parseVerificationCaseSummary, parseVerificationOutcome, } from "./evidence.js";
|
|
39
|
+
export type { EvidencePolicyVersion, EvidenceRoute, IdentityEvidenceFacts, MethodSupportedBy, VerificationCaseSummary, VerificationOutcome, } from "./evidence.js";
|
|
40
|
+
export { REFUSAL_CODES, isAppealable, parseRefusal } from "./refusal.js";
|
|
41
|
+
export type { ContractResult, Refusal, RefusalCode } from "./refusal.js";
|
|
42
|
+
export { ASSERTION_OPERATIONS, parseAssertionIssuanceRecord, parseAssertionRequest, } from "./assertion.js";
|
|
43
|
+
export type { AssertionIssuanceRecord, AssertionOperation, AssertionRequest, AssertionScope, IssuanceOutcome, } from "./assertion.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@percayso/identity-contracts` — the identity vocabulary this pillar publishes.
|
|
3
|
+
*
|
|
4
|
+
* WHAT IS HERE. The identifiers of §9 and §15, the closed lifecycle states of §5.2
|
|
5
|
+
* and §11.3, the closed refusal union, the evidence vocabulary of §8.2 and §8.3, and
|
|
6
|
+
* the assertion request and issuance shapes. Every one of them is a WIRE type with a
|
|
7
|
+
* decoder beside it. Where the canon states an invariant, it is written twice: once
|
|
8
|
+
* as a type that cannot express the violation, and once as a value the decoder
|
|
9
|
+
* refuses.
|
|
10
|
+
*
|
|
11
|
+
* WHAT IS DELIBERATELY NOT HERE. No server, no policy engine, no verifier, no
|
|
12
|
+
* database access and no figures. The thin client ships from the separate `./client`
|
|
13
|
+
* entry point so that a consumer wanting only the types does not pay for it, and so
|
|
14
|
+
* that the packaging test can prove the type surface stands alone.
|
|
15
|
+
*
|
|
16
|
+
* NOTHING RUNS ON IMPORT. Importing this module defines types, freezes a handful of
|
|
17
|
+
* tables and nothing else: no connection, no environment read, no clock. The rule is
|
|
18
|
+
* this repository's and the sibling's alike, and it is what makes a contract package
|
|
19
|
+
* safe to depend on from anywhere.
|
|
20
|
+
*
|
|
21
|
+
* NO RUNTIME DEPENDENCY. `@cayso/contracts` appears here only in type position.
|
|
22
|
+
* Assertion, assurance and operation types are IMPORTED from it and never restated,
|
|
23
|
+
* and the package manifest records it as a peer dependency, exact-pinned; the
|
|
24
|
+
* package README explains why that shape rather than either of the other two. The
|
|
25
|
+
* built JavaScript names it nowhere, and `packaging.test.ts` asserts so against the
|
|
26
|
+
* real tarball.
|
|
27
|
+
*/
|
|
28
|
+
export { ContractParseError, readArray, readBoolean, readClosedRecord, readMember, readObject, readString, refuseValue, } from "./parse.js";
|
|
29
|
+
export { COMMUNITY_EMAIL_DOMAINS, parseApplicationId, parseCanonicalHandle, parseCitizenId, parseCommunityEmailAddress, parseCorrelationReference, parseDeploymentId, parseDisplayHandle, parsePiperPlatformSubject, parseVerificationCaseId, readApplicationId, readCitizenId, readVerificationCaseId, } from "./ids.js";
|
|
30
|
+
export { APPLICATION_STATES, APPLICATION_TRANSITIONS, isApplicationTransitionPermitted, isRetryable, isTerminalApplicationState, parseApplicationState, parseApplicationSummary, parseApprovedApplication, } from "./application.js";
|
|
31
|
+
export { CITIZEN_STATUSES, parseCitizenCreation, parseCitizenNames, parseCitizenRecord, parseCitizenStatus, } from "./citizen.js";
|
|
32
|
+
export { PROVISIONING_STATES, PROVISIONING_TRANSITIONS, isProvisioningTransitionPermitted, parseCaysoConfirmation, parseProvisioningCoordination, } from "./provisioning.js";
|
|
33
|
+
export { VERIFICATION_OUTCOMES, evidenceSupportsMethod, methodSupportedByEvidence, parseEvidencePolicyVersion, parseEvidenceRoute, parseIdentityEvidenceFacts, parseVerificationCaseSummary, parseVerificationOutcome, } from "./evidence.js";
|
|
34
|
+
export { REFUSAL_CODES, isAppealable, parseRefusal } from "./refusal.js";
|
|
35
|
+
export { ASSERTION_OPERATIONS, parseAssertionIssuanceRecord, parseAssertionRequest, } from "./assertion.js";
|