@hasna/hooks 0.7.9 → 0.8.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 +32 -6
- package/bin/index.js +1573 -695
- package/bin/serve.js +713 -335
- package/dist/config.d.ts +25 -19
- package/dist/db/index.d.ts +3 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1162 -473
- package/dist/lib/app-home.d.ts +74 -0
- package/dist/lib/installer.d.ts +0 -9
- package/dist/lib/local-opt-in.d.ts +80 -0
- package/dist/lib/profiles.d.ts +14 -1
- package/dist/lib/resolver-types.d.ts +45 -0
- package/dist/lib/sync.d.ts +13 -8
- package/dist/lib/transport.d.ts +87 -0
- package/dist/openapi.d.ts +1 -1
- package/dist/serve.d.ts +7 -0
- package/dist/storage.js +141 -27
- package/hooks/hook-affected-tests/package.json +1 -1
- package/hooks/hook-agent-rules-version-check/package.json +1 -1
- package/hooks/hook-agentmessages/package.json +1 -1
- package/hooks/hook-announce-start/package.json +1 -1
- package/hooks/hook-announce-stop/package.json +1 -1
- package/hooks/hook-autoformat/package.json +1 -1
- package/hooks/hook-branchprotect/package.json +1 -1
- package/hooks/hook-checkbugs/package.json +1 -1
- package/hooks/hook-checkdocs/package.json +1 -1
- package/hooks/hook-checkfiles/package.json +1 -1
- package/hooks/hook-checklint/package.json +1 -1
- package/hooks/hook-checkpoint/package.json +1 -1
- package/hooks/hook-checksecurity/package.json +1 -1
- package/hooks/hook-checktasks/package.json +1 -1
- package/hooks/hook-checktests/package.json +1 -1
- package/hooks/hook-conflict-detect/package.json +1 -1
- package/hooks/hook-contextrefresh/package.json +1 -1
- package/hooks/hook-desktopnotify/package.json +1 -1
- package/hooks/hook-dm-inject/package.json +1 -1
- package/hooks/hook-envsetup/package.json +1 -1
- package/hooks/hook-failure-to-task/package.json +1 -1
- package/hooks/hook-filelock/package.json +1 -1
- package/hooks/hook-fleet-blockers-gate/package.json +1 -1
- package/hooks/hook-fleet-catchup/package.json +1 -1
- package/hooks/hook-gitguard/package.json +1 -1
- package/hooks/hook-packageage/package.json +1 -1
- package/hooks/hook-permissionguard/package.json +1 -1
- package/hooks/hook-phonenotify/package.json +1 -1
- package/hooks/hook-precompact/package.json +1 -1
- package/hooks/hook-protectfiles/package.json +1 -1
- package/hooks/hook-stylescheck/package.json +1 -1
- package/hooks/hook-typecheck-gate/package.json +1 -1
- package/package.json +6 -9
- package/scripts/artifact-scan.ts +49 -0
- package/scripts/ensure-profiles-dir.mjs +99 -0
- package/scripts/validate-package.ts +280 -0
package/dist/index.js
CHANGED
|
@@ -15,6 +15,123 @@ var __export = (target, all) => {
|
|
|
15
15
|
};
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
17
|
|
|
18
|
+
// src/lib/app-home.ts
|
|
19
|
+
import { existsSync } from "fs";
|
|
20
|
+
import { homedir } from "os";
|
|
21
|
+
import { join, resolve } from "path";
|
|
22
|
+
import { homedir as pathsResolverHomedir } from "os";
|
|
23
|
+
import { join as pathsResolverJoin } from "path";
|
|
24
|
+
function pathsResolverAssertApp(app) {
|
|
25
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
26
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
27
|
+
}
|
|
28
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
|
|
29
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function pathsResolverAssertKind(kind) {
|
|
33
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
|
|
34
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function pathsResolverBaseDir(kind, options) {
|
|
38
|
+
pathsResolverAssertKind(kind);
|
|
39
|
+
const env = options.env ?? process.env;
|
|
40
|
+
const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
|
|
41
|
+
if (typeof override === "string" && override.length > 0)
|
|
42
|
+
return override;
|
|
43
|
+
const home = options.home ?? pathsResolverHomedir();
|
|
44
|
+
const platform = options.platform ?? process.platform;
|
|
45
|
+
if (platform === "darwin") {
|
|
46
|
+
switch (kind) {
|
|
47
|
+
case "config":
|
|
48
|
+
case "data":
|
|
49
|
+
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
50
|
+
case "cache":
|
|
51
|
+
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
52
|
+
case "state":
|
|
53
|
+
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
switch (kind) {
|
|
57
|
+
case "config":
|
|
58
|
+
return pathsResolverJoin(home, ".config", "hasna");
|
|
59
|
+
case "data":
|
|
60
|
+
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
61
|
+
case "state":
|
|
62
|
+
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
63
|
+
case "cache":
|
|
64
|
+
return pathsResolverJoin(home, ".cache", "hasna");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function pathsResolverResolve(kind, options) {
|
|
68
|
+
pathsResolverAssertApp(options.app);
|
|
69
|
+
const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
|
|
70
|
+
return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
|
|
71
|
+
}
|
|
72
|
+
function dataDir(options) {
|
|
73
|
+
return pathsResolverResolve("data", options);
|
|
74
|
+
}
|
|
75
|
+
function getHomeDir(env = process.env) {
|
|
76
|
+
return env.HOME || env.USERPROFILE || homedir();
|
|
77
|
+
}
|
|
78
|
+
function getResolverDataRoot(env = process.env) {
|
|
79
|
+
return dataDir({ app: "hooks", home: getHomeDir(env), env });
|
|
80
|
+
}
|
|
81
|
+
function getLegacyDataRoot(env = process.env) {
|
|
82
|
+
return join(getHomeDir(env), ".hasna", "hooks");
|
|
83
|
+
}
|
|
84
|
+
function adoptResolverDataRoot(resolved, env = process.env) {
|
|
85
|
+
const dataOverride = env.HASNA_DATA_HOME;
|
|
86
|
+
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
87
|
+
return true;
|
|
88
|
+
return existsSync(join(resolved, STORE_MARKER));
|
|
89
|
+
}
|
|
90
|
+
function getExplicitDataDir(env = process.env) {
|
|
91
|
+
const dir = env.HASNA_HOOKS_DATA_DIR ?? env.HOOKS_DATA_DIR;
|
|
92
|
+
if (typeof dir === "string" && dir.trim().length > 0)
|
|
93
|
+
return dir.trim();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
function getExactDataRoot(env = process.env) {
|
|
97
|
+
const dir = env.HASNA_HOOKS_HOME?.trim() || env.HOOKS_HOME?.trim();
|
|
98
|
+
if (dir)
|
|
99
|
+
return resolve(dir);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
function getEffectiveDataRoot(env = process.env) {
|
|
103
|
+
const explicit = getExplicitDataDir(env);
|
|
104
|
+
if (explicit)
|
|
105
|
+
return explicit;
|
|
106
|
+
const exact = getExactDataRoot(env);
|
|
107
|
+
if (exact)
|
|
108
|
+
return exact;
|
|
109
|
+
const resolved = getResolverDataRoot(env);
|
|
110
|
+
return adoptResolverDataRoot(resolved, env) ? resolve(resolved) : getLegacyDataRoot(env);
|
|
111
|
+
}
|
|
112
|
+
function getReportedDbPath(env = process.env) {
|
|
113
|
+
const explicit = getExplicitDbPath(env);
|
|
114
|
+
if (explicit)
|
|
115
|
+
return explicit;
|
|
116
|
+
return join(getEffectiveDataRoot(env), "hooks.db");
|
|
117
|
+
}
|
|
118
|
+
function getExplicitDbPath(env = process.env) {
|
|
119
|
+
const db = env.HASNA_HOOKS_DB_PATH ?? env.HOOKS_DB_PATH;
|
|
120
|
+
if (typeof db === "string" && db.trim().length > 0)
|
|
121
|
+
return db.trim();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE, STORE_MARKER = "hooks.db";
|
|
125
|
+
var init_app_home = __esm(() => {
|
|
126
|
+
PATHS_RESOLVER_KIND_ENV = {
|
|
127
|
+
config: "HASNA_CONFIG_HOME",
|
|
128
|
+
data: "HASNA_DATA_HOME",
|
|
129
|
+
state: "HASNA_STATE_HOME",
|
|
130
|
+
cache: "HASNA_CACHE_HOME"
|
|
131
|
+
};
|
|
132
|
+
PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
133
|
+
});
|
|
134
|
+
|
|
18
135
|
// src/db/schema.ts
|
|
19
136
|
var CREATE_HOOK_EVENTS_TABLE = `
|
|
20
137
|
CREATE TABLE IF NOT EXISTS hook_events (
|
|
@@ -209,9 +326,9 @@ var init_migrations = __esm(() => {
|
|
|
209
326
|
});
|
|
210
327
|
|
|
211
328
|
// src/db/legacy-import.ts
|
|
212
|
-
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
213
|
-
import { join } from "path";
|
|
214
|
-
import { homedir } from "os";
|
|
329
|
+
import { existsSync as existsSync2, readdirSync, readFileSync } from "fs";
|
|
330
|
+
import { join as join2 } from "path";
|
|
331
|
+
import { homedir as homedir2 } from "os";
|
|
215
332
|
function ensureMetaTable(db) {
|
|
216
333
|
db.exec(`
|
|
217
334
|
CREATE TABLE IF NOT EXISTS _meta (
|
|
@@ -285,25 +402,25 @@ function importErrorsLog(db, filePath) {
|
|
|
285
402
|
} catch {}
|
|
286
403
|
return count;
|
|
287
404
|
}
|
|
288
|
-
function runLegacyImport(db, homeDir =
|
|
405
|
+
function runLegacyImport(db, homeDir = homedir2()) {
|
|
289
406
|
try {
|
|
290
407
|
if (isAlreadyDone(db))
|
|
291
408
|
return;
|
|
292
409
|
let total = 0;
|
|
293
|
-
const claudeProjectsDir =
|
|
294
|
-
if (
|
|
410
|
+
const claudeProjectsDir = join2(homeDir, ".claude", "projects");
|
|
411
|
+
if (existsSync2(claudeProjectsDir)) {
|
|
295
412
|
try {
|
|
296
413
|
const projectDirs = readdirSync(claudeProjectsDir);
|
|
297
414
|
for (const dir of projectDirs) {
|
|
298
|
-
const projectDir =
|
|
415
|
+
const projectDir = join2(claudeProjectsDir, dir);
|
|
299
416
|
try {
|
|
300
417
|
const files = readdirSync(projectDir);
|
|
301
418
|
for (const file of files) {
|
|
302
419
|
if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
|
|
303
|
-
total += importJsonlFile(db,
|
|
420
|
+
total += importJsonlFile(db, join2(projectDir, file));
|
|
304
421
|
}
|
|
305
422
|
if (file === "errors.log") {
|
|
306
|
-
total += importErrorsLog(db,
|
|
423
|
+
total += importErrorsLog(db, join2(projectDir, file));
|
|
307
424
|
}
|
|
308
425
|
}
|
|
309
426
|
} catch {}
|
|
@@ -339,31 +456,27 @@ function runRetention(db, days) {
|
|
|
339
456
|
|
|
340
457
|
// src/db/index.ts
|
|
341
458
|
import { Database } from "bun:sqlite";
|
|
342
|
-
import { existsSync as
|
|
343
|
-
import { join as
|
|
344
|
-
import { homedir as homedir2 } from "os";
|
|
459
|
+
import { existsSync as existsSync3, mkdirSync, cpSync } from "fs";
|
|
460
|
+
import { join as join3 } from "path";
|
|
345
461
|
function resolveDataDir() {
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (!existsSync2(newDir) && existsSync2(oldDir)) {
|
|
352
|
-
mkdirSync(join2(homedir2(), ".hasna"), { recursive: true });
|
|
353
|
-
cpSync(oldDir, newDir, { recursive: true });
|
|
462
|
+
const effective = getEffectiveDataRoot();
|
|
463
|
+
const oldDir = join3(getHomeDir(), ".hooks");
|
|
464
|
+
if (!existsSync3(effective) && existsSync3(oldDir)) {
|
|
465
|
+
mkdirSync(effective, { recursive: true });
|
|
466
|
+
cpSync(oldDir, effective, { recursive: true });
|
|
354
467
|
}
|
|
355
|
-
return
|
|
468
|
+
return effective;
|
|
356
469
|
}
|
|
357
470
|
function getDbPath() {
|
|
358
471
|
const explicitDb = process.env.HASNA_HOOKS_DB_PATH ?? process.env.HOOKS_DB_PATH;
|
|
359
472
|
if (explicitDb)
|
|
360
473
|
return explicitDb;
|
|
361
|
-
const
|
|
362
|
-
return
|
|
474
|
+
const dataDir2 = resolveDataDir();
|
|
475
|
+
return join3(dataDir2, "hooks.db");
|
|
363
476
|
}
|
|
364
477
|
function ensureDir(dbPath) {
|
|
365
478
|
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
366
|
-
if (dir && !
|
|
479
|
+
if (dir && !existsSync3(dir)) {
|
|
367
480
|
mkdirSync(dir, { recursive: true });
|
|
368
481
|
}
|
|
369
482
|
}
|
|
@@ -371,7 +484,7 @@ function getDb() {
|
|
|
371
484
|
if (instance)
|
|
372
485
|
return instance;
|
|
373
486
|
const dbPath = getDbPath();
|
|
374
|
-
const isNew = dbPath === ":memory:" || !
|
|
487
|
+
const isNew = dbPath === ":memory:" || !existsSync3(dbPath);
|
|
375
488
|
ensureDir(dbPath);
|
|
376
489
|
instance = new Database(dbPath);
|
|
377
490
|
instance.exec("PRAGMA busy_timeout=5000");
|
|
@@ -395,6 +508,7 @@ function getDb() {
|
|
|
395
508
|
}
|
|
396
509
|
var instance = null;
|
|
397
510
|
var init_db = __esm(() => {
|
|
511
|
+
init_app_home();
|
|
398
512
|
init_migrations();
|
|
399
513
|
init_legacy_import();
|
|
400
514
|
});
|
|
@@ -445,8 +559,8 @@ var init_redact = __esm(() => {
|
|
|
445
559
|
/AKIA[0-9A-Z]{16}/g,
|
|
446
560
|
/ASIA[0-9A-Z]{16}/g,
|
|
447
561
|
/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
448
|
-
/-----BEGIN [A-Z0-9 ]*
|
|
449
|
-
/-----BEGIN OPENSSH
|
|
562
|
+
/-----BEGIN [A-Z0-9 ]*PRIVAT[E] KEY-----/g,
|
|
563
|
+
/-----BEGIN OPENSSH PRIVAT[E] KEY-----/g,
|
|
450
564
|
/\bBearer\s+[A-Za-z0-9._~+/=-]{6,}/g,
|
|
451
565
|
/\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/gi,
|
|
452
566
|
/\b(?:password|passwd|pwd|token|api[_-]?key|secret|access[_-]?key|client[_-]?secret|authorization|auth|credential|database_url|db_url|connection_string|dsn)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;"']{6,})/gi
|
|
@@ -4419,289 +4533,10 @@ var init_zod = __esm(() => {
|
|
|
4419
4533
|
init_external();
|
|
4420
4534
|
});
|
|
4421
4535
|
|
|
4422
|
-
// ../contracts/dist/client/transport.js
|
|
4423
|
-
import { readFileSync as readFileSync2, statSync } from "fs";
|
|
4424
|
-
import { join as join3 } from "path";
|
|
4425
|
-
function envToken(name) {
|
|
4426
|
-
return name.toUpperCase().replace(/-/g, "_");
|
|
4427
|
-
}
|
|
4428
|
-
function clientTransportEnvKeys(name) {
|
|
4429
|
-
const envSegment = envToken(name);
|
|
4430
|
-
return {
|
|
4431
|
-
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
4432
|
-
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
4433
|
-
};
|
|
4434
|
-
}
|
|
4435
|
-
function credentialOverrideEnvKey(name) {
|
|
4436
|
-
return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
|
|
4437
|
-
}
|
|
4438
|
-
function homeDir(env) {
|
|
4439
|
-
const home = env.HOME?.trim();
|
|
4440
|
-
return home ? home : null;
|
|
4441
|
-
}
|
|
4442
|
-
function credentialDiskSources(name, env) {
|
|
4443
|
-
return profileDiskSources(name, env, null);
|
|
4444
|
-
}
|
|
4445
|
-
function profileDiskSources(name, env, profile) {
|
|
4446
|
-
const home = homeDir(env);
|
|
4447
|
-
if (!home || !SAFE_APP_SLUG.test(name))
|
|
4448
|
-
return [];
|
|
4449
|
-
const stem = profile ? `${name}.${profile}` : name;
|
|
4450
|
-
const configStem = profile ? `${name}-${profile}` : name;
|
|
4451
|
-
return [
|
|
4452
|
-
join3(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
|
|
4453
|
-
join3(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`)
|
|
4454
|
-
];
|
|
4455
|
-
}
|
|
4456
|
-
function parseEnvFile(text) {
|
|
4457
|
-
const values = new Map;
|
|
4458
|
-
for (const rawLine of text.split(/\r?\n/)) {
|
|
4459
|
-
const line = rawLine.trim();
|
|
4460
|
-
if (line.length === 0 || line.startsWith("#"))
|
|
4461
|
-
continue;
|
|
4462
|
-
const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
|
4463
|
-
const equals = withoutExport.indexOf("=");
|
|
4464
|
-
if (equals <= 0)
|
|
4465
|
-
continue;
|
|
4466
|
-
const key = withoutExport.slice(0, equals).trim();
|
|
4467
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
4468
|
-
continue;
|
|
4469
|
-
let value = withoutExport.slice(equals + 1).trim();
|
|
4470
|
-
const quote = value[0];
|
|
4471
|
-
if (quote === '"' || quote === "'") {
|
|
4472
|
-
if (value.length < 2 || !value.endsWith(quote))
|
|
4473
|
-
continue;
|
|
4474
|
-
value = value.slice(1, -1);
|
|
4475
|
-
}
|
|
4476
|
-
if (value.length === 0)
|
|
4477
|
-
continue;
|
|
4478
|
-
values.set(key, value);
|
|
4479
|
-
}
|
|
4480
|
-
return values;
|
|
4481
|
-
}
|
|
4482
|
-
function readAppConfigFile(path) {
|
|
4483
|
-
let text;
|
|
4484
|
-
try {
|
|
4485
|
-
const stats = statSync(path);
|
|
4486
|
-
if (!stats.isFile() || stats.size > MAX_CREDENTIAL_FILE_BYTES)
|
|
4487
|
-
return null;
|
|
4488
|
-
text = readFileSync2(path, "utf8");
|
|
4489
|
-
} catch {
|
|
4490
|
-
return null;
|
|
4491
|
-
}
|
|
4492
|
-
return parseEnvFile(text);
|
|
4493
|
-
}
|
|
4494
|
-
function readCredentialFile(path, apiKeyKeys) {
|
|
4495
|
-
const values = readAppConfigFile(path);
|
|
4496
|
-
if (!values)
|
|
4497
|
-
return null;
|
|
4498
|
-
for (const key of apiKeyKeys) {
|
|
4499
|
-
const value = values.get(key)?.trim();
|
|
4500
|
-
if (value)
|
|
4501
|
-
return value;
|
|
4502
|
-
}
|
|
4503
|
-
return null;
|
|
4504
|
-
}
|
|
4505
|
-
function assertUsableCredential(appName, source, value) {
|
|
4506
|
-
if (!ILLEGAL_IN_HEADER_VALUE.test(value))
|
|
4507
|
-
return;
|
|
4508
|
-
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]);
|
|
4509
|
-
}
|
|
4510
|
-
function sealCredential(fields) {
|
|
4511
|
-
const { apiKey } = fields;
|
|
4512
|
-
const visible = {
|
|
4513
|
-
tier: fields.tier,
|
|
4514
|
-
source: fields.source,
|
|
4515
|
-
deliberate: fields.deliberate,
|
|
4516
|
-
deprecated: fields.deprecated,
|
|
4517
|
-
diskCandidates: Object.freeze([...fields.diskCandidates]),
|
|
4518
|
-
warning: fields.warning
|
|
4519
|
-
};
|
|
4520
|
-
const sealed = { ...visible };
|
|
4521
|
-
Object.defineProperty(sealed, "apiKey", {
|
|
4522
|
-
value: apiKey,
|
|
4523
|
-
enumerable: false,
|
|
4524
|
-
writable: false,
|
|
4525
|
-
configurable: false
|
|
4526
|
-
});
|
|
4527
|
-
Object.defineProperty(sealed, INSPECT_CUSTOM, {
|
|
4528
|
-
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
4529
|
-
enumerable: false,
|
|
4530
|
-
writable: false,
|
|
4531
|
-
configurable: false
|
|
4532
|
-
});
|
|
4533
|
-
Object.defineProperty(sealed, CREDENTIAL_SEAL, {
|
|
4534
|
-
value: true,
|
|
4535
|
-
enumerable: false,
|
|
4536
|
-
writable: false,
|
|
4537
|
-
configurable: false
|
|
4538
|
-
});
|
|
4539
|
-
return Object.freeze(sealed);
|
|
4540
|
-
}
|
|
4541
|
-
function firstEnvValue(env, keys) {
|
|
4542
|
-
for (const key of keys) {
|
|
4543
|
-
const value = env[key]?.trim();
|
|
4544
|
-
if (value)
|
|
4545
|
-
return { key, value };
|
|
4546
|
-
}
|
|
4547
|
-
return null;
|
|
4548
|
-
}
|
|
4549
|
-
function deprecationNotified() {
|
|
4550
|
-
const host = globalThis;
|
|
4551
|
-
const existing = host[DEPRECATION_REGISTRY];
|
|
4552
|
-
if (existing instanceof Set)
|
|
4553
|
-
return existing;
|
|
4554
|
-
const created = new Set;
|
|
4555
|
-
host[DEPRECATION_REGISTRY] = created;
|
|
4556
|
-
return created;
|
|
4557
|
-
}
|
|
4558
|
-
function defaultDeprecationSink(message) {
|
|
4559
|
-
if (typeof process !== "undefined" && process.stderr) {
|
|
4560
|
-
process.stderr.write(`${message}
|
|
4561
|
-
`);
|
|
4562
|
-
}
|
|
4563
|
-
}
|
|
4564
|
-
function resolveCredential(name, env, options = {}) {
|
|
4565
|
-
const { apiKeyKeys } = clientTransportEnvKeys(name);
|
|
4566
|
-
const diskPaths = credentialDiskSources(name, env);
|
|
4567
|
-
const explicitKey = options.apiKey?.trim();
|
|
4568
|
-
if (explicitKey) {
|
|
4569
|
-
assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
|
|
4570
|
-
return sealCredential({
|
|
4571
|
-
apiKey: explicitKey,
|
|
4572
|
-
tier: "argument",
|
|
4573
|
-
source: "explicit apiKey argument",
|
|
4574
|
-
deliberate: true,
|
|
4575
|
-
deprecated: false,
|
|
4576
|
-
diskCandidates: diskPaths,
|
|
4577
|
-
warning: null
|
|
4578
|
-
});
|
|
4579
|
-
}
|
|
4580
|
-
const overrideKeyName = credentialOverrideEnvKey(name);
|
|
4581
|
-
const overrideRaw = env[overrideKeyName];
|
|
4582
|
-
if (overrideRaw !== undefined) {
|
|
4583
|
-
const override = overrideRaw.trim();
|
|
4584
|
-
if (!override) {
|
|
4585
|
-
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]);
|
|
4586
|
-
}
|
|
4587
|
-
assertUsableCredential(name, overrideKeyName, override);
|
|
4588
|
-
return sealCredential({
|
|
4589
|
-
apiKey: override,
|
|
4590
|
-
tier: "override",
|
|
4591
|
-
source: overrideKeyName,
|
|
4592
|
-
deliberate: true,
|
|
4593
|
-
deprecated: false,
|
|
4594
|
-
diskCandidates: diskPaths,
|
|
4595
|
-
warning: null
|
|
4596
|
-
});
|
|
4597
|
-
}
|
|
4598
|
-
const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
|
|
4599
|
-
if (profile) {
|
|
4600
|
-
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
|
|
4601
|
-
if (!SAFE_PROFILE.test(profile)) {
|
|
4602
|
-
throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
|
|
4603
|
-
}
|
|
4604
|
-
const paths = profileDiskSources(name, env, profile);
|
|
4605
|
-
for (const path of paths) {
|
|
4606
|
-
const value = readCredentialFile(path, apiKeyKeys);
|
|
4607
|
-
if (value) {
|
|
4608
|
-
assertUsableCredential(name, path, value);
|
|
4609
|
-
return sealCredential({
|
|
4610
|
-
apiKey: value,
|
|
4611
|
-
tier: "profile",
|
|
4612
|
-
source: path,
|
|
4613
|
-
deliberate: true,
|
|
4614
|
-
deprecated: false,
|
|
4615
|
-
diskCandidates: paths,
|
|
4616
|
-
warning: null
|
|
4617
|
-
});
|
|
4618
|
-
}
|
|
4619
|
-
}
|
|
4620
|
-
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);
|
|
4621
|
-
}
|
|
4622
|
-
const diskHits = diskPaths.map((path) => ({ path, value: readCredentialFile(path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
4623
|
-
if (diskHits.length > 0) {
|
|
4624
|
-
const winner = diskHits[0];
|
|
4625
|
-
assertUsableCredential(name, winner.path, winner.value);
|
|
4626
|
-
const divergentSources = [
|
|
4627
|
-
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.path),
|
|
4628
|
-
...(() => {
|
|
4629
|
-
const legacyHit = firstEnvValue(env, apiKeyKeys);
|
|
4630
|
-
return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
|
|
4631
|
-
})()
|
|
4632
|
-
];
|
|
4633
|
-
const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.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;
|
|
4634
|
-
return sealCredential({
|
|
4635
|
-
apiKey: winner.value,
|
|
4636
|
-
tier: "disk",
|
|
4637
|
-
source: winner.path,
|
|
4638
|
-
deliberate: false,
|
|
4639
|
-
deprecated: false,
|
|
4640
|
-
diskCandidates: diskPaths,
|
|
4641
|
-
warning
|
|
4642
|
-
});
|
|
4643
|
-
}
|
|
4644
|
-
const legacy = firstEnvValue(env, apiKeyKeys);
|
|
4645
|
-
if (legacy) {
|
|
4646
|
-
assertUsableCredential(name, legacy.key, legacy.value);
|
|
4647
|
-
const where = diskPaths.length > 0 ? `Put the current key in ${diskPaths[0]} \u2014 it is re-read on every call, so rotations take effect immediately.` : `This environment has no HOME, so no credential file could be consulted at all; the disk tier is ` + `unavailable here and this process will keep using the environment snapshot.`;
|
|
4648
|
-
const message = `[${name}] DEPRECATED: the API key came from ${legacy.key} in this process's environment. ` + `Environment variables are a snapshot taken when this process started, so a shell that started ` + `before a key rotation keeps using the old key until it exits. ${where}`;
|
|
4649
|
-
const sink = options.onDeprecation ?? defaultDeprecationSink;
|
|
4650
|
-
const notified = deprecationNotified();
|
|
4651
|
-
if (!notified.has(name)) {
|
|
4652
|
-
notified.add(name);
|
|
4653
|
-
sink(message);
|
|
4654
|
-
}
|
|
4655
|
-
return sealCredential({
|
|
4656
|
-
apiKey: legacy.value,
|
|
4657
|
-
tier: "legacy-env",
|
|
4658
|
-
source: legacy.key,
|
|
4659
|
-
deliberate: false,
|
|
4660
|
-
deprecated: true,
|
|
4661
|
-
diskCandidates: diskPaths,
|
|
4662
|
-
warning: message
|
|
4663
|
-
});
|
|
4664
|
-
}
|
|
4665
|
-
return null;
|
|
4666
|
-
}
|
|
4667
|
-
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", CredentialResolutionError, HASNA_STATE_DIR = ".hasna", FLEET_CREDENTIAL_DIR = "cloud", CONFIG_DIR = ".config", CONFIG_NAMESPACE = "hasna", MAX_CREDENTIAL_FILE_BYTES, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, INSPECT_CUSTOM, CREDENTIAL_SEAL, DEPRECATION_REGISTRY, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
|
|
4668
|
-
var init_transport = __esm(() => {
|
|
4669
|
-
CredentialResolutionError = class CredentialResolutionError extends Error {
|
|
4670
|
-
appName;
|
|
4671
|
-
attempted;
|
|
4672
|
-
constructor(appName, message, attempted) {
|
|
4673
|
-
super(message);
|
|
4674
|
-
this.name = "CredentialResolutionError";
|
|
4675
|
-
this.appName = appName;
|
|
4676
|
-
this.attempted = attempted;
|
|
4677
|
-
}
|
|
4678
|
-
};
|
|
4679
|
-
MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
4680
|
-
SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
4681
|
-
SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
4682
|
-
ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
4683
|
-
INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
4684
|
-
CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
4685
|
-
DEPRECATION_REGISTRY = Symbol.for("hasna:contracts:credentialDeprecationNotices");
|
|
4686
|
-
IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
4687
|
-
AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
4688
|
-
"host",
|
|
4689
|
-
":authority",
|
|
4690
|
-
"forwarded",
|
|
4691
|
-
"x-forwarded-host",
|
|
4692
|
-
"x-original-host"
|
|
4693
|
-
]);
|
|
4694
|
-
});
|
|
4695
|
-
|
|
4696
4536
|
// src/config.ts
|
|
4697
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
4698
4537
|
import { join as join4 } from "path";
|
|
4699
|
-
import { homedir as homedir3 } from "os";
|
|
4700
4538
|
function getHooksDataDir() {
|
|
4701
|
-
|
|
4702
|
-
if (explicit)
|
|
4703
|
-
return explicit;
|
|
4704
|
-
return join4(homedir3(), ".hasna", "hooks");
|
|
4539
|
+
return getEffectiveDataRoot();
|
|
4705
4540
|
}
|
|
4706
4541
|
function getCustomHooksDir() {
|
|
4707
4542
|
return join4(getHooksDataDir(), "hooks");
|
|
@@ -4712,41 +4547,8 @@ function getLockPath() {
|
|
|
4712
4547
|
return explicit;
|
|
4713
4548
|
return join4(getHooksDataDir(), "hooks.lock");
|
|
4714
4549
|
}
|
|
4715
|
-
function getConfigPath() {
|
|
4716
|
-
const explicit = process.env.HASNA_HOOKS_CONFIG_PATH ?? process.env.HOOKS_CONFIG_PATH;
|
|
4717
|
-
if (explicit)
|
|
4718
|
-
return explicit;
|
|
4719
|
-
return join4(getHooksDataDir(), "config.json");
|
|
4720
|
-
}
|
|
4721
|
-
function readConfig() {
|
|
4722
|
-
try {
|
|
4723
|
-
const path = getConfigPath();
|
|
4724
|
-
if (!existsSync3(path))
|
|
4725
|
-
return {};
|
|
4726
|
-
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
4727
|
-
} catch {
|
|
4728
|
-
return {};
|
|
4729
|
-
}
|
|
4730
|
-
}
|
|
4731
|
-
function writeConfig(config) {
|
|
4732
|
-
const path = getConfigPath();
|
|
4733
|
-
mkdirSync2(join4(path, ".."), { recursive: true });
|
|
4734
|
-
writeFileSync(path, JSON.stringify(config, null, 2) + `
|
|
4735
|
-
`, "utf-8");
|
|
4736
|
-
return path;
|
|
4737
|
-
}
|
|
4738
|
-
function resolveApiUrl() {
|
|
4739
|
-
const envUrl = process.env.HASNA_HOOKS_API_URL ?? process.env.HOOKS_API_URL ?? process.env.HASNA_HOOKS_REGISTRY_URL ?? process.env.HOOKS_REGISTRY_URL;
|
|
4740
|
-
if (envUrl)
|
|
4741
|
-
return envUrl.replace(/\/+$/, "");
|
|
4742
|
-
const cfg = readConfig();
|
|
4743
|
-
return cfg.api_url ? cfg.api_url.replace(/\/+$/, "") : undefined;
|
|
4744
|
-
}
|
|
4745
|
-
function resolveApiKey() {
|
|
4746
|
-
return resolveCredential("hooks", process.env)?.apiKey;
|
|
4747
|
-
}
|
|
4748
4550
|
var init_config = __esm(() => {
|
|
4749
|
-
|
|
4551
|
+
init_app_home();
|
|
4750
4552
|
});
|
|
4751
4553
|
|
|
4752
4554
|
// src/lib/semver.ts
|
|
@@ -4773,8 +4575,8 @@ __export(exports_manifest, {
|
|
|
4773
4575
|
ScriptContainmentError: () => ScriptContainmentError,
|
|
4774
4576
|
HOOK_NAME_RE: () => HOOK_NAME_RE
|
|
4775
4577
|
});
|
|
4776
|
-
import { existsSync as existsSync4, mkdirSync as
|
|
4777
|
-
import { basename, dirname, isAbsolute, join as join5, normalize, resolve, sep } from "path";
|
|
4578
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, realpathSync, writeFileSync } from "fs";
|
|
4579
|
+
import { basename, dirname, isAbsolute, join as join5, normalize, resolve as resolve2, sep } from "path";
|
|
4778
4580
|
function parseManifest(json) {
|
|
4779
4581
|
const parsed = JSON.parse(json);
|
|
4780
4582
|
return manifestSchema.parse(parsed);
|
|
@@ -4789,8 +4591,8 @@ function isWithin(child, parent) {
|
|
|
4789
4591
|
return child === parent || child.startsWith(parent + sep);
|
|
4790
4592
|
}
|
|
4791
4593
|
function assertContainedLexical(target, root, raw) {
|
|
4792
|
-
const lexical =
|
|
4793
|
-
const rootLexical =
|
|
4594
|
+
const lexical = resolve2(target);
|
|
4595
|
+
const rootLexical = resolve2(root);
|
|
4794
4596
|
if (!isWithin(lexical, rootLexical)) {
|
|
4795
4597
|
throw new ScriptContainmentError(target, root, raw);
|
|
4796
4598
|
}
|
|
@@ -4843,7 +4645,7 @@ function resolveScript(manifest, manifestDir) {
|
|
|
4843
4645
|
if (!existsSync4(resolved)) {
|
|
4844
4646
|
throw new Error(`manifest script file not found: ${resolved}`);
|
|
4845
4647
|
}
|
|
4846
|
-
return { path: rel, content:
|
|
4648
|
+
return { path: rel, content: readFileSync2(resolved, "utf-8") };
|
|
4847
4649
|
}
|
|
4848
4650
|
function customHookDir(name) {
|
|
4849
4651
|
return join5(getCustomHooksDir(), shortManifestName(name));
|
|
@@ -4854,7 +4656,7 @@ function readCustomManifest(name) {
|
|
|
4854
4656
|
if (!existsSync4(manifestPath))
|
|
4855
4657
|
return;
|
|
4856
4658
|
try {
|
|
4857
|
-
const manifest = parseManifest(
|
|
4659
|
+
const manifest = parseManifest(readFileSync2(manifestPath, "utf-8"));
|
|
4858
4660
|
const script = resolveScript(manifest, dir);
|
|
4859
4661
|
const scriptIsInline = isInlineScript(manifest);
|
|
4860
4662
|
return { manifest, scriptPath: join5(dir, script.path), scriptContent: script.content, scriptIsInline };
|
|
@@ -4883,13 +4685,13 @@ function writeCustomHook(name, manifest, scriptContent, scriptRelPath) {
|
|
|
4883
4685
|
}
|
|
4884
4686
|
const scriptPath = scriptRelPath.includes("/") ? join5(dir, scriptRelPath) : join5(dir, scriptRelPath);
|
|
4885
4687
|
assertContainedLexical(scriptPath, dir, scriptRelPath);
|
|
4886
|
-
|
|
4688
|
+
mkdirSync2(dir, { recursive: true });
|
|
4887
4689
|
assertContainedReal(dir, scriptPath, scriptRelPath);
|
|
4888
4690
|
const scriptDir = dirname(scriptPath);
|
|
4889
4691
|
if (scriptDir !== dir)
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4692
|
+
mkdirSync2(scriptDir, { recursive: true });
|
|
4693
|
+
writeFileSync(scriptPath, scriptContent, "utf-8");
|
|
4694
|
+
writeFileSync(join5(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + `
|
|
4893
4695
|
`, "utf-8");
|
|
4894
4696
|
return { dir, scriptPath };
|
|
4895
4697
|
}
|
|
@@ -4947,7 +4749,7 @@ __export(exports_store, {
|
|
|
4947
4749
|
LockFileError: () => LockFileError
|
|
4948
4750
|
});
|
|
4949
4751
|
import { createHash } from "crypto";
|
|
4950
|
-
import { existsSync as existsSync6, mkdirSync as
|
|
4752
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync } from "fs";
|
|
4951
4753
|
import { readFile } from "fs/promises";
|
|
4952
4754
|
import { randomBytes } from "crypto";
|
|
4953
4755
|
function sha256Of(input) {
|
|
@@ -5013,7 +4815,7 @@ function readLock() {
|
|
|
5013
4815
|
return { hooks: {} };
|
|
5014
4816
|
let raw;
|
|
5015
4817
|
try {
|
|
5016
|
-
raw =
|
|
4818
|
+
raw = readFileSync3(path, "utf-8");
|
|
5017
4819
|
} catch {
|
|
5018
4820
|
throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
|
|
5019
4821
|
}
|
|
@@ -5028,13 +4830,13 @@ function readLock() {
|
|
|
5028
4830
|
}
|
|
5029
4831
|
function writeLock(lock) {
|
|
5030
4832
|
const path = getLockPath();
|
|
5031
|
-
|
|
4833
|
+
mkdirSync3(path.substring(0, path.lastIndexOf("/")) || ".", { recursive: true });
|
|
5032
4834
|
const sorted = {};
|
|
5033
4835
|
for (const name of Object.keys(lock.hooks).sort()) {
|
|
5034
4836
|
sorted[name] = lock.hooks[name];
|
|
5035
4837
|
}
|
|
5036
4838
|
const tmp = `${path}.tmp-${randomBytes(8).toString("hex")}`;
|
|
5037
|
-
|
|
4839
|
+
writeFileSync2(tmp, JSON.stringify({ hooks: sorted }, null, 2) + `
|
|
5038
4840
|
`, "utf-8");
|
|
5039
4841
|
renameSync(tmp, path);
|
|
5040
4842
|
return path;
|
|
@@ -5110,7 +4912,7 @@ async function verifyScriptHash(name, scriptPath) {
|
|
|
5110
4912
|
return checkScriptHash(name, await sha256File(scriptPath));
|
|
5111
4913
|
}
|
|
5112
4914
|
function retrustHook(name, scriptPath, version, source) {
|
|
5113
|
-
const actual = sha256Of(
|
|
4915
|
+
const actual = sha256Of(readFileSync3(scriptPath));
|
|
5114
4916
|
const db = getDb();
|
|
5115
4917
|
upsertHookRecord(db, {
|
|
5116
4918
|
name,
|
|
@@ -5147,32 +4949,29 @@ __export(exports_profiles, {
|
|
|
5147
4949
|
deleteProfile: () => deleteProfile,
|
|
5148
4950
|
createProfile: () => createProfile
|
|
5149
4951
|
});
|
|
5150
|
-
import { existsSync as existsSync8, mkdirSync as
|
|
4952
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
|
|
5151
4953
|
import { join as join8 } from "path";
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
const
|
|
5155
|
-
const oldDir = join8(homedir5(), ".hooks", "profiles");
|
|
4954
|
+
function getProfilesDir() {
|
|
4955
|
+
const newDir = join8(getEffectiveDataRoot(), "profiles");
|
|
4956
|
+
const oldDir = join8(getHomeDir(), ".hooks", "profiles");
|
|
5156
4957
|
if (!existsSync8(newDir) && existsSync8(oldDir)) {
|
|
5157
|
-
|
|
4958
|
+
mkdirSync5(newDir, { recursive: true });
|
|
5158
4959
|
cpSync2(oldDir, newDir, { recursive: true });
|
|
5159
4960
|
}
|
|
5160
4961
|
return newDir;
|
|
5161
4962
|
}
|
|
5162
4963
|
function ensureProfilesDir() {
|
|
5163
|
-
|
|
5164
|
-
|
|
4964
|
+
const dir = getProfilesDir();
|
|
4965
|
+
if (!existsSync8(dir)) {
|
|
4966
|
+
mkdirSync5(dir, { recursive: true });
|
|
5165
4967
|
}
|
|
5166
4968
|
}
|
|
5167
4969
|
function profilePath(id) {
|
|
5168
|
-
return join8(
|
|
4970
|
+
return join8(getProfilesDir(), `${id}.json`);
|
|
5169
4971
|
}
|
|
5170
4972
|
function shortUuid() {
|
|
5171
4973
|
return crypto.randomUUID().slice(0, 8);
|
|
5172
4974
|
}
|
|
5173
|
-
function getProfilesDir() {
|
|
5174
|
-
return PROFILES_DIR;
|
|
5175
|
-
}
|
|
5176
4975
|
function createProfile(input) {
|
|
5177
4976
|
ensureProfilesDir();
|
|
5178
4977
|
const id = shortUuid();
|
|
@@ -5187,7 +4986,7 @@ function createProfile(input) {
|
|
|
5187
4986
|
if (input.name) {
|
|
5188
4987
|
profile.name = input.name;
|
|
5189
4988
|
}
|
|
5190
|
-
|
|
4989
|
+
writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
5191
4990
|
`);
|
|
5192
4991
|
return profile;
|
|
5193
4992
|
}
|
|
@@ -5196,20 +4995,21 @@ function getProfile(id) {
|
|
|
5196
4995
|
try {
|
|
5197
4996
|
if (!existsSync8(path))
|
|
5198
4997
|
return null;
|
|
5199
|
-
return JSON.parse(
|
|
4998
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
5200
4999
|
} catch {
|
|
5201
5000
|
return null;
|
|
5202
5001
|
}
|
|
5203
5002
|
}
|
|
5204
5003
|
function listProfiles() {
|
|
5205
|
-
|
|
5004
|
+
const dir = getProfilesDir();
|
|
5005
|
+
if (!existsSync8(dir))
|
|
5206
5006
|
return [];
|
|
5207
5007
|
try {
|
|
5208
|
-
const files =
|
|
5008
|
+
const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
|
|
5209
5009
|
const profiles = [];
|
|
5210
5010
|
for (const file of files) {
|
|
5211
5011
|
try {
|
|
5212
|
-
const content =
|
|
5012
|
+
const content = readFileSync5(join8(dir, file), "utf-8");
|
|
5213
5013
|
profiles.push(JSON.parse(content));
|
|
5214
5014
|
} catch {}
|
|
5215
5015
|
}
|
|
@@ -5226,7 +5026,7 @@ function updateProfile(id, data) {
|
|
|
5226
5026
|
profile.name = data.name;
|
|
5227
5027
|
if (data.preferences !== undefined)
|
|
5228
5028
|
profile.preferences = data.preferences;
|
|
5229
|
-
|
|
5029
|
+
writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
5230
5030
|
`);
|
|
5231
5031
|
return profile;
|
|
5232
5032
|
}
|
|
@@ -5235,7 +5035,7 @@ function deleteProfile(id) {
|
|
|
5235
5035
|
if (!existsSync8(path))
|
|
5236
5036
|
return false;
|
|
5237
5037
|
try {
|
|
5238
|
-
|
|
5038
|
+
rmSync(path);
|
|
5239
5039
|
return true;
|
|
5240
5040
|
} catch {
|
|
5241
5041
|
return false;
|
|
@@ -5246,7 +5046,7 @@ function touchProfile(id) {
|
|
|
5246
5046
|
if (!profile)
|
|
5247
5047
|
return;
|
|
5248
5048
|
profile.last_seen_at = new Date().toISOString();
|
|
5249
|
-
|
|
5049
|
+
writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
|
|
5250
5050
|
`);
|
|
5251
5051
|
}
|
|
5252
5052
|
function exportProfiles() {
|
|
@@ -5266,20 +5066,19 @@ function importProfiles(profiles) {
|
|
|
5266
5066
|
skipped++;
|
|
5267
5067
|
continue;
|
|
5268
5068
|
}
|
|
5269
|
-
|
|
5069
|
+
writeFileSync4(path, JSON.stringify(profile, null, 2) + `
|
|
5270
5070
|
`);
|
|
5271
5071
|
imported++;
|
|
5272
5072
|
}
|
|
5273
5073
|
return { imported, skipped };
|
|
5274
5074
|
}
|
|
5275
|
-
var PROFILES_DIR;
|
|
5276
5075
|
var init_profiles = __esm(() => {
|
|
5277
|
-
|
|
5076
|
+
init_app_home();
|
|
5278
5077
|
});
|
|
5279
5078
|
|
|
5280
5079
|
// src/lib/hook-env.ts
|
|
5281
|
-
import { dirname as dirname5, isAbsolute as
|
|
5282
|
-
import { existsSync as existsSync10, statSync
|
|
5080
|
+
import { dirname as dirname5, isAbsolute as isAbsolute3 } from "path";
|
|
5081
|
+
import { existsSync as existsSync10, statSync } from "fs";
|
|
5283
5082
|
function isDeniedEnvName(name) {
|
|
5284
5083
|
const upper = name.toUpperCase();
|
|
5285
5084
|
if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
|
|
@@ -5297,7 +5096,7 @@ function isBashFunctionEnvName(name) {
|
|
|
5297
5096
|
return name.startsWith("BASH_FUNC_");
|
|
5298
5097
|
}
|
|
5299
5098
|
function isUnsafePathEntry(entry, home) {
|
|
5300
|
-
if (entry === "" || !
|
|
5099
|
+
if (entry === "" || !isAbsolute3(entry))
|
|
5301
5100
|
return true;
|
|
5302
5101
|
if (home && (entry === home || entry.startsWith(home + "/")))
|
|
5303
5102
|
return true;
|
|
@@ -5306,7 +5105,7 @@ function isUnsafePathEntry(entry, home) {
|
|
|
5306
5105
|
if (entry === "/var/tmp" || entry.startsWith("/var/tmp/"))
|
|
5307
5106
|
return true;
|
|
5308
5107
|
try {
|
|
5309
|
-
if ((
|
|
5108
|
+
if ((statSync(entry).mode & 2) !== 0)
|
|
5310
5109
|
return true;
|
|
5311
5110
|
} catch {}
|
|
5312
5111
|
return false;
|
|
@@ -5438,8 +5237,8 @@ __export(exports_run, {
|
|
|
5438
5237
|
HookTimeoutError: () => HookTimeoutError
|
|
5439
5238
|
});
|
|
5440
5239
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
5441
|
-
import { rmSync as
|
|
5442
|
-
import { basename as basename2, dirname as dirname6, isAbsolute as
|
|
5240
|
+
import { rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
5241
|
+
import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute4, join as join12 } from "path";
|
|
5443
5242
|
function extensionOf(scriptPath) {
|
|
5444
5243
|
const base = basename2(scriptPath);
|
|
5445
5244
|
const dot = base.lastIndexOf(".");
|
|
@@ -5510,7 +5309,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
5510
5309
|
async function executeVerifiedScript(options) {
|
|
5511
5310
|
const scriptDir = dirname6(options.scriptPath);
|
|
5512
5311
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
5513
|
-
const tempPath =
|
|
5312
|
+
const tempPath = join12(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
5514
5313
|
let proc = null;
|
|
5515
5314
|
let timedOut = false;
|
|
5516
5315
|
const timeoutTimer = options.timeout ? setTimeout(() => {
|
|
@@ -5518,8 +5317,8 @@ async function executeVerifiedScript(options) {
|
|
|
5518
5317
|
killGroup(proc, { leaderAlive: proc !== null && proc.exitCode === null });
|
|
5519
5318
|
}, options.timeout) : null;
|
|
5520
5319
|
try {
|
|
5521
|
-
|
|
5522
|
-
proc = Bun.spawn([...interpreter.command,
|
|
5320
|
+
writeFileSync5(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
5321
|
+
proc = Bun.spawn([...interpreter.command, isAbsolute4(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
5523
5322
|
stdin: new Response(options.stdin),
|
|
5524
5323
|
stdout: "pipe",
|
|
5525
5324
|
stderr: "pipe",
|
|
@@ -5540,7 +5339,7 @@ async function executeVerifiedScript(options) {
|
|
|
5540
5339
|
if (timeoutTimer)
|
|
5541
5340
|
clearTimeout(timeoutTimer);
|
|
5542
5341
|
try {
|
|
5543
|
-
|
|
5342
|
+
rmSync3(tempPath, { force: true });
|
|
5544
5343
|
} catch {}
|
|
5545
5344
|
}
|
|
5546
5345
|
}
|
|
@@ -6125,6 +5924,7 @@ function coerceForSqlite(value) {
|
|
|
6125
5924
|
return String(value);
|
|
6126
5925
|
}
|
|
6127
5926
|
// src/lib/registry.ts
|
|
5927
|
+
init_app_home();
|
|
6128
5928
|
var HOOK_EVENTS = [
|
|
6129
5929
|
"PreToolUse",
|
|
6130
5930
|
"PostToolUse",
|
|
@@ -6462,7 +6262,7 @@ var HOOKS = [
|
|
|
6462
6262
|
{
|
|
6463
6263
|
name: "sessionlog",
|
|
6464
6264
|
displayName: "Session Log",
|
|
6465
|
-
description:
|
|
6265
|
+
description: `Logs every tool call to SQLite (${getReportedDbPath()})`,
|
|
6466
6266
|
version: "0.1.0",
|
|
6467
6267
|
category: "Observability",
|
|
6468
6268
|
event: "PostToolUse",
|
|
@@ -6472,7 +6272,7 @@ var HOOKS = [
|
|
|
6472
6272
|
{
|
|
6473
6273
|
name: "commandlog",
|
|
6474
6274
|
displayName: "Command Log",
|
|
6475
|
-
description:
|
|
6275
|
+
description: `Logs every Bash command to SQLite (${getReportedDbPath()})`,
|
|
6476
6276
|
version: "0.1.0",
|
|
6477
6277
|
category: "Observability",
|
|
6478
6278
|
event: "PostToolUse",
|
|
@@ -6492,7 +6292,7 @@ var HOOKS = [
|
|
|
6492
6292
|
{
|
|
6493
6293
|
name: "errornotify",
|
|
6494
6294
|
displayName: "Error Notify",
|
|
6495
|
-
description:
|
|
6295
|
+
description: `Detects tool failures and logs errors to SQLite (${getReportedDbPath()})`,
|
|
6496
6296
|
version: "0.1.0",
|
|
6497
6297
|
category: "Observability",
|
|
6498
6298
|
event: "PostToolUse",
|
|
@@ -6674,9 +6474,9 @@ function getHook(name) {
|
|
|
6674
6474
|
return HOOKS.find((h) => h.name === name);
|
|
6675
6475
|
}
|
|
6676
6476
|
// src/lib/installer.ts
|
|
6677
|
-
import { existsSync as existsSync7, mkdirSync as
|
|
6477
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, rmdirSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
6678
6478
|
import { join as join7, dirname as dirname3 } from "path";
|
|
6679
|
-
import { homedir as
|
|
6479
|
+
import { homedir as homedir3 } from "os";
|
|
6680
6480
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6681
6481
|
|
|
6682
6482
|
// src/lib/resolve.ts
|
|
@@ -6840,7 +6640,7 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
|
|
|
6840
6640
|
if (scope === "project") {
|
|
6841
6641
|
return target === "codewith" ? join7(process.cwd(), dir, "config.toml") : join7(process.cwd(), dir, "settings.json");
|
|
6842
6642
|
}
|
|
6843
|
-
return target === "codewith" ? join7(
|
|
6643
|
+
return target === "codewith" ? join7(homedir3(), dir, "config.toml") : join7(homedir3(), dir, "settings.json");
|
|
6844
6644
|
}
|
|
6845
6645
|
function getHookPath(name) {
|
|
6846
6646
|
const shortName = shortHookName(name);
|
|
@@ -6856,7 +6656,7 @@ function readSettings(scope = "global", target = "claude") {
|
|
|
6856
6656
|
const path = getSettingsPath(scope, target);
|
|
6857
6657
|
try {
|
|
6858
6658
|
if (existsSync7(path)) {
|
|
6859
|
-
return JSON.parse(
|
|
6659
|
+
return JSON.parse(readFileSync4(path, "utf-8"));
|
|
6860
6660
|
}
|
|
6861
6661
|
} catch (error) {
|
|
6862
6662
|
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -6867,9 +6667,9 @@ function writeSettings(settings, scope = "global", target = "claude") {
|
|
|
6867
6667
|
const path = getSettingsPath(scope, target);
|
|
6868
6668
|
const dir = dirname3(path);
|
|
6869
6669
|
if (!existsSync7(dir)) {
|
|
6870
|
-
|
|
6670
|
+
mkdirSync4(dir, { recursive: true });
|
|
6871
6671
|
}
|
|
6872
|
-
|
|
6672
|
+
writeFileSync3(path, JSON.stringify(settings, null, 2) + `
|
|
6873
6673
|
`);
|
|
6874
6674
|
}
|
|
6875
6675
|
function getTargetEventName(internalEvent, target) {
|
|
@@ -6955,7 +6755,7 @@ function buildCodewithTomlFragment(name, profile) {
|
|
|
6955
6755
|
function readCodewithConfig(scope, configPath) {
|
|
6956
6756
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
6957
6757
|
try {
|
|
6958
|
-
return existsSync7(path) ?
|
|
6758
|
+
return existsSync7(path) ? readFileSync4(path, "utf-8") : "";
|
|
6959
6759
|
} catch {
|
|
6960
6760
|
return "";
|
|
6961
6761
|
}
|
|
@@ -6964,12 +6764,12 @@ function appendCodewithFragment(fragment, scope, configPath) {
|
|
|
6964
6764
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
6965
6765
|
const dir = dirname3(path);
|
|
6966
6766
|
if (!existsSync7(dir))
|
|
6967
|
-
|
|
6968
|
-
const existing = existsSync7(path) ?
|
|
6767
|
+
mkdirSync4(dir, { recursive: true });
|
|
6768
|
+
const existing = existsSync7(path) ? readFileSync4(path, "utf-8") : "";
|
|
6969
6769
|
const sep2 = existing.trim() ? `
|
|
6970
6770
|
|
|
6971
6771
|
` : "";
|
|
6972
|
-
|
|
6772
|
+
writeFileSync3(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
|
|
6973
6773
|
return path;
|
|
6974
6774
|
}
|
|
6975
6775
|
function detectConflict(name, scope, target) {
|
|
@@ -7269,11 +7069,25 @@ function codewithHasHookEntry(name, scope = "global") {
|
|
|
7269
7069
|
if (!existsSync7(path))
|
|
7270
7070
|
return false;
|
|
7271
7071
|
try {
|
|
7272
|
-
return codewithHasHookEntryInText(
|
|
7072
|
+
return codewithHasHookEntryInText(readFileSync4(path, "utf-8"), name);
|
|
7273
7073
|
} catch {
|
|
7274
7074
|
return false;
|
|
7275
7075
|
}
|
|
7276
7076
|
}
|
|
7077
|
+
function removeStoreDirFailClosed(dir) {
|
|
7078
|
+
const entries = readdirSync3(dir, { withFileTypes: true });
|
|
7079
|
+
for (const entry of entries) {
|
|
7080
|
+
if (entry.isDirectory()) {
|
|
7081
|
+
removeStoreDirFailClosed(join7(dir, entry.name));
|
|
7082
|
+
}
|
|
7083
|
+
}
|
|
7084
|
+
for (const entry of entries) {
|
|
7085
|
+
if (!entry.isDirectory()) {
|
|
7086
|
+
unlinkSync(join7(dir, entry.name));
|
|
7087
|
+
}
|
|
7088
|
+
}
|
|
7089
|
+
rmdirSync(dir);
|
|
7090
|
+
}
|
|
7277
7091
|
function uninstallHook(name, scope = "global", target = "claude") {
|
|
7278
7092
|
const shortName = shortHookName(name);
|
|
7279
7093
|
const custom2 = readCustomManifest(shortName);
|
|
@@ -7301,10 +7115,10 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
7301
7115
|
if (target === "codewith" || target === "all") {
|
|
7302
7116
|
const codewithPath = getSettingsPath(scope, "codewith");
|
|
7303
7117
|
if (existsSync7(codewithPath)) {
|
|
7304
|
-
const before =
|
|
7118
|
+
const before = readFileSync4(codewithPath, "utf-8");
|
|
7305
7119
|
const after = removeCodewithHookEntry(before, shortName);
|
|
7306
7120
|
if (after.removed) {
|
|
7307
|
-
|
|
7121
|
+
writeFileSync3(codewithPath, after.text, "utf-8");
|
|
7308
7122
|
}
|
|
7309
7123
|
if (codewithHasHookEntryInText(after.removed ? after.text : before, shortName)) {
|
|
7310
7124
|
registrationsRemaining.push("codewith");
|
|
@@ -7313,7 +7127,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
7313
7127
|
}
|
|
7314
7128
|
if (custom2) {
|
|
7315
7129
|
try {
|
|
7316
|
-
|
|
7130
|
+
removeStoreDirFailClosed(customHookDir(shortName));
|
|
7317
7131
|
} catch {
|
|
7318
7132
|
return {
|
|
7319
7133
|
name: shortName,
|
|
@@ -7343,27 +7157,896 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
7343
7157
|
// src/index.ts
|
|
7344
7158
|
init_profiles();
|
|
7345
7159
|
init_config();
|
|
7346
|
-
|
|
7347
|
-
init_manifest();
|
|
7348
|
-
import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
|
|
7160
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
|
|
7349
7161
|
|
|
7350
|
-
//
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
import {
|
|
7354
|
-
import {
|
|
7355
|
-
import {
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
}
|
|
7366
|
-
|
|
7162
|
+
// ../contracts/dist/client/transport.js
|
|
7163
|
+
import { isIP } from "net";
|
|
7164
|
+
import { spawnSync } from "child_process";
|
|
7165
|
+
import { closeSync, fstatSync, openSync, readFileSync as readFileSync6 } from "fs";
|
|
7166
|
+
import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
|
|
7167
|
+
import { createRequire } from "module";
|
|
7168
|
+
import { hostname as osHostname } from "os";
|
|
7169
|
+
import { isAbsolute as isAbsolute2, join as join9 } from "path";
|
|
7170
|
+
function envToken(name) {
|
|
7171
|
+
return name.toUpperCase().replace(/-/g, "_");
|
|
7172
|
+
}
|
|
7173
|
+
function clientTransportEnvKeys(name) {
|
|
7174
|
+
const envSegment = envToken(name);
|
|
7175
|
+
return {
|
|
7176
|
+
apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
|
|
7177
|
+
apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
|
|
7178
|
+
};
|
|
7179
|
+
}
|
|
7180
|
+
function credentialOverrideEnvKey(name) {
|
|
7181
|
+
return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
|
|
7182
|
+
}
|
|
7183
|
+
var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
|
|
7184
|
+
function credentialPointerEnvKey(name) {
|
|
7185
|
+
return `HASNA_${envToken(name)}_API_KEY_REF`;
|
|
7186
|
+
}
|
|
7187
|
+
|
|
7188
|
+
class CredentialResolutionError extends Error {
|
|
7189
|
+
appName;
|
|
7190
|
+
attempted;
|
|
7191
|
+
constructor(appName, message, attempted) {
|
|
7192
|
+
super(message);
|
|
7193
|
+
this.name = "CredentialResolutionError";
|
|
7194
|
+
this.appName = appName;
|
|
7195
|
+
this.attempted = attempted;
|
|
7196
|
+
}
|
|
7197
|
+
}
|
|
7198
|
+
|
|
7199
|
+
class CredentialFileUnsafeError extends Error {
|
|
7200
|
+
path;
|
|
7201
|
+
constructor(path, reason) {
|
|
7202
|
+
super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
|
|
7203
|
+
this.name = "CredentialFileUnsafeError";
|
|
7204
|
+
this.path = path;
|
|
7205
|
+
}
|
|
7206
|
+
}
|
|
7207
|
+
var HASNA_HOME_ENV_KEY = "HASNA_HOME";
|
|
7208
|
+
var HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME";
|
|
7209
|
+
var KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION";
|
|
7210
|
+
var HASNA_HOME_DIR = ".hasna";
|
|
7211
|
+
var CONFIG_SUBDIR = "config";
|
|
7212
|
+
var CREDENTIALS_FILE = "credentials";
|
|
7213
|
+
var KEYCHAIN_SECURITY_BIN = "/usr/bin/security";
|
|
7214
|
+
var KEYCHAIN_SERVICE_PREFIX = "hasna.credentials";
|
|
7215
|
+
var KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44;
|
|
7216
|
+
var KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4;
|
|
7217
|
+
var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
|
|
7218
|
+
var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
7219
|
+
var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
7220
|
+
var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
|
|
7221
|
+
var VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
|
|
7222
|
+
function homeDir(env) {
|
|
7223
|
+
const home = env.HOME?.trim();
|
|
7224
|
+
return home ? home : null;
|
|
7225
|
+
}
|
|
7226
|
+
function absoluteOverride(env, key) {
|
|
7227
|
+
const value = env[key]?.trim();
|
|
7228
|
+
return value && isAbsolute2(value) ? value : null;
|
|
7229
|
+
}
|
|
7230
|
+
function hasnaHomeDir(env) {
|
|
7231
|
+
const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
|
|
7232
|
+
if (override)
|
|
7233
|
+
return override;
|
|
7234
|
+
const home = homeDir(env);
|
|
7235
|
+
return home ? join9(home, HASNA_HOME_DIR) : null;
|
|
7236
|
+
}
|
|
7237
|
+
function appConfigDir(name, env) {
|
|
7238
|
+
const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
|
|
7239
|
+
if (configRoot)
|
|
7240
|
+
return join9(configRoot, name);
|
|
7241
|
+
const root = hasnaHomeDir(env);
|
|
7242
|
+
return root ? join9(root, name, CONFIG_SUBDIR) : null;
|
|
7243
|
+
}
|
|
7244
|
+
function credentialDiskSourceList(name, env, profile = null) {
|
|
7245
|
+
if (!SAFE_APP_SLUG.test(name))
|
|
7246
|
+
return [];
|
|
7247
|
+
const directory = appConfigDir(name, env);
|
|
7248
|
+
if (!directory)
|
|
7249
|
+
return [];
|
|
7250
|
+
const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
|
|
7251
|
+
return [{ path: join9(directory, file), tier: "disk" }];
|
|
7252
|
+
}
|
|
7253
|
+
function credentialDiskSources(name, env) {
|
|
7254
|
+
return credentialDiskSourceList(name, env, null).map((s) => s.path);
|
|
7255
|
+
}
|
|
7256
|
+
function profileDiskSources(name, env, profile) {
|
|
7257
|
+
return credentialDiskSourceList(name, env, profile).map((s) => s.path);
|
|
7258
|
+
}
|
|
7259
|
+
function parseEnvFile(text) {
|
|
7260
|
+
const values = new Map;
|
|
7261
|
+
const unusable = new Set;
|
|
7262
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
7263
|
+
const line = rawLine.trim();
|
|
7264
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
7265
|
+
continue;
|
|
7266
|
+
const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
|
|
7267
|
+
const equals = withoutExport.indexOf("=");
|
|
7268
|
+
if (equals <= 0)
|
|
7269
|
+
continue;
|
|
7270
|
+
const key = withoutExport.slice(0, equals).trim();
|
|
7271
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
7272
|
+
continue;
|
|
7273
|
+
let value = withoutExport.slice(equals + 1).trim();
|
|
7274
|
+
const quote = value[0];
|
|
7275
|
+
if (quote === '"' || quote === "'") {
|
|
7276
|
+
if (value.length < 2 || !value.endsWith(quote)) {
|
|
7277
|
+
unusable.add(key);
|
|
7278
|
+
continue;
|
|
7279
|
+
}
|
|
7280
|
+
value = value.slice(1, -1);
|
|
7281
|
+
}
|
|
7282
|
+
if (value.trim().length === 0) {
|
|
7283
|
+
unusable.add(key);
|
|
7284
|
+
continue;
|
|
7285
|
+
}
|
|
7286
|
+
if (values.has(key) && values.get(key) !== value)
|
|
7287
|
+
unusable.add(key);
|
|
7288
|
+
values.set(key, value);
|
|
7289
|
+
}
|
|
7290
|
+
return { values, unusable };
|
|
7291
|
+
}
|
|
7292
|
+
function configFileModeAllowed(mode) {
|
|
7293
|
+
const permissions = mode & 4095;
|
|
7294
|
+
return permissions === 256 || permissions === 384;
|
|
7295
|
+
}
|
|
7296
|
+
function configFileReadsCoherent(before, after) {
|
|
7297
|
+
return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
|
|
7298
|
+
}
|
|
7299
|
+
function readAppConfigFile(path) {
|
|
7300
|
+
const unsafe = (reason) => {
|
|
7301
|
+
throw new CredentialFileUnsafeError(path, reason);
|
|
7302
|
+
};
|
|
7303
|
+
let fd = -1;
|
|
7304
|
+
try {
|
|
7305
|
+
fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
|
|
7306
|
+
} catch (error) {
|
|
7307
|
+
const code = error.code;
|
|
7308
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
7309
|
+
return null;
|
|
7310
|
+
if (code === "ELOOP")
|
|
7311
|
+
unsafe("the path is a symlink");
|
|
7312
|
+
unsafe(`the path could not be opened (${code ?? "unknown error"})`);
|
|
7313
|
+
}
|
|
7314
|
+
try {
|
|
7315
|
+
const before = fstatSync(fd);
|
|
7316
|
+
if (!before.isFile())
|
|
7317
|
+
unsafe("the path is not a regular file");
|
|
7318
|
+
if (!configFileModeAllowed(before.mode)) {
|
|
7319
|
+
unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
|
|
7320
|
+
}
|
|
7321
|
+
const uid = process.getuid?.() ?? process.geteuid?.();
|
|
7322
|
+
if (uid !== undefined && before.uid !== uid)
|
|
7323
|
+
unsafe("the file is not owned by the current user");
|
|
7324
|
+
if (before.size > MAX_CREDENTIAL_FILE_BYTES)
|
|
7325
|
+
unsafe("the file exceeds the size limit");
|
|
7326
|
+
const bytes = readFileSync6(fd);
|
|
7327
|
+
const after = fstatSync(fd);
|
|
7328
|
+
if (!configFileReadsCoherent(before, after)) {
|
|
7329
|
+
unsafe("the file changed while being read");
|
|
7330
|
+
}
|
|
7331
|
+
return parseEnvFile(bytes.toString("utf8"));
|
|
7332
|
+
} finally {
|
|
7333
|
+
if (fd !== -1)
|
|
7334
|
+
closeSync(fd);
|
|
7335
|
+
}
|
|
7336
|
+
}
|
|
7337
|
+
function readCredentialFile(path, apiKeyKeys) {
|
|
7338
|
+
const parsed = readAppConfigFile(path);
|
|
7339
|
+
if (!parsed)
|
|
7340
|
+
return null;
|
|
7341
|
+
for (const key of apiKeyKeys) {
|
|
7342
|
+
if (parsed.unusable.has(key)) {
|
|
7343
|
+
throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
|
|
7344
|
+
}
|
|
7345
|
+
}
|
|
7346
|
+
const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
7347
|
+
if (new Set(values).size > 1) {
|
|
7348
|
+
throw new CredentialFileUnsafeError(path, "credential aliases disagree");
|
|
7349
|
+
}
|
|
7350
|
+
return values[0] ?? null;
|
|
7351
|
+
}
|
|
7352
|
+
var CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
|
|
7353
|
+
function appConfigDiskValue(name, env, keys) {
|
|
7354
|
+
const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
|
|
7355
|
+
if (wanted.length === 0)
|
|
7356
|
+
return null;
|
|
7357
|
+
for (const path of credentialDiskSources(name, env)) {
|
|
7358
|
+
const parsed = readAppConfigFile(path);
|
|
7359
|
+
if (!parsed)
|
|
7360
|
+
continue;
|
|
7361
|
+
if (wanted.some((key) => parsed.unusable.has(key))) {
|
|
7362
|
+
return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
|
|
7363
|
+
}
|
|
7364
|
+
const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
|
|
7365
|
+
if (new Set(values).size > 1)
|
|
7366
|
+
throw new CredentialFileUnsafeError(path, "configuration aliases disagree");
|
|
7367
|
+
for (const key of wanted) {
|
|
7368
|
+
if (parsed.unusable.has(key))
|
|
7369
|
+
return { key, value: "", path, unusable: true };
|
|
7370
|
+
const value = parsed.values.get(key)?.trim();
|
|
7371
|
+
if (value)
|
|
7372
|
+
return { key, value, path };
|
|
7373
|
+
}
|
|
7374
|
+
}
|
|
7375
|
+
return null;
|
|
7376
|
+
}
|
|
7377
|
+
function assertUsableCredential(appName, source, value) {
|
|
7378
|
+
if (VAULT_POINTER_SHAPE.test(value)) {
|
|
7379
|
+
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]);
|
|
7380
|
+
}
|
|
7381
|
+
if (!ILLEGAL_IN_HEADER_VALUE.test(value))
|
|
7382
|
+
return;
|
|
7383
|
+
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]);
|
|
7384
|
+
}
|
|
7385
|
+
var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
|
|
7386
|
+
var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
|
|
7387
|
+
function sealCredential(fields) {
|
|
7388
|
+
const { apiKey } = fields;
|
|
7389
|
+
const visible = {
|
|
7390
|
+
tier: fields.tier,
|
|
7391
|
+
source: fields.source,
|
|
7392
|
+
deliberate: fields.deliberate,
|
|
7393
|
+
diskCandidates: Object.freeze([...fields.diskCandidates]),
|
|
7394
|
+
warning: fields.warning
|
|
7395
|
+
};
|
|
7396
|
+
const sealed = { ...visible };
|
|
7397
|
+
Object.defineProperty(sealed, "apiKey", {
|
|
7398
|
+
value: apiKey,
|
|
7399
|
+
enumerable: false,
|
|
7400
|
+
writable: false,
|
|
7401
|
+
configurable: false
|
|
7402
|
+
});
|
|
7403
|
+
if (fields.pointerVaultKey !== undefined) {
|
|
7404
|
+
Object.defineProperty(sealed, "pointerVaultKey", {
|
|
7405
|
+
value: fields.pointerVaultKey,
|
|
7406
|
+
enumerable: false,
|
|
7407
|
+
writable: false,
|
|
7408
|
+
configurable: false
|
|
7409
|
+
});
|
|
7410
|
+
}
|
|
7411
|
+
Object.defineProperty(sealed, INSPECT_CUSTOM, {
|
|
7412
|
+
value: () => ({ ...visible, apiKey: "[redacted]" }),
|
|
7413
|
+
enumerable: false,
|
|
7414
|
+
writable: false,
|
|
7415
|
+
configurable: false
|
|
7416
|
+
});
|
|
7417
|
+
Object.defineProperty(sealed, CREDENTIAL_SEAL, {
|
|
7418
|
+
value: true,
|
|
7419
|
+
enumerable: false,
|
|
7420
|
+
writable: false,
|
|
7421
|
+
configurable: false
|
|
7422
|
+
});
|
|
7423
|
+
return Object.freeze(sealed);
|
|
7424
|
+
}
|
|
7425
|
+
function firstEnvValue(env, keys) {
|
|
7426
|
+
for (const key of keys) {
|
|
7427
|
+
if (!Object.prototype.hasOwnProperty.call(env, key))
|
|
7428
|
+
continue;
|
|
7429
|
+
const value = env[key]?.trim();
|
|
7430
|
+
if (value)
|
|
7431
|
+
return { key, value };
|
|
7432
|
+
}
|
|
7433
|
+
return null;
|
|
7434
|
+
}
|
|
7435
|
+
var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
7436
|
+
function isAmbientEnvironment(env) {
|
|
7437
|
+
return env === process.env || env[AMBIENT_ENVIRONMENT] === true;
|
|
7438
|
+
}
|
|
7439
|
+
function defaultKeychainRunner(argv) {
|
|
7440
|
+
const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {
|
|
7441
|
+
encoding: "utf8",
|
|
7442
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
7443
|
+
timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
|
|
7444
|
+
});
|
|
7445
|
+
return {
|
|
7446
|
+
status: result.status,
|
|
7447
|
+
stdout: result.stdout ?? "",
|
|
7448
|
+
stderr: result.error ? result.error.message : result.stderr ?? ""
|
|
7449
|
+
};
|
|
7450
|
+
}
|
|
7451
|
+
function keychainTierEnabled(env, options) {
|
|
7452
|
+
if ((options.platform ?? process.platform) !== "darwin")
|
|
7453
|
+
return false;
|
|
7454
|
+
if (options.enabled !== undefined)
|
|
7455
|
+
return options.enabled;
|
|
7456
|
+
return options.run !== undefined || isAmbientEnvironment(env);
|
|
7457
|
+
}
|
|
7458
|
+
function keychainAccount(env, options) {
|
|
7459
|
+
const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
|
|
7460
|
+
if (station)
|
|
7461
|
+
return station;
|
|
7462
|
+
const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
|
|
7463
|
+
if (host)
|
|
7464
|
+
return host;
|
|
7465
|
+
const user = env.USER?.trim();
|
|
7466
|
+
return user || null;
|
|
7467
|
+
}
|
|
7468
|
+
function keychainFailureHint(text) {
|
|
7469
|
+
const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
|
|
7470
|
+
const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
|
|
7471
|
+
return clean ? `: ${clean}` : "";
|
|
7472
|
+
}
|
|
7473
|
+
function readKeychainItem(name, env, kind, options) {
|
|
7474
|
+
if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
|
|
7475
|
+
return null;
|
|
7476
|
+
const account = keychainAccount(env, options);
|
|
7477
|
+
if (!account)
|
|
7478
|
+
return null;
|
|
7479
|
+
const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
|
|
7480
|
+
const source = `keychain:${service}@${account}`;
|
|
7481
|
+
const run = options.run ?? defaultKeychainRunner;
|
|
7482
|
+
let result;
|
|
7483
|
+
try {
|
|
7484
|
+
result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
|
|
7485
|
+
} catch (error) {
|
|
7486
|
+
const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
|
|
7487
|
+
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]);
|
|
7488
|
+
}
|
|
7489
|
+
if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
|
|
7490
|
+
return null;
|
|
7491
|
+
if (result.status !== 0) {
|
|
7492
|
+
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]);
|
|
7493
|
+
}
|
|
7494
|
+
const value = result.stdout.trim();
|
|
7495
|
+
if (!value) {
|
|
7496
|
+
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]);
|
|
7497
|
+
}
|
|
7498
|
+
return { value, source };
|
|
7499
|
+
}
|
|
7500
|
+
function keychainConfigValue(name, env, options = {}) {
|
|
7501
|
+
return readKeychainItem(name, env, "api-url", options);
|
|
7502
|
+
}
|
|
7503
|
+
function snapshotClientEnvironment(name, env) {
|
|
7504
|
+
const keys = clientTransportEnvKeys(name);
|
|
7505
|
+
const ambient = isAmbientEnvironment(env);
|
|
7506
|
+
const snapshot = Object.create(null);
|
|
7507
|
+
for (const key of [
|
|
7508
|
+
...keys.apiUrlKeys,
|
|
7509
|
+
...keys.apiKeyKeys,
|
|
7510
|
+
credentialOverrideEnvKey(name),
|
|
7511
|
+
credentialPointerEnvKey(name),
|
|
7512
|
+
CREDENTIAL_PROFILE_ENV_KEY,
|
|
7513
|
+
"HOME",
|
|
7514
|
+
HASNA_HOME_ENV_KEY,
|
|
7515
|
+
HASNA_CONFIG_HOME_ENV_KEY,
|
|
7516
|
+
KEYCHAIN_STATION_ENV_KEY,
|
|
7517
|
+
"USER"
|
|
7518
|
+
]) {
|
|
7519
|
+
const descriptor = Object.getOwnPropertyDescriptor(env, key);
|
|
7520
|
+
if (!descriptor)
|
|
7521
|
+
continue;
|
|
7522
|
+
if (!("value" in descriptor)) {
|
|
7523
|
+
throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
|
|
7524
|
+
}
|
|
7525
|
+
if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
|
|
7526
|
+
throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
|
|
7527
|
+
}
|
|
7528
|
+
snapshot[key] = descriptor.value;
|
|
7529
|
+
}
|
|
7530
|
+
if (ambient) {
|
|
7531
|
+
Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT, {
|
|
7532
|
+
value: true,
|
|
7533
|
+
enumerable: false,
|
|
7534
|
+
writable: false,
|
|
7535
|
+
configurable: false
|
|
7536
|
+
});
|
|
7537
|
+
}
|
|
7538
|
+
return Object.freeze(snapshot);
|
|
7539
|
+
}
|
|
7540
|
+
function resolveCredential(name, env, options = {}) {
|
|
7541
|
+
env = snapshotClientEnvironment(name, env);
|
|
7542
|
+
const { apiKeyKeys } = clientTransportEnvKeys(name);
|
|
7543
|
+
const diskPaths = credentialDiskSources(name, env);
|
|
7544
|
+
if (options.apiKey !== undefined) {
|
|
7545
|
+
const explicitKey = options.apiKey.trim();
|
|
7546
|
+
if (!explicitKey) {
|
|
7547
|
+
throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
|
|
7548
|
+
}
|
|
7549
|
+
assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
|
|
7550
|
+
return sealCredential({
|
|
7551
|
+
apiKey: explicitKey,
|
|
7552
|
+
tier: "argument",
|
|
7553
|
+
source: "explicit apiKey argument",
|
|
7554
|
+
deliberate: true,
|
|
7555
|
+
diskCandidates: diskPaths,
|
|
7556
|
+
warning: null
|
|
7557
|
+
});
|
|
7558
|
+
}
|
|
7559
|
+
const overrideKeyName = credentialOverrideEnvKey(name);
|
|
7560
|
+
const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
|
|
7561
|
+
if (overrideRaw !== undefined) {
|
|
7562
|
+
const override = overrideRaw.trim();
|
|
7563
|
+
if (!override) {
|
|
7564
|
+
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]);
|
|
7565
|
+
}
|
|
7566
|
+
assertUsableCredential(name, overrideKeyName, override);
|
|
7567
|
+
return sealCredential({
|
|
7568
|
+
apiKey: override,
|
|
7569
|
+
tier: "override",
|
|
7570
|
+
source: overrideKeyName,
|
|
7571
|
+
deliberate: true,
|
|
7572
|
+
diskCandidates: diskPaths,
|
|
7573
|
+
warning: null
|
|
7574
|
+
});
|
|
7575
|
+
}
|
|
7576
|
+
const pointerKeyName = credentialPointerEnvKey(name);
|
|
7577
|
+
const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
|
|
7578
|
+
if (pointerRaw !== undefined) {
|
|
7579
|
+
const pointer = pointerRaw.trim();
|
|
7580
|
+
if (!pointer) {
|
|
7581
|
+
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]);
|
|
7582
|
+
}
|
|
7583
|
+
if (!VAULT_POINTER_SHAPE.test(pointer)) {
|
|
7584
|
+
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]);
|
|
7585
|
+
}
|
|
7586
|
+
return sealCredential({
|
|
7587
|
+
apiKey: "",
|
|
7588
|
+
pointerVaultKey: pointer,
|
|
7589
|
+
tier: "pointer",
|
|
7590
|
+
source: pointerKeyName,
|
|
7591
|
+
deliberate: true,
|
|
7592
|
+
diskCandidates: diskPaths,
|
|
7593
|
+
warning: null
|
|
7594
|
+
});
|
|
7595
|
+
}
|
|
7596
|
+
if (options.profile !== undefined && !options.profile.trim()) {
|
|
7597
|
+
throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
|
|
7598
|
+
}
|
|
7599
|
+
const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY) ? env[CREDENTIAL_PROFILE_ENV_KEY] : undefined;
|
|
7600
|
+
if (profileRaw !== undefined && !profileRaw.trim()) {
|
|
7601
|
+
throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY]);
|
|
7602
|
+
}
|
|
7603
|
+
const profile = options.profile?.trim() || profileRaw?.trim();
|
|
7604
|
+
if (profile) {
|
|
7605
|
+
const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
|
|
7606
|
+
if (!SAFE_PROFILE.test(profile)) {
|
|
7607
|
+
throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
|
|
7608
|
+
}
|
|
7609
|
+
const paths = profileDiskSources(name, env, profile);
|
|
7610
|
+
for (const path of paths) {
|
|
7611
|
+
const value = readCredentialFile(path, apiKeyKeys);
|
|
7612
|
+
if (value) {
|
|
7613
|
+
assertUsableCredential(name, path, value);
|
|
7614
|
+
return sealCredential({
|
|
7615
|
+
apiKey: value,
|
|
7616
|
+
tier: "profile",
|
|
7617
|
+
source: path,
|
|
7618
|
+
deliberate: true,
|
|
7619
|
+
diskCandidates: paths,
|
|
7620
|
+
warning: null
|
|
7621
|
+
});
|
|
7622
|
+
}
|
|
7623
|
+
}
|
|
7624
|
+
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);
|
|
7625
|
+
}
|
|
7626
|
+
const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
|
|
7627
|
+
const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
|
|
7628
|
+
if (blankEnv) {
|
|
7629
|
+
throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
|
|
7630
|
+
}
|
|
7631
|
+
if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
|
|
7632
|
+
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));
|
|
7633
|
+
}
|
|
7634
|
+
const envHit = firstEnvValue(env, apiKeyKeys);
|
|
7635
|
+
const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
|
|
7636
|
+
if (keychainHit) {
|
|
7637
|
+
assertUsableCredential(name, keychainHit.source, keychainHit.value);
|
|
7638
|
+
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;
|
|
7639
|
+
return sealCredential({
|
|
7640
|
+
apiKey: keychainHit.value,
|
|
7641
|
+
tier: "keychain",
|
|
7642
|
+
source: keychainHit.source,
|
|
7643
|
+
deliberate: false,
|
|
7644
|
+
diskCandidates: diskPaths,
|
|
7645
|
+
warning
|
|
7646
|
+
});
|
|
7647
|
+
}
|
|
7648
|
+
const diskSourceList = credentialDiskSourceList(name, env, null);
|
|
7649
|
+
const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
|
|
7650
|
+
if (diskHits.length > 0) {
|
|
7651
|
+
const winner = diskHits[0];
|
|
7652
|
+
assertUsableCredential(name, winner.src.path, winner.value);
|
|
7653
|
+
const divergentSources = [
|
|
7654
|
+
...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
|
|
7655
|
+
...envHit && envHit.value !== winner.value ? [envHit.key] : []
|
|
7656
|
+
];
|
|
7657
|
+
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;
|
|
7658
|
+
return sealCredential({
|
|
7659
|
+
apiKey: winner.value,
|
|
7660
|
+
tier: winner.src.tier,
|
|
7661
|
+
source: winner.src.path,
|
|
7662
|
+
deliberate: false,
|
|
7663
|
+
diskCandidates: diskPaths,
|
|
7664
|
+
warning
|
|
7665
|
+
});
|
|
7666
|
+
}
|
|
7667
|
+
if (envHit) {
|
|
7668
|
+
assertUsableCredential(name, envHit.key, envHit.value);
|
|
7669
|
+
return sealCredential({
|
|
7670
|
+
apiKey: envHit.value,
|
|
7671
|
+
tier: "env",
|
|
7672
|
+
source: envHit.key,
|
|
7673
|
+
deliberate: false,
|
|
7674
|
+
diskCandidates: diskPaths,
|
|
7675
|
+
warning: null
|
|
7676
|
+
});
|
|
7677
|
+
}
|
|
7678
|
+
return null;
|
|
7679
|
+
}
|
|
7680
|
+
var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
|
|
7681
|
+
var requireSecretsSdk = createRequire(import.meta.url);
|
|
7682
|
+
var DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com";
|
|
7683
|
+
var DEFAULT_AUTHORITY_SOURCE = "default";
|
|
7684
|
+
function defaultFleetGatewayBaseUrl(name) {
|
|
7685
|
+
return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
|
|
7686
|
+
}
|
|
7687
|
+
var ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
7688
|
+
var DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
7689
|
+
function isValidDnsDomain(value) {
|
|
7690
|
+
if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
|
|
7691
|
+
return false;
|
|
7692
|
+
}
|
|
7693
|
+
return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
|
|
7694
|
+
}
|
|
7695
|
+
function validateAppSlug(name) {
|
|
7696
|
+
if (name.length > 63 || !DNS_LABEL_PATTERN.test(name)) {
|
|
7697
|
+
throw new Error("App name must be one lowercase DNS label.");
|
|
7698
|
+
}
|
|
7699
|
+
return name;
|
|
7700
|
+
}
|
|
7701
|
+
function rawAuthority(value) {
|
|
7702
|
+
const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
|
|
7703
|
+
if (!match)
|
|
7704
|
+
throw new Error("API URL must be absolute.");
|
|
7705
|
+
const afterScheme = value.slice(match[0].length);
|
|
7706
|
+
const boundary = afterScheme.search(/[/?#]/);
|
|
7707
|
+
const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
|
|
7708
|
+
if (!authority)
|
|
7709
|
+
throw new Error("API URL must include a hostname.");
|
|
7710
|
+
return authority;
|
|
7711
|
+
}
|
|
7712
|
+
function assertCanonicalPort(port) {
|
|
7713
|
+
if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
|
|
7714
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
7715
|
+
}
|
|
7716
|
+
const numericPort = Number(port);
|
|
7717
|
+
if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
|
|
7718
|
+
throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
|
|
7719
|
+
}
|
|
7720
|
+
}
|
|
7721
|
+
function canonicalAuthorityHostname(authority) {
|
|
7722
|
+
let rawHostname;
|
|
7723
|
+
if (authority.startsWith("[")) {
|
|
7724
|
+
const closingBracket = authority.indexOf("]");
|
|
7725
|
+
if (closingBracket === -1) {
|
|
7726
|
+
throw new Error("API URL authority must contain a canonical hostname.");
|
|
7727
|
+
}
|
|
7728
|
+
rawHostname = authority.slice(0, closingBracket + 1);
|
|
7729
|
+
const portSuffix = authority.slice(closingBracket + 1);
|
|
7730
|
+
if (portSuffix) {
|
|
7731
|
+
if (!portSuffix.startsWith(":")) {
|
|
7732
|
+
throw new Error("API URL authority must contain a canonical hostname and port.");
|
|
7733
|
+
}
|
|
7734
|
+
assertCanonicalPort(portSuffix.slice(1));
|
|
7735
|
+
}
|
|
7736
|
+
if (isIP(rawHostname.slice(1, -1)) !== 6) {
|
|
7737
|
+
throw new Error("API URL authority must contain a canonical IPv6 literal.");
|
|
7738
|
+
}
|
|
7739
|
+
} else {
|
|
7740
|
+
const firstColon = authority.indexOf(":");
|
|
7741
|
+
const lastColon = authority.lastIndexOf(":");
|
|
7742
|
+
if (firstColon !== lastColon) {
|
|
7743
|
+
throw new Error("IPv6 API URL authorities must use brackets.");
|
|
7744
|
+
}
|
|
7745
|
+
if (lastColon !== -1) {
|
|
7746
|
+
const port = authority.slice(lastColon + 1);
|
|
7747
|
+
assertCanonicalPort(port);
|
|
7748
|
+
rawHostname = authority.slice(0, lastColon);
|
|
7749
|
+
} else {
|
|
7750
|
+
rawHostname = authority;
|
|
7751
|
+
}
|
|
7752
|
+
const ipVersion = isIP(rawHostname);
|
|
7753
|
+
const numericAddressParts = rawHostname.split(".");
|
|
7754
|
+
const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
|
|
7755
|
+
if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
|
|
7756
|
+
throw new Error("API URL authority must contain a canonical ASCII hostname.");
|
|
7757
|
+
}
|
|
7758
|
+
}
|
|
7759
|
+
return rawHostname.toLowerCase();
|
|
7760
|
+
}
|
|
7761
|
+
function isDeliberateLoopbackHttpAuthority(authority) {
|
|
7762
|
+
return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
|
|
7763
|
+
}
|
|
7764
|
+
function toV1BaseUrl(apiUrl) {
|
|
7765
|
+
if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
|
|
7766
|
+
throw new Error("API URL must not contain ASCII control characters.");
|
|
7767
|
+
}
|
|
7768
|
+
const input = apiUrl.trim();
|
|
7769
|
+
const authority = rawAuthority(input);
|
|
7770
|
+
if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
|
|
7771
|
+
throw new Error("API URL authority must be canonical ASCII without credentials.");
|
|
7772
|
+
}
|
|
7773
|
+
const canonicalHostname = canonicalAuthorityHostname(authority);
|
|
7774
|
+
const url = new URL(input);
|
|
7775
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
7776
|
+
throw new Error("API URL must use http or https.");
|
|
7777
|
+
}
|
|
7778
|
+
if (url.username || url.password) {
|
|
7779
|
+
throw new Error("API URL must not include credentials.");
|
|
7780
|
+
}
|
|
7781
|
+
if (!url.hostname || url.hostname.endsWith(".")) {
|
|
7782
|
+
throw new Error("API URL must include a canonical hostname.");
|
|
7783
|
+
}
|
|
7784
|
+
if (url.hostname.toLowerCase() !== canonicalHostname) {
|
|
7785
|
+
throw new Error("API URL authority must not rely on parser hostname normalization.");
|
|
7786
|
+
}
|
|
7787
|
+
if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
|
|
7788
|
+
throw new Error("API URL must not use IDN or punycode hostnames.");
|
|
7789
|
+
}
|
|
7790
|
+
if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
|
|
7791
|
+
throw new Error("API URL may use http only for an exact loopback authority.");
|
|
7792
|
+
}
|
|
7793
|
+
if (url.search || url.hash) {
|
|
7794
|
+
throw new Error("API URL must not include a query string or fragment.");
|
|
7795
|
+
}
|
|
7796
|
+
let path = url.pathname.replace(/\/+$/, "");
|
|
7797
|
+
if (path.endsWith("/v1"))
|
|
7798
|
+
path = path.slice(0, -"/v1".length);
|
|
7799
|
+
url.pathname = `${path}/v1`;
|
|
7800
|
+
return url.toString().replace(/\/+$/, "");
|
|
7801
|
+
}
|
|
7802
|
+
class ClientTransportConfigurationError extends Error {
|
|
7803
|
+
appName;
|
|
7804
|
+
sources;
|
|
7805
|
+
constructor(appName, message, sources = []) {
|
|
7806
|
+
super(message);
|
|
7807
|
+
this.name = "ClientTransportConfigurationError";
|
|
7808
|
+
this.appName = appName;
|
|
7809
|
+
this.sources = Object.freeze([...sources]);
|
|
7810
|
+
}
|
|
7811
|
+
}
|
|
7812
|
+
function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
|
|
7813
|
+
env = snapshotClientEnvironment(name, env);
|
|
7814
|
+
const keys = clientTransportEnvKeys(name);
|
|
7815
|
+
const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
|
|
7816
|
+
const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
|
|
7817
|
+
if (blankUrl) {
|
|
7818
|
+
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]);
|
|
7819
|
+
}
|
|
7820
|
+
const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
|
|
7821
|
+
if (controlledUrl) {
|
|
7822
|
+
throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
|
|
7823
|
+
}
|
|
7824
|
+
const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
|
|
7825
|
+
if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
|
|
7826
|
+
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));
|
|
7827
|
+
}
|
|
7828
|
+
const envUrlHit = usableUrlEntries[0] ?? null;
|
|
7829
|
+
const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
|
|
7830
|
+
const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
|
|
7831
|
+
if (diskConfigUrlHit?.unusable) {
|
|
7832
|
+
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]);
|
|
7833
|
+
}
|
|
7834
|
+
const urlCandidates = [
|
|
7835
|
+
...envUrlHit ? [envUrlHit] : [],
|
|
7836
|
+
...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
|
|
7837
|
+
...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
|
|
7838
|
+
];
|
|
7839
|
+
const configuredUrl = urlCandidates[0] ?? null;
|
|
7840
|
+
const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
|
|
7841
|
+
if (configuredUrl && divergentUrls.length > 0) {
|
|
7842
|
+
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));
|
|
7843
|
+
}
|
|
7844
|
+
const warnings = [];
|
|
7845
|
+
if (configuredUrl && !envUrlHit) {
|
|
7846
|
+
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.`);
|
|
7847
|
+
}
|
|
7848
|
+
const credential = resolveCredential(name, env, options.credentials);
|
|
7849
|
+
if (!credential) {
|
|
7850
|
+
const diskHint = credentialDiskSourcesForMessage(name, env);
|
|
7851
|
+
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`;
|
|
7852
|
+
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.`);
|
|
7853
|
+
throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
|
|
7854
|
+
}
|
|
7855
|
+
if (credential.warning)
|
|
7856
|
+
warnings.push(credential.warning);
|
|
7857
|
+
let urlHit;
|
|
7858
|
+
if (configuredUrl) {
|
|
7859
|
+
urlHit = configuredUrl;
|
|
7860
|
+
} else {
|
|
7861
|
+
try {
|
|
7862
|
+
urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
|
|
7863
|
+
} catch (error) {
|
|
7864
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7865
|
+
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]]);
|
|
7866
|
+
}
|
|
7867
|
+
}
|
|
7868
|
+
const apiUrlSource = urlHit.key;
|
|
7869
|
+
let baseUrl;
|
|
7870
|
+
try {
|
|
7871
|
+
baseUrl = toV1BaseUrl(urlHit.value);
|
|
7872
|
+
} catch (error) {
|
|
7873
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7874
|
+
throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
|
|
7875
|
+
}
|
|
7876
|
+
return {
|
|
7877
|
+
resolution: {
|
|
7878
|
+
transport: "http",
|
|
7879
|
+
transportSource: urlHit.key,
|
|
7880
|
+
baseUrl,
|
|
7881
|
+
apiUrlSource,
|
|
7882
|
+
apiKeyPresent: true,
|
|
7883
|
+
apiKeySource: credential.source,
|
|
7884
|
+
apiKeyTier: credential.tier,
|
|
7885
|
+
misconfigured: false,
|
|
7886
|
+
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
7887
|
+
},
|
|
7888
|
+
credential
|
|
7889
|
+
};
|
|
7890
|
+
}
|
|
7891
|
+
function resolveClientTransport(name, env = process.env, options = {}) {
|
|
7892
|
+
return resolveClientTransportSnapshot(name, env, options).resolution;
|
|
7893
|
+
}
|
|
7894
|
+
function credentialDiskSourcesForMessage(name, env) {
|
|
7895
|
+
const paths = credentialDiskSources(name, env);
|
|
7896
|
+
return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
|
|
7897
|
+
}
|
|
7898
|
+
var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
7899
|
+
var AUTHORITY_OVERRIDE_HEADERS = new Set([
|
|
7900
|
+
"host",
|
|
7901
|
+
":authority",
|
|
7902
|
+
"forwarded",
|
|
7903
|
+
"x-forwarded-host",
|
|
7904
|
+
"x-original-host"
|
|
7905
|
+
]);
|
|
7906
|
+
|
|
7907
|
+
// src/lib/transport.ts
|
|
7908
|
+
init_config();
|
|
7909
|
+
|
|
7910
|
+
// src/lib/local-opt-in.ts
|
|
7911
|
+
var HOOKS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_HOOKS_LOCAL", "HOOKS_LOCAL"];
|
|
7912
|
+
function isHooksLocalOptIn(env = process.env) {
|
|
7913
|
+
return HOOKS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env[key] ?? "").trim() !== "");
|
|
7914
|
+
}
|
|
7915
|
+
function hooksAuthorityEnvKeys() {
|
|
7916
|
+
const keys = clientTransportEnvKeys("hooks");
|
|
7917
|
+
return [
|
|
7918
|
+
...keys.apiUrlKeys,
|
|
7919
|
+
...keys.apiKeyKeys,
|
|
7920
|
+
credentialOverrideEnvKey("hooks"),
|
|
7921
|
+
credentialPointerEnvKey("hooks"),
|
|
7922
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
7923
|
+
];
|
|
7924
|
+
}
|
|
7925
|
+
function hasHooksEnvAuthorityIntent(env = process.env) {
|
|
7926
|
+
return hooksAuthorityEnvKeys().some((key) => (env[key] ?? "").trim() !== "");
|
|
7927
|
+
}
|
|
7928
|
+
function selectsHooksLocalStore(env = process.env) {
|
|
7929
|
+
return !hasHooksEnvAuthorityIntent(env) && isHooksLocalOptIn(env);
|
|
7930
|
+
}
|
|
7931
|
+
function hooksResolverEnv(env) {
|
|
7932
|
+
const blanks = hooksAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
|
|
7933
|
+
if (blanks.length === 0)
|
|
7934
|
+
return env;
|
|
7935
|
+
const next = { ...env };
|
|
7936
|
+
for (const key of blanks)
|
|
7937
|
+
delete next[key];
|
|
7938
|
+
return next;
|
|
7939
|
+
}
|
|
7940
|
+
var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
7941
|
+
function isAmbientHooksEnv(env) {
|
|
7942
|
+
if (typeof process !== "undefined" && env === process.env)
|
|
7943
|
+
return true;
|
|
7944
|
+
return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
|
|
7945
|
+
}
|
|
7946
|
+
function hooksResolverInputs(env, credentials = {}) {
|
|
7947
|
+
const normalised = hooksResolverEnv(env);
|
|
7948
|
+
if (normalised === env)
|
|
7949
|
+
return { env: normalised, credentials };
|
|
7950
|
+
const keychain = { ...credentials.keychain };
|
|
7951
|
+
if (keychain.enabled === undefined && keychain.run === undefined) {
|
|
7952
|
+
keychain.enabled = isAmbientHooksEnv(env);
|
|
7953
|
+
}
|
|
7954
|
+
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
7955
|
+
}
|
|
7956
|
+
|
|
7957
|
+
// src/lib/transport.ts
|
|
7958
|
+
var localNoticePrinted = false;
|
|
7959
|
+
function announceLocal(notice, reason) {
|
|
7960
|
+
if (localNoticePrinted)
|
|
7961
|
+
return;
|
|
7962
|
+
localNoticePrinted = true;
|
|
7963
|
+
const line = `hooks: LOCAL mode \u2014 ${reason}; using the bundled registry and the local store at ` + `${getHooksDataDir()}, not a remote registry. Set HASNA_HOOKS_API_KEY (or the Keychain item ` + `hasna.credentials.hooks.api-key / ~/.hasna/hooks/config/credentials) to go remote.`;
|
|
7964
|
+
if (notice)
|
|
7965
|
+
notice(line);
|
|
7966
|
+
else if (typeof process !== "undefined")
|
|
7967
|
+
process.stderr.write(`${line}
|
|
7968
|
+
`);
|
|
7969
|
+
}
|
|
7970
|
+
function hooksRegistryOrigin(v1BaseUrl) {
|
|
7971
|
+
return v1BaseUrl.replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
7972
|
+
}
|
|
7973
|
+
function rethrowHooksAuthorityFailure(error) {
|
|
7974
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7975
|
+
const name = error instanceof Error ? error.name : "";
|
|
7976
|
+
if (name === "CredentialResolutionError" || name === "CredentialFileUnsafeError") {
|
|
7977
|
+
throw new Error(`REMOTE_API_CREDENTIAL_INVALID: ${message} There is no local fallback: ` + "local mode is opt-in only (HASNA_HOOKS_LOCAL=1) and is disabled by default \u2014 failing closed", { cause: error });
|
|
7978
|
+
}
|
|
7979
|
+
if (/no API key could be resolved/.test(message)) {
|
|
7980
|
+
if (/is not set and no API key could be resolved/.test(message)) {
|
|
7981
|
+
throw new Error("REMOTE_API_CONFIG_MISSING: no hooks registry credential resolved from the Keychain item " + `hasna.credentials.hooks.api-key, ~/.hasna/hooks/config/credentials, or HASNA_HOOKS_API_KEY. ${message} ` + "There is no local fallback: local mode is opt-in only (HASNA_HOOKS_LOCAL=1, alias HOOKS_LOCAL=1) " + "and is disabled by default \u2014 failing closed instead of serving the local store", { cause: error });
|
|
7982
|
+
}
|
|
7983
|
+
throw new Error("REMOTE_API_KEY_MISSING: the remote hooks registry requires HASNA_HOOKS_API_KEY, the Keychain item " + `hasna.credentials.hooks.api-key, or ~/.hasna/hooks/config/credentials. ${message} ` + "There is no local fallback: local mode is opt-in only (HASNA_HOOKS_LOCAL=1) and is disabled by default \u2014 failing closed", { cause: error });
|
|
7984
|
+
}
|
|
7985
|
+
throw new Error(`REMOTE_API_URL_INVALID: ${message} local mode fallback is disabled`, { cause: error });
|
|
7986
|
+
}
|
|
7987
|
+
function resolveHooksTransport(env = process.env, options = {}) {
|
|
7988
|
+
if (selectsHooksLocalStore(env)) {
|
|
7989
|
+
announceLocal(options.notice, "HASNA_HOOKS_LOCAL is set and nothing configures a registry authority");
|
|
7990
|
+
return { mode: "local", source: "local-opt-in", authority: null };
|
|
7991
|
+
}
|
|
7992
|
+
const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
|
|
7993
|
+
const tier1 = options.credentials?.apiKey;
|
|
7994
|
+
const credentialOptions = tier1 !== undefined ? { ...credentials, apiKey: tier1 } : credentials;
|
|
7995
|
+
let resolution;
|
|
7996
|
+
let credential;
|
|
7997
|
+
try {
|
|
7998
|
+
credential = resolveCredential("hooks", resolverEnv, credentialOptions);
|
|
7999
|
+
resolution = resolveClientTransport("hooks", resolverEnv, {
|
|
8000
|
+
credentials: credential ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
|
|
8001
|
+
});
|
|
8002
|
+
} catch (error) {
|
|
8003
|
+
rethrowHooksAuthorityFailure(error);
|
|
8004
|
+
}
|
|
8005
|
+
if (!credential) {
|
|
8006
|
+
throw new Error("REMOTE_API_CONFIG_MISSING: no hooks registry credential resolved; " + "local mode fallback is disabled");
|
|
8007
|
+
}
|
|
8008
|
+
return {
|
|
8009
|
+
mode: "remote",
|
|
8010
|
+
source: `${credential.source}+${resolution.apiUrlSource ?? "default"}`,
|
|
8011
|
+
authority: {
|
|
8012
|
+
origin: hooksRegistryOrigin(resolution.baseUrl),
|
|
8013
|
+
apiKey: credential.apiKey,
|
|
8014
|
+
apiUrlSource: resolution.apiUrlSource,
|
|
8015
|
+
apiKeySource: credential.source,
|
|
8016
|
+
apiKeyTier: credential.tier,
|
|
8017
|
+
v1BaseUrl: resolution.baseUrl,
|
|
8018
|
+
warning: resolution.warning
|
|
8019
|
+
}
|
|
8020
|
+
};
|
|
8021
|
+
}
|
|
8022
|
+
function resolveHooksServePublishKey(env = process.env, options = {}) {
|
|
8023
|
+
const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
|
|
8024
|
+
const tier1 = options.credentials?.apiKey;
|
|
8025
|
+
const credentialOptions = tier1 !== undefined ? { ...credentials, apiKey: tier1 } : credentials;
|
|
8026
|
+
return resolveCredential("hooks", resolverEnv, credentialOptions)?.apiKey;
|
|
8027
|
+
}
|
|
8028
|
+
|
|
8029
|
+
// src/index.ts
|
|
8030
|
+
init_store();
|
|
8031
|
+
init_manifest();
|
|
8032
|
+
|
|
8033
|
+
// src/lib/custom-install.ts
|
|
8034
|
+
init_config();
|
|
8035
|
+
init_manifest();
|
|
8036
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync2 } from "fs";
|
|
8037
|
+
import { join as join10 } from "path";
|
|
8038
|
+
import { tmpdir } from "os";
|
|
8039
|
+
function manifestAt(dir) {
|
|
8040
|
+
const path = join10(dir, "manifest.json");
|
|
8041
|
+
if (!existsSync9(path))
|
|
8042
|
+
return;
|
|
8043
|
+
try {
|
|
8044
|
+
return parseManifest(readFileSync7(path, "utf-8"));
|
|
8045
|
+
} catch {
|
|
8046
|
+
return;
|
|
8047
|
+
}
|
|
8048
|
+
}
|
|
8049
|
+
function copyManifestDir(srcDir, source, kind) {
|
|
7367
8050
|
const manifest = manifestAt(srcDir);
|
|
7368
8051
|
if (!manifest) {
|
|
7369
8052
|
throw new Error(`No valid manifest.json found at ${srcDir}`);
|
|
@@ -7377,8 +8060,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
|
|
|
7377
8060
|
return { name, version: manifest.version, kind, source, dir, scriptPath };
|
|
7378
8061
|
}
|
|
7379
8062
|
async function cloneToTemp(gitUrl) {
|
|
7380
|
-
const tmp = mkdtempSync(
|
|
7381
|
-
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl,
|
|
8063
|
+
const tmp = mkdtempSync(join10(tmpdir(), "hooks-install-"));
|
|
8064
|
+
const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join10(tmp, "repo")], {
|
|
7382
8065
|
stdout: "pipe",
|
|
7383
8066
|
stderr: "pipe"
|
|
7384
8067
|
});
|
|
@@ -7388,10 +8071,10 @@ async function cloneToTemp(gitUrl) {
|
|
|
7388
8071
|
proc.exited
|
|
7389
8072
|
]);
|
|
7390
8073
|
if (exitCode !== 0) {
|
|
7391
|
-
|
|
8074
|
+
rmSync2(tmp, { recursive: true, force: true });
|
|
7392
8075
|
throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
|
|
7393
8076
|
}
|
|
7394
|
-
return
|
|
8077
|
+
return join10(tmp, "repo");
|
|
7395
8078
|
}
|
|
7396
8079
|
function isGitUrl(value) {
|
|
7397
8080
|
return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
|
|
@@ -7409,7 +8092,7 @@ async function installCustomSource(source) {
|
|
|
7409
8092
|
try {
|
|
7410
8093
|
return copyManifestDir(repoDir, source, "git");
|
|
7411
8094
|
} finally {
|
|
7412
|
-
|
|
8095
|
+
rmSync2(repoDir, { recursive: true, force: true });
|
|
7413
8096
|
}
|
|
7414
8097
|
}
|
|
7415
8098
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
@@ -7419,7 +8102,7 @@ async function installCustomSource(source) {
|
|
|
7419
8102
|
throw new Error(`No such local path: ${source}`);
|
|
7420
8103
|
}
|
|
7421
8104
|
if (source.endsWith("manifest.json")) {
|
|
7422
|
-
const manifest = parseManifest(
|
|
8105
|
+
const manifest = parseManifest(readFileSync7(source, "utf-8"));
|
|
7423
8106
|
return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
|
|
7424
8107
|
}
|
|
7425
8108
|
return copyManifestDir(source, source, "local");
|
|
@@ -7450,8 +8133,7 @@ async function installManifestUrl(url) {
|
|
|
7450
8133
|
}
|
|
7451
8134
|
// src/lib/sync.ts
|
|
7452
8135
|
import { createHash as createHash2 } from "crypto";
|
|
7453
|
-
import { readFileSync as
|
|
7454
|
-
init_config();
|
|
8136
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
7455
8137
|
init_db();
|
|
7456
8138
|
init_store();
|
|
7457
8139
|
init_manifest();
|
|
@@ -7459,7 +8141,7 @@ function sha256OfText(text) {
|
|
|
7459
8141
|
return createHash2("sha256").update(text).digest("hex");
|
|
7460
8142
|
}
|
|
7461
8143
|
function sha256OfPath(path) {
|
|
7462
|
-
return createHash2("sha256").update(
|
|
8144
|
+
return createHash2("sha256").update(readFileSync8(path)).digest("hex");
|
|
7463
8145
|
}
|
|
7464
8146
|
function collectBundledCatalog() {
|
|
7465
8147
|
const out = [];
|
|
@@ -7475,9 +8157,8 @@ function collectBundledCatalog() {
|
|
|
7475
8157
|
}
|
|
7476
8158
|
return out;
|
|
7477
8159
|
}
|
|
7478
|
-
async function fetchJson(base, path, opts) {
|
|
8160
|
+
async function fetchJson(base, path, apiKey, opts) {
|
|
7479
8161
|
const headers = { accept: "application/json" };
|
|
7480
|
-
const apiKey = resolveApiKey();
|
|
7481
8162
|
if (apiKey)
|
|
7482
8163
|
headers["x-api-key"] = apiKey;
|
|
7483
8164
|
const res = await fetch(`${base}${path}`, {
|
|
@@ -7486,7 +8167,7 @@ async function fetchJson(base, path, opts) {
|
|
|
7486
8167
|
signal: AbortSignal.timeout(15000)
|
|
7487
8168
|
});
|
|
7488
8169
|
if (res.status === 401) {
|
|
7489
|
-
throw new Error("registry requires API key \u2014 set HASNA_HOOKS_API_KEY or
|
|
8170
|
+
throw new Error("registry requires API key \u2014 set HASNA_HOOKS_API_KEY, the Keychain item hasna.credentials.hooks.api-key, " + "or ~/.hasna/hooks/config/credentials (resolved per call, never stored)");
|
|
7490
8171
|
}
|
|
7491
8172
|
if (!res.ok) {
|
|
7492
8173
|
throw new Error(`GET ${path} failed with status ${res.status}`);
|
|
@@ -7497,9 +8178,9 @@ async function fetchJson(base, path, opts) {
|
|
|
7497
8178
|
}
|
|
7498
8179
|
return body;
|
|
7499
8180
|
}
|
|
7500
|
-
async function fetchRemoteState(apiUrl) {
|
|
7501
|
-
const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
|
|
7502
|
-
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
8181
|
+
async function fetchRemoteState(apiUrl, apiKey) {
|
|
8182
|
+
const catalog = await fetchJson(apiUrl, "/api/v1/catalog", apiKey);
|
|
8183
|
+
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock", apiKey);
|
|
7503
8184
|
if (!catalog.hooks || !Array.isArray(catalog.hooks)) {
|
|
7504
8185
|
throw new Error("remote catalog is malformed: missing hooks array");
|
|
7505
8186
|
}
|
|
@@ -7542,20 +8223,25 @@ function computeDiff(catalog, remoteLock) {
|
|
|
7542
8223
|
return diff;
|
|
7543
8224
|
}
|
|
7544
8225
|
async function planSync(options = {}) {
|
|
7545
|
-
const
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
8226
|
+
const transport = resolveHooksTransport(options.env ?? process.env, {
|
|
8227
|
+
credentials: options.credentials
|
|
8228
|
+
});
|
|
8229
|
+
if (transport.mode === "remote" && transport.authority) {
|
|
8230
|
+
const { origin, apiKey } = transport.authority;
|
|
8231
|
+
const { catalog, remoteLock } = await fetchRemoteState(origin, apiKey);
|
|
8232
|
+
return { apiUrl: origin, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
|
|
7549
8233
|
}
|
|
7550
8234
|
const diff = computeDiff(collectBundledCatalog(), null);
|
|
7551
8235
|
return { apiUrl: null, dryRun: options.dryRun ?? false, diff };
|
|
7552
8236
|
}
|
|
7553
8237
|
async function syncHooks(options = {}) {
|
|
7554
|
-
const
|
|
7555
|
-
|
|
8238
|
+
const transport = resolveHooksTransport(options.env ?? process.env, {
|
|
8239
|
+
credentials: options.credentials
|
|
8240
|
+
});
|
|
8241
|
+
const plan = await planSync(options);
|
|
7556
8242
|
if (options.dryRun)
|
|
7557
8243
|
return { ...plan, dryRun: true };
|
|
7558
|
-
if (!
|
|
8244
|
+
if (transport.mode !== "remote" || !transport.authority) {
|
|
7559
8245
|
const db = getDb();
|
|
7560
8246
|
for (const entry of collectBundledCatalog()) {
|
|
7561
8247
|
setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
|
|
@@ -7570,19 +8256,20 @@ async function syncHooks(options = {}) {
|
|
|
7570
8256
|
}
|
|
7571
8257
|
return plan;
|
|
7572
8258
|
}
|
|
7573
|
-
const {
|
|
8259
|
+
const { origin, apiKey } = transport.authority;
|
|
8260
|
+
const { catalog, remoteLock } = await fetchRemoteState(origin, apiKey);
|
|
7574
8261
|
const diff = computeDiff(catalog, remoteLock);
|
|
7575
|
-
const staged = await stageSyncArtifacts(
|
|
7576
|
-
await commitSyncArtifacts(staged,
|
|
7577
|
-
return { apiUrl, dryRun: false, diff };
|
|
8262
|
+
const staged = await stageSyncArtifacts(origin, apiKey, [...diff.added, ...diff.updated], remoteLock);
|
|
8263
|
+
await commitSyncArtifacts(staged, origin, remoteLock);
|
|
8264
|
+
return { apiUrl: origin, dryRun: false, diff };
|
|
7578
8265
|
}
|
|
7579
|
-
async function stageSyncArtifacts(apiUrl, names, remoteLock) {
|
|
8266
|
+
async function stageSyncArtifacts(apiUrl, apiKey, names, remoteLock) {
|
|
7580
8267
|
const staged = [];
|
|
7581
8268
|
for (const name of names) {
|
|
7582
8269
|
const entry = remoteLock.hooks[name];
|
|
7583
8270
|
if (!entry)
|
|
7584
8271
|
continue;
|
|
7585
|
-
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(entry.version)}
|
|
8272
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(entry.version)}`, apiKey);
|
|
7586
8273
|
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
7587
8274
|
throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
|
|
7588
8275
|
}
|
|
@@ -7649,8 +8336,8 @@ async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
|
|
|
7649
8336
|
throw err;
|
|
7650
8337
|
}
|
|
7651
8338
|
}
|
|
7652
|
-
async function fetchPinnedHook(name, version, apiUrl) {
|
|
7653
|
-
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
|
|
8339
|
+
async function fetchPinnedHook(name, version, apiUrl, apiKey) {
|
|
8340
|
+
const remoteLock = await fetchJson(apiUrl, "/api/v1/lock", apiKey);
|
|
7654
8341
|
const entry = remoteLock.hooks?.[name];
|
|
7655
8342
|
if (!entry) {
|
|
7656
8343
|
throw new Error(`Hook '${name}' is not in the remote registry lock`);
|
|
@@ -7659,7 +8346,7 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
7659
8346
|
if (!isLatest && !(entry.versions ?? []).includes(version)) {
|
|
7660
8347
|
throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${[entry.version, ...entry.versions ?? []].join(", ")})`);
|
|
7661
8348
|
}
|
|
7662
|
-
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, { withHeader: "_headerSha" });
|
|
8349
|
+
const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, apiKey, { withHeader: "_headerSha" });
|
|
7663
8350
|
if (!artifact.manifest || typeof artifact.script !== "string") {
|
|
7664
8351
|
throw new Error(`artifact for '${name}@${version}' is malformed`);
|
|
7665
8352
|
}
|
|
@@ -7713,9 +8400,8 @@ async function fetchPinnedHook(name, version, apiUrl) {
|
|
|
7713
8400
|
// src/serve.ts
|
|
7714
8401
|
init_manifest();
|
|
7715
8402
|
init_store();
|
|
7716
|
-
|
|
7717
|
-
import {
|
|
7718
|
-
import { dirname as dirname4, join as join10 } from "path";
|
|
8403
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
8404
|
+
import { dirname as dirname4, join as join11 } from "path";
|
|
7719
8405
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7720
8406
|
|
|
7721
8407
|
// src/lib/secure-compare.ts
|
|
@@ -7876,7 +8562,7 @@ var openApiDocument = {
|
|
|
7876
8562
|
apiKey: {
|
|
7877
8563
|
type: "http",
|
|
7878
8564
|
scheme: "bearer",
|
|
7879
|
-
description: "The registry API key (HASNA_HOOKS_API_KEY /
|
|
8565
|
+
description: "The registry API key \u2014 resolved by the client through the @hasna/contracts chain (HASNA_HOOKS_API_KEY, the Keychain item hasna.credentials.hooks.api-key, or ~/.hasna/hooks/config/credentials); the server compares the inbound bearer/x-api-key value."
|
|
7880
8566
|
}
|
|
7881
8567
|
}
|
|
7882
8568
|
}
|
|
@@ -7889,8 +8575,8 @@ var SERVE_HOST = "127.0.0.1";
|
|
|
7889
8575
|
var SERVE_SERVICE_NAME = "hooks-registry";
|
|
7890
8576
|
function packageVersion() {
|
|
7891
8577
|
try {
|
|
7892
|
-
const pkgPath =
|
|
7893
|
-
return JSON.parse(
|
|
8578
|
+
const pkgPath = join11(dirname4(fileURLToPath3(import.meta.url)), "..", "package.json");
|
|
8579
|
+
return JSON.parse(readFileSync9(pkgPath, "utf-8")).version || "0.0.0";
|
|
7894
8580
|
} catch {
|
|
7895
8581
|
return "0.0.0";
|
|
7896
8582
|
}
|
|
@@ -8038,15 +8724,20 @@ function handleServeRequest(req, apiKey) {
|
|
|
8038
8724
|
}
|
|
8039
8725
|
return Promise.resolve(json({ error: "Not Found" }, 404));
|
|
8040
8726
|
}
|
|
8727
|
+
function resolveServeOptions(options) {
|
|
8728
|
+
const envPort = process.env.PORT?.trim();
|
|
8729
|
+
const envHost = process.env.HOST?.trim();
|
|
8730
|
+
const port = options.port ?? (envPort && /^[0-9]+$/.test(envPort) ? parseInt(envPort, 10) : DEFAULT_SERVE_PORT);
|
|
8731
|
+
const host = options.host ?? (envHost && envHost.length > 0 ? envHost : SERVE_HOST);
|
|
8732
|
+
return { port, host };
|
|
8733
|
+
}
|
|
8041
8734
|
function startServeServer(options) {
|
|
8042
|
-
const port = options
|
|
8043
|
-
const host = options.host ?? SERVE_HOST;
|
|
8044
|
-
const apiKey = resolveApiKey();
|
|
8735
|
+
const { port, host } = resolveServeOptions(options);
|
|
8045
8736
|
const server = Bun.serve({
|
|
8046
8737
|
hostname: host,
|
|
8047
8738
|
port,
|
|
8048
8739
|
async fetch(req) {
|
|
8049
|
-
return handleServeRequest(req,
|
|
8740
|
+
return handleServeRequest(req, resolveHooksServePublishKey());
|
|
8050
8741
|
}
|
|
8051
8742
|
});
|
|
8052
8743
|
console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
|
|
@@ -8155,7 +8846,7 @@ async function runHook(name, input, options = {}) {
|
|
|
8155
8846
|
if (!script)
|
|
8156
8847
|
throw new Error(`Hook script not found: ${name}`);
|
|
8157
8848
|
const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
8158
|
-
const content =
|
|
8849
|
+
const content = readFileSync10(script);
|
|
8159
8850
|
const check = checkScriptHash2(name, sha256Of2(content));
|
|
8160
8851
|
if (!check.ok) {
|
|
8161
8852
|
throw new Error(`Hook '${name}' script changed since it was trusted (sha256 ${check.expected} != ${check.actual}). Run 'hooks trust ${name}' to trust the new content.`);
|
|
@@ -8246,7 +8937,6 @@ async function runHook(name, input, options = {}) {
|
|
|
8246
8937
|
export {
|
|
8247
8938
|
writeLock,
|
|
8248
8939
|
writeCustomHook,
|
|
8249
|
-
writeConfig,
|
|
8250
8940
|
verifyScriptHash,
|
|
8251
8941
|
upsertHookRecord,
|
|
8252
8942
|
updateProfile,
|
|
@@ -8267,11 +8957,11 @@ export {
|
|
|
8267
8957
|
resolveTables,
|
|
8268
8958
|
resolveScriptPath,
|
|
8269
8959
|
resolveScript,
|
|
8960
|
+
resolveHooksTransport,
|
|
8961
|
+
resolveHooksServePublishKey,
|
|
8270
8962
|
resolveHookMeta,
|
|
8271
8963
|
resolveHookDir,
|
|
8272
8964
|
resolveHook,
|
|
8273
|
-
resolveApiUrl,
|
|
8274
|
-
resolveApiKey,
|
|
8275
8965
|
removeProjectHook,
|
|
8276
8966
|
removePinnedHook,
|
|
8277
8967
|
removeHookRecord,
|
|
@@ -8279,7 +8969,6 @@ export {
|
|
|
8279
8969
|
removeHook,
|
|
8280
8970
|
readLock,
|
|
8281
8971
|
readCustomManifest,
|
|
8282
|
-
readConfig,
|
|
8283
8972
|
provisionCloudflareResources,
|
|
8284
8973
|
planSync,
|
|
8285
8974
|
pinInstalledHook,
|
|
@@ -8297,6 +8986,7 @@ export {
|
|
|
8297
8986
|
installHook,
|
|
8298
8987
|
installCustomSource,
|
|
8299
8988
|
importProfiles,
|
|
8989
|
+
hooksRegistryOrigin,
|
|
8300
8990
|
hookExists,
|
|
8301
8991
|
handleServeRequest,
|
|
8302
8992
|
getSyncMetaAll,
|
|
@@ -8321,7 +9011,6 @@ export {
|
|
|
8321
9011
|
getHookEvents,
|
|
8322
9012
|
getHook,
|
|
8323
9013
|
getCustomHooksDir,
|
|
8324
|
-
getConfigPath,
|
|
8325
9014
|
fetchPinnedHook,
|
|
8326
9015
|
exportProfiles,
|
|
8327
9016
|
deleteProfile,
|