@hasna/hooks 0.6.5 → 0.6.6

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/bin/serve.js CHANGED
@@ -527,7 +527,7 @@ function getHook(name) {
527
527
  import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "fs";
528
528
  import { basename, dirname, isAbsolute, join as join2, normalize, resolve, sep } from "path";
529
529
 
530
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
530
+ // node_modules/zod/v3/external.js
531
531
  var exports_external = {};
532
532
  __export(exports_external, {
533
533
  void: () => voidType,
@@ -639,7 +639,7 @@ __export(exports_external, {
639
639
  BRAND: () => BRAND
640
640
  });
641
641
 
642
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
642
+ // node_modules/zod/v3/helpers/util.js
643
643
  var util;
644
644
  (function(util2) {
645
645
  util2.assertEqual = (_) => {};
@@ -770,7 +770,7 @@ var getParsedType = (data) => {
770
770
  }
771
771
  };
772
772
 
773
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/ZodError.js
773
+ // node_modules/zod/v3/ZodError.js
774
774
  var ZodIssueCode = util.arrayToEnum([
775
775
  "invalid_type",
776
776
  "invalid_literal",
@@ -889,7 +889,7 @@ ZodError.create = (issues) => {
889
889
  return error;
890
890
  };
891
891
 
892
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/locales/en.js
892
+ // node_modules/zod/v3/locales/en.js
893
893
  var errorMap = (issue, _ctx) => {
894
894
  let message;
895
895
  switch (issue.code) {
@@ -992,7 +992,7 @@ var errorMap = (issue, _ctx) => {
992
992
  };
993
993
  var en_default = errorMap;
994
994
 
995
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/errors.js
995
+ // node_modules/zod/v3/errors.js
996
996
  var overrideErrorMap = en_default;
997
997
  function setErrorMap(map) {
998
998
  overrideErrorMap = map;
@@ -1000,7 +1000,7 @@ function setErrorMap(map) {
1000
1000
  function getErrorMap() {
1001
1001
  return overrideErrorMap;
1002
1002
  }
1003
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
1003
+ // node_modules/zod/v3/helpers/parseUtil.js
1004
1004
  var makeIssue = (params) => {
1005
1005
  const { data, path, errorMaps, issueData } = params;
1006
1006
  const fullPath = [...path, ...issueData.path || []];
@@ -1106,14 +1106,14 @@ var isAborted = (x) => x.status === "aborted";
1106
1106
  var isDirty = (x) => x.status === "dirty";
1107
1107
  var isValid = (x) => x.status === "valid";
1108
1108
  var isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
1109
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
1109
+ // node_modules/zod/v3/helpers/errorUtil.js
1110
1110
  var errorUtil;
1111
1111
  (function(errorUtil2) {
1112
1112
  errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {};
1113
1113
  errorUtil2.toString = (message) => typeof message === "string" ? message : message?.message;
1114
1114
  })(errorUtil || (errorUtil = {}));
1115
1115
 
1116
- // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/types.js
1116
+ // node_modules/zod/v3/types.js
1117
1117
  class ParseInputLazyPath {
1118
1118
  constructor(parent, value, path, key) {
1119
1119
  this._cachedPath = [];
@@ -4518,20 +4518,22 @@ function getLockPath() {
4518
4518
  return explicit;
4519
4519
  return join(getHooksDataDir(), "hooks.lock");
4520
4520
  }
4521
- function resolveApiKey(flagKey) {
4522
- if (flagKey && flagKey.length > 0)
4523
- return flagKey;
4521
+ function resolveApiKey() {
4524
4522
  return process.env.HASNA_HOOKS_API_KEY ?? process.env.HOOKS_API_KEY;
4525
4523
  }
4526
4524
 
4525
+ // src/lib/semver.ts
4526
+ var SEMVER_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
4527
+
4527
4528
  // src/lib/manifest.ts
4528
4529
  var HOOK_NAME_RE = /^[\w-]+$/;
4529
4530
  var manifestSchema = exports_external.object({
4530
4531
  name: exports_external.string().regex(HOOK_NAME_RE, "hook name must be /^[\\w-]+$/"),
4531
- version: exports_external.string().regex(/^\d+\.\d+\.\d+/, "version must be semver (e.g. 1.2.3)"),
4532
+ version: exports_external.string().regex(SEMVER_PATTERN, "version must be full semver (e.g. 1.2.3 or 1.2.3-beta.1)"),
4532
4533
  description: exports_external.string().optional(),
4533
4534
  events: exports_external.array(exports_external.string()).min(1, "at least one event is required"),
4534
4535
  script: exports_external.string().min(1, "script must name a relative path or inline content"),
4536
+ script_kind: exports_external.enum(["inline", "file"]).optional(),
4535
4537
  args: exports_external.array(exports_external.string()).optional(),
4536
4538
  timeout_ms: exports_external.number().int().positive().optional()
4537
4539
  });
@@ -4584,16 +4586,25 @@ function assertContained(target, root, raw) {
4584
4586
  assertContainedReal(root, lexical, raw);
4585
4587
  return lexical;
4586
4588
  }
4587
- function resolveScript(manifest, manifestDir) {
4588
- if (manifest.script.includes(`
4589
- `)) {
4589
+ function isInlineScript(manifest) {
4590
+ return manifest.script_kind === "inline" || manifest.script_kind === undefined && manifest.script.includes(`
4591
+ `);
4592
+ }
4593
+ function scriptRelFor(manifest) {
4594
+ if (isInlineScript(manifest)) {
4590
4595
  const ext = manifest.script.trim().startsWith("#!") ? ".sh" : ".ts";
4591
- return { path: `script${ext}`, content: manifest.script };
4596
+ return `script${ext}`;
4592
4597
  }
4593
4598
  const rel = normalize(manifest.script);
4594
4599
  if (isAbsolute(rel)) {
4595
4600
  throw new Error(`manifest script must be relative or inline, got absolute path '${manifest.script}'`);
4596
4601
  }
4602
+ return rel;
4603
+ }
4604
+ function resolveScript(manifest, manifestDir) {
4605
+ if (isInlineScript(manifest))
4606
+ return { path: scriptRelFor(manifest), content: manifest.script };
4607
+ const rel = scriptRelFor(manifest);
4597
4608
  const resolved = assertContained(join2(manifestDir, rel), manifestDir, manifest.script);
4598
4609
  if (!existsSync(resolved)) {
4599
4610
  throw new Error(`manifest script file not found: ${resolved}`);
@@ -4611,8 +4622,8 @@ function readCustomManifest(name) {
4611
4622
  try {
4612
4623
  const manifest = parseManifest(readFileSync(manifestPath, "utf-8"));
4613
4624
  const script = resolveScript(manifest, dir);
4614
- return { manifest, scriptPath: join2(dir, script.path), scriptContent: script.content, scriptIsInline: manifest.script.includes(`
4615
- `) };
4625
+ const scriptIsInline = isInlineScript(manifest);
4626
+ return { manifest, scriptPath: join2(dir, script.path), scriptContent: script.content, scriptIsInline };
4616
4627
  } catch (err) {
4617
4628
  if (err instanceof ScriptContainmentError)
4618
4629
  throw err;
@@ -4641,7 +4652,7 @@ function readdirSafe(dir) {
4641
4652
 
4642
4653
  // src/lib/store.ts
4643
4654
  import { createHash } from "crypto";
4644
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
4655
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync } from "fs";
4645
4656
  import { readFile } from "fs/promises";
4646
4657
 
4647
4658
  // src/db/index.ts
@@ -5002,6 +5013,7 @@ function getDb() {
5002
5013
  }
5003
5014
 
5004
5015
  // src/lib/store.ts
5016
+ import { randomBytes } from "crypto";
5005
5017
  function sha256Of(input) {
5006
5018
  return createHash("sha256").update(input).digest("hex");
5007
5019
  }
@@ -5037,16 +5049,45 @@ function upsertHookRecord(db, record) {
5037
5049
  record.last_verified_at ?? null
5038
5050
  ]);
5039
5051
  }
5052
+ class LockFileError extends Error {
5053
+ constructor(message) {
5054
+ super(message);
5055
+ this.name = "LockFileError";
5056
+ }
5057
+ }
5058
+ function assertLockShape(parsed, path) {
5059
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
5060
+ throw new LockFileError(`hooks.lock (${path}) is malformed: expected a JSON object with a "hooks" map. ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it. ` + `Nothing was trusted or rewritten \u2014 hooks are left in the refuse-to-run state until the lock is repaired.`);
5061
+ }
5062
+ const hooks = parsed.hooks;
5063
+ if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) {
5064
+ throw new LockFileError(`hooks.lock (${path}) is malformed: missing or invalid "hooks" map. ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it. ` + `Nothing was trusted or rewritten \u2014 hooks are left in the refuse-to-run state until the lock is repaired.`);
5065
+ }
5066
+ for (const [name, entry] of Object.entries(hooks)) {
5067
+ const e = entry;
5068
+ if (e === null || typeof e !== "object" || typeof e.version !== "string" || typeof e.sha256 !== "string") {
5069
+ throw new LockFileError(`hooks.lock (${path}) is malformed: entry for '${name}' is not a valid pin (version and sha256 strings required). ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it.`);
5070
+ }
5071
+ }
5072
+ }
5040
5073
  function readLock() {
5074
+ const path = getLockPath();
5075
+ if (!existsSync4(path))
5076
+ return { hooks: {} };
5077
+ let raw;
5041
5078
  try {
5042
- const path = getLockPath();
5043
- if (!existsSync4(path))
5044
- return { hooks: {} };
5045
- const parsed = JSON.parse(readFileSync3(path, "utf-8"));
5046
- return { hooks: parsed.hooks ?? {} };
5079
+ raw = readFileSync3(path, "utf-8");
5047
5080
  } catch {
5048
- return { hooks: {} };
5081
+ throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
5082
+ }
5083
+ let parsed;
5084
+ try {
5085
+ parsed = JSON.parse(raw);
5086
+ } catch {
5087
+ throw new LockFileError(`hooks.lock (${path}) is malformed: not valid JSON (${String((raw ?? "").slice(0, 80))}). ` + `Repair: fix the file by hand, or if it is unrecoverable move it aside and run 'hooks sync' to rebuild it. ` + `Nothing was trusted or rewritten \u2014 hooks are left in the refuse-to-run state until the lock is repaired.`);
5049
5088
  }
5089
+ assertLockShape(parsed, path);
5090
+ return parsed;
5050
5091
  }
5051
5092
  function writeLock(lock) {
5052
5093
  const path = getLockPath();
@@ -5055,8 +5096,10 @@ function writeLock(lock) {
5055
5096
  for (const name of Object.keys(lock.hooks).sort()) {
5056
5097
  sorted[name] = lock.hooks[name];
5057
5098
  }
5058
- writeFileSync2(path, JSON.stringify({ hooks: sorted }, null, 2) + `
5099
+ const tmp = `${path}.tmp-${randomBytes(8).toString("hex")}`;
5100
+ writeFileSync2(tmp, JSON.stringify({ hooks: sorted }, null, 2) + `
5059
5101
  `, "utf-8");
5102
+ renameSync(tmp, path);
5060
5103
  return path;
5061
5104
  }
5062
5105
  function setPinnedHook(name, entry) {
@@ -5185,24 +5228,29 @@ async function buildCatalog() {
5185
5228
  const scriptPath = resolveHook(meta.name)?.scriptPath;
5186
5229
  if (!scriptPath)
5187
5230
  continue;
5231
+ const sha = await sha256File(scriptPath);
5188
5232
  byName.set(meta.name, {
5189
5233
  name: meta.name,
5190
5234
  version: meta.version,
5191
- sha256: await sha256File(scriptPath),
5235
+ sha256: sha,
5192
5236
  events: meta.events && meta.events.length > 0 ? meta.events : [meta.event],
5193
5237
  description: meta.description,
5194
- source: "bundled"
5238
+ source: "bundled",
5239
+ versions: [meta.version]
5195
5240
  });
5196
5241
  }
5197
5242
  for (const custom2 of listCustomHooks()) {
5198
5243
  const name = shortManifestName(custom2.manifest.name);
5244
+ const sha = await sha256File(custom2.scriptPath);
5245
+ const existing = byName.get(name);
5199
5246
  byName.set(name, {
5200
5247
  name,
5201
5248
  version: custom2.manifest.version,
5202
- sha256: await sha256File(custom2.scriptPath),
5249
+ sha256: sha,
5203
5250
  events: custom2.manifest.events,
5204
5251
  description: custom2.manifest.description ?? "Custom hook",
5205
- source: byName.has(name) ? "custom-overrides-bundled" : "custom"
5252
+ source: existing ? "custom-overrides-bundled" : "custom",
5253
+ versions: existing ? [...new Set([...existing.versions, custom2.manifest.version])] : [custom2.manifest.version]
5206
5254
  });
5207
5255
  }
5208
5256
  return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
@@ -5220,6 +5268,7 @@ async function artifactFor(name, version) {
5220
5268
  description: resolved.description,
5221
5269
  events: resolved.events,
5222
5270
  script: custom2 ? custom2.manifest.script : `src/hook.ts`,
5271
+ script_kind: custom2?.manifest.script_kind,
5223
5272
  args: custom2?.manifest.args,
5224
5273
  timeout_ms: custom2?.manifest.timeout_ms
5225
5274
  };
@@ -5249,11 +5298,32 @@ function handleServeRequest(req, apiKey) {
5249
5298
  return buildCatalog().then((catalog) => json({ hooks: catalog }));
5250
5299
  }
5251
5300
  if (url.pathname === "/api/v1/lock" && req.method === "GET") {
5252
- return Promise.resolve(json(readLock()));
5301
+ return buildCatalog().then((catalog) => {
5302
+ const lock = readLock();
5303
+ const byVersion = {};
5304
+ for (const entry of catalog)
5305
+ byVersion[entry.name] = entry.versions;
5306
+ const hooks = {};
5307
+ for (const [name, pin] of Object.entries(lock.hooks)) {
5308
+ hooks[name] = { ...pin, versions: byVersion[name] ?? [pin.version] };
5309
+ }
5310
+ return json({ hooks });
5311
+ });
5253
5312
  }
5254
- const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/(\d+\.\d+\.\d+)$/.exec(url.pathname);
5313
+ const artifactMatch = /^\/api\/v1\/hooks\/([\w-]+)\/([0-9A-Za-z.%+_-]+)$/.exec(url.pathname);
5255
5314
  if (artifactMatch && req.method === "GET") {
5256
- const [, name, version] = artifactMatch;
5315
+ const [, rawName, rawVersion] = artifactMatch;
5316
+ let name;
5317
+ let version;
5318
+ try {
5319
+ name = decodeURIComponent(rawName);
5320
+ version = decodeURIComponent(rawVersion);
5321
+ } catch {
5322
+ return Promise.resolve(json({ error: "invalid URL encoding" }, 400));
5323
+ }
5324
+ if (!SEMVER_PATTERN.test(version)) {
5325
+ return Promise.resolve(json({ error: `invalid semver version '${version}'` }, 400));
5326
+ }
5257
5327
  return artifactFor(name, version).then(async (artifact) => {
5258
5328
  if (!artifact)
5259
5329
  return json({ error: `Hook '${name}@${version}' not found locally` }, 404);
@@ -5284,7 +5354,7 @@ function handleServeRequest(req, apiKey) {
5284
5354
  function startServeServer(options) {
5285
5355
  const port = options.port ?? DEFAULT_SERVE_PORT;
5286
5356
  const host = options.host ?? SERVE_HOST;
5287
- const apiKey = resolveApiKey(options.apiKey);
5357
+ const apiKey = resolveApiKey();
5288
5358
  const server = Bun.serve({
5289
5359
  hostname: host,
5290
5360
  port,
@@ -5296,7 +5366,17 @@ function startServeServer(options) {
5296
5366
  return server;
5297
5367
  }
5298
5368
  if (import.meta.main) {
5299
- startServeServer({});
5369
+ const argv = process.argv.slice(2);
5370
+ const flagValue = (name) => {
5371
+ const idx = argv.indexOf(name);
5372
+ return idx >= 0 && argv[idx + 1] ? argv[idx + 1] : undefined;
5373
+ };
5374
+ const portArg = flagValue("--port");
5375
+ const hostArg = flagValue("--host");
5376
+ startServeServer({
5377
+ port: portArg ? parseInt(portArg, 10) : undefined,
5378
+ host: hostArg
5379
+ });
5300
5380
  }
5301
5381
  export {
5302
5382
  startServeServer,
package/dist/config.d.ts CHANGED
@@ -15,4 +15,11 @@ export interface HooksConfig {
15
15
  export declare function readConfig(): HooksConfig;
16
16
  export declare function writeConfig(config: HooksConfig): string;
17
17
  export declare function resolveApiUrl(): string | undefined;
18
- export declare function resolveApiKey(flagKey?: string): string | undefined;
18
+ /**
19
+ * P1-8: the registry API key resolves from the environment ONLY —
20
+ * HASNA_HOOKS_API_KEY / HOOKS_API_KEY. A secret-valued CLI flag is removed
21
+ * (a flag value is visible in process listings and shell history); the
22
+ * vault-key-NAME reference (config api_key_ref) is a name, not a value, and
23
+ * is untouched.
24
+ */
25
+ export declare function resolveApiKey(): string | undefined;
@@ -1,3 +1,25 @@
1
+ /**
2
+ * P1-5: verified TLS by default. `sslmode=require` / `ssl=true` previously
3
+ * produced `rejectUnauthorized: false` — TLS that verified nothing, so the
4
+ * "encrypted" channel was open to a MITM. The default is now
5
+ * `rejectUnauthorized: true`.
6
+ *
7
+ * Insecure verification exists ONLY through the explicit dev-only config key
8
+ * HASNA_HOOKS_PG_INSECURE_TLS=1 (or HOOKS_PG_INSECURE_TLS) and is refused
9
+ * under a production-shaped environment (NODE_ENV=production) — a dev key
10
+ * silently disabling verification in prod would be the exact failure this
11
+ * finding corrects.
12
+ *
13
+ * P3-12: sslmode/ssl are parsed from the connection-string QUERY, not by
14
+ * substring scan — `sslmode = require` (spaces around the separator), other
15
+ * casings and non-URL DSN forms are all resolved from the parameter value.
16
+ *
17
+ * Exported for tests; the connection-string forms are the documented
18
+ * sslmode values the pg client understands.
19
+ */
20
+ export declare function sslConfigFor(connectionString: string): {
21
+ rejectUnauthorized: boolean;
22
+ } | undefined;
1
23
  export declare class PgAdapterAsync {
2
24
  private readonly pool;
3
25
  constructor(connectionString: string);