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.
- package/dist/Agent.d.ts +20 -20
- package/dist/Agent.d.ts.map +1 -1
- package/dist/Agent.js +9 -9
- package/dist/Agent.js.map +1 -1
- package/dist/Agent.test.js +1 -0
- package/dist/Agent.test.js.map +1 -1
- package/dist/AgentExecutor.d.ts +62 -101
- package/dist/AgentExecutor.d.ts.map +1 -1
- package/dist/AgentExecutor.js +28 -7
- package/dist/AgentExecutor.js.map +1 -1
- package/dist/AgentTools.d.ts +6 -6
- package/dist/AgentTools.d.ts.map +1 -1
- package/dist/AgentTools.js +5 -5
- package/dist/AgentTools.js.map +1 -1
- package/dist/ApplyPatch.test.js +3 -3
- package/dist/ApplyPatch.test.js.map +1 -1
- package/dist/ChunkRepo.d.ts +16 -16
- package/dist/ChunkRepo.d.ts.map +1 -1
- package/dist/ChunkRepo.js +7 -6
- package/dist/ChunkRepo.js.map +1 -1
- package/dist/CodeChunker.d.ts +2 -2
- package/dist/CodeChunker.d.ts.map +1 -1
- package/dist/CodeChunker.js +2 -2
- package/dist/CodeChunker.js.map +1 -1
- package/dist/Codex.d.ts +1 -1
- package/dist/Codex.d.ts.map +1 -1
- package/dist/CodexAuth.d.ts +13 -7
- package/dist/CodexAuth.d.ts.map +1 -1
- package/dist/CodexAuth.js +13 -8
- package/dist/CodexAuth.js.map +1 -1
- package/dist/CodexAuth.test.js +1 -262
- package/dist/CodexAuth.test.js.map +1 -1
- package/dist/Copilot.d.ts +1 -1
- package/dist/Copilot.d.ts.map +1 -1
- package/dist/CopilotAuth.d.ts +13 -7
- package/dist/CopilotAuth.d.ts.map +1 -1
- package/dist/CopilotAuth.js +10 -5
- package/dist/CopilotAuth.js.map +1 -1
- package/dist/CopilotAuth.test.js +7 -8
- package/dist/CopilotAuth.test.js.map +1 -1
- package/dist/DeviceCodeHandler.d.ts +14 -0
- package/dist/DeviceCodeHandler.d.ts.map +1 -0
- package/dist/DeviceCodeHandler.js +9 -0
- package/dist/DeviceCodeHandler.js.map +1 -0
- package/dist/ExaSearch.d.ts +3 -3
- package/dist/ExaSearch.d.ts.map +1 -1
- package/dist/ExaSearch.js +2 -2
- package/dist/ExaSearch.js.map +1 -1
- package/dist/McpClient.d.ts +3 -3
- package/dist/McpClient.d.ts.map +1 -1
- package/dist/McpClient.js +2 -2
- package/dist/McpClient.js.map +1 -1
- package/dist/OutputFormatter.d.ts +2 -2
- package/dist/OutputFormatter.d.ts.map +1 -1
- package/dist/OutputFormatter.js +2 -2
- package/dist/OutputFormatter.js.map +1 -1
- package/dist/SemanticSearch/Service.d.ts +2 -2
- package/dist/SemanticSearch/Service.d.ts.map +1 -1
- package/dist/SemanticSearch/Service.js +2 -2
- package/dist/SemanticSearch/Service.js.map +1 -1
- package/dist/SemanticSearch.js +3 -3
- package/dist/SemanticSearch.js.map +1 -1
- package/dist/ToolkitRenderer.d.ts +2 -2
- package/dist/ToolkitRenderer.d.ts.map +1 -1
- package/dist/ToolkitRenderer.js +2 -2
- package/dist/ToolkitRenderer.js.map +1 -1
- package/dist/WebToMarkdown.d.ts +2 -2
- package/dist/WebToMarkdown.d.ts.map +1 -1
- package/dist/WebToMarkdown.js +2 -2
- package/dist/WebToMarkdown.js.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/package.json +8 -6
- package/src/Agent.test.ts +1 -0
- package/src/Agent.ts +9 -9
- package/src/AgentExecutor.ts +46 -13
- package/src/AgentTools.ts +7 -7
- package/src/ApplyPatch.test.ts +3 -3
- package/src/ChunkRepo.ts +8 -6
- package/src/CodeChunker.ts +2 -2
- package/src/CodexAuth.test.ts +0 -433
- package/src/CodexAuth.ts +16 -19
- package/src/CopilotAuth.test.ts +11 -7
- package/src/CopilotAuth.ts +11 -7
- package/src/DeviceCodeHandler.ts +21 -0
- package/src/ExaSearch.ts +2 -2
- package/src/McpClient.ts +2 -2
- package/src/OutputFormatter.ts +2 -2
- package/src/SemanticSearch/Service.ts +2 -2
- package/src/SemanticSearch.ts +3 -3
- package/src/ToolkitRenderer.ts +2 -2
- package/src/WebToMarkdown.ts +2 -2
- package/src/cli.ts +2 -0
- package/src/fixtures/fiber.txt +9 -9
- package/src/fixtures/patch18-broken.txt +5 -5
- package/src/fixtures/patch18-fixed.txt +5 -5
- package/src/index.ts +5 -0
package/src/CodexAuth.test.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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*
|
|
344
|
-
|
|
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(
|
|
351
|
-
Effect.
|
|
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(
|
|
358
|
-
Effect.
|
|
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)),
|
package/src/CopilotAuth.test.ts
CHANGED
|
@@ -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(
|
|
56
|
-
Effect.
|
|
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*
|
|
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.
|
|
384
|
+
Effect.provideServiceEffect(
|
|
382
385
|
GithubCopilotAuth,
|
|
383
|
-
|
|
386
|
+
GithubCopilotAuth.make.pipe(
|
|
384
387
|
Effect.provideService(HttpClient.HttpClient, client),
|
|
388
|
+
Effect.provide(DeviceCodeHandler.layerConsole),
|
|
385
389
|
),
|
|
386
390
|
),
|
|
387
391
|
)
|
package/src/CopilotAuth.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 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
|
|
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
|
|
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
|
-
|
|
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*
|
|
377
|
-
|
|
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)),
|