@ours.network/notifications 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +98 -0
- package/README.md +231 -0
- package/dist/cli.js +446 -0
- package/dist/config.js +104 -0
- package/dist/daemon.js +353 -0
- package/dist/http.js +31 -0
- package/dist/log-store.js +48 -0
- package/dist/presence.js +157 -0
- package/dist/provision.js +29 -0
- package/dist/push.js +6 -0
- package/dist/web-push-sender.js +41 -0
- package/mufl_code/20E39399689579D5AFA5132C59DEDDD328C3BCA1A74D546F2193B36585DDD283.muflo +0 -0
- package/mufl_code/config.mufl +21 -0
- package/mufl_code/core/.github/FUNDING.yml +3 -0
- package/mufl_code/core/.github/workflows/docs.yml +39 -0
- package/mufl_code/core/.github/workflows/tests.yml +66 -0
- package/mufl_code/core/COMMERCIAL-LICENCE.md +25 -0
- package/mufl_code/core/COMPATIBILITY.md +221 -0
- package/mufl_code/core/CONTRIBUTING.md +37 -0
- package/mufl_code/core/LICENSE +98 -0
- package/mufl_code/core/NOTIFICATIONS_CONTRACT.md +290 -0
- package/mufl_code/core/README.md +91 -0
- package/mufl_code/core/SECURITY.md +40 -0
- package/mufl_code/core/TRADEMARKS.md +24 -0
- package/mufl_code/core/a2a_capabilities.mm +449 -0
- package/mufl_code/core/a2a_cluster.mm +601 -0
- package/mufl_code/core/a2a_control.mm +90 -0
- package/mufl_code/core/a2a_messaging.mm +4817 -0
- package/mufl_code/core/a2a_monitoring.mm +94 -0
- package/mufl_code/core/a2a_notification_integration.mm +144 -0
- package/mufl_code/core/a2a_notifications.mm +1127 -0
- package/mufl_code/core/a2a_protocol.mm +246 -0
- package/mufl_code/core/a2a_versions.mm +801 -0
- package/mufl_code/core/config.mufl +29 -0
- package/mufl_code/core/docs/.vitepress/config.mts +58 -0
- package/mufl_code/core/docs/citations.json +688 -0
- package/mufl_code/core/docs/guide/01-vendor-the-core.md +50 -0
- package/mufl_code/core/docs/guide/02-configure-and-compile.md +100 -0
- package/mufl_code/core/docs/guide/03-wire-the-host.md +154 -0
- package/mufl_code/core/docs/guide/04-connect-and-message.md +156 -0
- package/mufl_code/core/docs/guide/05-test-your-app.md +75 -0
- package/mufl_code/core/docs/guide/index.md +63 -0
- package/mufl_code/core/docs/how-it-works/capabilities-and-control.md +77 -0
- package/mufl_code/core/docs/how-it-works/cluster.md +65 -0
- package/mufl_code/core/docs/how-it-works/identity.md +56 -0
- package/mufl_code/core/docs/how-it-works/invites-and-contacts.md +76 -0
- package/mufl_code/core/docs/how-it-works/messaging.md +55 -0
- package/mufl_code/core/docs/how-it-works/monitoring-and-config.md +72 -0
- package/mufl_code/core/docs/how-it-works/overview.md +45 -0
- package/mufl_code/core/docs/how-it-works/versioning.md +116 -0
- package/mufl_code/core/docs/index.md +27 -0
- package/mufl_code/core/docs/package-lock.json +3467 -0
- package/mufl_code/core/docs/package.json +15 -0
- package/mufl_code/core/docs/reference/contributing.md +26 -0
- package/mufl_code/core/docs/reference/glossary.md +108 -0
- package/mufl_code/core/docs/reference/implementations.md +23 -0
- package/mufl_code/core/docs/reference/modules.md +19 -0
- package/mufl_code/core/docs/scripts/check-citations.mjs +36 -0
- package/mufl_code/core/docs/scripts/check-denylist.mjs +30 -0
- package/mufl_code/core/docs/scripts/gen-llms.mjs +32 -0
- package/mufl_code/core/docs/workflows/cluster.md +89 -0
- package/mufl_code/core/docs/workflows/contact-exchange.md +60 -0
- package/mufl_code/core/docs/workflows/contact-restore.md +60 -0
- package/mufl_code/core/docs/workflows/control-verbs.md +70 -0
- package/mufl_code/core/docs/workflows/index.md +57 -0
- package/mufl_code/core/docs/workflows/introductions.md +60 -0
- package/mufl_code/core/docs/workflows/messaging.md +75 -0
- package/mufl_code/core/docs/workflows/monitoring.md +84 -0
- package/mufl_code/core/docs/workflows/notifications.md +109 -0
- package/mufl_code/core/release-notes/0.1.md +60 -0
- package/mufl_code/core/release-notes/0.3.md +22 -0
- package/mufl_code/core/release-notes/0.4.md +97 -0
- package/mufl_code/core/tests/README.md +83 -0
- package/mufl_code/core/tests/compat/README.md +82 -0
- package/mufl_code/core/tests/compat/compat.mjs +245 -0
- package/mufl_code/core/tests/compat/compat_peer.mjs +146 -0
- package/mufl_code/core/tests/compat/run_compat.sh +96 -0
- package/mufl_code/core/tests/corpus.mjs +139 -0
- package/mufl_code/core/tests/corpus_actor.mu +242 -0
- package/mufl_code/core/tests/fatcompile_actor.mu +52 -0
- package/mufl_code/core/tests/mig.mjs +805 -0
- package/mufl_code/core/tests/mig_actor.mu +518 -0
- package/mufl_code/core/tests/migapp.mjs +431 -0
- package/mufl_code/core/tests/migapp_actor.mu +294 -0
- package/mufl_code/core/tests/mufl_semantics/run.sh +69 -0
- package/mufl_code/core/tests/mufl_semantics/t11_three_version.mufl +47 -0
- package/mufl_code/core/tests/mufl_semantics/t2_extra_fields.mufl +11 -0
- package/mufl_code/core/tests/mufl_semantics/t7_runtime.mufl +30 -0
- package/mufl_code/core/tests/mufl_semantics/t9_passthrough.mufl +19 -0
- package/mufl_code/core/tests/mufl_semantics/t_str_order.mufl +19 -0
- package/mufl_code/core/tests/notif.mjs +755 -0
- package/mufl_code/core/tests/notif_actor.mu +450 -0
- package/mufl_code/core/tests/protocol_container.mm +18 -0
- package/mufl_code/core/tests/run.sh +65 -0
- package/mufl_code/core/tests/run_corpus.sh +51 -0
- package/mufl_code/core/tests/run_fatcompile.sh +40 -0
- package/mufl_code/core/tests/run_mig.sh +51 -0
- package/mufl_code/core/tests/run_migapp.sh +51 -0
- package/mufl_code/core/tests/run_notif.sh +57 -0
- package/mufl_code/core/tests/test.mjs +750 -0
- package/mufl_code/core/tests/test_actor.mu +564 -0
- package/mufl_code/core/tests/test_common.mjs +93 -0
- package/mufl_code/core/version.mm +24 -0
- package/mufl_code/packet.mu +170 -0
- package/mufl_code/protocol_container.mm +18 -0
- package/package.json +43 -0
- package/scripts/gen-vapid.mjs +16 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
// Migration test actor — the E2E per-connection migration FSM scenarios (spec §5).
|
|
2
|
+
//
|
|
3
|
+
// SPLIT from test_actor.mu deliberately (same reason as corpus_actor.mu): the
|
|
4
|
+
// compiler bounds meta-stage type-level reduction PER COMPILED UNIT (1M steps).
|
|
5
|
+
// test_actor loads BOTH heavy libraries a2a_messaging (139KB) AND a2a_notifications
|
|
6
|
+
// (57KB) and already sits at the fuel ceiling. a2a_messaging has ZERO references to
|
|
7
|
+
// a2a_notifications (the dep is one-way: notifications -> messaging), so a
|
|
8
|
+
// migration/messaging-focused unit can drop a2a_notifications entirely and reclaim
|
|
9
|
+
// its whole reduction budget — leaving ample room for the 0.9.0 migration surface
|
|
10
|
+
// (mig_state_t/e2e_epoch_t stores, the FSM handlers in phase C, the §5.8 matrix).
|
|
11
|
+
application actor loads libraries
|
|
12
|
+
identity_proof_document,
|
|
13
|
+
attestation_document,
|
|
14
|
+
native_attestation_document,
|
|
15
|
+
transaction_message_decoder,
|
|
16
|
+
address_document,
|
|
17
|
+
address_document_types,
|
|
18
|
+
key_utils,
|
|
19
|
+
key_storage,
|
|
20
|
+
e2e,
|
|
21
|
+
continuation,
|
|
22
|
+
encrypted_channel,
|
|
23
|
+
a2a_versions,
|
|
24
|
+
a2a_capabilities,
|
|
25
|
+
a2a_protocol,
|
|
26
|
+
a2a_messaging,
|
|
27
|
+
current_transaction_info,
|
|
28
|
+
protocol_container,
|
|
29
|
+
version
|
|
30
|
+
uses transactions
|
|
31
|
+
{
|
|
32
|
+
hidden
|
|
33
|
+
{
|
|
34
|
+
fn _return_data (payload: any) = (transaction::action::return_data ($kind -> $data, $payload -> payload)).
|
|
35
|
+
// Host wiring the crypto facade needs at runtime (Phase-B e2e qa trns):
|
|
36
|
+
// _read_or_abort deserializes blobs; key_storage derives the e2e pickle key
|
|
37
|
+
// from the identity signing secret; encrypted_channel is a2a_messaging's dep.
|
|
38
|
+
_read_or_abort = grab( _read_or_abort ).
|
|
39
|
+
key_storage::init ($_read_or_abort -> _read_or_abort).
|
|
40
|
+
encrypted_channel::init ($_read_or_abort -> _read_or_abort).
|
|
41
|
+
// Increment-B app-e2e delivery observability: the storage hooks RECORD the last delivered
|
|
42
|
+
// message/file into these mutables so the driver can prove handle_receive_e2e_message
|
|
43
|
+
// actually decrypted + delivered the plaintext to the app hook (not just decoded it). The
|
|
44
|
+
// togglable qa_recv_abort makes the message hook ABORT — used to prove must-fix-C rollback
|
|
45
|
+
// (the do_ic promotion + pins + flush roll back with the tx when delivery aborts).
|
|
46
|
+
qa_recv_text is str = "".
|
|
47
|
+
qa_recv_wire is str = "".
|
|
48
|
+
qa_recv_file is str = "".
|
|
49
|
+
qa_recv_flen is int = 0.
|
|
50
|
+
qa_recv_count is int = 0.
|
|
51
|
+
qa_recv_abort is bool = FALSE.
|
|
52
|
+
// a2a_messaging needs its own _read_or_abort + storage hooks (the migration
|
|
53
|
+
// handlers deserialize snapshots via it). The message/file hooks RECORD delivery.
|
|
54
|
+
a2a_messaging::init (
|
|
55
|
+
$_read_or_abort -> _read_or_abort,
|
|
56
|
+
$on_message_received -> fn (a: any) -> transaction::action::type[] {
|
|
57
|
+
abort "qa_recv_abort: app hook rejected the message (must-fix-C rollback probe)" WHEN qa_recv_abort.
|
|
58
|
+
qa_recv_text -> ((a $text) safe str).
|
|
59
|
+
if (a $wire_id) != NIL { qa_recv_wire -> ((a $wire_id) safe str). }
|
|
60
|
+
qa_recv_count -> (qa_recv_count + 1).
|
|
61
|
+
return []. },
|
|
62
|
+
$on_message_sent -> fn (_: any) -> transaction::action::type[] { return []. },
|
|
63
|
+
$on_contact_removed -> fn (_: any) -> transaction::action::type[] { return []. },
|
|
64
|
+
$on_file_received -> fn (a: any) -> transaction::action::type[] {
|
|
65
|
+
qa_recv_file -> ((a $filename) safe str).
|
|
66
|
+
qa_recv_flen -> (_binlen ((a $data) safe bin)).
|
|
67
|
+
qa_recv_count -> (qa_recv_count + 1).
|
|
68
|
+
return []. },
|
|
69
|
+
$on_file_sent -> fn (_: any) -> transaction::action::type[] { return []. },
|
|
70
|
+
$on_receipt_received -> fn (_: any) -> transaction::action::type[] { return []. }
|
|
71
|
+
).
|
|
72
|
+
// Phase-B decode-seam test hook: a togglable "migration pending" flag the installed
|
|
73
|
+
// e2e hook reads, so the driver can exercise decrypt_and_commit's stage-vs-replace
|
|
74
|
+
// decision (in the real inbound path) without wiring the full core FSM.
|
|
75
|
+
qa_mig_pending is bool = FALSE.
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Phase-A compile/headroom probe + export/import round-trip for the three new
|
|
79
|
+
// migration stores (contact_migration / contact_e2e_epoch / mig_deferred).
|
|
80
|
+
// Seeds a synthetic FSM entry + epoch pin + deferred queue for a cid, exports,
|
|
81
|
+
// and reports what the blob carries; a companion trn re-imports a pre-0.9 blob
|
|
82
|
+
// (the fields absent) and reports the stores stay empty (= legacy, spec §5.1).
|
|
83
|
+
trn qa_mig_export_roundtrip _
|
|
84
|
+
{
|
|
85
|
+
current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
|
|
86
|
+
cid = _new_id "qa mig peer".
|
|
87
|
+
nonce = _hex_string_to_binary "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf".
|
|
88
|
+
ep = _hex_string_to_binary "c0c1c2c3c4c5c6c7c8c9cacbcccdcecf".
|
|
89
|
+
sid = _hex_string_to_binary "d0d1d2d3d4d5d6d7d8d9dadbdcdddedf".
|
|
90
|
+
now = (current_transaction_info::get_transaction_time())?.
|
|
91
|
+
// Seed all three stores directly (cross-library store write, as the qa
|
|
92
|
+
// setters in test_actor do for contact_pv/contact_caps).
|
|
93
|
+
a2a_messaging::contact_migration cid -> (
|
|
94
|
+
$phase -> "active", $initiator -> TRUE, $local_nonce -> nonce,
|
|
95
|
+
$peer_nonce -> nonce, $epoch -> ep, $session_id -> sid,
|
|
96
|
+
$local_bundle -> NIL, $local_fp -> NIL, $attempts -> 1, $updated -> now ).
|
|
97
|
+
a2a_messaging::contact_e2e_epoch cid -> ($epoch -> ep, $session_id -> sid).
|
|
98
|
+
a2a_messaging::mig_deferred cid -> [ ($text -> "queued", $wire_id -> "w1", $reply_to -> NIL, $date -> now) ].
|
|
99
|
+
|
|
100
|
+
blob = a2a_messaging::export_core_state NIL.
|
|
101
|
+
return transaction::success [ _return_data (
|
|
102
|
+
$has_migration -> ((blob $contact_migration) != NIL),
|
|
103
|
+
$has_epoch -> ((blob $contact_e2e_epoch) != NIL),
|
|
104
|
+
$has_deferred -> ((blob $mig_deferred) != NIL),
|
|
105
|
+
$phase -> (((blob $contact_migration) as any) cid $phase),
|
|
106
|
+
$epoch_sid_present -> (((((blob $contact_e2e_epoch) as any) cid) $session_id) != NIL),
|
|
107
|
+
$deferred_len -> (_count (((blob $mig_deferred) as any) cid|))
|
|
108
|
+
) ].
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Pre-0.9 import: a blob WITHOUT the three fields must leave the stores empty
|
|
112
|
+
// (absence = legacy). We hand import_core_state a minimal blob and confirm.
|
|
113
|
+
trn qa_mig_import_legacy _
|
|
114
|
+
{
|
|
115
|
+
current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
|
|
116
|
+
// A faithful pre-0.9 blob: the original-schema required fields (my_name,
|
|
117
|
+
// contacts, peer_ads) present, the three 0.9.0 migration fields ABSENT —
|
|
118
|
+
// exactly what a 0.8-era export looks like. Import must leave the migration
|
|
119
|
+
// stores empty (guarded reads skip absent fields; absence = legacy, §5.1).
|
|
120
|
+
a2a_messaging::import_core_state ( $format_version -> 1, $my_name -> "", $contacts -> (,), $peer_ads -> (,) ).
|
|
121
|
+
exp = a2a_messaging::export_core_state NIL.
|
|
122
|
+
probe = _new_id "qa legacy probe".
|
|
123
|
+
return transaction::success [ _return_data (
|
|
124
|
+
$migration_absent -> (((exp $contact_migration) as any) probe == NIL),
|
|
125
|
+
$epoch_absent -> (((exp $contact_e2e_epoch) as any) probe == NIL),
|
|
126
|
+
$deferred_absent -> (((exp $mig_deferred) as any) probe == NIL)
|
|
127
|
+
) ].
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
trn qa_mig_import_pre_dr _
|
|
131
|
+
{
|
|
132
|
+
current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
|
|
133
|
+
// A faithful PRE-0.11 / pre-stamp (v0) blob: the original-schema required
|
|
134
|
+
// fields only — NO $format_version (absent stamp == version 0 per the
|
|
135
|
+
// migration contract) and, critically, NO $e2e_sessions. This is what
|
|
136
|
+
// every deployed consumer's state_data.bin looks like before the DR
|
|
137
|
+
// upgrade. Import must SUCCEED (the #137 gap: the unguarded
|
|
138
|
+
// $e2e_sessions record-cast raised "SAFE cast to record failed" on this
|
|
139
|
+
// exact shape, failing the whole import — contacts included).
|
|
140
|
+
a2a_messaging::import_core_state ( $my_name -> "pre-dr", $contacts -> (,), $peer_ads -> (,) ).
|
|
141
|
+
exp = a2a_messaging::export_core_state NIL.
|
|
142
|
+
return transaction::success [ _return_data (
|
|
143
|
+
$imported_name -> (exp $my_name),
|
|
144
|
+
$format_stamped -> ((exp $format_version) != NIL)
|
|
145
|
+
) ].
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---- Phase-B staged-rotation exercise (adapt e2e §5.5) ----------------
|
|
149
|
+
// Thin qa wrappers over the adapt e2e facade so a two-node driver can drive
|
|
150
|
+
// establish -> stage -> commit and assert the rotate-once property. The driver
|
|
151
|
+
// shuttles the opaque envelope fields + the sender's e2e identity key between
|
|
152
|
+
// two packets (there is no core FSM here — this is the crypto-layer property).
|
|
153
|
+
trn readonly qa_e2e_bundle _ { return ($bundle -> (_write (e2e::my_public_bundle()))). }
|
|
154
|
+
trn readonly qa_e2e_ik _ { return ($ik -> ((e2e::my_public_bundle()) $ik_curve)). }
|
|
155
|
+
|
|
156
|
+
// Establish the LIVE session (encrypt_to lazily creates it) and emit the pre-key envelope.
|
|
157
|
+
trn qa_e2e_first_send _:($cid -> cid: global_id, $pt -> pt: bin, $peer -> peer_blob: bin)
|
|
158
|
+
{
|
|
159
|
+
peer = (_read_or_abort peer_blob) safe address_document_types::t_e2e_bundle.
|
|
160
|
+
env = e2e::encrypt_to cid pt peer.
|
|
161
|
+
e = env $e2e_envelope.
|
|
162
|
+
return transaction::success [ _return_data ($session_id -> (e $session_id), $olm_type -> (e $olm_type), $ciphertext -> (e $ciphertext)) ].
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Receive on the LIVE session (decode-seam: establishes/decrypts + commits m_sessions).
|
|
166
|
+
trn qa_e2e_recv _:($from -> from_cid: global_id, $ik -> ik: bin, $olm_type -> ot: int, $ciphertext -> ct: bin)
|
|
167
|
+
{
|
|
168
|
+
r = e2e::decrypt_and_commit from_cid ik ot ct.
|
|
169
|
+
pt is bin+ = NIL. if r $ok { pt -> (r $plaintext) as bin. }
|
|
170
|
+
return transaction::success [ _return_data ( $ok -> (r $ok), $plaintext -> pt,
|
|
171
|
+
$code -> ((r $ok) ?? "" ; ((r $error)?) $code) ) ].
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Stage a FRESH outbound rotation (live session untouched); returns the staged session id.
|
|
175
|
+
trn qa_e2e_stage_out _:($cid -> cid: global_id, $peer -> peer_blob: bin)
|
|
176
|
+
{
|
|
177
|
+
peer = (_read_or_abort peer_blob) safe address_document_types::t_e2e_bundle.
|
|
178
|
+
return transaction::success [ _return_data ($sid -> (e2e::stage_outbound_rotation cid peer)) ].
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Encrypt on the STAGED session (pre-key on the fresh session).
|
|
182
|
+
trn qa_e2e_enc_staged _:($cid -> cid: global_id, $pt -> pt: bin)
|
|
183
|
+
{
|
|
184
|
+
env = e2e::encrypt_staged cid pt.
|
|
185
|
+
e = env $e2e_envelope.
|
|
186
|
+
return transaction::success [ _return_data ($session_id -> (e $session_id), $olm_type -> (e $olm_type), $ciphertext -> (e $ciphertext)) ].
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Stage an INBOUND rotation from the peer's pre-key (m_sessions untouched).
|
|
190
|
+
trn qa_e2e_stage_in _:($from -> from_cid: global_id, $ik -> ik: bin, $ciphertext -> ct: bin)
|
|
191
|
+
{
|
|
192
|
+
r = e2e::stage_inbound_rotation from_cid ik ct.
|
|
193
|
+
pt is bin+ = NIL. if r $ok { pt -> (r $plaintext) as bin. }
|
|
194
|
+
return transaction::success [ _return_data ( $ok -> (r $ok), $plaintext -> pt,
|
|
195
|
+
$code -> ((r $ok) ?? "" ; ((r $error)?) $code) ) ].
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Promote staged -> active (idempotent; NIL if nothing staged).
|
|
199
|
+
trn qa_e2e_commit _:($cid -> cid: global_id)
|
|
200
|
+
{
|
|
201
|
+
sid = e2e::commit_rotation cid.
|
|
202
|
+
return transaction::success [ _return_data ($sid -> sid, $committed -> (sid != NIL)) ].
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
trn readonly qa_e2e_active _:($cid -> cid: global_id) { return ($sid -> (e2e::active_session_id cid)). }
|
|
206
|
+
trn readonly qa_e2e_staged _:($cid -> cid: global_id) { return ($sid -> (e2e::staged_session_id cid)). }
|
|
207
|
+
|
|
208
|
+
// Phase D §5.6 — the app-data route verdict for a cid (5-state).
|
|
209
|
+
trn readonly qa_e2e_route _:($cid -> cid: global_id) { return ($route -> (a2a_messaging::e2e_route cid)). }
|
|
210
|
+
|
|
211
|
+
// ── Increment-B app-e2e delivery observability. qa_recv_last surfaces the last plaintext the
|
|
212
|
+
// on_message_received hook stored (proof handle_receive_e2e_message decrypted + delivered it);
|
|
213
|
+
// qa_recv_reset clears it between assertions; qa_recv_set_abort toggles the must-fix-C rollback
|
|
214
|
+
// probe (the app hook aborts → the do_ic promotion rolls back with the tx).
|
|
215
|
+
trn readonly qa_recv_last _ { return ($text -> qa_recv_text, $wire -> qa_recv_wire, $count -> qa_recv_count, $filename -> qa_recv_file, $flen -> qa_recv_flen). }
|
|
216
|
+
trn qa_recv_reset _ { qa_recv_text -> "". qa_recv_wire -> "". qa_recv_file -> "". qa_recv_flen -> 0. return transaction::success [ _return_data ($ok -> TRUE) ]. }
|
|
217
|
+
trn qa_recv_set_abort _:($abort -> ab: bool) { qa_recv_abort -> ab. return transaction::success [ _return_data ($abort -> qa_recv_abort) ]. }
|
|
218
|
+
|
|
219
|
+
// ── Full-state persistence (reload port). The host stores the WHOLE export_core_state
|
|
220
|
+
// as ONE opaque field $core (design §3) and replays it on boot; commit_e2e_restore then
|
|
221
|
+
// installs the restored $e2e_sessions. Same shape as test_actor.mu and the mcp host.
|
|
222
|
+
trn readonly export_state _ { return ($core -> (a2a_messaging::export_core_state NIL)). }
|
|
223
|
+
trn import_state data: any
|
|
224
|
+
{
|
|
225
|
+
current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
|
|
226
|
+
a2a_messaging::import_core_state (data $core).
|
|
227
|
+
return transaction::success [ _return_data ($imported -> TRUE) ].
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Force a LEGACY plaintext box (receive_message_tx) to a contact, BYPASSING e2e_route — lets the
|
|
231
|
+
// driver deliver a legacy inbound at a receiver regardless of the sender's route, to prove the
|
|
232
|
+
// §5.7 receive-side downgrade gate: refused at an EPOCH-pinned receiver, delivered at a
|
|
233
|
+
// seen-not-epoch one. Mirrors send_message's legacy box branch.
|
|
234
|
+
trn qa_send_legacy _:($contact -> cref: str, $text -> text: str)
|
|
235
|
+
{
|
|
236
|
+
target_id = a2a_messaging::resolve_contact cref.
|
|
237
|
+
wid = _str (_new_id "qa legacy").
|
|
238
|
+
// Direct send_encrypted_tx (no execute_transaction wrapper — the migration commit/confirm
|
|
239
|
+
// handlers relay this way too; the wrapper's generic would blow the unit's meta fuel).
|
|
240
|
+
return transaction::success [
|
|
241
|
+
encrypted_channel::send_encrypted_tx target_id (
|
|
242
|
+
$name -> "::a2a_messaging::receive_message",
|
|
243
|
+
$targ -> ($text -> text, $wire_id -> wid, $pv -> a2a_versions::wire_version) ),
|
|
244
|
+
_return_data ($sent_to -> target_id, $wire_id -> wid) ].
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Synthesize a box-only COMMITTED-INITIATOR FSM entry over an already-staged rotation (the
|
|
248
|
+
// guard-matrix implicit-confirm setup, without a live broker handshake). $session_id is read
|
|
249
|
+
// from the current staged slot so committed_match holds. $seen=TRUE makes it PRODUCTION-LIKE (a
|
|
250
|
+
// real migrating pair advertises cap_e2e ⇒ contact_e2e_seen is set by `committed`), which is
|
|
251
|
+
// exactly the state where do_ic must STILL fire (MigrationReview #3 do_ic-decoupling bug).
|
|
252
|
+
// NOTE: forgery-through-the-handler (bad-pv/tampered-emsig/wrong-$to/relay-rebox → ABORT) is NOT
|
|
253
|
+
// re-tested at the handler level — the mig_actor unit is at the meta-fuel ceiling (encrypt_to +
|
|
254
|
+
// send_encrypted_tx in one trn blows it), and the handler calls the SAME audited
|
|
255
|
+
// e2e::decode_migration_envelope the existing decode-GUARD section already exercises for all four
|
|
256
|
+
// forgeries; the happy round-trip proves the handler invokes it end-to-end and gates on $ok.
|
|
257
|
+
|
|
258
|
+
// §5.4 trigger-GATE test helpers (criterion-1 boundary). ISOLATED: advertising cap_e2e_migrate
|
|
259
|
+
// here does NOT touch the full suite's test_actor. qa_mig_should_trigger reads the PURE gate
|
|
260
|
+
// (no send) so we exercise fail-closed WITHOUT needing a registered peer.
|
|
261
|
+
trn qa_init_caps _:($advertise -> adv: str[])
|
|
262
|
+
{
|
|
263
|
+
a2a_capabilities::init (
|
|
264
|
+
$describe -> fn (_: any) -> a2a_capabilities::app_manifest_t
|
|
265
|
+
{ return ($version -> 1, $app_id -> "mig.actor", $name -> "actor", $description -> "", $monitoring_status -> "off", $capabilities -> (,)). },
|
|
266
|
+
$supported -> [], $handlers -> (,),
|
|
267
|
+
$on_unknown -> fn (_: any) -> transaction::action::type[] { return []. },
|
|
268
|
+
$authorizer -> NIL, $advertise -> adv,
|
|
269
|
+
$on_advertise -> a2a_messaging::handle_capability_advertise,
|
|
270
|
+
$on_advertise_ack -> a2a_messaging::handle_capability_advertise_ack ).
|
|
271
|
+
return transaction::success [ _return_data ($set -> TRUE) ].
|
|
272
|
+
}
|
|
273
|
+
trn qa_learn_peer _:($cid -> cid: global_id, $pv -> pv: int, $caps -> caps: str[])
|
|
274
|
+
{
|
|
275
|
+
a2a_messaging::learn_contact_version cid pv caps.
|
|
276
|
+
return transaction::success [ _return_data ($ok -> TRUE) ].
|
|
277
|
+
}
|
|
278
|
+
trn readonly qa_mig_should_trigger _:($cid -> cid: global_id) { return ($fire -> (a2a_messaging::mig_should_trigger cid)). }
|
|
279
|
+
trn readonly qa_cap_state _:($cid -> cid: global_id)
|
|
280
|
+
{
|
|
281
|
+
pc = a2a_messaging::contact_caps cid.
|
|
282
|
+
confirmed = a2a_messaging::contact_advertised_caps cid.
|
|
283
|
+
exported = (a2a_messaging::export_core_state NIL) $contact_advertised_caps cid.
|
|
284
|
+
return (
|
|
285
|
+
$caps -> (pc == NIL ?? [] ; pc?),
|
|
286
|
+
$confirmed -> (confirmed == NIL ?? "" ; confirmed?),
|
|
287
|
+
$confirmed_persisted -> (exported != NIL && exported? == (confirmed == NIL ?? "" ; confirmed?)),
|
|
288
|
+
$current -> (a2a_messaging::caps_fingerprint NIL),
|
|
289
|
+
$pinned -> (a2a_messaging::e2e_pinned cid)
|
|
290
|
+
).
|
|
291
|
+
}
|
|
292
|
+
trn qa_clear_cap_ack _:($cid -> cid: global_id)
|
|
293
|
+
{
|
|
294
|
+
if (a2a_messaging::contact_advertised_caps cid) != NIL
|
|
295
|
+
{ delete a2a_messaging::contact_advertised_caps cid. }
|
|
296
|
+
return transaction::success [ _return_data ($cleared -> TRUE) ].
|
|
297
|
+
}
|
|
298
|
+
trn readonly qa_receipt_gate _:($cid -> cid: global_id)
|
|
299
|
+
{ return ($enabled -> (a2a_messaging::receipt_gate cid)). }
|
|
300
|
+
|
|
301
|
+
// core 0.10 born-DR probe/override. A pair established through a real invite handshake presents a
|
|
302
|
+
// v2 (bundle-carrying) AD, so core marks it born-DR (contact_born_dr=TRUE) — correctly INELIGIBLE
|
|
303
|
+
// for migration (B2 invariant). qa_mig_born_dr reads that marker; qa_mig_clear_born_dr deletes it,
|
|
304
|
+
// turning the just-handshaked pair into a genuinely PRE-EXISTING LEGACY session (as if it had been
|
|
305
|
+
// registered from a v1 AD before DR) so the proactive-trigger paths (advertise_migrate / sweep) can
|
|
306
|
+
// be exercised end-to-end against a legacy pair. No crypto state touched — only the eligibility flag.
|
|
307
|
+
trn readonly qa_mig_born_dr _:($cid -> cid: global_id) { return ($born_dr -> ((a2a_messaging::contact_born_dr cid) == TRUE)). }
|
|
308
|
+
trn qa_mig_clear_born_dr _:($cid -> cid: global_id)
|
|
309
|
+
{
|
|
310
|
+
delete a2a_messaging::contact_born_dr cid.
|
|
311
|
+
return transaction::success [ _return_data ($born_dr -> ((a2a_messaging::contact_born_dr cid) == TRUE)) ].
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// §5.6 sweep test helpers. Force an `offered` FSM entry (real snapshot, via mig_offer_actions —
|
|
315
|
+
// discard the send) so the sweep has something to re-drive; and set $attempts to exercise the cap.
|
|
316
|
+
trn qa_mig_force_offered _:($cid -> cid: global_id)
|
|
317
|
+
{
|
|
318
|
+
sc a2a_messaging::mig_offer_actions cid -- ( -> a) { } // side-effect: writes contact_migration=offered
|
|
319
|
+
return transaction::success [ _return_data ($ok -> TRUE) ].
|
|
320
|
+
}
|
|
321
|
+
trn qa_mig_set_attempts _:($cid -> cid: global_id, $n -> n: int)
|
|
322
|
+
{
|
|
323
|
+
st = a2a_messaging::contact_migration cid.
|
|
324
|
+
a2a_messaging::contact_migration cid -> ( $phase -> ((st?) $phase), $initiator -> ((st?) $initiator),
|
|
325
|
+
$local_nonce -> ((st?) $local_nonce), $peer_nonce -> ((st?) $peer_nonce), $epoch -> ((st?) $epoch),
|
|
326
|
+
$session_id -> ((st?) $session_id), $local_bundle -> ((st?) $local_bundle), $local_fp -> ((st?) $local_fp),
|
|
327
|
+
$attempts -> n, $updated -> ((st?) $updated) ).
|
|
328
|
+
return transaction::success [ _return_data ($ok -> TRUE) ].
|
|
329
|
+
}
|
|
330
|
+
// Simulate my published bundle rotating since the snapshot: corrupt $local_fp so it no longer
|
|
331
|
+
// matches the live produce-fp (drives the sweep's §5.4-5 supersession path).
|
|
332
|
+
trn qa_mig_corrupt_fp _:($cid -> cid: global_id)
|
|
333
|
+
{
|
|
334
|
+
st = a2a_messaging::contact_migration cid.
|
|
335
|
+
bogus is bin = _hash_code_to_binary (_value_id ($x -> "bogus-rotated-fp")).
|
|
336
|
+
a2a_messaging::contact_migration cid -> ( $phase -> ((st?) $phase), $initiator -> ((st?) $initiator),
|
|
337
|
+
$local_nonce -> ((st?) $local_nonce), $peer_nonce -> ((st?) $peer_nonce), $epoch -> ((st?) $epoch),
|
|
338
|
+
$session_id -> ((st?) $session_id), $local_bundle -> ((st?) $local_bundle), $local_fp -> bogus,
|
|
339
|
+
$attempts -> ((st?) $attempts), $updated -> ((st?) $updated) ).
|
|
340
|
+
return transaction::success [ _return_data ($ok -> TRUE) ].
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Phase D §5.6 flush test helpers. Inject a 3-message mig_deferred queue for `cid` (via the
|
|
344
|
+
// import path — REPLACE-if-present, leaves the active pin untouched), read the queued wire_ids
|
|
345
|
+
// in order, and invoke the real flush (drain FIFO + clear via a2a_messaging::flush_mig_deferred_actions).
|
|
346
|
+
trn qa_mig_inject_deferred _:($cid -> cid: global_id)
|
|
347
|
+
{
|
|
348
|
+
now = (current_transaction_info::get_transaction_time())?.
|
|
349
|
+
q is a2a_messaging::deferred_msg_t[] = [
|
|
350
|
+
($text -> "m0", $wire_id -> "w0", $reply_to -> NIL, $date -> now),
|
|
351
|
+
($text -> "m1", $wire_id -> "w1", $reply_to -> NIL, $date -> now),
|
|
352
|
+
($text -> "m2", $wire_id -> "w2", $reply_to -> NIL, $date -> now) ].
|
|
353
|
+
a2a_messaging::mig_deferred cid -> q.
|
|
354
|
+
return transaction::success [ _return_data ($injected -> 3) ].
|
|
355
|
+
}
|
|
356
|
+
trn readonly qa_mig_deferred_ids _:($cid -> cid: global_id)
|
|
357
|
+
{
|
|
358
|
+
ids is str[] = [].
|
|
359
|
+
q = a2a_messaging::mig_deferred cid.
|
|
360
|
+
if q != NIL { sc q? -- ( -> m) { ids (_count ids|) -> (m $wire_id). } }
|
|
361
|
+
return ($ids -> ids, $count -> (_count ids|)).
|
|
362
|
+
}
|
|
363
|
+
trn qa_mig_flush _:($cid -> cid: global_id)
|
|
364
|
+
{
|
|
365
|
+
order is str = "".
|
|
366
|
+
q = a2a_messaging::mig_deferred cid.
|
|
367
|
+
if q != NIL { sc q? -- ( -> m) { order -> order + (m $wire_id) + ",". } }
|
|
368
|
+
acts is transaction::action::type[] = a2a_messaging::flush_mig_deferred_actions cid.
|
|
369
|
+
return transaction::success [ _return_data ($flushed -> (_count acts|), $order -> order) ].
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ---- decode_migration_envelope GUARD matrix (point-1 divergence + binding + forgery/replay) ----
|
|
373
|
+
// This packet's OWN address document (e2e bundle + sign keys), so a receiver can authenticate a
|
|
374
|
+
// migration envelope as coming from this cid (the sender AD decode_migration_envelope binds to).
|
|
375
|
+
trn readonly qa_produce_ad _ { return ($ad -> (_write (address_document::produce_my_address_document()))). }
|
|
376
|
+
// Encrypt on the STAGED session and return the FULL e2e envelope + emsig (blobs), so the driver
|
|
377
|
+
// can feed them (and tampered variants) to qa_mig_decode.
|
|
378
|
+
trn qa_mig_enc_full _:($cid -> cid: global_id, $pt -> pt: bin)
|
|
379
|
+
{
|
|
380
|
+
env = e2e::encrypt_staged cid pt.
|
|
381
|
+
return transaction::success [ _return_data ($env -> (_write (env $e2e_envelope)), $emsig -> (_write (env $emsignature))) ].
|
|
382
|
+
}
|
|
383
|
+
// Drive e2e::decode_migration_envelope with driver-supplied (tamperable) from/to/AD/env/emsig.
|
|
384
|
+
// Forgery — bad wire_pv (S2), tampered/foreign emsig or wrong $to (S1), AD.cid≠from_cid (binding)
|
|
385
|
+
// — ABORTS the tx (§1.1); an Olm-level failure / replayed pre-key returns $ok=FALSE. Proves the
|
|
386
|
+
// shared S1/S2 path is REAL (not a no-op → the recv_authenticated divergence guard) and that the
|
|
387
|
+
// forgery-abort vs replay-reject split + decode_migration_envelope's own binding gates hold.
|
|
388
|
+
// pv_override >= 0 rebuilds the envelope with that $pv (to exercise the S2 dialect check).
|
|
389
|
+
trn qa_mig_decode _:($from -> from_cid: global_id, $to -> to_cid: global_id, $ad -> ad_blob: bin, $env -> env_blob: bin, $emsig -> emsig_blob: bin, $pv_override -> pvo: int)
|
|
390
|
+
{
|
|
391
|
+
ad = (_read_or_abort ad_blob) safe address_document_types::t_address_document.
|
|
392
|
+
e0 = (_read_or_abort env_blob) safe e2e::t_e2e_envelope.
|
|
393
|
+
emsig = (_read_or_abort emsig_blob) safe crypto_signature.
|
|
394
|
+
env is e2e::t_e2e_envelope+ = e0.
|
|
395
|
+
if pvo >= 0 { env -> ( $session_id -> ((e0?) $session_id), $olm_type -> ((e0?) $olm_type), $ciphertext -> ((e0?) $ciphertext), $pv -> pvo ) safe e2e::t_e2e_envelope. }
|
|
396
|
+
r = e2e::decode_migration_envelope from_cid to_cid (ad?) (env?) (emsig?).
|
|
397
|
+
pt is bin+ = NIL. if r $ok { pt -> (r $plaintext) as bin. }
|
|
398
|
+
return transaction::success [ _return_data ( $ok -> (r $ok), $plaintext -> pt, $code -> ((r $ok) ?? "" ; ((r $error)?) $code) ) ].
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Decode-seam test controls: install a mig-pending hook that reads the togglable flag,
|
|
402
|
+
// then flip the flag to steer decrypt_and_commit's stage-vs-replace decision.
|
|
403
|
+
trn qa_e2e_install_mig_hook _
|
|
404
|
+
{
|
|
405
|
+
e2e::set_mig_pending_hook (fn (_: global_id) -> bool { return qa_mig_pending. }).
|
|
406
|
+
return transaction::success [ _return_data ($ok -> TRUE) ].
|
|
407
|
+
}
|
|
408
|
+
trn qa_e2e_set_mig_pending _:($pending -> p: bool)
|
|
409
|
+
{
|
|
410
|
+
qa_mig_pending -> p.
|
|
411
|
+
return transaction::success [ _return_data ($pending -> qa_mig_pending) ].
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ---- Cross-library atomicity gate (spec §5.5 / plan B gate 5, blocks C/D) ----
|
|
415
|
+
// Within ONE transaction: write ADAPT-library state (e2e m_staged via
|
|
416
|
+
// stage_outbound_rotation), write CORE-library state (a2a_messaging
|
|
417
|
+
// contact_migration), queue an ACTION (a send), then ABORT. If the toolchain
|
|
418
|
+
// gives cross-library atomic rollback, a subsequent read sees NEITHER write
|
|
419
|
+
// (and the action never fired). If it does NOT, §5.5 handlers must switch to
|
|
420
|
+
// explicit prepare/commit records — so this gate's result decides phase C's shape.
|
|
421
|
+
trn qa_atomicity_abort _:($cid -> cid: global_id, $peer -> peer_blob: bin)
|
|
422
|
+
{
|
|
423
|
+
peer = (_read_or_abort peer_blob) safe address_document_types::t_e2e_bundle.
|
|
424
|
+
now = (current_transaction_info::get_transaction_time())?.
|
|
425
|
+
// 1) ADAPT-library state write (e2e packet state):
|
|
426
|
+
sid = e2e::stage_outbound_rotation cid peer.
|
|
427
|
+
// 2) CORE-library state write (a2a_messaging packet state):
|
|
428
|
+
a2a_messaging::contact_migration cid -> (
|
|
429
|
+
$phase -> "offered", $initiator -> TRUE, $local_nonce -> sid,
|
|
430
|
+
$peer_nonce -> NIL, $epoch -> NIL, $session_id -> NIL,
|
|
431
|
+
$local_bundle -> NIL, $local_fp -> NIL, $attempts -> 1, $updated -> now ).
|
|
432
|
+
// 3) ABORT the whole tx AFTER both cross-library writes. Any transaction::action
|
|
433
|
+
// (send / _save_state) would ride the success return, which is never reached — so
|
|
434
|
+
// a queued action is inherently discarded on abort (MUFL actions fire ONLY on
|
|
435
|
+
// success). The load-bearing question is whether the two cross-LIBRARY STATE writes
|
|
436
|
+
// above roll back together; qa_atomicity_check reads them after.
|
|
437
|
+
abort ("atomicity probe: forced rollback after adapt-write + core-write (staged " + (_str (_value_id sid)) + ")") when TRUE.
|
|
438
|
+
return transaction::success [ _return_data ($unreachable -> TRUE) ].
|
|
439
|
+
}
|
|
440
|
+
// Fault injection (MigrationReview): a self-heal REPLACE on a live session whose inner
|
|
441
|
+
// dispatch then ABORTS. decrypt_and_commit replaces m_sessions[from_cid] (0.8.0 self-heal),
|
|
442
|
+
// then we abort — modelling the inner tx failing. If the decode tx is atomic, the replace
|
|
443
|
+
// rolls back and the live session is UNCHANGED (the immediate-replace design is only safe
|
|
444
|
+
// BECAUSE of this; without it, an aborted dispatch would clobber the live session — the very
|
|
445
|
+
// unilateral-reset the seam closes).
|
|
446
|
+
trn qa_e2e_recv_abort _:($from -> from_cid: global_id, $ik -> ik: bin, $olm_type -> ot: int, $ciphertext -> ct: bin)
|
|
447
|
+
{
|
|
448
|
+
r = e2e::decrypt_and_commit from_cid ik ot ct.
|
|
449
|
+
abort ("fault-injection: inner dispatch aborts after self-heal replace (ok=" + (_str (r $ok)) + ")") when TRUE.
|
|
450
|
+
return transaction::success [ _return_data ($ok -> (r $ok)) ].
|
|
451
|
+
}
|
|
452
|
+
// ---- Phase-C §5.2 helpers (election + epoch determinism) ----
|
|
453
|
+
trn readonly qa_mig_initiator _:($peer -> peer: global_id)
|
|
454
|
+
{ return ($initiator -> (a2a_messaging::mig_initiator peer), $mycid -> (_get_container_id())). }
|
|
455
|
+
trn readonly qa_mig_epoch _:($lo -> lo: global_id, $hi -> hi: global_id, $nlo -> nlo: bin, $nhi -> nhi: bin, $flo -> flo: bin, $fhi -> fhi: bin)
|
|
456
|
+
{ return ($epoch -> (a2a_messaging::mig_epoch lo hi nlo nhi flo fhi)). }
|
|
457
|
+
trn readonly qa_mig_bundle_fp _
|
|
458
|
+
{ return ($fp -> (a2a_messaging::e2e_bundle_fp (address_document::produce_my_address_document()))). }
|
|
459
|
+
|
|
460
|
+
// Trigger an OFFER to a contact (invokes mig_offer_actions: persists offered + sends).
|
|
461
|
+
trn qa_mig_trigger_offer _:($peer -> peer: global_id)
|
|
462
|
+
{
|
|
463
|
+
current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
|
|
464
|
+
actions is transaction::action::type[] = a2a_messaging::mig_offer_actions peer.
|
|
465
|
+
actions (_count actions|) -> (transaction::action::return_data ($kind -> $data, $payload -> ($triggered -> TRUE))).
|
|
466
|
+
actions (_count actions|) -> (transaction::action::return_data ($kind -> $save_state)).
|
|
467
|
+
return transaction::success actions.
|
|
468
|
+
}
|
|
469
|
+
// Re-send the STORED offer byte-identically from the initiator's FSM snapshot
|
|
470
|
+
// (simulates the sweep retransmit / a broker duplicate — same nonce, same bundle).
|
|
471
|
+
trn qa_mig_resend_offer _:($peer -> peer: global_id)
|
|
472
|
+
{
|
|
473
|
+
current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
|
|
474
|
+
st = a2a_messaging::contact_migration peer.
|
|
475
|
+
s = _read_or_abort (((st?) $local_bundle)?).
|
|
476
|
+
return transaction::success [
|
|
477
|
+
encrypted_channel::send_encrypted_tx peer (
|
|
478
|
+
$name -> a2a_messaging::e2e_migrate_offer_tx,
|
|
479
|
+
$targ -> ( $ad -> (s $ad), $cert -> (s $cert), $root_profile -> (s $root_profile),
|
|
480
|
+
$cp_binding -> (s $cp_binding), $nonce -> ((st?) $local_nonce), $peer_nonce -> NIL,
|
|
481
|
+
$pv -> a2a_versions::wire_version, $caps -> (a2a_capabilities::self_cap_ids NIL) ) ),
|
|
482
|
+
(transaction::action::return_data ($kind -> $data, $payload -> ($resent -> TRUE))) ].
|
|
483
|
+
}
|
|
484
|
+
// Read the FSM entry for a contact (phase / epoch / initiator).
|
|
485
|
+
trn readonly qa_mig_state _:($cid -> cid: global_id)
|
|
486
|
+
{
|
|
487
|
+
st = a2a_messaging::contact_migration cid.
|
|
488
|
+
return (
|
|
489
|
+
$present -> (st != NIL),
|
|
490
|
+
$phase -> ((st == NIL ?? "" ; ((st?) $phase))),
|
|
491
|
+
$epoch -> ((st == NIL ?? NIL ; ((st?) $epoch))),
|
|
492
|
+
$initiator -> ((st == NIL ?? FALSE ; ((st?) $initiator)))
|
|
493
|
+
).
|
|
494
|
+
}
|
|
495
|
+
// Read the committed-epoch pin (contact_e2e_epoch) + the advertisement pin (contact_e2e_seen).
|
|
496
|
+
trn readonly qa_mig_pin _:($cid -> cid: global_id)
|
|
497
|
+
{
|
|
498
|
+
ep = a2a_messaging::contact_e2e_epoch cid.
|
|
499
|
+
epb is bin+ = NIL.
|
|
500
|
+
esid is bin+ = NIL.
|
|
501
|
+
if ep != NIL { epb -> (ep?) $epoch. esid -> (ep?) $session_id. }
|
|
502
|
+
return (
|
|
503
|
+
$pinned -> (ep != NIL),
|
|
504
|
+
$epoch -> epb,
|
|
505
|
+
$session_id -> esid,
|
|
506
|
+
$seen -> ((a2a_messaging::contact_e2e_seen cid) == TRUE)
|
|
507
|
+
).
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Read both libraries' state for `cid` AFTER the aborted tx: both must be absent.
|
|
511
|
+
trn readonly qa_atomicity_check _:($cid -> cid: global_id)
|
|
512
|
+
{
|
|
513
|
+
return (
|
|
514
|
+
$core_present -> ((a2a_messaging::contact_migration cid) != NIL),
|
|
515
|
+
$adapt_present -> ((e2e::staged_session_id cid) != NIL)
|
|
516
|
+
).
|
|
517
|
+
}
|
|
518
|
+
}
|