orizu 0.1.5 → 0.2.0
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/README.md +5 -0
- package/dist/credentials.js +55 -6
- package/dist/http.js +41 -4
- package/dist/index.js +253 -136
- package/package.json +1 -1
- package/src/credentials.ts +82 -11
- package/src/http.ts +54 -5
- package/src/index.ts +305 -147
- package/src/types.ts +19 -4
package/package.json
CHANGED
package/src/credentials.ts
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
closeSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
fsyncSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
openSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'fs'
|
|
2
13
|
import { join } from 'path'
|
|
3
14
|
import { homedir } from 'os'
|
|
4
|
-
import {
|
|
15
|
+
import { randomBytes } from 'crypto'
|
|
16
|
+
import {
|
|
17
|
+
ServerCredentials,
|
|
18
|
+
StoredCredentialsV1,
|
|
19
|
+
StoredCredentialsV2,
|
|
20
|
+
StoredCredentialsV3,
|
|
21
|
+
} from './types.js'
|
|
5
22
|
|
|
6
23
|
function getConfigDir(): string {
|
|
7
24
|
if (process.env.ORIZU_CONFIG_DIR) {
|
|
@@ -23,6 +40,15 @@ function isStoredCredentialsV2(value: unknown): value is StoredCredentialsV2 {
|
|
|
23
40
|
return typed.version === 2 && !!typed.servers && typeof typed.servers === 'object'
|
|
24
41
|
}
|
|
25
42
|
|
|
43
|
+
function isStoredCredentialsV3(value: unknown): value is StoredCredentialsV3 {
|
|
44
|
+
if (!value || typeof value !== 'object') {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const typed = value as Partial<StoredCredentialsV3>
|
|
49
|
+
return typed.version === 3 && !!typed.servers && typeof typed.servers === 'object'
|
|
50
|
+
}
|
|
51
|
+
|
|
26
52
|
function isStoredCredentialsV1(value: unknown): value is StoredCredentialsV1 {
|
|
27
53
|
if (!value || typeof value !== 'object') {
|
|
28
54
|
return false
|
|
@@ -51,23 +77,50 @@ function migrateToV2(stored: StoredCredentialsV1): StoredCredentialsV2 {
|
|
|
51
77
|
}
|
|
52
78
|
}
|
|
53
79
|
|
|
54
|
-
function writeCredentials(config: StoredCredentialsV2) {
|
|
80
|
+
function writeCredentials(config: StoredCredentialsV2 | StoredCredentialsV3) {
|
|
55
81
|
const dir = getConfigDir()
|
|
56
|
-
mkdirSync(dir, { recursive: true })
|
|
82
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
83
|
+
chmodSync(dir, 0o700)
|
|
84
|
+
|
|
57
85
|
const path = getCredentialsPath()
|
|
58
|
-
|
|
86
|
+
const tempPath = join(
|
|
87
|
+
dir,
|
|
88
|
+
`.credentials.json.${process.pid}.${randomBytes(8).toString('hex')}.tmp`
|
|
89
|
+
)
|
|
90
|
+
const payload = JSON.stringify(config, null, 2) + '\n'
|
|
91
|
+
const fd = openSync(tempPath, 'wx', 0o600)
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
writeFileSync(fd, payload, 'utf-8')
|
|
95
|
+
fsyncSync(fd)
|
|
96
|
+
} finally {
|
|
97
|
+
closeSync(fd)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
chmodSync(tempPath, 0o600)
|
|
101
|
+
try {
|
|
102
|
+
renameSync(tempPath, path)
|
|
103
|
+
} catch (error: any) {
|
|
104
|
+
if (process.platform === 'win32' && error?.code === 'EEXIST') {
|
|
105
|
+
rmSync(path, { force: true })
|
|
106
|
+
renameSync(tempPath, path)
|
|
107
|
+
} else {
|
|
108
|
+
rmSync(tempPath, { force: true })
|
|
109
|
+
throw error
|
|
110
|
+
}
|
|
111
|
+
}
|
|
59
112
|
chmodSync(path, 0o600)
|
|
60
113
|
}
|
|
61
114
|
|
|
62
|
-
function createEmptyCredentialsConfig():
|
|
115
|
+
function createEmptyCredentialsConfig(): StoredCredentialsV3 {
|
|
63
116
|
return {
|
|
64
|
-
version:
|
|
117
|
+
version: 3 as const,
|
|
65
118
|
activeBaseUrl: null,
|
|
66
119
|
servers: {},
|
|
67
120
|
}
|
|
68
121
|
}
|
|
69
122
|
|
|
70
|
-
function loadCredentialsConfigForWrite(): StoredCredentialsV2 {
|
|
123
|
+
function loadCredentialsConfigForWrite(): StoredCredentialsV2 | StoredCredentialsV3 {
|
|
71
124
|
try {
|
|
72
125
|
return loadCredentialsConfig() || createEmptyCredentialsConfig()
|
|
73
126
|
} catch {
|
|
@@ -75,7 +128,7 @@ function loadCredentialsConfigForWrite(): StoredCredentialsV2 {
|
|
|
75
128
|
}
|
|
76
129
|
}
|
|
77
130
|
|
|
78
|
-
export function loadCredentialsConfig(): StoredCredentialsV2 | null {
|
|
131
|
+
export function loadCredentialsConfig(): StoredCredentialsV2 | StoredCredentialsV3 | null {
|
|
79
132
|
const path = getCredentialsPath()
|
|
80
133
|
if (!existsSync(path)) {
|
|
81
134
|
return null
|
|
@@ -91,6 +144,10 @@ export function loadCredentialsConfig(): StoredCredentialsV2 | null {
|
|
|
91
144
|
return null
|
|
92
145
|
}
|
|
93
146
|
|
|
147
|
+
if (isStoredCredentialsV3(parsed)) {
|
|
148
|
+
return parsed
|
|
149
|
+
}
|
|
150
|
+
|
|
94
151
|
if (isStoredCredentialsV2(parsed)) {
|
|
95
152
|
return parsed
|
|
96
153
|
}
|
|
@@ -117,7 +174,14 @@ export function getServerCredentials(baseUrl: string): ServerCredentials | null
|
|
|
117
174
|
}
|
|
118
175
|
|
|
119
176
|
export function saveServerCredentials(baseUrl: string, credentials: ServerCredentials) {
|
|
120
|
-
const
|
|
177
|
+
const loaded = loadCredentialsConfigForWrite()
|
|
178
|
+
const config: StoredCredentialsV3 = loaded.version === 3
|
|
179
|
+
? loaded
|
|
180
|
+
: {
|
|
181
|
+
version: 3,
|
|
182
|
+
activeBaseUrl: loaded.activeBaseUrl,
|
|
183
|
+
servers: loaded.servers,
|
|
184
|
+
}
|
|
121
185
|
|
|
122
186
|
config.servers[baseUrl] = credentials
|
|
123
187
|
config.activeBaseUrl = baseUrl
|
|
@@ -125,7 +189,14 @@ export function saveServerCredentials(baseUrl: string, credentials: ServerCreden
|
|
|
125
189
|
}
|
|
126
190
|
|
|
127
191
|
export function updateServerCredentials(baseUrl: string, credentials: ServerCredentials) {
|
|
128
|
-
const
|
|
192
|
+
const loaded = loadCredentialsConfigForWrite()
|
|
193
|
+
const config: StoredCredentialsV3 = loaded.version === 3
|
|
194
|
+
? loaded
|
|
195
|
+
: {
|
|
196
|
+
version: 3,
|
|
197
|
+
activeBaseUrl: loaded.activeBaseUrl,
|
|
198
|
+
servers: loaded.servers,
|
|
199
|
+
}
|
|
129
200
|
config.servers[baseUrl] = credentials
|
|
130
201
|
writeCredentials(config)
|
|
131
202
|
}
|
package/src/http.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getActiveBaseUrl, getServerCredentials, updateServerCredentials } from './credentials.js'
|
|
2
2
|
import { getFlagBaseUrl, GlobalFlags, normalizeBaseUrl } from './global-flags.js'
|
|
3
|
-
import { LoginResponse, ServerCredentials } from './types.js'
|
|
3
|
+
import { LoginResponse, ServerCredentials, SessionServerCredentials } from './types.js'
|
|
4
4
|
|
|
5
5
|
let runtimeFlags: GlobalFlags = { local: false, server: null }
|
|
6
6
|
|
|
@@ -45,12 +45,55 @@ export function getBaseUrl(): string {
|
|
|
45
45
|
return resolveBaseUrl()
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function isLoopbackHostname(hostname: string): boolean {
|
|
49
|
+
const normalized = hostname.toLowerCase()
|
|
50
|
+
return (
|
|
51
|
+
normalized === 'localhost' ||
|
|
52
|
+
normalized === '[::1]' ||
|
|
53
|
+
/^127(?:\.\d{1,3}){3}$/.test(normalized)
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function assertSecureTokenTransport(baseUrl: string) {
|
|
58
|
+
let parsed: URL
|
|
59
|
+
try {
|
|
60
|
+
parsed = new URL(baseUrl)
|
|
61
|
+
} catch {
|
|
62
|
+
throw new Error(`Invalid server URL: '${baseUrl}'`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (parsed.protocol === 'https:') {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (parsed.protocol === 'http:' && isLoopbackHostname(parsed.hostname)) {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Refusing to send CLI tokens to ${baseUrl}. Use HTTPS, or --local for loopback development.`
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isSessionCredentials(credentials: ServerCredentials): credentials is SessionServerCredentials {
|
|
79
|
+
return 'accessToken' in credentials
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getAuthorizationToken(credentials: ServerCredentials): string {
|
|
83
|
+
return isSessionCredentials(credentials) ? credentials.accessToken : credentials.apiKey
|
|
84
|
+
}
|
|
85
|
+
|
|
48
86
|
function isExpired(expiresAt: number): boolean {
|
|
49
87
|
const nowUnix = Math.floor(Date.now() / 1000)
|
|
50
88
|
return expiresAt <= nowUnix + 30
|
|
51
89
|
}
|
|
52
90
|
|
|
53
91
|
async function refreshCredentials(baseUrl: string, credentials: ServerCredentials): Promise<ServerCredentials> {
|
|
92
|
+
if (!isSessionCredentials(credentials)) {
|
|
93
|
+
throw new Error('API key credentials do not refresh. Run `orizu login` again if access fails.')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
assertSecureTokenTransport(baseUrl)
|
|
54
97
|
const response = await fetch(`${baseUrl}/api/cli/auth/refresh`, {
|
|
55
98
|
method: 'POST',
|
|
56
99
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -62,7 +105,12 @@ async function refreshCredentials(baseUrl: string, credentials: ServerCredential
|
|
|
62
105
|
}
|
|
63
106
|
|
|
64
107
|
const data = await response.json() as LoginResponse
|
|
108
|
+
if (!data.accessToken || !data.refreshToken || !data.expiresAt) {
|
|
109
|
+
throw new Error('Server returned invalid refresh credentials. Run `orizu login` again.')
|
|
110
|
+
}
|
|
111
|
+
|
|
65
112
|
const refreshed = {
|
|
113
|
+
credentialType: 'session' as const,
|
|
66
114
|
accessToken: data.accessToken,
|
|
67
115
|
refreshToken: data.refreshToken,
|
|
68
116
|
expiresAt: data.expiresAt,
|
|
@@ -73,13 +121,14 @@ async function refreshCredentials(baseUrl: string, credentials: ServerCredential
|
|
|
73
121
|
|
|
74
122
|
export async function authedFetch(path: string, init: RequestInit = {}) {
|
|
75
123
|
const baseUrl = resolveBaseUrl()
|
|
124
|
+
assertSecureTokenTransport(baseUrl)
|
|
76
125
|
const credentials = getServerCredentials(baseUrl)
|
|
77
126
|
if (!credentials) {
|
|
78
127
|
throw new Error(`Not logged in for ${baseUrl}. Run \`orizu login --server ${baseUrl}\` (or \`--local\`) first.`)
|
|
79
128
|
}
|
|
80
129
|
|
|
81
130
|
let activeCredentials = credentials
|
|
82
|
-
if (isExpired(activeCredentials.expiresAt)) {
|
|
131
|
+
if (isSessionCredentials(activeCredentials) && isExpired(activeCredentials.expiresAt)) {
|
|
83
132
|
activeCredentials = await refreshCredentials(baseUrl, activeCredentials)
|
|
84
133
|
}
|
|
85
134
|
|
|
@@ -87,17 +136,17 @@ export async function authedFetch(path: string, init: RequestInit = {}) {
|
|
|
87
136
|
...init,
|
|
88
137
|
headers: {
|
|
89
138
|
...(init.headers || {}),
|
|
90
|
-
Authorization: `Bearer ${activeCredentials
|
|
139
|
+
Authorization: `Bearer ${getAuthorizationToken(activeCredentials)}`,
|
|
91
140
|
},
|
|
92
141
|
})
|
|
93
142
|
|
|
94
|
-
if (response.status === 401) {
|
|
143
|
+
if (response.status === 401 && isSessionCredentials(activeCredentials)) {
|
|
95
144
|
activeCredentials = await refreshCredentials(baseUrl, activeCredentials)
|
|
96
145
|
response = await fetch(`${baseUrl}${path}`, {
|
|
97
146
|
...init,
|
|
98
147
|
headers: {
|
|
99
148
|
...(init.headers || {}),
|
|
100
|
-
Authorization: `Bearer ${activeCredentials
|
|
149
|
+
Authorization: `Bearer ${getAuthorizationToken(activeCredentials)}`,
|
|
101
150
|
},
|
|
102
151
|
})
|
|
103
152
|
}
|