bravenlab 0.0.0-stage → 0.1.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/dist/auth.js ADDED
@@ -0,0 +1,626 @@
1
+ // src/auth.ts
2
+ import {
3
+ Cause,
4
+ Console as Console2,
5
+ Duration as Duration2,
6
+ Effect as Effect4,
7
+ Match as Match2,
8
+ Option as Option4,
9
+ Ref,
10
+ Schedule
11
+ } from "effect";
12
+ import { HttpClient as HttpClient2, HttpClientRequest as HttpClientRequest2, HttpClientResponse as HttpClientResponse2 } from "effect/http";
13
+ import * as Schema4 from "effect/Schema";
14
+ import * as Tuple from "effect/Tuple";
15
+
16
+ // ../identity-store/src/environment.ts
17
+ import { Context, Layer, Match, Option } from "effect";
18
+ import * as Str from "effect/String";
19
+ var Environment = class extends Context.Service()("braven/Environment") {
20
+ };
21
+
22
+ // ../identity-store/src/errors.ts
23
+ import { Effect } from "effect";
24
+ import * as Schema from "effect/Schema";
25
+ import * as Str2 from "effect/String";
26
+ var EXIT_FAILURE = 1;
27
+ var CliFailure = class extends Schema.TaggedError()("CliFailure", {
28
+ message: Schema.String,
29
+ exitCode: Schema.Int
30
+ }) {
31
+ };
32
+ var fail = (message, exitCode = EXIT_FAILURE) => Effect.fail(new CliFailure({ message, exitCode }));
33
+ var oneLine = (value) => Str2.trim(Str2.replace(/\s+/g, " ")(String(value))).slice(0, 300);
34
+
35
+ // ../identity-store/src/identity.ts
36
+ import { Entry } from "@napi-rs/keyring";
37
+ import { Context as Context2, Effect as Effect2, Layer as Layer2, Option as Option2, pipe } from "effect";
38
+ import * as Arr from "effect/Array";
39
+ import * as R from "effect/Record";
40
+ import * as FileSystem from "effect/FileSystem";
41
+ import * as Path from "effect/Path";
42
+ import * as Predicate from "effect/Predicate";
43
+ import * as Schema2 from "effect/Schema";
44
+ import * as Str3 from "effect/String";
45
+ var CONFIG_VERSION = 1;
46
+ var KEYCHAIN_SERVICE = "braven";
47
+ var CredentialKind = Schema2.Literals(["session", "apiKey"]);
48
+ var Account = class extends Schema2.Class("Account")({
49
+ origin: Schema2.String,
50
+ email: Schema2.String,
51
+ kind: CredentialKind,
52
+ label: Schema2.OptionFromOptionalKey(Schema2.String)
53
+ }) {
54
+ };
55
+ var ConfigFile = class extends Schema2.Class("ConfigFile")({
56
+ version: Schema2.Int.pipe(
57
+ Schema2.withConstructorDefault(Effect2.succeed(CONFIG_VERSION)),
58
+ Schema2.withDecodingDefaultKey(Effect2.succeed(CONFIG_VERSION))
59
+ ),
60
+ current: Schema2.OptionFromNullishOr(Schema2.String, {
61
+ onNoneEncoding: null
62
+ }).pipe(
63
+ Schema2.withConstructorDefault(Effect2.succeed(Option2.none())),
64
+ Schema2.withDecodingDefaultKey(Effect2.succeed(null))
65
+ ),
66
+ accounts: Schema2.Array(Account).pipe(
67
+ Schema2.withConstructorDefault(Effect2.succeed([])),
68
+ Schema2.withDecodingDefaultKey(Effect2.succeed([]))
69
+ ),
70
+ defaults: Schema2.Record(Schema2.String, Schema2.Unknown).pipe(
71
+ Schema2.withConstructorDefault(Effect2.succeed({})),
72
+ Schema2.withDecodingDefaultKey(Effect2.succeed({}))
73
+ ),
74
+ watcher: Schema2.Record(Schema2.String, Schema2.Unknown).pipe(
75
+ Schema2.withConstructorDefault(Effect2.succeed({})),
76
+ Schema2.withDecodingDefaultKey(Effect2.succeed({}))
77
+ )
78
+ }) {
79
+ };
80
+ var ConfigJson = Schema2.fromJsonString(ConfigFile, { space: 2 });
81
+ var StoreConfig = class extends Context2.Service()("braven/StoreConfig") {
82
+ };
83
+ var Keychain = class extends Context2.Service()(
84
+ "braven/Keychain"
85
+ ) {
86
+ };
87
+ var entry = (account) => new Entry(KEYCHAIN_SERVICE, account);
88
+ var keychainAction = (action, run) => Effect2.try({
89
+ try: run,
90
+ catch: (cause) => new CliFailure({
91
+ message: `keychain ${action} failed: ${oneLine(cause)}`,
92
+ exitCode: EXIT_FAILURE
93
+ })
94
+ });
95
+ var keychainLayer = Layer2.succeed(Keychain)({
96
+ get: (account) => keychainAction("read", () => entry(account).getPassword()).pipe(
97
+ Effect2.map(Option2.fromNullishOr)
98
+ ),
99
+ set: (account, secret) => keychainAction("write", () => {
100
+ entry(account).setPassword(secret);
101
+ }),
102
+ remove: (account) => keychainAction("delete", () => {
103
+ entry(account).deletePassword();
104
+ })
105
+ });
106
+ var accountKey = (origin, email) => `${normalizeOrigin(origin)}|${email}`;
107
+ var normalizeOrigin = (origin) => Str3.replace(/\/+$/, "")(origin);
108
+ var storageFailure = (action, path) => (cause) => new CliFailure({
109
+ message: `${action} ${path}: ${oneLine(cause.message)}`,
110
+ exitCode: EXIT_FAILURE
111
+ });
112
+ var loadConfig = () => Effect2.gen(function* () {
113
+ const { path } = yield* StoreConfig;
114
+ const fs = yield* FileSystem.FileSystem;
115
+ if (!(yield* fs.exists(path).pipe(Effect2.mapError(storageFailure("read", path))))) {
116
+ return new ConfigFile({});
117
+ }
118
+ const text = yield* fs.readFileString(path).pipe(Effect2.mapError(storageFailure("read", path)));
119
+ const config = yield* Schema2.decodeUnknownEffect(ConfigJson)(text).pipe(
120
+ Effect2.mapError(
121
+ (cause) => new CliFailure({
122
+ message: `${path} is not a version-${CONFIG_VERSION} config: ${oneLine(cause.message)}`,
123
+ exitCode: EXIT_FAILURE
124
+ })
125
+ )
126
+ );
127
+ if (config.version !== CONFIG_VERSION) {
128
+ return yield* fail(
129
+ `${path} is version ${config.version}, expected ${CONFIG_VERSION}`
130
+ );
131
+ }
132
+ return config;
133
+ });
134
+ var saveConfig = (config) => Effect2.gen(function* () {
135
+ const { path } = yield* StoreConfig;
136
+ const fs = yield* FileSystem.FileSystem;
137
+ const pathService = yield* Path.Path;
138
+ const text = yield* Schema2.encodeUnknownEffect(ConfigJson)(config).pipe(
139
+ Effect2.mapError(storageFailure("encode", path))
140
+ );
141
+ yield* fs.makeDirectory(pathService.dirname(path), { recursive: true }).pipe(Effect2.mapError(storageFailure("create", path)));
142
+ const temporary = `${path}.tmp`;
143
+ yield* fs.writeFileString(temporary, text).pipe(Effect2.mapError(storageFailure("write", path)));
144
+ yield* fs.rename(temporary, path).pipe(Effect2.mapError(storageFailure("write", path)));
145
+ });
146
+ var storeCredential = (credential, label = Option2.none()) => Effect2.gen(function* () {
147
+ if (credential.secret.length === 0) {
148
+ return yield* fail("refusing to store an empty secret");
149
+ }
150
+ if (credential.email.length === 0) {
151
+ return yield* fail("storing a credential needs an email");
152
+ }
153
+ const origin = normalizeOrigin(credential.origin);
154
+ const key = accountKey(origin, credential.email);
155
+ const keychain = yield* Keychain;
156
+ yield* keychain.set(key, credential.secret);
157
+ const config = yield* loadConfig();
158
+ const account = new Account({
159
+ origin,
160
+ email: credential.email,
161
+ kind: credential.kind,
162
+ label
163
+ });
164
+ yield* saveConfig(
165
+ new ConfigFile({
166
+ ...config,
167
+ accounts: [
168
+ ...config.accounts.filter(
169
+ (stored) => accountKey(stored.origin, stored.email) !== key
170
+ ),
171
+ account
172
+ ],
173
+ current: Option2.some(key)
174
+ })
175
+ );
176
+ return account;
177
+ });
178
+
179
+ // ../identity-store/src/login.ts
180
+ import open from "open";
181
+ import { createServer } from "node:http";
182
+ import { NodeHttpServer } from "@effect/platform-node";
183
+ import {
184
+ Console,
185
+ Context as Context3,
186
+ Deferred,
187
+ Duration,
188
+ Effect as Effect3,
189
+ Layer as Layer3,
190
+ Option as Option3,
191
+ Predicate as Predicate2,
192
+ Schema as Schema3
193
+ } from "effect";
194
+ import {
195
+ HttpClient,
196
+ HttpClientRequest,
197
+ HttpClientResponse,
198
+ HttpRouter,
199
+ HttpServer,
200
+ HttpServerRequest,
201
+ HttpServerResponse
202
+ } from "effect/http";
203
+ import "effect/FileSystem";
204
+ import * as NetAddress from "effect/net/NetAddress";
205
+ import "effect/Path";
206
+ import * as Str4 from "effect/String";
207
+ var LOGIN_TIMEOUT = Duration.minutes(5);
208
+ var SignInReason = Schema3.Literals([
209
+ "timeout",
210
+ "listener",
211
+ "browser",
212
+ "rejected",
213
+ "storage"
214
+ ]);
215
+ var SignInFailure = class extends Schema3.TaggedError()(
216
+ "SignInFailure",
217
+ { reason: SignInReason, message: Schema3.String }
218
+ ) {
219
+ };
220
+ var Browser = class extends Context3.Service()("braven/Browser") {
221
+ };
222
+ var browserLayer = Layer3.succeed(Browser)({
223
+ open: (url) => Effect3.tryPromise({
224
+ try: () => open(url),
225
+ catch: (cause) => new CliFailure({
226
+ message: `cannot open a browser for ${url}: ${oneLine(cause)}`,
227
+ exitCode: EXIT_FAILURE
228
+ })
229
+ }).pipe(Effect3.asVoid)
230
+ });
231
+ var Sessions = class extends Context3.Service()("braven/Sessions") {
232
+ };
233
+ var SessionUser = class SessionUser2 extends Schema3.Class(
234
+ "SessionUser"
235
+ )({ email: Schema3.NonEmptyString }) {
236
+ };
237
+ var SessionBody = class SessionBody2 extends Schema3.Class(
238
+ "SessionBody"
239
+ )({ user: SessionUser }) {
240
+ };
241
+ var lookupFailure = (origin, cause) => new CliFailure({
242
+ message: `cannot read a session from ${origin}: ${oneLine(cause.message)}`,
243
+ exitCode: EXIT_FAILURE
244
+ });
245
+ var lookupEmail = (origin, token) => Effect3.gen(function* () {
246
+ const client = yield* HttpClient.HttpClient;
247
+ const response = yield* client.get(`${origin}/api/auth/get-session`, {
248
+ headers: { authorization: `Bearer ${token}` }
249
+ }).pipe(Effect3.mapError((cause) => lookupFailure(origin, cause)));
250
+ if (response.status !== 200) {
251
+ const detail = yield* response.text.pipe(Effect3.orElseSucceed(() => ""));
252
+ return yield* fail(
253
+ `the API rejected that sign-in (${response.status})${detail ? `: ${oneLine(detail)}` : ""} \u2014 run \`braven login\` again`
254
+ );
255
+ }
256
+ const body = yield* HttpClientResponse.schemaBodyJson(SessionBody)(
257
+ response
258
+ ).pipe(Effect3.mapError((cause) => lookupFailure(origin, cause)));
259
+ return body.user.email;
260
+ });
261
+ var revokeSession = (origin, token) => Effect3.gen(function* () {
262
+ const client = yield* HttpClient.HttpClient;
263
+ const response = yield* client.execute(
264
+ HttpClientRequest.post(`${origin}/api/auth/sign-out`).pipe(
265
+ HttpClientRequest.bearerToken(token),
266
+ HttpClientRequest.bodyJsonUnsafe({})
267
+ )
268
+ ).pipe(
269
+ Effect3.mapError(
270
+ (cause) => new CliFailure({
271
+ message: `cannot reach ${origin} to sign out: ${oneLine(cause.message)}`,
272
+ exitCode: EXIT_FAILURE
273
+ })
274
+ )
275
+ );
276
+ if (response.status === 200 || response.status === 401) return;
277
+ const detail = yield* response.text.pipe(Effect3.orElseSucceed(() => ""));
278
+ return yield* fail(
279
+ `the API refused the sign-out (${response.status})${detail ? `: ${oneLine(detail)}` : ""}`
280
+ );
281
+ });
282
+ var sessionsLayer = Layer3.effect(Sessions)(
283
+ Effect3.map(HttpClient.HttpClient, (client) => ({
284
+ emailFor: (origin, token) => lookupEmail(origin, token).pipe(
285
+ Effect3.provideService(HttpClient.HttpClient, client)
286
+ ),
287
+ revoke: (origin, token) => revokeSession(origin, token).pipe(
288
+ Effect3.provideService(HttpClient.HttpClient, client)
289
+ )
290
+ }))
291
+ );
292
+ var tokenOf = (params) => Option3.fromNullishOr(params["token"]).pipe(
293
+ Option3.flatMap(
294
+ (value) => Predicate2.isString(value) ? Option3.some(value) : Option3.none()
295
+ ),
296
+ Option3.filter(Str4.isNonEmpty)
297
+ );
298
+ var tokenRoute = (tokens) => HttpRouter.add(
299
+ "GET",
300
+ "/",
301
+ Effect3.gen(function* () {
302
+ const params = yield* HttpServerRequest.ParsedSearchParams;
303
+ return yield* Option3.match(tokenOf(params), {
304
+ onNone: () => Effect3.succeed(
305
+ HttpServerResponse.text("Missing token.", { status: 400 })
306
+ ),
307
+ onSome: (token) => Effect3.gen(function* () {
308
+ yield* Deferred.succeed(tokens, token);
309
+ return HttpServerResponse.html(
310
+ "<h1>Authenticated. You can close this window.</h1>"
311
+ );
312
+ })
313
+ });
314
+ })
315
+ );
316
+ var listenerFailure = (message) => new SignInFailure({ reason: "listener", message });
317
+ var portOf = (address) => NetAddress.isInetAddress(address) ? Effect3.succeed(address.port) : Effect3.fail(
318
+ listenerFailure("the login listener did not bind a TCP port")
319
+ );
320
+ var openingLines = (url) => `Opening ${url}
321
+ If your browser did not open, visit that URL to finish signing in.`;
322
+ var awaitToken = (options) => Effect3.gen(function* () {
323
+ const tokens = yield* Deferred.make();
324
+ const app = HttpRouter.serve(
325
+ Layer3.mergeAll(
326
+ tokenRoute(tokens),
327
+ HttpRouter.add(
328
+ "*",
329
+ "*",
330
+ HttpServerResponse.text("Missing token.", { status: 400 })
331
+ )
332
+ ),
333
+ {
334
+ disableLogger: true,
335
+ disableListenLog: true
336
+ }
337
+ ).pipe(
338
+ Layer3.provideMerge(
339
+ NodeHttpServer.layer(createServer, { host: "127.0.0.1", port: 0 })
340
+ )
341
+ );
342
+ return yield* Effect3.gen(function* () {
343
+ const server = yield* HttpServer.HttpServer;
344
+ const port = yield* portOf(server.address);
345
+ const browser = yield* Browser;
346
+ const loginUrl = `${options.webOrigin}/cli-login?port=${port}`;
347
+ yield* Option3.match(Option3.fromNullishOr(options.onLoginUrl), {
348
+ onNone: () => Console.log(openingLines(loginUrl)),
349
+ onSome: (report) => report(loginUrl)
350
+ });
351
+ yield* browser.open(loginUrl).pipe(
352
+ Effect3.mapError(
353
+ (failure) => new SignInFailure({
354
+ reason: "browser",
355
+ message: failure.message
356
+ })
357
+ )
358
+ );
359
+ return { token: yield* Deferred.await(tokens), port };
360
+ }).pipe(
361
+ Effect3.timeoutOrElse({
362
+ duration: options.timeout ?? LOGIN_TIMEOUT,
363
+ orElse: () => Effect3.fail(
364
+ new SignInFailure({
365
+ reason: "timeout",
366
+ message: "timed out waiting for the browser sign-in (5 minutes) \u2014 run `braven login` again"
367
+ })
368
+ )
369
+ }),
370
+ Effect3.provide(app),
371
+ Effect3.catchTag(
372
+ "ServeError",
373
+ (cause) => Effect3.fail(
374
+ listenerFailure(
375
+ `cannot start the login listener: ${oneLine(cause.message)}`
376
+ )
377
+ )
378
+ )
379
+ );
380
+ });
381
+ var asSignInFailure = (reason) => (failure) => new SignInFailure({ reason, message: failure.message });
382
+ var rememberSession = (origin, token) => Effect3.gen(function* () {
383
+ const sessions = yield* Sessions;
384
+ const email = yield* sessions.emailFor(origin, token).pipe(Effect3.mapError(asSignInFailure("rejected")));
385
+ return yield* storeCredential({
386
+ origin,
387
+ email,
388
+ kind: "session",
389
+ secret: token
390
+ }).pipe(Effect3.mapError(asSignInFailure("storage")));
391
+ });
392
+ var signIn = (options) => Effect3.gen(function* () {
393
+ const { token } = yield* awaitToken(options);
394
+ return yield* rememberSession(options.origin, token);
395
+ });
396
+
397
+ // src/auth.ts
398
+ var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
399
+ var DEVICE_SCOPE = "openid profile email";
400
+ var DEVICE_CODE_LIFETIME = Duration2.minutes(30);
401
+ var DEVICE_POLL_INTERVAL = Duration2.seconds(5);
402
+ var DEVICE_SLOW_DOWN = Duration2.seconds(5);
403
+ var deviceUrl = (origin, endpoint) => `${origin}/api/auth/device/${endpoint}`;
404
+ var DeviceCode = class extends Schema4.Class("DeviceCode")({
405
+ device_code: Schema4.NonEmptyString,
406
+ user_code: Schema4.NonEmptyString,
407
+ verification_uri: Schema4.NonEmptyString,
408
+ verification_uri_complete: Schema4.OptionFromOptionalKey(Schema4.String),
409
+ expires_in: Schema4.Int.pipe(
410
+ Schema4.withConstructorDefault(
411
+ Effect4.succeed(Duration2.toSeconds(DEVICE_CODE_LIFETIME))
412
+ ),
413
+ Schema4.withDecodingDefaultKey(
414
+ Effect4.succeed(Duration2.toSeconds(DEVICE_CODE_LIFETIME))
415
+ )
416
+ ),
417
+ interval: Schema4.Int.pipe(
418
+ Schema4.withConstructorDefault(
419
+ Effect4.succeed(Duration2.toSeconds(DEVICE_POLL_INTERVAL))
420
+ ),
421
+ Schema4.withDecodingDefaultKey(
422
+ Effect4.succeed(Duration2.toSeconds(DEVICE_POLL_INTERVAL))
423
+ )
424
+ )
425
+ }) {
426
+ };
427
+ var DeviceToken = class extends Schema4.Class("DeviceToken")({
428
+ access_token: Schema4.OptionFromOptionalKey(Schema4.String),
429
+ error: Schema4.OptionFromOptionalKey(Schema4.String),
430
+ error_description: Schema4.OptionFromOptionalKey(Schema4.String)
431
+ }) {
432
+ };
433
+ var retryLine = "run `braven login --device` again";
434
+ var deviceExpiredFailure = () => new CliFailure({
435
+ message: `the device code expired before it was approved \u2014 ${retryLine}`,
436
+ exitCode: EXIT_FAILURE
437
+ });
438
+ var transportFailure = (origin, cause) => new CliFailure({
439
+ message: `cannot reach the sign-in API at ${origin}: ${oneLine(cause.message)}`,
440
+ exitCode: EXIT_FAILURE
441
+ });
442
+ var detailOf = (description) => Option4.match(description, {
443
+ onNone: () => "",
444
+ onSome: (value) => `: ${oneLine(value)}`
445
+ });
446
+ var refusalFor = (error, description) => Match2.value(error).pipe(
447
+ Match2.when("expired_token", deviceExpiredFailure),
448
+ Match2.when(
449
+ "access_denied",
450
+ () => new CliFailure({
451
+ message: `the sign-in was denied in the browser \u2014 ${retryLine}`,
452
+ exitCode: EXIT_FAILURE
453
+ })
454
+ ),
455
+ Match2.when(
456
+ "invalid_grant",
457
+ () => new CliFailure({
458
+ message: `the device code is no longer valid \u2014 ${retryLine}`,
459
+ exitCode: EXIT_FAILURE
460
+ })
461
+ ),
462
+ Match2.orElse(
463
+ (other) => new CliFailure({
464
+ message: `the device sign-in failed (${other})${detailOf(description)}`,
465
+ exitCode: EXIT_FAILURE
466
+ })
467
+ )
468
+ );
469
+ var refusal = (cause) => ({
470
+ _tag: "Refused",
471
+ failure: cause
472
+ });
473
+ var verdictOf = (body) => Option4.match(body.access_token, {
474
+ onSome: (token) => Effect4.succeed({ _tag: "Granted", token }),
475
+ onNone: () => Match2.value(Option4.getOrElse(body.error, () => "unknown_error")).pipe(
476
+ Match2.when(
477
+ "authorization_pending",
478
+ () => Effect4.succeed({ _tag: "Pending" })
479
+ ),
480
+ Match2.when(
481
+ "slow_down",
482
+ () => Effect4.succeed({ _tag: "SlowDown" })
483
+ ),
484
+ Match2.orElse(
485
+ (error) => Effect4.fail(refusalFor(error, body.error_description))
486
+ )
487
+ )
488
+ });
489
+ var requestDeviceCode = (origin, clientId) => Effect4.gen(function* () {
490
+ const client = yield* HttpClient2.HttpClient;
491
+ const request = yield* HttpClientRequest2.post(
492
+ deviceUrl(origin, "code")
493
+ ).pipe(
494
+ HttpClientRequest2.bodyJson({
495
+ client_id: clientId,
496
+ scope: DEVICE_SCOPE
497
+ }),
498
+ Effect4.mapError((cause) => transportFailure(origin, cause))
499
+ );
500
+ const response = yield* client.execute(request).pipe(Effect4.mapError((cause) => transportFailure(origin, cause)));
501
+ if (response.status !== 200) {
502
+ const detail = yield* response.text.pipe(Effect4.orElseSucceed(() => ""));
503
+ return yield* fail(
504
+ `the sign-in API refused a device code (${response.status})${detail ? `: ${oneLine(detail)}` : ""} \u2014 ${retryLine}`
505
+ );
506
+ }
507
+ return yield* HttpClientResponse2.schemaBodyJson(DeviceCode)(response).pipe(
508
+ Effect4.mapError(
509
+ (cause) => new CliFailure({
510
+ message: `cannot read a device code from ${origin}: ${oneLine(cause.message)}`,
511
+ exitCode: EXIT_FAILURE
512
+ })
513
+ )
514
+ );
515
+ });
516
+ var pollDeviceToken = (origin, clientId, deviceCode) => Effect4.gen(function* () {
517
+ const client = yield* HttpClient2.HttpClient;
518
+ const response = yield* client.execute(
519
+ HttpClientRequest2.post(deviceUrl(origin, "token")).pipe(
520
+ // better-auth's `/device/token` reads JSON only; a form body is 415.
521
+ HttpClientRequest2.bodyJsonUnsafe({
522
+ grant_type: DEVICE_GRANT_TYPE,
523
+ device_code: deviceCode,
524
+ client_id: clientId
525
+ })
526
+ )
527
+ ).pipe(Effect4.mapError((cause) => transportFailure(origin, cause)));
528
+ const body = yield* HttpClientResponse2.schemaBodyJson(DeviceToken)(
529
+ response
530
+ ).pipe(
531
+ Effect4.mapError(
532
+ () => new CliFailure({
533
+ message: `the sign-in API at ${origin} returned an unreadable device-token answer`,
534
+ exitCode: EXIT_FAILURE
535
+ })
536
+ )
537
+ );
538
+ return yield* verdictOf(body);
539
+ }).pipe(
540
+ // The poll's contract is a verdict, never a failure: a transport or decode
541
+ // error stops the schedule as `Refused` and surfaces one line later.
542
+ Effect4.catch((cause) => Effect4.succeed(refusal(cause)))
543
+ );
544
+ var devicePollSchedule = (start) => Schedule.fromStepWithMetadata(
545
+ Effect4.map(
546
+ Ref.make(start),
547
+ (interval) => (metadata) => Match2.value(metadata.input).pipe(
548
+ Match2.tag("Granted", () => Cause.done(metadata.input)),
549
+ Match2.tag("Refused", () => Cause.done(metadata.input)),
550
+ Match2.tag(
551
+ "Pending",
552
+ () => Ref.get(interval).pipe(
553
+ Effect4.map((delay) => Tuple.make(metadata.input, delay))
554
+ )
555
+ ),
556
+ Match2.tag(
557
+ "SlowDown",
558
+ () => Ref.updateAndGet(
559
+ interval,
560
+ (delay) => Duration2.millis(
561
+ Duration2.toMillis(delay) + Duration2.toMillis(DEVICE_SLOW_DOWN)
562
+ )
563
+ ).pipe(Effect4.map((delay) => Tuple.make(metadata.input, delay)))
564
+ ),
565
+ Match2.exhaustive
566
+ )
567
+ )
568
+ );
569
+ var deviceLogin = (options) => Effect4.gen(function* () {
570
+ const code = yield* requestDeviceCode(options.origin, options.clientId);
571
+ const verification = Option4.getOrElse(
572
+ code.verification_uri_complete,
573
+ () => code.verification_uri
574
+ );
575
+ yield* Console2.log(
576
+ `Sign in at ${verification}
577
+ and confirm the code ${code.user_code} in the browser.`
578
+ );
579
+ const browser = yield* Browser;
580
+ yield* browser.open(verification);
581
+ const interval = Duration2.seconds(code.interval);
582
+ const poll = pollDeviceToken(
583
+ options.origin,
584
+ options.clientId,
585
+ code.device_code
586
+ );
587
+ const untilFirstPoll = yield* Ref.make(true);
588
+ const outcome = yield* Effect4.suspend(
589
+ () => Effect4.flatMap(
590
+ Ref.getAndSet(untilFirstPoll, false),
591
+ (isFirst) => isFirst ? Effect4.delay(poll, interval) : poll
592
+ )
593
+ ).pipe(
594
+ Effect4.repeat(devicePollSchedule(interval)),
595
+ Effect4.timeoutOrElse({
596
+ duration: options.timeout ?? Duration2.seconds(code.expires_in),
597
+ orElse: () => Effect4.fail(deviceExpiredFailure())
598
+ })
599
+ );
600
+ return yield* Match2.value(outcome).pipe(
601
+ Match2.tag("Granted", (granted) => Effect4.succeed(granted.token)),
602
+ Match2.tag("Refused", (refused) => Effect4.fail(refused.failure)),
603
+ Match2.orElse(() => Effect4.fail(deviceExpiredFailure()))
604
+ );
605
+ });
606
+ export {
607
+ Browser,
608
+ DEVICE_GRANT_TYPE,
609
+ DEVICE_SCOPE,
610
+ DeviceCode,
611
+ DeviceToken,
612
+ LOGIN_TIMEOUT,
613
+ Sessions,
614
+ SignInFailure,
615
+ SignInReason,
616
+ awaitToken,
617
+ browserLayer,
618
+ deviceLogin,
619
+ devicePollSchedule,
620
+ lookupEmail,
621
+ rememberSession,
622
+ requestDeviceCode,
623
+ sessionsLayer,
624
+ signIn
625
+ };
626
+ //# sourceMappingURL=auth.js.map