@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/refusal.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The closed refusal union: what a caller is told when this pillar says no, and
|
|
3
|
+
* everything it is deliberately not told.
|
|
4
|
+
*
|
|
5
|
+
* §14.4 sets both halves of the requirement, and they pull against each other. "A
|
|
6
|
+
* Citizen sees actionable, accessible status and refusal text without exposure of
|
|
7
|
+
* internal risk signals." Actionable, so a refusal cannot be an opaque failure;
|
|
8
|
+
* without internal risk signals, so it cannot be the reason. A closed union of codes
|
|
9
|
+
* satisfies both: the code says what the caller may do next, and the consumer
|
|
10
|
+
* renders the text — accessibly, in the Citizen's language, at the right reading
|
|
11
|
+
* level — because a wire string can be none of those things.
|
|
12
|
+
*
|
|
13
|
+
* ENFORCED TWICE, AND THE SECOND HALF IS THE INTERESTING ONE.
|
|
14
|
+
*
|
|
15
|
+
* `Refusal` has no message field, no detail field and no reason field. There is
|
|
16
|
+
* nowhere to put prose, so no handler can attach a provider's explanation to a
|
|
17
|
+
* refusal on its way out; that is the type half, and it is the half that survives a
|
|
18
|
+
* hurried afternoon. `parseRefusal` is the decoder half and refuses those field
|
|
19
|
+
* names explicitly rather than by the general unknown-key rule, because a caller
|
|
20
|
+
* sending `reason` has a provider payload in their hand and needs to be told why it
|
|
21
|
+
* stops here.
|
|
22
|
+
*
|
|
23
|
+
* WHY THE UNION DOES NOT ENUMERATE.
|
|
24
|
+
*
|
|
25
|
+
* Several members cover more than one underlying cause on purpose. `not_permitted`
|
|
26
|
+
* answers both "this is not yours" and "this does not exist", because two codes
|
|
27
|
+
* would make the refusal an existence oracle for anything addressed by identifier.
|
|
28
|
+
* `onboarding_result_not_redeemable` covers every one of §14.4's five cases — a
|
|
29
|
+
* second redemption, another client, expiry, an altered Citizen, a different
|
|
30
|
+
* transaction verifier — as one member, because telling an attacker WHICH of the
|
|
31
|
+
* five they tripped is how they work out what to change. The union cannot express
|
|
32
|
+
* the distinction, which is stronger than a convention that it should not be
|
|
33
|
+
* exposed.
|
|
34
|
+
*
|
|
35
|
+
* A REFUSAL IS NOT A PARSE ERROR. `ContractParseError` in `parse.ts` is detailed
|
|
36
|
+
* because it is private to a consumer's own process. A `Refusal` crosses a network
|
|
37
|
+
* boundary and carries a code and a correlation reference, and nothing else. Joining
|
|
38
|
+
* them up — returning a parse message to a caller — would leak exactly what §14.4's
|
|
39
|
+
* disclosure criterion protects.
|
|
40
|
+
*/
|
|
41
|
+
import { parseCorrelationReference } from "./ids.js";
|
|
42
|
+
import { readClosedRecord, readString, refuseValue } from "./parse.js";
|
|
43
|
+
/** Every refusal code. A consumer that must render them all reads this. */
|
|
44
|
+
export const REFUSAL_CODES = Object.freeze([
|
|
45
|
+
"unauthenticated",
|
|
46
|
+
"step_up_required",
|
|
47
|
+
"not_permitted",
|
|
48
|
+
"state_not_permitted",
|
|
49
|
+
"input_not_acceptable",
|
|
50
|
+
"handle_unavailable",
|
|
51
|
+
"document_already_bound",
|
|
52
|
+
"evidence_insufficient",
|
|
53
|
+
"onboarding_result_not_redeemable",
|
|
54
|
+
"rate_limited",
|
|
55
|
+
"temporarily_unavailable",
|
|
56
|
+
]);
|
|
57
|
+
/**
|
|
58
|
+
* The refusals a person may appeal, per §8.6's route.
|
|
59
|
+
*
|
|
60
|
+
* `document_already_bound` is here because §14.4 names it: "Reuse of a document
|
|
61
|
+
* identifier already bound to an active Citizen is refused, and the refusal is
|
|
62
|
+
* appealable", and §16's R7 says why — a false duplicate refusal permanently
|
|
63
|
+
* excludes a legitimate person if the appeal cannot override the control itself.
|
|
64
|
+
* `evidence_insufficient` is here because an appeal is reconsideration of a
|
|
65
|
+
* decision about evidence.
|
|
66
|
+
*
|
|
67
|
+
* The others are not appealable and saying so is not a diminishment: appealing
|
|
68
|
+
* `rate_limited` is waiting, and appealing `unauthenticated` is signing in. An
|
|
69
|
+
* Application REJECTION is a lifecycle state (§5.2's `rejected --> under_appeal`)
|
|
70
|
+
* rather than a refusal code, and its appeal route is that transition.
|
|
71
|
+
*/
|
|
72
|
+
const APPEALABLE_REFUSALS = Object.freeze([
|
|
73
|
+
"document_already_bound",
|
|
74
|
+
"evidence_insufficient",
|
|
75
|
+
]);
|
|
76
|
+
/** Whether §8.6's appeal route applies to this refusal. */
|
|
77
|
+
export const isAppealable = (code) => APPEALABLE_REFUSALS.includes(code);
|
|
78
|
+
const REFUSAL_FIELDS = Object.freeze([
|
|
79
|
+
"refusal",
|
|
80
|
+
"correlationReference",
|
|
81
|
+
]);
|
|
82
|
+
/**
|
|
83
|
+
* Fields that would turn a refusal into a disclosure. Refused by name so the
|
|
84
|
+
* message can say why, rather than reporting an unrecognised key.
|
|
85
|
+
*/
|
|
86
|
+
const DISCLOSING_FIELDS = Object.freeze([
|
|
87
|
+
"message",
|
|
88
|
+
"detail",
|
|
89
|
+
"details",
|
|
90
|
+
"reason",
|
|
91
|
+
"internalReason",
|
|
92
|
+
"providerReason",
|
|
93
|
+
"riskSignal",
|
|
94
|
+
"riskSignals",
|
|
95
|
+
"stack",
|
|
96
|
+
]);
|
|
97
|
+
/** A `Refusal` from a wire value, or a refusal of its own. */
|
|
98
|
+
export const parseRefusal = (value, field = "refusal") => {
|
|
99
|
+
const record = readClosedRecord(value, field, [
|
|
100
|
+
...REFUSAL_FIELDS,
|
|
101
|
+
...DISCLOSING_FIELDS,
|
|
102
|
+
]);
|
|
103
|
+
for (const key of Object.keys(record)) {
|
|
104
|
+
if (DISCLOSING_FIELDS.includes(key)) {
|
|
105
|
+
refuseValue(`${field}.${key}`, "carries free text or an internal signal. A refusal is a code and a correlation reference; the text is the consumer's to render, accessibly (§14.4)");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const code = readString(record, field, "refusal");
|
|
109
|
+
if (!REFUSAL_CODES.includes(code)) {
|
|
110
|
+
refuseValue(`${field}.refusal`, `must be one of the ${REFUSAL_CODES.length} refusal codes of this contract`);
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
refusal: code,
|
|
114
|
+
correlationReference: parseCorrelationReference(readString(record, field, "correlationReference"), `${field}.correlationReference`),
|
|
115
|
+
};
|
|
116
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@percayso/identity-contracts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Percayso ID wire types: identifiers, closed lifecycle states, the closed refusal union, the evidence vocabulary and a thin client.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Percayso-Community/identity.platform.git",
|
|
9
|
+
"directory": "packages/identity-contracts"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./client": {
|
|
21
|
+
"types": "./dist/client.d.ts",
|
|
22
|
+
"default": "./dist/client.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public",
|
|
32
|
+
"registry": "https://registry.npmjs.org"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
36
|
+
"lint": "eslint .",
|
|
37
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
38
|
+
"test": "vitest run"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@cayso/contracts": "0.6.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@cayso/contracts": "0.6.1",
|
|
45
|
+
"typescript": "6.0.3",
|
|
46
|
+
"vitest": "5.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|