@oh-my-pi/pi-ai 16.3.13 → 16.3.15

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.
@@ -0,0 +1,92 @@
1
+ import * as AIError from "../../error";
2
+
3
+ const DEVICE_FLOW_CANCEL_MESSAGE = "Login cancelled";
4
+ const DEVICE_FLOW_TIMEOUT_MESSAGE = "Device flow timed out";
5
+ const DEVICE_FLOW_SLOW_DOWN_TIMEOUT_MESSAGE =
6
+ "Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again.";
7
+ const MINIMUM_DEVICE_FLOW_INTERVAL_MS = 1000;
8
+ const DEFAULT_DEVICE_FLOW_INTERVAL_SECONDS = 5;
9
+ const SLOW_DOWN_INTERVAL_INCREMENT_MS = 5000;
10
+
11
+ /** Result returned by one OAuth device-code polling attempt. */
12
+ export type OAuthDeviceCodePollResult<T> =
13
+ | { status: "complete"; value: T }
14
+ | { status: "pending" }
15
+ | { status: "slow_down" }
16
+ | { status: "failed"; message: string };
17
+
18
+ /** Options for polling an RFC 8628-style OAuth device-code flow. */
19
+ export interface OAuthDeviceCodeFlowOptions<T> {
20
+ /** Poll the provider once and classify the response. */
21
+ poll(): OAuthDeviceCodePollResult<T> | Promise<OAuthDeviceCodePollResult<T>>;
22
+ /** Provider-requested polling cadence; defaults to RFC 8628's five seconds. */
23
+ intervalSeconds?: number;
24
+ /** Provider-issued expiry window for the device code. */
25
+ expiresInSeconds?: number;
26
+ /** Cancels the flow with the legacy "Login cancelled" error. */
27
+ signal?: AbortSignal;
28
+ }
29
+
30
+ async function abortableDeviceFlowSleep(ms: number, signal: AbortSignal | undefined): Promise<void> {
31
+ if (!signal) {
32
+ await Bun.sleep(ms);
33
+ return;
34
+ }
35
+ if (signal.aborted) {
36
+ throw new AIError.LoginCancelledError(DEVICE_FLOW_CANCEL_MESSAGE);
37
+ }
38
+
39
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
40
+ let timer: Timer | undefined;
41
+ const onAbort = () => {
42
+ clearTimeout(timer);
43
+ reject(new AIError.LoginCancelledError(DEVICE_FLOW_CANCEL_MESSAGE));
44
+ };
45
+ timer = setTimeout(() => {
46
+ signal.removeEventListener("abort", onAbort);
47
+ resolve();
48
+ }, ms);
49
+ signal.addEventListener("abort", onAbort, { once: true });
50
+ await promise;
51
+ }
52
+
53
+ /** Poll an OAuth device-code flow until completion, provider failure, timeout, or cancellation. */
54
+ export async function pollOAuthDeviceCodeFlow<T>(options: OAuthDeviceCodeFlowOptions<T>): Promise<T> {
55
+ const deadline =
56
+ typeof options.expiresInSeconds === "number"
57
+ ? Date.now() + options.expiresInSeconds * 1000
58
+ : Number.POSITIVE_INFINITY;
59
+ let intervalMs = Math.max(
60
+ MINIMUM_DEVICE_FLOW_INTERVAL_MS,
61
+ Math.floor((options.intervalSeconds ?? DEFAULT_DEVICE_FLOW_INTERVAL_SECONDS) * 1000),
62
+ );
63
+ let slowDownResponses = 0;
64
+
65
+ while (Date.now() < deadline) {
66
+ if (options.signal?.aborted) {
67
+ throw new AIError.LoginCancelledError(DEVICE_FLOW_CANCEL_MESSAGE);
68
+ }
69
+ const result = await options.poll();
70
+ if (result.status === "complete") {
71
+ return result.value;
72
+ }
73
+ if (result.status === "failed") {
74
+ throw new AIError.OAuthError(result.message, { kind: "polling" });
75
+ }
76
+ if (result.status === "slow_down") {
77
+ slowDownResponses += 1;
78
+ intervalMs = Math.max(MINIMUM_DEVICE_FLOW_INTERVAL_MS, intervalMs + SLOW_DOWN_INTERVAL_INCREMENT_MS);
79
+ }
80
+
81
+ const remainingMs = deadline - Date.now();
82
+ if (remainingMs <= 0) {
83
+ break;
84
+ }
85
+ await abortableDeviceFlowSleep(Math.min(intervalMs, remainingMs), options.signal);
86
+ }
87
+
88
+ throw new AIError.OAuthError(
89
+ slowDownResponses > 0 ? DEVICE_FLOW_SLOW_DOWN_TIMEOUT_MESSAGE : DEVICE_FLOW_TIMEOUT_MESSAGE,
90
+ { kind: "timeout" },
91
+ );
92
+ }
@@ -12,99 +12,9 @@ import type {
12
12
  OAuthProviderInterface,
13
13
  } from "./types";
14
14
 
15
+ export * from "./device-code";
15
16
  export type * from "./types";
16
17
 
17
- const DEVICE_FLOW_CANCEL_MESSAGE = "Login cancelled";
18
- const DEVICE_FLOW_TIMEOUT_MESSAGE = "Device flow timed out";
19
- const DEVICE_FLOW_SLOW_DOWN_TIMEOUT_MESSAGE =
20
- "Device flow timed out after one or more slow_down responses. This is often caused by clock drift in WSL or VM environments. Please sync or restart the VM clock and try again.";
21
- const MINIMUM_DEVICE_FLOW_INTERVAL_MS = 1000;
22
- const DEFAULT_DEVICE_FLOW_INTERVAL_SECONDS = 5;
23
- const SLOW_DOWN_INTERVAL_INCREMENT_MS = 5000;
24
-
25
- /** Result returned by one OAuth device-code polling attempt. */
26
- export type OAuthDeviceCodePollResult<T> =
27
- | { status: "complete"; value: T }
28
- | { status: "pending" }
29
- | { status: "slow_down" }
30
- | { status: "failed"; message: string };
31
-
32
- /** Options for polling an RFC 8628-style OAuth device-code flow. */
33
- export interface OAuthDeviceCodeFlowOptions<T> {
34
- /** Poll the provider once and classify the response. */
35
- poll(): OAuthDeviceCodePollResult<T> | Promise<OAuthDeviceCodePollResult<T>>;
36
- /** Provider-requested polling cadence; defaults to RFC 8628's five seconds. */
37
- intervalSeconds?: number;
38
- /** Provider-issued expiry window for the device code. */
39
- expiresInSeconds?: number;
40
- /** Cancels the flow with the legacy "Login cancelled" error. */
41
- signal?: AbortSignal;
42
- }
43
-
44
- async function abortableDeviceFlowSleep(ms: number, signal: AbortSignal | undefined): Promise<void> {
45
- if (!signal) {
46
- await Bun.sleep(ms);
47
- return;
48
- }
49
- if (signal.aborted) {
50
- throw new AIError.LoginCancelledError(DEVICE_FLOW_CANCEL_MESSAGE);
51
- }
52
-
53
- const { promise, resolve, reject } = Promise.withResolvers<void>();
54
- let timer: Timer | undefined;
55
- const onAbort = () => {
56
- if (timer) clearTimeout(timer);
57
- reject(new AIError.LoginCancelledError(DEVICE_FLOW_CANCEL_MESSAGE));
58
- };
59
- timer = setTimeout(() => {
60
- signal.removeEventListener("abort", onAbort);
61
- resolve();
62
- }, ms);
63
- signal.addEventListener("abort", onAbort, { once: true });
64
- await promise;
65
- }
66
-
67
- /** Poll an OAuth device-code flow until completion, provider failure, timeout, or cancellation. */
68
- export async function pollOAuthDeviceCodeFlow<T>(options: OAuthDeviceCodeFlowOptions<T>): Promise<T> {
69
- const deadline =
70
- typeof options.expiresInSeconds === "number"
71
- ? Date.now() + options.expiresInSeconds * 1000
72
- : Number.POSITIVE_INFINITY;
73
- let intervalMs = Math.max(
74
- MINIMUM_DEVICE_FLOW_INTERVAL_MS,
75
- Math.floor((options.intervalSeconds ?? DEFAULT_DEVICE_FLOW_INTERVAL_SECONDS) * 1000),
76
- );
77
- let slowDownResponses = 0;
78
-
79
- while (Date.now() < deadline) {
80
- if (options.signal?.aborted) {
81
- throw new AIError.LoginCancelledError(DEVICE_FLOW_CANCEL_MESSAGE);
82
- }
83
- const result = await options.poll();
84
- if (result.status === "complete") {
85
- return result.value;
86
- }
87
- if (result.status === "failed") {
88
- throw new AIError.OAuthError(result.message, { kind: "polling" });
89
- }
90
- if (result.status === "slow_down") {
91
- slowDownResponses += 1;
92
- intervalMs = Math.max(MINIMUM_DEVICE_FLOW_INTERVAL_MS, intervalMs + SLOW_DOWN_INTERVAL_INCREMENT_MS);
93
- }
94
-
95
- const remainingMs = deadline - Date.now();
96
- if (remainingMs <= 0) {
97
- break;
98
- }
99
- await abortableDeviceFlowSleep(Math.min(intervalMs, remainingMs), options.signal);
100
- }
101
-
102
- throw new AIError.OAuthError(
103
- slowDownResponses > 0 ? DEVICE_FLOW_SLOW_DOWN_TIMEOUT_MESSAGE : DEVICE_FLOW_TIMEOUT_MESSAGE,
104
- { kind: "timeout" },
105
- );
106
- }
107
-
108
18
  const builtInOAuthProviders: OAuthProviderInfo[] = PROVIDER_REGISTRY.filter(
109
19
  provider => provider.login && provider.showInLoginList !== false,
110
20
  ).map(provider => ({