@hasna/instructions 0.4.34 → 0.4.35
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 +6 -7
- package/dist/cli/index.js +689 -185
- package/dist/data/config-store.d.ts +4 -4
- package/dist/data/config-store.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/generated/storage-kit/backend.d.ts +20 -0
- package/dist/generated/storage-kit/backend.d.ts.map +1 -0
- package/dist/generated/storage-kit/index.d.ts +2 -2
- package/dist/generated/storage-kit/index.d.ts.map +1 -1
- package/dist/generated/storage-kit/migrations.d.ts.map +1 -1
- package/dist/generated/storage-kit/own.d.ts +11 -0
- package/dist/generated/storage-kit/own.d.ts.map +1 -0
- package/dist/generated/storage-kit/pool.d.ts +7 -6
- package/dist/generated/storage-kit/pool.d.ts.map +1 -1
- package/dist/generated/storage-kit/tls.d.ts +30 -3
- package/dist/generated/storage-kit/tls.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +45 -38
- package/dist/lib/apply.d.ts.map +1 -1
- package/dist/lib/cursor-authority.d.ts +3 -26
- package/dist/lib/cursor-authority.d.ts.map +1 -1
- package/dist/lib/retired-storage-mode.d.ts +8 -0
- package/dist/lib/retired-storage-mode.d.ts.map +1 -0
- package/dist/mcp/index.js +73 -73
- package/dist/server/cloud.d.ts +2 -2
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +456 -271
- package/package.json +2 -1
- package/dist/generated/storage-kit/mode.d.ts +0 -48
- package/dist/generated/storage-kit/mode.d.ts.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -47,7 +47,7 @@ var __export = (target, all) => {
|
|
|
47
47
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
48
48
|
var __require = import.meta.require;
|
|
49
49
|
|
|
50
|
-
// node_modules/commander/lib/error.js
|
|
50
|
+
// ../../node_modules/.bun/commander@13.1.0/node_modules/commander/lib/error.js
|
|
51
51
|
var require_error = __commonJS((exports) => {
|
|
52
52
|
class CommanderError extends Error {
|
|
53
53
|
constructor(exitCode, code, message) {
|
|
@@ -71,7 +71,7 @@ var require_error = __commonJS((exports) => {
|
|
|
71
71
|
exports.InvalidArgumentError = InvalidArgumentError;
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
// node_modules/commander/lib/argument.js
|
|
74
|
+
// ../../node_modules/.bun/commander@13.1.0/node_modules/commander/lib/argument.js
|
|
75
75
|
var require_argument = __commonJS((exports) => {
|
|
76
76
|
var { InvalidArgumentError } = require_error();
|
|
77
77
|
|
|
@@ -150,7 +150,7 @@ var require_argument = __commonJS((exports) => {
|
|
|
150
150
|
exports.humanReadableArgName = humanReadableArgName;
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
-
// node_modules/commander/lib/help.js
|
|
153
|
+
// ../../node_modules/.bun/commander@13.1.0/node_modules/commander/lib/help.js
|
|
154
154
|
var require_help = __commonJS((exports) => {
|
|
155
155
|
var { humanReadableArgName } = require_argument();
|
|
156
156
|
|
|
@@ -500,7 +500,7 @@ ${itemIndentStr}`);
|
|
|
500
500
|
exports.stripColor = stripColor;
|
|
501
501
|
});
|
|
502
502
|
|
|
503
|
-
// node_modules/commander/lib/option.js
|
|
503
|
+
// ../../node_modules/.bun/commander@13.1.0/node_modules/commander/lib/option.js
|
|
504
504
|
var require_option = __commonJS((exports) => {
|
|
505
505
|
var { InvalidArgumentError } = require_error();
|
|
506
506
|
|
|
@@ -678,7 +678,7 @@ var require_option = __commonJS((exports) => {
|
|
|
678
678
|
exports.DualOptions = DualOptions;
|
|
679
679
|
});
|
|
680
680
|
|
|
681
|
-
// node_modules/commander/lib/suggestSimilar.js
|
|
681
|
+
// ../../node_modules/.bun/commander@13.1.0/node_modules/commander/lib/suggestSimilar.js
|
|
682
682
|
var require_suggestSimilar = __commonJS((exports) => {
|
|
683
683
|
var maxDistance = 3;
|
|
684
684
|
function editDistance(a, b) {
|
|
@@ -751,7 +751,7 @@ var require_suggestSimilar = __commonJS((exports) => {
|
|
|
751
751
|
exports.suggestSimilar = suggestSimilar;
|
|
752
752
|
});
|
|
753
753
|
|
|
754
|
-
// node_modules/commander/lib/command.js
|
|
754
|
+
// ../../node_modules/.bun/commander@13.1.0/node_modules/commander/lib/command.js
|
|
755
755
|
var require_command = __commonJS((exports) => {
|
|
756
756
|
var EventEmitter = __require("events").EventEmitter;
|
|
757
757
|
var childProcess = __require("child_process");
|
|
@@ -993,7 +993,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
993
993
|
this._exitCallback = (err) => {
|
|
994
994
|
if (err.code !== "commander.executeSubCommandAsync") {
|
|
995
995
|
throw err;
|
|
996
|
-
}
|
|
996
|
+
} else {}
|
|
997
997
|
};
|
|
998
998
|
}
|
|
999
999
|
return this;
|
|
@@ -2061,7 +2061,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
2061
2061
|
exports.useColor = useColor;
|
|
2062
2062
|
});
|
|
2063
2063
|
|
|
2064
|
-
// node_modules/commander/index.js
|
|
2064
|
+
// ../../node_modules/.bun/commander@13.1.0/node_modules/commander/index.js
|
|
2065
2065
|
var require_commander = __commonJS((exports) => {
|
|
2066
2066
|
var { Argument } = require_argument();
|
|
2067
2067
|
var { Command } = require_command();
|
|
@@ -2136,6 +2136,30 @@ var init_types = __esm(() => {
|
|
|
2136
2136
|
};
|
|
2137
2137
|
});
|
|
2138
2138
|
|
|
2139
|
+
// src/lib/retired-storage-mode.ts
|
|
2140
|
+
function firstDefinedEnvKey(env, keys) {
|
|
2141
|
+
for (const key of keys) {
|
|
2142
|
+
if (Object.hasOwn(env, key) && env[key] !== undefined)
|
|
2143
|
+
return key;
|
|
2144
|
+
}
|
|
2145
|
+
return null;
|
|
2146
|
+
}
|
|
2147
|
+
function assertNoLegacyStorageMode(env = process.env) {
|
|
2148
|
+
const legacyKey = firstDefinedEnvKey(env, LEGACY_STORAGE_MODE_KEYS);
|
|
2149
|
+
if (!legacyKey)
|
|
2150
|
+
return;
|
|
2151
|
+
throw new Error(`${legacyKey} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `The client uses the local SQLite store, or the HTTP API selected by ` + `HASNA_INSTRUCTIONS_API_URL + HASNA_INSTRUCTIONS_API_KEY. ` + `On the server, set HASNA_INSTRUCTIONS_DATABASE_URL to select the postgresql backend, ` + `or leave it unset for sqlite.`);
|
|
2152
|
+
}
|
|
2153
|
+
var LEGACY_STORAGE_MODE_KEYS;
|
|
2154
|
+
var init_retired_storage_mode = __esm(() => {
|
|
2155
|
+
LEGACY_STORAGE_MODE_KEYS = [
|
|
2156
|
+
"HASNA_INSTRUCTIONS_STORAGE_MODE",
|
|
2157
|
+
"HASNA_INSTRUCTIONS_MODE",
|
|
2158
|
+
"INSTRUCTIONS_STORAGE_MODE",
|
|
2159
|
+
"INSTRUCTIONS_MODE"
|
|
2160
|
+
];
|
|
2161
|
+
});
|
|
2162
|
+
|
|
2139
2163
|
// src/db/database.ts
|
|
2140
2164
|
import { Database } from "bun:sqlite";
|
|
2141
2165
|
import { existsSync as existsSync2, mkdirSync, rmSync } from "fs";
|
|
@@ -2162,8 +2186,9 @@ function slugify(name) {
|
|
|
2162
2186
|
function getDatabase(path) {
|
|
2163
2187
|
if (_db)
|
|
2164
2188
|
return _db;
|
|
2189
|
+
assertNoLegacyStorageMode();
|
|
2165
2190
|
if (!path && process.env["HASNA_INSTRUCTIONS_API_URL"] && process.env["HASNA_INSTRUCTIONS_API_KEY"]) {
|
|
2166
|
-
throw new Error("instructions is
|
|
2191
|
+
throw new Error("instructions is using the HTTP API transport (HASNA_INSTRUCTIONS_API_URL set): this command is not wired to the API yet. " + "Unset HASNA_INSTRUCTIONS_API_URL / HASNA_INSTRUCTIONS_API_KEY to use it against the local store.");
|
|
2167
2192
|
}
|
|
2168
2193
|
const dbPath = path || getDbPath();
|
|
2169
2194
|
const db = new Database(dbPath);
|
|
@@ -2244,6 +2269,7 @@ function insertFeedback(input, db) {
|
|
|
2244
2269
|
}
|
|
2245
2270
|
var MIGRATIONS, _db = null;
|
|
2246
2271
|
var init_database = __esm(() => {
|
|
2272
|
+
init_retired_storage_mode();
|
|
2247
2273
|
MIGRATIONS = [
|
|
2248
2274
|
`
|
|
2249
2275
|
CREATE TABLE IF NOT EXISTS configs (
|
|
@@ -3037,7 +3063,7 @@ Policy reference: \`${CODEWITH_SHARED_TODOS_STORAGE_POLICY_REFERENCE}\`
|
|
|
3037
3063
|
`;
|
|
3038
3064
|
});
|
|
3039
3065
|
|
|
3040
|
-
// node_modules/zod/v3/helpers/util.js
|
|
3066
|
+
// ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
|
|
3041
3067
|
var util, objectUtil, ZodParsedType, getParsedType = (data) => {
|
|
3042
3068
|
const t = typeof data;
|
|
3043
3069
|
switch (t) {
|
|
@@ -3168,7 +3194,7 @@ var init_util = __esm(() => {
|
|
|
3168
3194
|
]);
|
|
3169
3195
|
});
|
|
3170
3196
|
|
|
3171
|
-
// node_modules/zod/v3/ZodError.js
|
|
3197
|
+
// ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/ZodError.js
|
|
3172
3198
|
var ZodIssueCode, quotelessJson = (obj) => {
|
|
3173
3199
|
const json = JSON.stringify(obj, null, 2);
|
|
3174
3200
|
return json.replace(/"([^"]+)":/g, "$1:");
|
|
@@ -3289,7 +3315,7 @@ var init_ZodError = __esm(() => {
|
|
|
3289
3315
|
};
|
|
3290
3316
|
});
|
|
3291
3317
|
|
|
3292
|
-
// node_modules/zod/v3/locales/en.js
|
|
3318
|
+
// ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/locales/en.js
|
|
3293
3319
|
var errorMap = (issue, _ctx) => {
|
|
3294
3320
|
let message;
|
|
3295
3321
|
switch (issue.code) {
|
|
@@ -3396,7 +3422,7 @@ var init_en = __esm(() => {
|
|
|
3396
3422
|
en_default = errorMap;
|
|
3397
3423
|
});
|
|
3398
3424
|
|
|
3399
|
-
// node_modules/zod/v3/errors.js
|
|
3425
|
+
// ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/errors.js
|
|
3400
3426
|
function setErrorMap(map) {
|
|
3401
3427
|
overrideErrorMap = map;
|
|
3402
3428
|
}
|
|
@@ -3409,7 +3435,7 @@ var init_errors = __esm(() => {
|
|
|
3409
3435
|
overrideErrorMap = en_default;
|
|
3410
3436
|
});
|
|
3411
3437
|
|
|
3412
|
-
// node_modules/zod/v3/helpers/parseUtil.js
|
|
3438
|
+
// ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
|
|
3413
3439
|
function addIssueToContext(ctx, issueData) {
|
|
3414
3440
|
const overrideMap = getErrorMap();
|
|
3415
3441
|
const issue = makeIssue({
|
|
@@ -3514,10 +3540,10 @@ var init_parseUtil = __esm(() => {
|
|
|
3514
3540
|
});
|
|
3515
3541
|
});
|
|
3516
3542
|
|
|
3517
|
-
// node_modules/zod/v3/helpers/typeAliases.js
|
|
3543
|
+
// ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/typeAliases.js
|
|
3518
3544
|
var init_typeAliases = () => {};
|
|
3519
3545
|
|
|
3520
|
-
// node_modules/zod/v3/helpers/errorUtil.js
|
|
3546
|
+
// ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
|
|
3521
3547
|
var errorUtil;
|
|
3522
3548
|
var init_errorUtil = __esm(() => {
|
|
3523
3549
|
(function(errorUtil2) {
|
|
@@ -3526,7 +3552,7 @@ var init_errorUtil = __esm(() => {
|
|
|
3526
3552
|
})(errorUtil || (errorUtil = {}));
|
|
3527
3553
|
});
|
|
3528
3554
|
|
|
3529
|
-
// node_modules/zod/v3/types.js
|
|
3555
|
+
// ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/types.js
|
|
3530
3556
|
class ParseInputLazyPath {
|
|
3531
3557
|
constructor(parent, value, path, key) {
|
|
3532
3558
|
this._cachedPath = [];
|
|
@@ -6877,7 +6903,7 @@ var init_types2 = __esm(() => {
|
|
|
6877
6903
|
NEVER = INVALID;
|
|
6878
6904
|
});
|
|
6879
6905
|
|
|
6880
|
-
// node_modules/zod/v3/external.js
|
|
6906
|
+
// ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
|
|
6881
6907
|
var exports_external = {};
|
|
6882
6908
|
__export(exports_external, {
|
|
6883
6909
|
void: () => voidType,
|
|
@@ -6997,7 +7023,7 @@ var init_external = __esm(() => {
|
|
|
6997
7023
|
init_ZodError();
|
|
6998
7024
|
});
|
|
6999
7025
|
|
|
7000
|
-
// node_modules/zod/index.js
|
|
7026
|
+
// ../../node_modules/.bun/zod@3.25.76/node_modules/zod/index.js
|
|
7001
7027
|
var init_zod = __esm(() => {
|
|
7002
7028
|
init_external();
|
|
7003
7029
|
init_external();
|
|
@@ -7189,7 +7215,7 @@ var init_redact = __esm(() => {
|
|
|
7189
7215
|
VALUE_PATTERNS = [
|
|
7190
7216
|
{ re: /npm_[A-Za-z0-9]{36,}/, reason: "npm token" },
|
|
7191
7217
|
{ re: /gh[pousr]_[A-Za-z0-9_]{36,}/, reason: "GitHub token" },
|
|
7192
|
-
{ re: /sk-ant-[A-Za-z0-9\-_]{40,}/, reason: "Anthropic API key" },
|
|
7218
|
+
{ re: /sk[-]ant-[A-Za-z0-9\-_]{40,}/, reason: "Anthropic API key" },
|
|
7193
7219
|
{ re: /sk-[A-Za-z0-9]{48,}/, reason: "OpenAI API key" },
|
|
7194
7220
|
{ re: /xoxb-[0-9]+-[A-Za-z0-9\-]+/, reason: "Slack bot token" },
|
|
7195
7221
|
{ re: /AIza[0-9A-Za-z\-_]{35}/, reason: "Google API key" },
|
|
@@ -10718,34 +10744,18 @@ function observeCursorGlobalAuthorityPath(authorityPath, readFile2) {
|
|
|
10718
10744
|
return {
|
|
10719
10745
|
...base,
|
|
10720
10746
|
fileType,
|
|
10721
|
-
status: "
|
|
10747
|
+
status: "unmanaged",
|
|
10722
10748
|
sha256: contentSha256,
|
|
10723
10749
|
markers,
|
|
10724
10750
|
markerSha256,
|
|
10725
10751
|
provenance: {
|
|
10726
10752
|
source: "filesystem",
|
|
10727
|
-
authority: "
|
|
10753
|
+
authority: "unmanaged",
|
|
10728
10754
|
observedPath: authorityPath,
|
|
10729
|
-
detection: "
|
|
10755
|
+
detection: "unknown-content"
|
|
10730
10756
|
}
|
|
10731
10757
|
};
|
|
10732
10758
|
}
|
|
10733
|
-
function isCursorGlobalAuthorityPath(path) {
|
|
10734
|
-
return resolve3(path) === resolve3(join5(homeDir(), CURSOR_GLOBAL_AUTHORITY_RELATIVE_PATH));
|
|
10735
|
-
}
|
|
10736
|
-
function stampCursorGlobalAuthorityMarker(content) {
|
|
10737
|
-
if (CURSOR_GLOBAL_AUTHORITY_MARKER_PATTERN.test(content))
|
|
10738
|
-
return content;
|
|
10739
|
-
const digest = sha2564(content);
|
|
10740
|
-
const markerLine = `<!-- ${CURSOR_GLOBAL_AUTHORITY_MANAGED_MARKER} hash=sha256:${digest} -->`;
|
|
10741
|
-
const frontmatter = content.match(CURSOR_GLOBAL_AUTHORITY_FRONTMATTER_PATTERN)?.[0];
|
|
10742
|
-
if (frontmatter) {
|
|
10743
|
-
return `${frontmatter}${markerLine}
|
|
10744
|
-
${content.slice(frontmatter.length)}`;
|
|
10745
|
-
}
|
|
10746
|
-
return `${markerLine}
|
|
10747
|
-
${content}`;
|
|
10748
|
-
}
|
|
10749
10759
|
function detectCursorAuthorityConflicts(observation = observeCursorGlobalAuthority()) {
|
|
10750
10760
|
if (observation.status === "absent" || observation.status === "managed")
|
|
10751
10761
|
return [];
|
|
@@ -10767,11 +10777,10 @@ function detectCursorAuthorityConflicts(observation = observeCursorGlobalAuthori
|
|
|
10767
10777
|
reason: invalid ? `Cursor fixed global authority ${CURSOR_GLOBAL_AUTHORITY_RELATIVE_PATH} is not a verifiable regular managed file; refusing to render project rules.` : `Cursor fixed global authority ${CURSOR_GLOBAL_AUTHORITY_RELATIVE_PATH} is unmanaged; refusing to guess whether it conflicts with the managed project render.`
|
|
10768
10778
|
}];
|
|
10769
10779
|
}
|
|
10770
|
-
var CURSOR_GLOBAL_AUTHORITY_RELATIVE_PATH = ".cursor/rules/hasna-global.mdc", CURSOR_GLOBAL_AUTHORITY_MAX_BYTES, CURSOR_GLOBAL_AUTHORITY_MANAGED_MARKER = "Managed by @hasna/configs cursor global authority", CURSOR_GLOBAL_AUTHORITY_MARKER_PATTERN
|
|
10780
|
+
var CURSOR_GLOBAL_AUTHORITY_RELATIVE_PATH = ".cursor/rules/hasna-global.mdc", CURSOR_GLOBAL_AUTHORITY_MAX_BYTES, CURSOR_GLOBAL_AUTHORITY_MANAGED_MARKER = "Managed by @hasna/configs cursor global authority", CURSOR_GLOBAL_AUTHORITY_MARKER_PATTERN;
|
|
10771
10781
|
var init_cursor_authority = __esm(() => {
|
|
10772
10782
|
CURSOR_GLOBAL_AUTHORITY_MAX_BYTES = 256 * 1024;
|
|
10773
10783
|
CURSOR_GLOBAL_AUTHORITY_MARKER_PATTERN = /^<!-- Managed by @hasna\/configs cursor global authority hash=(sha256:[a-f0-9]{64}) -->$/m;
|
|
10774
|
-
CURSOR_GLOBAL_AUTHORITY_FRONTMATTER_PATTERN = /^---\n[\s\S]*?\n---(?:\n|$)/;
|
|
10775
10784
|
});
|
|
10776
10785
|
|
|
10777
10786
|
// src/lib/session-render.ts
|
|
@@ -13354,16 +13363,17 @@ function formatCliError(err, env = process.env) {
|
|
|
13354
13363
|
return err instanceof Error ? err.message : String(err);
|
|
13355
13364
|
}
|
|
13356
13365
|
function resolveCloudConfig(env = process.env) {
|
|
13366
|
+
assertNoLegacyStorageMode(env);
|
|
13357
13367
|
const apiUrl = env[API_URL_ENV]?.trim();
|
|
13358
13368
|
const apiKey = env[API_KEY_ENV]?.trim();
|
|
13359
13369
|
if (!apiUrl && !apiKey)
|
|
13360
13370
|
return null;
|
|
13361
13371
|
if (!apiUrl || !apiKey) {
|
|
13362
|
-
throw new Error(`API mode requires BOTH ${API_URL_ENV} and ${API_KEY_ENV}; only ` + `${apiUrl ? API_URL_ENV : API_KEY_ENV} is set. Set both to use the
|
|
13372
|
+
throw new Error(`API mode requires BOTH ${API_URL_ENV} and ${API_KEY_ENV}; only ` + `${apiUrl ? API_URL_ENV : API_KEY_ENV} is set. Set both to use the HTTP API, ` + `or unset both to use the local store.`);
|
|
13363
13373
|
}
|
|
13364
13374
|
return { apiUrl, apiKey };
|
|
13365
13375
|
}
|
|
13366
|
-
function
|
|
13376
|
+
function isApiTransport(env = process.env) {
|
|
13367
13377
|
return resolveCloudConfig(env) !== null;
|
|
13368
13378
|
}
|
|
13369
13379
|
|
|
@@ -13849,6 +13859,7 @@ var init_config_store = __esm(() => {
|
|
|
13849
13859
|
init_types();
|
|
13850
13860
|
init_bounded_read();
|
|
13851
13861
|
init_instruction_graph();
|
|
13862
|
+
init_retired_storage_mode();
|
|
13852
13863
|
CloudHttpError = class CloudHttpError extends Error {
|
|
13853
13864
|
status;
|
|
13854
13865
|
body;
|
|
@@ -13996,9 +14007,8 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
|
|
|
13996
14007
|
throw new ConfigApplyError(`Antigravity rule file ${renderedTargetPath} is ${renderedContent.length} characters; split it before applying because Antigravity limits rule files to ${ANTIGRAVITY_RULE_FILE_CHAR_LIMIT} characters.`);
|
|
13997
14008
|
}
|
|
13998
14009
|
const path = expandPath(renderedTargetPath);
|
|
13999
|
-
const renderedForTarget = isCursorGlobalAuthorityPath(path) ? stampCursorGlobalAuthorityMarker(renderedContent) : renderedContent;
|
|
14000
14010
|
const previousContent = existsSync7(path) ? readFileSync5(path, "utf-8") : null;
|
|
14001
|
-
const changed = previousContent !==
|
|
14011
|
+
const changed = previousContent !== renderedContent;
|
|
14002
14012
|
if (!opts.dryRun) {
|
|
14003
14013
|
const dir = dirname4(path);
|
|
14004
14014
|
if (!existsSync7(dir)) {
|
|
@@ -14008,13 +14018,13 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
|
|
|
14008
14018
|
const store = opts.store ?? resolveConfigStore();
|
|
14009
14019
|
await store.createSnapshot(config.id, previousContent, config.version);
|
|
14010
14020
|
}
|
|
14011
|
-
writeFileSync2(path,
|
|
14021
|
+
writeFileSync2(path, renderedContent, "utf-8");
|
|
14012
14022
|
}
|
|
14013
14023
|
return {
|
|
14014
14024
|
config_id: config.id,
|
|
14015
14025
|
path,
|
|
14016
14026
|
previous_content: previousContent,
|
|
14017
|
-
new_content:
|
|
14027
|
+
new_content: renderedContent,
|
|
14018
14028
|
dry_run: opts.dryRun ?? false,
|
|
14019
14029
|
changed,
|
|
14020
14030
|
primary_changed: changed,
|
|
@@ -14375,7 +14385,6 @@ var init_apply = __esm(() => {
|
|
|
14375
14385
|
init_redact();
|
|
14376
14386
|
init_template();
|
|
14377
14387
|
init_transforms();
|
|
14378
|
-
init_cursor_authority();
|
|
14379
14388
|
});
|
|
14380
14389
|
|
|
14381
14390
|
// src/lib/sync-dir.ts
|
|
@@ -15396,15 +15405,16 @@ var init_package_manager_guard = __esm(() => {
|
|
|
15396
15405
|
TOKEN_VALUE_PATTERNS = [
|
|
15397
15406
|
{ re: /npm_[A-Za-z0-9]{36,}/, rule: "literal-npm-token", detail: "literal npm token-like value" },
|
|
15398
15407
|
{ re: /gh[pousr]_[A-Za-z0-9_]{36,}/, rule: "literal-github-token", detail: "literal GitHub token-like value" },
|
|
15399
|
-
{ re: /sk-ant-[A-Za-z0-9\-_]{40,}/, rule: "literal-anthropic-key", detail: "literal Anthropic key-like value" },
|
|
15408
|
+
{ re: /sk[-]ant-[A-Za-z0-9\-_]{40,}/, rule: "literal-anthropic-key", detail: "literal Anthropic key-like value" },
|
|
15400
15409
|
{ re: /sk-[A-Za-z0-9]{48,}/, rule: "literal-openai-key", detail: "literal OpenAI key-like value" },
|
|
15401
15410
|
{ re: /AKIA[0-9A-Z]{16}/, rule: "literal-aws-access-key", detail: "literal AWS access-key-like value" },
|
|
15402
15411
|
{ re: /xoxb-[0-9]+-[A-Za-z0-9-]+/, rule: "literal-slack-token", detail: "literal Slack token-like value" }
|
|
15403
15412
|
];
|
|
15404
15413
|
});
|
|
15405
15414
|
|
|
15406
|
-
// node_modules/@hasna/events/dist/commander.js
|
|
15415
|
+
// ../../node_modules/.bun/@hasna+events@0.1.15/node_modules/@hasna/events/dist/commander.js
|
|
15407
15416
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
15417
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
15408
15418
|
import { existsSync } from "fs";
|
|
15409
15419
|
import { homedir } from "os";
|
|
15410
15420
|
import { join } from "path";
|
|
@@ -15420,29 +15430,83 @@ function getPathValue(input, path) {
|
|
|
15420
15430
|
return;
|
|
15421
15431
|
}, input);
|
|
15422
15432
|
}
|
|
15423
|
-
function
|
|
15424
|
-
const
|
|
15425
|
-
|
|
15433
|
+
function getFieldValues(input, path) {
|
|
15434
|
+
const values = [];
|
|
15435
|
+
const push = (value) => {
|
|
15436
|
+
if (!values.some((item) => Object.is(item, value)))
|
|
15437
|
+
values.push(value);
|
|
15438
|
+
};
|
|
15439
|
+
if (path.includes(".") && path in input)
|
|
15440
|
+
push(input[path]);
|
|
15441
|
+
const nestedValue = getPathValue(input, path);
|
|
15442
|
+
if (nestedValue !== undefined || !path.includes("."))
|
|
15443
|
+
push(nestedValue);
|
|
15444
|
+
return values;
|
|
15445
|
+
}
|
|
15446
|
+
function wildcardToRegExp(pattern, options = {}) {
|
|
15447
|
+
let body = "";
|
|
15448
|
+
for (let index = 0;index < pattern.length; index += 1) {
|
|
15449
|
+
const char = pattern[index];
|
|
15450
|
+
if (char === "*") {
|
|
15451
|
+
if (pattern[index + 1] === "*") {
|
|
15452
|
+
body += ".*";
|
|
15453
|
+
index += 1;
|
|
15454
|
+
} else {
|
|
15455
|
+
body += options.segmentSafe ? "[^/]*" : ".*";
|
|
15456
|
+
}
|
|
15457
|
+
} else {
|
|
15458
|
+
body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
15459
|
+
}
|
|
15460
|
+
}
|
|
15461
|
+
return new RegExp(`^${body}$`);
|
|
15426
15462
|
}
|
|
15427
|
-
function matchString(value, matcher) {
|
|
15463
|
+
function matchString(value, matcher, options = {}) {
|
|
15428
15464
|
if (matcher === undefined)
|
|
15429
15465
|
return true;
|
|
15430
15466
|
if (value === undefined)
|
|
15431
15467
|
return false;
|
|
15432
15468
|
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
15433
|
-
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
15469
|
+
return matchers.some((item) => wildcardToRegExp(item, options).test(value));
|
|
15434
15470
|
}
|
|
15435
15471
|
function matchRecord(input, matcher) {
|
|
15436
15472
|
if (!matcher)
|
|
15437
15473
|
return true;
|
|
15438
15474
|
return Object.entries(matcher).every(([path, expected]) => {
|
|
15439
|
-
const
|
|
15440
|
-
|
|
15441
|
-
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
15442
|
-
}
|
|
15443
|
-
return actual === expected;
|
|
15475
|
+
const actualValues = getFieldValues(input, path);
|
|
15476
|
+
return matchField(actualValues, expected, path);
|
|
15444
15477
|
});
|
|
15445
15478
|
}
|
|
15479
|
+
function matchField(actualValues, expected, path) {
|
|
15480
|
+
if (isNegativeMatcher(expected)) {
|
|
15481
|
+
return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
|
|
15482
|
+
}
|
|
15483
|
+
return actualValues.some((actual) => matchPositiveField(actual, expected, path));
|
|
15484
|
+
}
|
|
15485
|
+
function matchPositiveField(actual, expected, path) {
|
|
15486
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
15487
|
+
return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
|
|
15488
|
+
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
15489
|
+
}));
|
|
15490
|
+
}
|
|
15491
|
+
if (Array.isArray(actual)) {
|
|
15492
|
+
return actual.some((item) => item === expected);
|
|
15493
|
+
}
|
|
15494
|
+
return actual === expected;
|
|
15495
|
+
}
|
|
15496
|
+
function stringCandidates(actual) {
|
|
15497
|
+
if (actual === undefined)
|
|
15498
|
+
return [];
|
|
15499
|
+
if (Array.isArray(actual)) {
|
|
15500
|
+
return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
|
|
15501
|
+
}
|
|
15502
|
+
return [String(actual)];
|
|
15503
|
+
}
|
|
15504
|
+
function isPrimitiveFieldValue(value) {
|
|
15505
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
15506
|
+
}
|
|
15507
|
+
function isNegativeMatcher(value) {
|
|
15508
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
|
|
15509
|
+
}
|
|
15446
15510
|
function eventMatchesFilter(event, filter) {
|
|
15447
15511
|
return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
|
|
15448
15512
|
}
|
|
@@ -15455,16 +15519,29 @@ function channelMatchesEvent(channel, event) {
|
|
|
15455
15519
|
}
|
|
15456
15520
|
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
15457
15521
|
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
15522
|
+
var LOCAL_JSON_EVENT_CURSOR_PREFIX = "local-json-v1:";
|
|
15523
|
+
var DEFAULT_EVENT_PAGE_LIMIT = 100;
|
|
15524
|
+
var MAX_EVENT_PAGE_LIMIT = 1000;
|
|
15458
15525
|
function getEventsDataDir(override) {
|
|
15459
15526
|
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
|
|
15460
15527
|
}
|
|
15528
|
+
function getActiveEventsDirEnv() {
|
|
15529
|
+
if (process.env[HASNA_EVENTS_DIR_ENV])
|
|
15530
|
+
return HASNA_EVENTS_DIR_ENV;
|
|
15531
|
+
if (process.env[HASNA_EVENTS_HOME_ENV])
|
|
15532
|
+
return HASNA_EVENTS_HOME_ENV;
|
|
15533
|
+
return null;
|
|
15534
|
+
}
|
|
15535
|
+
|
|
15461
15536
|
class JsonEventsStore {
|
|
15462
15537
|
dataDir;
|
|
15538
|
+
runtime;
|
|
15463
15539
|
channelsPath;
|
|
15464
15540
|
eventsPath;
|
|
15465
15541
|
deliveriesPath;
|
|
15466
15542
|
constructor(dataDir = getEventsDataDir()) {
|
|
15467
15543
|
this.dataDir = dataDir;
|
|
15544
|
+
this.runtime = localJsonRuntime(dataDir);
|
|
15468
15545
|
this.channelsPath = join(dataDir, "channels.json");
|
|
15469
15546
|
this.eventsPath = join(dataDir, "events.json");
|
|
15470
15547
|
this.deliveriesPath = join(dataDir, "deliveries.json");
|
|
@@ -15512,13 +15589,58 @@ class JsonEventsStore {
|
|
|
15512
15589
|
await this.writeJson(this.eventsPath, events);
|
|
15513
15590
|
return event;
|
|
15514
15591
|
}
|
|
15515
|
-
async
|
|
15592
|
+
async appendEventOnce(event, options = {}) {
|
|
15593
|
+
await this.init();
|
|
15594
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
15595
|
+
const dedupe = options.dedupe !== false;
|
|
15596
|
+
if (dedupe) {
|
|
15597
|
+
const existing = findEventByIdentity(events, { id: event.id, dedupeKey: event.dedupeKey });
|
|
15598
|
+
if (existing) {
|
|
15599
|
+
return {
|
|
15600
|
+
event: existing,
|
|
15601
|
+
stored: false,
|
|
15602
|
+
deduped: true,
|
|
15603
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
15604
|
+
};
|
|
15605
|
+
}
|
|
15606
|
+
}
|
|
15607
|
+
events.push(event);
|
|
15608
|
+
await this.writeJson(this.eventsPath, events);
|
|
15609
|
+
return {
|
|
15610
|
+
event,
|
|
15611
|
+
stored: true,
|
|
15612
|
+
deduped: false,
|
|
15613
|
+
identity: { id: event.id, dedupeKey: event.dedupeKey }
|
|
15614
|
+
};
|
|
15615
|
+
}
|
|
15616
|
+
async listEvents(options = {}) {
|
|
15516
15617
|
await this.init();
|
|
15517
|
-
|
|
15618
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
15619
|
+
return queryEvents(events, options);
|
|
15620
|
+
}
|
|
15621
|
+
async listEventsPage(options = {}) {
|
|
15622
|
+
await this.init();
|
|
15623
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
15624
|
+
const queried = queryEvents(events, {
|
|
15625
|
+
eventId: options.eventId,
|
|
15626
|
+
source: options.source,
|
|
15627
|
+
type: options.type
|
|
15628
|
+
});
|
|
15629
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
15630
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
15631
|
+
const pageEvents = queried.slice(offset, offset + limit);
|
|
15632
|
+
const nextOffset = offset + pageEvents.length;
|
|
15633
|
+
const hasMore = nextOffset < queried.length;
|
|
15634
|
+
return {
|
|
15635
|
+
events: pageEvents,
|
|
15636
|
+
cursor: options.cursor,
|
|
15637
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
15638
|
+
hasMore
|
|
15639
|
+
};
|
|
15518
15640
|
}
|
|
15519
15641
|
async findEventByIdentity(identity) {
|
|
15520
15642
|
const events = await this.listEvents();
|
|
15521
|
-
return events
|
|
15643
|
+
return findEventByIdentity(events, identity);
|
|
15522
15644
|
}
|
|
15523
15645
|
async appendDelivery(result) {
|
|
15524
15646
|
await this.init();
|
|
@@ -15569,6 +15691,130 @@ class JsonEventsStore {
|
|
|
15569
15691
|
});
|
|
15570
15692
|
}
|
|
15571
15693
|
}
|
|
15694
|
+
function localJsonRuntime(dataDir = getEventsDataDir()) {
|
|
15695
|
+
return {
|
|
15696
|
+
mode: "local-files",
|
|
15697
|
+
name: "json-events-store",
|
|
15698
|
+
remote: false,
|
|
15699
|
+
localFiles: true,
|
|
15700
|
+
localSqlite: false,
|
|
15701
|
+
postgres: false,
|
|
15702
|
+
s3: false,
|
|
15703
|
+
aws: false,
|
|
15704
|
+
durable: true,
|
|
15705
|
+
idempotency: "best-effort-local",
|
|
15706
|
+
replayCursors: true,
|
|
15707
|
+
description: `Local JSON files in ${dataDir}; no SQLite, Postgres, S3, or AWS runtime is configured by this store.`
|
|
15708
|
+
};
|
|
15709
|
+
}
|
|
15710
|
+
function encodeLocalJsonEventCursor(offset, options = {}) {
|
|
15711
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
15712
|
+
throw new Error(`Invalid event cursor offset: ${offset}`);
|
|
15713
|
+
const payload = {
|
|
15714
|
+
offset,
|
|
15715
|
+
eventId: options.eventId,
|
|
15716
|
+
source: options.source,
|
|
15717
|
+
type: options.type
|
|
15718
|
+
};
|
|
15719
|
+
return `${LOCAL_JSON_EVENT_CURSOR_PREFIX}${Buffer2.from(JSON.stringify(payload), "utf-8").toString("base64url")}`;
|
|
15720
|
+
}
|
|
15721
|
+
function decodeLocalJsonEventCursor(cursor, options = {}) {
|
|
15722
|
+
if (!cursor)
|
|
15723
|
+
return 0;
|
|
15724
|
+
if (!cursor.startsWith(LOCAL_JSON_EVENT_CURSOR_PREFIX))
|
|
15725
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
15726
|
+
const rawPayload = cursor.slice(LOCAL_JSON_EVENT_CURSOR_PREFIX.length);
|
|
15727
|
+
let payload;
|
|
15728
|
+
try {
|
|
15729
|
+
payload = JSON.parse(Buffer2.from(rawPayload, "base64url").toString("utf-8"));
|
|
15730
|
+
} catch {
|
|
15731
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
15732
|
+
}
|
|
15733
|
+
const offset = payload.offset;
|
|
15734
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
15735
|
+
throw new Error(`Invalid local JSON event cursor: ${cursor}`);
|
|
15736
|
+
assertCursorFilter("eventId", payload.eventId, options.eventId);
|
|
15737
|
+
assertCursorFilter("source", payload.source, options.source);
|
|
15738
|
+
assertCursorFilter("type", payload.type, options.type);
|
|
15739
|
+
return offset;
|
|
15740
|
+
}
|
|
15741
|
+
function normalizeEventPageLimit(limit) {
|
|
15742
|
+
if (limit === undefined)
|
|
15743
|
+
return DEFAULT_EVENT_PAGE_LIMIT;
|
|
15744
|
+
if (!Number.isInteger(limit) || limit < 1)
|
|
15745
|
+
throw new Error(`Event page limit must be a positive integer, got ${limit}`);
|
|
15746
|
+
return Math.min(limit, MAX_EVENT_PAGE_LIMIT);
|
|
15747
|
+
}
|
|
15748
|
+
function queryEvents(events, options) {
|
|
15749
|
+
let rows = events;
|
|
15750
|
+
if (options.eventId)
|
|
15751
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
15752
|
+
if (options.source)
|
|
15753
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
15754
|
+
if (options.type)
|
|
15755
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
15756
|
+
if (options.cursor) {
|
|
15757
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
15758
|
+
rows = rows.slice(offset);
|
|
15759
|
+
}
|
|
15760
|
+
if (options.limit !== undefined)
|
|
15761
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
15762
|
+
return rows;
|
|
15763
|
+
}
|
|
15764
|
+
function assertCursorFilter(name, cursorValue, optionValue) {
|
|
15765
|
+
if (cursorValue !== optionValue)
|
|
15766
|
+
throw new Error(`Local JSON event cursor ${name} filter mismatch`);
|
|
15767
|
+
}
|
|
15768
|
+
function findEventByIdentity(events, identity) {
|
|
15769
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
15770
|
+
}
|
|
15771
|
+
async function getEventsStatus(dataDir) {
|
|
15772
|
+
const store = new JsonEventsStore(dataDir);
|
|
15773
|
+
await store.init();
|
|
15774
|
+
const [channels, events, deliveries] = await Promise.all([
|
|
15775
|
+
store.listChannels(),
|
|
15776
|
+
store.listEvents(),
|
|
15777
|
+
store.listDeliveries()
|
|
15778
|
+
]);
|
|
15779
|
+
const transports = channels.reduce((counts, channel) => {
|
|
15780
|
+
counts[channel.transport] = (counts[channel.transport] ?? 0) + 1;
|
|
15781
|
+
return counts;
|
|
15782
|
+
}, {});
|
|
15783
|
+
return {
|
|
15784
|
+
service: "events",
|
|
15785
|
+
schemaVersion: "1.0",
|
|
15786
|
+
dataDir: store.dataDir,
|
|
15787
|
+
storage: store.runtime,
|
|
15788
|
+
env: {
|
|
15789
|
+
primary: HASNA_EVENTS_DIR_ENV,
|
|
15790
|
+
fallback: HASNA_EVENTS_HOME_ENV,
|
|
15791
|
+
active: getActiveEventsDirEnv()
|
|
15792
|
+
},
|
|
15793
|
+
files: {
|
|
15794
|
+
channels: statusFile(store.dataDir, "channels.json", channels.length),
|
|
15795
|
+
events: statusFile(store.dataDir, "events.json", events.length),
|
|
15796
|
+
deliveries: statusFile(store.dataDir, "deliveries.json", deliveries.length)
|
|
15797
|
+
},
|
|
15798
|
+
counts: {
|
|
15799
|
+
channels: channels.length,
|
|
15800
|
+
enabledChannels: channels.filter((channel) => channel.enabled).length,
|
|
15801
|
+
disabledChannels: channels.filter((channel) => !channel.enabled).length,
|
|
15802
|
+
events: events.length,
|
|
15803
|
+
deliveries: deliveries.length
|
|
15804
|
+
},
|
|
15805
|
+
transports,
|
|
15806
|
+
safety: {
|
|
15807
|
+
includesEventPayloads: false,
|
|
15808
|
+
includesWebhookSecrets: false,
|
|
15809
|
+
listOutputsRedactSecrets: true,
|
|
15810
|
+
statusOutputIsMetadataOnly: true
|
|
15811
|
+
}
|
|
15812
|
+
};
|
|
15813
|
+
}
|
|
15814
|
+
function statusFile(dataDir, fileName, records) {
|
|
15815
|
+
const path = join(dataDir, fileName);
|
|
15816
|
+
return { path, exists: existsSync(path), records };
|
|
15817
|
+
}
|
|
15572
15818
|
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
15573
15819
|
function buildSignatureBase(timestamp, body) {
|
|
15574
15820
|
return `${timestamp}.${body}`;
|
|
@@ -15583,21 +15829,27 @@ function now() {
|
|
|
15583
15829
|
function truncate(value, max = 4096) {
|
|
15584
15830
|
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
15585
15831
|
}
|
|
15586
|
-
function buildWebhookRequest(event, channel) {
|
|
15832
|
+
function buildWebhookRequest(event, channel, options = {}) {
|
|
15587
15833
|
if (!channel.webhook)
|
|
15588
15834
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
15835
|
+
for (const name of Object.keys(channel.webhook.headers ?? {})) {
|
|
15836
|
+
if (/^x-hasna-/i.test(name)) {
|
|
15837
|
+
throw new Error(`Webhook header ${name} is reserved for signed delivery metadata`);
|
|
15838
|
+
}
|
|
15839
|
+
}
|
|
15589
15840
|
const body = JSON.stringify(event);
|
|
15590
|
-
const timestamp =
|
|
15841
|
+
const timestamp = options.timestamp ?? new Date().toISOString();
|
|
15591
15842
|
const headers = {
|
|
15592
15843
|
"Content-Type": "application/json",
|
|
15593
15844
|
"User-Agent": "@hasna/events",
|
|
15594
15845
|
"X-Hasna-Event-Id": event.id,
|
|
15595
15846
|
"X-Hasna-Event-Type": event.type,
|
|
15596
|
-
|
|
15597
|
-
|
|
15847
|
+
...channel.webhook.headers,
|
|
15848
|
+
"X-Hasna-Timestamp": timestamp
|
|
15598
15849
|
};
|
|
15599
|
-
|
|
15600
|
-
|
|
15850
|
+
const secret = options.secret ?? channel.webhook.secret;
|
|
15851
|
+
if (secret) {
|
|
15852
|
+
headers["X-Hasna-Signature"] = signPayload(secret, timestamp, body);
|
|
15601
15853
|
}
|
|
15602
15854
|
return { body, headers };
|
|
15603
15855
|
}
|
|
@@ -15605,7 +15857,21 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
15605
15857
|
if (!channel.webhook)
|
|
15606
15858
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
15607
15859
|
const startedAt = now();
|
|
15608
|
-
|
|
15860
|
+
let secret = channel.webhook.secret;
|
|
15861
|
+
if (channel.webhook.secretRef) {
|
|
15862
|
+
if (!options.secretResolver) {
|
|
15863
|
+
return failedAttempt(startedAt, "Webhook secret reference has no runtime resolver");
|
|
15864
|
+
}
|
|
15865
|
+
try {
|
|
15866
|
+
secret = await options.secretResolver(channel.webhook.secretRef);
|
|
15867
|
+
} catch {
|
|
15868
|
+
return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
|
|
15869
|
+
}
|
|
15870
|
+
if (!secret)
|
|
15871
|
+
return failedAttempt(startedAt, "Webhook secret reference could not be resolved");
|
|
15872
|
+
}
|
|
15873
|
+
const timestamp = (options.now?.() ?? new Date).toISOString();
|
|
15874
|
+
const { body, headers } = buildWebhookRequest(event, channel, { secret, timestamp });
|
|
15609
15875
|
const controller = new AbortController;
|
|
15610
15876
|
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
15611
15877
|
try {
|
|
@@ -15637,6 +15903,15 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
15637
15903
|
clearTimeout(timeout);
|
|
15638
15904
|
}
|
|
15639
15905
|
}
|
|
15906
|
+
function failedAttempt(startedAt, error) {
|
|
15907
|
+
return {
|
|
15908
|
+
attempt: 1,
|
|
15909
|
+
status: "failed",
|
|
15910
|
+
startedAt,
|
|
15911
|
+
completedAt: now(),
|
|
15912
|
+
error
|
|
15913
|
+
};
|
|
15914
|
+
}
|
|
15640
15915
|
async function dispatchCommand(event, channel) {
|
|
15641
15916
|
if (!channel.command)
|
|
15642
15917
|
throw new Error(`Channel ${channel.id} has no command config`);
|
|
@@ -15725,6 +16000,90 @@ function createDeliveryResult(event, channel, attempts) {
|
|
|
15725
16000
|
completedAt: attempts.at(-1)?.completedAt ?? now()
|
|
15726
16001
|
};
|
|
15727
16002
|
}
|
|
16003
|
+
|
|
16004
|
+
class EventValidationError extends Error {
|
|
16005
|
+
eventType;
|
|
16006
|
+
issues;
|
|
16007
|
+
constructor(eventType, issues) {
|
|
16008
|
+
const detail = issues.map((issue) => `${issue.path || "<root>"}: ${issue.message}`).join("; ");
|
|
16009
|
+
super(`Event validation failed for type "${eventType}": ${detail}`);
|
|
16010
|
+
this.name = "EventValidationError";
|
|
16011
|
+
this.eventType = eventType;
|
|
16012
|
+
this.issues = issues;
|
|
16013
|
+
}
|
|
16014
|
+
}
|
|
16015
|
+
|
|
16016
|
+
class EventTypeCatalog {
|
|
16017
|
+
definitions = new Map;
|
|
16018
|
+
register(definition) {
|
|
16019
|
+
this.definitions.set(definition.type, definition);
|
|
16020
|
+
return this;
|
|
16021
|
+
}
|
|
16022
|
+
unregister(type) {
|
|
16023
|
+
return this.definitions.delete(type);
|
|
16024
|
+
}
|
|
16025
|
+
has(type) {
|
|
16026
|
+
return this.definitions.has(type);
|
|
16027
|
+
}
|
|
16028
|
+
get(type) {
|
|
16029
|
+
return this.definitions.get(type);
|
|
16030
|
+
}
|
|
16031
|
+
list() {
|
|
16032
|
+
return [...this.definitions.values()];
|
|
16033
|
+
}
|
|
16034
|
+
validateEvent(event) {
|
|
16035
|
+
const definition = this.definitions.get(event.type);
|
|
16036
|
+
if (!definition)
|
|
16037
|
+
return { ok: true };
|
|
16038
|
+
return definition.validate(event.data, event);
|
|
16039
|
+
}
|
|
16040
|
+
assertEventValid(event) {
|
|
16041
|
+
const result = this.validateEvent(event);
|
|
16042
|
+
if (!result.ok) {
|
|
16043
|
+
throw new EventValidationError(event.type, result.issues);
|
|
16044
|
+
}
|
|
16045
|
+
}
|
|
16046
|
+
}
|
|
16047
|
+
var defaultEventTypeCatalog = new EventTypeCatalog;
|
|
16048
|
+
var APP_EVENT_V1_MAX_DATA_BYTES = 32 * 1024;
|
|
16049
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
16050
|
+
if (paths.length === 0)
|
|
16051
|
+
return event;
|
|
16052
|
+
const copy = structuredClone(event);
|
|
16053
|
+
for (const path of paths) {
|
|
16054
|
+
setPath(copy, path, replacement);
|
|
16055
|
+
}
|
|
16056
|
+
return copy;
|
|
16057
|
+
}
|
|
16058
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
16059
|
+
return redactValue(event, replacement);
|
|
16060
|
+
}
|
|
16061
|
+
function shouldRedactKey(key) {
|
|
16062
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
16063
|
+
}
|
|
16064
|
+
function redactValue(value, replacement) {
|
|
16065
|
+
if (Array.isArray(value))
|
|
16066
|
+
return value.map((item) => redactValue(item, replacement));
|
|
16067
|
+
if (!value || typeof value !== "object")
|
|
16068
|
+
return value;
|
|
16069
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
16070
|
+
key,
|
|
16071
|
+
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
16072
|
+
]));
|
|
16073
|
+
}
|
|
16074
|
+
function setPath(input, path, replacement) {
|
|
16075
|
+
const parts = path.split(".");
|
|
16076
|
+
let cursor = input;
|
|
16077
|
+
for (const part of parts.slice(0, -1)) {
|
|
16078
|
+
const next = cursor[part];
|
|
16079
|
+
if (!next || typeof next !== "object")
|
|
16080
|
+
return;
|
|
16081
|
+
cursor = next;
|
|
16082
|
+
}
|
|
16083
|
+
const last = parts.at(-1);
|
|
16084
|
+
if (last && last in cursor)
|
|
16085
|
+
cursor[last] = replacement;
|
|
16086
|
+
}
|
|
15728
16087
|
function createEvent(input) {
|
|
15729
16088
|
return {
|
|
15730
16089
|
id: input.id ?? randomUUID2(),
|
|
@@ -15745,10 +16104,18 @@ class EventsClient {
|
|
|
15745
16104
|
store;
|
|
15746
16105
|
redactors;
|
|
15747
16106
|
transportOptions;
|
|
16107
|
+
catalog;
|
|
16108
|
+
validateCatalogTypes;
|
|
15748
16109
|
constructor(options = {}) {
|
|
15749
16110
|
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
15750
16111
|
this.redactors = options.redactors ?? [];
|
|
15751
|
-
this.transportOptions = {
|
|
16112
|
+
this.transportOptions = {
|
|
16113
|
+
fetchImpl: options.fetchImpl,
|
|
16114
|
+
secretResolver: options.secretResolver,
|
|
16115
|
+
now: options.now
|
|
16116
|
+
};
|
|
16117
|
+
this.catalog = options.catalog ?? defaultEventTypeCatalog;
|
|
16118
|
+
this.validateCatalogTypes = options.validateCatalogTypes ?? false;
|
|
15752
16119
|
}
|
|
15753
16120
|
async addChannel(input) {
|
|
15754
16121
|
const timestamp = new Date().toISOString();
|
|
@@ -15766,18 +16133,40 @@ class EventsClient {
|
|
|
15766
16133
|
}
|
|
15767
16134
|
async emit(input, options = {}) {
|
|
15768
16135
|
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
15769
|
-
if (options.
|
|
15770
|
-
|
|
15771
|
-
|
|
15772
|
-
|
|
15773
|
-
|
|
15774
|
-
|
|
15775
|
-
|
|
15776
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
15777
|
-
return { event, deliveries, deduped: false };
|
|
15778
|
-
}
|
|
15779
|
-
async listEvents() {
|
|
15780
|
-
|
|
16136
|
+
if (options.validate ?? this.validateCatalogTypes) {
|
|
16137
|
+
this.catalog.assertEventValid(event);
|
|
16138
|
+
}
|
|
16139
|
+
const append = await this.appendEvent(event, { dedupe: options.dedupe !== false });
|
|
16140
|
+
if (append.deduped) {
|
|
16141
|
+
return { event: append.event, deliveries: [], deduped: true };
|
|
16142
|
+
}
|
|
16143
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(append.event);
|
|
16144
|
+
return { event: append.event, deliveries, deduped: false };
|
|
16145
|
+
}
|
|
16146
|
+
async listEvents(options = {}) {
|
|
16147
|
+
if (Object.keys(options).length === 0)
|
|
16148
|
+
return this.store.listEvents();
|
|
16149
|
+
return queryClientEvents(await this.store.listEvents(), options);
|
|
16150
|
+
}
|
|
16151
|
+
async listEventsPage(options = {}) {
|
|
16152
|
+
if (this.store.listEventsPage)
|
|
16153
|
+
return this.store.listEventsPage(options);
|
|
16154
|
+
const events = queryClientEvents(await this.store.listEvents(), {
|
|
16155
|
+
eventId: options.eventId,
|
|
16156
|
+
source: options.source,
|
|
16157
|
+
type: options.type
|
|
16158
|
+
});
|
|
16159
|
+
const offset = decodeLocalJsonEventCursor(options.cursor, options);
|
|
16160
|
+
const limit = normalizeEventPageLimit(options.limit);
|
|
16161
|
+
const pageEvents = events.slice(offset, offset + limit);
|
|
16162
|
+
const nextOffset = offset + pageEvents.length;
|
|
16163
|
+
const hasMore = nextOffset < events.length;
|
|
16164
|
+
return {
|
|
16165
|
+
events: pageEvents,
|
|
16166
|
+
cursor: options.cursor,
|
|
16167
|
+
nextCursor: hasMore ? encodeLocalJsonEventCursor(nextOffset, options) : undefined,
|
|
16168
|
+
hasMore
|
|
16169
|
+
};
|
|
15781
16170
|
}
|
|
15782
16171
|
async listDeliveries() {
|
|
15783
16172
|
return this.store.listDeliveries();
|
|
@@ -15794,7 +16183,7 @@ class EventsClient {
|
|
|
15794
16183
|
}
|
|
15795
16184
|
return deliveries;
|
|
15796
16185
|
}
|
|
15797
|
-
async
|
|
16186
|
+
async matchChannel(id, input = {}) {
|
|
15798
16187
|
const channel = await this.store.getChannel(id);
|
|
15799
16188
|
if (!channel)
|
|
15800
16189
|
throw new Error(`Channel not found: ${id}`);
|
|
@@ -15811,28 +16200,71 @@ class EventsClient {
|
|
|
15811
16200
|
time: input.time,
|
|
15812
16201
|
id: input.id
|
|
15813
16202
|
});
|
|
16203
|
+
const matched = channelMatchesEvent(channel, event);
|
|
16204
|
+
return {
|
|
16205
|
+
channelId: channel.id,
|
|
16206
|
+
matched,
|
|
16207
|
+
event,
|
|
16208
|
+
filters: channel.filters,
|
|
16209
|
+
reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
|
|
16210
|
+
};
|
|
16211
|
+
}
|
|
16212
|
+
async testChannel(id, input = {}, options = {}) {
|
|
16213
|
+
const channel = await this.store.getChannel(id);
|
|
16214
|
+
if (!channel)
|
|
16215
|
+
throw new Error(`Channel not found: ${id}`);
|
|
16216
|
+
const match = await this.matchChannel(id, input);
|
|
16217
|
+
const event = match.event;
|
|
16218
|
+
if (options.honorFilters && !match.matched) {
|
|
16219
|
+
const timestamp = new Date().toISOString();
|
|
16220
|
+
const result2 = createDeliveryResult(event, channel, [{
|
|
16221
|
+
attempt: 1,
|
|
16222
|
+
status: "skipped",
|
|
16223
|
+
startedAt: timestamp,
|
|
16224
|
+
completedAt: timestamp,
|
|
16225
|
+
error: match.reason
|
|
16226
|
+
}]);
|
|
16227
|
+
result2.metadata = { reason: "filter_mismatch" };
|
|
16228
|
+
await this.store.appendDelivery(result2);
|
|
16229
|
+
return result2;
|
|
16230
|
+
}
|
|
15814
16231
|
const eventForChannel = await this.applyRedaction(event, channel);
|
|
15815
16232
|
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
15816
16233
|
await this.store.appendDelivery(result);
|
|
15817
16234
|
return result;
|
|
15818
16235
|
}
|
|
15819
16236
|
async replay(options = {}) {
|
|
15820
|
-
const
|
|
15821
|
-
if (options.eventId && event.id !== options.eventId)
|
|
15822
|
-
return false;
|
|
15823
|
-
if (options.source && event.source !== options.source)
|
|
15824
|
-
return false;
|
|
15825
|
-
if (options.type && event.type !== options.type)
|
|
15826
|
-
return false;
|
|
15827
|
-
return true;
|
|
15828
|
-
});
|
|
16237
|
+
const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
|
|
15829
16238
|
if (options.dryRun)
|
|
15830
|
-
return { events, deliveries: [] };
|
|
16239
|
+
return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
15831
16240
|
const deliveries = [];
|
|
15832
|
-
for (const event of events) {
|
|
16241
|
+
for (const event of page.events) {
|
|
15833
16242
|
deliveries.push(...await this.deliver(event));
|
|
15834
16243
|
}
|
|
15835
|
-
return { events, deliveries };
|
|
16244
|
+
return { events: page.events, deliveries, cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
16245
|
+
}
|
|
16246
|
+
async appendEvent(event, options) {
|
|
16247
|
+
if (this.store.appendEventOnce) {
|
|
16248
|
+
return this.store.appendEventOnce(event, { dedupe: options.dedupe });
|
|
16249
|
+
}
|
|
16250
|
+
if (options.dedupe) {
|
|
16251
|
+
const existing = await this.store.findEventByIdentity({ id: event.id, dedupeKey: event.dedupeKey });
|
|
16252
|
+
if (existing) {
|
|
16253
|
+
return {
|
|
16254
|
+
event: existing,
|
|
16255
|
+
stored: false,
|
|
16256
|
+
deduped: true,
|
|
16257
|
+
identity: { id: existing.id, dedupeKey: existing.dedupeKey }
|
|
16258
|
+
};
|
|
16259
|
+
}
|
|
16260
|
+
}
|
|
16261
|
+
const stored = await this.store.appendEvent(event);
|
|
16262
|
+
return {
|
|
16263
|
+
event: stored,
|
|
16264
|
+
stored: true,
|
|
16265
|
+
deduped: false,
|
|
16266
|
+
identity: { id: stored.id, dedupeKey: stored.dedupeKey }
|
|
16267
|
+
};
|
|
15836
16268
|
}
|
|
15837
16269
|
async applyRedaction(event, channel) {
|
|
15838
16270
|
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
@@ -15859,15 +16291,6 @@ class EventsClient {
|
|
|
15859
16291
|
return createDeliveryResult(event, channel, attempts);
|
|
15860
16292
|
}
|
|
15861
16293
|
}
|
|
15862
|
-
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
15863
|
-
if (paths.length === 0)
|
|
15864
|
-
return event;
|
|
15865
|
-
const copy = structuredClone(event);
|
|
15866
|
-
for (const path of paths) {
|
|
15867
|
-
setPath(copy, path, replacement);
|
|
15868
|
-
}
|
|
15869
|
-
return copy;
|
|
15870
|
-
}
|
|
15871
16294
|
function sanitizeChannelForOutput(channel) {
|
|
15872
16295
|
const copy = structuredClone(channel);
|
|
15873
16296
|
if (copy.webhook?.secret)
|
|
@@ -15880,34 +16303,19 @@ function sanitizeChannelForOutput(channel) {
|
|
|
15880
16303
|
function sanitizeChannelsForOutput(channels) {
|
|
15881
16304
|
return channels.map(sanitizeChannelForOutput);
|
|
15882
16305
|
}
|
|
15883
|
-
function
|
|
15884
|
-
|
|
15885
|
-
|
|
15886
|
-
|
|
15887
|
-
|
|
15888
|
-
|
|
15889
|
-
|
|
15890
|
-
|
|
15891
|
-
|
|
15892
|
-
|
|
15893
|
-
|
|
15894
|
-
|
|
15895
|
-
|
|
15896
|
-
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
15897
|
-
]));
|
|
15898
|
-
}
|
|
15899
|
-
function setPath(input, path, replacement) {
|
|
15900
|
-
const parts = path.split(".");
|
|
15901
|
-
let cursor = input;
|
|
15902
|
-
for (const part of parts.slice(0, -1)) {
|
|
15903
|
-
const next = cursor[part];
|
|
15904
|
-
if (!next || typeof next !== "object")
|
|
15905
|
-
return;
|
|
15906
|
-
cursor = next;
|
|
15907
|
-
}
|
|
15908
|
-
const last = parts.at(-1);
|
|
15909
|
-
if (last && last in cursor)
|
|
15910
|
-
cursor[last] = replacement;
|
|
16306
|
+
function queryClientEvents(events, options) {
|
|
16307
|
+
let rows = events;
|
|
16308
|
+
if (options.eventId)
|
|
16309
|
+
rows = rows.filter((event) => event.id === options.eventId);
|
|
16310
|
+
if (options.source)
|
|
16311
|
+
rows = rows.filter((event) => event.source === options.source);
|
|
16312
|
+
if (options.type)
|
|
16313
|
+
rows = rows.filter((event) => event.type === options.type);
|
|
16314
|
+
if (options.cursor)
|
|
16315
|
+
rows = rows.slice(decodeLocalJsonEventCursor(options.cursor, options));
|
|
16316
|
+
if (options.limit !== undefined)
|
|
16317
|
+
rows = rows.slice(0, normalizeEventPageLimit(options.limit));
|
|
16318
|
+
return rows;
|
|
15911
16319
|
}
|
|
15912
16320
|
function normalizeTime(value) {
|
|
15913
16321
|
if (!value)
|
|
@@ -15921,6 +16329,77 @@ function normalizeRetryPolicy(policy) {
|
|
|
15921
16329
|
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
15922
16330
|
};
|
|
15923
16331
|
}
|
|
16332
|
+
function parseFieldMatchers(values, label, typed = false) {
|
|
16333
|
+
if (!values?.length)
|
|
16334
|
+
return;
|
|
16335
|
+
const result = {};
|
|
16336
|
+
for (const value of values) {
|
|
16337
|
+
const parsed = parseMatcherExpression(value, label);
|
|
16338
|
+
const path = parsed.path;
|
|
16339
|
+
if (path in result)
|
|
16340
|
+
throw new Error(`Duplicate ${label} filter path: ${path}`);
|
|
16341
|
+
const matcherValue = typed ? parseTypedMatcherValue(parsed.rawValue, label) : parsed.rawValue;
|
|
16342
|
+
result[path] = parsed.negated ? { not: matcherValue } : matcherValue;
|
|
16343
|
+
}
|
|
16344
|
+
return result;
|
|
16345
|
+
}
|
|
16346
|
+
function parseFilterOptions(options) {
|
|
16347
|
+
const filter2 = {};
|
|
16348
|
+
if (options.source)
|
|
16349
|
+
filter2.source = options.source;
|
|
16350
|
+
if (options.type)
|
|
16351
|
+
filter2.type = options.type;
|
|
16352
|
+
if (options.subject)
|
|
16353
|
+
filter2.subject = options.subject;
|
|
16354
|
+
if (options.severity)
|
|
16355
|
+
filter2.severity = options.severity;
|
|
16356
|
+
const data = mergeMatchers(parseFieldMatchers(options.data, "data"), parseFieldMatchers(options.dataJson, "data-json", true));
|
|
16357
|
+
const metadata = mergeMatchers(parseFieldMatchers(options.metadata, "metadata"), parseFieldMatchers(options.metadataJson, "metadata-json", true));
|
|
16358
|
+
if (Object.keys(data).length > 0)
|
|
16359
|
+
filter2.data = data;
|
|
16360
|
+
if (Object.keys(metadata).length > 0)
|
|
16361
|
+
filter2.metadata = metadata;
|
|
16362
|
+
return Object.keys(filter2).length > 0 ? [filter2] : undefined;
|
|
16363
|
+
}
|
|
16364
|
+
function mergeMatchers(...records) {
|
|
16365
|
+
const result = {};
|
|
16366
|
+
for (const record of records) {
|
|
16367
|
+
if (!record)
|
|
16368
|
+
continue;
|
|
16369
|
+
for (const [path, value] of Object.entries(record)) {
|
|
16370
|
+
if (path in result)
|
|
16371
|
+
throw new Error(`Duplicate filter path: ${path}`);
|
|
16372
|
+
result[path] = value;
|
|
16373
|
+
}
|
|
16374
|
+
}
|
|
16375
|
+
return result;
|
|
16376
|
+
}
|
|
16377
|
+
function parseTypedMatcherValue(value, label) {
|
|
16378
|
+
const parsed = JSON.parse(value);
|
|
16379
|
+
if (parsed === null || typeof parsed === "string" || typeof parsed === "number" || typeof parsed === "boolean" || Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
|
|
16380
|
+
return parsed;
|
|
16381
|
+
}
|
|
16382
|
+
throw new Error(`${label} filter JSON values must be string, string[], number, boolean, or null`);
|
|
16383
|
+
}
|
|
16384
|
+
function parseMatcherExpression(value, label) {
|
|
16385
|
+
const negativeSeparator = value.indexOf("!=");
|
|
16386
|
+
if (negativeSeparator > 0) {
|
|
16387
|
+
return {
|
|
16388
|
+
path: value.slice(0, negativeSeparator),
|
|
16389
|
+
rawValue: value.slice(negativeSeparator + 2),
|
|
16390
|
+
negated: true
|
|
16391
|
+
};
|
|
16392
|
+
}
|
|
16393
|
+
const separator = value.indexOf("=");
|
|
16394
|
+
if (separator <= 0)
|
|
16395
|
+
throw new Error(`Invalid ${label} filter, expected path=value or path!=value: ${value}`);
|
|
16396
|
+
return {
|
|
16397
|
+
path: value.slice(0, separator),
|
|
16398
|
+
rawValue: value.slice(separator + 1),
|
|
16399
|
+
negated: false
|
|
16400
|
+
};
|
|
16401
|
+
}
|
|
16402
|
+
var DEFAULT_EVENT_LIST_LIMIT = 100;
|
|
15924
16403
|
function parseJsonObject(value, fallback) {
|
|
15925
16404
|
if (!value)
|
|
15926
16405
|
return fallback;
|
|
@@ -15942,18 +16421,6 @@ function parseHeaders(values) {
|
|
|
15942
16421
|
}
|
|
15943
16422
|
return headers;
|
|
15944
16423
|
}
|
|
15945
|
-
function parseFilter(options) {
|
|
15946
|
-
const filter2 = {};
|
|
15947
|
-
if (options.source)
|
|
15948
|
-
filter2.source = options.source;
|
|
15949
|
-
if (options.type)
|
|
15950
|
-
filter2.type = options.type;
|
|
15951
|
-
if (options.subject)
|
|
15952
|
-
filter2.subject = options.subject;
|
|
15953
|
-
if (options.severity)
|
|
15954
|
-
filter2.severity = options.severity;
|
|
15955
|
-
return Object.keys(filter2).length > 0 ? [filter2] : undefined;
|
|
15956
|
-
}
|
|
15957
16424
|
function createClient(options) {
|
|
15958
16425
|
if (options.createClient)
|
|
15959
16426
|
return options.createClient();
|
|
@@ -15965,22 +16432,30 @@ function print(value, json, text) {
|
|
|
15965
16432
|
else
|
|
15966
16433
|
console.log(text);
|
|
15967
16434
|
}
|
|
16435
|
+
function fail(error, json) {
|
|
16436
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16437
|
+
if (json)
|
|
16438
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
16439
|
+
else
|
|
16440
|
+
console.error(message);
|
|
16441
|
+
process.exitCode = 1;
|
|
16442
|
+
}
|
|
15968
16443
|
function hasJsonOption(options) {
|
|
15969
16444
|
return Boolean(options?.json || options?.opts?.().json || options?.optsWithGlobals?.().json || options?.parent?.opts?.().json || options?.parent?.optsWithGlobals?.().json);
|
|
15970
16445
|
}
|
|
15971
16446
|
function wantsJson(actionOptions, command) {
|
|
15972
16447
|
return hasJsonOption(actionOptions) || hasJsonOption(command);
|
|
15973
16448
|
}
|
|
15974
|
-
function
|
|
15975
|
-
const
|
|
15976
|
-
|
|
16449
|
+
function registerChannelCommands(program, options) {
|
|
16450
|
+
const channels = program.command(options.channelsCommandName ?? "channels").description("Manage Hasna event channels");
|
|
16451
|
+
channels.command("add").description("Add or replace a channel").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--data <path=value...>", "Event data field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--metadata <path=value...>", "Event metadata field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--data-json <path=json...>", "Event data field filter with typed JSON value; path!=json negatives supported", collectValues, []).option("--metadata-json <path=json...>", "Event metadata field filter with typed JSON value; path!=json negatives supported", collectValues, []).option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions, command) => {
|
|
15977
16452
|
const timestamp = new Date().toISOString();
|
|
15978
16453
|
const channel = {
|
|
15979
16454
|
id: actionOptions.id,
|
|
15980
16455
|
name: actionOptions.name,
|
|
15981
16456
|
enabled: !actionOptions.disabled,
|
|
15982
16457
|
transport: actionOptions.transport,
|
|
15983
|
-
filters:
|
|
16458
|
+
filters: parseFilterOptions(actionOptions),
|
|
15984
16459
|
retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
|
|
15985
16460
|
redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
|
|
15986
16461
|
createdAt: timestamp,
|
|
@@ -15996,35 +16471,61 @@ function registerWebhookCommands(program, options) {
|
|
|
15996
16471
|
const saved = await createClient(options).addChannel(channel);
|
|
15997
16472
|
print(sanitizeChannelForOutput(saved), wantsJson(actionOptions, command), `Added ${saved.transport} channel ${saved.id}`);
|
|
15998
16473
|
});
|
|
15999
|
-
|
|
16000
|
-
const
|
|
16474
|
+
channels.command("list").description("List configured channels").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
16475
|
+
const channels2 = await createClient(options).listChannels();
|
|
16001
16476
|
if (wantsJson(actionOptions, command)) {
|
|
16002
|
-
console.log(JSON.stringify(sanitizeChannelsForOutput(
|
|
16477
|
+
console.log(JSON.stringify(sanitizeChannelsForOutput(channels2), null, 2));
|
|
16003
16478
|
return;
|
|
16004
16479
|
}
|
|
16005
|
-
if (!
|
|
16480
|
+
if (!channels2.length) {
|
|
16006
16481
|
console.log("No channels configured.");
|
|
16007
16482
|
return;
|
|
16008
16483
|
}
|
|
16009
|
-
for (const channel of
|
|
16484
|
+
for (const channel of channels2) {
|
|
16010
16485
|
console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
|
|
16011
16486
|
}
|
|
16012
16487
|
});
|
|
16013
|
-
|
|
16488
|
+
channels.command("status").description("Show events channel storage status").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
16489
|
+
const status = await getEventsStatus(options.dataDir);
|
|
16490
|
+
print(status, wantsJson(actionOptions, command), `events dataDir: ${status.dataDir}`);
|
|
16491
|
+
});
|
|
16492
|
+
channels.command("remove").description("Remove a channel").argument("<id>", "Channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
16014
16493
|
const removed = await createClient(options).removeChannel(id);
|
|
16015
16494
|
print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
|
|
16016
16495
|
});
|
|
16017
|
-
|
|
16018
|
-
const
|
|
16019
|
-
|
|
16020
|
-
|
|
16021
|
-
|
|
16022
|
-
|
|
16023
|
-
|
|
16024
|
-
|
|
16025
|
-
|
|
16496
|
+
channels.command("test").description("Send a test event to one channel").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--honor-filters", "Skip delivery when the sample event does not match channel filters", false).option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
16497
|
+
const json = wantsJson(actionOptions, command);
|
|
16498
|
+
try {
|
|
16499
|
+
const result = await createClient(options).testChannel(id, {
|
|
16500
|
+
source: actionOptions.source ?? options.source,
|
|
16501
|
+
type: actionOptions.type,
|
|
16502
|
+
subject: actionOptions.subject ?? id,
|
|
16503
|
+
message: actionOptions.message,
|
|
16504
|
+
data: parseJsonObject(actionOptions.data, { test: true }),
|
|
16505
|
+
metadata: parseJsonObject(actionOptions.metadata, {})
|
|
16506
|
+
}, { honorFilters: actionOptions.honorFilters });
|
|
16507
|
+
print(result, json, `${result.status}: ${result.channelId}`);
|
|
16508
|
+
} catch (error) {
|
|
16509
|
+
fail(error, json);
|
|
16510
|
+
}
|
|
16026
16511
|
});
|
|
16027
|
-
|
|
16512
|
+
channels.command("match").description("Check whether a sample event matches one channel without delivering").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events match preview").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
16513
|
+
const json = wantsJson(actionOptions, command);
|
|
16514
|
+
try {
|
|
16515
|
+
const result = await createClient(options).matchChannel(id, {
|
|
16516
|
+
source: actionOptions.source ?? options.source,
|
|
16517
|
+
type: actionOptions.type,
|
|
16518
|
+
subject: actionOptions.subject ?? id,
|
|
16519
|
+
message: actionOptions.message,
|
|
16520
|
+
data: parseJsonObject(actionOptions.data, { test: true }),
|
|
16521
|
+
metadata: parseJsonObject(actionOptions.metadata, {})
|
|
16522
|
+
});
|
|
16523
|
+
print(result, json, `${result.matched ? "matched" : "skipped"}: ${result.channelId}`);
|
|
16524
|
+
} catch (error) {
|
|
16525
|
+
fail(error, json);
|
|
16526
|
+
}
|
|
16527
|
+
});
|
|
16528
|
+
return channels;
|
|
16028
16529
|
}
|
|
16029
16530
|
function registerEventCommands(program, options) {
|
|
16030
16531
|
const events = program.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
|
|
@@ -16041,7 +16542,8 @@ function registerEventCommands(program, options) {
|
|
|
16041
16542
|
}, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
|
|
16042
16543
|
print(result, wantsJson(actionOptions, command), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
|
|
16043
16544
|
});
|
|
16044
|
-
|
|
16545
|
+
const defaultListLimit = options.defaultEventListLimit ?? DEFAULT_EVENT_LIST_LIMIT;
|
|
16546
|
+
events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", `Limit to the most recent <n> events (default ${defaultListLimit}; use 0 for all)`, parseNumber, defaultListLimit).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
16045
16547
|
let rows = await createClient(options).listEvents();
|
|
16046
16548
|
if (actionOptions.source)
|
|
16047
16549
|
rows = rows.filter((event) => event.source === actionOptions.source);
|
|
@@ -16060,19 +16562,21 @@ function registerEventCommands(program, options) {
|
|
|
16060
16562
|
for (const event of rows)
|
|
16061
16563
|
console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
|
|
16062
16564
|
});
|
|
16063
|
-
events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
16565
|
+
events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--cursor <cursor>", "Opaque replay cursor from a previous page").option("--limit <n>", "Maximum events to replay", parseNumber).option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
16064
16566
|
const result = await createClient(options).replay({
|
|
16065
16567
|
eventId: actionOptions.id,
|
|
16066
16568
|
source: actionOptions.source,
|
|
16067
16569
|
type: actionOptions.type,
|
|
16570
|
+
cursor: actionOptions.cursor,
|
|
16571
|
+
limit: actionOptions.limit,
|
|
16068
16572
|
dryRun: actionOptions.dryRun
|
|
16069
16573
|
});
|
|
16070
|
-
print(result, wantsJson(actionOptions, command),
|
|
16574
|
+
print(result, wantsJson(actionOptions, command), replaySummary(result.events.length, result.deliveries.length, result.nextCursor));
|
|
16071
16575
|
});
|
|
16072
16576
|
return events;
|
|
16073
16577
|
}
|
|
16074
16578
|
function registerEventsCommands(program, options) {
|
|
16075
|
-
|
|
16579
|
+
registerChannelCommands(program, options);
|
|
16076
16580
|
registerEventCommands(program, options);
|
|
16077
16581
|
}
|
|
16078
16582
|
function parseNumber(value) {
|
|
@@ -16085,8 +16589,12 @@ function collectValues(value, previous) {
|
|
|
16085
16589
|
previous.push(value);
|
|
16086
16590
|
return previous;
|
|
16087
16591
|
}
|
|
16592
|
+
function replaySummary(events, deliveries, nextCursor) {
|
|
16593
|
+
const suffix = nextCursor ? `, next cursor: ${nextCursor}` : "";
|
|
16594
|
+
return `Replayed ${events} event(s), ${deliveries} delivery result(s)${suffix}`;
|
|
16595
|
+
}
|
|
16088
16596
|
|
|
16089
|
-
// node_modules/commander/esm.mjs
|
|
16597
|
+
// ../../node_modules/.bun/commander@13.1.0/node_modules/commander/esm.mjs
|
|
16090
16598
|
var import__ = __toESM(require_commander(), 1);
|
|
16091
16599
|
var {
|
|
16092
16600
|
program,
|
|
@@ -18403,10 +18911,10 @@ program.command("import <file>").description("Import configs from a tar.gz bundl
|
|
|
18403
18911
|
});
|
|
18404
18912
|
program.command("whoami").description("Show setup summary").action(async () => {
|
|
18405
18913
|
const store = resolveConfigStore();
|
|
18406
|
-
const dbPath =
|
|
18914
|
+
const dbPath = isApiTransport() ? `${process.env["HASNA_INSTRUCTIONS_API_URL"]}/v1` : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join16(homedir8(), ".hasna", "instructions", "instructions.db");
|
|
18407
18915
|
const stats = await store.getConfigStats();
|
|
18408
18916
|
console.log(chalk.bold("@hasna/instructions") + chalk.dim(" v" + pkg.version));
|
|
18409
|
-
console.log(chalk.cyan(
|
|
18917
|
+
console.log(chalk.cyan(isApiTransport() ? "API:" : "DB:") + " " + dbPath);
|
|
18410
18918
|
console.log(chalk.cyan("Total configs:") + " " + (stats["total"] || 0));
|
|
18411
18919
|
console.log();
|
|
18412
18920
|
console.log(chalk.bold("By category:"));
|
|
@@ -18794,8 +19302,6 @@ sessionCmd.command("plan").description("Produce a dry-run render plan for profil
|
|
|
18794
19302
|
...planJsonForOutput(plan),
|
|
18795
19303
|
...globalCoverage ? { globalSourceCoverage: globalCoverage } : {}
|
|
18796
19304
|
});
|
|
18797
|
-
if (plan.blocked)
|
|
18798
|
-
process.exitCode = 1;
|
|
18799
19305
|
return;
|
|
18800
19306
|
}
|
|
18801
19307
|
console.log(chalk.bold(`${plan.tool} session render plan`) + chalk.dim(` (${plan.adapter.mode})`));
|
|
@@ -18822,8 +19328,6 @@ sessionCmd.command("plan").description("Produce a dry-run render plan for profil
|
|
|
18822
19328
|
if (globalCoverage.complete)
|
|
18823
19329
|
console.log(chalk.dim(`global source coverage: ${globalCoverage.expectedSlugs.length}/${globalCoverage.expectedSlugs.length} complete`));
|
|
18824
19330
|
}
|
|
18825
|
-
if (plan.blocked)
|
|
18826
|
-
process.exitCode = 1;
|
|
18827
19331
|
console.log(chalk.dim("Dry run only. No files were written."));
|
|
18828
19332
|
} catch (e) {
|
|
18829
19333
|
console.error(chalk.red(formatCliError(e)));
|
|
@@ -19252,9 +19756,9 @@ DB stats:`));
|
|
|
19252
19756
|
if (count > 0)
|
|
19253
19757
|
console.log(` ${key.padEnd(18)} ${count}`);
|
|
19254
19758
|
}
|
|
19255
|
-
const location =
|
|
19759
|
+
const location = isApiTransport() ? `${process.env["HASNA_INSTRUCTIONS_API_URL"]}/v1` : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join16(homedir8(), ".hasna", "instructions", "instructions.db");
|
|
19256
19760
|
console.log(chalk.dim(`
|
|
19257
|
-
${
|
|
19761
|
+
${isApiTransport() ? "API" : "DB"}: ${location}`));
|
|
19258
19762
|
});
|
|
19259
19763
|
program.command("status").description("Health check: total configs, drift from disk, unredacted secrets").option("--json", "output metadata-only JSON").action(async (opts) => {
|
|
19260
19764
|
const status = await getConfigsStatus(resolveConfigStore());
|
|
@@ -19295,7 +19799,7 @@ program.command("doctor").description("Validate configs: syntax, permissions, mi
|
|
|
19295
19799
|
const store = resolveConfigStore();
|
|
19296
19800
|
let issues = 0;
|
|
19297
19801
|
const pass = (msg) => console.log(chalk.green(" \u2713 ") + msg);
|
|
19298
|
-
const
|
|
19802
|
+
const fail2 = (msg) => {
|
|
19299
19803
|
issues++;
|
|
19300
19804
|
console.log(chalk.red(" \u2717 ") + msg);
|
|
19301
19805
|
};
|
|
@@ -19305,9 +19809,9 @@ program.command("doctor").description("Validate configs: syntax, permissions, mi
|
|
|
19305
19809
|
console.log(chalk.cyan("Known files on disk:"));
|
|
19306
19810
|
for (const k of KNOWN_CONFIGS) {
|
|
19307
19811
|
if (k.rulesDir) {
|
|
19308
|
-
existsSync16(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) :
|
|
19812
|
+
existsSync16(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail2(`${k.rulesDir}/ not found`);
|
|
19309
19813
|
} else {
|
|
19310
|
-
existsSync16(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) :
|
|
19814
|
+
existsSync16(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) : fail2(`${k.path} not found`);
|
|
19311
19815
|
}
|
|
19312
19816
|
}
|
|
19313
19817
|
const allConfigs = await store.listConfigs();
|
|
@@ -19320,7 +19824,7 @@ Stored configs (${allConfigs.length}):`));
|
|
|
19320
19824
|
JSON.parse(c.content);
|
|
19321
19825
|
validCount++;
|
|
19322
19826
|
} catch {
|
|
19323
|
-
|
|
19827
|
+
fail2(`${c.slug}: invalid JSON`);
|
|
19324
19828
|
}
|
|
19325
19829
|
} else {
|
|
19326
19830
|
validCount++;
|
|
@@ -19332,13 +19836,13 @@ Stored configs (${allConfigs.length}):`));
|
|
|
19332
19836
|
const found = scanSecrets(c.content, c.format);
|
|
19333
19837
|
secretCount += found.length;
|
|
19334
19838
|
}
|
|
19335
|
-
secretCount === 0 ? pass("No unredacted secrets") :
|
|
19839
|
+
secretCount === 0 ? pass("No unredacted secrets") : fail2(`${secretCount} unredacted secret(s) \u2014 run \`configs scan --fix\``);
|
|
19336
19840
|
const duplicateTargets = findDuplicateTargetPathGroups(allConfigs);
|
|
19337
19841
|
if (duplicateTargets.length === 0) {
|
|
19338
19842
|
pass("No target path is claimed by more than one config");
|
|
19339
19843
|
} else {
|
|
19340
19844
|
const rowCount = duplicateTargets.reduce((total, group) => total + group.configs.length, 0);
|
|
19341
|
-
|
|
19845
|
+
fail2(`${duplicateTargets.length} target path(s) claimed by more than one config (${rowCount} rows) \u2014 apply order between them is undefined`);
|
|
19342
19846
|
for (const group of duplicateTargets) {
|
|
19343
19847
|
console.log(chalk.yellow(` ${group.target_path}`));
|
|
19344
19848
|
for (const c of group.configs) {
|
|
@@ -19352,7 +19856,7 @@ Stored configs (${allConfigs.length}):`));
|
|
|
19352
19856
|
pass("No reference config name is claimed by more than one row");
|
|
19353
19857
|
} else {
|
|
19354
19858
|
const rowCount = duplicateReferenceNames.reduce((total, group) => total + group.configs.length, 0);
|
|
19355
|
-
|
|
19859
|
+
fail2(`${duplicateReferenceNames.length} reference name(s) claimed by more than one row (${rowCount} rows) \u2014 only one is live in the next render`);
|
|
19356
19860
|
for (const group of duplicateReferenceNames) {
|
|
19357
19861
|
console.log(chalk.yellow(` ${group.name}`));
|
|
19358
19862
|
for (const c of group.configs) {
|