Changelog
spdgt.auth 3.0.0
Breaking change.
api_delete()returns the response instead of parsing it, the same asapi_get(),api_post()andapi_patch(). Pipe it intoparse_json2list()for the list it used to return. It was the only request function that parsed inside itself, and that is what broke itsdry_run: a dry run returns the request that would have been sent, not a response, and parsing that list stopped with “respmust be an HTTP response object, not a list”.api_delete(dry_run = TRUE)therefore always failed, in every package that passesdry_runthrough – the preview you most want before a destructive call. It now returns the dry-run request.Callers that use the parsed result need to parse it themselves. spdgt.api.admin reads
resp$messageafter its deletes, which is nowNULLand drops “Deleted” from its messages rather than erroring, and umbel’sdelete_db()returns the response instead of a list. spdgt.telem and spdgt.depredation.admin never used the parsed result, so they need no change and theirdry_runnow works.add_api()records the package that called it. It recorded spdgt.auth for every registration, because the package was worked out one call too deep, inside spdgt.auth itself. So the warning about an API registered without regional addresses told you to update spdgt.auth’sadd_api()call rather than the package that made it. And because that warning appears once per package, the first unmigrated package to load silenced the warning for all the others. Each package is now named and warned about separately, and a registration made from the console is attributed to “The registering package”.api_discover()suggests the closest endpoints when one matches nothing, since a miss is most often a typo:api_discover("counts", "/ipm/estimats")now saysClosest endpoint: '/api/ipm/estimates'. Nothing is suggested when no published path is near.api_discover()mentions an API whose description it could not read only when that matters. Asking for an endpoint that was found no longer warns about the rest of the realm, soapi_discover("counts", …)stops warning about the support site on every call. When nothing matches, the error names the unreadable API as where the endpoint may be. Listing a whole realm still warns, because that listing is incomplete.The response-patterns vignette said a delete may return an empty 204. It does not: the Laravel APIs’ shared
handleDestroy()answers 200 with amessage, and neither count-api nor telemetry-api overrides it.
spdgt.auth 2.0.3
-
parse_api_datetime()honours a UTC offset.strptime()stops once a format is satisfied and ignores what follows, so"2026-08-20T02:42:43-06:00"parsed as 02:42 UTC rather than 08:42 – a confident answer six hours wrong, and an expiry rather than a parse failure. Two shapes needed normalising before%zcould read them at all:Zis an offset that is not spelled like one, and%zrejects the colon ISO 8601 puts in+HH:MM. - Neither API sends an offset today – both send
Z, measured againstmarks.spdgt.com– so this is a guard rather than a repair. It is also the third appearance of this defect shape, the second inside a function written to fix the first: the original bug wasas.POSIXct()falling back to"%Y-%m-%d"and discarding the time, and ignoring a trailing offset is the same mechanism one layer up. - Fixed in base R rather than by taking a date dependency:
rules/r-packages.mdasks foundation packages to stay light, and this parses one scalar, so neither vectorisation nor regex removal argues for more.
spdgt.auth 2.0.2
- A test named the timezone
US/Eastern, a legacy alias the build image’s tzdata does not carry.as.POSIXct()answers an unknown zone with UTC instead of erroring, so the test passed on a developer machine and asserted the wrong instant in CI. It now usesAmerica/New_York, and a companion test fails loudly if that zone ever goes missing too. - Declares
Config/spdgt/r-minor: 4.5. Nothing else records the R a package is developed under –Depends: R (>= 4.1.0)is a floor for consumers, not a statement of what this builds on. The build gate warns on an undeclared package and fails from 2026-11-05; this was the last package in the sight chain without it.
spdgt.auth 2.0.1
-
add_api()no longer fails when the API is already registered. 2.0.0 compared the registered address with the requested one using==, which compares element by element once an address names its regions, soif()received a condition of length two and stopped with “the condition has length > 1”. spdgt.auth registerscountsitself, so every downstream package reached that comparison and failed to load: installingspdgt.core, and anything depending on it, stopped working. Registering an API that is already registered with the same addresses succeeds silently again, and registering it with different ones warns and keeps the existing registration, as before.
spdgt.auth 2.0.0
Every API now declares one address per region when it is registered, instead of spdgt.auth deriving a regional hostname from the US one. A package registers
c(us = ..., ca = ...), orc(all = ...)for a service deployed once and shared by every region. The region picks between them when the URL is read, so it still does not matter whether the region was set before or after the package loaded.Required change for package authors. Registering a single unnamed URL is the older form. It still works in the
"us"region, with one warning per package per session, and is an error in any other region: the address there is genuinely unknown, and quietly falling back to the US host would send the request out of the country and succeed. Both messages name the package that must be updated, because whoever sees them usually cannot fix it.add_api()no longer guesses. Hostnames it could not previously derive an address for — the plumber APIs on*.run.app, alocalhostdevelopment server — stop being special cases: they declarec(all = ...)and resolve to it everywhere.
spdgt.auth 1.3.0
Missing values now mean the same thing in every request body.
NAclears the field, a column left out of the body leaves that field unchanged, andNaNorInfstops the request with an error naming the fields responsible. Previously the result depended on the column’s type and on which function you called: a numericNAwas sent as the text"NA"and rejected, a characterNAcleared the field, and in a multi-row update anNAwas dropped from the request altogether — so the API left that field alone, returned success, and the change silently did not happen. Seevignette("missing-values").Behavior change. If you relied on
NAmeaning “leave this field alone” inapi_patch_multi(), it now means “clear this field”. Select the columns you intend to write before sending, and the result is the same as before. Clearing a field the API requires now fails with a 422 rather than passing unnoticed.Records written with
api_post_df()could store the literal text"NA"where a value was missing, because every missing value in that path was sent as that string and a text column accepts it. New writes sendnull; existing records with"NA"in them are worth finding and clearing.report_bug()files a support ticket from your R session and returns a link to follow. You supply the code that went wrong and what you expected to happen; the R version, the packages you have loaded, who you are and which project you are in are all filled in for you.
spdgt.auth 1.2.4
- Restores the documentation site at
https://docs.spdgt.com/auth, which had been returning 404 since 26 August. Design notes had been committed inside the directory the site is generated into, and the site builder refuses to write to a directory it did not create, so each build published those notes in place of the site.
spdgt.auth 1.2.3
-
auth_status()no longer reports a working session as signed out. It decided by whether it knew your email address, but a token handed over directly — which is how Plumber APIs pass one along — authenticates without ever looking up who you are. Such a session made successful calls whileauth_status()said “Not authenticated” and listed no realms at all, which is the opposite of what the one diagnostic for auth problems should do. It now reports what each realm actually holds, and where the user has not been looked up it says so and points atauth_me()rather than guessing or quietly making a request of its own.
spdgt.auth 1.2.1
Writing a single record now works whether you pass a tibble or a data frame. A base data frame was sent as a one-element array rather than an object, which the API accepts with a success response while writing nothing — so
telem_update_*and similar calls appeared to succeed and silently did nothing. The two call shapes look identical in R, and now behave identically. A data frame with more than one row gets the same clear error a tibble does.Changing region no longer drags the previous region’s project along. Signing in to the US on one project and then switching
SPDGT_REGIONtocamade the next sign-in try to force the Canadian session onto the US project, which failed with a confusing warning about a project the Canadian server has never heard of. Projects belong to the deployment that holds them, so one recorded in another region is no longer treated as current.Trying to switch to a project from another region says so, rather than reporting it as a missing project or a permissions problem.
auth_status()no longer errors after signing in. It failed with “invalid ‘trim’ argument” when reporting a realm whose token came straight from a browser sign-in, because the OAuth library records the expiry as a plain number while the API’s own timestamps arrive as dates, and only one of the two can be formatted. Expiry times are now normalised when they are stored, so every reader gets the same kind of value.
spdgt.auth 1.2.0
One sign-in now covers every SpeedGoat API. Signing in for telemetry data and then needing a counts lookup previously meant authenticating a second time; it no longer does, and a refresh or a project switch applies everywhere at once.
Sessions can work with the Canadian deployment. If your projects are based in Canada, add
SPDGT_REGION=cato your.Renvironand restart R. Leave it unset for the United States. A few services have no Canadian deployment yet, and you will be warned once per session if a request would leave the country.When a request fails, the message now comes from the API. Failures were previously reported as a token-refresh problem whatever the real cause, which sent people looking in the wrong place.
New
api_url()shows the address an API resolves to — the way to confirm a region took effect. It warns once per session when an API has no address in your region, which means that request leaves the country.auth_login()now says plainly when a session has no project, and what to do about it. Some APIs refuse a session without one. The project is chosen at sign-in and cannot be added afterwards, so the fix isauth_logout()followed by any API call, which prompts you again. This affects administrator accounts; ordinary accounts are assigned a project and never see it.auth_status()shows the project abbreviation and the region, reports expiry times in UTC so they can be compared, and names the realm a shared sign-in came from.Changing region mid-session signs you out rather than sending the previous region’s credentials to the new one.
auth_switch_project()no longer reports success when nothing changed. A mistyped abbreviation was accepted silently, leaving the session on its previous project while the message said otherwise — so work could be done against the wrong project without any sign. It now confirms the project with the API and errors if the switch did not take. If you have used it with an abbreviation you are not certain of, it is worth checking which project that work landed in.auth_use_token()acceptsrefresh_tokenandauth_type, so a forwarded token keeps the lifetime and refresh ability of the one it came from instead of expiring after an hour with no way to renew. Relevant if you forward tokens from a Plumber API.Large responses with included relationships parse dramatically faster. Splitting a nested column into one tibble per row was done a row at a time; on a 5,000-row page with one include that step alone took 0.32s, and it now takes 0.004s. A 50,000-row telemetry pull with two includes was spending several seconds in the client before you saw anything.
Requests are retried when the server says it did not process them. Previously only rate limiting (429) was retried; a 503, which is what an API returns while it is being deployed, failed immediately. A 500 is still not retried, because the request may have been applied and repeating a write would duplicate it.
A dropped connection during token refresh reports a refresh failure rather than a bare network error, and refresh requests now time out instead of hanging.
Signing in makes fewer requests. Using a credential already in hand no longer re-checks the project, and an API-key sign-in asks for the project on the same call that validates the key rather than a second one.
New
spdgt_setup()prints the settings needed to install SpeedGoat packages and to choose a region, and offers to open the files they belong in.
spdgt.auth 1.1.0.19
New
api_discover()shows what an API can do. Pass a realm to list its endpoints and the methods each allows, an endpoint to see the filters, includes, sorts and body fields it accepts, or a fragment to search. It reads what each API publishes, so it cannot go out of date, and it needs no sign-in. A realm covers every API registered to it, soapi_discover("counts")returns endpoints from counts, sightability and sdm together.api_discover()reportsrequiredas the API documents it. That is written by hand alongside the rules the API actually enforces and can disagree with them, so treat it as a guide rather than a guarantee.api_discover()warns and returns what it can when one API in a realm cannot describe itself, rather than failing the whole call.api_discover()looks among the packages you have installed, so an API belonging to a package you do not have does not appear. One visible side effect:auth_status()may afterwards list realms you never loaded. Nothing is signed in by this.
spdgt.auth 1.1.0.18
Fixed token expiry times being read as midnight, which could make
auth_status()report a valid session as expired.Vignette examples now include
library(spdgt.auth), and are no longer extracted into script files that could not run. As printed, the code they showed would have failed.
spdgt.auth 1.1.0.16
- Removed a
public/directory that was shipping inside the built package. It was residue from the JS worm rather than a package asset, and nothing in it ran..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|>.