@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,294 @@
|
|
|
1
|
+
// App-e2e RECEIVE-path guard-matrix test actor (spec §5.5/§5.6/§4, increment C).
|
|
2
|
+
//
|
|
3
|
+
// SPLIT from mig_actor.mu for the SAME reason mig_actor split from test_actor: the
|
|
4
|
+
// compiler bounds meta-stage type-level reduction PER COMPILED UNIT (~1M steps), and
|
|
5
|
+
// mig_actor (which loads a2a_messaging + the full migration FSM surface) is already AT
|
|
6
|
+
// that ceiling — it cannot take the extra state-synthesis helper this matrix needs. This
|
|
7
|
+
// leaner unit loads a2a_messaging (NOT a2a_notifications) + only the qa surface the
|
|
8
|
+
// app-e2e guard matrix drives, reclaiming budget for qa_mig_set_committed.
|
|
9
|
+
application actor loads libraries
|
|
10
|
+
identity_proof_document,
|
|
11
|
+
attestation_document,
|
|
12
|
+
native_attestation_document,
|
|
13
|
+
transaction_message_decoder,
|
|
14
|
+
address_document,
|
|
15
|
+
address_document_types,
|
|
16
|
+
key_utils,
|
|
17
|
+
key_storage,
|
|
18
|
+
e2e,
|
|
19
|
+
continuation,
|
|
20
|
+
encrypted_channel,
|
|
21
|
+
a2a_versions,
|
|
22
|
+
a2a_capabilities,
|
|
23
|
+
a2a_protocol,
|
|
24
|
+
a2a_messaging,
|
|
25
|
+
current_transaction_info,
|
|
26
|
+
protocol_container,
|
|
27
|
+
version
|
|
28
|
+
uses transactions
|
|
29
|
+
{
|
|
30
|
+
hidden
|
|
31
|
+
{
|
|
32
|
+
fn _return_data (payload: any) = (transaction::action::return_data ($kind -> $data, $payload -> payload)).
|
|
33
|
+
_read_or_abort = grab( _read_or_abort ).
|
|
34
|
+
key_storage::init ($_read_or_abort -> _read_or_abort).
|
|
35
|
+
encrypted_channel::init ($_read_or_abort -> _read_or_abort).
|
|
36
|
+
// RECORDING storage hooks — surface the last delivered message/file so the driver can prove
|
|
37
|
+
// handle_receive_e2e_message/_file decrypted + DELIVERED the plaintext to the app hook.
|
|
38
|
+
// qa_recv_abort makes the message hook ABORT (must-fix-C rollback probe).
|
|
39
|
+
qa_recv_text is str = "".
|
|
40
|
+
qa_recv_wire is str = "".
|
|
41
|
+
qa_recv_file is str = "".
|
|
42
|
+
qa_recv_flen is int = 0.
|
|
43
|
+
qa_recv_count is int = 0.
|
|
44
|
+
qa_recv_abort is bool = FALSE.
|
|
45
|
+
a2a_messaging::init (
|
|
46
|
+
$_read_or_abort -> _read_or_abort,
|
|
47
|
+
$on_message_received -> fn (a: any) -> transaction::action::type[] {
|
|
48
|
+
abort "qa_recv_abort: app hook rejected the message (must-fix-C rollback probe)" WHEN qa_recv_abort.
|
|
49
|
+
qa_recv_text -> ((a $text) safe str).
|
|
50
|
+
if (a $wire_id) != NIL { qa_recv_wire -> ((a $wire_id) safe str). }
|
|
51
|
+
qa_recv_count -> (qa_recv_count + 1).
|
|
52
|
+
return []. },
|
|
53
|
+
$on_message_sent -> fn (_: any) -> transaction::action::type[] { return []. },
|
|
54
|
+
$on_contact_removed -> fn (_: any) -> transaction::action::type[] { return []. },
|
|
55
|
+
$on_file_received -> fn (a: any) -> transaction::action::type[] {
|
|
56
|
+
qa_recv_file -> ((a $filename) safe str).
|
|
57
|
+
qa_recv_flen -> (_binlen ((a $data) safe bin)).
|
|
58
|
+
qa_recv_count -> (qa_recv_count + 1).
|
|
59
|
+
return []. },
|
|
60
|
+
$on_file_sent -> fn (_: any) -> transaction::action::type[] { return []. },
|
|
61
|
+
$on_receipt_received -> fn (_: any) -> transaction::action::type[] { return []. }
|
|
62
|
+
).
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── e2e crypto-layer wrappers (shuttle opaque envelope fields + the peer ik between packets).
|
|
66
|
+
trn readonly qa_e2e_bundle _ { return ($bundle -> (_write (e2e::my_public_bundle()))). }
|
|
67
|
+
trn readonly qa_e2e_ik _ { return ($ik -> ((e2e::my_public_bundle()) $ik_curve)). }
|
|
68
|
+
|
|
69
|
+
trn qa_e2e_first_send _:($cid -> cid: global_id, $pt -> pt: bin, $peer -> peer_blob: bin)
|
|
70
|
+
{
|
|
71
|
+
peer = (_read_or_abort peer_blob) safe address_document_types::t_e2e_bundle.
|
|
72
|
+
env = e2e::encrypt_to cid pt peer.
|
|
73
|
+
e = env $e2e_envelope.
|
|
74
|
+
return transaction::success [ _return_data ($session_id -> (e $session_id), $olm_type -> (e $olm_type), $ciphertext -> (e $ciphertext)) ].
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
trn qa_e2e_recv _:($from -> from_cid: global_id, $ik -> ik: bin, $olm_type -> ot: int, $ciphertext -> ct: bin)
|
|
78
|
+
{
|
|
79
|
+
r = e2e::decrypt_and_commit from_cid ik ot ct.
|
|
80
|
+
pt is bin+ = NIL. if r $ok { pt -> (r $plaintext) as bin. }
|
|
81
|
+
return transaction::success [ _return_data ( $ok -> (r $ok), $plaintext -> pt ) ].
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
trn qa_e2e_stage_out _:($cid -> cid: global_id, $peer -> peer_blob: bin)
|
|
85
|
+
{
|
|
86
|
+
peer = (_read_or_abort peer_blob) safe address_document_types::t_e2e_bundle.
|
|
87
|
+
return transaction::success [ _return_data ($sid -> (e2e::stage_outbound_rotation cid peer)) ].
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
trn qa_e2e_enc_staged _:($cid -> cid: global_id, $pt -> pt: bin)
|
|
91
|
+
{
|
|
92
|
+
env = e2e::encrypt_staged cid pt.
|
|
93
|
+
e = env $e2e_envelope.
|
|
94
|
+
return transaction::success [ _return_data ($session_id -> (e $session_id), $olm_type -> (e $olm_type), $ciphertext -> (e $ciphertext)) ].
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
trn qa_e2e_stage_in _:($from -> from_cid: global_id, $ik -> ik: bin, $ciphertext -> ct: bin)
|
|
98
|
+
{
|
|
99
|
+
r = e2e::stage_inbound_rotation from_cid ik ct.
|
|
100
|
+
pt is bin+ = NIL. if r $ok { pt -> (r $plaintext) as bin. }
|
|
101
|
+
return transaction::success [ _return_data ( $ok -> (r $ok), $plaintext -> pt ) ].
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
trn qa_e2e_commit _:($cid -> cid: global_id)
|
|
105
|
+
{
|
|
106
|
+
sid = e2e::commit_rotation cid.
|
|
107
|
+
return transaction::success [ _return_data ($sid -> sid, $committed -> (sid != NIL)) ].
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
trn readonly qa_e2e_active _:($cid -> cid: global_id) { return ($sid -> (e2e::active_session_id cid)). }
|
|
111
|
+
trn readonly qa_e2e_staged _:($cid -> cid: global_id) { return ($sid -> (e2e::staged_session_id cid)). }
|
|
112
|
+
|
|
113
|
+
// ── delivery observability + the must-fix-C rollback probe.
|
|
114
|
+
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). }
|
|
115
|
+
trn qa_recv_reset _ { qa_recv_text -> "". qa_recv_wire -> "". qa_recv_file -> "". qa_recv_flen -> 0. return transaction::success [ _return_data ($ok -> TRUE) ]. }
|
|
116
|
+
trn qa_recv_set_abort _:($abort -> ab: bool) { qa_recv_abort -> ab. return transaction::success [ _return_data ($abort -> qa_recv_abort) ]. }
|
|
117
|
+
|
|
118
|
+
// Learn a peer's version/caps (caps incl. "core.e2e" → note_e2e_seen sets contact_e2e_seen).
|
|
119
|
+
trn qa_learn_peer _:($cid -> cid: global_id, $pv -> pv: int, $caps -> caps: str[])
|
|
120
|
+
{
|
|
121
|
+
a2a_messaging::learn_contact_version cid pv caps.
|
|
122
|
+
return transaction::success [ _return_data ($ok -> TRUE) ].
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Advertise a self-cap (e.g. cap_e2e_migrate) WITHOUT the proactive-offer side effect of the real
|
|
126
|
+
// advertise_migrate tx — so the D-path test can isolate the inbound-e2e trigger from the B-path.
|
|
127
|
+
trn qa_init_caps _:($advertise -> adv: str[])
|
|
128
|
+
{
|
|
129
|
+
a2a_capabilities::init (
|
|
130
|
+
$describe -> fn (_: any) -> a2a_capabilities::app_manifest_t
|
|
131
|
+
{ return ($version -> 1, $app_id -> "migapp.actor", $name -> "actor", $description -> "", $monitoring_status -> "off", $capabilities -> (,)). },
|
|
132
|
+
$supported -> [], $handlers -> (,),
|
|
133
|
+
$on_unknown -> fn (_: any) -> transaction::action::type[] { return []. },
|
|
134
|
+
$authorizer -> NIL, $advertise -> adv ).
|
|
135
|
+
return transaction::success [ _return_data ($set -> TRUE) ].
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Synthesize a box-only COMMITTED-INITIATOR FSM entry over an already-staged rotation (the
|
|
139
|
+
// implicit-confirm setup — no live broker handshake). $session_id must equal the staged slot's
|
|
140
|
+
// id so committed_match holds. $seen=TRUE makes it PRODUCTION-LIKE (a real migrating pair
|
|
141
|
+
// advertises cap_e2e ⇒ contact_e2e_seen set by `committed`) — the exact state where do_ic must
|
|
142
|
+
// STILL fire (MigrationReview #3). Fresh-budget unit ⇒ the inline mig_state_t literal fits.
|
|
143
|
+
trn qa_mig_set_committed _:($cid -> cid: global_id, $session_id -> sid: bin, $seen -> seen: bool)
|
|
144
|
+
{
|
|
145
|
+
now = (current_transaction_info::get_transaction_time())?.
|
|
146
|
+
a2a_messaging::contact_migration cid -> ( $phase -> "committed", $initiator -> TRUE,
|
|
147
|
+
$local_nonce -> sid, $peer_nonce -> sid, $epoch -> sid, $session_id -> sid,
|
|
148
|
+
$local_bundle -> sid, $local_fp -> sid, $attempts -> 0, $updated -> now ).
|
|
149
|
+
if seen { a2a_messaging::contact_e2e_seen cid -> TRUE. }
|
|
150
|
+
return transaction::success [ _return_data ($ok -> TRUE) ].
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Synthesize an ACTIVE responder over an already-PROMOTED rotation (post-commit state): FSM
|
|
154
|
+
// phase=active + the epoch pin bound to the LIVE active session (so mig_handle_replayed_commit's
|
|
155
|
+
// "pin.$session_id == active_session_id" evidence rule holds). $epoch is the shared migration
|
|
156
|
+
// epoch (== the initiator's committed epoch) so the re-confirm this responder emits on a
|
|
157
|
+
// redelivered commit carries the epoch the initiator gates on. Drives the §5.8 lost-confirm
|
|
158
|
+
// recovery: the initiator's sweep re-sends its commit → this active responder re-confirms.
|
|
159
|
+
trn qa_mig_set_active _:($cid -> cid: global_id, $epoch -> ep: bin, $initiator -> ini: bool)
|
|
160
|
+
{
|
|
161
|
+
now = (current_transaction_info::get_transaction_time())?.
|
|
162
|
+
asid = (e2e::active_session_id cid)?.
|
|
163
|
+
a2a_messaging::contact_migration cid -> ( $phase -> "active", $initiator -> ini,
|
|
164
|
+
$local_nonce -> ep, $peer_nonce -> ep, $epoch -> ep, $session_id -> asid,
|
|
165
|
+
$local_bundle -> ep, $local_fp -> ep, $attempts -> 0, $updated -> now ).
|
|
166
|
+
a2a_messaging::contact_e2e_epoch cid -> ( $epoch -> ep, $session_id -> asid ).
|
|
167
|
+
a2a_messaging::contact_e2e_seen cid -> TRUE.
|
|
168
|
+
return transaction::success [ _return_data ($ok -> TRUE, $asid -> asid) ].
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Synthesize an ACKNOWLEDGED responder with a stored migration epoch — the pre-commit FSM state
|
|
172
|
+
// (mig_pending TRUE ⇒ an inbound commit STAGES a fresh session). No epoch pin yet. Drives the
|
|
173
|
+
// §5.8 forged-commit-at-real-handler row: a crypto-valid commit whose INNER epoch/session is
|
|
174
|
+
// forged reaches handle_e2e_migrate_commit GATE 3 and is rejected (discard_rotation, stay acknowledged).
|
|
175
|
+
trn qa_mig_set_acknowledged _:($cid -> cid: global_id, $epoch -> ep: bin)
|
|
176
|
+
{
|
|
177
|
+
now = (current_transaction_info::get_transaction_time())?.
|
|
178
|
+
a2a_messaging::contact_migration cid -> ( $phase -> "acknowledged", $initiator -> FALSE,
|
|
179
|
+
$local_nonce -> ep, $peer_nonce -> ep, $epoch -> ep, $session_id -> ep,
|
|
180
|
+
$local_bundle -> ep, $local_fp -> ep, $attempts -> 0, $updated -> now ).
|
|
181
|
+
return transaction::success [ _return_data ($ok -> TRUE) ].
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Send a REAL migration COMMIT (initiator → responder): stage a fresh outbound rotation, encrypt
|
|
185
|
+
// the commit body (name + {epoch, session_id, pv}) as a PRE_KEY message on the staged session, box
|
|
186
|
+
// it as e2e_migrate_commit exactly as the initiator's commit leg does. The inner $session_id is
|
|
187
|
+
// ALWAYS the real staged session (so it matches the responder's carrier), leaving $epoch as the
|
|
188
|
+
// one caller-controlled gating field: pass the responder's stored epoch for a valid commit, or a
|
|
189
|
+
// different value to forge it. The crypto stays valid (S1/S2 + decrypt succeed), so a forged epoch
|
|
190
|
+
// is rejected at handle_e2e_migrate_commit GATE 3 — not at the decode seam. Returns the staged sid.
|
|
191
|
+
trn qa_send_commit _:($contact -> cref: str, $peer -> peer_blob: bin, $epoch -> ep: bin)
|
|
192
|
+
{
|
|
193
|
+
target_id = a2a_messaging::resolve_contact cref.
|
|
194
|
+
peer = (_read_or_abort peer_blob) safe address_document_types::t_e2e_bundle.
|
|
195
|
+
sid = e2e::stage_outbound_rotation target_id peer.
|
|
196
|
+
commit_body = _write ( $name -> "::a2a_messaging::e2e_migrate_commit",
|
|
197
|
+
$targ -> ( $epoch -> ep, $session_id -> sid, $pv -> a2a_versions::wire_version ) ).
|
|
198
|
+
env = e2e::encrypt_staged target_id commit_body.
|
|
199
|
+
return transaction::success [
|
|
200
|
+
encrypted_channel::send_encrypted_tx target_id (
|
|
201
|
+
$name -> "::a2a_messaging::e2e_migrate_commit",
|
|
202
|
+
$targ -> ( $e2e_envelope -> (env $e2e_envelope), $emsignature -> (env $emsignature) ) ),
|
|
203
|
+
_return_data ($sid -> sid) ].
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Set a migration entry's $attempts (drive the sweep to its cap without 30 real re-drives).
|
|
207
|
+
trn qa_mig_set_attempts _:($cid -> cid: global_id, $n -> n: int)
|
|
208
|
+
{
|
|
209
|
+
st = a2a_messaging::contact_migration cid.
|
|
210
|
+
a2a_messaging::contact_migration cid -> ( $phase -> ((st?) $phase), $initiator -> ((st?) $initiator),
|
|
211
|
+
$local_nonce -> ((st?) $local_nonce), $peer_nonce -> ((st?) $peer_nonce), $epoch -> ((st?) $epoch),
|
|
212
|
+
$session_id -> ((st?) $session_id), $local_bundle -> ((st?) $local_bundle), $local_fp -> ((st?) $local_fp),
|
|
213
|
+
$attempts -> n, $updated -> ((st?) $updated) ).
|
|
214
|
+
return transaction::success [ _return_data ($ok -> TRUE) ].
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Drop ALL peer address documents — the spec's "degraded contact" state (a breaking-change
|
|
218
|
+
// migration / restart lost peer_ads). Drives the §5.6 recovery composition: a pinned-but-degraded
|
|
219
|
+
// contact must RESTORE first (re-fetch the AD) before the migration route can deliver.
|
|
220
|
+
trn qa_strip_peer_ads _
|
|
221
|
+
{
|
|
222
|
+
a2a_messaging::peer_ads -> (,).
|
|
223
|
+
return transaction::success [ _return_data ($ok -> TRUE) ].
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Downgrade a peer's address document to v1 IN PLACE: keep the AD PRESENT in peer_ads but drop its
|
|
227
|
+
// $e2e_bundle (typed any+, so NIL is valid). This is the "v1-presenting / self-downgraded" peer —
|
|
228
|
+
// distinct from qa_strip_peer_ads (which removes the AD entirely = degraded). Drives §5.6 recovery
|
|
229
|
+
// br2: a pinned peer whose CURRENT AD carries no e2e bundle must fail closed (downgrade_refused),
|
|
230
|
+
// NOT enter the peer_ads==NIL restore-first branch (which would restore-loop on an AD that is present).
|
|
231
|
+
trn qa_downgrade_peer_ad _:($cid -> cid: global_id)
|
|
232
|
+
{
|
|
233
|
+
ad = a2a_messaging::peer_ads cid.
|
|
234
|
+
if ad == NIL { return transaction::success [ _return_data ($ok -> FALSE) ]. }
|
|
235
|
+
a = ad?.
|
|
236
|
+
a2a_messaging::peer_ads cid -> (
|
|
237
|
+
$version -> (a $version),
|
|
238
|
+
$identity -> ( $key_list -> (a $identity $key_list),
|
|
239
|
+
$default_keys -> (a $identity $default_keys),
|
|
240
|
+
$container_id -> (a $identity $container_id),
|
|
241
|
+
$e2e_bundle -> NIL ),
|
|
242
|
+
$authorizations -> (a $authorizations) ).
|
|
243
|
+
return transaction::success [ _return_data ($ok -> TRUE) ].
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
trn readonly qa_e2e_route _:($cid -> cid: global_id) { return ($route -> (a2a_messaging::e2e_route cid)). }
|
|
247
|
+
|
|
248
|
+
// Synthesize an epoch PIN for a cid (imported-migration state). With NO peer bundle in peer_ads,
|
|
249
|
+
// e2e_route returns "downgrade_refused" (§5.6: pinned but no v2 bundle → fail closed).
|
|
250
|
+
trn qa_set_epoch_pin _:($cid -> cid: global_id, $session_id -> sid: bin)
|
|
251
|
+
{
|
|
252
|
+
a2a_messaging::contact_e2e_epoch cid -> ( $epoch -> sid, $session_id -> sid ).
|
|
253
|
+
a2a_messaging::contact_e2e_seen cid -> TRUE.
|
|
254
|
+
return transaction::success [ _return_data ($ok -> TRUE) ].
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Send a REAL explicit migration CONFIRM (responder → initiator) — encrypt the confirm body on
|
|
258
|
+
// my active (rotation) session and box it as e2e_migrate_confirm, exactly as handle_e2e_migrate_
|
|
259
|
+
// commit does. Lets the interleave test drive {confirm-then-app} / {app-then-confirm} and assert
|
|
260
|
+
// exactly ONE promotion. $epoch must equal the initiator's committed epoch.
|
|
261
|
+
trn qa_send_confirm _:($contact -> cref: str, $peer -> peer_blob: bin, $epoch -> ep: bin)
|
|
262
|
+
{
|
|
263
|
+
target_id = a2a_messaging::resolve_contact cref.
|
|
264
|
+
peer = (_read_or_abort peer_blob) safe address_document_types::t_e2e_bundle.
|
|
265
|
+
confirm_body = _write ( $name -> "::a2a_messaging::e2e_migrate_confirm", $targ -> ( $epoch -> ep, $pv -> a2a_versions::wire_version ) ).
|
|
266
|
+
cenv = e2e::encrypt_to target_id confirm_body peer.
|
|
267
|
+
return transaction::success [
|
|
268
|
+
encrypted_channel::send_encrypted_tx target_id (
|
|
269
|
+
$name -> "::a2a_messaging::e2e_migrate_confirm",
|
|
270
|
+
$targ -> ( $e2e_envelope -> (cenv $e2e_envelope), $emsignature -> (cenv $emsignature) ) ),
|
|
271
|
+
_return_data ($sent_to -> target_id) ].
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
trn readonly qa_mig_state _:($cid -> cid: global_id)
|
|
275
|
+
{
|
|
276
|
+
st = a2a_messaging::contact_migration cid.
|
|
277
|
+
return (
|
|
278
|
+
$present -> (st != NIL),
|
|
279
|
+
$phase -> ((st == NIL ?? "" ; ((st?) $phase))),
|
|
280
|
+
$initiator -> ((st == NIL ?? FALSE ; ((st?) $initiator)))
|
|
281
|
+
).
|
|
282
|
+
}
|
|
283
|
+
trn readonly qa_mig_pin _:($cid -> cid: global_id)
|
|
284
|
+
{
|
|
285
|
+
ep = a2a_messaging::contact_e2e_epoch cid.
|
|
286
|
+
esid is bin+ = NIL.
|
|
287
|
+
if ep != NIL { esid -> (ep?) $session_id. }
|
|
288
|
+
return (
|
|
289
|
+
$pinned -> (ep != NIL),
|
|
290
|
+
$session_id -> esid,
|
|
291
|
+
$seen -> ((a2a_messaging::contact_e2e_seen cid) == TRUE)
|
|
292
|
+
).
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Language-semantics pins for the versioned type registry (PLAN Step 4.3 / SPEC Q3-residual).
|
|
3
|
+
#
|
|
4
|
+
# The registry's safety rests on mufl behaviors that are pinned by tests, not yet by written
|
|
5
|
+
# language docs: safe-cast tolerates+strips extra record fields, disjunction casts pick
|
|
6
|
+
# alternatives in CANONICAL (not declaration) order and rebuild the result, and the
|
|
7
|
+
# dispatch-then-exact-cast pattern (t11) works across three wire versions. If the vendored
|
|
8
|
+
# toolchain ever regresses any of these, this runner fails BEFORE the behavioral suite runs.
|
|
9
|
+
#
|
|
10
|
+
# MUFL_PKG path to the vendored @adapt-toolkit/mufl package (default: ours-mcp's)
|
|
11
|
+
set -u
|
|
12
|
+
HERE=$(cd "$(dirname "$0")" && pwd)
|
|
13
|
+
MUFL_PKG=${MUFL_PKG:-/home/fleet/ours.network/ours-mcp/node_modules/@adapt-toolkit/mufl}
|
|
14
|
+
RUN="node $MUFL_PKG/bin/mufl.js run"
|
|
15
|
+
fail=0
|
|
16
|
+
|
|
17
|
+
check () { # file, expected-grep...
|
|
18
|
+
local f=$1; shift
|
|
19
|
+
local out
|
|
20
|
+
out=$(cd "$HERE" && $RUN "$f" 2>&1)
|
|
21
|
+
for want in "$@"; do
|
|
22
|
+
if ! grep -qF "$want" <<<"$out"; then
|
|
23
|
+
echo "✗ $f: missing expected line: $want"
|
|
24
|
+
echo "$out" | sed 's/^/ /'
|
|
25
|
+
fail=1
|
|
26
|
+
return
|
|
27
|
+
fi
|
|
28
|
+
done
|
|
29
|
+
echo "✓ $f"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# safe cast: extra fields tolerated, STRIPPED from the rebuilt result (never abort).
|
|
33
|
+
check t2_extra_fields.mufl \
|
|
34
|
+
"w \$c = %%NIL" \
|
|
35
|
+
"same value_id: %%FALSE"
|
|
36
|
+
|
|
37
|
+
# runtime paths: stripping identical at runtime; disjunction picks by CANONICAL order
|
|
38
|
+
# under BOTH declaration orders (version-specific fields survive either way here, which
|
|
39
|
+
# is exactly why a cast-to-union must never be the version selector — REG-4).
|
|
40
|
+
check t7_runtime.mufl \
|
|
41
|
+
"runtime extra-field: stripped" \
|
|
42
|
+
"runtime old-first: name kept" \
|
|
43
|
+
"runtime new-first: name kept"
|
|
44
|
+
|
|
45
|
+
# disjunction cast REBUILDS (new value id) and strips fields unknown to all alternatives.
|
|
46
|
+
check t9_passthrough.mufl \
|
|
47
|
+
"disjunction cast: rebuilt" \
|
|
48
|
+
"unknown extra across union: future field LOST"
|
|
49
|
+
|
|
50
|
+
# the full registry pattern: three wire versions in, three branches taken.
|
|
51
|
+
check t11_three_version.mufl \
|
|
52
|
+
"v0.2 way: fallback name from sender cid" \
|
|
53
|
+
"v0.3 way: name=Bob" \
|
|
54
|
+
"v0.5 way: name=Carol pv=5"
|
|
55
|
+
|
|
56
|
+
# string '<' is a STRICT TOTAL ORDER — the invariant the 0.9.0 migration election
|
|
57
|
+
# (str_lt / mig_initiator) depends on; a regression here would split-brain the election.
|
|
58
|
+
check t_str_order.mufl \
|
|
59
|
+
"irreflexive: F" \
|
|
60
|
+
"lt: T" \
|
|
61
|
+
"antisym: F" \
|
|
62
|
+
"prefix1: T" \
|
|
63
|
+
"prefix2: T" \
|
|
64
|
+
"hexlt: T" \
|
|
65
|
+
"hexeq: F" \
|
|
66
|
+
"hexgt: F"
|
|
67
|
+
|
|
68
|
+
[ $fail -eq 0 ] && echo "SEMANTICS PINS: ALL GREEN" || echo "SEMANTICS PINS: FAILURES"
|
|
69
|
+
exit $fail
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// The real worked example: submit_invite_response across 0.2 / 0.3 / 0.5
|
|
2
|
+
script
|
|
3
|
+
{
|
|
4
|
+
// ---- versioned input types (the registry entries) ----
|
|
5
|
+
metadef sir_input_v02_t: ($invite_id -> str, $data -> str).
|
|
6
|
+
metadef sir_input_v03_t: ($invite_id -> str, $data -> str, $name -> str).
|
|
7
|
+
metadef sir_input_v05_t: ($invite_id -> str, $data -> str, $name -> str, $pv -> int).
|
|
8
|
+
// union of supported versions (type-level registry face)
|
|
9
|
+
metadef sir_input_t: sir_input_v05_t || sir_input_v03_t || sir_input_v02_t.
|
|
10
|
+
// ordered version vector (type-level list of registered versions)
|
|
11
|
+
metadef sir_versions_t: [sir_input_v02_t, sir_input_v03_t, sir_input_v05_t].
|
|
12
|
+
|
|
13
|
+
fn version_of (raw: any) -> int {
|
|
14
|
+
pvv = raw $pv.
|
|
15
|
+
if pvv != NIL { return pvv SAFE(int). }
|
|
16
|
+
return ((raw $name) != NIL ?? 3 ; 2). // pre-$pv versions: distinguish by shape
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fn handle_sir (input: sir_input_t) -> str {
|
|
20
|
+
raw = input as any.
|
|
21
|
+
v = version_of raw.
|
|
22
|
+
if v >= 5 {
|
|
23
|
+
p = raw SAFE(sir_input_v05_t).
|
|
24
|
+
return "v0.5 way: name=" + (p $name) + " pv=" + (_str (p $pv)).
|
|
25
|
+
}
|
|
26
|
+
if v == 3 {
|
|
27
|
+
p = raw SAFE(sir_input_v03_t).
|
|
28
|
+
return "v0.3 way: name=" + (p $name).
|
|
29
|
+
}
|
|
30
|
+
p = raw SAFE(sir_input_v02_t).
|
|
31
|
+
return "v0.2 way: fallback name from sender cid".
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fn narrow (raw: any) -> sir_input_t {
|
|
35
|
+
v = version_of raw.
|
|
36
|
+
if v >= 5 { return raw SAFE(sir_input_v05_t). }
|
|
37
|
+
if v == 3 { return raw SAFE(sir_input_v03_t). }
|
|
38
|
+
return raw SAFE(sir_input_v02_t).
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
p02 = ($invite_id -> "I", $data -> "D").
|
|
42
|
+
p03 = ($invite_id -> "I", $data -> "D", $name -> "Bob").
|
|
43
|
+
p05 = ($invite_id -> "I", $data -> "D", $name -> "Carol", $pv -> 5).
|
|
44
|
+
_print (handle_sir (narrow p02)) "\n".
|
|
45
|
+
_print (handle_sir (narrow p03)) "\n".
|
|
46
|
+
_print (handle_sir (narrow p05)) "\n".
|
|
47
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// T-B: safe cast of a record with EXTRA fields; is the extra field kept or stripped?
|
|
2
|
+
script
|
|
3
|
+
{
|
|
4
|
+
metadef small_t: ($a -> int, $b -> str).
|
|
5
|
+
v = ($a -> 1, $b -> "x", $c -> "EXTRA").
|
|
6
|
+
w = v SAFE(small_t).
|
|
7
|
+
_print "cast ok; w = " w "\n".
|
|
8
|
+
c = (w as any) $c.
|
|
9
|
+
_print "w $c = " c "\n".
|
|
10
|
+
_print "same value_id: " (_value_id v == _value_id w) "\n".
|
|
11
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Runtime (any-typed) behavior: stripping, disjunction ordering, union param check
|
|
2
|
+
script
|
|
3
|
+
{
|
|
4
|
+
metadef small_t: ($a -> int, $b -> str).
|
|
5
|
+
metadef sir_v0_t: ($invite_id -> str, $data -> str).
|
|
6
|
+
metadef sir_v1_t: ($invite_id -> str, $data -> str, $name -> str, $pv -> int).
|
|
7
|
+
metadef old_first_t: sir_v0_t || sir_v1_t.
|
|
8
|
+
metadef new_first_t: sir_v1_t || sir_v0_t.
|
|
9
|
+
|
|
10
|
+
fn strip_probe (v: any) -> str {
|
|
11
|
+
w = v SAFE(small_t).
|
|
12
|
+
c = (w as any) $c.
|
|
13
|
+
return (c == NIL ?? "stripped" ; "kept").
|
|
14
|
+
}
|
|
15
|
+
fn old_first_probe (v: any) -> str {
|
|
16
|
+
u = v SAFE(old_first_t).
|
|
17
|
+
n = (u as any) $name.
|
|
18
|
+
return (n == NIL ?? "name LOST" ; "name kept").
|
|
19
|
+
}
|
|
20
|
+
fn new_first_probe (v: any) -> str {
|
|
21
|
+
u = v SAFE(new_first_t).
|
|
22
|
+
n = (u as any) $name.
|
|
23
|
+
return (n == NIL ?? "name LOST" ; "name kept").
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_print "runtime extra-field: " (strip_probe ($a -> 1, $b -> "x", $c -> "EXTRA")) "\n".
|
|
27
|
+
np = ($invite_id -> "I2", $data -> "D", $name -> "Alice", $pv -> 1).
|
|
28
|
+
_print "runtime old-first: " (old_first_probe np) "\n".
|
|
29
|
+
_print "runtime new-first: " (new_first_probe np) "\n".
|
|
30
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Disjunction result identity: pass-through (same value) or rebuilt?
|
|
2
|
+
script
|
|
3
|
+
{
|
|
4
|
+
metadef sir_v0_t: ($invite_id -> str, $data -> str).
|
|
5
|
+
metadef sir_v1_t: ($invite_id -> str, $data -> str, $name -> str, $pv -> int).
|
|
6
|
+
metadef union_t: sir_v1_t || sir_v0_t.
|
|
7
|
+
fn probe (v: any) -> str {
|
|
8
|
+
u = v SAFE(union_t).
|
|
9
|
+
return (_value_id u == _value_id v ?? "pass-through (same value_id)" ; "rebuilt").
|
|
10
|
+
}
|
|
11
|
+
np = ($invite_id -> "I2", $data -> "D", $name -> "Alice", $pv -> 1, $future -> "F").
|
|
12
|
+
_print "disjunction cast: " (probe np) "\n".
|
|
13
|
+
fn probe2 (v: any) -> str {
|
|
14
|
+
u = v SAFE(union_t).
|
|
15
|
+
f = (u as any) $future.
|
|
16
|
+
return (f == NIL ?? "future field LOST" ; "future field kept").
|
|
17
|
+
}
|
|
18
|
+
_print "unknown extra across union: " (probe2 np) "\n".
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Language-semantics pin: MUFL string '<' is a STRICT TOTAL ORDER.
|
|
2
|
+
// The 0.9.0 migration election (a2a_messaging::str_lt / mig_initiator) rests on this:
|
|
3
|
+
// side A computes myCid < peerCid, side B computes peerCid < myCid; for exactly one
|
|
4
|
+
// proposer to be elected, '<' must be irreflexive + antisymmetric + a total order over
|
|
5
|
+
// the hex container-id space. If the vendored toolchain ever regressed string '<' to a
|
|
6
|
+
// non-order, the election would split-brain (both or neither propose). This fails BEFORE
|
|
7
|
+
// the behavioral suite so that never ships silently.
|
|
8
|
+
script
|
|
9
|
+
{
|
|
10
|
+
_print "irreflexive: " (("a" < "a") ?? "T" ; "F") "\n". // F — nothing is < itself
|
|
11
|
+
_print "lt: " (("a" < "b") ?? "T" ; "F") "\n". // T
|
|
12
|
+
_print "antisym: " (("b" < "a") ?? "T" ; "F") "\n". // F — a<b implies not b<a
|
|
13
|
+
_print "prefix1: " (("a" < "ab") ?? "T" ; "F") "\n". // T — proper prefix is smaller
|
|
14
|
+
_print "prefix2: " (("ab" < "b") ?? "T" ; "F") "\n". // T
|
|
15
|
+
// Equal-length hex — the actual container-id case (cids are fixed-length hex):
|
|
16
|
+
_print "hexlt: " (("0a1f" < "0a20") ?? "T" ; "F") "\n". // T
|
|
17
|
+
_print "hexeq: " (("deadbeef" < "deadbeef") ?? "T" ; "F") "\n". // F — irreflexive on hex too
|
|
18
|
+
_print "hexgt: " (("ff" < "00") ?? "T" ; "F") "\n". // F
|
|
19
|
+
}
|