diff --git a/CMakeLists.txt b/CMakeLists.txt index 3aac1e3..19fbc4b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -959,7 +959,7 @@ endif() # already owns the `wolfram` target name; OUTPUT_NAME keeps the binary # `wolfram`. if(NOT WOLFRAM_BUILD_EMBEDDED) - add_executable(wolfram_cli src/cli/main.c) + add_executable(wolfram_cli src/cli/main.c src/cli/cli_oauth.c) target_link_libraries(wolfram_cli PRIVATE wolfram) target_include_directories(wolfram_cli PRIVATE ${cjson_SOURCE_DIR} ${cjson_BINARY_DIR}) diff --git a/src/cli/cli_oauth.c b/src/cli/cli_oauth.c new file mode 100644 index 0000000..4666868 --- /dev/null +++ b/src/cli/cli_oauth.c @@ -0,0 +1,374 @@ +/* + * cli_oauth.c — the `oauth-login` / `oauth-callback` subcommands: the + * device/browser OAuth authorization flow, split out of main.c as its own + * self-contained concern. + */ + +#include "cli_oauth.h" +#include "main_internal.h" + +#include "wolfram/agent.h" +#include "wolfram/oauth.h" +#include "wolfram/xrpc.h" + +#include +#include +#include +#include +#include +#include + +/* Return a heap-owned default path for the persisted OAuth pending-state file + * (~/.wolfram_oauth_state.json), falling back to the cwd when $HOME is unset. + * Caller frees. */ +static char *oauth_default_state_path(void) { + const char *home = getenv("HOME"); + const char *name = ".wolfram_oauth_state.json"; + if (!home) home = "."; + size_t n = strlen(home) + 1 + strlen(name) + 1; + char *p = malloc(n); + if (p) snprintf(p, n, "%s/%s", home, name); + return p; +} + +/* Return a heap-owned default path for the persisted OAuth session file + * (~/.wolfram_session.json). Caller frees. */ +static char *oauth_default_session_path(void) { + const char *home = getenv("HOME"); + const char *name = ".wolfram_session.json"; + if (!home) home = "."; + size_t n = strlen(home) + 1 + strlen(name) + 1; + char *p = malloc(n); + if (p) snprintf(p, n, "%s/%s", home, name); + return p; +} + +/* Write `data` to `path` (text mode). Returns 0 on success, -1 on failure. */ +static int write_text_file(const char *path, const char *data) { + FILE *f = fopen(path, "wb"); + if (!f) return -1; + size_t n = fwrite(data, 1, strlen(data), f); + fclose(f); + return (n == strlen(data)) ? 0 : -1; +} + +/* Parse the query/fragment of a redirect URL into owned callback params. + * Mirrors examples/oauth_session.c; the caller frees the strdup'd fields. */ +static void parse_callback_url(const char *url, + wf_oauth_callback_params *params) { + memset(params, 0, sizeof(*params)); + const char *q = strchr(url, '?'); + if (!q) q = strchr(url, '#'); + if (!q) return; + q++; + while (*q) { + const char *amp = strchr(q, '&'); + size_t pair_len = amp ? (size_t)(amp - q) : strlen(q); + const char *eq = memchr(q, '=', pair_len); + if (!eq) { + q = amp ? amp + 1 : q + pair_len; + continue; + } + size_t name_len = (size_t)(eq - q); + size_t val_len = pair_len - name_len - 1; + if (name_len == 5 && memcmp(q, "state", 5) == 0) + params->state = strndup(eq + 1, val_len); + else if (name_len == 4 && memcmp(q, "code", 4) == 0) + params->code = strndup(eq + 1, val_len); + else if (name_len == 3 && memcmp(q, "iss", 3) == 0) + params->issuer = strndup(eq + 1, val_len); + else if (name_len == 5 && memcmp(q, "error", 5) == 0) + params->error = strndup(eq + 1, val_len); + q = amp ? amp + 1 : q + pair_len; + } +} + +/* wolfram oauth-callback --url --state + * [--state-file ] [--client-id ] [--redirect-uri ] + * [--session ] + * + * Completes the OAuth flow begun by `oauth-login`: validates the callback + * against the persisted pending state, exchanges the code for tokens, and + * writes the resulting session to a file (default ~/.wolfram_session.json). + * No HTTP callback server is run; the user pastes the redirect URL. */ +int cmd_oauth_callback(int argc, char **argv) { + if (argc < 2) { + usage_stream(stderr); + return 0; + } + const char *service = NULL; + const char *url = NULL; + const char *state = NULL; + const char *state_file = NULL; + const char *client_id = NULL; + const char *redirect_uri = NULL; + const char *session_path = NULL; + + char **pos = malloc(sizeof(char *) * (size_t)argc); + if (!pos) { + fprintf(stderr, "error: out of memory\n"); + return 1; + } + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--url") == 0 && i + 1 < argc) + url = argv[++i]; + else if (strcmp(argv[i], "--state") == 0 && i + 1 < argc) + state = argv[++i]; + else if (strcmp(argv[i], "--state-file") == 0 && i + 1 < argc) + state_file = argv[++i]; + else if (strcmp(argv[i], "--client-id") == 0 && i + 1 < argc) + client_id = argv[++i]; + else if (strcmp(argv[i], "--redirect-uri") == 0 && i + 1 < argc) + redirect_uri = argv[++i]; + else if (strcmp(argv[i], "--session") == 0 && i + 1 < argc) + session_path = argv[++i]; + else + pos[pi++] = argv[i]; + } + if (pi >= 1) service = pos[0]; + free(pos); + + if (!service || !url || !state) { + fprintf( + stderr, + "error: usage: wolfram oauth-callback --url " + "--state [--state-file ] [--client-id ] " + "[--redirect-uri ] [--session ]\n"); + return 1; + } + + char *def_state = oauth_default_state_path(); + const char *sf = state_file ? state_file : def_state; + char *state_json = read_text_file(sf); + free(def_state); + if (!state_json) { + fprintf(stderr, + "error: could not read pending state file '%s' " + "(run oauth-login first)\n", + sf); + return 1; + } + + if (!redirect_uri) redirect_uri = "https://localhost/callback"; + if (!client_id) client_id = redirect_uri; + + wf_xrpc_client *transport = wf_xrpc_client_new(service); + if (!transport) { + fprintf(stderr, "error: failed to create XRPC client\n"); + free(state_json); + return 1; + } + + wf_oauth_resource_metadata resource = {0}; + wf_oauth_server_metadata server = {0}; + wf_status s = wf_oauth_discover(transport, service, &resource, &server); + if (s != WF_OK) { + fprintf(stderr, "error: OAuth discovery failed (status %d)\n", (int)s); + wf_oauth_resource_metadata_free(&resource); + wf_oauth_server_metadata_free(&server); + wf_xrpc_client_free(transport); + free(state_json); + return 1; + } + + wf_oauth_client_metadata client = {0}; + s = wf_oauth_client_metadata_get(transport, client_id, &client); + if (s != WF_OK) { + fprintf(stderr, "error: client metadata fetch failed (status %d)\n", + (int)s); + wf_oauth_client_metadata_free(&client); + wf_oauth_resource_metadata_free(&resource); + wf_oauth_server_metadata_free(&server); + wf_xrpc_client_free(transport); + free(state_json); + return 1; + } + + wf_oauth_client_auth client_auth = { + .client_id = client_id, + .authorization_server_issuer = server.issuer, + .signing_key = NULL, + }; + + wf_oauth_callback_params cb = {0}; + parse_callback_url(url, &cb); + + wf_oauth_authorization_complete_result complete = {0}; + s = wf_oauth_authorization_complete( + transport, &server, &client, &client_auth, &cb, state, state_json, + strlen(state_json), redirect_uri, time(NULL), &complete); + + free((void *)cb.state); + free((void *)cb.code); + free((void *)cb.issuer); + free((void *)cb.error); + wf_oauth_client_metadata_free(&client); + wf_oauth_resource_metadata_free(&resource); + wf_oauth_server_metadata_free(&server); + wf_xrpc_client_free(transport); + free(state_json); + + if (s != WF_OK) { + fprintf(stderr, "error: authorization complete failed (status %d)\n", + (int)s); + if (complete.error) + fprintf(stderr, "server error: %s: %s\n", complete.error, + complete.error_description ? complete.error_description + : ""); + wf_oauth_authorization_complete_result_free(&complete); + return 1; + } + + char *def_session = oauth_default_session_path(); + const char *sp = session_path ? session_path : def_session; + if (complete.session_json) { + if (write_text_file(sp, complete.session_json) == 0) + printf("session saved to %s\n", sp); + else + fprintf(stderr, "error: failed to write session file '%s'\n", sp); + } + wf_oauth_authorization_complete_result_free(&complete); + free(def_session); + return 0; +} + +/* OAuth login demonstration — discover the authorization server for the + * protected resource and begin a PAR flow, printing the authorization URL the + * user must visit plus the flow state. No callback server is run. */ +int cmd_oauth_login(int argc, char **argv) { + if (argc < 3) { + usage_stream(stderr); + return 0; + } + const char *service = NULL; + const char *handle = NULL; + const char *client_id = NULL; + const char *redirect_uri = "https://localhost/callback"; + const char *state_file = NULL; + + char **pos = malloc(sizeof(char *) * (size_t)argc); + if (!pos) { + fprintf(stderr, "error: out of memory\n"); + return 1; + } + int pi = 0; + for (int i = 1; i < argc; ++i) { + if (strcmp(argv[i], "--state-file") == 0 && i + 1 < argc) { + state_file = argv[++i]; + } else { + pos[pi++] = argv[i]; + } + } + if (pi < 2) { + fprintf(stderr, "error: usage: wolfram oauth-login " + "[client-id] [redirect-uri] [--state-file ]\n"); + free(pos); + return 1; + } + service = pos[0]; + handle = pos[1]; + if (pi >= 3) client_id = pos[2]; + if (pi >= 4) redirect_uri = pos[3]; + free(pos); + + wf_xrpc_client *transport = wf_xrpc_client_new(service); + if (!transport) { + fprintf(stderr, "error: failed to create XRPC client\n"); + return 1; + } + + wf_oauth_resource_metadata resource = {0}; + wf_oauth_server_metadata server = {0}; + wf_status s = wf_oauth_discover(transport, service, &resource, &server); + if (s != WF_OK) { + fprintf(stderr, "error: OAuth discovery failed (status %d)\n", (int)s); + wf_oauth_resource_metadata_free(&resource); + wf_oauth_server_metadata_free(&server); + wf_xrpc_client_free(transport); + return 1; + } + + printf("authorization server: %s\n", + server.authorization_endpoint ? server.authorization_endpoint : "?"); + if (server.issuer) { + printf("issuer: %s\n", server.issuer); + } + + if (!client_id) { + printf("\nOAuth discovery complete. Provide a client-id " + "(and redirect-uri) to begin the PAR flow:\n" + " wolfram oauth-login %s %s [redirect-uri] " + "[--state-file ]\n", + service, handle); + wf_oauth_resource_metadata_free(&resource); + wf_oauth_server_metadata_free(&server); + wf_xrpc_client_free(transport); + return 0; + } + + wf_oauth_client_metadata client = {0}; + s = wf_oauth_client_metadata_get(transport, client_id, &client); + if (s != WF_OK) { + fprintf(stderr, "error: client metadata fetch failed (status %d)\n", + (int)s); + wf_oauth_resource_metadata_free(&resource); + wf_oauth_server_metadata_free(&server); + wf_xrpc_client_free(transport); + return 1; + } + + wf_oauth_client_auth client_auth = { + .client_id = client_id, + .authorization_server_issuer = server.issuer, + .signing_key = NULL, + }; + + wf_oauth_authorization_begin_options opts = { + .redirect_uri = redirect_uri, + .scope = "atproto", + .login_hint = handle, + .now = time(NULL), + .state_ttl = 600, + }; + + wf_oauth_authorization_begin_result begin = {0}; + s = wf_oauth_authorization_begin(transport, &server, &client, &client_auth, + &opts, &begin); + wf_oauth_client_metadata_free(&client); + wf_oauth_resource_metadata_free(&resource); + wf_oauth_server_metadata_free(&server); + wf_xrpc_client_free(transport); + + if (s != WF_OK) { + fprintf(stderr, "error: authorization begin failed (status %d)\n", + (int)s); + wf_oauth_authorization_begin_result_free(&begin); + return 1; + } + + /* Persist the pending authorization state so oauth-callback can finish the + * flow. The state file holds the serialized PKCE/DPoP material; the printed + * `state` is the opaque CSRF token the callback must echo back. */ + char *def_state = oauth_default_state_path(); + const char *sf = state_file ? state_file : def_state; + if (begin.state_json) { + if (write_text_file(sf, begin.state_json) == 0) + printf("\npending state saved to %s\n", sf); + else + fprintf(stderr, "warning: failed to write state file '%s'\n", sf); + } + free(def_state); + + printf("\nOpen this URL in your browser to authorize:\n%s\n", + begin.authorization_url ? begin.authorization_url : "(none)"); + printf("\nstate: %s\n", begin.state ? begin.state : "(none)"); + printf( + "\nAfter authorizing, run:\n" + " wolfram oauth-callback %s --url \"\" --state %s%s%s\n", + service, begin.state ? begin.state : "", + state_file ? " --state-file " : "", state_file ? state_file : ""); + + wf_oauth_authorization_begin_result_free(&begin); + return 0; +} diff --git a/src/cli/cli_oauth.h b/src/cli/cli_oauth.h new file mode 100644 index 0000000..1d30baf --- /dev/null +++ b/src/cli/cli_oauth.h @@ -0,0 +1,17 @@ +#ifndef WOLFRAM_CLI_OAUTH_H +#define WOLFRAM_CLI_OAUTH_H + +/* `oauth-login` / `oauth-callback` subcommand handlers, dispatched from + * main.c's argv switch. Not part of any installed API -- this is the CLI + * demo program, not the SDK. */ + +/* wolfram oauth-login [client-id] [redirect-uri] + * [--state-file ] */ +int cmd_oauth_login(int argc, char **argv); + +/* wolfram oauth-callback --url --state + * [--state-file ] [--client-id ] [--redirect-uri ] + * [--session ] */ +int cmd_oauth_callback(int argc, char **argv); + +#endif /* WOLFRAM_CLI_OAUTH_H */ diff --git a/src/cli/main.c b/src/cli/main.c index e58a664..1c3ed96 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -66,6 +66,9 @@ #include "wolfram/server.h" #include "wolfram/syntax.h" #include "wolfram/xrpc.h" + +#include "main_internal.h" +#include "cli_oauth.h" #include "wolfram/thread_typed.h" #include "wolfram/feed_typed.h" #include "wolfram/actor_typed.h" @@ -83,7 +86,7 @@ static bool g_json = false; /* Usage */ /* ----------------------------------------------------------------- */ -static void usage_stream(FILE *out) { +void usage_stream(FILE *out) { fprintf( out, "wolfram %s — a command-line client for the AT Protocol (via wolfram " @@ -332,7 +335,7 @@ static void cmd_help_stream(FILE *out, const char *cmd) { } /* Print usage and exit 0 (offline-safe: never touches the network). */ -static int usage_exit(void) { +int usage_exit(void) { usage_stream(stdout); return 0; } @@ -376,8 +379,8 @@ static char *join_args(int argc, char **argv, int first) { /* Resolve an actor argument (a handle or DID) to a heap-owned DID string. * When `actor` is already a valid DID, a copy is returned. Otherwise the * agent's handle-resolution endpoint is used. Caller frees *out_did. */ -static wf_status resolve_actor_to_did(wf_agent *agent, const char *actor, - char **out_did) { +wf_status resolve_actor_to_did(wf_agent *agent, const char *actor, + char **out_did) { if (!agent || !actor || !out_did) { return WF_ERR_INVALID_ARG; } @@ -415,8 +418,8 @@ static wf_agent *agent_login_or_err(const char *service, const char *handle, /* Resolve a post at-uri to its CID via app.bsky.feed.getPosts. * Caller frees *out_cid. Returns WF_OK or an error status. */ -static wf_status resolve_post_cid(wf_agent *agent, const char *at_uri, - char **out_cid) { +wf_status resolve_post_cid(wf_agent *agent, const char *at_uri, + char **out_cid) { if (!agent || !at_uri || !out_cid) { return WF_ERR_INVALID_ARG; } @@ -458,9 +461,9 @@ static wf_status resolve_post_cid(wf_agent *agent, const char *at_uri, * via app.bsky.feed.getPosts. Used by the reply subcommand to build the * proper reply ref. Caller frees *out_cid, *out_root_uri, *out_root_cid. * When the parent post has no reply ref, root defaults to the parent itself. */ -static wf_status resolve_post_for_reply(wf_agent *agent, const char *at_uri, - char **out_cid, char **out_root_uri, - char **out_root_cid) { +wf_status resolve_post_for_reply(wf_agent *agent, const char *at_uri, + char **out_cid, char **out_root_uri, + char **out_root_cid) { if (!agent || !at_uri || !out_cid || !out_root_uri || !out_root_cid) { return WF_ERR_INVALID_ARG; } @@ -1783,8 +1786,7 @@ static int cmd_labels(int argc, char **argv) { /* Print a raw JSON agent response, freeing resources, returning 0 on success. * On error prints to stderr and returns 1. */ -static int finish_agent_response(wf_agent *agent, wf_status s, - wf_response *res) { +int finish_agent_response(wf_agent *agent, wf_status s, wf_response *res) { int rc = 0; if (s != WF_OK) { fprintf(stderr, "error: request failed (status %d)\n", (int)s); @@ -1799,6 +1801,32 @@ static int finish_agent_response(wf_agent *agent, wf_status s, return rc; } +/* Read the entire text file `path` into a heap string (NUL terminated). + * Returns NULL on failure. Caller frees. */ +char *read_text_file(const char *path) { + FILE *f = fopen(path, "rb"); + if (!f) return NULL; + if (fseek(f, 0, SEEK_END) != 0) { + fclose(f); + return NULL; + } + long len = ftell(f); + if (len < 0) { + fclose(f); + return NULL; + } + rewind(f); + char *data = malloc((size_t)len + 1); + if (!data) { + fclose(f); + return NULL; + } + size_t n = fread(data, 1, (size_t)len, f); + fclose(f); + data[n] = '\0'; + return data; +} + /* wolfram follows [limit] */ static int cmd_follows(int argc, char **argv) { if (argc < 3) { @@ -2046,387 +2074,6 @@ static int cmd_video(int argc, char **argv) { return 1; } -/* Return a heap-owned default path for the persisted OAuth pending-state file - * (~/.wolfram_oauth_state.json), falling back to the cwd when $HOME is unset. - * Caller frees. */ -static char *oauth_default_state_path(void) { - const char *home = getenv("HOME"); - const char *name = ".wolfram_oauth_state.json"; - if (!home) home = "."; - size_t n = strlen(home) + 1 + strlen(name) + 1; - char *p = malloc(n); - if (p) snprintf(p, n, "%s/%s", home, name); - return p; -} - -/* Return a heap-owned default path for the persisted OAuth session file - * (~/.wolfram_session.json). Caller frees. */ -static char *oauth_default_session_path(void) { - const char *home = getenv("HOME"); - const char *name = ".wolfram_session.json"; - if (!home) home = "."; - size_t n = strlen(home) + 1 + strlen(name) + 1; - char *p = malloc(n); - if (p) snprintf(p, n, "%s/%s", home, name); - return p; -} - -/* Write `data` to `path` (text mode). Returns 0 on success, -1 on failure. */ -static int write_text_file(const char *path, const char *data) { - FILE *f = fopen(path, "wb"); - if (!f) return -1; - size_t n = fwrite(data, 1, strlen(data), f); - fclose(f); - return (n == strlen(data)) ? 0 : -1; -} - -/* Read the entire text file `path` into a heap string (NUL terminated). - * Returns NULL on failure. Caller frees. */ -static char *read_text_file(const char *path) { - FILE *f = fopen(path, "rb"); - if (!f) return NULL; - if (fseek(f, 0, SEEK_END) != 0) { - fclose(f); - return NULL; - } - long len = ftell(f); - if (len < 0) { - fclose(f); - return NULL; - } - rewind(f); - char *data = malloc((size_t)len + 1); - if (!data) { - fclose(f); - return NULL; - } - size_t n = fread(data, 1, (size_t)len, f); - fclose(f); - data[n] = '\0'; - return data; -} - -/* Parse the query/fragment of a redirect URL into owned callback params. - * Mirrors examples/oauth_session.c; the caller frees the strdup'd fields. */ -static void parse_callback_url(const char *url, - wf_oauth_callback_params *params) { - memset(params, 0, sizeof(*params)); - const char *q = strchr(url, '?'); - if (!q) q = strchr(url, '#'); - if (!q) return; - q++; - while (*q) { - const char *amp = strchr(q, '&'); - size_t pair_len = amp ? (size_t)(amp - q) : strlen(q); - const char *eq = memchr(q, '=', pair_len); - if (!eq) { - q = amp ? amp + 1 : q + pair_len; - continue; - } - size_t name_len = (size_t)(eq - q); - size_t val_len = pair_len - name_len - 1; - if (name_len == 5 && memcmp(q, "state", 5) == 0) - params->state = strndup(eq + 1, val_len); - else if (name_len == 4 && memcmp(q, "code", 4) == 0) - params->code = strndup(eq + 1, val_len); - else if (name_len == 3 && memcmp(q, "iss", 3) == 0) - params->issuer = strndup(eq + 1, val_len); - else if (name_len == 5 && memcmp(q, "error", 5) == 0) - params->error = strndup(eq + 1, val_len); - q = amp ? amp + 1 : q + pair_len; - } -} - -/* wolfram oauth-callback --url --state - * [--state-file ] [--client-id ] [--redirect-uri ] - * [--session ] - * - * Completes the OAuth flow begun by `oauth-login`: validates the callback - * against the persisted pending state, exchanges the code for tokens, and - * writes the resulting session to a file (default ~/.wolfram_session.json). - * No HTTP callback server is run; the user pastes the redirect URL. */ -static int cmd_oauth_callback(int argc, char **argv) { - if (argc < 2) { - usage_stream(stderr); - return 0; - } - const char *service = NULL; - const char *url = NULL; - const char *state = NULL; - const char *state_file = NULL; - const char *client_id = NULL; - const char *redirect_uri = NULL; - const char *session_path = NULL; - - char **pos = malloc(sizeof(char *) * (size_t)argc); - if (!pos) { - fprintf(stderr, "error: out of memory\n"); - return 1; - } - int pi = 0; - for (int i = 1; i < argc; ++i) { - if (strcmp(argv[i], "--url") == 0 && i + 1 < argc) - url = argv[++i]; - else if (strcmp(argv[i], "--state") == 0 && i + 1 < argc) - state = argv[++i]; - else if (strcmp(argv[i], "--state-file") == 0 && i + 1 < argc) - state_file = argv[++i]; - else if (strcmp(argv[i], "--client-id") == 0 && i + 1 < argc) - client_id = argv[++i]; - else if (strcmp(argv[i], "--redirect-uri") == 0 && i + 1 < argc) - redirect_uri = argv[++i]; - else if (strcmp(argv[i], "--session") == 0 && i + 1 < argc) - session_path = argv[++i]; - else - pos[pi++] = argv[i]; - } - if (pi >= 1) service = pos[0]; - free(pos); - - if (!service || !url || !state) { - fprintf( - stderr, - "error: usage: wolfram oauth-callback --url " - "--state [--state-file ] [--client-id ] " - "[--redirect-uri ] [--session ]\n"); - return 1; - } - - char *def_state = oauth_default_state_path(); - const char *sf = state_file ? state_file : def_state; - char *state_json = read_text_file(sf); - free(def_state); - if (!state_json) { - fprintf(stderr, - "error: could not read pending state file '%s' " - "(run oauth-login first)\n", - sf); - return 1; - } - - if (!redirect_uri) redirect_uri = "https://localhost/callback"; - if (!client_id) client_id = redirect_uri; - - wf_xrpc_client *transport = wf_xrpc_client_new(service); - if (!transport) { - fprintf(stderr, "error: failed to create XRPC client\n"); - free(state_json); - return 1; - } - - wf_oauth_resource_metadata resource = {0}; - wf_oauth_server_metadata server = {0}; - wf_status s = wf_oauth_discover(transport, service, &resource, &server); - if (s != WF_OK) { - fprintf(stderr, "error: OAuth discovery failed (status %d)\n", (int)s); - wf_oauth_resource_metadata_free(&resource); - wf_oauth_server_metadata_free(&server); - wf_xrpc_client_free(transport); - free(state_json); - return 1; - } - - wf_oauth_client_metadata client = {0}; - s = wf_oauth_client_metadata_get(transport, client_id, &client); - if (s != WF_OK) { - fprintf(stderr, "error: client metadata fetch failed (status %d)\n", - (int)s); - wf_oauth_client_metadata_free(&client); - wf_oauth_resource_metadata_free(&resource); - wf_oauth_server_metadata_free(&server); - wf_xrpc_client_free(transport); - free(state_json); - return 1; - } - - wf_oauth_client_auth client_auth = { - .client_id = client_id, - .authorization_server_issuer = server.issuer, - .signing_key = NULL, - }; - - wf_oauth_callback_params cb = {0}; - parse_callback_url(url, &cb); - - wf_oauth_authorization_complete_result complete = {0}; - s = wf_oauth_authorization_complete( - transport, &server, &client, &client_auth, &cb, state, state_json, - strlen(state_json), redirect_uri, time(NULL), &complete); - - free((void *)cb.state); - free((void *)cb.code); - free((void *)cb.issuer); - free((void *)cb.error); - wf_oauth_client_metadata_free(&client); - wf_oauth_resource_metadata_free(&resource); - wf_oauth_server_metadata_free(&server); - wf_xrpc_client_free(transport); - free(state_json); - - if (s != WF_OK) { - fprintf(stderr, "error: authorization complete failed (status %d)\n", - (int)s); - if (complete.error) - fprintf(stderr, "server error: %s: %s\n", complete.error, - complete.error_description ? complete.error_description - : ""); - wf_oauth_authorization_complete_result_free(&complete); - return 1; - } - - char *def_session = oauth_default_session_path(); - const char *sp = session_path ? session_path : def_session; - if (complete.session_json) { - if (write_text_file(sp, complete.session_json) == 0) - printf("session saved to %s\n", sp); - else - fprintf(stderr, "error: failed to write session file '%s'\n", sp); - } - wf_oauth_authorization_complete_result_free(&complete); - free(def_session); - return 0; -} - -/* OAuth login demonstration — discover the authorization server for the - * protected resource and begin a PAR flow, printing the authorization URL the - * user must visit plus the flow state. No callback server is run. */ -static int cmd_oauth_login(int argc, char **argv) { - if (argc < 3) { - usage_stream(stderr); - return 0; - } - const char *service = NULL; - const char *handle = NULL; - const char *client_id = NULL; - const char *redirect_uri = "https://localhost/callback"; - const char *state_file = NULL; - - char **pos = malloc(sizeof(char *) * (size_t)argc); - if (!pos) { - fprintf(stderr, "error: out of memory\n"); - return 1; - } - int pi = 0; - for (int i = 1; i < argc; ++i) { - if (strcmp(argv[i], "--state-file") == 0 && i + 1 < argc) { - state_file = argv[++i]; - } else { - pos[pi++] = argv[i]; - } - } - if (pi < 2) { - fprintf(stderr, "error: usage: wolfram oauth-login " - "[client-id] [redirect-uri] [--state-file ]\n"); - free(pos); - return 1; - } - service = pos[0]; - handle = pos[1]; - if (pi >= 3) client_id = pos[2]; - if (pi >= 4) redirect_uri = pos[3]; - free(pos); - - wf_xrpc_client *transport = wf_xrpc_client_new(service); - if (!transport) { - fprintf(stderr, "error: failed to create XRPC client\n"); - return 1; - } - - wf_oauth_resource_metadata resource = {0}; - wf_oauth_server_metadata server = {0}; - wf_status s = wf_oauth_discover(transport, service, &resource, &server); - if (s != WF_OK) { - fprintf(stderr, "error: OAuth discovery failed (status %d)\n", (int)s); - wf_oauth_resource_metadata_free(&resource); - wf_oauth_server_metadata_free(&server); - wf_xrpc_client_free(transport); - return 1; - } - - printf("authorization server: %s\n", - server.authorization_endpoint ? server.authorization_endpoint : "?"); - if (server.issuer) { - printf("issuer: %s\n", server.issuer); - } - - if (!client_id) { - printf("\nOAuth discovery complete. Provide a client-id " - "(and redirect-uri) to begin the PAR flow:\n" - " wolfram oauth-login %s %s [redirect-uri] " - "[--state-file ]\n", - service, handle); - wf_oauth_resource_metadata_free(&resource); - wf_oauth_server_metadata_free(&server); - wf_xrpc_client_free(transport); - return 0; - } - - wf_oauth_client_metadata client = {0}; - s = wf_oauth_client_metadata_get(transport, client_id, &client); - if (s != WF_OK) { - fprintf(stderr, "error: client metadata fetch failed (status %d)\n", - (int)s); - wf_oauth_resource_metadata_free(&resource); - wf_oauth_server_metadata_free(&server); - wf_xrpc_client_free(transport); - return 1; - } - - wf_oauth_client_auth client_auth = { - .client_id = client_id, - .authorization_server_issuer = server.issuer, - .signing_key = NULL, - }; - - wf_oauth_authorization_begin_options opts = { - .redirect_uri = redirect_uri, - .scope = "atproto", - .login_hint = handle, - .now = time(NULL), - .state_ttl = 600, - }; - - wf_oauth_authorization_begin_result begin = {0}; - s = wf_oauth_authorization_begin(transport, &server, &client, &client_auth, - &opts, &begin); - wf_oauth_client_metadata_free(&client); - wf_oauth_resource_metadata_free(&resource); - wf_oauth_server_metadata_free(&server); - wf_xrpc_client_free(transport); - - if (s != WF_OK) { - fprintf(stderr, "error: authorization begin failed (status %d)\n", - (int)s); - wf_oauth_authorization_begin_result_free(&begin); - return 1; - } - - /* Persist the pending authorization state so oauth-callback can finish the - * flow. The state file holds the serialized PKCE/DPoP material; the printed - * `state` is the opaque CSRF token the callback must echo back. */ - char *def_state = oauth_default_state_path(); - const char *sf = state_file ? state_file : def_state; - if (begin.state_json) { - if (write_text_file(sf, begin.state_json) == 0) - printf("\npending state saved to %s\n", sf); - else - fprintf(stderr, "warning: failed to write state file '%s'\n", sf); - } - free(def_state); - - printf("\nOpen this URL in your browser to authorize:\n%s\n", - begin.authorization_url ? begin.authorization_url : "(none)"); - printf("\nstate: %s\n", begin.state ? begin.state : "(none)"); - printf( - "\nAfter authorizing, run:\n" - " wolfram oauth-callback %s --url \"\" --state %s%s%s\n", - service, begin.state ? begin.state : "", - state_file ? " --state-file " : "", state_file ? state_file : ""); - - wf_oauth_authorization_begin_result_free(&begin); - return 0; -} - /* wolfram block */ static int cmd_block(int argc, char **argv) { if (argc < 5) { diff --git a/src/cli/main_internal.h b/src/cli/main_internal.h new file mode 100644 index 0000000..1ce047b --- /dev/null +++ b/src/cli/main_internal.h @@ -0,0 +1,45 @@ +#ifndef WOLFRAM_CLI_MAIN_INTERNAL_H +#define WOLFRAM_CLI_MAIN_INTERNAL_H + +/* Cross-cutting helpers `cmd_*` subcommand handlers share, split out of + * main.c so a subcommand group split into its own file (e.g. cli_oauth.c) + * can still reach them. Not part of any installed API -- this is the CLI + * demo program, not the SDK. */ + +#include "wolfram/agent.h" + +#include + +/* Print the full `wolfram ...` usage summary to `out`. */ +void usage_stream(FILE *out); + +/* Print usage to stdout and return the CLI's "printed usage, no error" + * exit code (0). */ +int usage_exit(void); + +/* Resolve `actor` (a handle or DID) to its DID via + * com.atproto.identity.resolveHandle when it is not already a DID. + * Heap-allocated; caller frees *out_did. */ +wf_status resolve_actor_to_did(wf_agent *agent, const char *actor, + char **out_did); + +/* Resolve an at:// post URI to its record CID via getPostThread. Heap + * -allocated; caller frees *out_cid. */ +wf_status resolve_post_cid(wf_agent *agent, const char *at_uri, char **out_cid); + +/* Resolve the CID and thread-root URI/CID needed to build a reply's + * `reply.parent`/`reply.root` refs. Heap-allocated; caller frees each + * out param. */ +wf_status resolve_post_for_reply(wf_agent *agent, const char *at_uri, + char **out_cid, char **out_root_uri, + char **out_root_cid); + +/* Common tail for a `cmd_*` handler that just made one agent call and wants + * to print `res` (or an error) and return the process's exit code. */ +int finish_agent_response(wf_agent *agent, wf_status s, wf_response *res); + +/* Read the entire text file `path` into a heap string (NUL terminated). + * Returns NULL on failure. Caller frees. */ +char *read_text_file(const char *path); + +#endif /* WOLFRAM_CLI_MAIN_INTERNAL_H */