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 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 &lt;datasetId&gt; --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 &lt;datasetId&gt;</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 &lt;datasetId&gt; --reason "Finalize for labeling"</code></td>
@@ -1,6 +1,7 @@
1
- import { mkdirSync, readFileSync, rmSync, writeFileSync, chmodSync, existsSync } from 'fs';
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
- writeFileSync(path, JSON.stringify(config, null, 2), 'utf-8');
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: 2,
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 config = loadCredentialsConfigForWrite();
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 config = loadCredentialsConfigForWrite();
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.accessToken}`,
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.accessToken}`,
118
+ Authorization: `Bearer ${getAuthorizationToken(activeCredentials)}`,
82
119
  },
83
120
  });
84
121
  }