orizu 0.5.49 → 0.5.50
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/account-directory.d.ts +2 -2
- package/dist/account-directory.d.ts.map +1 -1
- package/dist/account-directory.js +7 -32
- package/dist/auth-commands.d.ts.map +1 -1
- package/dist/auth-commands.js +22 -30
- package/dist/credentials.d.ts +5 -0
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +84 -3
- package/dist/dataset-transfer-upload.d.ts.map +1 -1
- package/dist/dataset-transfer-upload.js +6 -1
- package/dist/directory-list-read.d.ts +37 -0
- package/dist/directory-list-read.d.ts.map +1 -0
- package/dist/directory-list-read.js +87 -0
- package/dist/hosted-boot.d.ts.map +1 -1
- package/dist/hosted-boot.js +59 -41
- package/dist/hosted-harness-opencode.d.ts +31 -0
- package/dist/hosted-harness-opencode.d.ts.map +1 -1
- package/dist/hosted-harness-opencode.js +45 -7
- package/dist/hosted-loop.d.ts +39 -2
- package/dist/hosted-loop.d.ts.map +1 -1
- package/dist/hosted-loop.js +127 -18
- package/dist/hosted-session-seed.d.ts +2 -0
- package/dist/hosted-session-seed.d.ts.map +1 -1
- package/dist/hosted-session-seed.js +27 -10
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +39 -234
- package/dist/keyboard-select.d.ts.map +1 -1
- package/dist/keyboard-select.js +23 -45
- package/dist/project-create-workflow.d.ts +18 -0
- package/dist/project-create-workflow.d.ts.map +1 -0
- package/dist/project-create-workflow.js +121 -0
- package/dist/self-update-command.d.ts +8 -0
- package/dist/self-update-command.d.ts.map +1 -0
- package/dist/self-update-command.js +390 -0
- package/dist/self-update.d.ts +29 -16
- package/dist/self-update.d.ts.map +1 -1
- package/dist/self-update.js +55 -244
- package/dist/setup-account.d.ts.map +1 -1
- package/dist/setup-account.js +4 -1
- package/dist/setup-and-skills-commands.d.ts.map +1 -1
- package/dist/setup-and-skills-commands.js +88 -102
- package/dist/skill-install-workflow.d.ts +23 -0
- package/dist/skill-install-workflow.d.ts.map +1 -0
- package/dist/skill-install-workflow.js +184 -0
- package/dist/skill-installer.d.ts +8 -3
- package/dist/skill-installer.d.ts.map +1 -1
- package/dist/skill-installer.js +26 -33
- package/dist/stored-session-requests.d.ts +27 -0
- package/dist/stored-session-requests.d.ts.map +1 -0
- package/dist/stored-session-requests.js +264 -0
- package/dist/terminal-key-session.d.ts +23 -0
- package/dist/terminal-key-session.d.ts.map +1 -0
- package/dist/terminal-key-session.js +74 -0
- package/package.json +1 -1
- package/src/account-directory.ts +8 -42
- package/src/auth-commands.ts +20 -29
- package/src/credentials.ts +83 -3
- package/src/dataset-transfer-upload.ts +7 -1
- package/src/directory-list-read.ts +123 -0
- package/src/hosted-boot.ts +65 -43
- package/src/hosted-harness-opencode.ts +65 -9
- package/src/hosted-loop.ts +152 -18
- package/src/hosted-session-seed.ts +25 -8
- package/src/http.ts +39 -317
- package/src/keyboard-select.ts +18 -55
- package/src/project-create-workflow.ts +139 -0
- package/src/self-update-command.ts +419 -0
- package/src/self-update.ts +70 -266
- package/src/setup-account.ts +4 -1
- package/src/setup-and-skills-commands.ts +89 -121
- package/src/skill-install-workflow.ts +215 -0
- package/src/skill-installer.ts +31 -38
- package/src/stored-session-requests.ts +316 -0
- package/src/terminal-key-session.ts +88 -0
package/src/http.ts
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
-
clearServerCredentialsIfCurrent,
|
|
3
|
-
credentialsEqual,
|
|
4
2
|
resolveCredentialServer,
|
|
5
3
|
getActiveBaseUrl,
|
|
6
4
|
getServerCredentials,
|
|
7
5
|
rememberResolvedBearer,
|
|
8
6
|
resolveEnvBearerToken,
|
|
9
|
-
updateServerCredentialsIfCurrent,
|
|
10
|
-
withCredentialsTransactionLockAsync,
|
|
11
7
|
} from './credentials.js'
|
|
12
8
|
import { canonicalServerUrl, getFlagBaseUrl, GlobalFlags, normalizeBaseUrl } from './global-flags.js'
|
|
13
|
-
import {
|
|
14
|
-
describeLogoutHttpFailure,
|
|
15
|
-
describeLogoutTransportFailure,
|
|
16
|
-
} from './logout-diagnostic.js'
|
|
17
|
-
import { LoginResponse, ServerCredentials, SessionServerCredentials } from './types.js'
|
|
9
|
+
import { ServerCredentials } from './types.js'
|
|
18
10
|
|
|
19
11
|
let runtimeFlags: GlobalFlags = { local: false, server: null }
|
|
20
12
|
let lastResolvedBaseUrl: string | null = null
|
|
@@ -125,199 +117,11 @@ export function assertSecureTokenTransport(baseUrl: string) {
|
|
|
125
117
|
)
|
|
126
118
|
}
|
|
127
119
|
|
|
128
|
-
function isSessionCredentials(credentials: ServerCredentials): credentials is SessionServerCredentials {
|
|
129
|
-
return 'accessToken' in credentials
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function getAuthorizationToken(credentials: ServerCredentials): string {
|
|
133
|
-
return isSessionCredentials(credentials)
|
|
134
|
-
? rememberSessionCredentials(credentials).accessToken
|
|
135
|
-
: rememberResolvedBearer(credentials.apiKey)
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function rememberSessionCredentials(
|
|
139
|
-
credentials: SessionServerCredentials
|
|
140
|
-
): SessionServerCredentials {
|
|
141
|
-
rememberResolvedBearer(credentials.accessToken)
|
|
142
|
-
rememberResolvedBearer(credentials.refreshToken)
|
|
143
|
-
return credentials
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function isExpired(expiresAt: number): boolean {
|
|
147
|
-
const nowUnix = Math.floor(Date.now() / 1000)
|
|
148
|
-
return expiresAt <= nowUnix + 30
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const DEFAULT_CREDENTIALS_REFRESH_TIMEOUT_MS = 10_000
|
|
152
|
-
const MAX_CREDENTIALS_REFRESH_TIMEOUT_MS = 30_000
|
|
153
|
-
const DEFAULT_LOGOUT_TIMEOUT_MS = 10_000
|
|
154
|
-
const MAX_LOGOUT_TIMEOUT_MS = 30_000
|
|
155
|
-
|
|
156
|
-
function getCredentialsRefreshTimeoutMs(): number {
|
|
157
|
-
const configured = Number(process.env.ORIZU_CREDENTIALS_REFRESH_TIMEOUT_MS)
|
|
158
|
-
if (!Number.isSafeInteger(configured) || configured <= 0) {
|
|
159
|
-
return DEFAULT_CREDENTIALS_REFRESH_TIMEOUT_MS
|
|
160
|
-
}
|
|
161
|
-
return Math.min(configured, MAX_CREDENTIALS_REFRESH_TIMEOUT_MS)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function getLogoutTimeoutMs(): number {
|
|
165
|
-
const configured = Number(process.env.ORIZU_LOGOUT_TIMEOUT_MS)
|
|
166
|
-
if (!Number.isSafeInteger(configured) || configured <= 0) {
|
|
167
|
-
return DEFAULT_LOGOUT_TIMEOUT_MS
|
|
168
|
-
}
|
|
169
|
-
return Math.min(configured, MAX_LOGOUT_TIMEOUT_MS)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async function withAbortDeadline<T>(
|
|
173
|
-
operation: (signal: AbortSignal) => Promise<T>,
|
|
174
|
-
callerSignal: AbortSignal | null | undefined,
|
|
175
|
-
timeoutMs: number,
|
|
176
|
-
timeoutError: Error
|
|
177
|
-
): Promise<T> {
|
|
178
|
-
const controller = new AbortController()
|
|
179
|
-
const handleCallerAbort = () => controller.abort(callerSignal?.reason)
|
|
180
|
-
if (callerSignal?.aborted) handleCallerAbort()
|
|
181
|
-
else callerSignal?.addEventListener('abort', handleCallerAbort, { once: true })
|
|
182
|
-
|
|
183
|
-
const timer = setTimeout(() => controller.abort(timeoutError), timeoutMs)
|
|
184
|
-
const cancellation = new Promise<never>((_resolve, reject) => {
|
|
185
|
-
const handleAbort = () => reject(controller.signal.reason ?? new Error('Request aborted.'))
|
|
186
|
-
if (controller.signal.aborted) handleAbort()
|
|
187
|
-
else controller.signal.addEventListener('abort', handleAbort, { once: true })
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
return await Promise.race([operation(controller.signal), cancellation])
|
|
192
|
-
} finally {
|
|
193
|
-
clearTimeout(timer)
|
|
194
|
-
callerSignal?.removeEventListener('abort', handleCallerAbort)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
async function withCredentialsRefreshDeadline<T>(
|
|
199
|
-
operation: (signal: AbortSignal) => Promise<T>,
|
|
200
|
-
callerSignal?: AbortSignal | null
|
|
201
|
-
): Promise<T> {
|
|
202
|
-
const timeoutMs = getCredentialsRefreshTimeoutMs()
|
|
203
|
-
const timeoutError = new Error(
|
|
204
|
-
`ORIZU_CREDENTIALS_REFRESH_TIMEOUT: session refresh exceeded ${timeoutMs}ms.`
|
|
205
|
-
)
|
|
206
|
-
return await withAbortDeadline(operation, callerSignal, timeoutMs, timeoutError)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async function withLogoutDeadline<T>(
|
|
210
|
-
operation: (signal: AbortSignal) => Promise<T>,
|
|
211
|
-
callerSignal?: AbortSignal | null
|
|
212
|
-
): Promise<T> {
|
|
213
|
-
const timeoutMs = getLogoutTimeoutMs()
|
|
214
|
-
const timeoutError = new Error(
|
|
215
|
-
`ORIZU_LOGOUT_TIMEOUT: remote logout exceeded ${timeoutMs}ms.`
|
|
216
|
-
)
|
|
217
|
-
timeoutError.name = 'TimeoutError'
|
|
218
|
-
return await withAbortDeadline(operation, callerSignal, timeoutMs, timeoutError)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
120
|
export function credentialRequestRedirectPolicy(): Pick<RequestInit, 'redirect'> {
|
|
222
121
|
return process.env.ORIZU_CLI_AUTH_REDIRECT === 'error' ? { redirect: 'error' } : {}
|
|
223
122
|
}
|
|
224
123
|
|
|
225
|
-
|
|
226
|
-
baseUrl: string,
|
|
227
|
-
credentials: ServerCredentials,
|
|
228
|
-
callerSignal?: AbortSignal | null
|
|
229
|
-
): Promise<ServerCredentials> {
|
|
230
|
-
if (!isSessionCredentials(credentials)) {
|
|
231
|
-
throw new Error('API key credentials do not refresh. Run `orizu login` again if access fails.')
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
assertSecureTokenTransport(baseUrl)
|
|
235
|
-
const refreshToken = rememberResolvedBearer(credentials.refreshToken)
|
|
236
|
-
const data = await withCredentialsRefreshDeadline(async signal => {
|
|
237
|
-
const response = await fetch(`${baseUrl}/api/cli/auth/refresh`, {
|
|
238
|
-
method: 'POST',
|
|
239
|
-
headers: { 'Content-Type': 'application/json' },
|
|
240
|
-
body: JSON.stringify({ refreshToken }),
|
|
241
|
-
...credentialRequestRedirectPolicy(),
|
|
242
|
-
signal,
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
if (!response.ok) {
|
|
246
|
-
throw new Error('Session expired. Run `orizu login` again.')
|
|
247
|
-
}
|
|
248
|
-
return await response.json() as LoginResponse
|
|
249
|
-
}, callerSignal)
|
|
250
|
-
if (!data.accessToken || !data.refreshToken || !data.expiresAt) {
|
|
251
|
-
throw new Error('Server returned invalid refresh credentials. Run `orizu login` again.')
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const refreshed = rememberSessionCredentials({
|
|
255
|
-
credentialType: 'session' as const,
|
|
256
|
-
accessToken: data.accessToken,
|
|
257
|
-
refreshToken: data.refreshToken,
|
|
258
|
-
expiresAt: data.expiresAt,
|
|
259
|
-
})
|
|
260
|
-
if (isExpired(refreshed.expiresAt)) {
|
|
261
|
-
throw new Error(
|
|
262
|
-
'ORIZU_CREDENTIALS_REFRESH_UNUSABLE: refreshed session expires within the 30-second safety window.'
|
|
263
|
-
)
|
|
264
|
-
}
|
|
265
|
-
return refreshed
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
interface StoredRefreshResult {
|
|
269
|
-
status: 'refreshed' | 'changed'
|
|
270
|
-
credentials: ServerCredentials | null
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
async function refreshStoredCredentialsIfCurrent(
|
|
274
|
-
baseUrl: string,
|
|
275
|
-
expected: ServerCredentials,
|
|
276
|
-
callerSignal?: AbortSignal | null
|
|
277
|
-
): Promise<StoredRefreshResult> {
|
|
278
|
-
return await withCredentialsTransactionLockAsync(async () => {
|
|
279
|
-
const current = getServerCredentials(baseUrl)
|
|
280
|
-
if (!current || !credentialsEqual(current, expected)) {
|
|
281
|
-
return { status: 'changed', credentials: current }
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const refreshed = await refreshCredentials(baseUrl, expected, callerSignal)
|
|
285
|
-
if (!updateServerCredentialsIfCurrent(baseUrl, expected, refreshed)) {
|
|
286
|
-
return { status: 'changed', credentials: getServerCredentials(baseUrl) }
|
|
287
|
-
}
|
|
288
|
-
return { status: 'refreshed', credentials: refreshed }
|
|
289
|
-
})
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function requireCurrentCredentials(baseUrl: string, credentials: ServerCredentials | null): ServerCredentials {
|
|
293
|
-
if (credentials) return credentials
|
|
294
|
-
throw new Error(`Not logged in for ${baseUrl}. Run \`orizu login --server ${baseUrl}\` (or \`--local\`) first.`)
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
interface ResolvedStoredCredentials {
|
|
298
|
-
credentials: ServerCredentials
|
|
299
|
-
didRefresh: boolean
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
async function refreshExpiredStoredCredentials(
|
|
303
|
-
baseUrl: string,
|
|
304
|
-
initial: ServerCredentials,
|
|
305
|
-
callerSignal?: AbortSignal | null
|
|
306
|
-
): Promise<ResolvedStoredCredentials> {
|
|
307
|
-
if (!isSessionCredentials(initial) || !isExpired(initial.expiresAt)) {
|
|
308
|
-
return { credentials: initial, didRefresh: false }
|
|
309
|
-
}
|
|
310
|
-
const result = await refreshStoredCredentialsIfCurrent(baseUrl, initial, callerSignal)
|
|
311
|
-
if (result.status === 'changed') {
|
|
312
|
-
throw new Error(
|
|
313
|
-
'ORIZU_AUTH_CONTEXT_CHANGED: stored credentials changed before this request began; the request was not sent.'
|
|
314
|
-
)
|
|
315
|
-
}
|
|
316
|
-
return {
|
|
317
|
-
credentials: requireCurrentCredentials(baseUrl, result.credentials),
|
|
318
|
-
didRefresh: true,
|
|
319
|
-
}
|
|
320
|
-
}
|
|
124
|
+
const storedSessionRequests = () => import('./stored-session-requests.js')
|
|
321
125
|
|
|
322
126
|
export type AuthenticatedRequestSource = 'ORIZU_TOKEN' | 'ORIZU_TOKEN_FILE' | 'stored'
|
|
323
127
|
|
|
@@ -334,6 +138,10 @@ export interface LogoutResult {
|
|
|
334
138
|
localCredentialCleared: boolean
|
|
335
139
|
}
|
|
336
140
|
|
|
141
|
+
function notLoggedIn(baseUrl: string): Error {
|
|
142
|
+
return new Error(`Not logged in for ${baseUrl}. Run \`orizu login --server ${baseUrl}\` (or \`--local\`) first.`)
|
|
143
|
+
}
|
|
144
|
+
|
|
337
145
|
/**
|
|
338
146
|
* Captures one authenticated server context for a multi-step operation. The
|
|
339
147
|
* returned interface intentionally exposes no credential material. Its closure
|
|
@@ -387,95 +195,42 @@ export function captureAuthenticatedRequestContext(
|
|
|
387
195
|
}
|
|
388
196
|
|
|
389
197
|
const capturedCredentials = getServerCredentials(baseUrl)
|
|
390
|
-
if (!capturedCredentials)
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
-
void getAuthorizationToken(capturedCredentials)
|
|
198
|
+
if (!capturedCredentials) throw notLoggedIn(baseUrl)
|
|
199
|
+
rememberStoredBearers(capturedCredentials)
|
|
394
200
|
let activeCredentials = capturedCredentials
|
|
395
|
-
|
|
396
|
-
async function refreshCapturedCredentials(callerSignal?: AbortSignal | null): Promise<void> {
|
|
397
|
-
const result = await refreshStoredCredentialsIfCurrent(baseUrl, activeCredentials, callerSignal)
|
|
398
|
-
if (result.status === 'changed') {
|
|
399
|
-
throw new Error(
|
|
400
|
-
'ORIZU_CREDENTIALS_CHANGED: stored credentials changed before the captured session could refresh. Reconfirm the account and retry.'
|
|
401
|
-
)
|
|
402
|
-
}
|
|
403
|
-
activeCredentials = requireCurrentCredentials(baseUrl, result.credentials)
|
|
404
|
-
}
|
|
201
|
+
const changed = 'ORIZU_CREDENTIALS_CHANGED: stored credentials changed before the captured session could refresh. Reconfirm the account and retry.'
|
|
405
202
|
|
|
406
203
|
return {
|
|
407
204
|
baseUrl,
|
|
408
205
|
source: 'stored',
|
|
409
206
|
async fetch(path: string, init: RequestInit = {}, onRequestStart?: () => void) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
onRequestStart?.()
|
|
421
|
-
let response = await fetch(`${baseUrl}${path}`, {
|
|
422
|
-
...init,
|
|
423
|
-
headers: {
|
|
424
|
-
...(init.headers || {}),
|
|
425
|
-
Authorization: `Bearer ${getAuthorizationToken(activeCredentials)}`,
|
|
426
|
-
},
|
|
427
|
-
...credentialRequestRedirectPolicy(),
|
|
428
|
-
})
|
|
429
|
-
|
|
430
|
-
if (
|
|
431
|
-
allowRefresh
|
|
432
|
-
&& response.status === 401
|
|
433
|
-
&& isSessionCredentials(activeCredentials)
|
|
434
|
-
&& !didRefresh
|
|
435
|
-
) {
|
|
436
|
-
await refreshCapturedCredentials(init.signal)
|
|
437
|
-
onRequestStart?.()
|
|
438
|
-
response = await fetch(`${baseUrl}${path}`, {
|
|
439
|
-
...init,
|
|
440
|
-
headers: {
|
|
441
|
-
...(init.headers || {}),
|
|
442
|
-
Authorization: `Bearer ${getAuthorizationToken(activeCredentials)}`,
|
|
443
|
-
},
|
|
444
|
-
...credentialRequestRedirectPolicy(),
|
|
445
|
-
})
|
|
446
|
-
}
|
|
447
|
-
return response
|
|
207
|
+
const { storedRequest } = await storedSessionRequests()
|
|
208
|
+
return await storedRequest({
|
|
209
|
+
baseUrl,
|
|
210
|
+
credentials: activeCredentials,
|
|
211
|
+
allowRefresh,
|
|
212
|
+
changedBeforeRequest: changed,
|
|
213
|
+
changedBeforeReplay: changed,
|
|
214
|
+
onRefreshed: refreshed => { activeCredentials = refreshed },
|
|
215
|
+
}, path, init, onRequestStart)
|
|
448
216
|
},
|
|
449
217
|
async logout(callerSignal?: AbortSignal | null) {
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
? rememberResolvedBearer(activeCredentials.refreshToken)
|
|
453
|
-
: null
|
|
454
|
-
const secrets = [authorizationToken, ...(refreshToken !== null ? [refreshToken] : [])]
|
|
455
|
-
let remoteLogoutReason: string | null = null
|
|
456
|
-
try {
|
|
457
|
-
remoteLogoutReason = await withLogoutDeadline(async signal => {
|
|
458
|
-
const response = await fetch(`${baseUrl}/api/cli/auth/logout`, {
|
|
459
|
-
method: 'POST',
|
|
460
|
-
headers: {
|
|
461
|
-
'Content-Type': 'application/json',
|
|
462
|
-
Authorization: `Bearer ${authorizationToken}`,
|
|
463
|
-
},
|
|
464
|
-
body: refreshToken !== null ? JSON.stringify({ refreshToken }) : undefined,
|
|
465
|
-
...credentialRequestRedirectPolicy(),
|
|
466
|
-
signal,
|
|
467
|
-
})
|
|
468
|
-
return response.ok ? null : await describeLogoutHttpFailure(response, secrets)
|
|
469
|
-
}, callerSignal)
|
|
470
|
-
} catch (error) {
|
|
471
|
-
remoteLogoutReason = describeLogoutTransportFailure(error, secrets)
|
|
472
|
-
}
|
|
473
|
-
const localCredentialCleared = clearServerCredentialsIfCurrent(baseUrl, activeCredentials)
|
|
474
|
-
return { remoteError: remoteLogoutReason, localCredentialCleared }
|
|
218
|
+
const { storedLogout } = await storedSessionRequests()
|
|
219
|
+
return await storedLogout(baseUrl, baseUrl, activeCredentials, callerSignal)
|
|
475
220
|
},
|
|
476
221
|
}
|
|
477
222
|
}
|
|
478
223
|
|
|
224
|
+
// Redacts a stored login from diagnostics from the moment it is read.
|
|
225
|
+
function rememberStoredBearers(credentials: ServerCredentials): void {
|
|
226
|
+
if ('accessToken' in credentials) {
|
|
227
|
+
rememberResolvedBearer(credentials.accessToken)
|
|
228
|
+
rememberResolvedBearer(credentials.refreshToken)
|
|
229
|
+
} else {
|
|
230
|
+
rememberResolvedBearer(credentials.apiKey)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
479
234
|
// The optional observer runs immediately before a resource fetch, after auth
|
|
480
235
|
// preflight. Authentication refresh requests do not mark the resource as sent.
|
|
481
236
|
export async function authedFetch(path: string, init: RequestInit = {}, onRequestStart?: () => void) {
|
|
@@ -504,47 +259,14 @@ export async function authedFetch(path: string, init: RequestInit = {}, onReques
|
|
|
504
259
|
}
|
|
505
260
|
|
|
506
261
|
const credentials = getServerCredentials(baseUrl)
|
|
507
|
-
if (!credentials)
|
|
508
|
-
throw new Error(`Not logged in for ${baseUrl}. Run \`orizu login --server ${baseUrl}\` (or \`--local\`) first.`)
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
const resolved = await refreshExpiredStoredCredentials(baseUrl, credentials, init.signal)
|
|
512
|
-
let activeCredentials = resolved.credentials
|
|
513
|
-
|
|
514
|
-
let response = await fetchResource(`${baseUrl}${path}`, {
|
|
515
|
-
...init,
|
|
516
|
-
headers: {
|
|
517
|
-
...(init.headers || {}),
|
|
518
|
-
Authorization: `Bearer ${getAuthorizationToken(activeCredentials)}`,
|
|
519
|
-
},
|
|
520
|
-
...credentialRequestRedirectPolicy(),
|
|
521
|
-
})
|
|
522
|
-
|
|
523
|
-
if (
|
|
524
|
-
response.status === 401
|
|
525
|
-
&& isSessionCredentials(activeCredentials)
|
|
526
|
-
&& !resolved.didRefresh
|
|
527
|
-
) {
|
|
528
|
-
const refreshResult = await refreshStoredCredentialsIfCurrent(
|
|
529
|
-
baseUrl,
|
|
530
|
-
activeCredentials,
|
|
531
|
-
init.signal
|
|
532
|
-
)
|
|
533
|
-
if (refreshResult.status === 'changed') {
|
|
534
|
-
throw new Error(
|
|
535
|
-
'ORIZU_AUTH_CONTEXT_CHANGED: stored credentials changed after this request began; the request was not replayed.'
|
|
536
|
-
)
|
|
537
|
-
}
|
|
538
|
-
activeCredentials = requireCurrentCredentials(baseUrl, refreshResult.credentials)
|
|
539
|
-
response = await fetchResource(`${baseUrl}${path}`, {
|
|
540
|
-
...init,
|
|
541
|
-
headers: {
|
|
542
|
-
...(init.headers || {}),
|
|
543
|
-
Authorization: `Bearer ${getAuthorizationToken(activeCredentials)}`,
|
|
544
|
-
},
|
|
545
|
-
...credentialRequestRedirectPolicy(),
|
|
546
|
-
})
|
|
547
|
-
}
|
|
262
|
+
if (!credentials) throw notLoggedIn(baseUrl)
|
|
548
263
|
|
|
549
|
-
|
|
264
|
+
const { storedRequest } = await storedSessionRequests()
|
|
265
|
+
return await storedRequest({
|
|
266
|
+
baseUrl,
|
|
267
|
+
credentials,
|
|
268
|
+
allowRefresh: true,
|
|
269
|
+
changedBeforeRequest: 'ORIZU_AUTH_CONTEXT_CHANGED: stored credentials changed before this request began; the request was not sent.',
|
|
270
|
+
changedBeforeReplay: 'ORIZU_AUTH_CONTEXT_CHANGED: stored credentials changed after this request began; the request was not replayed.',
|
|
271
|
+
}, path, init, onRequestStart)
|
|
550
272
|
}
|
package/src/keyboard-select.ts
CHANGED
|
@@ -1,23 +1,12 @@
|
|
|
1
|
-
import { emitKeypressEvents } from 'readline'
|
|
2
1
|
import { stdin as input, stdout as output } from 'process'
|
|
3
2
|
|
|
4
3
|
import { sanitizeTerminalText } from './json-response.js'
|
|
5
4
|
|
|
6
|
-
interface KeypressInfo {
|
|
7
|
-
name?: string
|
|
8
|
-
ctrl?: boolean
|
|
9
|
-
}
|
|
10
|
-
|
|
11
5
|
interface KeyboardSelectOptions<T> {
|
|
12
6
|
onEscape?: () => T
|
|
13
7
|
escapeLabel?: string
|
|
14
8
|
}
|
|
15
9
|
|
|
16
|
-
type RawInput = typeof input & {
|
|
17
|
-
setRawMode?: (mode: boolean) => void
|
|
18
|
-
isRaw?: boolean
|
|
19
|
-
}
|
|
20
|
-
|
|
21
10
|
export async function promptKeyboardSelect<T>(
|
|
22
11
|
title: string,
|
|
23
12
|
items: T[],
|
|
@@ -36,9 +25,6 @@ export async function promptKeyboardSelect<T>(
|
|
|
36
25
|
|
|
37
26
|
let selected = 0
|
|
38
27
|
let rendered = false
|
|
39
|
-
const rawInput = input as RawInput
|
|
40
|
-
const wasRaw = Boolean(rawInput.isRaw)
|
|
41
|
-
emitKeypressEvents(input)
|
|
42
28
|
|
|
43
29
|
function render() {
|
|
44
30
|
if (rendered) output.write(`\x1b[${items.length}A`)
|
|
@@ -51,47 +37,24 @@ export async function promptKeyboardSelect<T>(
|
|
|
51
37
|
}
|
|
52
38
|
|
|
53
39
|
render()
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function onKeypress(_chunk: string, key: KeypressInfo) {
|
|
65
|
-
if (key.ctrl && key.name === 'c') {
|
|
66
|
-
cleanup()
|
|
67
|
-
rejectPromise(new Error('Setup cancelled.'))
|
|
68
|
-
return
|
|
69
|
-
}
|
|
70
|
-
if (key.name === 'escape') {
|
|
71
|
-
cleanup()
|
|
72
|
-
if (options?.onEscape) resolvePromise(options.onEscape())
|
|
73
|
-
else rejectPromise(new Error('Setup cancelled.'))
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
if (key.name === 'up') {
|
|
77
|
-
selected = (selected - 1 + items.length) % items.length
|
|
78
|
-
render()
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
if (key.name === 'down') {
|
|
82
|
-
selected = (selected + 1) % items.length
|
|
40
|
+
const onEscape = options?.onEscape
|
|
41
|
+
const { readKeys } = await import('./terminal-key-session.js')
|
|
42
|
+
let choice: { item: T } | { escaped: true }
|
|
43
|
+
try {
|
|
44
|
+
choice = await readKeys<{ item: T } | { escaped: true }>(key => {
|
|
45
|
+
if (key.name === 'escape') return onEscape ? { kind: 'done', value: { escaped: true } } : { kind: 'cancel' }
|
|
46
|
+
if (key.name === 'up' || key.name === 'down') {
|
|
47
|
+
selected = (selected + (key.name === 'up' ? -1 : 1) + items.length) % items.length
|
|
83
48
|
render()
|
|
84
|
-
return
|
|
49
|
+
return { kind: 'wait' }
|
|
85
50
|
}
|
|
86
|
-
if (key.name === 'return' || key.name === 'enter') {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
rawInput.setRawMode?.(true)
|
|
96
|
-
})
|
|
51
|
+
if (key.name === 'return' || key.name === 'enter') return { kind: 'done', value: { item: items[selected] } }
|
|
52
|
+
return { kind: 'wait' }
|
|
53
|
+
}, { hideCursor: true })
|
|
54
|
+
} finally {
|
|
55
|
+
output.write('\n')
|
|
56
|
+
}
|
|
57
|
+
if ('item' in choice) return choice.item
|
|
58
|
+
if (!onEscape) throw new Error('Setup cancelled.')
|
|
59
|
+
return onEscape()
|
|
97
60
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Cause, Effect, Exit, Result, Schema } from 'effect'
|
|
2
|
+
|
|
3
|
+
import { datasetTransferTimeoutMs } from './dataset-transfer-policy.js'
|
|
4
|
+
import { extractErrorMessage } from './error-response.js'
|
|
5
|
+
import { appErrorOf, unreachedCode } from './request-outcome.js'
|
|
6
|
+
|
|
7
|
+
// Loaded only through `await import(...)`, like the team create workflow:
|
|
8
|
+
// the project create that setup and `orizu projects create` send.
|
|
9
|
+
|
|
10
|
+
/** Sends one request; calls `onRequestStart` just before anything goes out. */
|
|
11
|
+
export type ProjectCreateRequest = (path: string, init: RequestInit, onRequestStart?: () => void) => Promise<Response>
|
|
12
|
+
|
|
13
|
+
const CreateReply = Schema.Struct({
|
|
14
|
+
project: Schema.Struct({
|
|
15
|
+
id: Schema.NonEmptyString,
|
|
16
|
+
name: Schema.String,
|
|
17
|
+
slug: Schema.NonEmptyString,
|
|
18
|
+
teamSlug: Schema.NonEmptyString,
|
|
19
|
+
teamId: Schema.optional(Schema.String),
|
|
20
|
+
teamName: Schema.optional(Schema.String),
|
|
21
|
+
role: Schema.optional(Schema.String),
|
|
22
|
+
}),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export interface CreatedProject {
|
|
26
|
+
id: string
|
|
27
|
+
name: string
|
|
28
|
+
slug: string
|
|
29
|
+
teamId: string
|
|
30
|
+
teamName: string
|
|
31
|
+
teamSlug: string
|
|
32
|
+
role: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Runs one request under the time limit. `run` gets the signal to pass on and
|
|
37
|
+
* the hook that marks the request as sent; the body is read inside `run`, so a
|
|
38
|
+
* reply that stalls part way is under the limit too.
|
|
39
|
+
*/
|
|
40
|
+
function withDeadline<A, E>(
|
|
41
|
+
timeoutMs: number,
|
|
42
|
+
run: (signal: AbortSignal, markSent: () => void) => Promise<A>,
|
|
43
|
+
onFailure: (cause: unknown, outcome: { timedOut: boolean, sent: boolean }) => E,
|
|
44
|
+
): Effect.Effect<A, E> {
|
|
45
|
+
return Effect.suspend(() => {
|
|
46
|
+
let sent = false
|
|
47
|
+
let timedOut = false
|
|
48
|
+
return Effect.tryPromise({
|
|
49
|
+
try: async signal => {
|
|
50
|
+
const controller = new AbortController()
|
|
51
|
+
const abort = () => controller.abort()
|
|
52
|
+
const timer = setTimeout(() => { timedOut = true; controller.abort() }, timeoutMs)
|
|
53
|
+
if (signal.aborted) abort()
|
|
54
|
+
else signal.addEventListener('abort', abort, { once: true })
|
|
55
|
+
try {
|
|
56
|
+
return await run(controller.signal, () => { sent = true })
|
|
57
|
+
} finally {
|
|
58
|
+
clearTimeout(timer)
|
|
59
|
+
signal.removeEventListener('abort', abort)
|
|
60
|
+
controller.abort()
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
catch: cause => onFailure(cause, { timedOut, sent }),
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function runToPromise<A>(effect: Effect.Effect<A, Error>): Promise<A> {
|
|
69
|
+
const exit = await Effect.runPromiseExit(effect)
|
|
70
|
+
if (Exit.isSuccess(exit)) return exit.value
|
|
71
|
+
throw Cause.squash(exit.cause)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const FAILED = 'Failed to create project'
|
|
75
|
+
|
|
76
|
+
const projectMayBeCreated = (teamSlug: string, name: string) =>
|
|
77
|
+
`The project may already be created: run \`orizu projects list --team ${teamSlug}\` and look for "${name}" before creating it again.`
|
|
78
|
+
|
|
79
|
+
class ProjectCreateFailure extends Error {
|
|
80
|
+
readonly _tag = 'ProjectCreateFailure'
|
|
81
|
+
constructor(message: string, readonly mayBeCreated: boolean) { super(message) }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* `orizu projects create` and setup's project create: one request with a time
|
|
86
|
+
* limit, never retried, since a retry after an unclear outcome could make a
|
|
87
|
+
* second project. When the outcome is unclear, the error says how to check.
|
|
88
|
+
*/
|
|
89
|
+
export async function createProjectWithDeadline(teamSlug: string, name: string, request: ProjectCreateRequest): Promise<CreatedProject> {
|
|
90
|
+
const create = withDeadline(datasetTransferTimeoutMs(), async (signal, markSent) => {
|
|
91
|
+
const response = await request('/api/cli/projects', {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: { 'Content-Type': 'application/json' },
|
|
94
|
+
body: JSON.stringify({ teamSlug, name }),
|
|
95
|
+
signal,
|
|
96
|
+
}, markSent)
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
const appError = await appErrorOf(response.clone())
|
|
99
|
+
const detail = await extractErrorMessage(response)
|
|
100
|
+
// A 5xx that is not the app's own error may come from a gateway that gave up while the app kept working.
|
|
101
|
+
if (response.status < 400 || (response.status >= 500 && appError === null)) {
|
|
102
|
+
throw new ProjectCreateFailure(`Project create got no clear answer (status ${response.status}).`, true)
|
|
103
|
+
}
|
|
104
|
+
throw new ProjectCreateFailure(detail.startsWith(FAILED) ? detail : `${FAILED}: ${detail}`, false)
|
|
105
|
+
}
|
|
106
|
+
const body = await response.text()
|
|
107
|
+
let data: unknown
|
|
108
|
+
try {
|
|
109
|
+
data = JSON.parse(body)
|
|
110
|
+
} catch {
|
|
111
|
+
data = undefined
|
|
112
|
+
}
|
|
113
|
+
const decoded = Schema.decodeUnknownResult(CreateReply)(data)
|
|
114
|
+
if (Result.isFailure(decoded)) {
|
|
115
|
+
throw new ProjectCreateFailure(`Project create returned an unexpected response (status ${response.status}).`, true)
|
|
116
|
+
}
|
|
117
|
+
const project = decoded.success.project
|
|
118
|
+
return {
|
|
119
|
+
id: project.id,
|
|
120
|
+
name: project.name,
|
|
121
|
+
slug: project.slug,
|
|
122
|
+
teamId: project.teamId || '',
|
|
123
|
+
teamName: project.teamName || project.teamSlug,
|
|
124
|
+
teamSlug: project.teamSlug,
|
|
125
|
+
role: project.role || 'admin',
|
|
126
|
+
}
|
|
127
|
+
}, (cause, { timedOut, sent }) => {
|
|
128
|
+
if (cause instanceof ProjectCreateFailure) return cause
|
|
129
|
+
if (timedOut) return new ProjectCreateFailure('Project create exceeded its deadline.', sent)
|
|
130
|
+
const unreached = unreachedCode(cause)
|
|
131
|
+
if (unreached) return new ProjectCreateFailure(`${FAILED}: could not reach the server (${unreached}).`, false)
|
|
132
|
+
const message = cause instanceof Error ? cause.message : String(cause)
|
|
133
|
+
if (!sent || message.startsWith('ORIZU_AUTH_CONTEXT_CHANGED:')) return new ProjectCreateFailure(message, false)
|
|
134
|
+
return new ProjectCreateFailure(`Project create did not finish: ${message}`, true)
|
|
135
|
+
})
|
|
136
|
+
return await runToPromise(create.pipe(
|
|
137
|
+
Effect.mapError(failure => new Error(failure.mayBeCreated ? `${failure.message}\n${projectMayBeCreated(teamSlug, name)}` : failure.message)),
|
|
138
|
+
))
|
|
139
|
+
}
|