Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Everything that changes for someone using Wolfram goes here, newest first, in th

## [Unreleased]

### Added

- `wf_agent_login_discovered` signs in with a handle or DID and no service: it resolves the account's PDS from its DID document (`#atproto_pds`), points the agent there, and logs in at that PDS. Clients no longer need the user to type the host their account lives on. ([#187](https://github.com/ewanc26/wolfram/pull/187))

### Fixed

- CI bounds each `apt-get` call to a timeout and retries it three times. A stuck package mirror had left jobs running for an hour, which blocked the release that waits on `CI gate`. ([#186](https://github.com/ewanc26/wolfram/pull/186))
Expand Down
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1713,6 +1713,11 @@ if(WOLFRAM_BUILD_TESTS)
target_include_directories(test_agent_set_bearer PRIVATE test ${cjson_SOURCE_DIR})
add_test(NAME agent_set_bearer COMMAND test_agent_set_bearer)

add_executable(test_agent_login_discovered test/test_agent_login_discovered.c)
target_link_libraries(test_agent_login_discovered PRIVATE wolfram)
target_include_directories(test_agent_login_discovered PRIVATE test ${cjson_SOURCE_DIR})
add_test(NAME agent_login_discovered COMMAND test_agent_login_discovered)

add_executable(test_time test/test_time.c)
target_link_libraries(test_time PRIVATE wolfram)
target_include_directories(test_time PRIVATE test ${cjson_SOURCE_DIR})
Expand Down
16 changes: 16 additions & 0 deletions include/wolfram/agent.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,22 @@ wf_status wf_agent_set_tls_rng(wf_agent *agent, wf_tls_rng_fn fn,
void *userdata);

/* Session management */
/*
* Sign in without knowing the account's PDS in advance. `identifier` is a
* handle or a DID; it is resolved (DNS TXT, then /.well-known/atproto-did for
* handles), the DID document's #atproto_pds endpoint is read, the agent is
* pointed at that PDS, and only then is the login sent there. So a user types
* their handle and password, and never the service they are hosted on.
*
* The agent's current base URL is used for resolution (any reachable host will
* do). `*out_pds` is set to the PDS the login was sent to and is
* heap-allocated; the caller frees it. On failure before the login nothing is
* changed: the agent keeps its previous base URL. WF_ERR_NOT_FOUND when the DID
* document has no PDS endpoint.
*/
wf_status wf_agent_login_discovered(wf_agent *agent, const char *identifier,
const char *password, char **out_pds);

wf_status wf_agent_login(wf_agent *agent, const char *identifier,
const char *password);
wf_status wf_agent_resume(wf_agent *agent, const wf_session_data *data);
Expand Down
42 changes: 42 additions & 0 deletions src/agent/agent.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "wolfram/util.h"

#include "wolfram/identity.h"
#include "wolfram/xrpc.h"
#include "wolfram/repo.h"
#include "wolfram/richtext.h"
#include "wolfram/server.h"
Expand Down Expand Up @@ -650,6 +651,47 @@ wf_status wf_agent_login(wf_agent *agent, const char *identifier,
return WF_OK;
}

wf_status wf_agent_login_discovered(wf_agent *agent, const char *identifier,
const char *password, char **out_pds) {
if (out_pds) {
*out_pds = NULL;
}
if (!agent || !agent->session || !agent->client || !identifier ||
!identifier[0] || !password || !out_pds) {
return WF_ERR_INVALID_ARG;
}

char *did = NULL;
char *pds = NULL;
wf_status status;
if (wf_did_method_of(identifier) != WF_DID_METHOD_UNKNOWN) {
did = wf_str_dup(identifier);
status = did ? WF_OK : WF_ERR_ALLOC;
} else {
status = wf_handle_resolve(agent->client, identifier, &did);
}
if (status == WF_OK) {
status =
wf_did_resolve_service_by_id(agent->client, did, "#atproto_pds",
"AtprotoPersonalDataServer", &pds);
}
if (status != WF_OK) {
free(did);
free(pds);
return status;
}

wf_xrpc_client_set_base_url(agent->client, pds);
status = wf_agent_login(agent, identifier, password);
free(did);
if (status != WF_OK) {
free(pds);
return status;
}
*out_pds = pds;
return WF_OK;
}

wf_status wf_agent_resume(wf_agent *agent, const wf_session_data *data) {
if (!agent || !agent->session || !agent->client || !data) {
return WF_ERR_INVALID_ARG;
Expand Down
40 changes: 40 additions & 0 deletions test/test_agent_login_discovered.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* test_agent_login_discovered.c -- wf_agent_login_discovered's argument checks,
* and that a handle which cannot be resolved fails before any login is sent,
* with no PDS reported. The network path is exercised by the consumers' e2e
* runs.
*/

#include "wolfram/agent.h"

#include "test.h"

#include <stdlib.h>

int main(void) {
wf_agent *a = wf_agent_new("https://bsky.social");
char *pds = NULL;

WF_CHECK(a != NULL);
if (!a) WF_TEST_SUMMARY();

WF_CHECK(wf_agent_login_discovered(NULL, "alice.example.com", "pw", &pds) ==
WF_ERR_INVALID_ARG);
WF_CHECK(pds == NULL);
WF_CHECK(wf_agent_login_discovered(a, "", "pw", &pds) ==
WF_ERR_INVALID_ARG);
WF_CHECK(wf_agent_login_discovered(a, "alice.example.com", NULL, &pds) ==
WF_ERR_INVALID_ARG);
WF_CHECK(wf_agent_login_discovered(a, "alice.example.com", "pw", NULL) ==
WF_ERR_INVALID_ARG);

/* `.invalid` never resolves (RFC 2606), so discovery fails and no login is
* sent: the out-parameter stays NULL. */
pds = (char *)0x1;
WF_CHECK(wf_agent_login_discovered(a, "nobody.invalid", "pw", &pds) !=
WF_OK);
WF_CHECK(pds == NULL);

wf_agent_free(a);
WF_TEST_SUMMARY();
}
Loading