@foldrun/core 9.4.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/secrets.ts CHANGED
@@ -26,6 +26,7 @@ import { platform } from "./platform.ts";
26
26
  import { fetchUntrusted } from "./untrusted-fetch.ts";
27
27
  import { oauthTokenRequest, readTokenResponse, tokenAuthProblem, type TokenAuth } from "./oauth-token.ts";
28
28
  import { isSecureUrl } from "./oauth-discovery.ts";
29
+ import type { AssertionAlg } from "./oauth-assertion.ts";
29
30
 
30
31
  /**
31
32
  * GCM's auth tag, pinned to its full 16 bytes at every cipher call. Without
@@ -945,6 +946,13 @@ export interface OAuth2Config {
945
946
  /** The provider's revocation endpoint (RFC 7009): deleting or replacing
946
947
  * this secret revokes its tokens there (revokeOAuth2). */
947
948
  revocation_url?: string;
949
+ /** token_auth private_key_jwt: the private key (PEM or JWK) the client
950
+ * signs its assertions with, sealed with the rest of the recipe; its kid,
951
+ * alg, and certificate when the provider names keys by it (Entra). */
952
+ client_assertion_key?: string;
953
+ client_assertion_kid?: string;
954
+ client_assertion_alg?: AssertionAlg;
955
+ client_assertion_cert?: string;
948
956
  }
949
957
 
950
958
  const OAUTH2_PREFIX = "@oauth2 ";
@@ -970,8 +978,8 @@ export function setOAuth2Secret(
970
978
  }
971
979
  // A public client (PKCE, no secret) refreshes with its client id alone;
972
980
  // client credentials are nothing without the secret.
973
- if (config.grant_type === "client_credentials" && !config.client_secret?.trim()) {
974
- throw new Error("oauth2 secret needs client_secret for client_credentials");
981
+ if (config.grant_type === "client_credentials" && !config.client_secret?.trim() && config.token_auth !== "private_key_jwt") {
982
+ throw new Error("oauth2 secret needs client_secret for client_credentials (or token_auth private_key_jwt with a key)");
975
983
  }
976
984
  if (config.grant_type !== "client_credentials" && !config.refresh_token?.trim()) {
977
985
  throw new Error("oauth2 secret needs refresh_token (or grant_type: client_credentials)");
@@ -1068,11 +1076,14 @@ async function exchange(config: OAuth2Config, cacheKey: string): Promise<OAuthLi
1068
1076
  const flight = (async () => {
1069
1077
  // One builder for every token request (oauth-token.ts): the client's
1070
1078
  // identity goes where its token_auth says, and an `extra` cannot set it.
1071
- const request = oauthTokenRequest(config, {
1079
+ // Built per try: a private_key_jwt assertion's jti is good once, so a
1080
+ // retry after a lost answer must carry a new one (Codex review).
1081
+ const build = () => oauthTokenRequest(config, {
1072
1082
  grant_type: config.grant_type ?? "refresh_token",
1073
1083
  ...(config.grant_type === "client_credentials" ? {} : { refresh_token: config.refresh_token! }),
1074
1084
  ...(config.extra ?? {}),
1075
- });
1085
+ }, { audience: config.token_url });
1086
+ build(); // a config that cannot make a request says so before any network
1076
1087
  // A network blip is retried; a provider's answer is not. "fetch failed"
1077
1088
  // (a reset, a DNS hiccup, a timeout) failed whole steps on the first try
1078
1089
  // — gbp-desk's Thursday posts, 7 Oct 2026. A 4xx (revoked, wrong client)
@@ -1083,6 +1094,7 @@ async function exchange(config: OAuth2Config, cacheKey: string): Promise<OAuthLi
1083
1094
  for (let attempt = 0; attempt < OAUTH_REFRESH_TRIES; attempt++) {
1084
1095
  if (attempt) await new Promise((r) => setTimeout(r, oauthRetryDelayMs(attempt)));
1085
1096
  try {
1097
+ const request = build();
1086
1098
  res = await fetchUntrusted(config.token_url, {
1087
1099
  method: "POST",
1088
1100
  headers: request.headers,
@@ -1231,7 +1243,7 @@ export async function revokeOAuth2Value(
1231
1243
  const out: OAuthRevokeResult = { revoked: [] };
1232
1244
  for (const [hint, token] of tokens) {
1233
1245
  try {
1234
- const request = oauthTokenRequest(config, { token, token_type_hint: hint });
1246
+ const request = oauthTokenRequest(config, { token, token_type_hint: hint }, { audience: config.token_url });
1235
1247
  const res = await fetcher(config.revocation_url, { method: "POST", headers: request.headers, body: request.body, redirect: "error", signal: AbortSignal.timeout(10_000) });
1236
1248
  if (res.ok) {
1237
1249
  await res.body?.cancel().catch(() => {});