@teever/ez-hook-effect 0.5.1 → 0.5.19

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/index.js CHANGED
@@ -1,615 +1,8 @@
1
- var __defProp = Object.defineProperty;
2
- var __export = (target, all) => {
3
- for (var name in all)
4
- __defProp(target, name, {
5
- get: all[name],
6
- enumerable: true,
7
- configurable: true,
8
- set: (newValue) => all[name] = () => newValue
9
- });
10
- };
11
-
12
- // src/errors/WebhookError.ts
13
- import { Data } from "effect";
14
-
15
- class WebhookError extends Data.TaggedError("WebhookError") {
16
- }
17
- var extractConstraint = (message) => {
18
- if (message.includes("at most") || message.includes("maximum"))
19
- return "MaxLength";
20
- if (message.includes("at least") || message.includes("minimum"))
21
- return "MinLength";
22
- if (message.includes("required") || message.includes("missing"))
23
- return "Required";
24
- if (message.includes("expected") && message.includes("to be"))
25
- return "Type";
26
- if (message.includes("URL") || message.includes("url"))
27
- return "URL";
28
- if (message.includes("integer") || message.includes("number"))
29
- return "Number";
30
- return;
31
- };
32
- var truncateValue = (value, maxLength = 100) => {
33
- if (typeof value === "string" && value.length > maxLength) {
34
- return `${value.slice(0, maxLength)}... (${value.length} chars)`;
35
- }
36
- if (Array.isArray(value) && value.length > 5) {
37
- return `[Array with ${value.length} items]`;
38
- }
39
- if (typeof value === "object" && value !== null) {
40
- const keys = Object.keys(value);
41
- if (keys.length > 5) {
42
- return `{Object with ${keys.length} keys}`;
43
- }
44
- }
45
- return value;
46
- };
47
- var pathToString = (path) => {
48
- if (path.length === 0)
49
- return "$";
50
- return path.reduce((acc, segment) => {
51
- if (typeof segment === "number") {
52
- return `${acc}[${segment}]`;
53
- }
54
- return `${acc}.${String(segment)}`;
55
- }, "$");
56
- };
57
- var toArray = (value) => Array.isArray(value) ? value : [value];
58
- var getIssueProp = (issue, key) => issue[key];
59
- var extractIssuesFromParseIssue = (issue, currentPath = []) => {
60
- const issues = [];
61
- switch (issue._tag) {
62
- case "InvalidType": {
63
- const ast = getIssueProp(issue, "ast");
64
- const message = getIssueProp(issue, "message") ?? `Expected ${ast?._tag}`;
65
- issues.push({
66
- path: pathToString(currentPath),
67
- message,
68
- constraint: extractConstraint(message) ?? "Type",
69
- actual: truncateValue(getIssueProp(issue, "actual"))
70
- });
71
- break;
72
- }
73
- case "Forbidden": {
74
- issues.push({
75
- path: pathToString(currentPath),
76
- message: getIssueProp(issue, "message") ?? "Value is forbidden",
77
- constraint: "Forbidden"
78
- });
79
- break;
80
- }
81
- case "MissingKey": {
82
- issues.push({
83
- path: pathToString(currentPath),
84
- message: "Required field is missing",
85
- constraint: "Required"
86
- });
87
- break;
88
- }
89
- case "UnexpectedKey": {
90
- issues.push({
91
- path: pathToString(currentPath),
92
- message: getIssueProp(issue, "message") ?? "Unexpected field",
93
- constraint: "Unexpected",
94
- actual: truncateValue(getIssueProp(issue, "actual"))
95
- });
96
- break;
97
- }
98
- case "Pointer": {
99
- const pathSegments = getIssueProp(issue, "path") ?? [];
100
- const newPath = [...currentPath, ...pathSegments];
101
- const nested = getIssueProp(issue, "issue");
102
- if (nested)
103
- issues.push(...extractIssuesFromParseIssue(nested, newPath));
104
- break;
105
- }
106
- case "Composite": {
107
- const rawIssues = getIssueProp(issue, "issues");
108
- const subIssues = rawIssues ? toArray(rawIssues) : [];
109
- for (const subIssue of subIssues) {
110
- issues.push(...extractIssuesFromParseIssue(subIssue, currentPath));
111
- }
112
- break;
113
- }
114
- case "Filter":
115
- case "Encoding": {
116
- const nested = getIssueProp(issue, "issue");
117
- if (nested)
118
- issues.push(...extractIssuesFromParseIssue(nested, currentPath));
119
- break;
120
- }
121
- case "InvalidValue":
122
- case "OneOf":
123
- case "AnyOf": {
124
- issues.push({
125
- path: pathToString(currentPath),
126
- message: getIssueProp(issue, "message") ?? "Invalid value",
127
- constraint: "Value",
128
- actual: truncateValue(getIssueProp(issue, "actual"))
129
- });
130
- break;
131
- }
132
- default: {
133
- const message = getIssueProp(issue, "message") ?? "Invalid value";
134
- const constraint = extractConstraint(message);
135
- issues.push({
136
- path: pathToString(currentPath),
137
- message,
138
- ...constraint !== undefined && { constraint },
139
- ...getIssueProp(issue, "actual") !== undefined && {
140
- actual: truncateValue(getIssueProp(issue, "actual"))
141
- }
142
- });
143
- break;
144
- }
145
- }
146
- return issues;
147
- };
148
- var parseErrorToIssues = (error) => {
149
- return extractIssuesFromParseIssue(error.issue);
150
- };
151
- var makeIssue = (field, message, options) => {
152
- const constraint = options?.constraint ?? extractConstraint(message);
153
- return {
154
- path: field.startsWith("$") ? field : `$.${field}`,
155
- message,
156
- ...constraint !== undefined && { constraint },
157
- ...options?.expected !== undefined && { expected: options.expected },
158
- ...options?.actual !== undefined && {
159
- actual: truncateValue(options.actual)
160
- }
161
- };
162
- };
163
-
164
- class ValidationError extends Data.TaggedError("ValidationError") {
165
- format(options) {
166
- const maxIssues = options?.maxIssues ?? 10;
167
- const displayIssues = this.issues.slice(0, maxIssues);
168
- const lines = displayIssues.map((issue) => {
169
- let line = ` - ${issue.path}: ${issue.message}`;
170
- if (issue.constraint) {
171
- line += ` [${issue.constraint}]`;
172
- }
173
- return line;
174
- });
175
- if (this.issues.length > maxIssues) {
176
- lines.push(` ... and ${this.issues.length - maxIssues} more issues`);
177
- }
178
- return `${this.message}
179
- ${lines.join(`
180
- `)}`;
181
- }
182
- static fromParseError(parseError, context) {
183
- const issues = parseErrorToIssues(parseError);
184
- const firstIssue = issues[0];
185
- const message = issues.length === 1 && firstIssue ? firstIssue.message : `Validation failed (${issues.length} issues)`;
186
- return new ValidationError({
187
- message,
188
- issues,
189
- ...context?.field !== undefined && { field: context.field },
190
- ...context?.value !== undefined && { value: context.value }
191
- });
192
- }
193
- static fromIssue(field, message, options) {
194
- const issue = makeIssue(field, message, options);
195
- return new ValidationError({
196
- message,
197
- issues: [issue],
198
- field,
199
- value: options?.actual
200
- });
201
- }
202
- }
203
-
204
- class NetworkError extends Data.TaggedError("NetworkError") {
205
- }
206
-
207
- class RateLimitError extends Data.TaggedError("RateLimitError") {
208
- }
209
-
210
- class ConfigError extends Data.TaggedError("ConfigError") {
211
- }
212
-
213
- class FileError extends Data.TaggedError("FileError") {
214
- }
215
-
216
- class HttpError extends Data.TaggedError("HttpError") {
217
- }
218
- var formatWebhookError = (error, options) => {
219
- switch (error._tag) {
220
- case "ValidationError":
221
- return error.format(options);
222
- case "RateLimitError":
223
- return `${error.message} (retryAfter=${error.retryAfter}ms)`;
224
- case "HttpError": {
225
- const status = error.response?.status;
226
- return status ? `${error.message} (status=${status})` : error.message;
227
- }
228
- default:
229
- return error.message;
230
- }
231
- };
232
- var webhookErrorToLogObject = (error) => {
233
- switch (error._tag) {
234
- case "ValidationError":
235
- return {
236
- tag: error._tag,
237
- message: error.message,
238
- issues: error.issues,
239
- field: error.field,
240
- value: error.value
241
- };
242
- case "RateLimitError":
243
- return {
244
- tag: error._tag,
245
- message: error.message,
246
- retryAfter: error.retryAfter,
247
- limit: error.limit,
248
- remaining: error.remaining,
249
- reset: error.reset
250
- };
251
- case "HttpError":
252
- return {
253
- tag: error._tag,
254
- message: error.message,
255
- request: error.request,
256
- response: error.response
257
- };
258
- case "NetworkError":
259
- return {
260
- tag: error._tag,
261
- message: error.message,
262
- statusCode: error.statusCode,
263
- response: error.response
264
- };
265
- case "ConfigError":
266
- return {
267
- tag: error._tag,
268
- message: error.message,
269
- parameter: error.parameter
270
- };
271
- case "FileError":
272
- return {
273
- tag: error._tag,
274
- message: error.message,
275
- filename: error.filename,
276
- size: error.size,
277
- maxSize: error.maxSize
278
- };
279
- default:
280
- return {
281
- tag: error._tag,
282
- message: error.message,
283
- cause: error.cause
284
- };
285
- }
286
- };
287
- // src/layers/Config.ts
288
- import { Effect, Layer, ServiceMap } from "effect";
289
-
290
- // src/schemas/Discord.ts
291
- import { Schema } from "effect";
292
- var DISCORD_WEBHOOK_REGEX = /^https:\/\/(?:(?:canary|ptb)\.)?discord(?:app)?\.com\/api(?:\/v\d+)?\/webhooks\/(?<id>\d+)\/(?<token>[\w-]+)\/?$/;
293
- var DiscordWebhookUrl = Schema.String.check(Schema.isPattern(DISCORD_WEBHOOK_REGEX, {
294
- message: "Invalid Discord webhook URL"
295
- }));
296
-
297
- // src/layers/Config.ts
298
- class Config extends ServiceMap.Service()("Config") {
299
- }
300
- var validateWebhookUrl = (url) => {
301
- if (!url || url.trim() === "") {
302
- return Effect.fail(new ConfigError({
303
- message: `Webhook URL is empty. Please provide a valid Discord webhook URL.
304
- ` + `Format: https://discord.com/api/webhooks/{webhook_id}/{webhook_token}
305
- ` + "You can create a webhook in Discord: Server Settings > Integrations > Webhooks",
306
- parameter: "webhookUrl"
307
- }));
308
- }
309
- if (!url.startsWith("https://")) {
310
- return Effect.fail(new ConfigError({
311
- message: `Webhook URL must use HTTPS protocol.
312
- ` + `Received: ${url.substring(0, 50)}${url.length > 50 ? "..." : ""}
313
- ` + "Expected format: https://discord.com/api/webhooks/{webhook_id}/{webhook_token}",
314
- parameter: "webhookUrl"
315
- }));
316
- }
317
- if (!DISCORD_WEBHOOK_REGEX.test(url)) {
318
- const isDiscordDomain = url.includes("discord.com") || url.includes("discordapp.com");
319
- const hasWebhooksPath = url.includes("/webhooks/");
320
- let hint = "";
321
- if (!isDiscordDomain) {
322
- hint = "URL must be from discord.com or discordapp.com domain.";
323
- } else if (!hasWebhooksPath) {
324
- hint = "URL must include /api/webhooks/ path.";
325
- } else {
326
- hint = "URL must include both webhook ID (numeric) and token after /webhooks/.";
327
- }
328
- return Effect.fail(new ConfigError({
329
- message: `Invalid Discord webhook URL format.
330
- ` + `${hint}
331
- ` + `Received: ${url.substring(0, 80)}${url.length > 80 ? "..." : ""}
332
- ` + `Expected format: https://discord.com/api/webhooks/{webhook_id}/{webhook_token}`,
333
- parameter: "webhookUrl"
334
- }));
335
- }
336
- return Effect.succeed(url);
337
- };
338
- var validateRetryOptions = (options) => {
339
- const errors = [];
340
- if (options?.maxRetries !== undefined) {
341
- if (!Number.isInteger(options.maxRetries) || options.maxRetries < 0) {
342
- errors.push(`maxRetries must be a non-negative integer (received: ${options.maxRetries})`);
343
- }
344
- if (options.maxRetries > 10) {
345
- errors.push(`maxRetries should not exceed 10 to avoid excessive retries (received: ${options.maxRetries})`);
346
- }
347
- }
348
- if (options?.baseDelayMs !== undefined) {
349
- if (!Number.isInteger(options.baseDelayMs) || options.baseDelayMs < 0) {
350
- errors.push(`baseDelayMs must be a non-negative integer (received: ${options.baseDelayMs})`);
351
- }
352
- if (options.baseDelayMs > 60000) {
353
- errors.push(`baseDelayMs should not exceed 60000ms (received: ${options.baseDelayMs})`);
354
- }
355
- }
356
- if (options?.maxDelayMs !== undefined) {
357
- if (!Number.isInteger(options.maxDelayMs) || options.maxDelayMs < 0) {
358
- errors.push(`maxDelayMs must be a non-negative integer (received: ${options.maxDelayMs})`);
359
- }
360
- if (options.baseDelayMs !== undefined && options.maxDelayMs < options.baseDelayMs) {
361
- errors.push(`maxDelayMs (${options.maxDelayMs}) must be >= baseDelayMs (${options.baseDelayMs})`);
362
- }
363
- }
364
- if (errors.length > 0) {
365
- return Effect.fail(new ConfigError({
366
- message: `Invalid retry configuration:
367
- ${errors.map((e) => ` - ${e}`).join(`
368
- `)}`,
369
- parameter: "retryConfig"
370
- }));
371
- }
372
- return Effect.succeed(options);
373
- };
374
- var makeConfig = (webhookUrl, options) => Effect.gen(function* () {
375
- const validatedUrl = yield* validateWebhookUrl(webhookUrl);
376
- yield* validateRetryOptions(options);
377
- return {
378
- webhook: {
379
- webhookUrl: validatedUrl,
380
- maxRetries: options?.maxRetries ?? 3,
381
- baseDelayMs: options?.baseDelayMs ?? 1000,
382
- maxDelayMs: options?.maxDelayMs ?? 60000,
383
- enableJitter: options?.enableJitter ?? true
384
- }
385
- };
386
- });
387
- var makeConfigLayer = (webhookUrl, options) => Layer.effect(Config, makeConfig(webhookUrl, options));
388
- var ConfigFromEnv = Layer.effect(Config, Effect.gen(function* () {
389
- const rawWebhookUrl = yield* Effect.sync(() => process.env.DISCORD_WEBHOOK_URL ?? "");
390
- if (!rawWebhookUrl) {
391
- return yield* Effect.fail(new ConfigError({
392
- message: `DISCORD_WEBHOOK_URL environment variable not set.
393
- ` + `Please set DISCORD_WEBHOOK_URL in your environment or .env file.
394
- ` + "See .env.example for all available configuration options.",
395
- parameter: "DISCORD_WEBHOOK_URL"
396
- }));
397
- }
398
- const webhookUrl = yield* validateWebhookUrl(rawWebhookUrl);
399
- const maxRetries = yield* Effect.sync(() => {
400
- const v = process.env.WEBHOOK_MAX_RETRIES;
401
- return v ? Number.parseInt(v, 10) : 3;
402
- });
403
- const baseDelayMs = yield* Effect.sync(() => {
404
- const v = process.env.WEBHOOK_BASE_DELAY_MS;
405
- return v ? Number.parseInt(v, 10) : 1000;
406
- });
407
- const maxDelayMs = yield* Effect.sync(() => {
408
- const v = process.env.WEBHOOK_MAX_DELAY_MS;
409
- return v ? Number.parseInt(v, 10) : 60000;
410
- });
411
- const enableJitter = yield* Effect.sync(() => {
412
- const v = process.env.WEBHOOK_ENABLE_JITTER;
413
- return v ? v === "true" : true;
414
- });
415
- yield* validateRetryOptions({ maxRetries, baseDelayMs, maxDelayMs });
416
- return {
417
- webhook: {
418
- webhookUrl,
419
- maxRetries,
420
- baseDelayMs,
421
- maxDelayMs,
422
- enableJitter
423
- }
424
- };
425
- }));
426
- var parseWebhookUrl = (url) => Effect.gen(function* () {
427
- const match = url.match(DISCORD_WEBHOOK_REGEX);
428
- if (!match?.groups?.id || !match.groups.token) {
429
- return yield* Effect.fail(new ConfigError({
430
- message: "Invalid webhook URL format",
431
- parameter: "webhookUrl"
432
- }));
433
- }
434
- const { id, token } = match.groups;
435
- return { id, token };
436
- });
437
- // src/layers/HttpClient.ts
438
- import { Duration, Effect as Effect2, Layer as Layer2, Schedule, ServiceMap as ServiceMap2 } from "effect";
439
- class HttpClient extends ServiceMap2.Service()("HttpClient") {
440
- }
441
- var parseRateLimitHeaders = (headers) => {
442
- const retryAfterHeader = headers["retry-after"];
443
- const rateLimitRemaining = headers["x-ratelimit-remaining"];
444
- if (!retryAfterHeader && rateLimitRemaining !== "0") {
445
- return null;
446
- }
447
- let retryAfter = 0;
448
- if (retryAfterHeader) {
449
- const parsed = Number.parseFloat(retryAfterHeader);
450
- if (!Number.isNaN(parsed)) {
451
- retryAfter = Math.ceil(parsed * 1000);
452
- }
453
- }
454
- const limit = headers["x-ratelimit-limit"];
455
- const remaining = headers["x-ratelimit-remaining"];
456
- const resetHeader = headers["x-ratelimit-reset"];
457
- const resetAfterHeader = headers["x-ratelimit-reset-after"];
458
- const globalHeader = headers["x-ratelimit-global"];
459
- let reset;
460
- if (resetHeader) {
461
- const resetTimestamp = Number.parseFloat(resetHeader);
462
- if (!Number.isNaN(resetTimestamp)) {
463
- reset = new Date(resetTimestamp * 1000);
464
- }
465
- } else if (resetAfterHeader) {
466
- const resetAfter = Number.parseFloat(resetAfterHeader);
467
- if (!Number.isNaN(resetAfter)) {
468
- reset = new Date(Date.now() + resetAfter * 1000);
469
- }
470
- }
471
- if (retryAfter === 0 && reset) {
472
- retryAfter = Math.max(0, reset.getTime() - Date.now());
473
- }
474
- if (retryAfter === 0) {
475
- retryAfter = 1000;
476
- }
477
- return {
478
- retryAfter,
479
- ...limit && { limit: Number.parseInt(limit, 10) },
480
- ...remaining && { remaining: Number.parseInt(remaining, 10) },
481
- ...reset && { reset },
482
- global: globalHeader === "true"
483
- };
484
- };
485
- var defaultRetryConfig = {
486
- maxRetries: 3,
487
- baseDelay: Duration.seconds(1),
488
- maxDelay: Duration.seconds(60),
489
- jitter: true
490
- };
491
- var createRetrySchedule = (config) => {
492
- const capDelay = (delay) => Duration.isLessThanOrEqualTo(delay, config.maxDelay) ? delay : config.maxDelay;
493
- let policy = Schedule.exponential(config.baseDelay, 2);
494
- if (config.jitter) {
495
- policy = policy.pipe(Schedule.modifyDelay((_, delay) => Effect2.sync(() => {
496
- const millis = Duration.toMillis(delay);
497
- return Duration.millis(millis + Math.random() * millis);
498
- })));
499
- }
500
- policy = policy.pipe(Schedule.modifyDelay((_, delay) => Effect2.succeed(capDelay(delay))));
501
- const capped = Schedule.both(policy, Schedule.recurs(config.maxRetries));
502
- return capped.pipe(Schedule.map(([delay]) => Effect2.succeed(delay)));
503
- };
504
- var FetchHttpClient = (retryConfig = defaultRetryConfig) => ({
505
- retryConfig,
506
- request: (request) => Effect2.gen(function* () {
507
- const controller = new AbortController;
508
- const timeout = request.timeout ? setTimeout(() => controller.abort(), Duration.toMillis(request.timeout)) : undefined;
509
- const requestHeaders = new Headers({
510
- "Content-Type": "application/json"
511
- });
512
- for (const [key, value] of Object.entries(request.headers ?? {})) {
513
- requestHeaders.set(key, value);
514
- }
515
- const response = yield* Effect2.tryPromise({
516
- try: () => fetch(request.url, {
517
- method: request.method,
518
- headers: requestHeaders,
519
- body: request.body ? JSON.stringify(request.body) : undefined,
520
- signal: controller.signal
521
- }),
522
- catch: (error) => new NetworkError({
523
- message: `Network request failed: ${error instanceof Error ? error.message : String(error)}`,
524
- statusCode: 0
525
- })
526
- });
527
- if (timeout)
528
- clearTimeout(timeout);
529
- const rawTextOrFn = yield* Effect2.tryPromise({
530
- try: () => response.text(),
531
- catch: () => new NetworkError({ message: "Failed to read response body" })
532
- });
533
- const text = typeof rawTextOrFn === "function" ? yield* Effect2.tryPromise({
534
- try: () => rawTextOrFn(),
535
- catch: () => new NetworkError({ message: "Failed to read response body" })
536
- }) : String(rawTextOrFn ?? "");
537
- const headers = {};
538
- response.headers.forEach((value, key) => {
539
- headers[key.toLowerCase()] = value;
540
- });
541
- let body = text;
542
- if (headers["content-type"]?.includes("application/json") && text) {
543
- try {
544
- body = JSON.parse(text);
545
- } catch {}
546
- }
547
- const httpResponse = {
548
- status: response.status,
549
- statusText: response.statusText,
550
- headers,
551
- body,
552
- text
553
- };
554
- if (!response.ok) {
555
- if (response.status === 429) {
556
- const rateLimitInfo = parseRateLimitHeaders(headers);
557
- return yield* Effect2.fail(new RateLimitError({
558
- message: `Rate limited: retry after ${rateLimitInfo?.retryAfter ?? 1000}ms`,
559
- retryAfter: rateLimitInfo?.retryAfter ?? 1000,
560
- ...rateLimitInfo?.limit !== undefined && {
561
- limit: rateLimitInfo.limit
562
- },
563
- ...rateLimitInfo?.remaining !== undefined && {
564
- remaining: rateLimitInfo.remaining
565
- },
566
- ...rateLimitInfo?.reset !== undefined && {
567
- reset: rateLimitInfo.reset
568
- }
569
- }));
570
- }
571
- return yield* Effect2.fail(new HttpError({
572
- message: `HTTP ${response.status}: ${response.statusText}`,
573
- request: {
574
- method: request.method,
575
- url: request.url
576
- },
577
- response: {
578
- status: response.status,
579
- statusText: response.statusText,
580
- headers,
581
- body
582
- }
583
- }));
584
- }
585
- return httpResponse;
586
- })
587
- });
588
- var HttpClientLive = Layer2.succeed(HttpClient, FetchHttpClient());
589
- var makeHttpClientLayer = (retryConfig) => Layer2.succeed(HttpClient, FetchHttpClient({ ...defaultRetryConfig, ...retryConfig }));
590
- var makeTestHttpClient = (resolver, retryConfig = defaultRetryConfig) => Layer2.succeed(HttpClient, {
591
- request: resolver,
592
- retryConfig
593
- });
594
- // src/layers/Default.ts
595
- import { Layer as Layer4 } from "effect";
596
-
597
- // src/services/WebhookService.ts
598
- import {
599
- Duration as Duration2,
600
- Effect as Effect3,
601
- Layer as Layer3,
602
- pipe,
603
- Schedule as Schedule2,
604
- Schema as Schema6,
605
- ServiceMap as ServiceMap3
606
- } from "effect";
607
-
608
1
  // src/schemas/Common.ts
609
- import { Schema as Schema2 } from "effect";
610
- var maxLength = (max) => Schema2.String.check(Schema2.isMaxLength(max));
611
- var positiveInt = Schema2.Number.check(Schema2.isInt(), Schema2.isGreaterThan(0));
612
- var UrlString = Schema2.String.pipe(Schema2.check(Schema2.makeFilter((value) => {
2
+ import { Schema } from "effect";
3
+ var maxLength = (max) => Schema.String.check(Schema.isMaxLength(max));
4
+ var positiveInt = Schema.Number.check(Schema.isInt(), Schema.isGreaterThan(0));
5
+ var UrlString = Schema.String.pipe(Schema.check(Schema.makeFilter((value) => {
613
6
  try {
614
7
  new URL(value);
615
8
  return true;
@@ -617,12 +10,19 @@ var UrlString = Schema2.String.pipe(Schema2.check(Schema2.makeFilter((value) =>
617
10
  return "Invalid URL format";
618
11
  }
619
12
  })));
620
- var ISO8601Timestamp = Schema2.String.pipe(Schema2.check(Schema2.makeFilter((value) => {
13
+ var ISO8601Timestamp = Schema.String.pipe(Schema.check(Schema.makeFilter((value) => {
621
14
  const iso8601Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/;
622
15
  return iso8601Regex.test(value) ? true : "Invalid ISO8601 timestamp format";
623
16
  })));
624
- var ColorCode = Schema2.Number.check(Schema2.isInt(), Schema2.isGreaterThanOrEqualTo(0), Schema2.isLessThanOrEqualTo(16777215));
625
- var Uint8ArraySchema = Schema2.instanceOf(Uint8Array);
17
+ var ColorCode = Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(16777215));
18
+ var Uint8ArraySchema = Schema.instanceOf(Uint8Array);
19
+
20
+ // src/schemas/Discord.ts
21
+ import { Schema as Schema2 } from "effect";
22
+ var DISCORD_WEBHOOK_REGEX = /^https:\/\/(?:(?:canary|ptb)\.)?discord(?:app)?\.com\/api(?:\/v\d+)?\/webhooks\/(?<id>\d+)\/(?<token>[\w-]+)\/?$/;
23
+ var DiscordWebhookUrl = Schema2.String.check(Schema2.isPattern(DISCORD_WEBHOOK_REGEX, {
24
+ message: "Invalid Discord webhook URL"
25
+ }));
626
26
 
627
27
  // src/schemas/Embed.ts
628
28
  import { Schema as Schema4 } from "effect";
@@ -768,354 +168,6 @@ class WebhookResponse extends Schema5.Class("WebhookResponse")({
768
168
  url: Schema5.optionalKey(UrlString)
769
169
  }) {
770
170
  }
771
-
772
- // src/utils/normalize.ts
773
- var stripUndefined = (input) => {
774
- if (Array.isArray(input)) {
775
- return input.map(stripUndefined);
776
- }
777
- if (input && typeof input === "object") {
778
- const proto = Object.getPrototypeOf(input);
779
- if (proto !== Object.prototype && proto !== null) {
780
- return input;
781
- }
782
- const entries = Object.entries(input).filter(([, value]) => value !== undefined).map(([key, value]) => [key, stripUndefined(value)]);
783
- return Object.fromEntries(entries);
784
- }
785
- return input;
786
- };
787
-
788
- // src/services/WebhookService.ts
789
- class WebhookService extends ServiceMap3.Service()("WebhookService") {
790
- }
791
- var makeWebhookService = Effect3.gen(function* () {
792
- const config = yield* Config;
793
- const httpClient = yield* HttpClient;
794
- const webhookUrl = config.webhook.webhookUrl;
795
- const retrySchedule = createRetrySchedule({
796
- maxRetries: config.webhook.maxRetries ?? httpClient.retryConfig.maxRetries,
797
- baseDelay: Duration2.millis(config.webhook.baseDelayMs ?? Duration2.toMillis(httpClient.retryConfig.baseDelay)),
798
- maxDelay: Duration2.millis(config.webhook.maxDelayMs ?? Duration2.toMillis(httpClient.retryConfig.maxDelay)),
799
- jitter: config.webhook.enableJitter ?? httpClient.retryConfig.jitter
800
- });
801
- const sendWebhookRaw = (webhook) => pipe(Schema6.decodeUnknownEffect(Webhook)(stripUndefined(webhook)), Effect3.mapError((e) => ValidationError.fromParseError(e, { field: "webhook", value: webhook })), Effect3.flatMap((validatedWebhook) => httpClient.request({
802
- method: "POST",
803
- url: webhookUrl,
804
- body: validatedWebhook
805
- }).pipe(Effect3.retry(Schedule2.while(retrySchedule, (meta) => Effect3.succeed(meta.input instanceof NetworkError || meta.input instanceof RateLimitError || meta.input instanceof HttpError && (meta.input.response?.status ?? 0) >= 500))))), Effect3.map((response) => ({
806
- status: response.status,
807
- ok: response.status >= 200 && response.status < 300,
808
- headers: response.headers,
809
- body: response.body,
810
- text: response.text
811
- })));
812
- const sendWebhook = (webhook) => pipe(sendWebhookRaw(webhook), Effect3.flatMap((response) => response.status === 204 && response.text === "" ? Effect3.void : Effect3.fail(new WebhookError({
813
- message: `Unexpected webhook response: ${response.status}`,
814
- cause: response
815
- }))));
816
- const modifyWebhook = (params) => pipe(Schema6.decodeUnknownEffect(WebhookParameter)(stripUndefined(params)), Effect3.mapError((e) => ValidationError.fromParseError(e, { field: "params", value: params })), Effect3.flatMap((validatedParams) => httpClient.request({
817
- method: "PATCH",
818
- url: webhookUrl,
819
- body: validatedParams
820
- })), Effect3.flatMap((response) => Schema6.decodeUnknownEffect(WebhookResponse)(response.body).pipe(Effect3.mapError(() => new WebhookError({
821
- message: "Invalid webhook response",
822
- cause: response.body
823
- })))));
824
- const getWebhook = () => pipe(httpClient.request({
825
- method: "GET",
826
- url: webhookUrl
827
- }), Effect3.flatMap((response) => Schema6.decodeUnknownEffect(WebhookResponse)(response.body).pipe(Effect3.mapError(() => new WebhookError({
828
- message: "Invalid webhook response",
829
- cause: response.body
830
- })))));
831
- const validateWebhook = () => pipe(httpClient.request({
832
- method: "GET",
833
- url: webhookUrl
834
- }), Effect3.as(true), Effect3.catch(() => Effect3.succeed(false)));
835
- const deleteWebhook = () => pipe(httpClient.request({
836
- method: "DELETE",
837
- url: webhookUrl
838
- }), Effect3.map((response) => response.status === 204));
839
- return {
840
- sendWebhook,
841
- sendWebhookRaw,
842
- modifyWebhook,
843
- getWebhook,
844
- deleteWebhook,
845
- validateWebhook
846
- };
847
- });
848
- var WebhookServiceLive = Layer3.effect(WebhookService, makeWebhookService);
849
- var sendWebhook = (webhook) => WebhookService.use((service) => service.sendWebhook(webhook));
850
- var sendWebhookRaw = (webhook) => WebhookService.use((service) => service.sendWebhookRaw(webhook));
851
- var modifyWebhook = (params) => WebhookService.use((service) => service.modifyWebhook(params));
852
- var getWebhook = () => WebhookService.use((service) => service.getWebhook());
853
- var deleteWebhook = () => WebhookService.use((service) => service.deleteWebhook());
854
- var validateWebhook = () => WebhookService.use((service) => service.validateWebhook());
855
-
856
- // src/layers/Default.ts
857
- var makeDefaultLayer = (url, options) => WebhookServiceLive.pipe(Layer4.provide(makeConfigLayer(url, options)), Layer4.provide(HttpClientLive));
858
- // src/pipes/Embed.ts
859
- var exports_Embed = {};
860
- __export(exports_Embed, {
861
- setVideo: () => setVideo,
862
- setURL: () => setURL,
863
- setTitle: () => setTitle,
864
- setTimestamp: () => setTimestamp,
865
- setThumbnail: () => setThumbnail,
866
- setProvider: () => setProvider,
867
- setImage: () => setImage,
868
- setFooter: () => setFooter,
869
- setFields: () => setFields,
870
- setDescription: () => setDescription,
871
- setColor: () => setColor,
872
- setAuthor: () => setAuthor,
873
- make: () => make,
874
- buildDirect: () => buildDirect,
875
- build: () => build,
876
- addFieldImpl: () => addFieldImpl,
877
- addFieldDirect: () => addFieldDirect,
878
- addField: () => addField
879
- });
880
- import { Effect as Effect4, pipe as pipe2, Schema as Schema7 } from "effect";
881
- var createDraft = () => ({});
882
- var setTitleImpl = (title) => (d) => pipe2(Schema7.decodeUnknownEffect(maxLength(256))(title), Effect4.mapError(() => ValidationError.fromIssue("title", "Title must be 256 characters or less", {
883
- constraint: "MaxLength",
884
- expected: "256 characters or less",
885
- actual: title.length
886
- })), Effect4.as({ ...d, title }));
887
- var setURLImpl = (url) => (d) => pipe2(Schema7.decodeUnknownEffect(UrlString)(url), Effect4.mapError(() => ValidationError.fromIssue("url", "Invalid URL", {
888
- constraint: "URL",
889
- expected: "valid URL",
890
- actual: url
891
- })), Effect4.as({ ...d, url }));
892
- var setDescriptionImpl = (description) => (d) => pipe2(Schema7.decodeUnknownEffect(maxLength(4096))(description), Effect4.mapError(() => ValidationError.fromIssue("description", "Description must be 4096 characters or less", {
893
- constraint: "MaxLength",
894
- expected: "4096 characters or less",
895
- actual: description.length
896
- })), Effect4.as({ ...d, description }));
897
- var setTimestampImpl = (date) => (d) => Effect4.succeed({ ...d, timestamp: (date ?? new Date).toISOString() });
898
- var setColorImpl = (color) => (d) => {
899
- const colorValue = typeof color === "string" ? parseInt(color.replace("#", ""), 16) : color;
900
- return pipe2(Schema7.decodeUnknownEffect(ColorCode)(colorValue), Effect4.mapError(() => ValidationError.fromIssue("color", "Invalid color code (must be 0-16777215)", {
901
- constraint: "Range",
902
- expected: "0-16777215",
903
- actual: colorValue
904
- })), Effect4.as({ ...d, color: colorValue }));
905
- };
906
- var setFooterImpl = (footerOrText, icon_url, proxy_icon_url) => (d) => {
907
- const footer = typeof footerOrText === "string" ? {
908
- text: footerOrText,
909
- ...icon_url === undefined ? {} : { icon_url },
910
- ...proxy_icon_url === undefined ? {} : { proxy_icon_url }
911
- } : (() => {
912
- const { text, icon_url: icon_url2, proxy_icon_url: proxy_icon_url2 } = footerOrText;
913
- return {
914
- text,
915
- ...icon_url2 === undefined ? {} : { icon_url: icon_url2 },
916
- ...proxy_icon_url2 === undefined ? {} : { proxy_icon_url: proxy_icon_url2 }
917
- };
918
- })();
919
- return pipe2(Schema7.decodeUnknownEffect(Footer)(footer), Effect4.mapError((e) => ValidationError.fromParseError(e, { field: "footer", value: footer })), Effect4.as({ ...d, footer }));
920
- };
921
- var setImageImpl = (imageOrUrl, height, width) => (d) => {
922
- const image = typeof imageOrUrl === "string" ? {
923
- url: imageOrUrl,
924
- ...height === undefined ? {} : { height },
925
- ...width === undefined ? {} : { width }
926
- } : (() => {
927
- const { url, proxy_url, height: height2, width: width2 } = imageOrUrl;
928
- return {
929
- url,
930
- ...proxy_url === undefined ? {} : { proxy_url },
931
- ...height2 === undefined ? {} : { height: height2 },
932
- ...width2 === undefined ? {} : { width: width2 }
933
- };
934
- })();
935
- return pipe2(Schema7.decodeUnknownEffect(Image)(image), Effect4.mapError((e) => ValidationError.fromParseError(e, { field: "image", value: image })), Effect4.as({ ...d, image }));
936
- };
937
- var setThumbnailImpl = (thumbnailOrUrl, height, width) => (d) => {
938
- const thumbnail = typeof thumbnailOrUrl === "string" ? {
939
- url: thumbnailOrUrl,
940
- ...height === undefined ? {} : { height },
941
- ...width === undefined ? {} : { width }
942
- } : (() => {
943
- const { url, proxy_url, height: height2, width: width2 } = thumbnailOrUrl;
944
- return {
945
- url,
946
- ...proxy_url === undefined ? {} : { proxy_url },
947
- ...height2 === undefined ? {} : { height: height2 },
948
- ...width2 === undefined ? {} : { width: width2 }
949
- };
950
- })();
951
- return pipe2(Schema7.decodeUnknownEffect(Thumbnail)(thumbnail), Effect4.mapError((e) => ValidationError.fromParseError(e, {
952
- field: "thumbnail",
953
- value: thumbnail
954
- })), Effect4.as({ ...d, thumbnail }));
955
- };
956
- var setVideoImpl = (videoOrUrl, height, width) => (d) => {
957
- const video = typeof videoOrUrl === "string" ? {
958
- url: videoOrUrl,
959
- ...height === undefined ? {} : { height },
960
- ...width === undefined ? {} : { width }
961
- } : (() => {
962
- const { url, proxy_url, height: height2, width: width2 } = videoOrUrl;
963
- return {
964
- ...url === undefined ? {} : { url },
965
- ...proxy_url === undefined ? {} : { proxy_url },
966
- ...height2 === undefined ? {} : { height: height2 },
967
- ...width2 === undefined ? {} : { width: width2 }
968
- };
969
- })();
970
- return pipe2(Schema7.decodeUnknownEffect(Video)(video), Effect4.mapError((e) => ValidationError.fromParseError(e, { field: "video", value: video })), Effect4.as({ ...d, video }));
971
- };
972
- var setProviderImpl = (providerOrName, url) => (d) => {
973
- const provider = typeof providerOrName === "string" ? {
974
- name: providerOrName,
975
- ...url === undefined ? {} : { url }
976
- } : (() => {
977
- const { name, url: url2 } = providerOrName;
978
- return {
979
- ...name === undefined ? {} : { name },
980
- ...url2 === undefined ? {} : { url: url2 }
981
- };
982
- })();
983
- return pipe2(Schema7.decodeUnknownEffect(Provider)(provider), Effect4.mapError((e) => ValidationError.fromParseError(e, {
984
- field: "provider",
985
- value: provider
986
- })), Effect4.as({ ...d, provider }));
987
- };
988
- var setAuthorImpl = (authorOrName, url, icon_url) => (d) => {
989
- const author = typeof authorOrName === "string" ? {
990
- name: authorOrName,
991
- ...url === undefined ? {} : { url },
992
- ...icon_url === undefined ? {} : { icon_url }
993
- } : (() => {
994
- const { name, url: url2, icon_url: icon_url2, proxy_icon_url } = authorOrName;
995
- return {
996
- ...name === undefined ? {} : { name },
997
- ...url2 === undefined ? {} : { url: url2 },
998
- ...icon_url2 === undefined ? {} : { icon_url: icon_url2 },
999
- ...proxy_icon_url === undefined ? {} : { proxy_icon_url }
1000
- };
1001
- })();
1002
- return pipe2(Schema7.decodeUnknownEffect(Author)(author), Effect4.mapError((e) => ValidationError.fromParseError(e, { field: "author", value: author })), Effect4.as({ ...d, author }));
1003
- };
1004
- var addFieldImpl = (fieldOrName, value, inline = false) => (d) => {
1005
- const currentFields = d.fields || [];
1006
- if (currentFields.length >= 25) {
1007
- return Effect4.fail(ValidationError.fromIssue("fields", "Cannot add more than 25 fields", {
1008
- constraint: "MaxItems",
1009
- expected: "25 fields or less",
1010
- actual: currentFields.length + 1
1011
- }));
1012
- }
1013
- const field = typeof fieldOrName === "string" ? { name: fieldOrName, value: value ?? "", inline } : (() => {
1014
- const { name, value: value2, inline: inline2 } = fieldOrName;
1015
- return {
1016
- name,
1017
- value: value2,
1018
- ...inline2 === undefined ? {} : { inline: inline2 }
1019
- };
1020
- })();
1021
- return pipe2(Schema7.decodeUnknownEffect(Field)(field), Effect4.mapError((e) => ValidationError.fromParseError(e, { field: "fields", value: field })), Effect4.map(() => ({ ...d, fields: [...currentFields, field] })));
1022
- };
1023
- var setFieldsImpl = (fields) => (d) => {
1024
- if (fields.length > 25) {
1025
- return Effect4.fail(ValidationError.fromIssue("fields", "Cannot have more than 25 fields", {
1026
- constraint: "MaxItems",
1027
- expected: "25 fields or less",
1028
- actual: fields.length
1029
- }));
1030
- }
1031
- const normalized = fields.map((field) => {
1032
- const { name, value, inline } = field;
1033
- return {
1034
- name,
1035
- value,
1036
- ...inline === undefined ? {} : { inline }
1037
- };
1038
- });
1039
- return pipe2(Effect4.forEach(normalized, (f) => Schema7.decodeUnknownEffect(Field)(f)), Effect4.mapError((e) => ValidationError.fromParseError(e, { field: "fields", value: fields })), Effect4.map((decoded) => ({ ...d, fields: decoded })));
1040
- };
1041
- var buildImpl = (d) => pipe2(Schema7.decodeUnknownEffect(Embed)(stripUndefined({ ...d, type: "rich" })), Effect4.mapError((e) => ValidationError.fromParseError(e, { field: "embed", value: d })));
1042
- var buildDirect = buildImpl;
1043
- var addFieldDirect = addFieldImpl;
1044
- var make = Effect4.succeed(createDraft());
1045
- var chain = (f) => (self) => pipe2(self, Effect4.flatMap(f));
1046
- var setTitle = (title) => chain(setTitleImpl(title));
1047
- var setURL = (url) => chain(setURLImpl(url));
1048
- var setDescription = (description) => chain(setDescriptionImpl(description));
1049
- var setTimestamp = (date) => chain(setTimestampImpl(date));
1050
- var setColor = (color) => chain(setColorImpl(color));
1051
- var setFooter = (footer, icon_url, proxy_icon_url) => chain(setFooterImpl(footer, icon_url, proxy_icon_url));
1052
- var setImage = (image, height, width) => chain(setImageImpl(image, height, width));
1053
- var setThumbnail = (thumbnail, height, width) => chain(setThumbnailImpl(thumbnail, height, width));
1054
- var setVideo = (video, height, width) => chain(setVideoImpl(video, height, width));
1055
- var setProvider = (provider, url) => chain(setProviderImpl(provider, url));
1056
- var setAuthor = (author, url, icon_url) => chain(setAuthorImpl(author, url, icon_url));
1057
- var addField = (field, value, inline = false) => chain(addFieldImpl(field, value, inline));
1058
- var setFields = (fields) => chain(setFieldsImpl(fields));
1059
- var build = (self) => pipe2(self, Effect4.flatMap(buildImpl));
1060
- // src/pipes/Webhook.ts
1061
- var exports_Webhook = {};
1062
- __export(exports_Webhook, {
1063
- validate: () => validate,
1064
- setUsername: () => setUsername,
1065
- setTTS: () => setTTS,
1066
- setFile: () => setFile,
1067
- setContent: () => setContent,
1068
- setAvatarUrl: () => setAvatarUrl,
1069
- modifyParams: () => modifyParams,
1070
- make: () => make2,
1071
- buildDirect: () => buildDirect2,
1072
- build: () => build2,
1073
- addEmbedImpl: () => addEmbedImpl,
1074
- addEmbedDirect: () => addEmbedDirect,
1075
- addEmbed: () => addEmbed
1076
- });
1077
- import { Effect as Effect5, pipe as pipe3, Schema as Schema8 } from "effect";
1078
- var createDraft2 = () => ({});
1079
- var setUsernameImpl = (username) => (d) => pipe3(Schema8.decodeUnknownEffect(maxLength(80))(username), Effect5.mapError(() => ValidationError.fromIssue("username", "Username must be 80 characters or less", {
1080
- constraint: "MaxLength",
1081
- expected: "80 characters or less",
1082
- actual: username.length
1083
- })), Effect5.as({ ...d, username }));
1084
- var setAvatarUrlImpl = (avatar_url) => (d) => pipe3(Schema8.decodeUnknownEffect(UrlString)(avatar_url), Effect5.mapError(() => ValidationError.fromIssue("avatar_url", "Invalid avatar URL", {
1085
- constraint: "URL",
1086
- expected: "valid URL",
1087
- actual: avatar_url
1088
- })), Effect5.as({ ...d, avatar_url }));
1089
- var setContentImpl = (content) => (d) => pipe3(Schema8.decodeUnknownEffect(maxLength(2000))(content), Effect5.mapError(() => ValidationError.fromIssue("content", "Content must be 2000 characters or less", {
1090
- constraint: "MaxLength",
1091
- expected: "2000 characters or less",
1092
- actual: content.length
1093
- })), Effect5.as({ ...d, content }));
1094
- var setTTSImpl = (tts) => (d) => Effect5.succeed({ ...d, tts });
1095
- var addEmbedImpl = (embed) => (d) => {
1096
- const current = d.embeds || [];
1097
- const next = [...current, embed];
1098
- return pipe3(Schema8.decodeUnknownEffect(EmbedArray)(stripUndefined(next)), Effect5.mapError(() => ValidationError.fromIssue("embeds", "Cannot have more than 10 embeds", {
1099
- constraint: "MaxItems",
1100
- expected: "10 embeds or less",
1101
- actual: next.length
1102
- })), Effect5.map((decoded) => ({ ...d, embeds: decoded })));
1103
- };
1104
- var setFileImpl = (file) => (d) => Effect5.succeed({ ...d, file });
1105
- var buildImpl2 = (d) => pipe3(Schema8.decodeUnknownEffect(Webhook)(stripUndefined(d)), Effect5.mapError((e) => ValidationError.fromParseError(e, { field: "webhook", value: d })));
1106
- var buildDirect2 = buildImpl2;
1107
- var addEmbedDirect = addEmbedImpl;
1108
- var validate = (input) => pipe3(Schema8.decodeUnknownEffect(Webhook)(stripUndefined(input)), Effect5.mapError((e) => ValidationError.fromParseError(e, { field: "webhook", value: input })));
1109
- var modifyParams = (p) => pipe3(Schema8.decodeUnknownEffect(WebhookParameter)(stripUndefined(p)), Effect5.mapError((e) => ValidationError.fromParseError(e, { field: "params", value: p })));
1110
- var make2 = Effect5.succeed(createDraft2());
1111
- var chain2 = (f) => (self) => pipe3(self, Effect5.flatMap(f));
1112
- var setUsername = (username) => chain2(setUsernameImpl(username));
1113
- var setAvatarUrl = (url) => chain2(setAvatarUrlImpl(url));
1114
- var setContent = (content) => chain2(setContentImpl(content));
1115
- var setTTS = (tts) => chain2(setTTSImpl(tts));
1116
- var addEmbed = (embed) => chain2(addEmbedImpl(embed));
1117
- var setFile = (file) => chain2(setFileImpl(file));
1118
- var build2 = (self) => pipe3(self, Effect5.flatMap(buildImpl2));
1119
171
  export {
1120
172
  webhookErrorToLogObject,
1121
173
  validateWebhook,
@@ -1145,7 +197,7 @@ export {
1145
197
  WebhookParameter,
1146
198
  WebhookFile,
1147
199
  WebhookError,
1148
- exports_Webhook as Webhook,
200
+ Webhook2 as Webhook,
1149
201
  Video,
1150
202
  ValidationError,
1151
203
  UrlString,
@@ -1166,7 +218,7 @@ export {
1166
218
  Field,
1167
219
  FetchHttpClient,
1168
220
  EmbedArray,
1169
- exports_Embed as Embed,
221
+ Embed2 as Embed,
1170
222
  DiscordWebhookUrl,
1171
223
  DISCORD_WEBHOOK_REGEX,
1172
224
  ConfigFromEnv,
@@ -1177,5 +229,5 @@ export {
1177
229
  Attachment
1178
230
  };
1179
231
 
1180
- //# debugId=484262147C37081864756E2164756E21
232
+ //# debugId=77B5F8F8A236B81364756E2164756E21
1181
233
  //# sourceMappingURL=index.js.map