@hasna/accounts 0.1.8 → 0.1.10
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 +802 -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/hasna-events-refresh-1781608411540/open-accounts/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,690 @@ 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 hasJsonOption(options) {
|
|
37385
|
+
return Boolean(options?.json || options?.opts?.().json || options?.optsWithGlobals?.().json || options?.parent?.opts?.().json || options?.parent?.optsWithGlobals?.().json);
|
|
37386
|
+
}
|
|
37387
|
+
function wantsJson(actionOptions, command) {
|
|
37388
|
+
return hasJsonOption(actionOptions) || hasJsonOption(command);
|
|
37389
|
+
}
|
|
37390
|
+
function registerWebhookCommands(program2, options) {
|
|
37391
|
+
const webhooks = program2.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
|
|
37392
|
+
webhooks.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions, command) => {
|
|
37393
|
+
const timestamp = new Date().toISOString();
|
|
37394
|
+
const channel = {
|
|
37395
|
+
id: actionOptions.id,
|
|
37396
|
+
name: actionOptions.name,
|
|
37397
|
+
enabled: !actionOptions.disabled,
|
|
37398
|
+
transport: actionOptions.transport,
|
|
37399
|
+
filters: parseFilter(actionOptions),
|
|
37400
|
+
retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
|
|
37401
|
+
redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
|
|
37402
|
+
createdAt: timestamp,
|
|
37403
|
+
updatedAt: timestamp
|
|
37404
|
+
};
|
|
37405
|
+
if (actionOptions.transport === "webhook") {
|
|
37406
|
+
channel.webhook = { url: target, secret: actionOptions.secret, headers: parseHeaders(actionOptions.header), timeoutMs: actionOptions.timeoutMs };
|
|
37407
|
+
} else if (actionOptions.transport === "command") {
|
|
37408
|
+
channel.command = { command: target, args: actionOptions.arg ?? [], timeoutMs: actionOptions.timeoutMs };
|
|
37409
|
+
} else {
|
|
37410
|
+
throw new Error(`Transport ${actionOptions.transport} is reserved for future use and cannot be added yet`);
|
|
37411
|
+
}
|
|
37412
|
+
const saved = await createClient(options).addChannel(channel);
|
|
37413
|
+
print(sanitizeChannelForOutput(saved), wantsJson(actionOptions, command), `Added ${saved.transport} channel ${saved.id}`);
|
|
37414
|
+
});
|
|
37415
|
+
webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
37416
|
+
const channels = await createClient(options).listChannels();
|
|
37417
|
+
if (wantsJson(actionOptions, command)) {
|
|
37418
|
+
console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
|
|
37419
|
+
return;
|
|
37420
|
+
}
|
|
37421
|
+
if (!channels.length) {
|
|
37422
|
+
console.log("No channels configured.");
|
|
37423
|
+
return;
|
|
37424
|
+
}
|
|
37425
|
+
for (const channel of channels) {
|
|
37426
|
+
console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
|
|
37427
|
+
}
|
|
37428
|
+
});
|
|
37429
|
+
webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
37430
|
+
const removed = await createClient(options).removeChannel(id);
|
|
37431
|
+
print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
|
|
37432
|
+
});
|
|
37433
|
+
webhooks.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/channel identifier").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
37434
|
+
const result = await createClient(options).testChannel(id, {
|
|
37435
|
+
source: options.source,
|
|
37436
|
+
type: actionOptions.type,
|
|
37437
|
+
subject: actionOptions.subject ?? id,
|
|
37438
|
+
message: actionOptions.message,
|
|
37439
|
+
data: parseJsonObject(actionOptions.data, { test: true })
|
|
37440
|
+
});
|
|
37441
|
+
print(result, wantsJson(actionOptions, command), `${result.status}: ${result.channelId}`);
|
|
37442
|
+
});
|
|
37443
|
+
return webhooks;
|
|
37444
|
+
}
|
|
37445
|
+
function registerEventCommands(program2, options) {
|
|
37446
|
+
const events = program2.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
|
|
37447
|
+
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, command) => {
|
|
37448
|
+
const result = await createClient(options).emit({
|
|
37449
|
+
source: actionOptions.source ?? options.source,
|
|
37450
|
+
type,
|
|
37451
|
+
subject: actionOptions.subject,
|
|
37452
|
+
severity: actionOptions.severity,
|
|
37453
|
+
message: actionOptions.message,
|
|
37454
|
+
dedupeKey: actionOptions.dedupeKey,
|
|
37455
|
+
data: parseJsonObject(actionOptions.data, {}),
|
|
37456
|
+
metadata: parseJsonObject(actionOptions.metadata, {})
|
|
37457
|
+
}, { deliver: actionOptions.deliver, dedupe: actionOptions.dedupe });
|
|
37458
|
+
print(result, wantsJson(actionOptions, command), `${result.deduped ? "Deduped" : "Emitted"} ${result.event.id} to ${result.deliveries.length} channel(s)`);
|
|
37459
|
+
});
|
|
37460
|
+
events.command("list").description("List recorded events").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--limit <n>", "Limit results", parseNumber).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
37461
|
+
let rows = await createClient(options).listEvents();
|
|
37462
|
+
if (actionOptions.source)
|
|
37463
|
+
rows = rows.filter((event) => event.source === actionOptions.source);
|
|
37464
|
+
if (actionOptions.type)
|
|
37465
|
+
rows = rows.filter((event) => event.type === actionOptions.type);
|
|
37466
|
+
if (actionOptions.limit)
|
|
37467
|
+
rows = rows.slice(-actionOptions.limit);
|
|
37468
|
+
if (wantsJson(actionOptions, command)) {
|
|
37469
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
37470
|
+
return;
|
|
37471
|
+
}
|
|
37472
|
+
if (!rows.length) {
|
|
37473
|
+
console.log("No events recorded.");
|
|
37474
|
+
return;
|
|
37475
|
+
}
|
|
37476
|
+
for (const event of rows)
|
|
37477
|
+
console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
|
|
37478
|
+
});
|
|
37479
|
+
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) => {
|
|
37480
|
+
const result = await createClient(options).replay({
|
|
37481
|
+
eventId: actionOptions.id,
|
|
37482
|
+
source: actionOptions.source,
|
|
37483
|
+
type: actionOptions.type,
|
|
37484
|
+
dryRun: actionOptions.dryRun
|
|
37485
|
+
});
|
|
37486
|
+
print(result, wantsJson(actionOptions, command), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
|
|
37487
|
+
});
|
|
37488
|
+
return events;
|
|
37489
|
+
}
|
|
37490
|
+
function registerEventsCommands(program2, options) {
|
|
37491
|
+
registerWebhookCommands(program2, options);
|
|
37492
|
+
registerEventCommands(program2, options);
|
|
37493
|
+
}
|
|
37494
|
+
function parseNumber(value) {
|
|
37495
|
+
const parsed = Number(value);
|
|
37496
|
+
if (!Number.isFinite(parsed))
|
|
37497
|
+
throw new Error(`Expected a number, got ${value}`);
|
|
37498
|
+
return parsed;
|
|
37499
|
+
}
|
|
37500
|
+
function collectValues(value, previous) {
|
|
37501
|
+
previous.push(value);
|
|
37502
|
+
return previous;
|
|
37503
|
+
}
|
|
37504
|
+
|
|
36821
37505
|
// node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
36822
37506
|
var ANSI_BACKGROUND_OFFSET = 10;
|
|
36823
37507
|
var wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`;
|
|
@@ -41321,20 +42005,20 @@ class AccountsError extends Error {
|
|
|
41321
42005
|
}
|
|
41322
42006
|
|
|
41323
42007
|
// src/lib/tools.ts
|
|
41324
|
-
import { homedir as
|
|
41325
|
-
import { join as
|
|
42008
|
+
import { homedir as homedir3 } from "node:os";
|
|
42009
|
+
import { join as join3 } from "node:path";
|
|
41326
42010
|
|
|
41327
42011
|
// src/storage.ts
|
|
41328
|
-
import { homedir } from "node:os";
|
|
42012
|
+
import { homedir as homedir2 } from "node:os";
|
|
41329
42013
|
import { hostname } from "node:os";
|
|
41330
|
-
import { join } from "node:path";
|
|
41331
|
-
import { existsSync as
|
|
42014
|
+
import { join as join2 } from "node:path";
|
|
42015
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "node:fs";
|
|
41332
42016
|
|
|
41333
42017
|
// src/lib/safe-path.ts
|
|
41334
|
-
import { existsSync, lstatSync, mkdirSync, realpathSync } from "node:fs";
|
|
42018
|
+
import { existsSync as existsSync2, lstatSync, mkdirSync, realpathSync } from "node:fs";
|
|
41335
42019
|
import { dirname, resolve } from "node:path";
|
|
41336
42020
|
function throwIfSymlink(path, label) {
|
|
41337
|
-
if (
|
|
42021
|
+
if (existsSync2(path) && lstatSync(path).isSymbolicLink()) {
|
|
41338
42022
|
throw new AccountsError(`${label}: ${path}`);
|
|
41339
42023
|
}
|
|
41340
42024
|
}
|
|
@@ -41370,11 +42054,11 @@ function assertDirChainSafe(targetFile, mustStayUnder) {
|
|
|
41370
42054
|
function assertSafeWritePath(filePath, opts) {
|
|
41371
42055
|
const absFile = resolve(filePath);
|
|
41372
42056
|
const parent = dirname(absFile);
|
|
41373
|
-
if (!
|
|
42057
|
+
if (!existsSync2(parent))
|
|
41374
42058
|
mkdirSync(parent, { recursive: true });
|
|
41375
42059
|
throwIfSymlink(absFile, "refusing to write through symlink");
|
|
41376
42060
|
assertDirChainSafe(absFile, opts?.mustStayUnder);
|
|
41377
|
-
const resolved = realpathSync(
|
|
42061
|
+
const resolved = realpathSync(existsSync2(absFile) ? absFile : parent);
|
|
41378
42062
|
if (opts?.mustStayUnder) {
|
|
41379
42063
|
const base = realpathSync(resolve(opts.mustStayUnder));
|
|
41380
42064
|
if (resolved !== base && !resolved.startsWith(base + "/")) {
|
|
@@ -41416,21 +42100,21 @@ function accountsHome() {
|
|
|
41416
42100
|
const override = process.env.ACCOUNTS_HOME;
|
|
41417
42101
|
if (override && override.trim())
|
|
41418
42102
|
return validateEnvPath(override, "ACCOUNTS_HOME");
|
|
41419
|
-
return
|
|
42103
|
+
return join2(homedir2(), ".hasna", "accounts");
|
|
41420
42104
|
}
|
|
41421
42105
|
function storePath() {
|
|
41422
42106
|
const override = process.env.ACCOUNTS_STORE_PATH;
|
|
41423
42107
|
if (override && override.trim())
|
|
41424
42108
|
return validateEnvPath(override, "ACCOUNTS_STORE_PATH");
|
|
41425
|
-
return
|
|
42109
|
+
return join2(accountsHome(), "accounts.json");
|
|
41426
42110
|
}
|
|
41427
42111
|
function profilesDir() {
|
|
41428
|
-
return
|
|
42112
|
+
return join2(accountsHome(), "profiles");
|
|
41429
42113
|
}
|
|
41430
42114
|
var EMPTY_STORE = { version: 1, current: {}, applied: {}, profiles: [], tools: [] };
|
|
41431
42115
|
function loadStore() {
|
|
41432
42116
|
const path = storePath();
|
|
41433
|
-
if (!
|
|
42117
|
+
if (!existsSync3(path))
|
|
41434
42118
|
return structuredClone(EMPTY_STORE);
|
|
41435
42119
|
let raw;
|
|
41436
42120
|
try {
|
|
@@ -41468,7 +42152,7 @@ function loadStore() {
|
|
|
41468
42152
|
function saveStore(store) {
|
|
41469
42153
|
const path = storePath();
|
|
41470
42154
|
assertSafeWritePath(path, { mustStayUnder: accountsHome() });
|
|
41471
|
-
mkdirSync2(
|
|
42155
|
+
mkdirSync2(join2(path, ".."), { recursive: true });
|
|
41472
42156
|
writeFileSync(path, JSON.stringify(store, null, 2) + `
|
|
41473
42157
|
`, { mode: 384 });
|
|
41474
42158
|
}
|
|
@@ -41616,7 +42300,7 @@ var BUILTIN_TOOLS = [
|
|
|
41616
42300
|
id: "claude",
|
|
41617
42301
|
label: "Claude Code",
|
|
41618
42302
|
envVar: "CLAUDE_CONFIG_DIR",
|
|
41619
|
-
defaultDir:
|
|
42303
|
+
defaultDir: join3(homedir3(), ".claude"),
|
|
41620
42304
|
bin: "claude",
|
|
41621
42305
|
loginHint: "run /login inside Claude, then /exit when done",
|
|
41622
42306
|
resumeArgs: ["--continue"],
|
|
@@ -41627,7 +42311,7 @@ var BUILTIN_TOOLS = [
|
|
|
41627
42311
|
id: "codex",
|
|
41628
42312
|
label: "Codex CLI",
|
|
41629
42313
|
envVar: "CODEX_HOME",
|
|
41630
|
-
defaultDir:
|
|
42314
|
+
defaultDir: join3(homedir3(), ".codex"),
|
|
41631
42315
|
bin: "codex",
|
|
41632
42316
|
loginArgs: ["login"],
|
|
41633
42317
|
loginHint: "complete the Codex login flow for this CODEX_HOME",
|
|
@@ -41637,7 +42321,7 @@ var BUILTIN_TOOLS = [
|
|
|
41637
42321
|
id: "takumi",
|
|
41638
42322
|
label: "Takumi",
|
|
41639
42323
|
envVar: "TAKUMI_CONFIG_DIR",
|
|
41640
|
-
defaultDir:
|
|
42324
|
+
defaultDir: join3(homedir3(), ".takumi"),
|
|
41641
42325
|
bin: "takumi",
|
|
41642
42326
|
loginHint: "complete Takumi auth in this TAKUMI_CONFIG_DIR",
|
|
41643
42327
|
resumeArgs: ["--continue"],
|
|
@@ -41648,7 +42332,7 @@ var BUILTIN_TOOLS = [
|
|
|
41648
42332
|
id: "gemini",
|
|
41649
42333
|
label: "Gemini CLI",
|
|
41650
42334
|
envVar: "GEMINI_CONFIG_DIR",
|
|
41651
|
-
defaultDir:
|
|
42335
|
+
defaultDir: join3(homedir3(), ".gemini"),
|
|
41652
42336
|
bin: "gemini",
|
|
41653
42337
|
loginHint: "complete Gemini auth in this GEMINI_CONFIG_DIR"
|
|
41654
42338
|
},
|
|
@@ -41660,7 +42344,7 @@ var BUILTIN_TOOLS = [
|
|
|
41660
42344
|
XDG_CONFIG_HOME: "{profileDir}/xdg-config",
|
|
41661
42345
|
XDG_DATA_HOME: "{profileDir}/xdg-data"
|
|
41662
42346
|
},
|
|
41663
|
-
defaultDir:
|
|
42347
|
+
defaultDir: join3(homedir3(), ".config", "opencode"),
|
|
41664
42348
|
bin: "opencode",
|
|
41665
42349
|
loginArgs: ["auth", "login"],
|
|
41666
42350
|
loginHint: "complete opencode auth login for this isolated config/data root",
|
|
@@ -41670,7 +42354,7 @@ var BUILTIN_TOOLS = [
|
|
|
41670
42354
|
id: "cursor",
|
|
41671
42355
|
label: "Cursor Agent",
|
|
41672
42356
|
envVar: "CURSOR_CONFIG_DIR",
|
|
41673
|
-
defaultDir:
|
|
42357
|
+
defaultDir: join3(homedir3(), ".cursor"),
|
|
41674
42358
|
bin: "cursor-agent",
|
|
41675
42359
|
loginArgs: ["login"],
|
|
41676
42360
|
loginHint: "complete cursor-agent login for this CURSOR_CONFIG_DIR"
|
|
@@ -41679,7 +42363,7 @@ var BUILTIN_TOOLS = [
|
|
|
41679
42363
|
id: "pi",
|
|
41680
42364
|
label: "Pi Coding Agent",
|
|
41681
42365
|
envVar: "PI_CODING_AGENT_HOME",
|
|
41682
|
-
defaultDir:
|
|
42366
|
+
defaultDir: join3(homedir3(), ".pi"),
|
|
41683
42367
|
bin: "pi",
|
|
41684
42368
|
loginHint: "complete Pi coding agent auth in this PI_CODING_AGENT_HOME"
|
|
41685
42369
|
},
|
|
@@ -41687,7 +42371,7 @@ var BUILTIN_TOOLS = [
|
|
|
41687
42371
|
id: "hermes",
|
|
41688
42372
|
label: "Hermes",
|
|
41689
42373
|
envVar: "HERMES_HOME",
|
|
41690
|
-
defaultDir:
|
|
42374
|
+
defaultDir: join3(homedir3(), ".hermes"),
|
|
41691
42375
|
bin: "hermes",
|
|
41692
42376
|
loginHint: "complete Hermes auth in this HERMES_HOME"
|
|
41693
42377
|
},
|
|
@@ -41695,7 +42379,7 @@ var BUILTIN_TOOLS = [
|
|
|
41695
42379
|
id: "kimi",
|
|
41696
42380
|
label: "Kimi Code",
|
|
41697
42381
|
envVar: "KIMI_CODE_HOME",
|
|
41698
|
-
defaultDir:
|
|
42382
|
+
defaultDir: join3(homedir3(), ".kimi-code"),
|
|
41699
42383
|
bin: "kimi",
|
|
41700
42384
|
loginArgs: ["login"],
|
|
41701
42385
|
loginHint: "complete kimi login for this KIMI_CODE_HOME"
|
|
@@ -41704,7 +42388,7 @@ var BUILTIN_TOOLS = [
|
|
|
41704
42388
|
id: "grok",
|
|
41705
42389
|
label: "Grok Build",
|
|
41706
42390
|
envVar: "HOME",
|
|
41707
|
-
defaultDir:
|
|
42391
|
+
defaultDir: join3(homedir3(), ".grok"),
|
|
41708
42392
|
bin: "grok",
|
|
41709
42393
|
loginArgs: ["login"],
|
|
41710
42394
|
loginHint: "complete grok login in this process-scoped HOME; prefer launch/shell over exporting HOME globally"
|
|
@@ -41765,19 +42449,19 @@ function removeCustomTool(id) {
|
|
|
41765
42449
|
}
|
|
41766
42450
|
|
|
41767
42451
|
// src/lib/profiles.ts
|
|
41768
|
-
import { homedir as
|
|
41769
|
-
import { isAbsolute, join as
|
|
41770
|
-
import { existsSync as
|
|
42452
|
+
import { homedir as homedir4 } from "node:os";
|
|
42453
|
+
import { isAbsolute, join as join5, resolve as resolve2 } from "node:path";
|
|
42454
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, rmSync } from "node:fs";
|
|
41771
42455
|
|
|
41772
42456
|
// src/lib/detect.ts
|
|
41773
|
-
import { existsSync as
|
|
41774
|
-
import { dirname as dirname2, join as
|
|
42457
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
|
|
42458
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
41775
42459
|
function detectEmail(dir, tool) {
|
|
41776
42460
|
if (!tool.accountFile || !tool.emailPath)
|
|
41777
42461
|
return;
|
|
41778
|
-
const candidates = [
|
|
42462
|
+
const candidates = [join4(dir, tool.accountFile)];
|
|
41779
42463
|
if (dir === tool.defaultDir)
|
|
41780
|
-
candidates.push(
|
|
42464
|
+
candidates.push(join4(dirname2(dir), tool.accountFile));
|
|
41781
42465
|
for (const file of candidates) {
|
|
41782
42466
|
const email = readEmail(file, tool.emailPath);
|
|
41783
42467
|
if (email)
|
|
@@ -41786,7 +42470,7 @@ function detectEmail(dir, tool) {
|
|
|
41786
42470
|
return;
|
|
41787
42471
|
}
|
|
41788
42472
|
function readEmail(file, path) {
|
|
41789
|
-
if (!
|
|
42473
|
+
if (!existsSync4(file))
|
|
41790
42474
|
return;
|
|
41791
42475
|
let cursor;
|
|
41792
42476
|
try {
|
|
@@ -41811,9 +42495,9 @@ function nowIso() {
|
|
|
41811
42495
|
function expandPath(p) {
|
|
41812
42496
|
let out = p;
|
|
41813
42497
|
if (out === "~")
|
|
41814
|
-
out =
|
|
42498
|
+
out = homedir4();
|
|
41815
42499
|
else if (out.startsWith("~/"))
|
|
41816
|
-
out =
|
|
42500
|
+
out = join5(homedir4(), out.slice(2));
|
|
41817
42501
|
return isAbsolute(out) ? out : resolve2(process.cwd(), out);
|
|
41818
42502
|
}
|
|
41819
42503
|
function listProfiles(toolId) {
|
|
@@ -41847,7 +42531,7 @@ function addProfile(opts) {
|
|
|
41847
42531
|
if (store.profiles.some((p) => p.name === name && p.tool === toolId)) {
|
|
41848
42532
|
throw new AccountsError(`a ${toolId} profile named "${name}" already exists`);
|
|
41849
42533
|
}
|
|
41850
|
-
const dir = opts.dir ? expandPath(opts.dir) :
|
|
42534
|
+
const dir = opts.dir ? expandPath(opts.dir) : join5(profilesDir(), toolId, name);
|
|
41851
42535
|
if (store.profiles.some((p) => p.dir === dir)) {
|
|
41852
42536
|
throw new AccountsError(`a profile already uses config dir ${dir}`);
|
|
41853
42537
|
}
|
|
@@ -41889,7 +42573,7 @@ function removeProfile(name, opts = {}) {
|
|
|
41889
42573
|
if (options.purge) {
|
|
41890
42574
|
const managed = profile.dir.startsWith(profilesDir());
|
|
41891
42575
|
const isDefault = profile.dir === getTool(profile.tool).defaultDir;
|
|
41892
|
-
if (managed && !isDefault &&
|
|
42576
|
+
if (managed && !isDefault && existsSync5(profile.dir)) {
|
|
41893
42577
|
rmSync(profile.dir, { recursive: true, force: true });
|
|
41894
42578
|
purged = true;
|
|
41895
42579
|
} else {
|
|
@@ -41988,12 +42672,12 @@ function currentProfile(toolId) {
|
|
|
41988
42672
|
}
|
|
41989
42673
|
|
|
41990
42674
|
// src/lib/claude-auth.ts
|
|
41991
|
-
import { copyFileSync, existsSync as
|
|
41992
|
-
import { dirname as dirname4, join as
|
|
42675
|
+
import { copyFileSync, existsSync as existsSync6, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync3, statSync, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
42676
|
+
import { dirname as dirname4, join as join7 } from "node:path";
|
|
41993
42677
|
|
|
41994
42678
|
// src/lib/claude-layout.ts
|
|
41995
|
-
import { homedir as
|
|
41996
|
-
import { dirname as dirname3, join as
|
|
42679
|
+
import { homedir as homedir5 } from "node:os";
|
|
42680
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
41997
42681
|
var CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
41998
42682
|
var ACCOUNTS_AUTH_DIR = ".accounts-auth";
|
|
41999
42683
|
var OAUTH_SNAPSHOT = "oauth-account.json";
|
|
@@ -42001,36 +42685,36 @@ var CREDENTIALS_SNAPSHOT = "credentials.json";
|
|
|
42001
42685
|
var KEYCHAIN_SNAPSHOT = "keychain.json";
|
|
42002
42686
|
function liveClaudeBase() {
|
|
42003
42687
|
const testBase = process.env.ACCOUNTS_TEST_LIVE_DIR;
|
|
42004
|
-
return testBase && testBase.trim() ? testBase :
|
|
42688
|
+
return testBase && testBase.trim() ? testBase : homedir5();
|
|
42005
42689
|
}
|
|
42006
42690
|
function liveClaudePaths() {
|
|
42007
42691
|
const base = liveClaudeBase();
|
|
42008
|
-
const configDir =
|
|
42692
|
+
const configDir = join6(base, ".claude");
|
|
42009
42693
|
return {
|
|
42010
42694
|
configDir,
|
|
42011
|
-
homeJson:
|
|
42012
|
-
credentialsFile:
|
|
42695
|
+
homeJson: join6(base, ".claude.json"),
|
|
42696
|
+
credentialsFile: join6(configDir, ".credentials.json")
|
|
42013
42697
|
};
|
|
42014
42698
|
}
|
|
42015
42699
|
function profileAccountJsonPaths(profileDir, tool) {
|
|
42016
42700
|
if (!tool.accountFile)
|
|
42017
42701
|
return [];
|
|
42018
|
-
const paths = [
|
|
42702
|
+
const paths = [join6(profileDir, tool.accountFile)];
|
|
42019
42703
|
if (profileDir === tool.defaultDir)
|
|
42020
|
-
paths.push(
|
|
42704
|
+
paths.push(join6(dirname3(profileDir), tool.accountFile));
|
|
42021
42705
|
return paths;
|
|
42022
42706
|
}
|
|
42023
42707
|
function profileAuthDir(profileDir) {
|
|
42024
|
-
return
|
|
42708
|
+
return join6(profileDir, ACCOUNTS_AUTH_DIR);
|
|
42025
42709
|
}
|
|
42026
42710
|
function profileOAuthSnapshot(profileDir) {
|
|
42027
|
-
return
|
|
42711
|
+
return join6(profileAuthDir(profileDir), OAUTH_SNAPSHOT);
|
|
42028
42712
|
}
|
|
42029
42713
|
function profileCredentialsSnapshot(profileDir) {
|
|
42030
|
-
return
|
|
42714
|
+
return join6(profileAuthDir(profileDir), CREDENTIALS_SNAPSHOT);
|
|
42031
42715
|
}
|
|
42032
42716
|
function profileKeychainSnapshot(profileDir) {
|
|
42033
|
-
return
|
|
42717
|
+
return join6(profileAuthDir(profileDir), KEYCHAIN_SNAPSHOT);
|
|
42034
42718
|
}
|
|
42035
42719
|
|
|
42036
42720
|
// src/lib/keychain.ts
|
|
@@ -42095,7 +42779,7 @@ function writeClaudeKeychain(cred) {
|
|
|
42095
42779
|
|
|
42096
42780
|
// src/lib/claude-auth.ts
|
|
42097
42781
|
function readJsonFile(path) {
|
|
42098
|
-
if (!
|
|
42782
|
+
if (!existsSync6(path))
|
|
42099
42783
|
return;
|
|
42100
42784
|
try {
|
|
42101
42785
|
return JSON.parse(readFileSync3(path, "utf8"));
|
|
@@ -42122,7 +42806,7 @@ function findOAuthSource(paths) {
|
|
|
42122
42806
|
return;
|
|
42123
42807
|
}
|
|
42124
42808
|
function snapshotIsStale(sourcePath, snapshotPath) {
|
|
42125
|
-
if (!
|
|
42809
|
+
if (!existsSync6(snapshotPath))
|
|
42126
42810
|
return true;
|
|
42127
42811
|
try {
|
|
42128
42812
|
return statSync(sourcePath).mtimeMs > statSync(snapshotPath).mtimeMs;
|
|
@@ -42161,13 +42845,13 @@ function liveOAuthEmail() {
|
|
|
42161
42845
|
}
|
|
42162
42846
|
function snapshotLiveAuthToProfile(profileDir, _tool) {
|
|
42163
42847
|
const authDir = profileAuthDir(profileDir);
|
|
42164
|
-
assertSafeWritePath(
|
|
42848
|
+
assertSafeWritePath(join7(authDir, OAUTH_SNAPSHOT), { mustStayUnder: profileDir });
|
|
42165
42849
|
mkdirSync4(authDir, { recursive: true });
|
|
42166
42850
|
const live = liveClaudePaths();
|
|
42167
42851
|
const oauth = readOAuthFromPaths([live.homeJson]);
|
|
42168
42852
|
if (oauth)
|
|
42169
42853
|
writeJsonFile(profileOAuthSnapshot(profileDir), { oauthAccount: oauth }, profileDir);
|
|
42170
|
-
if (
|
|
42854
|
+
if (existsSync6(live.credentialsFile)) {
|
|
42171
42855
|
const dest = profileCredentialsSnapshot(profileDir);
|
|
42172
42856
|
assertSafeWritePath(dest, { mustStayUnder: profileDir });
|
|
42173
42857
|
copyFileSync(live.credentialsFile, dest);
|
|
@@ -42180,16 +42864,16 @@ function snapshotLiveAuthToProfile(profileDir, _tool) {
|
|
|
42180
42864
|
}
|
|
42181
42865
|
function ensureProfileAuthSnapshot(profileDir, tool, opts = {}) {
|
|
42182
42866
|
const authDir = profileAuthDir(profileDir);
|
|
42183
|
-
assertSafeWritePath(
|
|
42867
|
+
assertSafeWritePath(join7(authDir, OAUTH_SNAPSHOT), { mustStayUnder: profileDir });
|
|
42184
42868
|
mkdirSync4(authDir, { recursive: true });
|
|
42185
42869
|
const oauthSource = findOAuthSource(profileAccountJsonPaths(profileDir, tool));
|
|
42186
42870
|
const oauthSnap = profileOAuthSnapshot(profileDir);
|
|
42187
42871
|
if (oauthSource && (opts.overwrite || snapshotIsStale(oauthSource.path, oauthSnap))) {
|
|
42188
42872
|
writeJsonFile(oauthSnap, { oauthAccount: oauthSource.oauth }, profileDir);
|
|
42189
42873
|
}
|
|
42190
|
-
const credFile =
|
|
42874
|
+
const credFile = join7(profileDir, ".credentials.json");
|
|
42191
42875
|
const credSnap = profileCredentialsSnapshot(profileDir);
|
|
42192
|
-
if (
|
|
42876
|
+
if (existsSync6(credFile) && (opts.overwrite || snapshotIsStale(credFile, credSnap))) {
|
|
42193
42877
|
assertSafeWritePath(credSnap, { mustStayUnder: profileDir });
|
|
42194
42878
|
copyFileSync(credFile, credSnap);
|
|
42195
42879
|
}
|
|
@@ -42214,12 +42898,12 @@ function restoreClaudeAuthFromProfile(profileDir, tool, profileName) {
|
|
|
42214
42898
|
assertSafeWritePath(live.homeJson, { mustStayUnder: liveRoot });
|
|
42215
42899
|
mergeOAuthInto([live.homeJson], oauth, false, liveRoot);
|
|
42216
42900
|
const credSnap = profileCredentialsSnapshot(profileDir);
|
|
42217
|
-
if (
|
|
42901
|
+
if (existsSync6(credSnap)) {
|
|
42218
42902
|
assertSafeWritePath(live.credentialsFile, { mustStayUnder: liveRoot });
|
|
42219
42903
|
assertSafeWritePath(credSnap, { mustStayUnder: profileDir });
|
|
42220
42904
|
copyFileSync(credSnap, live.credentialsFile);
|
|
42221
42905
|
writeFileSync2(live.credentialsFile, readFileSync3(live.credentialsFile), { mode: 384 });
|
|
42222
|
-
} else if (
|
|
42906
|
+
} else if (existsSync6(live.credentialsFile)) {
|
|
42223
42907
|
if (!lstatSync2(live.credentialsFile).isSymbolicLink())
|
|
42224
42908
|
unlinkSync(live.credentialsFile);
|
|
42225
42909
|
}
|
|
@@ -42241,14 +42925,14 @@ function restoreClaudeAuthFromProfile(profileDir, tool, profileName) {
|
|
|
42241
42925
|
}
|
|
42242
42926
|
}
|
|
42243
42927
|
function hasAuthSnapshot(profileDir) {
|
|
42244
|
-
return
|
|
42928
|
+
return existsSync6(profileOAuthSnapshot(profileDir)) || existsSync6(profileCredentialsSnapshot(profileDir)) || existsSync6(profileKeychainSnapshot(profileDir));
|
|
42245
42929
|
}
|
|
42246
42930
|
|
|
42247
42931
|
// src/lib/apply-lock.ts
|
|
42248
|
-
import { closeSync, existsSync as
|
|
42249
|
-
import { join as
|
|
42932
|
+
import { closeSync, existsSync as existsSync7, mkdirSync as mkdirSync5, openSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
42933
|
+
import { join as join8 } from "node:path";
|
|
42250
42934
|
function lockPath() {
|
|
42251
|
-
return
|
|
42935
|
+
return join8(accountsHome(), ".apply.lock");
|
|
42252
42936
|
}
|
|
42253
42937
|
function withApplyLock(fn) {
|
|
42254
42938
|
const home = accountsHome();
|
|
@@ -42273,7 +42957,7 @@ function withApplyLock(fn) {
|
|
|
42273
42957
|
if (fd !== undefined) {
|
|
42274
42958
|
closeSync(fd);
|
|
42275
42959
|
try {
|
|
42276
|
-
if (
|
|
42960
|
+
if (existsSync7(path))
|
|
42277
42961
|
unlinkSync2(path);
|
|
42278
42962
|
} catch {}
|
|
42279
42963
|
}
|
|
@@ -42319,7 +43003,7 @@ function applyProfileUnlocked(name, toolId) {
|
|
|
42319
43003
|
|
|
42320
43004
|
// src/lib/agents.ts
|
|
42321
43005
|
import { spawnSync } from "node:child_process";
|
|
42322
|
-
import { existsSync as
|
|
43006
|
+
import { existsSync as existsSync8, readFileSync as readFileSync4 } from "node:fs";
|
|
42323
43007
|
import { platform as platform2 } from "node:os";
|
|
42324
43008
|
function extractJsonArray(raw) {
|
|
42325
43009
|
const text = raw.replace(/\r/g, "");
|
|
@@ -42446,7 +43130,7 @@ function listAgentsAcrossProfiles(opts = {}) {
|
|
|
42446
43130
|
const registered = listProfiles(toolId);
|
|
42447
43131
|
const entries = [...registered];
|
|
42448
43132
|
const defaultDir = opts.defaultDir ?? tool.defaultDir;
|
|
42449
|
-
if (defaultDir && !registered.some((p) => p.dir === defaultDir) &&
|
|
43133
|
+
if (defaultDir && !registered.some((p) => p.dir === defaultDir) && existsSync8(defaultDir)) {
|
|
42450
43134
|
entries.unshift({ name: "(default)", tool: toolId, dir: defaultDir });
|
|
42451
43135
|
}
|
|
42452
43136
|
const wanted = entries.filter((p) => !opts.profile || p.name === opts.profile || opts.profile === "default" && p.name === "(default)");
|
|
@@ -42496,19 +43180,19 @@ function listAgentsAcrossProfiles(opts = {}) {
|
|
|
42496
43180
|
}
|
|
42497
43181
|
|
|
42498
43182
|
// src/lib/import-profile.ts
|
|
42499
|
-
import { cpSync, existsSync as
|
|
42500
|
-
import { join as
|
|
43183
|
+
import { cpSync, existsSync as existsSync9 } from "node:fs";
|
|
43184
|
+
import { join as join9 } from "node:path";
|
|
42501
43185
|
function importProfile(opts) {
|
|
42502
43186
|
const toolId = opts.tool ?? DEFAULT_TOOL;
|
|
42503
43187
|
const tool = getTool(toolId);
|
|
42504
43188
|
const name = opts.name ?? "main";
|
|
42505
43189
|
const sourceDir = opts.dir ? expandPath(opts.dir) : tool.defaultDir;
|
|
42506
|
-
if (!
|
|
43190
|
+
if (!existsSync9(sourceDir)) {
|
|
42507
43191
|
throw new AccountsError(`config dir does not exist: ${sourceDir}`);
|
|
42508
43192
|
}
|
|
42509
43193
|
if (opts.copy) {
|
|
42510
|
-
const targetDir =
|
|
42511
|
-
if (
|
|
43194
|
+
const targetDir = join9(profilesDir(), toolId, name);
|
|
43195
|
+
if (existsSync9(targetDir)) {
|
|
42512
43196
|
throw new AccountsError(`managed copy target already exists: ${targetDir}`);
|
|
42513
43197
|
}
|
|
42514
43198
|
cpSync(sourceDir, targetDir, { recursive: true });
|
|
@@ -42592,8 +43276,8 @@ async function pickProfile(opts = {}) {
|
|
|
42592
43276
|
}
|
|
42593
43277
|
|
|
42594
43278
|
// src/lib/hook.ts
|
|
42595
|
-
import { existsSync as
|
|
42596
|
-
import { join as
|
|
43279
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
43280
|
+
import { join as join10 } from "node:path";
|
|
42597
43281
|
var HOOK_FILE = "claude-hook.sh";
|
|
42598
43282
|
var MARKER = "# accounts-claude-hook";
|
|
42599
43283
|
var NAME_PATTERN = "^[a-z0-9][a-z0-9-]*$";
|
|
@@ -42601,7 +43285,7 @@ function shellQuotePath(path) {
|
|
|
42601
43285
|
return `'${path.replace(/'/g, `'\\''`)}'`;
|
|
42602
43286
|
}
|
|
42603
43287
|
function hookPath() {
|
|
42604
|
-
return
|
|
43288
|
+
return join10(accountsHome(), HOOK_FILE);
|
|
42605
43289
|
}
|
|
42606
43290
|
function hookScript() {
|
|
42607
43291
|
const quotedHook = shellQuotePath(hookPath());
|
|
@@ -42632,13 +43316,13 @@ claude() {
|
|
|
42632
43316
|
function installHook() {
|
|
42633
43317
|
const path = hookPath();
|
|
42634
43318
|
mkdirSync6(accountsHome(), { recursive: true });
|
|
42635
|
-
const created = !
|
|
43319
|
+
const created = !existsSync10(path);
|
|
42636
43320
|
writeFileSync4(path, hookScript(), { mode: 493 });
|
|
42637
43321
|
return { path, created };
|
|
42638
43322
|
}
|
|
42639
43323
|
function uninstallHook() {
|
|
42640
43324
|
const path = hookPath();
|
|
42641
|
-
if (!
|
|
43325
|
+
if (!existsSync10(path))
|
|
42642
43326
|
return false;
|
|
42643
43327
|
const content = readFileSync5(path, "utf8");
|
|
42644
43328
|
if (!content.includes(MARKER))
|
|
@@ -42730,24 +43414,24 @@ function switchProfile(name, opts = {}) {
|
|
|
42730
43414
|
}
|
|
42731
43415
|
|
|
42732
43416
|
// src/lib/supervisor.ts
|
|
42733
|
-
import { spawn } from "node:child_process";
|
|
43417
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
42734
43418
|
import { createHash } from "node:crypto";
|
|
42735
|
-
import { existsSync as
|
|
43419
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync7, readFileSync as readFileSync6, readdirSync, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "node:fs";
|
|
42736
43420
|
import { createConnection, createServer } from "node:net";
|
|
42737
|
-
import { basename, join as
|
|
43421
|
+
import { basename, join as join11 } from "node:path";
|
|
42738
43422
|
var STATE_SUFFIX = ".json";
|
|
42739
43423
|
function supervisorDir() {
|
|
42740
|
-
return
|
|
43424
|
+
return join11(accountsHome(), "supervisors");
|
|
42741
43425
|
}
|
|
42742
43426
|
function supervisorStatePath(toolId) {
|
|
42743
|
-
return
|
|
43427
|
+
return join11(supervisorDir(), `${toolId}${STATE_SUFFIX}`);
|
|
42744
43428
|
}
|
|
42745
43429
|
function supervisorSocketPath(toolId) {
|
|
42746
43430
|
if (process.platform === "win32") {
|
|
42747
43431
|
const hash = createHash("sha1").update(accountsHome()).digest("hex").slice(0, 12);
|
|
42748
43432
|
return `\\\\.\\pipe\\hasna-accounts-${hash}-${toolId}`;
|
|
42749
43433
|
}
|
|
42750
|
-
return
|
|
43434
|
+
return join11(supervisorDir(), `${toolId}.sock`);
|
|
42751
43435
|
}
|
|
42752
43436
|
function nowIso2() {
|
|
42753
43437
|
return new Date().toISOString();
|
|
@@ -42761,7 +43445,7 @@ function parseState(raw) {
|
|
|
42761
43445
|
}
|
|
42762
43446
|
function readSupervisorState(toolId) {
|
|
42763
43447
|
const path = supervisorStatePath(toolId);
|
|
42764
|
-
if (!
|
|
43448
|
+
if (!existsSync11(path))
|
|
42765
43449
|
return;
|
|
42766
43450
|
try {
|
|
42767
43451
|
return parseState(readFileSync6(path, "utf8"));
|
|
@@ -42771,7 +43455,7 @@ function readSupervisorState(toolId) {
|
|
|
42771
43455
|
}
|
|
42772
43456
|
function listSupervisorStates() {
|
|
42773
43457
|
const dir = supervisorDir();
|
|
42774
|
-
if (!
|
|
43458
|
+
if (!existsSync11(dir))
|
|
42775
43459
|
return [];
|
|
42776
43460
|
return readdirSync(dir).filter((name) => name.endsWith(STATE_SUFFIX)).map((name) => basename(name, STATE_SUFFIX)).map((toolId) => readSupervisorState(toolId)).filter((state) => state !== undefined);
|
|
42777
43461
|
}
|
|
@@ -42994,7 +43678,7 @@ async function runSupervisedTool(initialProfile, tool, initialArgs = [], opts =
|
|
|
42994
43678
|
useProfile(profile.name, tool.id);
|
|
42995
43679
|
const env2 = profileEnv(profile, tool);
|
|
42996
43680
|
log(`accounts supervisor: starting ${tool.bin} for ${profile.name}`);
|
|
42997
|
-
const proc =
|
|
43681
|
+
const proc = spawn2(tool.bin, childArgs, {
|
|
42998
43682
|
stdio: opts.stdio ?? "inherit",
|
|
42999
43683
|
env: { ...process.env, ...env2, ACCOUNTS_SUPERVISOR: "1", ACCOUNTS_ACTIVE: profile.name },
|
|
43000
43684
|
detached: process.platform !== "win32"
|
|
@@ -43178,7 +43862,7 @@ program2.command("show").argument("<name>", "profile name").description("show fu
|
|
|
43178
43862
|
console.log(` tool: ${p.tool} (${getTool(p.tool).label})`);
|
|
43179
43863
|
console.log(` active: ${active ? source_default.green("yes") : source_default.dim("no")}`);
|
|
43180
43864
|
console.log(` applied: ${isApplied ? source_default.magenta("yes") : source_default.dim("no")}`);
|
|
43181
|
-
console.log(` config dir: ${p.dir}${
|
|
43865
|
+
console.log(` config dir: ${p.dir}${existsSync12(p.dir) ? "" : source_default.red(" [missing]")}`);
|
|
43182
43866
|
console.log(` email: ${p.email ?? source_default.dim("(none)")}`);
|
|
43183
43867
|
console.log(` created: ${p.createdAt}`);
|
|
43184
43868
|
if (p.lastUsedAt)
|
|
@@ -43316,7 +44000,7 @@ program2.command("switch").argument("<name>", "profile name").argument("[args...
|
|
|
43316
44000
|
}
|
|
43317
44001
|
}));
|
|
43318
44002
|
var hook = program2.command("hook").description("install a shell wrapper for claude");
|
|
43319
|
-
hook.command("install").description(`write ${
|
|
44003
|
+
hook.command("install").description(`write ${join12(accountsHome(), "claude-hook.sh")}`).action(action(() => {
|
|
43320
44004
|
const { path, created } = installHook();
|
|
43321
44005
|
console.log(source_default.green(created ? `✓ installed hook at ${path}` : `✓ updated hook at ${path}`));
|
|
43322
44006
|
console.log(source_default.dim(` add to ~/.zshrc: ${shellSnippet()}`));
|
|
@@ -43541,7 +44225,7 @@ tools.command("add").argument("<id>", "tool id, e.g. cursor").description("regis
|
|
|
43541
44225
|
label: opts.label,
|
|
43542
44226
|
envVar: opts.envVar,
|
|
43543
44227
|
bin: opts.bin,
|
|
43544
|
-
defaultDir: opts.defaultDir ? expandPath(opts.defaultDir) :
|
|
44228
|
+
defaultDir: opts.defaultDir ? expandPath(opts.defaultDir) : join12(homedir6(), `.${id}`),
|
|
43545
44229
|
...Object.keys(extraEnv).length > 0 ? { extraEnv } : {},
|
|
43546
44230
|
...opts.loginArg ? { loginArgs: opts.loginArg } : {},
|
|
43547
44231
|
...opts.resumeArg ? { resumeArgs: opts.resumeArg } : {},
|
|
@@ -43562,7 +44246,7 @@ program2.command("doctor").description("check the store and profile dirs for pro
|
|
|
43562
44246
|
const profiles = listProfiles();
|
|
43563
44247
|
let problems = 0;
|
|
43564
44248
|
for (const p of profiles) {
|
|
43565
|
-
const missing = !
|
|
44249
|
+
const missing = !existsSync12(p.dir);
|
|
43566
44250
|
const noEmail = !p.email;
|
|
43567
44251
|
if (missing) {
|
|
43568
44252
|
console.log(source_default.red(` ✗ ${p.name}: config dir missing (${p.dir})`));
|
|
@@ -43606,12 +44290,13 @@ ${problems} problem(s) found.`));
|
|
|
43606
44290
|
console.log(source_default.green(`
|
|
43607
44291
|
healthy.`));
|
|
43608
44292
|
}));
|
|
44293
|
+
registerEventsCommands(program2, { source: "accounts" });
|
|
43609
44294
|
program2.parseAsync(process.argv);
|
|
43610
44295
|
function getVersion() {
|
|
43611
44296
|
try {
|
|
43612
44297
|
const here = dirname5(fileURLToPath(import.meta.url));
|
|
43613
|
-
for (const candidate of [
|
|
43614
|
-
if (
|
|
44298
|
+
for (const candidate of [join12(here, "..", "package.json"), join12(here, "package.json")]) {
|
|
44299
|
+
if (existsSync12(candidate)) {
|
|
43615
44300
|
const pkg = JSON.parse(readFileSync7(candidate, "utf8"));
|
|
43616
44301
|
if (pkg.version)
|
|
43617
44302
|
return pkg.version;
|