@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,125 @@
1
+ /**
2
+ * The assertion vocabulary: who may be the subject, what a request may ask for, and
3
+ * what an issuance record may keep afterwards.
4
+ *
5
+ * CONTEXT.md states the rule this file exists to hold: "The Citizen ID is the only
6
+ * identifier that may appear as an assertion subject." §8.7 lists the structural
7
+ * tests phase one relies on instead of the phase-two binding table, and two of them
8
+ * are about this type — "no Piper subject or deployment identifier appears in the
9
+ * Citizen-creation input" and "no assertion derives `sub` from a Piper identifier".
10
+ *
11
+ * ENFORCED TWICE.
12
+ *
13
+ * `AssertionRequest.subject` is a `CitizenId`. A `PiperPlatformSubject` is a
14
+ * different brand and is not assignable to it — in either direction, with no
15
+ * conversion function anywhere in this package — so an issuer that tried to pass one
16
+ * through does not compile. `parseAssertionRequest` is the runtime twin: the subject
17
+ * arrives as a string like everything else on a wire, and only `readCitizenId`
18
+ * accepts it.
19
+ *
20
+ * `DeploymentId` is present and carries nothing. §14.4: "Possession of any
21
+ * deployment credential is insufficient without independently verifiable Citizen
22
+ * proof", and Appendix B rejects "Piper deployment class as an authorisation basis"
23
+ * outright. It is recorded because §15's `deployments` row wants registration,
24
+ * admission and revocation as separate concepts — not because it authorises
25
+ * anything. Nothing in this package reads it, and no function here takes one where a
26
+ * subject is expected.
27
+ *
28
+ * SHORT-LIVED, SINGLE-USE, OPERATION-SCOPED — AND WHICH OF THE THREE THIS PACKAGE
29
+ * CAN HOLD.
30
+ *
31
+ * Operation-scoped it holds completely: `scope` is a non-empty tuple type, so an
32
+ * empty scope is not expressible, and the decoder refuses an empty array. An empty
33
+ * scope authorises nothing, and a caller who sends one has almost certainly built
34
+ * the list dynamically and lost it.
35
+ *
36
+ * Short-lived it holds partially and says so. `expiresAt` must be after `issuedAt`,
37
+ * which is checkable here; the CEILING on that interval is
38
+ * `MAX_ASSERTION_LIFETIME_SECONDS`, a runtime export of `@cayso/contracts` that this
39
+ * package may not value-import — it holds no runtime dependency, and the coupling
40
+ * scan's rule C enforces that mechanically. Restating the figure here would be
41
+ * exactly what the canon forbids: a number copied out of its one home, going stale
42
+ * in a published package that every consumer compiles against. The issuer takes
43
+ * that value dependency and enforces the ceiling. This package refuses the nonsense
44
+ * and leaves the figure where it lives.
45
+ *
46
+ * Single-use it does not hold at all. Replay refusal needs recorded state — §15's
47
+ * `assertion_issuance` row, with its unique `jti` or one-way representation — and
48
+ * this package has no store and no clock. The vocabulary is here; the enforcement is
49
+ * the issuer's and the verifier's.
50
+ */
51
+ import type { AssertionClaims, ContractOperation, Millis } from "@cayso/contracts";
52
+ import { type CitizenId, type DeploymentId } from "./ids.js";
53
+ /**
54
+ * The operations an assertion may be scoped to.
55
+ *
56
+ * Cayso's `ContractOperation`, imported and not restated: the scope of an assertion
57
+ * this pillar mints is read by Cayso's gate, so a second vocabulary would be two
58
+ * lists that disagree the first time one of them grows. `assertion.test.ts` asserts
59
+ * this package's runtime list is exactly Cayso's `CONTRACT_OPERATIONS`.
60
+ */
61
+ export type AssertionOperation = ContractOperation;
62
+ /**
63
+ * The operations, as a value this package can check a wire scope against.
64
+ *
65
+ * `CONTRACT_OPERATIONS` is a runtime export of `@cayso/contracts` and this package
66
+ * may not value-import it (coupling scan rule C). The list is therefore written once
67
+ * here and pinned to Cayso's by a test, in both directions, so an operation added or
68
+ * removed upstream fails this package's test run rather than passing through it.
69
+ */
70
+ export declare const ASSERTION_OPERATIONS: readonly AssertionOperation[];
71
+ /** A non-empty scope. The type cannot express the empty list; the decoder refuses it. */
72
+ export type AssertionScope = readonly [
73
+ AssertionOperation,
74
+ ...AssertionOperation[]
75
+ ];
76
+ /**
77
+ * What an issuer is asked for.
78
+ *
79
+ * `subject` is a `CitizenId` and nothing else. `audience` is the literal `"cayso"`,
80
+ * matching `AssertionClaims.aud` in `@cayso/contracts` — a request that could name
81
+ * its own audience is a request that could have an assertion minted for a relying
82
+ * party nobody registered, and §14.4 requires that a modified downloaded Instance
83
+ * "cannot … change its audience, redirect URI, operation or onboarding target".
84
+ */
85
+ export interface AssertionRequest {
86
+ readonly subject: CitizenId;
87
+ readonly audience: AssertionClaims["aud"];
88
+ readonly scope: AssertionScope;
89
+ /**
90
+ * The installation presenting the request, or `null`. Recorded, never trusted:
91
+ * it grants nothing, narrows nothing and substitutes for no part of Citizen proof.
92
+ */
93
+ readonly deploymentId: DeploymentId | null;
94
+ }
95
+ /** An `AssertionRequest` from a wire value, or a refusal. */
96
+ export declare const parseAssertionRequest: (value: unknown, field?: string) => AssertionRequest;
97
+ /** Whether issuance happened. A closed pair; there is no third answer and no "partial". */
98
+ export type IssuanceOutcome = "issued" | "refused";
99
+ /**
100
+ * What §15's `assertion_issuance` row keeps, and nothing from its "must not hold"
101
+ * column.
102
+ *
103
+ * There is no field for the encoded assertion, its signature, a legal identity value
104
+ * or a deployment secret — §15 forbids all four, and §14.4's disclosure criterion
105
+ * repeats it for logs, traces and analytics. `jtiDigest` is §15's "Unique `jti` or a
106
+ * one-way representation": a digest is enough to refuse a replay and is not enough
107
+ * to reconstruct anything.
108
+ *
109
+ * `safeReasonCode` is a `RefusalCode`-shaped idea deliberately typed as the closed
110
+ * refusal union's member set rather than free text — see `refusal.ts` for why a
111
+ * reason that can hold prose eventually holds a provider's payload.
112
+ */
113
+ export interface AssertionIssuanceRecord {
114
+ readonly jtiDigest: string;
115
+ readonly subject: CitizenId;
116
+ readonly scope: AssertionScope;
117
+ readonly audience: AssertionClaims["aud"];
118
+ /** The `kid` of the key that signed it. An identifier, never the key. */
119
+ readonly signingKeyId: string;
120
+ readonly issuedAt: Millis;
121
+ readonly expiresAt: Millis;
122
+ readonly outcome: IssuanceOutcome;
123
+ }
124
+ /** An `AssertionIssuanceRecord` from a wire value, or a refusal. */
125
+ export declare const parseAssertionIssuanceRecord: (value: unknown, field?: string) => AssertionIssuanceRecord;
@@ -0,0 +1,221 @@
1
+ /**
2
+ * The assertion vocabulary: who may be the subject, what a request may ask for, and
3
+ * what an issuance record may keep afterwards.
4
+ *
5
+ * CONTEXT.md states the rule this file exists to hold: "The Citizen ID is the only
6
+ * identifier that may appear as an assertion subject." §8.7 lists the structural
7
+ * tests phase one relies on instead of the phase-two binding table, and two of them
8
+ * are about this type — "no Piper subject or deployment identifier appears in the
9
+ * Citizen-creation input" and "no assertion derives `sub` from a Piper identifier".
10
+ *
11
+ * ENFORCED TWICE.
12
+ *
13
+ * `AssertionRequest.subject` is a `CitizenId`. A `PiperPlatformSubject` is a
14
+ * different brand and is not assignable to it — in either direction, with no
15
+ * conversion function anywhere in this package — so an issuer that tried to pass one
16
+ * through does not compile. `parseAssertionRequest` is the runtime twin: the subject
17
+ * arrives as a string like everything else on a wire, and only `readCitizenId`
18
+ * accepts it.
19
+ *
20
+ * `DeploymentId` is present and carries nothing. §14.4: "Possession of any
21
+ * deployment credential is insufficient without independently verifiable Citizen
22
+ * proof", and Appendix B rejects "Piper deployment class as an authorisation basis"
23
+ * outright. It is recorded because §15's `deployments` row wants registration,
24
+ * admission and revocation as separate concepts — not because it authorises
25
+ * anything. Nothing in this package reads it, and no function here takes one where a
26
+ * subject is expected.
27
+ *
28
+ * SHORT-LIVED, SINGLE-USE, OPERATION-SCOPED — AND WHICH OF THE THREE THIS PACKAGE
29
+ * CAN HOLD.
30
+ *
31
+ * Operation-scoped it holds completely: `scope` is a non-empty tuple type, so an
32
+ * empty scope is not expressible, and the decoder refuses an empty array. An empty
33
+ * scope authorises nothing, and a caller who sends one has almost certainly built
34
+ * the list dynamically and lost it.
35
+ *
36
+ * Short-lived it holds partially and says so. `expiresAt` must be after `issuedAt`,
37
+ * which is checkable here; the CEILING on that interval is
38
+ * `MAX_ASSERTION_LIFETIME_SECONDS`, a runtime export of `@cayso/contracts` that this
39
+ * package may not value-import — it holds no runtime dependency, and the coupling
40
+ * scan's rule C enforces that mechanically. Restating the figure here would be
41
+ * exactly what the canon forbids: a number copied out of its one home, going stale
42
+ * in a published package that every consumer compiles against. The issuer takes
43
+ * that value dependency and enforces the ceiling. This package refuses the nonsense
44
+ * and leaves the figure where it lives.
45
+ *
46
+ * Single-use it does not hold at all. Replay refusal needs recorded state — §15's
47
+ * `assertion_issuance` row, with its unique `jti` or one-way representation — and
48
+ * this package has no store and no clock. The vocabulary is here; the enforcement is
49
+ * the issuer's and the verifier's.
50
+ */
51
+ import { parseDeploymentId, readCitizenId, } from "./ids.js";
52
+ import { readArray, readClosedRecord, readString, refuseValue, } from "./parse.js";
53
+ /**
54
+ * The operations, as a value this package can check a wire scope against.
55
+ *
56
+ * `CONTRACT_OPERATIONS` is a runtime export of `@cayso/contracts` and this package
57
+ * may not value-import it (coupling scan rule C). The list is therefore written once
58
+ * here and pinned to Cayso's by a test, in both directions, so an operation added or
59
+ * removed upstream fails this package's test run rather than passing through it.
60
+ */
61
+ export const ASSERTION_OPERATIONS = Object.freeze([
62
+ "read-own-wallet",
63
+ "read-purchase-result",
64
+ "authorise-purchase",
65
+ "cancel-purchase",
66
+ "provision-citizenship",
67
+ "read-own-citizenship",
68
+ ]);
69
+ /**
70
+ * A scope that is known to be non-empty, or a refusal.
71
+ *
72
+ * The check and the type agree by construction: the first element is read out and
73
+ * refused when it is absent, so the tuple is built from a value the compiler has
74
+ * seen rather than asserted into existence by a cast.
75
+ */
76
+ const asScope = (operations, field) => {
77
+ const [first, ...rest] = operations;
78
+ if (first === undefined) {
79
+ refuseValue(field, "must name at least one operation. An empty scope authorises nothing");
80
+ }
81
+ return [first, ...rest];
82
+ };
83
+ const REQUEST_FIELDS = Object.freeze([
84
+ "subject",
85
+ "audience",
86
+ "scope",
87
+ "deploymentId",
88
+ ]);
89
+ /**
90
+ * Field names by which a caller might try to supply the subject from somewhere
91
+ * other than an established Percayso ID session.
92
+ *
93
+ * §14.4: "No browser, Piper API or installation registration can substitute a
94
+ * Citizen through path, query, body, header or cookie." A request carrying one of
95
+ * these is refused by name, because the caller needs to know the substitution was
96
+ * seen and rejected rather than ignored. The `PiperPlatformSubject` type in `ids.ts`
97
+ * exists precisely so that such a value has somewhere to live that is NOT here.
98
+ */
99
+ const SUBJECT_SUBSTITUTION_FIELDS = Object.freeze([
100
+ "piperSubject",
101
+ "piperPlatformSubject",
102
+ "localSubject",
103
+ "sub",
104
+ "onBehalfOf",
105
+ "actAs",
106
+ "impersonate",
107
+ ]);
108
+ /** An `AssertionRequest` from a wire value, or a refusal. */
109
+ export const parseAssertionRequest = (value, field = "assertionRequest") => {
110
+ const record = readClosedRecord(value, field, [
111
+ ...REQUEST_FIELDS,
112
+ ...SUBJECT_SUBSTITUTION_FIELDS,
113
+ ]);
114
+ for (const key of Object.keys(record)) {
115
+ if (SUBJECT_SUBSTITUTION_FIELDS.includes(key)) {
116
+ refuseValue(`${field}.${key}`, "cannot supply the subject. The Citizen ID is the only identifier that may appear as an assertion subject, and it comes from this pillar's own session (§8.7, §14.4)");
117
+ }
118
+ }
119
+ const audience = readString(record, field, "audience");
120
+ if (audience !== "cayso") {
121
+ refuseValue(`${field}.audience`, "must be cayso. A caller does not choose the audience an assertion is minted for (§14.4)");
122
+ }
123
+ const scope = readArray(record, field, "scope", (entry, elementField) => {
124
+ if (typeof entry !== "string" ||
125
+ !ASSERTION_OPERATIONS.includes(entry)) {
126
+ refuseValue(elementField, "must be an operation of the Cayso contract");
127
+ }
128
+ return entry;
129
+ });
130
+ const deploymentId = record["deploymentId"];
131
+ return {
132
+ subject: readCitizenId(record, field, "subject"),
133
+ audience: "cayso",
134
+ scope: asScope(scope, `${field}.scope`),
135
+ deploymentId: deploymentId === null
136
+ ? null
137
+ : parseDeploymentId(readString(record, field, "deploymentId"), `${field}.deploymentId`),
138
+ };
139
+ };
140
+ const ISSUANCE_FIELDS = Object.freeze([
141
+ "jtiDigest",
142
+ "subject",
143
+ "scope",
144
+ "audience",
145
+ "signingKeyId",
146
+ "issuedAt",
147
+ "expiresAt",
148
+ "outcome",
149
+ ]);
150
+ /**
151
+ * Field names carrying signing material or the encoded assertion itself.
152
+ *
153
+ * These spellings appear here as names this contract REFUSES. They are not this
154
+ * pillar's vocabulary for an Assertion — CONTEXT.md's banned-word table settles that
155
+ * — and a record that could carry one would put it in every audit export.
156
+ *
157
+ * THE LIST DELIBERATELY STOPS SHORT OF NAMING A PER-DEPLOYMENT SECRET. §14.4 is not
158
+ * that such a field must be refused; it is that no such thing exists — a Piper
159
+ * Platform deployment is a public client and holds no secret. Writing the name here
160
+ * to refuse it would put the term into the repository, where the credential scan
161
+ * finds it and is right to. A wire value carrying one is refused anyway, by
162
+ * `readClosedRecord`, as a field this contract type does not have.
163
+ */
164
+ const FORBIDDEN_MATERIAL_FIELDS = Object.freeze([
165
+ "assertion",
166
+ "encoded",
167
+ "signature",
168
+ "privateKey",
169
+ "signingKey",
170
+ "legalName",
171
+ "dateOfBirth",
172
+ ]);
173
+ const readMillis = (record, field, key) => {
174
+ const value = record[key];
175
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
176
+ refuseValue(`${field}.${key}`, "must be a non-negative integer count of milliseconds");
177
+ }
178
+ return value;
179
+ };
180
+ /** An `AssertionIssuanceRecord` from a wire value, or a refusal. */
181
+ export const parseAssertionIssuanceRecord = (value, field = "issuance") => {
182
+ const record = readClosedRecord(value, field, [
183
+ ...ISSUANCE_FIELDS,
184
+ ...FORBIDDEN_MATERIAL_FIELDS,
185
+ ]);
186
+ for (const key of Object.keys(record)) {
187
+ if (FORBIDDEN_MATERIAL_FIELDS.includes(key)) {
188
+ refuseValue(`${field}.${key}`, "is signing material, an encoded credential or a legal identity value. An issuance record holds none of them (§14.4, §15)");
189
+ }
190
+ }
191
+ const audience = readString(record, field, "audience");
192
+ if (audience !== "cayso") {
193
+ refuseValue(`${field}.audience`, "must be cayso");
194
+ }
195
+ const scope = readArray(record, field, "scope", (entry, elementField) => {
196
+ if (typeof entry !== "string" ||
197
+ !ASSERTION_OPERATIONS.includes(entry)) {
198
+ refuseValue(elementField, "must be an operation of the Cayso contract");
199
+ }
200
+ return entry;
201
+ });
202
+ const issuedAt = readMillis(record, field, "issuedAt");
203
+ const expiresAt = readMillis(record, field, "expiresAt");
204
+ if (expiresAt <= issuedAt) {
205
+ refuseValue(`${field}.expiresAt`, "must be after issuedAt. How much after is bounded by the Cayso contract's published maximum lifetime, which the issuer enforces and this package does not restate");
206
+ }
207
+ const outcome = readString(record, field, "outcome");
208
+ if (outcome !== "issued" && outcome !== "refused") {
209
+ refuseValue(`${field}.outcome`, "must be issued or refused");
210
+ }
211
+ return {
212
+ jtiDigest: readString(record, field, "jtiDigest"),
213
+ subject: readCitizenId(record, field, "subject"),
214
+ scope: asScope(scope, `${field}.scope`),
215
+ audience: "cayso",
216
+ signingKeyId: readString(record, field, "signingKeyId"),
217
+ issuedAt,
218
+ expiresAt,
219
+ outcome: outcome,
220
+ };
221
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The brand, which is how this package says that two strings are not each other.
3
+ *
4
+ * Several of the canon's hardest invariants are statements about WHICH identifier
5
+ * may appear WHERE, and every one of those identifiers is a string on the wire. A
6
+ * Citizen ID, a Piper Platform subject and a canonical Handle are all opaque text;
7
+ * nothing structural distinguishes them, so nothing structural can stop one being
8
+ * passed where another belongs. CONTEXT.md's rule — "the Citizen ID is the only
9
+ * identifier that may appear as an assertion subject" — would then be a sentence in
10
+ * a document rather than something a compiler can hold.
11
+ *
12
+ * A brand fixes that half. `CitizenId` and `PiperPlatformSubject` are both `string`
13
+ * at runtime and are mutually unassignable at compile time, so a function that asks
14
+ * for one cannot be handed the other, and there is no widening that quietly works.
15
+ *
16
+ * It is only half, deliberately. A brand is erased, so it says nothing about a value
17
+ * that arrived over a wire — which is why every branded type in `ids.ts` also has a
18
+ * parser, and why the parser is the layer a reviewer should read second.
19
+ */
20
+ declare const brand: unique symbol;
21
+ /**
22
+ * An opaque string that is only itself.
23
+ *
24
+ * `Name` never appears at runtime and is never read; it exists so that two brands
25
+ * declared with different names are different types.
26
+ */
27
+ export type Branded<Name extends string> = string & {
28
+ readonly [brand]: Name;
29
+ };
30
+ export {};
package/dist/brand.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The brand, which is how this package says that two strings are not each other.
3
+ *
4
+ * Several of the canon's hardest invariants are statements about WHICH identifier
5
+ * may appear WHERE, and every one of those identifiers is a string on the wire. A
6
+ * Citizen ID, a Piper Platform subject and a canonical Handle are all opaque text;
7
+ * nothing structural distinguishes them, so nothing structural can stop one being
8
+ * passed where another belongs. CONTEXT.md's rule — "the Citizen ID is the only
9
+ * identifier that may appear as an assertion subject" — would then be a sentence in
10
+ * a document rather than something a compiler can hold.
11
+ *
12
+ * A brand fixes that half. `CitizenId` and `PiperPlatformSubject` are both `string`
13
+ * at runtime and are mutually unassignable at compile time, so a function that asks
14
+ * for one cannot be handed the other, and there is no widening that quietly works.
15
+ *
16
+ * It is only half, deliberately. A brand is erased, so it says nothing about a value
17
+ * that arrived over a wire — which is why every branded type in `ids.ts` also has a
18
+ * parser, and why the parser is the layer a reviewer should read second.
19
+ */
20
+ export {};
@@ -0,0 +1,158 @@
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 type { Millis, VerificationMethod } from "@cayso/contracts";
37
+ import { type ApplicationSummary, type ApprovedApplication } from "./application.js";
38
+ import { type CitizenId, type CommunityEmailAddress, type DisplayHandle, type VerificationCaseId } from "./ids.js";
39
+ /**
40
+ * The Citizen statuses of §5.2 — three, and the third one's meaning is not settled.
41
+ *
42
+ * `closed` is carried because the canon carries it, and deliberately without any
43
+ * statement of what it entails: the meaning and duration of `closed` is a
44
+ * Community-wide question that has not been settled. Until it is, closure is
45
+ * reversible by policy — so nothing in this package treats `closed` as terminal, and
46
+ * no predicate here says it is.
47
+ */
48
+ export type CitizenStatus = "active" | "suspended" | "closed";
49
+ /** Every Citizen status, in §5.2's order. */
50
+ export declare const CITIZEN_STATUSES: readonly CitizenStatus[];
51
+ /**
52
+ * A Citizen status from a wire value, or a refusal.
53
+ *
54
+ * `pending` gets its own refusal rather than falling through the general one. The
55
+ * general message would say a value was outside a closed union, which is true and
56
+ * unhelpful; this one says where pending belongs, because a caller sending it has
57
+ * almost certainly read an Application state and written it into a Citizen field.
58
+ */
59
+ export declare const parseCitizenStatus: (value: unknown, field?: string) => CitizenStatus;
60
+ /**
61
+ * The Citizen record of §15, and nothing its "must not hold" column forbids.
62
+ *
63
+ * `assuranceProfileVersion` is a `string` because that is what
64
+ * `AssertionClaims.assuranceProfileVersion` is in `@cayso/contracts` — narrowing it
65
+ * to a union here would be this package inventing a rule about another pillar's
66
+ * field, and the one profile version that exists today is Cayso's value to publish,
67
+ * not ours to copy. `verificationMethod` is Cayso's `VerificationMethod` for the
68
+ * same reason, imported rather than restated: two closed unions describing one
69
+ * vocabulary diverge on the first addition, and the one that diverges silently is
70
+ * the one nobody is looking at.
71
+ */
72
+ export interface CitizenRecord {
73
+ readonly citizenId: CitizenId;
74
+ readonly status: CitizenStatus;
75
+ /** The verification case that justified approval. §15 requires the Citizen to be able to name it. */
76
+ readonly approvedVerificationCaseId: VerificationCaseId;
77
+ /** The assurance profile the approval was assessed under. Cayso's field, Cayso's vocabulary. */
78
+ readonly assuranceProfileVersion: string;
79
+ /** How the human was verified. Never widened, never inferred from the Citizen's age or activity. */
80
+ readonly verificationMethod: VerificationMethod;
81
+ readonly createdAt: Millis;
82
+ }
83
+ /** A `CitizenRecord` from a wire value, or a refusal. */
84
+ export declare const parseCitizenRecord: (value: unknown, field?: string) => CitizenRecord;
85
+ /**
86
+ * What a Citizen is called, as distinct from what a Citizen IS.
87
+ *
88
+ * §14.4: "A Handle or email address never appears as a Cayso ledger identity and is
89
+ * never the stable Citizen ID." The three identifiers here are three different
90
+ * branded types, so the one that may be an assertion subject cannot be substituted
91
+ * by either of the others, in either direction, at any call site — and the Handle
92
+ * and the address are mutually unassignable too, which stops the other likely slip.
93
+ *
94
+ * They are separate fields rather than one identity blob because §15 keeps them in
95
+ * separate tables with their own state and change history: a Handle is renameable,
96
+ * and the address is its **own** identifier with its own lifecycle. A consumer that
97
+ * cached this record cached a name, not a Citizen.
98
+ */
99
+ export interface CitizenNames {
100
+ readonly citizenId: CitizenId;
101
+ readonly handle: DisplayHandle;
102
+ readonly communityEmailAddress: CommunityEmailAddress;
103
+ }
104
+ /** A `CitizenNames` from a wire value, or a refusal. */
105
+ export declare const parseCitizenNames: (value: unknown, field?: string) => CitizenNames;
106
+ /**
107
+ * The only shape from which a Citizen record may be created.
108
+ *
109
+ * The `approvedApplication` field is required and its type cannot be satisfied by an
110
+ * Application in any other state, which is §14.4's "No route, script, production
111
+ * fixture or flag creates a Citizen outside an Application" made structural. A
112
+ * fixture that wants a Citizen must produce an approved Application first; there is
113
+ * no shorter path in this package, and adding one would be visible as a new exported
114
+ * type rather than as a flag.
115
+ *
116
+ * It deliberately carries no status. A newly created Citizen is `active` — §5.2
117
+ * leaves no other reading, since `suspended` and `closed` are consequences of later
118
+ * decisions — and letting a caller choose would let a creation path produce a
119
+ * Citizen who was born suspended.
120
+ */
121
+ export interface CitizenCreation {
122
+ readonly approvedApplication: ApprovedApplication;
123
+ readonly citizenId: CitizenId;
124
+ readonly assuranceProfileVersion: string;
125
+ readonly verificationMethod: VerificationMethod;
126
+ }
127
+ /**
128
+ * A `CitizenCreation` from a wire value, or a refusal.
129
+ *
130
+ * The refusal a caller is most likely to meet here is the `ContractParseError`
131
+ * `parseApprovedApplication` raises: `approvedApplication.state: must be approved`.
132
+ */
133
+ export declare const parseCitizenCreation: (value: unknown, field?: string) => CitizenCreation;
134
+ /**
135
+ * Where a person stands, as one discriminated union rather than three flags.
136
+ *
137
+ * §5.1 draws exactly this: `Visitor ──application created──▶ Applicant ──approval──▶
138
+ * Citizen`. Modelling it as a union is what makes "an Applicant is not a partial
139
+ * Citizen" hold at compile time — the `applicant` branch has no Citizen field to
140
+ * leave null, the `citizen` branch has no Application state to inspect, and no value
141
+ * can be in two branches at once. Three optional fields on one object would have
142
+ * permitted all eight combinations, including the partial Citizen §5.1 says does not
143
+ * exist.
144
+ *
145
+ * The `visitor` branch carries nothing at all, deliberately: a Visitor is a person
146
+ * this pillar knows nothing about, and a Visitor identifier would be the first step
147
+ * towards knowing something it has no basis to record.
148
+ */
149
+ export type Personhood = {
150
+ readonly stage: "visitor";
151
+ } | {
152
+ readonly stage: "applicant";
153
+ readonly application: ApplicationSummary;
154
+ } | {
155
+ readonly stage: "citizen";
156
+ readonly citizen: CitizenRecord;
157
+ readonly names: CitizenNames;
158
+ };