@zackbart/connecta 0.37.1 → 0.39.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 +94 -0
- package/README.md +5 -4
- package/bin/connecta.mjs +1 -39
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +2 -4
- package/dist/agent-identity.js +1 -1
- package/dist/agent-routes.js +231 -0
- package/dist/auth/clerk-sdk/generated.js +2 -1
- package/dist/auth/downstream-oauth/consent.js +519 -0
- package/dist/auth/downstream-oauth/grant-store.js +439 -0
- package/dist/auth/downstream-oauth/refresh-coordinator.js +562 -0
- package/dist/auth/downstream-oauth/refresh-records.js +447 -0
- package/dist/auth/downstream-oauth/sdk-hooks.js +242 -0
- package/dist/auth/downstream-oauth.js +66 -2516
- package/dist/auth/downstream-revocation.js +1 -1
- package/dist/auth/native.js +27 -0
- package/dist/auth/oauth-grant.js +24 -0
- package/dist/auth/oauth-token-response.js +1 -1
- package/dist/auth/static-oauth.js +153 -131
- package/dist/auth/vercel-agent.js +2 -1
- package/dist/auth-elicitation.js +9 -3
- package/dist/authorization-handoff.js +11 -5
- package/dist/branding.d.ts +1 -15
- package/dist/branding.js +2 -2
- package/dist/bytes.js +42 -0
- package/dist/call-admission-context.js +2 -5
- package/dist/call-recovery.js +0 -3
- package/dist/catalog-cache.js +130 -43
- package/dist/catalog-fingerprint.js +2 -18
- package/dist/catalog-policy.js +18 -0
- package/dist/catalog-service.js +285 -48
- package/dist/catalog.js +18 -2
- package/dist/config-defaults.d.ts +1 -0
- package/dist/config-defaults.js +1 -0
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +6 -2
- package/dist/config.d.ts +57 -25
- package/dist/config.js +30 -77
- package/dist/connector-caller.d.ts +1 -1
- package/dist/connector-health.js +59 -0
- package/dist/connector-status.js +117 -0
- package/dist/connectors/api-connector.d.ts +7 -2
- package/dist/connectors/api-connector.js +11 -7
- package/dist/connectors/guarded-fetch.d.ts +0 -6
- package/dist/connectors/guarded-fetch.js +1 -11
- package/dist/connectors/negotiation-cache.js +22 -4
- package/dist/connectors/remote-mcp/call.js +164 -0
- package/dist/connectors/remote-mcp/catalog.js +278 -0
- package/dist/connectors/remote-mcp/config.js +263 -0
- package/dist/connectors/remote-mcp/connection.js +671 -0
- package/dist/connectors/remote-mcp/errors.js +488 -0
- package/dist/connectors/remote-mcp/oauth.js +469 -0
- package/dist/connectors/remote-mcp/resources.js +473 -0
- package/dist/connectors/remote-mcp/runtime.js +1 -0
- package/dist/connectors/remote-mcp/transport.js +544 -0
- package/dist/connectors/remote-mcp.js +61 -2898
- package/dist/connectors/resource-uri.js +0 -4
- package/dist/credential-contract.d.ts +23 -0
- package/dist/credential-reads.js +17 -0
- package/dist/credential-verification.js +53 -0
- package/dist/credentials.d.ts +7 -0
- package/dist/credentials.js +60 -25
- package/dist/d1.d.ts +0 -89
- package/dist/d1.js +0 -34
- package/dist/describe-config.d.ts +12 -2
- package/dist/describe-config.js +2 -0
- package/dist/discovery-health.js +199 -0
- package/dist/downstream-input.js +4 -2
- package/dist/errors.d.ts +55 -17
- package/dist/errors.js +95 -41
- package/dist/execute.js +172 -125
- package/dist/executor-result.js +5 -2
- package/dist/executors/quickjs-runtime.js +2 -25
- package/dist/executors/quickjs.js +1 -1
- package/dist/guest-functions.js +83 -0
- package/dist/guest-runtime.js +63 -20
- package/dist/guest-types.d.ts +24 -10
- package/dist/http/argument-problems.js +35 -0
- package/dist/http/client.js +1 -1
- package/dist/http/errors.js +7 -6
- package/dist/http/fields.js +192 -13
- package/dist/http/hatch-response.js +73 -0
- package/dist/http/hatch.js +88 -23
- package/dist/http/json-schema.js +1 -1
- package/dist/http/node-transport.js +0 -1
- package/dist/http/oauth.js +4 -3
- package/dist/http/paging.js +6 -9
- package/dist/http/provider.js +33 -5
- package/dist/http/safety.js +9 -1
- package/dist/http/services.js +1 -8
- package/dist/http/tools.js +45 -30
- package/dist/http/vendor.js +7 -7
- package/dist/identity.js +2 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/invocation-auth.js +2 -1
- package/dist/invocation.js +21 -6
- package/dist/membership.d.ts +13 -5
- package/dist/membership.js +34 -2
- package/dist/meta-tools.js +50 -35
- package/dist/native-recovery.js +4 -16
- package/dist/native-tools.js +6 -2
- package/dist/oauth-handoff.js +2 -2
- package/dist/oauth-sealing.js +10 -0
- package/dist/operation-vocabulary.js +136 -14
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +2 -49
- package/dist/operator-ui/contract.d.ts +12 -2
- package/dist/operator-ui/generated.js +3 -3
- package/dist/operator-ui/model.d.ts +22 -1
- package/dist/program-writes.js +3 -8
- package/dist/provider-call.js +0 -7
- package/dist/provider.d.ts +1 -1
- package/dist/providers/_shared/google/kit.js +1 -6
- package/dist/providers/_shared/google/service-account.js +1 -3
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/_shared/guide.js +3 -0
- package/dist/providers/_shared/json.js +12 -0
- package/dist/providers/_shared/rest/compact-index.js +1 -1
- package/dist/providers/_shared/rest/kit.js +1 -3
- package/dist/providers/_shared/rest/operation-index.js +49 -53
- package/dist/providers/_shared/rest/value-safety.js +106 -28
- package/dist/providers/breeze/api.js +9 -8
- package/dist/providers/breeze/index.js +4 -2
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/breeze/tools.js +7 -7
- package/dist/providers/ccb/api.js +3 -7
- package/dist/providers/ccb/index.js +7 -1
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/ccb/tools.js +1 -6
- package/dist/providers/cloudflare/api.js +10 -3
- package/dist/providers/cloudflare/index.js +2 -1
- package/dist/providers/cloudflare/safety.js +3 -3
- package/dist/providers/cloudflare/skill.generated.js +1 -1
- package/dist/providers/docs/lib/schemas.js +24 -22
- package/dist/providers/docs/safety.js +2 -0
- package/dist/providers/docs/skill.generated.js +1 -1
- package/dist/providers/docs/tools.js +4 -14
- package/dist/providers/drive/api.js +1 -0
- package/dist/providers/drive/lib/contracts.js +8 -5
- package/dist/providers/drive/skill.generated.js +1 -1
- package/dist/providers/drive/tools.js +23 -41
- package/dist/providers/forms/api.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/forms/tools.js +11 -16
- package/dist/providers/github/api.js +1 -0
- package/dist/providers/github/lib/discovery.js +1 -1
- package/dist/providers/github/skill.generated.js +1 -1
- package/dist/providers/github/tools.js +61 -17
- package/dist/providers/gmail/lib/schemas.js +60 -50
- package/dist/providers/gmail/skill.generated.js +1 -1
- package/dist/providers/gmail/tools.js +12 -26
- package/dist/providers/granola/index.js +2 -0
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/api.js +8 -4
- package/dist/providers/overflow/index.js +2 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/overflow/tools.js +3 -7
- package/dist/providers/planning-center/api.js +5 -2
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/lib/json-api.js +1 -9
- package/dist/providers/planning-center/skill.generated.js +1 -1
- package/dist/providers/planning-center/tools.js +2 -1
- package/dist/providers/quickbooks/safety.js +80 -39
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/api.js +1 -0
- package/dist/providers/sheets/skill.generated.js +1 -1
- package/dist/providers/sheets/tools.js +14 -23
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/slides/tools.js +19 -26
- package/dist/providers/stripe/index.js +2 -1
- package/dist/providers/stripe/safety.js +2 -2
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/api.js +5 -8
- package/dist/providers/tithely/index.js +3 -2
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/tithely/tools.js +7 -5
- package/dist/providers/vercel/api.js +2 -7
- package/dist/providers/vercel/lib/pin.js +1 -1
- package/dist/providers/vercel/safety.js +1 -1
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/tools.js +6 -4
- package/dist/redaction-marker.js +2 -2
- package/dist/registry.d.ts +11 -5
- package/dist/registry.js +46 -20
- package/dist/request-principal.js +1 -1
- package/dist/request-state.js +2 -2
- package/dist/routes/access-tokens.js +1 -1
- package/dist/routes/activity-disclosure.js +1 -1
- package/dist/routes/connect.js +2 -2
- package/dist/routes/credentials.js +130 -61
- package/dist/routes/dispatch.js +5 -1
- package/dist/routes/mcp.js +34 -17
- package/dist/routes/oauth-management.js +1 -1
- package/dist/routes/operator.js +6 -5
- package/dist/routes/shared.d.ts +13 -8
- package/dist/routes/shared.js +15 -9
- package/dist/routes/ui-config.js +3 -3
- package/dist/routes/ui-data.js +126 -20
- package/dist/routes/ui.js +17 -1
- package/dist/runtime/call-admission.js +1 -1
- package/dist/runtime/call.js +8 -3
- package/dist/runtime/native-storage.js +9 -0
- package/dist/runtime/request-reads.js +182 -0
- package/dist/runtime/request-scope.js +16 -0
- package/dist/runtime/settlement.js +1 -1
- package/dist/runtime/shared-read.js +2 -15
- package/dist/runtime/storage.js +12 -0
- package/dist/sent-secrets.d.ts +1 -2
- package/dist/sent-secrets.js +3 -8
- package/dist/server.js +4 -4
- package/dist/skills.js +44 -54
- package/dist/sqlite.d.ts +0 -19
- package/dist/sqlite.js +2 -83
- package/dist/storage/keys.js +69 -128
- package/dist/storage/memory.js +8 -2
- package/dist/storage/sql-native.js +27 -101
- package/dist/storage/sql.d.ts +1 -24
- package/dist/storage/sql.js +13 -14
- package/dist/storage-digest.js +32 -0
- package/dist/timeout.js +8 -0
- package/dist/tool-error.js +4 -0
- package/dist/types.d.ts +33 -0
- package/dist/url-safety.js +0 -4
- package/dist/usage-guide.js +18 -37
- package/dist/validate.d.ts +3 -0
- package/dist/validate.js +220 -158
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +1 -1
- package/dist/write-outcome.js +0 -3
- package/documentation/README.md +19 -55
- package/documentation/architecture.md +436 -515
- package/documentation/auth.md +229 -88
- package/documentation/code-mode.md +201 -94
- package/documentation/deploying.md +25 -43
- package/documentation/integrating.md +18 -19
- package/documentation/meta-tools.md +225 -51
- package/documentation/migration-0.33.md +10 -6
- package/documentation/migration-0.34.md +5 -5
- package/documentation/migration-retired-layouts.md +74 -0
- package/documentation/operator-tests.md +20 -120
- package/documentation/operator-ui.md +108 -16
- package/documentation/pool-membership-upgrade.md +44 -0
- package/documentation/provider-auth-migration.md +4 -10
- package/documentation/provider-migration-0.29.md +4 -15
- package/documentation/providers.md +105 -0
- package/examples/worker/AGENTS.md +14 -16
- package/examples/worker/README.md +24 -118
- package/examples/worker/src/connecta.config.ts +1 -0
- package/guides/connecting-agents.md +9 -31
- package/guides/how-it-works.md +6 -4
- package/guides/safety-and-trust.md +4 -1
- package/package.json +19 -19
- package/templates/node/AGENTS.md +13 -12
- package/templates/node/README.md +9 -26
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +1 -0
- package/dist/auth/oauth-v2-migration.js +0 -135
- package/dist/provider-call-facts.d.ts +0 -115
- package/dist/provider-call.d.ts +0 -112
- package/dist/storage/d1-copy.js +0 -255
- package/dist/storage/sql-schema.d.ts +0 -6
- package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
- package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
|
@@ -1,73 +1,18 @@
|
|
|
1
|
+
import { META_ROUTES } from "../agent-routes.js";
|
|
1
2
|
import { UnauthorizedError } from "@modelcontextprotocol/client";
|
|
2
|
-
import { Effect, Exit } from "effect";
|
|
3
|
-
import { byteReadResponse } from "../byte-read-response.js";
|
|
4
3
|
import { deriveContext } from "../derived-context.js";
|
|
5
4
|
import { ConnectorCallError, resendAdvice } from "../errors.js";
|
|
6
|
-
import { oauthPartitionFor, retainOAuthPartition } from "../oauth-partition.js";
|
|
7
|
-
import { keepErrorName } from "../redaction-identity.js";
|
|
8
|
-
import { fromSignal, runEdge } from "../runtime/run.js";
|
|
9
|
-
import { Settlement } from "../runtime/settlement.js";
|
|
10
5
|
import { SentSecrets } from "../sent-secrets.js";
|
|
11
|
-
import { OAUTH_FLOW_TTL_SECONDS, OAUTH_REFRESH_LEASE_SECONDS, oauthFlowKeys, oauthGrantKeys, oauthRefreshActiveKeys, oauthRefreshKeys, oauthRefreshSpentKeys, } from "../storage/keys.js";
|
|
12
6
|
import { authenticateRemoteClient, downstreamClientMetadata, remoteClientAuthMethod, } from "./downstream-client-metadata.js";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
void reader.closed.catch(() => { });
|
|
23
|
-
let closed = false;
|
|
24
|
-
let abort;
|
|
25
|
-
const release = () => {
|
|
26
|
-
closed = true;
|
|
27
|
-
signal.removeEventListener("abort", abort);
|
|
28
|
-
reader.releaseLock();
|
|
29
|
-
};
|
|
30
|
-
const body = new ReadableStream({
|
|
31
|
-
start(controller) {
|
|
32
|
-
abort = () => {
|
|
33
|
-
if (closed)
|
|
34
|
-
return;
|
|
35
|
-
void reader.cancel().catch(() => { });
|
|
36
|
-
release();
|
|
37
|
-
controller.error(signal.reason);
|
|
38
|
-
};
|
|
39
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
40
|
-
if (signal.aborted)
|
|
41
|
-
abort();
|
|
42
|
-
},
|
|
43
|
-
async pull(controller) {
|
|
44
|
-
try {
|
|
45
|
-
const next = await reader.read();
|
|
46
|
-
if (closed)
|
|
47
|
-
return;
|
|
48
|
-
if (next.done) {
|
|
49
|
-
release();
|
|
50
|
-
controller.close();
|
|
51
|
-
}
|
|
52
|
-
else
|
|
53
|
-
controller.enqueue(next.value);
|
|
54
|
-
}
|
|
55
|
-
catch (error) {
|
|
56
|
-
if (!closed) {
|
|
57
|
-
release();
|
|
58
|
-
controller.error(error);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
cancel() {
|
|
63
|
-
if (!closed) {
|
|
64
|
-
void reader.cancel().catch(() => { });
|
|
65
|
-
release();
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
return byteReadResponse(new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers }));
|
|
70
|
-
}
|
|
7
|
+
import { authMethods } from "./native.js";
|
|
8
|
+
import { OAUTH_REFRESH_SKEW_MS } from "./oauth-grant.js";
|
|
9
|
+
import { consentMethods } from "./downstream-oauth/consent.js";
|
|
10
|
+
import { ACTIVE_EPOCH_PREFIX, GRANT, grantStoreMethods, isDisconnectedEpoch, } from "./downstream-oauth/grant-store.js";
|
|
11
|
+
import { aborted, tokenResponse, } from "./downstream-oauth/refresh-coordinator.js";
|
|
12
|
+
import { refreshRecordMethods } from "./downstream-oauth/refresh-records.js";
|
|
13
|
+
import { sdkHookMethods } from "./downstream-oauth/sdk-hooks.js";
|
|
14
|
+
export { earlyRenewalFetch, EarlyRenewals, OAuthRefreshCoordinator, refreshCoordinatorsByPartition, } from "./downstream-oauth/refresh-coordinator.js";
|
|
15
|
+
export { codeExchangeResponse, OAuthCallbackClaimedError, oauthStateDigest } from "./downstream-oauth/consent.js";
|
|
71
16
|
/** RFC 6749 section 3.3: one or more scope tokens, separated by single spaces. */
|
|
72
17
|
const OAUTH_SCOPE = /^[\x21\x23-\x5B\x5D-\x7E]+(?: [\x21\x23-\x5B\x5D-\x7E]+)*$/;
|
|
73
18
|
/** Refuse, at construction, a configured scope no authorization server could parse. */
|
|
@@ -86,651 +31,9 @@ export function assertOAuthScope(connectorId, scope) {
|
|
|
86
31
|
export function authorizingContext(ctx) {
|
|
87
32
|
return deriveContext(ctx, { allowAuthorization: true });
|
|
88
33
|
}
|
|
89
|
-
/**
|
|
90
|
-
* A callback whose consent another callback already claimed: fixed text, and
|
|
91
|
-
* nothing was sent. Its own class so the callback route can answer it as the
|
|
92
|
-
* already-used link it is, not as an exchange the provider refused.
|
|
93
|
-
*/
|
|
94
|
-
export class OAuthCallbackClaimedError extends ConnectorCallError {
|
|
95
|
-
constructor(connectorId) {
|
|
96
|
-
super("connector_call_failed", `Connector "${connectorId}" authorization callback was already used by another request; nothing was exchanged.`);
|
|
97
|
-
this.name = "OAuthCallbackClaimedError";
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
keepErrorName(OAuthCallbackClaimedError, "OAuthCallbackClaimedError");
|
|
101
34
|
/** A local destination guard proved that no OAuth HTTP request left. */
|
|
102
35
|
export class OAuthRequestNotSentError extends ConnectorCallError {
|
|
103
36
|
}
|
|
104
|
-
/** A 256-bit random opaque value, hex-encoded — used for the OAuth `state`. */
|
|
105
|
-
function randomState() {
|
|
106
|
-
const bytes = new Uint8Array(32);
|
|
107
|
-
crypto.getRandomValues(bytes);
|
|
108
|
-
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
109
|
-
}
|
|
110
|
-
/** Lowercase hex SHA-256 of a consent's `state`: the name of its flow record. */
|
|
111
|
-
export async function oauthStateDigest(state) {
|
|
112
|
-
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(state));
|
|
113
|
-
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
114
|
-
}
|
|
115
|
-
function storedRefreshSpent(raw) {
|
|
116
|
-
const value = raw === null ? undefined : parsed(raw);
|
|
117
|
-
return plainObject(value) &&
|
|
118
|
-
value.connectaOAuthRefreshSpent === 1 &&
|
|
119
|
-
typeof value.epoch === "string" &&
|
|
120
|
-
typeof value.holder === "string" &&
|
|
121
|
-
typeof value.activeKey === "string" &&
|
|
122
|
-
(value.state === "outstanding" ||
|
|
123
|
-
value.state === "ambiguous" ||
|
|
124
|
-
(value.state === "resolved" && typeof value.resolvedAt === "number" && Number.isFinite(value.resolvedAt)))
|
|
125
|
-
? value
|
|
126
|
-
: undefined;
|
|
127
|
-
}
|
|
128
|
-
const REFRESH_REQUEST_DEADLINE_MS = 20_000;
|
|
129
|
-
const REFRESH_COMMIT_DEADLINE_MS = 20_000;
|
|
130
|
-
const REFRESH_WAIT_DEADLINE_MS = 35_000;
|
|
131
|
-
function refreshWait(signal, timeoutMs = REFRESH_WAIT_DEADLINE_MS) {
|
|
132
|
-
const deadline = new AbortController();
|
|
133
|
-
let timer = setTimeout(() => deadline.abort(), timeoutMs);
|
|
134
|
-
let closed = false;
|
|
135
|
-
let transferred = false;
|
|
136
|
-
let requestStarted = false;
|
|
137
|
-
const abort = () => deadline.abort(aborted(signal));
|
|
138
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
139
|
-
if (signal?.aborted)
|
|
140
|
-
abort();
|
|
141
|
-
return {
|
|
142
|
-
signal: deadline.signal,
|
|
143
|
-
get requestStarted() {
|
|
144
|
-
return requestStarted;
|
|
145
|
-
},
|
|
146
|
-
run: authMethod((operation) => {
|
|
147
|
-
return authFlow(function* () {
|
|
148
|
-
// A race's losing operation may still start. Check before constructing it,
|
|
149
|
-
// then again before handing a late result back to preparation.
|
|
150
|
-
if (deadline.signal.aborted)
|
|
151
|
-
return (yield Effect.fail(aborted(deadline.signal)));
|
|
152
|
-
return (yield authThen(Effect.raceAllFirst([fromSignal(deadline.signal), authCall(operation, [])]), (value) => {
|
|
153
|
-
if (deadline.signal.aborted)
|
|
154
|
-
throw aborted(deadline.signal);
|
|
155
|
-
return value;
|
|
156
|
-
}));
|
|
157
|
-
});
|
|
158
|
-
}),
|
|
159
|
-
transfer: () => {
|
|
160
|
-
if (deadline.signal.aborted)
|
|
161
|
-
throw aborted(deadline.signal);
|
|
162
|
-
if (transferred)
|
|
163
|
-
return;
|
|
164
|
-
// Synchronous with CAS submission: cancellation can prevent the write
|
|
165
|
-
// only before this point. Keep the independent preparation timer alive.
|
|
166
|
-
signal?.removeEventListener("abort", abort);
|
|
167
|
-
transferred = true;
|
|
168
|
-
},
|
|
169
|
-
beginRequest: () => {
|
|
170
|
-
if (deadline.signal.aborted)
|
|
171
|
-
throw aborted(deadline.signal);
|
|
172
|
-
requestStarted = true;
|
|
173
|
-
clearTimeout(timer);
|
|
174
|
-
timer = setTimeout(() => {
|
|
175
|
-
deadline.abort(new DOMException("OAuth refresh timed out", "TimeoutError"));
|
|
176
|
-
}, REFRESH_REQUEST_DEADLINE_MS);
|
|
177
|
-
},
|
|
178
|
-
close: () => {
|
|
179
|
-
if (closed)
|
|
180
|
-
return;
|
|
181
|
-
closed = true;
|
|
182
|
-
clearTimeout(timer);
|
|
183
|
-
signal?.removeEventListener("abort", abort);
|
|
184
|
-
},
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
function aborted(signal) {
|
|
188
|
-
return signal?.reason ?? new DOMException("This operation was aborted", "AbortError");
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* The outcome of a flight another request owns, failures included. Rejects
|
|
192
|
-
* only with this caller's own abort.
|
|
193
|
-
*
|
|
194
|
-
* The owner settles the flight from its own request. Settlement wakes this
|
|
195
|
-
* waiter through a Promise this request created, so the wait — and the
|
|
196
|
-
* caller's storage reads after it — resume here, never inside the owner's
|
|
197
|
-
* call (src/runtime/settlement.ts).
|
|
198
|
-
*/
|
|
199
|
-
function waitForRefreshFlight(flight, signal, deadline) {
|
|
200
|
-
const settled = flight.outcome.wait();
|
|
201
|
-
// The abort goes first, so a caller that has already left never joins.
|
|
202
|
-
return runEdge(Effect.raceAllFirst([
|
|
203
|
-
...(signal ? [fromSignal(signal)] : []),
|
|
204
|
-
...(deadline ? [fromSignal(deadline)] : []),
|
|
205
|
-
settled,
|
|
206
|
-
]));
|
|
207
|
-
}
|
|
208
|
-
/** Tokens as a token response the SDK parses: only the client stamps an issuer. */
|
|
209
|
-
function tokenResponse(tokens) {
|
|
210
|
-
const { issuer: _issuer, ...answer } = tokens;
|
|
211
|
-
return Response.json(answer);
|
|
212
|
-
}
|
|
213
|
-
function refreshTokensDigest(tokens) {
|
|
214
|
-
return runEdge(authProgram(refreshTokensDigestNative, [tokens]));
|
|
215
|
-
}
|
|
216
|
-
const refreshTokensDigestNative = /* @__PURE__ */ registerAuth(refreshTokensDigest, function (tokens) {
|
|
217
|
-
return authFlow(function* () {
|
|
218
|
-
const { issuer: _issuer, ...answer } = tokens;
|
|
219
|
-
return (yield authCall(oauthStateDigest, [
|
|
220
|
-
JSON.stringify(Object.fromEntries(Object.entries(answer).sort(([a], [b]) => a.localeCompare(b)))),
|
|
221
|
-
]));
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
function requestedRefreshToken(init) {
|
|
225
|
-
return init?.body instanceof URLSearchParams
|
|
226
|
-
? init.body.get("refresh_token")
|
|
227
|
-
: new URLSearchParams(String(init?.body ?? "")).get("refresh_token");
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* The token endpoint as one connector runtime's OAuth flows reach it.
|
|
231
|
-
*
|
|
232
|
-
* A refresh generation is redeemed once per owner partition: the first request
|
|
233
|
-
* owns the fetch, classifies the answer, and stores an accepted rotation, by
|
|
234
|
-
* compare-and-set, before anyone is released, so a cancelled owner or an SDK
|
|
235
|
-
* that redirects instead of saving cannot strand a spent refresh token.
|
|
236
|
-
* Requests that arrive meanwhile join its outcome; a request whose token read
|
|
237
|
-
* predates a stored refresh is handed that refresh instead of redeeming again.
|
|
238
|
-
* A shared-storage lease prevents concurrent redemption across isolates;
|
|
239
|
-
* the grant CAS also fences stale rotations.
|
|
240
|
-
*
|
|
241
|
-
* A code exchange claims its consent's flow record and re-checks the epoch in
|
|
242
|
-
* the turn that dispatches it (`KvOAuthProvider.claimCodeExchange`,
|
|
243
|
-
* `dispatchCodeExchange`). Every token answer the SDK parses is rebuilt from
|
|
244
|
-
* the OAuth `error` code alone unless it is a token response.
|
|
245
|
-
*/
|
|
246
|
-
export class OAuthRefreshCoordinator {
|
|
247
|
-
retainPartition;
|
|
248
|
-
flights = new Map();
|
|
249
|
-
constructor(retainPartition = () => () => { }) {
|
|
250
|
-
this.retainPartition = retainPartition;
|
|
251
|
-
}
|
|
252
|
-
coordinatedFetch(provider, baseFetch, requestSignal, defer, resourceFetch = baseFetch, prepareCredentialRequest) {
|
|
253
|
-
return authMethod((input, init) => {
|
|
254
|
-
return authFlow(function* () {
|
|
255
|
-
// Freeze the form before any await: classification and the spent digest
|
|
256
|
-
// must describe the exact token sent, even if its caller mutates a form.
|
|
257
|
-
init = init
|
|
258
|
-
? { ...init, ...(init.body instanceof URLSearchParams ? { body: new URLSearchParams(init.body) } : {}) }
|
|
259
|
-
: undefined;
|
|
260
|
-
// Production adapters resolve the required host policy before a code or
|
|
261
|
-
// refresh claim can consume its single-use credential. Custom SDK leaves
|
|
262
|
-
// may supply their own policy; this hook performs no I/O and owns no data.
|
|
263
|
-
if (isOAuthCredentialRequest(init))
|
|
264
|
-
prepareCredentialRequest?.();
|
|
265
|
-
if (isRefreshTokenRequest(init)) {
|
|
266
|
-
const signal = requestSignal && init?.signal
|
|
267
|
-
? AbortSignal.any([requestSignal, init.signal])
|
|
268
|
-
: (requestSignal ?? init?.signal ?? undefined);
|
|
269
|
-
return (yield authInvoke(this, "refresh", [provider, input, init, baseFetch, signal, defer]));
|
|
270
|
-
}
|
|
271
|
-
const grantType = tokenGrantType(init);
|
|
272
|
-
const tokenRequest = isOAuthCredentialRequest(init);
|
|
273
|
-
const exchange = grantType === "authorization_code";
|
|
274
|
-
if (exchange) {
|
|
275
|
-
const answered = (yield authInvoke(provider, "claimCodeExchange", []));
|
|
276
|
-
if (answered)
|
|
277
|
-
return answered;
|
|
278
|
-
}
|
|
279
|
-
const dispatch = authMethod(() => {
|
|
280
|
-
return authFlow(function* () {
|
|
281
|
-
if (requestSignal?.aborted)
|
|
282
|
-
throw aborted(requestSignal);
|
|
283
|
-
const signal = requestSignal
|
|
284
|
-
? init?.signal
|
|
285
|
-
? AbortSignal.any([requestSignal, init.signal])
|
|
286
|
-
: requestSignal
|
|
287
|
-
: init?.signal;
|
|
288
|
-
// Credential-bearing OAuth requests use the raw send path. A redirect
|
|
289
|
-
// is a dispatched failure, never authority to send the form again.
|
|
290
|
-
return (yield tokenRequest
|
|
291
|
-
? authCall(baseFetch, [input, { ...init, redirect: "manual", ...(signal ? { signal } : {}) }])
|
|
292
|
-
: authCall(resourceFetch, [input, { ...init, ...(signal ? { signal } : {}) }]));
|
|
293
|
-
}.bind(this));
|
|
294
|
-
});
|
|
295
|
-
// Await the composed fetch here so workerd associates a rejection with
|
|
296
|
-
// the fetch the SDK is already awaiting.
|
|
297
|
-
const response = (yield exchange
|
|
298
|
-
? authInvoke(provider, "dispatchCodeExchange", [dispatch, input])
|
|
299
|
-
: authCall(dispatch, []));
|
|
300
|
-
if (!tokenRequest)
|
|
301
|
-
return response;
|
|
302
|
-
// A code exchange's failure is the SDK's to log as well.
|
|
303
|
-
const forSdk = (yield authCall(sdkSafeTokenResponse, [
|
|
304
|
-
response,
|
|
305
|
-
exchange
|
|
306
|
-
? authMethod((tokens) => {
|
|
307
|
-
return authInvoke(provider, "recordCodeExchangeTokens", [tokens]);
|
|
308
|
-
})
|
|
309
|
-
: undefined,
|
|
310
|
-
]));
|
|
311
|
-
if (exchange && forSdk !== response)
|
|
312
|
-
provider.recordCodeExchangeRefusal(forSdk);
|
|
313
|
-
return forSdk;
|
|
314
|
-
}.bind(this));
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
refresh(provider, input, init, baseFetch, signal, defer) {
|
|
318
|
-
return runEdge(authOwnMethod(OAuthRefreshCoordinator.prototype, this, "refresh", [
|
|
319
|
-
provider,
|
|
320
|
-
input,
|
|
321
|
-
init,
|
|
322
|
-
baseFetch,
|
|
323
|
-
signal,
|
|
324
|
-
defer,
|
|
325
|
-
]));
|
|
326
|
-
}
|
|
327
|
-
redeem(provider, epoch, requested, input, init, baseFetch, waiting) {
|
|
328
|
-
return runEdge(authOwnMethod(OAuthRefreshCoordinator.prototype, this, "redeem", [
|
|
329
|
-
provider,
|
|
330
|
-
epoch,
|
|
331
|
-
requested,
|
|
332
|
-
input,
|
|
333
|
-
init,
|
|
334
|
-
baseFetch,
|
|
335
|
-
waiting,
|
|
336
|
-
]));
|
|
337
|
-
}
|
|
338
|
-
/** A reset fenced `epoch`: wake its joiners now rather than at the answer. */
|
|
339
|
-
retire(epoch) {
|
|
340
|
-
const flight = this.flights.get(epoch);
|
|
341
|
-
if (!flight)
|
|
342
|
-
return;
|
|
343
|
-
this.flights.delete(epoch);
|
|
344
|
-
flight.outcome.settle(Exit.succeed({ status: "retired" }));
|
|
345
|
-
}
|
|
346
|
-
static {
|
|
347
|
-
registerAuthMethod(OAuthRefreshCoordinator.prototype, "refresh", function (provider, input, init, baseFetch, signal, defer) {
|
|
348
|
-
return authFlow(function* () {
|
|
349
|
-
if (signal?.aborted)
|
|
350
|
-
throw aborted(signal);
|
|
351
|
-
const waiting = refreshWait(signal);
|
|
352
|
-
let redeeming = false;
|
|
353
|
-
try {
|
|
354
|
-
// A verdict belongs to one refresh; never let an older one decide this.
|
|
355
|
-
provider.recordRefreshFailure(undefined);
|
|
356
|
-
const epoch = (yield authInvoke(waiting, "run", [
|
|
357
|
-
authMethod(() => {
|
|
358
|
-
return authInvoke(provider, "flowEpoch", []);
|
|
359
|
-
}),
|
|
360
|
-
]));
|
|
361
|
-
const requested = requestedRefreshToken(init);
|
|
362
|
-
for (let attempt = 0; attempt < 8; attempt++) {
|
|
363
|
-
if (signal?.aborted)
|
|
364
|
-
throw aborted(signal);
|
|
365
|
-
const joined = this.flights.get(epoch);
|
|
366
|
-
if (joined) {
|
|
367
|
-
const outcome = (yield authCall(waitForRefreshFlight, [
|
|
368
|
-
joined,
|
|
369
|
-
signal,
|
|
370
|
-
waiting.signal,
|
|
371
|
-
]));
|
|
372
|
-
if (signal?.aborted)
|
|
373
|
-
throw aborted(signal);
|
|
374
|
-
if (outcome.status === "failed") {
|
|
375
|
-
provider.recordRefreshFailure(outcome.verdict);
|
|
376
|
-
throw outcome.error;
|
|
377
|
-
}
|
|
378
|
-
if (outcome.status === "refreshed")
|
|
379
|
-
return provider.adoptRefresh(outcome.tokens);
|
|
380
|
-
continue;
|
|
381
|
-
}
|
|
382
|
-
// Preparation reads share the same bound as later claim waits.
|
|
383
|
-
const current = (yield authInvoke(waiting, "run", [
|
|
384
|
-
authMethod(() => {
|
|
385
|
-
return authInvoke(provider, "storedTokens", [waiting.signal]);
|
|
386
|
-
}),
|
|
387
|
-
]));
|
|
388
|
-
if (this.flights.has(epoch))
|
|
389
|
-
continue;
|
|
390
|
-
if (current?.refresh_token &&
|
|
391
|
-
(current.refresh_token !== requested || provider.refreshedSinceRead(current, epoch))) {
|
|
392
|
-
return provider.adoptRefresh(current);
|
|
393
|
-
}
|
|
394
|
-
if (current?.refresh_token !== requested) {
|
|
395
|
-
provider.recordRefreshFailure({ kind: "dead" });
|
|
396
|
-
return Response.json({ error: "invalid_grant", error_description: "Refresh token is no longer active." }, { status: 400 });
|
|
397
|
-
}
|
|
398
|
-
redeeming = true;
|
|
399
|
-
// Claim submission transfers renewal, including preparation, token
|
|
400
|
-
// request and commit, to a bounded native coordinator. Only
|
|
401
|
-
// this caller's wait races its signal; the coordinator receives
|
|
402
|
-
// no request/call Context authority or caller-owned deadline.
|
|
403
|
-
const redemption = authDetached(authInvoke(this, "redeem", [
|
|
404
|
-
provider,
|
|
405
|
-
epoch,
|
|
406
|
-
requested,
|
|
407
|
-
input,
|
|
408
|
-
init,
|
|
409
|
-
baseFetch,
|
|
410
|
-
waiting,
|
|
411
|
-
]).pipe(Effect.ensuring(Effect.sync(() => waiting.close()))), defer);
|
|
412
|
-
return (yield signal ? Effect.raceFirst(redemption, fromSignal(signal)) : redemption);
|
|
413
|
-
}
|
|
414
|
-
throw provider.refreshContended();
|
|
415
|
-
}
|
|
416
|
-
catch (error) {
|
|
417
|
-
if (signal?.aborted)
|
|
418
|
-
throw aborted(signal);
|
|
419
|
-
if (waiting.signal.aborted && !waiting.requestStarted)
|
|
420
|
-
throw provider.refreshContended();
|
|
421
|
-
throw error;
|
|
422
|
-
}
|
|
423
|
-
finally {
|
|
424
|
-
if (!redeeming)
|
|
425
|
-
waiting.close();
|
|
426
|
-
}
|
|
427
|
-
}.bind(this));
|
|
428
|
-
}, OAuthRefreshCoordinator.prototype.refresh);
|
|
429
|
-
registerAuthMethod(OAuthRefreshCoordinator.prototype, "redeem", function (provider, epoch, requested, input, init, baseFetch, waiting) {
|
|
430
|
-
return authFlow(function* () {
|
|
431
|
-
const flight = { outcome: new Settlement() };
|
|
432
|
-
this.flights.set(epoch, flight);
|
|
433
|
-
// Pin the owner partition until this coordinator exits, whoever is waiting.
|
|
434
|
-
const release = this.retainPartition();
|
|
435
|
-
let settled = false;
|
|
436
|
-
const settle = (outcome) => {
|
|
437
|
-
if (settled)
|
|
438
|
-
return;
|
|
439
|
-
settled = true;
|
|
440
|
-
if (this.flights.get(epoch) === flight)
|
|
441
|
-
this.flights.delete(epoch);
|
|
442
|
-
release();
|
|
443
|
-
// Last: joined waiters wake in their own requests, after this.
|
|
444
|
-
flight.outcome.settle(Exit.succeed(outcome));
|
|
445
|
-
};
|
|
446
|
-
const fail = (error, verdict) => {
|
|
447
|
-
if (verdict)
|
|
448
|
-
provider.recordRefreshFailure(verdict);
|
|
449
|
-
settle({ status: "failed", error, ...(verdict ? { verdict } : {}) });
|
|
450
|
-
};
|
|
451
|
-
let dispatched = false;
|
|
452
|
-
const unknownOutcome = () => new UnauthorizedError("OAuth refresh outcome is unknown; authorization required.");
|
|
453
|
-
// One budget for the entire commit/cleanup phase, including retries
|
|
454
|
-
// and lease release. Interrupt native continuations, never detach a
|
|
455
|
-
// Promise whose late read could still start a credential write.
|
|
456
|
-
const complete = authMethod((operation) => {
|
|
457
|
-
const completion = refreshWait(undefined, REFRESH_COMMIT_DEADLINE_MS);
|
|
458
|
-
return authInvoke(completion, "run", [operation]).pipe(Effect.catch((error) => Effect.fail(completion.signal.aborted ? unknownOutcome() : error)), Effect.ensuring(Effect.sync(() => completion.close())));
|
|
459
|
-
});
|
|
460
|
-
try {
|
|
461
|
-
// The authorization server this redemption answers to, fixed now.
|
|
462
|
-
const issuer = provider.refreshIssuer(epoch);
|
|
463
|
-
let lease;
|
|
464
|
-
try {
|
|
465
|
-
const claim = (yield authInvoke(provider, "claimRefresh", [epoch, requested, undefined, waiting]));
|
|
466
|
-
if (claim instanceof Response) {
|
|
467
|
-
const tokens = (yield authInvoke(waiting, "run", [
|
|
468
|
-
authMethod(() => {
|
|
469
|
-
return authInvoke(provider, "storedTokens", [waiting.signal]);
|
|
470
|
-
}),
|
|
471
|
-
]));
|
|
472
|
-
if (tokens)
|
|
473
|
-
settle({ status: "refreshed", tokens });
|
|
474
|
-
else
|
|
475
|
-
settle({ status: "retired" });
|
|
476
|
-
return claim;
|
|
477
|
-
}
|
|
478
|
-
lease = claim;
|
|
479
|
-
}
|
|
480
|
-
catch (error) {
|
|
481
|
-
fail(error, provider.refreshVerdict());
|
|
482
|
-
throw error;
|
|
483
|
-
}
|
|
484
|
-
const releaseLease = authMethod((verdict, resolution = "ambiguous") => {
|
|
485
|
-
return authFlow(function* () {
|
|
486
|
-
(yield authInvoke(provider, "releaseRefresh", [lease, verdict, undefined, resolution]));
|
|
487
|
-
}.bind(this));
|
|
488
|
-
});
|
|
489
|
-
let outcome;
|
|
490
|
-
const deadline = waiting.signal;
|
|
491
|
-
try {
|
|
492
|
-
const read = authFlow(function* () {
|
|
493
|
-
const response = (yield authInvoke(provider, "dispatchRefresh", [
|
|
494
|
-
lease,
|
|
495
|
-
authMethod(() => authFlow(function* () {
|
|
496
|
-
waiting.beginRequest();
|
|
497
|
-
dispatched = true;
|
|
498
|
-
return (yield authCall(baseFetch, [
|
|
499
|
-
input,
|
|
500
|
-
{ ...init, redirect: "manual", signal: deadline },
|
|
501
|
-
]));
|
|
502
|
-
}.bind(this))),
|
|
503
|
-
waiting,
|
|
504
|
-
]));
|
|
505
|
-
if (deadline.aborted) {
|
|
506
|
-
void response.body?.cancel().catch(() => { });
|
|
507
|
-
throw aborted(deadline);
|
|
508
|
-
}
|
|
509
|
-
return (yield authCall(refreshResponseOutcome, [response, deadline]));
|
|
510
|
-
}.bind(this));
|
|
511
|
-
outcome = (yield Effect.raceFirst(read, fromSignal(deadline)));
|
|
512
|
-
if (deadline.aborted)
|
|
513
|
-
throw aborted(deadline);
|
|
514
|
-
}
|
|
515
|
-
catch (error) {
|
|
516
|
-
if (!dispatched) {
|
|
517
|
-
// Only the coordinator's own preparation deadline can interrupt a
|
|
518
|
-
// submitted claim. Caller cancellation no longer reaches this work.
|
|
519
|
-
if (!waiting.signal.aborted) {
|
|
520
|
-
(yield authCatch(authInvoke(waiting, "run", [
|
|
521
|
-
authMethod(() => {
|
|
522
|
-
return authCall(releaseLease, []);
|
|
523
|
-
}),
|
|
524
|
-
]), () => { }));
|
|
525
|
-
if (provider.refreshVerdict()?.kind === "dead") {
|
|
526
|
-
(yield authCatch(authInvoke(waiting, "run", [
|
|
527
|
-
authMethod(() => {
|
|
528
|
-
return authInvoke(provider, "discardRefusedGrant", [requested, epoch]);
|
|
529
|
-
}),
|
|
530
|
-
]), () => { }));
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
const failure = waiting.signal.aborted ? provider.refreshContended() : error;
|
|
534
|
-
fail(failure, provider.refreshVerdict());
|
|
535
|
-
throw failure;
|
|
536
|
-
}
|
|
537
|
-
// A sent request without a definitive answer may have rotated the token.
|
|
538
|
-
// Persist the refusal before dropping tokens, so interrupted cleanup or a
|
|
539
|
-
// later write of the same fingerprint cannot make it redeemable again.
|
|
540
|
-
const failure = new UnauthorizedError("OAuth refresh outcome is unknown; authorization required.");
|
|
541
|
-
(yield authCall(complete, [
|
|
542
|
-
authMethod(() => authFlow(function* () {
|
|
543
|
-
(yield authCall(releaseLease, [{ kind: "dead" }]));
|
|
544
|
-
(yield authInvoke(provider, "discardRefusedGrant", [requested, epoch]));
|
|
545
|
-
})),
|
|
546
|
-
]));
|
|
547
|
-
fail(failure, { kind: "dead" });
|
|
548
|
-
throw failure;
|
|
549
|
-
}
|
|
550
|
-
finally {
|
|
551
|
-
waiting.close();
|
|
552
|
-
}
|
|
553
|
-
return (yield authCall(complete, [
|
|
554
|
-
authMethod(() => authFlow(function* () {
|
|
555
|
-
// An answer exists from here on, and is committed whatever the owner does.
|
|
556
|
-
if (outcome.failure) {
|
|
557
|
-
// Drop a refused grant while the flight still stands: a caller arriving
|
|
558
|
-
// meanwhile joins it instead of redeeming the dead token again.
|
|
559
|
-
(yield authInvoke(provider, "discardRefusedGrant", [requested, epoch]));
|
|
560
|
-
(yield authCall(releaseLease, [outcome.verdict, "resolved"]));
|
|
561
|
-
fail(outcome.failure, outcome.verdict);
|
|
562
|
-
return outcome.forSdk;
|
|
563
|
-
}
|
|
564
|
-
// The server consumed the old refresh token; the SDK merges it back the
|
|
565
|
-
// same way when a rotation omits one.
|
|
566
|
-
const accepted = {
|
|
567
|
-
...outcome.tokens,
|
|
568
|
-
...(outcome.tokens.refresh_token === undefined && requested !== null
|
|
569
|
-
? { refresh_token: requested }
|
|
570
|
-
: {}),
|
|
571
|
-
};
|
|
572
|
-
let committed;
|
|
573
|
-
try {
|
|
574
|
-
committed = (yield authInvoke(provider, "storeRefresh", [epoch, requested, accepted, issuer]));
|
|
575
|
-
}
|
|
576
|
-
catch {
|
|
577
|
-
const failure = new UnauthorizedError("OAuth refresh could not be committed; authorization required.");
|
|
578
|
-
(yield authCall(releaseLease, [{ kind: "dead" }, "resolved"]));
|
|
579
|
-
(yield authInvoke(provider, "discardRefusedGrant", [requested, epoch]));
|
|
580
|
-
fail(failure, { kind: "dead" });
|
|
581
|
-
throw failure;
|
|
582
|
-
}
|
|
583
|
-
if (!committed) {
|
|
584
|
-
// Restart, Disconnect, or another consent decides the newer grant.
|
|
585
|
-
(yield authCall(releaseLease, [{ kind: "dead" }, "resolved"]));
|
|
586
|
-
const failure = new UnauthorizedError("OAuth refresh grant is no longer active.");
|
|
587
|
-
fail(failure, { kind: "dead" });
|
|
588
|
-
throw failure;
|
|
589
|
-
}
|
|
590
|
-
(yield authInvoke(provider, "releaseRefresh", [lease, undefined, committed, "resolved"]));
|
|
591
|
-
settle({ status: "refreshed", tokens: committed });
|
|
592
|
-
return provider.adoptRefresh(committed);
|
|
593
|
-
})),
|
|
594
|
-
]));
|
|
595
|
-
}
|
|
596
|
-
catch (error) {
|
|
597
|
-
fail(error, dispatched ? { kind: "dead" } : provider.refreshVerdict());
|
|
598
|
-
throw error;
|
|
599
|
-
}
|
|
600
|
-
finally {
|
|
601
|
-
waiting.close();
|
|
602
|
-
// No storage in this finalizer: even interruption or a failed
|
|
603
|
-
// cleanup must drop the local flight and its owner retention.
|
|
604
|
-
if (!settled)
|
|
605
|
-
fail(unknownOutcome(), dispatched ? { kind: "dead" } : provider.refreshVerdict());
|
|
606
|
-
}
|
|
607
|
-
}.bind(this));
|
|
608
|
-
}, OAuthRefreshCoordinator.prototype.redeem);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
/**
|
|
612
|
-
* One refresh coordinator per owner partition, for one connector runtime.
|
|
613
|
-
* Long-lived enough for distinct request scopes to join one token redemption;
|
|
614
|
-
* it owns no client, transport, or request state. Production contexts carry a
|
|
615
|
-
* stable registry partition; hand-written contexts coordinate by their shared
|
|
616
|
-
* storage object instead.
|
|
617
|
-
*/
|
|
618
|
-
export function refreshCoordinatorsByPartition() {
|
|
619
|
-
const coordinators = new WeakMap();
|
|
620
|
-
return (ctx) => {
|
|
621
|
-
const partition = oauthPartitionFor(ctx) ?? ctx.storage;
|
|
622
|
-
let coordinator = coordinators.get(partition);
|
|
623
|
-
if (!coordinator) {
|
|
624
|
-
coordinator = new OAuthRefreshCoordinator(() => retainOAuthPartition(partition));
|
|
625
|
-
coordinators.set(partition, coordinator);
|
|
626
|
-
}
|
|
627
|
-
return coordinator;
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
const GRANT = oauthGrantKeys.grant;
|
|
631
|
-
/** The epoch of an owner with no grant record yet. */
|
|
632
|
-
const INITIAL_EPOCH = "initial";
|
|
633
|
-
/** The epoch of a record that does not parse: readers see no grant in it. */
|
|
634
|
-
const UNREADABLE_EPOCH = "unreadable";
|
|
635
|
-
const ACTIVE_EPOCH_PREFIX = "v3:";
|
|
636
|
-
const DISCONNECTED_EPOCH_PREFIX = "disconnected:";
|
|
637
|
-
const GRANT_VERSION = 3;
|
|
638
|
-
const FLOW_VERSION = 1;
|
|
639
|
-
/** Attempts at one grant write while other writers keep winning. */
|
|
640
|
-
const MAX_GRANT_WRITES = 32;
|
|
641
|
-
/**
|
|
642
|
-
* How long a pending authorization URL may be handed out again instead of
|
|
643
|
-
* starting a fresh flow. The authorization server's half of it expires: a
|
|
644
|
-
* pushed request URI lives seconds to minutes (RFC 9126), and login
|
|
645
|
-
* transactions are commonly held for minutes. Past this age a reissued URL
|
|
646
|
-
* more likely lands on an expired session than on consent, and a fresh start
|
|
647
|
-
* costs one authorization request, not a registration. It sits inside the
|
|
648
|
-
* flow record's fifteen minutes.
|
|
649
|
-
*/
|
|
650
|
-
const PENDING_AUTHORIZATION_MAX_AGE_MS = 10 * 60 * 1000;
|
|
651
|
-
function discoveryBinding(state) {
|
|
652
|
-
return runEdge(authProgram(discoveryBindingNative, [state]));
|
|
653
|
-
}
|
|
654
|
-
const discoveryBindingNative = /* @__PURE__ */ registerAuth(discoveryBinding, function (state) {
|
|
655
|
-
return authFlow(function* () {
|
|
656
|
-
return state
|
|
657
|
-
? {
|
|
658
|
-
issuer: discoveryIssuer(state),
|
|
659
|
-
tokenEndpoint: state.authorizationServerMetadata?.token_endpoint,
|
|
660
|
-
...(state.authorizationServerMetadata?.authorization_response_iss_parameter_supported === true
|
|
661
|
-
? { issRequired: true }
|
|
662
|
-
: {}),
|
|
663
|
-
discovery: (yield authCall(oauthStateDigest, [JSON.stringify(state)])),
|
|
664
|
-
}
|
|
665
|
-
: {};
|
|
666
|
-
});
|
|
667
|
-
});
|
|
668
|
-
function plainObject(value) {
|
|
669
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
670
|
-
}
|
|
671
|
-
function parsed(raw) {
|
|
672
|
-
try {
|
|
673
|
-
return JSON.parse(raw);
|
|
674
|
-
}
|
|
675
|
-
catch {
|
|
676
|
-
return undefined;
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
function storedGrant(raw) {
|
|
680
|
-
const value = parsed(raw);
|
|
681
|
-
return plainObject(value) &&
|
|
682
|
-
value.connectaOAuth === GRANT_VERSION &&
|
|
683
|
-
typeof value.epoch === "string" &&
|
|
684
|
-
(value.flow === undefined || typeof value.flow === "string") &&
|
|
685
|
-
(value.sealed === undefined || typeof value.sealed === "string")
|
|
686
|
-
? value
|
|
687
|
-
: undefined;
|
|
688
|
-
}
|
|
689
|
-
/** The epoch a raw grant record names, without opening its body. */
|
|
690
|
-
function epochOf(raw) {
|
|
691
|
-
if (raw === null)
|
|
692
|
-
return INITIAL_EPOCH;
|
|
693
|
-
return storedGrant(raw)?.epoch ?? UNREADABLE_EPOCH;
|
|
694
|
-
}
|
|
695
|
-
/** Nothing here trusts stored bytes: keep only what has the expected shape. */
|
|
696
|
-
function grantBody(value) {
|
|
697
|
-
if (!plainObject(value))
|
|
698
|
-
return {};
|
|
699
|
-
const { issuer, client, tokens, discovery, refreshConsent } = value;
|
|
700
|
-
return {
|
|
701
|
-
...(typeof issuer === "string" ? { issuer } : {}),
|
|
702
|
-
...(plainObject(client) && plainObject(client.value) && typeof client.value.client_id === "string"
|
|
703
|
-
? {
|
|
704
|
-
client: {
|
|
705
|
-
value: client.value,
|
|
706
|
-
...(typeof client.binding === "string" ? { binding: client.binding } : {}),
|
|
707
|
-
...(client.carried === true ? { carried: true } : {}),
|
|
708
|
-
...(client.registrationPath === "cimd" ||
|
|
709
|
-
client.registrationPath === "dcr" ||
|
|
710
|
-
client.registrationPath === "static"
|
|
711
|
-
? { registrationPath: client.registrationPath }
|
|
712
|
-
: {}),
|
|
713
|
-
},
|
|
714
|
-
}
|
|
715
|
-
: {}),
|
|
716
|
-
...(plainObject(tokens) && typeof tokens.access_token === "string"
|
|
717
|
-
? { tokens: tokens }
|
|
718
|
-
: {}),
|
|
719
|
-
...(discoveryIssuer(discovery) !== undefined ? { discovery: discovery } : {}),
|
|
720
|
-
...(plainObject(refreshConsent) &&
|
|
721
|
-
typeof refreshConsent.digest === "string" &&
|
|
722
|
-
typeof refreshConsent.resolution === "string"
|
|
723
|
-
? { refreshConsent: { digest: refreshConsent.digest, resolution: refreshConsent.resolution } }
|
|
724
|
-
: {}),
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
/** What an invalidation compares: the value exactly as this flow saw it. */
|
|
728
|
-
function fingerprint(value) {
|
|
729
|
-
return value === undefined ? undefined : JSON.stringify(value);
|
|
730
|
-
}
|
|
731
|
-
function isDisconnectedEpoch(epoch) {
|
|
732
|
-
return epoch.startsWith(DISCONNECTED_EPOCH_PREFIX);
|
|
733
|
-
}
|
|
734
37
|
/**
|
|
735
38
|
* OAuthClientProvider over one owner's grant record and its consents' flow
|
|
736
39
|
* records, in one connector's namespace.
|
|
@@ -743,6 +46,11 @@ function isDisconnectedEpoch(epoch) {
|
|
|
743
46
|
* to one epoch. Its reads fail once another epoch is live, and its writes are
|
|
744
47
|
* compare-and-sets against the record it read, so a restart or disconnect,
|
|
745
48
|
* which replaces the epoch, fences every flow that began before it.
|
|
49
|
+
*
|
|
50
|
+
* Each native method is defined once, in `./downstream-oauth/` by seam, and
|
|
51
|
+
* `authMethods` installs it with its Promise edge; the class declares only the
|
|
52
|
+
* edge's type. State those bodies read is not `private` for that reason; it
|
|
53
|
+
* remains internal to src/auth.
|
|
746
54
|
*/
|
|
747
55
|
export class KvOAuthProvider {
|
|
748
56
|
connectorId;
|
|
@@ -787,6 +95,17 @@ export class KvOAuthProvider {
|
|
|
787
95
|
refreshBasis;
|
|
788
96
|
/** The access token of a refresh the coordinator already stored. */
|
|
789
97
|
storedRefresh;
|
|
98
|
+
/**
|
|
99
|
+
* @internal When this flow's last token request other than a refresh was
|
|
100
|
+
* sent. The tokens the SDK saves from its answer expire counted from here,
|
|
101
|
+
* never from the later save, which a slow answer would push past expiry.
|
|
102
|
+
*/
|
|
103
|
+
tokenRequestSent;
|
|
104
|
+
/**
|
|
105
|
+
* The tokens of the grant this provider last opened or wrote, as facts
|
|
106
|
+
* only: no credential beyond the access token it already handed out.
|
|
107
|
+
*/
|
|
108
|
+
observed;
|
|
790
109
|
constructor(connectorId, storage, redirectUri, refreshCoordinator, allowAuthorization = true,
|
|
791
110
|
/** Present when the deployment's vault can seal; the grant is then ciphertext. */
|
|
792
111
|
sealer,
|
|
@@ -818,9 +137,31 @@ export class KvOAuthProvider {
|
|
|
818
137
|
if (client)
|
|
819
138
|
this.addClientAuthentication = (headers, params) => authenticateRemoteClient(client, headers, params);
|
|
820
139
|
}
|
|
821
|
-
/**
|
|
822
|
-
|
|
823
|
-
|
|
140
|
+
/**
|
|
141
|
+
* @internal The access token a call should renew before sending it: the
|
|
142
|
+
* one this provider last read, refreshable and within
|
|
143
|
+
* OAUTH_REFRESH_SKEW_MS of its stored expiry, in the epoch it is bound to.
|
|
144
|
+
* Memory only: no storage read, refresh or discovery. A grant without an
|
|
145
|
+
* expiry is never due; a 401 renews it.
|
|
146
|
+
*/
|
|
147
|
+
renewalDue(now = Date.now()) {
|
|
148
|
+
const seen = this.observed;
|
|
149
|
+
if (!seen?.refreshable || seen.expiresAt === undefined)
|
|
150
|
+
return undefined;
|
|
151
|
+
if (this.bound && seen.epoch !== this.epoch)
|
|
152
|
+
return undefined;
|
|
153
|
+
return seen.expiresAt - OAUTH_REFRESH_SKEW_MS <= now ? seen.accessToken : undefined;
|
|
154
|
+
}
|
|
155
|
+
observe(grant) {
|
|
156
|
+
const { tokens, expiresAt } = grant.body;
|
|
157
|
+
this.observed = tokens
|
|
158
|
+
? {
|
|
159
|
+
epoch: grant.epoch,
|
|
160
|
+
accessToken: tokens.access_token,
|
|
161
|
+
refreshable: typeof tokens.refresh_token === "string" && tokens.refresh_token !== "",
|
|
162
|
+
...(expiresAt !== undefined ? { expiresAt } : {}),
|
|
163
|
+
}
|
|
164
|
+
: undefined;
|
|
824
165
|
}
|
|
825
166
|
get redirectUrl() {
|
|
826
167
|
return this.redirectUri;
|
|
@@ -829,106 +170,17 @@ export class KvOAuthProvider {
|
|
|
829
170
|
const client = this.clientOptions?.client;
|
|
830
171
|
return downstreamClientMetadata(this.redirectUri, this.scope, this.clientOptions?.name, client ? remoteClientAuthMethod(client) : "none");
|
|
831
172
|
}
|
|
832
|
-
/** A preparation await owns no continuation after its call/request closes.
|
|
833
|
-
* null explicitly selects coordinator/reset ownership, never the caller. */
|
|
834
|
-
wait(work, signal = this.signal) {
|
|
835
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "wait", [work, signal]));
|
|
836
|
-
}
|
|
837
|
-
/** Legacy helpers must fence their inner reads/deletes too, not just our join. */
|
|
838
|
-
preparationStorage(signal) {
|
|
839
|
-
if (!signal)
|
|
840
|
-
return this.storage;
|
|
841
|
-
const storage = this.storage;
|
|
842
|
-
return {
|
|
843
|
-
...storage,
|
|
844
|
-
get: authMethod((key) => {
|
|
845
|
-
return authInvoke(this, "wait", [
|
|
846
|
-
authMethod(() => {
|
|
847
|
-
return authStorage(storage, "get", [key]);
|
|
848
|
-
}),
|
|
849
|
-
signal,
|
|
850
|
-
]);
|
|
851
|
-
}),
|
|
852
|
-
list: authMethod((prefix) => {
|
|
853
|
-
return authInvoke(this, "wait", [
|
|
854
|
-
authMethod(() => {
|
|
855
|
-
return authStorage(storage, "list", [prefix]);
|
|
856
|
-
}),
|
|
857
|
-
signal,
|
|
858
|
-
]);
|
|
859
|
-
}),
|
|
860
|
-
delete: authMethod((key) => {
|
|
861
|
-
return authInvoke(this, "wait", [
|
|
862
|
-
authMethod(() => {
|
|
863
|
-
return authStorage(storage, "delete", [key]);
|
|
864
|
-
}),
|
|
865
|
-
signal,
|
|
866
|
-
]);
|
|
867
|
-
}),
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
173
|
// --- the grant record ---------------------------------------------------
|
|
871
174
|
/** The grant's additional authenticated data: record and epoch. */
|
|
872
175
|
grantLabel(epoch) {
|
|
873
176
|
return `${GRANT}:${epoch}`;
|
|
874
177
|
}
|
|
875
|
-
encodeGrant(grant, signal = this.signal) {
|
|
876
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "encodeGrant", [grant, signal]));
|
|
877
|
-
}
|
|
878
|
-
/**
|
|
879
|
-
* Open a stored grant. A body that does not open — tampered, under a
|
|
880
|
-
* rotated key, moved from another connector, owner, or epoch, or sealed
|
|
881
|
-
* with no sealer configured — reads as empty: authorization again.
|
|
882
|
-
*/
|
|
883
|
-
openGrant(raw, signal = this.signal) {
|
|
884
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "openGrant", [raw, signal]));
|
|
885
|
-
}
|
|
886
|
-
/**
|
|
887
|
-
* The grant as stored now, migrating layout 2 on first sight. A plaintext
|
|
888
|
-
* body, written before the vault could seal, is sealed where it lies by
|
|
889
|
-
* compare-and-set; a failure leaves it readable for the next write to seal.
|
|
890
|
-
*/
|
|
891
|
-
readGrant(signal = this.signal) {
|
|
892
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "readGrant", [signal]));
|
|
893
|
-
}
|
|
894
|
-
/** Layout 2 migration and its cleanup retain the reading operation's owner. */
|
|
895
|
-
migrate(signal) {
|
|
896
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "migrate", [signal]));
|
|
897
|
-
}
|
|
898
|
-
/** Deletion is retried on every read, including reads after a reset. */
|
|
899
|
-
cleanupV2(raw, signal = this.signal) {
|
|
900
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "cleanupV2", [raw, signal]));
|
|
901
|
-
}
|
|
902
|
-
/** The grant a bound flow reads: a replaced epoch fails it. */
|
|
903
|
-
boundGrant(signal = this.signal) {
|
|
904
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "boundGrant", [signal]));
|
|
905
|
-
}
|
|
906
|
-
/** SDK preparation is call/request-owned; coordinator reads explicitly opt out. */
|
|
907
|
-
activeGrant() {
|
|
908
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "activeGrant", []));
|
|
909
|
-
}
|
|
910
|
-
/**
|
|
911
|
-
* The one way a grant changes: read, change, compare-and-set, again while
|
|
912
|
-
* other writers win. The write belongs to `epoch` (default: the flow's,
|
|
913
|
-
* captured now if unset) and lands nowhere once another is live: `quiet`
|
|
914
|
-
* drops it silently, otherwise a bound flow fails as superseded. `commit`
|
|
915
|
-
* writes after the request's abort too. `change` returning undefined
|
|
916
|
-
* leaves the grant as it is.
|
|
917
|
-
*/
|
|
918
|
-
updateGrant(change, options = {}) {
|
|
919
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "updateGrant", [change, options]));
|
|
920
|
-
}
|
|
921
|
-
/** Store a client or tokens for `issuer`'s grant, replacing another server's. */
|
|
922
|
-
writeCredential(field, value, issuer, refreshConsent) {
|
|
923
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "writeCredential", [field, value, issuer, refreshConsent]));
|
|
924
|
-
}
|
|
925
178
|
withIssuer(grant, body) {
|
|
926
179
|
if (grant.body.issuer !== undefined && grant.body.issuer !== body.issuer) {
|
|
927
180
|
if (this.codeExchange)
|
|
928
181
|
throw this.flowSuperseded();
|
|
929
182
|
return {
|
|
930
183
|
epoch: `${ACTIVE_EPOCH_PREFIX}${crypto.randomUUID()}`,
|
|
931
|
-
...(grant.cleanupPending ? { cleanupPending: true } : {}),
|
|
932
184
|
body,
|
|
933
185
|
};
|
|
934
186
|
}
|
|
@@ -938,12 +190,6 @@ export class KvOAuthProvider {
|
|
|
938
190
|
configuredAuthorizationBinding() {
|
|
939
191
|
return undefined;
|
|
940
192
|
}
|
|
941
|
-
bindingForGrant(grant) {
|
|
942
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "bindingForGrant", [grant]));
|
|
943
|
-
}
|
|
944
|
-
checkFlowBinding(grant) {
|
|
945
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "checkFlowBinding", [grant]));
|
|
946
|
-
}
|
|
947
193
|
/** Validate RFC 9207 against the server this consent selected. */
|
|
948
194
|
validateCallbackIssuer(issuer) {
|
|
949
195
|
if (!this.callback ||
|
|
@@ -957,55 +203,9 @@ export class KvOAuthProvider {
|
|
|
957
203
|
flowLabel(key, epoch) {
|
|
958
204
|
return `${key}:${epoch}`;
|
|
959
205
|
}
|
|
960
|
-
encodeFlow(key, flow) {
|
|
961
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "encodeFlow", [key, flow]));
|
|
962
|
-
}
|
|
963
|
-
/** A flow record, its verifier opened only when `withVerifier`. */
|
|
964
|
-
openFlow(key, raw, withVerifier) {
|
|
965
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "openFlow", [key, raw, withVerifier]));
|
|
966
|
-
}
|
|
967
|
-
/** The live epoch's latest consent, until a callback claims it. */
|
|
968
|
-
latestConsent() {
|
|
969
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "latestConsent", []));
|
|
970
|
-
}
|
|
971
|
-
// --- flows and epochs -----------------------------------------------------
|
|
972
|
-
/** The live epoch; an owner with no grant yet has the initial one. */
|
|
973
|
-
liveEpoch() {
|
|
974
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "liveEpoch", []));
|
|
975
|
-
}
|
|
976
|
-
/** True only after an operator disconnect, until an explicit start replaces it. */
|
|
977
|
-
operatorDisconnected() {
|
|
978
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "operatorDisconnected", []));
|
|
979
|
-
}
|
|
980
206
|
isOperatorDisconnectedEpoch(epoch) {
|
|
981
207
|
return isDisconnectedEpoch(epoch);
|
|
982
208
|
}
|
|
983
|
-
/**
|
|
984
|
-
* Begin a flow — a connect, whose 401 may refresh or start consent, or an
|
|
985
|
-
* `api()` call or start — in the live epoch, and return it. Nothing about
|
|
986
|
-
* the grant is decided here: a grant belongs to one server by construction.
|
|
987
|
-
* An expected epoch continues an existing attempt; a replacement fails it.
|
|
988
|
-
*/
|
|
989
|
-
beginFlow(expectedEpoch) {
|
|
990
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "beginFlow", [expectedEpoch]));
|
|
991
|
-
}
|
|
992
|
-
/** The epoch this flow is bound to, or the live one. */
|
|
993
|
-
flowEpoch() {
|
|
994
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "flowEpoch", []));
|
|
995
|
-
}
|
|
996
|
-
/**
|
|
997
|
-
* Bind a code exchange: to the epoch its verified consent was written in, or
|
|
998
|
-
* the live one for an exchange no callback verified. An epoch already
|
|
999
|
-
* replaced fails here, before the SDK can send anything. What the grant
|
|
1000
|
-
* holds now is all a refused code may invalidate.
|
|
1001
|
-
*/
|
|
1002
|
-
bindFlow() {
|
|
1003
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "bindFlow", []));
|
|
1004
|
-
}
|
|
1005
|
-
/** Retain an already verified snapshot only for the exact same state. */
|
|
1006
|
-
verifyCallbackState(state) {
|
|
1007
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "verifyCallbackState", [state]));
|
|
1008
|
-
}
|
|
1009
209
|
/** Whether `verifyState` matched a consent on this provider. */
|
|
1010
210
|
verified() {
|
|
1011
211
|
return this.callback !== undefined;
|
|
@@ -1038,19 +238,9 @@ export class KvOAuthProvider {
|
|
|
1038
238
|
const failure = this.refreshFailure;
|
|
1039
239
|
if (failure?.kind === "contended")
|
|
1040
240
|
return this.refreshContended();
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
clientInformation(ctx) {
|
|
1045
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "clientInformation", [
|
|
1046
|
-
ctx,
|
|
1047
|
-
]));
|
|
1048
|
-
}
|
|
1049
|
-
saveClientInformation(info, ctx) {
|
|
1050
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "saveClientInformation", [info, ctx]));
|
|
1051
|
-
}
|
|
1052
|
-
registrationPath() {
|
|
1053
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "registrationPath", []));
|
|
241
|
+
// Only the class reaches callers: remoteMcp replaces this error with its own
|
|
242
|
+
// typed auth error, so this text keeps the meta name.
|
|
243
|
+
return new UnauthorizedError(`Authorization required. Use ${META_ROUTES.authorize} or Connect to start consent.`);
|
|
1054
244
|
}
|
|
1055
245
|
/** Intake and saved URLs share a guard. Include the current grant's secrets
|
|
1056
246
|
* so a fresh request can reject an older cache before sending credentials. */
|
|
@@ -1102,130 +292,18 @@ export class KvOAuthProvider {
|
|
|
1102
292
|
throw new ConnectorCallError("connector_call_failed", "OAuth metadata or consent URL contains a credential; refusing authorization.", { retryable: false });
|
|
1103
293
|
}
|
|
1104
294
|
}
|
|
1105
|
-
/**
|
|
1106
|
-
|
|
1107
|
-
return
|
|
1108
|
-
}
|
|
1109
|
-
discoveryState() {
|
|
1110
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "discoveryState", []));
|
|
1111
|
-
}
|
|
1112
|
-
/**
|
|
1113
|
-
* Discovery names the grant's server: the same one keeps the client and
|
|
1114
|
-
* tokens beside it, another replaces them. The SDK discovers afresh only
|
|
1115
|
-
* without a cached record — a restart's carried client, say — and what a
|
|
1116
|
-
* different server leaves behind would never be sent to it anyway.
|
|
1117
|
-
*/
|
|
1118
|
-
saveDiscoveryState(state) {
|
|
1119
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "saveDiscoveryState", [state]));
|
|
1120
|
-
}
|
|
1121
|
-
/**
|
|
1122
|
-
* The grant's tokens, for the server that issued them only. An
|
|
1123
|
-
* issuer-aware read records what it handed the SDK, so the coordinator can
|
|
1124
|
-
* tell a refresh another request completed since.
|
|
1125
|
-
*/
|
|
1126
|
-
tokens(ctx) {
|
|
1127
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "tokens", [ctx]));
|
|
1128
|
-
}
|
|
1129
|
-
saveTokens(tokens, ctx) {
|
|
1130
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "saveTokens", [tokens, ctx]));
|
|
1131
|
-
}
|
|
1132
|
-
/**
|
|
1133
|
-
* OAuth `state`: 256 random bits the consent URL carries and the callback
|
|
1134
|
-
* must present, so nobody holding a consent URL can complete it with their
|
|
1135
|
-
* own account (login CSRF). Stored, by digest, with the consent.
|
|
1136
|
-
*/
|
|
1137
|
-
state() {
|
|
1138
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "state", []));
|
|
1139
|
-
}
|
|
1140
|
-
saveCodeVerifier(verifier) {
|
|
1141
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "saveCodeVerifier", [verifier]));
|
|
1142
|
-
}
|
|
1143
|
-
codeVerifier() {
|
|
1144
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "codeVerifier", []));
|
|
1145
|
-
}
|
|
1146
|
-
/**
|
|
1147
|
-
* Store the consent: its flow record first, by state digest with the
|
|
1148
|
-
* link's lifetime, then the grant's pointer to it, in the flow's epoch. A
|
|
1149
|
-
* reset that replaced the epoch fails the start, and the record is removed,
|
|
1150
|
-
* so no start hands out a consent its callback could not complete.
|
|
1151
|
-
*/
|
|
1152
|
-
redirectToAuthorization(authorizationUrl) {
|
|
1153
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "redirectToAuthorization", [authorizationUrl]));
|
|
1154
|
-
}
|
|
1155
|
-
/** The consent this provider stored, while its epoch is still live. */
|
|
1156
|
-
consentUrl(signal = this.signal) {
|
|
1157
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "consentUrl", [signal]));
|
|
1158
|
-
}
|
|
1159
|
-
/** The live epoch's pending consent URL, if one is unclaimed. */
|
|
1160
|
-
pendingAuthorizationUrl() {
|
|
1161
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "pendingAuthorizationUrl", []));
|
|
295
|
+
/** @internal The access token of the last refresh this flow adopted. */
|
|
296
|
+
adoptedRefresh() {
|
|
297
|
+
return this.storedRefresh;
|
|
1162
298
|
}
|
|
1163
|
-
/**
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
* either direction (clock skew between isolates), and naming the grant's
|
|
1167
|
-
* client. A callback whose exchange the server refused with
|
|
1168
|
-
* `invalid_client` drops the client, and its URL would send the operator to
|
|
1169
|
-
* a consent the server has already refused.
|
|
1170
|
-
*/
|
|
1171
|
-
reusablePendingAuthorizationUrl() {
|
|
1172
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "reusablePendingAuthorizationUrl", []));
|
|
1173
|
-
}
|
|
1174
|
-
/**
|
|
1175
|
-
* Find the consent a callback's `state` names. One read whatever the
|
|
1176
|
-
* answer; the callback route's refusals pay the same. A claimed consent
|
|
1177
|
-
* no longer matches.
|
|
1178
|
-
*/
|
|
1179
|
-
verifyState(candidate) {
|
|
1180
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "verifyState", [candidate]));
|
|
1181
|
-
}
|
|
1182
|
-
/**
|
|
1183
|
-
* @internal The first half of the fence a code exchange crosses after every
|
|
1184
|
-
* read it depends on, just before the code leaves: claim the consent by
|
|
1185
|
-
* compare-and-set from the exact record `verifyState` found to a claimed
|
|
1186
|
-
* one. Of several callbacks carrying one state, exactly one wins; the
|
|
1187
|
-
* others send nothing. A claim that fails because a restart swept the
|
|
1188
|
-
* consent reports the supersession. The claim is spent whether or not the
|
|
1189
|
-
* exchange succeeds; the SDK's one retry after a refused code is answered
|
|
1190
|
-
* with that refusal, never sent again.
|
|
1191
|
-
*/
|
|
1192
|
-
claimCodeExchange() {
|
|
1193
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "claimCodeExchange", []));
|
|
1194
|
-
}
|
|
1195
|
-
/** Terminate the verified consent on an error, sharing the code exchange CAS. */
|
|
1196
|
-
consumeAuthError() {
|
|
1197
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "consumeAuthError", []));
|
|
1198
|
-
}
|
|
1199
|
-
claimConsent() {
|
|
1200
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "claimConsent", []));
|
|
1201
|
-
}
|
|
1202
|
-
/**
|
|
1203
|
-
* @internal The second half: read the live epoch and, in the reaction to
|
|
1204
|
-
* that read, dispatch. No await stands between the check and `send`. A
|
|
1205
|
-
* reset published at or before the read fails the exchange with nothing
|
|
1206
|
-
* sent; one published after it, while the request is leaving, cannot be
|
|
1207
|
-
* ordered before the send without a lock across requests, and its tokens
|
|
1208
|
-
* are refused by the grant's compare-and-set: one more consent.
|
|
1209
|
-
*/
|
|
1210
|
-
dispatchCodeExchange(send, endpoint) {
|
|
1211
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "dispatchCodeExchange", [send, endpoint]));
|
|
299
|
+
/** @internal The stored expiry of `accessToken`, when this provider last read or wrote it. */
|
|
300
|
+
observedExpiry(accessToken) {
|
|
301
|
+
return this.observed?.accessToken === accessToken ? this.observed.expiresAt : undefined;
|
|
1212
302
|
}
|
|
1213
303
|
/** @internal Keep a refused code's answer for the SDK's retry to read. */
|
|
1214
304
|
recordCodeExchangeRefusal(forSdk) {
|
|
1215
305
|
this.exchangeRefusal = forSdk.clone();
|
|
1216
306
|
}
|
|
1217
|
-
/** @internal Observe resolution while accepting the code response, never on a later SDK save. */
|
|
1218
|
-
recordCodeExchangeTokens(tokens) {
|
|
1219
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "recordCodeExchangeTokens", [tokens]));
|
|
1220
|
-
}
|
|
1221
|
-
/**
|
|
1222
|
-
* Remove what the SDK asks, but only as this flow saw it: a refused code or
|
|
1223
|
-
* refresh says nothing about a client or tokens another flow wrote since.
|
|
1224
|
-
* The verifier lives in its consent, which the claim already spent.
|
|
1225
|
-
*/
|
|
1226
|
-
invalidateCredentials(scope) {
|
|
1227
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "invalidateCredentials", [scope]));
|
|
1228
|
-
}
|
|
1229
307
|
// --- refresh, for the coordinator ---------------------------------------------
|
|
1230
308
|
/**
|
|
1231
309
|
* @internal How the refresh this provider owned or joined failed; undefined
|
|
@@ -1235,34 +313,10 @@ export class KvOAuthProvider {
|
|
|
1235
313
|
recordRefreshFailure(failure) {
|
|
1236
314
|
this.refreshFailure = failure;
|
|
1237
315
|
}
|
|
1238
|
-
/** @internal A shared-storage claim precedes every refresh dispatch. */
|
|
1239
|
-
claimRefresh(epoch, requested, signal, sharedWait) {
|
|
1240
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "claimRefresh", [
|
|
1241
|
-
epoch,
|
|
1242
|
-
requested,
|
|
1243
|
-
signal,
|
|
1244
|
-
sharedWait,
|
|
1245
|
-
]));
|
|
1246
|
-
}
|
|
1247
|
-
/** @internal Only the successful claimed-to-dispatched CAS may send. */
|
|
1248
|
-
dispatchRefresh(lease, send, waiting) {
|
|
1249
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "dispatchRefresh", [lease, send, waiting]));
|
|
1250
|
-
}
|
|
1251
|
-
/** @internal Release by CAS after a commit or a classified failure. */
|
|
1252
|
-
releaseRefresh(lease, verdict, tokens, resolution = "ambiguous") {
|
|
1253
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "releaseRefresh", [lease, verdict, tokens, resolution]));
|
|
1254
|
-
}
|
|
1255
|
-
resolveRefreshSpent(lease, state) {
|
|
1256
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "resolveRefreshSpent", [lease, state]));
|
|
1257
|
-
}
|
|
1258
316
|
/** @internal The same typed verdict for local and cross-isolate joiners. */
|
|
1259
317
|
refreshVerdict() {
|
|
1260
318
|
return this.refreshFailure;
|
|
1261
319
|
}
|
|
1262
|
-
/** @internal The grant's tokens as stored, in the flow's epoch. */
|
|
1263
|
-
storedTokens(signal = null) {
|
|
1264
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "storedTokens", [signal]));
|
|
1265
|
-
}
|
|
1266
320
|
/** @internal True when a refresh landed after this flow's issuer-aware read. */
|
|
1267
321
|
refreshedSinceRead(current, epoch) {
|
|
1268
322
|
const basis = this.refreshBasis;
|
|
@@ -1278,71 +332,10 @@ export class KvOAuthProvider {
|
|
|
1278
332
|
this.storedRefresh = tokens.access_token;
|
|
1279
333
|
return tokenResponse(tokens);
|
|
1280
334
|
}
|
|
1281
|
-
/**
|
|
1282
|
-
* @internal Store an accepted rotation in `epoch`, even after the request
|
|
1283
|
-
* that redeemed it has left, unless a newer rotation, another server's
|
|
1284
|
-
* grant, or another epoch is there now. A token response names no issuer:
|
|
1285
|
-
* the one the refreshed grant was bound to is stamped here.
|
|
1286
|
-
*/
|
|
1287
|
-
storeRefresh(epoch, requested, tokens, issuer) {
|
|
1288
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "storeRefresh", [epoch, requested, tokens, issuer]));
|
|
1289
|
-
}
|
|
1290
|
-
/**
|
|
1291
|
-
* @internal Drop the tokens the authorization server just refused, only
|
|
1292
|
-
* while the grant still holds that refresh token in `epoch`: a consent
|
|
1293
|
-
* that completed meanwhile survives. Best effort: if storage fails, the
|
|
1294
|
-
* next refresh is refused again and ends the same way.
|
|
1295
|
-
*/
|
|
1296
|
-
discardRefusedGrant(refreshToken, epoch, beforeWrite, signal) {
|
|
1297
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "discardRefusedGrant", [
|
|
1298
|
-
refreshToken,
|
|
1299
|
-
epoch,
|
|
1300
|
-
beforeWrite,
|
|
1301
|
-
signal,
|
|
1302
|
-
]));
|
|
1303
|
-
}
|
|
1304
|
-
// --- restart and disconnect ---------------------------------------------------
|
|
1305
|
-
/**
|
|
1306
|
-
* Replace the epoch, by one compare-and-set: the grant goes with it, and
|
|
1307
|
-
* every flow bound to the old epoch fails at its next read or write. A
|
|
1308
|
-
* forced restart may carry the registration forward (`carriableClient`);
|
|
1309
|
-
* a disconnect leaves only its epoch, which no passive request leaves.
|
|
1310
|
-
* Then the old epochs' consents are removed; that is hygiene, since their
|
|
1311
|
-
* callbacks fail the epoch check anyway and the records expire.
|
|
1312
|
-
*/
|
|
1313
|
-
resetAuthorization(operatorDisconnected = false, preserveClient = false) {
|
|
1314
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "resetAuthorization", [
|
|
1315
|
-
operatorDisconnected,
|
|
1316
|
-
preserveClient,
|
|
1317
|
-
]));
|
|
1318
|
-
}
|
|
1319
|
-
/** Remove the local grant before a bounded, best-effort RFC 7009 request. */
|
|
1320
|
-
disconnectAuthorization(send) {
|
|
1321
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "disconnectAuthorization", [send]));
|
|
1322
|
-
}
|
|
1323
335
|
/** Static adapters supply trusted configuration without persisting client secrets. */
|
|
1324
336
|
revocationSnapshot(body) {
|
|
1325
337
|
return { body, ...(this.clientOptions?.client ? { client: this.clientOptions.client } : {}) };
|
|
1326
338
|
}
|
|
1327
|
-
performReset(operatorDisconnected, preserveClient, revoke) {
|
|
1328
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "performReset", [
|
|
1329
|
-
operatorDisconnected,
|
|
1330
|
-
preserveClient,
|
|
1331
|
-
revoke,
|
|
1332
|
-
]));
|
|
1333
|
-
}
|
|
1334
|
-
/**
|
|
1335
|
-
* Remove every consent of an epoch no longer live. The records are read
|
|
1336
|
-
* before the epoch, and epochs never recur, so a consent whose epoch is not
|
|
1337
|
-
* the live one then can never complete; each goes by compare-and-set
|
|
1338
|
-
* against what was read. Best effort.
|
|
1339
|
-
*/
|
|
1340
|
-
sweepConsents() {
|
|
1341
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "sweepConsents", []));
|
|
1342
|
-
}
|
|
1343
|
-
sweepRefreshLeases() {
|
|
1344
|
-
return runEdge(authOwnMethod(KvOAuthProvider.prototype, this, "sweepRefreshLeases", []));
|
|
1345
|
-
}
|
|
1346
339
|
/**
|
|
1347
340
|
* The registration a forced restart may carry into its new epoch, or
|
|
1348
341
|
* nothing. It must be bound to a server (`issuer`) and registered under this
|
|
@@ -1390,1454 +383,11 @@ export class KvOAuthProvider {
|
|
|
1390
383
|
};
|
|
1391
384
|
}
|
|
1392
385
|
static {
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
? (yield authAwait(authCall(work, []), { signal }.signal))
|
|
1400
|
-
: (yield authCall(work, []));
|
|
1401
|
-
if (signal?.aborted)
|
|
1402
|
-
throw aborted(signal);
|
|
1403
|
-
return value;
|
|
1404
|
-
});
|
|
1405
|
-
}, KvOAuthProvider.prototype.wait);
|
|
1406
|
-
registerAuthMethod(KvOAuthProvider.prototype, "encodeGrant", function (grant, signal) {
|
|
1407
|
-
signal = signal === undefined ? this.signal : signal;
|
|
1408
|
-
return authFlow(function* () {
|
|
1409
|
-
this.assertCredentialFree({ issuer: grant.body.issuer, discovery: grant.body.discovery }, grant.body);
|
|
1410
|
-
const stored = {
|
|
1411
|
-
connectaOAuth: GRANT_VERSION,
|
|
1412
|
-
epoch: grant.epoch,
|
|
1413
|
-
...(grant.cleanupPending ? { cleanupPending: true } : {}),
|
|
1414
|
-
...(grant.flow !== undefined ? { flow: grant.flow } : {}),
|
|
1415
|
-
};
|
|
1416
|
-
if (Object.keys(grant.body).length > 0) {
|
|
1417
|
-
if (this.sealer) {
|
|
1418
|
-
stored.sealed = (yield authInvoke(this, "wait", [
|
|
1419
|
-
authMethod(() => {
|
|
1420
|
-
return authInvoke(this.sealer, "seal", [
|
|
1421
|
-
this.grantLabel(grant.epoch),
|
|
1422
|
-
JSON.stringify(grant.body),
|
|
1423
|
-
]);
|
|
1424
|
-
}),
|
|
1425
|
-
signal,
|
|
1426
|
-
]));
|
|
1427
|
-
}
|
|
1428
|
-
else {
|
|
1429
|
-
stored.body = grant.body;
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
return JSON.stringify(stored);
|
|
1433
|
-
}.bind(this));
|
|
1434
|
-
}, KvOAuthProvider.prototype.encodeGrant);
|
|
1435
|
-
registerAuthMethod(KvOAuthProvider.prototype, "openGrant", function (raw, signal) {
|
|
1436
|
-
signal = signal === undefined ? this.signal : signal;
|
|
1437
|
-
return authFlow(function* () {
|
|
1438
|
-
if (raw === null)
|
|
1439
|
-
return { epoch: INITIAL_EPOCH, body: {} };
|
|
1440
|
-
const stored = storedGrant(raw);
|
|
1441
|
-
if (!stored)
|
|
1442
|
-
return { epoch: UNREADABLE_EPOCH, body: {} };
|
|
1443
|
-
let body = stored.body;
|
|
1444
|
-
if (stored.sealed !== undefined) {
|
|
1445
|
-
body = undefined;
|
|
1446
|
-
try {
|
|
1447
|
-
if (this.sealer)
|
|
1448
|
-
body = parsed((yield authInvoke(this, "wait", [
|
|
1449
|
-
authMethod(() => {
|
|
1450
|
-
return authInvoke(this.sealer, "open", [
|
|
1451
|
-
this.grantLabel(stored.epoch),
|
|
1452
|
-
stored.sealed,
|
|
1453
|
-
]);
|
|
1454
|
-
}),
|
|
1455
|
-
signal,
|
|
1456
|
-
])));
|
|
1457
|
-
}
|
|
1458
|
-
catch {
|
|
1459
|
-
if (signal?.aborted)
|
|
1460
|
-
throw aborted(signal);
|
|
1461
|
-
this.sealer?.warn(`[connecta] connector "${this.connectorId}" has a sealed OAuth grant ` +
|
|
1462
|
-
"the configured vault cannot open; treating it as absent, so the " +
|
|
1463
|
-
"connector needs authorization again. A changed vault key or " +
|
|
1464
|
-
"tampered storage causes this.");
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
return {
|
|
1468
|
-
epoch: stored.epoch,
|
|
1469
|
-
...(stored.cleanupPending ? { cleanupPending: true } : {}),
|
|
1470
|
-
...(stored.flow !== undefined ? { flow: stored.flow } : {}),
|
|
1471
|
-
body: grantBody(body),
|
|
1472
|
-
};
|
|
1473
|
-
}.bind(this));
|
|
1474
|
-
}, KvOAuthProvider.prototype.openGrant);
|
|
1475
|
-
registerAuthMethod(KvOAuthProvider.prototype, "storedAuthorization", function (issuer) {
|
|
1476
|
-
return authFlow(function* () {
|
|
1477
|
-
const raw = (yield authStorage(this.storage, "get", [GRANT]));
|
|
1478
|
-
const grant = (yield authInvoke(this, "openGrant", [raw]));
|
|
1479
|
-
if (isDisconnectedEpoch(grant.epoch))
|
|
1480
|
-
return false;
|
|
1481
|
-
const body = grant.body;
|
|
1482
|
-
const configured = issuer ?? this.clientOptions?.client?.issuer;
|
|
1483
|
-
const discovered = body.discovery;
|
|
1484
|
-
if (!configured &&
|
|
1485
|
-
(!discovered || discovered.authorizationServerMetadata?.issuer !== discovered.authorizationServerUrl))
|
|
1486
|
-
return false;
|
|
1487
|
-
const expected = configured ?? discoveryIssuer(discovered);
|
|
1488
|
-
return Boolean(expected && body.issuer === expected && body.tokens?.access_token);
|
|
1489
|
-
}.bind(this)).pipe(Effect.catch(() => Effect.succeed(false)));
|
|
1490
|
-
}, KvOAuthProvider.prototype.storedAuthorization);
|
|
1491
|
-
registerAuthMethod(KvOAuthProvider.prototype, "readGrant", function (signal) {
|
|
1492
|
-
signal = signal === undefined ? this.signal : signal;
|
|
1493
|
-
return authFlow(function* () {
|
|
1494
|
-
let raw = (yield authInvoke(this, "wait", [
|
|
1495
|
-
authMethod(() => {
|
|
1496
|
-
return authStorage(this.storage, "get", [GRANT]);
|
|
1497
|
-
}),
|
|
1498
|
-
signal,
|
|
1499
|
-
]));
|
|
1500
|
-
if (raw === null)
|
|
1501
|
-
raw = (yield authInvoke(this, "migrate", [signal]));
|
|
1502
|
-
if (raw !== null && storedGrant(raw)?.cleanupPending)
|
|
1503
|
-
raw = (yield authInvoke(this, "cleanupV2", [raw, signal]));
|
|
1504
|
-
const grant = (yield authInvoke(this, "openGrant", [raw, signal]));
|
|
1505
|
-
if (raw !== null && this.sealer && storedGrant(raw)?.body !== undefined) {
|
|
1506
|
-
const sealed = (yield authInvoke(this, "encodeGrant", [grant, signal]));
|
|
1507
|
-
if ((yield authInvoke(this, "wait", [
|
|
1508
|
-
authMethod(() => {
|
|
1509
|
-
return authCatch(authStorage(this.storage, "compareAndSet", [GRANT, raw, sealed]), () => false);
|
|
1510
|
-
}),
|
|
1511
|
-
signal,
|
|
1512
|
-
])))
|
|
1513
|
-
raw = sealed;
|
|
1514
|
-
}
|
|
1515
|
-
return { raw, grant };
|
|
1516
|
-
}.bind(this));
|
|
1517
|
-
}, KvOAuthProvider.prototype.readGrant);
|
|
1518
|
-
registerAuthMethod(KvOAuthProvider.prototype, "migrate", function (signal) {
|
|
1519
|
-
return authFlow(function* () {
|
|
1520
|
-
const sealer = this.sealer;
|
|
1521
|
-
const found = (yield authInvoke(this, "wait", [
|
|
1522
|
-
authMethod(() => {
|
|
1523
|
-
return authCall(readV2Grant, [
|
|
1524
|
-
this.preparationStorage(signal),
|
|
1525
|
-
sealer && {
|
|
1526
|
-
...sealer,
|
|
1527
|
-
open: authMethod((label, value) => {
|
|
1528
|
-
return authInvoke(this, "wait", [
|
|
1529
|
-
authMethod(() => {
|
|
1530
|
-
return authInvoke(sealer, "open", [label, value]);
|
|
1531
|
-
}),
|
|
1532
|
-
signal,
|
|
1533
|
-
]);
|
|
1534
|
-
}),
|
|
1535
|
-
},
|
|
1536
|
-
]);
|
|
1537
|
-
}),
|
|
1538
|
-
signal,
|
|
1539
|
-
]));
|
|
1540
|
-
if (!found)
|
|
1541
|
-
return null;
|
|
1542
|
-
const prefix = found.disconnected ? DISCONNECTED_EPOCH_PREFIX : ACTIVE_EPOCH_PREFIX;
|
|
1543
|
-
const encoded = (yield authInvoke(this, "encodeGrant", [
|
|
1544
|
-
{
|
|
1545
|
-
epoch: `${prefix}${crypto.randomUUID()}`,
|
|
1546
|
-
body: found.body,
|
|
1547
|
-
cleanupPending: true,
|
|
1548
|
-
},
|
|
1549
|
-
signal,
|
|
1550
|
-
]));
|
|
1551
|
-
let written;
|
|
1552
|
-
try {
|
|
1553
|
-
written = (yield authInvoke(this, "wait", [
|
|
1554
|
-
authMethod(() => {
|
|
1555
|
-
return authStorage(this.storage, "compareAndSet", [GRANT, null, encoded]);
|
|
1556
|
-
}),
|
|
1557
|
-
signal,
|
|
1558
|
-
]));
|
|
1559
|
-
}
|
|
1560
|
-
catch {
|
|
1561
|
-
if (signal?.aborted)
|
|
1562
|
-
throw aborted(signal);
|
|
1563
|
-
throw this.credentialWriteError();
|
|
1564
|
-
}
|
|
1565
|
-
return (yield written
|
|
1566
|
-
? authValue(() => encoded)
|
|
1567
|
-
: authInvoke(this, "wait", [
|
|
1568
|
-
authMethod(() => {
|
|
1569
|
-
return authStorage(this.storage, "get", [GRANT]);
|
|
1570
|
-
}),
|
|
1571
|
-
signal,
|
|
1572
|
-
]));
|
|
1573
|
-
}.bind(this));
|
|
1574
|
-
}, KvOAuthProvider.prototype.migrate);
|
|
1575
|
-
registerAuthMethod(KvOAuthProvider.prototype, "cleanupV2", function (raw, signal) {
|
|
1576
|
-
signal = signal === undefined ? this.signal : signal;
|
|
1577
|
-
return authFlow(function* () {
|
|
1578
|
-
try {
|
|
1579
|
-
if (!(yield authInvoke(this, "wait", [
|
|
1580
|
-
authMethod(() => {
|
|
1581
|
-
return authCall(deleteV2Keys, [this.preparationStorage(signal)]);
|
|
1582
|
-
}),
|
|
1583
|
-
signal,
|
|
1584
|
-
])))
|
|
1585
|
-
return raw;
|
|
1586
|
-
const grant = (yield authInvoke(this, "openGrant", [raw, signal]));
|
|
1587
|
-
delete grant.cleanupPending;
|
|
1588
|
-
const cleared = (yield authInvoke(this, "encodeGrant", [grant, signal]));
|
|
1589
|
-
if ((yield authInvoke(this, "wait", [
|
|
1590
|
-
authMethod(() => {
|
|
1591
|
-
return authStorage(this.storage, "compareAndSet", [GRANT, raw, cleared]);
|
|
1592
|
-
}),
|
|
1593
|
-
signal,
|
|
1594
|
-
])))
|
|
1595
|
-
return cleared;
|
|
1596
|
-
return (yield authInvoke(this, "wait", [
|
|
1597
|
-
authMethod(() => {
|
|
1598
|
-
return authStorage(this.storage, "get", [GRANT]);
|
|
1599
|
-
}),
|
|
1600
|
-
signal,
|
|
1601
|
-
]));
|
|
1602
|
-
}
|
|
1603
|
-
catch {
|
|
1604
|
-
if (signal?.aborted)
|
|
1605
|
-
throw aborted(signal);
|
|
1606
|
-
// The durable marker survives both deletion and marker-clear failures.
|
|
1607
|
-
return raw;
|
|
1608
|
-
}
|
|
1609
|
-
}.bind(this));
|
|
1610
|
-
}, KvOAuthProvider.prototype.cleanupV2);
|
|
1611
|
-
registerAuthMethod(KvOAuthProvider.prototype, "boundGrant", function (signal) {
|
|
1612
|
-
signal = signal === undefined ? this.signal : signal;
|
|
1613
|
-
return authFlow(function* () {
|
|
1614
|
-
const { grant } = (yield authInvoke(this, "readGrant", [signal]));
|
|
1615
|
-
if (this.bound && grant.epoch !== this.epoch)
|
|
1616
|
-
throw this.flowSuperseded();
|
|
1617
|
-
return grant;
|
|
1618
|
-
}.bind(this));
|
|
1619
|
-
}, KvOAuthProvider.prototype.boundGrant);
|
|
1620
|
-
registerAuthMethod(KvOAuthProvider.prototype, "activeGrant", function () {
|
|
1621
|
-
return authInvoke(this, "boundGrant", []);
|
|
1622
|
-
}, KvOAuthProvider.prototype.activeGrant);
|
|
1623
|
-
registerAuthMethod(KvOAuthProvider.prototype, "updateGrant", function (change, options = {}) {
|
|
1624
|
-
return authFlow(function* () {
|
|
1625
|
-
const signal = options.signal ?? (options.commit ? null : this.signal);
|
|
1626
|
-
for (let attempt = 0; attempt < MAX_GRANT_WRITES; attempt++) {
|
|
1627
|
-
if (!options.commit && this.signal?.aborted)
|
|
1628
|
-
return false;
|
|
1629
|
-
const { raw, grant } = (yield authInvoke(this, "readGrant", [signal]));
|
|
1630
|
-
const epoch = options.epoch ?? (this.epoch ??= grant.epoch);
|
|
1631
|
-
if (grant.epoch !== epoch) {
|
|
1632
|
-
if (options.quiet || !this.bound)
|
|
1633
|
-
return false;
|
|
1634
|
-
throw this.flowSuperseded();
|
|
1635
|
-
}
|
|
1636
|
-
// A disconnect's epoch holds nothing until an explicit start replaces it.
|
|
1637
|
-
if (isDisconnectedEpoch(epoch))
|
|
1638
|
-
return false;
|
|
1639
|
-
const next = change(grant);
|
|
1640
|
-
if (next === undefined)
|
|
1641
|
-
return true;
|
|
1642
|
-
const encoded = (yield authInvoke(this, "encodeGrant", [next, signal]));
|
|
1643
|
-
if (!options.commit && this.signal?.aborted)
|
|
1644
|
-
return false;
|
|
1645
|
-
let written;
|
|
1646
|
-
try {
|
|
1647
|
-
written = (yield authInvoke(this, "wait", [
|
|
1648
|
-
authMethod(() => {
|
|
1649
|
-
return authStorage(this.storage, "compareAndSet", [GRANT, raw, encoded], options.beforeWrite && (() => options.beforeWrite?.()));
|
|
1650
|
-
}),
|
|
1651
|
-
signal,
|
|
1652
|
-
]));
|
|
1653
|
-
}
|
|
1654
|
-
catch {
|
|
1655
|
-
if (signal?.aborted)
|
|
1656
|
-
throw aborted(signal);
|
|
1657
|
-
// A store's own error can quote the value it refused.
|
|
1658
|
-
throw this.credentialWriteError();
|
|
1659
|
-
}
|
|
1660
|
-
if (written) {
|
|
1661
|
-
if (next.epoch !== grant.epoch)
|
|
1662
|
-
this.epoch = next.epoch;
|
|
1663
|
-
return true;
|
|
1664
|
-
}
|
|
1665
|
-
}
|
|
1666
|
-
throw this.credentialWriteError();
|
|
1667
|
-
}.bind(this));
|
|
1668
|
-
}, KvOAuthProvider.prototype.updateGrant);
|
|
1669
|
-
registerAuthMethod(KvOAuthProvider.prototype, "writeCredential", function (field, value, issuer, refreshConsent) {
|
|
1670
|
-
return authFlow(function* () {
|
|
1671
|
-
(yield authInvoke(this, "updateGrant", [
|
|
1672
|
-
(grant) => {
|
|
1673
|
-
const body = issuer !== undefined && grant.body.issuer !== issuer ? { issuer } : { ...grant.body };
|
|
1674
|
-
if (field === "client")
|
|
1675
|
-
body.client = value;
|
|
1676
|
-
else {
|
|
1677
|
-
body.tokens = value;
|
|
1678
|
-
delete body.refreshConsent;
|
|
1679
|
-
if (refreshConsent)
|
|
1680
|
-
body.refreshConsent = refreshConsent;
|
|
1681
|
-
}
|
|
1682
|
-
return this.withIssuer(grant, body);
|
|
1683
|
-
},
|
|
1684
|
-
]));
|
|
1685
|
-
// A flow may invalidate what it wrote itself.
|
|
1686
|
-
this.seen[field] = fingerprint(field === "client" ? value.value : value);
|
|
1687
|
-
}.bind(this));
|
|
1688
|
-
}, KvOAuthProvider.prototype.writeCredential);
|
|
1689
|
-
registerAuthMethod(KvOAuthProvider.prototype, "bindingForGrant", function (grant) {
|
|
1690
|
-
return authFlow(function* () {
|
|
1691
|
-
const configured = this.configuredAuthorizationBinding();
|
|
1692
|
-
return configured
|
|
1693
|
-
? { ...configured, issuer: grant.body.issuer }
|
|
1694
|
-
: {
|
|
1695
|
-
...(yield authInvoke(this, "wait", [
|
|
1696
|
-
authMethod(() => {
|
|
1697
|
-
return authCall(discoveryBinding, [grant.body.discovery]);
|
|
1698
|
-
}),
|
|
1699
|
-
])),
|
|
1700
|
-
issuer: grant.body.issuer,
|
|
1701
|
-
clientId: grant.body.client?.value.client_id,
|
|
1702
|
-
};
|
|
1703
|
-
}.bind(this));
|
|
1704
|
-
}, KvOAuthProvider.prototype.bindingForGrant);
|
|
1705
|
-
registerAuthMethod(KvOAuthProvider.prototype, "checkFlowBinding", function (grant) {
|
|
1706
|
-
return authFlow(function* () {
|
|
1707
|
-
const binding = this.callback?.flow.binding;
|
|
1708
|
-
if (!binding ||
|
|
1709
|
-
fingerprint(binding) !== fingerprint((yield authInvoke(this, "bindingForGrant", [grant])))) {
|
|
1710
|
-
throw this.flowSuperseded();
|
|
1711
|
-
}
|
|
1712
|
-
}.bind(this));
|
|
1713
|
-
}, KvOAuthProvider.prototype.checkFlowBinding);
|
|
1714
|
-
registerAuthMethod(KvOAuthProvider.prototype, "encodeFlow", function (key, flow) {
|
|
1715
|
-
return authFlow(function* () {
|
|
1716
|
-
const { verifier, ...rest } = flow;
|
|
1717
|
-
const stored = { connectaOAuthFlow: FLOW_VERSION, ...rest };
|
|
1718
|
-
if (verifier !== undefined) {
|
|
1719
|
-
if (this.sealer) {
|
|
1720
|
-
stored.verifier = (yield authInvoke(this, "wait", [
|
|
1721
|
-
authMethod(() => {
|
|
1722
|
-
return authInvoke(this.sealer, "seal", [
|
|
1723
|
-
this.flowLabel(key, flow.epoch),
|
|
1724
|
-
verifier,
|
|
1725
|
-
]);
|
|
1726
|
-
}),
|
|
1727
|
-
]));
|
|
1728
|
-
stored.sealed = true;
|
|
1729
|
-
}
|
|
1730
|
-
else {
|
|
1731
|
-
stored.verifier = verifier;
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
return JSON.stringify(stored);
|
|
1735
|
-
}.bind(this));
|
|
1736
|
-
}, KvOAuthProvider.prototype.encodeFlow);
|
|
1737
|
-
registerAuthMethod(KvOAuthProvider.prototype, "openFlow", function (key, raw, withVerifier) {
|
|
1738
|
-
return authFlow(function* () {
|
|
1739
|
-
const value = parsed(raw);
|
|
1740
|
-
if (!plainObject(value) ||
|
|
1741
|
-
value.connectaOAuthFlow !== FLOW_VERSION ||
|
|
1742
|
-
typeof value.epoch !== "string" ||
|
|
1743
|
-
typeof value.at !== "number" ||
|
|
1744
|
-
(value.url !== undefined && typeof value.url !== "string") ||
|
|
1745
|
-
(value.verifier !== undefined && typeof value.verifier !== "string")) {
|
|
1746
|
-
return undefined;
|
|
1747
|
-
}
|
|
1748
|
-
const flow = {
|
|
1749
|
-
epoch: value.epoch,
|
|
1750
|
-
at: value.at,
|
|
1751
|
-
...(plainObject(value.binding) ? { binding: value.binding } : {}),
|
|
1752
|
-
...(typeof value.url === "string" ? { url: value.url } : {}),
|
|
1753
|
-
...(value.consumed === true ? { consumed: true } : {}),
|
|
1754
|
-
};
|
|
1755
|
-
if (!withVerifier || typeof value.verifier !== "string")
|
|
1756
|
-
return flow;
|
|
1757
|
-
if (value.sealed !== true)
|
|
1758
|
-
return { ...flow, verifier: value.verifier };
|
|
1759
|
-
try {
|
|
1760
|
-
if (!this.sealer)
|
|
1761
|
-
return undefined;
|
|
1762
|
-
return {
|
|
1763
|
-
...flow,
|
|
1764
|
-
verifier: (yield authInvoke(this, "wait", [
|
|
1765
|
-
authMethod(() => {
|
|
1766
|
-
return authInvoke(this.sealer, "open", [
|
|
1767
|
-
this.flowLabel(key, flow.epoch),
|
|
1768
|
-
value.verifier,
|
|
1769
|
-
]);
|
|
1770
|
-
}),
|
|
1771
|
-
])),
|
|
1772
|
-
};
|
|
1773
|
-
}
|
|
1774
|
-
catch {
|
|
1775
|
-
if (this.signal?.aborted)
|
|
1776
|
-
throw aborted(this.signal);
|
|
1777
|
-
this.sealer?.warn(`[connecta] connector "${this.connectorId}" has a sealed OAuth consent ` +
|
|
1778
|
-
"the configured vault cannot open; its callback is refused.");
|
|
1779
|
-
return undefined;
|
|
1780
|
-
}
|
|
1781
|
-
}.bind(this));
|
|
1782
|
-
}, KvOAuthProvider.prototype.openFlow);
|
|
1783
|
-
registerAuthMethod(KvOAuthProvider.prototype, "latestConsent", function () {
|
|
1784
|
-
return authFlow(function* () {
|
|
1785
|
-
const grant = (yield authInvoke(this, "boundGrant", []));
|
|
1786
|
-
if (grant.flow === undefined)
|
|
1787
|
-
return undefined;
|
|
1788
|
-
const key = oauthFlowKeys.flow(grant.flow);
|
|
1789
|
-
const raw = (yield authInvoke(this, "wait", [
|
|
1790
|
-
authMethod(() => {
|
|
1791
|
-
return authStorage(this.storage, "get", [key]);
|
|
1792
|
-
}),
|
|
1793
|
-
]));
|
|
1794
|
-
const flow = raw === null ? undefined : (yield authInvoke(this, "openFlow", [key, raw, false]));
|
|
1795
|
-
return flow && !flow.consumed && flow.epoch === grant.epoch && flow.url !== undefined
|
|
1796
|
-
? { ...flow, url: flow.url }
|
|
1797
|
-
: undefined;
|
|
1798
|
-
}.bind(this));
|
|
1799
|
-
}, KvOAuthProvider.prototype.latestConsent);
|
|
1800
|
-
registerAuthMethod(KvOAuthProvider.prototype, "liveEpoch", function () {
|
|
1801
|
-
return authFlow(function* () {
|
|
1802
|
-
return (yield authInvoke(this, "readGrant", [])).grant.epoch;
|
|
1803
|
-
}.bind(this));
|
|
1804
|
-
}, KvOAuthProvider.prototype.liveEpoch);
|
|
1805
|
-
registerAuthMethod(KvOAuthProvider.prototype, "operatorDisconnected", function () {
|
|
1806
|
-
return authFlow(function* () {
|
|
1807
|
-
return isDisconnectedEpoch((yield authInvoke(this, "liveEpoch", [])));
|
|
1808
|
-
}.bind(this));
|
|
1809
|
-
}, KvOAuthProvider.prototype.operatorDisconnected);
|
|
1810
|
-
registerAuthMethod(KvOAuthProvider.prototype, "beginFlow", function (expectedEpoch) {
|
|
1811
|
-
return authFlow(function* () {
|
|
1812
|
-
const epoch = (yield authInvoke(this, "liveEpoch", []));
|
|
1813
|
-
if (expectedEpoch !== undefined && epoch !== expectedEpoch)
|
|
1814
|
-
throw this.flowSuperseded();
|
|
1815
|
-
this.epoch = epoch;
|
|
1816
|
-
this.bound = true;
|
|
1817
|
-
return epoch;
|
|
1818
|
-
}.bind(this));
|
|
1819
|
-
}, KvOAuthProvider.prototype.beginFlow);
|
|
1820
|
-
registerAuthMethod(KvOAuthProvider.prototype, "flowEpoch", function () {
|
|
1821
|
-
return authFlow(function* () {
|
|
1822
|
-
return this.epoch ?? (yield authInvoke(this, "liveEpoch", []));
|
|
1823
|
-
}.bind(this));
|
|
1824
|
-
}, KvOAuthProvider.prototype.flowEpoch);
|
|
1825
|
-
registerAuthMethod(KvOAuthProvider.prototype, "bindFlow", function () {
|
|
1826
|
-
return authFlow(function* () {
|
|
1827
|
-
const { grant } = (yield authInvoke(this, "readGrant", []));
|
|
1828
|
-
const epoch = this.callback?.flow.epoch ?? this.epoch ?? grant.epoch;
|
|
1829
|
-
if (grant.epoch !== epoch)
|
|
1830
|
-
throw this.flowSuperseded();
|
|
1831
|
-
this.epoch = epoch;
|
|
1832
|
-
this.bound = true;
|
|
1833
|
-
this.codeExchange = true;
|
|
1834
|
-
(yield authInvoke(this, "checkFlowBinding", [grant]));
|
|
1835
|
-
this.seen = {
|
|
1836
|
-
client: fingerprint(grant.body.client?.value),
|
|
1837
|
-
tokens: fingerprint(grant.body.tokens),
|
|
1838
|
-
};
|
|
1839
|
-
}.bind(this));
|
|
1840
|
-
}, KvOAuthProvider.prototype.bindFlow);
|
|
1841
|
-
registerAuthMethod(KvOAuthProvider.prototype, "verifyCallbackState", function (state) {
|
|
1842
|
-
return authFlow(function* () {
|
|
1843
|
-
if (this.callback?.key ===
|
|
1844
|
-
oauthFlowKeys.flow((yield authInvoke(this, "wait", [
|
|
1845
|
-
authMethod(() => {
|
|
1846
|
-
return authCall(oauthStateDigest, [state]);
|
|
1847
|
-
}),
|
|
1848
|
-
]))))
|
|
1849
|
-
return true;
|
|
1850
|
-
return (yield authInvoke(this, "verifyState", [state]));
|
|
1851
|
-
}.bind(this));
|
|
1852
|
-
}, KvOAuthProvider.prototype.verifyCallbackState);
|
|
1853
|
-
registerAuthMethod(KvOAuthProvider.prototype, "clientInformation", function (ctx) {
|
|
1854
|
-
return authFlow(function* () {
|
|
1855
|
-
if (this.signal?.aborted)
|
|
1856
|
-
throw aborted(this.signal);
|
|
1857
|
-
const { body } = (yield authInvoke(this, "activeGrant", []));
|
|
1858
|
-
if (this.signal?.aborted)
|
|
1859
|
-
throw aborted(this.signal);
|
|
1860
|
-
const client = this.clientOptions?.client;
|
|
1861
|
-
if (!body.client && !this.allowAuthorization)
|
|
1862
|
-
throw this.authorizationRefused();
|
|
1863
|
-
if (client) {
|
|
1864
|
-
if ((ctx && ctx.issuer !== client.issuer) || body.issuer !== client.issuer)
|
|
1865
|
-
throw this.flowSuperseded();
|
|
1866
|
-
if (body.client &&
|
|
1867
|
-
(body.client.value.client_id !== client.clientId || body.client.binding !== this.clientBinding))
|
|
1868
|
-
throw this.flowSuperseded();
|
|
1869
|
-
return {
|
|
1870
|
-
client_id: client.clientId,
|
|
1871
|
-
issuer: client.issuer,
|
|
1872
|
-
token_endpoint_auth_method: this.clientMetadata.token_endpoint_auth_method,
|
|
1873
|
-
...(client.clientSecret !== undefined ? { client_secret: client.clientSecret } : {}),
|
|
1874
|
-
};
|
|
1875
|
-
}
|
|
1876
|
-
if (!body.client || (ctx && body.issuer !== ctx.issuer))
|
|
1877
|
-
return undefined;
|
|
1878
|
-
this.seen.client = fingerprint(body.client.value);
|
|
1879
|
-
return ctx ? { ...body.client.value, issuer: ctx.issuer } : body.client.value;
|
|
1880
|
-
}.bind(this));
|
|
1881
|
-
}, KvOAuthProvider.prototype.clientInformation);
|
|
1882
|
-
registerAuthMethod(KvOAuthProvider.prototype, "saveClientInformation", function (info, ctx) {
|
|
1883
|
-
return authFlow(function* () {
|
|
1884
|
-
try {
|
|
1885
|
-
const { body } = (yield authInvoke(this, "boundGrant", []));
|
|
1886
|
-
const registrationPath = this.clientOptions?.client
|
|
1887
|
-
? "static"
|
|
1888
|
-
: body.client?.value.client_id === info.client_id && body.client.registrationPath
|
|
1889
|
-
? body.client.registrationPath
|
|
1890
|
-
: this.clientMetadataUrl &&
|
|
1891
|
-
body.discovery?.authorizationServerMetadata?.client_id_metadata_document_supported === true
|
|
1892
|
-
? "cimd"
|
|
1893
|
-
: "dcr";
|
|
1894
|
-
(yield authInvoke(this, "writeCredential", [
|
|
1895
|
-
"client",
|
|
1896
|
-
{
|
|
1897
|
-
value: this.clientOptions?.client ? { client_id: this.clientOptions.client.clientId } : info,
|
|
1898
|
-
registrationPath,
|
|
1899
|
-
...(this.clientBinding !== undefined ? { binding: this.clientBinding } : {}),
|
|
1900
|
-
},
|
|
1901
|
-
ctx?.issuer,
|
|
1902
|
-
]));
|
|
1903
|
-
}
|
|
1904
|
-
catch (error) {
|
|
1905
|
-
if (!this.signal?.aborted)
|
|
1906
|
-
throw error;
|
|
1907
|
-
}
|
|
1908
|
-
}.bind(this));
|
|
1909
|
-
}, KvOAuthProvider.prototype.saveClientInformation);
|
|
1910
|
-
registerAuthMethod(KvOAuthProvider.prototype, "registrationPath", function () {
|
|
1911
|
-
return authFlow(function* () {
|
|
1912
|
-
const { body } = (yield authInvoke(this, "boundGrant", []));
|
|
1913
|
-
return body.client?.registrationPath;
|
|
1914
|
-
}.bind(this));
|
|
1915
|
-
}, KvOAuthProvider.prototype.registrationPath);
|
|
1916
|
-
registerAuthMethod(KvOAuthProvider.prototype, "validatedIssuerOrigin", function () {
|
|
1917
|
-
return authFlow(function* () {
|
|
1918
|
-
const { body } = (yield authInvoke(this, "boundGrant", []));
|
|
1919
|
-
const state = body.discovery;
|
|
1920
|
-
if (!state ||
|
|
1921
|
-
state.authorizationServerMetadata?.issuer !== state.authorizationServerUrl ||
|
|
1922
|
-
body.issuer !== state.authorizationServerUrl)
|
|
1923
|
-
return undefined;
|
|
1924
|
-
this.assertCredentialFree(state, body);
|
|
1925
|
-
return new URL(state.authorizationServerUrl).origin;
|
|
1926
|
-
}.bind(this));
|
|
1927
|
-
}, KvOAuthProvider.prototype.validatedIssuerOrigin);
|
|
1928
|
-
registerAuthMethod(KvOAuthProvider.prototype, "discoveryState", function () {
|
|
1929
|
-
return authFlow(function* () {
|
|
1930
|
-
if (this.signal?.aborted)
|
|
1931
|
-
throw aborted(this.signal);
|
|
1932
|
-
const { body } = (yield authInvoke(this, "activeGrant", []));
|
|
1933
|
-
if (this.signal?.aborted)
|
|
1934
|
-
throw aborted(this.signal);
|
|
1935
|
-
let state = body.discovery;
|
|
1936
|
-
if (state?.resourceMetadataUrl && this.clientOptions?.resourceOrigin) {
|
|
1937
|
-
let sameOrigin = false;
|
|
1938
|
-
try {
|
|
1939
|
-
sameOrigin = new URL(state.resourceMetadataUrl).origin === this.clientOptions.resourceOrigin;
|
|
1940
|
-
}
|
|
1941
|
-
catch {
|
|
1942
|
-
// A malformed legacy URL is no more usable than a foreign one.
|
|
1943
|
-
}
|
|
1944
|
-
if (!sameOrigin) {
|
|
1945
|
-
// Never hand the SDK a legacy foreign URL or its resource
|
|
1946
|
-
// document. Keep the issuer binding for callbacks, but force
|
|
1947
|
-
// same-origin resource discovery on every SDK path. This read
|
|
1948
|
-
// does not mutate the stored grant.
|
|
1949
|
-
state = { ...state };
|
|
1950
|
-
delete state.resourceMetadataUrl;
|
|
1951
|
-
delete state.resourceMetadata;
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
this.assertCredentialFree(state, body);
|
|
1955
|
-
// Bind consent to durable discovery until the SDK saves a new
|
|
1956
|
-
// state. A same-origin metadata 404 can leave the cache unchanged.
|
|
1957
|
-
this.consentDiscovery = body.discovery;
|
|
1958
|
-
return state;
|
|
1959
|
-
}.bind(this));
|
|
1960
|
-
}, KvOAuthProvider.prototype.discoveryState);
|
|
1961
|
-
registerAuthMethod(KvOAuthProvider.prototype, "saveDiscoveryState", function (state) {
|
|
1962
|
-
return authFlow(function* () {
|
|
1963
|
-
this.assertCredentialFree(state, (yield authInvoke(this, "boundGrant", [])).body);
|
|
1964
|
-
const issuer = discoveryIssuer(state);
|
|
1965
|
-
if (this.clientOptions?.client && issuer !== this.clientOptions.client.issuer)
|
|
1966
|
-
throw this.flowSuperseded();
|
|
1967
|
-
if (!this.allowAuthorization && !(yield authInvoke(this, "boundGrant", [])).body.client)
|
|
1968
|
-
throw this.authorizationRefused();
|
|
1969
|
-
this.consentDiscovery = state;
|
|
1970
|
-
(yield authInvoke(this, "updateGrant", [
|
|
1971
|
-
(grant) => {
|
|
1972
|
-
const body = grant.body.issuer !== issuer ? (issuer !== undefined ? { issuer } : {}) : { ...grant.body };
|
|
1973
|
-
this.assertCredentialFree(state, grant.body);
|
|
1974
|
-
body.discovery = state;
|
|
1975
|
-
return this.withIssuer(grant, body);
|
|
1976
|
-
},
|
|
1977
|
-
]));
|
|
1978
|
-
}.bind(this));
|
|
1979
|
-
}, KvOAuthProvider.prototype.saveDiscoveryState);
|
|
1980
|
-
registerAuthMethod(KvOAuthProvider.prototype, "tokens", function (ctx) {
|
|
1981
|
-
return authFlow(function* () {
|
|
1982
|
-
if (this.signal?.aborted)
|
|
1983
|
-
throw aborted(this.signal);
|
|
1984
|
-
const grant = (yield authInvoke(this, "activeGrant", []));
|
|
1985
|
-
if (this.signal?.aborted)
|
|
1986
|
-
throw aborted(this.signal);
|
|
1987
|
-
const { tokens } = grant.body;
|
|
1988
|
-
if (!tokens || (ctx && grant.body.issuer !== ctx.issuer))
|
|
1989
|
-
return undefined;
|
|
1990
|
-
this.seen.tokens = fingerprint(tokens);
|
|
1991
|
-
if (!ctx)
|
|
1992
|
-
return tokens;
|
|
1993
|
-
this.refreshBasis = { accessToken: tokens.access_token, epoch: grant.epoch, issuer: ctx.issuer };
|
|
1994
|
-
return { ...tokens, issuer: ctx.issuer };
|
|
1995
|
-
}.bind(this));
|
|
1996
|
-
}, KvOAuthProvider.prototype.tokens);
|
|
1997
|
-
registerAuthMethod(KvOAuthProvider.prototype, "saveTokens", function (tokens, ctx) {
|
|
1998
|
-
return authFlow(function* () {
|
|
1999
|
-
if (this.signal?.aborted)
|
|
2000
|
-
throw aborted(this.signal);
|
|
2001
|
-
if (this.refreshFailure?.kind === "dead")
|
|
2002
|
-
throw this.authorizationRefused();
|
|
2003
|
-
// The coordinator stored this refresh already, or handed on another's.
|
|
2004
|
-
if (this.storedRefresh !== undefined && tokens.access_token === this.storedRefresh)
|
|
2005
|
-
return;
|
|
2006
|
-
const refreshConsent = this.exchangeConsent &&
|
|
2007
|
-
tokens.refresh_token !== undefined &&
|
|
2008
|
-
this.exchangeConsent.digest ===
|
|
2009
|
-
(yield authInvoke(this, "wait", [
|
|
2010
|
-
authMethod(() => {
|
|
2011
|
-
return authCall(oauthStateDigest, [tokens.refresh_token]);
|
|
2012
|
-
}),
|
|
2013
|
-
]))
|
|
2014
|
-
? this.exchangeConsent
|
|
2015
|
-
: undefined;
|
|
2016
|
-
(yield authInvoke(this, "writeCredential", ["tokens", tokens, ctx?.issuer, refreshConsent]));
|
|
2017
|
-
this.refreshFailure = undefined;
|
|
2018
|
-
}.bind(this));
|
|
2019
|
-
}, KvOAuthProvider.prototype.saveTokens);
|
|
2020
|
-
registerAuthMethod(KvOAuthProvider.prototype, "state", function () {
|
|
2021
|
-
return authValue(() => {
|
|
2022
|
-
if (!this.allowAuthorization)
|
|
2023
|
-
throw this.authorizationRefused();
|
|
2024
|
-
const state = randomState();
|
|
2025
|
-
this.consent = { state };
|
|
2026
|
-
return state;
|
|
2027
|
-
});
|
|
2028
|
-
}, KvOAuthProvider.prototype.state);
|
|
2029
|
-
registerAuthMethod(KvOAuthProvider.prototype, "saveCodeVerifier", function (verifier) {
|
|
2030
|
-
return authValue(() => {
|
|
2031
|
-
if (this.signal?.aborted)
|
|
2032
|
-
throw aborted(this.signal);
|
|
2033
|
-
if (!this.allowAuthorization)
|
|
2034
|
-
throw this.authorizationRefused();
|
|
2035
|
-
this.consent = { ...this.consent, verifier };
|
|
2036
|
-
});
|
|
2037
|
-
}, KvOAuthProvider.prototype.saveCodeVerifier);
|
|
2038
|
-
registerAuthMethod(KvOAuthProvider.prototype, "codeVerifier", function () {
|
|
2039
|
-
return authValue(() => {
|
|
2040
|
-
const verifier = this.callback?.flow.verifier ?? this.consent.verifier;
|
|
2041
|
-
if (verifier === undefined) {
|
|
2042
|
-
throw new Error(`No PKCE code verifier for "${this.connectorId}"`);
|
|
2043
|
-
}
|
|
2044
|
-
return verifier;
|
|
2045
|
-
});
|
|
2046
|
-
}, KvOAuthProvider.prototype.codeVerifier);
|
|
2047
|
-
registerAuthMethod(KvOAuthProvider.prototype, "redirectToAuthorization", function (authorizationUrl) {
|
|
2048
|
-
return authFlow(function* () {
|
|
2049
|
-
if (this.signal?.aborted)
|
|
2050
|
-
return;
|
|
2051
|
-
if (!this.allowAuthorization)
|
|
2052
|
-
throw this.authorizationRefused();
|
|
2053
|
-
for (const [name, value] of this.clientOptions?.authorizationParams ?? []) {
|
|
2054
|
-
authorizationUrl.searchParams.set(name, value);
|
|
2055
|
-
}
|
|
2056
|
-
this.assertCredentialFree(authorizationUrl.toString(), (yield authInvoke(this, "boundGrant", [])).body, true);
|
|
2057
|
-
const state = this.consent.state ?? authorizationUrl.searchParams.get("state");
|
|
2058
|
-
if (!state)
|
|
2059
|
-
throw new Error(`OAuth consent for "${this.connectorId}" carries no state`);
|
|
2060
|
-
const client = this.clientOptions?.client;
|
|
2061
|
-
if (client && !(yield authInvoke(this, "boundGrant", [])).body.client) {
|
|
2062
|
-
(yield authInvoke(this, "saveClientInformation", [
|
|
2063
|
-
{ client_id: client.clientId },
|
|
2064
|
-
{ issuer: client.issuer },
|
|
2065
|
-
]));
|
|
2066
|
-
}
|
|
2067
|
-
const configured = this.configuredAuthorizationBinding();
|
|
2068
|
-
if (configured?.issuer !== undefined) {
|
|
2069
|
-
const issuer = configured.issuer;
|
|
2070
|
-
// Static adapters do not save discovery or client configuration. Publish
|
|
2071
|
-
// their server identity before consent so a changed endpoint fences the
|
|
2072
|
-
// old grant before any code exchange can leave.
|
|
2073
|
-
(yield authInvoke(this, "updateGrant", [
|
|
2074
|
-
(grant) => (grant.body.issuer === issuer ? undefined : this.withIssuer(grant, { issuer })),
|
|
2075
|
-
]));
|
|
2076
|
-
}
|
|
2077
|
-
const epoch = (yield authInvoke(this, "flowEpoch", []));
|
|
2078
|
-
this.epoch = epoch;
|
|
2079
|
-
const digest = (yield authInvoke(this, "wait", [
|
|
2080
|
-
authMethod(() => {
|
|
2081
|
-
return authCall(oauthStateDigest, [state]);
|
|
2082
|
-
}),
|
|
2083
|
-
]));
|
|
2084
|
-
const key = oauthFlowKeys.flow(digest);
|
|
2085
|
-
const url = authorizationUrl.toString();
|
|
2086
|
-
const { verifier } = this.consent;
|
|
2087
|
-
const grant = (yield authInvoke(this, "boundGrant", []));
|
|
2088
|
-
this.assertCredentialFree(url, grant.body, true);
|
|
2089
|
-
const binding = this.configuredAuthorizationBinding() ?? {
|
|
2090
|
-
...(yield authInvoke(this, "wait", [
|
|
2091
|
-
authMethod(() => {
|
|
2092
|
-
return authCall(discoveryBinding, [this.consentDiscovery ?? grant.body.discovery]);
|
|
2093
|
-
}),
|
|
2094
|
-
])),
|
|
2095
|
-
issuer: discoveryIssuer(this.consentDiscovery) ?? grant.body.issuer,
|
|
2096
|
-
clientId: authorizationUrl.searchParams.get("client_id") ?? grant.body.client?.value.client_id,
|
|
2097
|
-
};
|
|
2098
|
-
const record = (yield authInvoke(this, "encodeFlow", [
|
|
2099
|
-
key,
|
|
2100
|
-
{
|
|
2101
|
-
epoch,
|
|
2102
|
-
at: Date.now(),
|
|
2103
|
-
binding,
|
|
2104
|
-
url,
|
|
2105
|
-
...(verifier !== undefined ? { verifier } : {}),
|
|
2106
|
-
},
|
|
2107
|
-
]));
|
|
2108
|
-
if (this.signal?.aborted)
|
|
2109
|
-
return;
|
|
2110
|
-
if (!(yield authInvoke(this, "wait", [
|
|
2111
|
-
authMethod(() => {
|
|
2112
|
-
return authStorage(this.storage, "compareAndSet", [
|
|
2113
|
-
key,
|
|
2114
|
-
null,
|
|
2115
|
-
record,
|
|
2116
|
-
{ ttlSeconds: OAUTH_FLOW_TTL_SECONDS },
|
|
2117
|
-
]);
|
|
2118
|
-
}),
|
|
2119
|
-
]))) {
|
|
2120
|
-
throw new Error(`OAuth consent for "${this.connectorId}" reused a state`);
|
|
2121
|
-
}
|
|
2122
|
-
let published = false;
|
|
2123
|
-
try {
|
|
2124
|
-
published = (yield authInvoke(this, "updateGrant", [
|
|
2125
|
-
(grant) => ({ ...grant, flow: digest }),
|
|
2126
|
-
{ epoch },
|
|
2127
|
-
]));
|
|
2128
|
-
}
|
|
2129
|
-
finally {
|
|
2130
|
-
if (!published)
|
|
2131
|
-
(yield authCatch(authInvoke(this, "wait", [
|
|
2132
|
-
authMethod(() => {
|
|
2133
|
-
return authStorage(this.storage, "compareAndSet", [key, record, null]);
|
|
2134
|
-
}),
|
|
2135
|
-
]), () => { }));
|
|
2136
|
-
}
|
|
2137
|
-
if (published)
|
|
2138
|
-
this.published = { epoch, url };
|
|
2139
|
-
}.bind(this));
|
|
2140
|
-
}, KvOAuthProvider.prototype.redirectToAuthorization);
|
|
2141
|
-
registerAuthMethod(KvOAuthProvider.prototype, "consentUrl", function (signal) {
|
|
2142
|
-
signal = signal === undefined ? this.signal : signal;
|
|
2143
|
-
return authFlow(function* () {
|
|
2144
|
-
const published = this.published;
|
|
2145
|
-
if (!published)
|
|
2146
|
-
return undefined;
|
|
2147
|
-
if (epochOf((yield authInvoke(this, "wait", [
|
|
2148
|
-
authMethod(() => {
|
|
2149
|
-
return authStorage(this.storage, "get", [GRANT]);
|
|
2150
|
-
}),
|
|
2151
|
-
signal,
|
|
2152
|
-
]))) !== published.epoch)
|
|
2153
|
-
throw this.flowSuperseded();
|
|
2154
|
-
this.assertCredentialFree(published.url, (yield authInvoke(this, "boundGrant", [signal])).body, true);
|
|
2155
|
-
return published.url;
|
|
2156
|
-
}.bind(this));
|
|
2157
|
-
}, KvOAuthProvider.prototype.consentUrl);
|
|
2158
|
-
registerAuthMethod(KvOAuthProvider.prototype, "pendingAuthorizationUrl", function () {
|
|
2159
|
-
return authFlow(function* () {
|
|
2160
|
-
const url = (yield authInvoke(this, "latestConsent", []))?.url;
|
|
2161
|
-
this.assertCredentialFree(url, (yield authInvoke(this, "boundGrant", [])).body, true);
|
|
2162
|
-
return url;
|
|
2163
|
-
}.bind(this));
|
|
2164
|
-
}, KvOAuthProvider.prototype.pendingAuthorizationUrl);
|
|
2165
|
-
registerAuthMethod(KvOAuthProvider.prototype, "reusablePendingAuthorizationUrl", function () {
|
|
2166
|
-
return authFlow(function* () {
|
|
2167
|
-
const consent = (yield authInvoke(this, "latestConsent", []));
|
|
2168
|
-
if (!consent || Math.abs(Date.now() - consent.at) >= PENDING_AUTHORIZATION_MAX_AGE_MS) {
|
|
2169
|
-
return undefined;
|
|
2170
|
-
}
|
|
2171
|
-
this.assertCredentialFree(consent.url, (yield authInvoke(this, "boundGrant", [])).body, true);
|
|
2172
|
-
let clientId;
|
|
2173
|
-
try {
|
|
2174
|
-
clientId = new URL(consent.url).searchParams.get("client_id");
|
|
2175
|
-
}
|
|
2176
|
-
catch {
|
|
2177
|
-
return undefined;
|
|
2178
|
-
}
|
|
2179
|
-
const client = (yield authInvoke(this, "clientInformation", []));
|
|
2180
|
-
return clientId !== null && client?.client_id === clientId ? consent.url : undefined;
|
|
2181
|
-
}.bind(this));
|
|
2182
|
-
}, KvOAuthProvider.prototype.reusablePendingAuthorizationUrl);
|
|
2183
|
-
registerAuthMethod(KvOAuthProvider.prototype, "verifyState", function (candidate) {
|
|
2184
|
-
return authFlow(function* () {
|
|
2185
|
-
this.callback = undefined;
|
|
2186
|
-
if (candidate === null)
|
|
2187
|
-
return false;
|
|
2188
|
-
const key = oauthFlowKeys.flow((yield authInvoke(this, "wait", [
|
|
2189
|
-
authMethod(() => {
|
|
2190
|
-
return authCall(oauthStateDigest, [candidate]);
|
|
2191
|
-
}),
|
|
2192
|
-
])));
|
|
2193
|
-
const raw = (yield authInvoke(this, "wait", [
|
|
2194
|
-
authMethod(() => {
|
|
2195
|
-
return authStorage(this.storage, "get", [key]);
|
|
2196
|
-
}),
|
|
2197
|
-
]));
|
|
2198
|
-
const flow = raw === null ? undefined : (yield authInvoke(this, "openFlow", [key, raw, true]));
|
|
2199
|
-
if (raw === null || !flow || flow.consumed)
|
|
2200
|
-
return false;
|
|
2201
|
-
this.callback = { key, raw, flow, claimed: false };
|
|
2202
|
-
this.epoch = flow.epoch;
|
|
2203
|
-
return true;
|
|
2204
|
-
}.bind(this));
|
|
2205
|
-
}, KvOAuthProvider.prototype.verifyState);
|
|
2206
|
-
registerAuthMethod(KvOAuthProvider.prototype, "claimCodeExchange", function () {
|
|
2207
|
-
return authFlow(function* () {
|
|
2208
|
-
if (this.exchangeRefusal)
|
|
2209
|
-
return this.exchangeRefusal.clone();
|
|
2210
|
-
(yield authInvoke(this, "claimConsent", []));
|
|
2211
|
-
return undefined;
|
|
2212
|
-
}.bind(this));
|
|
2213
|
-
}, KvOAuthProvider.prototype.claimCodeExchange);
|
|
2214
|
-
registerAuthMethod(KvOAuthProvider.prototype, "consumeAuthError", function () {
|
|
2215
|
-
return authFlow(function* () {
|
|
2216
|
-
const { grant } = (yield authInvoke(this, "readGrant", []));
|
|
2217
|
-
if (!this.callback || grant.epoch !== this.callback.flow.epoch)
|
|
2218
|
-
throw this.flowSuperseded();
|
|
2219
|
-
(yield authInvoke(this, "checkFlowBinding", [grant]));
|
|
2220
|
-
(yield authInvoke(this, "claimConsent", []));
|
|
2221
|
-
this.callback = undefined;
|
|
2222
|
-
}.bind(this));
|
|
2223
|
-
}, KvOAuthProvider.prototype.consumeAuthError);
|
|
2224
|
-
registerAuthMethod(KvOAuthProvider.prototype, "claimConsent", function () {
|
|
2225
|
-
return authFlow(function* () {
|
|
2226
|
-
const callback = this.callback;
|
|
2227
|
-
if (!callback)
|
|
2228
|
-
throw this.flowSuperseded();
|
|
2229
|
-
if (callback.claimed)
|
|
2230
|
-
throw new OAuthCallbackClaimedError(this.connectorId);
|
|
2231
|
-
callback.claimed = true;
|
|
2232
|
-
const { epoch, at } = callback.flow;
|
|
2233
|
-
const claimed = JSON.stringify({
|
|
2234
|
-
connectaOAuthFlow: FLOW_VERSION,
|
|
2235
|
-
epoch,
|
|
2236
|
-
at,
|
|
2237
|
-
consumed: true,
|
|
2238
|
-
});
|
|
2239
|
-
const remaining = Math.ceil((at + OAUTH_FLOW_TTL_SECONDS * 1000 - Date.now()) / 1000);
|
|
2240
|
-
const ttlSeconds = Math.min(OAUTH_FLOW_TTL_SECONDS, Math.max(1, remaining));
|
|
2241
|
-
if ((yield authInvoke(this, "wait", [
|
|
2242
|
-
authMethod(() => {
|
|
2243
|
-
return authStorage(this.storage, "compareAndSet", [
|
|
2244
|
-
callback.key,
|
|
2245
|
-
callback.raw,
|
|
2246
|
-
claimed,
|
|
2247
|
-
{ ttlSeconds },
|
|
2248
|
-
]);
|
|
2249
|
-
}),
|
|
2250
|
-
]))) {
|
|
2251
|
-
return;
|
|
2252
|
-
}
|
|
2253
|
-
if (epochOf((yield authInvoke(this, "wait", [
|
|
2254
|
-
authMethod(() => {
|
|
2255
|
-
return authStorage(this.storage, "get", [GRANT]);
|
|
2256
|
-
}),
|
|
2257
|
-
]))) !== epoch)
|
|
2258
|
-
throw this.flowSuperseded();
|
|
2259
|
-
throw new OAuthCallbackClaimedError(this.connectorId);
|
|
2260
|
-
}.bind(this));
|
|
2261
|
-
}, KvOAuthProvider.prototype.claimConsent);
|
|
2262
|
-
registerAuthMethod(KvOAuthProvider.prototype, "dispatchCodeExchange", function (send, endpoint) {
|
|
2263
|
-
return authFlow(function* () {
|
|
2264
|
-
const epoch = this.epoch;
|
|
2265
|
-
if (!this.codeExchange || epoch === undefined)
|
|
2266
|
-
throw this.flowSuperseded();
|
|
2267
|
-
const raw = (yield authInvoke(this, "wait", [
|
|
2268
|
-
authMethod(() => {
|
|
2269
|
-
return authStorage(this.storage, "get", [GRANT]);
|
|
2270
|
-
}),
|
|
2271
|
-
]));
|
|
2272
|
-
const grant = (yield authInvoke(this, "openGrant", [raw]));
|
|
2273
|
-
if (grant.epoch !== epoch)
|
|
2274
|
-
throw this.flowSuperseded();
|
|
2275
|
-
(yield authInvoke(this, "checkFlowBinding", [grant]));
|
|
2276
|
-
if (endpoint !== undefined && this.callback?.flow.binding?.tokenEndpoint !== String(endpoint)) {
|
|
2277
|
-
throw this.flowSuperseded();
|
|
2278
|
-
}
|
|
2279
|
-
// Opening ciphertext may yield. Re-read the exact record after opening,
|
|
2280
|
-
// then check and send in the same reaction, without another await.
|
|
2281
|
-
return (yield authInvoke(this, "wait", [
|
|
2282
|
-
authMethod(() => {
|
|
2283
|
-
return authThen(authStorage(this.storage, "get", [GRANT]), authMethod((current) => {
|
|
2284
|
-
return authFlow(function* () {
|
|
2285
|
-
if (this.signal?.aborted)
|
|
2286
|
-
throw aborted(this.signal);
|
|
2287
|
-
if (current !== raw)
|
|
2288
|
-
throw this.flowSuperseded();
|
|
2289
|
-
const sent = send();
|
|
2290
|
-
sent.catch(() => { });
|
|
2291
|
-
return (yield authValue(() => sent));
|
|
2292
|
-
}.bind(this));
|
|
2293
|
-
}));
|
|
2294
|
-
}),
|
|
2295
|
-
]));
|
|
2296
|
-
}.bind(this));
|
|
2297
|
-
}, KvOAuthProvider.prototype.dispatchCodeExchange);
|
|
2298
|
-
registerAuthMethod(KvOAuthProvider.prototype, "recordCodeExchangeTokens", function (tokens) {
|
|
2299
|
-
return authFlow(function* () {
|
|
2300
|
-
this.exchangeConsent = undefined;
|
|
2301
|
-
if (tokens.refresh_token === undefined)
|
|
2302
|
-
return;
|
|
2303
|
-
try {
|
|
2304
|
-
const digest = (yield authInvoke(this, "wait", [
|
|
2305
|
-
authMethod(() => {
|
|
2306
|
-
return authCall(oauthStateDigest, [tokens.refresh_token]);
|
|
2307
|
-
}),
|
|
2308
|
-
]));
|
|
2309
|
-
const resolution = (yield authInvoke(this, "wait", [
|
|
2310
|
-
authMethod(() => {
|
|
2311
|
-
return authStorage(this.storage, "get", [oauthRefreshSpentKeys.spent(digest)]);
|
|
2312
|
-
}),
|
|
2313
|
-
]));
|
|
2314
|
-
const spent = storedRefreshSpent(resolution);
|
|
2315
|
-
// Observing the exact resolved storage write before consent completion
|
|
2316
|
-
// proves their order without comparing isolate clocks. A consent that
|
|
2317
|
-
// saw outstanding/ambiguous cannot become eligible on a delayed save.
|
|
2318
|
-
if (spent?.state === "resolved")
|
|
2319
|
-
this.exchangeConsent = { digest, resolution: resolution };
|
|
2320
|
-
}
|
|
2321
|
-
catch {
|
|
2322
|
-
if (this.signal?.aborted)
|
|
2323
|
-
throw aborted(this.signal);
|
|
2324
|
-
throw this.credentialWriteError();
|
|
2325
|
-
}
|
|
2326
|
-
}.bind(this));
|
|
2327
|
-
}, KvOAuthProvider.prototype.recordCodeExchangeTokens);
|
|
2328
|
-
registerAuthMethod(KvOAuthProvider.prototype, "invalidateCredentials", function (scope) {
|
|
2329
|
-
return authFlow(function* () {
|
|
2330
|
-
if (this.signal?.aborted)
|
|
2331
|
-
throw aborted(this.signal);
|
|
2332
|
-
const fields = {
|
|
2333
|
-
all: ["client", "tokens", "discovery"],
|
|
2334
|
-
client: ["client"],
|
|
2335
|
-
tokens: ["tokens"],
|
|
2336
|
-
discovery: ["discovery"],
|
|
2337
|
-
verifier: [],
|
|
2338
|
-
}[scope];
|
|
2339
|
-
if (fields.length > 0) {
|
|
2340
|
-
(yield authInvoke(this, "updateGrant", [
|
|
2341
|
-
(grant) => {
|
|
2342
|
-
const body = { ...grant.body };
|
|
2343
|
-
let changed = false;
|
|
2344
|
-
for (const field of fields) {
|
|
2345
|
-
const current = field === "client" ? body.client?.value : body[field];
|
|
2346
|
-
if (current === undefined)
|
|
2347
|
-
continue;
|
|
2348
|
-
if (field !== "discovery" && fingerprint(current) !== this.seen[field])
|
|
2349
|
-
continue;
|
|
2350
|
-
delete body[field];
|
|
2351
|
-
changed = true;
|
|
2352
|
-
}
|
|
2353
|
-
return changed ? { ...grant, body } : undefined;
|
|
2354
|
-
},
|
|
2355
|
-
{ quiet: true },
|
|
2356
|
-
]));
|
|
2357
|
-
}
|
|
2358
|
-
// The SDK invalidates after a refused refresh and then starts over, which
|
|
2359
|
-
// on `invalid_client` means registering a new client. A passive request
|
|
2360
|
-
// must not begin authorization, so it ends here, as auth_required.
|
|
2361
|
-
if (!this.allowAuthorization && this.refreshFailure?.kind === "dead") {
|
|
2362
|
-
throw this.authorizationRefused();
|
|
2363
|
-
}
|
|
2364
|
-
}.bind(this));
|
|
2365
|
-
}, KvOAuthProvider.prototype.invalidateCredentials);
|
|
2366
|
-
registerAuthMethod(KvOAuthProvider.prototype, "claimRefresh", function (epoch, requested, signal, sharedWait) {
|
|
2367
|
-
return authFlow(function* () {
|
|
2368
|
-
const waiting = sharedWait ?? refreshWait(signal);
|
|
2369
|
-
const wait = waiting.run;
|
|
2370
|
-
const read = (key) => authCall(wait, [authMethod(() => authStorage(this.storage, "get", [key]))]);
|
|
2371
|
-
const discard = () => authCall(wait, [
|
|
2372
|
-
authMethod(() => authInvoke(this, "discardRefusedGrant", [requested, epoch, waiting.transfer, waiting.signal])),
|
|
2373
|
-
]);
|
|
2374
|
-
const tokens = () => authCall(wait, [
|
|
2375
|
-
authMethod(() => authInvoke(this, "storedTokens", [waiting.signal])),
|
|
2376
|
-
]);
|
|
2377
|
-
let delay = 10;
|
|
2378
|
-
try {
|
|
2379
|
-
const digest = (yield authCall(wait, [
|
|
2380
|
-
authMethod(() => {
|
|
2381
|
-
return authCall(oauthStateDigest, [requested ?? ""]);
|
|
2382
|
-
}),
|
|
2383
|
-
]));
|
|
2384
|
-
const key = oauthRefreshKeys.lease(epoch, digest);
|
|
2385
|
-
const spentKey = oauthRefreshSpentKeys.spent(digest);
|
|
2386
|
-
while (!waiting.signal.aborted) {
|
|
2387
|
-
const current = (yield tokens());
|
|
2388
|
-
if (current?.refresh_token !== requested || (current && this.refreshedSinceRead(current, epoch))) {
|
|
2389
|
-
if (!current)
|
|
2390
|
-
this.recordRefreshFailure({ kind: "dead" });
|
|
2391
|
-
return current
|
|
2392
|
-
? this.adoptRefresh(current)
|
|
2393
|
-
: Response.json({ error: "invalid_grant" }, { status: 400 });
|
|
2394
|
-
}
|
|
2395
|
-
const raw = (yield read(key));
|
|
2396
|
-
const stored = raw === null ? undefined : parsed(raw);
|
|
2397
|
-
const committed = plainObject(stored) &&
|
|
2398
|
-
(stored.completed === true ||
|
|
2399
|
-
(stored.state === "claimed" && typeof stored.predecessor === "string")) &&
|
|
2400
|
-
current !== undefined &&
|
|
2401
|
-
stored.tokenDigest ===
|
|
2402
|
-
(yield authCall(wait, [
|
|
2403
|
-
authMethod(() => {
|
|
2404
|
-
return authCall(refreshTokensDigest, [current]);
|
|
2405
|
-
}),
|
|
2406
|
-
]));
|
|
2407
|
-
const completed = committed && stored.completed === true;
|
|
2408
|
-
if (plainObject(stored) && (plainObject(stored.verdict) || (stored.completed === true && !committed))) {
|
|
2409
|
-
this.recordRefreshFailure({ kind: "dead" });
|
|
2410
|
-
(yield discard());
|
|
2411
|
-
throw new UnauthorizedError("OAuth refresh grant is no longer active.");
|
|
2412
|
-
}
|
|
2413
|
-
if (plainObject(stored) && stored.state !== "claimed" && !completed) {
|
|
2414
|
-
// Never compare another isolate's clock for a sent request. The
|
|
2415
|
-
// durable record survives expiry of this storage-owned liveness key.
|
|
2416
|
-
const active = typeof stored.activeKey === "string"
|
|
2417
|
-
? (yield read(stored.activeKey))
|
|
2418
|
-
: null;
|
|
2419
|
-
if (active === null) {
|
|
2420
|
-
const refused = JSON.stringify({ ...stored, verdict: { kind: "dead" } });
|
|
2421
|
-
if ((yield authCall(wait, [
|
|
2422
|
-
authMethod(() => {
|
|
2423
|
-
return authStorage(this.storage, "compareAndSet", [key, raw, refused], () => waiting.transfer());
|
|
2424
|
-
}),
|
|
2425
|
-
]))) {
|
|
2426
|
-
(yield authCall(wait, [
|
|
2427
|
-
authMethod(() => {
|
|
2428
|
-
return authInvoke(this, "resolveRefreshSpent", [
|
|
2429
|
-
{ key, spentKey, spentExpected: null, raw: raw },
|
|
2430
|
-
"ambiguous",
|
|
2431
|
-
]);
|
|
2432
|
-
}),
|
|
2433
|
-
]));
|
|
2434
|
-
this.recordRefreshFailure({ kind: "dead" });
|
|
2435
|
-
(yield discard());
|
|
2436
|
-
throw new UnauthorizedError("OAuth refresh outcome is unknown; authorization required.");
|
|
2437
|
-
}
|
|
2438
|
-
continue;
|
|
2439
|
-
}
|
|
2440
|
-
}
|
|
2441
|
-
else {
|
|
2442
|
-
const spentExpected = (yield read(spentKey));
|
|
2443
|
-
if (spentExpected !== null) {
|
|
2444
|
-
const spent = storedRefreshSpent(spentExpected);
|
|
2445
|
-
const consent = (yield authCall(wait, [
|
|
2446
|
-
authMethod(() => {
|
|
2447
|
-
return authInvoke(this, "boundGrant", [waiting.signal]);
|
|
2448
|
-
}),
|
|
2449
|
-
])).body.refreshConsent;
|
|
2450
|
-
if (spent?.state !== "resolved" ||
|
|
2451
|
-
!(
|
|
2452
|
-
// A completed lease proves these exact tokens committed.
|
|
2453
|
-
// Reusable refresh tokens may start another generation,
|
|
2454
|
-
// but only after its own dispatch is durably resolved.
|
|
2455
|
-
(committed &&
|
|
2456
|
-
spent.epoch === epoch &&
|
|
2457
|
-
spent.holder === (completed ? stored.holder : stored.predecessor)) ||
|
|
2458
|
-
(spent.epoch !== epoch && consent?.digest === digest && consent.resolution === spentExpected))) {
|
|
2459
|
-
// Lease and fingerprint are separate records. A competing
|
|
2460
|
-
// generation can advance both between these reads; retry
|
|
2461
|
-
// its new lease instead of discarding a live grant.
|
|
2462
|
-
const latestLease = (yield read(key));
|
|
2463
|
-
if (latestLease !== raw)
|
|
2464
|
-
continue;
|
|
2465
|
-
// Reset can sweep the old lease while its request remains live.
|
|
2466
|
-
// Only storage-owned expiry can make an outstanding send ambiguous.
|
|
2467
|
-
if (spent?.state === "outstanding" && (yield read(spent.activeKey)) === null) {
|
|
2468
|
-
(yield authCall(wait, [
|
|
2469
|
-
authMethod(() => {
|
|
2470
|
-
return authStorage(this.storage, "compareAndSet", [spentKey, spentExpected, JSON.stringify({ ...spent, state: "ambiguous" })], () => waiting.transfer());
|
|
2471
|
-
}),
|
|
2472
|
-
]));
|
|
2473
|
-
}
|
|
2474
|
-
this.recordRefreshFailure({ kind: "dead" });
|
|
2475
|
-
(yield discard());
|
|
2476
|
-
throw new UnauthorizedError("OAuth refresh token was already spent; authorization required.");
|
|
2477
|
-
}
|
|
2478
|
-
}
|
|
2479
|
-
if (raw === null ||
|
|
2480
|
-
completed ||
|
|
2481
|
-
(plainObject(stored) &&
|
|
2482
|
-
typeof stored.expiresAt === "number" &&
|
|
2483
|
-
stored.expiresAt <= this.refreshNow())) {
|
|
2484
|
-
// Clock skew can replace an unsent claim; its dispatch CAS then loses.
|
|
2485
|
-
const predecessor = completed
|
|
2486
|
-
? stored.holder
|
|
2487
|
-
: plainObject(stored)
|
|
2488
|
-
? stored.predecessor
|
|
2489
|
-
: undefined;
|
|
2490
|
-
const claimed = JSON.stringify({
|
|
2491
|
-
connectaOAuthRefresh: 1,
|
|
2492
|
-
holder: crypto.randomUUID(),
|
|
2493
|
-
state: "claimed",
|
|
2494
|
-
expiresAt: this.refreshNow() + OAUTH_REFRESH_LEASE_SECONDS * 1000,
|
|
2495
|
-
...(committed && typeof predecessor === "string" && typeof stored.tokenDigest === "string"
|
|
2496
|
-
? {
|
|
2497
|
-
predecessor,
|
|
2498
|
-
tokenDigest: stored.tokenDigest,
|
|
2499
|
-
}
|
|
2500
|
-
: {}),
|
|
2501
|
-
});
|
|
2502
|
-
let won;
|
|
2503
|
-
try {
|
|
2504
|
-
won = (yield authCall(wait, [
|
|
2505
|
-
authMethod(() => {
|
|
2506
|
-
return authStorage(this.storage, "compareAndSet", [key, raw, claimed], () => waiting.transfer());
|
|
2507
|
-
}),
|
|
2508
|
-
]));
|
|
2509
|
-
}
|
|
2510
|
-
catch {
|
|
2511
|
-
throw this.credentialWriteError();
|
|
2512
|
-
}
|
|
2513
|
-
if (won) {
|
|
2514
|
-
try {
|
|
2515
|
-
const latest = (yield tokens());
|
|
2516
|
-
if (latest?.refresh_token !== requested || (latest && this.refreshedSinceRead(latest, epoch))) {
|
|
2517
|
-
(yield authCall(wait, [
|
|
2518
|
-
authMethod(() => {
|
|
2519
|
-
return authInvoke(this, "releaseRefresh", [
|
|
2520
|
-
{ key, spentKey, spentExpected, raw: claimed },
|
|
2521
|
-
]);
|
|
2522
|
-
}),
|
|
2523
|
-
]));
|
|
2524
|
-
return latest
|
|
2525
|
-
? this.adoptRefresh(latest)
|
|
2526
|
-
: Response.json({ error: "invalid_grant" }, { status: 400 });
|
|
2527
|
-
}
|
|
2528
|
-
return { key, spentKey, spentExpected, raw: claimed };
|
|
2529
|
-
}
|
|
2530
|
-
catch (error) {
|
|
2531
|
-
if (!waiting.signal.aborted) {
|
|
2532
|
-
(yield authCall(wait, [
|
|
2533
|
-
authMethod(() => {
|
|
2534
|
-
return authInvoke(this, "releaseRefresh", [
|
|
2535
|
-
{ key, spentKey, spentExpected, raw: claimed },
|
|
2536
|
-
]);
|
|
2537
|
-
}),
|
|
2538
|
-
]));
|
|
2539
|
-
}
|
|
2540
|
-
throw error;
|
|
2541
|
-
}
|
|
2542
|
-
}
|
|
2543
|
-
}
|
|
2544
|
-
}
|
|
2545
|
-
(yield Effect.raceAllFirst([Effect.sleep(delay), fromSignal(waiting.signal)]));
|
|
2546
|
-
delay = Math.min(250, delay * 2);
|
|
2547
|
-
}
|
|
2548
|
-
}
|
|
2549
|
-
catch (error) {
|
|
2550
|
-
if (!waiting.signal.aborted)
|
|
2551
|
-
throw error;
|
|
2552
|
-
}
|
|
2553
|
-
finally {
|
|
2554
|
-
if (!sharedWait)
|
|
2555
|
-
waiting.close();
|
|
2556
|
-
}
|
|
2557
|
-
throw this.refreshContended();
|
|
2558
|
-
}.bind(this));
|
|
2559
|
-
}, KvOAuthProvider.prototype.claimRefresh);
|
|
2560
|
-
registerAuthMethod(KvOAuthProvider.prototype, "dispatchRefresh", function (lease, send, waiting) {
|
|
2561
|
-
return authFlow(function* () {
|
|
2562
|
-
const stored = parsed(lease.raw);
|
|
2563
|
-
const epoch = (yield authInvoke(waiting, "run", [
|
|
2564
|
-
authMethod(() => {
|
|
2565
|
-
return authInvoke(this, "flowEpoch", []);
|
|
2566
|
-
}),
|
|
2567
|
-
]));
|
|
2568
|
-
const activeKey = oauthRefreshActiveKeys.holder(epoch, stored.holder);
|
|
2569
|
-
(yield authInvoke(waiting, "run", [
|
|
2570
|
-
authMethod(() => {
|
|
2571
|
-
return authStorage(this.storage, "set", [
|
|
2572
|
-
activeKey,
|
|
2573
|
-
JSON.stringify({ connectaOAuthRefresh: 1 }),
|
|
2574
|
-
{
|
|
2575
|
-
ttlSeconds: OAUTH_REFRESH_LEASE_SECONDS,
|
|
2576
|
-
},
|
|
2577
|
-
]);
|
|
2578
|
-
}),
|
|
2579
|
-
]));
|
|
2580
|
-
const dispatched = JSON.stringify({
|
|
2581
|
-
...stored,
|
|
2582
|
-
state: "dispatched",
|
|
2583
|
-
activeKey,
|
|
2584
|
-
});
|
|
2585
|
-
const won = (yield authInvoke(waiting, "run", [
|
|
2586
|
-
authMethod(() => authStorage(this.storage, "compareAndSet", [lease.key, lease.raw, dispatched])),
|
|
2587
|
-
]));
|
|
2588
|
-
if (!won)
|
|
2589
|
-
throw this.refreshContended();
|
|
2590
|
-
lease.raw = dispatched;
|
|
2591
|
-
if (waiting.signal.aborted)
|
|
2592
|
-
throw aborted(waiting.signal);
|
|
2593
|
-
// One CAS owns the fingerprint across epochs and generations. Only a
|
|
2594
|
-
// proven successful commit or later consent may replace a resolved send.
|
|
2595
|
-
lease.spentRaw = JSON.stringify({
|
|
2596
|
-
connectaOAuthRefreshSpent: 1,
|
|
2597
|
-
epoch,
|
|
2598
|
-
holder: stored.holder,
|
|
2599
|
-
activeKey,
|
|
2600
|
-
state: "outstanding",
|
|
2601
|
-
});
|
|
2602
|
-
let unspent;
|
|
2603
|
-
try {
|
|
2604
|
-
unspent = (yield authInvoke(waiting, "run", [
|
|
2605
|
-
authMethod(() => authStorage(this.storage, "compareAndSet", [
|
|
2606
|
-
lease.spentKey,
|
|
2607
|
-
lease.spentExpected,
|
|
2608
|
-
lease.spentRaw,
|
|
2609
|
-
])),
|
|
2610
|
-
]));
|
|
2611
|
-
}
|
|
2612
|
-
catch {
|
|
2613
|
-
this.recordRefreshFailure({ kind: "dead" });
|
|
2614
|
-
throw new UnauthorizedError("OAuth refresh dispatch could not be recorded; authorization required.");
|
|
2615
|
-
}
|
|
2616
|
-
if (!unspent) {
|
|
2617
|
-
this.recordRefreshFailure({ kind: "dead" });
|
|
2618
|
-
throw new UnauthorizedError("OAuth refresh token was already spent; authorization required.");
|
|
2619
|
-
}
|
|
2620
|
-
// Final owner check and physical submission share one reaction.
|
|
2621
|
-
// Submission already transferred ownership to the coordinator. Only
|
|
2622
|
-
// its own deadline can stop this send; the departed caller cannot.
|
|
2623
|
-
if (waiting.signal.aborted)
|
|
2624
|
-
throw aborted(waiting.signal);
|
|
2625
|
-
return (yield authCall(send, []));
|
|
2626
|
-
}.bind(this));
|
|
2627
|
-
}, KvOAuthProvider.prototype.dispatchRefresh);
|
|
2628
|
-
registerAuthMethod(KvOAuthProvider.prototype, "releaseRefresh", function (lease, verdict, tokens, resolution = "ambiguous") {
|
|
2629
|
-
return authFlow(function* () {
|
|
2630
|
-
const stored = parsed(lease.raw);
|
|
2631
|
-
// Unsent successors restore the prior completed proof; other unsent
|
|
2632
|
-
// claims may be removed. Dispatched outcomes never reopen a send.
|
|
2633
|
-
const next = stored.state === "claimed"
|
|
2634
|
-
? stored.predecessor && stored.tokenDigest
|
|
2635
|
-
? JSON.stringify({
|
|
2636
|
-
...stored,
|
|
2637
|
-
holder: stored.predecessor,
|
|
2638
|
-
state: "dispatched",
|
|
2639
|
-
completed: true,
|
|
2640
|
-
})
|
|
2641
|
-
: null
|
|
2642
|
-
: JSON.stringify({
|
|
2643
|
-
...stored,
|
|
2644
|
-
...(tokens
|
|
2645
|
-
? {
|
|
2646
|
-
completed: true,
|
|
2647
|
-
tokenDigest: (yield authCall(refreshTokensDigest, [tokens])),
|
|
2648
|
-
}
|
|
2649
|
-
: { verdict: { kind: "dead" } }),
|
|
2650
|
-
});
|
|
2651
|
-
// Verdicts contain fixed typed facts; no downstream causes or text.
|
|
2652
|
-
if (verdict)
|
|
2653
|
-
this.recordRefreshFailure(verdict);
|
|
2654
|
-
(yield authInvoke(this, "resolveRefreshSpent", [lease, resolution]));
|
|
2655
|
-
const released = (yield authCatch(authStorage(this.storage, "compareAndSet", [lease.key, lease.raw, next]), () => false));
|
|
2656
|
-
if (released && stored.activeKey)
|
|
2657
|
-
(yield authCatch(authStorage(this.storage, "delete", [stored.activeKey]), () => { }));
|
|
2658
|
-
}.bind(this));
|
|
2659
|
-
}, KvOAuthProvider.prototype.releaseRefresh);
|
|
2660
|
-
registerAuthMethod(KvOAuthProvider.prototype, "resolveRefreshSpent", function (lease, state) {
|
|
2661
|
-
return authFlow(function* () {
|
|
2662
|
-
try {
|
|
2663
|
-
const raw = lease.spentRaw ?? (yield authStorage(this.storage, "get", [lease.spentKey]));
|
|
2664
|
-
const spent = storedRefreshSpent(raw);
|
|
2665
|
-
const holder = parsed(lease.raw).holder;
|
|
2666
|
-
if (spent?.state !== "outstanding" || spent.holder !== holder)
|
|
2667
|
-
return;
|
|
2668
|
-
// The timestamp belongs to this resolution write, never to dispatch.
|
|
2669
|
-
// Consent copies the exact stored receipt, so wall clocks grant no sends.
|
|
2670
|
-
(yield authStorage(this.storage, "compareAndSet", [
|
|
2671
|
-
lease.spentKey,
|
|
2672
|
-
raw,
|
|
2673
|
-
JSON.stringify({
|
|
2674
|
-
...spent,
|
|
2675
|
-
state,
|
|
2676
|
-
...(state === "resolved" ? { resolvedAt: Date.now() } : {}),
|
|
2677
|
-
}),
|
|
2678
|
-
]));
|
|
2679
|
-
}
|
|
2680
|
-
catch {
|
|
2681
|
-
// A lost resolution answer leaves outstanding or the durable outcome;
|
|
2682
|
-
// either blocks replay until a later consent observes a resolved write.
|
|
2683
|
-
}
|
|
2684
|
-
}.bind(this));
|
|
2685
|
-
}, KvOAuthProvider.prototype.resolveRefreshSpent);
|
|
2686
|
-
registerAuthMethod(KvOAuthProvider.prototype, "storedTokens", function (signal = null) {
|
|
2687
|
-
return authFlow(function* () {
|
|
2688
|
-
return (yield authInvoke(this, "boundGrant", [signal])).body.tokens;
|
|
2689
|
-
}.bind(this));
|
|
2690
|
-
}, KvOAuthProvider.prototype.storedTokens);
|
|
2691
|
-
registerAuthMethod(KvOAuthProvider.prototype, "storeRefresh", function (epoch, requested, tokens, issuer) {
|
|
2692
|
-
return authFlow(function* () {
|
|
2693
|
-
for (let attempt = 0; attempt < MAX_GRANT_WRITES; attempt++) {
|
|
2694
|
-
try {
|
|
2695
|
-
const { raw, grant } = (yield authInvoke(this, "readGrant", [null]));
|
|
2696
|
-
if (grant.epoch !== epoch || (issuer !== undefined && grant.body.issuer !== issuer))
|
|
2697
|
-
return undefined;
|
|
2698
|
-
if (grant.body.tokens?.refresh_token !== requested)
|
|
2699
|
-
return grant.body.tokens;
|
|
2700
|
-
const committed = issuer !== undefined ? { ...tokens, issuer } : tokens;
|
|
2701
|
-
const encoded = (yield authInvoke(this, "encodeGrant", [
|
|
2702
|
-
{ ...grant, body: { ...grant.body, tokens: committed } },
|
|
2703
|
-
null,
|
|
2704
|
-
]));
|
|
2705
|
-
if ((yield authStorage(this.storage, "compareAndSet", [GRANT, raw, encoded])))
|
|
2706
|
-
return committed;
|
|
2707
|
-
}
|
|
2708
|
-
catch {
|
|
2709
|
-
// A failed or lost CAS answer may still have committed. Re-read the
|
|
2710
|
-
// epoch and retry these in-memory tokens without another HTTP request.
|
|
2711
|
-
}
|
|
2712
|
-
}
|
|
2713
|
-
throw this.credentialWriteError();
|
|
2714
|
-
}.bind(this));
|
|
2715
|
-
}, KvOAuthProvider.prototype.storeRefresh);
|
|
2716
|
-
registerAuthMethod(KvOAuthProvider.prototype, "discardRefusedGrant", function (refreshToken, epoch, beforeWrite, signal) {
|
|
2717
|
-
return authFlow(function* () {
|
|
2718
|
-
try {
|
|
2719
|
-
(yield authInvoke(this, "updateGrant", [
|
|
2720
|
-
(grant) => {
|
|
2721
|
-
if (grant.body.tokens?.refresh_token !== refreshToken)
|
|
2722
|
-
return undefined;
|
|
2723
|
-
const { tokens: _refused, ...body } = grant.body;
|
|
2724
|
-
return { ...grant, body };
|
|
2725
|
-
},
|
|
2726
|
-
{
|
|
2727
|
-
epoch,
|
|
2728
|
-
quiet: true,
|
|
2729
|
-
commit: true,
|
|
2730
|
-
...(beforeWrite ? { beforeWrite } : {}),
|
|
2731
|
-
...(signal ? { signal } : {}),
|
|
2732
|
-
},
|
|
2733
|
-
]));
|
|
2734
|
-
}
|
|
2735
|
-
catch {
|
|
2736
|
-
// See above: a refusal that could not be recorded recurs, it does not hide.
|
|
2737
|
-
}
|
|
2738
|
-
}.bind(this));
|
|
2739
|
-
}, KvOAuthProvider.prototype.discardRefusedGrant);
|
|
2740
|
-
registerAuthMethod(KvOAuthProvider.prototype, "resetAuthorization", function (operatorDisconnected = false, preserveClient = false) {
|
|
2741
|
-
return authFlow(function* () {
|
|
2742
|
-
const operation = authInvoke(this, "performReset", [operatorDisconnected, preserveClient]);
|
|
2743
|
-
if (!this.onReset)
|
|
2744
|
-
return (yield operation);
|
|
2745
|
-
const reset = runEdge(operation);
|
|
2746
|
-
this.onReset?.(reset);
|
|
2747
|
-
return (yield authValue(() => reset));
|
|
2748
|
-
}.bind(this));
|
|
2749
|
-
}, KvOAuthProvider.prototype.resetAuthorization);
|
|
2750
|
-
registerAuthMethod(KvOAuthProvider.prototype, "disconnectAuthorization", function (send) {
|
|
2751
|
-
return authFlow(function* () {
|
|
2752
|
-
const operation = authInvoke(this, "performReset", [true, false, send]);
|
|
2753
|
-
if (!this.onReset)
|
|
2754
|
-
return (yield operation);
|
|
2755
|
-
const reset = runEdge(operation);
|
|
2756
|
-
this.onReset?.(reset);
|
|
2757
|
-
return (yield authValue(() => reset));
|
|
2758
|
-
}.bind(this));
|
|
2759
|
-
}, KvOAuthProvider.prototype.disconnectAuthorization);
|
|
2760
|
-
registerAuthMethod(KvOAuthProvider.prototype, "performReset", function (operatorDisconnected, preserveClient, revoke) {
|
|
2761
|
-
return authFlow(function* () {
|
|
2762
|
-
const epoch = `${operatorDisconnected ? DISCONNECTED_EPOCH_PREFIX : ACTIVE_EPOCH_PREFIX}${crypto.randomUUID()}`;
|
|
2763
|
-
for (let attempt = 0; attempt < MAX_GRANT_WRITES; attempt++) {
|
|
2764
|
-
const { raw, grant } = (yield authInvoke(this, "readGrant", [null]));
|
|
2765
|
-
const carried = preserveClient && !operatorDisconnected ? this.carriableClient(grant) : undefined;
|
|
2766
|
-
const encoded = (yield authInvoke(this, "encodeGrant", [
|
|
2767
|
-
{ epoch, body: carried ?? {}, cleanupPending: true },
|
|
2768
|
-
null,
|
|
2769
|
-
]));
|
|
2770
|
-
let written;
|
|
2771
|
-
try {
|
|
2772
|
-
written = (yield authStorage(this.storage, "compareAndSet", [GRANT, raw, encoded]));
|
|
2773
|
-
}
|
|
2774
|
-
catch {
|
|
2775
|
-
throw this.credentialWriteError();
|
|
2776
|
-
}
|
|
2777
|
-
if (written) {
|
|
2778
|
-
this.refreshCoordinator?.retire(grant.epoch);
|
|
2779
|
-
try {
|
|
2780
|
-
(yield authInvoke(this, "cleanupV2", [encoded, null]));
|
|
2781
|
-
(yield authInvoke(this, "sweepConsents", []));
|
|
2782
|
-
(yield authInvoke(this, "sweepRefreshLeases", []));
|
|
2783
|
-
}
|
|
2784
|
-
finally {
|
|
2785
|
-
if (revoke) {
|
|
2786
|
-
const snapshot = this.revocationSnapshot(grant.body);
|
|
2787
|
-
(yield authCall(revokeDownstreamGrant, [snapshot.body, revoke, snapshot.client]));
|
|
2788
|
-
}
|
|
2789
|
-
}
|
|
2790
|
-
return;
|
|
2791
|
-
}
|
|
2792
|
-
}
|
|
2793
|
-
throw this.credentialWriteError();
|
|
2794
|
-
}.bind(this));
|
|
2795
|
-
}, KvOAuthProvider.prototype.performReset);
|
|
2796
|
-
registerAuthMethod(KvOAuthProvider.prototype, "sweepConsents", function () {
|
|
2797
|
-
return authFlow(function* () {
|
|
2798
|
-
try {
|
|
2799
|
-
const consents = [];
|
|
2800
|
-
for (const key of (yield authStorage(this.storage, "list", [oauthFlowKeys.prefix]))) {
|
|
2801
|
-
const raw = (yield authStorage(this.storage, "get", [key]));
|
|
2802
|
-
if (raw !== null)
|
|
2803
|
-
consents.push([key, raw]);
|
|
2804
|
-
}
|
|
2805
|
-
const live = epochOf((yield authStorage(this.storage, "get", [GRANT])));
|
|
2806
|
-
for (const [key, raw] of consents) {
|
|
2807
|
-
const flow = parsed(raw);
|
|
2808
|
-
if (plainObject(flow) && flow.epoch === live)
|
|
2809
|
-
continue;
|
|
2810
|
-
(yield authStorage(this.storage, "compareAndSet", [key, raw, null]));
|
|
2811
|
-
}
|
|
2812
|
-
}
|
|
2813
|
-
catch {
|
|
2814
|
-
// Hygiene only: the epoch, not this, fences a stale consent.
|
|
2815
|
-
}
|
|
2816
|
-
}.bind(this));
|
|
2817
|
-
}, KvOAuthProvider.prototype.sweepConsents);
|
|
2818
|
-
registerAuthMethod(KvOAuthProvider.prototype, "sweepRefreshLeases", function () {
|
|
2819
|
-
return authFlow(function* () {
|
|
2820
|
-
try {
|
|
2821
|
-
const records = [];
|
|
2822
|
-
// A reset may sweep an old lease while its HTTP request is still live.
|
|
2823
|
-
// Keep holder liveness until completion or its storage-owned TTL: spent
|
|
2824
|
-
// records in later epochs still use it to distinguish live from crashed.
|
|
2825
|
-
for (const key of (yield authStorage(this.storage, "list", [oauthRefreshKeys.prefix]))) {
|
|
2826
|
-
const raw = (yield authStorage(this.storage, "get", [key]));
|
|
2827
|
-
if (raw !== null)
|
|
2828
|
-
records.push([key, raw]);
|
|
2829
|
-
}
|
|
2830
|
-
const live = epochOf((yield authStorage(this.storage, "get", [GRANT])));
|
|
2831
|
-
for (const [key, raw] of records) {
|
|
2832
|
-
if (!key.startsWith(`${oauthRefreshKeys.prefix}${live}:`)) {
|
|
2833
|
-
(yield authStorage(this.storage, "compareAndSet", [key, raw, null]));
|
|
2834
|
-
}
|
|
2835
|
-
}
|
|
2836
|
-
}
|
|
2837
|
-
catch {
|
|
2838
|
-
// Stale dispatch records cannot grant credentials: epochs never recur.
|
|
2839
|
-
}
|
|
2840
|
-
}.bind(this));
|
|
2841
|
-
}, KvOAuthProvider.prototype.sweepRefreshLeases);
|
|
386
|
+
authMethods(KvOAuthProvider.prototype, {
|
|
387
|
+
...grantStoreMethods,
|
|
388
|
+
...consentMethods,
|
|
389
|
+
...sdkHookMethods,
|
|
390
|
+
...refreshRecordMethods,
|
|
391
|
+
});
|
|
2842
392
|
}
|
|
2843
393
|
}
|