@heavybit/shared-lib 1.3.1 → 2.1.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.
Files changed (128) hide show
  1. package/README.md +100 -0
  2. package/dist/cjs/common/errors.js +44 -5
  3. package/dist/cjs/common/errors.js.map +1 -1
  4. package/dist/cjs/common/phone.js +43 -10
  5. package/dist/cjs/common/phone.js.map +1 -1
  6. package/dist/cjs/common/response.js +78 -5
  7. package/dist/cjs/common/response.js.map +1 -1
  8. package/dist/cjs/config/index.js +1 -1
  9. package/dist/cjs/config/index.js.map +1 -1
  10. package/dist/cjs/context/index.js +49 -0
  11. package/dist/cjs/context/index.js.map +1 -0
  12. package/dist/cjs/contracts/index.js +0 -2
  13. package/dist/cjs/contracts/index.js.map +1 -1
  14. package/dist/cjs/contracts/notification-dispatch.js +87 -63
  15. package/dist/cjs/contracts/notification-dispatch.js.map +1 -1
  16. package/dist/cjs/express/auth.js +44 -11
  17. package/dist/cjs/express/auth.js.map +1 -1
  18. package/dist/cjs/express/index.js +1 -0
  19. package/dist/cjs/express/index.js.map +1 -1
  20. package/dist/cjs/express/middleware.js +154 -34
  21. package/dist/cjs/express/middleware.js.map +1 -1
  22. package/dist/cjs/express/respond.js +47 -0
  23. package/dist/cjs/express/respond.js.map +1 -0
  24. package/dist/cjs/index.js +1 -0
  25. package/dist/cjs/index.js.map +1 -1
  26. package/dist/cjs/logging/index.js +76 -40
  27. package/dist/cjs/logging/index.js.map +1 -1
  28. package/dist/cjs/logging/redact.js +117 -0
  29. package/dist/cjs/logging/redact.js.map +1 -0
  30. package/dist/cjs/messaging/index.js +1 -0
  31. package/dist/cjs/messaging/index.js.map +1 -1
  32. package/dist/cjs/messaging/staff-notifier.js +36 -44
  33. package/dist/cjs/messaging/staff-notifier.js.map +1 -1
  34. package/dist/cjs/messaging/staff-recipients.js +57 -0
  35. package/dist/cjs/messaging/staff-recipients.js.map +1 -0
  36. package/dist/cjs/validation/index.js +27 -3
  37. package/dist/cjs/validation/index.js.map +1 -1
  38. package/dist/esm/common/errors.js +42 -4
  39. package/dist/esm/common/errors.js.map +1 -1
  40. package/dist/esm/common/phone.js +43 -10
  41. package/dist/esm/common/phone.js.map +1 -1
  42. package/dist/esm/common/response.js +75 -5
  43. package/dist/esm/common/response.js.map +1 -1
  44. package/dist/esm/config/index.js +1 -1
  45. package/dist/esm/config/index.js.map +1 -1
  46. package/dist/esm/context/index.js +44 -0
  47. package/dist/esm/context/index.js.map +1 -0
  48. package/dist/esm/contracts/index.js +0 -2
  49. package/dist/esm/contracts/index.js.map +1 -1
  50. package/dist/esm/contracts/notification-dispatch.js +85 -62
  51. package/dist/esm/contracts/notification-dispatch.js.map +1 -1
  52. package/dist/esm/express/auth.js +45 -12
  53. package/dist/esm/express/auth.js.map +1 -1
  54. package/dist/esm/express/index.js +1 -0
  55. package/dist/esm/express/index.js.map +1 -1
  56. package/dist/esm/express/middleware.js +146 -27
  57. package/dist/esm/express/middleware.js.map +1 -1
  58. package/dist/esm/express/respond.js +42 -0
  59. package/dist/esm/express/respond.js.map +1 -0
  60. package/dist/esm/index.js +1 -0
  61. package/dist/esm/index.js.map +1 -1
  62. package/dist/esm/logging/index.js +68 -39
  63. package/dist/esm/logging/index.js.map +1 -1
  64. package/dist/esm/logging/redact.js +110 -0
  65. package/dist/esm/logging/redact.js.map +1 -0
  66. package/dist/esm/messaging/index.js +1 -0
  67. package/dist/esm/messaging/index.js.map +1 -1
  68. package/dist/esm/messaging/staff-notifier.js +36 -43
  69. package/dist/esm/messaging/staff-notifier.js.map +1 -1
  70. package/dist/esm/messaging/staff-recipients.js +53 -0
  71. package/dist/esm/messaging/staff-recipients.js.map +1 -0
  72. package/dist/esm/validation/index.js +27 -2
  73. package/dist/esm/validation/index.js.map +1 -1
  74. package/dist/types/alerting/index.d.ts +1 -1
  75. package/dist/types/alerting/index.d.ts.map +1 -1
  76. package/dist/types/cache/index.d.ts +1 -1
  77. package/dist/types/cache/index.d.ts.map +1 -1
  78. package/dist/types/common/errors.d.ts +48 -7
  79. package/dist/types/common/errors.d.ts.map +1 -1
  80. package/dist/types/common/phone.d.ts +35 -6
  81. package/dist/types/common/phone.d.ts.map +1 -1
  82. package/dist/types/common/response.d.ts +66 -5
  83. package/dist/types/common/response.d.ts.map +1 -1
  84. package/dist/types/config/index.d.ts +1 -1
  85. package/dist/types/config/index.d.ts.map +1 -1
  86. package/dist/types/context/index.d.ts +39 -0
  87. package/dist/types/context/index.d.ts.map +1 -0
  88. package/dist/types/contracts/index.d.ts +0 -2
  89. package/dist/types/contracts/index.d.ts.map +1 -1
  90. package/dist/types/contracts/notification-dispatch.d.ts +208 -90
  91. package/dist/types/contracts/notification-dispatch.d.ts.map +1 -1
  92. package/dist/types/express/auth.d.ts +42 -6
  93. package/dist/types/express/auth.d.ts.map +1 -1
  94. package/dist/types/express/index.d.ts +1 -0
  95. package/dist/types/express/index.d.ts.map +1 -1
  96. package/dist/types/express/middleware.d.ts +86 -7
  97. package/dist/types/express/middleware.d.ts.map +1 -1
  98. package/dist/types/express/respond.d.ts +41 -0
  99. package/dist/types/express/respond.d.ts.map +1 -0
  100. package/dist/types/index.d.ts +1 -0
  101. package/dist/types/index.d.ts.map +1 -1
  102. package/dist/types/logging/index.d.ts +30 -4
  103. package/dist/types/logging/index.d.ts.map +1 -1
  104. package/dist/types/logging/redact.d.ts +54 -0
  105. package/dist/types/logging/redact.d.ts.map +1 -0
  106. package/dist/types/messaging/index.d.ts +1 -0
  107. package/dist/types/messaging/index.d.ts.map +1 -1
  108. package/dist/types/messaging/staff-notifier.d.ts +67 -36
  109. package/dist/types/messaging/staff-notifier.d.ts.map +1 -1
  110. package/dist/types/messaging/staff-recipients.d.ts +50 -0
  111. package/dist/types/messaging/staff-recipients.d.ts.map +1 -0
  112. package/dist/types/types/index.d.ts +36 -1
  113. package/dist/types/types/index.d.ts.map +1 -1
  114. package/dist/types/validation/index.d.ts +33 -2
  115. package/dist/types/validation/index.d.ts.map +1 -1
  116. package/package.json +2 -2
  117. package/dist/cjs/contracts/editorial-draft.js +0 -120
  118. package/dist/cjs/contracts/editorial-draft.js.map +0 -1
  119. package/dist/cjs/contracts/upload-import.js +0 -54
  120. package/dist/cjs/contracts/upload-import.js.map +0 -1
  121. package/dist/esm/contracts/editorial-draft.js +0 -117
  122. package/dist/esm/contracts/editorial-draft.js.map +0 -1
  123. package/dist/esm/contracts/upload-import.js +0 -51
  124. package/dist/esm/contracts/upload-import.js.map +0 -1
  125. package/dist/types/contracts/editorial-draft.d.ts +0 -610
  126. package/dist/types/contracts/editorial-draft.d.ts.map +0 -1
  127. package/dist/types/contracts/upload-import.d.ts +0 -96
  128. package/dist/types/contracts/upload-import.d.ts.map +0 -1
@@ -1,33 +1,51 @@
1
+ /**
2
+ * Notification dispatch: a generic command contract (shared plumbing, no app vocabulary).
3
+ *
4
+ * Services never send email, SMS or push themselves. They stage one dispatch command (default
5
+ * `notification:dispatch`) through their outbox, and the app's notification-service renders a seeded
6
+ * template and delivers it. Both sides build their schema from {@link createNotificationDispatchSchema},
7
+ * so producers and the consumer cannot drift.
8
+ *
9
+ * App-specific parts are parameters: the app passes its own categories (e.g. `security`, `order`) and
10
+ * channels (e.g. `email`, `sms`, `in_app`). Template slugs and their variables also belong to the app
11
+ * (keep a table next to the notification-service seeds and test both sides against it).
12
+ */
1
13
  import { z } from 'zod';
2
14
  /**
3
- * `notification:dispatch` — the one command every Showbiz service stages (through its outbox) to have
4
- * notification-service render a seeded template and deliver it by email and/or the in-app feed. No
5
- * other service sends email itself. One contract for both sides: producers type their payload with it
6
- * and notification-service validates against it, so the two cannot drift.
15
+ * Default command name for a dispatch. An app may use another name and pass it to its producers.
16
+ *
17
+ * @example await outbox.command(tx, NOTIFICATION_DISPATCH, dispatch, { correlationId });
7
18
  */
8
19
  export declare const NOTIFICATION_DISPATCH = "notification:dispatch";
9
- /** Channels a dispatch may ask for. `in_app` needs a recipient `userId` (the bell feed is per user). */
10
- export declare const DISPATCH_CHANNELS: readonly ["email", "in_app"];
11
- export type DispatchChannel = (typeof DISPATCH_CHANNELS)[number];
12
20
  /**
13
- * What a dispatch is about. Account mail (`security`, `account`, `team`, `marketing`) goes to a user;
14
- * `staff` is an operational alert to a configured staff address.
21
+ * Size limits that keep a dispatch small enough for a queue message and safe to render.
22
+ *
23
+ * @example z.string().max(DISPATCH_LIMITS.template)
15
24
  */
16
- export declare const NOTIFICATION_CATEGORIES: readonly ["security", "account", "team", "marketing", "staff"];
17
- export type NotificationCategory = (typeof NOTIFICATION_CATEGORIES)[number];
18
25
  export declare const DISPATCH_LIMITS: {
19
26
  readonly template: 100;
20
27
  readonly email: 254;
21
28
  readonly name: 200;
29
+ readonly userId: 36;
22
30
  readonly dataKeys: 40;
23
31
  readonly dataKey: 64;
24
32
  readonly dataValue: 2000;
25
33
  readonly secretKeys: 10;
26
34
  readonly permission: 100;
27
35
  };
28
- /** Template variables are flat scalars: they are printed into the copy, never serialised objects. */
36
+ /**
37
+ * Template variables are flat scalars: they are printed into the copy, never serialised objects.
38
+ *
39
+ * @example dispatchValueSchema.parse('1250.00')
40
+ */
29
41
  export declare const dispatchValueSchema: z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>;
30
42
  export type DispatchValue = z.infer<typeof dispatchValueSchema>;
43
+ /**
44
+ * Who receives a dispatch. `userId` is needed for per-user channels such as an in-app feed;
45
+ * addresses with no account behind them carry only `email`.
46
+ *
47
+ * @example { email: 'wanjiru@example.com', name: 'Wanjiru', userId: '01J…', timezone: 'Africa/Nairobi' }
48
+ */
31
49
  export declare const dispatchRecipientSchema: z.ZodObject<{
32
50
  email: z.ZodString;
33
51
  name: z.ZodOptional<z.ZodString>;
@@ -37,22 +55,58 @@ export declare const dispatchRecipientSchema: z.ZodObject<{
37
55
  timezone: z.ZodOptional<z.ZodNullable<z.ZodString>>;
38
56
  }, "strip", z.ZodTypeAny, {
39
57
  email: string;
40
- name?: string | undefined;
41
58
  userId?: string | null | undefined;
59
+ name?: string | undefined;
42
60
  locale?: string | null | undefined;
43
61
  timezone?: string | null | undefined;
44
62
  }, {
45
63
  email: string;
46
- name?: string | undefined;
47
64
  userId?: string | null | undefined;
65
+ name?: string | undefined;
48
66
  locale?: string | null | undefined;
49
67
  timezone?: string | null | undefined;
50
68
  }>;
51
69
  export type DispatchRecipient = z.infer<typeof dispatchRecipientSchema>;
52
- export declare const notificationDispatchSchema: z.ZodEffects<z.ZodObject<{
53
- /** Seeded `notification_templates.slug`. */
70
+ /**
71
+ * A template variable name the renderer can match.
72
+ *
73
+ * @example dispatchDataKeySchema.parse('expiresInMinutes')
74
+ */
75
+ export declare const dispatchDataKeySchema: z.ZodString;
76
+ /**
77
+ * The app's vocabulary: which categories and channels a dispatch may name.
78
+ *
79
+ * @example { categories: ['security', 'order', 'staff'], channels: ['email', 'sms'] }
80
+ */
81
+ export interface DispatchVocabulary<C extends string, Ch extends string> {
82
+ /** What a dispatch is about, e.g. `['security', 'account', 'order', 'staff']`. */
83
+ categories: readonly [C, ...C[]];
84
+ /** Delivery channels the notification-service supports, e.g. `['email', 'sms', 'in_app']`. */
85
+ channels: readonly [Ch, ...Ch[]];
86
+ }
87
+ /**
88
+ * Build the dispatch schema for an app's categories and channels. Producers type their payload with it;
89
+ * the notification-service validates incoming commands with the same schema.
90
+ *
91
+ * Rules every app gets: a seeded `template` slug; one or more distinct channels; at most 40 flat-scalar `data`
92
+ * variables with renderer-safe names; `secretKeys` (one-time codes, reset links) must be keys of `data`;
93
+ * the consumer must use them only to render, never store them or show them in a feed.
94
+ *
95
+ * @typeParam C The app's categories (inferred as literals).
96
+ * @typeParam Ch The app's channels (inferred as literals).
97
+ * @param vocabulary The app's categories and channels.
98
+ * @returns A Zod schema; `data` defaults to `{}` and `secretKeys` to `[]`.
99
+ * @example
100
+ * export const dispatchSchema = createNotificationDispatchSchema({
101
+ * categories: ['security', 'account', 'order', 'staff'],
102
+ * channels: ['email', 'sms'],
103
+ * });
104
+ * export type Dispatch = z.input<typeof dispatchSchema>;
105
+ */
106
+ export declare function createNotificationDispatchSchema<const C extends string, const Ch extends string>(vocabulary: DispatchVocabulary<C, Ch>): z.ZodEffects<z.ZodObject<{
107
+ /** Seeded template slug in the notification-service. */
54
108
  template: z.ZodString;
55
- category: z.ZodEnum<["security", "account", "team", "marketing", "staff"]>;
109
+ category: z.ZodEnum<[C, ...C[]]>;
56
110
  recipient: z.ZodObject<{
57
111
  email: z.ZodString;
58
112
  name: z.ZodOptional<z.ZodString>;
@@ -62,114 +116,178 @@ export declare const notificationDispatchSchema: z.ZodEffects<z.ZodObject<{
62
116
  timezone: z.ZodOptional<z.ZodNullable<z.ZodString>>;
63
117
  }, "strip", z.ZodTypeAny, {
64
118
  email: string;
65
- name?: string | undefined;
66
119
  userId?: string | null | undefined;
120
+ name?: string | undefined;
67
121
  locale?: string | null | undefined;
68
122
  timezone?: string | null | undefined;
69
123
  }, {
70
124
  email: string;
71
- name?: string | undefined;
72
125
  userId?: string | null | undefined;
126
+ name?: string | undefined;
73
127
  locale?: string | null | undefined;
74
128
  timezone?: string | null | undefined;
75
129
  }>;
76
- channels: z.ZodArray<z.ZodEnum<["email", "in_app"]>, "many">;
130
+ channels: z.ZodEffects<z.ZodArray<z.ZodEnum<[Ch, ...Ch[]]>, "many">, Ch[], Ch[]>;
77
131
  data: z.ZodEffects<z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>>, Record<string, string | number | boolean | null>, Record<string, string | number | boolean | null> | undefined>;
78
- /**
79
- * Keys of `data` holding a one-time secret (a sign-in code, a reset or confirm link). They are used to
80
- * render the email and nothing else: notification-service never stores them and never puts them in the
81
- * in-app feed, and email-service masks them in what it keeps.
82
- */
132
+ /** Keys of `data` holding a one-time secret: used to render, never stored or shown in a feed. */
83
133
  secretKeys: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
84
- /** Security and staff alerts are always delivered, whatever the recipient's preferences. */
134
+ /** Deliver whatever the recipient's notification preferences say (for mail the recipient cannot opt out of). */
85
135
  bypassPreferences: z.ZodOptional<z.ZodBoolean>;
86
- /** The permission staff recipients hold; informational until recipients are resolved by permission. */
136
+ /** The permission the recipients were resolved by, when they were (traceability only). */
87
137
  targetPermission: z.ZodOptional<z.ZodString>;
88
- }, "strip", z.ZodTypeAny, {
89
- data: Record<string, string | number | boolean | null>;
90
- category: "security" | "account" | "team" | "marketing" | "staff";
91
- template: string;
92
- recipient: {
138
+ }, "strip", z.ZodTypeAny, z.objectUtil.addQuestionMarks<z.baseObjectOutputType<{
139
+ /** Seeded template slug in the notification-service. */
140
+ template: z.ZodString;
141
+ category: z.ZodEnum<[C, ...C[]]>;
142
+ recipient: z.ZodObject<{
143
+ email: z.ZodString;
144
+ name: z.ZodOptional<z.ZodString>;
145
+ userId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
146
+ locale: z.ZodOptional<z.ZodNullable<z.ZodString>>;
147
+ /** IANA zone; time variables are rendered in it. */
148
+ timezone: z.ZodOptional<z.ZodNullable<z.ZodString>>;
149
+ }, "strip", z.ZodTypeAny, {
93
150
  email: string;
151
+ userId?: string | null | undefined;
94
152
  name?: string | undefined;
153
+ locale?: string | null | undefined;
154
+ timezone?: string | null | undefined;
155
+ }, {
156
+ email: string;
95
157
  userId?: string | null | undefined;
158
+ name?: string | undefined;
96
159
  locale?: string | null | undefined;
97
160
  timezone?: string | null | undefined;
98
- };
99
- channels: ("email" | "in_app")[];
100
- secretKeys: string[];
101
- bypassPreferences?: boolean | undefined;
102
- targetPermission?: string | undefined;
103
- }, {
104
- category: "security" | "account" | "team" | "marketing" | "staff";
105
- template: string;
106
- recipient: {
161
+ }>;
162
+ channels: z.ZodEffects<z.ZodArray<z.ZodEnum<[Ch, ...Ch[]]>, "many">, Ch[], Ch[]>;
163
+ data: z.ZodEffects<z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>>, Record<string, string | number | boolean | null>, Record<string, string | number | boolean | null> | undefined>;
164
+ /** Keys of `data` holding a one-time secret: used to render, never stored or shown in a feed. */
165
+ secretKeys: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
166
+ /** Deliver whatever the recipient's notification preferences say (for mail the recipient cannot opt out of). */
167
+ bypassPreferences: z.ZodOptional<z.ZodBoolean>;
168
+ /** The permission the recipients were resolved by, when they were (traceability only). */
169
+ targetPermission: z.ZodOptional<z.ZodString>;
170
+ }>, any> extends infer T ? { [k in keyof T]: T[k]; } : never, z.baseObjectInputType<{
171
+ /** Seeded template slug in the notification-service. */
172
+ template: z.ZodString;
173
+ category: z.ZodEnum<[C, ...C[]]>;
174
+ recipient: z.ZodObject<{
175
+ email: z.ZodString;
176
+ name: z.ZodOptional<z.ZodString>;
177
+ userId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
178
+ locale: z.ZodOptional<z.ZodNullable<z.ZodString>>;
179
+ /** IANA zone; time variables are rendered in it. */
180
+ timezone: z.ZodOptional<z.ZodNullable<z.ZodString>>;
181
+ }, "strip", z.ZodTypeAny, {
107
182
  email: string;
183
+ userId?: string | null | undefined;
108
184
  name?: string | undefined;
185
+ locale?: string | null | undefined;
186
+ timezone?: string | null | undefined;
187
+ }, {
188
+ email: string;
109
189
  userId?: string | null | undefined;
190
+ name?: string | undefined;
110
191
  locale?: string | null | undefined;
111
192
  timezone?: string | null | undefined;
112
- };
113
- channels: ("email" | "in_app")[];
114
- data?: Record<string, string | number | boolean | null> | undefined;
115
- secretKeys?: string[] | undefined;
116
- bypassPreferences?: boolean | undefined;
117
- targetPermission?: string | undefined;
118
- }>, {
119
- data: Record<string, string | number | boolean | null>;
120
- category: "security" | "account" | "team" | "marketing" | "staff";
121
- template: string;
122
- recipient: {
193
+ }>;
194
+ channels: z.ZodEffects<z.ZodArray<z.ZodEnum<[Ch, ...Ch[]]>, "many">, Ch[], Ch[]>;
195
+ data: z.ZodEffects<z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>>, Record<string, string | number | boolean | null>, Record<string, string | number | boolean | null> | undefined>;
196
+ /** Keys of `data` holding a one-time secret: used to render, never stored or shown in a feed. */
197
+ secretKeys: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
198
+ /** Deliver whatever the recipient's notification preferences say (for mail the recipient cannot opt out of). */
199
+ bypassPreferences: z.ZodOptional<z.ZodBoolean>;
200
+ /** The permission the recipients were resolved by, when they were (traceability only). */
201
+ targetPermission: z.ZodOptional<z.ZodString>;
202
+ }> extends infer T_1 ? { [k_1 in keyof T_1]: T_1[k_1]; } : never>, z.objectUtil.addQuestionMarks<z.baseObjectOutputType<{
203
+ /** Seeded template slug in the notification-service. */
204
+ template: z.ZodString;
205
+ category: z.ZodEnum<[C, ...C[]]>;
206
+ recipient: z.ZodObject<{
207
+ email: z.ZodString;
208
+ name: z.ZodOptional<z.ZodString>;
209
+ userId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
210
+ locale: z.ZodOptional<z.ZodNullable<z.ZodString>>;
211
+ /** IANA zone; time variables are rendered in it. */
212
+ timezone: z.ZodOptional<z.ZodNullable<z.ZodString>>;
213
+ }, "strip", z.ZodTypeAny, {
123
214
  email: string;
215
+ userId?: string | null | undefined;
124
216
  name?: string | undefined;
217
+ locale?: string | null | undefined;
218
+ timezone?: string | null | undefined;
219
+ }, {
220
+ email: string;
125
221
  userId?: string | null | undefined;
222
+ name?: string | undefined;
126
223
  locale?: string | null | undefined;
127
224
  timezone?: string | null | undefined;
128
- };
129
- channels: ("email" | "in_app")[];
130
- secretKeys: string[];
131
- bypassPreferences?: boolean | undefined;
132
- targetPermission?: string | undefined;
133
- }, {
134
- category: "security" | "account" | "team" | "marketing" | "staff";
135
- template: string;
136
- recipient: {
225
+ }>;
226
+ channels: z.ZodEffects<z.ZodArray<z.ZodEnum<[Ch, ...Ch[]]>, "many">, Ch[], Ch[]>;
227
+ data: z.ZodEffects<z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>>, Record<string, string | number | boolean | null>, Record<string, string | number | boolean | null> | undefined>;
228
+ /** Keys of `data` holding a one-time secret: used to render, never stored or shown in a feed. */
229
+ secretKeys: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
230
+ /** Deliver whatever the recipient's notification preferences say (for mail the recipient cannot opt out of). */
231
+ bypassPreferences: z.ZodOptional<z.ZodBoolean>;
232
+ /** The permission the recipients were resolved by, when they were (traceability only). */
233
+ targetPermission: z.ZodOptional<z.ZodString>;
234
+ }>, any> extends infer T_2 ? { [k in keyof T_2]: T_2[k]; } : never, z.baseObjectInputType<{
235
+ /** Seeded template slug in the notification-service. */
236
+ template: z.ZodString;
237
+ category: z.ZodEnum<[C, ...C[]]>;
238
+ recipient: z.ZodObject<{
239
+ email: z.ZodString;
240
+ name: z.ZodOptional<z.ZodString>;
241
+ userId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
242
+ locale: z.ZodOptional<z.ZodNullable<z.ZodString>>;
243
+ /** IANA zone; time variables are rendered in it. */
244
+ timezone: z.ZodOptional<z.ZodNullable<z.ZodString>>;
245
+ }, "strip", z.ZodTypeAny, {
137
246
  email: string;
247
+ userId?: string | null | undefined;
138
248
  name?: string | undefined;
249
+ locale?: string | null | undefined;
250
+ timezone?: string | null | undefined;
251
+ }, {
252
+ email: string;
139
253
  userId?: string | null | undefined;
254
+ name?: string | undefined;
140
255
  locale?: string | null | undefined;
141
256
  timezone?: string | null | undefined;
142
- };
143
- channels: ("email" | "in_app")[];
144
- data?: Record<string, string | number | boolean | null> | undefined;
145
- secretKeys?: string[] | undefined;
146
- bypassPreferences?: boolean | undefined;
147
- targetPermission?: string | undefined;
148
- }>;
257
+ }>;
258
+ channels: z.ZodEffects<z.ZodArray<z.ZodEnum<[Ch, ...Ch[]]>, "many">, Ch[], Ch[]>;
259
+ data: z.ZodEffects<z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>>, Record<string, string | number | boolean | null>, Record<string, string | number | boolean | null> | undefined>;
260
+ /** Keys of `data` holding a one-time secret: used to render, never stored or shown in a feed. */
261
+ secretKeys: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
262
+ /** Deliver whatever the recipient's notification preferences say (for mail the recipient cannot opt out of). */
263
+ bypassPreferences: z.ZodOptional<z.ZodBoolean>;
264
+ /** The permission the recipients were resolved by, when they were (traceability only). */
265
+ targetPermission: z.ZodOptional<z.ZodString>;
266
+ }> extends infer T_3 ? { [k_1 in keyof T_3]: T_3[k_1]; } : never>;
149
267
  /**
150
- * The seeded templates Showbiz services dispatch, with the variables (`data` keys) each dispatch carries.
151
- * Producers send exactly these keys; a seeded template may use any subset (notification-service dead-letters
152
- * a dispatch missing a variable its template uses). Both sides test against this table.
268
+ * What a producer stages, as a plain type (usable while categories/channels are still generic, e.g. in
269
+ * shared helpers). Equivalent to {@link NotificationDispatchInput} for a concrete vocabulary.
270
+ *
271
+ * @example
272
+ * const dispatch: NotificationDispatchPayload<'security', 'email'> = {
273
+ * template: 'login-otp', category: 'security', recipient: { email }, channels: ['email'],
274
+ * data: { code }, secretKeys: ['code'], bypassPreferences: true,
275
+ * };
153
276
  */
154
- export declare const DISPATCH_TEMPLATE_VARIABLES: {
155
- readonly 'account-activation': readonly ["name", "code", "expiresInHours"];
156
- readonly 'login-otp': readonly ["name", "code", "expiresInMinutes", "device"];
157
- readonly 'password-reset': readonly ["name", "resetUrl", "expiresInMinutes"];
158
- readonly 'staff-invitation': readonly ["name", "resetUrl", "expiresInMinutes"];
159
- readonly 'password-changed': readonly ["name", "changedAt"];
160
- readonly 'new-device-login': readonly ["name", "device", "ip", "country", "occurredAt", "confirmUrl", "disputeUrl"];
161
- readonly 'team-member-added': readonly ["name", "profileName", "role", "invitedBy"];
162
- readonly 'account-suspended': readonly ["name", "reason"];
163
- readonly 'welcome-listener': readonly ["name"];
164
- readonly 'welcome-creator': readonly ["name"];
165
- readonly ai_budget_threshold: readonly ["requester", "period", "threshold_pct", "spent_usd", "limit_usd"];
166
- readonly newswire_source_unhealthy: readonly ["source_name", "source_slug", "consecutive_failures", "last_error", "last_success_at"];
167
- readonly newswire_block_rate_high: readonly ["blocked", "total", "block_rate_pct", "window_hours"];
168
- readonly autopublish_audit_alert: readonly ["problem_rate_pct", "problem_count", "sample_count", "window_days", "action_taken"];
169
- };
170
- export type DispatchTemplate = keyof typeof DISPATCH_TEMPLATE_VARIABLES;
277
+ export interface NotificationDispatchPayload<C extends string = string, Ch extends string = string> {
278
+ template: string;
279
+ category: C;
280
+ recipient: DispatchRecipient;
281
+ channels: Ch[];
282
+ data?: Record<string, DispatchValue>;
283
+ secretKeys?: string[];
284
+ bypassPreferences?: boolean;
285
+ targetPermission?: string;
286
+ }
287
+ /** The schema type for a given vocabulary. */
288
+ export type NotificationDispatchSchema<C extends string, Ch extends string> = ReturnType<typeof createNotificationDispatchSchema<C, Ch>>;
171
289
  /** The validated command (defaults applied). */
172
- export type NotificationDispatch = z.infer<typeof notificationDispatchSchema>;
290
+ export type NotificationDispatch<C extends string = string, Ch extends string = string> = z.output<NotificationDispatchSchema<C, Ch>>;
173
291
  /** What a producer stages: `data` and `secretKeys` may be omitted. */
174
- export type NotificationDispatchInput = z.input<typeof notificationDispatchSchema>;
292
+ export type NotificationDispatchInput<C extends string = string, Ch extends string = string> = z.input<NotificationDispatchSchema<C, Ch>>;
175
293
  //# sourceMappingURL=notification-dispatch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"notification-dispatch.d.ts","sourceRoot":"","sources":["../../../src/contracts/notification-dispatch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,0BAA0B,CAAC;AAE7D,wGAAwG;AACxG,eAAO,MAAM,iBAAiB,8BAA+B,CAAC;AAC9D,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjE;;;GAGG;AACH,eAAO,MAAM,uBAAuB,gEAAiE,CAAC;AACtG,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5E,eAAO,MAAM,eAAe;;;;;;;;;CASlB,CAAC;AAEX,qGAAqG;AACrG,eAAO,MAAM,mBAAmB,iEAAmG,CAAC;AACpI,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,eAAO,MAAM,uBAAuB;;;;;IAKlC,oDAAoD;;;;;;;;;;;;;;EAEpD,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAIxE,eAAO,MAAM,0BAA0B;IAEnC,4CAA4C;;;;;;;;QAT9C,oDAAoD;;;;;;;;;;;;;;;;;IAkBlD;;;;OAIG;;IAEH,4FAA4F;;IAE5F,uGAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EASvG,CAAC;AAEL;;;;GAIG;AACH,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;CAiBc,CAAC;AACvD,MAAM,MAAM,gBAAgB,GAAG,MAAM,OAAO,2BAA2B,CAAC;AAExE,gDAAgD;AAChD,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAC9E,sEAAsE;AACtE,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC"}
1
+ {"version":3,"file":"notification-dispatch.d.ts","sourceRoot":"","sources":["../../../src/contracts/notification-dispatch.ts"],"names":[],"mappings":"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"}
@@ -7,21 +7,57 @@ declare global {
7
7
  }
8
8
  }
9
9
  }
10
+ /** Options for the gateway identity middleware. */
10
11
  export interface GatewayAuthOptions {
12
+ /** HMAC secret shared with the gateway; verifies the signed identity headers. */
11
13
  secret: string;
14
+ /** Maximum age of the gateway timestamp before the identity counts as stale (replay guard). */
12
15
  maxSkewMs?: number;
16
+ /** Receives a warning (reason + path, no header values) when a forged or stale identity is rejected. */
13
17
  logger?: Logger;
14
18
  }
15
- /** Requires a valid gateway-signed identity; 401 otherwise. */
19
+ /**
20
+ * Requires a valid gateway-signed identity and sets `req.user`; 401 UNAUTHENTICATED (with `requestId`) otherwise.
21
+ *
22
+ * @param options Secret, skew window and logger.
23
+ * @returns Express middleware.
24
+ * @example router.use(authenticateFromGateway({ secret: config.gatewaySecret, logger }));
25
+ */
16
26
  export declare function authenticateFromGateway(options: GatewayAuthOptions): RequestHandler;
17
- /** Attaches `req.user` when a valid identity is present; continues anonymously otherwise. */
27
+ /**
28
+ * Attaches `req.user` when a valid identity is present and continues anonymously when none is sent.
29
+ * A forged or stale identity is still rejected with 401, never downgraded to anonymous.
30
+ *
31
+ * @param options Secret, skew window and logger.
32
+ * @returns Express middleware.
33
+ * @example router.get('/products', optionalAuthFromGateway({ secret }), controller.list);
34
+ */
18
35
  export declare function optionalAuthFromGateway(options: GatewayAuthOptions): RequestHandler;
19
- /** Allows callers holding any of `permissions`. */
36
+ /**
37
+ * Allows callers holding any of `permissions` (401 without identity, 403 FORBIDDEN otherwise).
38
+ *
39
+ * @example router.post('/refunds', requirePermission('refunds.create'), controller.create);
40
+ */
20
41
  export declare const requirePermission: (...permissions: string[]) => RequestHandler;
21
- /** Allows callers holding every one of `permissions`. */
42
+ /**
43
+ * Allows callers holding every one of `permissions`.
44
+ *
45
+ * @example router.delete('/x/:id', requireAllPermissions('x.read', 'x.delete'), controller.remove);
46
+ */
22
47
  export declare const requireAllPermissions: (...permissions: string[]) => RequestHandler;
23
- /** Allows callers with any of `roles`. Prefer permissions; roles are for coarse gates only. */
48
+ /**
49
+ * Allows callers with any of `roles`. Prefer permissions; roles are for coarse gates only.
50
+ *
51
+ * @example router.use('/ops', requireRole('platform_admin'));
52
+ */
24
53
  export declare const requireRole: (...roles: string[]) => RequestHandler;
25
- /** Guards `/internal/*` routes: requires the shared service secret header. */
54
+ /**
55
+ * Guards `/internal/*` routes: requires the service secret header (constant-time compare);
56
+ * 401 UNAUTHENTICATED with `requestId` otherwise.
57
+ *
58
+ * @param secret The expected `x-internal-service-secret`; an empty secret rejects every call (fail closed).
59
+ * @returns Express middleware.
60
+ * @example router.use('/internal', requireInternalServiceSecret(config.internalSecret));
61
+ */
26
62
  export declare function requireInternalServiceSecret(secret: string): RequestHandler;
27
63
  //# sourceMappingURL=auth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/express/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAyB,cAAc,EAAY,MAAM,SAAS,CAAC;AAK/E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE1D,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,IAAI,CAAC,EAAE,QAAQ,CAAC;SACjB;KACF;CACF;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAeD,+DAA+D;AAC/D,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,kBAAkB,GAAG,cAAc,CAKnF;AAED,6FAA6F;AAC7F,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,kBAAkB,GAAG,cAAc,CAQnF;AAgBD,mDAAmD;AACnD,eAAO,MAAM,iBAAiB,GAAI,GAAG,aAAa,MAAM,EAAE,KAAG,cAAgE,CAAC;AAE9H,yDAAyD;AACzD,eAAO,MAAM,qBAAqB,GAAI,GAAG,aAAa,MAAM,EAAE,KAAG,cAAoE,CAAC;AAEtI,+FAA+F;AAC/F,eAAO,MAAM,WAAW,GAAI,GAAG,OAAO,MAAM,EAAE,KAAG,cAAyE,CAAC;AAE3H,8EAA8E;AAC9E,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAM3E"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/express/auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAyB,cAAc,EAAY,MAAM,SAAS,CAAC;AAM/E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE1D,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,IAAI,CAAC,EAAE,QAAQ,CAAC;SACjB;KACF;CACF;AAED,mDAAmD;AACnD,MAAM,WAAW,kBAAkB;IACjC,iFAAiF;IACjF,MAAM,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wGAAwG;IACxG,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAeD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,kBAAkB,GAAG,cAAc,CAKnF;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,kBAAkB,GAAG,cAAc,CAQnF;AAgBD;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,GAAG,aAAa,MAAM,EAAE,KAAG,cAAgE,CAAC;AAE9H;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GAAI,GAAG,aAAa,MAAM,EAAE,KAAG,cAAoE,CAAC;AAEtI;;;;GAIG;AACH,eAAO,MAAM,WAAW,GAAI,GAAG,OAAO,MAAM,EAAE,KAAG,cAAyE,CAAC;AAE3H;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAM3E"}
@@ -2,4 +2,5 @@ export * from './auth.js';
2
2
  export * from './middleware.js';
3
3
  export * from './health.js';
4
4
  export * from './security.js';
5
+ export * from './respond.js';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/express/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/express/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC"}
@@ -1,20 +1,99 @@
1
1
  import type { ErrorRequestHandler, NextFunction, Request, RequestHandler, Response } from 'express';
2
2
  import type { ZodTypeAny } from 'zod';
3
3
  import type { Logger } from '../types/index.js';
4
- /** Ensures every request carries `x-correlation-id` (reused from the gateway when present). */
5
- export declare function correlationMiddleware(): RequestHandler;
4
+ /** An incoming correlation id is trusted only if it is 8–64 chars of `[A-Za-z0-9-]` (no log injection). */
5
+ export declare const CORRELATION_ID_PATTERN: RegExp;
6
+ /**
7
+ * Gives every request one correlation id and runs the rest of the request inside a
8
+ * {@link RequestContext}. A valid incoming `x-correlation-id` (see {@link CORRELATION_ID_PATTERN}) is
9
+ * kept; anything else is replaced with a new ULID. The id is echoed on the response, written back to
10
+ * the request header (so outbound calls forward it) and becomes every error body's `requestId`.
11
+ * Mount it first: correlation → json → routes → notFoundHandler → errorHandler.
12
+ *
13
+ * @param serviceName Recorded in the context (and so in log lines written without a service).
14
+ * @returns Express middleware.
15
+ * @example app.use(correlationMiddleware('orders'));
16
+ */
17
+ export declare function correlationMiddleware(serviceName?: string): RequestHandler;
18
+ /**
19
+ * The request's correlation id (the source of every error body's `requestId`): the active request
20
+ * context's id, else the `x-correlation-id` header.
21
+ *
22
+ * @param req Express request.
23
+ * @returns The id, or undefined when neither is present.
24
+ * @example logger.info('Order placed', { correlationId: correlationId(req) });
25
+ */
6
26
  export declare function correlationId(req: Request): string | undefined;
7
- /** Real client IP: gateway-forwarded header first, then Express's `req.ip`. */
27
+ /**
28
+ * Real client IP: gateway-forwarded header first, then Express's `req.ip`.
29
+ *
30
+ * @param req Express request.
31
+ * @returns The IP, or null when unknown.
32
+ * @example const ip = clientIp(req);
33
+ */
8
34
  export declare function clientIp(req: Request): string | null;
35
+ /**
36
+ * The caller's IANA time zone as forwarded by the gateway.
37
+ *
38
+ * @param req Express request.
39
+ * @returns e.g. `Europe/Paris`, or null when not sent.
40
+ * @example formatInTimeZone(date, clientTimezone(req) ?? 'UTC');
41
+ */
9
42
  export declare function clientTimezone(req: Request): string | null;
43
+ /**
44
+ * The anonymous visitor id forwarded by the gateway (for anonymous visitor state, rate limits and analytics).
45
+ *
46
+ * @param req Express request.
47
+ * @returns The id, or null when not sent.
48
+ * @example const visitor = req.user?.id ?? anonId(req);
49
+ */
10
50
  export declare function anonId(req: Request): string | null;
11
51
  type RequestPart = 'body' | 'query' | 'params';
12
- /** Validates and replaces `req[part]` with the parsed value; 422 with field errors on failure. */
13
- export declare function validate(schema: ZodTypeAny, part?: RequestPart): RequestHandler;
14
- /** Wraps an async handler so rejections reach the error middleware (needed on Express 4). */
52
+ /** Zod schemas for the parts of a request that {@link validate} checks. */
53
+ export type RequestSchemas = Partial<Record<RequestPart, ZodTypeAny>>;
54
+ /**
55
+ * Validates the request with Zod at the controller boundary. Each given part is parsed and
56
+ * `req[part]` is replaced with the parsed value, so unknown keys are stripped (or rejected with
57
+ * `.strict()`) and coercions/defaults apply. Every part is checked before failing, and the combined
58
+ * field errors are thrown as a 422 {@link ValidationError} for the error handler (body fields keep
59
+ * their path; params and query fields are prefixed, e.g. `params.id`).
60
+ *
61
+ * The older single-schema form `validate(schema, part = 'body')` still works.
62
+ *
63
+ * @param schemas `{ params?, query?, body? }`, or one schema.
64
+ * @param part With a single schema: the part it checks (default `body`).
65
+ * @returns Express middleware.
66
+ * @example router.patch('/:id', validate({ params: idParam, body: updateSchema }), controller.update);
67
+ */
68
+ export declare function validate(schemas: RequestSchemas | ZodTypeAny, part?: RequestPart): RequestHandler;
69
+ /**
70
+ * Wraps an async handler so rejections reach the error middleware (needed on Express 4; harmless on 5).
71
+ *
72
+ * @param fn Async route handler.
73
+ * @returns A handler that forwards rejections to `next(err)`.
74
+ * @example router.get('/:id', asyncHandler(controller.get));
75
+ */
15
76
  export declare function asyncHandler<Req extends Request = Request>(fn: (req: Req, res: Response, next: NextFunction) => Promise<unknown>): RequestHandler;
16
- /** Maps AppErrors to their status; everything else becomes a logged 500 with a generic message. */
77
+ /**
78
+ * Last error middleware. Every body is `{ success: false, code, message, errors?, requestId }`:
79
+ * - body-parser failures → 400 BAD_REQUEST / 413 PAYLOAD_TOO_LARGE;
80
+ * - a thrown ZodError (e.g. from `schema.parse`) → 422 VALIDATION_FAILED with field errors;
81
+ * - AppError → its status, code and errors; 4xx keep their message, 5xx get a generic one; a
82
+ * `retryAfterSeconds` becomes the `Retry-After` header;
83
+ * - anything else → logged in full, answered 500 INTERNAL_ERROR with a generic message.
84
+ * `requestId` is the request context's correlation id (set by {@link correlationMiddleware}).
85
+ *
86
+ * @param logger Receives 5xx details (message, stack, path, correlation id); never sent to the client.
87
+ * @returns Express error middleware.
88
+ * @example app.use(notFoundHandler()); app.use(errorHandler(logger));
89
+ */
17
90
  export declare function errorHandler(logger?: Logger): ErrorRequestHandler;
91
+ /**
92
+ * Catch-all for unmatched routes: 404 NOT_FOUND with the request id. Mount after every router.
93
+ *
94
+ * @returns Express middleware.
95
+ * @example app.use(notFoundHandler()); app.use(errorHandler(logger));
96
+ */
18
97
  export declare function notFoundHandler(): RequestHandler;
19
98
  export {};
20
99
  //# sourceMappingURL=middleware.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../../src/express/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACpG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAKtC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAGhD,+FAA+F;AAC/F,wBAAgB,qBAAqB,IAAI,cAAc,CAQtD;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAG9D;AAED,+EAA+E;AAC/E,wBAAgB,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAIpD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAG1D;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAGlD;AAED,KAAK,WAAW,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE/C,kGAAkG;AAClG,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,GAAE,WAAoB,GAAG,cAAc,CAWvF;AAED,6FAA6F;AAC7F,wBAAgB,YAAY,CAAC,GAAG,SAAS,OAAO,GAAG,OAAO,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,cAAc,CAIjJ;AAED,mGAAmG;AACnG,wBAAgB,YAAY,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,mBAAmB,CAqBjE;AAED,wBAAgB,eAAe,IAAI,cAAc,CAIhD"}
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../../src/express/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACpG,OAAO,KAAK,EAAY,UAAU,EAAE,MAAM,KAAK,CAAC;AAMhD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAGhD,2GAA2G;AAC3G,eAAO,MAAM,sBAAsB,QAAyB,CAAC;AAE7D;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,cAAc,CAQ1E;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAK9D;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAIpD;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAG1D;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAGlD;AAED,KAAK,WAAW,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE/C,2EAA2E;AAC3E,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC;AActE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,UAAU,EAAE,IAAI,GAAE,WAAoB,GAAG,cAAc,CAsBzG;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,SAAS,OAAO,GAAG,OAAO,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,cAAc,CAIjJ;AAOD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,mBAAmB,CA+BjE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,cAAc,CAIhD"}