@hasna/instructions 0.4.35 → 0.4.36
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 +8 -7
- package/dist/cli/index.js +191 -699
- 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/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/mode.d.ts +48 -0
- package/dist/generated/storage-kit/mode.d.ts.map +1 -0
- package/dist/generated/storage-kit/pool.d.ts +6 -7
- package/dist/generated/storage-kit/pool.d.ts.map +1 -1
- package/dist/generated/storage-kit/tls.d.ts +3 -30
- 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 +44 -55
- package/dist/lib/apply.d.ts.map +1 -1
- package/dist/lib/cursor-authority.d.ts +36 -3
- package/dist/lib/cursor-authority.d.ts.map +1 -1
- 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 +271 -456
- package/package.json +1 -2
- package/dist/generated/storage-kit/backend.d.ts +0 -20
- package/dist/generated/storage-kit/backend.d.ts.map +0 -1
- package/dist/generated/storage-kit/own.d.ts +0 -11
- package/dist/generated/storage-kit/own.d.ts.map +0 -1
- package/dist/lib/retired-storage-mode.d.ts +0 -8
- package/dist/lib/retired-storage-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
|
-
//
|
|
50
|
+
// 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
|
-
//
|
|
74
|
+
// 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
|
-
//
|
|
153
|
+
// 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
|
-
//
|
|
503
|
+
// 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
|
-
//
|
|
681
|
+
// 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
|
-
//
|
|
754
|
+
// 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
|
+
}
|
|
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
|
-
//
|
|
2064
|
+
// 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,30 +2136,6 @@ 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
|
-
|
|
2163
2139
|
// src/db/database.ts
|
|
2164
2140
|
import { Database } from "bun:sqlite";
|
|
2165
2141
|
import { existsSync as existsSync2, mkdirSync, rmSync } from "fs";
|
|
@@ -2186,9 +2162,8 @@ function slugify(name) {
|
|
|
2186
2162
|
function getDatabase(path) {
|
|
2187
2163
|
if (_db)
|
|
2188
2164
|
return _db;
|
|
2189
|
-
assertNoLegacyStorageMode();
|
|
2190
2165
|
if (!path && process.env["HASNA_INSTRUCTIONS_API_URL"] && process.env["HASNA_INSTRUCTIONS_API_KEY"]) {
|
|
2191
|
-
throw new Error("instructions is
|
|
2166
|
+
throw new Error("instructions is in self_hosted (cloud) mode: this command is not wired to the cloud API yet. " + "Unset HASNA_INSTRUCTIONS_API_URL / HASNA_INSTRUCTIONS_API_KEY to use it against the local store.");
|
|
2192
2167
|
}
|
|
2193
2168
|
const dbPath = path || getDbPath();
|
|
2194
2169
|
const db = new Database(dbPath);
|
|
@@ -2269,7 +2244,6 @@ function insertFeedback(input, db) {
|
|
|
2269
2244
|
}
|
|
2270
2245
|
var MIGRATIONS, _db = null;
|
|
2271
2246
|
var init_database = __esm(() => {
|
|
2272
|
-
init_retired_storage_mode();
|
|
2273
2247
|
MIGRATIONS = [
|
|
2274
2248
|
`
|
|
2275
2249
|
CREATE TABLE IF NOT EXISTS configs (
|
|
@@ -3063,7 +3037,7 @@ Policy reference: \`${CODEWITH_SHARED_TODOS_STORAGE_POLICY_REFERENCE}\`
|
|
|
3063
3037
|
`;
|
|
3064
3038
|
});
|
|
3065
3039
|
|
|
3066
|
-
//
|
|
3040
|
+
// node_modules/zod/v3/helpers/util.js
|
|
3067
3041
|
var util, objectUtil, ZodParsedType, getParsedType = (data) => {
|
|
3068
3042
|
const t = typeof data;
|
|
3069
3043
|
switch (t) {
|
|
@@ -3194,7 +3168,7 @@ var init_util = __esm(() => {
|
|
|
3194
3168
|
]);
|
|
3195
3169
|
});
|
|
3196
3170
|
|
|
3197
|
-
//
|
|
3171
|
+
// node_modules/zod/v3/ZodError.js
|
|
3198
3172
|
var ZodIssueCode, quotelessJson = (obj) => {
|
|
3199
3173
|
const json = JSON.stringify(obj, null, 2);
|
|
3200
3174
|
return json.replace(/"([^"]+)":/g, "$1:");
|
|
@@ -3315,7 +3289,7 @@ var init_ZodError = __esm(() => {
|
|
|
3315
3289
|
};
|
|
3316
3290
|
});
|
|
3317
3291
|
|
|
3318
|
-
//
|
|
3292
|
+
// node_modules/zod/v3/locales/en.js
|
|
3319
3293
|
var errorMap = (issue, _ctx) => {
|
|
3320
3294
|
let message;
|
|
3321
3295
|
switch (issue.code) {
|
|
@@ -3422,7 +3396,7 @@ var init_en = __esm(() => {
|
|
|
3422
3396
|
en_default = errorMap;
|
|
3423
3397
|
});
|
|
3424
3398
|
|
|
3425
|
-
//
|
|
3399
|
+
// node_modules/zod/v3/errors.js
|
|
3426
3400
|
function setErrorMap(map) {
|
|
3427
3401
|
overrideErrorMap = map;
|
|
3428
3402
|
}
|
|
@@ -3435,7 +3409,7 @@ var init_errors = __esm(() => {
|
|
|
3435
3409
|
overrideErrorMap = en_default;
|
|
3436
3410
|
});
|
|
3437
3411
|
|
|
3438
|
-
//
|
|
3412
|
+
// node_modules/zod/v3/helpers/parseUtil.js
|
|
3439
3413
|
function addIssueToContext(ctx, issueData) {
|
|
3440
3414
|
const overrideMap = getErrorMap();
|
|
3441
3415
|
const issue = makeIssue({
|
|
@@ -3540,10 +3514,10 @@ var init_parseUtil = __esm(() => {
|
|
|
3540
3514
|
});
|
|
3541
3515
|
});
|
|
3542
3516
|
|
|
3543
|
-
//
|
|
3517
|
+
// node_modules/zod/v3/helpers/typeAliases.js
|
|
3544
3518
|
var init_typeAliases = () => {};
|
|
3545
3519
|
|
|
3546
|
-
//
|
|
3520
|
+
// node_modules/zod/v3/helpers/errorUtil.js
|
|
3547
3521
|
var errorUtil;
|
|
3548
3522
|
var init_errorUtil = __esm(() => {
|
|
3549
3523
|
(function(errorUtil2) {
|
|
@@ -3552,7 +3526,7 @@ var init_errorUtil = __esm(() => {
|
|
|
3552
3526
|
})(errorUtil || (errorUtil = {}));
|
|
3553
3527
|
});
|
|
3554
3528
|
|
|
3555
|
-
//
|
|
3529
|
+
// node_modules/zod/v3/types.js
|
|
3556
3530
|
class ParseInputLazyPath {
|
|
3557
3531
|
constructor(parent, value, path, key) {
|
|
3558
3532
|
this._cachedPath = [];
|
|
@@ -6903,7 +6877,7 @@ var init_types2 = __esm(() => {
|
|
|
6903
6877
|
NEVER = INVALID;
|
|
6904
6878
|
});
|
|
6905
6879
|
|
|
6906
|
-
//
|
|
6880
|
+
// node_modules/zod/v3/external.js
|
|
6907
6881
|
var exports_external = {};
|
|
6908
6882
|
__export(exports_external, {
|
|
6909
6883
|
void: () => voidType,
|
|
@@ -7023,7 +6997,7 @@ var init_external = __esm(() => {
|
|
|
7023
6997
|
init_ZodError();
|
|
7024
6998
|
});
|
|
7025
6999
|
|
|
7026
|
-
//
|
|
7000
|
+
// node_modules/zod/index.js
|
|
7027
7001
|
var init_zod = __esm(() => {
|
|
7028
7002
|
init_external();
|
|
7029
7003
|
init_external();
|
|
@@ -7215,7 +7189,7 @@ var init_redact = __esm(() => {
|
|
|
7215
7189
|
VALUE_PATTERNS = [
|
|
7216
7190
|
{ re: /npm_[A-Za-z0-9]{36,}/, reason: "npm token" },
|
|
7217
7191
|
{ re: /gh[pousr]_[A-Za-z0-9_]{36,}/, reason: "GitHub token" },
|
|
7218
|
-
{ re: /sk
|
|
7192
|
+
{ re: /sk-ant-[A-Za-z0-9\-_]{40,}/, reason: "Anthropic API key" },
|
|
7219
7193
|
{ re: /sk-[A-Za-z0-9]{48,}/, reason: "OpenAI API key" },
|
|
7220
7194
|
{ re: /xoxb-[0-9]+-[A-Za-z0-9\-]+/, reason: "Slack bot token" },
|
|
7221
7195
|
{ re: /AIza[0-9A-Za-z\-_]{35}/, reason: "Google API key" },
|
|
@@ -10589,15 +10563,11 @@ function sha2564(content) {
|
|
|
10589
10563
|
function homeDir() {
|
|
10590
10564
|
return process.env["HOME"] || homedir3();
|
|
10591
10565
|
}
|
|
10592
|
-
function markerPayload(content, markerLine) {
|
|
10593
|
-
const
|
|
10594
|
-
|
|
10595
|
-
|
|
10596
|
-
|
|
10597
|
-
if (content.startsWith(markerLine))
|
|
10598
|
-
return content.slice(markerLine.length).replace(/^\n/, "");
|
|
10599
|
-
return content.replace(`${markerLine}
|
|
10600
|
-
`, "").replace(markerLine, "");
|
|
10566
|
+
function markerPayload(content, markerLine, markerIndex) {
|
|
10567
|
+
const index = markerIndex ?? content.indexOf(markerLine);
|
|
10568
|
+
if (index < 0)
|
|
10569
|
+
return content;
|
|
10570
|
+
return content.slice(0, index) + content.slice(index + markerLine.length).replace(/^\n/, "");
|
|
10601
10571
|
}
|
|
10602
10572
|
function baseObservation(path) {
|
|
10603
10573
|
return {
|
|
@@ -10724,7 +10694,7 @@ function observeCursorGlobalAuthorityPath(authorityPath, readFile2) {
|
|
|
10724
10694
|
};
|
|
10725
10695
|
}
|
|
10726
10696
|
const markerLine = markerMatch[0];
|
|
10727
|
-
const payloadSha256 = sha2564(markerPayload(content, markerLine));
|
|
10697
|
+
const payloadSha256 = sha2564(markerPayload(content, markerLine, markerMatch.index ?? -1));
|
|
10728
10698
|
if (payloadSha256 !== markerSha256) {
|
|
10729
10699
|
return {
|
|
10730
10700
|
...base,
|
|
@@ -10744,18 +10714,34 @@ function observeCursorGlobalAuthorityPath(authorityPath, readFile2) {
|
|
|
10744
10714
|
return {
|
|
10745
10715
|
...base,
|
|
10746
10716
|
fileType,
|
|
10747
|
-
status: "
|
|
10717
|
+
status: "managed",
|
|
10748
10718
|
sha256: contentSha256,
|
|
10749
10719
|
markers,
|
|
10750
10720
|
markerSha256,
|
|
10751
10721
|
provenance: {
|
|
10752
10722
|
source: "filesystem",
|
|
10753
|
-
authority: "
|
|
10723
|
+
authority: "managed",
|
|
10754
10724
|
observedPath: authorityPath,
|
|
10755
|
-
detection: "
|
|
10725
|
+
detection: "managed-marker"
|
|
10756
10726
|
}
|
|
10757
10727
|
};
|
|
10758
10728
|
}
|
|
10729
|
+
function isCursorGlobalAuthorityPath(path) {
|
|
10730
|
+
return resolve3(path) === resolve3(join5(homeDir(), CURSOR_GLOBAL_AUTHORITY_RELATIVE_PATH));
|
|
10731
|
+
}
|
|
10732
|
+
function stampCursorGlobalAuthorityMarker(content) {
|
|
10733
|
+
if (CURSOR_GLOBAL_AUTHORITY_MARKER_PATTERN.test(content))
|
|
10734
|
+
return content;
|
|
10735
|
+
const digest = sha2564(content);
|
|
10736
|
+
const markerLine = `<!-- ${CURSOR_GLOBAL_AUTHORITY_MANAGED_MARKER} hash=sha256:${digest} -->`;
|
|
10737
|
+
const frontmatter = content.match(CURSOR_GLOBAL_AUTHORITY_FRONTMATTER_PATTERN)?.[0];
|
|
10738
|
+
if (frontmatter) {
|
|
10739
|
+
return `${frontmatter}${markerLine}
|
|
10740
|
+
${content.slice(frontmatter.length)}`;
|
|
10741
|
+
}
|
|
10742
|
+
return `${markerLine}
|
|
10743
|
+
${content}`;
|
|
10744
|
+
}
|
|
10759
10745
|
function detectCursorAuthorityConflicts(observation = observeCursorGlobalAuthority()) {
|
|
10760
10746
|
if (observation.status === "absent" || observation.status === "managed")
|
|
10761
10747
|
return [];
|
|
@@ -10777,10 +10763,11 @@ function detectCursorAuthorityConflicts(observation = observeCursorGlobalAuthori
|
|
|
10777
10763
|
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.`
|
|
10778
10764
|
}];
|
|
10779
10765
|
}
|
|
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;
|
|
10766
|
+
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, CURSOR_GLOBAL_AUTHORITY_FRONTMATTER_PATTERN;
|
|
10781
10767
|
var init_cursor_authority = __esm(() => {
|
|
10782
10768
|
CURSOR_GLOBAL_AUTHORITY_MAX_BYTES = 256 * 1024;
|
|
10783
10769
|
CURSOR_GLOBAL_AUTHORITY_MARKER_PATTERN = /^<!-- Managed by @hasna\/configs cursor global authority hash=(sha256:[a-f0-9]{64}) -->$/m;
|
|
10770
|
+
CURSOR_GLOBAL_AUTHORITY_FRONTMATTER_PATTERN = /^---\n[\s\S]*?\n---(?:\n|$)/;
|
|
10784
10771
|
});
|
|
10785
10772
|
|
|
10786
10773
|
// src/lib/session-render.ts
|
|
@@ -13363,17 +13350,16 @@ function formatCliError(err, env = process.env) {
|
|
|
13363
13350
|
return err instanceof Error ? err.message : String(err);
|
|
13364
13351
|
}
|
|
13365
13352
|
function resolveCloudConfig(env = process.env) {
|
|
13366
|
-
assertNoLegacyStorageMode(env);
|
|
13367
13353
|
const apiUrl = env[API_URL_ENV]?.trim();
|
|
13368
13354
|
const apiKey = env[API_KEY_ENV]?.trim();
|
|
13369
13355
|
if (!apiUrl && !apiKey)
|
|
13370
13356
|
return null;
|
|
13371
13357
|
if (!apiUrl || !apiKey) {
|
|
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
|
|
13358
|
+
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 cloud API, ` + `or unset both to use the local store.`);
|
|
13373
13359
|
}
|
|
13374
13360
|
return { apiUrl, apiKey };
|
|
13375
13361
|
}
|
|
13376
|
-
function
|
|
13362
|
+
function isCloudMode(env = process.env) {
|
|
13377
13363
|
return resolveCloudConfig(env) !== null;
|
|
13378
13364
|
}
|
|
13379
13365
|
|
|
@@ -13859,7 +13845,6 @@ var init_config_store = __esm(() => {
|
|
|
13859
13845
|
init_types();
|
|
13860
13846
|
init_bounded_read();
|
|
13861
13847
|
init_instruction_graph();
|
|
13862
|
-
init_retired_storage_mode();
|
|
13863
13848
|
CloudHttpError = class CloudHttpError extends Error {
|
|
13864
13849
|
status;
|
|
13865
13850
|
body;
|
|
@@ -14007,8 +13992,9 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
|
|
|
14007
13992
|
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.`);
|
|
14008
13993
|
}
|
|
14009
13994
|
const path = expandPath(renderedTargetPath);
|
|
13995
|
+
const renderedForTarget = isCursorGlobalAuthorityPath(path) ? stampCursorGlobalAuthorityMarker(renderedContent) : renderedContent;
|
|
14010
13996
|
const previousContent = existsSync7(path) ? readFileSync5(path, "utf-8") : null;
|
|
14011
|
-
const changed = previousContent !==
|
|
13997
|
+
const changed = previousContent !== renderedForTarget;
|
|
14012
13998
|
if (!opts.dryRun) {
|
|
14013
13999
|
const dir = dirname4(path);
|
|
14014
14000
|
if (!existsSync7(dir)) {
|
|
@@ -14018,13 +14004,13 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
|
|
|
14018
14004
|
const store = opts.store ?? resolveConfigStore();
|
|
14019
14005
|
await store.createSnapshot(config.id, previousContent, config.version);
|
|
14020
14006
|
}
|
|
14021
|
-
writeFileSync2(path,
|
|
14007
|
+
writeFileSync2(path, renderedForTarget, "utf-8");
|
|
14022
14008
|
}
|
|
14023
14009
|
return {
|
|
14024
14010
|
config_id: config.id,
|
|
14025
14011
|
path,
|
|
14026
14012
|
previous_content: previousContent,
|
|
14027
|
-
new_content:
|
|
14013
|
+
new_content: renderedForTarget,
|
|
14028
14014
|
dry_run: opts.dryRun ?? false,
|
|
14029
14015
|
changed,
|
|
14030
14016
|
primary_changed: changed,
|
|
@@ -14385,6 +14371,7 @@ var init_apply = __esm(() => {
|
|
|
14385
14371
|
init_redact();
|
|
14386
14372
|
init_template();
|
|
14387
14373
|
init_transforms();
|
|
14374
|
+
init_cursor_authority();
|
|
14388
14375
|
});
|
|
14389
14376
|
|
|
14390
14377
|
// src/lib/sync-dir.ts
|
|
@@ -15405,16 +15392,15 @@ var init_package_manager_guard = __esm(() => {
|
|
|
15405
15392
|
TOKEN_VALUE_PATTERNS = [
|
|
15406
15393
|
{ re: /npm_[A-Za-z0-9]{36,}/, rule: "literal-npm-token", detail: "literal npm token-like value" },
|
|
15407
15394
|
{ re: /gh[pousr]_[A-Za-z0-9_]{36,}/, rule: "literal-github-token", detail: "literal GitHub token-like value" },
|
|
15408
|
-
{ re: /sk
|
|
15395
|
+
{ re: /sk-ant-[A-Za-z0-9\-_]{40,}/, rule: "literal-anthropic-key", detail: "literal Anthropic key-like value" },
|
|
15409
15396
|
{ re: /sk-[A-Za-z0-9]{48,}/, rule: "literal-openai-key", detail: "literal OpenAI key-like value" },
|
|
15410
15397
|
{ re: /AKIA[0-9A-Z]{16}/, rule: "literal-aws-access-key", detail: "literal AWS access-key-like value" },
|
|
15411
15398
|
{ re: /xoxb-[0-9]+-[A-Za-z0-9-]+/, rule: "literal-slack-token", detail: "literal Slack token-like value" }
|
|
15412
15399
|
];
|
|
15413
15400
|
});
|
|
15414
15401
|
|
|
15415
|
-
//
|
|
15402
|
+
// node_modules/@hasna/events/dist/commander.js
|
|
15416
15403
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
15417
|
-
import { Buffer as Buffer2 } from "buffer";
|
|
15418
15404
|
import { existsSync } from "fs";
|
|
15419
15405
|
import { homedir } from "os";
|
|
15420
15406
|
import { join } from "path";
|
|
@@ -15430,83 +15416,29 @@ function getPathValue(input, path) {
|
|
|
15430
15416
|
return;
|
|
15431
15417
|
}, input);
|
|
15432
15418
|
}
|
|
15433
|
-
function
|
|
15434
|
-
const
|
|
15435
|
-
|
|
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}$`);
|
|
15419
|
+
function wildcardToRegExp(pattern) {
|
|
15420
|
+
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
|
|
15421
|
+
return new RegExp(`^${escaped}$`);
|
|
15462
15422
|
}
|
|
15463
|
-
function matchString(value, matcher
|
|
15423
|
+
function matchString(value, matcher) {
|
|
15464
15424
|
if (matcher === undefined)
|
|
15465
15425
|
return true;
|
|
15466
15426
|
if (value === undefined)
|
|
15467
15427
|
return false;
|
|
15468
15428
|
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
15469
|
-
return matchers.some((item) => wildcardToRegExp(item
|
|
15429
|
+
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
15470
15430
|
}
|
|
15471
15431
|
function matchRecord(input, matcher) {
|
|
15472
15432
|
if (!matcher)
|
|
15473
15433
|
return true;
|
|
15474
15434
|
return Object.entries(matcher).every(([path, expected]) => {
|
|
15475
|
-
const
|
|
15476
|
-
|
|
15435
|
+
const actual = getPathValue(input, path);
|
|
15436
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
15437
|
+
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
15438
|
+
}
|
|
15439
|
+
return actual === expected;
|
|
15477
15440
|
});
|
|
15478
15441
|
}
|
|
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
|
-
}
|
|
15510
15442
|
function eventMatchesFilter(event, filter) {
|
|
15511
15443
|
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);
|
|
15512
15444
|
}
|
|
@@ -15519,29 +15451,16 @@ function channelMatchesEvent(channel, event) {
|
|
|
15519
15451
|
}
|
|
15520
15452
|
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
15521
15453
|
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;
|
|
15525
15454
|
function getEventsDataDir(override) {
|
|
15526
15455
|
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
|
|
15527
15456
|
}
|
|
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
|
-
|
|
15536
15457
|
class JsonEventsStore {
|
|
15537
15458
|
dataDir;
|
|
15538
|
-
runtime;
|
|
15539
15459
|
channelsPath;
|
|
15540
15460
|
eventsPath;
|
|
15541
15461
|
deliveriesPath;
|
|
15542
15462
|
constructor(dataDir = getEventsDataDir()) {
|
|
15543
15463
|
this.dataDir = dataDir;
|
|
15544
|
-
this.runtime = localJsonRuntime(dataDir);
|
|
15545
15464
|
this.channelsPath = join(dataDir, "channels.json");
|
|
15546
15465
|
this.eventsPath = join(dataDir, "events.json");
|
|
15547
15466
|
this.deliveriesPath = join(dataDir, "deliveries.json");
|
|
@@ -15589,58 +15508,13 @@ class JsonEventsStore {
|
|
|
15589
15508
|
await this.writeJson(this.eventsPath, events);
|
|
15590
15509
|
return event;
|
|
15591
15510
|
}
|
|
15592
|
-
async
|
|
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 = {}) {
|
|
15511
|
+
async listEvents() {
|
|
15617
15512
|
await this.init();
|
|
15618
|
-
|
|
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
|
-
};
|
|
15513
|
+
return this.readJson(this.eventsPath, []);
|
|
15640
15514
|
}
|
|
15641
15515
|
async findEventByIdentity(identity) {
|
|
15642
15516
|
const events = await this.listEvents();
|
|
15643
|
-
return
|
|
15517
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
15644
15518
|
}
|
|
15645
15519
|
async appendDelivery(result) {
|
|
15646
15520
|
await this.init();
|
|
@@ -15691,130 +15565,6 @@ class JsonEventsStore {
|
|
|
15691
15565
|
});
|
|
15692
15566
|
}
|
|
15693
15567
|
}
|
|
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
|
-
}
|
|
15818
15568
|
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
15819
15569
|
function buildSignatureBase(timestamp, body) {
|
|
15820
15570
|
return `${timestamp}.${body}`;
|
|
@@ -15829,27 +15579,21 @@ function now() {
|
|
|
15829
15579
|
function truncate(value, max = 4096) {
|
|
15830
15580
|
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
15831
15581
|
}
|
|
15832
|
-
function buildWebhookRequest(event, channel
|
|
15582
|
+
function buildWebhookRequest(event, channel) {
|
|
15833
15583
|
if (!channel.webhook)
|
|
15834
15584
|
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
|
-
}
|
|
15840
15585
|
const body = JSON.stringify(event);
|
|
15841
|
-
const timestamp =
|
|
15586
|
+
const timestamp = event.time;
|
|
15842
15587
|
const headers = {
|
|
15843
15588
|
"Content-Type": "application/json",
|
|
15844
15589
|
"User-Agent": "@hasna/events",
|
|
15845
15590
|
"X-Hasna-Event-Id": event.id,
|
|
15846
15591
|
"X-Hasna-Event-Type": event.type,
|
|
15847
|
-
|
|
15848
|
-
|
|
15592
|
+
"X-Hasna-Timestamp": timestamp,
|
|
15593
|
+
...channel.webhook.headers
|
|
15849
15594
|
};
|
|
15850
|
-
|
|
15851
|
-
|
|
15852
|
-
headers["X-Hasna-Signature"] = signPayload(secret, timestamp, body);
|
|
15595
|
+
if (channel.webhook.secret) {
|
|
15596
|
+
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
15853
15597
|
}
|
|
15854
15598
|
return { body, headers };
|
|
15855
15599
|
}
|
|
@@ -15857,21 +15601,7 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
15857
15601
|
if (!channel.webhook)
|
|
15858
15602
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
15859
15603
|
const startedAt = now();
|
|
15860
|
-
|
|
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 });
|
|
15604
|
+
const { body, headers } = buildWebhookRequest(event, channel);
|
|
15875
15605
|
const controller = new AbortController;
|
|
15876
15606
|
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
15877
15607
|
try {
|
|
@@ -15903,15 +15633,6 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
15903
15633
|
clearTimeout(timeout);
|
|
15904
15634
|
}
|
|
15905
15635
|
}
|
|
15906
|
-
function failedAttempt(startedAt, error) {
|
|
15907
|
-
return {
|
|
15908
|
-
attempt: 1,
|
|
15909
|
-
status: "failed",
|
|
15910
|
-
startedAt,
|
|
15911
|
-
completedAt: now(),
|
|
15912
|
-
error
|
|
15913
|
-
};
|
|
15914
|
-
}
|
|
15915
15636
|
async function dispatchCommand(event, channel) {
|
|
15916
15637
|
if (!channel.command)
|
|
15917
15638
|
throw new Error(`Channel ${channel.id} has no command config`);
|
|
@@ -16000,90 +15721,6 @@ function createDeliveryResult(event, channel, attempts) {
|
|
|
16000
15721
|
completedAt: attempts.at(-1)?.completedAt ?? now()
|
|
16001
15722
|
};
|
|
16002
15723
|
}
|
|
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
|
-
}
|
|
16087
15724
|
function createEvent(input) {
|
|
16088
15725
|
return {
|
|
16089
15726
|
id: input.id ?? randomUUID2(),
|
|
@@ -16104,18 +15741,10 @@ class EventsClient {
|
|
|
16104
15741
|
store;
|
|
16105
15742
|
redactors;
|
|
16106
15743
|
transportOptions;
|
|
16107
|
-
catalog;
|
|
16108
|
-
validateCatalogTypes;
|
|
16109
15744
|
constructor(options = {}) {
|
|
16110
15745
|
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
16111
15746
|
this.redactors = options.redactors ?? [];
|
|
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;
|
|
15747
|
+
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
16119
15748
|
}
|
|
16120
15749
|
async addChannel(input) {
|
|
16121
15750
|
const timestamp = new Date().toISOString();
|
|
@@ -16133,40 +15762,18 @@ class EventsClient {
|
|
|
16133
15762
|
}
|
|
16134
15763
|
async emit(input, options = {}) {
|
|
16135
15764
|
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
16136
|
-
if (options.
|
|
16137
|
-
this.
|
|
16138
|
-
|
|
16139
|
-
|
|
16140
|
-
|
|
16141
|
-
|
|
16142
|
-
|
|
16143
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(
|
|
16144
|
-
return { event
|
|
16145
|
-
}
|
|
16146
|
-
async listEvents(
|
|
16147
|
-
|
|
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
|
-
};
|
|
15765
|
+
if (options.dedupe !== false) {
|
|
15766
|
+
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
15767
|
+
if (existing) {
|
|
15768
|
+
return { event: existing, deliveries: [], deduped: true };
|
|
15769
|
+
}
|
|
15770
|
+
}
|
|
15771
|
+
await this.store.appendEvent(event);
|
|
15772
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
15773
|
+
return { event, deliveries, deduped: false };
|
|
15774
|
+
}
|
|
15775
|
+
async listEvents() {
|
|
15776
|
+
return this.store.listEvents();
|
|
16170
15777
|
}
|
|
16171
15778
|
async listDeliveries() {
|
|
16172
15779
|
return this.store.listDeliveries();
|
|
@@ -16183,7 +15790,7 @@ class EventsClient {
|
|
|
16183
15790
|
}
|
|
16184
15791
|
return deliveries;
|
|
16185
15792
|
}
|
|
16186
|
-
async
|
|
15793
|
+
async testChannel(id, input = {}) {
|
|
16187
15794
|
const channel = await this.store.getChannel(id);
|
|
16188
15795
|
if (!channel)
|
|
16189
15796
|
throw new Error(`Channel not found: ${id}`);
|
|
@@ -16200,71 +15807,28 @@ class EventsClient {
|
|
|
16200
15807
|
time: input.time,
|
|
16201
15808
|
id: input.id
|
|
16202
15809
|
});
|
|
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
|
-
}
|
|
16231
15810
|
const eventForChannel = await this.applyRedaction(event, channel);
|
|
16232
15811
|
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
16233
15812
|
await this.store.appendDelivery(result);
|
|
16234
15813
|
return result;
|
|
16235
15814
|
}
|
|
16236
15815
|
async replay(options = {}) {
|
|
16237
|
-
const
|
|
15816
|
+
const events = (await this.store.listEvents()).filter((event) => {
|
|
15817
|
+
if (options.eventId && event.id !== options.eventId)
|
|
15818
|
+
return false;
|
|
15819
|
+
if (options.source && event.source !== options.source)
|
|
15820
|
+
return false;
|
|
15821
|
+
if (options.type && event.type !== options.type)
|
|
15822
|
+
return false;
|
|
15823
|
+
return true;
|
|
15824
|
+
});
|
|
16238
15825
|
if (options.dryRun)
|
|
16239
|
-
return { events
|
|
15826
|
+
return { events, deliveries: [] };
|
|
16240
15827
|
const deliveries = [];
|
|
16241
|
-
for (const event of
|
|
15828
|
+
for (const event of events) {
|
|
16242
15829
|
deliveries.push(...await this.deliver(event));
|
|
16243
15830
|
}
|
|
16244
|
-
return { events
|
|
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
|
-
};
|
|
15831
|
+
return { events, deliveries };
|
|
16268
15832
|
}
|
|
16269
15833
|
async applyRedaction(event, channel) {
|
|
16270
15834
|
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
@@ -16291,6 +15855,15 @@ class EventsClient {
|
|
|
16291
15855
|
return createDeliveryResult(event, channel, attempts);
|
|
16292
15856
|
}
|
|
16293
15857
|
}
|
|
15858
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
15859
|
+
if (paths.length === 0)
|
|
15860
|
+
return event;
|
|
15861
|
+
const copy = structuredClone(event);
|
|
15862
|
+
for (const path of paths) {
|
|
15863
|
+
setPath(copy, path, replacement);
|
|
15864
|
+
}
|
|
15865
|
+
return copy;
|
|
15866
|
+
}
|
|
16294
15867
|
function sanitizeChannelForOutput(channel) {
|
|
16295
15868
|
const copy = structuredClone(channel);
|
|
16296
15869
|
if (copy.webhook?.secret)
|
|
@@ -16303,19 +15876,34 @@ function sanitizeChannelForOutput(channel) {
|
|
|
16303
15876
|
function sanitizeChannelsForOutput(channels) {
|
|
16304
15877
|
return channels.map(sanitizeChannelForOutput);
|
|
16305
15878
|
}
|
|
16306
|
-
function
|
|
16307
|
-
|
|
16308
|
-
|
|
16309
|
-
|
|
16310
|
-
|
|
16311
|
-
|
|
16312
|
-
|
|
16313
|
-
|
|
16314
|
-
|
|
16315
|
-
|
|
16316
|
-
|
|
16317
|
-
|
|
16318
|
-
|
|
15879
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
15880
|
+
return redactValue(event, replacement);
|
|
15881
|
+
}
|
|
15882
|
+
function shouldRedactKey(key) {
|
|
15883
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
15884
|
+
}
|
|
15885
|
+
function redactValue(value, replacement) {
|
|
15886
|
+
if (Array.isArray(value))
|
|
15887
|
+
return value.map((item) => redactValue(item, replacement));
|
|
15888
|
+
if (!value || typeof value !== "object")
|
|
15889
|
+
return value;
|
|
15890
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
15891
|
+
key,
|
|
15892
|
+
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
15893
|
+
]));
|
|
15894
|
+
}
|
|
15895
|
+
function setPath(input, path, replacement) {
|
|
15896
|
+
const parts = path.split(".");
|
|
15897
|
+
let cursor = input;
|
|
15898
|
+
for (const part of parts.slice(0, -1)) {
|
|
15899
|
+
const next = cursor[part];
|
|
15900
|
+
if (!next || typeof next !== "object")
|
|
15901
|
+
return;
|
|
15902
|
+
cursor = next;
|
|
15903
|
+
}
|
|
15904
|
+
const last = parts.at(-1);
|
|
15905
|
+
if (last && last in cursor)
|
|
15906
|
+
cursor[last] = replacement;
|
|
16319
15907
|
}
|
|
16320
15908
|
function normalizeTime(value) {
|
|
16321
15909
|
if (!value)
|
|
@@ -16329,77 +15917,6 @@ function normalizeRetryPolicy(policy) {
|
|
|
16329
15917
|
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
16330
15918
|
};
|
|
16331
15919
|
}
|
|
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;
|
|
16403
15920
|
function parseJsonObject(value, fallback) {
|
|
16404
15921
|
if (!value)
|
|
16405
15922
|
return fallback;
|
|
@@ -16421,6 +15938,18 @@ function parseHeaders(values) {
|
|
|
16421
15938
|
}
|
|
16422
15939
|
return headers;
|
|
16423
15940
|
}
|
|
15941
|
+
function parseFilter(options) {
|
|
15942
|
+
const filter2 = {};
|
|
15943
|
+
if (options.source)
|
|
15944
|
+
filter2.source = options.source;
|
|
15945
|
+
if (options.type)
|
|
15946
|
+
filter2.type = options.type;
|
|
15947
|
+
if (options.subject)
|
|
15948
|
+
filter2.subject = options.subject;
|
|
15949
|
+
if (options.severity)
|
|
15950
|
+
filter2.severity = options.severity;
|
|
15951
|
+
return Object.keys(filter2).length > 0 ? [filter2] : undefined;
|
|
15952
|
+
}
|
|
16424
15953
|
function createClient(options) {
|
|
16425
15954
|
if (options.createClient)
|
|
16426
15955
|
return options.createClient();
|
|
@@ -16432,30 +15961,22 @@ function print(value, json, text) {
|
|
|
16432
15961
|
else
|
|
16433
15962
|
console.log(text);
|
|
16434
15963
|
}
|
|
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
|
-
}
|
|
16443
15964
|
function hasJsonOption(options) {
|
|
16444
15965
|
return Boolean(options?.json || options?.opts?.().json || options?.optsWithGlobals?.().json || options?.parent?.opts?.().json || options?.parent?.optsWithGlobals?.().json);
|
|
16445
15966
|
}
|
|
16446
15967
|
function wantsJson(actionOptions, command) {
|
|
16447
15968
|
return hasJsonOption(actionOptions) || hasJsonOption(command);
|
|
16448
15969
|
}
|
|
16449
|
-
function
|
|
16450
|
-
const
|
|
16451
|
-
|
|
15970
|
+
function registerWebhookCommands(program, options) {
|
|
15971
|
+
const webhooks = program.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
|
|
15972
|
+
webhooks.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/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("--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) => {
|
|
16452
15973
|
const timestamp = new Date().toISOString();
|
|
16453
15974
|
const channel = {
|
|
16454
15975
|
id: actionOptions.id,
|
|
16455
15976
|
name: actionOptions.name,
|
|
16456
15977
|
enabled: !actionOptions.disabled,
|
|
16457
15978
|
transport: actionOptions.transport,
|
|
16458
|
-
filters:
|
|
15979
|
+
filters: parseFilter(actionOptions),
|
|
16459
15980
|
retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
|
|
16460
15981
|
redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
|
|
16461
15982
|
createdAt: timestamp,
|
|
@@ -16471,61 +15992,35 @@ function registerChannelCommands(program, options) {
|
|
|
16471
15992
|
const saved = await createClient(options).addChannel(channel);
|
|
16472
15993
|
print(sanitizeChannelForOutput(saved), wantsJson(actionOptions, command), `Added ${saved.transport} channel ${saved.id}`);
|
|
16473
15994
|
});
|
|
16474
|
-
|
|
16475
|
-
const
|
|
15995
|
+
webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
15996
|
+
const channels = await createClient(options).listChannels();
|
|
16476
15997
|
if (wantsJson(actionOptions, command)) {
|
|
16477
|
-
console.log(JSON.stringify(sanitizeChannelsForOutput(
|
|
15998
|
+
console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
|
|
16478
15999
|
return;
|
|
16479
16000
|
}
|
|
16480
|
-
if (!
|
|
16001
|
+
if (!channels.length) {
|
|
16481
16002
|
console.log("No channels configured.");
|
|
16482
16003
|
return;
|
|
16483
16004
|
}
|
|
16484
|
-
for (const channel of
|
|
16005
|
+
for (const channel of channels) {
|
|
16485
16006
|
console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
|
|
16486
16007
|
}
|
|
16487
16008
|
});
|
|
16488
|
-
|
|
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) => {
|
|
16009
|
+
webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
16493
16010
|
const removed = await createClient(options).removeChannel(id);
|
|
16494
16011
|
print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
|
|
16495
16012
|
});
|
|
16496
|
-
|
|
16497
|
-
const
|
|
16498
|
-
|
|
16499
|
-
|
|
16500
|
-
|
|
16501
|
-
|
|
16502
|
-
|
|
16503
|
-
|
|
16504
|
-
|
|
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
|
-
}
|
|
16511
|
-
});
|
|
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
|
-
}
|
|
16013
|
+
webhooks.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/channel identifier").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("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
16014
|
+
const result = await createClient(options).testChannel(id, {
|
|
16015
|
+
source: options.source,
|
|
16016
|
+
type: actionOptions.type,
|
|
16017
|
+
subject: actionOptions.subject ?? id,
|
|
16018
|
+
message: actionOptions.message,
|
|
16019
|
+
data: parseJsonObject(actionOptions.data, { test: true })
|
|
16020
|
+
});
|
|
16021
|
+
print(result, wantsJson(actionOptions, command), `${result.status}: ${result.channelId}`);
|
|
16527
16022
|
});
|
|
16528
|
-
return
|
|
16023
|
+
return webhooks;
|
|
16529
16024
|
}
|
|
16530
16025
|
function registerEventCommands(program, options) {
|
|
16531
16026
|
const events = program.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
|
|
@@ -16542,8 +16037,7 @@ function registerEventCommands(program, options) {
|
|
|
16542
16037
|
}, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
|
|
16543
16038
|
print(result, wantsJson(actionOptions, command), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
|
|
16544
16039
|
});
|
|
16545
|
-
|
|
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) => {
|
|
16040
|
+
events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", "Limit results", parseNumber).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
16547
16041
|
let rows = await createClient(options).listEvents();
|
|
16548
16042
|
if (actionOptions.source)
|
|
16549
16043
|
rows = rows.filter((event) => event.source === actionOptions.source);
|
|
@@ -16562,21 +16056,19 @@ function registerEventCommands(program, options) {
|
|
|
16562
16056
|
for (const event of rows)
|
|
16563
16057
|
console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
|
|
16564
16058
|
});
|
|
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("--
|
|
16059
|
+
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) => {
|
|
16566
16060
|
const result = await createClient(options).replay({
|
|
16567
16061
|
eventId: actionOptions.id,
|
|
16568
16062
|
source: actionOptions.source,
|
|
16569
16063
|
type: actionOptions.type,
|
|
16570
|
-
cursor: actionOptions.cursor,
|
|
16571
|
-
limit: actionOptions.limit,
|
|
16572
16064
|
dryRun: actionOptions.dryRun
|
|
16573
16065
|
});
|
|
16574
|
-
print(result, wantsJson(actionOptions, command),
|
|
16066
|
+
print(result, wantsJson(actionOptions, command), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
|
|
16575
16067
|
});
|
|
16576
16068
|
return events;
|
|
16577
16069
|
}
|
|
16578
16070
|
function registerEventsCommands(program, options) {
|
|
16579
|
-
|
|
16071
|
+
registerWebhookCommands(program, options);
|
|
16580
16072
|
registerEventCommands(program, options);
|
|
16581
16073
|
}
|
|
16582
16074
|
function parseNumber(value) {
|
|
@@ -16589,12 +16081,8 @@ function collectValues(value, previous) {
|
|
|
16589
16081
|
previous.push(value);
|
|
16590
16082
|
return previous;
|
|
16591
16083
|
}
|
|
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
|
-
}
|
|
16596
16084
|
|
|
16597
|
-
//
|
|
16085
|
+
// node_modules/commander/esm.mjs
|
|
16598
16086
|
var import__ = __toESM(require_commander(), 1);
|
|
16599
16087
|
var {
|
|
16600
16088
|
program,
|
|
@@ -18911,10 +18399,10 @@ program.command("import <file>").description("Import configs from a tar.gz bundl
|
|
|
18911
18399
|
});
|
|
18912
18400
|
program.command("whoami").description("Show setup summary").action(async () => {
|
|
18913
18401
|
const store = resolveConfigStore();
|
|
18914
|
-
const dbPath =
|
|
18402
|
+
const dbPath = isCloudMode() ? `${process.env["HASNA_INSTRUCTIONS_API_URL"]}/v1 (self_hosted)` : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join16(homedir8(), ".hasna", "instructions", "instructions.db");
|
|
18915
18403
|
const stats = await store.getConfigStats();
|
|
18916
18404
|
console.log(chalk.bold("@hasna/instructions") + chalk.dim(" v" + pkg.version));
|
|
18917
|
-
console.log(chalk.cyan(
|
|
18405
|
+
console.log(chalk.cyan(isCloudMode() ? "API:" : "DB:") + " " + dbPath);
|
|
18918
18406
|
console.log(chalk.cyan("Total configs:") + " " + (stats["total"] || 0));
|
|
18919
18407
|
console.log();
|
|
18920
18408
|
console.log(chalk.bold("By category:"));
|
|
@@ -19302,6 +18790,8 @@ sessionCmd.command("plan").description("Produce a dry-run render plan for profil
|
|
|
19302
18790
|
...planJsonForOutput(plan),
|
|
19303
18791
|
...globalCoverage ? { globalSourceCoverage: globalCoverage } : {}
|
|
19304
18792
|
});
|
|
18793
|
+
if (plan.blocked)
|
|
18794
|
+
process.exitCode = 1;
|
|
19305
18795
|
return;
|
|
19306
18796
|
}
|
|
19307
18797
|
console.log(chalk.bold(`${plan.tool} session render plan`) + chalk.dim(` (${plan.adapter.mode})`));
|
|
@@ -19328,6 +18818,8 @@ sessionCmd.command("plan").description("Produce a dry-run render plan for profil
|
|
|
19328
18818
|
if (globalCoverage.complete)
|
|
19329
18819
|
console.log(chalk.dim(`global source coverage: ${globalCoverage.expectedSlugs.length}/${globalCoverage.expectedSlugs.length} complete`));
|
|
19330
18820
|
}
|
|
18821
|
+
if (plan.blocked)
|
|
18822
|
+
process.exitCode = 1;
|
|
19331
18823
|
console.log(chalk.dim("Dry run only. No files were written."));
|
|
19332
18824
|
} catch (e) {
|
|
19333
18825
|
console.error(chalk.red(formatCliError(e)));
|
|
@@ -19756,9 +19248,9 @@ DB stats:`));
|
|
|
19756
19248
|
if (count > 0)
|
|
19757
19249
|
console.log(` ${key.padEnd(18)} ${count}`);
|
|
19758
19250
|
}
|
|
19759
|
-
const location =
|
|
19251
|
+
const location = isCloudMode() ? `${process.env["HASNA_INSTRUCTIONS_API_URL"]}/v1 (self_hosted)` : process.env["HASNA_INSTRUCTIONS_DB_PATH"] || join16(homedir8(), ".hasna", "instructions", "instructions.db");
|
|
19760
19252
|
console.log(chalk.dim(`
|
|
19761
|
-
${
|
|
19253
|
+
${isCloudMode() ? "API" : "DB"}: ${location}`));
|
|
19762
19254
|
});
|
|
19763
19255
|
program.command("status").description("Health check: total configs, drift from disk, unredacted secrets").option("--json", "output metadata-only JSON").action(async (opts) => {
|
|
19764
19256
|
const status = await getConfigsStatus(resolveConfigStore());
|
|
@@ -19799,7 +19291,7 @@ program.command("doctor").description("Validate configs: syntax, permissions, mi
|
|
|
19799
19291
|
const store = resolveConfigStore();
|
|
19800
19292
|
let issues = 0;
|
|
19801
19293
|
const pass = (msg) => console.log(chalk.green(" \u2713 ") + msg);
|
|
19802
|
-
const
|
|
19294
|
+
const fail = (msg) => {
|
|
19803
19295
|
issues++;
|
|
19804
19296
|
console.log(chalk.red(" \u2717 ") + msg);
|
|
19805
19297
|
};
|
|
@@ -19809,9 +19301,9 @@ program.command("doctor").description("Validate configs: syntax, permissions, mi
|
|
|
19809
19301
|
console.log(chalk.cyan("Known files on disk:"));
|
|
19810
19302
|
for (const k of KNOWN_CONFIGS) {
|
|
19811
19303
|
if (k.rulesDir) {
|
|
19812
|
-
existsSync16(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) :
|
|
19304
|
+
existsSync16(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail(`${k.rulesDir}/ not found`);
|
|
19813
19305
|
} else {
|
|
19814
|
-
existsSync16(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) :
|
|
19306
|
+
existsSync16(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) : fail(`${k.path} not found`);
|
|
19815
19307
|
}
|
|
19816
19308
|
}
|
|
19817
19309
|
const allConfigs = await store.listConfigs();
|
|
@@ -19824,7 +19316,7 @@ Stored configs (${allConfigs.length}):`));
|
|
|
19824
19316
|
JSON.parse(c.content);
|
|
19825
19317
|
validCount++;
|
|
19826
19318
|
} catch {
|
|
19827
|
-
|
|
19319
|
+
fail(`${c.slug}: invalid JSON`);
|
|
19828
19320
|
}
|
|
19829
19321
|
} else {
|
|
19830
19322
|
validCount++;
|
|
@@ -19836,13 +19328,13 @@ Stored configs (${allConfigs.length}):`));
|
|
|
19836
19328
|
const found = scanSecrets(c.content, c.format);
|
|
19837
19329
|
secretCount += found.length;
|
|
19838
19330
|
}
|
|
19839
|
-
secretCount === 0 ? pass("No unredacted secrets") :
|
|
19331
|
+
secretCount === 0 ? pass("No unredacted secrets") : fail(`${secretCount} unredacted secret(s) \u2014 run \`configs scan --fix\``);
|
|
19840
19332
|
const duplicateTargets = findDuplicateTargetPathGroups(allConfigs);
|
|
19841
19333
|
if (duplicateTargets.length === 0) {
|
|
19842
19334
|
pass("No target path is claimed by more than one config");
|
|
19843
19335
|
} else {
|
|
19844
19336
|
const rowCount = duplicateTargets.reduce((total, group) => total + group.configs.length, 0);
|
|
19845
|
-
|
|
19337
|
+
fail(`${duplicateTargets.length} target path(s) claimed by more than one config (${rowCount} rows) \u2014 apply order between them is undefined`);
|
|
19846
19338
|
for (const group of duplicateTargets) {
|
|
19847
19339
|
console.log(chalk.yellow(` ${group.target_path}`));
|
|
19848
19340
|
for (const c of group.configs) {
|
|
@@ -19856,7 +19348,7 @@ Stored configs (${allConfigs.length}):`));
|
|
|
19856
19348
|
pass("No reference config name is claimed by more than one row");
|
|
19857
19349
|
} else {
|
|
19858
19350
|
const rowCount = duplicateReferenceNames.reduce((total, group) => total + group.configs.length, 0);
|
|
19859
|
-
|
|
19351
|
+
fail(`${duplicateReferenceNames.length} reference name(s) claimed by more than one row (${rowCount} rows) \u2014 only one is live in the next render`);
|
|
19860
19352
|
for (const group of duplicateReferenceNames) {
|
|
19861
19353
|
console.log(chalk.yellow(` ${group.name}`));
|
|
19862
19354
|
for (const c of group.configs) {
|