@hraness/peopleblade 0.3.6 → 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.
@@ -0,0 +1,88 @@
1
+ PRAGMA foreign_keys = ON;
2
+
3
+ -- The exposure domain is quarantined, owner-run evidence: consent events are
4
+ -- append-only, scans and findings are immutable once recorded, and nothing in
5
+ -- these tables may create contact methods, provider resources, or identity
6
+ -- evidence. Findings store attribute classes and salted digests only, never
7
+ -- raw exposed values or credential material.
8
+ CREATE TABLE exposure_consents (
9
+ id INTEGER PRIMARY KEY,
10
+ person_id INTEGER NOT NULL REFERENCES people(id) ON DELETE RESTRICT,
11
+ action TEXT NOT NULL CHECK(action IN ('grant','revoke')),
12
+ scope TEXT NOT NULL CHECK(scope IN ('self','contact')),
13
+ channel TEXT NOT NULL CHECK(length(channel) BETWEEN 1 AND 128),
14
+ note TEXT CHECK(note IS NULL OR length(note) BETWEEN 1 AND 4096),
15
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
16
+ ) STRICT;
17
+ CREATE INDEX exposure_consents_person ON exposure_consents(person_id, id);
18
+
19
+ CREATE TABLE exposure_scans (
20
+ id INTEGER PRIMARY KEY,
21
+ person_id INTEGER NOT NULL REFERENCES people(id) ON DELETE RESTRICT,
22
+ tool TEXT NOT NULL CHECK(length(tool) BETWEEN 1 AND 64),
23
+ contract TEXT NOT NULL CHECK(length(contract) BETWEEN 1 AND 64),
24
+ artifact_sha256 TEXT CHECK(artifact_sha256 IS NULL OR (length(artifact_sha256) = 64 AND artifact_sha256 NOT GLOB '*[^0-9a-f]*')),
25
+ subject_input_sha256 TEXT NOT NULL CHECK(length(subject_input_sha256) = 64 AND subject_input_sha256 NOT GLOB '*[^0-9a-f]*'),
26
+ observed_at TEXT NOT NULL,
27
+ stats_json TEXT NOT NULL DEFAULT '{}' CHECK(json_valid(stats_json)),
28
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
29
+ UNIQUE(person_id, tool, artifact_sha256)
30
+ ) STRICT;
31
+ CREATE INDEX exposure_scans_person ON exposure_scans(person_id, id);
32
+
33
+ CREATE TABLE exposure_findings (
34
+ id INTEGER PRIMARY KEY,
35
+ person_id INTEGER NOT NULL REFERENCES people(id) ON DELETE RESTRICT,
36
+ scan_id INTEGER REFERENCES exposure_scans(id) ON DELETE RESTRICT,
37
+ category TEXT NOT NULL CHECK(category IN (
38
+ 'breach','data-broker','account-existence','court-record','property-record',
39
+ 'voter-record','financial-disclosure','professional-license','face-index','web-presence'
40
+ )),
41
+ presence TEXT NOT NULL CHECK(presence IN ('found','absent','unknown')),
42
+ attribute_classes_json TEXT NOT NULL DEFAULT '[]'
43
+ CHECK(json_valid(attribute_classes_json) AND json_type(attribute_classes_json) = 'array'),
44
+ source TEXT NOT NULL CHECK(length(source) BETWEEN 1 AND 128),
45
+ binding_kind TEXT NOT NULL CHECK(binding_kind IN (
46
+ 'exact-email','exact-phone','exact-handle','exact-profile-url','name-plus-attribute','unbound'
47
+ )),
48
+ binding_value_sha256 TEXT CHECK(binding_value_sha256 IS NULL OR (length(binding_value_sha256) = 64 AND binding_value_sha256 NOT GLOB '*[^0-9a-f]*')),
49
+ citation_json TEXT NOT NULL CHECK(json_valid(citation_json)),
50
+ confidence TEXT NOT NULL CHECK(confidence IN ('confirmed','candidate')),
51
+ severity TEXT NOT NULL CHECK(severity IN ('high','medium','low')),
52
+ observed_at TEXT NOT NULL,
53
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
54
+ CHECK(
55
+ (binding_kind = 'unbound' AND binding_value_sha256 IS NULL)
56
+ OR (binding_kind <> 'unbound' AND binding_value_sha256 IS NOT NULL)
57
+ )
58
+ ) STRICT;
59
+ CREATE INDEX exposure_findings_person ON exposure_findings(person_id, category, severity);
60
+ CREATE INDEX exposure_findings_scan ON exposure_findings(scan_id);
61
+
62
+ CREATE TABLE exposure_remediations (
63
+ id INTEGER PRIMARY KEY,
64
+ finding_id INTEGER NOT NULL REFERENCES exposure_findings(id) ON DELETE RESTRICT,
65
+ action TEXT NOT NULL CHECK(action IN ('opt-out-submitted','verified-removed','relisted','dismissed','not-applicable')),
66
+ channel TEXT CHECK(channel IS NULL OR length(channel) BETWEEN 1 AND 128),
67
+ note TEXT CHECK(note IS NULL OR length(note) BETWEEN 1 AND 4096),
68
+ occurred_at TEXT NOT NULL,
69
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
70
+ ) STRICT;
71
+ CREATE INDEX exposure_remediations_finding ON exposure_remediations(finding_id, occurred_at, id);
72
+
73
+ CREATE TRIGGER exposure_consents_immutable_update BEFORE UPDATE ON exposure_consents FOR EACH ROW
74
+ BEGIN SELECT RAISE(ABORT, 'exposure consents are immutable'); END;
75
+ CREATE TRIGGER exposure_consents_immutable_delete BEFORE DELETE ON exposure_consents FOR EACH ROW
76
+ BEGIN SELECT RAISE(ABORT, 'exposure consents are immutable'); END;
77
+ CREATE TRIGGER exposure_scans_immutable_update BEFORE UPDATE ON exposure_scans FOR EACH ROW
78
+ BEGIN SELECT RAISE(ABORT, 'exposure scans are immutable'); END;
79
+ CREATE TRIGGER exposure_scans_immutable_delete BEFORE DELETE ON exposure_scans FOR EACH ROW
80
+ BEGIN SELECT RAISE(ABORT, 'exposure scans are immutable'); END;
81
+ CREATE TRIGGER exposure_findings_immutable_update BEFORE UPDATE ON exposure_findings FOR EACH ROW
82
+ BEGIN SELECT RAISE(ABORT, 'exposure findings are immutable'); END;
83
+ CREATE TRIGGER exposure_findings_immutable_delete BEFORE DELETE ON exposure_findings FOR EACH ROW
84
+ BEGIN SELECT RAISE(ABORT, 'exposure findings are immutable'); END;
85
+ CREATE TRIGGER exposure_remediations_immutable_update BEFORE UPDATE ON exposure_remediations FOR EACH ROW
86
+ BEGIN SELECT RAISE(ABORT, 'exposure remediations are immutable'); END;
87
+ CREATE TRIGGER exposure_remediations_immutable_delete BEFORE DELETE ON exposure_remediations FOR EACH ROW
88
+ BEGIN SELECT RAISE(ABORT, 'exposure remediations are immutable'); END;
@@ -0,0 +1,44 @@
1
+ PRAGMA foreign_keys = ON;
2
+
3
+ -- Signed aggregate-only exposure attestations. 'issued' rows are packets this
4
+ -- database produced to hand a contact; 'received' rows are verified peer
5
+ -- packets. person_id is NULL on a received packet whose salted identifier
6
+ -- digests match no stored person (or match ambiguously). The packet body is
7
+ -- immutable evidence; only the lifecycle status and the first-seen key_changed
8
+ -- flag may update. Packet contents carry digests and aggregate counts only,
9
+ -- never raw identifiers.
10
+ CREATE TABLE exposure_attestations (
11
+ id INTEGER PRIMARY KEY,
12
+ direction TEXT NOT NULL CHECK(direction IN ('issued','received')),
13
+ person_id INTEGER REFERENCES people(id) ON DELETE RESTRICT,
14
+ attester_public_key TEXT NOT NULL CHECK(length(attester_public_key) BETWEEN 40 AND 256),
15
+ packet_sha256 TEXT NOT NULL CHECK(length(packet_sha256) = 64 AND packet_sha256 NOT GLOB '*[^0-9a-f]*'),
16
+ packet_json TEXT NOT NULL CHECK(json_valid(packet_json)),
17
+ issued_at TEXT NOT NULL,
18
+ status TEXT NOT NULL CHECK(status IN ('current','superseded')),
19
+ key_changed INTEGER NOT NULL DEFAULT 0 CHECK(key_changed IN (0,1)),
20
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
21
+ UNIQUE(direction, packet_sha256)
22
+ ) STRICT;
23
+ CREATE INDEX exposure_attestations_person ON exposure_attestations(person_id, direction, status, issued_at);
24
+
25
+ -- The packet is immutable evidence: only the lifecycle status may transition.
26
+ CREATE TRIGGER exposure_attestations_immutable_update BEFORE UPDATE ON exposure_attestations
27
+ FOR EACH ROW WHEN
28
+ NEW.direction <> OLD.direction
29
+ OR NEW.person_id IS NOT OLD.person_id
30
+ OR NEW.attester_public_key <> OLD.attester_public_key
31
+ OR NEW.packet_sha256 <> OLD.packet_sha256
32
+ OR NEW.packet_json <> OLD.packet_json
33
+ OR NEW.issued_at <> OLD.issued_at
34
+ OR NEW.key_changed <> OLD.key_changed
35
+ OR NEW.created_at <> OLD.created_at
36
+ BEGIN SELECT RAISE(ABORT, 'exposure attestation packets are immutable'); END;
37
+ CREATE TRIGGER exposure_attestations_immutable_delete BEFORE DELETE ON exposure_attestations FOR EACH ROW
38
+ BEGIN SELECT RAISE(ABORT, 'exposure attestations are immutable'); END;
39
+
40
+ -- The local Ed25519 attestation identity (JWK {kty,crv,x,d}). Populated lazily
41
+ -- on the first `exposure attest`; the private component never leaves this
42
+ -- database.
43
+ ALTER TABLE local_state ADD COLUMN exposure_attestation_key TEXT
44
+ CHECK(exposure_attestation_key IS NULL OR json_valid(exposure_attestation_key));
@@ -0,0 +1,46 @@
1
+ PRAGMA foreign_keys = ON;
2
+
3
+ -- Delivery ledger for signed exposure attestation packets sent through
4
+ -- Ghostget's scoped messaging-automation host. One row is one dispatch
5
+ -- attempt: identity fields (attestation, recipient, provider, route digest,
6
+ -- intent) are immutable; the host journal ids, run state, and reason update
7
+ -- as the delivery settles or is reconciled. Raw routes and packet contents
8
+ -- are never stored — only digests and host journal ids.
9
+ CREATE TABLE exposure_attestation_sends (
10
+ id INTEGER PRIMARY KEY,
11
+ attestation_id INTEGER NOT NULL REFERENCES exposure_attestations(id) ON DELETE RESTRICT,
12
+ person_id INTEGER NOT NULL REFERENCES people(id) ON DELETE RESTRICT,
13
+ provider TEXT NOT NULL CHECK(provider IN ('imessage','whatsapp')),
14
+ coordinate_sha256 TEXT NOT NULL CHECK(length(coordinate_sha256) = 64 AND coordinate_sha256 NOT GLOB '*[^0-9a-f]*'),
15
+ auth_id TEXT NOT NULL CHECK(length(auth_id) BETWEEN 1 AND 128),
16
+ intent_id TEXT NOT NULL UNIQUE,
17
+ attempt INTEGER NOT NULL CHECK(attempt >= 1),
18
+ enrollment_id TEXT,
19
+ grant_id TEXT,
20
+ plan_id TEXT,
21
+ run_id TEXT,
22
+ state TEXT NOT NULL CHECK(state IN ('prepared','started','accepted','failed','partial','indeterminate')),
23
+ reason TEXT,
24
+ message_id TEXT,
25
+ provider_receipt_id TEXT,
26
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
27
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
28
+ ) STRICT;
29
+ CREATE INDEX exposure_attestation_sends_attestation ON exposure_attestation_sends(attestation_id, state, created_at);
30
+ CREATE INDEX exposure_attestation_sends_person ON exposure_attestation_sends(person_id, state, created_at);
31
+
32
+ -- Identity fields are immutable evidence; only journal ids, run state,
33
+ -- reason, delivery ids, and the update timestamp may change.
34
+ CREATE TRIGGER exposure_attestation_sends_immutable_update BEFORE UPDATE ON exposure_attestation_sends
35
+ FOR EACH ROW WHEN
36
+ NEW.attestation_id <> OLD.attestation_id
37
+ OR NEW.person_id <> OLD.person_id
38
+ OR NEW.provider <> OLD.provider
39
+ OR NEW.coordinate_sha256 <> OLD.coordinate_sha256
40
+ OR NEW.auth_id <> OLD.auth_id
41
+ OR NEW.intent_id <> OLD.intent_id
42
+ OR NEW.attempt <> OLD.attempt
43
+ OR NEW.created_at <> OLD.created_at
44
+ BEGIN SELECT RAISE(ABORT, 'exposure attestation send identity is immutable'); END;
45
+ CREATE TRIGGER exposure_attestation_sends_immutable_delete BEFORE DELETE ON exposure_attestation_sends FOR EACH ROW
46
+ BEGIN SELECT RAISE(ABORT, 'exposure attestation sends are immutable'); END;
@@ -0,0 +1,212 @@
1
+ PRAGMA foreign_keys = ON;
2
+
3
+ CREATE VIEW person_identity_components_computed AS
4
+ WITH RECURSIVE
5
+ accepted(left_person_id, right_person_id) AS (
6
+ SELECT left_person_id, right_person_id
7
+ FROM identity_current_decisions
8
+ WHERE action = 'accept'
9
+ ),
10
+ reach(person_id, member_id) AS (
11
+ SELECT id, id FROM people
12
+ UNION
13
+ SELECT reach.person_id,
14
+ CASE WHEN accepted.left_person_id = reach.member_id
15
+ THEN accepted.right_person_id ELSE accepted.left_person_id END
16
+ FROM reach
17
+ JOIN accepted
18
+ ON accepted.left_person_id = reach.member_id OR accepted.right_person_id = reach.member_id
19
+ ),
20
+ raw_components(person_id, raw_canonical_person_id) AS (
21
+ SELECT person_id, min(member_id)
22
+ FROM reach
23
+ GROUP BY person_id
24
+ ),
25
+ observed_targets(raw_canonical_person_id, target_person_id, decision_id) AS (
26
+ SELECT target.raw_canonical_person_id,
27
+ CAST(json_extract(decision.evidence_json, '$.reviewedTarget.anchorPersonId') AS INTEGER),
28
+ decision.id
29
+ FROM identity_decisions decision
30
+ JOIN raw_components target ON target.person_id = CAST(
31
+ json_extract(decision.evidence_json, '$.reviewedTarget.anchorPersonId') AS INTEGER
32
+ )
33
+ WHERE decision.action = 'accept'
34
+ AND json_extract(decision.evidence_json, '$.version') = 2
35
+ AND json_extract(decision.evidence_json, '$.kind') IN ('observed-email','observed-phone')
36
+ ),
37
+ component_ids(raw_canonical_person_id) AS (
38
+ SELECT DISTINCT raw_canonical_person_id FROM raw_components
39
+ ),
40
+ component_representatives(raw_canonical_person_id, canonical_person_id) AS (
41
+ SELECT component.raw_canonical_person_id,
42
+ coalesce((
43
+ SELECT target.target_person_id
44
+ FROM observed_targets target
45
+ WHERE target.raw_canonical_person_id = component.raw_canonical_person_id
46
+ ORDER BY target.decision_id
47
+ LIMIT 1
48
+ ), component.raw_canonical_person_id)
49
+ FROM component_ids component
50
+ )
51
+ SELECT raw.person_id, representative.canonical_person_id
52
+ FROM raw_components raw
53
+ JOIN component_representatives representative
54
+ ON representative.raw_canonical_person_id = raw.raw_canonical_person_id;
55
+
56
+ CREATE TABLE identity_component_members(
57
+ person_id INTEGER PRIMARY KEY REFERENCES people(id) ON DELETE CASCADE,
58
+ canonical_person_id INTEGER NOT NULL REFERENCES people(id) ON DELETE RESTRICT
59
+ ) STRICT;
60
+
61
+ CREATE INDEX identity_component_members_canonical
62
+ ON identity_component_members(canonical_person_id, person_id);
63
+
64
+ CREATE TABLE identity_component_refresh_state(
65
+ canonical_person_id INTEGER PRIMARY KEY REFERENCES people(id) ON DELETE CASCADE
66
+ ) STRICT;
67
+
68
+ INSERT INTO identity_component_members(person_id, canonical_person_id)
69
+ SELECT person_id, canonical_person_id FROM person_identity_components_computed;
70
+
71
+ DROP VIEW person_identity_components;
72
+
73
+ CREATE VIEW person_identity_components AS
74
+ SELECT person_id, canonical_person_id FROM identity_component_members;
75
+
76
+ CREATE TRIGGER identity_component_member_after_person_insert
77
+ AFTER INSERT ON people FOR EACH ROW
78
+ BEGIN
79
+ INSERT INTO identity_component_members(person_id, canonical_person_id)
80
+ VALUES (NEW.id, NEW.id);
81
+ END;
82
+
83
+ CREATE TRIGGER identity_component_members_after_accept
84
+ AFTER INSERT ON identity_decisions FOR EACH ROW
85
+ WHEN NEW.action = 'accept'
86
+ BEGIN
87
+ DELETE FROM identity_component_refresh_state;
88
+ INSERT INTO identity_component_refresh_state(canonical_person_id)
89
+ SELECT canonical_person_id FROM identity_component_members
90
+ WHERE person_id IN (NEW.left_person_id, NEW.right_person_id)
91
+ ON CONFLICT DO NOTHING;
92
+ UPDATE identity_component_members
93
+ SET canonical_person_id = coalesce((
94
+ SELECT CAST(json_extract(decision.evidence_json, '$.reviewedTarget.anchorPersonId') AS INTEGER)
95
+ FROM identity_decisions decision
96
+ JOIN identity_component_members target
97
+ ON target.person_id = CAST(json_extract(decision.evidence_json, '$.reviewedTarget.anchorPersonId') AS INTEGER)
98
+ JOIN identity_component_refresh_state state
99
+ ON state.canonical_person_id = target.canonical_person_id
100
+ WHERE decision.action = 'accept'
101
+ AND json_extract(decision.evidence_json, '$.version') = 2
102
+ AND json_extract(decision.evidence_json, '$.kind') IN ('observed-email','observed-phone')
103
+ ORDER BY decision.id
104
+ LIMIT 1
105
+ ), (
106
+ SELECT min(member.person_id)
107
+ FROM identity_component_members member
108
+ JOIN identity_component_refresh_state state
109
+ ON state.canonical_person_id = member.canonical_person_id
110
+ ))
111
+ WHERE canonical_person_id IN (
112
+ SELECT canonical_person_id FROM identity_component_refresh_state
113
+ );
114
+ DELETE FROM identity_component_refresh_state;
115
+ END;
116
+
117
+ CREATE TRIGGER identity_component_members_after_separate
118
+ AFTER INSERT ON identity_decisions FOR EACH ROW
119
+ WHEN NEW.action = 'separate'
120
+ BEGIN
121
+ UPDATE identity_component_members
122
+ SET canonical_person_id = coalesce((
123
+ SELECT CAST(json_extract(decision.evidence_json, '$.reviewedTarget.anchorPersonId') AS INTEGER)
124
+ FROM identity_decisions decision
125
+ WHERE decision.action = 'accept'
126
+ AND json_extract(decision.evidence_json, '$.version') = 2
127
+ AND json_extract(decision.evidence_json, '$.kind') IN ('observed-email','observed-phone')
128
+ AND CAST(json_extract(decision.evidence_json, '$.reviewedTarget.anchorPersonId') AS INTEGER) IN (
129
+ WITH RECURSIVE accepted(left_person_id, right_person_id) AS (
130
+ SELECT left_person_id, right_person_id FROM identity_current_decisions WHERE action = 'accept'
131
+ ), reach(member_id) AS (
132
+ SELECT NEW.left_person_id
133
+ UNION
134
+ SELECT CASE WHEN accepted.left_person_id = reach.member_id
135
+ THEN accepted.right_person_id ELSE accepted.left_person_id END
136
+ FROM reach JOIN accepted
137
+ ON accepted.left_person_id = reach.member_id OR accepted.right_person_id = reach.member_id
138
+ ) SELECT member_id FROM reach
139
+ )
140
+ ORDER BY decision.id
141
+ LIMIT 1
142
+ ), (
143
+ WITH RECURSIVE accepted(left_person_id, right_person_id) AS (
144
+ SELECT left_person_id, right_person_id FROM identity_current_decisions WHERE action = 'accept'
145
+ ), reach(member_id) AS (
146
+ SELECT NEW.left_person_id
147
+ UNION
148
+ SELECT CASE WHEN accepted.left_person_id = reach.member_id
149
+ THEN accepted.right_person_id ELSE accepted.left_person_id END
150
+ FROM reach JOIN accepted
151
+ ON accepted.left_person_id = reach.member_id OR accepted.right_person_id = reach.member_id
152
+ ) SELECT min(member_id) FROM reach
153
+ ))
154
+ WHERE person_id IN (
155
+ WITH RECURSIVE accepted(left_person_id, right_person_id) AS (
156
+ SELECT left_person_id, right_person_id FROM identity_current_decisions WHERE action = 'accept'
157
+ ), reach(member_id) AS (
158
+ SELECT NEW.left_person_id
159
+ UNION
160
+ SELECT CASE WHEN accepted.left_person_id = reach.member_id
161
+ THEN accepted.right_person_id ELSE accepted.left_person_id END
162
+ FROM reach JOIN accepted
163
+ ON accepted.left_person_id = reach.member_id OR accepted.right_person_id = reach.member_id
164
+ ) SELECT member_id FROM reach
165
+ );
166
+
167
+ UPDATE identity_component_members
168
+ SET canonical_person_id = coalesce((
169
+ SELECT CAST(json_extract(decision.evidence_json, '$.reviewedTarget.anchorPersonId') AS INTEGER)
170
+ FROM identity_decisions decision
171
+ WHERE decision.action = 'accept'
172
+ AND json_extract(decision.evidence_json, '$.version') = 2
173
+ AND json_extract(decision.evidence_json, '$.kind') IN ('observed-email','observed-phone')
174
+ AND CAST(json_extract(decision.evidence_json, '$.reviewedTarget.anchorPersonId') AS INTEGER) IN (
175
+ WITH RECURSIVE accepted(left_person_id, right_person_id) AS (
176
+ SELECT left_person_id, right_person_id FROM identity_current_decisions WHERE action = 'accept'
177
+ ), reach(member_id) AS (
178
+ SELECT NEW.right_person_id
179
+ UNION
180
+ SELECT CASE WHEN accepted.left_person_id = reach.member_id
181
+ THEN accepted.right_person_id ELSE accepted.left_person_id END
182
+ FROM reach JOIN accepted
183
+ ON accepted.left_person_id = reach.member_id OR accepted.right_person_id = reach.member_id
184
+ ) SELECT member_id FROM reach
185
+ )
186
+ ORDER BY decision.id
187
+ LIMIT 1
188
+ ), (
189
+ WITH RECURSIVE accepted(left_person_id, right_person_id) AS (
190
+ SELECT left_person_id, right_person_id FROM identity_current_decisions WHERE action = 'accept'
191
+ ), reach(member_id) AS (
192
+ SELECT NEW.right_person_id
193
+ UNION
194
+ SELECT CASE WHEN accepted.left_person_id = reach.member_id
195
+ THEN accepted.right_person_id ELSE accepted.left_person_id END
196
+ FROM reach JOIN accepted
197
+ ON accepted.left_person_id = reach.member_id OR accepted.right_person_id = reach.member_id
198
+ ) SELECT min(member_id) FROM reach
199
+ ))
200
+ WHERE person_id IN (
201
+ WITH RECURSIVE accepted(left_person_id, right_person_id) AS (
202
+ SELECT left_person_id, right_person_id FROM identity_current_decisions WHERE action = 'accept'
203
+ ), reach(member_id) AS (
204
+ SELECT NEW.right_person_id
205
+ UNION
206
+ SELECT CASE WHEN accepted.left_person_id = reach.member_id
207
+ THEN accepted.right_person_id ELSE accepted.left_person_id END
208
+ FROM reach JOIN accepted
209
+ ON accepted.left_person_id = reach.member_id OR accepted.right_person_id = reach.member_id
210
+ ) SELECT member_id FROM reach
211
+ );
212
+ END;