@cursorpool-dev/cli 0.5.6
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/bin/cursor-pool.mjs +9 -0
- package/bin/cursor-pool.ts +169 -0
- package/node_modules/@cursor-pool/extension/dist/extension.js +2910 -0
- package/node_modules/@cursor-pool/extension/package.json +64 -0
- package/node_modules/@cursor-pool/extension/resources/cursor-pool.svg +6 -0
- package/node_modules/@cursor-pool/extension/src/api.ts +545 -0
- package/node_modules/@cursor-pool/extension/src/extension.ts +104 -0
- package/node_modules/@cursor-pool/extension/src/index.ts +1 -0
- package/node_modules/@cursor-pool/extension/src/panel.ts +569 -0
- package/node_modules/@cursor-pool/extension/src/runtime.ts +22 -0
- package/node_modules/@cursor-pool/extension/test/panel.test.ts +1785 -0
- package/node_modules/@cursor-pool/patcher/package.json +17 -0
- package/node_modules/@cursor-pool/patcher/src/alwaysLocalMarker.ts +86 -0
- package/node_modules/@cursor-pool/patcher/src/hash.ts +7 -0
- package/node_modules/@cursor-pool/patcher/src/index.ts +55 -0
- package/node_modules/@cursor-pool/patcher/src/marker.ts +159 -0
- package/node_modules/@cursor-pool/patcher/src/patchCursorAgentExec.ts +154 -0
- package/node_modules/@cursor-pool/patcher/src/patchCursorAlwaysLocal.ts +142 -0
- package/node_modules/@cursor-pool/patcher/src/patchCursorWorkbenchAuthGate.ts +140 -0
- package/node_modules/@cursor-pool/patcher/src/restoreCursorAgentExec.ts +52 -0
- package/node_modules/@cursor-pool/patcher/src/restoreCursorAlwaysLocal.ts +52 -0
- package/node_modules/@cursor-pool/patcher/src/restoreCursorWorkbenchAuthGate.ts +70 -0
- package/node_modules/@cursor-pool/patcher/src/workbenchAuthGateMarker.ts +243 -0
- package/node_modules/@cursor-pool/patcher/test/patchCursorAgentExec.test.ts +630 -0
- package/node_modules/@cursor-pool/patcher/test/patchCursorAlwaysLocal.test.ts +144 -0
- package/node_modules/@cursor-pool/patcher/test/patchCursorWorkbench.test.ts +770 -0
- package/node_modules/@cursor-pool/patcher/test/restoreCursorAgentExec.test.ts +139 -0
- package/node_modules/@cursor-pool/service/package.json +17 -0
- package/node_modules/@cursor-pool/service/src/canary.ts +61 -0
- package/node_modules/@cursor-pool/service/src/diagnostics.ts +385 -0
- package/node_modules/@cursor-pool/service/src/entry.ts +161 -0
- package/node_modules/@cursor-pool/service/src/health.ts +10 -0
- package/node_modules/@cursor-pool/service/src/index.ts +29 -0
- package/node_modules/@cursor-pool/service/src/metadata.ts +22 -0
- package/node_modules/@cursor-pool/service/src/platformSession.ts +1178 -0
- package/node_modules/@cursor-pool/service/src/requestCheck.ts +81 -0
- package/node_modules/@cursor-pool/service/src/requestGate.ts +100 -0
- package/node_modules/@cursor-pool/service/src/requestGateway.ts +441 -0
- package/node_modules/@cursor-pool/service/src/runtime.ts +48 -0
- package/node_modules/@cursor-pool/service/src/server.ts +939 -0
- package/node_modules/@cursor-pool/service/src/takeover.ts +111 -0
- package/node_modules/@cursor-pool/service/test/canary.test.ts +140 -0
- package/node_modules/@cursor-pool/service/test/diagnostics.test.ts +506 -0
- package/node_modules/@cursor-pool/service/test/metadata.test.ts +63 -0
- package/node_modules/@cursor-pool/service/test/platformSession.test.ts +2428 -0
- package/node_modules/@cursor-pool/service/test/requestCheck.test.ts +152 -0
- package/node_modules/@cursor-pool/service/test/requestGate.test.ts +207 -0
- package/node_modules/@cursor-pool/service/test/requestGateway.test.ts +466 -0
- package/node_modules/@cursor-pool/service/test/runtime.test.ts +47 -0
- package/node_modules/@cursor-pool/service/test/server.test.ts +2570 -0
- package/node_modules/@cursor-pool/shared/package.json +17 -0
- package/node_modules/@cursor-pool/shared/src/clientConfig.ts +49 -0
- package/node_modules/@cursor-pool/shared/src/index.ts +14 -0
- package/node_modules/@cursor-pool/shared/src/manifest.ts +36 -0
- package/node_modules/@cursor-pool/shared/src/metadata.ts +19 -0
- package/node_modules/@cursor-pool/shared/src/paths.ts +5 -0
- package/node_modules/@cursor-pool/shared/src/runtime.ts +3 -0
- package/node_modules/@cursor-pool/shared/test/index.test.ts +56 -0
- package/node_modules/@cursor-pool/shared/test/manifest.test.ts +65 -0
- package/node_modules/@cursor-pool/shared/test/metadata.test.ts +25 -0
- package/node_modules/@cursor-pool/shared/test/runtime.test.ts +8 -0
- package/package.json +28 -0
- package/src/adHocResign.ts +65 -0
- package/src/autostart.ts +240 -0
- package/src/compat.ts +282 -0
- package/src/confirm.ts +76 -0
- package/src/cursor.ts +94 -0
- package/src/diagnostics.ts +558 -0
- package/src/environment.ts +18 -0
- package/src/extensionBundle.ts +111 -0
- package/src/extensionLink.ts +168 -0
- package/src/index.ts +23 -0
- package/src/install.ts +614 -0
- package/src/installRecord.ts +105 -0
- package/src/launch.ts +182 -0
- package/src/patchSet.ts +182 -0
- package/src/platform.ts +132 -0
- package/src/repair.ts +383 -0
- package/src/restore.ts +153 -0
- package/src/serviceCommands.ts +79 -0
- package/src/serviceProcess.ts +188 -0
- package/src/status.ts +241 -0
- package/src/target.ts +37 -0
- package/src/trial.ts +133 -0
- package/src/uninstall.ts +213 -0
- package/test/autostart.test.ts +151 -0
- package/test/compat.test.ts +192 -0
- package/test/confirm.test.ts +114 -0
- package/test/cursor-pool-bin.test.ts +658 -0
- package/test/cursor.test.ts +20 -0
- package/test/diagnostics.test.ts +709 -0
- package/test/e2e-install.test.ts +773 -0
- package/test/extensionBundle.test.ts +161 -0
- package/test/extensionLink.test.ts +209 -0
- package/test/install.test.ts +862 -0
- package/test/installRecord.test.ts +107 -0
- package/test/launch.test.ts +138 -0
- package/test/platform.test.ts +226 -0
- package/test/repair.test.ts +575 -0
- package/test/restore.test.ts +211 -0
- package/test/serviceCommands.test.ts +135 -0
- package/test/serviceProcess.test.ts +280 -0
- package/test/status.test.ts +615 -0
- package/test/target.test.ts +49 -0
- package/test/trial.test.ts +146 -0
|
@@ -0,0 +1,1178 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_PLATFORM_SESSION_FILE = '~/.cursor-pool/session.json';
|
|
6
|
+
|
|
7
|
+
export type PlatformUserSummary = {
|
|
8
|
+
id: string;
|
|
9
|
+
email: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type PlatformDeviceSummary = {
|
|
13
|
+
id: string;
|
|
14
|
+
status: string;
|
|
15
|
+
lastHeartbeatAt: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type PlatformProductSummary = {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
status: 'available' | 'unavailable';
|
|
23
|
+
minCredits: number;
|
|
24
|
+
usageLabel: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type PoolSessionSummary = {
|
|
28
|
+
id: string;
|
|
29
|
+
productId: string;
|
|
30
|
+
providerType: string;
|
|
31
|
+
status: 'active' | 'refresh-required' | 'stopping' | 'stopped' | 'expired' | 'failed';
|
|
32
|
+
startedAt: string;
|
|
33
|
+
expiresAt: string;
|
|
34
|
+
routeTokenExpiresAt: string;
|
|
35
|
+
routeStrategy: 'platform-gateway';
|
|
36
|
+
bannedModels: string[];
|
|
37
|
+
availableModels?: string[];
|
|
38
|
+
capabilities: {
|
|
39
|
+
streaming: boolean;
|
|
40
|
+
usageEstimate: boolean;
|
|
41
|
+
hardSpendLimit: boolean;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type LocalRouteTokenState = {
|
|
46
|
+
token: string;
|
|
47
|
+
expiresAt: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type SafeRouteState =
|
|
51
|
+
| { state: 'missing' }
|
|
52
|
+
| { state: 'ready'; expiresAt: string }
|
|
53
|
+
| { state: 'expired'; expiresAt: string };
|
|
54
|
+
|
|
55
|
+
export type PlatformModeReleaseReason =
|
|
56
|
+
| 'product-missing'
|
|
57
|
+
| 'product-unavailable'
|
|
58
|
+
| 'insufficient-credits'
|
|
59
|
+
| 'invalid-token'
|
|
60
|
+
| 'device-inactive';
|
|
61
|
+
|
|
62
|
+
export type PlatformModeSnapshot =
|
|
63
|
+
| { state: 'inactive'; releaseReason?: PlatformModeReleaseReason; releasedAt?: string }
|
|
64
|
+
| { state: 'active'; productId: string; startedAt: string };
|
|
65
|
+
|
|
66
|
+
export type PlatformModeResult =
|
|
67
|
+
| PlatformModeSnapshot
|
|
68
|
+
| { state: 'logged-out' }
|
|
69
|
+
| { state: 'offline' }
|
|
70
|
+
| { state: 'invalid-token' }
|
|
71
|
+
| { state: 'product-not-selected' }
|
|
72
|
+
| { state: 'product-not-found'; productId: string }
|
|
73
|
+
| { state: 'product-unavailable'; productId: string }
|
|
74
|
+
| { state: 'provider-unavailable' }
|
|
75
|
+
| { state: 'provider-rate-limited' }
|
|
76
|
+
| { state: 'manual-review-required' }
|
|
77
|
+
| { state: 'model-banned' }
|
|
78
|
+
| { state: 'device-inactive' }
|
|
79
|
+
| {
|
|
80
|
+
state: 'insufficient-credits';
|
|
81
|
+
productId: string;
|
|
82
|
+
requiredCredits: number;
|
|
83
|
+
currentCredits: number;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type PlatformSession = {
|
|
87
|
+
apiBaseUrl: string;
|
|
88
|
+
deviceToken: string;
|
|
89
|
+
createdAt: string;
|
|
90
|
+
user: PlatformUserSummary;
|
|
91
|
+
device: PlatformDeviceSummary;
|
|
92
|
+
selectedProductId?: string;
|
|
93
|
+
platformMode?: 'active' | 'inactive';
|
|
94
|
+
activeProductId?: string;
|
|
95
|
+
platformModeStartedAt?: string;
|
|
96
|
+
lastModeReleaseReason?: PlatformModeReleaseReason;
|
|
97
|
+
lastModeReleasedAt?: string;
|
|
98
|
+
poolSession?: PoolSessionSummary;
|
|
99
|
+
routeToken?: LocalRouteTokenState;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type PlatformStatus =
|
|
103
|
+
| { state: 'logged-out' }
|
|
104
|
+
| {
|
|
105
|
+
state: 'logged-in';
|
|
106
|
+
user: PlatformUserSummary;
|
|
107
|
+
device: PlatformDeviceSummary;
|
|
108
|
+
mode: PlatformModeSnapshot;
|
|
109
|
+
}
|
|
110
|
+
| {
|
|
111
|
+
state: 'offline';
|
|
112
|
+
user?: PlatformUserSummary;
|
|
113
|
+
device?: PlatformDeviceSummary;
|
|
114
|
+
mode?: PlatformModeSnapshot;
|
|
115
|
+
}
|
|
116
|
+
| {
|
|
117
|
+
state: 'invalid-token';
|
|
118
|
+
user?: PlatformUserSummary;
|
|
119
|
+
device?: PlatformDeviceSummary;
|
|
120
|
+
mode?: PlatformModeSnapshot;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export type PlatformCatalog =
|
|
124
|
+
| { state: 'logged-out'; products: [] }
|
|
125
|
+
| {
|
|
126
|
+
state: 'logged-in';
|
|
127
|
+
account: { credits: number };
|
|
128
|
+
mode: PlatformModeSnapshot;
|
|
129
|
+
selectedProductId?: string;
|
|
130
|
+
products: PlatformProductSummary[];
|
|
131
|
+
}
|
|
132
|
+
| { state: 'offline'; products: [] }
|
|
133
|
+
| { state: 'invalid-token'; mode?: PlatformModeSnapshot; products: [] };
|
|
134
|
+
|
|
135
|
+
export type PlatformSelectionResult =
|
|
136
|
+
| { state: 'selected'; selectedProductId: string }
|
|
137
|
+
| { state: 'logged-out' }
|
|
138
|
+
| { state: 'offline' }
|
|
139
|
+
| { state: 'invalid-token' }
|
|
140
|
+
| { state: 'product-not-found'; productId: string }
|
|
141
|
+
| { state: 'product-unavailable'; productId: string };
|
|
142
|
+
|
|
143
|
+
export type PlatformDeviceInput = {
|
|
144
|
+
name?: string;
|
|
145
|
+
os?: string;
|
|
146
|
+
arch?: string;
|
|
147
|
+
cursorVersion?: string;
|
|
148
|
+
cursorCommit?: string;
|
|
149
|
+
cliVersion?: string;
|
|
150
|
+
extensionVersion?: string;
|
|
151
|
+
serviceVersion?: string;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export type HeartbeatPayload = {
|
|
155
|
+
cursorVersion?: string;
|
|
156
|
+
cursorCommit?: string;
|
|
157
|
+
serviceStatus?: string;
|
|
158
|
+
patchStatus?: string;
|
|
159
|
+
extensionStatus?: string;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
type DeviceTokenResponse = {
|
|
163
|
+
deviceToken: string;
|
|
164
|
+
user: PlatformUserSummary;
|
|
165
|
+
device: PlatformDeviceSummary;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
type HeartbeatResponse = {
|
|
169
|
+
ok: boolean;
|
|
170
|
+
device: PlatformDeviceSummary;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
type MeResponse = {
|
|
174
|
+
user: PlatformUserSummary;
|
|
175
|
+
device: PlatformDeviceSummary;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
type AccountSummaryResponse = {
|
|
179
|
+
credits: number;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
type ProductsResponse = {
|
|
183
|
+
products: unknown[];
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
type RequestJsonError = Error & {
|
|
187
|
+
status?: number;
|
|
188
|
+
platformCode?: string;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
type RequestErrorStatusOptions = PlatformSessionOptions & {
|
|
192
|
+
releaseActiveModeOnUnauthorized?: boolean;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const SAFE_POOL_TOKEN_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
196
|
+
const SECRET_LIKE_PATTERN = /(api[_-]?key|authorization|bearer|cursor[_-]?auth|provider[_-]?secret|secret|token|sk-[A-Za-z0-9])/i;
|
|
197
|
+
|
|
198
|
+
export type LoginWithCodeOptions = {
|
|
199
|
+
code: string;
|
|
200
|
+
apiBaseUrl: string;
|
|
201
|
+
sessionFile?: string;
|
|
202
|
+
device?: PlatformDeviceInput;
|
|
203
|
+
exchangeDeviceToken?: (request: {
|
|
204
|
+
code: string;
|
|
205
|
+
apiBaseUrl: string;
|
|
206
|
+
device: PlatformDeviceInput;
|
|
207
|
+
}) => Promise<DeviceTokenResponse>;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export type LoginWithPasswordOptions = {
|
|
211
|
+
email: string;
|
|
212
|
+
password: string;
|
|
213
|
+
apiBaseUrl: string;
|
|
214
|
+
sessionFile?: string;
|
|
215
|
+
device?: PlatformDeviceInput;
|
|
216
|
+
exchangePasswordDeviceToken?: (request: {
|
|
217
|
+
email: string;
|
|
218
|
+
password: string;
|
|
219
|
+
apiBaseUrl: string;
|
|
220
|
+
device: PlatformDeviceInput;
|
|
221
|
+
}) => Promise<DeviceTokenResponse>;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export type PlatformSessionOptions = {
|
|
225
|
+
sessionFile?: string;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export type PoolSessionFailureReason =
|
|
229
|
+
| 'INSUFFICIENT_CREDITS'
|
|
230
|
+
| 'DEVICE_INVALID'
|
|
231
|
+
| 'PRODUCT_UNAVAILABLE'
|
|
232
|
+
| 'PROVIDER_UNAVAILABLE'
|
|
233
|
+
| 'MODEL_BANNED'
|
|
234
|
+
| 'RATE_LIMITED'
|
|
235
|
+
| 'MANUAL_REVIEW_REQUIRED';
|
|
236
|
+
|
|
237
|
+
export type StartPoolSessionResult =
|
|
238
|
+
| { state: 'started'; session: PoolSessionSummary; routeToken: string }
|
|
239
|
+
| { state: 'failed'; reason: PoolSessionFailureReason };
|
|
240
|
+
|
|
241
|
+
export type StartPoolSessionRequest = {
|
|
242
|
+
session: PlatformSession;
|
|
243
|
+
productId: string;
|
|
244
|
+
client?: {
|
|
245
|
+
runtimeId?: string;
|
|
246
|
+
serviceVersion?: string;
|
|
247
|
+
cursorVersion?: string;
|
|
248
|
+
cursorCommit?: string;
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export type StartPlatformModeOptions = PlatformSessionOptions & {
|
|
253
|
+
startPoolSession?: (request: StartPoolSessionRequest) => Promise<StartPoolSessionResult>;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
export type RefreshPlatformRouteResult =
|
|
257
|
+
| { state: 'active'; productId: string; startedAt: string }
|
|
258
|
+
| { state: 'not-refreshable' }
|
|
259
|
+
| { state: 'failed'; reason: PlatformModeResult['state'] };
|
|
260
|
+
|
|
261
|
+
export type StopPoolSessionReason =
|
|
262
|
+
| 'user-stop'
|
|
263
|
+
| 'logout'
|
|
264
|
+
| 'invalid-token'
|
|
265
|
+
| 'device-inactive'
|
|
266
|
+
| 'product-unavailable'
|
|
267
|
+
| 'insufficient-credits'
|
|
268
|
+
| 'session-expired'
|
|
269
|
+
| 'local-error';
|
|
270
|
+
|
|
271
|
+
export type StopPoolSessionRequest = {
|
|
272
|
+
session: PlatformSession;
|
|
273
|
+
poolSessionId: string;
|
|
274
|
+
reason: StopPoolSessionReason;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
export type StopPoolSessionResult = {
|
|
278
|
+
state: 'stopped';
|
|
279
|
+
sessionId: string;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
export type StopPlatformModeOptions = PlatformSessionOptions & {
|
|
283
|
+
stopPoolSession?: (request: StopPoolSessionRequest) => Promise<StopPoolSessionResult>;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export type PlatformSessionSnapshot =
|
|
287
|
+
| { state: 'missing' }
|
|
288
|
+
| { state: 'invalid' }
|
|
289
|
+
| { state: 'valid'; session: PlatformSession };
|
|
290
|
+
|
|
291
|
+
export type SendHeartbeatOptions = PlatformSessionOptions & {
|
|
292
|
+
payload?: HeartbeatPayload;
|
|
293
|
+
postHeartbeat?: (request: {
|
|
294
|
+
session: PlatformSession;
|
|
295
|
+
payload: HeartbeatPayload;
|
|
296
|
+
}) => Promise<HeartbeatResponse>;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
export type LogoutPlatformOptions = PlatformSessionOptions & {
|
|
300
|
+
postLogout?: (request: { session: PlatformSession }) => Promise<void>;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
export function resolvePlatformSessionFile(sessionFile = DEFAULT_PLATFORM_SESSION_FILE) {
|
|
304
|
+
if (sessionFile.startsWith('~/')) {
|
|
305
|
+
return join(homedir(), sessionFile.slice(2));
|
|
306
|
+
}
|
|
307
|
+
return sessionFile;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function cleanApiBaseUrl(apiBaseUrl: string) {
|
|
311
|
+
return apiBaseUrl.replace(/\/+$/, '');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function isSafePoolToken(value: unknown): value is string {
|
|
315
|
+
return typeof value === 'string' && SAFE_POOL_TOKEN_PATTERN.test(value) && !SECRET_LIKE_PATTERN.test(value);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function cleanPoolTokens(values: string[]) {
|
|
319
|
+
return values.filter(isSafePoolToken);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function sanitizePoolSessionSummary(value: PoolSessionSummary): PoolSessionSummary {
|
|
323
|
+
return {
|
|
324
|
+
id: value.id,
|
|
325
|
+
productId: value.productId,
|
|
326
|
+
providerType: value.providerType,
|
|
327
|
+
status: value.status,
|
|
328
|
+
startedAt: value.startedAt,
|
|
329
|
+
expiresAt: value.expiresAt,
|
|
330
|
+
routeTokenExpiresAt: value.routeTokenExpiresAt,
|
|
331
|
+
routeStrategy: 'platform-gateway',
|
|
332
|
+
bannedModels: cleanPoolTokens(value.bannedModels),
|
|
333
|
+
...(value.availableModels !== undefined
|
|
334
|
+
? { availableModels: cleanPoolTokens(value.availableModels) }
|
|
335
|
+
: {}),
|
|
336
|
+
capabilities: {
|
|
337
|
+
streaming: value.capabilities.streaming,
|
|
338
|
+
usageEstimate: value.capabilities.usageEstimate,
|
|
339
|
+
hardSpendLimit: value.capabilities.hardSpendLimit,
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function asStatus(session: PlatformSession): PlatformStatus {
|
|
345
|
+
return {
|
|
346
|
+
state: 'logged-in',
|
|
347
|
+
user: session.user,
|
|
348
|
+
device: session.device,
|
|
349
|
+
mode: platformModeFromSession(session),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function isPlatformModeReleaseReason(value: unknown): value is PlatformModeReleaseReason {
|
|
354
|
+
return (
|
|
355
|
+
value === 'product-missing' ||
|
|
356
|
+
value === 'product-unavailable' ||
|
|
357
|
+
value === 'insufficient-credits' ||
|
|
358
|
+
value === 'invalid-token' ||
|
|
359
|
+
value === 'device-inactive'
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function inactiveModeFromSession(session: PlatformSession | null): PlatformModeSnapshot {
|
|
364
|
+
if (
|
|
365
|
+
isPlatformModeReleaseReason(session?.lastModeReleaseReason) &&
|
|
366
|
+
typeof session.lastModeReleasedAt === 'string'
|
|
367
|
+
) {
|
|
368
|
+
return {
|
|
369
|
+
state: 'inactive',
|
|
370
|
+
releaseReason: session.lastModeReleaseReason,
|
|
371
|
+
releasedAt: session.lastModeReleasedAt,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return { state: 'inactive' };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function platformModeFromSession(session: PlatformSession | null): PlatformModeSnapshot {
|
|
379
|
+
if (
|
|
380
|
+
session?.platformMode === 'active' &&
|
|
381
|
+
typeof session.activeProductId === 'string' &&
|
|
382
|
+
typeof session.platformModeStartedAt === 'string'
|
|
383
|
+
) {
|
|
384
|
+
return {
|
|
385
|
+
state: 'active',
|
|
386
|
+
productId: session.activeProductId,
|
|
387
|
+
startedAt: session.platformModeStartedAt,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return inactiveModeFromSession(session);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function clearPlatformMode(session: PlatformSession): PlatformSession {
|
|
395
|
+
const {
|
|
396
|
+
platformMode: _platformMode,
|
|
397
|
+
activeProductId: _activeProductId,
|
|
398
|
+
platformModeStartedAt: _platformModeStartedAt,
|
|
399
|
+
poolSession: _poolSession,
|
|
400
|
+
routeToken: _routeToken,
|
|
401
|
+
...inactiveSession
|
|
402
|
+
} = session;
|
|
403
|
+
return inactiveSession;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function clearModeRelease(session: PlatformSession): PlatformSession {
|
|
407
|
+
const {
|
|
408
|
+
lastModeReleaseReason: _lastModeReleaseReason,
|
|
409
|
+
lastModeReleasedAt: _lastModeReleasedAt,
|
|
410
|
+
...activeSession
|
|
411
|
+
} = session;
|
|
412
|
+
return activeSession;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function releasePlatformMode(
|
|
416
|
+
session: PlatformSession,
|
|
417
|
+
reason: PlatformModeReleaseReason,
|
|
418
|
+
): { session: PlatformSession; mode: PlatformModeSnapshot } {
|
|
419
|
+
const releasedAt = new Date().toISOString();
|
|
420
|
+
return {
|
|
421
|
+
session: {
|
|
422
|
+
...clearPlatformMode(session),
|
|
423
|
+
lastModeReleaseReason: reason,
|
|
424
|
+
lastModeReleasedAt: releasedAt,
|
|
425
|
+
},
|
|
426
|
+
mode: { state: 'inactive', releaseReason: reason, releasedAt },
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function releaseActivePlatformMode(
|
|
431
|
+
session: PlatformSession,
|
|
432
|
+
reason: PlatformModeReleaseReason,
|
|
433
|
+
options: PlatformSessionOptions,
|
|
434
|
+
): Promise<{ session: PlatformSession; mode: PlatformModeSnapshot }> {
|
|
435
|
+
const mode = platformModeFromSession(session);
|
|
436
|
+
if (mode.state !== 'active') {
|
|
437
|
+
return { session, mode };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const released = releasePlatformMode(session, reason);
|
|
441
|
+
await writePlatformSession(released.session, options);
|
|
442
|
+
return released;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function releaseForInactiveDevice(
|
|
446
|
+
session: PlatformSession,
|
|
447
|
+
options: PlatformSessionOptions,
|
|
448
|
+
): Promise<{ session: PlatformSession; mode: PlatformModeSnapshot }> {
|
|
449
|
+
if (session.device.status === 'active') {
|
|
450
|
+
return { session, mode: platformModeFromSession(session) };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return releaseActivePlatformMode(session, 'device-inactive', options);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function isUserSummary(value: unknown): value is PlatformUserSummary {
|
|
457
|
+
const user = value as PlatformUserSummary;
|
|
458
|
+
return typeof user?.id === 'string' && typeof user.email === 'string';
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function isDeviceSummary(value: unknown): value is PlatformDeviceSummary {
|
|
462
|
+
const device = value as PlatformDeviceSummary;
|
|
463
|
+
return (
|
|
464
|
+
typeof device?.id === 'string' &&
|
|
465
|
+
typeof device.status === 'string' &&
|
|
466
|
+
typeof device.lastHeartbeatAt === 'string'
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function isProductSummary(value: unknown): value is PlatformProductSummary {
|
|
471
|
+
const product = value as PlatformProductSummary;
|
|
472
|
+
return (
|
|
473
|
+
typeof product?.id === 'string' &&
|
|
474
|
+
typeof product.name === 'string' &&
|
|
475
|
+
typeof product.description === 'string' &&
|
|
476
|
+
(product.status === 'available' || product.status === 'unavailable') &&
|
|
477
|
+
Number.isInteger(product.minCredits) &&
|
|
478
|
+
product.minCredits >= 0 &&
|
|
479
|
+
typeof product.usageLabel === 'string'
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function sanitizeProductSummary(value: unknown): PlatformProductSummary | undefined {
|
|
484
|
+
if (!isProductSummary(value)) {
|
|
485
|
+
return undefined;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
id: value.id,
|
|
490
|
+
name: value.name,
|
|
491
|
+
description: value.description,
|
|
492
|
+
status: value.status,
|
|
493
|
+
minCredits: value.minCredits,
|
|
494
|
+
usageLabel: value.usageLabel,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function isPoolSessionSummary(value: unknown): value is PoolSessionSummary {
|
|
499
|
+
const session = value as PoolSessionSummary;
|
|
500
|
+
return (
|
|
501
|
+
typeof session?.id === 'string' &&
|
|
502
|
+
typeof session.productId === 'string' &&
|
|
503
|
+
typeof session.providerType === 'string' &&
|
|
504
|
+
(
|
|
505
|
+
session.status === 'active' ||
|
|
506
|
+
session.status === 'refresh-required' ||
|
|
507
|
+
session.status === 'stopping' ||
|
|
508
|
+
session.status === 'stopped' ||
|
|
509
|
+
session.status === 'expired' ||
|
|
510
|
+
session.status === 'failed'
|
|
511
|
+
) &&
|
|
512
|
+
typeof session.startedAt === 'string' &&
|
|
513
|
+
typeof session.expiresAt === 'string' &&
|
|
514
|
+
typeof session.routeTokenExpiresAt === 'string' &&
|
|
515
|
+
session.routeStrategy === 'platform-gateway' &&
|
|
516
|
+
Array.isArray(session.bannedModels) &&
|
|
517
|
+
session.bannedModels.every((model) => typeof model === 'string') &&
|
|
518
|
+
(
|
|
519
|
+
session.availableModels === undefined ||
|
|
520
|
+
(
|
|
521
|
+
Array.isArray(session.availableModels) &&
|
|
522
|
+
session.availableModels.every((model) => typeof model === 'string')
|
|
523
|
+
)
|
|
524
|
+
) &&
|
|
525
|
+
typeof session.capabilities?.streaming === 'boolean' &&
|
|
526
|
+
typeof session.capabilities.usageEstimate === 'boolean' &&
|
|
527
|
+
typeof session.capabilities.hardSpendLimit === 'boolean'
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function isLocalRouteTokenState(value: unknown): value is LocalRouteTokenState {
|
|
532
|
+
const route = value as LocalRouteTokenState;
|
|
533
|
+
return typeof route?.token === 'string' && typeof route.expiresAt === 'string';
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function isPlatformSession(value: unknown): value is PlatformSession {
|
|
537
|
+
const session = value as PlatformSession;
|
|
538
|
+
return (
|
|
539
|
+
typeof session?.apiBaseUrl === 'string' &&
|
|
540
|
+
typeof session.deviceToken === 'string' &&
|
|
541
|
+
typeof session.createdAt === 'string' &&
|
|
542
|
+
isUserSummary(session.user) &&
|
|
543
|
+
isDeviceSummary(session.device) &&
|
|
544
|
+
(session.selectedProductId === undefined || typeof session.selectedProductId === 'string') &&
|
|
545
|
+
(session.platformMode === undefined || session.platformMode === 'active' || session.platformMode === 'inactive') &&
|
|
546
|
+
(session.activeProductId === undefined || typeof session.activeProductId === 'string') &&
|
|
547
|
+
(session.platformModeStartedAt === undefined || typeof session.platformModeStartedAt === 'string') &&
|
|
548
|
+
(
|
|
549
|
+
session.lastModeReleaseReason === undefined ||
|
|
550
|
+
isPlatformModeReleaseReason(session.lastModeReleaseReason)
|
|
551
|
+
) &&
|
|
552
|
+
(session.lastModeReleasedAt === undefined || typeof session.lastModeReleasedAt === 'string') &&
|
|
553
|
+
(session.poolSession === undefined || isPoolSessionSummary(session.poolSession)) &&
|
|
554
|
+
(session.routeToken === undefined || isLocalRouteTokenState(session.routeToken))
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export async function readPlatformSessionSnapshot(
|
|
559
|
+
options: PlatformSessionOptions = {},
|
|
560
|
+
): Promise<PlatformSessionSnapshot> {
|
|
561
|
+
const sessionFile = resolvePlatformSessionFile(options.sessionFile);
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
const session = JSON.parse(await readFile(sessionFile, 'utf8')) as unknown;
|
|
565
|
+
return isPlatformSession(session) ? { state: 'valid', session } : { state: 'invalid' };
|
|
566
|
+
} catch (error) {
|
|
567
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
568
|
+
return { state: 'missing' };
|
|
569
|
+
}
|
|
570
|
+
if (error instanceof SyntaxError) {
|
|
571
|
+
return { state: 'invalid' };
|
|
572
|
+
}
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export async function readPlatformRouteForwardContext(
|
|
578
|
+
options: PlatformSessionOptions = {},
|
|
579
|
+
): Promise<{
|
|
580
|
+
routeToken?: string;
|
|
581
|
+
poolSessionId?: string;
|
|
582
|
+
}> {
|
|
583
|
+
const snapshot = await readPlatformSessionSnapshot(options);
|
|
584
|
+
if (snapshot.state !== 'valid') {
|
|
585
|
+
return {};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return {
|
|
589
|
+
routeToken: snapshot.session.routeToken?.token,
|
|
590
|
+
poolSessionId: snapshot.session.poolSession?.id,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export async function readPlatformGatewayForwardContext(
|
|
595
|
+
options: PlatformSessionOptions = {},
|
|
596
|
+
): Promise<{
|
|
597
|
+
apiBaseUrl?: string;
|
|
598
|
+
deviceToken?: string;
|
|
599
|
+
routeToken?: string;
|
|
600
|
+
poolSessionId?: string;
|
|
601
|
+
}> {
|
|
602
|
+
const snapshot = await readPlatformSessionSnapshot(options);
|
|
603
|
+
if (snapshot.state !== 'valid') {
|
|
604
|
+
return {};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return {
|
|
608
|
+
apiBaseUrl: snapshot.session.apiBaseUrl,
|
|
609
|
+
deviceToken: snapshot.session.deviceToken,
|
|
610
|
+
routeToken: snapshot.session.routeToken?.token,
|
|
611
|
+
poolSessionId: snapshot.session.poolSession?.id,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async function readPlatformSession(options: PlatformSessionOptions = {}): Promise<PlatformSession | null> {
|
|
616
|
+
const snapshot = await readPlatformSessionSnapshot(options);
|
|
617
|
+
return snapshot.state === 'valid' ? snapshot.session : null;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export async function cachedPlatformStatus(options: PlatformSessionOptions = {}): Promise<PlatformStatus> {
|
|
621
|
+
const session = await readPlatformSession(options);
|
|
622
|
+
if (!session) {
|
|
623
|
+
return { state: 'logged-out' };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return { state: 'offline', user: session.user, device: session.device };
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
async function writePlatformSession(session: PlatformSession, options: PlatformSessionOptions = {}) {
|
|
630
|
+
const sessionFile = resolvePlatformSessionFile(options.sessionFile);
|
|
631
|
+
await mkdir(dirname(sessionFile), { recursive: true, mode: 0o700 });
|
|
632
|
+
await writeFile(sessionFile, `${JSON.stringify(session, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
633
|
+
await chmod(sessionFile, 0o600);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
async function clearPlatformSession(options: PlatformSessionOptions = {}) {
|
|
637
|
+
await rm(resolvePlatformSessionFile(options.sessionFile), { force: true });
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async function requestJson<T>(
|
|
641
|
+
url: string,
|
|
642
|
+
options: { method?: string; body?: Record<string, unknown>; token?: string } = {},
|
|
643
|
+
): Promise<T> {
|
|
644
|
+
const headers: Record<string, string> = {};
|
|
645
|
+
if (options.body) {
|
|
646
|
+
headers['content-type'] = 'application/json';
|
|
647
|
+
}
|
|
648
|
+
if (options.token) {
|
|
649
|
+
headers.authorization = `Bearer ${options.token}`;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const response = await fetch(url, {
|
|
653
|
+
method: options.method ?? 'GET',
|
|
654
|
+
headers,
|
|
655
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
if (!response.ok) {
|
|
659
|
+
const error = new Error(`Request failed with status ${response.status}`) as RequestJsonError;
|
|
660
|
+
error.status = response.status;
|
|
661
|
+
try {
|
|
662
|
+
const body = await response.json() as {
|
|
663
|
+
detail?: unknown;
|
|
664
|
+
};
|
|
665
|
+
const detail = body.detail as { code?: unknown } | undefined;
|
|
666
|
+
if (typeof detail?.code === 'string' && /^[A-Z0-9_:-]{1,96}$/.test(detail.code)) {
|
|
667
|
+
error.platformCode = detail.code;
|
|
668
|
+
}
|
|
669
|
+
} catch {
|
|
670
|
+
// Platform errors may be non-JSON; status is still enough for offline/invalid-token handling.
|
|
671
|
+
}
|
|
672
|
+
throw error;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return (await response.json()) as T;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async function exchangeDeviceToken(request: {
|
|
679
|
+
code: string;
|
|
680
|
+
apiBaseUrl: string;
|
|
681
|
+
device: PlatformDeviceInput;
|
|
682
|
+
}) {
|
|
683
|
+
return requestJson<DeviceTokenResponse>(`${cleanApiBaseUrl(request.apiBaseUrl)}/auth/device-token`, {
|
|
684
|
+
method: 'POST',
|
|
685
|
+
body: {
|
|
686
|
+
code: request.code,
|
|
687
|
+
device: request.device,
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
async function exchangePasswordDeviceToken(request: {
|
|
693
|
+
email: string;
|
|
694
|
+
password: string;
|
|
695
|
+
apiBaseUrl: string;
|
|
696
|
+
device: PlatformDeviceInput;
|
|
697
|
+
}) {
|
|
698
|
+
return requestJson<DeviceTokenResponse>(`${cleanApiBaseUrl(request.apiBaseUrl)}/auth/password-device-token`, {
|
|
699
|
+
method: 'POST',
|
|
700
|
+
body: {
|
|
701
|
+
email: request.email,
|
|
702
|
+
password: request.password,
|
|
703
|
+
device: request.device,
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async function fetchMe(session: PlatformSession) {
|
|
709
|
+
return requestJson<MeResponse>(`${cleanApiBaseUrl(session.apiBaseUrl)}/me`, {
|
|
710
|
+
token: session.deviceToken,
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async function fetchAccountSummary(session: PlatformSession) {
|
|
715
|
+
return requestJson<AccountSummaryResponse>(`${cleanApiBaseUrl(session.apiBaseUrl)}/account/summary`, {
|
|
716
|
+
token: session.deviceToken,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
async function fetchProducts(session: PlatformSession) {
|
|
721
|
+
const response = await requestJson<ProductsResponse>(`${cleanApiBaseUrl(session.apiBaseUrl)}/products`, {
|
|
722
|
+
token: session.deviceToken,
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
return {
|
|
726
|
+
products: Array.isArray(response.products)
|
|
727
|
+
? response.products.flatMap((product) => {
|
|
728
|
+
const sanitized = sanitizeProductSummary(product);
|
|
729
|
+
return sanitized ? [sanitized] : [];
|
|
730
|
+
})
|
|
731
|
+
: [],
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
async function postHeartbeat(request: { session: PlatformSession; payload: HeartbeatPayload }) {
|
|
736
|
+
return requestJson<HeartbeatResponse>(`${cleanApiBaseUrl(request.session.apiBaseUrl)}/devices/heartbeat`, {
|
|
737
|
+
method: 'POST',
|
|
738
|
+
body: request.payload,
|
|
739
|
+
token: request.session.deviceToken,
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async function postLogout(request: { session: PlatformSession }) {
|
|
744
|
+
await requestJson(`${cleanApiBaseUrl(request.session.apiBaseUrl)}/auth/logout`, {
|
|
745
|
+
method: 'POST',
|
|
746
|
+
token: request.session.deviceToken,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
async function startPoolSession(request: StartPoolSessionRequest): Promise<StartPoolSessionResult> {
|
|
751
|
+
return requestJson<StartPoolSessionResult>(
|
|
752
|
+
`${cleanApiBaseUrl(request.session.apiBaseUrl)}/pool-sessions/start`,
|
|
753
|
+
{
|
|
754
|
+
method: 'POST',
|
|
755
|
+
token: request.session.deviceToken,
|
|
756
|
+
body: {
|
|
757
|
+
productId: request.productId,
|
|
758
|
+
...(request.client ? { client: request.client } : {}),
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
async function stopPoolSession(request: StopPoolSessionRequest): Promise<StopPoolSessionResult> {
|
|
765
|
+
return requestJson<StopPoolSessionResult>(
|
|
766
|
+
`${cleanApiBaseUrl(request.session.apiBaseUrl)}/pool-sessions/${request.poolSessionId}/stop`,
|
|
767
|
+
{
|
|
768
|
+
method: 'POST',
|
|
769
|
+
token: request.session.deviceToken,
|
|
770
|
+
body: {
|
|
771
|
+
reason: request.reason,
|
|
772
|
+
},
|
|
773
|
+
},
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function poolFailureToModeResult(
|
|
778
|
+
reason: PoolSessionFailureReason,
|
|
779
|
+
product: PlatformProductSummary,
|
|
780
|
+
currentCredits: number,
|
|
781
|
+
): PlatformModeResult {
|
|
782
|
+
if (reason === 'INSUFFICIENT_CREDITS') {
|
|
783
|
+
return {
|
|
784
|
+
state: 'insufficient-credits',
|
|
785
|
+
productId: product.id,
|
|
786
|
+
requiredCredits: product.minCredits,
|
|
787
|
+
currentCredits,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
if (reason === 'DEVICE_INVALID') {
|
|
791
|
+
return { state: 'device-inactive' };
|
|
792
|
+
}
|
|
793
|
+
if (reason === 'PRODUCT_UNAVAILABLE') {
|
|
794
|
+
return { state: 'product-unavailable', productId: product.id };
|
|
795
|
+
}
|
|
796
|
+
if (reason === 'PROVIDER_UNAVAILABLE') {
|
|
797
|
+
return { state: 'provider-unavailable' };
|
|
798
|
+
}
|
|
799
|
+
if (reason === 'RATE_LIMITED') {
|
|
800
|
+
return { state: 'provider-rate-limited' };
|
|
801
|
+
}
|
|
802
|
+
if (reason === 'MODEL_BANNED') {
|
|
803
|
+
return { state: 'model-banned' };
|
|
804
|
+
}
|
|
805
|
+
return { state: 'manual-review-required' };
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async function statusFromRequestError(
|
|
809
|
+
error: unknown,
|
|
810
|
+
session: PlatformSession,
|
|
811
|
+
options: RequestErrorStatusOptions = {},
|
|
812
|
+
): Promise<PlatformStatus> {
|
|
813
|
+
if ((error as RequestJsonError).status === 401) {
|
|
814
|
+
const currentSession = await readPlatformSession(options) ?? session;
|
|
815
|
+
const { mode } = options.releaseActiveModeOnUnauthorized
|
|
816
|
+
? await releaseActivePlatformMode(currentSession, 'invalid-token', options)
|
|
817
|
+
: { mode: platformModeFromSession(currentSession) };
|
|
818
|
+
return { state: 'invalid-token', user: currentSession.user, device: currentSession.device, mode };
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return { state: 'offline', user: session.user, device: session.device, mode: platformModeFromSession(session) };
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
export async function loginWithCode(options: LoginWithCodeOptions): Promise<PlatformStatus> {
|
|
825
|
+
const tokenResponse = await (options.exchangeDeviceToken ?? exchangeDeviceToken)({
|
|
826
|
+
code: options.code,
|
|
827
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
828
|
+
device: options.device ?? {},
|
|
829
|
+
});
|
|
830
|
+
const session: PlatformSession = {
|
|
831
|
+
apiBaseUrl: cleanApiBaseUrl(options.apiBaseUrl),
|
|
832
|
+
deviceToken: tokenResponse.deviceToken,
|
|
833
|
+
createdAt: new Date().toISOString(),
|
|
834
|
+
user: tokenResponse.user,
|
|
835
|
+
device: tokenResponse.device,
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
await writePlatformSession(session, { sessionFile: options.sessionFile });
|
|
839
|
+
return asStatus(session);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
export async function loginWithPassword(options: LoginWithPasswordOptions): Promise<PlatformStatus> {
|
|
843
|
+
const tokenResponse = await (options.exchangePasswordDeviceToken ?? exchangePasswordDeviceToken)({
|
|
844
|
+
email: options.email,
|
|
845
|
+
password: options.password,
|
|
846
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
847
|
+
device: options.device ?? {},
|
|
848
|
+
});
|
|
849
|
+
const session: PlatformSession = {
|
|
850
|
+
apiBaseUrl: cleanApiBaseUrl(options.apiBaseUrl),
|
|
851
|
+
deviceToken: tokenResponse.deviceToken,
|
|
852
|
+
createdAt: new Date().toISOString(),
|
|
853
|
+
user: tokenResponse.user,
|
|
854
|
+
device: tokenResponse.device,
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
await writePlatformSession(session, { sessionFile: options.sessionFile });
|
|
858
|
+
return asStatus(session);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
export async function platformStatus(options: PlatformSessionOptions = {}): Promise<PlatformStatus> {
|
|
862
|
+
const session = await readPlatformSession(options);
|
|
863
|
+
if (!session) {
|
|
864
|
+
return { state: 'logged-out' };
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
let me: MeResponse;
|
|
868
|
+
try {
|
|
869
|
+
me = await fetchMe(session);
|
|
870
|
+
} catch (error) {
|
|
871
|
+
return statusFromRequestError(error, session, {
|
|
872
|
+
...options,
|
|
873
|
+
releaseActiveModeOnUnauthorized: true,
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const currentSession = await readPlatformSession(options);
|
|
878
|
+
if (!currentSession) {
|
|
879
|
+
return { state: 'logged-out' };
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const updatedSession = { ...currentSession, user: me.user, device: me.device };
|
|
883
|
+
await writePlatformSession(updatedSession, options);
|
|
884
|
+
const released = await releaseForInactiveDevice(updatedSession, options);
|
|
885
|
+
return {
|
|
886
|
+
state: 'logged-in',
|
|
887
|
+
user: released.session.user,
|
|
888
|
+
device: released.session.device,
|
|
889
|
+
mode: released.mode,
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
export async function platformCatalog(options: PlatformSessionOptions = {}): Promise<PlatformCatalog> {
|
|
894
|
+
const session = await readPlatformSession(options);
|
|
895
|
+
if (!session) {
|
|
896
|
+
return { state: 'logged-out', products: [] };
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const [account, products] = await Promise.allSettled([
|
|
900
|
+
fetchAccountSummary(session),
|
|
901
|
+
fetchProducts(session),
|
|
902
|
+
]);
|
|
903
|
+
|
|
904
|
+
const failures = [account, products].filter((result) => result.status === 'rejected');
|
|
905
|
+
if (failures.some((result) => (result.reason as RequestJsonError).status === 401)) {
|
|
906
|
+
const { mode } = await releaseActivePlatformMode(session, 'invalid-token', options);
|
|
907
|
+
return { state: 'invalid-token', mode, products: [] };
|
|
908
|
+
}
|
|
909
|
+
if (failures.length > 0) {
|
|
910
|
+
return { state: 'offline', products: [] };
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const catalogProducts = products.value.products;
|
|
914
|
+
const selectedProductId = catalogProducts.some((product) => product.id === session.selectedProductId)
|
|
915
|
+
? session.selectedProductId
|
|
916
|
+
: undefined;
|
|
917
|
+
let updatedSession = session;
|
|
918
|
+
let mode = platformModeFromSession(session);
|
|
919
|
+
let shouldWriteSession = false;
|
|
920
|
+
if (session.selectedProductId && !selectedProductId) {
|
|
921
|
+
const { selectedProductId: _staleSelection, ...sessionWithoutStaleSelection } = session;
|
|
922
|
+
if (mode.state === 'active') {
|
|
923
|
+
const released = releasePlatformMode(sessionWithoutStaleSelection, 'product-missing');
|
|
924
|
+
updatedSession = released.session;
|
|
925
|
+
mode = released.mode;
|
|
926
|
+
} else {
|
|
927
|
+
updatedSession = clearPlatformMode(sessionWithoutStaleSelection);
|
|
928
|
+
mode = { state: 'inactive' };
|
|
929
|
+
}
|
|
930
|
+
shouldWriteSession = true;
|
|
931
|
+
} else if (mode.state === 'active') {
|
|
932
|
+
const activeProduct = catalogProducts.find((product) => product.id === mode.productId);
|
|
933
|
+
const releaseReason = !activeProduct
|
|
934
|
+
? 'product-missing'
|
|
935
|
+
: activeProduct.status !== 'available'
|
|
936
|
+
? 'product-unavailable'
|
|
937
|
+
: account.value.credits < activeProduct.minCredits
|
|
938
|
+
? 'insufficient-credits'
|
|
939
|
+
: undefined;
|
|
940
|
+
if (releaseReason) {
|
|
941
|
+
const released = releasePlatformMode(updatedSession, releaseReason);
|
|
942
|
+
updatedSession = released.session;
|
|
943
|
+
mode = released.mode;
|
|
944
|
+
shouldWriteSession = true;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (shouldWriteSession) {
|
|
949
|
+
await writePlatformSession(updatedSession, options);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
return {
|
|
953
|
+
state: 'logged-in',
|
|
954
|
+
account: { credits: account.value.credits },
|
|
955
|
+
mode,
|
|
956
|
+
...(selectedProductId ? { selectedProductId } : {}),
|
|
957
|
+
products: catalogProducts,
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
export async function selectPlatformProduct(
|
|
962
|
+
options: PlatformSessionOptions & { productId: string },
|
|
963
|
+
): Promise<PlatformSelectionResult> {
|
|
964
|
+
const session = await readPlatformSession(options);
|
|
965
|
+
if (!session) {
|
|
966
|
+
return { state: 'logged-out' };
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const catalog = await platformCatalog(options);
|
|
970
|
+
if (catalog.state === 'invalid-token' || catalog.state === 'offline') {
|
|
971
|
+
return { state: catalog.state };
|
|
972
|
+
}
|
|
973
|
+
if (catalog.state === 'logged-out') {
|
|
974
|
+
return { state: 'logged-out' };
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const currentSession = await readPlatformSession(options);
|
|
978
|
+
if (!currentSession) {
|
|
979
|
+
return { state: 'logged-out' };
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const product = catalog.products.find((candidate) => candidate.id === options.productId);
|
|
983
|
+
if (!product) {
|
|
984
|
+
return { state: 'product-not-found', productId: options.productId };
|
|
985
|
+
}
|
|
986
|
+
if (product.status !== 'available') {
|
|
987
|
+
return { state: 'product-unavailable', productId: options.productId };
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
await writePlatformSession({ ...currentSession, selectedProductId: options.productId }, options);
|
|
991
|
+
return { state: 'selected', selectedProductId: options.productId };
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
export async function startPlatformMode(options: StartPlatformModeOptions = {}): Promise<PlatformModeResult> {
|
|
995
|
+
const session = await readPlatformSession(options);
|
|
996
|
+
if (!session) {
|
|
997
|
+
return { state: 'logged-out' };
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const catalog = await platformCatalog(options);
|
|
1001
|
+
if (catalog.state === 'logged-out' || catalog.state === 'offline' || catalog.state === 'invalid-token') {
|
|
1002
|
+
return { state: catalog.state };
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const currentSession = await readPlatformSession(options);
|
|
1006
|
+
if (!currentSession) {
|
|
1007
|
+
return { state: 'logged-out' };
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (!currentSession.selectedProductId) {
|
|
1011
|
+
return { state: 'product-not-selected' };
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const product = catalog.products.find((candidate) => candidate.id === currentSession.selectedProductId);
|
|
1015
|
+
if (!product) {
|
|
1016
|
+
return { state: 'product-not-found', productId: currentSession.selectedProductId };
|
|
1017
|
+
}
|
|
1018
|
+
if (product.status !== 'available') {
|
|
1019
|
+
return { state: 'product-unavailable', productId: product.id };
|
|
1020
|
+
}
|
|
1021
|
+
if (catalog.account.credits < product.minCredits) {
|
|
1022
|
+
return {
|
|
1023
|
+
state: 'insufficient-credits',
|
|
1024
|
+
productId: product.id,
|
|
1025
|
+
requiredCredits: product.minCredits,
|
|
1026
|
+
currentCredits: catalog.account.credits,
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
let pool: StartPoolSessionResult;
|
|
1031
|
+
try {
|
|
1032
|
+
pool = await (options.startPoolSession ?? startPoolSession)({
|
|
1033
|
+
session: currentSession,
|
|
1034
|
+
productId: product.id,
|
|
1035
|
+
});
|
|
1036
|
+
} catch {
|
|
1037
|
+
return { state: 'offline' };
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
if (pool.state === 'failed') {
|
|
1041
|
+
return poolFailureToModeResult(pool.reason, product, catalog.account.credits);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const startedAt = new Date().toISOString();
|
|
1045
|
+
await writePlatformSession({
|
|
1046
|
+
...clearModeRelease(currentSession),
|
|
1047
|
+
platformMode: 'active',
|
|
1048
|
+
activeProductId: product.id,
|
|
1049
|
+
platformModeStartedAt: startedAt,
|
|
1050
|
+
poolSession: sanitizePoolSessionSummary(pool.session),
|
|
1051
|
+
routeToken: {
|
|
1052
|
+
token: pool.routeToken,
|
|
1053
|
+
expiresAt: pool.session.routeTokenExpiresAt,
|
|
1054
|
+
},
|
|
1055
|
+
}, options);
|
|
1056
|
+
|
|
1057
|
+
return { state: 'active', productId: product.id, startedAt };
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
export async function refreshPlatformRoute(
|
|
1061
|
+
options: StartPlatformModeOptions = {},
|
|
1062
|
+
): Promise<RefreshPlatformRouteResult> {
|
|
1063
|
+
const session = await readPlatformSession(options);
|
|
1064
|
+
if (
|
|
1065
|
+
!session ||
|
|
1066
|
+
session.device.status !== 'active' ||
|
|
1067
|
+
session.platformMode !== 'active' ||
|
|
1068
|
+
!session.activeProductId
|
|
1069
|
+
) {
|
|
1070
|
+
return { state: 'not-refreshable' };
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
let pool: StartPoolSessionResult;
|
|
1074
|
+
try {
|
|
1075
|
+
pool = await (options.startPoolSession ?? startPoolSession)({
|
|
1076
|
+
session,
|
|
1077
|
+
productId: session.activeProductId,
|
|
1078
|
+
});
|
|
1079
|
+
} catch {
|
|
1080
|
+
return { state: 'failed', reason: 'offline' };
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if (pool.state === 'failed') {
|
|
1084
|
+
return { state: 'failed', reason: poolFailureToModeResult(pool.reason, {
|
|
1085
|
+
id: session.activeProductId,
|
|
1086
|
+
name: session.activeProductId,
|
|
1087
|
+
description: '',
|
|
1088
|
+
status: 'available',
|
|
1089
|
+
minCredits: 0,
|
|
1090
|
+
usageLabel: '',
|
|
1091
|
+
}, 0).state };
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
const startedAt = new Date().toISOString();
|
|
1095
|
+
await writePlatformSession({
|
|
1096
|
+
...clearModeRelease(session),
|
|
1097
|
+
platformMode: 'active',
|
|
1098
|
+
activeProductId: session.activeProductId,
|
|
1099
|
+
platformModeStartedAt: startedAt,
|
|
1100
|
+
poolSession: sanitizePoolSessionSummary(pool.session),
|
|
1101
|
+
routeToken: {
|
|
1102
|
+
token: pool.routeToken,
|
|
1103
|
+
expiresAt: pool.session.routeTokenExpiresAt,
|
|
1104
|
+
},
|
|
1105
|
+
}, options);
|
|
1106
|
+
|
|
1107
|
+
return { state: 'active', productId: session.activeProductId, startedAt };
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
export async function stopPlatformMode(options: StopPlatformModeOptions = {}): Promise<PlatformModeSnapshot> {
|
|
1111
|
+
const session = await readPlatformSession(options);
|
|
1112
|
+
if (session) {
|
|
1113
|
+
if (session.poolSession) {
|
|
1114
|
+
try {
|
|
1115
|
+
await (options.stopPoolSession ?? stopPoolSession)({
|
|
1116
|
+
session,
|
|
1117
|
+
poolSessionId: session.poolSession.id,
|
|
1118
|
+
reason: 'user-stop',
|
|
1119
|
+
});
|
|
1120
|
+
} catch {
|
|
1121
|
+
// Local stop must clear route capability even when the platform API is unreachable.
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
await writePlatformSession(clearPlatformMode(session), options);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
return { state: 'inactive' };
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
export async function sendHeartbeat(options: SendHeartbeatOptions = {}): Promise<PlatformStatus> {
|
|
1132
|
+
const session = await readPlatformSession(options);
|
|
1133
|
+
if (!session) {
|
|
1134
|
+
return { state: 'logged-out' };
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
let heartbeat: HeartbeatResponse;
|
|
1138
|
+
try {
|
|
1139
|
+
heartbeat = await (options.postHeartbeat ?? postHeartbeat)({
|
|
1140
|
+
session,
|
|
1141
|
+
payload: options.payload ?? {},
|
|
1142
|
+
});
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
return statusFromRequestError(error, session, {
|
|
1145
|
+
...options,
|
|
1146
|
+
releaseActiveModeOnUnauthorized: true,
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const currentSession = await readPlatformSession(options);
|
|
1151
|
+
if (!currentSession) {
|
|
1152
|
+
return { state: 'logged-out' };
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const updatedSession = { ...currentSession, device: heartbeat.device };
|
|
1156
|
+
await writePlatformSession(updatedSession, options);
|
|
1157
|
+
const released = await releaseForInactiveDevice(updatedSession, options);
|
|
1158
|
+
return {
|
|
1159
|
+
state: 'logged-in',
|
|
1160
|
+
user: released.session.user,
|
|
1161
|
+
device: released.session.device,
|
|
1162
|
+
mode: released.mode,
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
export async function logoutPlatform(options: LogoutPlatformOptions = {}): Promise<PlatformStatus> {
|
|
1167
|
+
const session = await readPlatformSession(options);
|
|
1168
|
+
if (session) {
|
|
1169
|
+
try {
|
|
1170
|
+
await (options.postLogout ?? postLogout)({ session });
|
|
1171
|
+
} catch {
|
|
1172
|
+
// Local logout must succeed even when the platform API is unreachable.
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
await clearPlatformSession(options);
|
|
1177
|
+
return { state: 'logged-out' };
|
|
1178
|
+
}
|