clanka 0.2.49 → 0.2.51

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 (101) hide show
  1. package/dist/Agent.d.ts +20 -20
  2. package/dist/Agent.d.ts.map +1 -1
  3. package/dist/Agent.js +9 -9
  4. package/dist/Agent.js.map +1 -1
  5. package/dist/Agent.test.js +1 -0
  6. package/dist/Agent.test.js.map +1 -1
  7. package/dist/AgentExecutor.d.ts +62 -101
  8. package/dist/AgentExecutor.d.ts.map +1 -1
  9. package/dist/AgentExecutor.js +28 -7
  10. package/dist/AgentExecutor.js.map +1 -1
  11. package/dist/AgentTools.d.ts +6 -6
  12. package/dist/AgentTools.d.ts.map +1 -1
  13. package/dist/AgentTools.js +5 -5
  14. package/dist/AgentTools.js.map +1 -1
  15. package/dist/ApplyPatch.test.js +3 -3
  16. package/dist/ApplyPatch.test.js.map +1 -1
  17. package/dist/ChunkRepo.d.ts +16 -16
  18. package/dist/ChunkRepo.d.ts.map +1 -1
  19. package/dist/ChunkRepo.js +7 -6
  20. package/dist/ChunkRepo.js.map +1 -1
  21. package/dist/CodeChunker.d.ts +2 -2
  22. package/dist/CodeChunker.d.ts.map +1 -1
  23. package/dist/CodeChunker.js +2 -2
  24. package/dist/CodeChunker.js.map +1 -1
  25. package/dist/Codex.d.ts +1 -1
  26. package/dist/Codex.d.ts.map +1 -1
  27. package/dist/CodexAuth.d.ts +13 -7
  28. package/dist/CodexAuth.d.ts.map +1 -1
  29. package/dist/CodexAuth.js +13 -8
  30. package/dist/CodexAuth.js.map +1 -1
  31. package/dist/CodexAuth.test.js +1 -262
  32. package/dist/CodexAuth.test.js.map +1 -1
  33. package/dist/Copilot.d.ts +1 -1
  34. package/dist/Copilot.d.ts.map +1 -1
  35. package/dist/CopilotAuth.d.ts +13 -7
  36. package/dist/CopilotAuth.d.ts.map +1 -1
  37. package/dist/CopilotAuth.js +10 -5
  38. package/dist/CopilotAuth.js.map +1 -1
  39. package/dist/CopilotAuth.test.js +7 -8
  40. package/dist/CopilotAuth.test.js.map +1 -1
  41. package/dist/DeviceCodeHandler.d.ts +14 -0
  42. package/dist/DeviceCodeHandler.d.ts.map +1 -0
  43. package/dist/DeviceCodeHandler.js +9 -0
  44. package/dist/DeviceCodeHandler.js.map +1 -0
  45. package/dist/ExaSearch.d.ts +3 -3
  46. package/dist/ExaSearch.d.ts.map +1 -1
  47. package/dist/ExaSearch.js +2 -2
  48. package/dist/ExaSearch.js.map +1 -1
  49. package/dist/McpClient.d.ts +3 -3
  50. package/dist/McpClient.d.ts.map +1 -1
  51. package/dist/McpClient.js +2 -2
  52. package/dist/McpClient.js.map +1 -1
  53. package/dist/OutputFormatter.d.ts +2 -2
  54. package/dist/OutputFormatter.d.ts.map +1 -1
  55. package/dist/OutputFormatter.js +2 -2
  56. package/dist/OutputFormatter.js.map +1 -1
  57. package/dist/SemanticSearch/Service.d.ts +2 -2
  58. package/dist/SemanticSearch/Service.d.ts.map +1 -1
  59. package/dist/SemanticSearch/Service.js +2 -2
  60. package/dist/SemanticSearch/Service.js.map +1 -1
  61. package/dist/SemanticSearch.js +3 -3
  62. package/dist/SemanticSearch.js.map +1 -1
  63. package/dist/ToolkitRenderer.d.ts +2 -2
  64. package/dist/ToolkitRenderer.d.ts.map +1 -1
  65. package/dist/ToolkitRenderer.js +2 -2
  66. package/dist/ToolkitRenderer.js.map +1 -1
  67. package/dist/WebToMarkdown.d.ts +2 -2
  68. package/dist/WebToMarkdown.d.ts.map +1 -1
  69. package/dist/WebToMarkdown.js +2 -2
  70. package/dist/WebToMarkdown.js.map +1 -1
  71. package/dist/cli.js +2 -0
  72. package/dist/cli.js.map +1 -1
  73. package/dist/index.d.ts +4 -0
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +4 -0
  76. package/dist/index.js.map +1 -1
  77. package/package.json +8 -6
  78. package/src/Agent.test.ts +1 -0
  79. package/src/Agent.ts +9 -9
  80. package/src/AgentExecutor.ts +46 -13
  81. package/src/AgentTools.ts +7 -7
  82. package/src/ApplyPatch.test.ts +3 -3
  83. package/src/ChunkRepo.ts +8 -6
  84. package/src/CodeChunker.ts +2 -2
  85. package/src/CodexAuth.test.ts +0 -433
  86. package/src/CodexAuth.ts +16 -19
  87. package/src/CopilotAuth.test.ts +11 -7
  88. package/src/CopilotAuth.ts +11 -7
  89. package/src/DeviceCodeHandler.ts +21 -0
  90. package/src/ExaSearch.ts +2 -2
  91. package/src/McpClient.ts +2 -2
  92. package/src/OutputFormatter.ts +2 -2
  93. package/src/SemanticSearch/Service.ts +2 -2
  94. package/src/SemanticSearch.ts +3 -3
  95. package/src/ToolkitRenderer.ts +2 -2
  96. package/src/WebToMarkdown.ts +2 -2
  97. package/src/cli.ts +2 -0
  98. package/src/fixtures/fiber.txt +9 -9
  99. package/src/fixtures/patch18-broken.txt +5 -5
  100. package/src/fixtures/patch18-fixed.txt +5 -5
  101. package/src/index.ts +5 -0
@@ -1,20 +1,10 @@
1
1
  import { assert, describe, it } from "@effect/vitest"
2
- import * as Deferred from "effect/Deferred"
3
2
  import * as Effect from "effect/Effect"
4
3
  import * as Encoding from "effect/Encoding"
5
- import * as Fiber from "effect/Fiber"
6
4
  import * as Option from "effect/Option"
7
- import * as Ref from "effect/Ref"
8
- import {
9
- HttpClient,
10
- type HttpClientRequest,
11
- HttpClientResponse,
12
- } from "effect/unstable/http"
13
5
  import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore"
14
6
  import {
15
- CodexAuth,
16
7
  CodexAuthError,
17
- ISSUER,
18
8
  STORE_PREFIX,
19
9
  STORE_TOKEN_KEY,
20
10
  TOKEN_EXPIRY_BUFFER_MS,
@@ -32,75 +22,6 @@ const createJwt = (payload: string): string =>
32
22
  const createTestJwt = (payload: Record<string, unknown>): string =>
33
23
  createJwt(JSON.stringify(payload))
34
24
 
35
- const jsonResponse = (body: unknown, status = 200): Response =>
36
- new Response(JSON.stringify(body), {
37
- status,
38
- headers: {
39
- "content-type": "application/json",
40
- },
41
- })
42
-
43
- const getBody = (request: HttpClientRequest.HttpClientRequest): string => {
44
- if (request.body._tag !== "Uint8Array") {
45
- throw new Error("Expected request body to be a Uint8Array payload")
46
- }
47
-
48
- return new TextDecoder().decode(request.body.body)
49
- }
50
-
51
- const makeClient = Effect.fn("makeClient")(function* (
52
- handler: (
53
- request: HttpClientRequest.HttpClientRequest,
54
- attempt: number,
55
- ) => Response,
56
- ) {
57
- const attempts = yield* Ref.make(0)
58
- const requests = yield* Ref.make<Array<HttpClientRequest.HttpClientRequest>>(
59
- [],
60
- )
61
- const client = HttpClient.make((request) =>
62
- Effect.gen(function* () {
63
- const attempt = yield* Ref.updateAndGet(attempts, (count) => count + 1)
64
- yield* Ref.update(requests, (current) => [...current, request])
65
- return HttpClientResponse.fromWeb(request, handler(request, attempt))
66
- }),
67
- )
68
-
69
- return {
70
- attempts,
71
- client,
72
- requests,
73
- } as const
74
- })
75
-
76
- const makeEffectClient = Effect.fn("makeEffectClient")(function* (
77
- handler: (
78
- request: HttpClientRequest.HttpClientRequest,
79
- attempt: number,
80
- ) => Effect.Effect<Response>,
81
- ) {
82
- const attempts = yield* Ref.make(0)
83
- const requests = yield* Ref.make<Array<HttpClientRequest.HttpClientRequest>>(
84
- [],
85
- )
86
- const client = HttpClient.make((request) =>
87
- Effect.gen(function* () {
88
- const attempt = yield* Ref.updateAndGet(attempts, (count) => count + 1)
89
- yield* Ref.update(requests, (current) => [...current, request])
90
- return HttpClientResponse.fromWeb(
91
- request,
92
- yield* handler(request, attempt),
93
- )
94
- }),
95
- )
96
-
97
- return {
98
- attempts,
99
- client,
100
- requests,
101
- } as const
102
- })
103
-
104
25
  describe("CodexAuth", () => {
105
26
  it.effect(
106
27
  "persists token data through the prefixed schema store",
@@ -377,358 +298,4 @@ describe("CodexAuth", () => {
377
298
  true,
378
299
  )
379
300
  })
380
-
381
- it.effect(
382
- "preserves the stored account id when refreshed tokens omit parseable claims",
383
- () =>
384
- Effect.gen(function* () {
385
- const kvs = yield* KeyValueStore.KeyValueStore
386
- const tokenStore = toTokenStore(kvs)
387
- yield* Effect.orDie(
388
- tokenStore.set(
389
- STORE_TOKEN_KEY,
390
- new TokenData({
391
- access: "stale-access-token",
392
- refresh: "stale-refresh-token",
393
- expires: Date.now() - 60_000,
394
- accountId: Option.some("persisted-account"),
395
- }),
396
- ),
397
- )
398
-
399
- const { attempts, client } = yield* makeClient(() =>
400
- jsonResponse({
401
- id_token: "invalid",
402
- access_token: "also-invalid",
403
- refresh_token: "next-refresh-token",
404
- expires_in: 120,
405
- }),
406
- )
407
-
408
- const auth = yield* CodexAuth.make.pipe(
409
- Effect.provideService(HttpClient.HttpClient, client),
410
- )
411
-
412
- const token = yield* auth.get
413
-
414
- assert.strictEqual(token.refresh, "next-refresh-token")
415
- assert.strictEqual(
416
- Option.getOrUndefined(token.accountId),
417
- "persisted-account",
418
- )
419
- assert.strictEqual(yield* Ref.get(attempts), 1)
420
-
421
- const stored = yield* Effect.orDie(tokenStore.get(STORE_TOKEN_KEY))
422
- assert.strictEqual(Option.isSome(stored), true)
423
- if (Option.isNone(stored)) {
424
- return
425
- }
426
-
427
- assert.strictEqual(
428
- Option.getOrUndefined(stored.value.accountId),
429
- "persisted-account",
430
- )
431
- }).pipe(Effect.provide(KeyValueStore.layerMemory)),
432
- )
433
-
434
- it.effect(
435
- "falls back to device auth when refreshing an expired token fails",
436
- () =>
437
- Effect.gen(function* () {
438
- const kvs = yield* KeyValueStore.KeyValueStore
439
- const tokenStore = toTokenStore(kvs)
440
- yield* Effect.orDie(
441
- tokenStore.set(
442
- STORE_TOKEN_KEY,
443
- new TokenData({
444
- access: "stale-access-token",
445
- refresh: "stale-refresh-token",
446
- expires: Date.now() - 60_000,
447
- accountId: Option.some("stale-account"),
448
- }),
449
- ),
450
- )
451
-
452
- const { client, requests } = yield* makeClient((request) => {
453
- if (request.url === `${ISSUER}/api/accounts/deviceauth/usercode`) {
454
- return jsonResponse({
455
- device_auth_id: "device-auth-id",
456
- user_code: "WXYZ-9876",
457
- interval: "1",
458
- })
459
- }
460
-
461
- if (request.url === `${ISSUER}/api/accounts/deviceauth/token`) {
462
- return jsonResponse({
463
- authorization_code: "authorization-code",
464
- code_verifier: "code-verifier",
465
- })
466
- }
467
-
468
- if (request.url === `${ISSUER}/oauth/token`) {
469
- const body = new URLSearchParams(getBody(request))
470
- if (body.get("grant_type") === "refresh_token") {
471
- return new Response(null, { status: 401 })
472
- }
473
-
474
- return jsonResponse({
475
- id_token: createTestJwt({
476
- chatgpt_account_id: "account-from-id",
477
- }),
478
- access_token: createTestJwt({
479
- chatgpt_account_id: "account-from-access",
480
- }),
481
- refresh_token: "next-refresh-token",
482
- expires_in: 120,
483
- })
484
- }
485
-
486
- return new Response(null, { status: 500 })
487
- })
488
-
489
- const auth = yield* CodexAuth.make.pipe(
490
- Effect.provideService(HttpClient.HttpClient, client),
491
- )
492
-
493
- const token = yield* auth.get
494
-
495
- assert.strictEqual(token.refresh, "next-refresh-token")
496
- assert.strictEqual(
497
- Option.getOrUndefined(token.accountId),
498
- "account-from-id",
499
- )
500
-
501
- const stored = yield* Effect.orDie(tokenStore.get(STORE_TOKEN_KEY))
502
- assert.strictEqual(Option.isSome(stored), true)
503
- if (Option.isSome(stored)) {
504
- assert.strictEqual(stored.value.refresh, "next-refresh-token")
505
- }
506
-
507
- const seenRequests = yield* Ref.get(requests)
508
- assert.strictEqual(seenRequests.length, 4)
509
- assert.strictEqual(seenRequests[0]?.url, `${ISSUER}/oauth/token`)
510
- assert.strictEqual(
511
- new URLSearchParams(getBody(seenRequests[0]!)).get("grant_type"),
512
- "refresh_token",
513
- )
514
- assert.strictEqual(
515
- new URLSearchParams(getBody(seenRequests[3]!)).get("grant_type"),
516
- "authorization_code",
517
- )
518
- }).pipe(Effect.provide(KeyValueStore.layerMemory)),
519
- )
520
-
521
- it.effect("clears corrupted persisted tokens before re-authenticating", () =>
522
- Effect.gen(function* () {
523
- const kvs = yield* KeyValueStore.KeyValueStore
524
- yield* Effect.orDie(
525
- kvs.set(`${STORE_PREFIX}${STORE_TOKEN_KEY}`, "not-json"),
526
- )
527
-
528
- const { attempts, client } = yield* makeClient((request) => {
529
- if (request.url === `${ISSUER}/api/accounts/deviceauth/usercode`) {
530
- return jsonResponse({
531
- device_auth_id: "device-auth-id",
532
- user_code: "ABCD-EFGH",
533
- interval: "1",
534
- })
535
- }
536
-
537
- if (request.url === `${ISSUER}/api/accounts/deviceauth/token`) {
538
- return jsonResponse({
539
- authorization_code: "authorization-code",
540
- code_verifier: "code-verifier",
541
- })
542
- }
543
-
544
- if (request.url === `${ISSUER}/oauth/token`) {
545
- return jsonResponse({
546
- access_token: createTestJwt({
547
- chatgpt_account_id: "fresh-account",
548
- }),
549
- refresh_token: "fresh-refresh-token",
550
- expires_in: 120,
551
- })
552
- }
553
-
554
- return new Response(null, { status: 500 })
555
- })
556
-
557
- const auth = yield* CodexAuth.make.pipe(
558
- Effect.provideService(HttpClient.HttpClient, client),
559
- )
560
-
561
- assert.strictEqual(
562
- yield* Effect.orDie(kvs.get(`${STORE_PREFIX}${STORE_TOKEN_KEY}`)),
563
- undefined,
564
- )
565
- assert.strictEqual(yield* Ref.get(attempts), 0)
566
-
567
- const token = yield* auth.get
568
-
569
- assert.strictEqual(token.refresh, "fresh-refresh-token")
570
- assert.strictEqual(
571
- Option.getOrUndefined(token.accountId),
572
- "fresh-account",
573
- )
574
- assert.strictEqual(yield* Ref.get(attempts), 3)
575
- }).pipe(Effect.provide(KeyValueStore.layerMemory)),
576
- )
577
-
578
- it.effect("serializes concurrent get calls behind one refresh", () =>
579
- Effect.gen(function* () {
580
- const kvs = yield* KeyValueStore.KeyValueStore
581
- const tokenStore = toTokenStore(kvs)
582
- yield* Effect.orDie(
583
- tokenStore.set(
584
- STORE_TOKEN_KEY,
585
- new TokenData({
586
- access: "expired-access-token",
587
- refresh: "refresh-token",
588
- expires: Date.now() - 60_000,
589
- accountId: Option.none(),
590
- }),
591
- ),
592
- )
593
-
594
- const refreshStarted = yield* Deferred.make<void>()
595
- const releaseRefresh = yield* Deferred.make<void>()
596
- const { attempts, client } = yield* makeEffectClient((request) => {
597
- if (request.url !== `${ISSUER}/oauth/token`) {
598
- return Effect.succeed(new Response(null, { status: 500 }))
599
- }
600
-
601
- const body = new URLSearchParams(getBody(request))
602
- if (body.get("grant_type") !== "refresh_token") {
603
- return Effect.succeed(new Response(null, { status: 500 }))
604
- }
605
-
606
- return Effect.gen(function* () {
607
- yield* Deferred.succeed(refreshStarted, void 0)
608
- yield* Deferred.await(releaseRefresh)
609
- return jsonResponse({
610
- access_token: createTestJwt({
611
- chatgpt_account_id: "fresh-account",
612
- }),
613
- refresh_token: "fresh-refresh-token",
614
- expires_in: 120,
615
- })
616
- })
617
- })
618
-
619
- const auth = yield* CodexAuth.make.pipe(
620
- Effect.provideService(HttpClient.HttpClient, client),
621
- )
622
-
623
- const firstFiber = yield* auth.get.pipe(
624
- Effect.forkChild({ startImmediately: true }),
625
- )
626
- yield* Deferred.await(refreshStarted)
627
-
628
- const secondFiber = yield* auth.get.pipe(
629
- Effect.forkChild({ startImmediately: true }),
630
- )
631
- yield* Effect.yieldNow
632
-
633
- assert.strictEqual(yield* Ref.get(attempts), 1)
634
-
635
- yield* Deferred.succeed(releaseRefresh, void 0)
636
-
637
- const first = yield* Fiber.join(firstFiber)
638
- const second = yield* Fiber.join(secondFiber)
639
-
640
- assert.strictEqual(yield* Ref.get(attempts), 1)
641
- assert.strictEqual(first.refresh, "fresh-refresh-token")
642
- assert.strictEqual(second.refresh, "fresh-refresh-token")
643
- assert.strictEqual(
644
- Option.getOrUndefined(first.accountId),
645
- "fresh-account",
646
- )
647
- assert.strictEqual(
648
- Option.getOrUndefined(second.accountId),
649
- "fresh-account",
650
- )
651
- }).pipe(Effect.provide(KeyValueStore.layerMemory)),
652
- )
653
-
654
- it.effect(
655
- "forces device auth on authenticate and clears cache plus storage on logout",
656
- () =>
657
- Effect.gen(function* () {
658
- const kvs = yield* KeyValueStore.KeyValueStore
659
- const tokenStore = toTokenStore(kvs)
660
- yield* Effect.orDie(
661
- tokenStore.set(
662
- STORE_TOKEN_KEY,
663
- new TokenData({
664
- access: "cached-access-token",
665
- refresh: "cached-refresh-token",
666
- expires: Date.now() + 600_000,
667
- accountId: Option.some("cached-account"),
668
- }),
669
- ),
670
- )
671
-
672
- let deviceFlowCount = 0
673
- const { attempts, client } = yield* makeClient((request) => {
674
- if (request.url === `${ISSUER}/api/accounts/deviceauth/usercode`) {
675
- deviceFlowCount += 1
676
- return jsonResponse({
677
- device_auth_id: `device-auth-${deviceFlowCount}`,
678
- user_code: `CODE-${deviceFlowCount}`,
679
- interval: "1",
680
- })
681
- }
682
-
683
- if (request.url === `${ISSUER}/api/accounts/deviceauth/token`) {
684
- return jsonResponse({
685
- authorization_code: `authorization-code-${deviceFlowCount}`,
686
- code_verifier: `code-verifier-${deviceFlowCount}`,
687
- })
688
- }
689
-
690
- if (request.url === `${ISSUER}/oauth/token`) {
691
- return jsonResponse({
692
- access_token: createTestJwt({
693
- chatgpt_account_id: `device-account-${deviceFlowCount}`,
694
- }),
695
- refresh_token: `device-refresh-${deviceFlowCount}`,
696
- expires_in: 120,
697
- })
698
- }
699
-
700
- return new Response(null, { status: 500 })
701
- })
702
-
703
- const auth = yield* CodexAuth.make.pipe(
704
- Effect.provideService(HttpClient.HttpClient, client),
705
- )
706
-
707
- const cachedToken = yield* auth.get
708
- assert.strictEqual(cachedToken.refresh, "cached-refresh-token")
709
- assert.strictEqual(yield* Ref.get(attempts), 0)
710
-
711
- const authenticated = yield* auth.authenticate
712
- assert.strictEqual(authenticated.refresh, "device-refresh-1")
713
- assert.strictEqual(
714
- Option.getOrUndefined(authenticated.accountId),
715
- "device-account-1",
716
- )
717
- assert.strictEqual(yield* Ref.get(attempts), 3)
718
-
719
- yield* auth.logout
720
- const storedAfterLogout = yield* Effect.orDie(
721
- tokenStore.get(STORE_TOKEN_KEY),
722
- )
723
- assert.strictEqual(Option.isNone(storedAfterLogout), true)
724
-
725
- const tokenAfterLogout = yield* auth.get
726
- assert.strictEqual(tokenAfterLogout.refresh, "device-refresh-2")
727
- assert.strictEqual(
728
- Option.getOrUndefined(tokenAfterLogout.accountId),
729
- "device-account-2",
730
- )
731
- assert.strictEqual(yield* Ref.get(attempts), 6)
732
- }).pipe(Effect.provide(KeyValueStore.layerMemory)),
733
- )
734
301
  })
package/src/CodexAuth.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
- import * as Console from "effect/Console"
5
4
  import * as Effect from "effect/Effect"
6
5
  import * as Encoding from "effect/Encoding"
7
6
  import * as Function from "effect/Function"
@@ -11,11 +10,12 @@ import * as Result from "effect/Result"
11
10
  import * as Schedule from "effect/Schedule"
12
11
  import * as Schema from "effect/Schema"
13
12
  import * as Semaphore from "effect/Semaphore"
14
- import * as ServiceMap from "effect/ServiceMap"
13
+ import * as Context from "effect/Context"
15
14
  import * as HttpClient from "effect/unstable/http/HttpClient"
16
15
  import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"
17
16
  import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
18
17
  import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore"
18
+ import { DeviceCodeHandler } from "./DeviceCodeHandler.ts"
19
19
 
20
20
  export const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
21
21
  export const ISSUER = "https://auth.openai.com"
@@ -284,15 +284,17 @@ export const toCodexAuthKeyValueStore = (store: KeyValueStore.KeyValueStore) =>
284
284
  export const toTokenStore = (store: KeyValueStore.KeyValueStore) =>
285
285
  KeyValueStore.toSchemaStore(toCodexAuthKeyValueStore(store), TokenData)
286
286
 
287
- export class CodexAuth extends ServiceMap.Service<
287
+ export class CodexAuth extends Context.Service<
288
288
  CodexAuth,
289
289
  {
290
+ readonly verifyUrl: string
290
291
  readonly get: Effect.Effect<TokenData, CodexAuthError>
291
292
  readonly authenticate: Effect.Effect<TokenData, CodexAuthError>
292
293
  readonly logout: Effect.Effect<void>
293
294
  }
294
295
  >()("clanka/CodexAuth") {
295
296
  static readonly make = Effect.gen(function* () {
297
+ const verfication = yield* DeviceCodeHandler
296
298
  const tokenStore = toTokenStore(yield* KeyValueStore.KeyValueStore)
297
299
  const httpClient = (yield* HttpClient.HttpClient).pipe(
298
300
  HttpClient.mapRequest(
@@ -310,7 +312,7 @@ export class CodexAuth extends ServiceMap.Service<
310
312
 
311
313
  let currentToken = yield* tokenStore.get(STORE_TOKEN_KEY).pipe(
312
314
  Effect.catchTag("SchemaError", (error) =>
313
- Console.warn(
315
+ Effect.logDebug(
314
316
  `Failed to decode persisted Codex token, clearing it: ${error.message}`,
315
317
  ).pipe(
316
318
  Effect.andThen(tokenStore.remove(STORE_TOKEN_KEY)),
@@ -340,22 +342,23 @@ export class CodexAuth extends ServiceMap.Service<
340
342
 
341
343
  const authenticateWithDeviceFlow = Effect.gen(function* () {
342
344
  const deviceCode = yield* requestDeviceCode
343
- yield* Console.log(
344
- `Open ${ISSUER}${DEVICE_VERIFICATION_URL} and enter code: ${deviceCode.userCode}`,
345
- )
345
+ yield* verfication.onCode({
346
+ verifyUrl: ISSUER + DEVICE_VERIFICATION_URL,
347
+ deviceCode: deviceCode.userCode,
348
+ })
346
349
  const authorization = yield* pollAuthorization(deviceCode)
347
350
  return yield* exchangeAuthorizationCode(authorization)
348
351
  })
349
352
 
350
- const authenticateNoLock = Effect.uninterruptibleMask((restore) =>
351
- Effect.gen(function* () {
353
+ const authenticateNoLock = Effect.uninterruptibleMask(
354
+ Effect.fnUntraced(function* (restore) {
352
355
  const token = yield* restore(authenticateWithDeviceFlow)
353
356
  return yield* saveToken(token)
354
357
  }),
355
358
  )
356
359
 
357
- const getNoLock = Effect.uninterruptibleMask((restore) =>
358
- Effect.gen(function* () {
360
+ const getNoLock = Effect.uninterruptibleMask(
361
+ Effect.fnUntraced(function* (restore) {
359
362
  if (Option.isSome(currentToken) && !currentToken.value.isExpired()) {
360
363
  return currentToken.value
361
364
  }
@@ -366,14 +369,7 @@ export class CodexAuth extends ServiceMap.Service<
366
369
  }
367
370
 
368
371
  const refreshedToken = yield* restore(
369
- refreshToken(currentToken.value.refresh).pipe(
370
- Effect.tapError((error) =>
371
- Console.warn(
372
- `Codex token refresh failed, falling back to device auth: ${error.message}`,
373
- ),
374
- ),
375
- Effect.option,
376
- ),
372
+ refreshToken(currentToken.value.refresh).pipe(Effect.option),
377
373
  )
378
374
 
379
375
  if (Option.isSome(refreshedToken)) {
@@ -537,6 +533,7 @@ export class CodexAuth extends ServiceMap.Service<
537
533
  })
538
534
 
539
535
  return CodexAuth.of({
536
+ verifyUrl: ISSUER + DEVICE_VERIFICATION_URL,
540
537
  get: semaphore.withPermit(getNoLock),
541
538
  authenticate: semaphore.withPermit(authenticateNoLock),
542
539
  logout: semaphore.withPermit(Effect.uninterruptible(clearToken)),
@@ -25,6 +25,7 @@ import {
25
25
  toGithubCopilotAuthKeyValueStore,
26
26
  toTokenStore,
27
27
  } from "./CopilotAuth.ts"
28
+ import * as DeviceCodeHandler from "./DeviceCodeHandler.ts"
28
29
 
29
30
  const jsonResponse = (body: unknown, status = 200): Response =>
30
31
  new Response(JSON.stringify(body), {
@@ -52,8 +53,8 @@ const makeClient = Effect.fn("makeClient")(function* (
52
53
  const requests = yield* Ref.make<Array<HttpClientRequest.HttpClientRequest>>(
53
54
  [],
54
55
  )
55
- const client = HttpClient.make((request) =>
56
- Effect.gen(function* () {
56
+ const client = HttpClient.make(
57
+ Effect.fnUntraced(function* (request) {
57
58
  const attempt = yield* Ref.updateAndGet(attempts, (count) => count + 1)
58
59
  yield* Ref.update(requests, (current) => [...current, request])
59
60
  return HttpClientResponse.fromWeb(request, handler(request, attempt))
@@ -181,6 +182,7 @@ describe("GithubCopilotAuth", () => {
181
182
 
182
183
  const auth = yield* GithubCopilotAuth.make.pipe(
183
184
  Effect.provideService(HttpClient.HttpClient, client),
185
+ Effect.provide(DeviceCodeHandler.layerConsole),
184
186
  )
185
187
 
186
188
  const token = yield* auth.get
@@ -220,6 +222,7 @@ describe("GithubCopilotAuth", () => {
220
222
 
221
223
  const auth = yield* GithubCopilotAuth.make.pipe(
222
224
  Effect.provideService(HttpClient.HttpClient, client),
225
+ Effect.provide(DeviceCodeHandler.layerConsole),
223
226
  )
224
227
 
225
228
  const authenticated = yield* auth.authenticate
@@ -282,6 +285,7 @@ describe("GithubCopilotAuth", () => {
282
285
 
283
286
  const auth = yield* GithubCopilotAuth.make.pipe(
284
287
  Effect.provideService(HttpClient.HttpClient, client),
288
+ Effect.provide(DeviceCodeHandler.layerConsole),
285
289
  )
286
290
 
287
291
  assert.strictEqual(
@@ -328,6 +332,7 @@ describe("GithubCopilotAuth", () => {
328
332
 
329
333
  const auth = yield* GithubCopilotAuth.make.pipe(
330
334
  Effect.provideService(HttpClient.HttpClient, client),
335
+ Effect.provide(DeviceCodeHandler.layerConsole),
331
336
  )
332
337
 
333
338
  const firstFiber = yield* auth.get.pipe(
@@ -373,15 +378,14 @@ describe("GithubCopilotAuth", () => {
373
378
  jsonResponse({ ok: true }),
374
379
  )
375
380
 
376
- const wrappedClient = yield* Effect.gen(function* () {
377
- return yield* HttpClient.HttpClient
378
- }).pipe(
381
+ const wrappedClient = yield* HttpClient.HttpClient.asEffect().pipe(
379
382
  Effect.provide(GithubCopilotAuth.layerClientNoDeps),
380
383
  Effect.provideService(HttpClient.HttpClient, client),
381
- Effect.provideService(
384
+ Effect.provideServiceEffect(
382
385
  GithubCopilotAuth,
383
- yield* GithubCopilotAuth.make.pipe(
386
+ GithubCopilotAuth.make.pipe(
384
387
  Effect.provideService(HttpClient.HttpClient, client),
388
+ Effect.provide(DeviceCodeHandler.layerConsole),
385
389
  ),
386
390
  ),
387
391
  )
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * @since 1.0.0
3
3
  */
4
- import * as Console from "effect/Console"
5
4
  import * as Effect from "effect/Effect"
6
5
  import * as Function from "effect/Function"
7
6
  import * as Layer from "effect/Layer"
@@ -9,11 +8,12 @@ import * as Option from "effect/Option"
9
8
  import * as Schedule from "effect/Schedule"
10
9
  import * as Schema from "effect/Schema"
11
10
  import * as Semaphore from "effect/Semaphore"
12
- import * as ServiceMap from "effect/ServiceMap"
11
+ import * as Context from "effect/Context"
13
12
  import * as HttpClient from "effect/unstable/http/HttpClient"
14
13
  import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"
15
14
  import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
16
15
  import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore"
16
+ import { DeviceCodeHandler } from "./DeviceCodeHandler.ts"
17
17
 
18
18
  export const CLIENT_ID = "Ov23li8tweQw6odWQebz"
19
19
  export const ISSUER = "https://github.com"
@@ -213,15 +213,17 @@ export const toTokenStore = (store: KeyValueStore.KeyValueStore) =>
213
213
  TokenData,
214
214
  )
215
215
 
216
- export class GithubCopilotAuth extends ServiceMap.Service<
216
+ export class GithubCopilotAuth extends Context.Service<
217
217
  GithubCopilotAuth,
218
218
  {
219
+ readonly verifyUrl: string
219
220
  readonly get: Effect.Effect<TokenData, GithubCopilotAuthError>
220
221
  readonly authenticate: Effect.Effect<TokenData, GithubCopilotAuthError>
221
222
  readonly logout: Effect.Effect<void>
222
223
  }
223
224
  >()("clanka/GithubCopilotAuth") {
224
225
  static readonly make = Effect.gen(function* () {
226
+ const verfication = yield* DeviceCodeHandler
225
227
  const tokenStore = toTokenStore(yield* KeyValueStore.KeyValueStore)
226
228
  const httpClient = (yield* HttpClient.HttpClient).pipe(
227
229
  HttpClient.mapRequest(
@@ -242,7 +244,7 @@ export class GithubCopilotAuth extends ServiceMap.Service<
242
244
 
243
245
  let currentToken = yield* tokenStore.get(STORE_TOKEN_KEY).pipe(
244
246
  Effect.catchTag("SchemaError", (error) =>
245
- Console.warn(
247
+ Effect.logDebug(
246
248
  `Failed to decode persisted GitHub Copilot token, clearing it: ${error.message}`,
247
249
  ).pipe(
248
250
  Effect.andThen(tokenStore.remove(STORE_TOKEN_KEY)),
@@ -373,9 +375,10 @@ export class GithubCopilotAuth extends ServiceMap.Service<
373
375
 
374
376
  const authenticateWithDeviceFlow = Effect.gen(function* () {
375
377
  const deviceCode = yield* requestDeviceCode()
376
- yield* Console.log(
377
- `Open ${deviceCode.verificationUri} and enter code: ${deviceCode.userCode}`,
378
- )
378
+ yield* verfication.onCode({
379
+ verifyUrl: deviceCode.verificationUri,
380
+ deviceCode: deviceCode.userCode,
381
+ })
379
382
  return yield* pollAccessToken(deviceCode)
380
383
  })
381
384
 
@@ -402,6 +405,7 @@ export class GithubCopilotAuth extends ServiceMap.Service<
402
405
  )
403
406
 
404
407
  return GithubCopilotAuth.of({
408
+ verifyUrl: ISSUER + DEVICE_VERIFICATION_URL,
405
409
  get: semaphore.withPermit(getNoLock),
406
410
  authenticate: semaphore.withPermit(authenticateNoLock),
407
411
  logout: semaphore.withPermit(Effect.uninterruptible(clearToken)),