@dual-os/sdk 0.0.0-stage → 0.8.0-next.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.
Files changed (214) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +19 -2
  3. package/dist/Action.d.ts +201 -0
  4. package/dist/Action.d.ts.map +1 -0
  5. package/dist/Action.js +172 -0
  6. package/dist/Action.js.map +1 -0
  7. package/dist/AgentChannel.d.ts +1717 -0
  8. package/dist/AgentChannel.d.ts.map +1 -0
  9. package/dist/AgentChannel.js +708 -0
  10. package/dist/AgentChannel.js.map +1 -0
  11. package/dist/AgentChannelConformance.d.ts +1543 -0
  12. package/dist/AgentChannelConformance.d.ts.map +1 -0
  13. package/dist/AgentChannelConformance.js +163 -0
  14. package/dist/AgentChannelConformance.js.map +1 -0
  15. package/dist/AgentChannelRuntime.d.ts +78 -0
  16. package/dist/AgentChannelRuntime.d.ts.map +1 -0
  17. package/dist/AgentChannelRuntime.js +270 -0
  18. package/dist/AgentChannelRuntime.js.map +1 -0
  19. package/dist/AiProvider.d.ts +132 -0
  20. package/dist/AiProvider.d.ts.map +1 -0
  21. package/dist/AiProvider.js +100 -0
  22. package/dist/AiProvider.js.map +1 -0
  23. package/dist/AuthMethod.d.ts +381 -0
  24. package/dist/AuthMethod.d.ts.map +1 -0
  25. package/dist/AuthMethod.js +225 -0
  26. package/dist/AuthMethod.js.map +1 -0
  27. package/dist/AuthMethodBinding.d.ts +77 -0
  28. package/dist/AuthMethodBinding.d.ts.map +1 -0
  29. package/dist/AuthMethodBinding.js +31 -0
  30. package/dist/AuthMethodBinding.js.map +1 -0
  31. package/dist/AuthMethodExecution.d.ts +223 -0
  32. package/dist/AuthMethodExecution.d.ts.map +1 -0
  33. package/dist/AuthMethodExecution.js +277 -0
  34. package/dist/AuthMethodExecution.js.map +1 -0
  35. package/dist/ConnectionResolver.d.ts +219 -0
  36. package/dist/ConnectionResolver.d.ts.map +1 -0
  37. package/dist/ConnectionResolver.js +153 -0
  38. package/dist/ConnectionResolver.js.map +1 -0
  39. package/dist/DefinitionError.d.ts +67 -0
  40. package/dist/DefinitionError.d.ts.map +1 -0
  41. package/dist/DefinitionError.js +82 -0
  42. package/dist/DefinitionError.js.map +1 -0
  43. package/dist/DualRegistry.d.ts +71 -0
  44. package/dist/DualRegistry.d.ts.map +1 -0
  45. package/dist/DualRegistry.js +96 -0
  46. package/dist/DualRegistry.js.map +1 -0
  47. package/dist/DualRuntime.d.ts +279 -0
  48. package/dist/DualRuntime.d.ts.map +1 -0
  49. package/dist/DualRuntime.js +311 -0
  50. package/dist/DualRuntime.js.map +1 -0
  51. package/dist/Files.d.ts +80 -0
  52. package/dist/Files.d.ts.map +1 -0
  53. package/dist/Files.js +66 -0
  54. package/dist/Files.js.map +1 -0
  55. package/dist/Group.d.ts +106 -0
  56. package/dist/Group.d.ts.map +1 -0
  57. package/dist/Group.js +86 -0
  58. package/dist/Group.js.map +1 -0
  59. package/dist/Http.d.ts +342 -0
  60. package/dist/Http.d.ts.map +1 -0
  61. package/dist/Http.js +677 -0
  62. package/dist/Http.js.map +1 -0
  63. package/dist/Mcp.d.ts +109 -0
  64. package/dist/Mcp.d.ts.map +1 -0
  65. package/dist/Mcp.js +176 -0
  66. package/dist/Mcp.js.map +1 -0
  67. package/dist/OAuth2TokenRefresher.d.ts +18 -0
  68. package/dist/OAuth2TokenRefresher.d.ts.map +1 -0
  69. package/dist/OAuth2TokenRefresher.js +19 -0
  70. package/dist/OAuth2TokenRefresher.js.map +1 -0
  71. package/dist/OAuthFlow.d.ts +71 -0
  72. package/dist/OAuthFlow.d.ts.map +1 -0
  73. package/dist/OAuthFlow.js +488 -0
  74. package/dist/OAuthFlow.js.map +1 -0
  75. package/dist/Plugin.d.ts +383 -0
  76. package/dist/Plugin.d.ts.map +1 -0
  77. package/dist/Plugin.js +249 -0
  78. package/dist/Plugin.js.map +1 -0
  79. package/dist/PluginRuntime.d.ts +3186 -0
  80. package/dist/PluginRuntime.d.ts.map +1 -0
  81. package/dist/PluginRuntime.js +331 -0
  82. package/dist/PluginRuntime.js.map +1 -0
  83. package/dist/Runner.d.ts +1 -0
  84. package/dist/Runner.js +124 -0
  85. package/dist/Runner.js.map +1 -0
  86. package/dist/ToolFile.d.ts +23 -0
  87. package/dist/ToolFile.d.ts.map +1 -0
  88. package/dist/ToolFile.js +33 -0
  89. package/dist/ToolFile.js.map +1 -0
  90. package/dist/Trigger.d.ts +358 -0
  91. package/dist/Trigger.d.ts.map +1 -0
  92. package/dist/Trigger.js +287 -0
  93. package/dist/Trigger.js.map +1 -0
  94. package/dist/_virtual/_rolldown/runtime.js +13 -0
  95. package/dist/index.d.ts +24 -0
  96. package/dist/index.js +24 -0
  97. package/dist/internal/auth-context.d.ts +23 -0
  98. package/dist/internal/auth-context.d.ts.map +1 -0
  99. package/dist/internal/auth-context.js +1 -0
  100. package/dist/internal/auth-execution.d.ts +27 -0
  101. package/dist/internal/auth-execution.d.ts.map +1 -0
  102. package/dist/internal/auth-execution.js +66 -0
  103. package/dist/internal/auth-execution.js.map +1 -0
  104. package/dist/internal/auth-runtime.d.ts +5 -0
  105. package/dist/internal/auth-runtime.d.ts.map +1 -0
  106. package/dist/internal/auth-runtime.js +16 -0
  107. package/dist/internal/auth-runtime.js.map +1 -0
  108. package/dist/internal/capabilities.d.ts +24 -0
  109. package/dist/internal/capabilities.d.ts.map +1 -0
  110. package/dist/internal/capabilities.js +22 -0
  111. package/dist/internal/capabilities.js.map +1 -0
  112. package/dist/internal/children.d.ts +34 -0
  113. package/dist/internal/children.d.ts.map +1 -0
  114. package/dist/internal/children.js +53 -0
  115. package/dist/internal/children.js.map +1 -0
  116. package/dist/internal/collection-utils.d.ts +4 -0
  117. package/dist/internal/collection-utils.d.ts.map +1 -0
  118. package/dist/internal/collection-utils.js +6 -0
  119. package/dist/internal/collection-utils.js.map +1 -0
  120. package/dist/internal/dual/actions.d.ts +33 -0
  121. package/dist/internal/dual/actions.d.ts.map +1 -0
  122. package/dist/internal/dual/actions.js +26 -0
  123. package/dist/internal/dual/actions.js.map +1 -0
  124. package/dist/internal/dual/invoke.d.ts +63 -0
  125. package/dist/internal/dual/invoke.d.ts.map +1 -0
  126. package/dist/internal/dual/invoke.js +75 -0
  127. package/dist/internal/dual/invoke.js.map +1 -0
  128. package/dist/internal/dual/registry.d.ts +125 -0
  129. package/dist/internal/dual/registry.d.ts.map +1 -0
  130. package/dist/internal/dual/registry.js +172 -0
  131. package/dist/internal/dual/registry.js.map +1 -0
  132. package/dist/internal/dual/triggers.d.ts +29 -0
  133. package/dist/internal/dual/triggers.d.ts.map +1 -0
  134. package/dist/internal/dual/triggers.js +26 -0
  135. package/dist/internal/dual/triggers.js.map +1 -0
  136. package/dist/internal/handler-context.d.ts +9 -0
  137. package/dist/internal/handler-context.d.ts.map +1 -0
  138. package/dist/internal/handler-context.js +31 -0
  139. package/dist/internal/handler-context.js.map +1 -0
  140. package/dist/internal/identifier.d.ts +6 -0
  141. package/dist/internal/identifier.d.ts.map +1 -0
  142. package/dist/internal/identifier.js +28 -0
  143. package/dist/internal/identifier.js.map +1 -0
  144. package/dist/internal/plugin-handlers.d.ts +38 -0
  145. package/dist/internal/plugin-handlers.d.ts.map +1 -0
  146. package/dist/internal/plugin-handlers.js +52 -0
  147. package/dist/internal/plugin-handlers.js.map +1 -0
  148. package/dist/internal/proto.d.ts +7 -0
  149. package/dist/internal/proto.d.ts.map +1 -0
  150. package/dist/internal/proto.js +10 -0
  151. package/dist/internal/proto.js.map +1 -0
  152. package/dist/internal/schema-runtime.d.ts +6 -0
  153. package/dist/internal/schema-runtime.d.ts.map +1 -0
  154. package/dist/internal/schema-runtime.js +10 -0
  155. package/dist/internal/schema-runtime.js.map +1 -0
  156. package/dist/internal/trigger-runtime-shared.d.ts +41 -0
  157. package/dist/internal/trigger-runtime-shared.d.ts.map +1 -0
  158. package/dist/internal/trigger-runtime-shared.js +132 -0
  159. package/dist/internal/trigger-runtime-shared.js.map +1 -0
  160. package/dist/internal/trigger-runtime.d.ts +12 -0
  161. package/dist/internal/trigger-runtime.d.ts.map +1 -0
  162. package/dist/internal/trigger-runtime.js +95 -0
  163. package/dist/internal/trigger-runtime.js.map +1 -0
  164. package/dist/internal/types.d.ts +7 -0
  165. package/dist/internal/types.d.ts.map +1 -0
  166. package/dist/internal/types.js +1 -0
  167. package/dist/remote-runtime.d.ts +585 -0
  168. package/dist/remote-runtime.d.ts.map +1 -0
  169. package/dist/remote-runtime.js +367 -0
  170. package/dist/remote-runtime.js.map +1 -0
  171. package/package.json +55 -4
  172. package/src/Action.ts +370 -0
  173. package/src/AgentChannel.ts +860 -0
  174. package/src/AgentChannelConformance.ts +124 -0
  175. package/src/AgentChannelRuntime.ts +616 -0
  176. package/src/AiProvider.ts +255 -0
  177. package/src/AuthMethod.ts +954 -0
  178. package/src/AuthMethodBinding.ts +112 -0
  179. package/src/AuthMethodExecution.ts +542 -0
  180. package/src/ConnectionResolver.ts +265 -0
  181. package/src/DefinitionError.ts +115 -0
  182. package/src/DualRegistry.ts +152 -0
  183. package/src/DualRuntime.ts +1081 -0
  184. package/src/Files.ts +89 -0
  185. package/src/Group.ts +292 -0
  186. package/src/Http.ts +1205 -0
  187. package/src/Mcp.ts +299 -0
  188. package/src/OAuth2TokenRefresher.ts +22 -0
  189. package/src/OAuthFlow.ts +874 -0
  190. package/src/Plugin.ts +912 -0
  191. package/src/PluginRuntime.ts +414 -0
  192. package/src/Runner.ts +248 -0
  193. package/src/ToolFile.ts +28 -0
  194. package/src/Trigger.ts +722 -0
  195. package/src/index.ts +158 -0
  196. package/src/internal/auth-context.ts +27 -0
  197. package/src/internal/auth-execution.ts +241 -0
  198. package/src/internal/auth-runtime.ts +20 -0
  199. package/src/internal/capabilities.ts +55 -0
  200. package/src/internal/children.ts +126 -0
  201. package/src/internal/collection-utils.ts +8 -0
  202. package/src/internal/dual/actions.ts +116 -0
  203. package/src/internal/dual/invoke.ts +307 -0
  204. package/src/internal/dual/registry.ts +340 -0
  205. package/src/internal/dual/triggers.ts +128 -0
  206. package/src/internal/handler-context.ts +57 -0
  207. package/src/internal/identifier.ts +27 -0
  208. package/src/internal/plugin-handlers.ts +146 -0
  209. package/src/internal/proto.ts +8 -0
  210. package/src/internal/schema-runtime.ts +15 -0
  211. package/src/internal/trigger-runtime-shared.ts +449 -0
  212. package/src/internal/trigger-runtime.ts +223 -0
  213. package/src/internal/types.ts +12 -0
  214. package/src/remote-runtime.ts +469 -0
@@ -0,0 +1,874 @@
1
+ import type { Config } from "effect";
2
+ import {
3
+ Array,
4
+ Clock,
5
+ ConfigProvider,
6
+ Context,
7
+ Effect,
8
+ HashSet,
9
+ Layer,
10
+ Predicate,
11
+ Random,
12
+ Redacted,
13
+ Record,
14
+ Schema,
15
+ String,
16
+ pipe,
17
+ } from "effect";
18
+
19
+ /**
20
+ * OAuth protocol runner used by ConnectionStore.
21
+ *
22
+ * OAuthFlow owns provider-facing protocol mechanics: resolving OAuth client
23
+ * configuration, optional discovery, authorization URL construction, token
24
+ * exchange, token refresh, and token response normalization. It does not persist
25
+ * OAuth sessions or create Connections; ConnectionStore owns those product
26
+ * lifecycle records and calls this service for the protocol steps.
27
+ */
28
+ import { Base64, Base64Url } from "effect/encoding";
29
+ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/http";
30
+
31
+ import * as AuthMethod from "./AuthMethod.ts";
32
+ import * as OAuth2TokenRefresher from "./OAuth2TokenRefresher.ts";
33
+
34
+ export class OAuthConnectionValidationFailed extends Schema.TaggedError<OAuthConnectionValidationFailed>()(
35
+ "OAuthConnectionValidationFailed",
36
+ { cause: Schema.optionalKey(Schema.Redacted(Schema.Defect())), message: Schema.String },
37
+ ) {}
38
+ export class OAuthProviderRejected extends Schema.TaggedError<OAuthProviderRejected>()(
39
+ "OAuthProviderRejected",
40
+ { error: Schema.String, errorDescription: Schema.optional(Schema.String) },
41
+ ) {}
42
+ export class OAuthGrantRejected extends Schema.TaggedError<OAuthGrantRejected>()(
43
+ "OAuthGrantRejected",
44
+ { errorSubtype: Schema.optionalKey(Schema.String), message: Schema.String },
45
+ ) {}
46
+
47
+ const DefaultFlowTtlMs = 10 * 60 * 1000;
48
+
49
+ const OAuthEndpoints = Schema.Struct({
50
+ authorizationUrl: Schema.String,
51
+ expectedIssuer: Schema.optional(Schema.String),
52
+ tokenUrl: Schema.String,
53
+ });
54
+ type OAuthEndpoints = typeof OAuthEndpoints.Type;
55
+
56
+ const ClientAuthentication = Schema.Union([
57
+ Schema.Literal("basic"),
58
+ Schema.Literal("post"),
59
+ Schema.Literal("none"),
60
+ ]);
61
+
62
+ const ReservedTokenHeaders = HashSet.fromIterable([
63
+ "accept",
64
+ "accept-charset",
65
+ "accept-encoding",
66
+ "access-control-request-headers",
67
+ "access-control-request-method",
68
+ "authorization",
69
+ "connection",
70
+ "content-encoding",
71
+ "content-length",
72
+ "content-type",
73
+ "cookie",
74
+ "cookie2",
75
+ "date",
76
+ "dnt",
77
+ "expect",
78
+ "host",
79
+ "keep-alive",
80
+ "origin",
81
+ "permissions-policy",
82
+ "referer",
83
+ "set-cookie",
84
+ "te",
85
+ "trailer",
86
+ "transfer-encoding",
87
+ "upgrade",
88
+ "user-agent",
89
+ "via",
90
+ ]);
91
+
92
+ type TokenHeaders = typeof TokenHeaders.Type;
93
+ const TokenHeaders = Schema.Record(
94
+ Schema.String,
95
+ Schema.String.check(Schema.isPattern(/^[\t\x20-\x7e\x80-\xff]*$/u)),
96
+ ).check(
97
+ Schema.makeFilter(
98
+ headers => {
99
+ const names = Array.map(Record.keys(headers), String.toLowerCase);
100
+ return (
101
+ HashSet.size(HashSet.fromIterable(names)) === names.length &&
102
+ Array.every(
103
+ names,
104
+ name =>
105
+ /^[!#$%&'*+.^_`|~0-9a-z-]+$/u.test(name) &&
106
+ !HashSet.has(ReservedTokenHeaders, name) &&
107
+ !String.startsWith("proxy-")(name) &&
108
+ !String.startsWith("sec-")(name),
109
+ )
110
+ );
111
+ },
112
+ { message: "Invalid provider token headers" },
113
+ ),
114
+ );
115
+
116
+ const ResolvedOAuth2Config = Schema.Struct({
117
+ authorizationParams: Schema.Record(Schema.String, Schema.String),
118
+ authorizationUrl: Schema.String,
119
+ clientAuthentication: ClientAuthentication,
120
+ clientId: Schema.String,
121
+ clientSecret: Schema.optional(Schema.String),
122
+ expectedIssuer: Schema.optional(Schema.String),
123
+ pkce: Schema.Boolean,
124
+ scopes: Schema.Array(Schema.String),
125
+ tokenHeaders: TokenHeaders,
126
+ tokenParams: Schema.Record(Schema.String, Schema.String),
127
+ tokenRequestEncoding: Schema.Literals(["form", "json"]),
128
+ tokenUrl: Schema.String,
129
+ });
130
+ type ResolvedOAuth2Config = typeof ResolvedOAuth2Config.Type;
131
+
132
+ const ProviderDiscoveryDocument = Schema.Struct({
133
+ authorization_endpoint: Schema.String,
134
+ issuer: Schema.optional(Schema.String),
135
+ token_endpoint: Schema.String,
136
+ });
137
+
138
+ const decodeDiscoveryDocument = Schema.decodeUnknownSync(ProviderDiscoveryDocument);
139
+ const encodeJsonTokenParams = Schema.encodeSync(
140
+ Schema.fromJsonString(Schema.Record(Schema.String, Schema.String)),
141
+ );
142
+ const decodeProviderTokenError = Schema.decodeSync(
143
+ Schema.fromJsonString(
144
+ Schema.Struct({
145
+ error: Schema.Literals([
146
+ "invalid_request",
147
+ "invalid_client",
148
+ "invalid_grant",
149
+ "unauthorized_client",
150
+ "unsupported_grant_type",
151
+ "invalid_scope",
152
+ "server_error",
153
+ "temporarily_unavailable",
154
+ ]),
155
+ }),
156
+ ),
157
+ );
158
+ // Only Google's documented reauthentication subtypes pass, so an echoed secret never reaches an error.
159
+ const decodeProviderTokenErrorSubtype = Schema.decodeSync(
160
+ Schema.fromJsonString(
161
+ Schema.Struct({ error_subtype: Schema.Literals(["invalid_rapt", "rapt_required"]) }),
162
+ ),
163
+ );
164
+
165
+ const randomBase64Url = (bytes: number): string => {
166
+ const data = new Uint8Array(bytes);
167
+ crypto.getRandomValues(data);
168
+ return Base64Url.encode(data);
169
+ };
170
+
171
+ const sha256Base64Url = (value: string): Effect.Effect<string> =>
172
+ Effect.promise(() => crypto.subtle.digest("SHA-256", new TextEncoder().encode(value))).pipe(
173
+ Effect.map(digest => Base64Url.encode(new Uint8Array(digest))),
174
+ );
175
+
176
+ const requiredString = (
177
+ value: unknown,
178
+ message: string,
179
+ ): Effect.Effect<string, OAuthConnectionValidationFailed> =>
180
+ typeof value === "string" && value.trim() !== ""
181
+ ? Effect.succeed(value)
182
+ : Effect.fail(new OAuthConnectionValidationFailed({ message }));
183
+
184
+ const fetchDiscovery = Effect.fn("OAuthFlow.fetchDiscovery")(function* (
185
+ discoveryUrl: string,
186
+ client: HttpClient.HttpClient,
187
+ ): Effect.fn.Return<OAuthEndpoints, OAuthConnectionValidationFailed> {
188
+ const rawDocument = yield* pipe(
189
+ HttpClient.filterStatusOk(client).get(discoveryUrl, {
190
+ headers: { Accept: "application/json" },
191
+ }),
192
+ Effect.flatMap(response => response.json),
193
+ Effect.mapError(cause =>
194
+ OAuthConnectionValidationFailed.make({
195
+ cause: Redacted.make(cause),
196
+ message: "failed to load oauth discovery document",
197
+ }),
198
+ ),
199
+ );
200
+
201
+ const document = yield* Effect.try({
202
+ catch: () =>
203
+ new OAuthConnectionValidationFailed({ message: "invalid oauth discovery document" }),
204
+ try: () => decodeDiscoveryDocument(rawDocument),
205
+ });
206
+
207
+ const authorizationUrl = yield* requiredString(
208
+ document.authorization_endpoint,
209
+ "oauth discovery document missing authorization_endpoint",
210
+ );
211
+ const tokenUrl = yield* requiredString(
212
+ document.token_endpoint,
213
+ "oauth discovery document missing token_endpoint",
214
+ );
215
+ const expectedIssuer =
216
+ typeof document.issuer === "string" && document.issuer.trim() !== ""
217
+ ? document.issuer
218
+ : undefined;
219
+
220
+ return {
221
+ authorizationUrl,
222
+ tokenUrl,
223
+ ...(expectedIssuer === undefined ? {} : { expectedIssuer }),
224
+ };
225
+ });
226
+
227
+ const resolveEndpoint = (
228
+ value: AuthMethod.OAuth2Endpoint<unknown> | undefined,
229
+ setup: unknown,
230
+ ): string | undefined =>
231
+ value === undefined ? undefined : typeof value === "function" ? value({ setup }) : value;
232
+
233
+ const resolveParams = (
234
+ value: AuthMethod.OAuth2Params<unknown> | undefined,
235
+ setup: unknown,
236
+ ): Readonly<Record<string, string>> =>
237
+ typeof value === "function" ? value({ setup }) : (value ?? {});
238
+
239
+ function invalidTokenHeaders() {
240
+ return OAuthConnectionValidationFailed.make({ message: "invalid oauth token headers" });
241
+ }
242
+
243
+ const resolveTokenHeaders = Effect.fnUntraced(function* (input: {
244
+ value: AuthMethod.OAuth2Headers<unknown> | undefined;
245
+ setup: unknown;
246
+ }): Effect.fn.Return<TokenHeaders, OAuthConnectionValidationFailed> {
247
+ const raw = yield* Effect.try({
248
+ catch: invalidTokenHeaders,
249
+ try: () => {
250
+ if (Predicate.isFunction(input.value)) {
251
+ return input.value({ setup: input.setup });
252
+ }
253
+ if (input.value === undefined) {
254
+ return {};
255
+ }
256
+ return input.value;
257
+ },
258
+ });
259
+ return yield* pipe(Schema.decodeEffect(TokenHeaders)(raw), Effect.mapError(invalidTokenHeaders));
260
+ });
261
+
262
+ const resolveClientValue = Effect.fn("OAuthFlow.resolveClientValue")(function* (input: {
263
+ readonly value: string | Config.Config<string> | undefined;
264
+ readonly field: string;
265
+ readonly configProvider: ConfigProvider.ConfigProvider;
266
+ }): Effect.fn.Return<string | undefined, OAuthConnectionValidationFailed> {
267
+ if (input.value === undefined) {
268
+ return undefined;
269
+ }
270
+ if (typeof input.value === "string") {
271
+ return yield* requiredString(input.value, `${input.field} is empty`);
272
+ }
273
+
274
+ return yield* input.value
275
+ .parse(input.configProvider)
276
+ .pipe(
277
+ Effect.mapError(
278
+ cause =>
279
+ new OAuthConnectionValidationFailed({
280
+ message: `failed to resolve ${input.field}: ${cause.message}`,
281
+ }),
282
+ ),
283
+ );
284
+ });
285
+
286
+ const resolveOAuthConfig = Effect.fn("OAuthFlow.resolveOAuthConfig")(function* (input: {
287
+ readonly method: AuthMethod.Any;
288
+ readonly setup: unknown;
289
+ readonly configProvider: ConfigProvider.ConfigProvider;
290
+ readonly client: HttpClient.HttpClient;
291
+ }): Effect.fn.Return<ResolvedOAuth2Config, OAuthConnectionValidationFailed> {
292
+ const oauth = input.method.oauth2;
293
+ if (oauth === undefined) {
294
+ return yield* new OAuthConnectionValidationFailed({ message: "auth method is not oauth2" });
295
+ }
296
+
297
+ const tokenHeaders = yield* resolveTokenHeaders({
298
+ setup: input.setup,
299
+ value: oauth.tokenHeaders,
300
+ });
301
+
302
+ const client =
303
+ typeof oauth.client === "function" ? oauth.client({ setup: input.setup }) : oauth.client;
304
+ const clientId = yield* resolveClientValue({
305
+ configProvider: input.configProvider,
306
+ field: "clientId",
307
+ value: client.clientId,
308
+ });
309
+ const clientSecret = yield* resolveClientValue({
310
+ configProvider: input.configProvider,
311
+ field: "clientSecret",
312
+ value: client.clientSecret,
313
+ });
314
+ if (clientId === undefined) {
315
+ return yield* new OAuthConnectionValidationFailed({ message: "missing oauth client id" });
316
+ }
317
+
318
+ const authorizationUrl = resolveEndpoint(oauth.authorizationUrl, input.setup);
319
+ const tokenUrl = resolveEndpoint(oauth.tokenUrl, input.setup);
320
+ const discoveryUrl = resolveEndpoint(oauth.discoveryUrl, input.setup);
321
+
322
+ const endpoints =
323
+ authorizationUrl !== undefined && tokenUrl !== undefined
324
+ ? { authorizationUrl, tokenUrl }
325
+ : discoveryUrl === undefined
326
+ ? yield* new OAuthConnectionValidationFailed({
327
+ message: "oauth method requires explicit endpoints or discoveryUrl",
328
+ })
329
+ : yield* fetchDiscovery(discoveryUrl, input.client);
330
+
331
+ return {
332
+ ...endpoints,
333
+ authorizationParams: resolveParams(oauth.authorizationParams, input.setup),
334
+ clientAuthentication: client.authentication ?? "basic",
335
+ clientId,
336
+ clientSecret,
337
+ pkce: oauth.pkce ?? true,
338
+ scopes: oauth.scopes,
339
+ tokenHeaders,
340
+ tokenParams: resolveParams(oauth.tokenParams, input.setup),
341
+ tokenRequestEncoding: oauth.tokenRequestEncoding ?? "form",
342
+ };
343
+ });
344
+
345
+ const buildAuthorizationUrl = (input: {
346
+ readonly config: ResolvedOAuth2Config;
347
+ readonly redirectUri: string;
348
+ readonly state: string;
349
+ readonly pkceChallenge: string | undefined;
350
+ }): string => {
351
+ const url = new URL(input.config.authorizationUrl);
352
+ url.searchParams.set("response_type", "code");
353
+ url.searchParams.set("client_id", input.config.clientId);
354
+ url.searchParams.set("redirect_uri", input.redirectUri);
355
+ url.searchParams.set("state", input.state);
356
+ if (input.config.scopes.length > 0) {
357
+ url.searchParams.set("scope", input.config.scopes.join(" "));
358
+ }
359
+ if (input.pkceChallenge !== undefined) {
360
+ url.searchParams.set("code_challenge", input.pkceChallenge);
361
+ url.searchParams.set("code_challenge_method", "S256");
362
+ }
363
+ for (const [key, value] of Object.entries(input.config.authorizationParams)) {
364
+ url.searchParams.set(key, value);
365
+ }
366
+ return url.toString();
367
+ };
368
+
369
+ const httpStatusClass = (status: number): string => {
370
+ if (status >= 500) {
371
+ return "5xx";
372
+ }
373
+ if (status >= 400) {
374
+ return "4xx";
375
+ }
376
+ return "unexpected";
377
+ };
378
+
379
+ const oauthProtocolErrorCode = (body: string): string | undefined => {
380
+ try {
381
+ return decodeProviderTokenError(body).error;
382
+ } catch {
383
+ return undefined;
384
+ }
385
+ };
386
+
387
+ const oauthProtocolErrorSubtype = (body: string): string | undefined => {
388
+ try {
389
+ return decodeProviderTokenErrorSubtype(body).error_subtype;
390
+ } catch {
391
+ return undefined;
392
+ }
393
+ };
394
+
395
+ const tokenRequestHeaders = (
396
+ config: ResolvedOAuth2Config,
397
+ ): Effect.Effect<Record<string, string>, OAuthConnectionValidationFailed> => {
398
+ const headers: Record<string, string> = {
399
+ ...config.tokenHeaders,
400
+ Accept: "application/json",
401
+ "Content-Type":
402
+ config.tokenRequestEncoding === "json"
403
+ ? "application/json"
404
+ : "application/x-www-form-urlencoded",
405
+ };
406
+ if (config.clientAuthentication === "basic") {
407
+ if (config.clientSecret === undefined) {
408
+ return Effect.fail(
409
+ new OAuthConnectionValidationFailed({
410
+ message: "missing client secret for basic token auth",
411
+ }),
412
+ );
413
+ }
414
+ headers["Authorization"] =
415
+ `Basic ${Base64.encode(`${config.clientId}:${config.clientSecret}`)}`;
416
+ }
417
+ return Effect.succeed(headers);
418
+ };
419
+
420
+ const postTokenRequest = Effect.fn("OAuthFlow.postTokenRequest")(function* (input: {
421
+ readonly config: ResolvedOAuth2Config;
422
+ readonly params: URLSearchParams;
423
+ readonly failurePrefix: string;
424
+ readonly client: HttpClient.HttpClient;
425
+ }): Effect.fn.Return<
426
+ Record<string, unknown>,
427
+ OAuthConnectionValidationFailed | OAuthGrantRejected
428
+ > {
429
+ if (input.config.clientAuthentication === "post") {
430
+ input.params.set("client_id", input.config.clientId);
431
+ if (input.config.clientSecret !== undefined) {
432
+ input.params.set("client_secret", input.config.clientSecret);
433
+ }
434
+ } else if (input.config.clientAuthentication === "none") {
435
+ input.params.set("client_id", input.config.clientId);
436
+ }
437
+
438
+ for (const [key, value] of Object.entries(input.config.tokenParams)) {
439
+ input.params.set(key, value);
440
+ }
441
+
442
+ const headers = yield* tokenRequestHeaders(input.config);
443
+ const request = HttpClientRequest.post(input.config.tokenUrl, { headers }).pipe(
444
+ HttpClientRequest.bodyText(
445
+ input.config.tokenRequestEncoding === "json"
446
+ ? encodeJsonTokenParams(Record.fromEntries(input.params))
447
+ : input.params.toString(),
448
+ headers["Content-Type"],
449
+ ),
450
+ );
451
+ const response = yield* pipe(
452
+ input.client.execute(request),
453
+ Effect.provideService(
454
+ FetchHttpClient.RequestInit,
455
+ Record.isEmptyRecord(input.config.tokenHeaders) ? {} : { redirect: "error" },
456
+ ),
457
+ Effect.mapError(cause =>
458
+ OAuthConnectionValidationFailed.make({
459
+ cause: Redacted.make(cause),
460
+ message: `${input.failurePrefix} (network)`,
461
+ }),
462
+ ),
463
+ );
464
+ if (response.status < 200 || response.status >= 300) {
465
+ const body = yield* pipe(
466
+ response.text,
467
+ Effect.mapError(cause =>
468
+ OAuthConnectionValidationFailed.make({
469
+ cause: Redacted.make(cause),
470
+ message: `${input.failurePrefix} (unreadable response)`,
471
+ }),
472
+ ),
473
+ );
474
+ const statusClass = httpStatusClass(response.status);
475
+ const protocolError = oauthProtocolErrorCode(body);
476
+ const errorSubtype = oauthProtocolErrorSubtype(body);
477
+ const reason = Array.join(
478
+ Array.filter([protocolError, errorSubtype], Predicate.isNotUndefined),
479
+ ", ",
480
+ );
481
+ const message = `${input.failurePrefix} (${Array.join(Array.filter([statusClass, reason], String.isNonEmpty), ": ")})`;
482
+ if (protocolError === "invalid_grant") {
483
+ return yield* OAuthGrantRejected.make({
484
+ message,
485
+ ...(errorSubtype === undefined ? {} : { errorSubtype }),
486
+ });
487
+ }
488
+ return yield* OAuthConnectionValidationFailed.make({ message });
489
+ }
490
+ return yield* pipe(
491
+ response.json,
492
+ Effect.mapError(cause =>
493
+ OAuthConnectionValidationFailed.make({
494
+ cause: Redacted.make(cause),
495
+ message: `${input.failurePrefix} (network)`,
496
+ }),
497
+ ),
498
+ Effect.flatMap(raw =>
499
+ pipe(
500
+ Schema.decodeUnknownEffect(Schema.Record(Schema.String, Schema.Unknown))(raw),
501
+ Effect.mapError(() =>
502
+ OAuthConnectionValidationFailed.make({ message: "invalid oauth token response" }),
503
+ ),
504
+ ),
505
+ ),
506
+ );
507
+ });
508
+
509
+ const numberFromToken = (value: unknown): number | undefined => {
510
+ if (typeof value === "number" && Number.isFinite(value)) {
511
+ return value;
512
+ }
513
+ if (typeof value === "string" && value.trim() !== "") {
514
+ const parsed = Number(value);
515
+ return Number.isFinite(parsed) ? parsed : undefined;
516
+ }
517
+ return undefined;
518
+ };
519
+
520
+ const normalizeTokenResponse = Effect.fn("OAuthFlow.normalizeTokenResponse")(function* (input: {
521
+ readonly raw: Record<string, unknown>;
522
+ readonly method: AuthMethod.Any;
523
+ readonly previousRefreshToken?: string | undefined;
524
+ }): Effect.fn.Return<AuthMethod.OAuth2TokenResponse, OAuthConnectionValidationFailed> {
525
+ const response = yield* pipe(
526
+ Schema.decodeUnknownEffect(
527
+ input.method.oauth2?.tokenResponse ?? AuthMethod.OAuth2ProviderTokenResponse,
528
+ )(input.raw),
529
+ Effect.mapError(() =>
530
+ OAuthConnectionValidationFailed.make({ message: "invalid oauth token response" }),
531
+ ),
532
+ );
533
+ const accessToken = yield* requiredString(
534
+ response.access_token,
535
+ "oauth token response missing access_token",
536
+ );
537
+ if (response.refresh_token?.trim() === "") {
538
+ return yield* OAuthConnectionValidationFailed.make({ message: "invalid oauth token response" });
539
+ }
540
+ const refreshToken = response.refresh_token ?? input.previousRefreshToken;
541
+ const expiresIn = numberFromToken(response.expires_in);
542
+ if (response.expires_in !== undefined && (expiresIn === undefined || expiresIn < 0)) {
543
+ return yield* OAuthConnectionValidationFailed.make({ message: "invalid oauth token response" });
544
+ }
545
+ const now = yield* Clock.currentTimeMillis;
546
+ const absoluteExpiry =
547
+ response.expires_at === undefined ? undefined : Date.parse(response.expires_at);
548
+ if (absoluteExpiry !== undefined && !Number.isFinite(absoluteExpiry)) {
549
+ return yield* OAuthConnectionValidationFailed.make({ message: "invalid oauth token response" });
550
+ }
551
+
552
+ return {
553
+ accessToken,
554
+ ...(refreshToken === undefined ? {} : { refreshToken }),
555
+ ...(response.token_type === undefined ? {} : { tokenType: response.token_type }),
556
+ ...(expiresIn === undefined ? {} : { expiresAt: now + expiresIn * 1000, expiresIn }),
557
+ ...(absoluteExpiry === undefined ? {} : { expiresAt: absoluteExpiry }),
558
+ ...(response.scope === undefined ? {} : { scope: response.scope }),
559
+ raw: input.raw,
560
+ };
561
+ });
562
+
563
+ export type OAuthFlowError = OAuthConnectionValidationFailed | OAuthProviderRejected;
564
+
565
+ export interface OAuthFlowContinuation {
566
+ readonly state: string;
567
+ readonly redirectUri: string;
568
+ readonly pkceVerifier: string | null;
569
+ readonly expiresAt: Date;
570
+ }
571
+
572
+ export interface OAuthFlowStarted {
573
+ readonly authorizationUrl: string;
574
+ readonly continuation: OAuthFlowContinuation;
575
+ }
576
+
577
+ export interface OAuthFlowCompleted<Material> {
578
+ readonly tokens: AuthMethod.OAuth2TokenResponse;
579
+ readonly material: Material;
580
+ }
581
+
582
+ export class Service extends Context.Service<
583
+ Service,
584
+ {
585
+ readonly start: <Method extends AuthMethod.Any>(input: {
586
+ readonly method: Method;
587
+ readonly setup: AuthMethod.Setup<Method>;
588
+ readonly redirectUri: string;
589
+ readonly ttlMs?: number;
590
+ }) => Effect.Effect<OAuthFlowStarted, OAuthFlowError>;
591
+ readonly complete: <Method extends AuthMethod.Any>(input: {
592
+ readonly method: Method;
593
+ readonly setup: AuthMethod.Setup<Method>;
594
+ readonly continuation: OAuthFlowContinuation;
595
+ readonly query: Readonly<Record<string, string | undefined>>;
596
+ }) => Effect.Effect<OAuthFlowCompleted<AuthMethod.OAuth2TokenResponse>, OAuthFlowError>;
597
+ readonly refresh: <Method extends AuthMethod.Any>(input: {
598
+ readonly method: Method;
599
+ readonly setup: AuthMethod.Setup<Method>;
600
+ readonly refreshToken: string;
601
+ }) => Effect.Effect<
602
+ OAuthFlowCompleted<AuthMethod.OAuth2TokenResponse>,
603
+ OAuthFlowError | OAuthGrantRejected
604
+ >;
605
+ }
606
+ >()("@dual-os/sdk/OAuthFlow") {}
607
+
608
+ /** Deterministic OAuthFlow test layer that avoids external provider traffic. */
609
+ export const testing = (
610
+ options: {
611
+ readonly accessToken?: string;
612
+ readonly refreshToken?: string;
613
+ readonly refreshedAccessToken?: string;
614
+ readonly tokenType?: string;
615
+ readonly expiresIn?: number;
616
+ } = {},
617
+ ) =>
618
+ Layer.succeed(
619
+ Service,
620
+ Service.of({
621
+ complete: Effect.fn("OAuthFlow.testing.complete")(function* <
622
+ Method extends AuthMethod.Any,
623
+ >(input: {
624
+ readonly method: Method;
625
+ readonly setup: AuthMethod.Setup<Method>;
626
+ readonly continuation: OAuthFlowContinuation;
627
+ readonly query: Readonly<Record<string, string | undefined>>;
628
+ }) {
629
+ if (input.query["state"] !== input.continuation.state) {
630
+ return yield* new OAuthConnectionValidationFailed({ message: "oauth state mismatch" });
631
+ }
632
+ if (input.query["error"] !== undefined) {
633
+ return yield* new OAuthProviderRejected({
634
+ error: input.query["error"],
635
+ errorDescription: input.query["error_description"],
636
+ });
637
+ }
638
+ if (input.query["code"] === undefined || input.query["code"].trim() === "") {
639
+ return yield* new OAuthConnectionValidationFailed({
640
+ message: "missing oauth authorization code",
641
+ });
642
+ }
643
+
644
+ const tokens: AuthMethod.OAuth2TokenResponse = {
645
+ accessToken: options.accessToken ?? `test_access_${input.query["code"]}`,
646
+ expiresIn: options.expiresIn ?? 3600,
647
+ raw: { code: input.query["code"], grantType: "authorization_code", testing: true },
648
+ refreshToken: options.refreshToken ?? `test_refresh_${input.query["code"]}`,
649
+ tokenType: options.tokenType ?? "Bearer",
650
+ };
651
+ return { material: tokens, tokens };
652
+ }),
653
+ refresh: Effect.fn("OAuthFlow.testing.refresh")(function* <
654
+ Method extends AuthMethod.Any,
655
+ >(input: {
656
+ readonly method: Method;
657
+ readonly setup: AuthMethod.Setup<Method>;
658
+ readonly refreshToken: string;
659
+ }) {
660
+ const tokens: AuthMethod.OAuth2TokenResponse = {
661
+ accessToken: options.refreshedAccessToken ?? `test_refreshed_${input.refreshToken}`,
662
+ expiresIn: options.expiresIn ?? 3600,
663
+ raw: { grantType: "refresh_token", testing: true },
664
+ refreshToken: input.refreshToken,
665
+ tokenType: options.tokenType ?? "Bearer",
666
+ };
667
+ yield* Effect.void;
668
+ return { material: tokens, tokens };
669
+ }),
670
+ start: Effect.fn("OAuthFlow.testing.start")(function* <Method extends AuthMethod.Any>(input: {
671
+ readonly method: Method;
672
+ readonly setup: AuthMethod.Setup<Method>;
673
+ readonly redirectUri: string;
674
+ readonly ttlMs?: number;
675
+ }) {
676
+ const oauth = input.method.oauth2;
677
+ if (oauth === undefined) {
678
+ return yield* new OAuthConnectionValidationFailed({
679
+ message: "auth method is not oauth2",
680
+ });
681
+ }
682
+ const client =
683
+ typeof oauth.client === "function" ? oauth.client({ setup: input.setup }) : oauth.client;
684
+ const state = `test_state_${yield* Random.nextInt}`;
685
+ const pkceVerifier = oauth.pkce === false ? null : `test_pkce_${yield* Random.nextInt}`;
686
+ const authorizationUrl = new URL(
687
+ resolveEndpoint(oauth.authorizationUrl, input.setup) ??
688
+ "https://oauth.testing.local/authorize",
689
+ );
690
+ authorizationUrl.searchParams.set("response_type", "code");
691
+ authorizationUrl.searchParams.set("client_id", globalThis.String(client.clientId));
692
+ authorizationUrl.searchParams.set("redirect_uri", input.redirectUri);
693
+ authorizationUrl.searchParams.set("state", state);
694
+ if (oauth.scopes.length > 0) {
695
+ authorizationUrl.searchParams.set("scope", oauth.scopes.join(" "));
696
+ }
697
+ if (pkceVerifier !== null) {
698
+ authorizationUrl.searchParams.set("code_challenge", "testing-code-challenge");
699
+ authorizationUrl.searchParams.set("code_challenge_method", "S256");
700
+ }
701
+ for (const [key, value] of Object.entries(
702
+ resolveParams(oauth.authorizationParams, input.setup),
703
+ )) {
704
+ authorizationUrl.searchParams.set(key, value);
705
+ }
706
+
707
+ return {
708
+ authorizationUrl: authorizationUrl.toString(),
709
+ continuation: {
710
+ expiresAt: new Date(
711
+ (yield* Clock.currentTimeMillis) + (input.ttlMs ?? DefaultFlowTtlMs),
712
+ ),
713
+ pkceVerifier,
714
+ redirectUri: input.redirectUri,
715
+ state,
716
+ },
717
+ };
718
+ }),
719
+ }),
720
+ );
721
+
722
+ /** Live protocol layer that reads OAuth client config from Effect ConfigProvider. */
723
+ export const layer = Layer.effect(
724
+ Service,
725
+ Effect.gen(function* () {
726
+ const configProvider = yield* ConfigProvider.ConfigProvider;
727
+ const client = yield* HttpClient.HttpClient;
728
+
729
+ const start = Effect.fn("OAuthFlow.start")(function* <Method extends AuthMethod.Any>(input: {
730
+ readonly method: Method;
731
+ readonly setup: AuthMethod.Setup<Method>;
732
+ readonly redirectUri: string;
733
+ readonly ttlMs?: number;
734
+ }) {
735
+ const config = yield* resolveOAuthConfig({
736
+ client,
737
+ configProvider,
738
+ method: input.method,
739
+ setup: input.setup,
740
+ });
741
+ const expiresAt = new Date(
742
+ (yield* Clock.currentTimeMillis) + (input.ttlMs ?? DefaultFlowTtlMs),
743
+ );
744
+ const state = randomBase64Url(32);
745
+ const pkceVerifier = config.pkce ? randomBase64Url(32) : undefined;
746
+ const pkceChallenge =
747
+ pkceVerifier === undefined ? undefined : yield* sha256Base64Url(pkceVerifier);
748
+
749
+ return {
750
+ authorizationUrl: buildAuthorizationUrl({
751
+ config,
752
+ pkceChallenge,
753
+ redirectUri: input.redirectUri,
754
+ state,
755
+ }),
756
+ continuation: {
757
+ expiresAt,
758
+ pkceVerifier: pkceVerifier ?? null,
759
+ redirectUri: input.redirectUri,
760
+ state,
761
+ },
762
+ };
763
+ });
764
+
765
+ const complete = Effect.fn("OAuthFlow.complete")(function* <
766
+ Method extends AuthMethod.Any,
767
+ >(input: {
768
+ readonly method: Method;
769
+ readonly setup: AuthMethod.Setup<Method>;
770
+ readonly continuation: OAuthFlowContinuation;
771
+ readonly query: Readonly<Record<string, string | undefined>>;
772
+ }) {
773
+ if (input.query["state"] !== input.continuation.state) {
774
+ return yield* new OAuthConnectionValidationFailed({ message: "oauth state mismatch" });
775
+ }
776
+ if (input.query["error"] !== undefined) {
777
+ return yield* new OAuthProviderRejected({
778
+ error: input.query["error"],
779
+ errorDescription: input.query["error_description"],
780
+ });
781
+ }
782
+ const code = input.query["code"];
783
+ if (code === undefined || code.trim() === "") {
784
+ return yield* new OAuthConnectionValidationFailed({
785
+ message: "missing oauth authorization code",
786
+ });
787
+ }
788
+
789
+ const config = yield* resolveOAuthConfig({
790
+ client,
791
+ configProvider,
792
+ method: input.method,
793
+ setup: input.setup,
794
+ });
795
+ if (
796
+ config.expectedIssuer !== undefined &&
797
+ input.query["iss"] !== undefined &&
798
+ input.query["iss"] !== config.expectedIssuer
799
+ ) {
800
+ return yield* new OAuthConnectionValidationFailed({ message: "oauth issuer mismatch" });
801
+ }
802
+
803
+ const tokenParams = new URLSearchParams({
804
+ code,
805
+ grant_type: "authorization_code",
806
+ redirect_uri: input.continuation.redirectUri,
807
+ });
808
+ if (input.continuation.pkceVerifier !== null) {
809
+ tokenParams.set("code_verifier", input.continuation.pkceVerifier);
810
+ }
811
+
812
+ const rawTokens = yield* pipe(
813
+ postTokenRequest({
814
+ client,
815
+ config,
816
+ failurePrefix: "oauth token exchange failed",
817
+ params: tokenParams,
818
+ }),
819
+ Effect.catchTag("OAuthGrantRejected", error =>
820
+ Effect.fail(OAuthConnectionValidationFailed.make({ message: error.message })),
821
+ ),
822
+ );
823
+ const tokens = yield* normalizeTokenResponse({ method: input.method, raw: rawTokens });
824
+ return { material: tokens, tokens };
825
+ });
826
+
827
+ const refresh = Effect.fn("OAuthFlow.refresh")(function* <
828
+ Method extends AuthMethod.Any,
829
+ >(input: {
830
+ readonly method: Method;
831
+ readonly setup: AuthMethod.Setup<Method>;
832
+ readonly refreshToken: string;
833
+ }) {
834
+ const config = yield* resolveOAuthConfig({
835
+ client,
836
+ configProvider,
837
+ method: input.method,
838
+ setup: input.setup,
839
+ });
840
+ const rawTokens = yield* postTokenRequest({
841
+ client,
842
+ config,
843
+ failurePrefix: "oauth token refresh failed",
844
+ params: new URLSearchParams({
845
+ grant_type: "refresh_token",
846
+ refresh_token: input.refreshToken,
847
+ }),
848
+ });
849
+ const tokens = yield* normalizeTokenResponse({
850
+ method: input.method,
851
+ previousRefreshToken: input.refreshToken,
852
+ raw: rawTokens,
853
+ });
854
+ return { material: tokens, tokens };
855
+ });
856
+
857
+ return Service.of({ complete, refresh, start });
858
+ }),
859
+ ).pipe(Layer.provide(FetchHttpClient.layer));
860
+
861
+ export const oauth2TokenRefresherLayer = Layer.effect(
862
+ OAuth2TokenRefresher.Service,
863
+ Effect.gen(function* () {
864
+ const flow = yield* Service;
865
+ return OAuth2TokenRefresher.Service.of({
866
+ refresh: input =>
867
+ flow
868
+ .refresh({ method: input.method, refreshToken: input.refreshToken, setup: input.setup })
869
+ .pipe(Effect.map(refreshed => refreshed.tokens)),
870
+ });
871
+ }),
872
+ );
873
+
874
+ export * as OAuthFlow from "./OAuthFlow.ts";