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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orizu",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,24 @@
1
- import { mkdirSync, readFileSync, rmSync, writeFileSync, chmodSync, existsSync } from 'fs'
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 { ServerCredentials, StoredCredentialsV1, StoredCredentialsV2 } from './types.js'
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
- writeFileSync(path, JSON.stringify(config, null, 2), 'utf-8')
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(): StoredCredentialsV2 {
115
+ function createEmptyCredentialsConfig(): StoredCredentialsV3 {
63
116
  return {
64
- version: 2 as const,
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 config = loadCredentialsConfigForWrite()
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 config = loadCredentialsConfigForWrite()
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.accessToken}`,
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.accessToken}`,
149
+ Authorization: `Bearer ${getAuthorizationToken(activeCredentials)}`,
101
150
  },
102
151
  })
103
152
  }