@hasna/instructions 0.4.33 → 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 +682 -155
- 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 +39 -14
- 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 +43 -14
- 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" },
|
|
@@ -13337,16 +13363,17 @@ function formatCliError(err, env = process.env) {
|
|
|
13337
13363
|
return err instanceof Error ? err.message : String(err);
|
|
13338
13364
|
}
|
|
13339
13365
|
function resolveCloudConfig(env = process.env) {
|
|
13366
|
+
assertNoLegacyStorageMode(env);
|
|
13340
13367
|
const apiUrl = env[API_URL_ENV]?.trim();
|
|
13341
13368
|
const apiKey = env[API_KEY_ENV]?.trim();
|
|
13342
13369
|
if (!apiUrl && !apiKey)
|
|
13343
13370
|
return null;
|
|
13344
13371
|
if (!apiUrl || !apiKey) {
|
|
13345
|
-
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.`);
|
|
13346
13373
|
}
|
|
13347
13374
|
return { apiUrl, apiKey };
|
|
13348
13375
|
}
|
|
13349
|
-
function
|
|
13376
|
+
function isApiTransport(env = process.env) {
|
|
13350
13377
|
return resolveCloudConfig(env) !== null;
|
|
13351
13378
|
}
|
|
13352
13379
|
|
|
@@ -13832,6 +13859,7 @@ var init_config_store = __esm(() => {
|
|
|
13832
13859
|
init_types();
|
|
13833
13860
|
init_bounded_read();
|
|
13834
13861
|
init_instruction_graph();
|
|
13862
|
+
init_retired_storage_mode();
|
|
13835
13863
|
CloudHttpError = class CloudHttpError extends Error {
|
|
13836
13864
|
status;
|
|
13837
13865
|
body;
|
|
@@ -15377,15 +15405,16 @@ var init_package_manager_guard = __esm(() => {
|
|
|
15377
15405
|
TOKEN_VALUE_PATTERNS = [
|
|
15378
15406
|
{ re: /npm_[A-Za-z0-9]{36,}/, rule: "literal-npm-token", detail: "literal npm token-like value" },
|
|
15379
15407
|
{ re: /gh[pousr]_[A-Za-z0-9_]{36,}/, rule: "literal-github-token", detail: "literal GitHub token-like value" },
|
|
15380
|
-
{ 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" },
|
|
15381
15409
|
{ re: /sk-[A-Za-z0-9]{48,}/, rule: "literal-openai-key", detail: "literal OpenAI key-like value" },
|
|
15382
15410
|
{ re: /AKIA[0-9A-Z]{16}/, rule: "literal-aws-access-key", detail: "literal AWS access-key-like value" },
|
|
15383
15411
|
{ re: /xoxb-[0-9]+-[A-Za-z0-9-]+/, rule: "literal-slack-token", detail: "literal Slack token-like value" }
|
|
15384
15412
|
];
|
|
15385
15413
|
});
|
|
15386
15414
|
|
|
15387
|
-
// node_modules/@hasna/events/dist/commander.js
|
|
15415
|
+
// ../../node_modules/.bun/@hasna+events@0.1.15/node_modules/@hasna/events/dist/commander.js
|
|
15388
15416
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
15417
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
15389
15418
|
import { existsSync } from "fs";
|
|
15390
15419
|
import { homedir } from "os";
|
|
15391
15420
|
import { join } from "path";
|
|
@@ -15401,29 +15430,83 @@ function getPathValue(input, path) {
|
|
|
15401
15430
|
return;
|
|
15402
15431
|
}, input);
|
|
15403
15432
|
}
|
|
15404
|
-
function
|
|
15405
|
-
const
|
|
15406
|
-
|
|
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}$`);
|
|
15407
15462
|
}
|
|
15408
|
-
function matchString(value, matcher) {
|
|
15463
|
+
function matchString(value, matcher, options = {}) {
|
|
15409
15464
|
if (matcher === undefined)
|
|
15410
15465
|
return true;
|
|
15411
15466
|
if (value === undefined)
|
|
15412
15467
|
return false;
|
|
15413
15468
|
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
15414
|
-
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
15469
|
+
return matchers.some((item) => wildcardToRegExp(item, options).test(value));
|
|
15415
15470
|
}
|
|
15416
15471
|
function matchRecord(input, matcher) {
|
|
15417
15472
|
if (!matcher)
|
|
15418
15473
|
return true;
|
|
15419
15474
|
return Object.entries(matcher).every(([path, expected]) => {
|
|
15420
|
-
const
|
|
15421
|
-
|
|
15422
|
-
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
15423
|
-
}
|
|
15424
|
-
return actual === expected;
|
|
15475
|
+
const actualValues = getFieldValues(input, path);
|
|
15476
|
+
return matchField(actualValues, expected, path);
|
|
15425
15477
|
});
|
|
15426
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
|
+
}
|
|
15427
15510
|
function eventMatchesFilter(event, filter) {
|
|
15428
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);
|
|
15429
15512
|
}
|
|
@@ -15436,16 +15519,29 @@ function channelMatchesEvent(channel, event) {
|
|
|
15436
15519
|
}
|
|
15437
15520
|
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
15438
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;
|
|
15439
15525
|
function getEventsDataDir(override) {
|
|
15440
15526
|
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
|
|
15441
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
|
+
|
|
15442
15536
|
class JsonEventsStore {
|
|
15443
15537
|
dataDir;
|
|
15538
|
+
runtime;
|
|
15444
15539
|
channelsPath;
|
|
15445
15540
|
eventsPath;
|
|
15446
15541
|
deliveriesPath;
|
|
15447
15542
|
constructor(dataDir = getEventsDataDir()) {
|
|
15448
15543
|
this.dataDir = dataDir;
|
|
15544
|
+
this.runtime = localJsonRuntime(dataDir);
|
|
15449
15545
|
this.channelsPath = join(dataDir, "channels.json");
|
|
15450
15546
|
this.eventsPath = join(dataDir, "events.json");
|
|
15451
15547
|
this.deliveriesPath = join(dataDir, "deliveries.json");
|
|
@@ -15493,13 +15589,58 @@ class JsonEventsStore {
|
|
|
15493
15589
|
await this.writeJson(this.eventsPath, events);
|
|
15494
15590
|
return event;
|
|
15495
15591
|
}
|
|
15496
|
-
async
|
|
15592
|
+
async appendEventOnce(event, options = {}) {
|
|
15497
15593
|
await this.init();
|
|
15498
|
-
|
|
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 = {}) {
|
|
15617
|
+
await this.init();
|
|
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
|
+
};
|
|
15499
15640
|
}
|
|
15500
15641
|
async findEventByIdentity(identity) {
|
|
15501
15642
|
const events = await this.listEvents();
|
|
15502
|
-
return events
|
|
15643
|
+
return findEventByIdentity(events, identity);
|
|
15503
15644
|
}
|
|
15504
15645
|
async appendDelivery(result) {
|
|
15505
15646
|
await this.init();
|
|
@@ -15550,6 +15691,130 @@ class JsonEventsStore {
|
|
|
15550
15691
|
});
|
|
15551
15692
|
}
|
|
15552
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
|
+
}
|
|
15553
15818
|
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
15554
15819
|
function buildSignatureBase(timestamp, body) {
|
|
15555
15820
|
return `${timestamp}.${body}`;
|
|
@@ -15564,21 +15829,27 @@ function now() {
|
|
|
15564
15829
|
function truncate(value, max = 4096) {
|
|
15565
15830
|
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
15566
15831
|
}
|
|
15567
|
-
function buildWebhookRequest(event, channel) {
|
|
15832
|
+
function buildWebhookRequest(event, channel, options = {}) {
|
|
15568
15833
|
if (!channel.webhook)
|
|
15569
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
|
+
}
|
|
15570
15840
|
const body = JSON.stringify(event);
|
|
15571
|
-
const timestamp =
|
|
15841
|
+
const timestamp = options.timestamp ?? new Date().toISOString();
|
|
15572
15842
|
const headers = {
|
|
15573
15843
|
"Content-Type": "application/json",
|
|
15574
15844
|
"User-Agent": "@hasna/events",
|
|
15575
15845
|
"X-Hasna-Event-Id": event.id,
|
|
15576
15846
|
"X-Hasna-Event-Type": event.type,
|
|
15577
|
-
|
|
15578
|
-
|
|
15847
|
+
...channel.webhook.headers,
|
|
15848
|
+
"X-Hasna-Timestamp": timestamp
|
|
15579
15849
|
};
|
|
15580
|
-
|
|
15581
|
-
|
|
15850
|
+
const secret = options.secret ?? channel.webhook.secret;
|
|
15851
|
+
if (secret) {
|
|
15852
|
+
headers["X-Hasna-Signature"] = signPayload(secret, timestamp, body);
|
|
15582
15853
|
}
|
|
15583
15854
|
return { body, headers };
|
|
15584
15855
|
}
|
|
@@ -15586,7 +15857,21 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
15586
15857
|
if (!channel.webhook)
|
|
15587
15858
|
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
15588
15859
|
const startedAt = now();
|
|
15589
|
-
|
|
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 });
|
|
15590
15875
|
const controller = new AbortController;
|
|
15591
15876
|
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
15592
15877
|
try {
|
|
@@ -15618,6 +15903,15 @@ async function dispatchWebhook(event, channel, options = {}) {
|
|
|
15618
15903
|
clearTimeout(timeout);
|
|
15619
15904
|
}
|
|
15620
15905
|
}
|
|
15906
|
+
function failedAttempt(startedAt, error) {
|
|
15907
|
+
return {
|
|
15908
|
+
attempt: 1,
|
|
15909
|
+
status: "failed",
|
|
15910
|
+
startedAt,
|
|
15911
|
+
completedAt: now(),
|
|
15912
|
+
error
|
|
15913
|
+
};
|
|
15914
|
+
}
|
|
15621
15915
|
async function dispatchCommand(event, channel) {
|
|
15622
15916
|
if (!channel.command)
|
|
15623
15917
|
throw new Error(`Channel ${channel.id} has no command config`);
|
|
@@ -15706,6 +16000,90 @@ function createDeliveryResult(event, channel, attempts) {
|
|
|
15706
16000
|
completedAt: attempts.at(-1)?.completedAt ?? now()
|
|
15707
16001
|
};
|
|
15708
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
|
+
}
|
|
15709
16087
|
function createEvent(input) {
|
|
15710
16088
|
return {
|
|
15711
16089
|
id: input.id ?? randomUUID2(),
|
|
@@ -15726,10 +16104,18 @@ class EventsClient {
|
|
|
15726
16104
|
store;
|
|
15727
16105
|
redactors;
|
|
15728
16106
|
transportOptions;
|
|
16107
|
+
catalog;
|
|
16108
|
+
validateCatalogTypes;
|
|
15729
16109
|
constructor(options = {}) {
|
|
15730
16110
|
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
15731
16111
|
this.redactors = options.redactors ?? [];
|
|
15732
|
-
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;
|
|
15733
16119
|
}
|
|
15734
16120
|
async addChannel(input) {
|
|
15735
16121
|
const timestamp = new Date().toISOString();
|
|
@@ -15747,18 +16133,40 @@ class EventsClient {
|
|
|
15747
16133
|
}
|
|
15748
16134
|
async emit(input, options = {}) {
|
|
15749
16135
|
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
15750
|
-
if (options.
|
|
15751
|
-
|
|
15752
|
-
|
|
15753
|
-
|
|
15754
|
-
|
|
15755
|
-
|
|
15756
|
-
|
|
15757
|
-
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
15758
|
-
return { event, deliveries, deduped: false };
|
|
15759
|
-
}
|
|
15760
|
-
async listEvents() {
|
|
15761
|
-
|
|
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
|
+
};
|
|
15762
16170
|
}
|
|
15763
16171
|
async listDeliveries() {
|
|
15764
16172
|
return this.store.listDeliveries();
|
|
@@ -15775,7 +16183,7 @@ class EventsClient {
|
|
|
15775
16183
|
}
|
|
15776
16184
|
return deliveries;
|
|
15777
16185
|
}
|
|
15778
|
-
async
|
|
16186
|
+
async matchChannel(id, input = {}) {
|
|
15779
16187
|
const channel = await this.store.getChannel(id);
|
|
15780
16188
|
if (!channel)
|
|
15781
16189
|
throw new Error(`Channel not found: ${id}`);
|
|
@@ -15792,28 +16200,71 @@ class EventsClient {
|
|
|
15792
16200
|
time: input.time,
|
|
15793
16201
|
id: input.id
|
|
15794
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
|
+
}
|
|
15795
16231
|
const eventForChannel = await this.applyRedaction(event, channel);
|
|
15796
16232
|
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
15797
16233
|
await this.store.appendDelivery(result);
|
|
15798
16234
|
return result;
|
|
15799
16235
|
}
|
|
15800
16236
|
async replay(options = {}) {
|
|
15801
|
-
const
|
|
15802
|
-
if (options.eventId && event.id !== options.eventId)
|
|
15803
|
-
return false;
|
|
15804
|
-
if (options.source && event.source !== options.source)
|
|
15805
|
-
return false;
|
|
15806
|
-
if (options.type && event.type !== options.type)
|
|
15807
|
-
return false;
|
|
15808
|
-
return true;
|
|
15809
|
-
});
|
|
16237
|
+
const page = options.cursor || options.limit !== undefined ? await this.listEventsPage(options) : { events: await this.listEvents(options), hasMore: false };
|
|
15810
16238
|
if (options.dryRun)
|
|
15811
|
-
return { events, deliveries: [] };
|
|
16239
|
+
return { events: page.events, deliveries: [], cursor: page.cursor, nextCursor: page.nextCursor, hasMore: page.hasMore };
|
|
15812
16240
|
const deliveries = [];
|
|
15813
|
-
for (const event of events) {
|
|
16241
|
+
for (const event of page.events) {
|
|
15814
16242
|
deliveries.push(...await this.deliver(event));
|
|
15815
16243
|
}
|
|
15816
|
-
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
|
+
};
|
|
15817
16268
|
}
|
|
15818
16269
|
async applyRedaction(event, channel) {
|
|
15819
16270
|
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
@@ -15840,15 +16291,6 @@ class EventsClient {
|
|
|
15840
16291
|
return createDeliveryResult(event, channel, attempts);
|
|
15841
16292
|
}
|
|
15842
16293
|
}
|
|
15843
|
-
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
15844
|
-
if (paths.length === 0)
|
|
15845
|
-
return event;
|
|
15846
|
-
const copy = structuredClone(event);
|
|
15847
|
-
for (const path of paths) {
|
|
15848
|
-
setPath(copy, path, replacement);
|
|
15849
|
-
}
|
|
15850
|
-
return copy;
|
|
15851
|
-
}
|
|
15852
16294
|
function sanitizeChannelForOutput(channel) {
|
|
15853
16295
|
const copy = structuredClone(channel);
|
|
15854
16296
|
if (copy.webhook?.secret)
|
|
@@ -15861,34 +16303,19 @@ function sanitizeChannelForOutput(channel) {
|
|
|
15861
16303
|
function sanitizeChannelsForOutput(channels) {
|
|
15862
16304
|
return channels.map(sanitizeChannelForOutput);
|
|
15863
16305
|
}
|
|
15864
|
-
function
|
|
15865
|
-
|
|
15866
|
-
|
|
15867
|
-
|
|
15868
|
-
|
|
15869
|
-
|
|
15870
|
-
|
|
15871
|
-
|
|
15872
|
-
|
|
15873
|
-
|
|
15874
|
-
|
|
15875
|
-
|
|
15876
|
-
|
|
15877
|
-
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
15878
|
-
]));
|
|
15879
|
-
}
|
|
15880
|
-
function setPath(input, path, replacement) {
|
|
15881
|
-
const parts = path.split(".");
|
|
15882
|
-
let cursor = input;
|
|
15883
|
-
for (const part of parts.slice(0, -1)) {
|
|
15884
|
-
const next = cursor[part];
|
|
15885
|
-
if (!next || typeof next !== "object")
|
|
15886
|
-
return;
|
|
15887
|
-
cursor = next;
|
|
15888
|
-
}
|
|
15889
|
-
const last = parts.at(-1);
|
|
15890
|
-
if (last && last in cursor)
|
|
15891
|
-
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;
|
|
15892
16319
|
}
|
|
15893
16320
|
function normalizeTime(value) {
|
|
15894
16321
|
if (!value)
|
|
@@ -15902,6 +16329,77 @@ function normalizeRetryPolicy(policy) {
|
|
|
15902
16329
|
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
15903
16330
|
};
|
|
15904
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;
|
|
15905
16403
|
function parseJsonObject(value, fallback) {
|
|
15906
16404
|
if (!value)
|
|
15907
16405
|
return fallback;
|
|
@@ -15923,18 +16421,6 @@ function parseHeaders(values) {
|
|
|
15923
16421
|
}
|
|
15924
16422
|
return headers;
|
|
15925
16423
|
}
|
|
15926
|
-
function parseFilter(options) {
|
|
15927
|
-
const filter2 = {};
|
|
15928
|
-
if (options.source)
|
|
15929
|
-
filter2.source = options.source;
|
|
15930
|
-
if (options.type)
|
|
15931
|
-
filter2.type = options.type;
|
|
15932
|
-
if (options.subject)
|
|
15933
|
-
filter2.subject = options.subject;
|
|
15934
|
-
if (options.severity)
|
|
15935
|
-
filter2.severity = options.severity;
|
|
15936
|
-
return Object.keys(filter2).length > 0 ? [filter2] : undefined;
|
|
15937
|
-
}
|
|
15938
16424
|
function createClient(options) {
|
|
15939
16425
|
if (options.createClient)
|
|
15940
16426
|
return options.createClient();
|
|
@@ -15946,22 +16432,30 @@ function print(value, json, text) {
|
|
|
15946
16432
|
else
|
|
15947
16433
|
console.log(text);
|
|
15948
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
|
+
}
|
|
15949
16443
|
function hasJsonOption(options) {
|
|
15950
16444
|
return Boolean(options?.json || options?.opts?.().json || options?.optsWithGlobals?.().json || options?.parent?.opts?.().json || options?.parent?.optsWithGlobals?.().json);
|
|
15951
16445
|
}
|
|
15952
16446
|
function wantsJson(actionOptions, command) {
|
|
15953
16447
|
return hasJsonOption(actionOptions) || hasJsonOption(command);
|
|
15954
16448
|
}
|
|
15955
|
-
function
|
|
15956
|
-
const
|
|
15957
|
-
|
|
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) => {
|
|
15958
16452
|
const timestamp = new Date().toISOString();
|
|
15959
16453
|
const channel = {
|
|
15960
16454
|
id: actionOptions.id,
|
|
15961
16455
|
name: actionOptions.name,
|
|
15962
16456
|
enabled: !actionOptions.disabled,
|
|
15963
16457
|
transport: actionOptions.transport,
|
|
15964
|
-
filters:
|
|
16458
|
+
filters: parseFilterOptions(actionOptions),
|
|
15965
16459
|
retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
|
|
15966
16460
|
redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
|
|
15967
16461
|
createdAt: timestamp,
|
|
@@ -15977,35 +16471,61 @@ function registerWebhookCommands(program, options) {
|
|
|
15977
16471
|
const saved = await createClient(options).addChannel(channel);
|
|
15978
16472
|
print(sanitizeChannelForOutput(saved), wantsJson(actionOptions, command), `Added ${saved.transport} channel ${saved.id}`);
|
|
15979
16473
|
});
|
|
15980
|
-
|
|
15981
|
-
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();
|
|
15982
16476
|
if (wantsJson(actionOptions, command)) {
|
|
15983
|
-
console.log(JSON.stringify(sanitizeChannelsForOutput(
|
|
16477
|
+
console.log(JSON.stringify(sanitizeChannelsForOutput(channels2), null, 2));
|
|
15984
16478
|
return;
|
|
15985
16479
|
}
|
|
15986
|
-
if (!
|
|
16480
|
+
if (!channels2.length) {
|
|
15987
16481
|
console.log("No channels configured.");
|
|
15988
16482
|
return;
|
|
15989
16483
|
}
|
|
15990
|
-
for (const channel of
|
|
16484
|
+
for (const channel of channels2) {
|
|
15991
16485
|
console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
|
|
15992
16486
|
}
|
|
15993
16487
|
});
|
|
15994
|
-
|
|
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) => {
|
|
15995
16493
|
const removed = await createClient(options).removeChannel(id);
|
|
15996
16494
|
print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
|
|
15997
16495
|
});
|
|
15998
|
-
|
|
15999
|
-
const
|
|
16000
|
-
|
|
16001
|
-
|
|
16002
|
-
|
|
16003
|
-
|
|
16004
|
-
|
|
16005
|
-
|
|
16006
|
-
|
|
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
|
+
}
|
|
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
|
+
}
|
|
16007
16527
|
});
|
|
16008
|
-
return
|
|
16528
|
+
return channels;
|
|
16009
16529
|
}
|
|
16010
16530
|
function registerEventCommands(program, options) {
|
|
16011
16531
|
const events = program.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
|
|
@@ -16022,7 +16542,8 @@ function registerEventCommands(program, options) {
|
|
|
16022
16542
|
}, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
|
|
16023
16543
|
print(result, wantsJson(actionOptions, command), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
|
|
16024
16544
|
});
|
|
16025
|
-
|
|
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) => {
|
|
16026
16547
|
let rows = await createClient(options).listEvents();
|
|
16027
16548
|
if (actionOptions.source)
|
|
16028
16549
|
rows = rows.filter((event) => event.source === actionOptions.source);
|
|
@@ -16041,19 +16562,21 @@ function registerEventCommands(program, options) {
|
|
|
16041
16562
|
for (const event of rows)
|
|
16042
16563
|
console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
|
|
16043
16564
|
});
|
|
16044
|
-
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) => {
|
|
16045
16566
|
const result = await createClient(options).replay({
|
|
16046
16567
|
eventId: actionOptions.id,
|
|
16047
16568
|
source: actionOptions.source,
|
|
16048
16569
|
type: actionOptions.type,
|
|
16570
|
+
cursor: actionOptions.cursor,
|
|
16571
|
+
limit: actionOptions.limit,
|
|
16049
16572
|
dryRun: actionOptions.dryRun
|
|
16050
16573
|
});
|
|
16051
|
-
print(result, wantsJson(actionOptions, command),
|
|
16574
|
+
print(result, wantsJson(actionOptions, command), replaySummary(result.events.length, result.deliveries.length, result.nextCursor));
|
|
16052
16575
|
});
|
|
16053
16576
|
return events;
|
|
16054
16577
|
}
|
|
16055
16578
|
function registerEventsCommands(program, options) {
|
|
16056
|
-
|
|
16579
|
+
registerChannelCommands(program, options);
|
|
16057
16580
|
registerEventCommands(program, options);
|
|
16058
16581
|
}
|
|
16059
16582
|
function parseNumber(value) {
|
|
@@ -16066,8 +16589,12 @@ function collectValues(value, previous) {
|
|
|
16066
16589
|
previous.push(value);
|
|
16067
16590
|
return previous;
|
|
16068
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
|
+
}
|
|
16069
16596
|
|
|
16070
|
-
// node_modules/commander/esm.mjs
|
|
16597
|
+
// ../../node_modules/.bun/commander@13.1.0/node_modules/commander/esm.mjs
|
|
16071
16598
|
var import__ = __toESM(require_commander(), 1);
|
|
16072
16599
|
var {
|
|
16073
16600
|
program,
|
|
@@ -18384,10 +18911,10 @@ program.command("import <file>").description("Import configs from a tar.gz bundl
|
|
|
18384
18911
|
});
|
|
18385
18912
|
program.command("whoami").description("Show setup summary").action(async () => {
|
|
18386
18913
|
const store = resolveConfigStore();
|
|
18387
|
-
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");
|
|
18388
18915
|
const stats = await store.getConfigStats();
|
|
18389
18916
|
console.log(chalk.bold("@hasna/instructions") + chalk.dim(" v" + pkg.version));
|
|
18390
|
-
console.log(chalk.cyan(
|
|
18917
|
+
console.log(chalk.cyan(isApiTransport() ? "API:" : "DB:") + " " + dbPath);
|
|
18391
18918
|
console.log(chalk.cyan("Total configs:") + " " + (stats["total"] || 0));
|
|
18392
18919
|
console.log();
|
|
18393
18920
|
console.log(chalk.bold("By category:"));
|
|
@@ -19229,9 +19756,9 @@ DB stats:`));
|
|
|
19229
19756
|
if (count > 0)
|
|
19230
19757
|
console.log(` ${key.padEnd(18)} ${count}`);
|
|
19231
19758
|
}
|
|
19232
|
-
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");
|
|
19233
19760
|
console.log(chalk.dim(`
|
|
19234
|
-
${
|
|
19761
|
+
${isApiTransport() ? "API" : "DB"}: ${location}`));
|
|
19235
19762
|
});
|
|
19236
19763
|
program.command("status").description("Health check: total configs, drift from disk, unredacted secrets").option("--json", "output metadata-only JSON").action(async (opts) => {
|
|
19237
19764
|
const status = await getConfigsStatus(resolveConfigStore());
|
|
@@ -19272,7 +19799,7 @@ program.command("doctor").description("Validate configs: syntax, permissions, mi
|
|
|
19272
19799
|
const store = resolveConfigStore();
|
|
19273
19800
|
let issues = 0;
|
|
19274
19801
|
const pass = (msg) => console.log(chalk.green(" \u2713 ") + msg);
|
|
19275
|
-
const
|
|
19802
|
+
const fail2 = (msg) => {
|
|
19276
19803
|
issues++;
|
|
19277
19804
|
console.log(chalk.red(" \u2717 ") + msg);
|
|
19278
19805
|
};
|
|
@@ -19282,9 +19809,9 @@ program.command("doctor").description("Validate configs: syntax, permissions, mi
|
|
|
19282
19809
|
console.log(chalk.cyan("Known files on disk:"));
|
|
19283
19810
|
for (const k of KNOWN_CONFIGS) {
|
|
19284
19811
|
if (k.rulesDir) {
|
|
19285
|
-
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`);
|
|
19286
19813
|
} else {
|
|
19287
|
-
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`);
|
|
19288
19815
|
}
|
|
19289
19816
|
}
|
|
19290
19817
|
const allConfigs = await store.listConfigs();
|
|
@@ -19297,7 +19824,7 @@ Stored configs (${allConfigs.length}):`));
|
|
|
19297
19824
|
JSON.parse(c.content);
|
|
19298
19825
|
validCount++;
|
|
19299
19826
|
} catch {
|
|
19300
|
-
|
|
19827
|
+
fail2(`${c.slug}: invalid JSON`);
|
|
19301
19828
|
}
|
|
19302
19829
|
} else {
|
|
19303
19830
|
validCount++;
|
|
@@ -19309,13 +19836,13 @@ Stored configs (${allConfigs.length}):`));
|
|
|
19309
19836
|
const found = scanSecrets(c.content, c.format);
|
|
19310
19837
|
secretCount += found.length;
|
|
19311
19838
|
}
|
|
19312
|
-
secretCount === 0 ? pass("No unredacted secrets") :
|
|
19839
|
+
secretCount === 0 ? pass("No unredacted secrets") : fail2(`${secretCount} unredacted secret(s) \u2014 run \`configs scan --fix\``);
|
|
19313
19840
|
const duplicateTargets = findDuplicateTargetPathGroups(allConfigs);
|
|
19314
19841
|
if (duplicateTargets.length === 0) {
|
|
19315
19842
|
pass("No target path is claimed by more than one config");
|
|
19316
19843
|
} else {
|
|
19317
19844
|
const rowCount = duplicateTargets.reduce((total, group) => total + group.configs.length, 0);
|
|
19318
|
-
|
|
19845
|
+
fail2(`${duplicateTargets.length} target path(s) claimed by more than one config (${rowCount} rows) \u2014 apply order between them is undefined`);
|
|
19319
19846
|
for (const group of duplicateTargets) {
|
|
19320
19847
|
console.log(chalk.yellow(` ${group.target_path}`));
|
|
19321
19848
|
for (const c of group.configs) {
|
|
@@ -19329,7 +19856,7 @@ Stored configs (${allConfigs.length}):`));
|
|
|
19329
19856
|
pass("No reference config name is claimed by more than one row");
|
|
19330
19857
|
} else {
|
|
19331
19858
|
const rowCount = duplicateReferenceNames.reduce((total, group) => total + group.configs.length, 0);
|
|
19332
|
-
|
|
19859
|
+
fail2(`${duplicateReferenceNames.length} reference name(s) claimed by more than one row (${rowCount} rows) \u2014 only one is live in the next render`);
|
|
19333
19860
|
for (const group of duplicateReferenceNames) {
|
|
19334
19861
|
console.log(chalk.yellow(` ${group.name}`));
|
|
19335
19862
|
for (const c of group.configs) {
|