@real-life/trust-protocol 0.3.1 → 0.4.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/CHANGELOG.md +32 -0
- package/README.md +187 -78
- package/dist/ack.d.ts +84 -0
- package/dist/ack.d.ts.map +1 -0
- package/dist/ack.js +114 -0
- package/dist/ack.js.map +1 -0
- package/dist/adapters/vti-mediator.d.ts +154 -0
- package/dist/adapters/vti-mediator.d.ts.map +1 -0
- package/dist/adapters/vti-mediator.js +621 -0
- package/dist/adapters/vti-mediator.js.map +1 -0
- package/dist/anchor.d.ts +226 -0
- package/dist/anchor.d.ts.map +1 -0
- package/dist/anchor.js +347 -0
- package/dist/anchor.js.map +1 -0
- package/dist/{holder.d.ts → carrier-holder.d.ts} +1 -1
- package/dist/carrier-holder.d.ts.map +1 -0
- package/dist/{holder.js → carrier-holder.js} +5 -4
- package/dist/carrier-holder.js.map +1 -0
- package/dist/carrier-identity.js +4 -4
- package/dist/carrier-identity.js.map +1 -1
- package/dist/carrier.d.ts.map +1 -1
- package/dist/carrier.js +4 -3
- package/dist/carrier.js.map +1 -1
- package/dist/ceremony.d.ts +3 -2
- package/dist/ceremony.d.ts.map +1 -1
- package/dist/ceremony.js +16 -26
- package/dist/ceremony.js.map +1 -1
- package/dist/codec.d.ts +46 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +410 -0
- package/dist/codec.js.map +1 -0
- package/dist/continuity.d.ts +67 -0
- package/dist/continuity.d.ts.map +1 -0
- package/dist/continuity.js +110 -0
- package/dist/continuity.js.map +1 -0
- package/dist/core.d.ts +4 -0
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js.map +1 -1
- package/dist/crypto.d.ts +23 -7
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +54 -20
- package/dist/crypto.js.map +1 -1
- package/dist/delivery.d.ts +31 -3
- package/dist/delivery.d.ts.map +1 -1
- package/dist/delivery.js +75 -28
- package/dist/delivery.js.map +1 -1
- package/dist/encounter/ceremony.d.ts +37 -0
- package/dist/encounter/ceremony.d.ts.map +1 -0
- package/dist/encounter/ceremony.js +662 -0
- package/dist/encounter/ceremony.js.map +1 -0
- package/dist/encounter.d.ts +91 -1
- package/dist/encounter.d.ts.map +1 -1
- package/dist/encounter.js +191 -12
- package/dist/encounter.js.map +1 -1
- package/dist/engine.d.ts +291 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +1395 -0
- package/dist/engine.js.map +1 -0
- package/dist/entropy.d.ts +28 -0
- package/dist/entropy.d.ts.map +1 -0
- package/dist/entropy.js +96 -0
- package/dist/entropy.js.map +1 -0
- package/dist/events.d.ts +177 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +2 -0
- package/dist/events.js.map +1 -0
- package/dist/fixtures.d.ts +30 -0
- package/dist/fixtures.d.ts.map +1 -0
- package/dist/fixtures.js +155 -0
- package/dist/fixtures.js.map +1 -0
- package/dist/forge.d.ts +47 -0
- package/dist/forge.d.ts.map +1 -0
- package/dist/forge.js +20 -0
- package/dist/forge.js.map +1 -0
- package/dist/group-star.d.ts +251 -0
- package/dist/group-star.d.ts.map +1 -0
- package/dist/group-star.js +348 -0
- package/dist/group-star.js.map +1 -0
- package/dist/holder-api.d.ts +137 -0
- package/dist/holder-api.d.ts.map +1 -0
- package/dist/holder-api.js +132 -0
- package/dist/holder-api.js.map +1 -0
- package/dist/holder-state-v1.d.ts +801 -0
- package/dist/holder-state-v1.d.ts.map +1 -0
- package/dist/holder-state-v1.js +2 -0
- package/dist/holder-state-v1.js.map +1 -0
- package/dist/identity-state.d.ts +43 -0
- package/dist/identity-state.d.ts.map +1 -0
- package/dist/identity-state.js +345 -0
- package/dist/identity-state.js.map +1 -0
- package/dist/identity.d.ts +24 -2
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +70 -11
- package/dist/identity.js.map +1 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +77 -29
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +36 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +22 -0
- package/dist/internal.js.map +1 -0
- package/dist/introduction/holder.d.ts +18 -0
- package/dist/introduction/holder.d.ts.map +1 -0
- package/dist/introduction/holder.js +757 -0
- package/dist/introduction/holder.js.map +1 -0
- package/dist/introduction.d.ts +101 -0
- package/dist/introduction.d.ts.map +1 -0
- package/dist/introduction.js +132 -0
- package/dist/introduction.js.map +1 -0
- package/dist/invariants.d.ts +43 -0
- package/dist/invariants.d.ts.map +1 -0
- package/dist/invariants.js +883 -0
- package/dist/invariants.js.map +1 -0
- package/dist/keys-seed.d.ts +7 -0
- package/dist/keys-seed.d.ts.map +1 -0
- package/dist/keys-seed.js +133 -0
- package/dist/keys-seed.js.map +1 -0
- package/dist/log-memory.d.ts +12 -0
- package/dist/log-memory.d.ts.map +1 -0
- package/dist/log-memory.js +101 -0
- package/dist/log-memory.js.map +1 -0
- package/dist/membership/holder.d.ts +35 -0
- package/dist/membership/holder.d.ts.map +1 -0
- package/dist/membership/holder.js +852 -0
- package/dist/membership/holder.js.map +1 -0
- package/dist/membership.d.ts +92 -0
- package/dist/membership.d.ts.map +1 -0
- package/dist/membership.js +157 -0
- package/dist/membership.js.map +1 -0
- package/dist/ports.d.ts +237 -0
- package/dist/ports.d.ts.map +1 -0
- package/dist/ports.js +2 -0
- package/dist/ports.js.map +1 -0
- package/dist/profile.d.ts +56 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +193 -0
- package/dist/profile.js.map +1 -0
- package/dist/prompts.d.ts +14 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +69 -0
- package/dist/prompts.js.map +1 -0
- package/dist/receive.d.ts +133 -0
- package/dist/receive.d.ts.map +1 -0
- package/dist/receive.js +298 -0
- package/dist/receive.js.map +1 -0
- package/dist/results.d.ts +158 -0
- package/dist/results.d.ts.map +1 -0
- package/dist/results.js +21 -0
- package/dist/results.js.map +1 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +42 -38
- package/dist/schemas.js.map +1 -1
- package/dist/sha256.d.ts +3 -0
- package/dist/sha256.d.ts.map +1 -0
- package/dist/sha256.js +69 -0
- package/dist/sha256.js.map +1 -0
- package/dist/star.d.ts +31 -0
- package/dist/star.d.ts.map +1 -0
- package/dist/star.js +56 -0
- package/dist/star.js.map +1 -0
- package/dist/state-builder.d.ts +42 -0
- package/dist/state-builder.d.ts.map +1 -0
- package/dist/state-builder.js +100 -0
- package/dist/state-builder.js.map +1 -0
- package/dist/state.d.ts +853 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +36 -0
- package/dist/state.js.map +1 -0
- package/dist/store-contract.d.ts +11 -0
- package/dist/store-contract.d.ts.map +1 -0
- package/dist/store-contract.js +189 -0
- package/dist/store-contract.js.map +1 -0
- package/dist/store-memory.d.ts +8 -0
- package/dist/store-memory.d.ts.map +1 -0
- package/dist/store-memory.js +17 -0
- package/dist/store-memory.js.map +1 -0
- package/dist/testing.d.ts +22 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +21 -0
- package/dist/testing.js.map +1 -0
- package/dist/trust/continuity.d.ts +71 -0
- package/dist/trust/continuity.d.ts.map +1 -0
- package/dist/trust/continuity.js +1216 -0
- package/dist/trust/continuity.js.map +1 -0
- package/dist/trust/duties.d.ts +67 -0
- package/dist/trust/duties.d.ts.map +1 -0
- package/dist/trust/duties.js +179 -0
- package/dist/trust/duties.js.map +1 -0
- package/dist/trust/entries.d.ts +139 -0
- package/dist/trust/entries.d.ts.map +1 -0
- package/dist/trust/entries.js +694 -0
- package/dist/trust/entries.js.map +1 -0
- package/dist/trust/group-star.d.ts +43 -0
- package/dist/trust/group-star.d.ts.map +1 -0
- package/dist/trust/group-star.js +188 -0
- package/dist/trust/group-star.js.map +1 -0
- package/dist/trust/mapping.d.ts +56 -0
- package/dist/trust/mapping.d.ts.map +1 -0
- package/dist/trust/mapping.js +584 -0
- package/dist/trust/mapping.js.map +1 -0
- package/dist/trust/profile.d.ts +25 -0
- package/dist/trust/profile.d.ts.map +1 -0
- package/dist/trust/profile.js +249 -0
- package/dist/trust/profile.js.map +1 -0
- package/dist/trust/star.d.ts +208 -0
- package/dist/trust/star.d.ts.map +1 -0
- package/dist/trust/star.js +588 -0
- package/dist/trust/star.js.map +1 -0
- package/dist/trust/state.d.ts +83 -0
- package/dist/trust/state.d.ts.map +1 -0
- package/dist/trust/state.js +154 -0
- package/dist/trust/state.js.map +1 -0
- package/dist/util-freeze.d.ts +3 -0
- package/dist/util-freeze.d.ts.map +1 -0
- package/dist/util-freeze.js +11 -0
- package/dist/util-freeze.js.map +1 -0
- package/dist/views.d.ts +147 -0
- package/dist/views.d.ts.map +1 -0
- package/dist/views.js +268 -0
- package/dist/views.js.map +1 -0
- package/dist/visibility.d.ts +5 -5
- package/dist/visibility.d.ts.map +1 -1
- package/dist/visibility.js +22 -28
- package/dist/visibility.js.map +1 -1
- package/dist/wire.d.ts +178 -84
- package/dist/wire.d.ts.map +1 -1
- package/dist/wire.js.map +1 -1
- package/package.json +58 -11
- package/schemas/holder-state-v1.schema.json +3880 -0
- package/dist/holder.d.ts.map +0 -1
- package/dist/holder.js.map +0 -1
- package/dist/probe/acks.d.ts +0 -27
- package/dist/probe/acks.d.ts.map +0 -1
- package/dist/probe/acks.js +0 -152
- package/dist/probe/acks.js.map +0 -1
- package/dist/probe/continuity.d.ts +0 -20
- package/dist/probe/continuity.d.ts.map +0 -1
- package/dist/probe/continuity.js +0 -805
- package/dist/probe/continuity.js.map +0 -1
- package/dist/probe/deps.d.ts +0 -61
- package/dist/probe/deps.d.ts.map +0 -1
- package/dist/probe/deps.js +0 -54
- package/dist/probe/deps.js.map +0 -1
- package/dist/probe/encounter.d.ts +0 -26
- package/dist/probe/encounter.d.ts.map +0 -1
- package/dist/probe/encounter.js +0 -938
- package/dist/probe/encounter.js.map +0 -1
- package/dist/probe/introduce.d.ts +0 -70
- package/dist/probe/introduce.d.ts.map +0 -1
- package/dist/probe/introduce.js +0 -295
- package/dist/probe/introduce.js.map +0 -1
- package/dist/probe/membership.d.ts +0 -90
- package/dist/probe/membership.d.ts.map +0 -1
- package/dist/probe/membership.js +0 -450
- package/dist/probe/membership.js.map +0 -1
- package/dist/probe/trust.d.ts +0 -156
- package/dist/probe/trust.d.ts.map +0 -1
- package/dist/probe/trust.js +0 -1233
- package/dist/probe/trust.js.map +0 -1
- package/dist/probe.d.ts +0 -7
- package/dist/probe.d.ts.map +0 -1
- package/dist/probe.js +0 -42
- package/dist/probe.js.map +0 -1
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
// encounter/ceremony — the Encounter ceremony on the holder (lib Stage 2,
|
|
2
|
+
// PR 7a; Encounter 0.31 §5–§7 on Delivery 0.81 §4.1–4.3). Graduated from
|
|
3
|
+
// probe/encounter.ts (show, scan, counter, captureSentCard,
|
|
4
|
+
// receiveEncounter, resolve) onto HolderState and the engine; the probe
|
|
5
|
+
// stays as it is for the simulators until PR 7b. The pure checks are the
|
|
6
|
+
// root module's (encounter.ts).
|
|
7
|
+
//
|
|
8
|
+
// What it registers (encounterModule):
|
|
9
|
+
// human acts encounter.show a displayed card under a fresh pair
|
|
10
|
+
// label with a fresh own challenge (5.3):
|
|
11
|
+
// the label and its relationship nonce
|
|
12
|
+
// into the register before display (4110,
|
|
13
|
+
// Identity 6.1), the key's lastShown in
|
|
14
|
+
// `addresses` (Delivery §5), the challenge
|
|
15
|
+
// held open; event encounter.shown carries
|
|
16
|
+
// the card, the optical payload
|
|
17
|
+
// encounter.scan the scanner's step (5.8 trunk 2–3):
|
|
18
|
+
// the displayed card checked, a fresh
|
|
19
|
+
// label and own challenge c_A, the record
|
|
20
|
+
// (5.5), the step credential and the sent
|
|
21
|
+
// card, the fresh tuple (Trust openTuple:
|
|
22
|
+
// its prior candidates frozen, its
|
|
23
|
+
// continuity probe pending, 6820) and the
|
|
24
|
+
// bundle into the outbox, in one commit;
|
|
25
|
+
// event encounter.scanned carries the
|
|
26
|
+
// sent card for the optical leg
|
|
27
|
+
// encounter.capture the optical leg (5.8, 5710): a sent
|
|
28
|
+
// card presented to the displayer
|
|
29
|
+
// encounter.counter the counter-step (5.8 step 4)
|
|
30
|
+
// tasks encounter-bundle/0.1 (proof absent, Delivery 4.1; channel
|
|
31
|
+
// binding: the bundle establishes the tuple) and
|
|
32
|
+
// encounter-credential-delivery/0.1 (proof absent, 4.3;
|
|
33
|
+
// the tuple binding); both signature-class: their acks are
|
|
34
|
+
// signed (receive.ts)
|
|
35
|
+
// decision encounter-confirm: accept issues the counter-step, decline
|
|
36
|
+
// closes the prompt (the edge stays one-sided, 5750)
|
|
37
|
+
// retain tick drops the challenges past their age bound (5200,
|
|
38
|
+
// 5210, 5230)
|
|
39
|
+
//
|
|
40
|
+
// The commit discipline of the probe (prepare, verify, commit under a
|
|
41
|
+
// lock set) is the engine's: preparation is async against a snapshot,
|
|
42
|
+
// apply is synchronous on that same snapshot, and the commit is a CAS on
|
|
43
|
+
// its version (engine.ts). A state that moved is a CAS conflict and the
|
|
44
|
+
// receive re-enters at stage 1, which re-checks the effect cache at stage
|
|
45
|
+
// 4 (Delivery 6.2's waiter rule). So the provisional resolution of 4.1
|
|
46
|
+
// and the authoritative one coincide: apply resolves once, on the
|
|
47
|
+
// snapshot the commit lands on, and selects the branch.
|
|
48
|
+
//
|
|
49
|
+
// Choices this file makes where spec and note leave them open:
|
|
50
|
+
// - ids: the contact and the relationship of a ceremony-born tuple are
|
|
51
|
+
// named after the own fresh pair label, `c-<digest>` and `r-<digest>`
|
|
52
|
+
// (the label's digest component): unique by freshness (4.4)
|
|
53
|
+
// - the aging latch (5260, 5270): an aged value is removed (5210, 5230:
|
|
54
|
+
// no challenge history beyond open values and records), so it never
|
|
55
|
+
// resolves open again. tick removes every aged value; a bundle or an
|
|
56
|
+
// optical card refused because its authoritative resolution found its
|
|
57
|
+
// bound value aged commits that removal and the raised lastWhen even
|
|
58
|
+
// though it refuses — the Encounter latch exception of the engine
|
|
59
|
+
// (receive.ts TaskRefusal.latch): no cache entry, no ack, no journal
|
|
60
|
+
// - the scanner's own challenge c_A goes straight to its record: the
|
|
61
|
+
// record and the issuance commit together (5630: record, then issue);
|
|
62
|
+
// nothing is released before the commit, so "record before issuing"
|
|
63
|
+
// holds and no resumable half-state exists (the probe's bundlePending)
|
|
64
|
+
// - the bundle's effect on `open` creates the record, accepts the
|
|
65
|
+
// credential, opens the fresh tuple and the confirm prompt in one
|
|
66
|
+
// commit; the prompt is "arrival is not acceptance: this device
|
|
67
|
+
// decides" — accept issues the counter-step. It expires when the
|
|
68
|
+
// counter credential could no longer be accepted: the end of the
|
|
69
|
+
// issuance window of the challenge it binds, c_A (5.6 step 6)
|
|
70
|
+
// - the credential delivery's defined effect (4.3, buffering) and the
|
|
71
|
+
// 5.6 acceptance commit together; the ack does not depend on the
|
|
72
|
+
// acceptance's outcome and the outcome is never signaled (7.4): the
|
|
73
|
+
// document is acknowledged and its thread reserved either way
|
|
74
|
+
// - encounter.counter picks the newest record of the contact on the
|
|
75
|
+
// displayer side whose own credential is not issued; a new issuance
|
|
76
|
+
// needs an active tuple (tuple-deactivated), its document waits for
|
|
77
|
+
// the bundle's thread where the record arose on the optical leg (5780)
|
|
78
|
+
// and goes out with the bundle's record-aware effect
|
|
79
|
+
// - encounter.capture of a card already recorded with the same material
|
|
80
|
+
// is an idempotent success (5430): the act commits its journal entry only
|
|
81
|
+
// - no automatic optical switch (5690 RECOMMENDS ack-wait as its
|
|
82
|
+
// trigger): the app shows the sent card (encounter.scanned) when it
|
|
83
|
+
// sees the bundle undelivered (deliveryStatus)
|
|
84
|
+
import { b64uOf, iso, jcs, makeValidator, sameDigest } from '../core.js';
|
|
85
|
+
import { diSignWith, diVerify, digestBytes, digestDoc } from '../crypto.js';
|
|
86
|
+
import { seal } from '../delivery.js';
|
|
87
|
+
import { BUNDLE_TYPE, CEREMONY, CHALLENGE_MAX_AGE, SKEW_TOLERANCE, DELIVERY_TYPE, acceptTail, bundleForm, cardBody, cardOk, challengeAged, challengeOf, credHead, credentialBody, deliveryForm, dispositionOf, futureOk, issuanceEnd, issuanceOpen, windowOk, } from '../encounter.js';
|
|
88
|
+
import { SCHEMAS } from '../schemas.js';
|
|
89
|
+
import { withPairNonce } from '../identity-state.js';
|
|
90
|
+
import { tupleByRkid } from '../receive.js';
|
|
91
|
+
import { openTuple } from '../trust/continuity.js';
|
|
92
|
+
import { entryOf, findContact, tupleByLabel } from '../trust/state.js';
|
|
93
|
+
export { BUNDLE_TYPE, DELIVERY_TYPE };
|
|
94
|
+
/** The command names of the human acts. */
|
|
95
|
+
export const ENCOUNTER_COMMANDS = Object.freeze({
|
|
96
|
+
show: 'encounter.show',
|
|
97
|
+
scan: 'encounter.scan',
|
|
98
|
+
capture: 'encounter.capture',
|
|
99
|
+
counter: 'encounter.counter',
|
|
100
|
+
});
|
|
101
|
+
const V = makeValidator(SCHEMAS);
|
|
102
|
+
const schemaOk = (file, data) => V.validate(data, SCHEMAS[file], SCHEMAS[file]).length === 0;
|
|
103
|
+
const refuse = (reason, ref) => ({ ok: false, reason, ...(ref ? { ref } : {}) });
|
|
104
|
+
const no = (code, ref) => ({ ok: false, code, ...(ref ? { ref } : {}) });
|
|
105
|
+
const isRefusal = (x) => x?.ok === false;
|
|
106
|
+
/** The contact and relationship ids of the tuple under an own pair label. */
|
|
107
|
+
export const idsOf = (label) => {
|
|
108
|
+
const d = label.slice('pair/'.length);
|
|
109
|
+
return { contactId: 'c-' + d, relId: 'r-' + d };
|
|
110
|
+
};
|
|
111
|
+
/** The prompt that asks to confirm the enactment of record `own`. */
|
|
112
|
+
export const confirmPromptId = (own) => 'encounter:' + own;
|
|
113
|
+
/** The own-challenge resolution (5240): recorded, open, else unknown. */
|
|
114
|
+
export function resolve(state, value, when) {
|
|
115
|
+
if (Object.hasOwn(state.encounter.records, value))
|
|
116
|
+
return 'recorded';
|
|
117
|
+
const c = Object.hasOwn(state.encounter.challenges, value) ? state.encounter.challenges[value] : undefined;
|
|
118
|
+
return !c || challengeAged(c.tCh, when) ? 'unknown' : 'open';
|
|
119
|
+
}
|
|
120
|
+
/** The aging latch (5260, 5270): where a resolution finds a held value
|
|
121
|
+
* past its age bound, the state with that value removed — committed even
|
|
122
|
+
* by a refusal (receive.ts, the Encounter latch exception), so a clock
|
|
123
|
+
* that moves back never resolves it open again; else undefined. */
|
|
124
|
+
function latchOf(state, value, when) {
|
|
125
|
+
const c = Object.hasOwn(state.encounter.challenges, value) ? state.encounter.challenges[value] : undefined;
|
|
126
|
+
if (!c || !challengeAged(c.tCh, when) || Object.hasOwn(state.encounter.records, value))
|
|
127
|
+
return undefined;
|
|
128
|
+
const challenges = { ...state.encounter.challenges };
|
|
129
|
+
delete challenges[value];
|
|
130
|
+
return { ...state, encounter: { ...state.encounter, challenges } };
|
|
131
|
+
}
|
|
132
|
+
/** 5170: a challenge value consumed or held — on either side of any
|
|
133
|
+
* record, or among the own open challenges — enters no new enactment. */
|
|
134
|
+
const challengeUsed = (state, value) => Object.hasOwn(state.encounter.challenges, value) || Object.values(state.encounter.records).some((r) => r.own === value || r.peer === value);
|
|
135
|
+
const facts = (r) => ({ counterpart: r.counterpart, ceremony: r.ceremony, tCh: r.tCh, bind: r.bind });
|
|
136
|
+
/** Is the thread used: by a record (its bundle's or a later one) or the outbox? (Delivery 3, 4.1) */
|
|
137
|
+
function threadInUse(state, id) {
|
|
138
|
+
for (const r of Object.values(state.encounter.records))
|
|
139
|
+
if (r.thread === id || Object.hasOwn(r.threads, id))
|
|
140
|
+
return true;
|
|
141
|
+
return Object.values(state.outbox).some((o) => o.threadId === id);
|
|
142
|
+
}
|
|
143
|
+
/** The original bundle of a thread, re-evaluated (4.2: after the cache's
|
|
144
|
+
* retention) or re-issued on its own fresh thread: not a new document on
|
|
145
|
+
* a used thread. */
|
|
146
|
+
function sameOpeningBundle(state, anchor, threadId, digest) {
|
|
147
|
+
for (const r of Object.values(state.encounter.records)) {
|
|
148
|
+
if (r.counterpart !== anchor)
|
|
149
|
+
continue;
|
|
150
|
+
if (r.thread === threadId && r.bundleDigest !== null && sameDigest(r.bundleDigest, digest))
|
|
151
|
+
return true;
|
|
152
|
+
if (Object.hasOwn(r.threads, threadId) && sameDigest(r.threads[threadId], digest))
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
/** The record of the tuple under an own pair label. */
|
|
158
|
+
const recordOfLabel = (state, label) => Object.values(state.encounter.records).find((r) => r.label === label);
|
|
159
|
+
const withRecord = (state, r) => ({ ...state, encounter: { ...state.encounter, records: { ...state.encounter.records, [r.own]: r } } });
|
|
160
|
+
/** The entry a record's tuple feeds, and its relationship (by the tuple: a
|
|
161
|
+
* chaining may have moved it, 6220). */
|
|
162
|
+
function placeOf(state, r) {
|
|
163
|
+
const at = tupleByLabel(state, r.label);
|
|
164
|
+
if (!at)
|
|
165
|
+
return { contactId: idsOf(r.label).contactId, relationshipId: r.relId };
|
|
166
|
+
return { contactId: entryOf(state, at.rel)?.id ?? at.tuple.entryId, relationshipId: at.rel.id };
|
|
167
|
+
}
|
|
168
|
+
const sealWith = (ctx, doc, keyAgreement) => seal(doc, keyAgreement, { ephSeed: ctx.entropy.bytes('seal.eph', 32), nonce: ctx.entropy.bytes('seal.nonce', 12) }, ctx.crypto.subtle);
|
|
169
|
+
const signer = (ctx, label) => (b) => ctx.keys.sign(label, b);
|
|
170
|
+
/** The display name a card carries: the input's, else the own profile's, at most 200 characters (6090). */
|
|
171
|
+
const nameFor = (state, name) => {
|
|
172
|
+
const n = typeof name === 'string' ? name : state.profile.body?.name;
|
|
173
|
+
return typeof n === 'string' && n.length ? [...n].slice(0, 200).join('') : undefined;
|
|
174
|
+
};
|
|
175
|
+
/** A fresh pair label from 32 fresh nonce bytes (Identity 6.1, Encounter 4.4). */
|
|
176
|
+
async function freshLabel(ctx) {
|
|
177
|
+
const n = ctx.entropy.bytes('founding.nonce', 32);
|
|
178
|
+
return { label: 'pair/' + await digestBytes(n, ctx.crypto.subtle), nonce: b64uOf(n) };
|
|
179
|
+
}
|
|
180
|
+
const showDef = {
|
|
181
|
+
human: true,
|
|
182
|
+
async prepare(ctx, input) {
|
|
183
|
+
const { label, nonce } = await freshLabel(ctx);
|
|
184
|
+
const own = await ctx.keys.anchorOf(label);
|
|
185
|
+
const value = challengeOf(ctx.entropy.bytes('challenge', 17));
|
|
186
|
+
const at = iso(ctx.when);
|
|
187
|
+
const name = nameFor(ctx.snapshot, input?.name);
|
|
188
|
+
const body = cardBody(own, { ...(name !== undefined ? { name } : {}), challenge: { value, issuedAt: at } });
|
|
189
|
+
const card = await diSignWith(own.anchor, signer(ctx, label), body, at, ctx.crypto.subtle);
|
|
190
|
+
return { label, nonce, rkid: own.keyAgreement, value, card };
|
|
191
|
+
},
|
|
192
|
+
apply(state, prepared, _input, when) {
|
|
193
|
+
const p = prepared.value;
|
|
194
|
+
// fresh-always (4.4, 4150) and single use (5140): fresh entropy never
|
|
195
|
+
// repeats; a repeat is a broken entropy source, not a human's refusal
|
|
196
|
+
if (state.register.labels.includes(p.label) || tupleByLabel(state, p.label))
|
|
197
|
+
throw new Error(`encounter.show: the pair label ${p.label} is not fresh`);
|
|
198
|
+
if (resolve(state, p.value, when) !== 'unknown' || Object.hasOwn(state.encounter.challenges, p.value))
|
|
199
|
+
throw new Error('encounter.show: the challenge value is not fresh');
|
|
200
|
+
return {
|
|
201
|
+
state: {
|
|
202
|
+
...state,
|
|
203
|
+
// 4110: the label in the register before the card is displayed
|
|
204
|
+
register: withPairNonce(state.register, p.label, p.nonce),
|
|
205
|
+
addresses: { ...state.addresses, keys: { ...state.addresses.keys, [p.rkid]: { label: p.label, lastShown: when } } },
|
|
206
|
+
encounter: { ...state.encounter, challenges: { ...state.encounter.challenges, [p.value]: { value: p.value, tCh: when, label: p.label, card: p.card } } },
|
|
207
|
+
},
|
|
208
|
+
events: [{ type: 'encounter.shown', challenge: p.value, card: p.card }],
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
const scanDef = {
|
|
213
|
+
human: true,
|
|
214
|
+
async prepare(ctx, input) {
|
|
215
|
+
const peer = input?.card;
|
|
216
|
+
if ((await cardOk(peer, ctx.crypto.subtle)) !== null)
|
|
217
|
+
return no('card-invalid', 'RLTP-ENC-6010');
|
|
218
|
+
// a displayed card carries neither sentTo nor boundTo (6070)
|
|
219
|
+
if (peer.sentTo !== undefined || peer.boundTo !== undefined)
|
|
220
|
+
return no('card-invalid', 'RLTP-ENC-6070');
|
|
221
|
+
const S = ctx.crypto.subtle;
|
|
222
|
+
const { label, nonce } = await freshLabel(ctx);
|
|
223
|
+
const me = await ctx.keys.anchorOf(label);
|
|
224
|
+
// c_A: fresh, created now, never the displayed value (5150, 5160)
|
|
225
|
+
let own = challengeOf(ctx.entropy.bytes('challenge', 17));
|
|
226
|
+
for (let i = 0; own === peer.challenge.value && i < 4; i++)
|
|
227
|
+
own = challengeOf(ctx.entropy.bytes('challenge', 17));
|
|
228
|
+
if (own === peer.challenge.value)
|
|
229
|
+
throw new Error('encounter.scan: no fresh challenge distinct from the displayed one');
|
|
230
|
+
const bind = await digestDoc({ ceremony: CEREMONY, challenges: [own, peer.challenge.value].sort() }, S);
|
|
231
|
+
const at = iso(ctx.when);
|
|
232
|
+
const sign = signer(ctx, label);
|
|
233
|
+
const credential = await diSignWith(me.anchor, sign, credentialBody(me.anchor, peer.anchor, CEREMONY, peer.challenge.value, bind, at), at, S);
|
|
234
|
+
const name = nameFor(ctx.snapshot, input?.name);
|
|
235
|
+
const sent = cardBody(me, { ...(name !== undefined ? { name } : {}), challenge: { value: own, issuedAt: at }, sentTo: peer.anchor, boundTo: peer.challenge.value });
|
|
236
|
+
const sentCard = await diSignWith(me.anchor, sign, sent, at, S);
|
|
237
|
+
const threadId = ctx.entropy.id('thread.id');
|
|
238
|
+
const doc = { id: ctx.entropy.id('doc.id'), type: BUNDLE_TYPE, issuer: me.anchor, recipient: peer.anchor, threadId, issuedAt: at, payload: { card: sentCard, credential } };
|
|
239
|
+
return {
|
|
240
|
+
peer, label, nonce, rkid: me.keyAgreement, own, bind, threadId, credential, credDigest: await digestDoc(credential, S), sentCard,
|
|
241
|
+
doc: { digest: await digestDoc(doc, S), envelope: await sealWith(ctx, doc, peer.keyAgreement) },
|
|
242
|
+
};
|
|
243
|
+
},
|
|
244
|
+
apply(state, prepared, _input, when) {
|
|
245
|
+
const p = prepared.value;
|
|
246
|
+
if (isRefusal(p))
|
|
247
|
+
return p;
|
|
248
|
+
const peerValue = p.peer.challenge.value;
|
|
249
|
+
// 5.3 single use from the scanner's side: a displayed value already in
|
|
250
|
+
// one of the own records never enters a second enactment (5170)
|
|
251
|
+
if (challengeUsed(state, peerValue))
|
|
252
|
+
return no('already-scanned', 'RLTP-ENC-5170');
|
|
253
|
+
if (state.register.labels.includes(p.label) || tupleByLabel(state, p.label))
|
|
254
|
+
throw new Error(`encounter.scan: the pair label ${p.label} is not fresh`);
|
|
255
|
+
if (resolve(state, p.own, when) !== 'unknown')
|
|
256
|
+
throw new Error('encounter.scan: the own challenge is not fresh');
|
|
257
|
+
if (threadInUse(state, p.threadId))
|
|
258
|
+
throw new Error('encounter.scan: the thread is not fresh');
|
|
259
|
+
// the credential binds c_B: issued only inside its window (5.6 step 6)
|
|
260
|
+
if (!issuanceOpen(Date.parse(p.peer.challenge.issuedAt), when))
|
|
261
|
+
return no('stale-issuance', 'RLTP-ENC-5530');
|
|
262
|
+
const ids = idsOf(p.label);
|
|
263
|
+
const opened = openTuple(state, {
|
|
264
|
+
relId: ids.relId, contactId: ids.contactId, ownPairLabel: p.label, counterpartAnchor: p.peer.anchor,
|
|
265
|
+
counterpartKeyAgreement: p.peer.keyAgreement, provenance: { kind: 'ceremony' }, nonce: p.nonce,
|
|
266
|
+
}, p.rkid, when);
|
|
267
|
+
const record = {
|
|
268
|
+
ceremony: CEREMONY, own: p.own, tCh: when, peer: peerValue, counterpart: p.peer.anchor, card: p.peer, bind: p.bind,
|
|
269
|
+
at: when, label: p.label, relId: ids.relId, role: 'scanner', out: { digest: p.credDigest, credential: p.credential }, in: null,
|
|
270
|
+
thread: p.threadId, bundleDigest: p.doc.digest, threads: {}, sentCard: p.sentCard, counterPending: false,
|
|
271
|
+
};
|
|
272
|
+
return {
|
|
273
|
+
state: withRecord(opened.state, record),
|
|
274
|
+
outbound: [{
|
|
275
|
+
digest: p.doc.digest, envelope: p.doc.envelope, type: BUNDLE_TYPE, threadId: p.threadId,
|
|
276
|
+
recipient: { contactId: ids.contactId, rkid: p.rkid, keyAgreement: p.peer.keyAgreement },
|
|
277
|
+
}],
|
|
278
|
+
events: [
|
|
279
|
+
...(opened.events ?? []),
|
|
280
|
+
{ type: 'encounter.recorded', contactId: ids.contactId, relationshipId: ids.relId, role: 'scanner' },
|
|
281
|
+
{ type: 'encounter.scanned', contactId: ids.contactId, card: p.sentCard },
|
|
282
|
+
],
|
|
283
|
+
duties: opened.duties,
|
|
284
|
+
};
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
const captureDef = {
|
|
288
|
+
human: true,
|
|
289
|
+
async prepare(ctx, input) {
|
|
290
|
+
const card = input?.card;
|
|
291
|
+
if ((await cardOk(card, ctx.crypto.subtle)) !== null)
|
|
292
|
+
return no('card-invalid', 'RLTP-ENC-6010');
|
|
293
|
+
if (typeof card.sentTo !== 'string' || typeof card.boundTo !== 'string')
|
|
294
|
+
return no('card-invalid', 'RLTP-ENC-6030');
|
|
295
|
+
if (card.challenge.value === card.boundTo)
|
|
296
|
+
return no('card-invalid', 'RLTP-ENC-5160');
|
|
297
|
+
// sentTo = the own anchor that displayed boundTo (6050, 4.4)
|
|
298
|
+
const s = ctx.snapshot.encounter;
|
|
299
|
+
const label = Object.hasOwn(s.records, card.boundTo) ? s.records[card.boundTo].label
|
|
300
|
+
: Object.hasOwn(s.challenges, card.boundTo) ? s.challenges[card.boundTo].label : null;
|
|
301
|
+
if (label !== null && (await ctx.keys.anchorOf(label)).anchor !== card.sentTo)
|
|
302
|
+
return no('card-invalid', 'RLTP-ENC-6050');
|
|
303
|
+
return { card, label: label ?? '', bind: await digestDoc({ ceremony: CEREMONY, challenges: [card.boundTo, card.challenge.value].sort() }, ctx.crypto.subtle) };
|
|
304
|
+
},
|
|
305
|
+
apply(state, prepared, _input, when) {
|
|
306
|
+
const p = prepared.value;
|
|
307
|
+
if (isRefusal(p))
|
|
308
|
+
return p;
|
|
309
|
+
const { card } = p;
|
|
310
|
+
const st = resolve(state, card.boundTo, when);
|
|
311
|
+
if (st === 'unknown') {
|
|
312
|
+
const latch = latchOf(state, card.boundTo, when);
|
|
313
|
+
return { ...no('gate-expired', 'RLTP-ENC-5460'), ...(latch ? { latch } : {}) };
|
|
314
|
+
}
|
|
315
|
+
if (st === 'recorded') {
|
|
316
|
+
// the taxonomy of 5.5 (5430–5450)
|
|
317
|
+
const r = state.encounter.records[card.boundTo];
|
|
318
|
+
if (r.counterpart !== card.anchor)
|
|
319
|
+
return no('consumed-challenge', 'RLTP-ENC-5440');
|
|
320
|
+
if (jcs(r.card) !== jcs(card))
|
|
321
|
+
return no('card-mismatch', 'RLTP-ENC-5450');
|
|
322
|
+
return { state: state };
|
|
323
|
+
}
|
|
324
|
+
const ch = state.encounter.challenges[card.boundTo];
|
|
325
|
+
if (challengeUsed(state, card.challenge.value))
|
|
326
|
+
return no('consumed-challenge', 'RLTP-ENC-5170');
|
|
327
|
+
if (!futureOk(ch.tCh, when))
|
|
328
|
+
return no('gate-future', 'RLTP-ENC-5390');
|
|
329
|
+
return recordCreating(state, { label: ch.label, own: ch.value, tCh: ch.tCh, card, bind: p.bind, rkid: rkidOf(state, ch.label), when, in: null, thread: null, digest: null });
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
/** The own key-agreement id a label derives under, from `addresses`. */
|
|
333
|
+
function rkidOf(state, label) {
|
|
334
|
+
for (const [rkid, k] of Object.entries(state.addresses.keys))
|
|
335
|
+
if (k.label === label)
|
|
336
|
+
return rkid;
|
|
337
|
+
throw new Error(`encounter: no address key for ${label}`);
|
|
338
|
+
}
|
|
339
|
+
/** The displayer's record-creating effect (5.5, 5650, 5710): the record,
|
|
340
|
+
* the fresh tuple, the challenge superseded (5220), the confirm prompt. */
|
|
341
|
+
function recordCreating(state, x) {
|
|
342
|
+
const ids = idsOf(x.label);
|
|
343
|
+
const nonce = Object.hasOwn(state.register.pairNonces, x.label) ? state.register.pairNonces[x.label] : undefined;
|
|
344
|
+
const opened = openTuple(state, {
|
|
345
|
+
relId: ids.relId, contactId: ids.contactId, ownPairLabel: x.label, counterpartAnchor: x.card.anchor,
|
|
346
|
+
counterpartKeyAgreement: x.card.keyAgreement, provenance: { kind: 'ceremony' }, ...(nonce !== undefined ? { nonce } : {}),
|
|
347
|
+
}, x.rkid, x.when);
|
|
348
|
+
const record = {
|
|
349
|
+
ceremony: CEREMONY, own: x.own, tCh: x.tCh, peer: x.card.challenge.value, counterpart: x.card.anchor, card: x.card, bind: x.bind,
|
|
350
|
+
at: x.when, label: x.label, relId: ids.relId, role: 'displayer', out: null, in: x.in, thread: x.thread, bundleDigest: x.digest,
|
|
351
|
+
threads: {}, sentCard: null, counterPending: false,
|
|
352
|
+
};
|
|
353
|
+
const challenges = { ...opened.state.encounter.challenges };
|
|
354
|
+
delete challenges[x.own];
|
|
355
|
+
const prompt = {
|
|
356
|
+
id: confirmPromptId(x.own), kind: 'encounter-confirm', contactId: ids.contactId, openedAt: x.when,
|
|
357
|
+
// the counter credential binds c_A: acceptable only inside its window (5.6 step 6)
|
|
358
|
+
expiresAt: issuanceEnd(Date.parse(x.card.challenge.issuedAt)),
|
|
359
|
+
};
|
|
360
|
+
const next = withRecord({ ...opened.state, encounter: { ...opened.state.encounter, challenges } }, record);
|
|
361
|
+
return {
|
|
362
|
+
state: { ...next, prompts: { ...next.prompts, [prompt.id]: prompt } },
|
|
363
|
+
events: [
|
|
364
|
+
...(opened.events ?? []),
|
|
365
|
+
{ type: 'encounter.recorded', contactId: ids.contactId, relationshipId: ids.relId, role: 'displayer' },
|
|
366
|
+
...(x.in ? [{ type: 'encounter.accepted', contactId: ids.contactId, relationshipId: ids.relId, mutual: false }] : []),
|
|
367
|
+
{ type: 'prompt.opened', promptId: prompt.id, kind: 'encounter-confirm' },
|
|
368
|
+
],
|
|
369
|
+
duties: opened.duties,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
/** The counter-step's delivery document in the bundle's thread (4.3, 5760). */
|
|
373
|
+
async function counterDoc(ctx, r, issuer, credential, threadId) {
|
|
374
|
+
const doc = {
|
|
375
|
+
id: ctx.entropy.id('doc.id'), type: DELIVERY_TYPE, issuer, recipient: r.counterpart, threadId, issuedAt: iso(ctx.when),
|
|
376
|
+
ceremony: { step: 'counter', enactment: r.bind }, payload: { credential },
|
|
377
|
+
};
|
|
378
|
+
return { digest: await digestDoc(doc, ctx.crypto.subtle), envelope: await sealWith(ctx, doc, r.card.keyAgreement), threadId };
|
|
379
|
+
}
|
|
380
|
+
/** The displayer's record of a contact the counter-step issues on: the
|
|
381
|
+
* newest whose own credential is not issued. */
|
|
382
|
+
function counterRecord(state, contactId) {
|
|
383
|
+
const c = findContact(state, contactId);
|
|
384
|
+
if (!c)
|
|
385
|
+
return no('unknown-contact');
|
|
386
|
+
const ids = new Set([c.id, ...c.aliases]);
|
|
387
|
+
const mine = Object.values(state.encounter.records).filter((r) => r.role === 'displayer' && ids.has(placeOf(state, r).contactId));
|
|
388
|
+
if (!mine.length)
|
|
389
|
+
return no('no-record');
|
|
390
|
+
const open = mine.filter((r) => r.out === null).sort((a, b) => b.at - a.at);
|
|
391
|
+
return open.length ? open[0] : no('already-issued');
|
|
392
|
+
}
|
|
393
|
+
async function prepareCounter(ctx, r) {
|
|
394
|
+
const S = ctx.crypto.subtle;
|
|
395
|
+
const me = await ctx.keys.anchorOf(r.label);
|
|
396
|
+
const at = iso(ctx.when);
|
|
397
|
+
// the subject's challenge is c_A, the record's peer value (5320)
|
|
398
|
+
const credential = await diSignWith(me.anchor, signer(ctx, r.label), credentialBody(me.anchor, r.counterpart, CEREMONY, r.peer, r.bind, at), at, S);
|
|
399
|
+
return { own: r.own, credential, digest: await digestDoc(credential, S), doc: r.thread !== null ? await counterDoc(ctx, r, me.anchor, credential, r.thread) : null };
|
|
400
|
+
}
|
|
401
|
+
/** The counter-step's effect: the own credential committed (5770), its
|
|
402
|
+
* document out where the bundle's thread is known, else pending (5780);
|
|
403
|
+
* the confirm prompt of the record closes. */
|
|
404
|
+
function applyCounter(state, p, when, closePrompt) {
|
|
405
|
+
if (isRefusal(p))
|
|
406
|
+
return p;
|
|
407
|
+
const r = state.encounter.records[p.own];
|
|
408
|
+
if (!r || r.role !== 'displayer')
|
|
409
|
+
return no('no-record');
|
|
410
|
+
if (r.out !== null)
|
|
411
|
+
return no('already-issued');
|
|
412
|
+
const at = tupleByLabel(state, r.label);
|
|
413
|
+
if (!at || at.tuple.deactivated)
|
|
414
|
+
return no('tuple-deactivated');
|
|
415
|
+
if (!issuanceOpen(Date.parse(r.card.challenge.issuedAt), when))
|
|
416
|
+
return no('stale-issuance', 'RLTP-ENC-5530');
|
|
417
|
+
if ((p.doc === null) !== (r.thread === null))
|
|
418
|
+
throw new Error('encounter.counter: prepared on another thread state');
|
|
419
|
+
const place = placeOf(state, r);
|
|
420
|
+
let next = withRecord(state, { ...r, out: { digest: p.digest, credential: p.credential }, counterPending: p.doc === null });
|
|
421
|
+
const events = [];
|
|
422
|
+
const pid = confirmPromptId(r.own);
|
|
423
|
+
if (closePrompt && Object.hasOwn(next.prompts, pid)) {
|
|
424
|
+
const prompts = { ...next.prompts };
|
|
425
|
+
delete prompts[pid];
|
|
426
|
+
next = { ...next, prompts };
|
|
427
|
+
events.push({ type: 'prompt.decided', promptId: pid, kind: 'encounter-confirm', choice: 'accept' });
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
state: next,
|
|
431
|
+
outbound: p.doc ? [{ digest: p.doc.digest, envelope: p.doc.envelope, type: DELIVERY_TYPE, threadId: p.doc.threadId, recipient: { contactId: place.contactId, rkid: at.tuple.rkid, keyAgreement: at.tuple.counterpartKeyAgreement } }] : [],
|
|
432
|
+
events,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
const counterDef = {
|
|
436
|
+
human: true,
|
|
437
|
+
async prepare(ctx, input) {
|
|
438
|
+
const r = counterRecord(ctx.snapshot, input?.contactId);
|
|
439
|
+
return isRefusal(r) ? r : prepareCounter(ctx, r);
|
|
440
|
+
},
|
|
441
|
+
apply: (state, prepared, _input, when) => applyCounter(state, prepared.value, when, true),
|
|
442
|
+
};
|
|
443
|
+
/** encounter-confirm: accept issues the counter-step; decline only closes. */
|
|
444
|
+
const confirmDecision = {
|
|
445
|
+
async prepare(ctx, prompt, choice) {
|
|
446
|
+
if (choice !== 'accept')
|
|
447
|
+
return null;
|
|
448
|
+
const r = ctx.snapshot.encounter.records[prompt.id.slice('encounter:'.length)];
|
|
449
|
+
return r ? prepareCounter(ctx, r) : no('no-record');
|
|
450
|
+
},
|
|
451
|
+
apply(state, prompt, choice, when, prepared) {
|
|
452
|
+
if (choice === 'decline')
|
|
453
|
+
return { state: state };
|
|
454
|
+
// the prompts module closes the prompt and announces the decision
|
|
455
|
+
return applyCounter(state, prepared.value ?? no('no-record'), when, false);
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
/** Delivery 4.1 checks 5–6 under a resolution that found the value held:
|
|
459
|
+
* the ceremony (5520), the enacting anchor (4100), the enactment binding
|
|
460
|
+
* (5560), the issuance window (5530); null when they pass. */
|
|
461
|
+
function checks56(state, credential, own, st, bind, rkid) {
|
|
462
|
+
const held = st === 'open' ? state.encounter.challenges[own] : state.encounter.records[own];
|
|
463
|
+
if (credential.credentialSubject.ceremony !== CEREMONY)
|
|
464
|
+
return refuse('validation-failed', 'RLTP-ENC-5520');
|
|
465
|
+
// the enacting anchor is the one that displayed the bound challenge (4.4)
|
|
466
|
+
if (held.label !== state.addresses.keys[rkid]?.label)
|
|
467
|
+
return refuse('validation-failed', 'RLTP-ENC-4100');
|
|
468
|
+
if (!sameDigest(credential.credentialSubject.enactmentBinding, bind))
|
|
469
|
+
return refuse('validation-failed', 'RLTP-ENC-5560');
|
|
470
|
+
if (!windowOk(credential, held.tCh))
|
|
471
|
+
return refuse('stale-issuance', 'RLTP-ENC-5530');
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
const bundleTask = {
|
|
475
|
+
proof: 'absent',
|
|
476
|
+
decode: (payload) => (schemaOk('payload-encounter-bundle.schema.json', payload) ? payload : refuse('malformed')),
|
|
477
|
+
// check 1 (structure, outer/inner, credential format), then the binding:
|
|
478
|
+
// the arrival tuple where the record exists and the issuer is its
|
|
479
|
+
// counterpart, else the bundle itself — it establishes the tuple (4.1),
|
|
480
|
+
// its issuer = card.anchor = credential.issuer
|
|
481
|
+
channel(state, doc, rkid) {
|
|
482
|
+
const f = bundleForm(doc);
|
|
483
|
+
if (f)
|
|
484
|
+
return refuse(f.reason);
|
|
485
|
+
const at = tupleByRkid(state, rkid);
|
|
486
|
+
if (at && doc.issuer === at.tuple.counterpartAnchor)
|
|
487
|
+
return { keyAgreement: at.tuple.counterpartKeyAgreement, contactId: entryOf(state, at.rel)?.id ?? at.rel.contactId };
|
|
488
|
+
// another issuer on a recorded display key: the record decides at stage
|
|
489
|
+
// 9 (consumed-challenge, 4.1); its ack is never produced
|
|
490
|
+
const k = state.addresses.keys[rkid];
|
|
491
|
+
if (!k || !k.label.startsWith('pair/'))
|
|
492
|
+
return refuse('validation-failed');
|
|
493
|
+
return { keyAgreement: doc.payload.card.keyAgreement, contactId: idsOf(k.label).contactId };
|
|
494
|
+
},
|
|
495
|
+
// checks 2–4: card proof, credential proof, addressee (no state touched);
|
|
496
|
+
// then check 5 the provisional resolution and, where it found the value
|
|
497
|
+
// held, checks 5–6 at stage 8 (an unknown value goes to stage 9)
|
|
498
|
+
async prepare(ctx, doc, arrival) {
|
|
499
|
+
const { card, credential } = doc.payload;
|
|
500
|
+
if (!(await diVerify(card, card.anchor, ctx.crypto.subtle)))
|
|
501
|
+
return refuse('validation-failed', 'RLTP-ENC-6020');
|
|
502
|
+
const h = await credHead(credential, doc.recipient, false, ctx.crypto.subtle);
|
|
503
|
+
if (h)
|
|
504
|
+
return refuse(dispositionOf(h), errRef(h));
|
|
505
|
+
const S = ctx.crypto.subtle;
|
|
506
|
+
const own = credential.credentialSubject.challenge;
|
|
507
|
+
const r = Object.hasOwn(ctx.snapshot.encounter.records, own) ? ctx.snapshot.encounter.records[own] : undefined;
|
|
508
|
+
// a counter-step issued on the optical leg waited for this thread (5780)
|
|
509
|
+
const counter = r && r.counterPending && r.out && r.counterpart === card.anchor
|
|
510
|
+
? await counterDoc(ctx, r, doc.recipient, r.out.credential, r.thread ?? doc.threadId) : null;
|
|
511
|
+
const bind = await digestDoc({ ceremony: CEREMONY, challenges: [own, card.challenge.value].sort() }, S);
|
|
512
|
+
const st = resolve(ctx.snapshot, own, ctx.when);
|
|
513
|
+
if (st !== 'unknown') {
|
|
514
|
+
const bad = checks56(ctx.snapshot, credential, own, st, bind, arrival.rkid);
|
|
515
|
+
if (bad)
|
|
516
|
+
return bad;
|
|
517
|
+
}
|
|
518
|
+
return { bind, credDigest: await digestDoc(credential, S), counter, st };
|
|
519
|
+
},
|
|
520
|
+
// the authoritative resolution and the branch it selects (4.1); checks
|
|
521
|
+
// 5–6 again only where it differs from the provisional one
|
|
522
|
+
apply(state, prepared, doc, arrival) {
|
|
523
|
+
const p = prepared.value;
|
|
524
|
+
const { card, credential } = doc.payload;
|
|
525
|
+
const when = arrival.when;
|
|
526
|
+
const own = credential.credentialSubject.challenge;
|
|
527
|
+
const st = resolve(state, own, when);
|
|
528
|
+
if (st === 'unknown') {
|
|
529
|
+
const latch = latchOf(state, own, when);
|
|
530
|
+
return { ...refuse('validation-failed', 'RLTP-ENC-5240'), ...(latch ? { latch } : {}) };
|
|
531
|
+
}
|
|
532
|
+
const held = st === 'open' ? state.encounter.challenges[own] : state.encounter.records[own];
|
|
533
|
+
if (st !== p.st) {
|
|
534
|
+
const bad = checks56(state, credential, own, st, p.bind, arrival.rkid);
|
|
535
|
+
if (bad)
|
|
536
|
+
return bad;
|
|
537
|
+
}
|
|
538
|
+
if (threadInUse(state, doc.threadId) && !sameOpeningBundle(state, card.anchor, doc.threadId, arrival.digest))
|
|
539
|
+
return refuse('validation-failed');
|
|
540
|
+
if (st === 'open') {
|
|
541
|
+
if (challengeUsed(state, card.challenge.value))
|
|
542
|
+
return refuse('validation-failed', 'RLTP-ENC-5170');
|
|
543
|
+
if (!futureOk(held.tCh, when))
|
|
544
|
+
return refuse('gate-future', 'RLTP-ENC-5390');
|
|
545
|
+
return recordCreating(state, {
|
|
546
|
+
label: held.label, own, tCh: held.tCh, card, bind: p.bind, rkid: arrival.rkid, when,
|
|
547
|
+
in: { digest: p.credDigest, credential: credential }, thread: doc.threadId, digest: arrival.digest,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
// recorded: the record decides (4.1)
|
|
551
|
+
const r = held;
|
|
552
|
+
if (r.counterpart !== card.anchor)
|
|
553
|
+
return refuse('consumed-challenge', 'RLTP-ENC-5440');
|
|
554
|
+
if (jcs(r.card) !== jcs(card))
|
|
555
|
+
return refuse('validation-failed', 'RLTP-ENC-5730');
|
|
556
|
+
const e = acceptTail(credential, facts(r), 'bundle');
|
|
557
|
+
if (e)
|
|
558
|
+
return refuse(dispositionOf(e), errRef(e));
|
|
559
|
+
if (r.in && !sameDigest(r.in.digest, p.credDigest))
|
|
560
|
+
return refuse('validation-failed', 'RLTP-ENC-5570');
|
|
561
|
+
// the record-aware effect: no gate, no second record (5740)
|
|
562
|
+
const thread = r.thread ?? doc.threadId;
|
|
563
|
+
const next = {
|
|
564
|
+
...r,
|
|
565
|
+
in: r.in ?? { digest: p.credDigest, credential: credential },
|
|
566
|
+
thread,
|
|
567
|
+
bundleDigest: r.bundleDigest ?? arrival.digest,
|
|
568
|
+
threads: thread === doc.threadId ? r.threads : { ...r.threads, [doc.threadId]: arrival.digest },
|
|
569
|
+
counterPending: r.counterPending && !p.counter,
|
|
570
|
+
};
|
|
571
|
+
const place = placeOf(state, r);
|
|
572
|
+
const at = tupleByLabel(state, r.label);
|
|
573
|
+
const events = r.in ? [] : [{ type: 'encounter.accepted', contactId: place.contactId, relationshipId: place.relationshipId, mutual: r.out !== null }];
|
|
574
|
+
const outbound = p.counter && at
|
|
575
|
+
? [{ digest: p.counter.digest, envelope: p.counter.envelope, type: DELIVERY_TYPE, threadId: p.counter.threadId, recipient: { contactId: place.contactId, rkid: at.tuple.rkid, keyAgreement: at.tuple.counterpartKeyAgreement } }]
|
|
576
|
+
: [];
|
|
577
|
+
return { state: withRecord(state, next), events, outbound };
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
const errRef = (e) => ({
|
|
581
|
+
ERR_VERSION: 'RLTP-ENC-5480', ERR_SIG: 'RLTP-ENC-5490', ERR_ADDRESSEE: 'RLTP-ENC-5500', ERR_NO_RECORD: 'RLTP-ENC-5510',
|
|
582
|
+
ERR_CEREMONY: 'RLTP-ENC-5520', ERR_STALE_ISSUANCE: 'RLTP-ENC-5530', ERR_BINDING: 'RLTP-ENC-5560', ERR_CONFLICT: 'RLTP-ENC-5570',
|
|
583
|
+
}[e]);
|
|
584
|
+
const deliveryTask = {
|
|
585
|
+
proof: 'absent',
|
|
586
|
+
// bound to the enactment record on its tuple (below): a late counter
|
|
587
|
+
// credential is taken against the record even where a chaining has
|
|
588
|
+
// deactivated that tuple since (4.2; encounter review 2/14); no new
|
|
589
|
+
// issuance goes out on it
|
|
590
|
+
pinned: true,
|
|
591
|
+
decode: (payload) => (schemaOk('payload-encounter-credential-delivery.schema.json', payload) ? payload : refuse('malformed')),
|
|
592
|
+
async prepare(ctx, doc, arrival) {
|
|
593
|
+
const f = deliveryForm(doc);
|
|
594
|
+
if (f)
|
|
595
|
+
return refuse('validation-failed', 'RLTP-ENC-5760');
|
|
596
|
+
// the relationship channel to the issuer: a record on the arrival tuple
|
|
597
|
+
const at = tupleByRkid(ctx.snapshot, arrival.rkid);
|
|
598
|
+
if (!at || !recordOfLabel(ctx.snapshot, at.tuple.ownPairLabel))
|
|
599
|
+
return refuse('validation-failed');
|
|
600
|
+
// 5.6 steps 1–3, no state touched; the rest runs in the transaction
|
|
601
|
+
return { verdict: await credHead(doc.payload.credential, doc.recipient, true, ctx.crypto.subtle), credDigest: await digestDoc(doc.payload.credential, ctx.crypto.subtle) };
|
|
602
|
+
},
|
|
603
|
+
apply(state, prepared, doc, arrival) {
|
|
604
|
+
const at = tupleByRkid(state, arrival.rkid);
|
|
605
|
+
const r = recordOfLabel(state, at.tuple.ownPairLabel);
|
|
606
|
+
// the thread rule of 4.3: counter in the bundle's thread, deliver outside it
|
|
607
|
+
const step = doc.ceremony?.step;
|
|
608
|
+
const inBundle = r.thread !== null && doc.threadId === r.thread;
|
|
609
|
+
if (step === 'counter' && !inBundle)
|
|
610
|
+
return refuse('validation-failed', 'RLTP-ENC-5760');
|
|
611
|
+
if (step === 'deliver' && inBundle)
|
|
612
|
+
return refuse('validation-failed');
|
|
613
|
+
if (!inBundle && threadInUse(state, doc.threadId))
|
|
614
|
+
return refuse('validation-failed');
|
|
615
|
+
// the defined effect: buffering (the thread reserved); acceptance (5.6)
|
|
616
|
+
// in the same transaction, its outcome never signaled (7.4)
|
|
617
|
+
const cred = doc.payload.credential;
|
|
618
|
+
const p = prepared.value;
|
|
619
|
+
const bound = Object.hasOwn(state.encounter.records, cred.credentialSubject.challenge) ? state.encounter.records[cred.credentialSubject.challenge] : undefined;
|
|
620
|
+
let e = p.verdict ?? acceptTail(cred, bound && bound.label === r.label ? facts(bound) : undefined, 'credential');
|
|
621
|
+
if (!e && r.in && !sameDigest(r.in.digest, p.credDigest))
|
|
622
|
+
e = 'ERR_CONFLICT';
|
|
623
|
+
let next = inBundle ? r : { ...r, threads: { ...r.threads, [doc.threadId]: arrival.digest } };
|
|
624
|
+
const events = [];
|
|
625
|
+
if (!e && !r.in) {
|
|
626
|
+
next = { ...next, in: { digest: p.credDigest, credential: cred } };
|
|
627
|
+
const place = placeOf(state, r);
|
|
628
|
+
events.push({ type: 'encounter.accepted', contactId: place.contactId, relationshipId: place.relationshipId, mutual: r.out !== null });
|
|
629
|
+
}
|
|
630
|
+
return { state: next === r ? state : withRecord(state, next), events };
|
|
631
|
+
},
|
|
632
|
+
};
|
|
633
|
+
// ── retention: the aging latch's end (5200–5230) ───────────────────────
|
|
634
|
+
function retainChallenges(state, when) {
|
|
635
|
+
const aged = Object.values(state.encounter.challenges).filter((c) => challengeAged(c.tCh, when));
|
|
636
|
+
if (!aged.length)
|
|
637
|
+
return state;
|
|
638
|
+
const challenges = { ...state.encounter.challenges };
|
|
639
|
+
for (const c of aged)
|
|
640
|
+
delete challenges[c.value];
|
|
641
|
+
return { ...state, encounter: { ...state.encounter, challenges } };
|
|
642
|
+
}
|
|
643
|
+
/** When retainChallenges drops an aged challenge (5.3): the first whole
|
|
644
|
+
* second past its maximum age and the skew (the timer contract, engine.ts). */
|
|
645
|
+
const challengeDeadlines = (state) => Object.values(state.encounter.challenges).map((c) => (Math.floor(c.tCh / 1000) + (CHALLENGE_MAX_AGE + SKEW_TOLERANCE) / 1000 + 1) * 1000);
|
|
646
|
+
/** The encounter module createHolder registers. */
|
|
647
|
+
export function encounterModule() {
|
|
648
|
+
return Object.freeze({
|
|
649
|
+
name: 'encounter',
|
|
650
|
+
commands: Object.freeze({
|
|
651
|
+
[ENCOUNTER_COMMANDS.show]: showDef,
|
|
652
|
+
[ENCOUNTER_COMMANDS.scan]: scanDef,
|
|
653
|
+
[ENCOUNTER_COMMANDS.capture]: captureDef,
|
|
654
|
+
[ENCOUNTER_COMMANDS.counter]: counterDef,
|
|
655
|
+
}),
|
|
656
|
+
tasks: Object.freeze({ [BUNDLE_TYPE]: bundleTask, [DELIVERY_TYPE]: deliveryTask }),
|
|
657
|
+
decisions: Object.freeze({ 'encounter-confirm': confirmDecision }),
|
|
658
|
+
retain: retainChallenges,
|
|
659
|
+
deadlines: challengeDeadlines,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
//# sourceMappingURL=ceremony.js.map
|