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/README.md
CHANGED
|
@@ -50,6 +50,7 @@ orizu login
|
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
This will open a browser tab for you to login with. You must either have an account on the [Orizu platform](https://orizu.ai) or it will help you create one.
|
|
53
|
+
Approving the login creates a personal access token for the CLI and stores it in your local Orizu credentials file. You can revoke CLI tokens from the Personal Tokens page in Orizu.
|
|
53
54
|
|
|
54
55
|
**Check the signed-in user**
|
|
55
56
|
|
|
@@ -120,6 +121,10 @@ orizu logout
|
|
|
120
121
|
<td>Append dataset rows</td>
|
|
121
122
|
<td><code>orizu datasets append --dataset <datasetId> --file ./datasets/support-extra.jsonl</code></td>
|
|
122
123
|
</tr>
|
|
124
|
+
<tr>
|
|
125
|
+
<td>Delete a dataset</td>
|
|
126
|
+
<td><code>orizu datasets delete --dataset <datasetId></code> (interactive confirmation required)</td>
|
|
127
|
+
</tr>
|
|
123
128
|
<tr>
|
|
124
129
|
<td>Lock a dataset</td>
|
|
125
130
|
<td><code>orizu datasets lock --dataset <datasetId> --reason "Finalize for labeling"</code></td>
|
package/dist/credentials.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, rmSync, writeFileSync,
|
|
1
|
+
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
+
import { randomBytes } from 'crypto';
|
|
4
5
|
function getConfigDir() {
|
|
5
6
|
if (process.env.ORIZU_CONFIG_DIR) {
|
|
6
7
|
return process.env.ORIZU_CONFIG_DIR;
|
|
@@ -17,6 +18,13 @@ function isStoredCredentialsV2(value) {
|
|
|
17
18
|
const typed = value;
|
|
18
19
|
return typed.version === 2 && !!typed.servers && typeof typed.servers === 'object';
|
|
19
20
|
}
|
|
21
|
+
function isStoredCredentialsV3(value) {
|
|
22
|
+
if (!value || typeof value !== 'object') {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const typed = value;
|
|
26
|
+
return typed.version === 3 && !!typed.servers && typeof typed.servers === 'object';
|
|
27
|
+
}
|
|
20
28
|
function isStoredCredentialsV1(value) {
|
|
21
29
|
if (!value || typeof value !== 'object') {
|
|
22
30
|
return false;
|
|
@@ -42,14 +50,38 @@ function migrateToV2(stored) {
|
|
|
42
50
|
}
|
|
43
51
|
function writeCredentials(config) {
|
|
44
52
|
const dir = getConfigDir();
|
|
45
|
-
mkdirSync(dir, { recursive: true });
|
|
53
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
54
|
+
chmodSync(dir, 0o700);
|
|
46
55
|
const path = getCredentialsPath();
|
|
47
|
-
|
|
56
|
+
const tempPath = join(dir, `.credentials.json.${process.pid}.${randomBytes(8).toString('hex')}.tmp`);
|
|
57
|
+
const payload = JSON.stringify(config, null, 2) + '\n';
|
|
58
|
+
const fd = openSync(tempPath, 'wx', 0o600);
|
|
59
|
+
try {
|
|
60
|
+
writeFileSync(fd, payload, 'utf-8');
|
|
61
|
+
fsyncSync(fd);
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
closeSync(fd);
|
|
65
|
+
}
|
|
66
|
+
chmodSync(tempPath, 0o600);
|
|
67
|
+
try {
|
|
68
|
+
renameSync(tempPath, path);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (process.platform === 'win32' && error?.code === 'EEXIST') {
|
|
72
|
+
rmSync(path, { force: true });
|
|
73
|
+
renameSync(tempPath, path);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
rmSync(tempPath, { force: true });
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
48
80
|
chmodSync(path, 0o600);
|
|
49
81
|
}
|
|
50
82
|
function createEmptyCredentialsConfig() {
|
|
51
83
|
return {
|
|
52
|
-
version:
|
|
84
|
+
version: 3,
|
|
53
85
|
activeBaseUrl: null,
|
|
54
86
|
servers: {},
|
|
55
87
|
};
|
|
@@ -76,6 +108,9 @@ export function loadCredentialsConfig() {
|
|
|
76
108
|
console.warn('Warning: credentials file contains invalid JSON — please re-login with `orizu login`');
|
|
77
109
|
return null;
|
|
78
110
|
}
|
|
111
|
+
if (isStoredCredentialsV3(parsed)) {
|
|
112
|
+
return parsed;
|
|
113
|
+
}
|
|
79
114
|
if (isStoredCredentialsV2(parsed)) {
|
|
80
115
|
return parsed;
|
|
81
116
|
}
|
|
@@ -96,13 +131,27 @@ export function getServerCredentials(baseUrl) {
|
|
|
96
131
|
return config.servers[baseUrl] || null;
|
|
97
132
|
}
|
|
98
133
|
export function saveServerCredentials(baseUrl, credentials) {
|
|
99
|
-
const
|
|
134
|
+
const loaded = loadCredentialsConfigForWrite();
|
|
135
|
+
const config = loaded.version === 3
|
|
136
|
+
? loaded
|
|
137
|
+
: {
|
|
138
|
+
version: 3,
|
|
139
|
+
activeBaseUrl: loaded.activeBaseUrl,
|
|
140
|
+
servers: loaded.servers,
|
|
141
|
+
};
|
|
100
142
|
config.servers[baseUrl] = credentials;
|
|
101
143
|
config.activeBaseUrl = baseUrl;
|
|
102
144
|
writeCredentials(config);
|
|
103
145
|
}
|
|
104
146
|
export function updateServerCredentials(baseUrl, credentials) {
|
|
105
|
-
const
|
|
147
|
+
const loaded = loadCredentialsConfigForWrite();
|
|
148
|
+
const config = loaded.version === 3
|
|
149
|
+
? loaded
|
|
150
|
+
: {
|
|
151
|
+
version: 3,
|
|
152
|
+
activeBaseUrl: loaded.activeBaseUrl,
|
|
153
|
+
servers: loaded.servers,
|
|
154
|
+
};
|
|
106
155
|
config.servers[baseUrl] = credentials;
|
|
107
156
|
writeCredentials(config);
|
|
108
157
|
}
|
package/dist/http.js
CHANGED
|
@@ -33,11 +33,43 @@ export function resolveLoginBaseUrl(flags = runtimeFlags) {
|
|
|
33
33
|
export function getBaseUrl() {
|
|
34
34
|
return resolveBaseUrl();
|
|
35
35
|
}
|
|
36
|
+
function isLoopbackHostname(hostname) {
|
|
37
|
+
const normalized = hostname.toLowerCase();
|
|
38
|
+
return (normalized === 'localhost' ||
|
|
39
|
+
normalized === '[::1]' ||
|
|
40
|
+
/^127(?:\.\d{1,3}){3}$/.test(normalized));
|
|
41
|
+
}
|
|
42
|
+
export function assertSecureTokenTransport(baseUrl) {
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = new URL(baseUrl);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error(`Invalid server URL: '${baseUrl}'`);
|
|
49
|
+
}
|
|
50
|
+
if (parsed.protocol === 'https:') {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (parsed.protocol === 'http:' && isLoopbackHostname(parsed.hostname)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`Refusing to send CLI tokens to ${baseUrl}. Use HTTPS, or --local for loopback development.`);
|
|
57
|
+
}
|
|
58
|
+
function isSessionCredentials(credentials) {
|
|
59
|
+
return 'accessToken' in credentials;
|
|
60
|
+
}
|
|
61
|
+
function getAuthorizationToken(credentials) {
|
|
62
|
+
return isSessionCredentials(credentials) ? credentials.accessToken : credentials.apiKey;
|
|
63
|
+
}
|
|
36
64
|
function isExpired(expiresAt) {
|
|
37
65
|
const nowUnix = Math.floor(Date.now() / 1000);
|
|
38
66
|
return expiresAt <= nowUnix + 30;
|
|
39
67
|
}
|
|
40
68
|
async function refreshCredentials(baseUrl, credentials) {
|
|
69
|
+
if (!isSessionCredentials(credentials)) {
|
|
70
|
+
throw new Error('API key credentials do not refresh. Run `orizu login` again if access fails.');
|
|
71
|
+
}
|
|
72
|
+
assertSecureTokenTransport(baseUrl);
|
|
41
73
|
const response = await fetch(`${baseUrl}/api/cli/auth/refresh`, {
|
|
42
74
|
method: 'POST',
|
|
43
75
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -47,7 +79,11 @@ async function refreshCredentials(baseUrl, credentials) {
|
|
|
47
79
|
throw new Error('Session expired. Run `orizu login` again.');
|
|
48
80
|
}
|
|
49
81
|
const data = await response.json();
|
|
82
|
+
if (!data.accessToken || !data.refreshToken || !data.expiresAt) {
|
|
83
|
+
throw new Error('Server returned invalid refresh credentials. Run `orizu login` again.');
|
|
84
|
+
}
|
|
50
85
|
const refreshed = {
|
|
86
|
+
credentialType: 'session',
|
|
51
87
|
accessToken: data.accessToken,
|
|
52
88
|
refreshToken: data.refreshToken,
|
|
53
89
|
expiresAt: data.expiresAt,
|
|
@@ -57,28 +93,29 @@ async function refreshCredentials(baseUrl, credentials) {
|
|
|
57
93
|
}
|
|
58
94
|
export async function authedFetch(path, init = {}) {
|
|
59
95
|
const baseUrl = resolveBaseUrl();
|
|
96
|
+
assertSecureTokenTransport(baseUrl);
|
|
60
97
|
const credentials = getServerCredentials(baseUrl);
|
|
61
98
|
if (!credentials) {
|
|
62
99
|
throw new Error(`Not logged in for ${baseUrl}. Run \`orizu login --server ${baseUrl}\` (or \`--local\`) first.`);
|
|
63
100
|
}
|
|
64
101
|
let activeCredentials = credentials;
|
|
65
|
-
if (isExpired(activeCredentials.expiresAt)) {
|
|
102
|
+
if (isSessionCredentials(activeCredentials) && isExpired(activeCredentials.expiresAt)) {
|
|
66
103
|
activeCredentials = await refreshCredentials(baseUrl, activeCredentials);
|
|
67
104
|
}
|
|
68
105
|
let response = await fetch(`${baseUrl}${path}`, {
|
|
69
106
|
...init,
|
|
70
107
|
headers: {
|
|
71
108
|
...(init.headers || {}),
|
|
72
|
-
Authorization: `Bearer ${activeCredentials
|
|
109
|
+
Authorization: `Bearer ${getAuthorizationToken(activeCredentials)}`,
|
|
73
110
|
},
|
|
74
111
|
});
|
|
75
|
-
if (response.status === 401) {
|
|
112
|
+
if (response.status === 401 && isSessionCredentials(activeCredentials)) {
|
|
76
113
|
activeCredentials = await refreshCredentials(baseUrl, activeCredentials);
|
|
77
114
|
response = await fetch(`${baseUrl}${path}`, {
|
|
78
115
|
...init,
|
|
79
116
|
headers: {
|
|
80
117
|
...(init.headers || {}),
|
|
81
|
-
Authorization: `Bearer ${activeCredentials
|
|
118
|
+
Authorization: `Bearer ${getAuthorizationToken(activeCredentials)}`,
|
|
82
119
|
},
|
|
83
120
|
});
|
|
84
121
|
}
|