@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.
- package/README.md +57 -10
- package/THIRD_PARTY_NOTICES.md +61 -2
- package/dist/cli.ts +1 -1
- package/dist/cloud-sync-client.js +538 -15
- package/dist/migrations/018_exposure.sql +88 -0
- package/dist/migrations/019_exposure_attestations.sql +44 -0
- package/dist/migrations/020_exposure_attestation_sends.sql +46 -0
- package/dist/migrations/021_identity_component_projection.sql +212 -0
- package/dist/peopleblade.js +13943 -5247
- package/package.json +1 -1
|
@@ -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;
|