@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 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 wildcardToRegExp(pattern) {
2904
- const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
2905
- return new RegExp(`^${escaped}$`);
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 actual = getPathValue(input, path);
2920
- if (typeof expected === "string" || Array.isArray(expected)) {
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 testChannel(id, input = {}) {
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 registerWebhookCommands(program, options) {
3456
- const webhooks = program.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
3457
- 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) => {
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: parseFilter(actionOptions),
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
- webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
3481
- const channels = await createClient(options).listChannels();
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(channels), null, 2));
3687
+ console.log(JSON.stringify(sanitizeChannelsForOutput(channels2), null, 2));
3484
3688
  return;
3485
3689
  }
3486
- if (!channels.length) {
3690
+ if (!channels2.length) {
3487
3691
  console.log("No channels configured.");
3488
3692
  return;
3489
3693
  }
3490
- for (const channel of channels) {
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
- webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
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
- 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) => {
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
- return webhooks;
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
- registerWebhookCommands(program, options);
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
- // ../open-contracts/dist/client/storage.js
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.client.delete("links", link.slug, maybeSlug ? { query: { domain: domainOrSlug } } : {});
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) => {
@@ -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>;
@@ -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
- // ../open-contracts/dist/client/storage.js
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.client.delete("links", link.slug, maybeSlug ? { query: { domain: domainOrSlug } } : {});
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
- // ../open-contracts/dist/auth/index.js
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)