@hogsend/cli 0.7.0 → 0.8.0

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/bin.js CHANGED
@@ -85,6 +85,10 @@ function createAdminClient(cfg) {
85
85
  post: (path, body) => request(cfg.baseUrl, cfg.adminKey, missing, "POST", path, {
86
86
  body,
87
87
  auth: true
88
+ }),
89
+ del: (path, body) => request(cfg.baseUrl, cfg.adminKey, missing, "DELETE", path, {
90
+ body,
91
+ auth: true
88
92
  })
89
93
  };
90
94
  }
@@ -1057,8 +1061,8 @@ async function run3(ctx) {
1057
1061
  if (!ok) process.exit(1);
1058
1062
  return;
1059
1063
  }
1060
- const badge5 = `${color.bgMagenta(color.black(" hogsend "))} doctor`;
1061
- ctx.out.intro(badge5);
1064
+ const badge6 = `${color.bgMagenta(color.black(" hogsend "))} doctor`;
1065
+ ctx.out.intro(badge6);
1062
1066
  const verdictColor = verdict === "ok" ? color.green : verdict === "degraded" ? color.red : color.yellow;
1063
1067
  const lines = [
1064
1068
  `${verdictColor("\u25CF")} ${color.bold(verdict)}`,
@@ -2944,6 +2948,437 @@ var upgradeCommand = {
2944
2948
  run: run13
2945
2949
  };
2946
2950
 
2951
+ // src/commands/webhooks.ts
2952
+ import { parseArgs as parseArgs14 } from "util";
2953
+ var WEBHOOK_EVENT_TYPES = [
2954
+ "contact.created",
2955
+ "contact.updated",
2956
+ "contact.deleted",
2957
+ "contact.unsubscribed",
2958
+ "email.sent",
2959
+ "email.delivered",
2960
+ "email.opened",
2961
+ "email.clicked",
2962
+ "email.bounced",
2963
+ "journey.completed",
2964
+ "bucket.entered",
2965
+ "bucket.left"
2966
+ ];
2967
+ var usage14 = `hogsend webhooks <subcommand> [options]
2968
+
2969
+ Manage outbound webhook endpoints \u2014 the Svix-style signed event stream Hogsend
2970
+ emits to your URLs. Wraps the admin routes (/v1/admin/webhooks), so this command
2971
+ REQUIRES an admin key (--admin-key / HOGSEND_ADMIN_KEY), not the data key.
2972
+
2973
+ Subcommands:
2974
+ list List endpoints.
2975
+ get <id> Show one endpoint.
2976
+ create Register an endpoint (prints the secret ONCE).
2977
+ update <id> Patch an endpoint.
2978
+ delete <id> Hard-delete an endpoint (drops its deliveries).
2979
+ rotate-secret <id> Issue a new signing secret (prints it ONCE).
2980
+ test <id> Enqueue an out-of-band webhook.test delivery.
2981
+
2982
+ list options:
2983
+ --include-disabled Include disabled endpoints.
2984
+ --limit <n> Page size.
2985
+ --offset <n> Page offset.
2986
+
2987
+ create options (--url required, plus at least one event):
2988
+ --url <url> Destination URL (required).
2989
+ --event <type> Subscribe to an event; repeatable.
2990
+ --all-events Subscribe to all 12 event types.
2991
+ --description <text> Human label.
2992
+ --disabled Create the endpoint disabled.
2993
+
2994
+ update options (only the provided fields change):
2995
+ --url <url> New destination URL.
2996
+ --event <type> Replace the subscribed events (repeatable).
2997
+ --all-events Subscribe to all 12 event types.
2998
+ --description <text> New description.
2999
+ --disabled / --enabled Disable or enable the endpoint.
3000
+
3001
+ Event types:
3002
+ ${WEBHOOK_EVENT_TYPES.join(", ")}
3003
+
3004
+ Global options (handled by the router): --url, --admin-key, --data-key, --json,
3005
+ -h/--help.
3006
+
3007
+ Examples:
3008
+ hogsend webhooks create --url https://x.com/hook --event contact.created --event email.sent
3009
+ hogsend webhooks create --url https://x.com/hook --all-events --json
3010
+ hogsend webhooks list --include-disabled
3011
+ hogsend webhooks rotate-secret we_123
3012
+ hogsend webhooks test we_123`;
3013
+ var badge5 = `${color.bgMagenta(color.black(" hogsend "))} webhooks`;
3014
+ async function fetchOrFail2(ctx, label, fn) {
3015
+ try {
3016
+ return await ctx.out.step(label, fn);
3017
+ } catch (err) {
3018
+ if (isHttpError(err)) {
3019
+ if (err.status === 404) {
3020
+ ctx.out.fail(err.message || "webhook endpoint not found");
3021
+ }
3022
+ ctx.out.fail(err.message);
3023
+ }
3024
+ throw err;
3025
+ }
3026
+ }
3027
+ function resolveEvents(ctx, allEvents, events) {
3028
+ if (allEvents) {
3029
+ return [...WEBHOOK_EVENT_TYPES];
3030
+ }
3031
+ if (!events || events.length === 0) {
3032
+ return void 0;
3033
+ }
3034
+ const valid = new Set(WEBHOOK_EVENT_TYPES);
3035
+ const out = [];
3036
+ for (const ev of events) {
3037
+ if (!valid.has(ev)) {
3038
+ ctx.out.fail(
3039
+ `unknown event type "${ev}" \u2014 expected one of: ${WEBHOOK_EVENT_TYPES.join(", ")}`
3040
+ );
3041
+ }
3042
+ if (!out.includes(ev)) {
3043
+ out.push(ev);
3044
+ }
3045
+ }
3046
+ return out;
3047
+ }
3048
+ function printSecretOnce(ctx, secret) {
3049
+ ctx.out.note(
3050
+ `${color.yellow("Store this signing secret now \u2014 it is shown only once and cannot be recovered.")}
3051
+
3052
+ ${color.bold(secret)}`,
3053
+ color.yellow("Signing secret")
3054
+ );
3055
+ }
3056
+ async function runList4(ctx, argv) {
3057
+ const { values } = parseArgs14({
3058
+ args: argv,
3059
+ allowPositionals: true,
3060
+ options: {
3061
+ "include-disabled": { type: "boolean", default: false },
3062
+ limit: { type: "string" },
3063
+ offset: { type: "string" },
3064
+ help: { type: "boolean", short: "h", default: false }
3065
+ }
3066
+ });
3067
+ if (values.help) {
3068
+ ctx.out.log(usage14);
3069
+ return;
3070
+ }
3071
+ const query = {
3072
+ includeDisabled: values["include-disabled"] ? "true" : void 0,
3073
+ limit: values.limit,
3074
+ offset: values.offset
3075
+ };
3076
+ if (!ctx.json) ctx.out.intro(`${badge5} list`);
3077
+ const res = await fetchOrFail2(
3078
+ ctx,
3079
+ "Fetching webhooks",
3080
+ () => ctx.http.get("/v1/admin/webhooks", query)
3081
+ );
3082
+ if (ctx.json) {
3083
+ ctx.out.json(res);
3084
+ return;
3085
+ }
3086
+ ctx.out.table(
3087
+ res.endpoints.map((ep) => ({
3088
+ id: ep.id,
3089
+ url: ep.url,
3090
+ status: ep.status === "enabled" ? color.green(ep.status) : color.yellow(ep.status),
3091
+ events: ep.eventTypes.length,
3092
+ lastDeliveryAt: ep.lastDeliveryAt ?? color.dim("(never)")
3093
+ })),
3094
+ ["id", "url", "status", "events", "lastDeliveryAt"]
3095
+ );
3096
+ ctx.out.outro(
3097
+ `${res.endpoints.length} of ${res.total} endpoint(s) \u2014 offset ${res.offset}, limit ${res.limit}`
3098
+ );
3099
+ }
3100
+ function renderEndpoint(ctx, ep, title) {
3101
+ ctx.out.kv(
3102
+ {
3103
+ id: ep.id,
3104
+ url: ep.url,
3105
+ description: ep.description ?? color.dim("(none)"),
3106
+ status: ep.status === "enabled" ? color.green(ep.status) : color.yellow(ep.status),
3107
+ eventTypes: ep.eventTypes,
3108
+ secretPrefix: ep.secretPrefix,
3109
+ lastDeliveryAt: ep.lastDeliveryAt ?? color.dim("(never)"),
3110
+ createdAt: ep.createdAt,
3111
+ updatedAt: ep.updatedAt
3112
+ },
3113
+ title
3114
+ );
3115
+ }
3116
+ async function runGet3(ctx, argv) {
3117
+ const { values, positionals } = parseArgs14({
3118
+ args: argv,
3119
+ allowPositionals: true,
3120
+ options: { help: { type: "boolean", short: "h", default: false } }
3121
+ });
3122
+ if (values.help) {
3123
+ ctx.out.log(usage14);
3124
+ return;
3125
+ }
3126
+ const id = positionals[0];
3127
+ if (!id) {
3128
+ ctx.out.fail(
3129
+ "webhooks get requires an endpoint id, e.g. hogsend webhooks get we_123"
3130
+ );
3131
+ }
3132
+ if (!ctx.json) ctx.out.intro(`${badge5} get`);
3133
+ const res = await fetchOrFail2(
3134
+ ctx,
3135
+ "Fetching webhook",
3136
+ () => ctx.http.get(
3137
+ `/v1/admin/webhooks/${encodeURIComponent(id)}`
3138
+ )
3139
+ );
3140
+ if (ctx.json) {
3141
+ ctx.out.json(res);
3142
+ return;
3143
+ }
3144
+ renderEndpoint(ctx, res, "Endpoint");
3145
+ ctx.out.outro(`${res.url} \u2192 ${res.status}`);
3146
+ }
3147
+ async function runCreate(ctx, argv) {
3148
+ const { values } = parseArgs14({
3149
+ args: argv,
3150
+ allowPositionals: true,
3151
+ options: {
3152
+ url: { type: "string" },
3153
+ event: { type: "string", multiple: true },
3154
+ "all-events": { type: "boolean", default: false },
3155
+ description: { type: "string" },
3156
+ disabled: { type: "boolean", default: false },
3157
+ help: { type: "boolean", short: "h", default: false }
3158
+ }
3159
+ });
3160
+ if (values.help) {
3161
+ ctx.out.log(usage14);
3162
+ return;
3163
+ }
3164
+ const url = values.url;
3165
+ if (!url) {
3166
+ ctx.out.fail(
3167
+ "webhooks create requires --url, e.g. hogsend webhooks create --url https://x.com/hook --all-events"
3168
+ );
3169
+ }
3170
+ const eventTypes = resolveEvents(ctx, values["all-events"], values.event);
3171
+ if (!eventTypes || eventTypes.length === 0) {
3172
+ ctx.out.fail(
3173
+ "webhooks create requires at least one --event <type> (or --all-events)"
3174
+ );
3175
+ }
3176
+ const body = { url, eventTypes };
3177
+ if (values.description !== void 0) body.description = values.description;
3178
+ if (values.disabled) body.disabled = true;
3179
+ if (!ctx.json) ctx.out.intro(`${badge5} create`);
3180
+ const res = await fetchOrFail2(
3181
+ ctx,
3182
+ "Creating webhook",
3183
+ () => ctx.http.post("/v1/admin/webhooks", body)
3184
+ );
3185
+ if (ctx.json) {
3186
+ ctx.out.json(res);
3187
+ return;
3188
+ }
3189
+ const { secret, ...endpoint } = res;
3190
+ renderEndpoint(ctx, endpoint, "Endpoint created");
3191
+ printSecretOnce(ctx, secret);
3192
+ ctx.out.outro(`${color.green("Created")} ${res.id} \u2192 ${res.url}`);
3193
+ }
3194
+ async function runUpdate(ctx, argv) {
3195
+ const { values, positionals } = parseArgs14({
3196
+ args: argv,
3197
+ allowPositionals: true,
3198
+ options: {
3199
+ url: { type: "string" },
3200
+ event: { type: "string", multiple: true },
3201
+ "all-events": { type: "boolean", default: false },
3202
+ description: { type: "string" },
3203
+ disabled: { type: "boolean", default: false },
3204
+ enabled: { type: "boolean", default: false },
3205
+ help: { type: "boolean", short: "h", default: false }
3206
+ }
3207
+ });
3208
+ if (values.help) {
3209
+ ctx.out.log(usage14);
3210
+ return;
3211
+ }
3212
+ const id = positionals[0];
3213
+ if (!id) {
3214
+ ctx.out.fail(
3215
+ "webhooks update requires an endpoint id, e.g. hogsend webhooks update we_123 --enabled"
3216
+ );
3217
+ }
3218
+ if (values.disabled && values.enabled) {
3219
+ ctx.out.fail("webhooks update: pass at most one of --disabled / --enabled");
3220
+ }
3221
+ const eventTypes = resolveEvents(ctx, values["all-events"], values.event);
3222
+ const body = {};
3223
+ if (values.url !== void 0) body.url = values.url;
3224
+ if (eventTypes !== void 0) body.eventTypes = eventTypes;
3225
+ if (values.description !== void 0) body.description = values.description;
3226
+ if (values.disabled) body.disabled = true;
3227
+ if (values.enabled) body.disabled = false;
3228
+ if (Object.keys(body).length === 0) {
3229
+ ctx.out.fail(
3230
+ "webhooks update: nothing to change \u2014 pass --url / --event / --description / --disabled / --enabled"
3231
+ );
3232
+ }
3233
+ if (!ctx.json) ctx.out.intro(`${badge5} update`);
3234
+ const res = await fetchOrFail2(
3235
+ ctx,
3236
+ "Updating webhook",
3237
+ () => ctx.http.patch(
3238
+ `/v1/admin/webhooks/${encodeURIComponent(id)}`,
3239
+ body
3240
+ )
3241
+ );
3242
+ if (ctx.json) {
3243
+ ctx.out.json(res);
3244
+ return;
3245
+ }
3246
+ renderEndpoint(ctx, res, "Endpoint updated");
3247
+ ctx.out.outro(`${color.green("Updated")} ${res.id} \u2192 ${res.status}`);
3248
+ }
3249
+ async function runDelete(ctx, argv) {
3250
+ const { values, positionals } = parseArgs14({
3251
+ args: argv,
3252
+ allowPositionals: true,
3253
+ options: { help: { type: "boolean", short: "h", default: false } }
3254
+ });
3255
+ if (values.help) {
3256
+ ctx.out.log(usage14);
3257
+ return;
3258
+ }
3259
+ const id = positionals[0];
3260
+ if (!id) {
3261
+ ctx.out.fail(
3262
+ "webhooks delete requires an endpoint id, e.g. hogsend webhooks delete we_123"
3263
+ );
3264
+ }
3265
+ if (!ctx.json) ctx.out.intro(`${badge5} delete`);
3266
+ const res = await fetchOrFail2(
3267
+ ctx,
3268
+ "Deleting webhook",
3269
+ () => ctx.http.del(
3270
+ `/v1/admin/webhooks/${encodeURIComponent(id)}`
3271
+ )
3272
+ );
3273
+ if (ctx.json) {
3274
+ ctx.out.json(res);
3275
+ return;
3276
+ }
3277
+ ctx.out.outro(`${color.green("Deleted")} ${id}`);
3278
+ }
3279
+ async function runRotate(ctx, argv) {
3280
+ const { values, positionals } = parseArgs14({
3281
+ args: argv,
3282
+ allowPositionals: true,
3283
+ options: { help: { type: "boolean", short: "h", default: false } }
3284
+ });
3285
+ if (values.help) {
3286
+ ctx.out.log(usage14);
3287
+ return;
3288
+ }
3289
+ const id = positionals[0];
3290
+ if (!id) {
3291
+ ctx.out.fail(
3292
+ "webhooks rotate-secret requires an endpoint id, e.g. hogsend webhooks rotate-secret we_123"
3293
+ );
3294
+ }
3295
+ if (!ctx.json) ctx.out.intro(`${badge5} rotate-secret`);
3296
+ const res = await fetchOrFail2(
3297
+ ctx,
3298
+ "Rotating signing secret",
3299
+ () => ctx.http.post(
3300
+ `/v1/admin/webhooks/${encodeURIComponent(id)}/rotate-secret`,
3301
+ {}
3302
+ )
3303
+ );
3304
+ if (ctx.json) {
3305
+ ctx.out.json(res);
3306
+ return;
3307
+ }
3308
+ ctx.out.kv({ id: res.id, secretPrefix: res.secretPrefix }, "Secret rotated");
3309
+ printSecretOnce(ctx, res.secret);
3310
+ ctx.out.outro(
3311
+ `${color.green("Rotated")} \u2014 the old secret is now invalid. Update every subscriber.`
3312
+ );
3313
+ }
3314
+ async function runTest(ctx, argv) {
3315
+ const { values, positionals } = parseArgs14({
3316
+ args: argv,
3317
+ allowPositionals: true,
3318
+ options: { help: { type: "boolean", short: "h", default: false } }
3319
+ });
3320
+ if (values.help) {
3321
+ ctx.out.log(usage14);
3322
+ return;
3323
+ }
3324
+ const id = positionals[0];
3325
+ if (!id) {
3326
+ ctx.out.fail(
3327
+ "webhooks test requires an endpoint id, e.g. hogsend webhooks test we_123"
3328
+ );
3329
+ }
3330
+ if (!ctx.json) ctx.out.intro(`${badge5} test`);
3331
+ const res = await fetchOrFail2(
3332
+ ctx,
3333
+ "Enqueuing test delivery",
3334
+ () => ctx.http.post(
3335
+ `/v1/admin/webhooks/${encodeURIComponent(id)}/test`,
3336
+ {}
3337
+ )
3338
+ );
3339
+ if (ctx.json) {
3340
+ ctx.out.json(res);
3341
+ return;
3342
+ }
3343
+ ctx.out.outro(
3344
+ `${color.green("Enqueued")} a ${color.cyan(res.eventType)} delivery to ${id}.`
3345
+ );
3346
+ }
3347
+ async function run14(ctx) {
3348
+ const sub = ctx.argv[0];
3349
+ switch (sub) {
3350
+ case "list":
3351
+ return runList4(ctx, ctx.argv.slice(1));
3352
+ case "get":
3353
+ return runGet3(ctx, ctx.argv.slice(1));
3354
+ case "create":
3355
+ return runCreate(ctx, ctx.argv.slice(1));
3356
+ case "update":
3357
+ return runUpdate(ctx, ctx.argv.slice(1));
3358
+ case "delete":
3359
+ return runDelete(ctx, ctx.argv.slice(1));
3360
+ case "rotate-secret":
3361
+ return runRotate(ctx, ctx.argv.slice(1));
3362
+ case "test":
3363
+ return runTest(ctx, ctx.argv.slice(1));
3364
+ case void 0:
3365
+ ctx.out.fail(
3366
+ "webhooks requires a subcommand: list | get | create | update | delete | rotate-secret | test (see hogsend webhooks --help)"
3367
+ );
3368
+ break;
3369
+ default:
3370
+ ctx.out.fail(
3371
+ `unknown webhooks subcommand "${sub}" \u2014 expected one of list | get | create | update | delete | rotate-secret | test`
3372
+ );
3373
+ }
3374
+ }
3375
+ var webhooksCommand = {
3376
+ name: "webhooks",
3377
+ summary: "Manage outbound webhook endpoints (create, rotate, test)",
3378
+ usage: usage14,
3379
+ run: run14
3380
+ };
3381
+
2947
3382
  // src/commands/index.ts
2948
3383
  var commands = [
2949
3384
  doctorCommand,
@@ -2953,6 +3388,7 @@ var commands = [
2953
3388
  eventsCommand,
2954
3389
  emailsCommand,
2955
3390
  campaignsCommand,
3391
+ webhooksCommand,
2956
3392
  studioCommand,
2957
3393
  setupCommand,
2958
3394
  skillsCommand,
@@ -2964,10 +3400,10 @@ var commands = [
2964
3400
  // src/lib/config.ts
2965
3401
  import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
2966
3402
  import { join as join8 } from "path";
2967
- import { parseArgs as parseArgs14 } from "util";
3403
+ import { parseArgs as parseArgs15 } from "util";
2968
3404
  var DEFAULT_BASE_URL = "http://localhost:3002";
2969
3405
  function parseGlobalFlags(argv) {
2970
- const { values, tokens } = parseArgs14({
3406
+ const { values, tokens } = parseArgs15({
2971
3407
  args: argv,
2972
3408
  allowPositionals: true,
2973
3409
  strict: false,