@hasna/instructions 0.5.6 → 0.6.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 +64 -20
- package/dist/cli/fail-closed-no-env.test.d.ts +2 -0
- package/dist/cli/fail-closed-no-env.test.d.ts.map +1 -0
- package/dist/cli/index.js +1711 -422
- package/dist/data/config-store.d.ts +62 -33
- package/dist/data/config-store.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/generated/storage-kit/backend.d.ts +4 -4
- package/dist/generated/storage-kit/backend.d.ts.map +1 -1
- package/dist/generated/storage-kit/index.d.ts +1 -1
- package/dist/generated/storage-kit/index.d.ts.map +1 -1
- package/dist/generated/storage-kit/migrations.d.ts +21 -0
- package/dist/generated/storage-kit/migrations.d.ts.map +1 -1
- package/dist/generated/storage-kit/pool.d.ts +2 -5
- package/dist/generated/storage-kit/pool.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2486 -386
- package/dist/lib/app-home.d.ts +9 -0
- package/dist/lib/app-home.d.ts.map +1 -1
- package/dist/lib/client-types.d.ts +130 -0
- package/dist/lib/client-types.d.ts.map +1 -0
- package/dist/lib/client-types.test.d.ts +2 -0
- package/dist/lib/client-types.test.d.ts.map +1 -0
- package/dist/lib/local-opt-in.d.ts +82 -0
- package/dist/lib/local-opt-in.d.ts.map +1 -0
- package/dist/lib/project-context.d.ts +14 -14
- package/dist/lib/project-context.d.ts.map +1 -1
- package/dist/lib/project-dashboard-standard.d.ts +1 -1
- package/dist/lib/project-dashboard-standard.d.ts.map +1 -1
- package/dist/lib/session-render-state-hermeticity.test.d.ts +2 -0
- package/dist/lib/session-render-state-hermeticity.test.d.ts.map +1 -0
- package/dist/lib/session-render-state.d.ts +9 -0
- package/dist/lib/session-render-state.d.ts.map +1 -1
- package/dist/lib/transport-resolver.d.ts +80 -0
- package/dist/lib/transport-resolver.d.ts.map +1 -0
- package/dist/lib/transport-resolver.test.d.ts +2 -0
- package/dist/lib/transport-resolver.test.d.ts.map +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1439 -195
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +22 -0
- package/dist/sdk/index.d.ts.map +1 -0
- package/dist/sdk/index.js +1042 -0
- package/dist/sdk/resolve.d.ts +82 -0
- package/dist/sdk/resolve.d.ts.map +1 -0
- package/dist/sdk/resolve.test.d.ts +2 -0
- package/dist/sdk/resolve.test.d.ts.map +1 -0
- package/dist/sdk/sdk-bundle-self-contained.test.d.ts +2 -0
- package/dist/sdk/sdk-bundle-self-contained.test.d.ts.map +1 -0
- package/dist/sdk/v1.generated.d.ts +288 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -0
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +55 -64
- package/dist/test-support/preload-state-home.d.ts +2 -0
- package/dist/test-support/preload-state-home.d.ts.map +1 -0
- package/package.json +12 -12
- package/dashboard/README.md +0 -37
- package/dist/lib/retired-storage-mode.d.ts +0 -8
- package/dist/lib/retired-storage-mode.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -107,25 +107,799 @@ import { existsSync as existsSync2, mkdirSync, rmSync } from "fs";
|
|
|
107
107
|
import { join as join3 } from "path";
|
|
108
108
|
import { randomUUID } from "crypto";
|
|
109
109
|
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
110
|
+
// ../contracts/dist/client/transport.js
|
|
111
|
+
import { isIP } from "net";
|
|
112
|
+
import { spawnSync } from "child_process";
|
|
113
|
+
import { closeSync, fstatSync, openSync, readFileSync } from "fs";
|
|
114
|
+
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
115
|
+
import { createRequire } from "module";
|
|
116
|
+
import { hostname as osHostname } from "os";
|
|
117
|
+
import { isAbsolute, join } from "path";
|
|
118
|
+
function envToken(name) {
|
|
119
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
120
|
+
}
|
|
121
|
+
function clientTransportEnvKeys(name) {
|
|
122
|
+
const envSegment = envToken(name);
|
|
123
|
+
return {
|
|
124
|
+
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
125
|
+
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function credentialOverrideEnvKey(name) {
|
|
129
|
+
return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
|
|
130
|
+
}
|
|
131
|
+
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
|
|
132
|
+
function credentialPointerEnvKey(name) {
|
|
133
|
+
return `HASNA_${envToken(name)}_API_KEY_REF`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
class CredentialResolutionError extends Error {
|
|
137
|
+
appName;
|
|
138
|
+
attempted;
|
|
139
|
+
constructor(appName, message, attempted) {
|
|
140
|
+
super(message);
|
|
141
|
+
this.name = "CredentialResolutionError";
|
|
142
|
+
this.appName = appName;
|
|
143
|
+
this.attempted = attempted;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
class CredentialFileUnsafeError extends Error {
|
|
148
|
+
path;
|
|
149
|
+
constructor(path, reason) {
|
|
150
|
+
super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
|
|
151
|
+
this.name = "CredentialFileUnsafeError";
|
|
152
|
+
this.path = path;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
var HASNA_HOME_ENV_KEY = "HASNA_HOME";
|
|
156
|
+
var HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME";
|
|
157
|
+
var KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION";
|
|
158
|
+
var HASNA_HOME_DIR = ".hasna";
|
|
159
|
+
var CONFIG_SUBDIR = "config";
|
|
160
|
+
var CREDENTIALS_FILE = "credentials";
|
|
161
|
+
var KEYCHAIN_SECURITY_BIN = "/usr/bin/security";
|
|
162
|
+
var KEYCHAIN_SERVICE_PREFIX = "hasna.credentials";
|
|
163
|
+
var KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44;
|
|
164
|
+
var KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4;
|
|
165
|
+
var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
166
|
+
var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
167
|
+
var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
168
|
+
var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
169
|
+
var VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
|
|
170
|
+
function homeDir(env) {
|
|
171
|
+
const home = env.HOME?.trim();
|
|
172
|
+
return home ? home : null;
|
|
173
|
+
}
|
|
174
|
+
function absoluteOverride(env, key) {
|
|
175
|
+
const value = env[key]?.trim();
|
|
176
|
+
return value && isAbsolute(value) ? value : null;
|
|
177
|
+
}
|
|
178
|
+
function hasnaHomeDir(env) {
|
|
179
|
+
const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
|
|
180
|
+
if (override)
|
|
181
|
+
return override;
|
|
182
|
+
const home = homeDir(env);
|
|
183
|
+
return home ? join(home, HASNA_HOME_DIR) : null;
|
|
184
|
+
}
|
|
185
|
+
function appConfigDir(name, env) {
|
|
186
|
+
const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
|
|
187
|
+
if (configRoot)
|
|
188
|
+
return join(configRoot, name);
|
|
189
|
+
const root = hasnaHomeDir(env);
|
|
190
|
+
return root ? join(root, name, CONFIG_SUBDIR) : null;
|
|
191
|
+
}
|
|
192
|
+
function credentialDiskSourceList(name, env, profile = null) {
|
|
193
|
+
if (!SAFE_APP_SLUG.test(name))
|
|
194
|
+
return [];
|
|
195
|
+
const directory = appConfigDir(name, env);
|
|
196
|
+
if (!directory)
|
|
197
|
+
return [];
|
|
198
|
+
const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
|
|
199
|
+
return [{ path: join(directory, file), tier: "disk" }];
|
|
200
|
+
}
|
|
201
|
+
function credentialDiskSources(name, env) {
|
|
202
|
+
return credentialDiskSourceList(name, env, null).map((s) => s.path);
|
|
203
|
+
}
|
|
204
|
+
function profileDiskSources(name, env, profile) {
|
|
205
|
+
return credentialDiskSourceList(name, env, profile).map((s) => s.path);
|
|
206
|
+
}
|
|
207
|
+
function parseEnvFile(text) {
|
|
208
|
+
const values = new Map;
|
|
209
|
+
const unusable = new Set;
|
|
210
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
211
|
+
const line = rawLine.trim();
|
|
212
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
213
|
+
continue;
|
|
214
|
+
const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
|
215
|
+
const equals = withoutExport.indexOf("=");
|
|
216
|
+
if (equals <= 0)
|
|
217
|
+
continue;
|
|
218
|
+
const key = withoutExport.slice(0, equals).trim();
|
|
219
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
220
|
+
continue;
|
|
221
|
+
let value = withoutExport.slice(equals + 1).trim();
|
|
222
|
+
const quote = value[0];
|
|
223
|
+
if (quote === '"' || quote === "'") {
|
|
224
|
+
if (value.length < 2 || !value.endsWith(quote)) {
|
|
225
|
+
unusable.add(key);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
value = value.slice(1, -1);
|
|
229
|
+
}
|
|
230
|
+
if (value.trim().length === 0) {
|
|
231
|
+
unusable.add(key);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (values.has(key) && values.get(key) !== value)
|
|
235
|
+
unusable.add(key);
|
|
236
|
+
values.set(key, value);
|
|
237
|
+
}
|
|
238
|
+
return { values, unusable };
|
|
239
|
+
}
|
|
240
|
+
function configFileModeAllowed(mode) {
|
|
241
|
+
const permissions = mode & 4095;
|
|
242
|
+
return permissions === 256 || permissions === 384;
|
|
243
|
+
}
|
|
244
|
+
function configFileReadsCoherent(before, after) {
|
|
245
|
+
return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
|
|
246
|
+
}
|
|
247
|
+
function readAppConfigFile(path) {
|
|
248
|
+
const unsafe = (reason) => {
|
|
249
|
+
throw new CredentialFileUnsafeError(path, reason);
|
|
250
|
+
};
|
|
251
|
+
let fd = -1;
|
|
252
|
+
try {
|
|
253
|
+
fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
const code = error.code;
|
|
256
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
257
|
+
return null;
|
|
258
|
+
if (code === "ELOOP")
|
|
259
|
+
unsafe("the path is a symlink");
|
|
260
|
+
unsafe(`the path could not be opened (${code ?? "unknown error"})`);
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const before = fstatSync(fd);
|
|
264
|
+
if (!before.isFile())
|
|
265
|
+
unsafe("the path is not a regular file");
|
|
266
|
+
if (!configFileModeAllowed(before.mode)) {
|
|
267
|
+
unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
|
|
268
|
+
}
|
|
269
|
+
const uid = process.getuid?.() ?? process.geteuid?.();
|
|
270
|
+
if (uid !== undefined && before.uid !== uid)
|
|
271
|
+
unsafe("the file is not owned by the current user");
|
|
272
|
+
if (before.size > MAX_CREDENTIAL_FILE_BYTES)
|
|
273
|
+
unsafe("the file exceeds the size limit");
|
|
274
|
+
const bytes = readFileSync(fd);
|
|
275
|
+
const after = fstatSync(fd);
|
|
276
|
+
if (!configFileReadsCoherent(before, after)) {
|
|
277
|
+
unsafe("the file changed while being read");
|
|
278
|
+
}
|
|
279
|
+
return parseEnvFile(bytes.toString("utf8"));
|
|
280
|
+
} finally {
|
|
281
|
+
if (fd !== -1)
|
|
282
|
+
closeSync(fd);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function readCredentialFile(path, apiKeyKeys) {
|
|
286
|
+
const parsed = readAppConfigFile(path);
|
|
287
|
+
if (!parsed)
|
|
288
|
+
return null;
|
|
289
|
+
for (const key of apiKeyKeys) {
|
|
290
|
+
if (parsed.unusable.has(key)) {
|
|
291
|
+
throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
295
|
+
if (new Set(values).size > 1) {
|
|
296
|
+
throw new CredentialFileUnsafeError(path, "credential aliases disagree");
|
|
297
|
+
}
|
|
298
|
+
return values[0] ?? null;
|
|
299
|
+
}
|
|
300
|
+
var CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
|
|
301
|
+
function appConfigDiskValue(name, env, keys) {
|
|
302
|
+
const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
|
|
303
|
+
if (wanted.length === 0)
|
|
304
|
+
return null;
|
|
305
|
+
for (const path of credentialDiskSources(name, env)) {
|
|
306
|
+
const parsed = readAppConfigFile(path);
|
|
307
|
+
if (!parsed)
|
|
308
|
+
continue;
|
|
309
|
+
if (wanted.some((key) => parsed.unusable.has(key))) {
|
|
310
|
+
return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
|
|
311
|
+
}
|
|
312
|
+
const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
313
|
+
if (new Set(values).size > 1)
|
|
314
|
+
throw new CredentialFileUnsafeError(path, "configuration aliases disagree");
|
|
315
|
+
for (const key of wanted) {
|
|
316
|
+
if (parsed.unusable.has(key))
|
|
317
|
+
return { key, value: "", path, unusable: true };
|
|
318
|
+
const value = parsed.values.get(key)?.trim();
|
|
319
|
+
if (value)
|
|
320
|
+
return { key, value, path };
|
|
321
|
+
}
|
|
121
322
|
}
|
|
122
323
|
return null;
|
|
123
324
|
}
|
|
124
|
-
function
|
|
125
|
-
|
|
126
|
-
|
|
325
|
+
function assertUsableCredential(appName, source, value) {
|
|
326
|
+
if (VAULT_POINTER_SHAPE.test(value)) {
|
|
327
|
+
throw new CredentialResolutionError(appName, `The credential from ${source} looks like a secrets-vault pointer (a path-shaped reference like ` + `'namespace/app/live/api_key'). A vault path is NEVER accepted as a literal API key. ` + `Use ${credentialPointerEnvKey(appName)} to resolve the key through the vault, or provide the actual key value.`, [source]);
|
|
328
|
+
}
|
|
329
|
+
if (!ILLEGAL_IN_HEADER_VALUE.test(value))
|
|
127
330
|
return;
|
|
128
|
-
throw new
|
|
331
|
+
throw new CredentialResolutionError(appName, `The credential from ${source} contains characters that cannot be sent in an HTTP header ` + `(a control character or non-ASCII byte). A file written with CR-only line endings is the usual ` + `cause. Rewrite that credential file with one LF-terminated KEY=value line. ` + `The value is not shown here, and is deliberately never logged.`, [source]);
|
|
332
|
+
}
|
|
333
|
+
var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
334
|
+
var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
335
|
+
function sealCredential(fields) {
|
|
336
|
+
const { apiKey } = fields;
|
|
337
|
+
const visible = {
|
|
338
|
+
tier: fields.tier,
|
|
339
|
+
source: fields.source,
|
|
340
|
+
deliberate: fields.deliberate,
|
|
341
|
+
diskCandidates: Object.freeze([...fields.diskCandidates]),
|
|
342
|
+
warning: fields.warning
|
|
343
|
+
};
|
|
344
|
+
const sealed = { ...visible };
|
|
345
|
+
Object.defineProperty(sealed, "apiKey", {
|
|
346
|
+
value: apiKey,
|
|
347
|
+
enumerable: false,
|
|
348
|
+
writable: false,
|
|
349
|
+
configurable: false
|
|
350
|
+
});
|
|
351
|
+
if (fields.pointerVaultKey !== undefined) {
|
|
352
|
+
Object.defineProperty(sealed, "pointerVaultKey", {
|
|
353
|
+
value: fields.pointerVaultKey,
|
|
354
|
+
enumerable: false,
|
|
355
|
+
writable: false,
|
|
356
|
+
configurable: false
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
Object.defineProperty(sealed, INSPECT_CUSTOM, {
|
|
360
|
+
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
361
|
+
enumerable: false,
|
|
362
|
+
writable: false,
|
|
363
|
+
configurable: false
|
|
364
|
+
});
|
|
365
|
+
Object.defineProperty(sealed, CREDENTIAL_SEAL, {
|
|
366
|
+
value: true,
|
|
367
|
+
enumerable: false,
|
|
368
|
+
writable: false,
|
|
369
|
+
configurable: false
|
|
370
|
+
});
|
|
371
|
+
return Object.freeze(sealed);
|
|
372
|
+
}
|
|
373
|
+
function firstEnvValue(env, keys) {
|
|
374
|
+
for (const key of keys) {
|
|
375
|
+
if (!Object.prototype.hasOwnProperty.call(env, key))
|
|
376
|
+
continue;
|
|
377
|
+
const value = env[key]?.trim();
|
|
378
|
+
if (value)
|
|
379
|
+
return { key, value };
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
384
|
+
function isAmbientEnvironment(env) {
|
|
385
|
+
return env === process.env || env[AMBIENT_ENVIRONMENT] === true;
|
|
386
|
+
}
|
|
387
|
+
function defaultKeychainRunner(argv) {
|
|
388
|
+
const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {
|
|
389
|
+
encoding: "utf8",
|
|
390
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
391
|
+
timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
|
|
392
|
+
});
|
|
393
|
+
return {
|
|
394
|
+
status: result.status,
|
|
395
|
+
stdout: result.stdout ?? "",
|
|
396
|
+
stderr: result.error ? result.error.message : result.stderr ?? ""
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function keychainTierEnabled(env, options) {
|
|
400
|
+
if ((options.platform ?? process.platform) !== "darwin")
|
|
401
|
+
return false;
|
|
402
|
+
if (options.enabled !== undefined)
|
|
403
|
+
return options.enabled;
|
|
404
|
+
return options.run !== undefined || isAmbientEnvironment(env);
|
|
405
|
+
}
|
|
406
|
+
function keychainAccount(env, options) {
|
|
407
|
+
const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
|
|
408
|
+
if (station)
|
|
409
|
+
return station;
|
|
410
|
+
const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
|
|
411
|
+
if (host)
|
|
412
|
+
return host;
|
|
413
|
+
const user = env.USER?.trim();
|
|
414
|
+
return user || null;
|
|
415
|
+
}
|
|
416
|
+
function keychainFailureHint(text) {
|
|
417
|
+
const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
|
|
418
|
+
const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
|
|
419
|
+
return clean ? `: ${clean}` : "";
|
|
420
|
+
}
|
|
421
|
+
function readKeychainItem(name, env, kind, options) {
|
|
422
|
+
if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
|
|
423
|
+
return null;
|
|
424
|
+
const account = keychainAccount(env, options);
|
|
425
|
+
if (!account)
|
|
426
|
+
return null;
|
|
427
|
+
const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
|
|
428
|
+
const source = `keychain:${service}@${account}`;
|
|
429
|
+
const run = options.run ?? defaultKeychainRunner;
|
|
430
|
+
let result;
|
|
431
|
+
try {
|
|
432
|
+
result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
|
|
435
|
+
throw new CredentialResolutionError(name, `The Keychain lookup for ${source} could not run${reason}. A Keychain failure is never resolved ` + `around: fix the keychain, or delete the item to fall through to the credential on disk.`, [source]);
|
|
436
|
+
}
|
|
437
|
+
if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
|
|
438
|
+
return null;
|
|
439
|
+
if (result.status !== 0) {
|
|
440
|
+
throw new CredentialResolutionError(name, `The Keychain lookup for ${source} failed (security exited ` + `${result.status ?? "without a status"}${keychainFailureHint(result.stderr)}). A Keychain item that ` + `exists but cannot be read is never resolved around: unlock the keychain, run from a session that ` + `may use it, or delete the item to fall through to the credential on disk.`, [source]);
|
|
441
|
+
}
|
|
442
|
+
const value = result.stdout.trim();
|
|
443
|
+
if (!value) {
|
|
444
|
+
throw new CredentialResolutionError(name, `${source} exists but holds an empty value; a declared item never falls through to another ` + `identity. Store a value in it or delete the item.`, [source]);
|
|
445
|
+
}
|
|
446
|
+
return { value, source };
|
|
447
|
+
}
|
|
448
|
+
function keychainConfigValue(name, env, options = {}) {
|
|
449
|
+
return readKeychainItem(name, env, "api-url", options);
|
|
450
|
+
}
|
|
451
|
+
function snapshotClientEnvironment(name, env) {
|
|
452
|
+
const keys = clientTransportEnvKeys(name);
|
|
453
|
+
const ambient = isAmbientEnvironment(env);
|
|
454
|
+
const snapshot = Object.create(null);
|
|
455
|
+
for (const key of [
|
|
456
|
+
...keys.apiUrlKeys,
|
|
457
|
+
...keys.apiKeyKeys,
|
|
458
|
+
credentialOverrideEnvKey(name),
|
|
459
|
+
credentialPointerEnvKey(name),
|
|
460
|
+
CREDENTIAL_PROFILE_ENV_KEY,
|
|
461
|
+
"HOME",
|
|
462
|
+
HASNA_HOME_ENV_KEY,
|
|
463
|
+
HASNA_CONFIG_HOME_ENV_KEY,
|
|
464
|
+
KEYCHAIN_STATION_ENV_KEY,
|
|
465
|
+
"USER"
|
|
466
|
+
]) {
|
|
467
|
+
const descriptor = Object.getOwnPropertyDescriptor(env, key);
|
|
468
|
+
if (!descriptor)
|
|
469
|
+
continue;
|
|
470
|
+
if (!("value" in descriptor)) {
|
|
471
|
+
throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
|
|
472
|
+
}
|
|
473
|
+
if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
|
|
474
|
+
throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
|
|
475
|
+
}
|
|
476
|
+
snapshot[key] = descriptor.value;
|
|
477
|
+
}
|
|
478
|
+
if (ambient) {
|
|
479
|
+
Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT, {
|
|
480
|
+
value: true,
|
|
481
|
+
enumerable: false,
|
|
482
|
+
writable: false,
|
|
483
|
+
configurable: false
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
return Object.freeze(snapshot);
|
|
487
|
+
}
|
|
488
|
+
function resolveCredential(name, env, options = {}) {
|
|
489
|
+
env = snapshotClientEnvironment(name, env);
|
|
490
|
+
const { apiKeyKeys } = clientTransportEnvKeys(name);
|
|
491
|
+
const diskPaths = credentialDiskSources(name, env);
|
|
492
|
+
if (options.apiKey !== undefined) {
|
|
493
|
+
const explicitKey = options.apiKey.trim();
|
|
494
|
+
if (!explicitKey) {
|
|
495
|
+
throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
|
|
496
|
+
}
|
|
497
|
+
assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
|
|
498
|
+
return sealCredential({
|
|
499
|
+
apiKey: explicitKey,
|
|
500
|
+
tier: "argument",
|
|
501
|
+
source: "explicit apiKey argument",
|
|
502
|
+
deliberate: true,
|
|
503
|
+
diskCandidates: diskPaths,
|
|
504
|
+
warning: null
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
const overrideKeyName = credentialOverrideEnvKey(name);
|
|
508
|
+
const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
|
|
509
|
+
if (overrideRaw !== undefined) {
|
|
510
|
+
const override = overrideRaw.trim();
|
|
511
|
+
if (!override) {
|
|
512
|
+
throw new CredentialResolutionError(name, `${overrideKeyName} is set but empty. It is a deliberate override, so it is not resolved around: ` + `either give it a real key or unset it to fall back to the credential on disk.`, [overrideKeyName]);
|
|
513
|
+
}
|
|
514
|
+
assertUsableCredential(name, overrideKeyName, override);
|
|
515
|
+
return sealCredential({
|
|
516
|
+
apiKey: override,
|
|
517
|
+
tier: "override",
|
|
518
|
+
source: overrideKeyName,
|
|
519
|
+
deliberate: true,
|
|
520
|
+
diskCandidates: diskPaths,
|
|
521
|
+
warning: null
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
const pointerKeyName = credentialPointerEnvKey(name);
|
|
525
|
+
const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
|
|
526
|
+
if (pointerRaw !== undefined) {
|
|
527
|
+
const pointer = pointerRaw.trim();
|
|
528
|
+
if (!pointer) {
|
|
529
|
+
throw new CredentialResolutionError(name, `${pointerKeyName} is set but empty. It is a deliberate vault pointer, so it is not resolved around: ` + `either give it a vault item key or unset it to fall back to the credential on disk.`, [pointerKeyName]);
|
|
530
|
+
}
|
|
531
|
+
if (!VAULT_POINTER_SHAPE.test(pointer)) {
|
|
532
|
+
throw new CredentialResolutionError(name, `${pointerKeyName} must name a vault ITEM KEY (a path-shaped reference like ` + `'namespace/app/live/api_key'), not a credential value. A pointer that carries a literal is refused.`, [pointerKeyName]);
|
|
533
|
+
}
|
|
534
|
+
return sealCredential({
|
|
535
|
+
apiKey: "",
|
|
536
|
+
pointerVaultKey: pointer,
|
|
537
|
+
tier: "pointer",
|
|
538
|
+
source: pointerKeyName,
|
|
539
|
+
deliberate: true,
|
|
540
|
+
diskCandidates: diskPaths,
|
|
541
|
+
warning: null
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
if (options.profile !== undefined && !options.profile.trim()) {
|
|
545
|
+
throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
|
|
546
|
+
}
|
|
547
|
+
const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY) ? env[CREDENTIAL_PROFILE_ENV_KEY] : undefined;
|
|
548
|
+
if (profileRaw !== undefined && !profileRaw.trim()) {
|
|
549
|
+
throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY]);
|
|
550
|
+
}
|
|
551
|
+
const profile = options.profile?.trim() || profileRaw?.trim();
|
|
552
|
+
if (profile) {
|
|
553
|
+
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
|
|
554
|
+
if (!SAFE_PROFILE.test(profile)) {
|
|
555
|
+
throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
|
|
556
|
+
}
|
|
557
|
+
const paths = profileDiskSources(name, env, profile);
|
|
558
|
+
for (const path of paths) {
|
|
559
|
+
const value = readCredentialFile(path, apiKeyKeys);
|
|
560
|
+
if (value) {
|
|
561
|
+
assertUsableCredential(name, path, value);
|
|
562
|
+
return sealCredential({
|
|
563
|
+
apiKey: value,
|
|
564
|
+
tier: "profile",
|
|
565
|
+
source: path,
|
|
566
|
+
deliberate: true,
|
|
567
|
+
diskCandidates: paths,
|
|
568
|
+
warning: null
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
|
|
573
|
+
}
|
|
574
|
+
const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
|
|
575
|
+
const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
|
|
576
|
+
if (blankEnv) {
|
|
577
|
+
throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
|
|
578
|
+
}
|
|
579
|
+
if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
|
|
580
|
+
throw new CredentialResolutionError(name, `${definedEnvEntries.map((entry) => entry.key).join(" and ")} disagree; credential aliases must be identical or only one may be set.`, definedEnvEntries.map((entry) => entry.key));
|
|
581
|
+
}
|
|
582
|
+
const envHit = firstEnvValue(env, apiKeyKeys);
|
|
583
|
+
const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
|
|
584
|
+
if (keychainHit) {
|
|
585
|
+
assertUsableCredential(name, keychainHit.source, keychainHit.value);
|
|
586
|
+
const warning = envHit && envHit.value !== keychainHit.value ? `Credential sources disagree for '${name}': ${keychainHit.source} and ${envHit.key} hold ` + `different keys. ${keychainHit.source} wins, because the Keychain is re-read on every call while ` + `an environment variable is a snapshot. Reconcile them \u2014 a rotation that updated only one leaves ` + `the other to fail 401 wherever it is loaded first.` : null;
|
|
587
|
+
return sealCredential({
|
|
588
|
+
apiKey: keychainHit.value,
|
|
589
|
+
tier: "keychain",
|
|
590
|
+
source: keychainHit.source,
|
|
591
|
+
deliberate: false,
|
|
592
|
+
diskCandidates: diskPaths,
|
|
593
|
+
warning
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
597
|
+
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
598
|
+
if (diskHits.length > 0) {
|
|
599
|
+
const winner = diskHits[0];
|
|
600
|
+
assertUsableCredential(name, winner.src.path, winner.value);
|
|
601
|
+
const divergentSources = [
|
|
602
|
+
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
603
|
+
...envHit && envHit.value !== winner.value ? [envHit.key] : []
|
|
604
|
+
];
|
|
605
|
+
const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.src.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.src.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
|
|
606
|
+
return sealCredential({
|
|
607
|
+
apiKey: winner.value,
|
|
608
|
+
tier: winner.src.tier,
|
|
609
|
+
source: winner.src.path,
|
|
610
|
+
deliberate: false,
|
|
611
|
+
diskCandidates: diskPaths,
|
|
612
|
+
warning
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
if (envHit) {
|
|
616
|
+
assertUsableCredential(name, envHit.key, envHit.value);
|
|
617
|
+
return sealCredential({
|
|
618
|
+
apiKey: envHit.value,
|
|
619
|
+
tier: "env",
|
|
620
|
+
source: envHit.key,
|
|
621
|
+
deliberate: false,
|
|
622
|
+
diskCandidates: diskPaths,
|
|
623
|
+
warning: null
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
629
|
+
var requireSecretsSdk = createRequire(import.meta.url);
|
|
630
|
+
var DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com";
|
|
631
|
+
var DEFAULT_AUTHORITY_SOURCE = "default";
|
|
632
|
+
function defaultFleetGatewayBaseUrl(name) {
|
|
633
|
+
return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
|
|
634
|
+
}
|
|
635
|
+
var ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
636
|
+
var DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
637
|
+
function isValidDnsDomain(value) {
|
|
638
|
+
if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
|
|
642
|
+
}
|
|
643
|
+
function validateAppSlug(name) {
|
|
644
|
+
if (name.length > 63 || !DNS_LABEL_PATTERN.test(name)) {
|
|
645
|
+
throw new Error("App name must be one lowercase DNS label.");
|
|
646
|
+
}
|
|
647
|
+
return name;
|
|
648
|
+
}
|
|
649
|
+
function rawAuthority(value) {
|
|
650
|
+
const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
|
|
651
|
+
if (!match)
|
|
652
|
+
throw new Error("API URL must be absolute.");
|
|
653
|
+
const afterScheme = value.slice(match[0].length);
|
|
654
|
+
const boundary = afterScheme.search(/[/?#]/);
|
|
655
|
+
const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
|
|
656
|
+
if (!authority)
|
|
657
|
+
throw new Error("API URL must include a hostname.");
|
|
658
|
+
return authority;
|
|
659
|
+
}
|
|
660
|
+
function assertCanonicalPort(port) {
|
|
661
|
+
if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
|
|
662
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
663
|
+
}
|
|
664
|
+
const numericPort = Number(port);
|
|
665
|
+
if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
|
|
666
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function canonicalAuthorityHostname(authority) {
|
|
670
|
+
let rawHostname;
|
|
671
|
+
if (authority.startsWith("[")) {
|
|
672
|
+
const closingBracket = authority.indexOf("]");
|
|
673
|
+
if (closingBracket === -1) {
|
|
674
|
+
throw new Error("API URL authority must contain a canonical hostname.");
|
|
675
|
+
}
|
|
676
|
+
rawHostname = authority.slice(0, closingBracket + 1);
|
|
677
|
+
const portSuffix = authority.slice(closingBracket + 1);
|
|
678
|
+
if (portSuffix) {
|
|
679
|
+
if (!portSuffix.startsWith(":")) {
|
|
680
|
+
throw new Error("API URL authority must contain a canonical hostname and port.");
|
|
681
|
+
}
|
|
682
|
+
assertCanonicalPort(portSuffix.slice(1));
|
|
683
|
+
}
|
|
684
|
+
if (isIP(rawHostname.slice(1, -1)) !== 6) {
|
|
685
|
+
throw new Error("API URL authority must contain a canonical IPv6 literal.");
|
|
686
|
+
}
|
|
687
|
+
} else {
|
|
688
|
+
const firstColon = authority.indexOf(":");
|
|
689
|
+
const lastColon = authority.lastIndexOf(":");
|
|
690
|
+
if (firstColon !== lastColon) {
|
|
691
|
+
throw new Error("IPv6 API URL authorities must use brackets.");
|
|
692
|
+
}
|
|
693
|
+
if (lastColon !== -1) {
|
|
694
|
+
const port = authority.slice(lastColon + 1);
|
|
695
|
+
assertCanonicalPort(port);
|
|
696
|
+
rawHostname = authority.slice(0, lastColon);
|
|
697
|
+
} else {
|
|
698
|
+
rawHostname = authority;
|
|
699
|
+
}
|
|
700
|
+
const ipVersion = isIP(rawHostname);
|
|
701
|
+
const numericAddressParts = rawHostname.split(".");
|
|
702
|
+
const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
|
|
703
|
+
if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
|
|
704
|
+
throw new Error("API URL authority must contain a canonical ASCII hostname.");
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return rawHostname.toLowerCase();
|
|
708
|
+
}
|
|
709
|
+
function isDeliberateLoopbackHttpAuthority(authority) {
|
|
710
|
+
return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
|
|
711
|
+
}
|
|
712
|
+
function toV1BaseUrl(apiUrl) {
|
|
713
|
+
if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
|
|
714
|
+
throw new Error("API URL must not contain ASCII control characters.");
|
|
715
|
+
}
|
|
716
|
+
const input = apiUrl.trim();
|
|
717
|
+
const authority = rawAuthority(input);
|
|
718
|
+
if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
|
|
719
|
+
throw new Error("API URL authority must be canonical ASCII without credentials.");
|
|
720
|
+
}
|
|
721
|
+
const canonicalHostname = canonicalAuthorityHostname(authority);
|
|
722
|
+
const url = new URL(input);
|
|
723
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
724
|
+
throw new Error("API URL must use http or https.");
|
|
725
|
+
}
|
|
726
|
+
if (url.username || url.password) {
|
|
727
|
+
throw new Error("API URL must not include credentials.");
|
|
728
|
+
}
|
|
729
|
+
if (!url.hostname || url.hostname.endsWith(".")) {
|
|
730
|
+
throw new Error("API URL must include a canonical hostname.");
|
|
731
|
+
}
|
|
732
|
+
if (url.hostname.toLowerCase() !== canonicalHostname) {
|
|
733
|
+
throw new Error("API URL authority must not rely on parser hostname normalization.");
|
|
734
|
+
}
|
|
735
|
+
if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
|
|
736
|
+
throw new Error("API URL must not use IDN or punycode hostnames.");
|
|
737
|
+
}
|
|
738
|
+
if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
|
|
739
|
+
throw new Error("API URL may use http only for an exact loopback authority.");
|
|
740
|
+
}
|
|
741
|
+
if (url.search || url.hash) {
|
|
742
|
+
throw new Error("API URL must not include a query string or fragment.");
|
|
743
|
+
}
|
|
744
|
+
let path = url.pathname.replace(/\/+$/, "");
|
|
745
|
+
if (path.endsWith("/v1"))
|
|
746
|
+
path = path.slice(0, -"/v1".length);
|
|
747
|
+
url.pathname = `${path}/v1`;
|
|
748
|
+
return url.toString().replace(/\/+$/, "");
|
|
749
|
+
}
|
|
750
|
+
class ClientTransportConfigurationError extends Error {
|
|
751
|
+
appName;
|
|
752
|
+
sources;
|
|
753
|
+
constructor(appName, message, sources = []) {
|
|
754
|
+
super(message);
|
|
755
|
+
this.name = "ClientTransportConfigurationError";
|
|
756
|
+
this.appName = appName;
|
|
757
|
+
this.sources = Object.freeze([...sources]);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
|
|
761
|
+
env = snapshotClientEnvironment(name, env);
|
|
762
|
+
const keys = clientTransportEnvKeys(name);
|
|
763
|
+
const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
|
|
764
|
+
const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
|
|
765
|
+
if (blankUrl) {
|
|
766
|
+
throw new ClientTransportConfigurationError(name, `${blankUrl.key} is set but blank; public clients require an explicit HTTPS API URL and never select local storage.`, [blankUrl.key]);
|
|
767
|
+
}
|
|
768
|
+
const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
|
|
769
|
+
if (controlledUrl) {
|
|
770
|
+
throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
|
|
771
|
+
}
|
|
772
|
+
const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
|
|
773
|
+
if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
|
|
774
|
+
throw new ClientTransportConfigurationError(name, `${usableUrlEntries.map((entry) => entry.key).join(" and ")} disagree; client authority aliases must be identical or only one may be set.`, usableUrlEntries.map((entry) => entry.key));
|
|
775
|
+
}
|
|
776
|
+
const envUrlHit = usableUrlEntries[0] ?? null;
|
|
777
|
+
const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
|
|
778
|
+
const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
|
|
779
|
+
if (diskConfigUrlHit?.unusable) {
|
|
780
|
+
throw new ClientTransportConfigurationError(name, `${diskConfigUrlHit.key} in ${diskConfigUrlHit.path} is declared but blank or malformed; public clients require a valid HTTPS service authority.`, [diskConfigUrlHit.path]);
|
|
781
|
+
}
|
|
782
|
+
const urlCandidates = [
|
|
783
|
+
...envUrlHit ? [envUrlHit] : [],
|
|
784
|
+
...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
|
|
785
|
+
...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
|
|
786
|
+
];
|
|
787
|
+
const configuredUrl = urlCandidates[0] ?? null;
|
|
788
|
+
const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
|
|
789
|
+
if (configuredUrl && divergentUrls.length > 0) {
|
|
790
|
+
throw new ClientTransportConfigurationError(name, `${configuredUrl.key} and ${divergentUrls.map((candidate) => candidate.key).join(" and ")} select different service authorities; refusing to send a credential written for one authority to the other.`, urlCandidates.map((candidate) => candidate.key));
|
|
791
|
+
}
|
|
792
|
+
const warnings = [];
|
|
793
|
+
if (configuredUrl && !envUrlHit) {
|
|
794
|
+
warnings.push(`No ${keys.apiUrlKeys[0]} in the environment; the server URL in ${configuredUrl.key} was used, so this client connects to the server. ` + `Keep that entry aligned with the intended service authority.`);
|
|
795
|
+
}
|
|
796
|
+
const credential = resolveCredential(name, env, options.credentials);
|
|
797
|
+
if (!credential) {
|
|
798
|
+
const diskHint = credentialDiskSourcesForMessage(name, env);
|
|
799
|
+
const lead = configuredUrl ? `${configuredUrl.key} selects the HTTP server for '${name}', but no API key could be resolved` : `${keys.apiUrlKeys[0]} is not set and no API key could be resolved for '${name}'; a credential is required before the default fleet gateway authority applies`;
|
|
800
|
+
warnings.push(`${lead}; refusing to create an unauthenticated client \u2014 public clients never fall back to SQLite or another local store. ` + `Looked in the Keychain (macOS only), then for a credential file at ${diskHint}, then for ${keys.apiKeyKeys[0]} in the environment.`);
|
|
801
|
+
throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
|
|
802
|
+
}
|
|
803
|
+
if (credential.warning)
|
|
804
|
+
warnings.push(credential.warning);
|
|
805
|
+
let urlHit;
|
|
806
|
+
if (configuredUrl) {
|
|
807
|
+
urlHit = configuredUrl;
|
|
808
|
+
} else {
|
|
809
|
+
try {
|
|
810
|
+
urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
|
|
811
|
+
} catch (error) {
|
|
812
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
813
|
+
throw new ClientTransportConfigurationError(name, `No ${keys.apiUrlKeys[0]} is configured and the default fleet gateway authority cannot be composed for '${name}': ${message}`, [keys.apiUrlKeys[0]]);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
const apiUrlSource = urlHit.key;
|
|
817
|
+
let baseUrl;
|
|
818
|
+
try {
|
|
819
|
+
baseUrl = toV1BaseUrl(urlHit.value);
|
|
820
|
+
} catch (error) {
|
|
821
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
822
|
+
throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
|
|
823
|
+
}
|
|
824
|
+
return {
|
|
825
|
+
resolution: {
|
|
826
|
+
transport: "http",
|
|
827
|
+
transportSource: urlHit.key,
|
|
828
|
+
baseUrl,
|
|
829
|
+
apiUrlSource,
|
|
830
|
+
apiKeyPresent: true,
|
|
831
|
+
apiKeySource: credential.source,
|
|
832
|
+
apiKeyTier: credential.tier,
|
|
833
|
+
misconfigured: false,
|
|
834
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
835
|
+
},
|
|
836
|
+
credential
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
function resolveClientTransport(name, env = process.env, options = {}) {
|
|
840
|
+
return resolveClientTransportSnapshot(name, env, options).resolution;
|
|
841
|
+
}
|
|
842
|
+
function credentialDiskSourcesForMessage(name, env) {
|
|
843
|
+
const paths = credentialDiskSources(name, env);
|
|
844
|
+
return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
|
|
845
|
+
}
|
|
846
|
+
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
847
|
+
var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
848
|
+
"host",
|
|
849
|
+
":authority",
|
|
850
|
+
"forwarded",
|
|
851
|
+
"x-forwarded-host",
|
|
852
|
+
"x-original-host"
|
|
853
|
+
]);
|
|
854
|
+
|
|
855
|
+
// src/lib/local-opt-in.ts
|
|
856
|
+
var INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_INSTRUCTIONS_LOCAL"];
|
|
857
|
+
function instructionsLocalModeNotice() {
|
|
858
|
+
return `instructions: local mode \u2014 ${INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS[0]}=1 selects the on-box SQLite store, ` + `and no hosted authority was configured. Set HASNA_INSTRUCTIONS_API_KEY (or add the Keychain item ` + `hasna.credentials.instructions.api-key, or write ~/.hasna/instructions/config/credentials) to go hosted.`;
|
|
859
|
+
}
|
|
860
|
+
function isInstructionsLocalOptIn(env = process.env) {
|
|
861
|
+
return INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env[key] ?? "").trim() === "1");
|
|
862
|
+
}
|
|
863
|
+
function instructionsAuthorityEnvKeys() {
|
|
864
|
+
const keys = clientTransportEnvKeys("instructions");
|
|
865
|
+
return [
|
|
866
|
+
...keys.apiUrlKeys,
|
|
867
|
+
...keys.apiKeyKeys,
|
|
868
|
+
credentialOverrideEnvKey("instructions"),
|
|
869
|
+
credentialPointerEnvKey("instructions"),
|
|
870
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
871
|
+
];
|
|
872
|
+
}
|
|
873
|
+
function hasInstructionsEnvAuthorityIntent(env = process.env) {
|
|
874
|
+
return instructionsAuthorityEnvKeys().some((key) => (env[key] ?? "").trim() !== "");
|
|
875
|
+
}
|
|
876
|
+
function selectsInstructionsLocalStore(env = process.env) {
|
|
877
|
+
return !hasInstructionsEnvAuthorityIntent(env) && isInstructionsLocalOptIn(env);
|
|
878
|
+
}
|
|
879
|
+
function instructionsResolverEnv(env) {
|
|
880
|
+
const blanks = instructionsAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
|
|
881
|
+
if (blanks.length === 0)
|
|
882
|
+
return env;
|
|
883
|
+
const next = { ...env };
|
|
884
|
+
for (const key of blanks)
|
|
885
|
+
delete next[key];
|
|
886
|
+
return next;
|
|
887
|
+
}
|
|
888
|
+
var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
889
|
+
function isAmbientInstructionsEnv(env) {
|
|
890
|
+
if (typeof process !== "undefined" && env === process.env)
|
|
891
|
+
return true;
|
|
892
|
+
return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
|
|
893
|
+
}
|
|
894
|
+
function instructionsResolverInputs(env, credentials = {}) {
|
|
895
|
+
const normalised = instructionsResolverEnv(env);
|
|
896
|
+
if (normalised === env)
|
|
897
|
+
return { env: normalised, credentials };
|
|
898
|
+
const keychain = { ...credentials.keychain };
|
|
899
|
+
if (keychain.enabled === undefined && keychain.run === undefined) {
|
|
900
|
+
keychain.enabled = isAmbientInstructionsEnv(env);
|
|
901
|
+
}
|
|
902
|
+
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
129
903
|
}
|
|
130
904
|
|
|
131
905
|
// src/lib/raw-store-root.ts
|
|
@@ -133,87 +907,77 @@ import { resolve as resolve2 } from "path";
|
|
|
133
907
|
|
|
134
908
|
// src/lib/app-home.ts
|
|
135
909
|
import { existsSync } from "fs";
|
|
136
|
-
import { homedir as homedir2 } from "os";
|
|
137
|
-
import { join as join2, resolve } from "path";
|
|
138
|
-
|
|
139
|
-
// ../../node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
|
|
140
910
|
import { homedir } from "os";
|
|
141
|
-
import { join } from "path";
|
|
142
|
-
|
|
911
|
+
import { join as join2, resolve } from "path";
|
|
912
|
+
import { homedir as pathsResolverHomedir } from "os";
|
|
913
|
+
import { join as pathsResolverJoin } from "path";
|
|
914
|
+
var PATHS_RESOLVER_KIND_ENV = {
|
|
143
915
|
config: "HASNA_CONFIG_HOME",
|
|
144
916
|
data: "HASNA_DATA_HOME",
|
|
145
917
|
state: "HASNA_STATE_HOME",
|
|
146
918
|
cache: "HASNA_CACHE_HOME"
|
|
147
919
|
};
|
|
148
|
-
var
|
|
149
|
-
function
|
|
920
|
+
var PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
921
|
+
function pathsResolverAssertApp(app) {
|
|
150
922
|
if (typeof app !== "string" || app.length === 0) {
|
|
151
923
|
throw new TypeError("paths: app must be a non-empty string");
|
|
152
924
|
}
|
|
153
|
-
if (!
|
|
925
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
|
|
154
926
|
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
155
927
|
}
|
|
156
928
|
}
|
|
157
|
-
function
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const value = envOf(options)[KIND_ENV[kind]];
|
|
162
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
163
|
-
}
|
|
164
|
-
function isMacOS(platform) {
|
|
165
|
-
return platform === "darwin";
|
|
929
|
+
function pathsResolverAssertKind(kind) {
|
|
930
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
|
|
931
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
|
|
932
|
+
}
|
|
166
933
|
}
|
|
167
|
-
function
|
|
168
|
-
|
|
169
|
-
|
|
934
|
+
function pathsResolverBaseDir(kind, options) {
|
|
935
|
+
pathsResolverAssertKind(kind);
|
|
936
|
+
const env = options.env ?? process.env;
|
|
937
|
+
const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
|
|
938
|
+
if (typeof override === "string" && override.length > 0)
|
|
170
939
|
return override;
|
|
171
|
-
const home = options.home ??
|
|
940
|
+
const home = options.home ?? pathsResolverHomedir();
|
|
172
941
|
const platform = options.platform ?? process.platform;
|
|
173
|
-
if (
|
|
942
|
+
if (platform === "darwin") {
|
|
174
943
|
switch (kind) {
|
|
175
944
|
case "config":
|
|
176
945
|
case "data":
|
|
177
|
-
return
|
|
946
|
+
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
178
947
|
case "cache":
|
|
179
|
-
return
|
|
948
|
+
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
180
949
|
case "state":
|
|
181
|
-
return
|
|
950
|
+
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
182
951
|
}
|
|
183
952
|
}
|
|
184
953
|
switch (kind) {
|
|
185
954
|
case "config":
|
|
186
|
-
return
|
|
955
|
+
return pathsResolverJoin(home, ".config", "hasna");
|
|
187
956
|
case "data":
|
|
188
|
-
return
|
|
957
|
+
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
189
958
|
case "state":
|
|
190
|
-
return
|
|
959
|
+
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
191
960
|
case "cache":
|
|
192
|
-
return
|
|
961
|
+
return pathsResolverJoin(home, ".cache", "hasna");
|
|
193
962
|
}
|
|
194
963
|
}
|
|
195
|
-
function
|
|
196
|
-
|
|
197
|
-
const appSegment = options.internal === true ?
|
|
198
|
-
return
|
|
964
|
+
function pathsResolverResolve(kind, options) {
|
|
965
|
+
pathsResolverAssertApp(options.app);
|
|
966
|
+
const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
|
|
967
|
+
return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
|
|
199
968
|
}
|
|
200
969
|
function configDir(options) {
|
|
201
|
-
return
|
|
970
|
+
return pathsResolverResolve("config", options);
|
|
202
971
|
}
|
|
203
|
-
function stateDir(options) {
|
|
204
|
-
return resolvePath("state", options);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// src/lib/app-home.ts
|
|
208
972
|
var HASNA_CONFIGS_HOME_ENV = "HASNA_CONFIGS_HOME";
|
|
209
|
-
function
|
|
210
|
-
return env["HOME"] || env["USERPROFILE"] ||
|
|
973
|
+
function homeDir2(env = process.env) {
|
|
974
|
+
return env["HOME"] || env["USERPROFILE"] || homedir();
|
|
211
975
|
}
|
|
212
976
|
function legacyStoreHome(env = process.env) {
|
|
213
|
-
return resolve(join2(
|
|
977
|
+
return resolve(join2(homeDir2(env), ".hasna", "instructions"));
|
|
214
978
|
}
|
|
215
979
|
function resolverStoreHome(env = process.env) {
|
|
216
|
-
return configDir({ app: "configs", env, home: env.HOME || env.USERPROFILE ||
|
|
980
|
+
return configDir({ app: "configs", env, home: env.HOME || env.USERPROFILE || homedir() });
|
|
217
981
|
}
|
|
218
982
|
function adoptResolverStoreHome(resolved, env = process.env) {
|
|
219
983
|
const override = env.HASNA_CONFIG_HOME;
|
|
@@ -343,9 +1107,8 @@ var _db = null;
|
|
|
343
1107
|
function getDatabase(path) {
|
|
344
1108
|
if (_db)
|
|
345
1109
|
return _db;
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
throw new Error("instructions is using the HTTP API transport (HASNA_INSTRUCTIONS_API_URL set): this command is not wired to the API yet. " + "Unset HASNA_INSTRUCTIONS_API_URL / HASNA_INSTRUCTIONS_API_KEY to use it against the local store.");
|
|
1110
|
+
if (!path && hasInstructionsEnvAuthorityIntent(process.env)) {
|
|
1111
|
+
throw new Error("instructions is using the hosted API transport (a HASNA_INSTRUCTIONS_* credential is configured): this command is not wired to the API yet. " + "Point this run at the local store (HASNA_INSTRUCTIONS_LOCAL=1 with no hosted credential) to use it against the local SQLite store.");
|
|
349
1112
|
}
|
|
350
1113
|
const dbPath = path || getDbPath();
|
|
351
1114
|
const db = new Database(dbPath);
|
|
@@ -642,7 +1405,7 @@ function getConfigStats(db) {
|
|
|
642
1405
|
}
|
|
643
1406
|
|
|
644
1407
|
// src/lib/machine.ts
|
|
645
|
-
import { arch as currentArch, homedir as
|
|
1408
|
+
import { arch as currentArch, homedir as homedir2, hostname as currentHostname, type as currentOsType } from "os";
|
|
646
1409
|
import { existsSync as existsSync3 } from "fs";
|
|
647
1410
|
import { join as join4 } from "path";
|
|
648
1411
|
|
|
@@ -717,10 +1480,10 @@ function normalizeOsFamily(os) {
|
|
|
717
1480
|
return value || "unknown";
|
|
718
1481
|
}
|
|
719
1482
|
function detectMachineContext(overrides = {}) {
|
|
720
|
-
const
|
|
1483
|
+
const homeDir3 = overrides.home_dir ?? process.env["CONFIGS_HOME"] ?? process.env["HOME"] ?? homedir2();
|
|
721
1484
|
const os = overrides.os ?? currentOsType();
|
|
722
1485
|
const osFamily = normalizeOsFamily(os);
|
|
723
|
-
const bunBinDir = overrides.bun_bin_dir ?? join4(
|
|
1486
|
+
const bunBinDir = overrides.bun_bin_dir ?? join4(homeDir3, ".bun", "bin");
|
|
724
1487
|
const defaultBunPath = osFamily === "macos" && existsSync3(BREW_BUN_PATH) ? BREW_BUN_PATH : join4(bunBinDir, "bun");
|
|
725
1488
|
return {
|
|
726
1489
|
id: "current-machine",
|
|
@@ -730,8 +1493,8 @@ function detectMachineContext(overrides = {}) {
|
|
|
730
1493
|
last_applied_at: null,
|
|
731
1494
|
created_at: "",
|
|
732
1495
|
os_family: osFamily,
|
|
733
|
-
home_dir:
|
|
734
|
-
workspace_root: overrides.workspace_root ?? join4(
|
|
1496
|
+
home_dir: homeDir3,
|
|
1497
|
+
workspace_root: overrides.workspace_root ?? join4(homeDir3, osFamily === "macos" ? "Workspace" : "workspace"),
|
|
735
1498
|
bun_bin_dir: bunBinDir,
|
|
736
1499
|
bun_path: overrides.bun_path ?? defaultBunPath,
|
|
737
1500
|
path_prefix: overrides.path_prefix ?? (osFamily === "macos" ? `${join4("/opt", "homebrew", "bin")}:${bunBinDir}` : bunBinDir)
|
|
@@ -852,9 +1615,9 @@ import { createHash as createHash7 } from "crypto";
|
|
|
852
1615
|
|
|
853
1616
|
// src/lib/session-render.ts
|
|
854
1617
|
import { createHash as createHash6 } from "crypto";
|
|
855
|
-
import { existsSync as existsSync7, readFileSync as
|
|
856
|
-
import { homedir as
|
|
857
|
-
import { basename as basename4, dirname as dirname4, extname as extname2, isAbsolute as
|
|
1618
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync3 } from "fs";
|
|
1619
|
+
import { homedir as homedir6 } from "os";
|
|
1620
|
+
import { basename as basename4, dirname as dirname4, extname as extname2, isAbsolute as isAbsolute4, join as join9, parse as parse2, posix as posix2, relative as relative2, resolve as resolve8 } from "path";
|
|
858
1621
|
|
|
859
1622
|
// src/lib/global-agent-rules-standard.ts
|
|
860
1623
|
import { createHash } from "crypto";
|
|
@@ -1165,22 +1928,22 @@ import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
|
1165
1928
|
import { execFileSync } from "child_process";
|
|
1166
1929
|
import { dlopen, FFIType } from "bun:ffi";
|
|
1167
1930
|
import {
|
|
1168
|
-
closeSync,
|
|
1931
|
+
closeSync as closeSync2,
|
|
1169
1932
|
constants,
|
|
1170
1933
|
existsSync as existsSync5,
|
|
1171
|
-
fstatSync,
|
|
1934
|
+
fstatSync as fstatSync2,
|
|
1172
1935
|
fsyncSync,
|
|
1173
1936
|
lstatSync,
|
|
1174
1937
|
linkSync,
|
|
1175
1938
|
mkdirSync as mkdirSync2,
|
|
1176
|
-
openSync,
|
|
1177
|
-
readFileSync,
|
|
1939
|
+
openSync as openSync2,
|
|
1940
|
+
readFileSync as readFileSync2,
|
|
1178
1941
|
renameSync,
|
|
1179
1942
|
rmSync as rmSync2,
|
|
1180
1943
|
statSync,
|
|
1181
1944
|
writeFileSync
|
|
1182
1945
|
} from "fs";
|
|
1183
|
-
import { basename, dirname as dirname2, isAbsolute, join as join6, parse, relative, resolve as resolve4 } from "path";
|
|
1946
|
+
import { basename, dirname as dirname2, isAbsolute as isAbsolute2, join as join6, parse, relative, resolve as resolve4 } from "path";
|
|
1184
1947
|
|
|
1185
1948
|
// ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
|
|
1186
1949
|
var exports_external = {};
|
|
@@ -5375,11 +6138,71 @@ var SESSION_INSTRUCTION_LAYERS = [
|
|
|
5375
6138
|
|
|
5376
6139
|
// src/lib/session-render-state.ts
|
|
5377
6140
|
import { existsSync as existsSync4, readdirSync } from "fs";
|
|
5378
|
-
import { homedir as
|
|
6141
|
+
import { homedir as homedir3 } from "os";
|
|
5379
6142
|
import { dirname, join as join5, resolve as resolve3 } from "path";
|
|
6143
|
+
import { homedir as pathsResolverHomedir2 } from "os";
|
|
6144
|
+
import { join as pathsResolverJoin2 } from "path";
|
|
6145
|
+
var PATHS_RESOLVER_KIND_ENV2 = {
|
|
6146
|
+
config: "HASNA_CONFIG_HOME",
|
|
6147
|
+
data: "HASNA_DATA_HOME",
|
|
6148
|
+
state: "HASNA_STATE_HOME",
|
|
6149
|
+
cache: "HASNA_CACHE_HOME"
|
|
6150
|
+
};
|
|
6151
|
+
var PATHS_RESOLVER_APP_SLUG_RE2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
6152
|
+
function pathsResolverAssertApp2(app) {
|
|
6153
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
6154
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
6155
|
+
}
|
|
6156
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE2.test(app)) {
|
|
6157
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
6158
|
+
}
|
|
6159
|
+
}
|
|
6160
|
+
function pathsResolverAssertKind2(kind) {
|
|
6161
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV2).includes(kind)) {
|
|
6162
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV2).join(", ")}`);
|
|
6163
|
+
}
|
|
6164
|
+
}
|
|
6165
|
+
function pathsResolverBaseDir2(kind, options) {
|
|
6166
|
+
pathsResolverAssertKind2(kind);
|
|
6167
|
+
const env = options.env ?? process.env;
|
|
6168
|
+
const override = env[PATHS_RESOLVER_KIND_ENV2[kind]];
|
|
6169
|
+
if (typeof override === "string" && override.length > 0)
|
|
6170
|
+
return override;
|
|
6171
|
+
const home = options.home ?? pathsResolverHomedir2();
|
|
6172
|
+
const platform = options.platform ?? process.platform;
|
|
6173
|
+
if (platform === "darwin") {
|
|
6174
|
+
switch (kind) {
|
|
6175
|
+
case "config":
|
|
6176
|
+
case "data":
|
|
6177
|
+
return pathsResolverJoin2(home, "Library", "Application Support", "Hasna");
|
|
6178
|
+
case "cache":
|
|
6179
|
+
return pathsResolverJoin2(home, "Library", "Caches", "Hasna");
|
|
6180
|
+
case "state":
|
|
6181
|
+
return pathsResolverJoin2(home, "Library", "Logs", "Hasna");
|
|
6182
|
+
}
|
|
6183
|
+
}
|
|
6184
|
+
switch (kind) {
|
|
6185
|
+
case "config":
|
|
6186
|
+
return pathsResolverJoin2(home, ".config", "hasna");
|
|
6187
|
+
case "data":
|
|
6188
|
+
return pathsResolverJoin2(home, ".local", "share", "hasna");
|
|
6189
|
+
case "state":
|
|
6190
|
+
return pathsResolverJoin2(home, ".local", "state", "hasna");
|
|
6191
|
+
case "cache":
|
|
6192
|
+
return pathsResolverJoin2(home, ".cache", "hasna");
|
|
6193
|
+
}
|
|
6194
|
+
}
|
|
6195
|
+
function pathsResolverResolve2(kind, options) {
|
|
6196
|
+
pathsResolverAssertApp2(options.app);
|
|
6197
|
+
const appSegment = options.internal === true ? pathsResolverJoin2("internal", options.app) : options.app;
|
|
6198
|
+
return pathsResolverJoin2(pathsResolverBaseDir2(kind, options), appSegment);
|
|
6199
|
+
}
|
|
6200
|
+
function stateDir(options) {
|
|
6201
|
+
return pathsResolverResolve2("state", options);
|
|
6202
|
+
}
|
|
5380
6203
|
var SESSION_RENDER_STATE_APP = "instructions";
|
|
5381
|
-
function
|
|
5382
|
-
return env["HOME"] || env["USERPROFILE"] ||
|
|
6204
|
+
function homeDir3(env = process.env) {
|
|
6205
|
+
return env["HOME"] || env["USERPROFILE"] || homedir3();
|
|
5383
6206
|
}
|
|
5384
6207
|
function legacySnapshotDir(targetHome) {
|
|
5385
6208
|
return resolve3(join5(targetHome, ".hasna", "session-render-snapshots"));
|
|
@@ -5390,10 +6213,10 @@ function resolverSnapshotDir(env = process.env) {
|
|
|
5390
6213
|
return stateDir({
|
|
5391
6214
|
app: SESSION_RENDER_STATE_APP,
|
|
5392
6215
|
env: { ...env, HASNA_STATE_HOME: undefined },
|
|
5393
|
-
home:
|
|
6216
|
+
home: homeDir3(env)
|
|
5394
6217
|
});
|
|
5395
6218
|
}
|
|
5396
|
-
return stateDir({ app: SESSION_RENDER_STATE_APP, env, home:
|
|
6219
|
+
return stateDir({ app: SESSION_RENDER_STATE_APP, env, home: homeDir3(env) });
|
|
5397
6220
|
}
|
|
5398
6221
|
function adoptResolverSnapshotDir(resolved, env = process.env) {
|
|
5399
6222
|
const override = env.HASNA_STATE_HOME;
|
|
@@ -5430,10 +6253,10 @@ var PROJECT_CONTEXT_MAX_APPROX_TOKENS = 1000;
|
|
|
5430
6253
|
var PROJECT_CONTEXT_MAX_COMMANDS = 6;
|
|
5431
6254
|
var PROJECT_CONTEXT_MAX_WARNINGS = 3;
|
|
5432
6255
|
var PROJECT_CONTEXT_FRAGMENT_PATH = ".hasna/instructions/project-context.md";
|
|
5433
|
-
var PROJECT_CONTEXT_MANIFEST_PATH = ".hasna/project-context-manifest.json";
|
|
5434
|
-
var PROJECT_CONTEXT_CACHE_PATH = ".hasna/project-context-cache.json";
|
|
5435
|
-
var PROJECT_CONTEXT_LOCK_PATH = ".hasna/project-context.lock";
|
|
5436
|
-
var PROJECT_CONTEXT_SNAPSHOT_DIR = ".hasna/project-context-snapshots";
|
|
6256
|
+
var PROJECT_CONTEXT_MANIFEST_PATH = ".hasna/projects/project-context-manifest.json";
|
|
6257
|
+
var PROJECT_CONTEXT_CACHE_PATH = ".hasna/projects/project-context-cache.json";
|
|
6258
|
+
var PROJECT_CONTEXT_LOCK_PATH = ".hasna/projects/project-context.lock";
|
|
6259
|
+
var PROJECT_CONTEXT_SNAPSHOT_DIR = ".hasna/projects/project-context-snapshots";
|
|
5437
6260
|
var PROJECT_CONTEXT_CACHE_SCHEMA = "hasna.instructions.project-context-cache/v1";
|
|
5438
6261
|
var PROJECT_CONTEXT_MANAGED_COMMENT = "Managed by @hasna/configs project context";
|
|
5439
6262
|
var SESSION_COMPATIBILITY_MANIFEST_MAX_BYTES = 8 * 1024 * 1024;
|
|
@@ -5483,7 +6306,7 @@ var safeOptionalDisplay = exports_external.string().min(1).max(512).refine(isSaf
|
|
|
5483
6306
|
var isoTimestamp = exports_external.string().min(20).max(40).refine(isStrictIsoTimestamp, "must be a strict ISO timestamp with timezone");
|
|
5484
6307
|
var revisionSchema = exports_external.string().min(1).max(512).refine((value) => revisionKey(value) !== null, "must be a monotonic rev-N or timestamp revision");
|
|
5485
6308
|
var hashSchema = exports_external.string().regex(/^sha256:[a-f0-9]{64}$/);
|
|
5486
|
-
var absolutePath = exports_external.string().min(1).max(4096).refine((value) =>
|
|
6309
|
+
var absolutePath = exports_external.string().min(1).max(4096).refine((value) => isAbsolute2(value), "must be absolute").refine(isSafeSingleLine, "must be safe").nullable();
|
|
5487
6310
|
var commandArg = exports_external.string().min(1).max(1024).refine((value) => isSafeCommandArgument(value), "unsafe argv item");
|
|
5488
6311
|
var financeText = exports_external.string().min(1).max(512).refine((value) => value === value.trim(), "must be normalized");
|
|
5489
6312
|
var financeLegalEntity = exports_external.string().min(1).max(256).refine((value) => value === value.trim(), "must be normalized");
|
|
@@ -6561,7 +7384,7 @@ function sanitizeLegacyEnvironment(value) {
|
|
|
6561
7384
|
}
|
|
6562
7385
|
const result = {};
|
|
6563
7386
|
for (const [key, item] of Object.entries(value)) {
|
|
6564
|
-
if (!/^[A-Z][A-Z0-9_]{0,63}$/.test(key) || typeof item !== "string" || !
|
|
7387
|
+
if (!/^[A-Z][A-Z0-9_]{0,63}$/.test(key) || typeof item !== "string" || !isAbsolute2(item) || !isSafeSingleLine(item) || item.length > 4096) {
|
|
6565
7388
|
throw new ProjectContextError("PROJECT_CONTEXT_MANIFEST_INVALID", "provider session environment metadata contains an unsafe entry");
|
|
6566
7389
|
}
|
|
6567
7390
|
if (scanSecrets(`${key}=${item}`, "text").length > 0) {
|
|
@@ -6636,7 +7459,7 @@ function sanitizeLegacyFiles(value) {
|
|
|
6636
7459
|
return value.map((entry) => {
|
|
6637
7460
|
const file = entry;
|
|
6638
7461
|
const relativePath = safeLegacyMetadataString(file["relativePath"], "");
|
|
6639
|
-
if (!relativePath ||
|
|
7462
|
+
if (!relativePath || isAbsolute2(relativePath) || relativePath === ".." || relativePath.startsWith("../") || relativePath.includes("/../")) {
|
|
6640
7463
|
throw new ProjectContextError("PROJECT_CONTEXT_MANIFEST_INVALID", "provider session file inventory contains an unsafe relative path");
|
|
6641
7464
|
}
|
|
6642
7465
|
const sha = safeLegacyMetadataString(file["sha256"], "");
|
|
@@ -6893,7 +7716,7 @@ function atomicWriteFile(path, content, workspaceRoot, defaultMode, expectedHash
|
|
|
6893
7716
|
fd = anchoredOpenExclusive(directory, tempName, previousMode);
|
|
6894
7717
|
writeFileSync(fd, content, { encoding: "utf8" });
|
|
6895
7718
|
fsyncSync(fd);
|
|
6896
|
-
|
|
7719
|
+
closeSync2(fd);
|
|
6897
7720
|
fd = null;
|
|
6898
7721
|
beforeInstall?.(tempPath);
|
|
6899
7722
|
assertManagedDirectoryStable(dir, workspaceRoot, directory.identity);
|
|
@@ -6988,14 +7811,14 @@ function atomicWriteFile(path, content, workspaceRoot, defaultMode, expectedHash
|
|
|
6988
7811
|
fsyncSync(directory.fd);
|
|
6989
7812
|
} catch (error) {
|
|
6990
7813
|
if (fd !== null)
|
|
6991
|
-
|
|
7814
|
+
closeSync2(fd);
|
|
6992
7815
|
if (!preserveTemp)
|
|
6993
7816
|
directory.ops.unlinkat(directory.fd, tempName);
|
|
6994
7817
|
if (directoryChanged)
|
|
6995
7818
|
fsyncSync(directory.fd);
|
|
6996
7819
|
throw error;
|
|
6997
7820
|
} finally {
|
|
6998
|
-
|
|
7821
|
+
closeSync2(directory.fd);
|
|
6999
7822
|
}
|
|
7000
7823
|
}
|
|
7001
7824
|
function atomicWritePortable(path, content, workspaceRoot, defaultMode, expectedHash, beforeInstall, maxObservedBytes, allowReplacement = false) {
|
|
@@ -7019,12 +7842,12 @@ function atomicWritePortable(path, content, workspaceRoot, defaultMode, expected
|
|
|
7019
7842
|
let fd = null;
|
|
7020
7843
|
let tempIdentity = null;
|
|
7021
7844
|
try {
|
|
7022
|
-
fd =
|
|
7023
|
-
const opened =
|
|
7845
|
+
fd = openSync2(tempPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, defaultMode);
|
|
7846
|
+
const opened = fstatSync2(fd);
|
|
7024
7847
|
tempIdentity = { dev: opened.dev, ino: opened.ino };
|
|
7025
7848
|
writeFileSync(fd, content, { encoding: "utf8" });
|
|
7026
7849
|
fsyncSync(fd);
|
|
7027
|
-
|
|
7850
|
+
closeSync2(fd);
|
|
7028
7851
|
fd = null;
|
|
7029
7852
|
beforeInstall?.(tempPath);
|
|
7030
7853
|
assertManagedDirectoryStable(dir, workspaceRoot, directoryIdentity);
|
|
@@ -7049,7 +7872,7 @@ function atomicWritePortable(path, content, workspaceRoot, defaultMode, expected
|
|
|
7049
7872
|
fsyncDirectory(dir);
|
|
7050
7873
|
} catch (error) {
|
|
7051
7874
|
if (fd !== null)
|
|
7052
|
-
|
|
7875
|
+
closeSync2(fd);
|
|
7053
7876
|
if (tempIdentity && managedDirectoryMatches(dir, workspaceRoot, directoryIdentity)) {
|
|
7054
7877
|
try {
|
|
7055
7878
|
const current = lstatSync(tempPath);
|
|
@@ -7077,12 +7900,12 @@ function atomicWritePortableReplacement(path, content, workspaceRoot, expectedHa
|
|
|
7077
7900
|
let fd = null;
|
|
7078
7901
|
let tempIdentity = null;
|
|
7079
7902
|
try {
|
|
7080
|
-
fd =
|
|
7081
|
-
const opened =
|
|
7903
|
+
fd = openSync2(tempPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, current.mode & 511);
|
|
7904
|
+
const opened = fstatSync2(fd);
|
|
7082
7905
|
tempIdentity = { dev: opened.dev, ino: opened.ino };
|
|
7083
7906
|
writeFileSync(fd, content, { encoding: "utf8" });
|
|
7084
7907
|
fsyncSync(fd);
|
|
7085
|
-
|
|
7908
|
+
closeSync2(fd);
|
|
7086
7909
|
fd = null;
|
|
7087
7910
|
beforeInstall?.(tempPath);
|
|
7088
7911
|
assertManagedDirectoryStable(dir, workspaceRoot, directoryIdentity);
|
|
@@ -7100,7 +7923,7 @@ function atomicWritePortableReplacement(path, content, workspaceRoot, expectedHa
|
|
|
7100
7923
|
fsyncDirectory(dir);
|
|
7101
7924
|
} catch (error) {
|
|
7102
7925
|
if (fd !== null)
|
|
7103
|
-
|
|
7926
|
+
closeSync2(fd);
|
|
7104
7927
|
if (tempIdentity && managedDirectoryMatches(dir, workspaceRoot, directoryIdentity)) {
|
|
7105
7928
|
try {
|
|
7106
7929
|
const candidate = lstatSync(tempPath);
|
|
@@ -7138,7 +7961,7 @@ function portableFileHash(path, workspaceRoot, maxObservedBytes) {
|
|
|
7138
7961
|
if (maxObservedBytes !== null && stat.size > maxObservedBytes) {
|
|
7139
7962
|
throw new ProjectContextHashRace(`managed output exceeds the safe read limit: ${relativePosix(workspaceRoot, path)}`);
|
|
7140
7963
|
}
|
|
7141
|
-
return createHash2("sha256").update(
|
|
7964
|
+
return createHash2("sha256").update(readFileSync2(path)).digest("hex");
|
|
7142
7965
|
}
|
|
7143
7966
|
function writeProjectContextCoordinatedFile(input) {
|
|
7144
7967
|
atomicWriteFile(resolve4(input.path), input.content, assertSafeWorkspaceRoot(input.workspace_root), input.default_mode ?? 420, input.expected_hash, undefined, false, input.test_hooks?.before_install, input.force_portable_file_ops ?? false, input.max_observed_bytes, input.allow_portable_replacement ?? false);
|
|
@@ -7188,7 +8011,7 @@ function removeProjectContextCoordinatedFile(input) {
|
|
|
7188
8011
|
}
|
|
7189
8012
|
throw error;
|
|
7190
8013
|
} finally {
|
|
7191
|
-
|
|
8014
|
+
closeSync2(directory.fd);
|
|
7192
8015
|
}
|
|
7193
8016
|
}
|
|
7194
8017
|
function restoreAnchoredDisplacedFile(directory, displacedName, targetName, expected) {
|
|
@@ -7269,19 +8092,19 @@ function openAnchoredDirectory(path, workspaceRoot, providedOps, maxObservedByte
|
|
|
7269
8092
|
const identity = captureManagedDirectoryIdentity(path, workspaceRoot);
|
|
7270
8093
|
let fd;
|
|
7271
8094
|
try {
|
|
7272
|
-
fd =
|
|
8095
|
+
fd = openSync2(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
7273
8096
|
} catch {
|
|
7274
8097
|
throw new ProjectContextHashRace(`managed parent directory changed while opening: ${relativePosix(workspaceRoot, path)}`);
|
|
7275
8098
|
}
|
|
7276
8099
|
try {
|
|
7277
|
-
const opened =
|
|
8100
|
+
const opened = fstatSync2(fd);
|
|
7278
8101
|
if (!opened.isDirectory() || opened.dev !== identity.dev || opened.ino !== identity.ino) {
|
|
7279
8102
|
throw new ProjectContextHashRace(`managed parent directory changed while opening: ${relativePosix(workspaceRoot, path)}`);
|
|
7280
8103
|
}
|
|
7281
8104
|
assertManagedDirectoryStable(path, workspaceRoot, identity);
|
|
7282
8105
|
return { fd, path, workspaceRoot, identity, ops, maxObservedBytes };
|
|
7283
8106
|
} catch (error) {
|
|
7284
|
-
|
|
8107
|
+
closeSync2(fd);
|
|
7285
8108
|
throw error;
|
|
7286
8109
|
}
|
|
7287
8110
|
}
|
|
@@ -7289,12 +8112,12 @@ function anchoredOpenExclusive(directory, name, mode) {
|
|
|
7289
8112
|
const requestedMode = mode & 4095;
|
|
7290
8113
|
let fd;
|
|
7291
8114
|
try {
|
|
7292
|
-
fd =
|
|
8115
|
+
fd = openSync2(join6(directory.path, name), constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, requestedMode);
|
|
7293
8116
|
} catch {
|
|
7294
8117
|
throw new ProjectContextHashRace(`could not create prepared managed file in ${relativePosix(directory.workspaceRoot, directory.path)}`);
|
|
7295
8118
|
}
|
|
7296
8119
|
try {
|
|
7297
|
-
const opened =
|
|
8120
|
+
const opened = fstatSync2(fd);
|
|
7298
8121
|
if (!opened.isFile()) {
|
|
7299
8122
|
throw new ProjectContextHashRace("prepared managed output is not a regular file");
|
|
7300
8123
|
}
|
|
@@ -7308,7 +8131,7 @@ function anchoredOpenExclusive(directory, name, mode) {
|
|
|
7308
8131
|
}
|
|
7309
8132
|
return fd;
|
|
7310
8133
|
} catch (error) {
|
|
7311
|
-
|
|
8134
|
+
closeSync2(fd);
|
|
7312
8135
|
throw error;
|
|
7313
8136
|
}
|
|
7314
8137
|
}
|
|
@@ -7329,7 +8152,7 @@ function anchoredFileObservation(directory, name) {
|
|
|
7329
8152
|
if (fd < 0)
|
|
7330
8153
|
return null;
|
|
7331
8154
|
try {
|
|
7332
|
-
const stat =
|
|
8155
|
+
const stat = fstatSync2(fd);
|
|
7333
8156
|
if (!stat.isFile())
|
|
7334
8157
|
throw new ProjectContextHashRace("managed output is not a regular file");
|
|
7335
8158
|
const relativePath = relativePosix(directory.workspaceRoot, join6(directory.path, name));
|
|
@@ -7340,11 +8163,11 @@ function anchoredFileObservation(directory, name) {
|
|
|
7340
8163
|
return {
|
|
7341
8164
|
dev: stat.dev,
|
|
7342
8165
|
ino: stat.ino,
|
|
7343
|
-
hash: createHash2("sha256").update(
|
|
8166
|
+
hash: createHash2("sha256").update(readFileSync2(fd)).digest("hex"),
|
|
7344
8167
|
mode: stat.mode & 511
|
|
7345
8168
|
};
|
|
7346
8169
|
} finally {
|
|
7347
|
-
|
|
8170
|
+
closeSync2(fd);
|
|
7348
8171
|
}
|
|
7349
8172
|
}
|
|
7350
8173
|
function anchoredFileHash(directory, name) {
|
|
@@ -7516,8 +8339,8 @@ function acquireWorkspaceLock(workspaceRoot, lockPath, afterOpen, beforeStaleRem
|
|
|
7516
8339
|
let linked = false;
|
|
7517
8340
|
let preserveTemp = false;
|
|
7518
8341
|
try {
|
|
7519
|
-
fd =
|
|
7520
|
-
const opened =
|
|
8342
|
+
fd = openSync2(tempPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 384);
|
|
8343
|
+
const opened = fstatSync2(fd);
|
|
7521
8344
|
openedIdentity = { dev: opened.dev, ino: opened.ino };
|
|
7522
8345
|
const content = `${JSON.stringify({
|
|
7523
8346
|
schema: "hasna.instructions.project-context-lock/v1",
|
|
@@ -7565,7 +8388,7 @@ function acquireWorkspaceLock(workspaceRoot, lockPath, afterOpen, beforeStaleRem
|
|
|
7565
8388
|
}
|
|
7566
8389
|
if (fd !== null) {
|
|
7567
8390
|
try {
|
|
7568
|
-
|
|
8391
|
+
closeSync2(fd);
|
|
7569
8392
|
} catch {}
|
|
7570
8393
|
}
|
|
7571
8394
|
throw error;
|
|
@@ -7578,7 +8401,7 @@ function removeOwnedLockByInode(lockPath, identity, expectedHash) {
|
|
|
7578
8401
|
const current = lstatSync(lockPath);
|
|
7579
8402
|
if (current.isSymbolicLink() || current.dev !== identity.dev || current.ino !== identity.ino)
|
|
7580
8403
|
return;
|
|
7581
|
-
if (expectedHash !== undefined && sha2562(
|
|
8404
|
+
if (expectedHash !== undefined && sha2562(readFileSync2(lockPath, "utf8")) !== expectedHash)
|
|
7582
8405
|
return;
|
|
7583
8406
|
rmSync2(lockPath);
|
|
7584
8407
|
fsyncDirectory(resolve4(lockPath, ".."));
|
|
@@ -7700,7 +8523,7 @@ function processStartIdentity(pid) {
|
|
|
7700
8523
|
return null;
|
|
7701
8524
|
if (process.platform === "linux") {
|
|
7702
8525
|
try {
|
|
7703
|
-
const stat =
|
|
8526
|
+
const stat = readFileSync2(`/proc/${pid}/stat`, "utf8");
|
|
7704
8527
|
const close = stat.lastIndexOf(")");
|
|
7705
8528
|
if (close < 0)
|
|
7706
8529
|
return null;
|
|
@@ -7708,7 +8531,7 @@ function processStartIdentity(pid) {
|
|
|
7708
8531
|
const startTicks = fields[19];
|
|
7709
8532
|
if (!startTicks || !/^[0-9]+$/.test(startTicks))
|
|
7710
8533
|
return null;
|
|
7711
|
-
const bootId =
|
|
8534
|
+
const bootId = readFileSync2("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
7712
8535
|
return /^[a-f0-9-]{36}$/i.test(bootId) ? `linux:${bootId}:${startTicks}` : null;
|
|
7713
8536
|
} catch {
|
|
7714
8537
|
return null;
|
|
@@ -7734,7 +8557,7 @@ function releaseWorkspaceLock(lockPath, lock, workspaceRoot) {
|
|
|
7734
8557
|
removeOwnedLockByInode(lockPath, lock.identity, lock.contentHash);
|
|
7735
8558
|
} finally {
|
|
7736
8559
|
try {
|
|
7737
|
-
|
|
8560
|
+
closeSync2(lock.fd);
|
|
7738
8561
|
} catch {}
|
|
7739
8562
|
}
|
|
7740
8563
|
return;
|
|
@@ -7746,8 +8569,8 @@ function releaseWorkspaceLock(lockPath, lock, workspaceRoot) {
|
|
|
7746
8569
|
let releaseHash = null;
|
|
7747
8570
|
let exchanged = false;
|
|
7748
8571
|
try {
|
|
7749
|
-
releaseFd =
|
|
7750
|
-
const opened =
|
|
8572
|
+
releaseFd = openSync2(releasePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 384);
|
|
8573
|
+
const opened = fstatSync2(releaseFd);
|
|
7751
8574
|
releaseIdentity = { dev: opened.dev, ino: opened.ino };
|
|
7752
8575
|
const releaseContent = `${JSON.stringify({
|
|
7753
8576
|
schema: "hasna.instructions.project-context-lock/v1",
|
|
@@ -7760,7 +8583,7 @@ function releaseWorkspaceLock(lockPath, lock, workspaceRoot) {
|
|
|
7760
8583
|
releaseHash = sha2562(releaseContent);
|
|
7761
8584
|
writeFileSync(releaseFd, releaseContent);
|
|
7762
8585
|
fsyncSync(releaseFd);
|
|
7763
|
-
|
|
8586
|
+
closeSync2(releaseFd);
|
|
7764
8587
|
releaseFd = null;
|
|
7765
8588
|
atomicExchangePaths(releasePath, lockPath);
|
|
7766
8589
|
exchanged = true;
|
|
@@ -7791,7 +8614,7 @@ function releaseWorkspaceLock(lockPath, lock, workspaceRoot) {
|
|
|
7791
8614
|
} finally {
|
|
7792
8615
|
if (releaseFd !== null) {
|
|
7793
8616
|
try {
|
|
7794
|
-
|
|
8617
|
+
closeSync2(releaseFd);
|
|
7795
8618
|
} catch {}
|
|
7796
8619
|
}
|
|
7797
8620
|
if (!exchanged && existsSync5(releasePath)) {
|
|
@@ -7800,21 +8623,21 @@ function releaseWorkspaceLock(lockPath, lock, workspaceRoot) {
|
|
|
7800
8623
|
} catch {}
|
|
7801
8624
|
}
|
|
7802
8625
|
try {
|
|
7803
|
-
|
|
8626
|
+
closeSync2(lock.fd);
|
|
7804
8627
|
} catch {}
|
|
7805
8628
|
}
|
|
7806
8629
|
}
|
|
7807
8630
|
function fsyncDirectory(path) {
|
|
7808
|
-
const fd =
|
|
8631
|
+
const fd = openSync2(path, constants.O_RDONLY);
|
|
7809
8632
|
try {
|
|
7810
8633
|
fsyncSync(fd);
|
|
7811
8634
|
} finally {
|
|
7812
|
-
|
|
8635
|
+
closeSync2(fd);
|
|
7813
8636
|
}
|
|
7814
8637
|
}
|
|
7815
8638
|
function ensureSafeDirectory(path, workspaceRoot, mode) {
|
|
7816
8639
|
const rel = relative(workspaceRoot, path);
|
|
7817
|
-
if (rel === ".." || rel.startsWith("../") ||
|
|
8640
|
+
if (rel === ".." || rel.startsWith("../") || isAbsolute2(rel)) {
|
|
7818
8641
|
throw new ProjectContextError("PROJECT_CONTEXT_PATH_ESCAPE", "managed directory escapes the workspace root");
|
|
7819
8642
|
}
|
|
7820
8643
|
const segments = rel.split(/[\\/]+/).filter(Boolean);
|
|
@@ -7946,7 +8769,7 @@ function assertCodewithTargetIsConsumed(workspaceRoot, runtime) {
|
|
|
7946
8769
|
throw new ProjectContextError("PROJECT_CONTEXT_SHADOWED", ".codewith/CODEWITH.override.md shadows .codewith/CODEWITH.md");
|
|
7947
8770
|
}
|
|
7948
8771
|
function assertSafeWorkspaceRoot(path) {
|
|
7949
|
-
if (!
|
|
8772
|
+
if (!isAbsolute2(path))
|
|
7950
8773
|
throw new ProjectContextError("PROJECT_CONTEXT_PATH_INVALID", "workspace root must be absolute");
|
|
7951
8774
|
const normalized = resolve4(path);
|
|
7952
8775
|
if (normalized === parse(normalized).root)
|
|
@@ -7960,7 +8783,7 @@ function assertSafeWorkspaceRoot(path) {
|
|
|
7960
8783
|
}
|
|
7961
8784
|
function assertNoSymlinkSegments(root, target) {
|
|
7962
8785
|
const rel = relative(root, target);
|
|
7963
|
-
if (rel === "" || rel === ".." || rel.startsWith("../") ||
|
|
8786
|
+
if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute2(rel)) {
|
|
7964
8787
|
throw new ProjectContextError("PROJECT_CONTEXT_PATH_ESCAPE", "managed path escapes workspace root");
|
|
7965
8788
|
}
|
|
7966
8789
|
let current = root;
|
|
@@ -7989,7 +8812,7 @@ function readUtf8RegularFile(path, workspaceRoot, maxBytes = FOREIGN_INPUT_MAX_B
|
|
|
7989
8812
|
throw new ProjectContextError("PROJECT_CONTEXT_PATH_INVALID", `managed path is not a regular file: ${path}`);
|
|
7990
8813
|
if (stat.size > maxBytes)
|
|
7991
8814
|
throw new ProjectContextError("PROJECT_CONTEXT_INPUT_TOO_LARGE", `managed input exceeds ${maxBytes} bytes`);
|
|
7992
|
-
return
|
|
8815
|
+
return readFileSync2(path, "utf8");
|
|
7993
8816
|
}
|
|
7994
8817
|
function currentFileHash(path, workspaceRoot) {
|
|
7995
8818
|
if (!existsSync5(path))
|
|
@@ -8012,7 +8835,7 @@ function fragmentMatchesBundle(path, bundle, workspaceRoot) {
|
|
|
8012
8835
|
function durableSourcePath(path, workspaceRoot) {
|
|
8013
8836
|
if (!path || path.startsWith("/dev/fd/"))
|
|
8014
8837
|
return resolve4(workspaceRoot, ...PROJECT_CONTEXT_CACHE_PATH.split("/"));
|
|
8015
|
-
const normalized =
|
|
8838
|
+
const normalized = isAbsolute2(path) ? resolve4(path) : resolve4(workspaceRoot, path);
|
|
8016
8839
|
if (normalized.startsWith("/dev/fd/"))
|
|
8017
8840
|
return resolve4(workspaceRoot, ...PROJECT_CONTEXT_CACHE_PATH.split("/"));
|
|
8018
8841
|
return normalized;
|
|
@@ -8355,7 +9178,7 @@ function compareProviderVersions(left, right) {
|
|
|
8355
9178
|
|
|
8356
9179
|
// src/lib/asset-plan.ts
|
|
8357
9180
|
import { createHash as createHash3 } from "crypto";
|
|
8358
|
-
import { isAbsolute as
|
|
9181
|
+
import { isAbsolute as isAbsolute3, posix, resolve as resolve5 } from "path";
|
|
8359
9182
|
var ASSET_PLAN_SCHEMA = "hasna.instructions.asset-plan/v1";
|
|
8360
9183
|
var ASSET_CAPABILITY_SCHEMA = "hasna.instructions.asset-capability/v1";
|
|
8361
9184
|
var ASSET_BUNDLE_SCHEMA = "hasna.instructions.asset-bundle/v1";
|
|
@@ -8617,7 +9440,7 @@ function resolveAssetDestination(item, roots) {
|
|
|
8617
9440
|
const root = item.destination.root === "target-home" ? roots.targetHome : roots.projectRoot;
|
|
8618
9441
|
if (!root)
|
|
8619
9442
|
throw new Error(`Asset ${item.assetKey} requires an explicit project root.`);
|
|
8620
|
-
if (!
|
|
9443
|
+
if (!isAbsolute3(root))
|
|
8621
9444
|
throw new Error(`Asset ${item.assetKey} destination root must be absolute.`);
|
|
8622
9445
|
const relativePath = safeRelativePath(item.destination.relativePath);
|
|
8623
9446
|
const target = resolve5(root, ...relativePath.split("/"));
|
|
@@ -8742,8 +9565,8 @@ function deepFreeze(value) {
|
|
|
8742
9565
|
|
|
8743
9566
|
// src/lib/cursor-authority.ts
|
|
8744
9567
|
import { createHash as createHash4 } from "crypto";
|
|
8745
|
-
import { lstatSync as lstatSync2, readFileSync as
|
|
8746
|
-
import { homedir as
|
|
9568
|
+
import { lstatSync as lstatSync2, readFileSync as readFileSync3 } from "fs";
|
|
9569
|
+
import { homedir as homedir4 } from "os";
|
|
8747
9570
|
import { join as join7, resolve as resolve6 } from "path";
|
|
8748
9571
|
var CURSOR_GLOBAL_AUTHORITY_RELATIVE_PATH = ".cursor/rules/hasna-global.mdc";
|
|
8749
9572
|
var CURSOR_GLOBAL_AUTHORITY_MAX_BYTES = 256 * 1024;
|
|
@@ -8753,8 +9576,8 @@ var CURSOR_GLOBAL_AUTHORITY_FRONTMATTER_PATTERN = /^---\n[\s\S]*?\n---(?:\n|$)/;
|
|
|
8753
9576
|
function sha2564(content) {
|
|
8754
9577
|
return createHash4("sha256").update(content).digest("hex");
|
|
8755
9578
|
}
|
|
8756
|
-
function
|
|
8757
|
-
return process.env["HOME"] ||
|
|
9579
|
+
function homeDir4() {
|
|
9580
|
+
return process.env["HOME"] || homedir4();
|
|
8758
9581
|
}
|
|
8759
9582
|
function markerPayload(content, markerLine, markerIndex) {
|
|
8760
9583
|
const index = markerIndex ?? content.indexOf(markerLine);
|
|
@@ -8770,12 +9593,12 @@ function baseObservation(path) {
|
|
|
8770
9593
|
};
|
|
8771
9594
|
}
|
|
8772
9595
|
function observeCursorGlobalAuthority(options = {}) {
|
|
8773
|
-
const authorityPath = resolve6(join7(options.home ??
|
|
8774
|
-
const readFile = options.readFile ?? ((path) =>
|
|
9596
|
+
const authorityPath = resolve6(join7(options.home ?? homeDir4(), CURSOR_GLOBAL_AUTHORITY_RELATIVE_PATH));
|
|
9597
|
+
const readFile = options.readFile ?? ((path) => readFileSync3(path, "utf8"));
|
|
8775
9598
|
return observeCursorGlobalAuthorityPath(authorityPath, readFile);
|
|
8776
9599
|
}
|
|
8777
9600
|
function observeCursorGlobalAuthorityAtPath(authorityPath) {
|
|
8778
|
-
return observeCursorGlobalAuthorityPath(resolve6(authorityPath), (path) =>
|
|
9601
|
+
return observeCursorGlobalAuthorityPath(resolve6(authorityPath), (path) => readFileSync3(path, "utf8"));
|
|
8779
9602
|
}
|
|
8780
9603
|
function observeCursorGlobalAuthorityPath(authorityPath, readFile) {
|
|
8781
9604
|
const base = baseObservation(authorityPath);
|
|
@@ -8920,7 +9743,7 @@ function observeCursorGlobalAuthorityPath(authorityPath, readFile) {
|
|
|
8920
9743
|
};
|
|
8921
9744
|
}
|
|
8922
9745
|
function isCursorGlobalAuthorityPath(path) {
|
|
8923
|
-
return resolve6(path) === resolve6(join7(
|
|
9746
|
+
return resolve6(path) === resolve6(join7(homeDir4(), CURSOR_GLOBAL_AUTHORITY_RELATIVE_PATH));
|
|
8924
9747
|
}
|
|
8925
9748
|
function stampCursorGlobalAuthorityMarker(content) {
|
|
8926
9749
|
const existing = content.match(CURSOR_GLOBAL_AUTHORITY_MARKER_PATTERN);
|
|
@@ -8969,8 +9792,8 @@ function detectCursorAuthorityConflicts(observation = observeCursorGlobalAuthori
|
|
|
8969
9792
|
|
|
8970
9793
|
// src/lib/session-authority.ts
|
|
8971
9794
|
import { createHash as createHash5 } from "crypto";
|
|
8972
|
-
import { lstatSync as lstatSync3, readFileSync as
|
|
8973
|
-
import { homedir as
|
|
9795
|
+
import { lstatSync as lstatSync3, readFileSync as readFileSync4, realpathSync, statSync as statSync2 } from "fs";
|
|
9796
|
+
import { homedir as homedir5 } from "os";
|
|
8974
9797
|
import { join as join8, resolve as resolve7 } from "path";
|
|
8975
9798
|
var CLAUDE_LEGACY_AUTHORITY_RELATIVE_PATH = "AGENTS.md";
|
|
8976
9799
|
var CLAUDE_LEGACY_AUTHORITY_MAX_BYTES = 256 * 1024;
|
|
@@ -8983,7 +9806,7 @@ function sha2565(content) {
|
|
|
8983
9806
|
return createHash5("sha256").update(content).digest("hex");
|
|
8984
9807
|
}
|
|
8985
9808
|
function configHomeDir() {
|
|
8986
|
-
return process.env["CONFIGS_HOME"] || process.env["HOME"] ||
|
|
9809
|
+
return process.env["CONFIGS_HOME"] || process.env["HOME"] || homedir5();
|
|
8987
9810
|
}
|
|
8988
9811
|
function normalizeOwnedTargetPath(p) {
|
|
8989
9812
|
const expanded = p.startsWith("~/") ? resolve7(configHomeDir(), p.slice(2)) : resolve7(p);
|
|
@@ -9036,7 +9859,7 @@ function detectClaudeAuthorityConflicts(targetHome, ownedAuthorities = []) {
|
|
|
9036
9859
|
reason: `Claude target contains unmanaged AGENTS.md larger than ${CLAUDE_LEGACY_AUTHORITY_MAX_BYTES} bytes; authority cannot be classified safely.`
|
|
9037
9860
|
}];
|
|
9038
9861
|
}
|
|
9039
|
-
const content =
|
|
9862
|
+
const content = readFileSync4(authorityPath, "utf8");
|
|
9040
9863
|
const owned = ownedAuthorities.find((authority) => normalizeOwnedTargetPath(authority.targetPath) === normalizeOwnedTargetPath(authorityPath));
|
|
9041
9864
|
if (owned) {
|
|
9042
9865
|
if (owned.content === content)
|
|
@@ -9353,7 +10176,7 @@ function yamlQuote2(value) {
|
|
|
9353
10176
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
9354
10177
|
}
|
|
9355
10178
|
function defaultTargetHome(tool, profile, sessionId) {
|
|
9356
|
-
const home = process.env["HOME"] ||
|
|
10179
|
+
const home = process.env["HOME"] || homedir6();
|
|
9357
10180
|
return join9(home, ".hasna", "accounts", "profiles", tool, slug(profile));
|
|
9358
10181
|
}
|
|
9359
10182
|
function joinTarget(targetHome, relativePath) {
|
|
@@ -9929,7 +10752,7 @@ function buildOpenCodeFiles(targetHome, adapter, profile, sources, providerConfi
|
|
|
9929
10752
|
...sources.flatMap((source) => source.resolvedRules.map((rule) => rule.id))
|
|
9930
10753
|
]);
|
|
9931
10754
|
const existingConfigPath = joinTarget(targetHome, adapter.configFile);
|
|
9932
|
-
const selectedConfig = existsSync7(existingConfigPath) ? readOpenCodeConfig(
|
|
10755
|
+
const selectedConfig = existsSync7(existingConfigPath) ? readOpenCodeConfig(readFileSync5(existingConfigPath, "utf8"), existingConfigPath) : providerConfig ? readOpenCodeConfig(providerConfig.content, providerConfig.sourceId) : {};
|
|
9933
10756
|
const preservedInstructions = normalizeOpenCodeInstructions(selectedConfig["instructions"]).filter((path) => !pathIsManagedOpenCodeInstruction(path, adapter.managedDir));
|
|
9934
10757
|
const config = {
|
|
9935
10758
|
...selectedConfig,
|
|
@@ -10070,7 +10893,7 @@ function buildAssetFiles(input, targetHome, blocked) {
|
|
|
10070
10893
|
...input.projectRoot ? { projectRoot: resolveSessionPath(input.projectRoot) } : {}
|
|
10071
10894
|
});
|
|
10072
10895
|
const relativePath = relative2(targetHome, path).replaceAll("\\", "/");
|
|
10073
|
-
if (!relativePath || relativePath === ".." || relativePath.startsWith("../") ||
|
|
10896
|
+
if (!relativePath || relativePath === ".." || relativePath.startsWith("../") || isAbsolute4(relativePath)) {
|
|
10074
10897
|
throw new Error(`Asset ${item.assetKey} is outside the session snapshot root; use a project-scoped session plan for atomic application.`);
|
|
10075
10898
|
}
|
|
10076
10899
|
return {
|
|
@@ -10118,7 +10941,7 @@ function adapterFor(input) {
|
|
|
10118
10941
|
return gatedNativeImports ? CODEWITH_NATIVE_ADAPTER : CODEWITH_FLATTENED_ADAPTER;
|
|
10119
10942
|
}
|
|
10120
10943
|
function getHomeDir() {
|
|
10121
|
-
return process.env["CONFIGS_HOME"] || process.env["HOME"] ||
|
|
10944
|
+
return process.env["CONFIGS_HOME"] || process.env["HOME"] || homedir6();
|
|
10122
10945
|
}
|
|
10123
10946
|
function cleanSessionPathInput(path) {
|
|
10124
10947
|
const trimmed = path.trim();
|
|
@@ -10156,7 +10979,7 @@ function assertSafeRelativePath(relativePath) {
|
|
|
10156
10979
|
return normalized;
|
|
10157
10980
|
}
|
|
10158
10981
|
function assertSafeTargetRoot(targetHome) {
|
|
10159
|
-
if (!
|
|
10982
|
+
if (!isAbsolute4(targetHome))
|
|
10160
10983
|
throw new Error(`Session render target must be an absolute path: ${targetHome}`);
|
|
10161
10984
|
const normalized = resolve8(targetHome);
|
|
10162
10985
|
if (normalized === parse2(normalized).root) {
|
|
@@ -10756,10 +11579,10 @@ function layerFromIdentityKind(kind, exportShape) {
|
|
|
10756
11579
|
function contentFromIdentitySourcePaths(sourcePaths, exportPath, sourceId) {
|
|
10757
11580
|
if (sourcePaths.length === 0 || !exportPath)
|
|
10758
11581
|
return;
|
|
10759
|
-
const
|
|
11582
|
+
const baseDir = dirname4(resolveSessionPath(exportPath));
|
|
10760
11583
|
const contents = [];
|
|
10761
11584
|
for (const sourcePath of sourcePaths) {
|
|
10762
|
-
const content = readIdentitySourcePath(sourcePath,
|
|
11585
|
+
const content = readIdentitySourcePath(sourcePath, baseDir, sourceId);
|
|
10763
11586
|
if (content !== undefined)
|
|
10764
11587
|
contents.push({ path: sourcePath.path, content });
|
|
10765
11588
|
}
|
|
@@ -10772,8 +11595,8 @@ ${item.content.trimEnd()}`).join(`
|
|
|
10772
11595
|
|
|
10773
11596
|
`));
|
|
10774
11597
|
}
|
|
10775
|
-
function readIdentitySourcePath(sourcePath,
|
|
10776
|
-
const resolvedPath = resolveIdentitySourcePath(sourcePath.path,
|
|
11598
|
+
function readIdentitySourcePath(sourcePath, baseDir, sourceId) {
|
|
11599
|
+
const resolvedPath = resolveIdentitySourcePath(sourcePath.path, baseDir, sourceId);
|
|
10777
11600
|
if (!existsSync7(resolvedPath)) {
|
|
10778
11601
|
if (sourcePath.required) {
|
|
10779
11602
|
throw new Error(`Required identity instruction source path not found for ${sourceId}: ${sourcePath.path}`);
|
|
@@ -10784,28 +11607,28 @@ function readIdentitySourcePath(sourcePath, baseDir2, sourceId) {
|
|
|
10784
11607
|
if (!stat.isFile()) {
|
|
10785
11608
|
throw new Error(`Identity instruction source path is not a file for ${sourceId}: ${sourcePath.path}`);
|
|
10786
11609
|
}
|
|
10787
|
-
const realBase = realpathSync2(
|
|
11610
|
+
const realBase = realpathSync2(baseDir);
|
|
10788
11611
|
const realPath = realpathSync2(resolvedPath);
|
|
10789
11612
|
if (!pathIsInside(realPath, realBase)) {
|
|
10790
11613
|
throw new Error(`Identity instruction source path escapes export directory for ${sourceId}: ${sourcePath.path}`);
|
|
10791
11614
|
}
|
|
10792
|
-
return
|
|
11615
|
+
return readFileSync5(realPath, "utf-8");
|
|
10793
11616
|
}
|
|
10794
|
-
function resolveIdentitySourcePath(path,
|
|
11617
|
+
function resolveIdentitySourcePath(path, baseDir, sourceId) {
|
|
10795
11618
|
const cleaned = cleanSessionPathInput(path);
|
|
10796
11619
|
if (!cleaned)
|
|
10797
11620
|
throw new Error(`Identity instruction source path cannot be empty for ${sourceId}.`);
|
|
10798
11621
|
if (cleaned.includes("\\"))
|
|
10799
11622
|
throw new Error(`Identity instruction source path must use POSIX separators for ${sourceId}: ${path}`);
|
|
10800
|
-
const resolvedPath =
|
|
10801
|
-
if (!pathIsInside(resolvedPath, resolve8(
|
|
11623
|
+
const resolvedPath = isAbsolute4(cleaned) ? resolve8(cleaned) : resolve8(baseDir, cleaned);
|
|
11624
|
+
if (!pathIsInside(resolvedPath, resolve8(baseDir))) {
|
|
10802
11625
|
throw new Error(`Identity instruction source path escapes export directory for ${sourceId}: ${path}`);
|
|
10803
11626
|
}
|
|
10804
11627
|
return resolvedPath;
|
|
10805
11628
|
}
|
|
10806
|
-
function pathIsInside(path,
|
|
10807
|
-
const rel = relative2(
|
|
10808
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
11629
|
+
function pathIsInside(path, baseDir) {
|
|
11630
|
+
const rel = relative2(baseDir, path);
|
|
11631
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
|
|
10809
11632
|
}
|
|
10810
11633
|
function providerTargetsTool(targets, tool) {
|
|
10811
11634
|
return targets.map((target) => target.toLowerCase()).some((target) => target === tool || target === "all" || target === "generic");
|
|
@@ -11678,63 +12501,1354 @@ function listMachines(db) {
|
|
|
11678
12501
|
return d.query("SELECT * FROM machines ORDER BY last_applied_at DESC NULLS LAST").all();
|
|
11679
12502
|
}
|
|
11680
12503
|
|
|
11681
|
-
//
|
|
11682
|
-
|
|
11683
|
-
|
|
11684
|
-
|
|
11685
|
-
|
|
11686
|
-
|
|
11687
|
-
|
|
11688
|
-
|
|
11689
|
-
|
|
11690
|
-
|
|
11691
|
-
}
|
|
11692
|
-
function
|
|
11693
|
-
const
|
|
11694
|
-
const consumed = Number(page?.cursor) + (page?.items?.length ?? 0);
|
|
11695
|
-
const complete = Boolean(page && Number.isSafeInteger(page.total) && consumed >= Number(page.total));
|
|
11696
|
-
if (!page || !Array.isArray(page.items) || !Number.isSafeInteger(page.total) || Number(page.total) < 0 || !Number.isSafeInteger(page.limit) || Number(page.limit) < 1 || !Number.isSafeInteger(page.cursor) || Number(page.cursor) < 0 || page.items.length > Number(page.limit) || typeof page.has_more !== "boolean" || typeof page.complete !== "boolean" || page.truncated !== false || page.next_cursor !== null && !Number.isSafeInteger(page.next_cursor) || page.complete !== complete || page.has_more !== !complete || page.next_cursor !== (complete ? null : consumed)) {
|
|
11697
|
-
throw new CloudHttpError(502, `${label} returned an invalid or truncated bounded-read envelope`, value);
|
|
11698
|
-
}
|
|
12504
|
+
// ../contracts/dist/client/storage.js
|
|
12505
|
+
import { isIP as isIP2 } from "net";
|
|
12506
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
12507
|
+
import { closeSync as closeSync3, fstatSync as fstatSync3, openSync as openSync3, readFileSync as readFileSync6 } from "fs";
|
|
12508
|
+
import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
|
|
12509
|
+
import { createRequire as createRequire2 } from "module";
|
|
12510
|
+
import { hostname as osHostname2 } from "os";
|
|
12511
|
+
import { isAbsolute as isAbsolute5, join as join10 } from "path";
|
|
12512
|
+
function envToken2(name) {
|
|
12513
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
12514
|
+
}
|
|
12515
|
+
function clientTransportEnvKeys2(name) {
|
|
12516
|
+
const envSegment = envToken2(name);
|
|
11699
12517
|
return {
|
|
11700
|
-
|
|
11701
|
-
|
|
12518
|
+
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
12519
|
+
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
11702
12520
|
};
|
|
11703
12521
|
}
|
|
11704
|
-
function
|
|
11705
|
-
|
|
11706
|
-
const candidate = value;
|
|
11707
|
-
if ("items" in candidate || "total" in candidate || "complete" in candidate || "truncated" in candidate || "next_cursor" in candidate) {
|
|
11708
|
-
return parseBoundedPagePayload(value, label);
|
|
11709
|
-
}
|
|
11710
|
-
}
|
|
11711
|
-
if (!Array.isArray(legacyItems)) {
|
|
11712
|
-
throw new CloudHttpError(502, `${label} returned neither a bounded envelope nor a complete legacy array`, value);
|
|
11713
|
-
}
|
|
11714
|
-
const normalized = normalizeBoundedReadOptions(options);
|
|
11715
|
-
const page = boundedReadPage(legacyItems.slice(normalized.cursor, normalized.cursor + normalized.limit), legacyItems.length, normalized);
|
|
11716
|
-
return { ...page, source_bounded: false };
|
|
12522
|
+
function credentialOverrideEnvKey2(name) {
|
|
12523
|
+
return `HASNA_${envToken2(name)}_API_KEY_OVERRIDE`;
|
|
11717
12524
|
}
|
|
11718
|
-
var
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
assertNoLegacyStorageMode(env);
|
|
11722
|
-
const apiUrl = env[API_URL_ENV]?.trim();
|
|
11723
|
-
const apiKey = env[API_KEY_ENV]?.trim();
|
|
11724
|
-
if (!apiUrl && !apiKey)
|
|
11725
|
-
return null;
|
|
11726
|
-
if (!apiUrl || !apiKey) {
|
|
11727
|
-
throw new Error(`API mode requires BOTH ${API_URL_ENV} and ${API_KEY_ENV}; only ` + `${apiUrl ? API_URL_ENV : API_KEY_ENV} is set. Set both to use the HTTP API, ` + `or unset both to use the local store.`);
|
|
11728
|
-
}
|
|
11729
|
-
return { apiUrl, apiKey };
|
|
12525
|
+
var CREDENTIAL_PROFILE_ENV_KEY2 = "HASNA_PROFILE";
|
|
12526
|
+
function credentialPointerEnvKey2(name) {
|
|
12527
|
+
return `HASNA_${envToken2(name)}_API_KEY_REF`;
|
|
11730
12528
|
}
|
|
11731
|
-
|
|
11732
|
-
|
|
12529
|
+
|
|
12530
|
+
class CredentialResolutionError2 extends Error {
|
|
12531
|
+
appName;
|
|
12532
|
+
attempted;
|
|
12533
|
+
constructor(appName, message, attempted) {
|
|
12534
|
+
super(message);
|
|
12535
|
+
this.name = "CredentialResolutionError";
|
|
12536
|
+
this.appName = appName;
|
|
12537
|
+
this.attempted = attempted;
|
|
12538
|
+
}
|
|
11733
12539
|
}
|
|
11734
12540
|
|
|
11735
|
-
class
|
|
11736
|
-
|
|
12541
|
+
class CredentialFileUnsafeError2 extends Error {
|
|
12542
|
+
path;
|
|
12543
|
+
constructor(path, reason) {
|
|
12544
|
+
super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
|
|
12545
|
+
this.name = "CredentialFileUnsafeError";
|
|
12546
|
+
this.path = path;
|
|
12547
|
+
}
|
|
12548
|
+
}
|
|
12549
|
+
var HASNA_HOME_ENV_KEY2 = "HASNA_HOME";
|
|
12550
|
+
var HASNA_CONFIG_HOME_ENV_KEY2 = "HASNA_CONFIG_HOME";
|
|
12551
|
+
var KEYCHAIN_STATION_ENV_KEY2 = "HASNA_STATION";
|
|
12552
|
+
var HASNA_HOME_DIR2 = ".hasna";
|
|
12553
|
+
var CONFIG_SUBDIR2 = "config";
|
|
12554
|
+
var CREDENTIALS_FILE2 = "credentials";
|
|
12555
|
+
var KEYCHAIN_SECURITY_BIN2 = "/usr/bin/security";
|
|
12556
|
+
var KEYCHAIN_SERVICE_PREFIX2 = "hasna.credentials";
|
|
12557
|
+
var KEYCHAIN_ITEM_NOT_FOUND_STATUS2 = 44;
|
|
12558
|
+
var KEYCHAIN_SPAWN_TIMEOUT_MS2 = 1e4;
|
|
12559
|
+
var MAX_CREDENTIAL_FILE_BYTES2 = 64 * 1024;
|
|
12560
|
+
var SAFE_APP_SLUG2 = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
12561
|
+
var SAFE_PROFILE2 = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
12562
|
+
var ILLEGAL_IN_HEADER_VALUE2 = /[^\t\x20-\x7e]/;
|
|
12563
|
+
var VAULT_POINTER_SHAPE2 = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
|
|
12564
|
+
function homeDir5(env) {
|
|
12565
|
+
const home = env.HOME?.trim();
|
|
12566
|
+
return home ? home : null;
|
|
12567
|
+
}
|
|
12568
|
+
function absoluteOverride2(env, key) {
|
|
12569
|
+
const value = env[key]?.trim();
|
|
12570
|
+
return value && isAbsolute5(value) ? value : null;
|
|
12571
|
+
}
|
|
12572
|
+
function hasnaHomeDir2(env) {
|
|
12573
|
+
const override = absoluteOverride2(env, HASNA_HOME_ENV_KEY2);
|
|
12574
|
+
if (override)
|
|
12575
|
+
return override;
|
|
12576
|
+
const home = homeDir5(env);
|
|
12577
|
+
return home ? join10(home, HASNA_HOME_DIR2) : null;
|
|
12578
|
+
}
|
|
12579
|
+
function appConfigDir2(name, env) {
|
|
12580
|
+
const configRoot = absoluteOverride2(env, HASNA_CONFIG_HOME_ENV_KEY2);
|
|
12581
|
+
if (configRoot)
|
|
12582
|
+
return join10(configRoot, name);
|
|
12583
|
+
const root = hasnaHomeDir2(env);
|
|
12584
|
+
return root ? join10(root, name, CONFIG_SUBDIR2) : null;
|
|
12585
|
+
}
|
|
12586
|
+
function credentialDiskSourceList2(name, env, profile = null) {
|
|
12587
|
+
if (!SAFE_APP_SLUG2.test(name))
|
|
12588
|
+
return [];
|
|
12589
|
+
const directory = appConfigDir2(name, env);
|
|
12590
|
+
if (!directory)
|
|
12591
|
+
return [];
|
|
12592
|
+
const file = profile ? `${CREDENTIALS_FILE2}-${profile}` : CREDENTIALS_FILE2;
|
|
12593
|
+
return [{ path: join10(directory, file), tier: "disk" }];
|
|
12594
|
+
}
|
|
12595
|
+
function credentialDiskSources2(name, env) {
|
|
12596
|
+
return credentialDiskSourceList2(name, env, null).map((s) => s.path);
|
|
12597
|
+
}
|
|
12598
|
+
function profileDiskSources2(name, env, profile) {
|
|
12599
|
+
return credentialDiskSourceList2(name, env, profile).map((s) => s.path);
|
|
12600
|
+
}
|
|
12601
|
+
function parseEnvFile2(text) {
|
|
12602
|
+
const values = new Map;
|
|
12603
|
+
const unusable = new Set;
|
|
12604
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
12605
|
+
const line = rawLine.trim();
|
|
12606
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
12607
|
+
continue;
|
|
12608
|
+
const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
|
12609
|
+
const equals = withoutExport.indexOf("=");
|
|
12610
|
+
if (equals <= 0)
|
|
12611
|
+
continue;
|
|
12612
|
+
const key = withoutExport.slice(0, equals).trim();
|
|
12613
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
12614
|
+
continue;
|
|
12615
|
+
let value = withoutExport.slice(equals + 1).trim();
|
|
12616
|
+
const quote = value[0];
|
|
12617
|
+
if (quote === '"' || quote === "'") {
|
|
12618
|
+
if (value.length < 2 || !value.endsWith(quote)) {
|
|
12619
|
+
unusable.add(key);
|
|
12620
|
+
continue;
|
|
12621
|
+
}
|
|
12622
|
+
value = value.slice(1, -1);
|
|
12623
|
+
}
|
|
12624
|
+
if (value.trim().length === 0) {
|
|
12625
|
+
unusable.add(key);
|
|
12626
|
+
continue;
|
|
12627
|
+
}
|
|
12628
|
+
if (values.has(key) && values.get(key) !== value)
|
|
12629
|
+
unusable.add(key);
|
|
12630
|
+
values.set(key, value);
|
|
12631
|
+
}
|
|
12632
|
+
return { values, unusable };
|
|
12633
|
+
}
|
|
12634
|
+
function configFileModeAllowed2(mode) {
|
|
12635
|
+
const permissions = mode & 4095;
|
|
12636
|
+
return permissions === 256 || permissions === 384;
|
|
12637
|
+
}
|
|
12638
|
+
function configFileReadsCoherent2(before, after) {
|
|
12639
|
+
return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
|
|
12640
|
+
}
|
|
12641
|
+
function readAppConfigFile2(path) {
|
|
12642
|
+
const unsafe = (reason) => {
|
|
12643
|
+
throw new CredentialFileUnsafeError2(path, reason);
|
|
12644
|
+
};
|
|
12645
|
+
let fd = -1;
|
|
12646
|
+
try {
|
|
12647
|
+
fd = openSync3(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
|
|
12648
|
+
} catch (error) {
|
|
12649
|
+
const code = error.code;
|
|
12650
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
12651
|
+
return null;
|
|
12652
|
+
if (code === "ELOOP")
|
|
12653
|
+
unsafe("the path is a symlink");
|
|
12654
|
+
unsafe(`the path could not be opened (${code ?? "unknown error"})`);
|
|
12655
|
+
}
|
|
12656
|
+
try {
|
|
12657
|
+
const before = fstatSync3(fd);
|
|
12658
|
+
if (!before.isFile())
|
|
12659
|
+
unsafe("the path is not a regular file");
|
|
12660
|
+
if (!configFileModeAllowed2(before.mode)) {
|
|
12661
|
+
unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
|
|
12662
|
+
}
|
|
12663
|
+
const uid = process.getuid?.() ?? process.geteuid?.();
|
|
12664
|
+
if (uid !== undefined && before.uid !== uid)
|
|
12665
|
+
unsafe("the file is not owned by the current user");
|
|
12666
|
+
if (before.size > MAX_CREDENTIAL_FILE_BYTES2)
|
|
12667
|
+
unsafe("the file exceeds the size limit");
|
|
12668
|
+
const bytes = readFileSync6(fd);
|
|
12669
|
+
const after = fstatSync3(fd);
|
|
12670
|
+
if (!configFileReadsCoherent2(before, after)) {
|
|
12671
|
+
unsafe("the file changed while being read");
|
|
12672
|
+
}
|
|
12673
|
+
return parseEnvFile2(bytes.toString("utf8"));
|
|
12674
|
+
} finally {
|
|
12675
|
+
if (fd !== -1)
|
|
12676
|
+
closeSync3(fd);
|
|
12677
|
+
}
|
|
12678
|
+
}
|
|
12679
|
+
function readCredentialFile2(path, apiKeyKeys) {
|
|
12680
|
+
const parsed = readAppConfigFile2(path);
|
|
12681
|
+
if (!parsed)
|
|
12682
|
+
return null;
|
|
12683
|
+
for (const key of apiKeyKeys) {
|
|
12684
|
+
if (parsed.unusable.has(key)) {
|
|
12685
|
+
throw new CredentialFileUnsafeError2(path, `${key} is declared but blank or malformed`);
|
|
12686
|
+
}
|
|
12687
|
+
}
|
|
12688
|
+
const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
12689
|
+
if (new Set(values).size > 1) {
|
|
12690
|
+
throw new CredentialFileUnsafeError2(path, "credential aliases disagree");
|
|
12691
|
+
}
|
|
12692
|
+
return values[0] ?? null;
|
|
12693
|
+
}
|
|
12694
|
+
var CREDENTIAL_SHAPED_KEY2 = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
|
|
12695
|
+
function appConfigDiskValue2(name, env, keys) {
|
|
12696
|
+
const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY2.test(key));
|
|
12697
|
+
if (wanted.length === 0)
|
|
12698
|
+
return null;
|
|
12699
|
+
for (const path of credentialDiskSources2(name, env)) {
|
|
12700
|
+
const parsed = readAppConfigFile2(path);
|
|
12701
|
+
if (!parsed)
|
|
12702
|
+
continue;
|
|
12703
|
+
if (wanted.some((key) => parsed.unusable.has(key))) {
|
|
12704
|
+
return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
|
|
12705
|
+
}
|
|
12706
|
+
const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
12707
|
+
if (new Set(values).size > 1)
|
|
12708
|
+
throw new CredentialFileUnsafeError2(path, "configuration aliases disagree");
|
|
12709
|
+
for (const key of wanted) {
|
|
12710
|
+
if (parsed.unusable.has(key))
|
|
12711
|
+
return { key, value: "", path, unusable: true };
|
|
12712
|
+
const value = parsed.values.get(key)?.trim();
|
|
12713
|
+
if (value)
|
|
12714
|
+
return { key, value, path };
|
|
12715
|
+
}
|
|
12716
|
+
}
|
|
12717
|
+
return null;
|
|
12718
|
+
}
|
|
12719
|
+
function assertUsableCredential2(appName, source, value) {
|
|
12720
|
+
if (VAULT_POINTER_SHAPE2.test(value)) {
|
|
12721
|
+
throw new CredentialResolutionError2(appName, `The credential from ${source} looks like a secrets-vault pointer (a path-shaped reference like ` + `'namespace/app/live/api_key'). A vault path is NEVER accepted as a literal API key. ` + `Use ${credentialPointerEnvKey2(appName)} to resolve the key through the vault, or provide the actual key value.`, [source]);
|
|
12722
|
+
}
|
|
12723
|
+
if (!ILLEGAL_IN_HEADER_VALUE2.test(value))
|
|
12724
|
+
return;
|
|
12725
|
+
throw new CredentialResolutionError2(appName, `The credential from ${source} contains characters that cannot be sent in an HTTP header ` + `(a control character or non-ASCII byte). A file written with CR-only line endings is the usual ` + `cause. Rewrite that credential file with one LF-terminated KEY=value line. ` + `The value is not shown here, and is deliberately never logged.`, [source]);
|
|
12726
|
+
}
|
|
12727
|
+
var INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
|
|
12728
|
+
var CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
|
|
12729
|
+
var CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE = "caller-supplied CredentialProvider";
|
|
12730
|
+
function sealCredential2(fields) {
|
|
12731
|
+
const { apiKey } = fields;
|
|
12732
|
+
const visible = {
|
|
12733
|
+
tier: fields.tier,
|
|
12734
|
+
source: fields.source,
|
|
12735
|
+
deliberate: fields.deliberate,
|
|
12736
|
+
diskCandidates: Object.freeze([...fields.diskCandidates]),
|
|
12737
|
+
warning: fields.warning
|
|
12738
|
+
};
|
|
12739
|
+
const sealed = { ...visible };
|
|
12740
|
+
Object.defineProperty(sealed, "apiKey", {
|
|
12741
|
+
value: apiKey,
|
|
12742
|
+
enumerable: false,
|
|
12743
|
+
writable: false,
|
|
12744
|
+
configurable: false
|
|
12745
|
+
});
|
|
12746
|
+
if (fields.pointerVaultKey !== undefined) {
|
|
12747
|
+
Object.defineProperty(sealed, "pointerVaultKey", {
|
|
12748
|
+
value: fields.pointerVaultKey,
|
|
12749
|
+
enumerable: false,
|
|
12750
|
+
writable: false,
|
|
12751
|
+
configurable: false
|
|
12752
|
+
});
|
|
12753
|
+
}
|
|
12754
|
+
Object.defineProperty(sealed, INSPECT_CUSTOM2, {
|
|
12755
|
+
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
12756
|
+
enumerable: false,
|
|
12757
|
+
writable: false,
|
|
12758
|
+
configurable: false
|
|
12759
|
+
});
|
|
12760
|
+
Object.defineProperty(sealed, CREDENTIAL_SEAL2, {
|
|
12761
|
+
value: true,
|
|
12762
|
+
enumerable: false,
|
|
12763
|
+
writable: false,
|
|
12764
|
+
configurable: false
|
|
12765
|
+
});
|
|
12766
|
+
return Object.freeze(sealed);
|
|
12767
|
+
}
|
|
12768
|
+
function isSealedCredential(credential) {
|
|
12769
|
+
return credential[CREDENTIAL_SEAL2] === true;
|
|
12770
|
+
}
|
|
12771
|
+
function explicitCredential(appName, apiKey) {
|
|
12772
|
+
const source = "explicit apiKey option";
|
|
12773
|
+
assertUsableCredential2(appName, source, apiKey);
|
|
12774
|
+
return sealCredential2({
|
|
12775
|
+
apiKey,
|
|
12776
|
+
tier: "argument",
|
|
12777
|
+
source,
|
|
12778
|
+
deliberate: true,
|
|
12779
|
+
diskCandidates: [],
|
|
12780
|
+
warning: null
|
|
12781
|
+
});
|
|
12782
|
+
}
|
|
12783
|
+
function validateAndSealResolvedCredential(appName, credential) {
|
|
12784
|
+
const apiKey = credential.apiKey;
|
|
12785
|
+
assertUsableCredential2(appName, CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE, apiKey);
|
|
12786
|
+
if (!isSealedCredential(credential)) {
|
|
12787
|
+
return sealCredential2({
|
|
12788
|
+
apiKey,
|
|
12789
|
+
tier: "argument",
|
|
12790
|
+
source: CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE,
|
|
12791
|
+
deliberate: true,
|
|
12792
|
+
diskCandidates: [],
|
|
12793
|
+
warning: null
|
|
12794
|
+
});
|
|
12795
|
+
}
|
|
12796
|
+
return sealCredential2({
|
|
12797
|
+
apiKey,
|
|
12798
|
+
tier: credential.tier,
|
|
12799
|
+
source: credential.source,
|
|
12800
|
+
deliberate: credential.deliberate,
|
|
12801
|
+
diskCandidates: credential.diskCandidates,
|
|
12802
|
+
warning: credential.warning,
|
|
12803
|
+
...credential.pointerVaultKey !== undefined ? { pointerVaultKey: credential.pointerVaultKey } : {}
|
|
12804
|
+
});
|
|
12805
|
+
}
|
|
12806
|
+
function firstEnvValue2(env, keys) {
|
|
12807
|
+
for (const key of keys) {
|
|
12808
|
+
if (!Object.prototype.hasOwnProperty.call(env, key))
|
|
12809
|
+
continue;
|
|
12810
|
+
const value = env[key]?.trim();
|
|
12811
|
+
if (value)
|
|
12812
|
+
return { key, value };
|
|
12813
|
+
}
|
|
12814
|
+
return null;
|
|
12815
|
+
}
|
|
12816
|
+
var AMBIENT_ENVIRONMENT2 = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
12817
|
+
function isAmbientEnvironment2(env) {
|
|
12818
|
+
return env === process.env || env[AMBIENT_ENVIRONMENT2] === true;
|
|
12819
|
+
}
|
|
12820
|
+
function defaultKeychainRunner2(argv) {
|
|
12821
|
+
const result = spawnSync2(KEYCHAIN_SECURITY_BIN2, [...argv], {
|
|
12822
|
+
encoding: "utf8",
|
|
12823
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
12824
|
+
timeout: KEYCHAIN_SPAWN_TIMEOUT_MS2
|
|
12825
|
+
});
|
|
12826
|
+
return {
|
|
12827
|
+
status: result.status,
|
|
12828
|
+
stdout: result.stdout ?? "",
|
|
12829
|
+
stderr: result.error ? result.error.message : result.stderr ?? ""
|
|
12830
|
+
};
|
|
12831
|
+
}
|
|
12832
|
+
function keychainTierEnabled2(env, options) {
|
|
12833
|
+
if ((options.platform ?? process.platform) !== "darwin")
|
|
12834
|
+
return false;
|
|
12835
|
+
if (options.enabled !== undefined)
|
|
12836
|
+
return options.enabled;
|
|
12837
|
+
return options.run !== undefined || isAmbientEnvironment2(env);
|
|
12838
|
+
}
|
|
12839
|
+
function keychainAccount2(env, options) {
|
|
12840
|
+
const station = env[KEYCHAIN_STATION_ENV_KEY2]?.trim();
|
|
12841
|
+
if (station)
|
|
12842
|
+
return station;
|
|
12843
|
+
const host = (options.hostname ?? osHostname2)().split(".")[0]?.trim() ?? "";
|
|
12844
|
+
if (host)
|
|
12845
|
+
return host;
|
|
12846
|
+
const user = env.USER?.trim();
|
|
12847
|
+
return user || null;
|
|
12848
|
+
}
|
|
12849
|
+
function keychainFailureHint2(text) {
|
|
12850
|
+
const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
|
|
12851
|
+
const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
|
|
12852
|
+
return clean ? `: ${clean}` : "";
|
|
12853
|
+
}
|
|
12854
|
+
function readKeychainItem2(name, env, kind, options) {
|
|
12855
|
+
if (!SAFE_APP_SLUG2.test(name) || !keychainTierEnabled2(env, options))
|
|
12856
|
+
return null;
|
|
12857
|
+
const account = keychainAccount2(env, options);
|
|
12858
|
+
if (!account)
|
|
12859
|
+
return null;
|
|
12860
|
+
const service = `${KEYCHAIN_SERVICE_PREFIX2}.${name}.${kind}`;
|
|
12861
|
+
const source = `keychain:${service}@${account}`;
|
|
12862
|
+
const run = options.run ?? defaultKeychainRunner2;
|
|
12863
|
+
let result;
|
|
12864
|
+
try {
|
|
12865
|
+
result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
|
|
12866
|
+
} catch (error) {
|
|
12867
|
+
const reason = keychainFailureHint2(error instanceof Error ? error.message : String(error));
|
|
12868
|
+
throw new CredentialResolutionError2(name, `The Keychain lookup for ${source} could not run${reason}. A Keychain failure is never resolved ` + `around: fix the keychain, or delete the item to fall through to the credential on disk.`, [source]);
|
|
12869
|
+
}
|
|
12870
|
+
if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS2)
|
|
12871
|
+
return null;
|
|
12872
|
+
if (result.status !== 0) {
|
|
12873
|
+
throw new CredentialResolutionError2(name, `The Keychain lookup for ${source} failed (security exited ` + `${result.status ?? "without a status"}${keychainFailureHint2(result.stderr)}). A Keychain item that ` + `exists but cannot be read is never resolved around: unlock the keychain, run from a session that ` + `may use it, or delete the item to fall through to the credential on disk.`, [source]);
|
|
12874
|
+
}
|
|
12875
|
+
const value = result.stdout.trim();
|
|
12876
|
+
if (!value) {
|
|
12877
|
+
throw new CredentialResolutionError2(name, `${source} exists but holds an empty value; a declared item never falls through to another ` + `identity. Store a value in it or delete the item.`, [source]);
|
|
12878
|
+
}
|
|
12879
|
+
return { value, source };
|
|
12880
|
+
}
|
|
12881
|
+
function keychainConfigValue2(name, env, options = {}) {
|
|
12882
|
+
return readKeychainItem2(name, env, "api-url", options);
|
|
12883
|
+
}
|
|
12884
|
+
function snapshotClientEnvironment2(name, env) {
|
|
12885
|
+
const keys = clientTransportEnvKeys2(name);
|
|
12886
|
+
const ambient = isAmbientEnvironment2(env);
|
|
12887
|
+
const snapshot = Object.create(null);
|
|
12888
|
+
for (const key of [
|
|
12889
|
+
...keys.apiUrlKeys,
|
|
12890
|
+
...keys.apiKeyKeys,
|
|
12891
|
+
credentialOverrideEnvKey2(name),
|
|
12892
|
+
credentialPointerEnvKey2(name),
|
|
12893
|
+
CREDENTIAL_PROFILE_ENV_KEY2,
|
|
12894
|
+
"HOME",
|
|
12895
|
+
HASNA_HOME_ENV_KEY2,
|
|
12896
|
+
HASNA_CONFIG_HOME_ENV_KEY2,
|
|
12897
|
+
KEYCHAIN_STATION_ENV_KEY2,
|
|
12898
|
+
"USER"
|
|
12899
|
+
]) {
|
|
12900
|
+
const descriptor = Object.getOwnPropertyDescriptor(env, key);
|
|
12901
|
+
if (!descriptor)
|
|
12902
|
+
continue;
|
|
12903
|
+
if (!("value" in descriptor)) {
|
|
12904
|
+
throw new CredentialResolutionError2(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
|
|
12905
|
+
}
|
|
12906
|
+
if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
|
|
12907
|
+
throw new CredentialResolutionError2(name, `${key} must be a string data property.`, [key]);
|
|
12908
|
+
}
|
|
12909
|
+
snapshot[key] = descriptor.value;
|
|
12910
|
+
}
|
|
12911
|
+
if (ambient) {
|
|
12912
|
+
Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT2, {
|
|
12913
|
+
value: true,
|
|
12914
|
+
enumerable: false,
|
|
12915
|
+
writable: false,
|
|
12916
|
+
configurable: false
|
|
12917
|
+
});
|
|
12918
|
+
}
|
|
12919
|
+
return Object.freeze(snapshot);
|
|
12920
|
+
}
|
|
12921
|
+
function resolveCredential2(name, env, options = {}) {
|
|
12922
|
+
env = snapshotClientEnvironment2(name, env);
|
|
12923
|
+
const { apiKeyKeys } = clientTransportEnvKeys2(name);
|
|
12924
|
+
const diskPaths = credentialDiskSources2(name, env);
|
|
12925
|
+
if (options.apiKey !== undefined) {
|
|
12926
|
+
const explicitKey = options.apiKey.trim();
|
|
12927
|
+
if (!explicitKey) {
|
|
12928
|
+
throw new CredentialResolutionError2(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
|
|
12929
|
+
}
|
|
12930
|
+
assertUsableCredential2(name, "the explicit apiKey argument", explicitKey);
|
|
12931
|
+
return sealCredential2({
|
|
12932
|
+
apiKey: explicitKey,
|
|
12933
|
+
tier: "argument",
|
|
12934
|
+
source: "explicit apiKey argument",
|
|
12935
|
+
deliberate: true,
|
|
12936
|
+
diskCandidates: diskPaths,
|
|
12937
|
+
warning: null
|
|
12938
|
+
});
|
|
12939
|
+
}
|
|
12940
|
+
const overrideKeyName = credentialOverrideEnvKey2(name);
|
|
12941
|
+
const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
|
|
12942
|
+
if (overrideRaw !== undefined) {
|
|
12943
|
+
const override = overrideRaw.trim();
|
|
12944
|
+
if (!override) {
|
|
12945
|
+
throw new CredentialResolutionError2(name, `${overrideKeyName} is set but empty. It is a deliberate override, so it is not resolved around: ` + `either give it a real key or unset it to fall back to the credential on disk.`, [overrideKeyName]);
|
|
12946
|
+
}
|
|
12947
|
+
assertUsableCredential2(name, overrideKeyName, override);
|
|
12948
|
+
return sealCredential2({
|
|
12949
|
+
apiKey: override,
|
|
12950
|
+
tier: "override",
|
|
12951
|
+
source: overrideKeyName,
|
|
12952
|
+
deliberate: true,
|
|
12953
|
+
diskCandidates: diskPaths,
|
|
12954
|
+
warning: null
|
|
12955
|
+
});
|
|
12956
|
+
}
|
|
12957
|
+
const pointerKeyName = credentialPointerEnvKey2(name);
|
|
12958
|
+
const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
|
|
12959
|
+
if (pointerRaw !== undefined) {
|
|
12960
|
+
const pointer = pointerRaw.trim();
|
|
12961
|
+
if (!pointer) {
|
|
12962
|
+
throw new CredentialResolutionError2(name, `${pointerKeyName} is set but empty. It is a deliberate vault pointer, so it is not resolved around: ` + `either give it a vault item key or unset it to fall back to the credential on disk.`, [pointerKeyName]);
|
|
12963
|
+
}
|
|
12964
|
+
if (!VAULT_POINTER_SHAPE2.test(pointer)) {
|
|
12965
|
+
throw new CredentialResolutionError2(name, `${pointerKeyName} must name a vault ITEM KEY (a path-shaped reference like ` + `'namespace/app/live/api_key'), not a credential value. A pointer that carries a literal is refused.`, [pointerKeyName]);
|
|
12966
|
+
}
|
|
12967
|
+
return sealCredential2({
|
|
12968
|
+
apiKey: "",
|
|
12969
|
+
pointerVaultKey: pointer,
|
|
12970
|
+
tier: "pointer",
|
|
12971
|
+
source: pointerKeyName,
|
|
12972
|
+
deliberate: true,
|
|
12973
|
+
diskCandidates: diskPaths,
|
|
12974
|
+
warning: null
|
|
12975
|
+
});
|
|
12976
|
+
}
|
|
12977
|
+
if (options.profile !== undefined && !options.profile.trim()) {
|
|
12978
|
+
throw new CredentialResolutionError2(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
|
|
12979
|
+
}
|
|
12980
|
+
const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY2) ? env[CREDENTIAL_PROFILE_ENV_KEY2] : undefined;
|
|
12981
|
+
if (profileRaw !== undefined && !profileRaw.trim()) {
|
|
12982
|
+
throw new CredentialResolutionError2(name, `${CREDENTIAL_PROFILE_ENV_KEY2} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY2]);
|
|
12983
|
+
}
|
|
12984
|
+
const profile = options.profile?.trim() || profileRaw?.trim();
|
|
12985
|
+
if (profile) {
|
|
12986
|
+
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY2;
|
|
12987
|
+
if (!SAFE_PROFILE2.test(profile)) {
|
|
12988
|
+
throw new CredentialResolutionError2(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
|
|
12989
|
+
}
|
|
12990
|
+
const paths = profileDiskSources2(name, env, profile);
|
|
12991
|
+
for (const path of paths) {
|
|
12992
|
+
const value = readCredentialFile2(path, apiKeyKeys);
|
|
12993
|
+
if (value) {
|
|
12994
|
+
assertUsableCredential2(name, path, value);
|
|
12995
|
+
return sealCredential2({
|
|
12996
|
+
apiKey: value,
|
|
12997
|
+
tier: "profile",
|
|
12998
|
+
source: path,
|
|
12999
|
+
deliberate: true,
|
|
13000
|
+
diskCandidates: paths,
|
|
13001
|
+
warning: null
|
|
13002
|
+
});
|
|
13003
|
+
}
|
|
13004
|
+
}
|
|
13005
|
+
throw new CredentialResolutionError2(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY2}.`, paths);
|
|
13006
|
+
}
|
|
13007
|
+
const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
|
|
13008
|
+
const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
|
|
13009
|
+
if (blankEnv) {
|
|
13010
|
+
throw new CredentialResolutionError2(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
|
|
13011
|
+
}
|
|
13012
|
+
if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
|
|
13013
|
+
throw new CredentialResolutionError2(name, `${definedEnvEntries.map((entry) => entry.key).join(" and ")} disagree; credential aliases must be identical or only one may be set.`, definedEnvEntries.map((entry) => entry.key));
|
|
13014
|
+
}
|
|
13015
|
+
const envHit = firstEnvValue2(env, apiKeyKeys);
|
|
13016
|
+
const keychainHit = readKeychainItem2(name, env, "api-key", options.keychain ?? {});
|
|
13017
|
+
if (keychainHit) {
|
|
13018
|
+
assertUsableCredential2(name, keychainHit.source, keychainHit.value);
|
|
13019
|
+
const warning = envHit && envHit.value !== keychainHit.value ? `Credential sources disagree for '${name}': ${keychainHit.source} and ${envHit.key} hold ` + `different keys. ${keychainHit.source} wins, because the Keychain is re-read on every call while ` + `an environment variable is a snapshot. Reconcile them \u2014 a rotation that updated only one leaves ` + `the other to fail 401 wherever it is loaded first.` : null;
|
|
13020
|
+
return sealCredential2({
|
|
13021
|
+
apiKey: keychainHit.value,
|
|
13022
|
+
tier: "keychain",
|
|
13023
|
+
source: keychainHit.source,
|
|
13024
|
+
deliberate: false,
|
|
13025
|
+
diskCandidates: diskPaths,
|
|
13026
|
+
warning
|
|
13027
|
+
});
|
|
13028
|
+
}
|
|
13029
|
+
const diskSourceList = credentialDiskSourceList2(name, env, null);
|
|
13030
|
+
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile2(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
13031
|
+
if (diskHits.length > 0) {
|
|
13032
|
+
const winner = diskHits[0];
|
|
13033
|
+
assertUsableCredential2(name, winner.src.path, winner.value);
|
|
13034
|
+
const divergentSources = [
|
|
13035
|
+
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
13036
|
+
...envHit && envHit.value !== winner.value ? [envHit.key] : []
|
|
13037
|
+
];
|
|
13038
|
+
const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.src.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.src.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
|
|
13039
|
+
return sealCredential2({
|
|
13040
|
+
apiKey: winner.value,
|
|
13041
|
+
tier: winner.src.tier,
|
|
13042
|
+
source: winner.src.path,
|
|
13043
|
+
deliberate: false,
|
|
13044
|
+
diskCandidates: diskPaths,
|
|
13045
|
+
warning
|
|
13046
|
+
});
|
|
13047
|
+
}
|
|
13048
|
+
if (envHit) {
|
|
13049
|
+
assertUsableCredential2(name, envHit.key, envHit.value);
|
|
13050
|
+
return sealCredential2({
|
|
13051
|
+
apiKey: envHit.value,
|
|
13052
|
+
tier: "env",
|
|
13053
|
+
source: envHit.key,
|
|
13054
|
+
deliberate: false,
|
|
13055
|
+
diskCandidates: diskPaths,
|
|
13056
|
+
warning: null
|
|
13057
|
+
});
|
|
13058
|
+
}
|
|
13059
|
+
return null;
|
|
13060
|
+
}
|
|
13061
|
+
var SECRETS_PACKAGE_SPECIFIER2 = "@hasna/" + "secrets";
|
|
13062
|
+
var requireSecretsSdk2 = createRequire2(import.meta.url);
|
|
13063
|
+
async function completePointerCredential(name, pointerResolution, env = process.env) {
|
|
13064
|
+
const vaultKey = pointerResolution.pointerVaultKey;
|
|
13065
|
+
const pointerEnvKey = pointerResolution.source;
|
|
13066
|
+
if (!vaultKey) {
|
|
13067
|
+
throw new CredentialResolutionError2(name, `Pointer resolution from ${pointerEnvKey} carries no vault item key; this is a defect in the resolver.`, [pointerEnvKey]);
|
|
13068
|
+
}
|
|
13069
|
+
let secretsSdk;
|
|
13070
|
+
try {
|
|
13071
|
+
secretsSdk = requireSecretsSdk2(SECRETS_PACKAGE_SPECIFIER2);
|
|
13072
|
+
} catch {
|
|
13073
|
+
throw new CredentialResolutionError2(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets SDK (@hasna/secrets) is not installed ` + `in this process. A vault pointer is TERMINAL: install @hasna/secrets to resolve it, or unset ${pointerEnvKey}.`, [pointerEnvKey]);
|
|
13074
|
+
}
|
|
13075
|
+
let client;
|
|
13076
|
+
try {
|
|
13077
|
+
client = secretsSdk.createSecretsClientFromEnv(env);
|
|
13078
|
+
} catch {
|
|
13079
|
+
throw new CredentialResolutionError2(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets client could not be configured from this ` + `environment (the secrets service URL and key env are missing or invalid). A vault pointer is TERMINAL and ` + `never falls through to a literal or disk credential.`, [pointerEnvKey]);
|
|
13080
|
+
}
|
|
13081
|
+
let secret;
|
|
13082
|
+
try {
|
|
13083
|
+
secret = await client.getSecret({ key: vaultKey });
|
|
13084
|
+
} catch {
|
|
13085
|
+
throw new CredentialResolutionError2(name, `${pointerEnvKey} names vault item '${vaultKey}', but the vault could not be reached or the item is ` + `unavailable. A vault pointer is TERMINAL and never falls through to a literal or disk credential.`, [pointerEnvKey]);
|
|
13086
|
+
}
|
|
13087
|
+
const value = secret.value;
|
|
13088
|
+
if (!value) {
|
|
13089
|
+
throw new CredentialResolutionError2(name, `${pointerEnvKey} resolved vault item '${vaultKey}', but it holds no value. A vault pointer is TERMINAL.`, [pointerEnvKey]);
|
|
13090
|
+
}
|
|
13091
|
+
assertUsableCredential2(name, `${pointerEnvKey} -> vault:${vaultKey}`, value);
|
|
13092
|
+
return sealCredential2({
|
|
13093
|
+
apiKey: value,
|
|
13094
|
+
tier: "pointer",
|
|
13095
|
+
source: `${pointerEnvKey} -> vault:${vaultKey}`,
|
|
13096
|
+
deliberate: true,
|
|
13097
|
+
diskCandidates: pointerResolution.diskCandidates,
|
|
13098
|
+
warning: null
|
|
13099
|
+
});
|
|
13100
|
+
}
|
|
13101
|
+
var DEFAULT_FLEET_GATEWAY_ORIGIN2 = "https://api.hasna.com";
|
|
13102
|
+
var DEFAULT_AUTHORITY_SOURCE2 = "default";
|
|
13103
|
+
function defaultFleetGatewayBaseUrl2(name) {
|
|
13104
|
+
return `${DEFAULT_FLEET_GATEWAY_ORIGIN2}/${validateAppSlug2(name)}`;
|
|
13105
|
+
}
|
|
13106
|
+
var ASCII_CONTROL_PATTERN2 = /[\u0000-\u001f\u007f]/;
|
|
13107
|
+
var DNS_LABEL_PATTERN2 = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
13108
|
+
function isValidDnsDomain2(value) {
|
|
13109
|
+
if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN2.test(value) || /[^\x00-\x7f]/.test(value)) {
|
|
13110
|
+
return false;
|
|
13111
|
+
}
|
|
13112
|
+
return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN2.test(label));
|
|
13113
|
+
}
|
|
13114
|
+
function validateAppSlug2(name) {
|
|
13115
|
+
if (name.length > 63 || !DNS_LABEL_PATTERN2.test(name)) {
|
|
13116
|
+
throw new Error("App name must be one lowercase DNS label.");
|
|
13117
|
+
}
|
|
13118
|
+
return name;
|
|
13119
|
+
}
|
|
13120
|
+
function rawAuthority2(value) {
|
|
13121
|
+
const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
|
|
13122
|
+
if (!match)
|
|
13123
|
+
throw new Error("API URL must be absolute.");
|
|
13124
|
+
const afterScheme = value.slice(match[0].length);
|
|
13125
|
+
const boundary = afterScheme.search(/[/?#]/);
|
|
13126
|
+
const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
|
|
13127
|
+
if (!authority)
|
|
13128
|
+
throw new Error("API URL must include a hostname.");
|
|
13129
|
+
return authority;
|
|
13130
|
+
}
|
|
13131
|
+
function assertCanonicalPort2(port) {
|
|
13132
|
+
if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
|
|
13133
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
13134
|
+
}
|
|
13135
|
+
const numericPort = Number(port);
|
|
13136
|
+
if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
|
|
13137
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
13138
|
+
}
|
|
13139
|
+
}
|
|
13140
|
+
function canonicalAuthorityHostname2(authority) {
|
|
13141
|
+
let rawHostname;
|
|
13142
|
+
if (authority.startsWith("[")) {
|
|
13143
|
+
const closingBracket = authority.indexOf("]");
|
|
13144
|
+
if (closingBracket === -1) {
|
|
13145
|
+
throw new Error("API URL authority must contain a canonical hostname.");
|
|
13146
|
+
}
|
|
13147
|
+
rawHostname = authority.slice(0, closingBracket + 1);
|
|
13148
|
+
const portSuffix = authority.slice(closingBracket + 1);
|
|
13149
|
+
if (portSuffix) {
|
|
13150
|
+
if (!portSuffix.startsWith(":")) {
|
|
13151
|
+
throw new Error("API URL authority must contain a canonical hostname and port.");
|
|
13152
|
+
}
|
|
13153
|
+
assertCanonicalPort2(portSuffix.slice(1));
|
|
13154
|
+
}
|
|
13155
|
+
if (isIP2(rawHostname.slice(1, -1)) !== 6) {
|
|
13156
|
+
throw new Error("API URL authority must contain a canonical IPv6 literal.");
|
|
13157
|
+
}
|
|
13158
|
+
} else {
|
|
13159
|
+
const firstColon = authority.indexOf(":");
|
|
13160
|
+
const lastColon = authority.lastIndexOf(":");
|
|
13161
|
+
if (firstColon !== lastColon) {
|
|
13162
|
+
throw new Error("IPv6 API URL authorities must use brackets.");
|
|
13163
|
+
}
|
|
13164
|
+
if (lastColon !== -1) {
|
|
13165
|
+
const port = authority.slice(lastColon + 1);
|
|
13166
|
+
assertCanonicalPort2(port);
|
|
13167
|
+
rawHostname = authority.slice(0, lastColon);
|
|
13168
|
+
} else {
|
|
13169
|
+
rawHostname = authority;
|
|
13170
|
+
}
|
|
13171
|
+
const ipVersion = isIP2(rawHostname);
|
|
13172
|
+
const numericAddressParts = rawHostname.split(".");
|
|
13173
|
+
const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
|
|
13174
|
+
if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain2(rawHostname.toLowerCase())) {
|
|
13175
|
+
throw new Error("API URL authority must contain a canonical ASCII hostname.");
|
|
13176
|
+
}
|
|
13177
|
+
}
|
|
13178
|
+
return rawHostname.toLowerCase();
|
|
13179
|
+
}
|
|
13180
|
+
function isDeliberateLoopbackHttpAuthority2(authority) {
|
|
13181
|
+
return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
|
|
13182
|
+
}
|
|
13183
|
+
function toV1BaseUrl2(apiUrl) {
|
|
13184
|
+
if (ASCII_CONTROL_PATTERN2.test(apiUrl)) {
|
|
13185
|
+
throw new Error("API URL must not contain ASCII control characters.");
|
|
13186
|
+
}
|
|
13187
|
+
const input = apiUrl.trim();
|
|
13188
|
+
const authority = rawAuthority2(input);
|
|
13189
|
+
if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
|
|
13190
|
+
throw new Error("API URL authority must be canonical ASCII without credentials.");
|
|
13191
|
+
}
|
|
13192
|
+
const canonicalHostname = canonicalAuthorityHostname2(authority);
|
|
13193
|
+
const url = new URL(input);
|
|
13194
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
13195
|
+
throw new Error("API URL must use http or https.");
|
|
13196
|
+
}
|
|
13197
|
+
if (url.username || url.password) {
|
|
13198
|
+
throw new Error("API URL must not include credentials.");
|
|
13199
|
+
}
|
|
13200
|
+
if (!url.hostname || url.hostname.endsWith(".")) {
|
|
13201
|
+
throw new Error("API URL must include a canonical hostname.");
|
|
13202
|
+
}
|
|
13203
|
+
if (url.hostname.toLowerCase() !== canonicalHostname) {
|
|
13204
|
+
throw new Error("API URL authority must not rely on parser hostname normalization.");
|
|
13205
|
+
}
|
|
13206
|
+
if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
|
|
13207
|
+
throw new Error("API URL must not use IDN or punycode hostnames.");
|
|
13208
|
+
}
|
|
13209
|
+
if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority2(authority)) {
|
|
13210
|
+
throw new Error("API URL may use http only for an exact loopback authority.");
|
|
13211
|
+
}
|
|
13212
|
+
if (url.search || url.hash) {
|
|
13213
|
+
throw new Error("API URL must not include a query string or fragment.");
|
|
13214
|
+
}
|
|
13215
|
+
let path = url.pathname.replace(/\/+$/, "");
|
|
13216
|
+
if (path.endsWith("/v1"))
|
|
13217
|
+
path = path.slice(0, -"/v1".length);
|
|
13218
|
+
url.pathname = `${path}/v1`;
|
|
13219
|
+
return url.toString().replace(/\/+$/, "");
|
|
13220
|
+
}
|
|
13221
|
+
class ClientTransportConfigurationError2 extends Error {
|
|
13222
|
+
appName;
|
|
13223
|
+
sources;
|
|
13224
|
+
constructor(appName, message, sources = []) {
|
|
13225
|
+
super(message);
|
|
13226
|
+
this.name = "ClientTransportConfigurationError";
|
|
13227
|
+
this.appName = appName;
|
|
13228
|
+
this.sources = Object.freeze([...sources]);
|
|
13229
|
+
}
|
|
13230
|
+
}
|
|
13231
|
+
function resolveClientTransportSnapshot2(name, env = process.env, options = {}) {
|
|
13232
|
+
env = snapshotClientEnvironment2(name, env);
|
|
13233
|
+
const keys = clientTransportEnvKeys2(name);
|
|
13234
|
+
const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
|
|
13235
|
+
const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
|
|
13236
|
+
if (blankUrl) {
|
|
13237
|
+
throw new ClientTransportConfigurationError2(name, `${blankUrl.key} is set but blank; public clients require an explicit HTTPS API URL and never select local storage.`, [blankUrl.key]);
|
|
13238
|
+
}
|
|
13239
|
+
const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN2.test(entry.raw));
|
|
13240
|
+
if (controlledUrl) {
|
|
13241
|
+
throw new ClientTransportConfigurationError2(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
|
|
13242
|
+
}
|
|
13243
|
+
const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
|
|
13244
|
+
if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
|
|
13245
|
+
throw new ClientTransportConfigurationError2(name, `${usableUrlEntries.map((entry) => entry.key).join(" and ")} disagree; client authority aliases must be identical or only one may be set.`, usableUrlEntries.map((entry) => entry.key));
|
|
13246
|
+
}
|
|
13247
|
+
const envUrlHit = usableUrlEntries[0] ?? null;
|
|
13248
|
+
const keychainUrlHit = keychainConfigValue2(name, env, options.credentials?.keychain);
|
|
13249
|
+
const diskConfigUrlHit = appConfigDiskValue2(name, env, keys.apiUrlKeys);
|
|
13250
|
+
if (diskConfigUrlHit?.unusable) {
|
|
13251
|
+
throw new ClientTransportConfigurationError2(name, `${diskConfigUrlHit.key} in ${diskConfigUrlHit.path} is declared but blank or malformed; public clients require a valid HTTPS service authority.`, [diskConfigUrlHit.path]);
|
|
13252
|
+
}
|
|
13253
|
+
const urlCandidates = [
|
|
13254
|
+
...envUrlHit ? [envUrlHit] : [],
|
|
13255
|
+
...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
|
|
13256
|
+
...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
|
|
13257
|
+
];
|
|
13258
|
+
const configuredUrl = urlCandidates[0] ?? null;
|
|
13259
|
+
const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
|
|
13260
|
+
if (configuredUrl && divergentUrls.length > 0) {
|
|
13261
|
+
throw new ClientTransportConfigurationError2(name, `${configuredUrl.key} and ${divergentUrls.map((candidate) => candidate.key).join(" and ")} select different service authorities; refusing to send a credential written for one authority to the other.`, urlCandidates.map((candidate) => candidate.key));
|
|
13262
|
+
}
|
|
13263
|
+
const warnings = [];
|
|
13264
|
+
if (configuredUrl && !envUrlHit) {
|
|
13265
|
+
warnings.push(`No ${keys.apiUrlKeys[0]} in the environment; the server URL in ${configuredUrl.key} was used, so this client connects to the server. ` + `Keep that entry aligned with the intended service authority.`);
|
|
13266
|
+
}
|
|
13267
|
+
const credential = resolveCredential2(name, env, options.credentials);
|
|
13268
|
+
if (!credential) {
|
|
13269
|
+
const diskHint = credentialDiskSourcesForMessage2(name, env);
|
|
13270
|
+
const lead = configuredUrl ? `${configuredUrl.key} selects the HTTP server for '${name}', but no API key could be resolved` : `${keys.apiUrlKeys[0]} is not set and no API key could be resolved for '${name}'; a credential is required before the default fleet gateway authority applies`;
|
|
13271
|
+
warnings.push(`${lead}; refusing to create an unauthenticated client \u2014 public clients never fall back to SQLite or another local store. ` + `Looked in the Keychain (macOS only), then for a credential file at ${diskHint}, then for ${keys.apiKeyKeys[0]} in the environment.`);
|
|
13272
|
+
throw new ClientTransportConfigurationError2(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
|
|
13273
|
+
}
|
|
13274
|
+
if (credential.warning)
|
|
13275
|
+
warnings.push(credential.warning);
|
|
13276
|
+
let urlHit;
|
|
13277
|
+
if (configuredUrl) {
|
|
13278
|
+
urlHit = configuredUrl;
|
|
13279
|
+
} else {
|
|
13280
|
+
try {
|
|
13281
|
+
urlHit = { key: DEFAULT_AUTHORITY_SOURCE2, value: defaultFleetGatewayBaseUrl2(name) };
|
|
13282
|
+
} catch (error) {
|
|
13283
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
13284
|
+
throw new ClientTransportConfigurationError2(name, `No ${keys.apiUrlKeys[0]} is configured and the default fleet gateway authority cannot be composed for '${name}': ${message}`, [keys.apiUrlKeys[0]]);
|
|
13285
|
+
}
|
|
13286
|
+
}
|
|
13287
|
+
const apiUrlSource = urlHit.key;
|
|
13288
|
+
let baseUrl;
|
|
13289
|
+
try {
|
|
13290
|
+
baseUrl = toV1BaseUrl2(urlHit.value);
|
|
13291
|
+
} catch (error) {
|
|
13292
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
13293
|
+
throw new ClientTransportConfigurationError2(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
|
|
13294
|
+
}
|
|
13295
|
+
return {
|
|
13296
|
+
resolution: {
|
|
13297
|
+
transport: "http",
|
|
13298
|
+
transportSource: urlHit.key,
|
|
13299
|
+
baseUrl,
|
|
13300
|
+
apiUrlSource,
|
|
13301
|
+
apiKeyPresent: true,
|
|
13302
|
+
apiKeySource: credential.source,
|
|
13303
|
+
apiKeyTier: credential.tier,
|
|
13304
|
+
misconfigured: false,
|
|
13305
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
13306
|
+
},
|
|
13307
|
+
credential
|
|
13308
|
+
};
|
|
13309
|
+
}
|
|
13310
|
+
function credentialDiskSourcesForMessage2(name, env) {
|
|
13311
|
+
const paths = credentialDiskSources2(name, env);
|
|
13312
|
+
return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
|
|
13313
|
+
}
|
|
13314
|
+
|
|
13315
|
+
class HasnaHttpError extends Error {
|
|
13316
|
+
status;
|
|
13317
|
+
method;
|
|
13318
|
+
path;
|
|
13319
|
+
credentialSource;
|
|
13320
|
+
credentialTier;
|
|
13321
|
+
constructor(method, path, status, body, credential) {
|
|
13322
|
+
const guidance = credential ? `. ${credential.guidance}` : "";
|
|
13323
|
+
super(`Hasna cloud request failed: ${method} ${path} -> ${status}${guidance}`);
|
|
13324
|
+
this.name = "HasnaHttpError";
|
|
13325
|
+
this.status = status;
|
|
13326
|
+
this.method = method;
|
|
13327
|
+
this.path = path;
|
|
13328
|
+
Object.defineProperty(this, "body", {
|
|
13329
|
+
value: body,
|
|
13330
|
+
enumerable: status !== 401 && status !== 403,
|
|
13331
|
+
writable: false,
|
|
13332
|
+
configurable: false
|
|
13333
|
+
});
|
|
13334
|
+
this.credentialSource = credential?.source ?? null;
|
|
13335
|
+
this.credentialTier = credential?.tier ?? null;
|
|
13336
|
+
}
|
|
13337
|
+
}
|
|
13338
|
+
function currentCredential(name, apiKey) {
|
|
13339
|
+
if (typeof apiKey === "function") {
|
|
13340
|
+
return validateAndSealResolvedCredential(name, apiKey());
|
|
13341
|
+
}
|
|
13342
|
+
return explicitCredential(name, apiKey);
|
|
13343
|
+
}
|
|
13344
|
+
async function resolveRequestCredential(name, apiKey, env = process.env) {
|
|
13345
|
+
const resolved = currentCredential(name, apiKey);
|
|
13346
|
+
if (resolved.tier === "pointer") {
|
|
13347
|
+
return completePointerCredential(name, resolved, env);
|
|
13348
|
+
}
|
|
13349
|
+
return resolved;
|
|
13350
|
+
}
|
|
13351
|
+
function authFailureGuidance(credential) {
|
|
13352
|
+
const origin = `The API key for this request came from ${credential.source}`;
|
|
13353
|
+
if (credential.deliberate) {
|
|
13354
|
+
const remedy = credential.source === CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE ? `Fix that provider so it returns the current key, or replace it with resolveCredential() ` + `so diagnostics can name the original source.` : `Rotate that key, or unset the override to use the credential on disk.`;
|
|
13355
|
+
return `${origin} \u2014 a credential you selected deliberately. It was NOT substituted with any other key: ` + `falling back here would authenticate as a different principal than the one you named, which is ` + `exactly the failure an override exists to prevent. ${remedy}`;
|
|
13356
|
+
}
|
|
13357
|
+
if (credential.tier === "env") {
|
|
13358
|
+
const target = credential.diskCandidates[0];
|
|
13359
|
+
const remedy = target ? `Store the CURRENT key in the Keychain or write it to ${target} \u2014 both are re-read on every call, so ` + `rotations take effect immediately and in every shell. Do not simply unset ${credential.source}: ` + `nothing was found in the Keychain or on disk, so that would leave this client with no credential at all.` : `This environment has no HOME or HASNA_HOME, so no credential file could be consulted; the disk tier is ` + `unavailable here and there is nothing to fall back to. Set HOME, or supply the key explicitly.`;
|
|
13360
|
+
return `${origin}, a variable in this process's environment. If a wrapper injected it for this one process, the ` + `wrapper re-reads its store on every invocation and the stored key itself is being rejected \u2014 rotate it. ` + `If this SHELL exported it, the export is a snapshot taken when the shell started: a STALE SHELL that ` + `exported the key before it was rotated keeps sending the old one until it exits. ${remedy}`;
|
|
13361
|
+
}
|
|
13362
|
+
if (credential.tier === "keychain") {
|
|
13363
|
+
return `${origin}, which was re-read from the Keychain on this very call \u2014 so a stale shell is NOT the cause ` + `here. The stored item is genuinely being rejected: update it with the current key, or re-run the fleet ` + `key distribution so this machine gets the current key.`;
|
|
13364
|
+
}
|
|
13365
|
+
return `${origin}, which was re-read from disk on this very call \u2014 so a stale shell is NOT the cause here. ` + `The stored credential is genuinely being rejected: rotate it, or re-run the fleet key distribution ` + `so this machine gets the current key.`;
|
|
13366
|
+
}
|
|
13367
|
+
var DEFAULT_RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
|
|
13368
|
+
var IDEMPOTENT_METHODS2 = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
13369
|
+
var AUTHORITY_OVERRIDE_HEADERS2 = new Set([
|
|
13370
|
+
"host",
|
|
13371
|
+
":authority",
|
|
13372
|
+
"forwarded",
|
|
13373
|
+
"x-forwarded-host",
|
|
13374
|
+
"x-original-host"
|
|
13375
|
+
]);
|
|
13376
|
+
function assertNoAuthorityOverrideHeaders(headers, source) {
|
|
13377
|
+
if (!headers)
|
|
13378
|
+
return;
|
|
13379
|
+
const forbidden = Object.keys(headers).find((name) => AUTHORITY_OVERRIDE_HEADERS2.has(name.trim().toLowerCase()));
|
|
13380
|
+
if (forbidden) {
|
|
13381
|
+
throw new Error(`Authenticated ${source} headers must not set authority header '${forbidden}'.`);
|
|
13382
|
+
}
|
|
13383
|
+
}
|
|
13384
|
+
function appendQuery(path, query) {
|
|
13385
|
+
if (!query)
|
|
13386
|
+
return path;
|
|
13387
|
+
const params = query instanceof URLSearchParams ? query : new URLSearchParams;
|
|
13388
|
+
if (!(query instanceof URLSearchParams)) {
|
|
13389
|
+
for (const [key, value] of Object.entries(query)) {
|
|
13390
|
+
if (value === null || value === undefined)
|
|
13391
|
+
continue;
|
|
13392
|
+
if (Array.isArray(value)) {
|
|
13393
|
+
for (const v of value)
|
|
13394
|
+
params.append(key, String(v));
|
|
13395
|
+
} else {
|
|
13396
|
+
params.append(key, String(value));
|
|
13397
|
+
}
|
|
13398
|
+
}
|
|
13399
|
+
}
|
|
13400
|
+
const qs = params.toString();
|
|
13401
|
+
if (!qs)
|
|
13402
|
+
return path;
|
|
13403
|
+
return `${path}${path.includes("?") ? "&" : "?"}${qs}`;
|
|
13404
|
+
}
|
|
13405
|
+
var defaultSleep = (ms) => new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
13406
|
+
function createHasnaHttpTransportInternal(options, requestBindingProvider) {
|
|
13407
|
+
const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
13408
|
+
const base = toV1BaseUrl2(options.baseUrl);
|
|
13409
|
+
const timeoutMs = options.timeoutMs ?? 30000;
|
|
13410
|
+
const sleep = options.sleepImpl ?? defaultSleep;
|
|
13411
|
+
const defaultRetry = options.retry;
|
|
13412
|
+
function resolveRetry(callRetry) {
|
|
13413
|
+
const chosen = callRetry !== undefined ? callRetry : defaultRetry;
|
|
13414
|
+
if (chosen === false)
|
|
13415
|
+
return null;
|
|
13416
|
+
const r = chosen ?? {};
|
|
13417
|
+
return {
|
|
13418
|
+
retries: r.retries ?? 2,
|
|
13419
|
+
baseDelayMs: r.baseDelayMs ?? 200,
|
|
13420
|
+
maxDelayMs: r.maxDelayMs ?? 2000,
|
|
13421
|
+
retryStatuses: r.retryStatuses ?? [...DEFAULT_RETRY_STATUSES]
|
|
13422
|
+
};
|
|
13423
|
+
}
|
|
13424
|
+
async function once(method, rel, url, body, opts, credential) {
|
|
13425
|
+
assertNoAuthorityOverrideHeaders(options.headers, "transport");
|
|
13426
|
+
assertNoAuthorityOverrideHeaders(opts.headers, "request");
|
|
13427
|
+
const headers = {
|
|
13428
|
+
"x-api-key": credential.apiKey,
|
|
13429
|
+
Authorization: `Bearer ${credential.apiKey}`,
|
|
13430
|
+
Accept: "application/json",
|
|
13431
|
+
...options.headers ?? {},
|
|
13432
|
+
...opts.headers ?? {}
|
|
13433
|
+
};
|
|
13434
|
+
if (opts.idempotencyKey)
|
|
13435
|
+
headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
13436
|
+
const init = {
|
|
13437
|
+
method,
|
|
13438
|
+
headers,
|
|
13439
|
+
redirect: "manual"
|
|
13440
|
+
};
|
|
13441
|
+
if (body !== undefined) {
|
|
13442
|
+
headers["Content-Type"] = "application/json";
|
|
13443
|
+
init.body = JSON.stringify(body);
|
|
13444
|
+
}
|
|
13445
|
+
const controller = new AbortController;
|
|
13446
|
+
const onAbort = () => controller.abort();
|
|
13447
|
+
if (opts.signal) {
|
|
13448
|
+
if (opts.signal.aborted)
|
|
13449
|
+
controller.abort();
|
|
13450
|
+
else
|
|
13451
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
13452
|
+
}
|
|
13453
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? timeoutMs);
|
|
13454
|
+
init.signal = controller.signal;
|
|
13455
|
+
let response;
|
|
13456
|
+
try {
|
|
13457
|
+
response = await fetchImpl(url, init);
|
|
13458
|
+
} catch (error) {
|
|
13459
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
13460
|
+
if (opts.signal?.aborted)
|
|
13461
|
+
return { ok: false, retryable: false, error: err };
|
|
13462
|
+
return { ok: false, retryable: true, error: err };
|
|
13463
|
+
} finally {
|
|
13464
|
+
clearTimeout(timer);
|
|
13465
|
+
if (opts.signal)
|
|
13466
|
+
opts.signal.removeEventListener("abort", onAbort);
|
|
13467
|
+
}
|
|
13468
|
+
const authenticationFailure = response.status === 401 || response.status === 403;
|
|
13469
|
+
let parsed = undefined;
|
|
13470
|
+
if (authenticationFailure) {
|
|
13471
|
+
try {
|
|
13472
|
+
await response.body?.cancel();
|
|
13473
|
+
} catch {}
|
|
13474
|
+
} else {
|
|
13475
|
+
const text = await response.text();
|
|
13476
|
+
if (text.length > 0) {
|
|
13477
|
+
try {
|
|
13478
|
+
parsed = JSON.parse(text);
|
|
13479
|
+
} catch {
|
|
13480
|
+
parsed = text;
|
|
13481
|
+
}
|
|
13482
|
+
}
|
|
13483
|
+
}
|
|
13484
|
+
if (!response.ok) {
|
|
13485
|
+
if (response.status >= 300 && response.status < 400) {
|
|
13486
|
+
return {
|
|
13487
|
+
ok: false,
|
|
13488
|
+
retryable: false,
|
|
13489
|
+
error: new HasnaHttpError(method, rel, response.status, parsed)
|
|
13490
|
+
};
|
|
13491
|
+
}
|
|
13492
|
+
if (authenticationFailure) {
|
|
13493
|
+
return {
|
|
13494
|
+
ok: false,
|
|
13495
|
+
retryable: false,
|
|
13496
|
+
error: new HasnaHttpError(method, rel, response.status, undefined, {
|
|
13497
|
+
source: credential.source,
|
|
13498
|
+
tier: credential.tier,
|
|
13499
|
+
guidance: authFailureGuidance(credential)
|
|
13500
|
+
})
|
|
13501
|
+
};
|
|
13502
|
+
}
|
|
13503
|
+
const retry = resolveRetry(opts.retry);
|
|
13504
|
+
const retryable = retry ? retry.retryStatuses.includes(response.status) : false;
|
|
13505
|
+
return { ok: false, retryable, error: new HasnaHttpError(method, rel, response.status, parsed) };
|
|
13506
|
+
}
|
|
13507
|
+
return { ok: true, value: parsed };
|
|
13508
|
+
}
|
|
13509
|
+
async function request(method, path, body, opts = {}) {
|
|
13510
|
+
const upper = method.toUpperCase();
|
|
13511
|
+
const rel = appendQuery(path.startsWith("/") ? path : `/${path}`, opts.query);
|
|
13512
|
+
const retry = resolveRetry(opts.retry);
|
|
13513
|
+
const methodRetryable = IDEMPOTENT_METHODS2.has(upper) || Boolean(opts.idempotencyKey);
|
|
13514
|
+
const maxAttempts = retry && methodRetryable ? retry.retries + 1 : 1;
|
|
13515
|
+
const binding = requestBindingProvider ? await requestBindingProvider() : {
|
|
13516
|
+
baseUrl: base,
|
|
13517
|
+
credential: await resolveRequestCredential(options.name, options.apiKey)
|
|
13518
|
+
};
|
|
13519
|
+
const url = `${binding.baseUrl}${rel}`;
|
|
13520
|
+
const credential = binding.credential;
|
|
13521
|
+
let last = null;
|
|
13522
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
13523
|
+
const result = await once(upper, rel, url, body, opts, credential);
|
|
13524
|
+
if (result.ok)
|
|
13525
|
+
return result.value;
|
|
13526
|
+
last = result;
|
|
13527
|
+
const canRetry = retry !== null && methodRetryable && result.retryable && attempt < maxAttempts;
|
|
13528
|
+
if (!canRetry)
|
|
13529
|
+
break;
|
|
13530
|
+
const backoff = Math.min(retry.maxDelayMs, retry.baseDelayMs * 2 ** (attempt - 1));
|
|
13531
|
+
const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
|
|
13532
|
+
await sleep(backoff + jitter);
|
|
13533
|
+
}
|
|
13534
|
+
throw last.error;
|
|
13535
|
+
}
|
|
13536
|
+
return {
|
|
13537
|
+
baseUrl: base,
|
|
13538
|
+
request,
|
|
13539
|
+
get: (path, opts) => request("GET", path, undefined, opts),
|
|
13540
|
+
post: (path, body, opts) => request("POST", path, body, opts),
|
|
13541
|
+
put: (path, body, opts) => request("PUT", path, body, opts),
|
|
13542
|
+
patch: (path, body, opts) => request("PATCH", path, body, opts),
|
|
13543
|
+
del: (path, body, opts) => request("DELETE", path, body, opts)
|
|
13544
|
+
};
|
|
13545
|
+
}
|
|
13546
|
+
function createClientTransport(name, env = process.env, overrides) {
|
|
13547
|
+
const credentialOptions = overrides?.credentials;
|
|
13548
|
+
const snapshotOptions = { ...credentialOptions ? { credentials: credentialOptions } : {} };
|
|
13549
|
+
const resolution = resolveClientTransportSnapshot2(name, env, snapshotOptions).resolution;
|
|
13550
|
+
const sameBinding = (left, right) => left.resolution.baseUrl === right.resolution.baseUrl && left.credential.apiKey === right.credential.apiKey && left.credential.pointerVaultKey === right.credential.pointerVaultKey && left.credential.source === right.credential.source && left.credential.tier === right.credential.tier;
|
|
13551
|
+
const unstableConfiguration = () => new ClientTransportConfigurationError2(name, "The configured service authority or credential changed while a request was being prepared; no authenticated request was sent.");
|
|
13552
|
+
const requestBindingProvider = async () => {
|
|
13553
|
+
const first = resolveClientTransportSnapshot2(name, env, snapshotOptions);
|
|
13554
|
+
const reviewed = resolveClientTransportSnapshot2(name, env, snapshotOptions);
|
|
13555
|
+
if (!sameBinding(first, reviewed))
|
|
13556
|
+
throw unstableConfiguration();
|
|
13557
|
+
if (reviewed.resolution.baseUrl !== resolution.baseUrl) {
|
|
13558
|
+
throw new ClientTransportConfigurationError2(name, "The configured service authority changed; rebuild the client before sending credentials.");
|
|
13559
|
+
}
|
|
13560
|
+
const credential = await resolveRequestCredential(name, () => reviewed.credential, env);
|
|
13561
|
+
const immediatelyBeforeDispatch = resolveClientTransportSnapshot2(name, env, snapshotOptions);
|
|
13562
|
+
if (!sameBinding(reviewed, immediatelyBeforeDispatch))
|
|
13563
|
+
throw unstableConfiguration();
|
|
13564
|
+
if (immediatelyBeforeDispatch.resolution.baseUrl !== resolution.baseUrl) {
|
|
13565
|
+
throw new ClientTransportConfigurationError2(name, "The configured service authority changed; rebuild the client before sending credentials.");
|
|
13566
|
+
}
|
|
13567
|
+
return { baseUrl: immediatelyBeforeDispatch.resolution.baseUrl, credential };
|
|
13568
|
+
};
|
|
13569
|
+
return {
|
|
13570
|
+
transport: "http",
|
|
13571
|
+
client: createHasnaHttpTransportInternal({
|
|
13572
|
+
name,
|
|
13573
|
+
baseUrl: resolution.baseUrl,
|
|
13574
|
+
apiKey: () => {
|
|
13575
|
+
throw new Error("The authenticated request binding provider was not invoked.");
|
|
13576
|
+
},
|
|
13577
|
+
...overrides?.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
|
|
13578
|
+
...overrides?.headers ? { headers: overrides.headers } : {},
|
|
13579
|
+
...overrides?.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
|
|
13580
|
+
...overrides?.retry !== undefined ? { retry: overrides.retry } : {},
|
|
13581
|
+
...overrides?.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
|
|
13582
|
+
}, requestBindingProvider),
|
|
13583
|
+
resolution
|
|
13584
|
+
};
|
|
13585
|
+
}
|
|
13586
|
+
function resourcePath(resource) {
|
|
13587
|
+
const trimmed = resource.replace(/^\/+|\/+$/g, "");
|
|
13588
|
+
if (!trimmed)
|
|
13589
|
+
throw new Error("resource must be a non-empty path segment");
|
|
13590
|
+
return `/${trimmed}`;
|
|
13591
|
+
}
|
|
13592
|
+
function entityPath(resource, id) {
|
|
13593
|
+
if (id === undefined || id === null || `${id}`.length === 0) {
|
|
13594
|
+
throw new Error("id must be a non-empty string");
|
|
13595
|
+
}
|
|
13596
|
+
return `${resourcePath(resource)}/${encodeURIComponent(String(id))}`;
|
|
13597
|
+
}
|
|
13598
|
+
function newIdempotencyKey() {
|
|
13599
|
+
const g = globalThis;
|
|
13600
|
+
if (g.crypto?.randomUUID)
|
|
13601
|
+
return g.crypto.randomUUID();
|
|
13602
|
+
return `idmp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
|
|
13603
|
+
}
|
|
13604
|
+
function extractItems(raw) {
|
|
13605
|
+
if (Array.isArray(raw))
|
|
13606
|
+
return raw;
|
|
13607
|
+
if (raw && typeof raw === "object") {
|
|
13608
|
+
const obj = raw;
|
|
13609
|
+
for (const key of ["items", "data", "results", "rows", "records"]) {
|
|
13610
|
+
if (Array.isArray(obj[key]))
|
|
13611
|
+
return obj[key];
|
|
13612
|
+
}
|
|
13613
|
+
}
|
|
13614
|
+
return [];
|
|
13615
|
+
}
|
|
13616
|
+
function extractTotal(raw) {
|
|
13617
|
+
if (raw && typeof raw === "object") {
|
|
13618
|
+
const obj = raw;
|
|
13619
|
+
for (const key of ["total", "count", "totalCount", "total_count"]) {
|
|
13620
|
+
if (typeof obj[key] === "number")
|
|
13621
|
+
return obj[key];
|
|
13622
|
+
}
|
|
13623
|
+
}
|
|
13624
|
+
return null;
|
|
13625
|
+
}
|
|
13626
|
+
function extractCursor(raw) {
|
|
13627
|
+
if (raw && typeof raw === "object") {
|
|
13628
|
+
const obj = raw;
|
|
13629
|
+
for (const key of ["cursor", "nextCursor", "next_cursor", "next"]) {
|
|
13630
|
+
if (typeof obj[key] === "string")
|
|
13631
|
+
return obj[key];
|
|
13632
|
+
}
|
|
13633
|
+
}
|
|
13634
|
+
return null;
|
|
13635
|
+
}
|
|
13636
|
+
function isNotFoundHttpError(error) {
|
|
13637
|
+
return typeof error === "object" && error !== null && error.name === "HasnaHttpError" && error.status === 404;
|
|
13638
|
+
}
|
|
13639
|
+
function createHasnaStorageClient(name, transport) {
|
|
13640
|
+
return {
|
|
13641
|
+
name,
|
|
13642
|
+
baseUrl: transport.baseUrl,
|
|
13643
|
+
transport,
|
|
13644
|
+
async list(resource, options = {}) {
|
|
13645
|
+
const raw = await transport.get(resourcePath(resource), options);
|
|
13646
|
+
return {
|
|
13647
|
+
items: extractItems(raw),
|
|
13648
|
+
total: extractTotal(raw),
|
|
13649
|
+
cursor: extractCursor(raw),
|
|
13650
|
+
raw
|
|
13651
|
+
};
|
|
13652
|
+
},
|
|
13653
|
+
async get(resource, id, options = {}) {
|
|
13654
|
+
try {
|
|
13655
|
+
return await transport.get(entityPath(resource, id), options);
|
|
13656
|
+
} catch (error) {
|
|
13657
|
+
if (isNotFoundHttpError(error))
|
|
13658
|
+
return null;
|
|
13659
|
+
throw error;
|
|
13660
|
+
}
|
|
13661
|
+
},
|
|
13662
|
+
async create(resource, body, options = {}) {
|
|
13663
|
+
const { idempotencyKey, ...rest } = options;
|
|
13664
|
+
return transport.post(resourcePath(resource), body, {
|
|
13665
|
+
...rest,
|
|
13666
|
+
idempotencyKey: idempotencyKey ?? newIdempotencyKey()
|
|
13667
|
+
});
|
|
13668
|
+
},
|
|
13669
|
+
async update(resource, id, patch, options = {}) {
|
|
13670
|
+
const { method = "PATCH", idempotencyKey, ...rest } = options;
|
|
13671
|
+
const call = method === "PUT" ? transport.put : transport.patch;
|
|
13672
|
+
return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey ? { idempotencyKey } : {} });
|
|
13673
|
+
},
|
|
13674
|
+
async delete(resource, id, options = {}) {
|
|
13675
|
+
try {
|
|
13676
|
+
await transport.del(entityPath(resource, id), undefined, options);
|
|
13677
|
+
} catch (error) {
|
|
13678
|
+
if (isNotFoundHttpError(error))
|
|
13679
|
+
return;
|
|
13680
|
+
throw error;
|
|
13681
|
+
}
|
|
13682
|
+
}
|
|
13683
|
+
};
|
|
13684
|
+
}
|
|
13685
|
+
function resolveStorageClient(name, env = process.env, overrides) {
|
|
13686
|
+
const wired = createClientTransport(name, env, overrides);
|
|
13687
|
+
return { transport: "http", client: createHasnaStorageClient(name, wired.client) };
|
|
13688
|
+
}
|
|
13689
|
+
|
|
13690
|
+
// src/lib/transport-resolver.ts
|
|
13691
|
+
var INSTRUCTIONS_APP = "instructions";
|
|
13692
|
+
var INSTRUCTIONS_LOCAL_OPT_IN_ENV = INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS[0];
|
|
13693
|
+
function rethrowInstructionsAuthorityFailure(error) {
|
|
13694
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
13695
|
+
const name = error instanceof Error ? error.name : "";
|
|
13696
|
+
const failure = (code, lead) => {
|
|
13697
|
+
throw new Error(`${code}: ${lead} ${message} There is no local fallback: local SQLite is opt-in only ` + `(${INSTRUCTIONS_LOCAL_OPT_IN_ENV}=1) and is disabled by default \u2014 failing closed`, { cause: error });
|
|
13698
|
+
};
|
|
13699
|
+
if (name === "CredentialResolutionError" || name === "CredentialFileUnsafeError") {
|
|
13700
|
+
return failure("REMOTE_API_CREDENTIAL_INVALID", "The configured Instructions credential could not be used.");
|
|
13701
|
+
}
|
|
13702
|
+
if (/no API key could be resolved/.test(message)) {
|
|
13703
|
+
if (/is not set and no API key could be resolved/.test(message)) {
|
|
13704
|
+
return failure("REMOTE_API_CONFIG_MISSING", "no Instructions credential resolved from the Keychain item " + "hasna.credentials.instructions.api-key, ~/.hasna/instructions/config/credentials, " + `or ${clientTransportEnvKeys(INSTRUCTIONS_APP).apiKeyKeys[0]}.`);
|
|
13705
|
+
}
|
|
13706
|
+
return failure("REMOTE_API_KEY_MISSING", "an Instructions authority is configured but no API key resolved \u2014 looked in " + "hasna.credentials.instructions.api-key, ~/.hasna/instructions/config/credentials, " + `and ${clientTransportEnvKeys(INSTRUCTIONS_APP).apiKeyKeys[0]}.`);
|
|
13707
|
+
}
|
|
13708
|
+
return failure("REMOTE_API_URL_INVALID", "the configured Instructions authority is invalid.");
|
|
13709
|
+
}
|
|
13710
|
+
function resolveInstructionsClientTransport(env = process.env, options = {}) {
|
|
13711
|
+
const inputs = instructionsResolverInputs(env, options.credentials);
|
|
13712
|
+
try {
|
|
13713
|
+
return resolveClientTransport(INSTRUCTIONS_APP, inputs.env, {
|
|
13714
|
+
credentials: inputs.credentials
|
|
13715
|
+
});
|
|
13716
|
+
} catch (error) {
|
|
13717
|
+
rethrowInstructionsAuthorityFailure(error);
|
|
13718
|
+
}
|
|
13719
|
+
}
|
|
13720
|
+
function resolveInstructionsStorageClient(env = process.env, options = {}) {
|
|
13721
|
+
const inputs = instructionsResolverInputs(env, options.credentials);
|
|
13722
|
+
try {
|
|
13723
|
+
return resolveStorageClient(INSTRUCTIONS_APP, inputs.env, {
|
|
13724
|
+
fetchImpl: (input, init) => fetch(input, { ...init, redirect: "manual" }),
|
|
13725
|
+
credentials: inputs.credentials
|
|
13726
|
+
});
|
|
13727
|
+
} catch (error) {
|
|
13728
|
+
rethrowInstructionsAuthorityFailure(error);
|
|
13729
|
+
}
|
|
13730
|
+
}
|
|
13731
|
+
function getInstructionsTransportStatus(env = process.env, options = {}) {
|
|
13732
|
+
if (selectsInstructionsLocalStore(env)) {
|
|
13733
|
+
return {
|
|
13734
|
+
selected: false,
|
|
13735
|
+
ok: true,
|
|
13736
|
+
transport: "local",
|
|
13737
|
+
api_url_configured: false,
|
|
13738
|
+
api_key_configured: false,
|
|
13739
|
+
api_url_source: null,
|
|
13740
|
+
api_key_source: null,
|
|
13741
|
+
api_key_tier: null,
|
|
13742
|
+
v1_base_url: null,
|
|
13743
|
+
issues: [],
|
|
13744
|
+
local_fallback: false
|
|
13745
|
+
};
|
|
13746
|
+
}
|
|
13747
|
+
try {
|
|
13748
|
+
const resolution = resolveInstructionsClientTransport(env, options);
|
|
13749
|
+
return {
|
|
13750
|
+
selected: true,
|
|
13751
|
+
ok: true,
|
|
13752
|
+
transport: "http",
|
|
13753
|
+
api_url_configured: resolution.apiUrlSource !== null && resolution.apiUrlSource !== "default",
|
|
13754
|
+
api_key_configured: resolution.apiKeyPresent,
|
|
13755
|
+
api_url_source: resolution.apiUrlSource,
|
|
13756
|
+
api_key_source: resolution.apiKeySource,
|
|
13757
|
+
api_key_tier: resolution.apiKeyTier,
|
|
13758
|
+
v1_base_url: resolution.baseUrl,
|
|
13759
|
+
issues: [],
|
|
13760
|
+
local_fallback: false
|
|
13761
|
+
};
|
|
13762
|
+
} catch (error) {
|
|
13763
|
+
const issue = error instanceof Error ? error.message : String(error);
|
|
13764
|
+
const keys = clientTransportEnvKeys(INSTRUCTIONS_APP);
|
|
13765
|
+
const declared = (names) => names.some((key) => (env[key] ?? "").trim() !== "");
|
|
13766
|
+
return {
|
|
13767
|
+
selected: true,
|
|
13768
|
+
ok: false,
|
|
13769
|
+
transport: "invalid",
|
|
13770
|
+
api_url_configured: declared(keys.apiUrlKeys),
|
|
13771
|
+
api_key_configured: declared(keys.apiKeyKeys),
|
|
13772
|
+
api_url_source: null,
|
|
13773
|
+
api_key_source: null,
|
|
13774
|
+
api_key_tier: null,
|
|
13775
|
+
v1_base_url: null,
|
|
13776
|
+
issues: [issue],
|
|
13777
|
+
local_fallback: false
|
|
13778
|
+
};
|
|
13779
|
+
}
|
|
13780
|
+
}
|
|
13781
|
+
var localNoticePrinted = false;
|
|
13782
|
+
function announceLocalInstructionsMode(write = (line) => process.stderr.write(`${line}
|
|
13783
|
+
`)) {
|
|
13784
|
+
if (localNoticePrinted)
|
|
13785
|
+
return false;
|
|
13786
|
+
localNoticePrinted = true;
|
|
13787
|
+
write(instructionsLocalModeNotice());
|
|
13788
|
+
return true;
|
|
13789
|
+
}
|
|
13790
|
+
|
|
13791
|
+
// src/data/config-store.ts
|
|
13792
|
+
function parseBoundedPagePayload(value, label) {
|
|
13793
|
+
const page = value;
|
|
13794
|
+
const consumed = Number(page?.cursor) + (page?.items?.length ?? 0);
|
|
13795
|
+
const complete = Boolean(page && Number.isSafeInteger(page.total) && consumed >= Number(page.total));
|
|
13796
|
+
if (!page || !Array.isArray(page.items) || !Number.isSafeInteger(page.total) || Number(page.total) < 0 || !Number.isSafeInteger(page.limit) || Number(page.limit) < 1 || !Number.isSafeInteger(page.cursor) || Number(page.cursor) < 0 || page.items.length > Number(page.limit) || typeof page.has_more !== "boolean" || typeof page.complete !== "boolean" || page.truncated !== false || page.next_cursor !== null && !Number.isSafeInteger(page.next_cursor) || page.complete !== complete || page.has_more !== !complete || page.next_cursor !== (complete ? null : consumed)) {
|
|
13797
|
+
throw new Error(`${label} returned an invalid or truncated bounded-read envelope`);
|
|
13798
|
+
}
|
|
13799
|
+
return {
|
|
13800
|
+
...page,
|
|
13801
|
+
source_bounded: page.source_bounded ?? true
|
|
13802
|
+
};
|
|
13803
|
+
}
|
|
13804
|
+
function parseBoundedOrLegacyPage(value, legacyItems, options, label) {
|
|
13805
|
+
if (value && typeof value === "object") {
|
|
13806
|
+
const candidate = value;
|
|
13807
|
+
if ("items" in candidate || "total" in candidate || "complete" in candidate || "truncated" in candidate || "next_cursor" in candidate) {
|
|
13808
|
+
return parseBoundedPagePayload(value, label);
|
|
13809
|
+
}
|
|
13810
|
+
}
|
|
13811
|
+
if (!Array.isArray(legacyItems)) {
|
|
13812
|
+
throw new Error(`${label} returned neither a bounded envelope nor a complete legacy array`);
|
|
13813
|
+
}
|
|
13814
|
+
const normalized = normalizeBoundedReadOptions(options);
|
|
13815
|
+
const page = boundedReadPage(legacyItems.slice(normalized.cursor, normalized.cursor + normalized.limit), legacyItems.length, normalized);
|
|
13816
|
+
return { ...page, source_bounded: false };
|
|
13817
|
+
}
|
|
13818
|
+
var LOCAL_OPT_IN_ENV = INSTRUCTIONS_LOCAL_OPT_IN_ENV;
|
|
13819
|
+
function isLocalOptIn(env = process.env) {
|
|
13820
|
+
return isInstructionsLocalOptIn(env);
|
|
13821
|
+
}
|
|
13822
|
+
function isApiTransport(env = process.env) {
|
|
13823
|
+
const { transport } = resolveInstructionsStorageClient(env);
|
|
13824
|
+
return transport === "http";
|
|
13825
|
+
}
|
|
13826
|
+
function isCloudAuthError(err) {
|
|
13827
|
+
return typeof err === "object" && err !== null && err.name === "HasnaHttpError" && (err.status === 401 || err.status === 403);
|
|
13828
|
+
}
|
|
13829
|
+
function isNotFoundHttpError2(err) {
|
|
13830
|
+
return typeof err === "object" && err !== null && err.name === "HasnaHttpError" && err.status === 404;
|
|
13831
|
+
}
|
|
13832
|
+
function formatCliError(err, env = process.env) {
|
|
13833
|
+
if (isCloudAuthError(err)) {
|
|
13834
|
+
return [
|
|
13835
|
+
`Instructions cloud API rejected the request (HTTP ${err.status}: authentication failed).`,
|
|
13836
|
+
` The API key in use is missing, expired, or revoked (the transport never echoes the server's 401/403 body).`,
|
|
13837
|
+
` To continue, either:`,
|
|
13838
|
+
` - set a valid key: export HASNA_INSTRUCTIONS_API_KEY=<new-key>`,
|
|
13839
|
+
` (or add the Keychain item hasna.credentials.instructions.api-key, or write`,
|
|
13840
|
+
` ~/.hasna/instructions/config/credentials)`,
|
|
13841
|
+
` - or opt in to the local store explicitly: export ${LOCAL_OPT_IN_ENV}=1`
|
|
13842
|
+
].join(`
|
|
13843
|
+
`);
|
|
13844
|
+
}
|
|
13845
|
+
return err instanceof Error ? err.message : String(err);
|
|
13846
|
+
}
|
|
13847
|
+
|
|
13848
|
+
class LocalConfigStore {
|
|
13849
|
+
db;
|
|
11737
13850
|
mode = "local";
|
|
13851
|
+
v1BaseUrl = null;
|
|
11738
13852
|
constructor(db) {
|
|
11739
13853
|
this.db = db;
|
|
11740
13854
|
}
|
|
@@ -11863,50 +13977,22 @@ class LocalConfigStore {
|
|
|
11863
13977
|
|
|
11864
13978
|
class CloudConfigStore {
|
|
11865
13979
|
mode = "api";
|
|
11866
|
-
|
|
11867
|
-
|
|
11868
|
-
|
|
11869
|
-
|
|
11870
|
-
this.
|
|
11871
|
-
this.apiKey = config.apiKey;
|
|
11872
|
-
this.timeoutMs = config.timeoutMs ?? 30000;
|
|
13980
|
+
v1BaseUrl;
|
|
13981
|
+
client;
|
|
13982
|
+
constructor(client) {
|
|
13983
|
+
this.client = client;
|
|
13984
|
+
this.v1BaseUrl = client.baseUrl;
|
|
11873
13985
|
}
|
|
11874
13986
|
async request(method, path, body, opts = {}) {
|
|
11875
|
-
const controller = new AbortController;
|
|
11876
|
-
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
11877
|
-
const headers = {
|
|
11878
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
11879
|
-
Accept: "application/json"
|
|
11880
|
-
};
|
|
11881
|
-
if (body !== undefined)
|
|
11882
|
-
headers["Content-Type"] = "application/json";
|
|
11883
|
-
if (opts.idempotent)
|
|
11884
|
-
headers["Idempotency-Key"] = randomUUID3();
|
|
11885
13987
|
try {
|
|
11886
|
-
const
|
|
11887
|
-
|
|
11888
|
-
headers,
|
|
11889
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
11890
|
-
signal: controller.signal
|
|
13988
|
+
const data = await this.client.transport.request(method, path, body, {
|
|
13989
|
+
...opts.idempotent ? { idempotencyKey: randomUUID3() } : {}
|
|
11891
13990
|
});
|
|
11892
|
-
|
|
13991
|
+
return { status: 200, data };
|
|
13992
|
+
} catch (err) {
|
|
13993
|
+
if (opts.allow404 && isNotFoundHttpError2(err))
|
|
11893
13994
|
return { status: 404, data: null };
|
|
11894
|
-
|
|
11895
|
-
let parsed = null;
|
|
11896
|
-
if (text) {
|
|
11897
|
-
try {
|
|
11898
|
-
parsed = JSON.parse(text);
|
|
11899
|
-
} catch {
|
|
11900
|
-
parsed = text;
|
|
11901
|
-
}
|
|
11902
|
-
}
|
|
11903
|
-
if (!res.ok) {
|
|
11904
|
-
const message = (parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : undefined) ?? `HTTP ${res.status} on ${method} ${path}`;
|
|
11905
|
-
throw new CloudHttpError(res.status, message, parsed);
|
|
11906
|
-
}
|
|
11907
|
-
return { status: res.status, data: parsed };
|
|
11908
|
-
} finally {
|
|
11909
|
-
clearTimeout(timer);
|
|
13995
|
+
throw err;
|
|
11910
13996
|
}
|
|
11911
13997
|
}
|
|
11912
13998
|
async listConfigs(filter = {}) {
|
|
@@ -12062,7 +14148,7 @@ class CloudConfigStore {
|
|
|
12062
14148
|
if (first.status !== 404) {
|
|
12063
14149
|
if (isUsable(first.data))
|
|
12064
14150
|
return first;
|
|
12065
|
-
throw new
|
|
14151
|
+
throw new Error(`Cloud /v1 returned an invalid profile response for ${pathForId(idOrSlug)}`);
|
|
12066
14152
|
}
|
|
12067
14153
|
const profiles = await this.listProfiles();
|
|
12068
14154
|
const profile = profiles.find((candidate) => candidate.id === idOrSlug || candidate.slug === idOrSlug);
|
|
@@ -12075,7 +14161,7 @@ class CloudConfigStore {
|
|
|
12075
14161
|
if (response.status !== 404) {
|
|
12076
14162
|
if (isUsable(response.data))
|
|
12077
14163
|
return response;
|
|
12078
|
-
throw new
|
|
14164
|
+
throw new Error(`Cloud /v1 returned an invalid profile response for ${pathForId(candidate)}`);
|
|
12079
14165
|
}
|
|
12080
14166
|
}
|
|
12081
14167
|
return { status: 404, data: null };
|
|
@@ -12158,7 +14244,7 @@ class CloudConfigStore {
|
|
|
12158
14244
|
}
|
|
12159
14245
|
if (data && "complete" in data) {
|
|
12160
14246
|
if (data.complete !== true || data.truncated !== false) {
|
|
12161
|
-
throw new
|
|
14247
|
+
throw new Error("Cloud /v1 profile resolve returned an incomplete or truncated read");
|
|
12162
14248
|
}
|
|
12163
14249
|
return { ...data, source_bounded: data.source_bounded ?? true };
|
|
12164
14250
|
}
|
|
@@ -12174,7 +14260,7 @@ class CloudConfigStore {
|
|
|
12174
14260
|
};
|
|
12175
14261
|
}
|
|
12176
14262
|
{
|
|
12177
|
-
throw new
|
|
14263
|
+
throw new Error("Cloud /v1 profile resolve returned an incomplete or truncated read");
|
|
12178
14264
|
}
|
|
12179
14265
|
}
|
|
12180
14266
|
async registerMachine(hostname2, os, arch2) {
|
|
@@ -12197,24 +14283,29 @@ class CloudConfigStore {
|
|
|
12197
14283
|
});
|
|
12198
14284
|
}
|
|
12199
14285
|
async reset() {
|
|
12200
|
-
throw new Error("`init --force` cannot wipe the shared cloud store from a client. " + "
|
|
14286
|
+
throw new Error("`init --force` cannot wipe the shared cloud store from a client. " + "Point this run at the local store (HASNA_INSTRUCTIONS_LOCAL=1 with no hosted credential) to reset it instead.");
|
|
12201
14287
|
}
|
|
12202
14288
|
}
|
|
12203
|
-
function resolveConfigStore(env = process.env) {
|
|
12204
|
-
|
|
12205
|
-
|
|
14289
|
+
function resolveConfigStore(env = process.env, options = {}) {
|
|
14290
|
+
if (selectsInstructionsLocalStore(env)) {
|
|
14291
|
+
if (env === process.env)
|
|
14292
|
+
announceLocalInstructionsMode();
|
|
14293
|
+
return new LocalConfigStore;
|
|
14294
|
+
}
|
|
14295
|
+
const { client } = resolveInstructionsStorageClient(env, options);
|
|
14296
|
+
return new CloudConfigStore(client);
|
|
12206
14297
|
}
|
|
12207
14298
|
// src/status.ts
|
|
12208
|
-
import { existsSync as existsSync12, readFileSync as
|
|
14299
|
+
import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
|
|
12209
14300
|
|
|
12210
14301
|
// src/lib/apply.ts
|
|
12211
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as
|
|
12212
|
-
import { basename as basename5, dirname as dirname6, join as
|
|
12213
|
-
import { homedir as
|
|
14302
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync8, realpathSync as realpathSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
14303
|
+
import { basename as basename5, dirname as dirname6, join as join12, resolve as resolve9 } from "path";
|
|
14304
|
+
import { homedir as homedir7 } from "os";
|
|
12214
14305
|
|
|
12215
14306
|
// src/lib/session-render-ownership.ts
|
|
12216
|
-
import { existsSync as existsSync8, readFileSync as
|
|
12217
|
-
import { dirname as dirname5, join as
|
|
14307
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, statSync as statSync4 } from "fs";
|
|
14308
|
+
import { dirname as dirname5, join as join11, parse as parse3, relative as relative3, sep } from "path";
|
|
12218
14309
|
var MANIFEST_ANCESTOR_LIMIT = 24;
|
|
12219
14310
|
var MANAGED_PATH_SEGMENTS = SESSION_RENDER_EXCLUSIVE_MANAGED_PATHS.map((managedPath) => managedPath.split("/").filter(Boolean));
|
|
12220
14311
|
var manifestCache = new Map;
|
|
@@ -12248,7 +14339,7 @@ function readManifestRelativePaths(manifestPath) {
|
|
|
12248
14339
|
}
|
|
12249
14340
|
let manifest;
|
|
12250
14341
|
try {
|
|
12251
|
-
manifest = JSON.parse(
|
|
14342
|
+
manifest = JSON.parse(readFileSync7(manifestPath, "utf-8"));
|
|
12252
14343
|
} catch {
|
|
12253
14344
|
return null;
|
|
12254
14345
|
}
|
|
@@ -12265,7 +14356,7 @@ function sessionRenderManifestClaimsPath(absolutePath2) {
|
|
|
12265
14356
|
const root = parse3(absolutePath2).root;
|
|
12266
14357
|
let home = dirname5(absolutePath2);
|
|
12267
14358
|
for (let depth = 0;depth < MANIFEST_ANCESTOR_LIMIT; depth += 1) {
|
|
12268
|
-
const manifestPath =
|
|
14359
|
+
const manifestPath = join11(home, ...SESSION_RENDER_MANIFEST_RELATIVE_PATH.split("/"));
|
|
12269
14360
|
const relativePaths = readManifestRelativePaths(manifestPath);
|
|
12270
14361
|
if (relativePaths) {
|
|
12271
14362
|
const claimed = relative3(home, absolutePath2).split(sep).join("/");
|
|
@@ -12285,7 +14376,7 @@ function sessionRenderOwnsPath(absolutePath2) {
|
|
|
12285
14376
|
|
|
12286
14377
|
// src/lib/apply.ts
|
|
12287
14378
|
function getConfigHome() {
|
|
12288
|
-
return process.env["CONFIGS_HOME"] || process.env["HOME"] ||
|
|
14379
|
+
return process.env["CONFIGS_HOME"] || process.env["HOME"] || homedir7();
|
|
12289
14380
|
}
|
|
12290
14381
|
function expandPath(p) {
|
|
12291
14382
|
if (p.startsWith("~/")) {
|
|
@@ -12332,7 +14423,7 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
|
|
|
12332
14423
|
}
|
|
12333
14424
|
const path = expandPath(renderedTargetPath);
|
|
12334
14425
|
const renderedForTarget = isCursorGlobalAuthorityPath(path) ? stampCursorGlobalAuthorityMarker(renderedContent) : renderedContent;
|
|
12335
|
-
const previousContent = existsSync9(path) ?
|
|
14426
|
+
const previousContent = existsSync9(path) ? readFileSync8(path, "utf-8") : null;
|
|
12336
14427
|
const changed = previousContent !== renderedForTarget;
|
|
12337
14428
|
if (!opts.dryRun) {
|
|
12338
14429
|
const dir = dirname6(path);
|
|
@@ -12372,7 +14463,7 @@ function wouldDestroyACredential(targetPath, renderedContent, format) {
|
|
|
12372
14463
|
const path = expandPath(targetPath);
|
|
12373
14464
|
if (!existsSync9(path))
|
|
12374
14465
|
return [];
|
|
12375
|
-
current =
|
|
14466
|
+
current = readFileSync8(path, "utf-8");
|
|
12376
14467
|
} catch {
|
|
12377
14468
|
return secretTokens;
|
|
12378
14469
|
}
|
|
@@ -12696,14 +14787,14 @@ function sessionRendererOwnsCanonicalTarget(normalized, opts) {
|
|
|
12696
14787
|
getConfigHome(),
|
|
12697
14788
|
opts.vars?.["HOME_DIR"]
|
|
12698
14789
|
].filter((home) => typeof home === "string" && home.length > 0));
|
|
12699
|
-
if ([...homes].some((home) => SESSION_RENDER_OWNED_CONFIG_TARGETS.some((relativePath) => normalized === normalizeTargetPath(
|
|
14790
|
+
if ([...homes].some((home) => SESSION_RENDER_OWNED_CONFIG_TARGETS.some((relativePath) => normalized === normalizeTargetPath(join12(home, ...relativePath.split("/"))))))
|
|
12700
14791
|
return true;
|
|
12701
14792
|
return sessionRenderOwnsPath(normalized);
|
|
12702
14793
|
}
|
|
12703
14794
|
|
|
12704
14795
|
// src/lib/package-version.ts
|
|
12705
|
-
import { existsSync as existsSync10, readFileSync as
|
|
12706
|
-
import { dirname as dirname7, join as
|
|
14796
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
|
|
14797
|
+
import { dirname as dirname7, join as join13 } from "path";
|
|
12707
14798
|
import { fileURLToPath } from "url";
|
|
12708
14799
|
var cached = null;
|
|
12709
14800
|
function getPackageVersion() {
|
|
@@ -12712,9 +14803,9 @@ function getPackageVersion() {
|
|
|
12712
14803
|
try {
|
|
12713
14804
|
let dir = dirname7(fileURLToPath(import.meta.url));
|
|
12714
14805
|
for (let i = 0;i < 8; i++) {
|
|
12715
|
-
const pkgPath =
|
|
14806
|
+
const pkgPath = join13(dir, "package.json");
|
|
12716
14807
|
if (existsSync10(pkgPath)) {
|
|
12717
|
-
const pkg = JSON.parse(
|
|
14808
|
+
const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
|
|
12718
14809
|
if (pkg.name === "@hasna/instructions" && pkg.version) {
|
|
12719
14810
|
cached = pkg.version;
|
|
12720
14811
|
return cached;
|
|
@@ -12732,18 +14823,18 @@ function getPackageVersion() {
|
|
|
12732
14823
|
|
|
12733
14824
|
// src/lib/managed-skill-runtimes.ts
|
|
12734
14825
|
import { createHash as createHash8 } from "crypto";
|
|
12735
|
-
import { spawnSync } from "child_process";
|
|
14826
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
12736
14827
|
import {
|
|
12737
14828
|
existsSync as existsSync11,
|
|
12738
14829
|
lstatSync as lstatSync4,
|
|
12739
14830
|
mkdirSync as mkdirSync4,
|
|
12740
|
-
readFileSync as
|
|
14831
|
+
readFileSync as readFileSync10,
|
|
12741
14832
|
renameSync as renameSync2,
|
|
12742
14833
|
rmSync as rmSync3,
|
|
12743
14834
|
writeFileSync as writeFileSync3
|
|
12744
14835
|
} from "fs";
|
|
12745
|
-
import { homedir as
|
|
12746
|
-
import { dirname as dirname8, join as
|
|
14836
|
+
import { homedir as homedir8 } from "os";
|
|
14837
|
+
import { dirname as dirname8, join as join14, parse as parse4, relative as relative4, resolve as resolve10 } from "path";
|
|
12747
14838
|
var INBOX_CONVERSATIONS_MINIMUM_VERSION = "0.5.28";
|
|
12748
14839
|
var INBOX_SKILL_MARKERS = [
|
|
12749
14840
|
[".claude", "skills", "inbox", "SKILL.md"],
|
|
@@ -12769,7 +14860,7 @@ function findSymlinkedAncestor(path) {
|
|
|
12769
14860
|
let current = parsed.root;
|
|
12770
14861
|
const rel = relative4(parsed.root, normalized);
|
|
12771
14862
|
for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
|
|
12772
|
-
current =
|
|
14863
|
+
current = join14(current, segment);
|
|
12773
14864
|
if (!existsSync11(current))
|
|
12774
14865
|
return null;
|
|
12775
14866
|
if (lstatSync4(current).isSymbolicLink())
|
|
@@ -12787,9 +14878,9 @@ function packagedInboxSkillPath(explicitPath) {
|
|
|
12787
14878
|
if (explicitPath)
|
|
12788
14879
|
return explicitPath;
|
|
12789
14880
|
const candidates = [
|
|
12790
|
-
|
|
12791
|
-
|
|
12792
|
-
|
|
14881
|
+
join14(import.meta.dir, "..", "..", "assets", "skills", "inbox", "SKILL.md"),
|
|
14882
|
+
join14(import.meta.dir, "..", "assets", "skills", "inbox", "SKILL.md"),
|
|
14883
|
+
join14(process.cwd(), "assets", "skills", "inbox", "SKILL.md")
|
|
12793
14884
|
];
|
|
12794
14885
|
const found = candidates.find((candidate) => existsSync11(candidate));
|
|
12795
14886
|
if (!found) {
|
|
@@ -12803,7 +14894,7 @@ function readCanonicalSkill(explicitPath) {
|
|
|
12803
14894
|
if (!stat?.isFile()) {
|
|
12804
14895
|
throw new Error("packaged inbox skill contract is not a regular file");
|
|
12805
14896
|
}
|
|
12806
|
-
const content =
|
|
14897
|
+
const content = readFileSync10(assetPath, "utf8");
|
|
12807
14898
|
if (!content.includes("conversations watch --from <agent> --all")) {
|
|
12808
14899
|
throw new Error("packaged inbox skill contract does not declare the canonical conversations watcher");
|
|
12809
14900
|
}
|
|
@@ -12813,7 +14904,7 @@ function readCanonicalSkill(explicitPath) {
|
|
|
12813
14904
|
return { content, sha256: sha2568(content) };
|
|
12814
14905
|
}
|
|
12815
14906
|
function runProbe(command, args) {
|
|
12816
|
-
const result =
|
|
14907
|
+
const result = spawnSync3(command, args, {
|
|
12817
14908
|
encoding: "utf8",
|
|
12818
14909
|
timeout: 5000,
|
|
12819
14910
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -12840,8 +14931,8 @@ function compareVersions(left, right) {
|
|
|
12840
14931
|
}
|
|
12841
14932
|
return 0;
|
|
12842
14933
|
}
|
|
12843
|
-
function inspectSkillMarkers(
|
|
12844
|
-
return INBOX_SKILL_MARKERS.map((parts) =>
|
|
14934
|
+
function inspectSkillMarkers(homeDir6) {
|
|
14935
|
+
return INBOX_SKILL_MARKERS.map((parts) => join14(homeDir6, ...parts)).map((path) => {
|
|
12845
14936
|
const stat = lstatOrNull(path);
|
|
12846
14937
|
if (!stat)
|
|
12847
14938
|
return null;
|
|
@@ -12850,16 +14941,16 @@ function inspectSkillMarkers(homeDir4) {
|
|
|
12850
14941
|
}
|
|
12851
14942
|
return {
|
|
12852
14943
|
path,
|
|
12853
|
-
content:
|
|
14944
|
+
content: readFileSync10(path, "utf8"),
|
|
12854
14945
|
mode: stat.mode & 511,
|
|
12855
14946
|
regular: true
|
|
12856
14947
|
};
|
|
12857
14948
|
}).filter((snapshot) => snapshot !== null);
|
|
12858
14949
|
}
|
|
12859
14950
|
function inspectInbox(options) {
|
|
12860
|
-
const
|
|
14951
|
+
const homeDir6 = options.homeDir ?? homedir8();
|
|
12861
14952
|
const runtimeCommand = options.conversationsCommand ?? "conversations";
|
|
12862
|
-
const snapshots = inspectSkillMarkers(
|
|
14953
|
+
const snapshots = inspectSkillMarkers(homeDir6);
|
|
12863
14954
|
const skillPresent = snapshots.length > 0;
|
|
12864
14955
|
let canonicalContent = null;
|
|
12865
14956
|
let canonicalSha256 = null;
|
|
@@ -12984,7 +15075,7 @@ function writeAtomic(path, content, mode) {
|
|
|
12984
15075
|
}
|
|
12985
15076
|
var DEFAULT_SKILL_WRITE_FILE_OPERATIONS = {
|
|
12986
15077
|
lstat: lstatOrNull,
|
|
12987
|
-
read: (path) =>
|
|
15078
|
+
read: (path) => readFileSync10(path, "utf8"),
|
|
12988
15079
|
write: writeAtomic
|
|
12989
15080
|
};
|
|
12990
15081
|
function writeSkillContractsTransactional(snapshots, canonicalContent, fileOperations = DEFAULT_SKILL_WRITE_FILE_OPERATIONS) {
|
|
@@ -13177,7 +15268,7 @@ async function getConfigsStatus(store = resolveConfigStore(), options = {}) {
|
|
|
13177
15268
|
missingTargets += 1;
|
|
13178
15269
|
continue;
|
|
13179
15270
|
}
|
|
13180
|
-
const disk =
|
|
15271
|
+
const disk = readFileSync11(targetPath, "utf-8");
|
|
13181
15272
|
const { content: redactedDisk } = redactContent(disk, redactFormatForTarget(config.target_path, config.format));
|
|
13182
15273
|
if (redactedDisk !== config.content) {
|
|
13183
15274
|
driftedTargets += 1;
|
|
@@ -13269,8 +15360,8 @@ async function getConfigsStatus(store = resolveConfigStore(), options = {}) {
|
|
|
13269
15360
|
}
|
|
13270
15361
|
// src/lib/provider-context.ts
|
|
13271
15362
|
import { createHash as createHash9 } from "crypto";
|
|
13272
|
-
import { existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as
|
|
13273
|
-
import { join as
|
|
15363
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync4 } from "fs";
|
|
15364
|
+
import { join as join15 } from "path";
|
|
13274
15365
|
var PROVIDER_CONTEXT_DIR = ".hasna/provider-context";
|
|
13275
15366
|
var PROVIDER_CONTEXT_MANIFEST = "manifest.json";
|
|
13276
15367
|
var PROVIDER_CONTEXT_SCHEMA = "hasna.instructions.provider-context/v1";
|
|
@@ -13415,18 +15506,18 @@ function resolveAndRenderProviderContext(opts) {
|
|
|
13415
15506
|
const recordedEndpoint = originAccepted ? `${opts.origin.host}${opts.origin.pathPrefix || ""}` : null;
|
|
13416
15507
|
const reason = entry === null && opts.rawEndpoint ? originAccepted ? `endpoint "${recordedEndpoint}" is not in the provider-context registry; using the invariant fragment` : "endpoint rejected (embedded credentials or unparseable); using the invariant fragment" : null;
|
|
13417
15508
|
const content = renderProviderFragment(entry);
|
|
13418
|
-
const dir =
|
|
15509
|
+
const dir = join15(opts.homeDir, PROVIDER_CONTEXT_DIR);
|
|
13419
15510
|
if (!existsSync13(dir))
|
|
13420
15511
|
mkdirSync5(dir, { recursive: true });
|
|
13421
15512
|
const filename = `${entry ? entry.key : "invariant"}.md`;
|
|
13422
|
-
const fragmentPath2 =
|
|
15513
|
+
const fragmentPath2 = join15(dir, filename);
|
|
13423
15514
|
const fragmentSha256 = sha2569(content);
|
|
13424
15515
|
writeFileSync4(fragmentPath2, content, "utf8");
|
|
13425
|
-
const manifestPath =
|
|
15516
|
+
const manifestPath = join15(dir, PROVIDER_CONTEXT_MANIFEST);
|
|
13426
15517
|
let manifest = { schema: PROVIDER_CONTEXT_SCHEMA, fragments: {} };
|
|
13427
15518
|
try {
|
|
13428
15519
|
if (existsSync13(manifestPath)) {
|
|
13429
|
-
const parsed = JSON.parse(
|
|
15520
|
+
const parsed = JSON.parse(readFileSync12(manifestPath, "utf8"));
|
|
13430
15521
|
if (parsed && typeof parsed === "object")
|
|
13431
15522
|
manifest = parsed;
|
|
13432
15523
|
}
|
|
@@ -13536,10 +15627,10 @@ var PG_MIGRATIONS = [
|
|
|
13536
15627
|
`CREATE INDEX IF NOT EXISTS profile_assets_source_config_idx ON profile_assets (source_config_id)`
|
|
13537
15628
|
];
|
|
13538
15629
|
// src/lib/station-profile.ts
|
|
13539
|
-
import { spawnSync as
|
|
13540
|
-
import { existsSync as existsSync14, lstatSync as lstatSync5, mkdirSync as mkdirSync6, readFileSync as
|
|
13541
|
-
import { arch as osArch, homedir as
|
|
13542
|
-
import { dirname as dirname9, join as
|
|
15630
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
15631
|
+
import { existsSync as existsSync14, lstatSync as lstatSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync13, readdirSync as readdirSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
15632
|
+
import { arch as osArch, homedir as homedir9, hostname as osHostname3, platform as osPlatform, userInfo as osUserInfo } from "os";
|
|
15633
|
+
import { dirname as dirname9, join as join16 } from "path";
|
|
13543
15634
|
var STATION_PROFILE_CACHE_FILENAME = "station-profile.md";
|
|
13544
15635
|
var STATION_PROFILE_SOURCE_ID = "station-profile";
|
|
13545
15636
|
var STATION_PROFILE_LAYER = "machine";
|
|
@@ -13549,23 +15640,23 @@ var STATION_PROFILE_FULL_NAMES_MAX = 6;
|
|
|
13549
15640
|
var STATION_PROFILE_PRIMARY_SCOPE = "@hasna";
|
|
13550
15641
|
var MACHINES_MANIFEST_PATH_ENV = "HASNA_MACHINES_MANIFEST_PATH";
|
|
13551
15642
|
var BUN_INSTALL_ENV = "BUN_INSTALL";
|
|
13552
|
-
function
|
|
13553
|
-
return env["HOME"] || env["USERPROFILE"] ||
|
|
15643
|
+
function homeDir6(env = process.env) {
|
|
15644
|
+
return env["HOME"] || env["USERPROFILE"] || homedir9();
|
|
13554
15645
|
}
|
|
13555
15646
|
function getStationProfileCachePath(env = process.env) {
|
|
13556
|
-
return
|
|
15647
|
+
return join16(getRawStoreRoot(env), STATION_PROFILE_CACHE_FILENAME);
|
|
13557
15648
|
}
|
|
13558
15649
|
function getMachinesManifestPath(env = process.env) {
|
|
13559
|
-
return env[MACHINES_MANIFEST_PATH_ENV] ||
|
|
15650
|
+
return env[MACHINES_MANIFEST_PATH_ENV] || join16(homeDir6(env), ".hasna", "machines", "machines.json");
|
|
13560
15651
|
}
|
|
13561
15652
|
function getBunGlobalModulesDir(env = process.env) {
|
|
13562
|
-
return
|
|
15653
|
+
return join16(env[BUN_INSTALL_ENV] || join16(homeDir6(env), ".bun"), "install", "global", "node_modules");
|
|
13563
15654
|
}
|
|
13564
15655
|
function readMachinesManifest(path) {
|
|
13565
15656
|
try {
|
|
13566
15657
|
if (!existsSync14(path))
|
|
13567
15658
|
return null;
|
|
13568
|
-
const parsed = JSON.parse(
|
|
15659
|
+
const parsed = JSON.parse(readFileSync13(path, "utf8"));
|
|
13569
15660
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
13570
15661
|
return null;
|
|
13571
15662
|
const machines = parsed["machines"];
|
|
@@ -13595,7 +15686,7 @@ function metadataUser(record) {
|
|
|
13595
15686
|
}
|
|
13596
15687
|
function probeMachineStatus(machineId) {
|
|
13597
15688
|
try {
|
|
13598
|
-
const result =
|
|
15689
|
+
const result = spawnSync4("machines", ["details", "--json", "--machine", machineId], {
|
|
13599
15690
|
encoding: "utf8",
|
|
13600
15691
|
timeout: 3000,
|
|
13601
15692
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -13617,11 +15708,11 @@ function probeMachineStatus(machineId) {
|
|
|
13617
15708
|
}
|
|
13618
15709
|
}
|
|
13619
15710
|
function resolveStationProfileMachine(env = process.env, options = {}) {
|
|
13620
|
-
const hostname2 =
|
|
15711
|
+
const hostname2 = osHostname3();
|
|
13621
15712
|
const record = findLocalManifestMachine(readMachinesManifest(getMachinesManifestPath(env)), hostname2);
|
|
13622
|
-
const home =
|
|
15713
|
+
const home = homeDir6(env);
|
|
13623
15714
|
const platform = stringField(record, "platform") ?? osPlatform();
|
|
13624
|
-
const workspacePath = stringField(record, "workspacePath") ??
|
|
15715
|
+
const workspacePath = stringField(record, "workspacePath") ?? join16(home, platform === "darwin" ? "Workspace" : "workspace");
|
|
13625
15716
|
const machine = {
|
|
13626
15717
|
id: stringField(record, "id") ?? hostname2,
|
|
13627
15718
|
hostname: stringField(record, "hostname") ?? hostname2,
|
|
@@ -13638,7 +15729,7 @@ function resolveStationProfileMachine(env = process.env, options = {}) {
|
|
|
13638
15729
|
return machine;
|
|
13639
15730
|
}
|
|
13640
15731
|
function scopedPackageNames(modulesDir, scope) {
|
|
13641
|
-
const scopeDir =
|
|
15732
|
+
const scopeDir = join16(modulesDir, scope);
|
|
13642
15733
|
try {
|
|
13643
15734
|
if (!existsSync14(scopeDir))
|
|
13644
15735
|
return null;
|
|
@@ -13650,7 +15741,7 @@ function scopedPackageNames(modulesDir, scope) {
|
|
|
13650
15741
|
function readdirNames(dir) {
|
|
13651
15742
|
return readdirSync2(dir).filter((name) => {
|
|
13652
15743
|
try {
|
|
13653
|
-
return lstatSync5(
|
|
15744
|
+
return lstatSync5(join16(dir, name)).isDirectory();
|
|
13654
15745
|
} catch {
|
|
13655
15746
|
return false;
|
|
13656
15747
|
}
|
|
@@ -13735,7 +15826,7 @@ function refreshStationProfile(options = {}) {
|
|
|
13735
15826
|
const path = getStationProfileCachePath(env);
|
|
13736
15827
|
const generatedAt = new Date().toISOString();
|
|
13737
15828
|
if (!options.dryRun) {
|
|
13738
|
-
const existing = existsSync14(path) ?
|
|
15829
|
+
const existing = existsSync14(path) ? readFileSync13(path, "utf8") : null;
|
|
13739
15830
|
if (existing !== content) {
|
|
13740
15831
|
mkdirSync6(dirname9(path), { recursive: true });
|
|
13741
15832
|
writeFileSync5(path, content, "utf8");
|
|
@@ -13756,7 +15847,7 @@ function readStationProfile(env = process.env) {
|
|
|
13756
15847
|
try {
|
|
13757
15848
|
if (!existsSync14(path))
|
|
13758
15849
|
return null;
|
|
13759
|
-
return
|
|
15850
|
+
return readFileSync13(path, "utf8");
|
|
13760
15851
|
} catch {
|
|
13761
15852
|
return null;
|
|
13762
15853
|
}
|
|
@@ -13781,11 +15872,11 @@ import {
|
|
|
13781
15872
|
existsSync as existsSync15,
|
|
13782
15873
|
lstatSync as lstatSync6,
|
|
13783
15874
|
mkdirSync as mkdirSync7,
|
|
13784
|
-
readFileSync as
|
|
15875
|
+
readFileSync as readFileSync14,
|
|
13785
15876
|
readdirSync as readdirSync3,
|
|
13786
15877
|
statSync as statSync5
|
|
13787
15878
|
} from "fs";
|
|
13788
|
-
import { dirname as dirname10, isAbsolute as
|
|
15879
|
+
import { dirname as dirname10, isAbsolute as isAbsolute6, join as join17, parse as parse5, relative as relative5, resolve as resolve11 } from "path";
|
|
13789
15880
|
class SessionApplyError extends Error {
|
|
13790
15881
|
constructor(message) {
|
|
13791
15882
|
super(message);
|
|
@@ -13949,7 +16040,7 @@ function checkSessionRenderDrift(targetHome, manifestPath) {
|
|
|
13949
16040
|
});
|
|
13950
16041
|
continue;
|
|
13951
16042
|
}
|
|
13952
|
-
const actualSha256 = sha25610(
|
|
16043
|
+
const actualSha256 = sha25610(readFileSync14(target, "utf-8"));
|
|
13953
16044
|
if (actualSha256 !== file.sha256) {
|
|
13954
16045
|
drifted.push({
|
|
13955
16046
|
path: target,
|
|
@@ -13975,10 +16066,10 @@ function restoreSessionRenderSnapshot(snapshotPath, options = {}) {
|
|
|
13975
16066
|
const resolvedSnapshotPath = resolve11(snapshotPath);
|
|
13976
16067
|
const snapshotDir = getSessionRenderSnapshotDir(targetHome);
|
|
13977
16068
|
const snapshotDirRelative = relative5(snapshotDir, resolvedSnapshotPath);
|
|
13978
|
-
const insideSnapshotDir = snapshotDirRelative !== ".." && !snapshotDirRelative.startsWith("../") && !
|
|
16069
|
+
const insideSnapshotDir = snapshotDirRelative !== ".." && !snapshotDirRelative.startsWith("../") && !isAbsolute6(snapshotDirRelative);
|
|
13979
16070
|
if (!insideSnapshotDir) {
|
|
13980
16071
|
const snapshotRelativePath = relative5(targetHome, resolvedSnapshotPath);
|
|
13981
|
-
if (snapshotRelativePath === "" || snapshotRelativePath === ".." || snapshotRelativePath.startsWith("../") ||
|
|
16072
|
+
if (snapshotRelativePath === "" || snapshotRelativePath === ".." || snapshotRelativePath.startsWith("../") || isAbsolute6(snapshotRelativePath)) {
|
|
13982
16073
|
throw new SessionApplyError("Session snapshot must be stored inside its session-render snapshot location.");
|
|
13983
16074
|
}
|
|
13984
16075
|
}
|
|
@@ -14107,7 +16198,7 @@ function readSessionRenderSnapshot(snapshotPath) {
|
|
|
14107
16198
|
}
|
|
14108
16199
|
let parsed;
|
|
14109
16200
|
try {
|
|
14110
|
-
parsed = JSON.parse(
|
|
16201
|
+
parsed = JSON.parse(readFileSync14(resolved, "utf8"));
|
|
14111
16202
|
} catch {
|
|
14112
16203
|
throw new SessionApplyError(`Session snapshot is not valid JSON: ${snapshotPath}`);
|
|
14113
16204
|
}
|
|
@@ -14185,7 +16276,7 @@ function reconstructPreRollbackLegacyV1Snapshot(snapshot, previousFiles, previou
|
|
|
14185
16276
|
}
|
|
14186
16277
|
let parsedManifest;
|
|
14187
16278
|
try {
|
|
14188
|
-
parsedManifest = JSON.parse(
|
|
16279
|
+
parsedManifest = JSON.parse(readFileSync14(manifestPath, "utf8"));
|
|
14189
16280
|
} catch {
|
|
14190
16281
|
throw new SessionApplyError(`Pre-rollback legacy v1 applied manifest is not valid JSON: ${snapshotPath}`);
|
|
14191
16282
|
}
|
|
@@ -14285,7 +16376,7 @@ function assertNoNewerSessionSnapshot(snapshotPath, createdAt, targetHome) {
|
|
|
14285
16376
|
if (candidateStat.isSymbolicLink() || !candidateStat.isFile() || candidateStat.size > 32 * 1024 * 1024)
|
|
14286
16377
|
continue;
|
|
14287
16378
|
try {
|
|
14288
|
-
const candidate = JSON.parse(
|
|
16379
|
+
const candidate = JSON.parse(readFileSync14(candidatePath, "utf8"));
|
|
14289
16380
|
const candidateCreatedAtMs = typeof candidate.createdAt === "string" ? Date.parse(candidate.createdAt) : Number.NaN;
|
|
14290
16381
|
if ((candidate.schema === "hasna.configs.session-render-snapshot/v1" || candidate.schema === "hasna.configs.session-render-snapshot/v2") && typeof candidate.targetHome === "string" && resolve11(candidate.targetHome) === targetHome && Number.isFinite(candidateCreatedAtMs) && candidateCreatedAtMs >= createdAtMs) {
|
|
14291
16382
|
throw new SessionApplyError(`Cannot restore pre-rollback legacy v1 snapshot after a newer session snapshot exists: ${candidatePath}`);
|
|
@@ -14363,7 +16454,7 @@ function resolveSnapshotFilePath(relativePath, recordedPath, targetHome) {
|
|
|
14363
16454
|
}
|
|
14364
16455
|
function planFileResult(plan, file, targetHome, previousHashes, previousManifest, options) {
|
|
14365
16456
|
const target = resolvePlannedFilePath(plan, file, targetHome);
|
|
14366
|
-
const previousContent = existsSync15(target) ?
|
|
16457
|
+
const previousContent = existsSync15(target) ? readFileSync14(target, "utf-8") : null;
|
|
14367
16458
|
const previousSha256 = previousContent === null ? null : sha25610(previousContent);
|
|
14368
16459
|
const previouslyManaged = isPreviouslyManaged(file, previousSha256, previousHashes, previousManifest);
|
|
14369
16460
|
const changed = previousContent !== file.content;
|
|
@@ -14464,7 +16555,7 @@ function planStaleFileResult(file, targetHome, options) {
|
|
|
14464
16555
|
const target = resolveManifestRelativePath(file.relativePath, targetHome);
|
|
14465
16556
|
if (!existsSync15(target))
|
|
14466
16557
|
return null;
|
|
14467
|
-
const previousContent =
|
|
16558
|
+
const previousContent = readFileSync14(target, "utf-8");
|
|
14468
16559
|
const previousSha256 = sha25610(previousContent);
|
|
14469
16560
|
if (!options.force && previousSha256 !== file.sha256) {
|
|
14470
16561
|
return {
|
|
@@ -14511,7 +16602,7 @@ function isPreviouslyManaged(file, previousSha256, previousHashes, previousManif
|
|
|
14511
16602
|
function resolvePlannedFilePath(plan, file, targetHome) {
|
|
14512
16603
|
const target = resolve11(targetHome, ...file.relativePath.split("/"));
|
|
14513
16604
|
const rel = relative5(targetHome, target);
|
|
14514
|
-
if (rel === "" || rel === ".." || rel.startsWith("../") ||
|
|
16605
|
+
if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute6(rel)) {
|
|
14515
16606
|
throw new SessionApplyError(`Session file escapes target home: ${file.relativePath}`);
|
|
14516
16607
|
}
|
|
14517
16608
|
if (resolve11(file.path) !== target) {
|
|
@@ -14523,7 +16614,7 @@ function resolvePlannedFilePath(plan, file, targetHome) {
|
|
|
14523
16614
|
function resolveManifestRelativePath(relativePath, targetHome) {
|
|
14524
16615
|
const target = resolve11(targetHome, ...relativePath.split(/[\\/]+/));
|
|
14525
16616
|
const rel = relative5(targetHome, target);
|
|
14526
|
-
if (rel === "" || rel === ".." || rel.startsWith("../") ||
|
|
16617
|
+
if (rel === "" || rel === ".." || rel.startsWith("../") || isAbsolute6(rel)) {
|
|
14527
16618
|
throw new SessionApplyError(`Session manifest file escapes target home: ${relativePath}`);
|
|
14528
16619
|
}
|
|
14529
16620
|
assertNoSymlinkSegments2(targetHome, target);
|
|
@@ -14533,7 +16624,7 @@ function readPreviousManifest(path) {
|
|
|
14533
16624
|
if (!existsSync15(path))
|
|
14534
16625
|
return null;
|
|
14535
16626
|
try {
|
|
14536
|
-
const parsed = JSON.parse(
|
|
16627
|
+
const parsed = JSON.parse(readFileSync14(path, "utf-8"));
|
|
14537
16628
|
if (parsed.schema !== SESSION_RENDER_SCHEMA)
|
|
14538
16629
|
return null;
|
|
14539
16630
|
if (!Array.isArray(parsed.files))
|
|
@@ -14578,7 +16669,7 @@ function currentSessionFileHash(path, targetHome) {
|
|
|
14578
16669
|
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
14579
16670
|
throw new SessionApplyError(`Session apply path is not a regular file: ${path}`);
|
|
14580
16671
|
}
|
|
14581
|
-
return sha25610(
|
|
16672
|
+
return sha25610(readFileSync14(path, "utf-8"));
|
|
14582
16673
|
}
|
|
14583
16674
|
function requiredPreviousHash(result) {
|
|
14584
16675
|
if (result.previousSha256 === null) {
|
|
@@ -14588,7 +16679,7 @@ function requiredPreviousHash(result) {
|
|
|
14588
16679
|
}
|
|
14589
16680
|
function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousManifest, coordination, allowPortableFallback, forcePortableFileOps) {
|
|
14590
16681
|
const existingFiles = results.filter((result) => result.action === "update" || result.action === "delete").filter((result) => existsSync15(result.path)).map((result) => {
|
|
14591
|
-
const content =
|
|
16682
|
+
const content = readFileSync14(result.path, "utf-8");
|
|
14592
16683
|
return {
|
|
14593
16684
|
path: result.path,
|
|
14594
16685
|
relativePath: result.relativePath,
|
|
@@ -14606,7 +16697,7 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
|
|
|
14606
16697
|
};
|
|
14607
16698
|
}
|
|
14608
16699
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
14609
|
-
const snapshotPath =
|
|
16700
|
+
const snapshotPath = join17(getSessionRenderSnapshotDir(targetHome), `${timestamp}-${randomUUID4()}.json`);
|
|
14610
16701
|
const afterFiles = results.map((result) => {
|
|
14611
16702
|
if (result.action === "conflict") {
|
|
14612
16703
|
throw new SessionApplyError(`Cannot snapshot unresolved conflict: ${result.relativePath}`);
|
|
@@ -14652,7 +16743,7 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
|
|
|
14652
16743
|
};
|
|
14653
16744
|
}
|
|
14654
16745
|
function assertSafeTargetHome(targetHome) {
|
|
14655
|
-
if (!
|
|
16746
|
+
if (!isAbsolute6(targetHome))
|
|
14656
16747
|
throw new SessionApplyError(`Session target home must be absolute: ${targetHome}`);
|
|
14657
16748
|
const normalized = resolve11(targetHome);
|
|
14658
16749
|
if (normalized === parse5(normalized).root) {
|
|
@@ -14669,7 +16760,7 @@ function assertNoSymlinkSegments2(root, target) {
|
|
|
14669
16760
|
const rel = relative5(root, target);
|
|
14670
16761
|
let current = root;
|
|
14671
16762
|
for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
|
|
14672
|
-
current =
|
|
16763
|
+
current = join17(current, segment);
|
|
14673
16764
|
if (existsSync15(current) && lstatSync6(current).isSymbolicLink()) {
|
|
14674
16765
|
throw new SessionApplyError(`Session apply path uses a symlink: ${current}`);
|
|
14675
16766
|
}
|
|
@@ -14681,7 +16772,7 @@ function assertNoSymlinkAncestors3(path) {
|
|
|
14681
16772
|
let current = parsed.root;
|
|
14682
16773
|
const rel = relative5(parsed.root, normalized);
|
|
14683
16774
|
for (const segment of rel.split(/[\\/]+/).filter(Boolean)) {
|
|
14684
|
-
current =
|
|
16775
|
+
current = join17(current, segment);
|
|
14685
16776
|
if (!existsSync15(current))
|
|
14686
16777
|
return;
|
|
14687
16778
|
if (lstatSync6(current).isSymbolicLink()) {
|
|
@@ -14695,9 +16786,6 @@ function sha25610(content) {
|
|
|
14695
16786
|
// src/lib/project-dashboard-standard.ts
|
|
14696
16787
|
var PROJECT_DASHBOARD_STANDARD_SLUG = "agent-managed-project-dashboard-standard";
|
|
14697
16788
|
var PROJECT_DASHBOARD_PROFILE_VARIABLES = {
|
|
14698
|
-
PROJECT_DASHBOARD_DIR: ".hasna/project",
|
|
14699
|
-
PROJECT_DASHBOARD_RENDER_MANIFEST: ".hasna/project/dashboard/render.json",
|
|
14700
|
-
PROJECT_DASHBOARD_SNAPSHOTS_DIR: ".hasna/project/dashboard/snapshots",
|
|
14701
16789
|
PROJECT_CHANNEL_PREFIX: ""
|
|
14702
16790
|
};
|
|
14703
16791
|
var PROJECT_DASHBOARD_STANDARD_CONTENT = `# Agent-Managed Project Dashboard Standard
|
|
@@ -14708,9 +16796,13 @@ evidence, tasks, knowledge, and dashboard output consistent.
|
|
|
14708
16796
|
|
|
14709
16797
|
## Canonical Files
|
|
14710
16798
|
|
|
14711
|
-
|
|
14712
|
-
|
|
14713
|
-
|
|
16799
|
+
There is exactly one project-layout convention: the canonical per-workspace
|
|
16800
|
+
store \`~/.hasna/projects/workspaces/<workspace_id>/\`. Never create a project
|
|
16801
|
+
layout directory inside the project folder itself.
|
|
16802
|
+
|
|
16803
|
+
- Per-workspace store root: \`~/.hasna/projects/workspaces/<workspace_id>/\`
|
|
16804
|
+
- Dashboard render manifest: \`~/.hasna/projects/workspaces/<workspace_id>/dashboard/render.json\`
|
|
16805
|
+
- Latest snapshot: \`~/.hasna/projects/workspaces/<workspace_id>/dashboard/snapshots/latest.snapshot.json\`
|
|
14714
16806
|
- Dashboard schema ids come from \`@hasna/contracts\`.
|
|
14715
16807
|
- Project folders may contain private documents, but render JSON must contain
|
|
14716
16808
|
only ids, counts, statuses, resource refs, evidence refs, and redacted
|
|
@@ -14993,13 +17085,13 @@ async function ensureDangerousOperationGuardStandardConfig(store = resolveConfig
|
|
|
14993
17085
|
}
|
|
14994
17086
|
}
|
|
14995
17087
|
// src/lib/sync.ts
|
|
14996
|
-
import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as
|
|
14997
|
-
import { basename as basename6, extname as extname3, join as
|
|
17088
|
+
import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync16 } from "fs";
|
|
17089
|
+
import { basename as basename6, extname as extname3, join as join19 } from "path";
|
|
14998
17090
|
|
|
14999
17091
|
// src/lib/sync-dir.ts
|
|
15000
|
-
import { existsSync as existsSync16, readdirSync as readdirSync4, readFileSync as
|
|
15001
|
-
import { join as
|
|
15002
|
-
import { homedir as
|
|
17092
|
+
import { existsSync as existsSync16, readdirSync as readdirSync4, readFileSync as readFileSync15, statSync as statSync6 } from "fs";
|
|
17093
|
+
import { join as join18, relative as relative6 } from "path";
|
|
17094
|
+
import { homedir as homedir10 } from "os";
|
|
15003
17095
|
var SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
|
|
15004
17096
|
function shouldSkip(p) {
|
|
15005
17097
|
return SKIP.some((s) => p.includes(s));
|
|
@@ -15009,9 +17101,9 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
15009
17101
|
const absDir = expandPath(dir);
|
|
15010
17102
|
if (!existsSync16(absDir))
|
|
15011
17103
|
return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
|
|
15012
|
-
const files = opts.recursive !== false ? walkDir(absDir) : readdirSync4(absDir).map((f) =>
|
|
17104
|
+
const files = opts.recursive !== false ? walkDir(absDir) : readdirSync4(absDir).map((f) => join18(absDir, f)).filter((f) => statSync6(f).isFile());
|
|
15013
17105
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
15014
|
-
const home =
|
|
17106
|
+
const home = homedir10();
|
|
15015
17107
|
const allConfigs = await store.listConfigs();
|
|
15016
17108
|
for (const file of files) {
|
|
15017
17109
|
if (shouldSkip(file)) {
|
|
@@ -15019,7 +17111,7 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
15019
17111
|
continue;
|
|
15020
17112
|
}
|
|
15021
17113
|
try {
|
|
15022
|
-
const content =
|
|
17114
|
+
const content = readFileSync15(file, "utf-8");
|
|
15023
17115
|
if (content.length > 500000) {
|
|
15024
17116
|
result.skipped.push(file + " (too large)");
|
|
15025
17117
|
continue;
|
|
@@ -15046,7 +17138,7 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
15046
17138
|
}
|
|
15047
17139
|
async function syncToDir(dir, opts = {}) {
|
|
15048
17140
|
const store = opts.store ?? resolveConfigStore();
|
|
15049
|
-
const home =
|
|
17141
|
+
const home = homedir10();
|
|
15050
17142
|
const absDir = expandPath(dir);
|
|
15051
17143
|
const normalized = dir.startsWith("~/") ? dir : absDir.replace(home, "~");
|
|
15052
17144
|
const configs = (await store.listConfigs()).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
|
|
@@ -15070,7 +17162,7 @@ async function syncToDir(dir, opts = {}) {
|
|
|
15070
17162
|
}
|
|
15071
17163
|
function walkDir(dir, files = []) {
|
|
15072
17164
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
15073
|
-
const full =
|
|
17165
|
+
const full = join18(dir, entry.name);
|
|
15074
17166
|
if (shouldSkip(full))
|
|
15075
17167
|
continue;
|
|
15076
17168
|
if (entry.isDirectory())
|
|
@@ -15204,11 +17296,11 @@ async function syncProject(opts) {
|
|
|
15204
17296
|
const allConfigs = await store.listConfigs();
|
|
15205
17297
|
const machine = detectMachineContext();
|
|
15206
17298
|
for (const pf of PROJECT_CONFIG_FILES) {
|
|
15207
|
-
const abs =
|
|
17299
|
+
const abs = join19(absDir, pf.file);
|
|
15208
17300
|
if (!existsSync17(abs))
|
|
15209
17301
|
continue;
|
|
15210
17302
|
try {
|
|
15211
|
-
const rawContent =
|
|
17303
|
+
const rawContent = readFileSync16(abs, "utf-8");
|
|
15212
17304
|
if (rawContent.length > 500000) {
|
|
15213
17305
|
result.skipped.push(pf.file);
|
|
15214
17306
|
continue;
|
|
@@ -15237,20 +17329,20 @@ async function syncProject(opts) {
|
|
|
15237
17329
|
}
|
|
15238
17330
|
}
|
|
15239
17331
|
for (const ruleDir of [
|
|
15240
|
-
{ dir:
|
|
15241
|
-
{ dir:
|
|
15242
|
-
{ dir:
|
|
15243
|
-
{ dir:
|
|
15244
|
-
{ dir:
|
|
15245
|
-
{ dir:
|
|
15246
|
-
{ dir:
|
|
17332
|
+
{ dir: join19(absDir, ".claude", "rules"), agent: "claude", namePrefix: "rules" },
|
|
17333
|
+
{ dir: join19(absDir, ".agents", "rules"), agent: "antigravity", namePrefix: "antigravity-rules" },
|
|
17334
|
+
{ dir: join19(absDir, ".cursor", "rules"), agent: "cursor", namePrefix: "cursor-rules" },
|
|
17335
|
+
{ dir: join19(absDir, ".github", "instructions"), agent: "copilot", namePrefix: "copilot-instructions" },
|
|
17336
|
+
{ dir: join19(absDir, ".devin", "rules"), agent: "devin", namePrefix: "devin-rules" },
|
|
17337
|
+
{ dir: join19(absDir, ".windsurf", "rules"), agent: "windsurf-legacy", namePrefix: "windsurf-rules" },
|
|
17338
|
+
{ dir: join19(absDir, ".clinerules"), agent: "cline", namePrefix: "cline-rules" }
|
|
15247
17339
|
]) {
|
|
15248
17340
|
if (!existsSync17(ruleDir.dir))
|
|
15249
17341
|
continue;
|
|
15250
17342
|
const mdFiles = readdirSync5(ruleDir.dir).filter((f) => f.endsWith(".md") || f.endsWith(".mdc"));
|
|
15251
17343
|
for (const f of mdFiles) {
|
|
15252
|
-
const abs =
|
|
15253
|
-
const raw =
|
|
17344
|
+
const abs = join19(ruleDir.dir, f);
|
|
17345
|
+
const raw = readFileSync16(abs, "utf-8");
|
|
15254
17346
|
const redacted = redactContent(raw, "markdown");
|
|
15255
17347
|
const machineAware = templateizeMachineContent(redacted.content, machine);
|
|
15256
17348
|
const content = machineAware.content;
|
|
@@ -15296,13 +17388,13 @@ async function syncKnown(opts = {}) {
|
|
|
15296
17388
|
const extensions = known.rulesExtensions ?? [".md", ".mdc"];
|
|
15297
17389
|
const ruleFiles = readdirSync5(absDir).filter((f) => extensions.some((ext) => f.endsWith(ext)));
|
|
15298
17390
|
for (const f of ruleFiles) {
|
|
15299
|
-
const abs2 =
|
|
17391
|
+
const abs2 = join19(absDir, f);
|
|
15300
17392
|
const targetPath = abs2.replace(home, "~");
|
|
15301
17393
|
if (existingOutputOwners.has(normalizeTargetPath(targetPath)) || isKnownGeneratedTargetPath(targetPath)) {
|
|
15302
17394
|
result.skipped.push(`${targetPath} (generated output)`);
|
|
15303
17395
|
continue;
|
|
15304
17396
|
}
|
|
15305
|
-
const raw =
|
|
17397
|
+
const raw = readFileSync16(abs2, "utf-8");
|
|
15306
17398
|
const redacted = redactContent(raw, "markdown");
|
|
15307
17399
|
const machineAware = templateizeMachineContent(redacted.content, machine);
|
|
15308
17400
|
const content = machineAware.content;
|
|
@@ -15335,7 +17427,7 @@ async function syncKnown(opts = {}) {
|
|
|
15335
17427
|
continue;
|
|
15336
17428
|
}
|
|
15337
17429
|
try {
|
|
15338
|
-
const rawContent = normalizeKnownConfigSource(known,
|
|
17430
|
+
const rawContent = normalizeKnownConfigSource(known, readFileSync16(abs, "utf-8"));
|
|
15339
17431
|
if (rawContent.length > 500000) {
|
|
15340
17432
|
result.skipped.push(known.path + " (too large)");
|
|
15341
17433
|
continue;
|
|
@@ -15440,7 +17532,7 @@ function buildDiff(expectedContent, targetPath, storedFormat, showSecrets) {
|
|
|
15440
17532
|
const path = expandPath(targetPath);
|
|
15441
17533
|
if (!existsSync17(path))
|
|
15442
17534
|
return `(file not found on disk: ${path})`;
|
|
15443
|
-
const diskContent =
|
|
17535
|
+
const diskContent = readFileSync16(path, "utf-8");
|
|
15444
17536
|
if (diskContent === expectedContent)
|
|
15445
17537
|
return "(no diff \u2014 identical)";
|
|
15446
17538
|
const format = redactFormatForTarget(targetPath, storedFormat);
|
|
@@ -15599,14 +17691,14 @@ function detectFormat(filePath) {
|
|
|
15599
17691
|
}
|
|
15600
17692
|
// src/lib/export.ts
|
|
15601
17693
|
import { existsSync as existsSync18, mkdirSync as mkdirSync8, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
15602
|
-
import { join as
|
|
17694
|
+
import { join as join20, resolve as resolve12 } from "path";
|
|
15603
17695
|
import { tmpdir } from "os";
|
|
15604
17696
|
async function exportConfigs(outputPath, opts = {}) {
|
|
15605
17697
|
const store = opts.store ?? resolveConfigStore();
|
|
15606
17698
|
const configs = await store.listConfigs(opts.filter);
|
|
15607
17699
|
const absOutput = resolve12(outputPath);
|
|
15608
|
-
const tmpDir =
|
|
15609
|
-
const contentsDir =
|
|
17700
|
+
const tmpDir = join20(tmpdir(), `configs-export-${Date.now()}`);
|
|
17701
|
+
const contentsDir = join20(tmpDir, "contents");
|
|
15610
17702
|
try {
|
|
15611
17703
|
mkdirSync8(contentsDir, { recursive: true });
|
|
15612
17704
|
const manifest = {
|
|
@@ -15614,10 +17706,10 @@ async function exportConfigs(outputPath, opts = {}) {
|
|
|
15614
17706
|
exported_at: new Date().toISOString(),
|
|
15615
17707
|
configs: configs.map(({ content: _content, ...meta }) => meta)
|
|
15616
17708
|
};
|
|
15617
|
-
writeFileSync6(
|
|
17709
|
+
writeFileSync6(join20(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
|
|
15618
17710
|
for (const config of configs) {
|
|
15619
17711
|
const fileName = `${config.slug}.${config.format === "text" ? "txt" : config.format}`;
|
|
15620
|
-
writeFileSync6(
|
|
17712
|
+
writeFileSync6(join20(contentsDir, fileName), config.content, "utf-8");
|
|
15621
17713
|
}
|
|
15622
17714
|
const proc = Bun.spawn(["tar", "czf", absOutput, "-C", tmpDir, "."], {
|
|
15623
17715
|
stdout: "pipe",
|
|
@@ -15636,14 +17728,14 @@ async function exportConfigs(outputPath, opts = {}) {
|
|
|
15636
17728
|
}
|
|
15637
17729
|
}
|
|
15638
17730
|
// src/lib/import.ts
|
|
15639
|
-
import { existsSync as existsSync19, mkdirSync as mkdirSync9, readFileSync as
|
|
15640
|
-
import { join as
|
|
17731
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync9, readFileSync as readFileSync17, rmSync as rmSync5 } from "fs";
|
|
17732
|
+
import { join as join21, resolve as resolve13 } from "path";
|
|
15641
17733
|
import { tmpdir as tmpdir2 } from "os";
|
|
15642
17734
|
async function importConfigs(bundlePath, opts = {}) {
|
|
15643
17735
|
const store = opts.store ?? resolveConfigStore();
|
|
15644
17736
|
const conflict = opts.conflict ?? "skip";
|
|
15645
17737
|
const absPath = resolve13(bundlePath);
|
|
15646
|
-
const tmpDir =
|
|
17738
|
+
const tmpDir = join21(tmpdir2(), `configs-import-${Date.now()}`);
|
|
15647
17739
|
const result = { created: 0, updated: 0, skipped: 0, errors: [] };
|
|
15648
17740
|
try {
|
|
15649
17741
|
mkdirSync9(tmpDir, { recursive: true });
|
|
@@ -15656,15 +17748,15 @@ async function importConfigs(bundlePath, opts = {}) {
|
|
|
15656
17748
|
const stderr = await new Response(proc.stderr).text();
|
|
15657
17749
|
throw new Error(`tar extraction failed: ${stderr}`);
|
|
15658
17750
|
}
|
|
15659
|
-
const manifestPath =
|
|
17751
|
+
const manifestPath = join21(tmpDir, "manifest.json");
|
|
15660
17752
|
if (!existsSync19(manifestPath))
|
|
15661
17753
|
throw new Error("Invalid bundle: missing manifest.json");
|
|
15662
|
-
const manifest = JSON.parse(
|
|
17754
|
+
const manifest = JSON.parse(readFileSync17(manifestPath, "utf-8"));
|
|
15663
17755
|
for (const meta of manifest.configs) {
|
|
15664
17756
|
try {
|
|
15665
17757
|
const ext = meta.format === "text" ? "txt" : meta.format;
|
|
15666
|
-
const contentFile =
|
|
15667
|
-
const content = existsSync19(contentFile) ?
|
|
17758
|
+
const contentFile = join21(tmpDir, "contents", `${meta.slug}.${ext}`);
|
|
17759
|
+
const content = existsSync19(contentFile) ? readFileSync17(contentFile, "utf-8") : "";
|
|
15668
17760
|
let existing = null;
|
|
15669
17761
|
try {
|
|
15670
17762
|
existing = await store.getConfig(meta.slug);
|
|
@@ -15705,9 +17797,9 @@ async function importConfigs(bundlePath, opts = {}) {
|
|
|
15705
17797
|
}
|
|
15706
17798
|
// src/lib/package-manager-guard.ts
|
|
15707
17799
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
15708
|
-
import { existsSync as existsSync20, lstatSync as lstatSync7, readdirSync as readdirSync6, readFileSync as
|
|
15709
|
-
import { homedir as
|
|
15710
|
-
import { basename as basename7, dirname as dirname11, isAbsolute as
|
|
17800
|
+
import { existsSync as existsSync20, lstatSync as lstatSync7, readdirSync as readdirSync6, readFileSync as readFileSync18 } from "fs";
|
|
17801
|
+
import { homedir as homedir11 } from "os";
|
|
17802
|
+
import { basename as basename7, dirname as dirname11, isAbsolute as isAbsolute7, join as join22, relative as relative7, resolve as resolve14 } from "path";
|
|
15711
17803
|
var SKIP_DIRS = new Set([
|
|
15712
17804
|
".git",
|
|
15713
17805
|
"node_modules",
|
|
@@ -15776,9 +17868,9 @@ function scanPackageManagerSecrets(options = {}) {
|
|
|
15776
17868
|
}
|
|
15777
17869
|
}
|
|
15778
17870
|
if (options.includeHome) {
|
|
15779
|
-
const home =
|
|
17871
|
+
const home = homedir11();
|
|
15780
17872
|
for (const name of HOME_FILES) {
|
|
15781
|
-
const file =
|
|
17873
|
+
const file = join22(home, name);
|
|
15782
17874
|
if (!existsSync20(file))
|
|
15783
17875
|
continue;
|
|
15784
17876
|
const text = readTextFile(file);
|
|
@@ -15803,12 +17895,12 @@ function collectRepoFiles(root) {
|
|
|
15803
17895
|
if (entry.isDirectory()) {
|
|
15804
17896
|
if (SKIP_DIRS.has(entry.name))
|
|
15805
17897
|
continue;
|
|
15806
|
-
visit(
|
|
17898
|
+
visit(join22(dir, entry.name));
|
|
15807
17899
|
continue;
|
|
15808
17900
|
}
|
|
15809
17901
|
if (!entry.isFile())
|
|
15810
17902
|
continue;
|
|
15811
|
-
const file =
|
|
17903
|
+
const file = join22(dir, entry.name);
|
|
15812
17904
|
if (shouldScanRepoFile(file))
|
|
15813
17905
|
out.push(file);
|
|
15814
17906
|
}
|
|
@@ -15846,7 +17938,7 @@ function readTextFile(file) {
|
|
|
15846
17938
|
const stat = lstatSync7(file);
|
|
15847
17939
|
if (!stat.isFile() || stat.size > 5000000)
|
|
15848
17940
|
return null;
|
|
15849
|
-
const buf =
|
|
17941
|
+
const buf = readFileSync18(file);
|
|
15850
17942
|
if (buf.includes(0))
|
|
15851
17943
|
return null;
|
|
15852
17944
|
return buf.toString("utf-8");
|
|
@@ -16076,10 +18168,10 @@ function stripInlineComment(value) {
|
|
|
16076
18168
|
return value.replace(/\s[#;].*$/, "").trim();
|
|
16077
18169
|
}
|
|
16078
18170
|
function displayPath(file, root) {
|
|
16079
|
-
const home =
|
|
18171
|
+
const home = homedir11();
|
|
16080
18172
|
if (root === home && (file === home || file.startsWith(home + "/")))
|
|
16081
18173
|
return "~/" + toPosix(relative7(home, file));
|
|
16082
|
-
if (
|
|
18174
|
+
if (isAbsolute7(root) && file.startsWith(root + "/"))
|
|
16083
18175
|
return toPosix(relative7(root, file));
|
|
16084
18176
|
if (file === home || file.startsWith(home + "/"))
|
|
16085
18177
|
return "~/" + toPosix(relative7(home, file));
|
|
@@ -16115,7 +18207,6 @@ export {
|
|
|
16115
18207
|
resolveSessionPath,
|
|
16116
18208
|
resolveProfileVariables,
|
|
16117
18209
|
resolveConfigStore,
|
|
16118
|
-
resolveCloudConfig,
|
|
16119
18210
|
resolveAssetDestination,
|
|
16120
18211
|
resolveAndRenderProviderContext,
|
|
16121
18212
|
resolveAgentOperatingRulesPayload,
|
|
@@ -16147,13 +18238,19 @@ export {
|
|
|
16147
18238
|
machineContextToVariables,
|
|
16148
18239
|
legacyProfileConfigBinding,
|
|
16149
18240
|
isTemplate,
|
|
18241
|
+
isLocalOptIn,
|
|
18242
|
+
isInstructionsLocalOptIn,
|
|
18243
|
+
isCloudAuthError,
|
|
16150
18244
|
isApiTransport,
|
|
18245
|
+
instructionsLocalModeNotice,
|
|
16151
18246
|
inspectManagedSkillRuntimes,
|
|
16152
18247
|
importConfigs,
|
|
16153
18248
|
hasSecrets,
|
|
16154
18249
|
getStationProfileCachePath,
|
|
16155
18250
|
getRawStoreRoot,
|
|
18251
|
+
getInstructionsTransportStatus,
|
|
16156
18252
|
getConfigsStatus,
|
|
18253
|
+
formatCliError,
|
|
16157
18254
|
extractTemplateVars,
|
|
16158
18255
|
exportConfigs,
|
|
16159
18256
|
expandPath,
|
|
@@ -16190,6 +18287,7 @@ export {
|
|
|
16190
18287
|
applyConfigsWithReport,
|
|
16191
18288
|
applyConfigs,
|
|
16192
18289
|
applyConfig,
|
|
18290
|
+
announceLocalInstructionsMode,
|
|
16193
18291
|
TemplateRenderError,
|
|
16194
18292
|
SessionApplyError,
|
|
16195
18293
|
STATION_PROFILE_SOURCE_ID,
|
|
@@ -16231,6 +18329,7 @@ export {
|
|
|
16231
18329
|
PLATFORM_PROFILE_PRESETS,
|
|
16232
18330
|
PG_MIGRATIONS,
|
|
16233
18331
|
LocalConfigStore,
|
|
18332
|
+
LOCAL_OPT_IN_ENV,
|
|
16234
18333
|
LEGACY_CONFIGS_PACKAGE,
|
|
16235
18334
|
LEGACY_CONFIGS_EXECUTABLE,
|
|
16236
18335
|
LEGACY_CONFIGS_COMPAT_VERSION,
|
|
@@ -16239,6 +18338,8 @@ export {
|
|
|
16239
18338
|
INSTRUCTION_GRAPH_PLAN_SCHEMA,
|
|
16240
18339
|
INSTRUCTION_FALLBACKS,
|
|
16241
18340
|
INSTRUCTION_ACTIVATION_MODES,
|
|
18341
|
+
INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS,
|
|
18342
|
+
INSTRUCTIONS_LOCAL_OPT_IN_ENV,
|
|
16242
18343
|
INBOX_CONVERSATIONS_MINIMUM_VERSION,
|
|
16243
18344
|
GLOBAL_AGENT_RULES_STANDARD_SLUG,
|
|
16244
18345
|
GLOBAL_AGENT_RULES_STANDARD_CONTENT,
|
|
@@ -16246,7 +18347,6 @@ export {
|
|
|
16246
18347
|
DANGEROUS_OPERATION_GUARD_STANDARD_CONTENT,
|
|
16247
18348
|
ConfigNotFoundError,
|
|
16248
18349
|
ConfigApplyError,
|
|
16249
|
-
CloudHttpError,
|
|
16250
18350
|
CloudConfigStore,
|
|
16251
18351
|
CONFIG_TRANSFORMS,
|
|
16252
18352
|
CONFIG_KINDS,
|