Changelog
spdgt.auth 1.1.0.19
New
api_discover()answers what you can do with an API: called with a realm it lists every endpoint of every API in that realm and the methods each allows, called with an exact endpoint it lists per method the body fields you send when writing and the filters, includes, appends, sorts and pagination you can read with, and called with a fragment it searches — count-api alone publishes 234 endpoints, so finding one beats scrolling. It reads the description each API publishes rather than carrying a hard-coded list, so it cannot drift, and needs no token: discovery works before you sign in. Realms cover every API registered to them, which is whyapi_discover("counts")returns 252 endpoints across count-api, sightability and sdm rather than count-api’s 234.realmis required, and also accepts a single API’s name; because each primary API is named after its realm, the realm wins and returns the superset.api_discover()finds each API’s description by trying the locations in use rather than assuming one. The Laravel APIs serve/docs?api-docs.jsonwith every path under/api; the plumber services (sightability, sdm) serve/openapi.jsonwith paths at the root, so the previous single URL and the hardcoded/api/prefix could not see them at all. Candidates are ordered by what the base URL hints at, the first reply that actually parses as a description wins — a status code cannot decide it, since the sightability service answers 401 for any path it does not recognise — and the winning URL is cached per API.api_discover()finds a realm’s APIs from the packages installed on this machine, loading each namespace withrequireNamespace()and never attaching it, so.onLoadregisters the APIs and URLs each package owns and your search path is untouched. No addresses are stored inspdgt.auth, so none can go stale, and a package you have not installed simply does not appear — its endpoints are ones you could not call anyway. The visible side effect is thatauth_status()may afterwards list realms you never loaded; nothing is authenticated.api_discover()output carries anapicolumn in both modes, and anendpointcolumn when reporting one endpoint’s parameters, so a row always says where it came from. The survey column naming methods ismethodsrather thanactions, to stop it reading as a plural of the per-methodactioncolumn.api_discover()labels a parameter outside api-skeleton’s vocabulary by where the description says it goes, usually"query", rather than"other". Nothing outside Laravel speaks that vocabulary: plumber declares even a POST’s inputs as query parameters, so every field ofPOST /fitlanded in"other", which told the reader nothing.api_discover()warns and returns the rest when one API in a realm cannot describe itself, rather than failing the call — one dead satellite should not hide a realm. Every API failing is still an error, since an empty tibble reads as “this realm has no endpoints”.api_discover()reportsrequiredfrom the API’s own description, which is hand-written alongside the validation rules and can disagree with them.POST /api/individualsis the current example: its description marksproject_idrequired where the rule issometimes|nullable, and lists aspeciesfield that does not exist. The documentation says so, and a required entry that is not a real field is dropped rather than offered as one.
spdgt.auth 1.1.0.18
Fixed silent truncation of API timestamps. Both APIs report
expires_atas ISO 8601 with aTseparator, microseconds and aZsuffix ("2026-08-20T02:42:43.000000Z", measured againstmarks.spdgt.com), and none ofas.POSIXct()’s default formats match that shape: it fell through to"%Y-%m-%d", which matched the leading date and discarded the time, so a token expiring at 02:42 was stored as midnight.auth_status()is the only reader of that value and compares it withSys.time(), so it could report a live token as expired. The new internal helperparse_api_datetime()parses both shapes the APIs send and returnsNULLrather thanNAfor an absent or unparseable timestamp, becauseNAwould break that comparison. Used byrefresh_oauth_token()andauth_switch_project().Two test fixtures could not have caught the above and now use the measured format: one carried a space-separated timestamp the APIs do not send, the other a midnight expiry that a truncating parser reproduces exactly.
Vignette chunks are now marked
purl = FALSE. Every chunk was alreadyeval = FALSE, but that governs evaluation when weaving, not inclusion when tangling — so all 56 chunks were written intoinst/doc/*.Ras scripts that cannot run, since no vignette attached the package.tools::checkVignettes(tangle = TRUE)sourced them and failed with “could not find function api_get”. Setting the option inknitr::opts_chunk$set()does not work: tangling reads chunk headers without evaluating the setup chunk, so it has to be on each header.R CMD checkwas never affected — it re-weaves rather than sourcing the tangle — and remains clean.Vignettes now show
library(spdgt.auth), which none of them did. The code they document could not have run as printed.
spdgt.auth 1.1.0.17
refresh_oauth_token()now usesrlang::is_interactive()instead of baseinteractive(). Behaviour is unchanged in production —rlang::is_interactive()falls back tointeractive()— but the recursion guard and the interactive re-login path were previously impossible to test: baseinteractive()is a primitive, and mocking it withlocal_mocked_bindings(.package = "base")does not reach the package namespace underdevtools::test(). Both branches are now covered.Tests for the OAuth refresh workflow now encode the statuses
/api/refreshactually returns, measured against count-api: a rejected refresh token gives 500 with{"message": "Server Error"}, while an empty, missing, or misnamedrefresh_tokenfield gives 422 with a Laravel validation envelope. The endpoint never returns 401, which the existing non-interactive test had assumed. Also covered: refresh-token rotation, and that a 422 validation envelope is not mistaken for a token payload.
spdgt.auth 1.1.0.16
- Removed the
public/directory, which was residue from the JS worm rather than a package asset — the malicious font deleted in4556af4was staged there, and the directory shipped in the built package..vscode/is now gitignored so the folder the worm used to execute cannot be committed again.
spdgt.auth 1.1.0.15
refresh_auth()now refreshes OAuth sessions correctly. It read the access token from the top level of count-api’s/api/refreshresponse, which nests it underdata, so a refresh reported success and the next request then failed with “No token available for realm”. OIDC refresh reads a different endpoint and was never affected.refresh_auth()no longer reports a successful refresh unless the response actually contained a token, and a realm is no longer marked as authenticated when it holds none. Together these turned a failed refresh into a contradictory pair of messages instead of one clear error.The authentication paths now read responses through
parse_json2list()rather than callinghttr2::resp_body_json()directly, and passcheck_type = FALSE, so a response whose body is valid JSON behind a mislabelled content type no longer fails. Genuinely malformed bodies still error.
spdgt.auth 1.1.0.14
Added telemetry realm configuration.
auth_login(realm = "telemetry")now authenticates directly against telemetry-api via OAuth, instead of copying a count-api token that would fail on telemetry-api. Auth dispatch is NULL-safe for realms without OIDC or API key env vars.refresh_auth()now handlesauth_type = "api_key"with a clear error message (“API key was rejected by the server”) instead of the misleading “Unknown authentication type for refresh”.auth_url()is deprecated. Useadd_api()to register API endpoints with their correct URLs instead.Removed legacy counts-hardcoded methods from
APIAuthState(set_api_token(),get_api_token(),forward_token(),set_expiration(),get_expiration(),get_refresh_token(),set_refresh_token(),get_auth_type(),set_auth_type(),set_is_auth(), and backward-compatible URL getters). Use the realm-aware equivalents (get_realm_token(),set_realm_token(), etc.) instead.set_api_key()is deprecated. Set theSPDGT_API_KEYenvironment variable in.Renvironinstead. Thekeyringpackage is no longer a dependency.
spdgt.auth 1.1.0.13
-
auth_login()now checks for API key credentials (environment variable and system keyring) before falling back to OAuth in interactive sessions. Previously, interactive sessions always used OAuth regardless of whether an API key was configured.
spdgt.auth 1.1.0.12
api_post(),api_post_multi(), andapi_post_df()now abort whenadd_project_id = TRUEbut no project is assigned to the authenticated user, instead of silently injectingNULLinto the request body. All three auth flows (authenticate_api_key,authenticate_oidc,authenticate_oauth) now warn at login when the server returns no project assignment.auth_login()now errors with an informative message when no authentication method is available in a non-interactive session, instead of silently returningNULL.auth_logout()andauth_me()now validate therealmargument and error early for unknown realms.parse_json2tibble()now errors when the requested data element is missing from the response, instead of silently returningNULL.parse_json2tibble(null_action=)is deprecated. The parameter was a no-op; tibble columns are neverNULL.parse_url()now validates that the export response contains all required fields before accessing them.type_convert()now warns when the API response has no schema andvalidate = TRUEwas requested, returning data un-validated instead of silently skipping.type_fun()now falls back toidentitywith a warning for unrecognized schema types (e.g., “date”, “datetime”, “decimal”) instead of crashing.auth_url()now warns when called with a non-counts realm, as URL overrides are currently only supported for the counts realm.format_timestamp()now acceptsPOSIXltvalues in addition toPOSIXctand character.Fixed OIDC token refresh to correctly send the request body as JSON and to update the correct realm-specific environment variable instead of always writing to the counts realm’s env var.
Fixed OAuth authentication to abort with a clear error when the
/meverification call fails, instead of silently returning with no credentials set.Removed unused internal helper
null2na().api_body_multi()now errors when passed a zero-row tibble instead of sending an empty request.api_patch()now validates thebodyargument:NULLis rejected with a clear error, and list bodies are accepted in addition to data frames.api_patch_multi()now merges theidvector into the request body for batch PATCH requests, matching the server’s expected format for/multipleendpoints.api_perform()now treats HTTP 202 and 204 as success, fixingapi_delete()which was erroring on standard 204 No Content responses.api_post(),api_post_multi(), andapi_post_df()now abort whenadd_user_id = TRUEbut no user is authenticated, instead of silently injectingNULL.spdgt_select()now requires a tibble (tbl_df) input; plain data frames and other objects are returned unchanged.Moved
lifecycleandR6toImportsand added%||%toimportFrom(rlang)to fix undeclared dependency issues.
spdgt.auth 1.1.0.11
api_filters()andapi_parameters()now silently dropNULLentries from their input lists viacompact_list(), so callers can passlist(a = val, b = NULL)withoutNULLvalues leaking into query parameters.New exported utility functions consolidate helpers previously duplicated across domain packages:
check_enum(),check_required_cols(),compact_list(),format_timestamp(), andparse_multi2tibble().Multi-realm token authentication allows independent per-realm token storage for APIs that have separate databases and token stores. A realm is a named group of APIs sharing one token authority (e.g.,
"counts","telemetry"). All existing behavior defaults to the"counts"realm for full backward compatibility.add_api()gains arealmparameter. Whenrealm = NULL(default), a new realm is created using the API name. When a realm name is provided, the API joins that existing realm and shares its token (e.g.,add_api("sightability", url, realm = "counts")).api_init()now tags each request withspdgt_api_nameso thatapi_perform()can automatically resolve the correct realm and use the appropriate token.api_perform()now performs per-realm token resolution. It checks the request for a realm tag, falls back to the API name mapping, and defaults to"counts". Token refresh is also per-realm.api_perform()now aborts with a structured error on any non-2xx final response, including the HTTP status code, the server’s error message, and field-level validation errors. Previously, some error statuses (e.g., 401 with a valid token, 403) were silently returned to the caller, and validation errors from the$errorsfield were lost.auth_check()gains arealmparameter to validate tokens against the correct realm’s/api/meendpoint.auth_forward_token()gains arealmparameter to forward tokens to a specific realm rather than always using the global state.auth_login()gains arealmparameter. Each realm uses its own OAuth client credentials, API key environment variable, and OIDC token, configured in an internal registry.auth_logout()gains arealmparameter. WhenNULL(default), all realms are logged out. When a specific realm is provided, only that realm is cleared.auth_me()gains arealmparameter to query a specific realm’s/api/meendpoint.set_api_key()gains arealmparameter to store API keys per-realm in the system keyring (e.g.,set_api_key(key, realm = "telemetry")). Keys previously stored under"spdgt"are still found by the legacy fallback.swagger_login()gains arealmparameter to authenticate against a specific realm’s login endpoint.Switched from magrittr pipe (
%>%) to the native pipe operator (|>) throughout the package. The%>%re-export has been removed. Downstream packages that imported%>%from spdgt.auth should import it from magrittr directly or switch to|>.