@zackbart/connecta 0.38.0 → 0.40.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 +91 -0
- package/README.md +3 -2
- package/bin/connecta.mjs +4 -8
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +6 -4
- package/dist/agent-routes.js +231 -0
- 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 -2382
- package/dist/auth/native.js +37 -0
- package/dist/auth/oauth-grant.js +13 -0
- package/dist/auth/static-oauth.js +153 -131
- package/dist/auth-elicitation.js +7 -1
- package/dist/authorization-handoff.js +11 -5
- package/dist/catalog-cache.js +219 -59
- package/dist/catalog-policy.js +18 -0
- package/dist/catalog-service.js +284 -47
- package/dist/catalog.js +18 -2
- package/dist/config.d.ts +16 -11
- package/dist/config.js +9 -6
- 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 +7 -4
- 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 +682 -0
- package/dist/connectors/remote-mcp/errors.js +488 -0
- package/dist/connectors/remote-mcp/oauth.js +473 -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 +555 -0
- package/dist/connectors/remote-mcp.js +62 -2863
- 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/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 +2 -0
- package/dist/errors.d.ts +55 -17
- package/dist/errors.js +95 -41
- package/dist/execute.js +152 -114
- package/dist/executor-result.js +3 -1
- package/dist/guest-functions.js +83 -0
- package/dist/guest-runtime.js +60 -19
- package/dist/guest-types.d.ts +19 -5
- package/dist/http/argument-problems.js +35 -0
- package/dist/http/errors.js +5 -4
- 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/oauth.js +3 -2
- package/dist/http/provider.js +32 -4
- package/dist/http/safety.js +8 -0
- package/dist/http/tools.js +44 -23
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -2
- package/dist/invocation.js +26 -8
- package/dist/membership.d.ts +13 -5
- package/dist/membership.js +34 -2
- package/dist/meta-tools.js +54 -31
- package/dist/native-recovery.js +4 -16
- package/dist/native-tools.js +5 -2
- package/dist/oauth-sealing.js +10 -0
- package/dist/operation-vocabulary.js +135 -13
- package/dist/operator-record.d.ts +2 -2
- package/dist/operator-record.js +1 -0
- package/dist/operator-ui/contract.d.ts +10 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +22 -1
- package/dist/provider-call-facts.js +6 -2
- package/dist/providers/_shared/rest/operation-index.js +48 -50
- package/dist/providers/_shared/rest/value-safety.js +80 -0
- package/dist/providers/breeze/api.js +7 -1
- package/dist/providers/breeze/skill.generated.js +1 -1
- package/dist/providers/breeze/tools.js +4 -3
- package/dist/providers/ccb/api.js +2 -1
- package/dist/providers/ccb/index.js +5 -0
- package/dist/providers/ccb/skill.generated.js +1 -1
- package/dist/providers/cloudflare/api.js +8 -1
- 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 +2 -4
- 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 +11 -16
- package/dist/providers/forms/api.js +1 -0
- package/dist/providers/forms/skill.generated.js +1 -1
- package/dist/providers/forms/tools.js +10 -7
- 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 +7 -5
- package/dist/providers/granola/index.js +2 -0
- package/dist/providers/granola/skill.generated.js +1 -1
- package/dist/providers/overflow/api.js +7 -1
- package/dist/providers/overflow/skill.generated.js +1 -1
- package/dist/providers/planning-center/api.js +3 -1
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/planning-center/skill.generated.js +1 -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 +13 -14
- package/dist/providers/slides/skill.generated.js +1 -1
- package/dist/providers/slides/tools.js +15 -13
- package/dist/providers/stripe/skill.generated.js +1 -1
- package/dist/providers/tithely/api.js +3 -1
- package/dist/providers/tithely/skill.generated.js +1 -1
- package/dist/providers/vercel/api.js +1 -1
- package/dist/providers/vercel/skill.generated.js +1 -1
- package/dist/providers/vercel/tools.js +4 -3
- package/dist/registry.d.ts +10 -4
- package/dist/registry.js +41 -13
- package/dist/routes/credentials.js +129 -60
- package/dist/routes/dispatch.js +5 -1
- package/dist/routes/mcp.js +30 -14
- package/dist/routes/operator.js +2 -2
- package/dist/routes/shared.d.ts +6 -1
- package/dist/routes/shared.js +7 -1
- package/dist/routes/ui-config.js +2 -2
- package/dist/routes/ui-data.js +126 -20
- package/dist/routes/ui.js +16 -0
- package/dist/runtime/call.js +7 -2
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +207 -0
- package/dist/runtime/request-scope.js +16 -0
- package/dist/runtime/storage.js +12 -0
- package/dist/skills.js +37 -20
- package/dist/storage/keys.js +53 -3
- package/dist/storage/memory.js +8 -2
- package/dist/storage/sql-native.js +53 -1
- package/dist/storage/sql.js +12 -2
- package/dist/storage-digest.js +32 -0
- package/dist/timeout.js +8 -0
- package/dist/types.d.ts +33 -0
- package/dist/usage-guide.js +16 -35
- 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/write-outcome.js +0 -3
- package/documentation/README.md +13 -13
- package/documentation/architecture.md +385 -20
- package/documentation/auth.md +233 -64
- package/documentation/code-mode.md +122 -39
- package/documentation/deploying.md +6 -3
- package/documentation/integrating.md +16 -2
- package/documentation/meta-tools.md +240 -49
- package/documentation/migration-0.33.md +5 -1
- package/documentation/migration-0.34.md +3 -1
- package/documentation/operator-tests.md +1 -1
- package/documentation/operator-ui.md +105 -12
- package/documentation/pool-membership-upgrade.md +44 -0
- package/documentation/providers.md +105 -0
- package/examples/worker/AGENTS.md +10 -10
- package/examples/worker/README.md +4 -3
- package/examples/worker/src/connecta.config.ts +1 -0
- package/guides/connecting-agents.md +2 -1
- package/guides/how-it-works.md +4 -2
- package/package.json +1 -1
- package/templates/node/AGENTS.md +13 -12
- package/templates/node/package.json +1 -1
- package/templates/node/src/connecta.config.ts +1 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { runEdge } from "../../runtime/run.js";
|
|
3
|
+
import { oauthGrantKeys, retiredOAuthKeys } from "../../storage/keys.js";
|
|
4
|
+
import { revokeDownstreamGrant } from "../downstream-revocation.js";
|
|
5
|
+
import { authAwait, authCall, authCatch, authFlow, authInvoke, authMethod, authStorage, authValue } from "../native.js";
|
|
6
|
+
import { discoveryIssuer, tokenExpiry } from "../oauth-grant.js";
|
|
7
|
+
import { aborted } from "./refresh-coordinator.js";
|
|
8
|
+
import { discoveryBinding } from "./consent.js";
|
|
9
|
+
export const GRANT = oauthGrantKeys.grant;
|
|
10
|
+
/** The epoch of an owner with no grant record yet. */
|
|
11
|
+
const INITIAL_EPOCH = "initial";
|
|
12
|
+
/** The epoch of a record that does not parse: readers see no grant in it. */
|
|
13
|
+
const UNREADABLE_EPOCH = "unreadable";
|
|
14
|
+
export const ACTIVE_EPOCH_PREFIX = "v3:";
|
|
15
|
+
const DISCONNECTED_EPOCH_PREFIX = "disconnected:";
|
|
16
|
+
const GRANT_VERSION = 3;
|
|
17
|
+
/** Attempts at one grant write while other writers keep winning. */
|
|
18
|
+
export const MAX_GRANT_WRITES = 32;
|
|
19
|
+
export function plainObject(value) {
|
|
20
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
export function parsed(raw) {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function storedGrant(raw) {
|
|
31
|
+
const value = parsed(raw);
|
|
32
|
+
return plainObject(value) &&
|
|
33
|
+
value.connectaOAuth === GRANT_VERSION &&
|
|
34
|
+
typeof value.epoch === "string" &&
|
|
35
|
+
(value.flow === undefined || typeof value.flow === "string") &&
|
|
36
|
+
(value.sealed === undefined || typeof value.sealed === "string")
|
|
37
|
+
? value
|
|
38
|
+
: undefined;
|
|
39
|
+
}
|
|
40
|
+
/** The epoch a raw grant record names, without opening its body. */
|
|
41
|
+
export function epochOf(raw) {
|
|
42
|
+
if (raw === null)
|
|
43
|
+
return INITIAL_EPOCH;
|
|
44
|
+
return storedGrant(raw)?.epoch ?? UNREADABLE_EPOCH;
|
|
45
|
+
}
|
|
46
|
+
/** Nothing here trusts stored bytes: keep only what has the expected shape. */
|
|
47
|
+
function grantBody(value) {
|
|
48
|
+
if (!plainObject(value))
|
|
49
|
+
return {};
|
|
50
|
+
const { issuer, client, tokens, expiresAt, discovery, refreshConsent } = value;
|
|
51
|
+
const hasTokens = plainObject(tokens) && typeof tokens.access_token === "string";
|
|
52
|
+
return {
|
|
53
|
+
...(typeof issuer === "string" ? { issuer } : {}),
|
|
54
|
+
...(plainObject(client) && plainObject(client.value) && typeof client.value.client_id === "string"
|
|
55
|
+
? {
|
|
56
|
+
client: {
|
|
57
|
+
value: client.value,
|
|
58
|
+
...(typeof client.binding === "string" ? { binding: client.binding } : {}),
|
|
59
|
+
...(client.carried === true ? { carried: true } : {}),
|
|
60
|
+
...(client.registrationPath === "cimd" ||
|
|
61
|
+
client.registrationPath === "dcr" ||
|
|
62
|
+
client.registrationPath === "static"
|
|
63
|
+
? { registrationPath: client.registrationPath }
|
|
64
|
+
: {}),
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
: {}),
|
|
68
|
+
...(hasTokens ? { tokens: tokens } : {}),
|
|
69
|
+
// Meaningful only beside the tokens it describes.
|
|
70
|
+
...(hasTokens && typeof expiresAt === "number" && Number.isFinite(expiresAt) ? { expiresAt } : {}),
|
|
71
|
+
...(discoveryIssuer(discovery) !== undefined ? { discovery: discovery } : {}),
|
|
72
|
+
...(plainObject(refreshConsent) &&
|
|
73
|
+
typeof refreshConsent.digest === "string" &&
|
|
74
|
+
typeof refreshConsent.resolution === "string"
|
|
75
|
+
? { refreshConsent: { digest: refreshConsent.digest, resolution: refreshConsent.resolution } }
|
|
76
|
+
: {}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** What an invalidation compares: the value exactly as this flow saw it. */
|
|
80
|
+
export function fingerprint(value) {
|
|
81
|
+
return value === undefined ? undefined : JSON.stringify(value);
|
|
82
|
+
}
|
|
83
|
+
export function isDisconnectedEpoch(epoch) {
|
|
84
|
+
return epoch.startsWith(DISCONNECTED_EPOCH_PREFIX);
|
|
85
|
+
}
|
|
86
|
+
/** KvOAuthProvider's grant and epoch methods; `authMethods` installs them. */
|
|
87
|
+
export const grantStoreMethods = {
|
|
88
|
+
wait: function (work, signal) {
|
|
89
|
+
signal = signal === undefined ? this.signal : signal;
|
|
90
|
+
return authFlow(function* () {
|
|
91
|
+
if (signal?.aborted)
|
|
92
|
+
throw aborted(signal);
|
|
93
|
+
const value = signal
|
|
94
|
+
? (yield authAwait(authCall(work, []), { signal }.signal))
|
|
95
|
+
: (yield authCall(work, []));
|
|
96
|
+
if (signal?.aborted)
|
|
97
|
+
throw aborted(signal);
|
|
98
|
+
return value;
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
encodeGrant: function (grant, signal) {
|
|
102
|
+
signal = signal === undefined ? this.signal : signal;
|
|
103
|
+
return authFlow(function* () {
|
|
104
|
+
this.assertCredentialFree({ issuer: grant.body.issuer, discovery: grant.body.discovery }, grant.body);
|
|
105
|
+
const stored = {
|
|
106
|
+
connectaOAuth: GRANT_VERSION,
|
|
107
|
+
epoch: grant.epoch,
|
|
108
|
+
...(grant.flow !== undefined ? { flow: grant.flow } : {}),
|
|
109
|
+
};
|
|
110
|
+
if (Object.keys(grant.body).length > 0) {
|
|
111
|
+
if (this.sealer) {
|
|
112
|
+
stored.sealed = (yield authInvoke(this, "wait", [
|
|
113
|
+
authMethod(() => {
|
|
114
|
+
return authInvoke(this.sealer, "seal", [
|
|
115
|
+
this.grantLabel(grant.epoch),
|
|
116
|
+
JSON.stringify(grant.body),
|
|
117
|
+
]);
|
|
118
|
+
}),
|
|
119
|
+
signal,
|
|
120
|
+
]));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
stored.body = grant.body;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return JSON.stringify(stored);
|
|
127
|
+
}.bind(this));
|
|
128
|
+
},
|
|
129
|
+
openGrant: function (raw, signal) {
|
|
130
|
+
signal = signal === undefined ? this.signal : signal;
|
|
131
|
+
return authFlow(function* () {
|
|
132
|
+
const stored = raw === null ? undefined : storedGrant(raw);
|
|
133
|
+
if (!stored) {
|
|
134
|
+
this.observed = undefined;
|
|
135
|
+
return { epoch: raw === null ? INITIAL_EPOCH : UNREADABLE_EPOCH, body: {} };
|
|
136
|
+
}
|
|
137
|
+
let body = stored.body;
|
|
138
|
+
if (stored.sealed !== undefined) {
|
|
139
|
+
body = undefined;
|
|
140
|
+
try {
|
|
141
|
+
if (this.sealer)
|
|
142
|
+
body = parsed((yield authInvoke(this, "wait", [
|
|
143
|
+
authMethod(() => {
|
|
144
|
+
return authInvoke(this.sealer, "open", [
|
|
145
|
+
this.grantLabel(stored.epoch),
|
|
146
|
+
stored.sealed,
|
|
147
|
+
]);
|
|
148
|
+
}),
|
|
149
|
+
signal,
|
|
150
|
+
])));
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
if (signal?.aborted)
|
|
154
|
+
throw aborted(signal);
|
|
155
|
+
this.sealer?.warn(`[connecta] connector "${this.connectorId}" has a sealed OAuth grant ` +
|
|
156
|
+
"the configured vault cannot open; treating it as absent, so the " +
|
|
157
|
+
"connector needs authorization again. A changed vault key or " +
|
|
158
|
+
"tampered storage causes this.");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const grant = {
|
|
162
|
+
epoch: stored.epoch,
|
|
163
|
+
...(stored.flow !== undefined ? { flow: stored.flow } : {}),
|
|
164
|
+
body: grantBody(body),
|
|
165
|
+
};
|
|
166
|
+
this.observe(grant);
|
|
167
|
+
return grant;
|
|
168
|
+
}.bind(this));
|
|
169
|
+
},
|
|
170
|
+
storedAuthorization: function (issuer) {
|
|
171
|
+
return authFlow(function* () {
|
|
172
|
+
const raw = (yield authStorage(this.storage, "get", [GRANT]));
|
|
173
|
+
const grant = (yield authInvoke(this, "openGrant", [raw]));
|
|
174
|
+
if (isDisconnectedEpoch(grant.epoch))
|
|
175
|
+
return false;
|
|
176
|
+
const body = grant.body;
|
|
177
|
+
const configured = issuer ?? this.clientOptions?.client?.issuer;
|
|
178
|
+
const discovered = body.discovery;
|
|
179
|
+
if (!configured &&
|
|
180
|
+
(!discovered || discovered.authorizationServerMetadata?.issuer !== discovered.authorizationServerUrl))
|
|
181
|
+
return false;
|
|
182
|
+
const expected = configured ?? discoveryIssuer(discovered);
|
|
183
|
+
return Boolean(expected && body.issuer === expected && body.tokens?.access_token);
|
|
184
|
+
}.bind(this)).pipe(Effect.catch(() => Effect.succeed(false)));
|
|
185
|
+
},
|
|
186
|
+
peekGrant: function () {
|
|
187
|
+
return authFlow(function* () {
|
|
188
|
+
const raw = (yield authInvoke(this, "wait", [
|
|
189
|
+
authMethod(() => authStorage(this.storage, "get", [GRANT])),
|
|
190
|
+
]));
|
|
191
|
+
const grant = (yield authInvoke(this, "openGrant", [raw]));
|
|
192
|
+
return { epoch: grant.epoch, ...(grant.body.tokens ? { tokens: grant.body.tokens } : {}) };
|
|
193
|
+
}.bind(this));
|
|
194
|
+
},
|
|
195
|
+
readGrant: function (signal) {
|
|
196
|
+
signal = signal === undefined ? this.signal : signal;
|
|
197
|
+
return authFlow(function* () {
|
|
198
|
+
let raw = (yield authInvoke(this, "wait", [
|
|
199
|
+
authMethod(() => {
|
|
200
|
+
return authStorage(this.storage, "get", [GRANT]);
|
|
201
|
+
}),
|
|
202
|
+
signal,
|
|
203
|
+
]));
|
|
204
|
+
if (raw === null) {
|
|
205
|
+
// Inspect names only: retired credentials are never opened or copied.
|
|
206
|
+
const keys = (yield authInvoke(this, "wait", [
|
|
207
|
+
authMethod(() => authStorage(this.storage, "list", [retiredOAuthKeys.scan])),
|
|
208
|
+
signal,
|
|
209
|
+
]));
|
|
210
|
+
if (keys.some((key) => retiredOAuthKeys.prefixes.some((prefix) => key.startsWith(prefix))))
|
|
211
|
+
(this.sealer ?? console).warn(`[connecta] connector "${this.connectorId}" has an unsupported pre-layout-3 OAuth grant; ` +
|
|
212
|
+
"treating it as absent. Connect again to authorize, or restore a backup and run the " +
|
|
213
|
+
"OAuth migration on Connecta 0.37.1 before upgrading.");
|
|
214
|
+
}
|
|
215
|
+
const grant = (yield authInvoke(this, "openGrant", [raw, signal]));
|
|
216
|
+
if (raw !== null && this.sealer && storedGrant(raw)?.body !== undefined) {
|
|
217
|
+
const sealed = (yield authInvoke(this, "encodeGrant", [grant, signal]));
|
|
218
|
+
if ((yield authInvoke(this, "wait", [
|
|
219
|
+
authMethod(() => {
|
|
220
|
+
return authCatch(authStorage(this.storage, "compareAndSet", [GRANT, raw, sealed]), () => false);
|
|
221
|
+
}),
|
|
222
|
+
signal,
|
|
223
|
+
])))
|
|
224
|
+
raw = sealed;
|
|
225
|
+
}
|
|
226
|
+
return { raw, grant };
|
|
227
|
+
}.bind(this));
|
|
228
|
+
},
|
|
229
|
+
boundGrant: function (signal) {
|
|
230
|
+
signal = signal === undefined ? this.signal : signal;
|
|
231
|
+
return authFlow(function* () {
|
|
232
|
+
const { grant } = (yield authInvoke(this, "readGrant", [signal]));
|
|
233
|
+
if (this.bound && grant.epoch !== this.epoch)
|
|
234
|
+
throw this.flowSuperseded();
|
|
235
|
+
return grant;
|
|
236
|
+
}.bind(this));
|
|
237
|
+
},
|
|
238
|
+
activeGrant: function () {
|
|
239
|
+
return authInvoke(this, "boundGrant", []);
|
|
240
|
+
},
|
|
241
|
+
updateGrant: function (change, options = {}) {
|
|
242
|
+
return authFlow(function* () {
|
|
243
|
+
const signal = options.signal ?? (options.commit ? null : this.signal);
|
|
244
|
+
for (let attempt = 0; attempt < MAX_GRANT_WRITES; attempt++) {
|
|
245
|
+
if (!options.commit && this.signal?.aborted)
|
|
246
|
+
return false;
|
|
247
|
+
const { raw, grant } = (yield authInvoke(this, "readGrant", [signal]));
|
|
248
|
+
const epoch = options.epoch ?? (this.epoch ??= grant.epoch);
|
|
249
|
+
if (grant.epoch !== epoch) {
|
|
250
|
+
if (options.quiet || !this.bound)
|
|
251
|
+
return false;
|
|
252
|
+
throw this.flowSuperseded();
|
|
253
|
+
}
|
|
254
|
+
// A disconnect's epoch holds nothing until an explicit start replaces it.
|
|
255
|
+
if (isDisconnectedEpoch(epoch))
|
|
256
|
+
return false;
|
|
257
|
+
const next = change(grant);
|
|
258
|
+
if (next === undefined)
|
|
259
|
+
return true;
|
|
260
|
+
const encoded = (yield authInvoke(this, "encodeGrant", [next, signal]));
|
|
261
|
+
if (!options.commit && this.signal?.aborted)
|
|
262
|
+
return false;
|
|
263
|
+
let written;
|
|
264
|
+
try {
|
|
265
|
+
written = (yield authInvoke(this, "wait", [
|
|
266
|
+
authMethod(() => {
|
|
267
|
+
return authStorage(this.storage, "compareAndSet", [GRANT, raw, encoded], options.beforeWrite && (() => options.beforeWrite?.()));
|
|
268
|
+
}),
|
|
269
|
+
signal,
|
|
270
|
+
]));
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
if (signal?.aborted)
|
|
274
|
+
throw aborted(signal);
|
|
275
|
+
// A store's own error can quote the value it refused.
|
|
276
|
+
throw this.credentialWriteError();
|
|
277
|
+
}
|
|
278
|
+
if (written) {
|
|
279
|
+
if (next.epoch !== grant.epoch)
|
|
280
|
+
this.epoch = next.epoch;
|
|
281
|
+
this.observe(next);
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
throw this.credentialWriteError();
|
|
286
|
+
}.bind(this));
|
|
287
|
+
},
|
|
288
|
+
writeCredential: function (field, value, issuer, refreshConsent) {
|
|
289
|
+
return authFlow(function* () {
|
|
290
|
+
// The SDK saves a code exchange's tokens once its answer is read;
|
|
291
|
+
// their lifetime started when its request was sent.
|
|
292
|
+
const issuedAt = (field === "tokens" ? this.tokenRequestSent : undefined) ?? Date.now();
|
|
293
|
+
(yield authInvoke(this, "updateGrant", [
|
|
294
|
+
(grant) => {
|
|
295
|
+
const body = issuer !== undefined && grant.body.issuer !== issuer ? { issuer } : { ...grant.body };
|
|
296
|
+
if (field === "client")
|
|
297
|
+
body.client = value;
|
|
298
|
+
else {
|
|
299
|
+
const tokens = value;
|
|
300
|
+
// The same access token saved again keeps the expiry it was issued with.
|
|
301
|
+
const expiresAt = body.tokens?.access_token === tokens.access_token && body.expiresAt !== undefined
|
|
302
|
+
? body.expiresAt
|
|
303
|
+
: tokenExpiry(tokens, issuedAt);
|
|
304
|
+
body.tokens = tokens;
|
|
305
|
+
delete body.expiresAt;
|
|
306
|
+
if (expiresAt !== undefined)
|
|
307
|
+
body.expiresAt = expiresAt;
|
|
308
|
+
delete body.refreshConsent;
|
|
309
|
+
if (refreshConsent)
|
|
310
|
+
body.refreshConsent = refreshConsent;
|
|
311
|
+
}
|
|
312
|
+
return this.withIssuer(grant, body);
|
|
313
|
+
},
|
|
314
|
+
]));
|
|
315
|
+
if (field === "tokens")
|
|
316
|
+
this.tokenRequestSent = undefined;
|
|
317
|
+
// A flow may invalidate what it wrote itself.
|
|
318
|
+
this.seen[field] = fingerprint(field === "client" ? value.value : value);
|
|
319
|
+
}.bind(this));
|
|
320
|
+
},
|
|
321
|
+
bindingForGrant: function (grant) {
|
|
322
|
+
return authFlow(function* () {
|
|
323
|
+
const configured = this.configuredAuthorizationBinding();
|
|
324
|
+
return configured
|
|
325
|
+
? { ...configured, issuer: grant.body.issuer }
|
|
326
|
+
: {
|
|
327
|
+
...(yield authInvoke(this, "wait", [
|
|
328
|
+
authMethod(() => {
|
|
329
|
+
return authCall(discoveryBinding, [grant.body.discovery]);
|
|
330
|
+
}),
|
|
331
|
+
])),
|
|
332
|
+
issuer: grant.body.issuer,
|
|
333
|
+
clientId: grant.body.client?.value.client_id,
|
|
334
|
+
};
|
|
335
|
+
}.bind(this));
|
|
336
|
+
},
|
|
337
|
+
checkFlowBinding: function (grant) {
|
|
338
|
+
return authFlow(function* () {
|
|
339
|
+
const binding = this.callback?.flow.binding;
|
|
340
|
+
if (!binding ||
|
|
341
|
+
fingerprint(binding) !== fingerprint((yield authInvoke(this, "bindingForGrant", [grant])))) {
|
|
342
|
+
throw this.flowSuperseded();
|
|
343
|
+
}
|
|
344
|
+
}.bind(this));
|
|
345
|
+
},
|
|
346
|
+
liveEpoch: function () {
|
|
347
|
+
return authFlow(function* () {
|
|
348
|
+
return (yield authInvoke(this, "readGrant", [])).grant.epoch;
|
|
349
|
+
}.bind(this));
|
|
350
|
+
},
|
|
351
|
+
operatorDisconnected: function () {
|
|
352
|
+
return authFlow(function* () {
|
|
353
|
+
return isDisconnectedEpoch((yield authInvoke(this, "liveEpoch", [])));
|
|
354
|
+
}.bind(this));
|
|
355
|
+
},
|
|
356
|
+
beginFlow: function (expectedEpoch) {
|
|
357
|
+
return authFlow(function* () {
|
|
358
|
+
const epoch = (yield authInvoke(this, "liveEpoch", []));
|
|
359
|
+
if (expectedEpoch !== undefined && epoch !== expectedEpoch)
|
|
360
|
+
throw this.flowSuperseded();
|
|
361
|
+
this.epoch = epoch;
|
|
362
|
+
this.bound = true;
|
|
363
|
+
return epoch;
|
|
364
|
+
}.bind(this));
|
|
365
|
+
},
|
|
366
|
+
flowEpoch: function () {
|
|
367
|
+
return authFlow(function* () {
|
|
368
|
+
return this.epoch ?? (yield authInvoke(this, "liveEpoch", []));
|
|
369
|
+
}.bind(this));
|
|
370
|
+
},
|
|
371
|
+
bindFlow: function () {
|
|
372
|
+
return authFlow(function* () {
|
|
373
|
+
const { grant } = (yield authInvoke(this, "readGrant", []));
|
|
374
|
+
const epoch = this.callback?.flow.epoch ?? this.epoch ?? grant.epoch;
|
|
375
|
+
if (grant.epoch !== epoch)
|
|
376
|
+
throw this.flowSuperseded();
|
|
377
|
+
this.epoch = epoch;
|
|
378
|
+
this.bound = true;
|
|
379
|
+
this.codeExchange = true;
|
|
380
|
+
(yield authInvoke(this, "checkFlowBinding", [grant]));
|
|
381
|
+
this.seen = {
|
|
382
|
+
client: fingerprint(grant.body.client?.value),
|
|
383
|
+
tokens: fingerprint(grant.body.tokens),
|
|
384
|
+
};
|
|
385
|
+
}.bind(this));
|
|
386
|
+
},
|
|
387
|
+
resetAuthorization: function (operatorDisconnected = false, preserveClient = false) {
|
|
388
|
+
return authFlow(function* () {
|
|
389
|
+
const operation = authInvoke(this, "performReset", [operatorDisconnected, preserveClient]);
|
|
390
|
+
if (!this.onReset)
|
|
391
|
+
return (yield operation);
|
|
392
|
+
const reset = runEdge(operation);
|
|
393
|
+
this.onReset?.(reset);
|
|
394
|
+
return (yield authValue(() => reset));
|
|
395
|
+
}.bind(this));
|
|
396
|
+
},
|
|
397
|
+
disconnectAuthorization: function (send) {
|
|
398
|
+
return authFlow(function* () {
|
|
399
|
+
const operation = authInvoke(this, "performReset", [true, false, send]);
|
|
400
|
+
if (!this.onReset)
|
|
401
|
+
return (yield operation);
|
|
402
|
+
const reset = runEdge(operation);
|
|
403
|
+
this.onReset?.(reset);
|
|
404
|
+
return (yield authValue(() => reset));
|
|
405
|
+
}.bind(this));
|
|
406
|
+
},
|
|
407
|
+
performReset: function (operatorDisconnected, preserveClient, revoke) {
|
|
408
|
+
return authFlow(function* () {
|
|
409
|
+
const epoch = `${operatorDisconnected ? DISCONNECTED_EPOCH_PREFIX : ACTIVE_EPOCH_PREFIX}${crypto.randomUUID()}`;
|
|
410
|
+
for (let attempt = 0; attempt < MAX_GRANT_WRITES; attempt++) {
|
|
411
|
+
const { raw, grant } = (yield authInvoke(this, "readGrant", [null]));
|
|
412
|
+
const carried = preserveClient && !operatorDisconnected ? this.carriableClient(grant) : undefined;
|
|
413
|
+
const encoded = (yield authInvoke(this, "encodeGrant", [{ epoch, body: carried ?? {} }, null]));
|
|
414
|
+
let written;
|
|
415
|
+
try {
|
|
416
|
+
written = (yield authStorage(this.storage, "compareAndSet", [GRANT, raw, encoded]));
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
throw this.credentialWriteError();
|
|
420
|
+
}
|
|
421
|
+
if (written) {
|
|
422
|
+
this.refreshCoordinator?.retire(grant.epoch);
|
|
423
|
+
try {
|
|
424
|
+
(yield authInvoke(this, "sweepConsents", []));
|
|
425
|
+
(yield authInvoke(this, "sweepRefreshLeases", []));
|
|
426
|
+
}
|
|
427
|
+
finally {
|
|
428
|
+
if (revoke) {
|
|
429
|
+
const snapshot = this.revocationSnapshot(grant.body);
|
|
430
|
+
(yield authCall(revokeDownstreamGrant, [snapshot.body, revoke, snapshot.client]));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
throw this.credentialWriteError();
|
|
437
|
+
}.bind(this));
|
|
438
|
+
},
|
|
439
|
+
};
|