@hasna/skills 0.1.68 → 0.1.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -448,7 +448,7 @@ skills/ # Public skill contracts and local OSS skills
448
448
 
449
449
  | Count | Value | Derived from |
450
450
  |---|---|---|
451
- | Catalog skills | 86 | `SKILLS.length` (`src/lib/registry-data/`) |
451
+ | Catalog skills | 87 | `SKILLS.length` (`src/lib/registry-data/`) |
452
452
  | Categories | 17 | `CATEGORIES` (`src/lib/registry-types.ts`) |
453
453
  | MCP tools | 37 | `tools/list` against a live `buildServer()` |
454
454
 
package/bin/index.js CHANGED
@@ -36860,7 +36860,7 @@ var package_default;
36860
36860
  var init_package = __esm(() => {
36861
36861
  package_default = {
36862
36862
  name: "@hasna/skills",
36863
- version: "0.1.68",
36863
+ version: "0.1.70",
36864
36864
  description: "Skills library for AI coding agents",
36865
36865
  type: "module",
36866
36866
  bin: {
@@ -36882,6 +36882,10 @@ var init_package = __esm(() => {
36882
36882
  "./sdk": {
36883
36883
  import: "./dist/sdk/index.js",
36884
36884
  types: "./dist/sdk/index.d.ts"
36885
+ },
36886
+ "./admin-contract": {
36887
+ import: "./dist/admin-contract.js",
36888
+ types: "./dist/admin-contract.d.ts"
36885
36889
  }
36886
36890
  },
36887
36891
  files: [
@@ -36900,14 +36904,15 @@ var init_package = __esm(() => {
36900
36904
  types: "./dist/index.d.ts",
36901
36905
  scripts: {
36902
36906
  clean: "rm -rf bin/ dist/",
36903
- build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
36904
- "build:js": "rm -rf dist && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
36907
+ build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
36908
+ "build:js": "rm -rf dist && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
36905
36909
  test: "bun test --timeout 30000",
36906
36910
  dev: "bun run ./src/cli/index.tsx",
36907
36911
  "dev:watch": "bun --watch run ./src/cli/index.tsx",
36908
36912
  "dev:mcp": "bun --watch run ./src/mcp/index.ts",
36909
36913
  "dev:server": "bun --watch run ./src/server/index.ts",
36910
36914
  "dev:worker": "bun --watch run ./src/server/worker.ts",
36915
+ "docker:check": "bash scripts/docker-build-check.sh",
36911
36916
  migrate: "bun run ./src/server/migrate.ts",
36912
36917
  typecheck: "tsc --noEmit",
36913
36918
  "verify:release": "bun run scripts/release-guard.ts",
@@ -36940,7 +36945,8 @@ var init_package = __esm(() => {
36940
36945
  "@types/react": "^18.2.0",
36941
36946
  "bun-types": "1.3.14",
36942
36947
  "react-devtools-core": "^7.0.1",
36943
- typescript: "^5"
36948
+ typescript: "^5",
36949
+ yaml: "^2.9.0"
36944
36950
  },
36945
36951
  dependencies: {
36946
36952
  "@aws-sdk/client-ecs": "^3.1079.0",
@@ -38316,6 +38322,14 @@ var init_development_tools = __esm(() => {
38316
38322
  category: "Development Tools",
38317
38323
  tags: ["config", "validation", "schema", "linting"]
38318
38324
  },
38325
+ {
38326
+ name: "oss-app-two-backend-storage",
38327
+ displayName: "OSS App Two-Backend Storage",
38328
+ description: "Recipe for the Hasna two-backend storage contract: client transport + HTTP store, server PG/SQLite backend, pg-migrations + apply script, fail-closed URL-without-key, bun bins, contract manifest, Dockerfile",
38329
+ category: "Development Tools",
38330
+ tags: ["storage", "backend", "postgresql", "sqlite", "two-backend", "oss-app"],
38331
+ kind: "instruction"
38332
+ },
38319
38333
  {
38320
38334
  name: "session-inject-monitor",
38321
38335
  displayName: "Session Inject Monitor",
@@ -62354,8 +62368,8 @@ var require_utils = __commonJS((exports, module) => {
62354
62368
  var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
62355
62369
  var HOST_DELIM_RE = /[@/?#:]/g;
62356
62370
  var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
62357
- function reescapeHostDelimiters(host, isIP) {
62358
- const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
62371
+ function reescapeHostDelimiters(host, isIP2) {
62372
+ const re = isIP2 ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
62359
62373
  re.lastIndex = 0;
62360
62374
  return host.replace(re, (ch) => HOST_DELIMS[ch]);
62361
62375
  }
@@ -62804,7 +62818,7 @@ var require_fast_uri = __commonJS((exports, module) => {
62804
62818
  fragment: undefined
62805
62819
  };
62806
62820
  let malformedAuthorityOrPort = false;
62807
- let isIP = false;
62821
+ let isIP2 = false;
62808
62822
  if (options.reference === "suffix") {
62809
62823
  if (options.scheme) {
62810
62824
  uri = options.scheme + ":" + uri;
@@ -62853,9 +62867,9 @@ var require_fast_uri = __commonJS((exports, module) => {
62853
62867
  if (ipv4result === false) {
62854
62868
  const ipv6result = normalizeIPv6(parsed.host);
62855
62869
  parsed.host = ipv6result.host.toLowerCase();
62856
- isIP = ipv6result.isIPV6;
62870
+ isIP2 = ipv6result.isIPV6;
62857
62871
  } else {
62858
- isIP = true;
62872
+ isIP2 = true;
62859
62873
  }
62860
62874
  }
62861
62875
  if (parsed.scheme === undefined && parsed.userinfo === undefined && parsed.host === undefined && parsed.port === undefined && parsed.query === undefined && !parsed.path) {
@@ -62872,7 +62886,7 @@ var require_fast_uri = __commonJS((exports, module) => {
62872
62886
  }
62873
62887
  const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme);
62874
62888
  if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) {
62875
- if (parsed.host && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
62889
+ if (parsed.host && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP2 === false && nonSimpleDomain(parsed.host)) {
62876
62890
  try {
62877
62891
  parsed.host = new URL("http://" + parsed.host).hostname;
62878
62892
  } catch (e) {
@@ -62886,7 +62900,7 @@ var require_fast_uri = __commonJS((exports, module) => {
62886
62900
  parsed.scheme = unescape(parsed.scheme);
62887
62901
  }
62888
62902
  if (parsed.host !== undefined) {
62889
- parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
62903
+ parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP2);
62890
62904
  }
62891
62905
  }
62892
62906
  if (parsed.path) {
@@ -69902,7 +69916,7 @@ var init_server3 = __esm(() => {
69902
69916
  server = buildServer();
69903
69917
  });
69904
69918
 
69905
- // ../../node_modules/.bun/@hono+node-server@1.19.17+470351d3cbe921c0/node_modules/@hono/node-server/dist/index.mjs
69919
+ // ../../node_modules/.bun/@hono+node-server@1.19.17+2145b681a064c8e9/node_modules/@hono/node-server/dist/index.mjs
69906
69920
  import { Readable } from "stream";
69907
69921
  import crypto2 from "crypto";
69908
69922
  var GlobalRequest, Request, newHeadersFromIncoming = (incoming) => {
@@ -74195,7 +74209,7 @@ import process12 from "process";
74195
74209
  // ../../node_modules/.bun/ink@5.2.1+6e93904c7bfe2418/node_modules/ink/build/ink.js
74196
74210
  var import_react10 = __toESM(require_react(), 1);
74197
74211
  import process11 from "process";
74198
- // ../../node_modules/.bun/es-toolkit@1.50.0/node_modules/es-toolkit/dist/function/debounce.mjs
74212
+ // ../../node_modules/.bun/es-toolkit@1.51.0/node_modules/es-toolkit/dist/function/debounce.mjs
74199
74213
  function debounce(func, debounceMs, { signal, edges } = {}) {
74200
74214
  let pendingThis = undefined;
74201
74215
  let pendingArgs = null;
@@ -74253,7 +74267,7 @@ function debounce(func, debounceMs, { signal, edges } = {}) {
74253
74267
  return debounced;
74254
74268
  }
74255
74269
 
74256
- // ../../node_modules/.bun/es-toolkit@1.50.0/node_modules/es-toolkit/dist/compat/function/debounce.mjs
74270
+ // ../../node_modules/.bun/es-toolkit@1.51.0/node_modules/es-toolkit/dist/compat/function/debounce.mjs
74257
74271
  function debounce2(func, debounceMs = 0, options = {}) {
74258
74272
  if (typeof options !== "object")
74259
74273
  options = {};
@@ -74293,7 +74307,7 @@ function debounce2(func, debounceMs = 0, options = {}) {
74293
74307
  return debounced;
74294
74308
  }
74295
74309
 
74296
- // ../../node_modules/.bun/es-toolkit@1.50.0/node_modules/es-toolkit/dist/compat/function/throttle.mjs
74310
+ // ../../node_modules/.bun/es-toolkit@1.51.0/node_modules/es-toolkit/dist/compat/function/throttle.mjs
74297
74311
  function throttle(func, throttleMs = 0, options = {}) {
74298
74312
  const { leading = true, trailing = true } = options;
74299
74313
  return debounce2(func, throttleMs, {
@@ -76209,10 +76223,10 @@ var import_react_reconciler = __toESM(require_react_reconciler(), 1);
76209
76223
  var import_constants = __toESM(require_constants(), 1);
76210
76224
  import process3 from "process";
76211
76225
 
76212
- // ../../node_modules/.bun/ansi-regex@6.2.2/node_modules/ansi-regex/index.js
76226
+ // ../../node_modules/.bun/ansi-regex@6.3.0/node_modules/ansi-regex/index.js
76213
76227
  function ansiRegex({ onlyFirst = false } = {}) {
76214
76228
  const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)";
76215
- const osc = `(?:\\u001B\\][\\s\\S]*?${ST})`;
76229
+ const osc = `(?:\\u001B\\][^\\u0007\\u001B\\u009C]*${ST})`;
76216
76230
  const csi = "[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]";
76217
76231
  const pattern = `${osc}|${csi}`;
76218
76232
  return new RegExp(pattern, onlyFirst ? undefined : "g");
@@ -79215,8 +79229,12 @@ import { existsSync as existsSync2 } from "fs";
79215
79229
  import { homedir } from "os";
79216
79230
  import { join as join2 } from "path";
79217
79231
  import { createHmac, timingSafeEqual } from "crypto";
79232
+ import { lookup as dnsLookup } from "dns/promises";
79233
+ import { isIP } from "net";
79218
79234
  import { randomUUID } from "crypto";
79219
79235
  import { spawn } from "child_process";
79236
+ import { request as nodeHttpRequest } from "http";
79237
+ import { request as nodeHttpsRequest } from "https";
79220
79238
  import { randomUUID as randomUUID2 } from "crypto";
79221
79239
  function getPathValue(input, path) {
79222
79240
  return path.split(".").reduce((value, part) => {
@@ -79619,6 +79637,214 @@ function signPayload(secret, timestamp, body) {
79619
79637
  const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
79620
79638
  return `sha256=${digest}`;
79621
79639
  }
79640
+ var DEFAULT_MAX_REDIRECTS = 5;
79641
+ var IPV4_PRIVATE_RANGES = [
79642
+ [0, 16777215],
79643
+ [167772160, 184549375],
79644
+ [1681915904, 1686110207],
79645
+ [2130706432, 2147483647],
79646
+ [2851995648, 2852061183],
79647
+ [2886729728, 2887778303],
79648
+ [3221225472, 3221225727],
79649
+ [3221225984, 3221226239],
79650
+ [3227017984, 3227018239],
79651
+ [3232235520, 3232301055],
79652
+ [3323068416, 3323199487],
79653
+ [3325256704, 3325256959],
79654
+ [3405803776, 3405804031],
79655
+ [3758096384, 4294967295]
79656
+ ];
79657
+ var IPV6_SPECIAL_PREFIXES = [
79658
+ { groups: [0, 0, 0, 0, 0, 0, 0, 0], bits: 128 },
79659
+ { groups: [0, 0, 0, 0, 0, 0, 0, 1], bits: 128 },
79660
+ { groups: [0, 0, 0, 0, 0, 65535, 0, 0], bits: 96 },
79661
+ { groups: [100, 65435, 0, 0, 0, 0, 0, 0], bits: 96 },
79662
+ { groups: [256, 0, 0, 0, 0, 0, 0, 0], bits: 64 },
79663
+ { groups: [8193, 0, 0, 0, 0, 0, 0, 0], bits: 32 },
79664
+ { groups: [8193, 2, 0, 0, 0, 0, 0, 0], bits: 48 },
79665
+ { groups: [8193, 16, 0, 0, 0, 0, 0, 0], bits: 28 },
79666
+ { groups: [8193, 3512, 0, 0, 0, 0, 0, 0], bits: 32 },
79667
+ { groups: [8194, 0, 0, 0, 0, 0, 0, 0], bits: 16 },
79668
+ { groups: [16383, 0, 0, 0, 0, 0, 0, 0], bits: 20 },
79669
+ { groups: [64512, 0, 0, 0, 0, 0, 0, 0], bits: 7 },
79670
+ { groups: [65152, 0, 0, 0, 0, 0, 0, 0], bits: 10 },
79671
+ { groups: [65216, 0, 0, 0, 0, 0, 0, 0], bits: 10 },
79672
+ { groups: [65280, 0, 0, 0, 0, 0, 0, 0], bits: 8 }
79673
+ ];
79674
+ function isPrivateAddress(address) {
79675
+ const normalized = stripZoneId(address);
79676
+ const version = isIP(normalized);
79677
+ if (version === 4) {
79678
+ const integer = ipv4ToInt(normalized);
79679
+ if (integer === undefined)
79680
+ return true;
79681
+ return IPV4_PRIVATE_RANGES.some(([low, high]) => integer >= low && integer <= high);
79682
+ }
79683
+ if (version === 6) {
79684
+ const groups = ipv6Groups(normalized);
79685
+ if (!groups)
79686
+ return true;
79687
+ for (const prefix of IPV6_SPECIAL_PREFIXES) {
79688
+ if (!ipv6MatchesPrefix(groups, prefix.groups, prefix.bits))
79689
+ continue;
79690
+ if (prefix.bits === 96 && groups[5] === 65535) {
79691
+ return isPrivateAddress(ipv4IntToString(groups[6] << 16 | groups[7]));
79692
+ }
79693
+ if (prefix.bits === 16 && groups[0] === 8194) {
79694
+ return isPrivateAddress(ipv4IntToString(groups[1] << 16 | groups[2]));
79695
+ }
79696
+ return true;
79697
+ }
79698
+ return false;
79699
+ }
79700
+ return true;
79701
+ }
79702
+ async function resolveWebhookTarget(url, policy = {}) {
79703
+ const hostname = normalizeHostname(url.hostname);
79704
+ const allowlist = (policy.allowPrivateHosts ?? []).map((entry) => normalizeHostname(entry.toLowerCase()));
79705
+ if (allowlist.includes(hostname)) {
79706
+ const version2 = isIP(hostname);
79707
+ if (version2 === 4 || version2 === 6) {
79708
+ return { hostname, addresses: [hostname] };
79709
+ }
79710
+ const lookup2 = policy.lookup ?? defaultTargetLookup;
79711
+ let resolved2;
79712
+ try {
79713
+ resolved2 = await lookup2(hostname);
79714
+ } catch {
79715
+ throw new Error(`Webhook target ${hostname} could not be resolved`);
79716
+ }
79717
+ if (!Array.isArray(resolved2) || resolved2.length === 0) {
79718
+ throw new Error(`Webhook target ${hostname} resolved to no addresses`);
79719
+ }
79720
+ const addresses = resolved2.map((entry) => normalizeHostname(entry.address));
79721
+ return { hostname, addresses };
79722
+ }
79723
+ const version = isIP(hostname);
79724
+ if (version === 4 || version === 6) {
79725
+ if (isPrivateAddress(hostname)) {
79726
+ throw new Error(`Webhook target ${hostname} is a private or special-use address`);
79727
+ }
79728
+ return { hostname, addresses: [hostname] };
79729
+ }
79730
+ const lookup = policy.lookup ?? defaultTargetLookup;
79731
+ let resolved;
79732
+ try {
79733
+ resolved = await lookup(hostname);
79734
+ } catch {
79735
+ throw new Error(`Webhook target ${hostname} could not be resolved`);
79736
+ }
79737
+ if (!Array.isArray(resolved) || resolved.length === 0) {
79738
+ throw new Error(`Webhook target ${hostname} resolved to no addresses`);
79739
+ }
79740
+ const allowed = [];
79741
+ for (const entry of resolved) {
79742
+ const address = normalizeHostname(entry.address);
79743
+ if (isPrivateAddress(address)) {
79744
+ if (allowlist.includes(address)) {
79745
+ allowed.push(address);
79746
+ continue;
79747
+ }
79748
+ throw new Error(`Webhook target ${hostname} resolves to private or special-use address ${address}`);
79749
+ }
79750
+ allowed.push(address);
79751
+ }
79752
+ if (allowed.length === 0) {
79753
+ throw new Error(`Webhook target ${hostname} resolved to no public addresses`);
79754
+ }
79755
+ return { hostname, addresses: allowed };
79756
+ }
79757
+ function normalizeMaxRedirects(value) {
79758
+ if (value === undefined)
79759
+ return DEFAULT_MAX_REDIRECTS;
79760
+ if (!Number.isInteger(value) || value < 0)
79761
+ throw new Error("webhookTargetPolicy.maxRedirects must be a non-negative integer");
79762
+ return value;
79763
+ }
79764
+ var defaultTargetLookup = async (hostname) => {
79765
+ return dnsLookup(hostname, { all: true, verbatim: false });
79766
+ };
79767
+ function normalizeHostname(hostname) {
79768
+ const lower = hostname.toLowerCase();
79769
+ if (lower.startsWith("[") && lower.endsWith("]"))
79770
+ return lower.slice(1, -1);
79771
+ return lower;
79772
+ }
79773
+ function stripZoneId(address) {
79774
+ const percent = address.indexOf("%");
79775
+ return percent === -1 ? address : address.slice(0, percent);
79776
+ }
79777
+ function ipv4ToInt(address) {
79778
+ const parts = address.split(".");
79779
+ if (parts.length !== 4)
79780
+ return;
79781
+ let value = 0;
79782
+ for (const part of parts) {
79783
+ if (!/^\d{1,3}$/.test(part))
79784
+ return;
79785
+ const octet = Number(part);
79786
+ if (octet > 255)
79787
+ return;
79788
+ value = value << 8 | octet;
79789
+ }
79790
+ return value >>> 0;
79791
+ }
79792
+ function ipv4IntToString(integer) {
79793
+ return [
79794
+ integer >>> 24 & 255,
79795
+ integer >>> 16 & 255,
79796
+ integer >>> 8 & 255,
79797
+ integer & 255
79798
+ ].join(".");
79799
+ }
79800
+ function ipv6Groups(address) {
79801
+ const raw = stripZoneId(address);
79802
+ const doubleColon = raw.indexOf("::");
79803
+ const headText = doubleColon === -1 ? raw : raw.slice(0, doubleColon);
79804
+ const tailText = doubleColon === -1 ? "" : raw.slice(doubleColon + 2);
79805
+ const parseGroups = (text) => {
79806
+ if (text === "")
79807
+ return [];
79808
+ const out = [];
79809
+ for (const part of text.split(":")) {
79810
+ if (part.includes(".")) {
79811
+ const v4 = ipv4ToInt(part);
79812
+ if (v4 === undefined)
79813
+ return;
79814
+ out.push(v4 >>> 16 & 65535, v4 & 65535);
79815
+ } else {
79816
+ if (!/^[0-9a-fA-F]{1,4}$/.test(part))
79817
+ return;
79818
+ out.push(parseInt(part, 16));
79819
+ }
79820
+ }
79821
+ return out;
79822
+ };
79823
+ const head2 = parseGroups(headText);
79824
+ if (!head2)
79825
+ return;
79826
+ const tail2 = parseGroups(tailText);
79827
+ if (!tail2)
79828
+ return;
79829
+ const total = head2.length + tail2.length;
79830
+ if (doubleColon === -1) {
79831
+ return total === 8 ? head2 : undefined;
79832
+ }
79833
+ if (total >= 8)
79834
+ return;
79835
+ return [...head2, ...new Array(8 - total).fill(0), ...tail2];
79836
+ }
79837
+ function ipv6MatchesPrefix(groups, prefixGroups, prefixBits) {
79838
+ let remaining = prefixBits;
79839
+ for (let index = 0;index < prefixGroups.length && remaining > 0; index += 1) {
79840
+ const take2 = Math.min(16, remaining);
79841
+ const mask = 65535 << 16 - take2 & 65535;
79842
+ if ((groups[index] & mask) !== (prefixGroups[index] & mask))
79843
+ return false;
79844
+ remaining -= take2;
79845
+ }
79846
+ return true;
79847
+ }
79622
79848
  function now2() {
79623
79849
  return new Date().toISOString();
79624
79850
  }
@@ -79649,9 +79875,18 @@ function buildWebhookRequest(event, channel, options = {}) {
79649
79875
  }
79650
79876
  return { body, headers };
79651
79877
  }
79878
+ function normalizeWebhookUrl(raw) {
79879
+ const url = new URL(raw);
79880
+ if (url.username !== "" || url.password !== "") {
79881
+ url.username = "";
79882
+ url.password = "";
79883
+ }
79884
+ return url.toString();
79885
+ }
79652
79886
  async function dispatchWebhook(event, channel, options = {}) {
79653
79887
  if (!channel.webhook)
79654
79888
  throw new Error(`Channel ${channel.id} has no webhook config`);
79889
+ const webhookUrl = normalizeWebhookUrl(channel.webhook.url);
79655
79890
  const startedAt = now2();
79656
79891
  let secret = channel.webhook.secret;
79657
79892
  if (channel.webhook.secretRef) {
@@ -79668,10 +79903,14 @@ async function dispatchWebhook(event, channel, options = {}) {
79668
79903
  }
79669
79904
  const timestamp = (options.now?.() ?? new Date).toISOString();
79670
79905
  const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp });
79906
+ const validateTargets = options.webhookTargetPolicy !== undefined || options.fetchImpl === undefined;
79907
+ if (validateTargets) {
79908
+ return dispatchValidatedWebhook(event, channel, { body, headers, startedAt, options });
79909
+ }
79671
79910
  const controller = new AbortController;
79672
79911
  const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
79673
79912
  try {
79674
- const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
79913
+ const response = await (options.fetchImpl ?? fetch)(webhookUrl, {
79675
79914
  method: "POST",
79676
79915
  headers,
79677
79916
  body,
@@ -79699,6 +79938,130 @@ async function dispatchWebhook(event, channel, options = {}) {
79699
79938
  clearTimeout(timeout);
79700
79939
  }
79701
79940
  }
79941
+ function isRedirectStatus(status) {
79942
+ return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
79943
+ }
79944
+ function redirectKeepsBody(status) {
79945
+ return status === 307 || status === 308;
79946
+ }
79947
+ async function pinnedNativeRequest(target, addresses, method2, headers, body, signal, tls) {
79948
+ const isHttps = target.protocol === "https:";
79949
+ if (!isHttps && target.protocol !== "http:") {
79950
+ throw new Error(`Webhook target uses unsupported protocol ${target.protocol}`);
79951
+ }
79952
+ const defaultPort = isHttps ? 443 : 80;
79953
+ const port = target.port ? Number(target.port) : defaultPort;
79954
+ const requestOptions = {
79955
+ hostname: target.hostname,
79956
+ port,
79957
+ path: `${target.pathname}${target.search}`,
79958
+ method: method2,
79959
+ headers,
79960
+ ...tls?.ca ? { ca: tls.ca } : {},
79961
+ lookup: (hostname, _options, callback) => {
79962
+ const entries = addresses.map((address) => ({
79963
+ address,
79964
+ family: address.includes(":") ? 6 : 4
79965
+ }));
79966
+ callback(null, entries);
79967
+ }
79968
+ };
79969
+ return new Promise((resolve, reject2) => {
79970
+ const request = isHttps ? nodeHttpsRequest(requestOptions, onResponse) : nodeHttpRequest(requestOptions, onResponse);
79971
+ const onAbort = () => {
79972
+ const error = new Error("The operation was aborted.");
79973
+ error.name = "AbortError";
79974
+ request.destroy(error);
79975
+ };
79976
+ if (signal.aborted)
79977
+ onAbort();
79978
+ else
79979
+ signal.addEventListener("abort", onAbort, { once: true });
79980
+ request.on("error", reject2);
79981
+ if (body !== undefined)
79982
+ request.write(body);
79983
+ request.end();
79984
+ function onResponse(response) {
79985
+ const chunks = [];
79986
+ response.on("data", (chunk2) => chunks.push(Buffer.from(chunk2)));
79987
+ response.on("error", reject2);
79988
+ response.on("end", () => {
79989
+ const headersRecord = {};
79990
+ for (const [name, value] of Object.entries(response.headers)) {
79991
+ if (typeof value === "string")
79992
+ headersRecord[name] = value;
79993
+ else if (Array.isArray(value))
79994
+ headersRecord[name] = value.join(", ");
79995
+ }
79996
+ resolve(new Response(Buffer.concat(chunks), { status: response.statusCode ?? 200, headers: headersRecord }));
79997
+ });
79998
+ }
79999
+ });
80000
+ }
80001
+ async function dispatchValidatedWebhook(event, channel, input) {
80002
+ const { body, headers, startedAt, options } = input;
80003
+ const webhook = channel.webhook;
80004
+ if (!webhook)
80005
+ throw new Error(`Channel ${channel.id} has no webhook config`);
80006
+ const policy = options.webhookTargetPolicy ?? {};
80007
+ const maxRedirects = normalizeMaxRedirects(policy.maxRedirects);
80008
+ const controller = new AbortController;
80009
+ const timeout = setTimeout(() => controller.abort(), webhook.timeoutMs ?? 15000);
80010
+ try {
80011
+ let target = new URL(normalizeWebhookUrl(webhook.url));
80012
+ let requestHeaders = headers;
80013
+ let method2 = "POST";
80014
+ let requestBody = body;
80015
+ let redirectsFollowed = 0;
80016
+ for (;; ) {
80017
+ const resolved = await resolveWebhookTarget(target, policy).catch((error) => {
80018
+ throw new Error(`Webhook target rejected by SSRF guard: ${error.message}`);
80019
+ });
80020
+ const response = options.fetchImpl ? await options.fetchImpl(target, {
80021
+ method: method2,
80022
+ headers: requestHeaders,
80023
+ body: requestBody,
80024
+ signal: controller.signal,
80025
+ redirect: "manual"
80026
+ }) : await pinnedNativeRequest(target, resolved.addresses, method2, requestHeaders, requestBody, controller.signal, options.tls);
80027
+ const location = response.headers.get("location");
80028
+ if (isRedirectStatus(response.status) && location) {
80029
+ if (redirectsFollowed >= maxRedirects) {
80030
+ return failedAttempt(startedAt, `Webhook target exceeded ${maxRedirects} redirects`);
80031
+ }
80032
+ redirectsFollowed += 1;
80033
+ const next = new URL(location, target);
80034
+ target = next;
80035
+ if (!redirectKeepsBody(response.status)) {
80036
+ method2 = "GET";
80037
+ requestBody = undefined;
80038
+ requestHeaders = Object.fromEntries(Object.entries(requestHeaders).filter(([name]) => name.toLowerCase() !== "content-type" && name.toLowerCase() !== "content-length"));
80039
+ }
80040
+ continue;
80041
+ }
80042
+ const responseBody = truncate2(await response.text());
80043
+ return {
80044
+ attempt: 1,
80045
+ status: response.ok ? "success" : "failed",
80046
+ startedAt,
80047
+ completedAt: now2(),
80048
+ responseStatus: response.status,
80049
+ responseBody,
80050
+ error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
80051
+ };
80052
+ }
80053
+ } catch (error) {
80054
+ return {
80055
+ attempt: 1,
80056
+ status: "failed",
80057
+ startedAt,
80058
+ completedAt: now2(),
80059
+ error: error instanceof Error ? error.message : String(error)
80060
+ };
80061
+ } finally {
80062
+ clearTimeout(timeout);
80063
+ }
80064
+ }
79702
80065
  function failedAttempt(startedAt, error) {
79703
80066
  return {
79704
80067
  attempt: 1,
@@ -79908,7 +80271,9 @@ class EventsClient {
79908
80271
  this.transportOptions = {
79909
80272
  fetchImpl: options.fetchImpl,
79910
80273
  secretResolver: options.secretResolver,
79911
- now: options.now
80274
+ now: options.now,
80275
+ tls: options.tls,
80276
+ webhookTargetPolicy: options.webhookTargetPolicy
79912
80277
  };
79913
80278
  this.catalog = options.catalog ?? defaultEventTypeCatalog;
79914
80279
  this.validateCatalogTypes = options.validateCatalogTypes ?? false;
package/bin/mcp.js CHANGED
@@ -12943,7 +12943,7 @@ class StdioServerTransport {
12943
12943
  // package.json
12944
12944
  var package_default = {
12945
12945
  name: "@hasna/skills",
12946
- version: "0.1.68",
12946
+ version: "0.1.70",
12947
12947
  description: "Skills library for AI coding agents",
12948
12948
  type: "module",
12949
12949
  bin: {
@@ -12965,6 +12965,10 @@ var package_default = {
12965
12965
  "./sdk": {
12966
12966
  import: "./dist/sdk/index.js",
12967
12967
  types: "./dist/sdk/index.d.ts"
12968
+ },
12969
+ "./admin-contract": {
12970
+ import: "./dist/admin-contract.js",
12971
+ types: "./dist/admin-contract.d.ts"
12968
12972
  }
12969
12973
  },
12970
12974
  files: [
@@ -12983,14 +12987,15 @@ var package_default = {
12983
12987
  types: "./dist/index.d.ts",
12984
12988
  scripts: {
12985
12989
  clean: "rm -rf bin/ dist/",
12986
- build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
12987
- "build:js": "rm -rf dist && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
12990
+ build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
12991
+ "build:js": "rm -rf dist && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
12988
12992
  test: "bun test --timeout 30000",
12989
12993
  dev: "bun run ./src/cli/index.tsx",
12990
12994
  "dev:watch": "bun --watch run ./src/cli/index.tsx",
12991
12995
  "dev:mcp": "bun --watch run ./src/mcp/index.ts",
12992
12996
  "dev:server": "bun --watch run ./src/server/index.ts",
12993
12997
  "dev:worker": "bun --watch run ./src/server/worker.ts",
12998
+ "docker:check": "bash scripts/docker-build-check.sh",
12994
12999
  migrate: "bun run ./src/server/migrate.ts",
12995
13000
  typecheck: "tsc --noEmit",
12996
13001
  "verify:release": "bun run scripts/release-guard.ts",
@@ -13023,7 +13028,8 @@ var package_default = {
13023
13028
  "@types/react": "^18.2.0",
13024
13029
  "bun-types": "1.3.14",
13025
13030
  "react-devtools-core": "^7.0.1",
13026
- typescript: "^5"
13031
+ typescript: "^5",
13032
+ yaml: "^2.9.0"
13027
13033
  },
13028
13034
  dependencies: {
13029
13035
  "@aws-sdk/client-ecs": "^3.1079.0",
@@ -21104,6 +21110,14 @@ var DEVELOPMENT_TOOLS_SKILLS = [
21104
21110
  category: "Development Tools",
21105
21111
  tags: ["config", "validation", "schema", "linting"]
21106
21112
  },
21113
+ {
21114
+ name: "oss-app-two-backend-storage",
21115
+ displayName: "OSS App Two-Backend Storage",
21116
+ description: "Recipe for the Hasna two-backend storage contract: client transport + HTTP store, server PG/SQLite backend, pg-migrations + apply script, fail-closed URL-without-key, bun bins, contract manifest, Dockerfile",
21117
+ category: "Development Tools",
21118
+ tags: ["storage", "backend", "postgresql", "sqlite", "two-backend", "oss-app"],
21119
+ kind: "instruction"
21120
+ },
21107
21121
  {
21108
21122
  name: "session-inject-monitor",
21109
21123
  displayName: "Session Inject Monitor",
@@ -26944,7 +26958,7 @@ var server = buildServer();
26944
26958
  // src/mcp/http.ts
26945
26959
  import { createServer } from "http";
26946
26960
 
26947
- // ../../node_modules/.bun/@hono+node-server@1.19.17+470351d3cbe921c0/node_modules/@hono/node-server/dist/index.mjs
26961
+ // ../../node_modules/.bun/@hono+node-server@1.19.17+2145b681a064c8e9/node_modules/@hono/node-server/dist/index.mjs
26948
26962
  import { Http2ServerRequest as Http2ServerRequest2, constants as h2constants } from "http2";
26949
26963
  import { Http2ServerRequest } from "http2";
26950
26964
  import { Readable } from "stream";