@hasna/shortlinks 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/cli/index.js +283 -42
- package/dist/client-store.d.ts +1 -0
- package/dist/cloud-store.d.ts +1 -0
- package/dist/index.js +61 -3
- package/dist/mcp/http.d.ts +17 -12
- package/dist/mcp/index.js +87 -15
- package/dist/pg-store.d.ts +1 -0
- package/dist/pg-store.js +7 -0
- package/dist/sdk/generated.d.ts +6 -0
- package/dist/sdk/index.js +25 -0
- package/dist/serve/index.js +237 -165
- package/dist/server.js +11 -0
- package/dist/store-interface.d.ts +5 -0
- package/dist/store.d.ts +1 -0
- package/package.json +3 -4
package/README.md
CHANGED
|
@@ -114,6 +114,12 @@ shortlinks domain add go.example.com --provider cloudflare
|
|
|
114
114
|
|
|
115
115
|
Generated links use the default domain unless `--domain` is passed.
|
|
116
116
|
|
|
117
|
+
Remove a domain (this also deletes all of its links and clicks):
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
shortlinks domain remove go.example.com
|
|
121
|
+
```
|
|
122
|
+
|
|
117
123
|
## Cloudflare
|
|
118
124
|
|
|
119
125
|
Create a dry-run plan:
|
package/dist/cli/index.js
CHANGED
|
@@ -2465,6 +2465,17 @@ class ShortlinksStore {
|
|
|
2465
2465
|
`).get(normalized, hostnameOrId);
|
|
2466
2466
|
return row ? domainFromRow(row) : null;
|
|
2467
2467
|
}
|
|
2468
|
+
deleteDomain(hostnameOrId) {
|
|
2469
|
+
const domain = this.getDomain(hostnameOrId);
|
|
2470
|
+
if (!domain)
|
|
2471
|
+
throw new Error("Domain not found.");
|
|
2472
|
+
this.database.db.query("DELETE FROM domains WHERE id = ?").run(domain.id);
|
|
2473
|
+
const config = loadConfig();
|
|
2474
|
+
if (config.defaultDomain && normalizeHostname(config.defaultDomain) === domain.hostname) {
|
|
2475
|
+
updateConfig({ defaultDomain: undefined, publicBaseUrl: undefined });
|
|
2476
|
+
}
|
|
2477
|
+
return domain;
|
|
2478
|
+
}
|
|
2468
2479
|
getDefaultDomain() {
|
|
2469
2480
|
const config = loadConfig();
|
|
2470
2481
|
if (config.defaultDomain) {
|
|
@@ -2883,7 +2894,7 @@ async function upsertCloudflareDnsRecord(options) {
|
|
|
2883
2894
|
return { id: created.id, action: "created" };
|
|
2884
2895
|
}
|
|
2885
2896
|
|
|
2886
|
-
// node_modules/@hasna/events/dist/commander.js
|
|
2897
|
+
// node_modules/.pnpm/@hasna+events@0.1.13/node_modules/@hasna/events/dist/commander.js
|
|
2887
2898
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
2888
2899
|
import { existsSync as existsSync3 } from "fs";
|
|
2889
2900
|
import { homedir as homedir2 } from "os";
|
|
@@ -2900,29 +2911,83 @@ function getPathValue(input, path) {
|
|
|
2900
2911
|
return;
|
|
2901
2912
|
}, input);
|
|
2902
2913
|
}
|
|
2903
|
-
function
|
|
2904
|
-
const
|
|
2905
|
-
|
|
2914
|
+
function getFieldValues(input, path) {
|
|
2915
|
+
const values = [];
|
|
2916
|
+
const push = (value) => {
|
|
2917
|
+
if (!values.some((item) => Object.is(item, value)))
|
|
2918
|
+
values.push(value);
|
|
2919
|
+
};
|
|
2920
|
+
if (path.includes(".") && path in input)
|
|
2921
|
+
push(input[path]);
|
|
2922
|
+
const nestedValue = getPathValue(input, path);
|
|
2923
|
+
if (nestedValue !== undefined || !path.includes("."))
|
|
2924
|
+
push(nestedValue);
|
|
2925
|
+
return values;
|
|
2926
|
+
}
|
|
2927
|
+
function wildcardToRegExp(pattern, options = {}) {
|
|
2928
|
+
let body = "";
|
|
2929
|
+
for (let index = 0;index < pattern.length; index += 1) {
|
|
2930
|
+
const char = pattern[index];
|
|
2931
|
+
if (char === "*") {
|
|
2932
|
+
if (pattern[index + 1] === "*") {
|
|
2933
|
+
body += ".*";
|
|
2934
|
+
index += 1;
|
|
2935
|
+
} else {
|
|
2936
|
+
body += options.segmentSafe ? "[^/]*" : ".*";
|
|
2937
|
+
}
|
|
2938
|
+
} else {
|
|
2939
|
+
body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
return new RegExp(`^${body}$`);
|
|
2906
2943
|
}
|
|
2907
|
-
function matchString(value, matcher) {
|
|
2944
|
+
function matchString(value, matcher, options = {}) {
|
|
2908
2945
|
if (matcher === undefined)
|
|
2909
2946
|
return true;
|
|
2910
2947
|
if (value === undefined)
|
|
2911
2948
|
return false;
|
|
2912
2949
|
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
2913
|
-
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
2950
|
+
return matchers.some((item) => wildcardToRegExp(item, options).test(value));
|
|
2914
2951
|
}
|
|
2915
2952
|
function matchRecord(input, matcher) {
|
|
2916
2953
|
if (!matcher)
|
|
2917
2954
|
return true;
|
|
2918
2955
|
return Object.entries(matcher).every(([path, expected]) => {
|
|
2919
|
-
const
|
|
2920
|
-
|
|
2921
|
-
return matchString(actual === undefined ? undefined : String(actual), expected);
|
|
2922
|
-
}
|
|
2923
|
-
return actual === expected;
|
|
2956
|
+
const actualValues = getFieldValues(input, path);
|
|
2957
|
+
return matchField(actualValues, expected, path);
|
|
2924
2958
|
});
|
|
2925
2959
|
}
|
|
2960
|
+
function matchField(actualValues, expected, path) {
|
|
2961
|
+
if (isNegativeMatcher(expected)) {
|
|
2962
|
+
return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
|
|
2963
|
+
}
|
|
2964
|
+
return actualValues.some((actual) => matchPositiveField(actual, expected, path));
|
|
2965
|
+
}
|
|
2966
|
+
function matchPositiveField(actual, expected, path) {
|
|
2967
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
2968
|
+
return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
|
|
2969
|
+
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
2970
|
+
}));
|
|
2971
|
+
}
|
|
2972
|
+
if (Array.isArray(actual)) {
|
|
2973
|
+
return actual.some((item) => item === expected);
|
|
2974
|
+
}
|
|
2975
|
+
return actual === expected;
|
|
2976
|
+
}
|
|
2977
|
+
function stringCandidates(actual) {
|
|
2978
|
+
if (actual === undefined)
|
|
2979
|
+
return [];
|
|
2980
|
+
if (Array.isArray(actual)) {
|
|
2981
|
+
return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
|
|
2982
|
+
}
|
|
2983
|
+
return [String(actual)];
|
|
2984
|
+
}
|
|
2985
|
+
function isPrimitiveFieldValue(value) {
|
|
2986
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
2987
|
+
}
|
|
2988
|
+
function isNegativeMatcher(value) {
|
|
2989
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
|
|
2990
|
+
}
|
|
2926
2991
|
function eventMatchesFilter(event, filter) {
|
|
2927
2992
|
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);
|
|
2928
2993
|
}
|
|
@@ -2938,6 +3003,13 @@ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
|
2938
3003
|
function getEventsDataDir(override) {
|
|
2939
3004
|
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join4(homedir2(), ".hasna", "events");
|
|
2940
3005
|
}
|
|
3006
|
+
function getActiveEventsDirEnv() {
|
|
3007
|
+
if (process.env[HASNA_EVENTS_DIR_ENV])
|
|
3008
|
+
return HASNA_EVENTS_DIR_ENV;
|
|
3009
|
+
if (process.env[HASNA_EVENTS_HOME_ENV])
|
|
3010
|
+
return HASNA_EVENTS_HOME_ENV;
|
|
3011
|
+
return null;
|
|
3012
|
+
}
|
|
2941
3013
|
|
|
2942
3014
|
class JsonEventsStore {
|
|
2943
3015
|
dataDir;
|
|
@@ -3050,6 +3122,52 @@ class JsonEventsStore {
|
|
|
3050
3122
|
});
|
|
3051
3123
|
}
|
|
3052
3124
|
}
|
|
3125
|
+
async function getEventsStatus(dataDir) {
|
|
3126
|
+
const store = new JsonEventsStore(dataDir);
|
|
3127
|
+
await store.init();
|
|
3128
|
+
const [channels, events, deliveries] = await Promise.all([
|
|
3129
|
+
store.listChannels(),
|
|
3130
|
+
store.listEvents(),
|
|
3131
|
+
store.listDeliveries()
|
|
3132
|
+
]);
|
|
3133
|
+
const transports = channels.reduce((counts, channel) => {
|
|
3134
|
+
counts[channel.transport] = (counts[channel.transport] ?? 0) + 1;
|
|
3135
|
+
return counts;
|
|
3136
|
+
}, {});
|
|
3137
|
+
return {
|
|
3138
|
+
service: "events",
|
|
3139
|
+
schemaVersion: "1.0",
|
|
3140
|
+
dataDir: store.dataDir,
|
|
3141
|
+
env: {
|
|
3142
|
+
primary: HASNA_EVENTS_DIR_ENV,
|
|
3143
|
+
fallback: HASNA_EVENTS_HOME_ENV,
|
|
3144
|
+
active: getActiveEventsDirEnv()
|
|
3145
|
+
},
|
|
3146
|
+
files: {
|
|
3147
|
+
channels: statusFile(store.dataDir, "channels.json", channels.length),
|
|
3148
|
+
events: statusFile(store.dataDir, "events.json", events.length),
|
|
3149
|
+
deliveries: statusFile(store.dataDir, "deliveries.json", deliveries.length)
|
|
3150
|
+
},
|
|
3151
|
+
counts: {
|
|
3152
|
+
channels: channels.length,
|
|
3153
|
+
enabledChannels: channels.filter((channel) => channel.enabled).length,
|
|
3154
|
+
disabledChannels: channels.filter((channel) => !channel.enabled).length,
|
|
3155
|
+
events: events.length,
|
|
3156
|
+
deliveries: deliveries.length
|
|
3157
|
+
},
|
|
3158
|
+
transports,
|
|
3159
|
+
safety: {
|
|
3160
|
+
includesEventPayloads: false,
|
|
3161
|
+
includesWebhookSecrets: false,
|
|
3162
|
+
listOutputsRedactSecrets: true,
|
|
3163
|
+
statusOutputIsMetadataOnly: true
|
|
3164
|
+
}
|
|
3165
|
+
};
|
|
3166
|
+
}
|
|
3167
|
+
function statusFile(dataDir, fileName, records) {
|
|
3168
|
+
const path = join4(dataDir, fileName);
|
|
3169
|
+
return { path, exists: existsSync3(path), records };
|
|
3170
|
+
}
|
|
3053
3171
|
var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
3054
3172
|
function buildSignatureBase(timestamp, body) {
|
|
3055
3173
|
return `${timestamp}.${body}`;
|
|
@@ -3275,7 +3393,7 @@ class EventsClient {
|
|
|
3275
3393
|
}
|
|
3276
3394
|
return deliveries;
|
|
3277
3395
|
}
|
|
3278
|
-
async
|
|
3396
|
+
async matchChannel(id, input = {}) {
|
|
3279
3397
|
const channel = await this.store.getChannel(id);
|
|
3280
3398
|
if (!channel)
|
|
3281
3399
|
throw new Error(`Channel not found: ${id}`);
|
|
@@ -3292,6 +3410,34 @@ class EventsClient {
|
|
|
3292
3410
|
time: input.time,
|
|
3293
3411
|
id: input.id
|
|
3294
3412
|
});
|
|
3413
|
+
const matched = channelMatchesEvent(channel, event);
|
|
3414
|
+
return {
|
|
3415
|
+
channelId: channel.id,
|
|
3416
|
+
matched,
|
|
3417
|
+
event,
|
|
3418
|
+
filters: channel.filters,
|
|
3419
|
+
reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
async testChannel(id, input = {}, options = {}) {
|
|
3423
|
+
const channel = await this.store.getChannel(id);
|
|
3424
|
+
if (!channel)
|
|
3425
|
+
throw new Error(`Channel not found: ${id}`);
|
|
3426
|
+
const match = await this.matchChannel(id, input);
|
|
3427
|
+
const event = match.event;
|
|
3428
|
+
if (options.honorFilters && !match.matched) {
|
|
3429
|
+
const timestamp = new Date().toISOString();
|
|
3430
|
+
const result2 = createDeliveryResult(event, channel, [{
|
|
3431
|
+
attempt: 1,
|
|
3432
|
+
status: "skipped",
|
|
3433
|
+
startedAt: timestamp,
|
|
3434
|
+
completedAt: timestamp,
|
|
3435
|
+
error: match.reason
|
|
3436
|
+
}]);
|
|
3437
|
+
result2.metadata = { reason: "filter_mismatch" };
|
|
3438
|
+
await this.store.appendDelivery(result2);
|
|
3439
|
+
return result2;
|
|
3440
|
+
}
|
|
3295
3441
|
const eventForChannel = await this.applyRedaction(event, channel);
|
|
3296
3442
|
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
3297
3443
|
await this.store.appendDelivery(result);
|
|
@@ -3402,6 +3548,76 @@ function normalizeRetryPolicy(policy) {
|
|
|
3402
3548
|
multiplier: Math.max(1, policy?.multiplier ?? 2)
|
|
3403
3549
|
};
|
|
3404
3550
|
}
|
|
3551
|
+
function parseFieldMatchers(values, label, typed = false) {
|
|
3552
|
+
if (!values?.length)
|
|
3553
|
+
return;
|
|
3554
|
+
const result = {};
|
|
3555
|
+
for (const value of values) {
|
|
3556
|
+
const parsed = parseMatcherExpression(value, label);
|
|
3557
|
+
const path = parsed.path;
|
|
3558
|
+
if (path in result)
|
|
3559
|
+
throw new Error(`Duplicate ${label} filter path: ${path}`);
|
|
3560
|
+
const matcherValue = typed ? parseTypedMatcherValue(parsed.rawValue, label) : parsed.rawValue;
|
|
3561
|
+
result[path] = parsed.negated ? { not: matcherValue } : matcherValue;
|
|
3562
|
+
}
|
|
3563
|
+
return result;
|
|
3564
|
+
}
|
|
3565
|
+
function parseFilterOptions(options) {
|
|
3566
|
+
const filter2 = {};
|
|
3567
|
+
if (options.source)
|
|
3568
|
+
filter2.source = options.source;
|
|
3569
|
+
if (options.type)
|
|
3570
|
+
filter2.type = options.type;
|
|
3571
|
+
if (options.subject)
|
|
3572
|
+
filter2.subject = options.subject;
|
|
3573
|
+
if (options.severity)
|
|
3574
|
+
filter2.severity = options.severity;
|
|
3575
|
+
const data = mergeMatchers(parseFieldMatchers(options.data, "data"), parseFieldMatchers(options.dataJson, "data-json", true));
|
|
3576
|
+
const metadata = mergeMatchers(parseFieldMatchers(options.metadata, "metadata"), parseFieldMatchers(options.metadataJson, "metadata-json", true));
|
|
3577
|
+
if (Object.keys(data).length > 0)
|
|
3578
|
+
filter2.data = data;
|
|
3579
|
+
if (Object.keys(metadata).length > 0)
|
|
3580
|
+
filter2.metadata = metadata;
|
|
3581
|
+
return Object.keys(filter2).length > 0 ? [filter2] : undefined;
|
|
3582
|
+
}
|
|
3583
|
+
function mergeMatchers(...records) {
|
|
3584
|
+
const result = {};
|
|
3585
|
+
for (const record of records) {
|
|
3586
|
+
if (!record)
|
|
3587
|
+
continue;
|
|
3588
|
+
for (const [path, value] of Object.entries(record)) {
|
|
3589
|
+
if (path in result)
|
|
3590
|
+
throw new Error(`Duplicate filter path: ${path}`);
|
|
3591
|
+
result[path] = value;
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
return result;
|
|
3595
|
+
}
|
|
3596
|
+
function parseTypedMatcherValue(value, label) {
|
|
3597
|
+
const parsed = JSON.parse(value);
|
|
3598
|
+
if (parsed === null || typeof parsed === "string" || typeof parsed === "number" || typeof parsed === "boolean" || Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
|
|
3599
|
+
return parsed;
|
|
3600
|
+
}
|
|
3601
|
+
throw new Error(`${label} filter JSON values must be string, string[], number, boolean, or null`);
|
|
3602
|
+
}
|
|
3603
|
+
function parseMatcherExpression(value, label) {
|
|
3604
|
+
const negativeSeparator = value.indexOf("!=");
|
|
3605
|
+
if (negativeSeparator > 0) {
|
|
3606
|
+
return {
|
|
3607
|
+
path: value.slice(0, negativeSeparator),
|
|
3608
|
+
rawValue: value.slice(negativeSeparator + 2),
|
|
3609
|
+
negated: true
|
|
3610
|
+
};
|
|
3611
|
+
}
|
|
3612
|
+
const separator = value.indexOf("=");
|
|
3613
|
+
if (separator <= 0)
|
|
3614
|
+
throw new Error(`Invalid ${label} filter, expected path=value or path!=value: ${value}`);
|
|
3615
|
+
return {
|
|
3616
|
+
path: value.slice(0, separator),
|
|
3617
|
+
rawValue: value.slice(separator + 1),
|
|
3618
|
+
negated: false
|
|
3619
|
+
};
|
|
3620
|
+
}
|
|
3405
3621
|
function parseJsonObject2(value, fallback) {
|
|
3406
3622
|
if (!value)
|
|
3407
3623
|
return fallback;
|
|
@@ -3423,18 +3639,6 @@ function parseHeaders(values) {
|
|
|
3423
3639
|
}
|
|
3424
3640
|
return headers;
|
|
3425
3641
|
}
|
|
3426
|
-
function parseFilter(options) {
|
|
3427
|
-
const filter2 = {};
|
|
3428
|
-
if (options.source)
|
|
3429
|
-
filter2.source = options.source;
|
|
3430
|
-
if (options.type)
|
|
3431
|
-
filter2.type = options.type;
|
|
3432
|
-
if (options.subject)
|
|
3433
|
-
filter2.subject = options.subject;
|
|
3434
|
-
if (options.severity)
|
|
3435
|
-
filter2.severity = options.severity;
|
|
3436
|
-
return Object.keys(filter2).length > 0 ? [filter2] : undefined;
|
|
3437
|
-
}
|
|
3438
3642
|
function createClient(options) {
|
|
3439
3643
|
if (options.createClient)
|
|
3440
3644
|
return options.createClient();
|
|
@@ -3452,16 +3656,16 @@ function hasJsonOption(options) {
|
|
|
3452
3656
|
function wantsJson(actionOptions, command) {
|
|
3453
3657
|
return hasJsonOption(actionOptions) || hasJsonOption(command);
|
|
3454
3658
|
}
|
|
3455
|
-
function
|
|
3456
|
-
const
|
|
3457
|
-
|
|
3659
|
+
function registerChannelCommands(program, options) {
|
|
3660
|
+
const channels = program.command(options.channelsCommandName ?? "channels").description("Manage Hasna event channels");
|
|
3661
|
+
channels.command("add").description("Add or replace a channel").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--data <path=value...>", "Event data field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--metadata <path=value...>", "Event metadata field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--data-json <path=json...>", "Event data field filter with typed JSON value; path!=json negatives supported", collectValues, []).option("--metadata-json <path=json...>", "Event metadata field filter with typed JSON value; path!=json negatives supported", collectValues, []).option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions, command) => {
|
|
3458
3662
|
const timestamp = new Date().toISOString();
|
|
3459
3663
|
const channel = {
|
|
3460
3664
|
id: actionOptions.id,
|
|
3461
3665
|
name: actionOptions.name,
|
|
3462
3666
|
enabled: !actionOptions.disabled,
|
|
3463
3667
|
transport: actionOptions.transport,
|
|
3464
|
-
filters:
|
|
3668
|
+
filters: parseFilterOptions(actionOptions),
|
|
3465
3669
|
retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
|
|
3466
3670
|
redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
|
|
3467
3671
|
createdAt: timestamp,
|
|
@@ -3477,35 +3681,51 @@ function registerWebhookCommands(program, options) {
|
|
|
3477
3681
|
const saved = await createClient(options).addChannel(channel);
|
|
3478
3682
|
print(sanitizeChannelForOutput(saved), wantsJson(actionOptions, command), `Added ${saved.transport} channel ${saved.id}`);
|
|
3479
3683
|
});
|
|
3480
|
-
|
|
3481
|
-
const
|
|
3684
|
+
channels.command("list").description("List configured channels").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
3685
|
+
const channels2 = await createClient(options).listChannels();
|
|
3482
3686
|
if (wantsJson(actionOptions, command)) {
|
|
3483
|
-
console.log(JSON.stringify(sanitizeChannelsForOutput(
|
|
3687
|
+
console.log(JSON.stringify(sanitizeChannelsForOutput(channels2), null, 2));
|
|
3484
3688
|
return;
|
|
3485
3689
|
}
|
|
3486
|
-
if (!
|
|
3690
|
+
if (!channels2.length) {
|
|
3487
3691
|
console.log("No channels configured.");
|
|
3488
3692
|
return;
|
|
3489
3693
|
}
|
|
3490
|
-
for (const channel of
|
|
3694
|
+
for (const channel of channels2) {
|
|
3491
3695
|
console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
|
|
3492
3696
|
}
|
|
3493
3697
|
});
|
|
3494
|
-
|
|
3698
|
+
channels.command("status").description("Show events channel storage status").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
|
|
3699
|
+
const status = await getEventsStatus(options.dataDir);
|
|
3700
|
+
print(status, wantsJson(actionOptions, command), `events dataDir: ${status.dataDir}`);
|
|
3701
|
+
});
|
|
3702
|
+
channels.command("remove").description("Remove a channel").argument("<id>", "Channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
3495
3703
|
const removed = await createClient(options).removeChannel(id);
|
|
3496
3704
|
print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
|
|
3497
3705
|
});
|
|
3498
|
-
|
|
3706
|
+
channels.command("test").description("Send a test event to one channel").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--honor-filters", "Skip delivery when the sample event does not match channel filters", false).option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
3499
3707
|
const result = await createClient(options).testChannel(id, {
|
|
3500
|
-
source: options.source,
|
|
3708
|
+
source: actionOptions.source ?? options.source,
|
|
3501
3709
|
type: actionOptions.type,
|
|
3502
3710
|
subject: actionOptions.subject ?? id,
|
|
3503
3711
|
message: actionOptions.message,
|
|
3504
|
-
data: parseJsonObject2(actionOptions.data, { test: true })
|
|
3505
|
-
|
|
3712
|
+
data: parseJsonObject2(actionOptions.data, { test: true }),
|
|
3713
|
+
metadata: parseJsonObject2(actionOptions.metadata, {})
|
|
3714
|
+
}, { honorFilters: actionOptions.honorFilters });
|
|
3506
3715
|
print(result, wantsJson(actionOptions, command), `${result.status}: ${result.channelId}`);
|
|
3507
3716
|
});
|
|
3508
|
-
|
|
3717
|
+
channels.command("match").description("Check whether a sample event matches one channel without delivering").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events match preview").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
|
|
3718
|
+
const result = await createClient(options).matchChannel(id, {
|
|
3719
|
+
source: actionOptions.source ?? options.source,
|
|
3720
|
+
type: actionOptions.type,
|
|
3721
|
+
subject: actionOptions.subject ?? id,
|
|
3722
|
+
message: actionOptions.message,
|
|
3723
|
+
data: parseJsonObject2(actionOptions.data, { test: true }),
|
|
3724
|
+
metadata: parseJsonObject2(actionOptions.metadata, {})
|
|
3725
|
+
});
|
|
3726
|
+
print(result, wantsJson(actionOptions, command), `${result.matched ? "matched" : "skipped"}: ${result.channelId}`);
|
|
3727
|
+
});
|
|
3728
|
+
return channels;
|
|
3509
3729
|
}
|
|
3510
3730
|
function registerEventCommands(program, options) {
|
|
3511
3731
|
const events = program.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
|
|
@@ -3553,7 +3773,7 @@ function registerEventCommands(program, options) {
|
|
|
3553
3773
|
return events;
|
|
3554
3774
|
}
|
|
3555
3775
|
function registerEventsCommands(program, options) {
|
|
3556
|
-
|
|
3776
|
+
registerChannelCommands(program, options);
|
|
3557
3777
|
registerEventCommands(program, options);
|
|
3558
3778
|
}
|
|
3559
3779
|
function parseNumber(value) {
|
|
@@ -4078,7 +4298,7 @@ import { dirname as dirname3, join as join6 } from "path";
|
|
|
4078
4298
|
import { fileURLToPath } from "url";
|
|
4079
4299
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
4080
4300
|
|
|
4081
|
-
//
|
|
4301
|
+
// node_modules/.pnpm/@hasna+contracts@0.5.2/node_modules/@hasna/contracts/dist/client/storage.js
|
|
4082
4302
|
var __defProp2 = Object.defineProperty;
|
|
4083
4303
|
var __returnValue = (v) => v;
|
|
4084
4304
|
function __exportSetter(name, newValue) {
|
|
@@ -10537,6 +10757,13 @@ class CloudShortlinksStore {
|
|
|
10537
10757
|
const domains = await this.listDomains();
|
|
10538
10758
|
return domains.find((d) => d.default_domain) ?? domains[0] ?? null;
|
|
10539
10759
|
}
|
|
10760
|
+
async deleteDomain(hostnameOrId) {
|
|
10761
|
+
const domain = await this.getDomain(hostnameOrId);
|
|
10762
|
+
if (!domain)
|
|
10763
|
+
throw new Error("Domain not found.");
|
|
10764
|
+
await this.transport.del(`/domains/${enc(domain.hostname)}`);
|
|
10765
|
+
return domain;
|
|
10766
|
+
}
|
|
10540
10767
|
async createLink(input) {
|
|
10541
10768
|
return this.client.create("links", {
|
|
10542
10769
|
url: input.destinationUrl,
|
|
@@ -10588,7 +10815,7 @@ class CloudShortlinksStore {
|
|
|
10588
10815
|
const link = maybeSlug ? await this.getLink(domainOrSlug, maybeSlug) : await this.getLink(domainOrSlug);
|
|
10589
10816
|
if (!link)
|
|
10590
10817
|
throw new Error("Link not found.");
|
|
10591
|
-
await this.
|
|
10818
|
+
await this.transport.del(`/links/${enc(link.slug)}`, undefined, maybeSlug ? { query: { domain: domainOrSlug } } : {});
|
|
10592
10819
|
return link;
|
|
10593
10820
|
}
|
|
10594
10821
|
async recordClick(_link, _input = {}) {
|
|
@@ -10628,6 +10855,9 @@ class LocalStore {
|
|
|
10628
10855
|
async getDefaultDomain() {
|
|
10629
10856
|
return this.inner.getDefaultDomain();
|
|
10630
10857
|
}
|
|
10858
|
+
async deleteDomain(hostnameOrId) {
|
|
10859
|
+
return this.inner.deleteDomain(hostnameOrId);
|
|
10860
|
+
}
|
|
10631
10861
|
async createLink(input) {
|
|
10632
10862
|
return this.inner.createLink(input);
|
|
10633
10863
|
}
|
|
@@ -10913,6 +11143,17 @@ domainCmd.command("get <hostname>").description("Show a configured domain").opti
|
|
|
10913
11143
|
handleError(error);
|
|
10914
11144
|
}
|
|
10915
11145
|
});
|
|
11146
|
+
domainCmd.command("remove <hostname>").alias("delete").alias("rm").description("Delete a domain and all of its links and clicks").option("-j, --json", "Output JSON").action(async (hostname2, opts) => {
|
|
11147
|
+
try {
|
|
11148
|
+
const domain = await withRuntimeStore((store) => store.deleteDomain(hostname2));
|
|
11149
|
+
print2({ deleted: true, hostname: domain.hostname }, opts, () => {
|
|
11150
|
+
console.log(source_default.green(`Domain removed: ${domain.hostname}`));
|
|
11151
|
+
console.log(source_default.dim("Its links and clicks were deleted."));
|
|
11152
|
+
});
|
|
11153
|
+
} catch (error) {
|
|
11154
|
+
handleError(error);
|
|
11155
|
+
}
|
|
11156
|
+
});
|
|
10916
11157
|
domainCmd.command("setup <hostname>").description("Add a domain locally and optionally prepare Cloudflare DNS").option("--default", "Make this the default domain").option("--origin <url>", "Origin redirect server URL").option("--cloudflare", "Upsert Cloudflare CNAME record").option("--target <hostname>", "CNAME target for Cloudflare DNS").option("--zone-id <id>", "Cloudflare zone ID").option("--dry-run", "Show the Cloudflare plan without changing DNS").option("-j, --json", "Output JSON").action(async (hostname2, opts) => {
|
|
10917
11158
|
try {
|
|
10918
11159
|
const result = await withRuntimeStore(async (store) => {
|
package/dist/client-store.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export declare class LocalStore implements Store {
|
|
|
17
17
|
listDomains(): Promise<Domain[]>;
|
|
18
18
|
getDomain(hostnameOrId: string): Promise<Domain | null>;
|
|
19
19
|
getDefaultDomain(): Promise<Domain | null>;
|
|
20
|
+
deleteDomain(hostnameOrId: string): Promise<Domain>;
|
|
20
21
|
createLink(input: CreateLinkInput): Promise<Link>;
|
|
21
22
|
listLinks(options?: ListLinksOptions): Promise<Link[]>;
|
|
22
23
|
getLink(domainOrSlug: string, maybeSlug?: string): Promise<Link | null>;
|
package/dist/cloud-store.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export declare class CloudShortlinksStore implements Store {
|
|
|
26
26
|
listDomains(): Promise<Domain[]>;
|
|
27
27
|
getDomain(hostnameOrId: string): Promise<Domain | null>;
|
|
28
28
|
getDefaultDomain(): Promise<Domain | null>;
|
|
29
|
+
deleteDomain(hostnameOrId: string): Promise<Domain>;
|
|
29
30
|
createLink(input: CreateLinkInput): Promise<Link>;
|
|
30
31
|
listLinks(options?: {
|
|
31
32
|
domain?: string;
|
package/dist/index.js
CHANGED
|
@@ -430,6 +430,17 @@ class ShortlinksStore {
|
|
|
430
430
|
`).get(normalized, hostnameOrId);
|
|
431
431
|
return row ? domainFromRow(row) : null;
|
|
432
432
|
}
|
|
433
|
+
deleteDomain(hostnameOrId) {
|
|
434
|
+
const domain = this.getDomain(hostnameOrId);
|
|
435
|
+
if (!domain)
|
|
436
|
+
throw new Error("Domain not found.");
|
|
437
|
+
this.database.db.query("DELETE FROM domains WHERE id = ?").run(domain.id);
|
|
438
|
+
const config = loadConfig();
|
|
439
|
+
if (config.defaultDomain && normalizeHostname(config.defaultDomain) === domain.hostname) {
|
|
440
|
+
updateConfig({ defaultDomain: undefined, publicBaseUrl: undefined });
|
|
441
|
+
}
|
|
442
|
+
return domain;
|
|
443
|
+
}
|
|
433
444
|
getDefaultDomain() {
|
|
434
445
|
const config = loadConfig();
|
|
435
446
|
if (config.defaultDomain) {
|
|
@@ -1003,6 +1014,13 @@ class PgShortlinksStore {
|
|
|
1003
1014
|
`);
|
|
1004
1015
|
return row ? domainFromRow2(row) : null;
|
|
1005
1016
|
}
|
|
1017
|
+
async deleteDomain(hostnameOrId) {
|
|
1018
|
+
const domain = await this.getDomain(hostnameOrId);
|
|
1019
|
+
if (!domain)
|
|
1020
|
+
throw new Error("Domain not found.");
|
|
1021
|
+
await this.pg.run("DELETE FROM domains WHERE id = ?", domain.id);
|
|
1022
|
+
return domain;
|
|
1023
|
+
}
|
|
1006
1024
|
async createLink(input) {
|
|
1007
1025
|
const domain = input.domain ? await this.getDomain(input.domain) : await this.getDefaultDomain();
|
|
1008
1026
|
if (!domain) {
|
|
@@ -1178,7 +1196,7 @@ class PgShortlinksStore {
|
|
|
1178
1196
|
throw new Error("Could not generate an unused slug after 32 attempts.");
|
|
1179
1197
|
}
|
|
1180
1198
|
}
|
|
1181
|
-
//
|
|
1199
|
+
// node_modules/.pnpm/@hasna+contracts@0.5.2/node_modules/@hasna/contracts/dist/client/storage.js
|
|
1182
1200
|
var __defProp2 = Object.defineProperty;
|
|
1183
1201
|
var __returnValue = (v) => v;
|
|
1184
1202
|
function __exportSetter(name, newValue) {
|
|
@@ -7637,6 +7655,13 @@ class CloudShortlinksStore {
|
|
|
7637
7655
|
const domains = await this.listDomains();
|
|
7638
7656
|
return domains.find((d) => d.default_domain) ?? domains[0] ?? null;
|
|
7639
7657
|
}
|
|
7658
|
+
async deleteDomain(hostnameOrId) {
|
|
7659
|
+
const domain = await this.getDomain(hostnameOrId);
|
|
7660
|
+
if (!domain)
|
|
7661
|
+
throw new Error("Domain not found.");
|
|
7662
|
+
await this.transport.del(`/domains/${enc(domain.hostname)}`);
|
|
7663
|
+
return domain;
|
|
7664
|
+
}
|
|
7640
7665
|
async createLink(input) {
|
|
7641
7666
|
return this.client.create("links", {
|
|
7642
7667
|
url: input.destinationUrl,
|
|
@@ -7688,7 +7713,7 @@ class CloudShortlinksStore {
|
|
|
7688
7713
|
const link = maybeSlug ? await this.getLink(domainOrSlug, maybeSlug) : await this.getLink(domainOrSlug);
|
|
7689
7714
|
if (!link)
|
|
7690
7715
|
throw new Error("Link not found.");
|
|
7691
|
-
await this.
|
|
7716
|
+
await this.transport.del(`/links/${enc(link.slug)}`, undefined, maybeSlug ? { query: { domain: domainOrSlug } } : {});
|
|
7692
7717
|
return link;
|
|
7693
7718
|
}
|
|
7694
7719
|
async recordClick(_link, _input = {}) {
|
|
@@ -7728,6 +7753,9 @@ class LocalStore {
|
|
|
7728
7753
|
async getDefaultDomain() {
|
|
7729
7754
|
return this.inner.getDefaultDomain();
|
|
7730
7755
|
}
|
|
7756
|
+
async deleteDomain(hostnameOrId) {
|
|
7757
|
+
return this.inner.deleteDomain(hostnameOrId);
|
|
7758
|
+
}
|
|
7731
7759
|
async createLink(input) {
|
|
7732
7760
|
return this.inner.createLink(input);
|
|
7733
7761
|
}
|
|
@@ -7773,7 +7801,7 @@ async function withStore(fn, env = process.env, options = {}) {
|
|
|
7773
7801
|
await store.close();
|
|
7774
7802
|
}
|
|
7775
7803
|
}
|
|
7776
|
-
//
|
|
7804
|
+
// node_modules/.pnpm/@hasna+contracts@0.5.2/node_modules/@hasna/contracts/dist/auth/index.js
|
|
7777
7805
|
import { createHash as createHash3, createHmac, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
7778
7806
|
var API_KEY_TOKEN_VERSION = 1;
|
|
7779
7807
|
var API_KEY_NAMESPACE = "hasna";
|
|
@@ -9975,6 +10003,11 @@ function buildOpenApiDocument(version) {
|
|
|
9975
10003
|
properties: { deleted: { type: "boolean" }, slug: { type: "string" } },
|
|
9976
10004
|
required: ["deleted"]
|
|
9977
10005
|
},
|
|
10006
|
+
DomainDeleteResponse: {
|
|
10007
|
+
type: "object",
|
|
10008
|
+
properties: { deleted: { type: "boolean" }, hostname: { type: "string" } },
|
|
10009
|
+
required: ["deleted"]
|
|
10010
|
+
},
|
|
9978
10011
|
HealthStatus: probe({ db_latency_ms: { type: "integer" } }),
|
|
9979
10012
|
ReadyStatus: probe({ pending_migrations: { type: "array", items: { type: "string" } } }),
|
|
9980
10013
|
VersionInfo: probe({ name: { type: "string" } }),
|
|
@@ -10045,6 +10078,19 @@ function buildOpenApiDocument(version) {
|
|
|
10045
10078
|
}
|
|
10046
10079
|
}
|
|
10047
10080
|
},
|
|
10081
|
+
"/v1/domains/{hostname}": {
|
|
10082
|
+
delete: {
|
|
10083
|
+
operationId: "deleteDomain",
|
|
10084
|
+
summary: "Delete a domain and all of its links and clicks.",
|
|
10085
|
+
security: [{ apiKey: [] }],
|
|
10086
|
+
parameters: [
|
|
10087
|
+
{ name: "hostname", in: "path", required: true, schema: { type: "string" } }
|
|
10088
|
+
],
|
|
10089
|
+
responses: {
|
|
10090
|
+
"200": { content: { "application/json": { schema: { $ref: "#/components/schemas/DomainDeleteResponse" } } } }
|
|
10091
|
+
}
|
|
10092
|
+
}
|
|
10093
|
+
},
|
|
10048
10094
|
"/v1/links": {
|
|
10049
10095
|
get: {
|
|
10050
10096
|
operationId: "listLinks",
|
|
@@ -10246,6 +10292,18 @@ function createServeApp(deps) {
|
|
|
10246
10292
|
return handleError(c, error);
|
|
10247
10293
|
}
|
|
10248
10294
|
});
|
|
10295
|
+
app.delete("/v1/domains/:hostname", async (c) => {
|
|
10296
|
+
const denied = await requireScopes(c, [`${APP_SLUG}:write`]);
|
|
10297
|
+
if (denied)
|
|
10298
|
+
return denied;
|
|
10299
|
+
const hostname2 = c.req.param("hostname");
|
|
10300
|
+
try {
|
|
10301
|
+
const domain = await store.deleteDomain(hostname2);
|
|
10302
|
+
return c.json({ deleted: true, hostname: domain.hostname });
|
|
10303
|
+
} catch (error) {
|
|
10304
|
+
return handleError(c, error);
|
|
10305
|
+
}
|
|
10306
|
+
});
|
|
10249
10307
|
app.get("/v1/links", async (c) => {
|
|
10250
10308
|
const denied = await requireScopes(c, [`${APP_SLUG}:read`]);
|
|
10251
10309
|
if (denied)
|