@hasna/accounts 0.1.8 → 0.1.9
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/dist/cli.js +796 -117
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -992,7 +992,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
992
992
|
this._exitCallback = (err) => {
|
|
993
993
|
if (err.code !== "commander.executeSubCommandAsync") {
|
|
994
994
|
throw err;
|
|
995
|
-
}
|
|
995
|
+
}
|
|
996
996
|
};
|
|
997
997
|
}
|
|
998
998
|
return this;
|
|
@@ -4307,7 +4307,7 @@ var require_config = __commonJS((exports) => {
|
|
|
4307
4307
|
};
|
|
4308
4308
|
var filePromises = {};
|
|
4309
4309
|
var fileIntercept = {};
|
|
4310
|
-
var
|
|
4310
|
+
var readFile2 = (path, options) => {
|
|
4311
4311
|
if (fileIntercept[path] !== undefined) {
|
|
4312
4312
|
return fileIntercept[path];
|
|
4313
4313
|
}
|
|
@@ -4330,10 +4330,10 @@ var require_config = __commonJS((exports) => {
|
|
|
4330
4330
|
resolvedConfigFilepath = node_path.join(homeDir, configFilepath.slice(2));
|
|
4331
4331
|
}
|
|
4332
4332
|
const parsedFiles = await Promise.all([
|
|
4333
|
-
|
|
4333
|
+
readFile2(resolvedConfigFilepath, {
|
|
4334
4334
|
ignoreCache: init.ignoreCache
|
|
4335
4335
|
}).then(parseIni).then(getConfigData).catch(swallowError$1),
|
|
4336
|
-
|
|
4336
|
+
readFile2(resolvedFilepath, {
|
|
4337
4337
|
ignoreCache: init.ignoreCache
|
|
4338
4338
|
}).then(parseIni).catch(swallowError$1)
|
|
4339
4339
|
]);
|
|
@@ -4344,7 +4344,7 @@ var require_config = __commonJS((exports) => {
|
|
|
4344
4344
|
};
|
|
4345
4345
|
var getSsoSessionData = (data) => Object.entries(data).filter(([key]) => key.startsWith(types2.IniSectionType.SSO_SESSION + CONFIG_PREFIX_SEPARATOR)).reduce((acc, [key, value]) => ({ ...acc, [key.substring(key.indexOf(CONFIG_PREFIX_SEPARATOR) + 1)]: value }), {});
|
|
4346
4346
|
var swallowError = () => ({});
|
|
4347
|
-
var loadSsoSessionData = async (init = {}) =>
|
|
4347
|
+
var loadSsoSessionData = async (init = {}) => readFile2(init.configFilepath ?? getConfigFilepath()).then(parseIni).then(getSsoSessionData).catch(swallowError);
|
|
4348
4348
|
var mergeConfigFiles = (...files) => {
|
|
4349
4349
|
const merged = {};
|
|
4350
4350
|
for (const file of files) {
|
|
@@ -4711,7 +4711,7 @@ var require_config = __commonJS((exports) => {
|
|
|
4711
4711
|
exports.nodeFipsConfigSelectors = nodeFipsConfigSelectors;
|
|
4712
4712
|
exports.numberSelector = numberSelector;
|
|
4713
4713
|
exports.parseKnownFiles = parseKnownFiles;
|
|
4714
|
-
exports.readFile =
|
|
4714
|
+
exports.readFile = readFile2;
|
|
4715
4715
|
exports.resolveCustomEndpointsConfig = resolveCustomEndpointsConfig;
|
|
4716
4716
|
exports.resolveDefaultsModeConfig = resolveDefaultsModeConfig;
|
|
4717
4717
|
exports.resolveEndpointsConfig = resolveEndpointsConfig;
|
|
@@ -5760,19 +5760,19 @@ var require_serde = __commonJS((exports) => {
|
|
|
5760
5760
|
};
|
|
5761
5761
|
var strictParseDouble = (value) => {
|
|
5762
5762
|
if (typeof value == "string") {
|
|
5763
|
-
return expectNumber(
|
|
5763
|
+
return expectNumber(parseNumber2(value));
|
|
5764
5764
|
}
|
|
5765
5765
|
return expectNumber(value);
|
|
5766
5766
|
};
|
|
5767
5767
|
var strictParseFloat = strictParseDouble;
|
|
5768
5768
|
var strictParseFloat32 = (value) => {
|
|
5769
5769
|
if (typeof value == "string") {
|
|
5770
|
-
return expectFloat32(
|
|
5770
|
+
return expectFloat32(parseNumber2(value));
|
|
5771
5771
|
}
|
|
5772
5772
|
return expectFloat32(value);
|
|
5773
5773
|
};
|
|
5774
5774
|
var NUMBER_REGEX = /(-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)|(-?Infinity)|(NaN)/g;
|
|
5775
|
-
var
|
|
5775
|
+
var parseNumber2 = (value) => {
|
|
5776
5776
|
const matches = value.match(NUMBER_REGEX);
|
|
5777
5777
|
if (matches === null || matches[0].length !== value.length) {
|
|
5778
5778
|
throw new TypeError(`Expected real number, got implicit NaN`);
|
|
@@ -5807,26 +5807,26 @@ var require_serde = __commonJS((exports) => {
|
|
|
5807
5807
|
};
|
|
5808
5808
|
var strictParseLong = (value) => {
|
|
5809
5809
|
if (typeof value === "string") {
|
|
5810
|
-
return expectLong(
|
|
5810
|
+
return expectLong(parseNumber2(value));
|
|
5811
5811
|
}
|
|
5812
5812
|
return expectLong(value);
|
|
5813
5813
|
};
|
|
5814
5814
|
var strictParseInt = strictParseLong;
|
|
5815
5815
|
var strictParseInt32 = (value) => {
|
|
5816
5816
|
if (typeof value === "string") {
|
|
5817
|
-
return expectInt32(
|
|
5817
|
+
return expectInt32(parseNumber2(value));
|
|
5818
5818
|
}
|
|
5819
5819
|
return expectInt32(value);
|
|
5820
5820
|
};
|
|
5821
5821
|
var strictParseShort = (value) => {
|
|
5822
5822
|
if (typeof value === "string") {
|
|
5823
|
-
return expectShort(
|
|
5823
|
+
return expectShort(parseNumber2(value));
|
|
5824
5824
|
}
|
|
5825
5825
|
return expectShort(value);
|
|
5826
5826
|
};
|
|
5827
5827
|
var strictParseByte = (value) => {
|
|
5828
5828
|
if (typeof value === "string") {
|
|
5829
|
-
return expectByte(
|
|
5829
|
+
return expectByte(parseNumber2(value));
|
|
5830
5830
|
}
|
|
5831
5831
|
return expectByte(value);
|
|
5832
5832
|
};
|
|
@@ -12259,7 +12259,7 @@ var require_es5 = __commonJS((exports, module) => {
|
|
|
12259
12259
|
|
|
12260
12260
|
// node_modules/@aws-sdk/core/dist-cjs/submodules/client/index.js
|
|
12261
12261
|
var require_client2 = __commonJS((exports) => {
|
|
12262
|
-
var __dirname = "/
|
|
12262
|
+
var __dirname = "/tmp/open-accounts-events-release-384975/node_modules/@aws-sdk/core/dist-cjs/submodules/client";
|
|
12263
12263
|
var retry = require_retry();
|
|
12264
12264
|
var protocols = require_protocols();
|
|
12265
12265
|
var lambdaInvokeStore = require_invoke_store();
|
|
@@ -14153,8 +14153,8 @@ ${serde.toHex(hashedRequest)}`;
|
|
|
14153
14153
|
throw new Error("Resolved credential object is not valid");
|
|
14154
14154
|
}
|
|
14155
14155
|
}
|
|
14156
|
-
formatDate(
|
|
14157
|
-
const longDate = iso8601(
|
|
14156
|
+
formatDate(now2) {
|
|
14157
|
+
const longDate = iso8601(now2).replace(/[\-:]/g, "");
|
|
14158
14158
|
return {
|
|
14159
14159
|
longDate,
|
|
14160
14160
|
shortDate: longDate.slice(0, 8)
|
|
@@ -19955,8 +19955,8 @@ var require_s3 = __commonJS((exports) => {
|
|
|
19955
19955
|
delete this.data[key];
|
|
19956
19956
|
}
|
|
19957
19957
|
async purgeExpired() {
|
|
19958
|
-
const
|
|
19959
|
-
if (this.lastPurgeTime + S3ExpressIdentityCache.EXPIRED_CREDENTIAL_PURGE_INTERVAL_MS >
|
|
19958
|
+
const now2 = Date.now();
|
|
19959
|
+
if (this.lastPurgeTime + S3ExpressIdentityCache.EXPIRED_CREDENTIAL_PURGE_INTERVAL_MS > now2) {
|
|
19960
19960
|
return;
|
|
19961
19961
|
}
|
|
19962
19962
|
for (const key in this.data) {
|
|
@@ -19964,7 +19964,7 @@ var require_s3 = __commonJS((exports) => {
|
|
|
19964
19964
|
if (!entry.isRefreshing) {
|
|
19965
19965
|
const credential = await entry.identity;
|
|
19966
19966
|
if (credential.expiration) {
|
|
19967
|
-
if (credential.expiration.getTime() <
|
|
19967
|
+
if (credential.expiration.getTime() < now2) {
|
|
19968
19968
|
delete this.data[key];
|
|
19969
19969
|
}
|
|
19970
19970
|
}
|
|
@@ -30963,11 +30963,11 @@ var require_dist_cjs15 = __commonJS((exports) => {
|
|
|
30963
30963
|
throw new config.TokenProviderError(`Value not present for '${key}' in SSO Token${forRefresh ? ". Cannot refresh" : ""}. ${REFRESH_MESSAGE}`, false);
|
|
30964
30964
|
}
|
|
30965
30965
|
};
|
|
30966
|
-
var { writeFile } = node_fs.promises;
|
|
30966
|
+
var { writeFile: writeFile2 } = node_fs.promises;
|
|
30967
30967
|
var writeSSOTokenToFile = (id, ssoToken) => {
|
|
30968
30968
|
const tokenFilepath = config.getSSOTokenFilepath(id);
|
|
30969
30969
|
const tokenString = JSON.stringify(ssoToken, null, 2);
|
|
30970
|
-
return
|
|
30970
|
+
return writeFile2(tokenFilepath, tokenString);
|
|
30971
30971
|
};
|
|
30972
30972
|
var lastRefreshAttemptTime = new Date(0);
|
|
30973
30973
|
var fromSso = (init = {}) => async ({ callerClientConfig } = {}) => {
|
|
@@ -32364,9 +32364,9 @@ var require_dist_cjs17 = __commonJS((exports) => {
|
|
|
32364
32364
|
throw new config.CredentialsProviderError(`Failed to load a token for session ${this.loginSession}, please re-authenticate using aws login`, { tryNextLink: false, logger: this.logger });
|
|
32365
32365
|
}
|
|
32366
32366
|
const accessToken = token.accessToken;
|
|
32367
|
-
const
|
|
32367
|
+
const now2 = Date.now();
|
|
32368
32368
|
const expiryTime = new Date(accessToken.expiresAt).getTime();
|
|
32369
|
-
const timeUntilExpiry = expiryTime -
|
|
32369
|
+
const timeUntilExpiry = expiryTime - now2;
|
|
32370
32370
|
if (timeUntilExpiry <= LoginCredentialsFetcher.REFRESH_THRESHOLD) {
|
|
32371
32371
|
return this.refresh(token);
|
|
32372
32372
|
}
|
|
@@ -36797,9 +36797,9 @@ var require_dist_cjs22 = __commonJS((exports) => {
|
|
|
36797
36797
|
|
|
36798
36798
|
// src/cli.ts
|
|
36799
36799
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
36800
|
-
import { existsSync as
|
|
36801
|
-
import { homedir as
|
|
36802
|
-
import { dirname as dirname5, join as
|
|
36800
|
+
import { existsSync as existsSync12, readFileSync as readFileSync7 } from "node:fs";
|
|
36801
|
+
import { homedir as homedir6 } from "node:os";
|
|
36802
|
+
import { dirname as dirname5, join as join12 } from "node:path";
|
|
36803
36803
|
import { fileURLToPath } from "node:url";
|
|
36804
36804
|
|
|
36805
36805
|
// node_modules/commander/esm.mjs
|
|
@@ -36818,6 +36818,684 @@ var {
|
|
|
36818
36818
|
Help
|
|
36819
36819
|
} = import__.default;
|
|
36820
36820
|
|
|
36821
|
+
// node_modules/@hasna/events/dist/commander.js
|
|
36822
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
36823
|
+
import { existsSync } from "fs";
|
|
36824
|
+
import { homedir } from "os";
|
|
36825
|
+
import { join } from "path";
|
|
36826
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
36827
|
+
import { randomUUID } from "crypto";
|
|
36828
|
+
import { spawn } from "child_process";
|
|
36829
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
36830
|
+
function getPathValue(input, path) {
|
|
36831
|
+
return path.split(".").reduce((value, part) => {
|
|
36832
|
+
if (value && typeof value === "object" && part in value) {
|
|
36833
|
+
return value[part];
|
|
36834
|
+
}
|
|
36835
|
+
return;
|
|
36836
|
+
}, input);
|
|
36837
|
+
}
|
|
36838
|
+
function wildcardToRegExp(pattern) {
|
|
36839
|
+
const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
|
|
36840
|
+
return new RegExp(`^${escaped}$`);
|
|
36841
|
+
}
|
|
36842
|
+
function matchString(value, matcher) {
|
|
36843
|
+
if (matcher === undefined)
|
|
36844
|
+
return true;
|
|
36845
|
+
if (value === undefined)
|
|
36846
|
+
return false;
|
|
36847
|
+
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
36848
|
+
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
36849
|
+
}
|
|
36850
|
+
function matchRecord(input, matcher) {
|
|
36851
|
+
if (!matcher)
|
|
36852
|
+
return true;
|
|
36853
|
+
return Object.entries(matcher).every(([path, expected]) => {
|
|
36854
|
+
const actual = getPathValue(input, path);
|
|
36855
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
36856
|
+
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
36857
|
+
}
|
|
36858
|
+
return actual === expected;
|
|
36859
|
+
});
|
|
36860
|
+
}
|
|
36861
|
+
function eventMatchesFilter(event, filter) {
|
|
36862
|
+
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);
|
|
36863
|
+
}
|
|
36864
|
+
function channelMatchesEvent(channel, event) {
|
|
36865
|
+
if (!channel.enabled)
|
|
36866
|
+
return false;
|
|
36867
|
+
if (!channel.filters || channel.filters.length === 0)
|
|
36868
|
+
return true;
|
|
36869
|
+
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
36870
|
+
}
|
|
36871
|
+
var HASNA_EVENTS_DIR_ENV = "HASNA_EVENTS_DIR";
|
|
36872
|
+
var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
36873
|
+
function getEventsDataDir(override) {
|
|
36874
|
+
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
|
|
36875
|
+
}
|
|
36876
|
+
|
|
36877
|
+
class JsonEventsStore {
|
|
36878
|
+
dataDir;
|
|
36879
|
+
channelsPath;
|
|
36880
|
+
eventsPath;
|
|
36881
|
+
deliveriesPath;
|
|
36882
|
+
constructor(dataDir = getEventsDataDir()) {
|
|
36883
|
+
this.dataDir = dataDir;
|
|
36884
|
+
this.channelsPath = join(dataDir, "channels.json");
|
|
36885
|
+
this.eventsPath = join(dataDir, "events.json");
|
|
36886
|
+
this.deliveriesPath = join(dataDir, "deliveries.json");
|
|
36887
|
+
}
|
|
36888
|
+
async init() {
|
|
36889
|
+
await mkdir(this.dataDir, { recursive: true, mode: 448 });
|
|
36890
|
+
await chmod(this.dataDir, 448).catch(() => {
|
|
36891
|
+
return;
|
|
36892
|
+
});
|
|
36893
|
+
await this.ensureArrayFile(this.channelsPath);
|
|
36894
|
+
await this.ensureArrayFile(this.eventsPath);
|
|
36895
|
+
await this.ensureArrayFile(this.deliveriesPath);
|
|
36896
|
+
}
|
|
36897
|
+
async addChannel(channel) {
|
|
36898
|
+
await this.init();
|
|
36899
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
36900
|
+
const index = channels.findIndex((item) => item.id === channel.id);
|
|
36901
|
+
if (index >= 0) {
|
|
36902
|
+
channels[index] = { ...channel, createdAt: channels[index].createdAt, updatedAt: new Date().toISOString() };
|
|
36903
|
+
} else {
|
|
36904
|
+
channels.push(channel);
|
|
36905
|
+
}
|
|
36906
|
+
await this.writeJson(this.channelsPath, channels);
|
|
36907
|
+
return index >= 0 ? channels[index] : channel;
|
|
36908
|
+
}
|
|
36909
|
+
async listChannels() {
|
|
36910
|
+
await this.init();
|
|
36911
|
+
return this.readJson(this.channelsPath, []);
|
|
36912
|
+
}
|
|
36913
|
+
async getChannel(id) {
|
|
36914
|
+
const channels = await this.listChannels();
|
|
36915
|
+
return channels.find((channel) => channel.id === id);
|
|
36916
|
+
}
|
|
36917
|
+
async removeChannel(id) {
|
|
36918
|
+
await this.init();
|
|
36919
|
+
const channels = await this.readJson(this.channelsPath, []);
|
|
36920
|
+
const next = channels.filter((channel) => channel.id !== id);
|
|
36921
|
+
await this.writeJson(this.channelsPath, next);
|
|
36922
|
+
return next.length !== channels.length;
|
|
36923
|
+
}
|
|
36924
|
+
async appendEvent(event) {
|
|
36925
|
+
await this.init();
|
|
36926
|
+
const events = await this.readJson(this.eventsPath, []);
|
|
36927
|
+
events.push(event);
|
|
36928
|
+
await this.writeJson(this.eventsPath, events);
|
|
36929
|
+
return event;
|
|
36930
|
+
}
|
|
36931
|
+
async listEvents() {
|
|
36932
|
+
await this.init();
|
|
36933
|
+
return this.readJson(this.eventsPath, []);
|
|
36934
|
+
}
|
|
36935
|
+
async findEventByIdentity(identity) {
|
|
36936
|
+
const events = await this.listEvents();
|
|
36937
|
+
return events.find((event) => identity.id !== undefined && event.id === identity.id || identity.dedupeKey !== undefined && event.dedupeKey === identity.dedupeKey);
|
|
36938
|
+
}
|
|
36939
|
+
async appendDelivery(result) {
|
|
36940
|
+
await this.init();
|
|
36941
|
+
const deliveries = await this.readJson(this.deliveriesPath, []);
|
|
36942
|
+
deliveries.push(result);
|
|
36943
|
+
await this.writeJson(this.deliveriesPath, deliveries);
|
|
36944
|
+
return result;
|
|
36945
|
+
}
|
|
36946
|
+
async listDeliveries() {
|
|
36947
|
+
await this.init();
|
|
36948
|
+
return this.readJson(this.deliveriesPath, []);
|
|
36949
|
+
}
|
|
36950
|
+
async exportData() {
|
|
36951
|
+
return {
|
|
36952
|
+
channels: await this.listChannels(),
|
|
36953
|
+
events: await this.listEvents(),
|
|
36954
|
+
deliveries: await this.listDeliveries()
|
|
36955
|
+
};
|
|
36956
|
+
}
|
|
36957
|
+
async ensureArrayFile(path) {
|
|
36958
|
+
if (!existsSync(path)) {
|
|
36959
|
+
await writeFile(path, `[]
|
|
36960
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
36961
|
+
}
|
|
36962
|
+
await chmod(path, 384).catch(() => {
|
|
36963
|
+
return;
|
|
36964
|
+
});
|
|
36965
|
+
}
|
|
36966
|
+
async readJson(path, fallback) {
|
|
36967
|
+
try {
|
|
36968
|
+
const raw = await readFile(path, "utf-8");
|
|
36969
|
+
if (!raw.trim())
|
|
36970
|
+
return fallback;
|
|
36971
|
+
return JSON.parse(raw);
|
|
36972
|
+
} catch (error) {
|
|
36973
|
+
if (error.code === "ENOENT")
|
|
36974
|
+
return fallback;
|
|
36975
|
+
throw error;
|
|
36976
|
+
}
|
|
36977
|
+
}
|
|
36978
|
+
async writeJson(path, value) {
|
|
36979
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
36980
|
+
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}
|
|
36981
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
36982
|
+
await rename(tempPath, path);
|
|
36983
|
+
await chmod(path, 384).catch(() => {
|
|
36984
|
+
return;
|
|
36985
|
+
});
|
|
36986
|
+
}
|
|
36987
|
+
}
|
|
36988
|
+
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
36989
|
+
function buildSignatureBase(timestamp, body) {
|
|
36990
|
+
return `${timestamp}.${body}`;
|
|
36991
|
+
}
|
|
36992
|
+
function signPayload(secret, timestamp, body) {
|
|
36993
|
+
const digest = createHmac("sha256", secret).update(buildSignatureBase(timestamp, body)).digest("hex");
|
|
36994
|
+
return `sha256=${digest}`;
|
|
36995
|
+
}
|
|
36996
|
+
function now() {
|
|
36997
|
+
return new Date().toISOString();
|
|
36998
|
+
}
|
|
36999
|
+
function truncate(value, max = 4096) {
|
|
37000
|
+
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
37001
|
+
}
|
|
37002
|
+
function buildWebhookRequest(event, channel) {
|
|
37003
|
+
if (!channel.webhook)
|
|
37004
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
37005
|
+
const body = JSON.stringify(event);
|
|
37006
|
+
const timestamp = event.time;
|
|
37007
|
+
const headers = {
|
|
37008
|
+
"Content-Type": "application/json",
|
|
37009
|
+
"User-Agent": "@hasna/events",
|
|
37010
|
+
"X-Hasna-Event-Id": event.id,
|
|
37011
|
+
"X-Hasna-Event-Type": event.type,
|
|
37012
|
+
"X-Hasna-Timestamp": timestamp,
|
|
37013
|
+
...channel.webhook.headers
|
|
37014
|
+
};
|
|
37015
|
+
if (channel.webhook.secret) {
|
|
37016
|
+
headers["X-Hasna-Signature"] = signPayload(channel.webhook.secret, timestamp, body);
|
|
37017
|
+
}
|
|
37018
|
+
return { body, headers };
|
|
37019
|
+
}
|
|
37020
|
+
async function dispatchWebhook(event, channel, options = {}) {
|
|
37021
|
+
if (!channel.webhook)
|
|
37022
|
+
throw new Error(`Channel ${channel.id} has no webhook config`);
|
|
37023
|
+
const startedAt = now();
|
|
37024
|
+
const { body, headers } = buildWebhookRequest(event, channel);
|
|
37025
|
+
const controller = new AbortController;
|
|
37026
|
+
const timeout = setTimeout(() => controller.abort(), channel.webhook.timeoutMs ?? 15000);
|
|
37027
|
+
try {
|
|
37028
|
+
const response = await (options.fetchImpl ?? fetch)(channel.webhook.url, {
|
|
37029
|
+
method: "POST",
|
|
37030
|
+
headers,
|
|
37031
|
+
body,
|
|
37032
|
+
signal: controller.signal
|
|
37033
|
+
});
|
|
37034
|
+
const responseBody = truncate(await response.text());
|
|
37035
|
+
return {
|
|
37036
|
+
attempt: 1,
|
|
37037
|
+
status: response.ok ? "success" : "failed",
|
|
37038
|
+
startedAt,
|
|
37039
|
+
completedAt: now(),
|
|
37040
|
+
responseStatus: response.status,
|
|
37041
|
+
responseBody,
|
|
37042
|
+
error: response.ok ? undefined : `Webhook returned HTTP ${response.status}`
|
|
37043
|
+
};
|
|
37044
|
+
} catch (error) {
|
|
37045
|
+
return {
|
|
37046
|
+
attempt: 1,
|
|
37047
|
+
status: "failed",
|
|
37048
|
+
startedAt,
|
|
37049
|
+
completedAt: now(),
|
|
37050
|
+
error: error instanceof Error ? error.message : String(error)
|
|
37051
|
+
};
|
|
37052
|
+
} finally {
|
|
37053
|
+
clearTimeout(timeout);
|
|
37054
|
+
}
|
|
37055
|
+
}
|
|
37056
|
+
async function dispatchCommand(event, channel) {
|
|
37057
|
+
if (!channel.command)
|
|
37058
|
+
throw new Error(`Channel ${channel.id} has no command config`);
|
|
37059
|
+
const startedAt = now();
|
|
37060
|
+
const eventJson = JSON.stringify(event);
|
|
37061
|
+
const env = {
|
|
37062
|
+
...process.env,
|
|
37063
|
+
...channel.command.env,
|
|
37064
|
+
HASNA_CHANNEL_ID: channel.id,
|
|
37065
|
+
HASNA_EVENT_ID: event.id,
|
|
37066
|
+
HASNA_EVENT_TYPE: event.type,
|
|
37067
|
+
HASNA_EVENT_SOURCE: event.source,
|
|
37068
|
+
HASNA_EVENT_SUBJECT: event.subject ?? "",
|
|
37069
|
+
HASNA_EVENT_SEVERITY: event.severity,
|
|
37070
|
+
HASNA_EVENT_TIME: event.time,
|
|
37071
|
+
HASNA_EVENT_DEDUPE_KEY: event.dedupeKey ?? "",
|
|
37072
|
+
HASNA_EVENT_SCHEMA_VERSION: event.schemaVersion,
|
|
37073
|
+
HASNA_EVENT_JSON: eventJson
|
|
37074
|
+
};
|
|
37075
|
+
return new Promise((resolve) => {
|
|
37076
|
+
const child = spawn(channel.command.command, channel.command.args ?? [], {
|
|
37077
|
+
cwd: channel.command.cwd,
|
|
37078
|
+
env,
|
|
37079
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
37080
|
+
});
|
|
37081
|
+
let stdout = "";
|
|
37082
|
+
let stderr = "";
|
|
37083
|
+
const timeout = setTimeout(() => child.kill("SIGTERM"), channel.command.timeoutMs ?? 15000);
|
|
37084
|
+
child.stdin.end(eventJson);
|
|
37085
|
+
child.stdout.on("data", (chunk) => {
|
|
37086
|
+
stdout += chunk.toString();
|
|
37087
|
+
});
|
|
37088
|
+
child.stderr.on("data", (chunk) => {
|
|
37089
|
+
stderr += chunk.toString();
|
|
37090
|
+
});
|
|
37091
|
+
child.on("error", (error) => {
|
|
37092
|
+
clearTimeout(timeout);
|
|
37093
|
+
resolve({
|
|
37094
|
+
attempt: 1,
|
|
37095
|
+
status: "failed",
|
|
37096
|
+
startedAt,
|
|
37097
|
+
completedAt: now(),
|
|
37098
|
+
stdout: truncate(stdout),
|
|
37099
|
+
stderr: truncate(stderr),
|
|
37100
|
+
error: error.message
|
|
37101
|
+
});
|
|
37102
|
+
});
|
|
37103
|
+
child.on("close", (code, signal) => {
|
|
37104
|
+
clearTimeout(timeout);
|
|
37105
|
+
const success = code === 0;
|
|
37106
|
+
resolve({
|
|
37107
|
+
attempt: 1,
|
|
37108
|
+
status: success ? "success" : "failed",
|
|
37109
|
+
startedAt,
|
|
37110
|
+
completedAt: now(),
|
|
37111
|
+
stdout: truncate(stdout),
|
|
37112
|
+
stderr: truncate(stderr),
|
|
37113
|
+
error: success ? undefined : `Command exited with ${signal ? `signal ${signal}` : `code ${code}`}`
|
|
37114
|
+
});
|
|
37115
|
+
});
|
|
37116
|
+
});
|
|
37117
|
+
}
|
|
37118
|
+
async function dispatchChannel(event, channel, options = {}) {
|
|
37119
|
+
if (channel.transport === "webhook")
|
|
37120
|
+
return dispatchWebhook(event, channel, options);
|
|
37121
|
+
if (channel.transport === "command")
|
|
37122
|
+
return dispatchCommand(event, channel);
|
|
37123
|
+
return {
|
|
37124
|
+
attempt: 1,
|
|
37125
|
+
status: "skipped",
|
|
37126
|
+
startedAt: now(),
|
|
37127
|
+
completedAt: now(),
|
|
37128
|
+
error: `Unsupported transport: ${channel.transport}`
|
|
37129
|
+
};
|
|
37130
|
+
}
|
|
37131
|
+
function createDeliveryResult(event, channel, attempts) {
|
|
37132
|
+
const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
|
|
37133
|
+
return {
|
|
37134
|
+
id: randomUUID(),
|
|
37135
|
+
eventId: event.id,
|
|
37136
|
+
channelId: channel.id,
|
|
37137
|
+
transport: channel.transport,
|
|
37138
|
+
status,
|
|
37139
|
+
attempts,
|
|
37140
|
+
createdAt: attempts[0]?.startedAt ?? now(),
|
|
37141
|
+
completedAt: attempts.at(-1)?.completedAt ?? now()
|
|
37142
|
+
};
|
|
37143
|
+
}
|
|
37144
|
+
function createEvent(input) {
|
|
37145
|
+
return {
|
|
37146
|
+
id: input.id ?? randomUUID2(),
|
|
37147
|
+
source: input.source,
|
|
37148
|
+
type: input.type,
|
|
37149
|
+
time: normalizeTime(input.time),
|
|
37150
|
+
subject: input.subject,
|
|
37151
|
+
severity: input.severity ?? "info",
|
|
37152
|
+
data: input.data ?? {},
|
|
37153
|
+
message: input.message,
|
|
37154
|
+
dedupeKey: input.dedupeKey,
|
|
37155
|
+
schemaVersion: input.schemaVersion ?? "1.0",
|
|
37156
|
+
metadata: input.metadata ?? {}
|
|
37157
|
+
};
|
|
37158
|
+
}
|
|
37159
|
+
|
|
37160
|
+
class EventsClient {
|
|
37161
|
+
store;
|
|
37162
|
+
redactors;
|
|
37163
|
+
transportOptions;
|
|
37164
|
+
constructor(options = {}) {
|
|
37165
|
+
this.store = options.store ?? new JsonEventsStore(options.dataDir);
|
|
37166
|
+
this.redactors = options.redactors ?? [];
|
|
37167
|
+
this.transportOptions = { fetchImpl: options.fetchImpl };
|
|
37168
|
+
}
|
|
37169
|
+
async addChannel(input) {
|
|
37170
|
+
const timestamp = new Date().toISOString();
|
|
37171
|
+
return this.store.addChannel({
|
|
37172
|
+
...input,
|
|
37173
|
+
createdAt: input.createdAt ?? timestamp,
|
|
37174
|
+
updatedAt: input.updatedAt ?? timestamp
|
|
37175
|
+
});
|
|
37176
|
+
}
|
|
37177
|
+
async listChannels() {
|
|
37178
|
+
return this.store.listChannels();
|
|
37179
|
+
}
|
|
37180
|
+
async removeChannel(id) {
|
|
37181
|
+
return this.store.removeChannel(id);
|
|
37182
|
+
}
|
|
37183
|
+
async emit(input, options = {}) {
|
|
37184
|
+
const event = options.redactSensitiveData === false ? createEvent(input) : redactSensitiveKeys(createEvent(input));
|
|
37185
|
+
if (options.dedupe !== false) {
|
|
37186
|
+
const existing = await this.store.findEventByIdentity({ id: input.id, dedupeKey: event.dedupeKey });
|
|
37187
|
+
if (existing) {
|
|
37188
|
+
return { event: existing, deliveries: [], deduped: true };
|
|
37189
|
+
}
|
|
37190
|
+
}
|
|
37191
|
+
await this.store.appendEvent(event);
|
|
37192
|
+
const deliveries = options.deliver === false ? [] : await this.deliver(event);
|
|
37193
|
+
return { event, deliveries, deduped: false };
|
|
37194
|
+
}
|
|
37195
|
+
async listEvents() {
|
|
37196
|
+
return this.store.listEvents();
|
|
37197
|
+
}
|
|
37198
|
+
async listDeliveries() {
|
|
37199
|
+
return this.store.listDeliveries();
|
|
37200
|
+
}
|
|
37201
|
+
async deliver(event) {
|
|
37202
|
+
const channels = await this.store.listChannels();
|
|
37203
|
+
const selected = channels.filter((channel) => channelMatchesEvent(channel, event));
|
|
37204
|
+
const deliveries = [];
|
|
37205
|
+
for (const channel of selected) {
|
|
37206
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
37207
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
37208
|
+
await this.store.appendDelivery(result);
|
|
37209
|
+
deliveries.push(result);
|
|
37210
|
+
}
|
|
37211
|
+
return deliveries;
|
|
37212
|
+
}
|
|
37213
|
+
async testChannel(id, input = {}) {
|
|
37214
|
+
const channel = await this.store.getChannel(id);
|
|
37215
|
+
if (!channel)
|
|
37216
|
+
throw new Error(`Channel not found: ${id}`);
|
|
37217
|
+
const event = createEvent({
|
|
37218
|
+
source: input.source ?? "hasna.events",
|
|
37219
|
+
type: input.type ?? "events.test",
|
|
37220
|
+
subject: input.subject ?? id,
|
|
37221
|
+
severity: input.severity ?? "info",
|
|
37222
|
+
data: input.data ?? { test: true },
|
|
37223
|
+
message: input.message ?? "Hasna events test delivery",
|
|
37224
|
+
dedupeKey: input.dedupeKey,
|
|
37225
|
+
schemaVersion: input.schemaVersion,
|
|
37226
|
+
metadata: input.metadata,
|
|
37227
|
+
time: input.time,
|
|
37228
|
+
id: input.id
|
|
37229
|
+
});
|
|
37230
|
+
const eventForChannel = await this.applyRedaction(event, channel);
|
|
37231
|
+
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
37232
|
+
await this.store.appendDelivery(result);
|
|
37233
|
+
return result;
|
|
37234
|
+
}
|
|
37235
|
+
async replay(options = {}) {
|
|
37236
|
+
const events = (await this.store.listEvents()).filter((event) => {
|
|
37237
|
+
if (options.eventId && event.id !== options.eventId)
|
|
37238
|
+
return false;
|
|
37239
|
+
if (options.source && event.source !== options.source)
|
|
37240
|
+
return false;
|
|
37241
|
+
if (options.type && event.type !== options.type)
|
|
37242
|
+
return false;
|
|
37243
|
+
return true;
|
|
37244
|
+
});
|
|
37245
|
+
if (options.dryRun)
|
|
37246
|
+
return { events, deliveries: [] };
|
|
37247
|
+
const deliveries = [];
|
|
37248
|
+
for (const event of events) {
|
|
37249
|
+
deliveries.push(...await this.deliver(event));
|
|
37250
|
+
}
|
|
37251
|
+
return { events, deliveries };
|
|
37252
|
+
}
|
|
37253
|
+
async applyRedaction(event, channel) {
|
|
37254
|
+
let next = redactPaths(event, channel.redact?.paths ?? [], channel.redact?.replacement ?? "[REDACTED]");
|
|
37255
|
+
for (const redactor of this.redactors) {
|
|
37256
|
+
next = await redactor(next, channel);
|
|
37257
|
+
}
|
|
37258
|
+
return next;
|
|
37259
|
+
}
|
|
37260
|
+
async deliverWithRetry(event, channel) {
|
|
37261
|
+
const policy = normalizeRetryPolicy(channel.retry);
|
|
37262
|
+
const attempts = [];
|
|
37263
|
+
for (let index = 0;index < policy.maxAttempts; index += 1) {
|
|
37264
|
+
const attempt = await dispatchChannel(event, channel, this.transportOptions);
|
|
37265
|
+
attempt.attempt = index + 1;
|
|
37266
|
+
if (attempt.status === "failed" && index + 1 < policy.maxAttempts) {
|
|
37267
|
+
attempt.nextBackoffMs = Math.round(policy.backoffMs * policy.multiplier ** index);
|
|
37268
|
+
}
|
|
37269
|
+
attempts.push(attempt);
|
|
37270
|
+
if (attempt.status !== "failed")
|
|
37271
|
+
break;
|
|
37272
|
+
if (attempt.nextBackoffMs)
|
|
37273
|
+
await Bun.sleep(attempt.nextBackoffMs);
|
|
37274
|
+
}
|
|
37275
|
+
return createDeliveryResult(event, channel, attempts);
|
|
37276
|
+
}
|
|
37277
|
+
}
|
|
37278
|
+
function redactPaths(event, paths, replacement = "[REDACTED]") {
|
|
37279
|
+
if (paths.length === 0)
|
|
37280
|
+
return event;
|
|
37281
|
+
const copy = structuredClone(event);
|
|
37282
|
+
for (const path of paths) {
|
|
37283
|
+
setPath(copy, path, replacement);
|
|
37284
|
+
}
|
|
37285
|
+
return copy;
|
|
37286
|
+
}
|
|
37287
|
+
function sanitizeChannelForOutput(channel) {
|
|
37288
|
+
const copy = structuredClone(channel);
|
|
37289
|
+
if (copy.webhook?.secret)
|
|
37290
|
+
copy.webhook.secret = "[REDACTED]";
|
|
37291
|
+
if (copy.command?.env) {
|
|
37292
|
+
copy.command.env = Object.fromEntries(Object.entries(copy.command.env).map(([key, value]) => [key, shouldRedactKey(key) ? "[REDACTED]" : value]));
|
|
37293
|
+
}
|
|
37294
|
+
return copy;
|
|
37295
|
+
}
|
|
37296
|
+
function sanitizeChannelsForOutput(channels) {
|
|
37297
|
+
return channels.map(sanitizeChannelForOutput);
|
|
37298
|
+
}
|
|
37299
|
+
function redactSensitiveKeys(event, replacement = "[REDACTED]") {
|
|
37300
|
+
return redactValue(event, replacement);
|
|
37301
|
+
}
|
|
37302
|
+
function shouldRedactKey(key) {
|
|
37303
|
+
return /secret|token|password|api[_-]?key|authorization/i.test(key);
|
|
37304
|
+
}
|
|
37305
|
+
function redactValue(value, replacement) {
|
|
37306
|
+
if (Array.isArray(value))
|
|
37307
|
+
return value.map((item) => redactValue(item, replacement));
|
|
37308
|
+
if (!value || typeof value !== "object")
|
|
37309
|
+
return value;
|
|
37310
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
37311
|
+
key,
|
|
37312
|
+
shouldRedactKey(key) ? replacement : redactValue(item, replacement)
|
|
37313
|
+
]));
|
|
37314
|
+
}
|
|
37315
|
+
function setPath(input, path, replacement) {
|
|
37316
|
+
const parts = path.split(".");
|
|
37317
|
+
let cursor = input;
|
|
37318
|
+
for (const part of parts.slice(0, -1)) {
|
|
37319
|
+
const next = cursor[part];
|
|
37320
|
+
if (!next || typeof next !== "object")
|
|
37321
|
+
return;
|
|
37322
|
+
cursor = next;
|
|
37323
|
+
}
|
|
37324
|
+
const last = parts.at(-1);
|
|
37325
|
+
if (last && last in cursor)
|
|
37326
|
+
cursor[last] = replacement;
|
|
37327
|
+
}
|
|
37328
|
+
function normalizeTime(value) {
|
|
37329
|
+
if (!value)
|
|
37330
|
+
return new Date().toISOString();
|
|
37331
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
37332
|
+
}
|
|
37333
|
+
function normalizeRetryPolicy(policy) {
|
|
37334
|
+
return {
|
|
37335
|
+
maxAttempts: Math.max(1, policy?.maxAttempts ?? 1),
|
|
37336
|
+
backoffMs: Math.max(0, policy?.backoffMs ?? 250),
|
|
37337
|
+
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
37338
|
+
};
|
|
37339
|
+
}
|
|
37340
|
+
function parseJsonObject(value, fallback) {
|
|
37341
|
+
if (!value)
|
|
37342
|
+
return fallback;
|
|
37343
|
+
const parsed = JSON.parse(value);
|
|
37344
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
37345
|
+
throw new Error("Expected a JSON object");
|
|
37346
|
+
}
|
|
37347
|
+
return parsed;
|
|
37348
|
+
}
|
|
37349
|
+
function parseHeaders(values) {
|
|
37350
|
+
if (!values?.length)
|
|
37351
|
+
return;
|
|
37352
|
+
const headers = {};
|
|
37353
|
+
for (const value of values) {
|
|
37354
|
+
const separator = value.indexOf("=");
|
|
37355
|
+
if (separator === -1)
|
|
37356
|
+
throw new Error(`Invalid header, expected name=value: ${value}`);
|
|
37357
|
+
headers[value.slice(0, separator)] = value.slice(separator + 1);
|
|
37358
|
+
}
|
|
37359
|
+
return headers;
|
|
37360
|
+
}
|
|
37361
|
+
function parseFilter(options) {
|
|
37362
|
+
const filter2 = {};
|
|
37363
|
+
if (options.source)
|
|
37364
|
+
filter2.source = options.source;
|
|
37365
|
+
if (options.type)
|
|
37366
|
+
filter2.type = options.type;
|
|
37367
|
+
if (options.subject)
|
|
37368
|
+
filter2.subject = options.subject;
|
|
37369
|
+
if (options.severity)
|
|
37370
|
+
filter2.severity = options.severity;
|
|
37371
|
+
return Object.keys(filter2).length > 0 ? [filter2] : undefined;
|
|
37372
|
+
}
|
|
37373
|
+
function createClient(options) {
|
|
37374
|
+
if (options.createClient)
|
|
37375
|
+
return options.createClient();
|
|
37376
|
+
return new EventsClient({ store: new JsonEventsStore(options.dataDir) });
|
|
37377
|
+
}
|
|
37378
|
+
function print(value, json, text) {
|
|
37379
|
+
if (json)
|
|
37380
|
+
console.log(JSON.stringify(value, null, 2));
|
|
37381
|
+
else
|
|
37382
|
+
console.log(text);
|
|
37383
|
+
}
|
|
37384
|
+
function registerWebhookCommands(program2, options) {
|
|
37385
|
+
const webhooks = program2.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
|
|
37386
|
+
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) => {
|
|
37387
|
+
const timestamp = new Date().toISOString();
|
|
37388
|
+
const channel = {
|
|
37389
|
+
id: actionOptions.id,
|
|
37390
|
+
name: actionOptions.name,
|
|
37391
|
+
enabled: !actionOptions.disabled,
|
|
37392
|
+
transport: actionOptions.transport,
|
|
37393
|
+
filters: parseFilter(actionOptions),
|
|
37394
|
+
retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
|
|
37395
|
+
redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
|
|
37396
|
+
createdAt: timestamp,
|
|
37397
|
+
updatedAt: timestamp
|
|
37398
|
+
};
|
|
37399
|
+
if (actionOptions.transport === "webhook") {
|
|
37400
|
+
channel.webhook = { url: target, secret: actionOptions.secret, headers: parseHeaders(actionOptions.header), timeoutMs: actionOptions.timeoutMs };
|
|
37401
|
+
} else if (actionOptions.transport === "command") {
|
|
37402
|
+
channel.command = { command: target, args: actionOptions.arg ?? [], timeoutMs: actionOptions.timeoutMs };
|
|
37403
|
+
} else {
|
|
37404
|
+
throw new Error(`Transport ${actionOptions.transport} is reserved for future use and cannot be added yet`);
|
|
37405
|
+
}
|
|
37406
|
+
const saved = await createClient(options).addChannel(channel);
|
|
37407
|
+
print(sanitizeChannelForOutput(saved), Boolean(actionOptions.json), `Added ${saved.transport} channel ${saved.id}`);
|
|
37408
|
+
});
|
|
37409
|
+
webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions) => {
|
|
37410
|
+
const channels = await createClient(options).listChannels();
|
|
37411
|
+
if (actionOptions.json) {
|
|
37412
|
+
console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
|
|
37413
|
+
return;
|
|
37414
|
+
}
|
|
37415
|
+
if (!channels.length) {
|
|
37416
|
+
console.log("No channels configured.");
|
|
37417
|
+
return;
|
|
37418
|
+
}
|
|
37419
|
+
for (const channel of channels) {
|
|
37420
|
+
console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
|
|
37421
|
+
}
|
|
37422
|
+
});
|
|
37423
|
+
webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions) => {
|
|
37424
|
+
const removed = await createClient(options).removeChannel(id);
|
|
37425
|
+
print({ removed }, Boolean(actionOptions.json), removed ? `Removed ${id}` : `Channel not found: ${id}`);
|
|
37426
|
+
});
|
|
37427
|
+
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) => {
|
|
37428
|
+
const result = await createClient(options).testChannel(id, {
|
|
37429
|
+
source: options.source,
|
|
37430
|
+
type: actionOptions.type,
|
|
37431
|
+
subject: actionOptions.subject ?? id,
|
|
37432
|
+
message: actionOptions.message,
|
|
37433
|
+
data: parseJsonObject(actionOptions.data, { test: true })
|
|
37434
|
+
});
|
|
37435
|
+
print(result, Boolean(actionOptions.json), `${result.status}: ${result.channelId}`);
|
|
37436
|
+
});
|
|
37437
|
+
return webhooks;
|
|
37438
|
+
}
|
|
37439
|
+
function registerEventCommands(program2, options) {
|
|
37440
|
+
const events = program2.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
|
|
37441
|
+
events.command("emit").description("Emit an event from this app").argument("<type>", "Event type").option("--source <source>", "Event source override").option("--subject <subject>", "Event subject").option("--severity <severity>", "Event severity", "info").option("--message <message>", "Event message").option("--dedupe-key <key>", "Dedupe key").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--no-deliver", "Record without delivering").option("--no-dedupe", "Allow duplicate id/dedupeKey events").option("-j, --json", "Print JSON output", false).action(async (type, actionOptions) => {
|
|
37442
|
+
const result = await createClient(options).emit({
|
|
37443
|
+
source: actionOptions.source ?? options.source,
|
|
37444
|
+
type,
|
|
37445
|
+
subject: actionOptions.subject,
|
|
37446
|
+
severity: actionOptions.severity,
|
|
37447
|
+
message: actionOptions.message,
|
|
37448
|
+
dedupeKey: actionOptions.dedupeKey,
|
|
37449
|
+
data: parseJsonObject(actionOptions.data, {}),
|
|
37450
|
+
metadata: parseJsonObject(actionOptions.metadata, {})
|
|
37451
|
+
}, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
|
|
37452
|
+
print(result, Boolean(actionOptions.json), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
|
|
37453
|
+
});
|
|
37454
|
+
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) => {
|
|
37455
|
+
let rows = await createClient(options).listEvents();
|
|
37456
|
+
if (actionOptions.source)
|
|
37457
|
+
rows = rows.filter((event) => event.source === actionOptions.source);
|
|
37458
|
+
if (actionOptions.type)
|
|
37459
|
+
rows = rows.filter((event) => event.type === actionOptions.type);
|
|
37460
|
+
if (actionOptions.limit)
|
|
37461
|
+
rows = rows.slice(-actionOptions.limit);
|
|
37462
|
+
if (actionOptions.json) {
|
|
37463
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
37464
|
+
return;
|
|
37465
|
+
}
|
|
37466
|
+
if (!rows.length) {
|
|
37467
|
+
console.log("No events recorded.");
|
|
37468
|
+
return;
|
|
37469
|
+
}
|
|
37470
|
+
for (const event of rows)
|
|
37471
|
+
console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
|
|
37472
|
+
});
|
|
37473
|
+
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) => {
|
|
37474
|
+
const result = await createClient(options).replay({
|
|
37475
|
+
eventId: actionOptions.id,
|
|
37476
|
+
source: actionOptions.source,
|
|
37477
|
+
type: actionOptions.type,
|
|
37478
|
+
dryRun: actionOptions.dryRun
|
|
37479
|
+
});
|
|
37480
|
+
print(result, Boolean(actionOptions.json), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
|
|
37481
|
+
});
|
|
37482
|
+
return events;
|
|
37483
|
+
}
|
|
37484
|
+
function registerEventsCommands(program2, options) {
|
|
37485
|
+
registerWebhookCommands(program2, options);
|
|
37486
|
+
registerEventCommands(program2, options);
|
|
37487
|
+
}
|
|
37488
|
+
function parseNumber(value) {
|
|
37489
|
+
const parsed = Number(value);
|
|
37490
|
+
if (!Number.isFinite(parsed))
|
|
37491
|
+
throw new Error(`Expected a number, got ${value}`);
|
|
37492
|
+
return parsed;
|
|
37493
|
+
}
|
|
37494
|
+
function collectValues(value, previous) {
|
|
37495
|
+
previous.push(value);
|
|
37496
|
+
return previous;
|
|
37497
|
+
}
|
|
37498
|
+
|
|
36821
37499
|
// node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
36822
37500
|
var ANSI_BACKGROUND_OFFSET = 10;
|
|
36823
37501
|
var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
|
|
@@ -41321,20 +41999,20 @@ class AccountsError extends Error {
|
|
|
41321
41999
|
}
|
|
41322
42000
|
|
|
41323
42001
|
// src/lib/tools.ts
|
|
41324
|
-
import { homedir as
|
|
41325
|
-
import { join as
|
|
42002
|
+
import { homedir as homedir3 } from "node:os";
|
|
42003
|
+
import { join as join3 } from "node:path";
|
|
41326
42004
|
|
|
41327
42005
|
// src/storage.ts
|
|
41328
|
-
import { homedir } from "node:os";
|
|
42006
|
+
import { homedir as homedir2 } from "node:os";
|
|
41329
42007
|
import { hostname } from "node:os";
|
|
41330
|
-
import { join } from "node:path";
|
|
41331
|
-
import { existsSync as
|
|
42008
|
+
import { join as join2 } from "node:path";
|
|
42009
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "node:fs";
|
|
41332
42010
|
|
|
41333
42011
|
// src/lib/safe-path.ts
|
|
41334
|
-
import { existsSync, lstatSync, mkdirSync, realpathSync } from "node:fs";
|
|
42012
|
+
import { existsSync as existsSync2, lstatSync, mkdirSync, realpathSync } from "node:fs";
|
|
41335
42013
|
import { dirname, resolve } from "node:path";
|
|
41336
42014
|
function throwIfSymlink(path, label) {
|
|
41337
|
-
if (
|
|
42015
|
+
if (existsSync2(path) && lstatSync(path).isSymbolicLink()) {
|
|
41338
42016
|
throw new AccountsError(`${label}: ${path}`);
|
|
41339
42017
|
}
|
|
41340
42018
|
}
|
|
@@ -41370,11 +42048,11 @@ function assertDirChainSafe(targetFile, mustStayUnder) {
|
|
|
41370
42048
|
function assertSafeWritePath(filePath, opts) {
|
|
41371
42049
|
const absFile = resolve(filePath);
|
|
41372
42050
|
const parent = dirname(absFile);
|
|
41373
|
-
if (!
|
|
42051
|
+
if (!existsSync2(parent))
|
|
41374
42052
|
mkdirSync(parent, { recursive: true });
|
|
41375
42053
|
throwIfSymlink(absFile, "refusing to write through symlink");
|
|
41376
42054
|
assertDirChainSafe(absFile, opts?.mustStayUnder);
|
|
41377
|
-
const resolved = realpathSync(
|
|
42055
|
+
const resolved = realpathSync(existsSync2(absFile) ? absFile : parent);
|
|
41378
42056
|
if (opts?.mustStayUnder) {
|
|
41379
42057
|
const base = realpathSync(resolve(opts.mustStayUnder));
|
|
41380
42058
|
if (resolved !== base && !resolved.startsWith(base + "/")) {
|
|
@@ -41416,21 +42094,21 @@ function accountsHome() {
|
|
|
41416
42094
|
const override = process.env.ACCOUNTS_HOME;
|
|
41417
42095
|
if (override && override.trim())
|
|
41418
42096
|
return validateEnvPath(override, "ACCOUNTS_HOME");
|
|
41419
|
-
return
|
|
42097
|
+
return join2(homedir2(), ".hasna", "accounts");
|
|
41420
42098
|
}
|
|
41421
42099
|
function storePath() {
|
|
41422
42100
|
const override = process.env.ACCOUNTS_STORE_PATH;
|
|
41423
42101
|
if (override && override.trim())
|
|
41424
42102
|
return validateEnvPath(override, "ACCOUNTS_STORE_PATH");
|
|
41425
|
-
return
|
|
42103
|
+
return join2(accountsHome(), "accounts.json");
|
|
41426
42104
|
}
|
|
41427
42105
|
function profilesDir() {
|
|
41428
|
-
return
|
|
42106
|
+
return join2(accountsHome(), "profiles");
|
|
41429
42107
|
}
|
|
41430
42108
|
var EMPTY_STORE = { version: 1, current: {}, applied: {}, profiles: [], tools: [] };
|
|
41431
42109
|
function loadStore() {
|
|
41432
42110
|
const path = storePath();
|
|
41433
|
-
if (!
|
|
42111
|
+
if (!existsSync3(path))
|
|
41434
42112
|
return structuredClone(EMPTY_STORE);
|
|
41435
42113
|
let raw;
|
|
41436
42114
|
try {
|
|
@@ -41468,7 +42146,7 @@ function loadStore() {
|
|
|
41468
42146
|
function saveStore(store) {
|
|
41469
42147
|
const path = storePath();
|
|
41470
42148
|
assertSafeWritePath(path, { mustStayUnder: accountsHome() });
|
|
41471
|
-
mkdirSync2(
|
|
42149
|
+
mkdirSync2(join2(path, ".."), { recursive: true });
|
|
41472
42150
|
writeFileSync(path, JSON.stringify(store, null, 2) + `
|
|
41473
42151
|
`, { mode: 384 });
|
|
41474
42152
|
}
|
|
@@ -41616,7 +42294,7 @@ var BUILTIN_TOOLS = [
|
|
|
41616
42294
|
id: "claude",
|
|
41617
42295
|
label: "Claude Code",
|
|
41618
42296
|
envVar: "CLAUDE_CONFIG_DIR",
|
|
41619
|
-
defaultDir:
|
|
42297
|
+
defaultDir: join3(homedir3(), ".claude"),
|
|
41620
42298
|
bin: "claude",
|
|
41621
42299
|
loginHint: "run /login inside Claude, then /exit when done",
|
|
41622
42300
|
resumeArgs: ["--continue"],
|
|
@@ -41627,7 +42305,7 @@ var BUILTIN_TOOLS = [
|
|
|
41627
42305
|
id: "codex",
|
|
41628
42306
|
label: "Codex CLI",
|
|
41629
42307
|
envVar: "CODEX_HOME",
|
|
41630
|
-
defaultDir:
|
|
42308
|
+
defaultDir: join3(homedir3(), ".codex"),
|
|
41631
42309
|
bin: "codex",
|
|
41632
42310
|
loginArgs: ["login"],
|
|
41633
42311
|
loginHint: "complete the Codex login flow for this CODEX_HOME",
|
|
@@ -41637,7 +42315,7 @@ var BUILTIN_TOOLS = [
|
|
|
41637
42315
|
id: "takumi",
|
|
41638
42316
|
label: "Takumi",
|
|
41639
42317
|
envVar: "TAKUMI_CONFIG_DIR",
|
|
41640
|
-
defaultDir:
|
|
42318
|
+
defaultDir: join3(homedir3(), ".takumi"),
|
|
41641
42319
|
bin: "takumi",
|
|
41642
42320
|
loginHint: "complete Takumi auth in this TAKUMI_CONFIG_DIR",
|
|
41643
42321
|
resumeArgs: ["--continue"],
|
|
@@ -41648,7 +42326,7 @@ var BUILTIN_TOOLS = [
|
|
|
41648
42326
|
id: "gemini",
|
|
41649
42327
|
label: "Gemini CLI",
|
|
41650
42328
|
envVar: "GEMINI_CONFIG_DIR",
|
|
41651
|
-
defaultDir:
|
|
42329
|
+
defaultDir: join3(homedir3(), ".gemini"),
|
|
41652
42330
|
bin: "gemini",
|
|
41653
42331
|
loginHint: "complete Gemini auth in this GEMINI_CONFIG_DIR"
|
|
41654
42332
|
},
|
|
@@ -41660,7 +42338,7 @@ var BUILTIN_TOOLS = [
|
|
|
41660
42338
|
XDG_CONFIG_HOME: "{profileDir}/xdg-config",
|
|
41661
42339
|
XDG_DATA_HOME: "{profileDir}/xdg-data"
|
|
41662
42340
|
},
|
|
41663
|
-
defaultDir:
|
|
42341
|
+
defaultDir: join3(homedir3(), ".config", "opencode"),
|
|
41664
42342
|
bin: "opencode",
|
|
41665
42343
|
loginArgs: ["auth", "login"],
|
|
41666
42344
|
loginHint: "complete opencode auth login for this isolated config/data root",
|
|
@@ -41670,7 +42348,7 @@ var BUILTIN_TOOLS = [
|
|
|
41670
42348
|
id: "cursor",
|
|
41671
42349
|
label: "Cursor Agent",
|
|
41672
42350
|
envVar: "CURSOR_CONFIG_DIR",
|
|
41673
|
-
defaultDir:
|
|
42351
|
+
defaultDir: join3(homedir3(), ".cursor"),
|
|
41674
42352
|
bin: "cursor-agent",
|
|
41675
42353
|
loginArgs: ["login"],
|
|
41676
42354
|
loginHint: "complete cursor-agent login for this CURSOR_CONFIG_DIR"
|
|
@@ -41679,7 +42357,7 @@ var BUILTIN_TOOLS = [
|
|
|
41679
42357
|
id: "pi",
|
|
41680
42358
|
label: "Pi Coding Agent",
|
|
41681
42359
|
envVar: "PI_CODING_AGENT_HOME",
|
|
41682
|
-
defaultDir:
|
|
42360
|
+
defaultDir: join3(homedir3(), ".pi"),
|
|
41683
42361
|
bin: "pi",
|
|
41684
42362
|
loginHint: "complete Pi coding agent auth in this PI_CODING_AGENT_HOME"
|
|
41685
42363
|
},
|
|
@@ -41687,7 +42365,7 @@ var BUILTIN_TOOLS = [
|
|
|
41687
42365
|
id: "hermes",
|
|
41688
42366
|
label: "Hermes",
|
|
41689
42367
|
envVar: "HERMES_HOME",
|
|
41690
|
-
defaultDir:
|
|
42368
|
+
defaultDir: join3(homedir3(), ".hermes"),
|
|
41691
42369
|
bin: "hermes",
|
|
41692
42370
|
loginHint: "complete Hermes auth in this HERMES_HOME"
|
|
41693
42371
|
},
|
|
@@ -41695,7 +42373,7 @@ var BUILTIN_TOOLS = [
|
|
|
41695
42373
|
id: "kimi",
|
|
41696
42374
|
label: "Kimi Code",
|
|
41697
42375
|
envVar: "KIMI_CODE_HOME",
|
|
41698
|
-
defaultDir:
|
|
42376
|
+
defaultDir: join3(homedir3(), ".kimi-code"),
|
|
41699
42377
|
bin: "kimi",
|
|
41700
42378
|
loginArgs: ["login"],
|
|
41701
42379
|
loginHint: "complete kimi login for this KIMI_CODE_HOME"
|
|
@@ -41704,7 +42382,7 @@ var BUILTIN_TOOLS = [
|
|
|
41704
42382
|
id: "grok",
|
|
41705
42383
|
label: "Grok Build",
|
|
41706
42384
|
envVar: "HOME",
|
|
41707
|
-
defaultDir:
|
|
42385
|
+
defaultDir: join3(homedir3(), ".grok"),
|
|
41708
42386
|
bin: "grok",
|
|
41709
42387
|
loginArgs: ["login"],
|
|
41710
42388
|
loginHint: "complete grok login in this process-scoped HOME; prefer launch/shell over exporting HOME globally"
|
|
@@ -41765,19 +42443,19 @@ function removeCustomTool(id) {
|
|
|
41765
42443
|
}
|
|
41766
42444
|
|
|
41767
42445
|
// src/lib/profiles.ts
|
|
41768
|
-
import { homedir as
|
|
41769
|
-
import { isAbsolute, join as
|
|
41770
|
-
import { existsSync as
|
|
42446
|
+
import { homedir as homedir4 } from "node:os";
|
|
42447
|
+
import { isAbsolute, join as join5, resolve as resolve2 } from "node:path";
|
|
42448
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, rmSync } from "node:fs";
|
|
41771
42449
|
|
|
41772
42450
|
// src/lib/detect.ts
|
|
41773
|
-
import { existsSync as
|
|
41774
|
-
import { dirname as dirname2, join as
|
|
42451
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
|
|
42452
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
41775
42453
|
function detectEmail(dir, tool) {
|
|
41776
42454
|
if (!tool.accountFile || !tool.emailPath)
|
|
41777
42455
|
return;
|
|
41778
|
-
const candidates = [
|
|
42456
|
+
const candidates = [join4(dir, tool.accountFile)];
|
|
41779
42457
|
if (dir === tool.defaultDir)
|
|
41780
|
-
candidates.push(
|
|
42458
|
+
candidates.push(join4(dirname2(dir), tool.accountFile));
|
|
41781
42459
|
for (const file of candidates) {
|
|
41782
42460
|
const email = readEmail(file, tool.emailPath);
|
|
41783
42461
|
if (email)
|
|
@@ -41786,7 +42464,7 @@ function detectEmail(dir, tool) {
|
|
|
41786
42464
|
return;
|
|
41787
42465
|
}
|
|
41788
42466
|
function readEmail(file, path) {
|
|
41789
|
-
if (!
|
|
42467
|
+
if (!existsSync4(file))
|
|
41790
42468
|
return;
|
|
41791
42469
|
let cursor;
|
|
41792
42470
|
try {
|
|
@@ -41811,9 +42489,9 @@ function nowIso() {
|
|
|
41811
42489
|
function expandPath(p) {
|
|
41812
42490
|
let out = p;
|
|
41813
42491
|
if (out === "~")
|
|
41814
|
-
out =
|
|
42492
|
+
out = homedir4();
|
|
41815
42493
|
else if (out.startsWith("~/"))
|
|
41816
|
-
out =
|
|
42494
|
+
out = join5(homedir4(), out.slice(2));
|
|
41817
42495
|
return isAbsolute(out) ? out : resolve2(process.cwd(), out);
|
|
41818
42496
|
}
|
|
41819
42497
|
function listProfiles(toolId) {
|
|
@@ -41847,7 +42525,7 @@ function addProfile(opts) {
|
|
|
41847
42525
|
if (store.profiles.some((p) => p.name === name && p.tool === toolId)) {
|
|
41848
42526
|
throw new AccountsError(`a ${toolId} profile named "${name}" already exists`);
|
|
41849
42527
|
}
|
|
41850
|
-
const dir = opts.dir ? expandPath(opts.dir) :
|
|
42528
|
+
const dir = opts.dir ? expandPath(opts.dir) : join5(profilesDir(), toolId, name);
|
|
41851
42529
|
if (store.profiles.some((p) => p.dir === dir)) {
|
|
41852
42530
|
throw new AccountsError(`a profile already uses config dir ${dir}`);
|
|
41853
42531
|
}
|
|
@@ -41889,7 +42567,7 @@ function removeProfile(name, opts = {}) {
|
|
|
41889
42567
|
if (options.purge) {
|
|
41890
42568
|
const managed = profile.dir.startsWith(profilesDir());
|
|
41891
42569
|
const isDefault = profile.dir === getTool(profile.tool).defaultDir;
|
|
41892
|
-
if (managed && !isDefault &&
|
|
42570
|
+
if (managed && !isDefault && existsSync5(profile.dir)) {
|
|
41893
42571
|
rmSync(profile.dir, { recursive: true, force: true });
|
|
41894
42572
|
purged = true;
|
|
41895
42573
|
} else {
|
|
@@ -41988,12 +42666,12 @@ function currentProfile(toolId) {
|
|
|
41988
42666
|
}
|
|
41989
42667
|
|
|
41990
42668
|
// src/lib/claude-auth.ts
|
|
41991
|
-
import { copyFileSync, existsSync as
|
|
41992
|
-
import { dirname as dirname4, join as
|
|
42669
|
+
import { copyFileSync, existsSync as existsSync6, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync3, statSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
42670
|
+
import { dirname as dirname4, join as join7 } from "node:path";
|
|
41993
42671
|
|
|
41994
42672
|
// src/lib/claude-layout.ts
|
|
41995
|
-
import { homedir as
|
|
41996
|
-
import { dirname as dirname3, join as
|
|
42673
|
+
import { homedir as homedir5 } from "node:os";
|
|
42674
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
41997
42675
|
var CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
41998
42676
|
var ACCOUNTS_AUTH_DIR = ".accounts-auth";
|
|
41999
42677
|
var OAUTH_SNAPSHOT = "oauth-account.json";
|
|
@@ -42001,36 +42679,36 @@ var CREDENTIALS_SNAPSHOT = "credentials.json";
|
|
|
42001
42679
|
var KEYCHAIN_SNAPSHOT = "keychain.json";
|
|
42002
42680
|
function liveClaudeBase() {
|
|
42003
42681
|
const testBase = process.env.ACCOUNTS_TEST_LIVE_DIR;
|
|
42004
|
-
return testBase && testBase.trim() ? testBase :
|
|
42682
|
+
return testBase && testBase.trim() ? testBase : homedir5();
|
|
42005
42683
|
}
|
|
42006
42684
|
function liveClaudePaths() {
|
|
42007
42685
|
const base = liveClaudeBase();
|
|
42008
|
-
const configDir =
|
|
42686
|
+
const configDir = join6(base, ".claude");
|
|
42009
42687
|
return {
|
|
42010
42688
|
configDir,
|
|
42011
|
-
homeJson:
|
|
42012
|
-
credentialsFile:
|
|
42689
|
+
homeJson: join6(base, ".claude.json"),
|
|
42690
|
+
credentialsFile: join6(configDir, ".credentials.json")
|
|
42013
42691
|
};
|
|
42014
42692
|
}
|
|
42015
42693
|
function profileAccountJsonPaths(profileDir, tool) {
|
|
42016
42694
|
if (!tool.accountFile)
|
|
42017
42695
|
return [];
|
|
42018
|
-
const paths = [
|
|
42696
|
+
const paths = [join6(profileDir, tool.accountFile)];
|
|
42019
42697
|
if (profileDir === tool.defaultDir)
|
|
42020
|
-
paths.push(
|
|
42698
|
+
paths.push(join6(dirname3(profileDir), tool.accountFile));
|
|
42021
42699
|
return paths;
|
|
42022
42700
|
}
|
|
42023
42701
|
function profileAuthDir(profileDir) {
|
|
42024
|
-
return
|
|
42702
|
+
return join6(profileDir, ACCOUNTS_AUTH_DIR);
|
|
42025
42703
|
}
|
|
42026
42704
|
function profileOAuthSnapshot(profileDir) {
|
|
42027
|
-
return
|
|
42705
|
+
return join6(profileAuthDir(profileDir), OAUTH_SNAPSHOT);
|
|
42028
42706
|
}
|
|
42029
42707
|
function profileCredentialsSnapshot(profileDir) {
|
|
42030
|
-
return
|
|
42708
|
+
return join6(profileAuthDir(profileDir), CREDENTIALS_SNAPSHOT);
|
|
42031
42709
|
}
|
|
42032
42710
|
function profileKeychainSnapshot(profileDir) {
|
|
42033
|
-
return
|
|
42711
|
+
return join6(profileAuthDir(profileDir), KEYCHAIN_SNAPSHOT);
|
|
42034
42712
|
}
|
|
42035
42713
|
|
|
42036
42714
|
// src/lib/keychain.ts
|
|
@@ -42095,7 +42773,7 @@ function writeClaudeKeychain(cred) {
|
|
|
42095
42773
|
|
|
42096
42774
|
// src/lib/claude-auth.ts
|
|
42097
42775
|
function readJsonFile(path) {
|
|
42098
|
-
if (!
|
|
42776
|
+
if (!existsSync6(path))
|
|
42099
42777
|
return;
|
|
42100
42778
|
try {
|
|
42101
42779
|
return JSON.parse(readFileSync3(path, "utf8"));
|
|
@@ -42122,7 +42800,7 @@ function findOAuthSource(paths) {
|
|
|
42122
42800
|
return;
|
|
42123
42801
|
}
|
|
42124
42802
|
function snapshotIsStale(sourcePath, snapshotPath) {
|
|
42125
|
-
if (!
|
|
42803
|
+
if (!existsSync6(snapshotPath))
|
|
42126
42804
|
return true;
|
|
42127
42805
|
try {
|
|
42128
42806
|
return statSync(sourcePath).mtimeMs > statSync(snapshotPath).mtimeMs;
|
|
@@ -42161,13 +42839,13 @@ function liveOAuthEmail() {
|
|
|
42161
42839
|
}
|
|
42162
42840
|
function snapshotLiveAuthToProfile(profileDir, _tool) {
|
|
42163
42841
|
const authDir = profileAuthDir(profileDir);
|
|
42164
|
-
assertSafeWritePath(
|
|
42842
|
+
assertSafeWritePath(join7(authDir, OAUTH_SNAPSHOT), { mustStayUnder: profileDir });
|
|
42165
42843
|
mkdirSync4(authDir, { recursive: true });
|
|
42166
42844
|
const live = liveClaudePaths();
|
|
42167
42845
|
const oauth = readOAuthFromPaths([live.homeJson]);
|
|
42168
42846
|
if (oauth)
|
|
42169
42847
|
writeJsonFile(profileOAuthSnapshot(profileDir), { oauthAccount: oauth }, profileDir);
|
|
42170
|
-
if (
|
|
42848
|
+
if (existsSync6(live.credentialsFile)) {
|
|
42171
42849
|
const dest = profileCredentialsSnapshot(profileDir);
|
|
42172
42850
|
assertSafeWritePath(dest, { mustStayUnder: profileDir });
|
|
42173
42851
|
copyFileSync(live.credentialsFile, dest);
|
|
@@ -42180,16 +42858,16 @@ function snapshotLiveAuthToProfile(profileDir, _tool) {
|
|
|
42180
42858
|
}
|
|
42181
42859
|
function ensureProfileAuthSnapshot(profileDir, tool, opts = {}) {
|
|
42182
42860
|
const authDir = profileAuthDir(profileDir);
|
|
42183
|
-
assertSafeWritePath(
|
|
42861
|
+
assertSafeWritePath(join7(authDir, OAUTH_SNAPSHOT), { mustStayUnder: profileDir });
|
|
42184
42862
|
mkdirSync4(authDir, { recursive: true });
|
|
42185
42863
|
const oauthSource = findOAuthSource(profileAccountJsonPaths(profileDir, tool));
|
|
42186
42864
|
const oauthSnap = profileOAuthSnapshot(profileDir);
|
|
42187
42865
|
if (oauthSource && (opts.overwrite || snapshotIsStale(oauthSource.path, oauthSnap))) {
|
|
42188
42866
|
writeJsonFile(oauthSnap, { oauthAccount: oauthSource.oauth }, profileDir);
|
|
42189
42867
|
}
|
|
42190
|
-
const credFile =
|
|
42868
|
+
const credFile = join7(profileDir, ".credentials.json");
|
|
42191
42869
|
const credSnap = profileCredentialsSnapshot(profileDir);
|
|
42192
|
-
if (
|
|
42870
|
+
if (existsSync6(credFile) && (opts.overwrite || snapshotIsStale(credFile, credSnap))) {
|
|
42193
42871
|
assertSafeWritePath(credSnap, { mustStayUnder: profileDir });
|
|
42194
42872
|
copyFileSync(credFile, credSnap);
|
|
42195
42873
|
}
|
|
@@ -42214,12 +42892,12 @@ function restoreClaudeAuthFromProfile(profileDir, tool, profileName) {
|
|
|
42214
42892
|
assertSafeWritePath(live.homeJson, { mustStayUnder: liveRoot });
|
|
42215
42893
|
mergeOAuthInto([live.homeJson], oauth, false, liveRoot);
|
|
42216
42894
|
const credSnap = profileCredentialsSnapshot(profileDir);
|
|
42217
|
-
if (
|
|
42895
|
+
if (existsSync6(credSnap)) {
|
|
42218
42896
|
assertSafeWritePath(live.credentialsFile, { mustStayUnder: liveRoot });
|
|
42219
42897
|
assertSafeWritePath(credSnap, { mustStayUnder: profileDir });
|
|
42220
42898
|
copyFileSync(credSnap, live.credentialsFile);
|
|
42221
42899
|
writeFileSync2(live.credentialsFile, readFileSync3(live.credentialsFile), { mode: 384 });
|
|
42222
|
-
} else if (
|
|
42900
|
+
} else if (existsSync6(live.credentialsFile)) {
|
|
42223
42901
|
if (!lstatSync2(live.credentialsFile).isSymbolicLink())
|
|
42224
42902
|
unlinkSync(live.credentialsFile);
|
|
42225
42903
|
}
|
|
@@ -42241,14 +42919,14 @@ function restoreClaudeAuthFromProfile(profileDir, tool, profileName) {
|
|
|
42241
42919
|
}
|
|
42242
42920
|
}
|
|
42243
42921
|
function hasAuthSnapshot(profileDir) {
|
|
42244
|
-
return
|
|
42922
|
+
return existsSync6(profileOAuthSnapshot(profileDir)) || existsSync6(profileCredentialsSnapshot(profileDir)) || existsSync6(profileKeychainSnapshot(profileDir));
|
|
42245
42923
|
}
|
|
42246
42924
|
|
|
42247
42925
|
// src/lib/apply-lock.ts
|
|
42248
|
-
import { closeSync, existsSync as
|
|
42249
|
-
import { join as
|
|
42926
|
+
import { closeSync, existsSync as existsSync7, mkdirSync as mkdirSync5, openSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
42927
|
+
import { join as join8 } from "node:path";
|
|
42250
42928
|
function lockPath() {
|
|
42251
|
-
return
|
|
42929
|
+
return join8(accountsHome(), ".apply.lock");
|
|
42252
42930
|
}
|
|
42253
42931
|
function withApplyLock(fn) {
|
|
42254
42932
|
const home = accountsHome();
|
|
@@ -42273,7 +42951,7 @@ function withApplyLock(fn) {
|
|
|
42273
42951
|
if (fd !== undefined) {
|
|
42274
42952
|
closeSync(fd);
|
|
42275
42953
|
try {
|
|
42276
|
-
if (
|
|
42954
|
+
if (existsSync7(path))
|
|
42277
42955
|
unlinkSync2(path);
|
|
42278
42956
|
} catch {}
|
|
42279
42957
|
}
|
|
@@ -42319,7 +42997,7 @@ function applyProfileUnlocked(name, toolId) {
|
|
|
42319
42997
|
|
|
42320
42998
|
// src/lib/agents.ts
|
|
42321
42999
|
import { spawnSync } from "node:child_process";
|
|
42322
|
-
import { existsSync as
|
|
43000
|
+
import { existsSync as existsSync8, readFileSync as readFileSync4 } from "node:fs";
|
|
42323
43001
|
import { platform as platform2 } from "node:os";
|
|
42324
43002
|
function extractJsonArray(raw) {
|
|
42325
43003
|
const text = raw.replace(/\r/g, "");
|
|
@@ -42446,7 +43124,7 @@ function listAgentsAcrossProfiles(opts = {}) {
|
|
|
42446
43124
|
const registered = listProfiles(toolId);
|
|
42447
43125
|
const entries = [...registered];
|
|
42448
43126
|
const defaultDir = opts.defaultDir ?? tool.defaultDir;
|
|
42449
|
-
if (defaultDir && !registered.some((p) => p.dir === defaultDir) &&
|
|
43127
|
+
if (defaultDir && !registered.some((p) => p.dir === defaultDir) && existsSync8(defaultDir)) {
|
|
42450
43128
|
entries.unshift({ name: "(default)", tool: toolId, dir: defaultDir });
|
|
42451
43129
|
}
|
|
42452
43130
|
const wanted = entries.filter((p) => !opts.profile || p.name === opts.profile || opts.profile === "default" && p.name === "(default)");
|
|
@@ -42496,19 +43174,19 @@ function listAgentsAcrossProfiles(opts = {}) {
|
|
|
42496
43174
|
}
|
|
42497
43175
|
|
|
42498
43176
|
// src/lib/import-profile.ts
|
|
42499
|
-
import { cpSync, existsSync as
|
|
42500
|
-
import { join as
|
|
43177
|
+
import { cpSync, existsSync as existsSync9 } from "node:fs";
|
|
43178
|
+
import { join as join9 } from "node:path";
|
|
42501
43179
|
function importProfile(opts) {
|
|
42502
43180
|
const toolId = opts.tool ?? DEFAULT_TOOL;
|
|
42503
43181
|
const tool = getTool(toolId);
|
|
42504
43182
|
const name = opts.name ?? "main";
|
|
42505
43183
|
const sourceDir = opts.dir ? expandPath(opts.dir) : tool.defaultDir;
|
|
42506
|
-
if (!
|
|
43184
|
+
if (!existsSync9(sourceDir)) {
|
|
42507
43185
|
throw new AccountsError(`config dir does not exist: ${sourceDir}`);
|
|
42508
43186
|
}
|
|
42509
43187
|
if (opts.copy) {
|
|
42510
|
-
const targetDir =
|
|
42511
|
-
if (
|
|
43188
|
+
const targetDir = join9(profilesDir(), toolId, name);
|
|
43189
|
+
if (existsSync9(targetDir)) {
|
|
42512
43190
|
throw new AccountsError(`managed copy target already exists: ${targetDir}`);
|
|
42513
43191
|
}
|
|
42514
43192
|
cpSync(sourceDir, targetDir, { recursive: true });
|
|
@@ -42592,8 +43270,8 @@ async function pickProfile(opts = {}) {
|
|
|
42592
43270
|
}
|
|
42593
43271
|
|
|
42594
43272
|
// src/lib/hook.ts
|
|
42595
|
-
import { existsSync as
|
|
42596
|
-
import { join as
|
|
43273
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
43274
|
+
import { join as join10 } from "node:path";
|
|
42597
43275
|
var HOOK_FILE = "claude-hook.sh";
|
|
42598
43276
|
var MARKER = "# accounts-claude-hook";
|
|
42599
43277
|
var NAME_PATTERN = "^[a-z0-9][a-z0-9-]*$";
|
|
@@ -42601,7 +43279,7 @@ function shellQuotePath(path) {
|
|
|
42601
43279
|
return `'${path.replace(/'/g, `'\\''`)}'`;
|
|
42602
43280
|
}
|
|
42603
43281
|
function hookPath() {
|
|
42604
|
-
return
|
|
43282
|
+
return join10(accountsHome(), HOOK_FILE);
|
|
42605
43283
|
}
|
|
42606
43284
|
function hookScript() {
|
|
42607
43285
|
const quotedHook = shellQuotePath(hookPath());
|
|
@@ -42632,13 +43310,13 @@ claude() {
|
|
|
42632
43310
|
function installHook() {
|
|
42633
43311
|
const path = hookPath();
|
|
42634
43312
|
mkdirSync6(accountsHome(), { recursive: true });
|
|
42635
|
-
const created = !
|
|
43313
|
+
const created = !existsSync10(path);
|
|
42636
43314
|
writeFileSync4(path, hookScript(), { mode: 493 });
|
|
42637
43315
|
return { path, created };
|
|
42638
43316
|
}
|
|
42639
43317
|
function uninstallHook() {
|
|
42640
43318
|
const path = hookPath();
|
|
42641
|
-
if (!
|
|
43319
|
+
if (!existsSync10(path))
|
|
42642
43320
|
return false;
|
|
42643
43321
|
const content = readFileSync5(path, "utf8");
|
|
42644
43322
|
if (!content.includes(MARKER))
|
|
@@ -42730,24 +43408,24 @@ function switchProfile(name, opts = {}) {
|
|
|
42730
43408
|
}
|
|
42731
43409
|
|
|
42732
43410
|
// src/lib/supervisor.ts
|
|
42733
|
-
import { spawn } from "node:child_process";
|
|
43411
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
42734
43412
|
import { createHash } from "node:crypto";
|
|
42735
|
-
import { existsSync as
|
|
43413
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync6, readdirSync, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "node:fs";
|
|
42736
43414
|
import { createConnection, createServer } from "node:net";
|
|
42737
|
-
import { basename, join as
|
|
43415
|
+
import { basename, join as join11 } from "node:path";
|
|
42738
43416
|
var STATE_SUFFIX = ".json";
|
|
42739
43417
|
function supervisorDir() {
|
|
42740
|
-
return
|
|
43418
|
+
return join11(accountsHome(), "supervisors");
|
|
42741
43419
|
}
|
|
42742
43420
|
function supervisorStatePath(toolId) {
|
|
42743
|
-
return
|
|
43421
|
+
return join11(supervisorDir(), `${toolId}${STATE_SUFFIX}`);
|
|
42744
43422
|
}
|
|
42745
43423
|
function supervisorSocketPath(toolId) {
|
|
42746
43424
|
if (process.platform === "win32") {
|
|
42747
43425
|
const hash = createHash("sha1").update(accountsHome()).digest("hex").slice(0, 12);
|
|
42748
43426
|
return `\\\\.\\pipe\\hasna-accounts-${hash}-${toolId}`;
|
|
42749
43427
|
}
|
|
42750
|
-
return
|
|
43428
|
+
return join11(supervisorDir(), `${toolId}.sock`);
|
|
42751
43429
|
}
|
|
42752
43430
|
function nowIso2() {
|
|
42753
43431
|
return new Date().toISOString();
|
|
@@ -42761,7 +43439,7 @@ function parseState(raw) {
|
|
|
42761
43439
|
}
|
|
42762
43440
|
function readSupervisorState(toolId) {
|
|
42763
43441
|
const path = supervisorStatePath(toolId);
|
|
42764
|
-
if (!
|
|
43442
|
+
if (!existsSync11(path))
|
|
42765
43443
|
return;
|
|
42766
43444
|
try {
|
|
42767
43445
|
return parseState(readFileSync6(path, "utf8"));
|
|
@@ -42771,7 +43449,7 @@ function readSupervisorState(toolId) {
|
|
|
42771
43449
|
}
|
|
42772
43450
|
function listSupervisorStates() {
|
|
42773
43451
|
const dir = supervisorDir();
|
|
42774
|
-
if (!
|
|
43452
|
+
if (!existsSync11(dir))
|
|
42775
43453
|
return [];
|
|
42776
43454
|
return readdirSync(dir).filter((name) => name.endsWith(STATE_SUFFIX)).map((name) => basename(name, STATE_SUFFIX)).map((toolId) => readSupervisorState(toolId)).filter((state) => state !== undefined);
|
|
42777
43455
|
}
|
|
@@ -42994,7 +43672,7 @@ async function runSupervisedTool(initialProfile, tool, initialArgs = [], opts =
|
|
|
42994
43672
|
useProfile(profile.name, tool.id);
|
|
42995
43673
|
const env2 = profileEnv(profile, tool);
|
|
42996
43674
|
log(`accounts supervisor: starting ${tool.bin} for ${profile.name}`);
|
|
42997
|
-
const proc =
|
|
43675
|
+
const proc = spawn2(tool.bin, childArgs, {
|
|
42998
43676
|
stdio: opts.stdio ?? "inherit",
|
|
42999
43677
|
env: { ...process.env, ...env2, ACCOUNTS_SUPERVISOR: "1", ACCOUNTS_ACTIVE: profile.name },
|
|
43000
43678
|
detached: process.platform !== "win32"
|
|
@@ -43178,7 +43856,7 @@ program2.command("show").argument("<name>", "profile name").description("show fu
|
|
|
43178
43856
|
console.log(` tool: ${p.tool} (${getTool(p.tool).label})`);
|
|
43179
43857
|
console.log(` active: ${active ? source_default.green("yes") : source_default.dim("no")}`);
|
|
43180
43858
|
console.log(` applied: ${isApplied ? source_default.magenta("yes") : source_default.dim("no")}`);
|
|
43181
|
-
console.log(` config dir: ${p.dir}${
|
|
43859
|
+
console.log(` config dir: ${p.dir}${existsSync12(p.dir) ? "" : source_default.red(" [missing]")}`);
|
|
43182
43860
|
console.log(` email: ${p.email ?? source_default.dim("(none)")}`);
|
|
43183
43861
|
console.log(` created: ${p.createdAt}`);
|
|
43184
43862
|
if (p.lastUsedAt)
|
|
@@ -43316,7 +43994,7 @@ program2.command("switch").argument("<name>", "profile name").argument("[args...
|
|
|
43316
43994
|
}
|
|
43317
43995
|
}));
|
|
43318
43996
|
var hook = program2.command("hook").description("install a shell wrapper for claude");
|
|
43319
|
-
hook.command("install").description(`write ${
|
|
43997
|
+
hook.command("install").description(`write ${join12(accountsHome(), "claude-hook.sh")}`).action(action(() => {
|
|
43320
43998
|
const { path, created } = installHook();
|
|
43321
43999
|
console.log(source_default.green(created ? `✓ installed hook at ${path}` : `✓ updated hook at ${path}`));
|
|
43322
44000
|
console.log(source_default.dim(` add to ~/.zshrc: ${shellSnippet()}`));
|
|
@@ -43541,7 +44219,7 @@ tools.command("add").argument("<id>", "tool id, e.g. cursor").description("regis
|
|
|
43541
44219
|
label: opts.label,
|
|
43542
44220
|
envVar: opts.envVar,
|
|
43543
44221
|
bin: opts.bin,
|
|
43544
|
-
defaultDir: opts.defaultDir ? expandPath(opts.defaultDir) :
|
|
44222
|
+
defaultDir: opts.defaultDir ? expandPath(opts.defaultDir) : join12(homedir6(), `.${id}`),
|
|
43545
44223
|
...Object.keys(extraEnv).length > 0 ? { extraEnv } : {},
|
|
43546
44224
|
...opts.loginArg ? { loginArgs: opts.loginArg } : {},
|
|
43547
44225
|
...opts.resumeArg ? { resumeArgs: opts.resumeArg } : {},
|
|
@@ -43562,7 +44240,7 @@ program2.command("doctor").description("check the store and profile dirs for pro
|
|
|
43562
44240
|
const profiles = listProfiles();
|
|
43563
44241
|
let problems = 0;
|
|
43564
44242
|
for (const p of profiles) {
|
|
43565
|
-
const missing = !
|
|
44243
|
+
const missing = !existsSync12(p.dir);
|
|
43566
44244
|
const noEmail = !p.email;
|
|
43567
44245
|
if (missing) {
|
|
43568
44246
|
console.log(source_default.red(` ✗ ${p.name}: config dir missing (${p.dir})`));
|
|
@@ -43606,12 +44284,13 @@ ${problems} problem(s) found.`));
|
|
|
43606
44284
|
console.log(source_default.green(`
|
|
43607
44285
|
healthy.`));
|
|
43608
44286
|
}));
|
|
44287
|
+
registerEventsCommands(program2, { source: "accounts" });
|
|
43609
44288
|
program2.parseAsync(process.argv);
|
|
43610
44289
|
function getVersion() {
|
|
43611
44290
|
try {
|
|
43612
44291
|
const here = dirname5(fileURLToPath(import.meta.url));
|
|
43613
|
-
for (const candidate of [
|
|
43614
|
-
if (
|
|
44292
|
+
for (const candidate of [join12(here, "..", "package.json"), join12(here, "package.json")]) {
|
|
44293
|
+
if (existsSync12(candidate)) {
|
|
43615
44294
|
const pkg = JSON.parse(readFileSync7(candidate, "utf8"));
|
|
43616
44295
|
if (pkg.version)
|
|
43617
44296
|
return pkg.version;
|