diff --git a/crates/username_registry_coordinator/src/external_id_attestation.rs b/crates/username_registry_coordinator/src/external_id_attestation.rs index 4c47875..7d0ff01 100644 --- a/crates/username_registry_coordinator/src/external_id_attestation.rs +++ b/crates/username_registry_coordinator/src/external_id_attestation.rs @@ -1,3 +1,4 @@ +use core::str; use hdk::prelude::*; use holoom_types::{ ConfirmExternalIdRequestPayload, ExternalIdAttestation, GetAttestationForExternalIdPayload, @@ -226,3 +227,39 @@ pub fn delete_external_id_attestation( ) -> ExternResult { delete_entry(original_attestation_hash) } + +/// Add your agent to the global external ID attestors list. The `provider_name` is used to +/// indicate the identity provider for which you attest identities. +#[hdk_extern] +pub fn register_as_external_id_attestor(provider_name: String) -> ExternResult { + create_link( + hash_identifier("all_external_id_attestors".into())?, + agent_info()?.agent_initial_pubkey, + LinkTypes::ExternalIdAttestor, + provider_name, + ) +} + +/// Gets a list of `(agent, provider_name)` pairs representing all globally listed external ID +/// attestors. +/// +/// The `agent` is the attestor, and the `provider_name` is a `String` that names the identity +/// provider for which they are attesting identities. +#[hdk_extern] +pub fn get_all_external_id_attestors(_: ()) -> ExternResult> { + let pairs = get_links( + GetLinksInputBuilder::try_new( + hash_identifier("all_external_id_attestors".into())?, + LinkTypes::ExternalIdAttestor, + )? + .build(), + )? + .into_iter() + .filter_map(|link| { + let agent = AgentPubKey::try_from(link.target).ok()?; + let provider_name = str::from_utf8(&link.tag.into_inner()).ok()?.to_string(); + Some((agent, provider_name)) + }) + .collect(); + Ok(pairs) +} diff --git a/crates/username_registry_integrity/src/link_types.rs b/crates/username_registry_integrity/src/link_types.rs index 3aaa48e..c36924f 100644 --- a/crates/username_registry_integrity/src/link_types.rs +++ b/crates/username_registry_integrity/src/link_types.rs @@ -8,6 +8,7 @@ pub enum LinkTypes { AgentMetadata, AgentToWalletAttestations, AgentToExternalIdAttestation, + ExternalIdAttestor, ExternalIdToAttestation, Publisher, NameToOracleDocument, @@ -36,6 +37,7 @@ impl LinkTypes { LinkTypes::AgentToExternalIdAttestation => { validate_create_link_agent_to_external_id_attestations } + LinkTypes::ExternalIdAttestor => validate_create_link_external_id_attestor, LinkTypes::ExternalIdToAttestation => validate_create_link_external_id_to_attestation, LinkTypes::Publisher => validate_create_link_publisher, LinkTypes::NameToOracleDocument => validate_create_link_name_to_oracle_document, @@ -65,6 +67,7 @@ impl LinkTypes { LinkTypes::AgentToWalletAttestations => { validate_delete_link_agent_to_wallet_attestations } + LinkTypes::ExternalIdAttestor => validate_delete_link_external_id_attestor, LinkTypes::AgentToExternalIdAttestation => { validate_delete_link_agent_to_external_id_attestations } diff --git a/crates/username_registry_validation/src/external_id_attestor.rs b/crates/username_registry_validation/src/external_id_attestor.rs new file mode 100644 index 0000000..4cc3f55 --- /dev/null +++ b/crates/username_registry_validation/src/external_id_attestor.rs @@ -0,0 +1,35 @@ +use core::str; + +use hdi::prelude::*; + +pub fn validate_create_link_external_id_attestor( + action: CreateLink, + _base_address: AnyLinkableHash, + target_address: AnyLinkableHash, + tag: LinkTag, +) -> ExternResult { + if AnyLinkableHash::from(action.author) != target_address { + return Ok(ValidateCallbackResult::Invalid( + "Target of attestor link must author".to_string(), + )); + } + + if str::from_utf8(&tag.into_inner()).is_err() { + return Ok(ValidateCallbackResult::Invalid( + "Tag must be a valid utf-8 string".to_string(), + )); + } + + Ok(ValidateCallbackResult::Valid) +} +pub fn validate_delete_link_external_id_attestor( + _action: DeleteLink, + _original_action: CreateLink, + _base: AnyLinkableHash, + _target: AnyLinkableHash, + _tag: LinkTag, +) -> ExternResult { + Ok(ValidateCallbackResult::Invalid(String::from( + "Attestor links cannot be deleted", + ))) +} diff --git a/crates/username_registry_validation/src/lib.rs b/crates/username_registry_validation/src/lib.rs index e05caca..7ee4d60 100644 --- a/crates/username_registry_validation/src/lib.rs +++ b/crates/username_registry_validation/src/lib.rs @@ -34,3 +34,5 @@ pub mod evm_address_to_signing_offer; pub use evm_address_to_signing_offer::*; pub mod publisher; pub use publisher::*; +pub mod external_id_attestor; +pub use external_id_attestor::*; diff --git a/packages/tryorama/src/external_attestation/agents_can_list_as_external_id_attestors.test.ts b/packages/tryorama/src/external_attestation/agents_can_list_as_external_id_attestors.test.ts new file mode 100644 index 0000000..973f6d5 --- /dev/null +++ b/packages/tryorama/src/external_attestation/agents_can_list_as_external_id_attestors.test.ts @@ -0,0 +1,24 @@ +import { expect, test } from "vitest"; +import { dhtSync, runScenario } from "@holochain/tryorama"; + +import { setupPlayer } from "../utils/setup-happ.js"; + +test("Agents can list as external id attestors", async () => { + await runScenario(async (scenario) => { + const [alice, aliceCoordinators] = await setupPlayer(scenario); + const [bob, bobCoordinators] = await setupPlayer(scenario); + await scenario.shareAllAgents(); + + await expect( + aliceCoordinators.usernameRegistry.registerAsExternalIdAttestor( + "some-identity-provider" + ) + ).resolves.not.toThrow(); + + await dhtSync([alice, bob], alice.cells[0].cell_id[0]); + + await expect( + bobCoordinators.usernameRegistry.getAllExternalIdAttestors() + ).resolves.toEqual([[alice.agentPubKey, "some-identity-provider"]]); + }); +}); diff --git a/packages/tryorama/src/oracle/agents_can_list_as_publishers.test.ts b/packages/tryorama/src/oracle/agents_can_list_as_publishers.test.ts index b6b9678..c5215d3 100644 --- a/packages/tryorama/src/oracle/agents_can_list_as_publishers.test.ts +++ b/packages/tryorama/src/oracle/agents_can_list_as_publishers.test.ts @@ -9,14 +9,12 @@ test("Agents can list as publishers", async () => { const [bob, bobCoordinators] = await setupPlayer(scenario); await scenario.shareAllAgents(); - // Authority creates a external_id Attestation await expect( aliceCoordinators.usernameRegistry.registerAsPublisher("some-topic") ).resolves.not.toThrow(); await dhtSync([alice, bob], alice.cells[0].cell_id[0]); - // Authority gets the external_id Attestation await expect( bobCoordinators.usernameRegistry.getAllPublishers() ).resolves.toEqual([[alice.agentPubKey, "some-topic"]]); diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index f76ab5a..45b2a8b 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,3 +1,4 @@ export * from "./types"; export * from "./zome-functions"; export * from "./utils"; +export * from "./integrity-enums"; diff --git a/packages/types/src/integrity-enums/UsernameRegistryIntegrity.ts b/packages/types/src/integrity-enums/UsernameRegistryIntegrity.ts index 0ded43b..9f444af 100644 --- a/packages/types/src/integrity-enums/UsernameRegistryIntegrity.ts +++ b/packages/types/src/integrity-enums/UsernameRegistryIntegrity.ts @@ -17,11 +17,12 @@ export enum UsernameRegistryIntegrityLinkTypeIndex { AgentMetadata = 1, AgentToWalletAttestations = 2, AgentToExternalIdAttestation = 3, - ExternalIdToAttestation = 4, - Publisher = 5, - NameToOracleDocument = 6, - RelateOracleDocumentName = 7, - NameToRecipe = 8, - NameToSigningOffer = 9, - EvmAddressToSigningOffer = 10, + ExternalIdAttestor = 4, + ExternalIdToAttestation = 5, + Publisher = 6, + NameToOracleDocument = 7, + RelateOracleDocumentName = 8, + NameToRecipe = 9, + NameToSigningOffer = 10, + EvmAddressToSigningOffer = 11, } diff --git a/packages/types/src/zome-functions/UsernameRegistryCoordinator.ts b/packages/types/src/zome-functions/UsernameRegistryCoordinator.ts index d12642b..44735de 100644 --- a/packages/types/src/zome-functions/UsernameRegistryCoordinator.ts +++ b/packages/types/src/zome-functions/UsernameRegistryCoordinator.ts @@ -136,6 +136,10 @@ export class UsernameRegistryCoordinator { return this.callZomeFn("get_all_authored_username_attestations"); } + async getAllExternalIdAttestors(): Promise<[AgentPubKey, string][]> { + return this.callZomeFn("get_all_external_id_attestors"); + } + async getAllPublishers(): Promise<[AgentPubKey, string][]> { return this.callZomeFn("get_all_publishers"); } @@ -245,6 +249,10 @@ export class UsernameRegistryCoordinator { return this.callZomeFn("ingest_signed_username", signedUsername); } + async registerAsExternalIdAttestor(tag: string): Promise { + return this.callZomeFn("register_as_external_id_attestor", tag); + } + async registerAsPublisher(tag: string): Promise { return this.callZomeFn("register_as_publisher", tag); }