@zackbart/connecta 0.28.0 → 0.29.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/CHANGELOG.md +961 -5
- package/README.md +62 -39
- package/bin/connecta.mjs +110 -139
- package/bin/meta-tool-names.d.mts +2 -0
- package/bin/meta-tool-names.mjs +9 -0
- package/bin/version.mjs +3 -0
- package/dist/access-tokens.js +53 -63
- package/dist/activity-request.js +8 -0
- package/dist/activity.d.ts +76 -42
- package/dist/activity.js +110 -42
- package/dist/auth/clerk-sdk/client.d.ts +65 -0
- package/dist/auth/clerk-sdk/client.js +5 -0
- package/dist/auth/clerk-sdk/generated.js +1845 -0
- package/dist/auth/clerk-transport.js +19 -0
- package/dist/auth/clerk.d.ts +17 -7
- package/dist/auth/clerk.js +189 -107
- package/dist/auth/cloudflare-access.js +25 -26
- package/dist/auth/downstream-client-metadata.d.ts +29 -0
- package/dist/auth/downstream-client-metadata.js +133 -0
- package/dist/auth/downstream-oauth.js +1632 -1509
- package/dist/auth/downstream-revocation.js +106 -0
- package/dist/auth/oauth-token-response.js +231 -0
- package/dist/auth/oauth-v2-migration.js +116 -0
- package/dist/auth/static-oauth.js +526 -0
- package/dist/auth-elicitation.js +283 -0
- package/dist/branding.d.ts +8 -16
- package/dist/branding.js +26 -59
- package/dist/byte-read-response.js +39 -0
- package/dist/call-admission.js +12 -16
- package/dist/catalog-cache.js +739 -0
- package/dist/catalog-drift.d.ts +50 -15
- package/dist/catalog-drift.js +365 -128
- package/dist/catalog-fingerprint.js +2 -4
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-limits.js +2 -2
- package/dist/catalog-service.js +368 -224
- package/dist/catalog.js +43 -84
- package/dist/concurrency.js +3 -4
- package/dist/config-defaults.d.ts +39 -0
- package/dist/config-defaults.js +46 -0
- package/dist/config-schema.d.ts +181 -0
- package/dist/config-schema.js +435 -0
- package/dist/config-value-sources.js +102 -0
- package/dist/config.d.ts +631 -0
- package/dist/config.js +503 -0
- package/dist/connector-access.d.ts +1 -0
- package/dist/connector-access.js +13 -15
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +15 -6
- package/dist/connector-scope.js +1 -1
- package/dist/connectors/api-connector.d.ts +220 -0
- package/dist/connectors/api-connector.js +214 -0
- package/dist/connectors/api.d.ts +8 -71
- package/dist/connectors/api.js +13 -99
- package/dist/connectors/guarded-fetch.d.ts +144 -0
- package/dist/connectors/guarded-fetch.js +108 -25
- package/dist/connectors/negotiation-cache.js +46 -0
- package/dist/connectors/option-shapes.js +27 -0
- package/dist/connectors/remote-mcp.d.ts +30 -4
- package/dist/connectors/remote-mcp.js +1780 -351
- package/dist/connectors/resource-uri.js +335 -0
- package/dist/credential-contract.d.ts +10 -0
- package/dist/credential-rules.js +1 -3
- package/dist/credential-url.js +108 -0
- package/dist/credentials.d.ts +6 -1
- package/dist/credentials.js +27 -18
- package/dist/d1.d.ts +130 -0
- package/dist/d1.js +290 -0
- package/dist/describe-config.d.ts +165 -0
- package/dist/describe-config.js +315 -0
- package/dist/described.js +106 -0
- package/dist/downstream-input-context.js +119 -0
- package/dist/downstream-input.js +323 -0
- package/dist/errors.d.ts +97 -20
- package/dist/errors.js +158 -80
- package/dist/execute.js +453 -305
- package/dist/executor-admission.d.ts +1 -0
- package/dist/executor-admission.js +11 -15
- package/dist/executor-contract.d.ts +20 -0
- package/dist/executor-contract.js +48 -0
- package/dist/executor-result.js +24 -9
- package/dist/executors/quickjs-child.js +3 -4
- package/dist/executors/quickjs-runtime.js +147 -59
- package/dist/executors/quickjs.js +29 -35
- package/dist/favicon.js +1 -2
- package/dist/fix-prompt.js +1 -3
- package/dist/guest-runtime.js +188 -0
- package/dist/guest-types.d.ts +176 -0
- package/dist/guest-types.js +1 -0
- package/dist/html-security.js +24 -0
- package/dist/identity.js +1 -3
- package/dist/inbound-credential.js +52 -0
- package/dist/index.d.ts +25 -257
- package/dist/index.js +65 -478
- package/dist/invocation-auth.js +69 -0
- package/dist/invocation.js +230 -73
- package/dist/json-schema.d.ts +1 -1
- package/dist/mcp-client-context.js +35 -0
- package/dist/mcp-result.js +11 -7
- package/dist/meta-output.js +76 -0
- package/dist/meta-tool-names.d.ts +2 -0
- package/dist/meta-tool-names.js +3 -0
- package/dist/meta-tools.js +285 -209
- package/dist/module-contracts.d.ts +8 -23
- package/dist/node.d.ts +0 -2
- package/dist/node.js +2 -2
- package/dist/oauth-callback-outcome.js +1 -3
- package/dist/oauth-handoff.js +117 -0
- package/dist/oauth-partition.js +17 -0
- package/dist/oauth-sealing.js +20 -0
- package/dist/operator-record.d.ts +137 -0
- package/dist/operator-record.js +420 -0
- package/dist/operator-ui/assets.js +31 -0
- package/dist/operator-ui/contract.d.ts +72 -0
- package/dist/operator-ui/contract.js +1 -0
- package/dist/operator-ui/fix-prompts.d.ts +18 -0
- package/dist/operator-ui/generated.js +5 -4
- package/dist/operator-ui/model.d.ts +12 -87
- package/dist/operator-ui/model.js +2 -9
- package/dist/operator-ui/setup-commands.js +1 -3
- package/dist/operator-ui/view.d.ts +323 -0
- package/dist/operator-ui/view.js +62 -189
- package/dist/page-styles.js +2 -3
- package/dist/program-source.js +14 -2
- package/dist/{exempt-writes.js → program-writes.js} +55 -18
- package/dist/provider.d.ts +142 -0
- package/dist/provider.js +187 -0
- package/dist/providers/_shared/google/result-size.js +57 -0
- package/dist/providers/_shared/google/service-account.d.ts +58 -0
- package/dist/providers/_shared/google/service-account.js +500 -0
- package/dist/providers/_shared/google/workspace.d.ts +276 -0
- package/dist/providers/_shared/google/workspace.js +518 -0
- package/dist/providers/basecamp/index.d.ts +43 -0
- package/dist/providers/basecamp/index.js +997 -0
- package/dist/providers/basecamp/skill.generated.js +9 -0
- package/dist/providers/breeze/index.d.ts +34 -0
- package/dist/providers/breeze/index.js +1446 -0
- package/dist/providers/breeze/skill.generated.js +9 -0
- package/dist/providers/ccb/index.d.ts +103 -0
- package/dist/providers/ccb/index.js +1725 -0
- package/dist/providers/ccb/skill.generated.js +9 -0
- package/dist/providers/{cloudflare.d.ts → cloudflare/index.d.ts} +8 -7
- package/dist/providers/{cloudflare.js → cloudflare/index.js} +171 -738
- package/dist/providers/cloudflare/skill.generated.js +10 -0
- package/dist/providers/docs/index.d.ts +54 -0
- package/dist/providers/docs/index.js +1237 -0
- package/dist/providers/docs/skill.generated.js +9 -0
- package/dist/providers/drive/index.d.ts +53 -0
- package/dist/providers/drive/index.js +1523 -0
- package/dist/providers/drive/skill.generated.js +9 -0
- package/dist/providers/forms/index.d.ts +54 -0
- package/dist/providers/forms/index.js +1217 -0
- package/dist/providers/forms/skill.generated.js +11 -0
- package/dist/providers/github/auth.d.ts +32 -0
- package/dist/providers/github/auth.js +361 -0
- package/dist/providers/github/hosted.js +218 -0
- package/dist/providers/github/index.d.ts +13 -0
- package/dist/providers/github/index.js +140 -0
- package/dist/providers/github/key.js +100 -0
- package/dist/providers/github/rest.js +321 -0
- package/dist/providers/github/scope.d.ts +29 -0
- package/dist/providers/github/scope.js +124 -0
- package/dist/providers/github/skill.generated.js +8 -0
- package/dist/providers/gmail/index.d.ts +51 -0
- package/dist/providers/gmail/index.js +1436 -0
- package/dist/providers/gmail/skill.generated.js +9 -0
- package/dist/providers/{linear.d.ts → linear/index.d.ts} +11 -14
- package/dist/providers/linear/index.js +352 -0
- package/dist/providers/linear/skill.generated.js +8 -0
- package/dist/providers/{mixpanel.d.ts → mixpanel/index.d.ts} +5 -10
- package/dist/providers/mixpanel/index.js +416 -0
- package/dist/providers/mixpanel/skill.generated.js +9 -0
- package/dist/providers/{notion.d.ts → notion/index.d.ts} +8 -8
- package/dist/providers/{notion.js → notion/index.js} +226 -239
- package/dist/providers/notion/skill.generated.js +10 -0
- package/dist/providers/overflow/index.d.ts +60 -0
- package/dist/providers/overflow/index.js +1535 -0
- package/dist/providers/overflow/skill.generated.js +8 -0
- package/dist/providers/planning-center/index.d.ts +59 -0
- package/dist/providers/planning-center/index.js +2199 -0
- package/dist/providers/planning-center/skill.generated.js +10 -0
- package/dist/providers/{revenuecat.d.ts → revenuecat/index.d.ts} +5 -10
- package/dist/providers/revenuecat/index.js +557 -0
- package/dist/providers/revenuecat/skill.generated.js +11 -0
- package/dist/providers/sheets/index.d.ts +54 -0
- package/dist/providers/sheets/index.js +1237 -0
- package/dist/providers/sheets/skill.generated.js +10 -0
- package/dist/providers/slides/index.d.ts +53 -0
- package/dist/providers/slides/index.js +2645 -0
- package/dist/providers/slides/skill.generated.js +9 -0
- package/dist/providers/{stripe.d.ts → stripe/index.d.ts} +5 -12
- package/dist/providers/stripe/index.js +266 -0
- package/dist/providers/stripe/skill.generated.js +11 -0
- package/dist/providers/tithely/index.d.ts +49 -0
- package/dist/providers/tithely/index.js +1003 -0
- package/dist/providers/tithely/skill.generated.js +8 -0
- package/dist/providers/{vercel.d.ts → vercel/index.d.ts} +8 -8
- package/dist/providers/vercel/index.js +1038 -0
- package/dist/providers/vercel/mcp-ownership.js +212 -0
- package/dist/providers/vercel/skill.generated.js +11 -0
- package/dist/registry.d.ts +108 -171
- package/dist/registry.js +513 -925
- package/dist/request-state.js +21 -0
- package/dist/resource-read.js +100 -0
- package/dist/result-shapes.js +17 -49
- package/dist/retired-toolkits.js +1 -1
- package/dist/routes/access-tokens.js +3 -6
- package/dist/routes/activity-context.js +21 -0
- package/dist/routes/activity-disclosure.js +40 -0
- package/dist/routes/activity.js +59 -21
- package/dist/routes/actor-labels.js +3 -50
- package/dist/routes/connect.js +221 -0
- package/dist/routes/credentials.js +15 -27
- package/dist/routes/mcp.js +356 -130
- package/dist/routes/oauth-client-metadata.js +15 -0
- package/dist/routes/oauth-management.js +45 -107
- package/dist/routes/oauth.js +136 -57
- package/dist/routes/operator.js +3 -3
- package/dist/routes/shared.d.ts +25 -64
- package/dist/routes/shared.js +160 -80
- package/dist/routes/skills.js +30 -0
- package/dist/routes/ui-config.js +225 -0
- package/dist/routes/ui-data.js +40 -35
- package/dist/routes/ui.js +66 -56
- package/dist/runtime/admission.js +4 -2
- package/dist/runtime/call-admission.js +3 -1
- package/dist/runtime/connector-scope.js +1 -4
- package/dist/runtime/run.js +10 -39
- package/dist/runtime/services.js +12 -117
- package/dist/runtime/shared-read.js +1 -3
- package/dist/runtime/storage.js +8 -1
- package/dist/sent-secrets.d.ts +52 -0
- package/dist/sent-secrets.js +474 -0
- package/dist/server.js +28 -71
- package/dist/skills.js +406 -174
- package/dist/sqlite.d.ts +48 -0
- package/dist/sqlite.js +166 -0
- package/dist/storage/keys.js +428 -0
- package/dist/storage/memory.js +7 -0
- package/dist/storage/sql.d.ts +66 -0
- package/dist/storage/sql.js +534 -0
- package/dist/timeout.js +4 -2
- package/dist/tool-name.js +17 -0
- package/dist/tool-safety.d.ts +9 -35
- package/dist/tool-safety.js +20 -40
- package/dist/types.d.ts +320 -54
- package/dist/types.js +0 -1
- package/dist/ui.d.ts +7 -19
- package/dist/ui.js +76 -90
- package/dist/url-safety.js +3 -6
- package/dist/usage-guide.js +137 -0
- package/dist/validate.d.ts +7 -0
- package/dist/validate.js +309 -47
- package/dist/version.d.ts +1 -7
- package/dist/version.js +3 -7
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +434 -0
- package/documentation/architecture.md +665 -230
- package/documentation/auth.md +962 -458
- package/documentation/code-mode.md +501 -312
- package/documentation/meta-tools.md +449 -200
- package/documentation/operator-tests.md +93 -0
- package/documentation/provider-migration-0.29.md +60 -0
- package/examples/worker/AGENTS.md +34 -12
- package/examples/worker/README.md +286 -255
- package/examples/worker/kv-to-d1.wrangler.jsonc +26 -0
- package/examples/worker/scripts/copy-kv-to-d1.mjs +162 -0
- package/examples/worker/src/connecta.config.ts +151 -0
- package/examples/worker/src/index.ts +14 -197
- package/examples/worker/wrangler.jsonc +15 -35
- package/package.json +125 -35
- package/templates/node/.dockerignore +1 -2
- package/templates/node/.env.example +20 -21
- package/templates/node/AGENTS.md +21 -17
- package/templates/node/Dockerfile +3 -5
- package/templates/node/README.md +133 -58
- package/templates/node/docker-compose.yml +11 -12
- package/templates/node/package.json +4 -2
- package/templates/node/src/connecta.config.ts +119 -0
- package/templates/node/src/index.ts +9 -154
- package/templates/node/src/provision-token.ts +17 -0
- package/AGENTS.md +0 -170
- package/dist/artifacts/connector.d.ts +0 -58
- package/dist/artifacts/connector.js +0 -730
- package/dist/artifacts/document.js +0 -86
- package/dist/artifacts/guide.js +0 -140
- package/dist/artifacts/html-scan.js +0 -329
- package/dist/artifacts/json.js +0 -78
- package/dist/artifacts/kv-store.d.ts +0 -20
- package/dist/artifacts/kv-store.js +0 -140
- package/dist/artifacts/markdown.js +0 -478
- package/dist/artifacts/module.d.ts +0 -37
- package/dist/artifacts/module.js +0 -66
- package/dist/artifacts/operations.d.ts +0 -197
- package/dist/artifacts/operations.js +0 -1002
- package/dist/artifacts/refresh.d.ts +0 -55
- package/dist/artifacts/refresh.js +0 -236
- package/dist/artifacts/routes.js +0 -206
- package/dist/artifacts/types.d.ts +0 -214
- package/dist/artifacts/types.js +0 -22
- package/dist/artifacts/validate.d.ts +0 -62
- package/dist/artifacts/validate.js +0 -651
- package/dist/artifacts.d.ts +0 -6
- package/dist/artifacts.js +0 -9
- package/dist/auth/bearer.d.ts +0 -11
- package/dist/auth/bearer.js +0 -50
- package/dist/providers/linear.js +0 -217
- package/dist/providers/mixpanel.js +0 -247
- package/dist/providers/revenuecat.js +0 -317
- package/dist/providers/stripe.js +0 -253
- package/dist/providers/vercel.js +0 -1253
- package/dist/storage/file.d.ts +0 -15
- package/dist/storage/file.js +0 -455
- package/ethos.md +0 -93
- package/examples/worker/src/cloudflare-kv.ts +0 -43
- package/examples/worker/src/d1-activity-row.ts +0 -155
- package/examples/worker/src/d1-activity.ts +0 -147
- package/examples/worker/src/d1-storage.ts +0 -138
- package/examples/worker/src/r2-artifact-blobs.ts +0 -29
- package/templates/node/src/file-activity.ts +0 -182
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Google service account acting for Workspace users through domain-wide
|
|
3
|
+
* delegation: the key, the signed JWT-bearer assertion, and the short-lived
|
|
4
|
+
* access tokens it buys. Shared by every Workspace product provider; never its
|
|
5
|
+
* own export.
|
|
6
|
+
*
|
|
7
|
+
* Why not the vault. One key serves every Workspace connector a deployment
|
|
8
|
+
* declares — Gmail, Drive, Docs — and it is a deployment secret in the same
|
|
9
|
+
* sense as an OAuth client secret: an environment variable or Worker secret,
|
|
10
|
+
* read at construction. A per-connector operator slot would ask a human to
|
|
11
|
+
* paste the same key six times and would still not say whose mailbox to open.
|
|
12
|
+
*
|
|
13
|
+
* Why no library. `google-auth-library` is Node-bound and the OAuth client in
|
|
14
|
+
* the MCP SDK is the wrong grant. RS256 over Web Crypto is a page of code, so
|
|
15
|
+
* the signing path runs unchanged on Node and Workers and the provider bundles
|
|
16
|
+
* pull in nothing new.
|
|
17
|
+
*
|
|
18
|
+
* Tokens live in memory only, keyed by service account, subject, and scope
|
|
19
|
+
* set, for at most the hour Google grants and never past a minute before
|
|
20
|
+
* expiry. Nothing here writes to storage, and nothing here logs: not the key,
|
|
21
|
+
* not an assertion, not a token. An error message names the service account
|
|
22
|
+
* and the scopes — both are configuration a reader needs to fix the grant —
|
|
23
|
+
* and never the material that proves it.
|
|
24
|
+
*/
|
|
25
|
+
import { guardedFetch, retryAfterMs } from "../../../connectors/guarded-fetch.js";
|
|
26
|
+
import { ConnectorCallError } from "../../../errors.js";
|
|
27
|
+
/** Google's OAuth 2.0 token endpoint, and the JWT audience it requires. */
|
|
28
|
+
const GOOGLE_TOKEN_ORIGIN = "https://oauth2.googleapis.com";
|
|
29
|
+
const GOOGLE_TOKEN_URL = `${GOOGLE_TOKEN_ORIGIN}/token`;
|
|
30
|
+
const JWT_BEARER_GRANT = "urn:ietf:params:oauth:grant-type:jwt-bearer";
|
|
31
|
+
/** Google refuses an assertion that lives longer than an hour. */
|
|
32
|
+
const ASSERTION_LIFETIME_SECONDS = 3600;
|
|
33
|
+
/** A cached token is replaced this long before Google says it expires. */
|
|
34
|
+
const REFRESH_MARGIN_MS = 60_000;
|
|
35
|
+
/**
|
|
36
|
+
* Distinct (account, subject, scopes) tokens held at once. Past it the oldest
|
|
37
|
+
* goes first: a deployment with more simultaneous delegated users than this
|
|
38
|
+
* re-mints, which costs a round trip, not correctness.
|
|
39
|
+
*/
|
|
40
|
+
const MAX_CACHED_TOKENS = 512;
|
|
41
|
+
/** A token response is a few hundred bytes; anything near this is not one. */
|
|
42
|
+
const TOKEN_RESPONSE_BYTES = 64 * 1024;
|
|
43
|
+
const PEM = /-----BEGIN PRIVATE KEY-----([\s\S]*?)-----END PRIVATE KEY-----/;
|
|
44
|
+
function decodedKey(owner, pem) {
|
|
45
|
+
// A key pasted into an environment variable often keeps the JSON escapes.
|
|
46
|
+
const normalized = pem.replace(/\\n/g, "\n").trim();
|
|
47
|
+
if (/-----BEGIN RSA PRIVATE KEY-----/.test(normalized)) {
|
|
48
|
+
throw new Error(`${owner} serviceAccount.privateKey is a PKCS#1 RSA key. Google's JSON keys carry PKCS#8 ("BEGIN PRIVATE KEY"); paste the private_key field of the downloaded JSON key.`);
|
|
49
|
+
}
|
|
50
|
+
const body = PEM.exec(normalized)?.[1]?.replace(/\s+/g, "");
|
|
51
|
+
if (!body || !/^[A-Za-z0-9+/]+={0,2}$/.test(body)) {
|
|
52
|
+
throw new Error(`${owner} serviceAccount.privateKey is not a PEM private key; paste the private_key field of the service account's JSON key.`);
|
|
53
|
+
}
|
|
54
|
+
let binary;
|
|
55
|
+
try {
|
|
56
|
+
binary = atob(body);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new Error(`${owner} serviceAccount.privateKey is not valid base64 inside its PEM armor.`);
|
|
60
|
+
}
|
|
61
|
+
const der = new Uint8Array(binary.length);
|
|
62
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
63
|
+
der[index] = binary.charCodeAt(index);
|
|
64
|
+
}
|
|
65
|
+
// A truncated paste, another file, or another algorithm is cheaper to refuse
|
|
66
|
+
// here than to discover on the first call — and a key that only fails at
|
|
67
|
+
// use can hide behind a token another key already minted.
|
|
68
|
+
const modulusBits = rsaPkcs8ModulusBits(der);
|
|
69
|
+
if (modulusBits === undefined) {
|
|
70
|
+
throw new Error(`${owner} serviceAccount.privateKey does not decode to a PKCS#8 RSA private key; paste the private_key field of the service account's JSON key.`);
|
|
71
|
+
}
|
|
72
|
+
if (modulusBits < MIN_RSA_BITS) {
|
|
73
|
+
throw new Error(`${owner} serviceAccount.privateKey is a ${modulusBits}-bit RSA key; Google signs service-account assertions with ${MIN_RSA_BITS}-bit keys or larger.`);
|
|
74
|
+
}
|
|
75
|
+
return der;
|
|
76
|
+
}
|
|
77
|
+
/** Google issues 2048-bit service-account keys; RS256 wants no less. */
|
|
78
|
+
const MIN_RSA_BITS = 2048;
|
|
79
|
+
/** rsaEncryption, 1.2.840.113549.1.1.1, as its DER content octets. */
|
|
80
|
+
const RSA_ENCRYPTION_OID = [0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01];
|
|
81
|
+
/** One DER tag-length-value at `offset`, or undefined if it does not fit. */
|
|
82
|
+
function tlv(der, offset, limit = der.length) {
|
|
83
|
+
if (offset + 2 > limit)
|
|
84
|
+
return undefined;
|
|
85
|
+
const tag = der[offset];
|
|
86
|
+
let length = der[offset + 1];
|
|
87
|
+
let start = offset + 2;
|
|
88
|
+
if (length & 0x80) {
|
|
89
|
+
const octets = length & 0x7f;
|
|
90
|
+
// Indefinite lengths are BER, not DER; four octets is 4 GiB.
|
|
91
|
+
if (octets === 0 || octets > 4 || start + octets > limit)
|
|
92
|
+
return undefined;
|
|
93
|
+
length = 0;
|
|
94
|
+
for (let index = 0; index < octets; index += 1)
|
|
95
|
+
length = length * 256 + der[start + index];
|
|
96
|
+
start += octets;
|
|
97
|
+
}
|
|
98
|
+
const end = start + length;
|
|
99
|
+
return end > limit ? undefined : { tag, start, end };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The modulus size of a PKCS#8 `PrivateKeyInfo` wrapping an RSA key, or
|
|
103
|
+
* undefined for anything else. Walks exactly the structure RFC 5208 and RFC
|
|
104
|
+
* 8017 define — version 0, the rsaEncryption algorithm identifier with NULL
|
|
105
|
+
* parameters, and an octet string holding exactly the nine integers of a
|
|
106
|
+
* two-prime `RSAPrivateKey` — and requires every structure to be consumed
|
|
107
|
+
* whole, so a well-formed key of another algorithm, a PKCS#1 body in PKCS#8
|
|
108
|
+
* armor, altered parameters, trailing bytes, and a truncated paste are all
|
|
109
|
+
* refused here rather than by Google on the first call.
|
|
110
|
+
*/
|
|
111
|
+
function rsaPkcs8ModulusBits(der) {
|
|
112
|
+
const info = tlv(der, 0);
|
|
113
|
+
if (!info || info.tag !== 0x30 || info.end !== der.length)
|
|
114
|
+
return undefined;
|
|
115
|
+
const version = tlv(der, info.start, info.end);
|
|
116
|
+
if (!version || version.tag !== 0x02 || version.end - version.start !== 1 || der[version.start] !== 0) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
const algorithm = tlv(der, version.end, info.end);
|
|
120
|
+
if (!algorithm || algorithm.tag !== 0x30)
|
|
121
|
+
return undefined;
|
|
122
|
+
const oid = tlv(der, algorithm.start, algorithm.end);
|
|
123
|
+
if (!oid ||
|
|
124
|
+
oid.tag !== 0x06 ||
|
|
125
|
+
oid.end - oid.start !== RSA_ENCRYPTION_OID.length ||
|
|
126
|
+
RSA_ENCRYPTION_OID.some((octet, index) => der[oid.start + index] !== octet)) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
// rsaEncryption's parameters are exactly NULL, and nothing follows them.
|
|
130
|
+
const parameters = tlv(der, oid.end, algorithm.end);
|
|
131
|
+
if (!parameters ||
|
|
132
|
+
parameters.tag !== 0x05 ||
|
|
133
|
+
parameters.end !== parameters.start ||
|
|
134
|
+
parameters.end !== algorithm.end) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
const wrapped = tlv(der, algorithm.end, info.end);
|
|
138
|
+
if (!wrapped || wrapped.tag !== 0x04)
|
|
139
|
+
return undefined;
|
|
140
|
+
// PKCS#8 allows one optional [0] attributes set after the key; nothing else.
|
|
141
|
+
let tail = wrapped.end;
|
|
142
|
+
if (tail < info.end) {
|
|
143
|
+
const attributes = tlv(der, tail, info.end);
|
|
144
|
+
if (!attributes || attributes.tag !== 0xa0)
|
|
145
|
+
return undefined;
|
|
146
|
+
tail = attributes.end;
|
|
147
|
+
}
|
|
148
|
+
if (tail !== info.end)
|
|
149
|
+
return undefined;
|
|
150
|
+
const key = tlv(der, wrapped.start, wrapped.end);
|
|
151
|
+
if (!key || key.tag !== 0x30 || key.end !== wrapped.end)
|
|
152
|
+
return undefined;
|
|
153
|
+
// version (0: two primes, no otherPrimeInfos), n, e, d, p, q, dP, dQ, qInv
|
|
154
|
+
const integers = [];
|
|
155
|
+
let at = key.start;
|
|
156
|
+
for (let index = 0; index < 9; index += 1) {
|
|
157
|
+
const integer = tlv(der, at, key.end);
|
|
158
|
+
if (!integer || integer.tag !== 0x02 || integer.end === integer.start)
|
|
159
|
+
return undefined;
|
|
160
|
+
integers.push(integer);
|
|
161
|
+
at = integer.end;
|
|
162
|
+
}
|
|
163
|
+
const rsaVersion = integers[0];
|
|
164
|
+
if (at !== key.end || rsaVersion.end - rsaVersion.start !== 1 || der[rsaVersion.start] !== 0) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
const modulus = integers[1];
|
|
168
|
+
let first = modulus.start;
|
|
169
|
+
while (first < modulus.end - 1 && der[first] === 0)
|
|
170
|
+
first += 1;
|
|
171
|
+
const leading = der[first];
|
|
172
|
+
return leading === 0 ? undefined : (modulus.end - first - 1) * 8 + (32 - Math.clz32(leading));
|
|
173
|
+
}
|
|
174
|
+
function field(record, name) {
|
|
175
|
+
const value = record[name];
|
|
176
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : undefined;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Check a service account at construction: the object form, or the JSON key
|
|
180
|
+
* file's text. Structural mistakes throw here, so a deployment never boots into
|
|
181
|
+
* a connector whose every call would fail.
|
|
182
|
+
*/
|
|
183
|
+
export function parseServiceAccount(owner, input) {
|
|
184
|
+
let record;
|
|
185
|
+
let fromFile = false;
|
|
186
|
+
if (typeof input === "string") {
|
|
187
|
+
try {
|
|
188
|
+
record = JSON.parse(input);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
throw new Error(`${owner} serviceAccount must be { clientEmail, privateKey } or the JSON key file's text; the string given is not JSON.`);
|
|
192
|
+
}
|
|
193
|
+
if (!record || typeof record !== "object" || Array.isArray(record)) {
|
|
194
|
+
throw new Error(`${owner} serviceAccount JSON is not a key object.`);
|
|
195
|
+
}
|
|
196
|
+
if (record["type"] !== undefined && record["type"] !== "service_account") {
|
|
197
|
+
throw new Error(`${owner} serviceAccount JSON is a "${String(record["type"])}" credential, not a service_account key.`);
|
|
198
|
+
}
|
|
199
|
+
fromFile = true;
|
|
200
|
+
}
|
|
201
|
+
else if (input && typeof input === "object" && !Array.isArray(input)) {
|
|
202
|
+
record = input;
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
throw new Error(`${owner} requires serviceAccount: { clientEmail, privateKey } or the JSON key file's text.`);
|
|
206
|
+
}
|
|
207
|
+
const clientEmail = field(record, fromFile ? "client_email" : "clientEmail");
|
|
208
|
+
const privateKey = field(record, fromFile ? "private_key" : "privateKey");
|
|
209
|
+
const clientId = field(record, fromFile ? "client_id" : "clientId");
|
|
210
|
+
if (!clientEmail || !/^[^\s@]+@[^\s@]+$/.test(clientEmail)) {
|
|
211
|
+
throw new Error(`${owner} serviceAccount needs ${fromFile ? "client_email" : "clientEmail"}: the service account's email address.`);
|
|
212
|
+
}
|
|
213
|
+
if (!privateKey) {
|
|
214
|
+
throw new Error(`${owner} serviceAccount needs ${fromFile ? "private_key" : "privateKey"}: the PEM private key from the JSON key file.`);
|
|
215
|
+
}
|
|
216
|
+
if (clientId !== undefined && !/^\d+$/.test(clientId)) {
|
|
217
|
+
throw new Error(`${owner} serviceAccount clientId must be the numeric client ID.`);
|
|
218
|
+
}
|
|
219
|
+
return { clientEmail, clientId, der: decodedKey(owner, privateKey) };
|
|
220
|
+
}
|
|
221
|
+
// --- Signing --------------------------------------------------------------------
|
|
222
|
+
const encoder = new TextEncoder();
|
|
223
|
+
function base64Url(bytes) {
|
|
224
|
+
let binary = "";
|
|
225
|
+
for (let index = 0; index < bytes.length; index += 0x8000) {
|
|
226
|
+
binary += String.fromCharCode(...bytes.subarray(index, index + 0x8000));
|
|
227
|
+
}
|
|
228
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
229
|
+
}
|
|
230
|
+
/** One import per key per isolate; a failed import is retried, not cached. */
|
|
231
|
+
const signingKeys = new WeakMap();
|
|
232
|
+
function signingKey(account) {
|
|
233
|
+
let key = signingKeys.get(account);
|
|
234
|
+
if (!key) {
|
|
235
|
+
key = crypto.subtle
|
|
236
|
+
.importKey("pkcs8", account.der, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["sign"])
|
|
237
|
+
.catch((cause) => {
|
|
238
|
+
signingKeys.delete(account);
|
|
239
|
+
throw new ConnectorCallError("auth_required", `The private key for service account ${account.clientEmail} could not be imported as an RSA signing key. Replace it with the private_key of a current JSON key for that account.`, { cause });
|
|
240
|
+
});
|
|
241
|
+
signingKeys.set(account, key);
|
|
242
|
+
}
|
|
243
|
+
return key;
|
|
244
|
+
}
|
|
245
|
+
/** The RS256 JWT-bearer assertion for one subject and scope set. */
|
|
246
|
+
async function signAssertion(account, subject, scopes, nowSeconds) {
|
|
247
|
+
const header = base64Url(encoder.encode(JSON.stringify({ alg: "RS256", typ: "JWT" })));
|
|
248
|
+
const claims = base64Url(encoder.encode(JSON.stringify({
|
|
249
|
+
iss: account.clientEmail,
|
|
250
|
+
sub: subject,
|
|
251
|
+
scope: scopes.join(" "),
|
|
252
|
+
aud: GOOGLE_TOKEN_URL,
|
|
253
|
+
iat: nowSeconds,
|
|
254
|
+
exp: nowSeconds + ASSERTION_LIFETIME_SECONDS,
|
|
255
|
+
})));
|
|
256
|
+
const input = `${header}.${claims}`;
|
|
257
|
+
const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", await signingKey(account), encoder.encode(input));
|
|
258
|
+
return `${input}.${base64Url(new Uint8Array(signature))}`;
|
|
259
|
+
}
|
|
260
|
+
// --- Token exchange --------------------------------------------------------------
|
|
261
|
+
const tokenTransport = guardedFetch({
|
|
262
|
+
provider: "Google OAuth",
|
|
263
|
+
baseUrl: GOOGLE_TOKEN_ORIGIN,
|
|
264
|
+
headers: { Accept: "application/json" },
|
|
265
|
+
maxResponseBytes: TOKEN_RESPONSE_BYTES,
|
|
266
|
+
// The assertion in the body is the authentication; there is no header.
|
|
267
|
+
authenticate: () => ({}),
|
|
268
|
+
});
|
|
269
|
+
function asRecord(value) {
|
|
270
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
271
|
+
}
|
|
272
|
+
/** The token endpoint's OAuth `error` codes this module names back. */
|
|
273
|
+
const TOKEN_ERROR_CODES = new Set([
|
|
274
|
+
"invalid_request",
|
|
275
|
+
"invalid_client",
|
|
276
|
+
"invalid_grant",
|
|
277
|
+
"invalid_scope",
|
|
278
|
+
"unauthorized_client",
|
|
279
|
+
"unsupported_grant_type",
|
|
280
|
+
"access_denied",
|
|
281
|
+
"disabled_client",
|
|
282
|
+
"rate_limit_exceeded",
|
|
283
|
+
"server_error",
|
|
284
|
+
"temporarily_unavailable",
|
|
285
|
+
]);
|
|
286
|
+
/**
|
|
287
|
+
* What the token endpoint answered, in connecta's words: the step, the host,
|
|
288
|
+
* the status, and the OAuth `error` code when it is one of the known codes.
|
|
289
|
+
* Google's `error_description` is withheld. It is an authorization server's
|
|
290
|
+
* text, and the rule for those holds here as in `remoteMcp()`: whatever such
|
|
291
|
+
* a server writes may echo what it was sent, so none of it reaches an agent
|
|
292
|
+
* or a log, however benign Google's has been.
|
|
293
|
+
*/
|
|
294
|
+
function tokenAnswer(status, code) {
|
|
295
|
+
const named = typeof code === "string" && TOKEN_ERROR_CODES.has(code) ? ` with OAuth error ${code}` : "";
|
|
296
|
+
return ` The token request to ${GOOGLE_TOKEN_ORIGIN} was answered HTTP ${status}${named}; its description is withheld.`;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Map a refused token request to what fixes it. The codes are RFC 6749's and
|
|
300
|
+
* Google documents what each means for a delegated assertion.
|
|
301
|
+
*/
|
|
302
|
+
function tokenFailure(account, scopes, status, headers, payload) {
|
|
303
|
+
const code = asRecord(payload)["error"];
|
|
304
|
+
const said = tokenAnswer(status, code);
|
|
305
|
+
const client = account.clientId
|
|
306
|
+
? `client ID ${account.clientId} (${account.clientEmail})`
|
|
307
|
+
: `the client ID of ${account.clientEmail}`;
|
|
308
|
+
if (code === "unauthorized_client" || code === "access_denied" || code === "invalid_scope") {
|
|
309
|
+
return new ConnectorCallError("provider_permission_denied", `Google refused domain-wide delegation for ${account.clientEmail}.${said} A Workspace super admin must authorize ${client} in the Admin console (Security → Access and data control → API controls → Manage Domain Wide Delegation) with exactly these scopes: ${scopes.join(",")}. A new or changed grant can take up to 24 hours to apply.`);
|
|
310
|
+
}
|
|
311
|
+
if (code === "invalid_grant") {
|
|
312
|
+
return new ConnectorCallError("auth_required", `Google refused the delegated token.${said} The mapped Workspace user may not exist, may be suspended, or may be outside the domain that granted delegation; the service account key may have been deleted or disabled; or this host's clock may be minutes off. Check the deployment's subject mapping and the key before retrying.`);
|
|
313
|
+
}
|
|
314
|
+
if (code === "invalid_client" || code === "disabled_client") {
|
|
315
|
+
return new ConnectorCallError("auth_required", `Google rejected service account ${account.clientEmail} itself.${said} The account or its key may be disabled or deleted; create a new JSON key and redeploy.`);
|
|
316
|
+
}
|
|
317
|
+
if (status === 429) {
|
|
318
|
+
const wait = retryAfterMs(headers);
|
|
319
|
+
return new ConnectorCallError("rate_limited", `Google's token endpoint is rate limiting ${account.clientEmail}.${said}`, wait === undefined ? {} : { retryAfterMs: wait });
|
|
320
|
+
}
|
|
321
|
+
if (status >= 500) {
|
|
322
|
+
return new ConnectorCallError("unavailable", `Google's token endpoint answered HTTP ${status}.${said}`);
|
|
323
|
+
}
|
|
324
|
+
if (status === 403) {
|
|
325
|
+
return new ConnectorCallError("provider_permission_denied", `Google refused domain-wide delegation.${said} Ask a Workspace super admin to review ${client} in the Admin console's Manage Domain Wide Delegation entry and grant exactly these scopes: ${scopes.join(",")}.`);
|
|
326
|
+
}
|
|
327
|
+
return new ConnectorCallError("connector_call_failed", `Google's token endpoint refused the delegated assertion with HTTP ${status}.${said}`, { retryable: false });
|
|
328
|
+
}
|
|
329
|
+
async function mint(account, subject, scopes, ctx) {
|
|
330
|
+
const issuedAt = Date.now();
|
|
331
|
+
const assertion = await signAssertion(account, subject, scopes, Math.floor(issuedAt / 1000));
|
|
332
|
+
return await tokenTransport({
|
|
333
|
+
method: "POST",
|
|
334
|
+
path: "/token",
|
|
335
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
336
|
+
rawBody: new URLSearchParams({ grant_type: JWT_BEARER_GRANT, assertion }).toString(),
|
|
337
|
+
}, ctx, async (response) => {
|
|
338
|
+
const parsed = await response.jsonResult();
|
|
339
|
+
const payload = "value" in parsed ? parsed.value : undefined;
|
|
340
|
+
if (!response.ok) {
|
|
341
|
+
throw verdict(tokenFailure(account, scopes, response.status, response.headers, payload));
|
|
342
|
+
}
|
|
343
|
+
const body = asRecord(payload);
|
|
344
|
+
const accessToken = body["access_token"];
|
|
345
|
+
const expiresIn = Number(body["expires_in"] ?? ASSERTION_LIFETIME_SECONDS);
|
|
346
|
+
if (typeof accessToken !== "string" || accessToken === "" || !Number.isFinite(expiresIn)) {
|
|
347
|
+
throw verdict(new ConnectorCallError("connector_call_failed", "Google's token endpoint answered without an access token.", {
|
|
348
|
+
retryable: false,
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
// Measured from before the request left, so a slow answer only ever
|
|
352
|
+
// shortens the token's life here, never lengthens it.
|
|
353
|
+
return { accessToken, expiresAt: issuedAt + expiresIn * 1000 };
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
/** The longest a follower waits on another caller's mint. */
|
|
357
|
+
const FLIGHT_WAIT_MS = 30_000;
|
|
358
|
+
/** Errors carrying Google's own answer, the only failures a flight shares. */
|
|
359
|
+
const verdicts = new WeakSet();
|
|
360
|
+
function verdict(error) {
|
|
361
|
+
verdicts.add(error);
|
|
362
|
+
return error;
|
|
363
|
+
}
|
|
364
|
+
/** Module-level and in memory only: tokens never touch storage. */
|
|
365
|
+
const tokens = new Map();
|
|
366
|
+
const flights = new Map();
|
|
367
|
+
/**
|
|
368
|
+
* The key's own identity, a SHA-256 of its DER. Part of every cache key, so a
|
|
369
|
+
* rotated key — or a replacement that would not sign — never answers with a
|
|
370
|
+
* token another key minted for the same service account.
|
|
371
|
+
*/
|
|
372
|
+
const keyIdentities = new WeakMap();
|
|
373
|
+
function keyIdentity(account) {
|
|
374
|
+
let identity = keyIdentities.get(account);
|
|
375
|
+
if (!identity) {
|
|
376
|
+
identity = crypto.subtle
|
|
377
|
+
.digest("SHA-256", account.der)
|
|
378
|
+
.then((digest) => Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""));
|
|
379
|
+
keyIdentities.set(account, identity);
|
|
380
|
+
}
|
|
381
|
+
return identity;
|
|
382
|
+
}
|
|
383
|
+
async function cacheKey({ account, subject, scopes }) {
|
|
384
|
+
// Workspace addresses are case-insensitive; scope order is not a new grant.
|
|
385
|
+
return [account.clientEmail, await keyIdentity(account), subject.toLowerCase(), [...scopes].sort().join(" ")].join("\n");
|
|
386
|
+
}
|
|
387
|
+
function remember(key, minted) {
|
|
388
|
+
tokens.delete(key);
|
|
389
|
+
tokens.set(key, minted);
|
|
390
|
+
while (tokens.size > MAX_CACHED_TOKENS) {
|
|
391
|
+
const oldest = tokens.keys().next().value;
|
|
392
|
+
if (oldest === undefined)
|
|
393
|
+
break;
|
|
394
|
+
tokens.delete(oldest);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Wait on another caller's flight under this caller's own terms: its own
|
|
399
|
+
* signal, its own timer to the flight's deadline, and a promise created here,
|
|
400
|
+
* so the continuation runs in this caller's request whichever request settles
|
|
401
|
+
* the flight. Rejects only with this caller's own abort reason.
|
|
402
|
+
*/
|
|
403
|
+
function follow(flight, signal) {
|
|
404
|
+
return new Promise((resolve, reject) => {
|
|
405
|
+
if (signal?.aborted) {
|
|
406
|
+
reject(signal.reason);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
let done = false;
|
|
410
|
+
const finish = (outcome) => {
|
|
411
|
+
if (done)
|
|
412
|
+
return;
|
|
413
|
+
done = true;
|
|
414
|
+
clearTimeout(timer);
|
|
415
|
+
signal?.removeEventListener("abort", onAbort);
|
|
416
|
+
resolve(outcome);
|
|
417
|
+
};
|
|
418
|
+
const onAbort = () => {
|
|
419
|
+
if (done)
|
|
420
|
+
return;
|
|
421
|
+
done = true;
|
|
422
|
+
clearTimeout(timer);
|
|
423
|
+
reject(signal.reason);
|
|
424
|
+
};
|
|
425
|
+
const timer = setTimeout(() => finish({ kind: "abandoned" }), Math.max(0, flight.deadline - Date.now()));
|
|
426
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
427
|
+
void flight.outcome.then(finish);
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* An access token for this subject and scope set: the cached one while it has
|
|
432
|
+
* more than a minute left, otherwise one fresh mint shared by every concurrent
|
|
433
|
+
* caller asking for the same key.
|
|
434
|
+
*
|
|
435
|
+
* Followers share only a scalar outcome and a deadline. The owner records
|
|
436
|
+
* what happened inside its own request — a token, Google's refusal, or
|
|
437
|
+
* `abandoned` for anything else — and a follower that hears `abandoned`, or
|
|
438
|
+
* reaches the deadline first, mints for itself. A refusal is rebuilt for each
|
|
439
|
+
* follower from its code and message, never handed over as the owner's object.
|
|
440
|
+
*/
|
|
441
|
+
export async function delegatedToken(request, ctx) {
|
|
442
|
+
const key = await cacheKey(request);
|
|
443
|
+
for (;;) {
|
|
444
|
+
const cached = tokens.get(key);
|
|
445
|
+
if (cached && cached.expiresAt - REFRESH_MARGIN_MS > Date.now()) {
|
|
446
|
+
return cached.accessToken;
|
|
447
|
+
}
|
|
448
|
+
const flight = flights.get(key);
|
|
449
|
+
if (flight && flight.deadline > Date.now()) {
|
|
450
|
+
const outcome = await follow(flight, ctx.signal);
|
|
451
|
+
if (outcome.kind === "token")
|
|
452
|
+
return outcome.minted.accessToken;
|
|
453
|
+
if (outcome.kind === "refused") {
|
|
454
|
+
throw new ConnectorCallError(outcome.code, outcome.message, outcome.retryAfterMs === undefined ? {} : { retryAfterMs: outcome.retryAfterMs });
|
|
455
|
+
}
|
|
456
|
+
if (flights.get(key) === flight)
|
|
457
|
+
flights.delete(key);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
let settle;
|
|
461
|
+
const own = {
|
|
462
|
+
outcome: new Promise((resolve) => {
|
|
463
|
+
settle = resolve;
|
|
464
|
+
}),
|
|
465
|
+
deadline: Date.now() + Math.min(ctx.timeoutMs ?? FLIGHT_WAIT_MS, FLIGHT_WAIT_MS),
|
|
466
|
+
};
|
|
467
|
+
flights.set(key, own);
|
|
468
|
+
try {
|
|
469
|
+
const minted = await mint(request.account, request.subject, request.scopes, ctx);
|
|
470
|
+
remember(key, minted);
|
|
471
|
+
settle({ kind: "token", minted });
|
|
472
|
+
return minted.accessToken;
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
settle(error instanceof ConnectorCallError && verdicts.has(error)
|
|
476
|
+
? {
|
|
477
|
+
kind: "refused",
|
|
478
|
+
code: error.code,
|
|
479
|
+
message: error.message,
|
|
480
|
+
retryAfterMs: error.retryAfterMs,
|
|
481
|
+
}
|
|
482
|
+
: { kind: "abandoned" });
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
finally {
|
|
486
|
+
if (flights.get(key) === own)
|
|
487
|
+
flights.delete(key);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Drop a token the API has just rejected, so the next request mints anew —
|
|
493
|
+
* but only that token. A 401 that arrives after a newer token replaced it
|
|
494
|
+
* leaves the newer one alone.
|
|
495
|
+
*/
|
|
496
|
+
export async function forgetDelegatedToken(request, rejected) {
|
|
497
|
+
const key = await cacheKey(request);
|
|
498
|
+
if (tokens.get(key)?.accessToken === rejected)
|
|
499
|
+
tokens.delete(key);
|
|
500
|
+
}
|