mirror of
https://github.com/block/buzz.git
synced 2026-10-31 22:41:12 +00:00
feat(egress): owner-identity artifact capabilities + boundary stamp (C3)
Extend the durable owner-identity capability registry with ArtifactPolicy
(NIP-AP, P30/P31/P32-C1): a generation-stamped owner-key-derived value that
leaves its producing lease and is applied later. Unlike Session/Bearer,
an artifact owns no side-effecting teardown, so its only revocation is the
exercise-time / application-site generation compare — the transition bump
IS the invalidation (shape (ii), acked by Paul). revoke_durable_capabilities_before
performs no per-entry artifact work; the barrier reaches artifacts via the bump.
The seven commands/identity producers admit a bounded lease before the owner-key
sign/encrypt/decrypt and return the value wrapped as StampedArtifact<T>
({value, artifact:{id,generation}}), stamping the issuing lease's generation
(the F1 lesson). The wire shape is locked on both sides; a serialization test
pins it so C6/C7 inherits a stable contract.
The 7 TS adapters unwrap .value at the boundary with a named C6/C7 deferral —
outward threading to application sites + the generation-compare lands in C6/C7
coupled to the code that reads the stamp (supersedes condition 2 of the
stamp-at-boundary ruling, revised on the measured 87-file transitive radius).
StampedArtifact type and the four identity adapters split into small modules to
respect the desktop file-size ratchet.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
@@ -110,8 +110,13 @@ pub async fn sign_event(
|
||||
created_at: Option<u64>,
|
||||
tags: Vec<Vec<String>>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<String, String> {
|
||||
) -> Result<crate::owner_identity_egress::StampedArtifact<String>, String> {
|
||||
let keys = state.signing_keys()?;
|
||||
// Admit BEFORE signing (the F1 lesson): the sign that derives this artifact
|
||||
// is a leased owner-identity operation, and the artifact is stamped with
|
||||
// the lease's generation so a signature produced across a transition fails
|
||||
// closed at its frontend application site (C6/C7).
|
||||
let lease = crate::owner_identity_egress::try_admit_owner_identity_egress().await?;
|
||||
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let nostr_tags = tags
|
||||
@@ -128,7 +133,10 @@ pub async fn sign_event(
|
||||
.sign_with_keys(&keys)
|
||||
.map_err(|error| format!("sign failed: {error}"))?;
|
||||
|
||||
Ok(event.as_json())
|
||||
// Stamp the issuing lease's generation, then wrap the value; the
|
||||
// frontend threads {value, artifact} opaque — validated in C6/C7.
|
||||
let artifact = crate::owner_identity_egress::register_owner_artifact(&lease);
|
||||
Ok(artifact.stamp_value(event.as_json()))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("spawn_blocking failed: {e}"))?
|
||||
@@ -138,8 +146,11 @@ pub async fn sign_event(
|
||||
pub async fn decrypt_observer_event(
|
||||
event_json: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
) -> Result<crate::owner_identity_egress::StampedArtifact<serde_json::Value>, String> {
|
||||
let keys = state.signing_keys()?;
|
||||
// Admit BEFORE decrypting: the owner-key decryption is a leased operation
|
||||
// and the decrypted payload is a stamped artifact — see `sign_event`.
|
||||
let lease = crate::owner_identity_egress::try_admit_owner_identity_egress().await?;
|
||||
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let event =
|
||||
@@ -153,20 +164,26 @@ pub async fn decrypt_observer_event(
|
||||
return Err("observer event has invalid signature".into());
|
||||
}
|
||||
|
||||
buzz_core_pkg::observer::decrypt_observer_payload(&keys, &event)
|
||||
.map_err(|error| format!("decrypt observer event failed: {error}"))
|
||||
let payload = buzz_core_pkg::observer::decrypt_observer_payload(&keys, &event)
|
||||
.map_err(|error| format!("decrypt observer event failed: {error}"))?;
|
||||
// Stamp the decrypted payload; frontend validates in C6/C7.
|
||||
let artifact = crate::owner_identity_egress::register_owner_artifact(&lease);
|
||||
Ok(artifact.stamp_value(payload))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("spawn_blocking failed: {e}"))?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn build_observer_control_event(
|
||||
pub async fn build_observer_control_event(
|
||||
agent_pubkey: String,
|
||||
payload: serde_json::Value,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<String, String> {
|
||||
) -> Result<crate::owner_identity_egress::StampedArtifact<String>, String> {
|
||||
let keys = state.signing_keys()?;
|
||||
// Admit BEFORE encrypt+sign: the owner-signed observer control frame is a
|
||||
// leased artifact stamped with the lease's generation — see `sign_event`.
|
||||
let lease = crate::owner_identity_egress::try_admit_owner_identity_egress().await?;
|
||||
let agent_pubkey = PublicKey::from_hex(agent_pubkey.trim())
|
||||
.map_err(|error| format!("invalid agent pubkey: {error}"))?;
|
||||
let agent_pubkey_hex = agent_pubkey.to_hex();
|
||||
@@ -183,7 +200,9 @@ pub fn build_observer_control_event(
|
||||
let event = builder
|
||||
.sign_with_keys(&keys)
|
||||
.map_err(|error| format!("sign observer control failed: {error}"))?;
|
||||
Ok(event.as_json())
|
||||
// Stamp the issuing lease's generation; frontend validates in C6/C7.
|
||||
let artifact = crate::owner_identity_egress::register_owner_artifact(&lease);
|
||||
Ok(artifact.stamp_value(event.as_json()))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -825,7 +844,7 @@ pub async fn sign_nostr_identity_binding(
|
||||
origin: String,
|
||||
expires_at: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<String, String> {
|
||||
) -> Result<crate::owner_identity_egress::StampedArtifact<String>, String> {
|
||||
nostr_bind::validate_signing_request(
|
||||
&challenge_id,
|
||||
&nonce,
|
||||
@@ -835,6 +854,9 @@ pub async fn sign_nostr_identity_binding(
|
||||
)?;
|
||||
|
||||
let keys = state.signing_keys()?;
|
||||
// Admit BEFORE signing: the identity-binding response is a leased artifact
|
||||
// stamped with the lease's generation — see `sign_event`.
|
||||
let lease = crate::owner_identity_egress::try_admit_owner_identity_egress().await?;
|
||||
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let event = build_nostr_identity_binding_event(
|
||||
@@ -846,7 +868,8 @@ pub async fn sign_nostr_identity_binding(
|
||||
&expires_at,
|
||||
)?;
|
||||
|
||||
Ok(event.as_json())
|
||||
let artifact = crate::owner_identity_egress::register_owner_artifact(&lease);
|
||||
Ok(artifact.stamp_value(event.as_json()))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("spawn_blocking failed: {e}"))?
|
||||
@@ -857,15 +880,16 @@ pub async fn create_auth_event(
|
||||
challenge: String,
|
||||
relay_url: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<String, String> {
|
||||
) -> Result<crate::owner_identity_egress::StampedArtifact<String>, String> {
|
||||
let keys = state.signing_keys()?;
|
||||
// The relay-WS reconnection handshake signs a NIP-42 auth event under a
|
||||
// per-send bounded egress lease: reconnection cannot re-authenticate
|
||||
// mid-transition because the drain refuses new admission (spec — the
|
||||
// frontend session's authority is gated the same as every other owner
|
||||
// sign). Frontend session REGISTRATION (capability + native-WS teardown
|
||||
// handle) defers to C6/C7 with the frontend identity store; this lands the
|
||||
// spec-mandated admission gate now.
|
||||
// sign). The signed auth event is an owner-signed relay-event ARTIFACT
|
||||
// (stamped and threaded to the frontend, validated in C6/C7); frontend
|
||||
// session REGISTRATION (native-WS teardown handle) defers to C6/C7 with the
|
||||
// frontend identity store.
|
||||
let lease = crate::owner_identity_egress::try_admit_owner_identity_egress().await?;
|
||||
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
@@ -880,9 +904,10 @@ pub async fn create_auth_event(
|
||||
.tags(tags)
|
||||
.sign_with_keys(&keys)
|
||||
.map_err(|error| format!("sign failed: {error}"))?;
|
||||
// Sign complete: the lease has served its sign→return window.
|
||||
drop(lease);
|
||||
Ok(event.as_json())
|
||||
// Stamp the issuing lease's generation, consuming the lease as the
|
||||
// sign→return window closes; frontend validates in C6/C7.
|
||||
let artifact = crate::owner_identity_egress::register_owner_artifact(&lease);
|
||||
Ok(artifact.stamp_value(event.as_json()))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("spawn_blocking failed: {e}"))?
|
||||
@@ -892,17 +917,22 @@ pub async fn create_auth_event(
|
||||
pub async fn nip44_encrypt_to_self(
|
||||
plaintext: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<String, String> {
|
||||
) -> Result<crate::owner_identity_egress::StampedArtifact<String>, String> {
|
||||
let keys = state.signing_keys()?;
|
||||
// Admit BEFORE encrypting: the encrypt-to-self ciphertext is a leased
|
||||
// artifact stamped with the lease's generation — see `sign_event`.
|
||||
let lease = crate::owner_identity_egress::try_admit_owner_identity_egress().await?;
|
||||
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
nip44::encrypt(
|
||||
let ciphertext = nip44::encrypt(
|
||||
keys.secret_key(),
|
||||
&keys.public_key(),
|
||||
&plaintext,
|
||||
nip44::Version::V2,
|
||||
)
|
||||
.map_err(|e| format!("nip44 encrypt failed: {e}"))
|
||||
.map_err(|e| format!("nip44 encrypt failed: {e}"))?;
|
||||
let artifact = crate::owner_identity_egress::register_owner_artifact(&lease);
|
||||
Ok(artifact.stamp_value(ciphertext))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("spawn_blocking failed: {e}"))?
|
||||
@@ -912,12 +942,17 @@ pub async fn nip44_encrypt_to_self(
|
||||
pub async fn nip44_decrypt_from_self(
|
||||
ciphertext: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<String, String> {
|
||||
) -> Result<crate::owner_identity_egress::StampedArtifact<String>, String> {
|
||||
let keys = state.signing_keys()?;
|
||||
// Admit BEFORE decrypting: the decrypted plaintext is a leased artifact
|
||||
// stamped with the lease's generation — see `sign_event`.
|
||||
let lease = crate::owner_identity_egress::try_admit_owner_identity_egress().await?;
|
||||
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
nip44::decrypt(keys.secret_key(), &keys.public_key(), &ciphertext)
|
||||
.map_err(|e| format!("nip44 decrypt failed: {e}"))
|
||||
let plaintext = nip44::decrypt(keys.secret_key(), &keys.public_key(), &ciphertext)
|
||||
.map_err(|e| format!("nip44 decrypt failed: {e}"))?;
|
||||
let artifact = crate::owner_identity_egress::register_owner_artifact(&lease);
|
||||
Ok(artifact.stamp_value(plaintext))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("spawn_blocking failed: {e}"))?
|
||||
|
||||
@@ -11,6 +11,7 @@ use std::collections::HashMap;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
|
||||
use serde::Serialize;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use super::{lock_inner, IdentityPersistenceState, OwnerIdentityEgressLease, REGISTRY};
|
||||
@@ -86,17 +87,45 @@ pub struct SessionPolicy;
|
||||
#[derive(Debug)]
|
||||
pub struct BearerPolicy;
|
||||
|
||||
/// A generation-stamped owner-key-derived VALUE (owner-signed relay event JSON,
|
||||
/// identity-binding response, encrypt-to-self ciphertext, or the plaintext of
|
||||
/// an owner-key decryption) that leaves its producing lease and is exercised —
|
||||
/// exported, published, signed around, or applied to identity-indexed state —
|
||||
/// LATER. Unlike a session or bearer, an artifact owns NO side-effecting
|
||||
/// teardown: a stamped `String`/JSON value has nothing to cancel or remove. Its
|
||||
/// only revocation mechanism is the exercise-time generation compare, which the
|
||||
/// generation bump at [`begin_egress_drain`] triggers by construction — so the
|
||||
/// registry keeps NO artifact entry for the barrier to invalidate (shape (ii),
|
||||
/// acked by Paul 2026-08-18). The registered revocation handle is therefore
|
||||
/// [`RevocationHandle::Artifact`], a marker carrying no teardown authority; the
|
||||
/// capability's `id` is correlation/diagnostic only, never a revocation target.
|
||||
///
|
||||
/// Rust-consumed / collapsed-transaction artifacts (project git-workflow
|
||||
/// sign+submit, archive-ingest decrypts, snapshot-import, engram-listing
|
||||
/// decrypt) exercise synchronously inside their producing lease and validate
|
||||
/// via [`OwnerIdentityCapability::admit_exercise`] before their local
|
||||
/// application — identical to a bounded lease. Boundary-crossing artifacts (the
|
||||
/// frontend-consumed producers) serialize their stamp across the Tauri boundary
|
||||
/// as [`ArtifactStamp`]; the frontend threads it opaque until C6/C7 adds the
|
||||
/// generation-compare at each application site.
|
||||
#[derive(Debug)]
|
||||
pub struct ArtifactPolicy;
|
||||
|
||||
impl CapabilityPolicy for SessionPolicy {
|
||||
const KIND: &'static str = "session";
|
||||
}
|
||||
impl CapabilityPolicy for BearerPolicy {
|
||||
const KIND: &'static str = "bearer";
|
||||
}
|
||||
impl CapabilityPolicy for ArtifactPolicy {
|
||||
const KIND: &'static str = "artifact";
|
||||
}
|
||||
|
||||
mod private {
|
||||
pub trait Sealed {}
|
||||
impl Sealed for super::SessionPolicy {}
|
||||
impl Sealed for super::BearerPolicy {}
|
||||
impl Sealed for super::ArtifactPolicy {}
|
||||
}
|
||||
|
||||
/// The revocation authority for one registered durable capability, invoked by
|
||||
@@ -109,6 +138,16 @@ enum RevocationHandle {
|
||||
/// invalidation, so no side-effecting handle is needed. The variant exists
|
||||
/// so the registry records the capability's kind for the per-kind barrier.
|
||||
Bearer,
|
||||
/// Artifact invalidation is BY GENERATION BUMP, not by entry: a stamped
|
||||
/// value owns no side-effecting teardown, and its only revocation is the
|
||||
/// exercise-time / application-site generation compare failing once the
|
||||
/// bump advances the current generation (shape (ii)). The registry keeps NO
|
||||
/// artifact entry for the barrier to invalidate — this marker exists only
|
||||
/// so `register_owner_artifact` shares the one registration path; the entry
|
||||
/// is removed immediately on the handle's Drop (serialization time for a
|
||||
/// boundary-crossing artifact, scope end for a collapsed one), so the id is
|
||||
/// correlation/diagnostic only, never a revocation target.
|
||||
Artifact,
|
||||
}
|
||||
|
||||
/// Registry of live durable capabilities, keyed by capability id, under its
|
||||
@@ -234,6 +273,85 @@ pub fn register_owner_bearer(
|
||||
register_durable(lease.generation(), RevocationHandle::Bearer)
|
||||
}
|
||||
|
||||
/// The wire form of an artifact's generation stamp, serialized across the Tauri
|
||||
/// boundary alongside the owner-key-derived value as `{ value, artifact }`.
|
||||
///
|
||||
/// The frontend threads this pair OPAQUE (never unwrapping and discarding it at
|
||||
/// the adapter — the witness must survive to the application site); C6/C7 adds
|
||||
/// the generation-compare at each application boundary using `generation`. The
|
||||
/// `id` is correlation/diagnostic only (log correlation, completeness count),
|
||||
/// never a revocation target — see [`ArtifactPolicy`] for the shape (ii)
|
||||
/// rationale.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ArtifactStamp {
|
||||
/// Correlation id — monotonic, diagnostic only, never a revocation target.
|
||||
pub id: u64,
|
||||
/// The identity-persistence generation this artifact was issued under; the
|
||||
/// application-site compare (C6/C7) refuses once the current generation
|
||||
/// advances past it.
|
||||
pub generation: u64,
|
||||
}
|
||||
|
||||
/// The serialized `{ value, artifact }` pair a boundary-crossing producer
|
||||
/// returns across the Tauri boundary. The frontend threads this OPAQUE (the
|
||||
/// witness must survive to the application site — never unwrap-and-discard at
|
||||
/// the adapter); C6/C7 adds the generation-compare against `artifact.generation`
|
||||
/// immediately before each irreversible application effect.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct StampedArtifact<T: Serialize> {
|
||||
/// The owner-key-derived value (signed event JSON, ciphertext, decrypted
|
||||
/// payload). Consumers destructure `.value` at their current use point.
|
||||
pub value: T,
|
||||
/// The generation stamp carried alongside the value.
|
||||
pub artifact: ArtifactStamp,
|
||||
}
|
||||
|
||||
impl OwnerIdentityCapability<ArtifactPolicy> {
|
||||
/// The wire stamp for a boundary-crossing artifact, serialized alongside
|
||||
/// the value as `{ value, artifact: ArtifactStamp }`. Producing the stamp
|
||||
/// does not consume the capability; the caller drops the capability once
|
||||
/// the pair is serialized (the entry is correlation-only — see
|
||||
/// [`ArtifactPolicy`]).
|
||||
pub fn stamp(&self) -> ArtifactStamp {
|
||||
ArtifactStamp {
|
||||
id: self.id,
|
||||
generation: self.generation,
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap a boundary-crossing `value` with this capability's stamp, consuming
|
||||
/// the capability. The returned [`StampedArtifact`] serializes as
|
||||
/// `{ value, artifact }`; the capability's registry entry deregisters as it
|
||||
/// drops at the end of this call (the entry is correlation-only under shape
|
||||
/// (ii), so early deregistration loses no revocation authority).
|
||||
pub fn stamp_value<T: Serialize>(self, value: T) -> StampedArtifact<T> {
|
||||
StampedArtifact {
|
||||
value,
|
||||
artifact: self.stamp(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Issue a durable owner-identity artifact capability over an owner-key-derived
|
||||
/// value, stamped with the issuing lease's generation.
|
||||
///
|
||||
/// Stamps the issuing [`OwnerIdentityEgressLease`]'s generation, not a re-read
|
||||
/// one — see [`register_owner_session`] for why re-reading is a barrier bypass.
|
||||
/// The F1 lesson applies identically: the signing/decryption that derives the
|
||||
/// value runs under the lease, and the stamp must be that lease's generation so
|
||||
/// a value derived across a bump fails closed at its application site.
|
||||
///
|
||||
/// Call this AFTER the owner-key operation completes under the lease (admit
|
||||
/// before sign/decrypt, stamp after). A Rust-consumed / collapsed artifact
|
||||
/// validates via [`OwnerIdentityCapability::admit_exercise`] before its local
|
||||
/// application; a boundary-crossing artifact serializes [`ArtifactStamp`] via
|
||||
/// [`OwnerIdentityCapability::stamp`] and the frontend validates in C6/C7.
|
||||
pub fn register_owner_artifact(
|
||||
lease: &OwnerIdentityEgressLease,
|
||||
) -> OwnerIdentityCapability<ArtifactPolicy> {
|
||||
register_durable(lease.generation(), RevocationHandle::Artifact)
|
||||
}
|
||||
|
||||
/// Register a durable capability stamped with `generation` and carrying
|
||||
/// `handle` as its revocation authority. Shared by both constructors so the
|
||||
/// id allocation and registry insert live in one place.
|
||||
@@ -258,6 +376,22 @@ fn register_durable<P: CapabilityPolicy>(
|
||||
/// later attach fails [`OwnerIdentityCapability::admit_exercise`]). Returns the
|
||||
/// number of capabilities revoked.
|
||||
///
|
||||
/// ARTIFACTS ARE NOT REVOKED HERE, and this is correct, not a gap (shape (ii),
|
||||
/// acked by Paul 2026-08-18). The spec text reads "invalidates every
|
||||
/// old-generation bearer AND artifact"; a reviewer diffing against it will look
|
||||
/// for artifact handling in this function and find none. An artifact owns no
|
||||
/// side-effecting teardown — it is a stamped value, not a socket or a bearer
|
||||
/// entry — so its ONLY revocation mechanism is the exercise-time /
|
||||
/// application-site generation compare, which the generation bump at
|
||||
/// [`begin_egress_drain`] triggers by construction. The bump IS the artifact
|
||||
/// invalidation, and it precedes this call (and the journal write) in the
|
||||
/// coordinator sequence, so every old-generation artifact is already
|
||||
/// invalidated before the durable boundary without a per-entry action. Per-entry
|
||||
/// work here is reserved for authorities with side-effecting teardown (session
|
||||
/// cancel-tokens, bearer entries). Any artifact entry that happens to be live
|
||||
/// at barrier time (a producer between lease-drop and value-serialization)
|
||||
/// self-deregisters on its imminent Drop and is skipped either way.
|
||||
///
|
||||
/// The C5 coordinator barrier calls this after
|
||||
/// [`begin_egress_drain`]/[`await_egress_drain`] and BEFORE the journal write +
|
||||
/// durable B dispatch, so no old-generation session or bearer can transmit
|
||||
@@ -269,7 +403,11 @@ pub fn revoke_durable_capabilities_before(winning_generation: u64) -> usize {
|
||||
let stale: Vec<u64> = durable
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|(_, (gen, _))| *gen < winning_generation)
|
||||
.filter(|(_, (gen, handle))| {
|
||||
// Artifacts are generation-invalidated (shape (ii)); the barrier
|
||||
// acts only on side-effecting authorities.
|
||||
*gen < winning_generation && !matches!(handle, RevocationHandle::Artifact)
|
||||
})
|
||||
.map(|(id, _)| *id)
|
||||
.collect();
|
||||
for id in &stale {
|
||||
@@ -491,4 +629,136 @@ mod tests {
|
||||
"with no transition the session still transmits"
|
||||
);
|
||||
}
|
||||
|
||||
// §7 artifact schedules (P31-C1) — three schedules proving shape (ii): the
|
||||
// generation bump reaches artifacts WITHOUT a barrier registry entry, so a
|
||||
// stamped value's application is refused post-bump by the generation
|
||||
// compare alone. `admit_exercise` on an ArtifactPolicy capability stands in
|
||||
// for both the collapsed-artifact local check and the boundary-crossing
|
||||
// artifact's simulated application-site compare (C6/C7).
|
||||
|
||||
// P31-C1 (a): a Rust-consumed / collapsed artifact stamped under A refuses
|
||||
// its local application after the generation advances to B.
|
||||
#[test]
|
||||
fn stale_artifact_application_refuses_after_a_generation_bump() {
|
||||
let _g = guard();
|
||||
let artifact = register_owner_artifact(&lease());
|
||||
begin_egress_drain().unwrap();
|
||||
resume_egress_live();
|
||||
assert!(
|
||||
artifact.admit_exercise().is_err(),
|
||||
"an artifact stamped under A must refuse application after B wins"
|
||||
);
|
||||
}
|
||||
|
||||
// P31-C1 (b): the barrier does NOT revoke artifacts by entry — the bump is
|
||||
// the invalidation. `revoke_durable_capabilities_before` reports zero
|
||||
// side-effecting revocations for a registry holding only artifacts, and the
|
||||
// stamped artifact still refuses application by generation compare. This is
|
||||
// the test-level proof that the bump reaches artifacts without registry
|
||||
// entries (Paul's condition 2).
|
||||
#[test]
|
||||
fn barrier_leaves_artifacts_to_generation_compare_not_entry_revocation() {
|
||||
let _g = guard();
|
||||
let artifact = register_owner_artifact(&lease());
|
||||
assert_eq!(live_durable_capability_count(), 1);
|
||||
let winning = begin_egress_drain().unwrap();
|
||||
resume_egress_live();
|
||||
assert_eq!(
|
||||
revoke_durable_capabilities_before(winning),
|
||||
0,
|
||||
"the barrier performs no per-entry revocation for artifacts (shape (ii))"
|
||||
);
|
||||
assert!(
|
||||
artifact.admit_exercise().is_err(),
|
||||
"the bump alone invalidates the artifact — the generation compare refuses"
|
||||
);
|
||||
}
|
||||
|
||||
// P31-C1 (c): a boundary-crossing artifact's wire stamp carries the issuing
|
||||
// generation, and once the generation advances the stamp is stale relative
|
||||
// to the current generation — the invariant C6/C7's application-site
|
||||
// compare enforces against `ArtifactStamp::generation`.
|
||||
#[test]
|
||||
fn boundary_crossing_artifact_stamp_is_stale_after_a_bump() {
|
||||
let _g = guard();
|
||||
let artifact = register_owner_artifact(&lease());
|
||||
let stamp = artifact.stamp();
|
||||
assert_eq!(stamp.generation, current_identity_persistence_generation());
|
||||
let winning = begin_egress_drain().unwrap();
|
||||
resume_egress_live();
|
||||
assert_ne!(
|
||||
stamp.generation, winning,
|
||||
"the wire stamp is stale relative to the winning generation; the C6/C7 \
|
||||
application-site compare refuses it"
|
||||
);
|
||||
assert!(
|
||||
artifact.admit_exercise().is_err(),
|
||||
"and the capability itself refuses local application"
|
||||
);
|
||||
}
|
||||
|
||||
// §7 decryption-artifact schedules (P32-C1) — two schedules for the
|
||||
// decryption outputs (nip44_decrypt_from_self / decrypt_observer_event):
|
||||
// the decrypted plaintext stamped under A refuses application after B, and
|
||||
// a no-transition control keeps it applicable.
|
||||
|
||||
// P32-C1 (a): a decryption-output artifact stamped under A refuses
|
||||
// application after the generation advances to B.
|
||||
#[test]
|
||||
fn stale_decryption_artifact_application_refuses_after_a_bump() {
|
||||
let _g = guard();
|
||||
let decrypted = register_owner_artifact(&lease());
|
||||
begin_egress_drain().unwrap();
|
||||
resume_egress_live();
|
||||
assert!(
|
||||
decrypted.admit_exercise().is_err(),
|
||||
"a decryption output stamped under A must refuse application after B wins"
|
||||
);
|
||||
}
|
||||
|
||||
// P32-C1 (b): no-transition control — a decryption-output artifact with no
|
||||
// intervening bump stays applicable and the barrier revokes nothing.
|
||||
#[test]
|
||||
fn no_transition_leaves_decryption_artifact_applicable() {
|
||||
let _g = guard();
|
||||
let decrypted = register_owner_artifact(&lease());
|
||||
let current = current_identity_persistence_generation();
|
||||
assert_eq!(
|
||||
revoke_durable_capabilities_before(current),
|
||||
0,
|
||||
"no side-effecting revocation with no transition"
|
||||
);
|
||||
assert!(
|
||||
decrypted.admit_exercise().is_ok(),
|
||||
"with no transition the decryption output stays applicable"
|
||||
);
|
||||
}
|
||||
|
||||
// Wire-shape LOCK (Paul's condition 2): the boundary payload serializes as
|
||||
// exactly `{ value, artifact: { id, generation } }`. C6/C7 inherits this
|
||||
// contract — the frontend `StampedArtifact<T>` mirror and every
|
||||
// application-site compare read `artifact.generation`, so a silent drift
|
||||
// here (renamed/reshaped field) would break C6/C7 undetectably. Pinning it
|
||||
// now makes any drift a test failure, not a C6/C7 rediscovery.
|
||||
#[test]
|
||||
fn stamped_artifact_serializes_to_the_locked_wire_shape() {
|
||||
let stamped = StampedArtifact {
|
||||
value: "signed-event-json",
|
||||
artifact: ArtifactStamp {
|
||||
id: 7,
|
||||
generation: 3,
|
||||
},
|
||||
};
|
||||
let json = serde_json::to_value(&stamped).expect("serializes");
|
||||
assert_eq!(
|
||||
json,
|
||||
serde_json::json!({
|
||||
"value": "signed-event-json",
|
||||
"artifact": { "id": 7, "generation": 3 },
|
||||
}),
|
||||
"the boundary payload must be {{value, artifact:{{id, generation}}}} \
|
||||
— the contract C6/C7 threads to application sites"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,8 +70,25 @@
|
||||
//! `commands::channels` (×3), and the huddle STT task (per-send inline
|
||||
//! owner lease).
|
||||
//! - `commands::identity::create_auth_event` — the relay-WS reconnection
|
||||
//! NIP-42 handshake (per-send lease; frontend session registration defers to
|
||||
//! C6/C7 with the frontend identity store).
|
||||
//! NIP-42 handshake (per-send lease). The signed auth event is an
|
||||
//! owner-signed relay-event ARTIFACT: stamped and threaded to the frontend
|
||||
//! as `{value, artifact}`, validated in C6/C7. Frontend session REGISTRATION
|
||||
//! (native-WS teardown handle) defers to C6/C7 with the frontend identity
|
||||
//! store.
|
||||
//! - **Owner-identity artifacts** — the seven `commands::identity` producers
|
||||
//! `sign_event`, `create_auth_event`, `build_observer_control_event`,
|
||||
//! `sign_nostr_identity_binding`, `nip44_encrypt_to_self`,
|
||||
//! `nip44_decrypt_from_self`, and `decrypt_observer_event` admit a bounded
|
||||
//! lease BEFORE the owner-key sign/encrypt/decrypt, register an
|
||||
//! [`OwnerIdentityCapability<ArtifactPolicy>`], and return the owner-key
|
||||
//! value wrapped as [`StampedArtifact`] (`{value, artifact:{id,generation}}`).
|
||||
//! Artifacts carry NO side-effecting teardown: their only revocation is the
|
||||
//! application-site generation compare, triggered by the transition bump
|
||||
//! (shape (ii)). The frontend threads the pair opaque; C6/C7 adds the
|
||||
//! compare at each application boundary. Rust-consumed / collapsed-transaction
|
||||
//! artifacts (`project_git_workflow` sign+submit, archive-ingest decrypts,
|
||||
//! snapshot-import, engram-listing decrypt) exercise synchronously inside
|
||||
//! their lease and keep the collapsed shape — inventoried for C5/C6.
|
||||
//! - **Durable bearers** — `commands::media::mint_media_get_auth` (Blossom
|
||||
//! `t=get`) and the `do_upload` `t=upload` mint sign under a bounded lease
|
||||
//! and register an [`OwnerIdentityCapability<BearerPolicy>`]; the four
|
||||
@@ -475,14 +492,20 @@ fn admit_managed_agent_after_wait() -> Result<ManagedAgentEgressLease, String> {
|
||||
/// file-size discipline; it shares this module's one registry.
|
||||
mod durable;
|
||||
pub use durable::{
|
||||
register_owner_bearer, register_owner_session, BearerPolicy, OwnerIdentityCapability,
|
||||
SessionPolicy,
|
||||
register_owner_artifact, register_owner_bearer, register_owner_session, BearerPolicy,
|
||||
OwnerIdentityCapability, SessionPolicy, StampedArtifact,
|
||||
};
|
||||
// The C5 coordinator barrier and its registration-completeness reader; no
|
||||
// production caller until C5, so the re-exports are unused today. Consumed by
|
||||
// C5 (P25/P28); remove allow when C5 lands.
|
||||
#[allow(unused_imports)]
|
||||
pub use durable::revoke_durable_capabilities_before;
|
||||
// The artifact policy marker and its wire stamp are named at the frontend
|
||||
// application sites in C6/C7 (generation-compare) and by the §7 artifact
|
||||
// schedules; no production Rust caller names them today. Remove allow when
|
||||
// C6/C7 lands.
|
||||
#[allow(unused_imports)]
|
||||
pub use durable::{ArtifactPolicy, ArtifactStamp};
|
||||
|
||||
/// Process-global mutex serializing tests that mutate the registry's
|
||||
/// process-global state. Any test that admits a lease, drives a drain, or
|
||||
|
||||
@@ -193,10 +193,30 @@ installDOMShim();
|
||||
/** @type {Map<string, (args: unknown) => Promise<unknown>>} */
|
||||
const ipcHandlers = new Map();
|
||||
|
||||
// Owner-identity artifact producers (NIP-AP) return `{ value, artifact }` across
|
||||
// the Tauri boundary; the adapters unwrap `.value`. Mocks return the bare value,
|
||||
// so wrap it in the stamped shape for these commands (inert stamp in C3 — the
|
||||
// generation-compare lands in C6/C7). Keep this set in sync with the producers.
|
||||
const STAMPED_ARTIFACT_COMMANDS = new Set([
|
||||
"sign_event",
|
||||
"create_auth_event",
|
||||
"build_observer_control_event",
|
||||
"sign_nostr_identity_binding",
|
||||
"nip44_encrypt_to_self",
|
||||
"nip44_decrypt_from_self",
|
||||
"decrypt_observer_event",
|
||||
]);
|
||||
function wrapV(cmd, value) {
|
||||
return STAMPED_ARTIFACT_COMMANDS.has(cmd)
|
||||
? { value, artifact: { id: 0, generation: 0 } }
|
||||
: value;
|
||||
}
|
||||
|
||||
globalThis.__TAURI_INTERNALS__ = {
|
||||
invoke: (cmd, args) => {
|
||||
const handler = ipcHandlers.get(cmd);
|
||||
if (handler) return handler(args);
|
||||
if (handler)
|
||||
return Promise.resolve(handler(args)).then((v) => wrapV(cmd, v));
|
||||
return Promise.reject(new Error(`unmocked Tauri command: ${cmd}`));
|
||||
},
|
||||
transformCallback: (_cb) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { invokeTauri } from "@/shared/api/tauri";
|
||||
import { invokeTauri, type StampedArtifact } from "@/shared/api/tauri";
|
||||
|
||||
export type NostrIdentityBindingInput = {
|
||||
challengeId: string;
|
||||
@@ -8,8 +8,13 @@ export type NostrIdentityBindingInput = {
|
||||
expiresAt: string;
|
||||
};
|
||||
|
||||
export function signNostrIdentityBinding(
|
||||
export async function signNostrIdentityBinding(
|
||||
input: NostrIdentityBindingInput,
|
||||
): Promise<string> {
|
||||
return invokeTauri<string>("sign_nostr_identity_binding", input);
|
||||
// C6/C7: thread {value, artifact} to application sites — unwrap is temporary.
|
||||
const { value } = await invokeTauri<StampedArtifact<string>>(
|
||||
"sign_nostr_identity_binding",
|
||||
input,
|
||||
);
|
||||
return value;
|
||||
}
|
||||
|
||||
56
desktop/src/shared/api/identityArtifacts.ts
Normal file
56
desktop/src/shared/api/identityArtifacts.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { RelayEvent } from "@/shared/api/types";
|
||||
import { invokeTauri } from "./tauri";
|
||||
import type { StampedArtifact } from "./stampedArtifact";
|
||||
|
||||
// Owner-identity artifact producers (NIP-AP). Each Rust command returns the
|
||||
// owner-key-derived value wrapped as `{ value, artifact }`; C6/C7 threads the
|
||||
// pair to each identity-sensitive application site and adds the
|
||||
// generation-compare. Until then these adapters unwrap `.value` at the boundary
|
||||
// — the stamp is inert but the wire shape is pinned (see `StampedArtifact`).
|
||||
|
||||
export async function signRelayEvent(input: {
|
||||
kind: number;
|
||||
content: string;
|
||||
createdAt?: number;
|
||||
tags: string[][];
|
||||
}): Promise<RelayEvent> {
|
||||
// C6/C7: thread {value, artifact} to application sites — unwrap is temporary.
|
||||
const r = await invokeTauri<StampedArtifact<string>>("sign_event", input);
|
||||
return JSON.parse(r.value) as RelayEvent;
|
||||
}
|
||||
|
||||
export async function createAuthEvent(input: {
|
||||
challenge: string;
|
||||
relayUrl: string;
|
||||
}): Promise<RelayEvent> {
|
||||
// C6/C7: thread {value, artifact} to application sites — unwrap is temporary.
|
||||
const r = await invokeTauri<StampedArtifact<string>>(
|
||||
"create_auth_event",
|
||||
input,
|
||||
);
|
||||
return JSON.parse(r.value) as RelayEvent;
|
||||
}
|
||||
|
||||
export async function nip44EncryptToSelf(plaintext: string): Promise<string> {
|
||||
// C6/C7: thread {value, artifact} to application sites — unwrap is temporary.
|
||||
const r = await invokeTauri<StampedArtifact<string>>(
|
||||
"nip44_encrypt_to_self",
|
||||
{
|
||||
plaintext,
|
||||
},
|
||||
);
|
||||
return r.value;
|
||||
}
|
||||
|
||||
export async function nip44DecryptFromSelf(
|
||||
ciphertext: string,
|
||||
): Promise<string> {
|
||||
// C6/C7: thread {value, artifact} to application sites — unwrap is temporary.
|
||||
const r = await invokeTauri<StampedArtifact<string>>(
|
||||
"nip44_decrypt_from_self",
|
||||
{
|
||||
ciphertext,
|
||||
},
|
||||
);
|
||||
return r.value;
|
||||
}
|
||||
@@ -105,7 +105,13 @@ function setupTauriStubs(
|
||||
invoke: async (command, args) => {
|
||||
calls.invokeArgs.push({ command, args });
|
||||
if (command === "get_relay_http_url") return httpBase;
|
||||
if (command === "sign_event") return JSON.stringify(authEvent);
|
||||
// sign_event returns the NIP-AP stamped artifact `{ value, artifact }`;
|
||||
// the adapter unwraps `.value` (generation-compare lands in C6/C7).
|
||||
if (command === "sign_event")
|
||||
return {
|
||||
value: JSON.stringify(authEvent),
|
||||
artifact: { id: 0, generation: 0 },
|
||||
};
|
||||
throw new Error(`Unexpected Tauri command: ${command}`);
|
||||
},
|
||||
};
|
||||
|
||||
18
desktop/src/shared/api/stampedArtifact.ts
Normal file
18
desktop/src/shared/api/stampedArtifact.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* The `{ value, artifact }` pair the owner-identity artifact producers return
|
||||
* across the Tauri boundary. `artifact` is the generation stamp
|
||||
* (`OwnerIdentityCapability<ArtifactPolicy>`, NIP-AP): `id` is
|
||||
* correlation/diagnostic only, `generation` is the identity-persistence
|
||||
* generation the value was signed/decrypted under.
|
||||
*
|
||||
* The wire contract is locked here and on the Rust side (`StampedArtifact<T>`):
|
||||
* C6/C7 threads this pair to each identity-sensitive application site and adds
|
||||
* the generation-compare (refuse to apply once the current generation advances
|
||||
* past `artifact.generation`). Until then the adapters unwrap `.value` at the
|
||||
* boundary — the stamp is inert but the shape is pinned so C6/C7 inherits a
|
||||
* stable contract, not a rediscovery.
|
||||
*/
|
||||
export interface StampedArtifact<T> {
|
||||
value: T;
|
||||
artifact: { id: number; generation: number };
|
||||
}
|
||||
@@ -118,6 +118,13 @@ type RawRelayAgent = {
|
||||
};
|
||||
|
||||
import type { RestartDiffEntry as RawRestartDiffEntry } from "./restartDiff";
|
||||
export type { StampedArtifact } from "./stampedArtifact";
|
||||
export {
|
||||
signRelayEvent,
|
||||
createAuthEvent,
|
||||
nip44EncryptToSelf,
|
||||
nip44DecryptFromSelf,
|
||||
} from "./identityArtifacts";
|
||||
export type RawManagedAgent = {
|
||||
pubkey: string;
|
||||
name: string;
|
||||
@@ -655,24 +662,6 @@ export async function removeReaction(
|
||||
await invokeTauri("remove_reaction", { eventId, emoji });
|
||||
}
|
||||
|
||||
export async function signRelayEvent(input: {
|
||||
kind: number;
|
||||
content: string;
|
||||
createdAt?: number;
|
||||
tags: string[][];
|
||||
}): Promise<RelayEvent> {
|
||||
const eventJson = await invokeTauri<string>("sign_event", input);
|
||||
return JSON.parse(eventJson) as RelayEvent;
|
||||
}
|
||||
|
||||
export async function createAuthEvent(input: {
|
||||
challenge: string;
|
||||
relayUrl: string;
|
||||
}): Promise<RelayEvent> {
|
||||
const eventJson = await invokeTauri<string>("create_auth_event", input);
|
||||
return JSON.parse(eventJson) as RelayEvent;
|
||||
}
|
||||
|
||||
function fromRawRelayAgent(agent: RawRelayAgent): RelayAgent {
|
||||
return {
|
||||
pubkey: agent.pubkey,
|
||||
@@ -1116,16 +1105,6 @@ export async function probeBackendProvider(
|
||||
|
||||
// ── NIP-44 encrypt-to-self ───────────────────────────────────────────────────
|
||||
|
||||
export async function nip44EncryptToSelf(plaintext: string): Promise<string> {
|
||||
return invokeTauri<string>("nip44_encrypt_to_self", { plaintext });
|
||||
}
|
||||
|
||||
export async function nip44DecryptFromSelf(
|
||||
ciphertext: string,
|
||||
): Promise<string> {
|
||||
return invokeTauri<string>("nip44_decrypt_from_self", { ciphertext });
|
||||
}
|
||||
|
||||
export async function startPairing(): Promise<string> {
|
||||
return invokeTauri<string>("start_pairing");
|
||||
}
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import type { RelayEvent } from "@/shared/api/types";
|
||||
import { invokeTauri } from "./tauri";
|
||||
import { invokeTauri, type StampedArtifact } from "./tauri";
|
||||
|
||||
export async function decryptObserverEvent(
|
||||
event: RelayEvent,
|
||||
): Promise<unknown> {
|
||||
return invokeTauri<unknown>("decrypt_observer_event", {
|
||||
eventJson: JSON.stringify(event),
|
||||
});
|
||||
// C6/C7: thread {value, artifact} to application sites — unwrap is temporary.
|
||||
const { value } = await invokeTauri<StampedArtifact<unknown>>(
|
||||
"decrypt_observer_event",
|
||||
{ eventJson: JSON.stringify(event) },
|
||||
);
|
||||
return value;
|
||||
}
|
||||
|
||||
export async function buildObserverControlEvent(input: {
|
||||
agentPubkey: string;
|
||||
payload: unknown;
|
||||
}): Promise<RelayEvent> {
|
||||
const eventJson = await invokeTauri<string>("build_observer_control_event", {
|
||||
agentPubkey: input.agentPubkey,
|
||||
payload: input.payload,
|
||||
});
|
||||
return JSON.parse(eventJson) as RelayEvent;
|
||||
// C6/C7: thread {value, artifact} to application sites — unwrap is temporary.
|
||||
const { value } = await invokeTauri<StampedArtifact<string>>(
|
||||
"build_observer_control_event",
|
||||
{ agentPubkey: input.agentPubkey, payload: input.payload },
|
||||
);
|
||||
return JSON.parse(value) as RelayEvent;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,13 @@ const preference = {
|
||||
followSystem: false,
|
||||
};
|
||||
|
||||
// Owner-identity artifact producers (NIP-AP) return `{ value, artifact }` across
|
||||
// the Tauri boundary; the adapters unwrap `.value`. Wrap a mock's bare value in
|
||||
// the stamped shape (inert stamp in C3 — the generation-compare lands in C6/C7).
|
||||
function stampedArtifact(value) {
|
||||
return { value, artifact: { id: 0, generation: 0 } };
|
||||
}
|
||||
|
||||
function installFakeTimer() {
|
||||
globalThis.window ??= {};
|
||||
let callback = null;
|
||||
@@ -147,15 +154,18 @@ test("new remote invalidates no-op suppression for A to B to A", async () => {
|
||||
let signedEventId = "published-z";
|
||||
globalThis.window.__TAURI_INTERNALS__ = {
|
||||
invoke(command, args) {
|
||||
if (command === "nip44_encrypt_to_self") return Promise.resolve("cipher");
|
||||
if (command === "nip44_encrypt_to_self")
|
||||
return Promise.resolve(stampedArtifact("cipher"));
|
||||
if (command === "sign_event") {
|
||||
return Promise.resolve(
|
||||
JSON.stringify(
|
||||
relayEvent({
|
||||
id: signedEventId,
|
||||
content: args.content,
|
||||
created_at: args.createdAt,
|
||||
}),
|
||||
stampedArtifact(
|
||||
JSON.stringify(
|
||||
relayEvent({
|
||||
id: signedEventId,
|
||||
content: args.content,
|
||||
created_at: args.createdAt,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -201,16 +211,19 @@ test("serializes an in-flight publish before sending the latest edit", async ()
|
||||
let signed = 0;
|
||||
globalThis.window.__TAURI_INTERNALS__ = {
|
||||
invoke(command, args) {
|
||||
if (command === "nip44_encrypt_to_self") return Promise.resolve("cipher");
|
||||
if (command === "nip44_encrypt_to_self")
|
||||
return Promise.resolve(stampedArtifact("cipher"));
|
||||
if (command === "sign_event") {
|
||||
signed += 1;
|
||||
return Promise.resolve(
|
||||
JSON.stringify(
|
||||
relayEvent({
|
||||
id: `event-${signed}`,
|
||||
content: args.content,
|
||||
created_at: args.createdAt,
|
||||
}),
|
||||
stampedArtifact(
|
||||
JSON.stringify(
|
||||
relayEvent({
|
||||
id: `event-${signed}`,
|
||||
content: args.content,
|
||||
created_at: args.createdAt,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -256,16 +269,19 @@ test("republishes above a newer remote observed while publish is in flight", asy
|
||||
let signed = 0;
|
||||
globalThis.window.__TAURI_INTERNALS__ = {
|
||||
invoke(command, args) {
|
||||
if (command === "nip44_encrypt_to_self") return Promise.resolve("cipher");
|
||||
if (command === "nip44_encrypt_to_self")
|
||||
return Promise.resolve(stampedArtifact("cipher"));
|
||||
if (command === "sign_event") {
|
||||
signed += 1;
|
||||
return Promise.resolve(
|
||||
JSON.stringify(
|
||||
relayEvent({
|
||||
id: `event-${signed}`,
|
||||
content: args.content,
|
||||
created_at: args.createdAt,
|
||||
}),
|
||||
stampedArtifact(
|
||||
JSON.stringify(
|
||||
relayEvent({
|
||||
id: `event-${signed}`,
|
||||
content: args.content,
|
||||
created_at: args.createdAt,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -316,17 +332,21 @@ test("delayed live decryption fences publish acknowledgement and preserves local
|
||||
let signed = 0;
|
||||
globalThis.window.__TAURI_INTERNALS__ = {
|
||||
invoke(command, args) {
|
||||
if (command === "nip44_encrypt_to_self") return Promise.resolve("cipher");
|
||||
if (command === "nip44_decrypt_from_self") return remotePlaintext.promise;
|
||||
if (command === "nip44_encrypt_to_self")
|
||||
return Promise.resolve(stampedArtifact("cipher"));
|
||||
if (command === "nip44_decrypt_from_self")
|
||||
return remotePlaintext.promise.then(stampedArtifact);
|
||||
if (command === "sign_event") {
|
||||
signed += 1;
|
||||
return Promise.resolve(
|
||||
JSON.stringify(
|
||||
relayEvent({
|
||||
id: `event-${signed}`,
|
||||
content: args.content,
|
||||
created_at: args.createdAt,
|
||||
}),
|
||||
stampedArtifact(
|
||||
JSON.stringify(
|
||||
relayEvent({
|
||||
id: `event-${signed}`,
|
||||
content: args.content,
|
||||
created_at: args.createdAt,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -388,15 +408,18 @@ test("transient publish failure retries and acknowledges exact event", async ()
|
||||
let attempts = 0;
|
||||
globalThis.window.__TAURI_INTERNALS__ = {
|
||||
invoke(command, args) {
|
||||
if (command === "nip44_encrypt_to_self") return Promise.resolve("cipher");
|
||||
if (command === "nip44_encrypt_to_self")
|
||||
return Promise.resolve(stampedArtifact("cipher"));
|
||||
if (command === "sign_event") {
|
||||
return Promise.resolve(
|
||||
JSON.stringify(
|
||||
relayEvent({
|
||||
id: "published-event",
|
||||
content: args.content,
|
||||
created_at: args.createdAt,
|
||||
}),
|
||||
stampedArtifact(
|
||||
JSON.stringify(
|
||||
relayEvent({
|
||||
id: "published-event",
|
||||
content: args.content,
|
||||
created_at: args.createdAt,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user