@thejob/schema 2.1.8 → 2.2.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/index.js CHANGED
@@ -20,6 +20,102 @@ var UserIdAndCreatedAtSchema = object().shape({
20
20
  createdAt: string().required().label("Created At")
21
21
  });
22
22
 
23
+ // src/common/facet.schema.ts
24
+ import { array, boolean as boolean2, number as number2, object as object2, string as string2 } from "yup";
25
+ var tokens = (label) => array().of(string2().trim().required()).optional().default([]).label(label);
26
+ var codeTokens = (label) => array().of(string2().trim().uppercase().required()).optional().default([]).label(label);
27
+ var enumFacet = (values, label) => array().of(string2().oneOf(values).required()).optional().default([]).label(label);
28
+ var days = (label) => number2().integer().min(0).max(3650).optional().default(void 0).label(label);
29
+ var months = (label) => number2().integer().min(0).max(1200).optional().default(void 0).label(label);
30
+ var percent = (label) => number2().integer().min(0).max(100).optional().default(void 0).label(label);
31
+ var epoch = (label) => number2().integer().min(0).optional().default(void 0).label(label).meta({ widget: "date" });
32
+ var currencyBound = (label, currencyField) => number2().integer().min(0).optional().default(void 0).label(label).when(currencyField, {
33
+ is: (v) => !Array.isArray(v) || v.length !== 1,
34
+ then: (s) => s.test(
35
+ "requires-single-currency",
36
+ "Pick exactly one salary currency to filter by salary",
37
+ (value) => value === void 0 || value === null
38
+ )
39
+ });
40
+ var qualified = (qualifierValues, qualifierKey, label) => array().of(
41
+ // `rejectUnknownKeys`, not `.noUnknown()`, for the reason spelled out on
42
+ // that helper: outside strict mode `.noUnknown()` silently STRIPS an
43
+ // unknown key rather than failing, so a misspelled qualifier would arrive
44
+ // as a name-only entry and quietly widen the match instead of erroring.
45
+ rejectUnknownKeys(
46
+ object2({
47
+ name: string2().trim().required().label("Name"),
48
+ // `"open"` for a free-text qualifier (a certification's issuing body).
49
+ // An empty array would NOT mean "anything goes" — `.oneOf([])` rejects
50
+ // every value — so the open case is named rather than inferred from an
51
+ // empty vocabulary.
52
+ [qualifierKey]: qualifierValues === "open" ? string2().trim().optional().default(void 0).label(label) : string2().oneOf(qualifierValues).optional().default(void 0).label(label)
53
+ }).label(label).required(),
54
+ () => ["name", qualifierKey]
55
+ )
56
+ ).optional().default([]).label(label);
57
+ var conjunctionFieldName = (f) => typeof f === "string" ? f : f.name;
58
+ var conjunction = (fields, label) => {
59
+ const names = fields.map(conjunctionFieldName);
60
+ const isBoolean = (name) => fields.some(
61
+ (f) => typeof f !== "string" && f.name === name && f.type === "boolean"
62
+ );
63
+ return array().of(
64
+ rejectUnknownKeys(
65
+ object2(
66
+ Object.fromEntries(
67
+ fields.map((f) => {
68
+ const name = conjunctionFieldName(f);
69
+ if (typeof f !== "string" && f.type === "boolean") {
70
+ return [name, boolean2().optional().default(void 0).label(name)];
71
+ }
72
+ const base = string2().trim().optional().default(void 0).label(name);
73
+ return [
74
+ name,
75
+ typeof f !== "string" && f.oneOf ? base.oneOf([...f.oneOf, void 0]) : base
76
+ ];
77
+ })
78
+ )
79
+ ).test(
80
+ "at-least-one-field",
81
+ // Names the unknown key when that is the real problem. A misspelled
82
+ // field (`companyName`) is stripped before this test sees it, so it
83
+ // would otherwise report "needs at least one field set" — true, but
84
+ // it sends the reader looking in the wrong place.
85
+ ({ value }) => {
86
+ const unknown = Object.keys(
87
+ value ?? {}
88
+ ).filter((k) => !names.includes(k));
89
+ return unknown.length ? `${label} has unsupported keys: ${unknown.join(", ")}` : `${label} needs at least one of: ${names.join(", ")}`;
90
+ },
91
+ (value) => value == null || names.some((f) => {
92
+ const v = value[f];
93
+ if (isBoolean(f)) return typeof v === "boolean";
94
+ return typeof v === "string" && v.trim() !== "";
95
+ })
96
+ ).label(label).required(),
97
+ () => names
98
+ )
99
+ ).optional().default([]).label(label);
100
+ };
101
+ var rejectUnknownKeys = (schema, allowed) => schema.test(
102
+ "no-unknown-keys",
103
+ ({ value, label }) => {
104
+ const known = new Set(allowed());
105
+ const bad = Object.keys(value ?? {}).filter(
106
+ (k) => !known.has(k)
107
+ );
108
+ return `${label} has unsupported keys: ${bad.join(", ")}`;
109
+ },
110
+ (value) => {
111
+ if (value == null) return true;
112
+ const known = new Set(allowed());
113
+ return Object.keys(value).every(
114
+ (k) => known.has(k)
115
+ );
116
+ }
117
+ );
118
+
23
119
  // src/common/common.constant.ts
24
120
  var Common = /* @__PURE__ */ ((Common2) => {
25
121
  Common2["Yes"] = "yes";
@@ -76,7 +172,7 @@ var ExperienceLevel = /* @__PURE__ */ ((ExperienceLevel2) => {
76
172
  var SupportedExperienceLevels = Object.values(ExperienceLevel);
77
173
 
78
174
  // src/duration/duration.schema.ts
79
- import { boolean as boolean2, object as object2, ref } from "yup";
175
+ import { boolean as boolean3, object as object3, ref } from "yup";
80
176
 
81
177
  // src/extensions/date-string.extension.ts
82
178
  import dayjs from "dayjs";
@@ -131,7 +227,7 @@ var DurationSchema = (options) => {
131
227
  const { startKey = "startDate", endKey = "endDate", format } = options || {};
132
228
  const startLabel = options?.startLabel || startKey;
133
229
  const endLabel = options?.endLabel || endKey;
134
- return object2().shape({
230
+ return object3().shape({
135
231
  [startKey]: dateString().format(format, `${startLabel} must be in the format ${format}`).required(`${startLabel} is required`).typeError(`${startLabel} must be a valid date`),
136
232
  [endKey]: dateString().format(format, `${endLabel} must be in the format ${format}`).when("isActive", {
137
233
  is: (val) => Boolean(val) === false,
@@ -143,12 +239,12 @@ var DurationSchema = (options) => {
143
239
  // (value) => !value
144
240
  // ),ccc
145
241
  }),
146
- isActive: boolean2().optional().default(false).typeError("isActive must be a boolean value")
242
+ isActive: boolean3().optional().default(false).typeError("isActive must be a boolean value")
147
243
  });
148
244
  };
149
245
 
150
246
  // src/group/group.schema.ts
151
- import { array, boolean as boolean3, mixed, number as number2, object as object3, string as string2 } from "yup";
247
+ import { array as array2, boolean as boolean4, mixed, number as number3, object as object4, string as string3 } from "yup";
152
248
 
153
249
  // src/group/group.constant.ts
154
250
  var GroupVisibility = /* @__PURE__ */ ((GroupVisibility2) => {
@@ -172,80 +268,129 @@ var GroupStatus = /* @__PURE__ */ ((GroupStatus2) => {
172
268
  GroupStatus2["Archived"] = "archived";
173
269
  return GroupStatus2;
174
270
  })(GroupStatus || {});
271
+ var SupportedGroupStatuses = Object.values(GroupStatus);
272
+ var SupportedGroupVisibilities = Object.values(GroupVisibility);
273
+ var SupportedGroupManagedBy = Object.values(GroupManagedBy);
274
+ var SupportedGroupMembershipStatuses = Object.values(
275
+ GroupMembershipStatus
276
+ );
175
277
 
176
278
  // src/group/group.schema.ts
177
- var GroupTranslationSchema = object3({
178
- language: string2().required("Language is required"),
179
- name: string2().required("Name is required"),
180
- description: string2().optional(),
181
- slug: string2().optional(),
182
- seo: object3({
183
- description: string2().optional(),
184
- keywords: array().of(string2().required()).optional()
279
+ var GroupTranslationSchema = object4({
280
+ language: string3().required("Language is required"),
281
+ name: string3().required("Name is required"),
282
+ description: string3().optional(),
283
+ slug: string3().optional(),
284
+ seo: object4({
285
+ description: string3().optional(),
286
+ keywords: array2().of(string3().required()).optional()
185
287
  }).optional()
186
288
  });
187
- var GroupSchema = object3().shape({
188
- name: string2().required().min(3).max(100).label("Name"),
189
- description: string2().required().min(10).max(1e3).label("Description"),
190
- slug: string2().required().label("Slug"),
191
- banner: string2().label("Banner"),
192
- logo: object3().shape({
193
- light: string2().required().label("Light Logo"),
194
- dark: string2().optional().nullable().label("Dark Logo")
289
+ var GroupSchema = object4().shape({
290
+ name: string3().required().min(3).max(100).label("Name"),
291
+ description: string3().required().min(10).max(1e3).label("Description"),
292
+ slug: string3().required().label("Slug"),
293
+ banner: string3().label("Banner"),
294
+ logo: object4().shape({
295
+ light: string3().required().label("Light Logo"),
296
+ dark: string3().optional().nullable().label("Dark Logo")
195
297
  }).optional().nullable().default(null).label("Logo"),
196
- isFeatured: boolean3().label("Is Featured"),
197
- isDiscoverable: boolean3().default(true).label("Is Discoverable"),
198
- featuredMarkets: array().of(string2()).optional().default([]).label("Featured Markets"),
199
- followersCount: number2().min(0).label("Followers Count"),
200
- membersCount: number2().min(0).label("Members Count"),
298
+ isFeatured: boolean4().label("Is Featured"),
299
+ isDiscoverable: boolean4().default(true).label("Is Discoverable"),
300
+ featuredMarkets: array2().of(string3()).optional().default([]).label("Featured Markets"),
301
+ followersCount: number3().min(0).label("Followers Count"),
302
+ membersCount: number3().min(0).label("Members Count"),
201
303
  visibility: mixed().oneOf(Object.values(GroupVisibility)).default("public" /* Public */).label("Visibility"),
202
- groupRules: string2().optional().label("Group Rules"),
203
- countries: array().of(string2()).optional().label("Countries"),
204
- filterQuery: object3().shape({
205
- jobs: string2().label("Job"),
206
- courses: string2().label("Course"),
207
- discussions: string2().label("Discussion")
304
+ groupRules: string3().optional().label("Group Rules"),
305
+ countries: array2().of(string3()).optional().label("Countries"),
306
+ filterQuery: object4().shape({
307
+ jobs: string3().label("Job"),
308
+ courses: string3().label("Course"),
309
+ discussions: string3().label("Discussion")
208
310
  }).label("Filter Query"),
209
- seo: object3().shape({
210
- description: string2().optional().label("Description"),
211
- keywords: array().of(string2()).optional().label("Keywords")
311
+ seo: object4().shape({
312
+ description: string3().optional().label("Description"),
313
+ keywords: array2().of(string3()).optional().label("Keywords")
212
314
  }).optional().label("SEO"),
213
- translation: array().of(GroupTranslationSchema).optional().label("Translations"),
315
+ translation: array2().of(GroupTranslationSchema).optional().label("Translations"),
214
316
  managedBy: mixed().oneOf(Object.values(GroupManagedBy)).default("community" /* Community */).label("Managed By"),
215
317
  status: mixed().oneOf(Object.values(GroupStatus)).default("active" /* Active */).label("Status"),
216
- createdAt: number2().optional().label("Created At"),
217
- updatedAt: number2().optional().label("Updated At")
318
+ createdAt: number3().optional().label("Created At"),
319
+ updatedAt: number3().optional().label("Updated At")
218
320
  }).label("Group Schema");
219
- var GroupMembershipSchema = object3({
220
- groupId: string2().required().label("Group ID"),
221
- userId: string2().required().label("User ID"),
321
+ var GroupMembershipSchema = object4({
322
+ groupId: string3().required().label("Group ID"),
323
+ userId: string3().required().label("User ID"),
222
324
  status: mixed().oneOf(Object.values(GroupMembershipStatus)).required().label("Status"),
223
- createdAt: number2().optional().label("Created At"),
224
- updatedAt: number2().optional().label("Updated At")
325
+ createdAt: number3().optional().label("Created At"),
326
+ updatedAt: number3().optional().label("Updated At")
225
327
  });
226
328
 
329
+ // src/group/group-filter.schema.ts
330
+ import { boolean as boolean5, object as object5 } from "yup";
331
+ var GroupFilterShape = object5({
332
+ // ── Classification ──────────────────────────────────────────────────────────
333
+ // Closed vocabularies, matched exactly. Values are LOWERCASE (`active`,
334
+ // `public`, `system`) and that casing is data, not style: it is what is stored.
335
+ //
336
+ // All three are currently NON-SELECTIVE — every group in production is
337
+ // `active` / `public` / `system`. They are declared anyway because they are
338
+ // real vocabulary that gains meaning as the corpus grows, and a filter that
339
+ // silently ignores `status` would be worse than one that narrows to everything.
340
+ status: enumFacet(SupportedGroupStatuses, "Status"),
341
+ visibility: enumFacet(SupportedGroupVisibilities, "Visibility"),
342
+ managedBy: enumFacet(SupportedGroupManagedBy, "Managed By"),
343
+ // ── Flags ───────────────────────────────────────────────────────────────────
344
+ // No `.default(false)`: an unchecked box must mean "no constraint", not "only
345
+ // the non-featured ones", so absent simply omits the clause.
346
+ isFeatured: boolean5().optional().label("Featured Only"),
347
+ isDiscoverable: boolean5().optional().label("Discoverable Only"),
348
+ // ── Markets ─────────────────────────────────────────────────────────────────
349
+ /**
350
+ * `tokens`, NOT `codeTokens`. This is the trap in this file.
351
+ *
352
+ * Every other filter here uses `codeTokens` for country codes, because users
353
+ * and jobs store uppercase ISO alpha-2. Groups store them LOWERCASE (`in`,
354
+ * `se`, `ww` — verified against production). `codeTokens` uppercases its
355
+ * input, so `$in: ["IN"]` against a stored `"in"` matches nothing: no error,
356
+ * empty result, empty mail.
357
+ *
358
+ * If these are ever normalised to uppercase, this becomes `codeTokens` in the
359
+ * same change as the data migration, not before it.
360
+ */
361
+ countries: tokens("Countries"),
362
+ featuredMarkets: tokens("Featured Markets"),
363
+ /** Group SLUGS, for naming specific groups. A display name breaks on rename. */
364
+ slugs: tokens("Slugs")
365
+ }).label("Group Filter");
366
+ var GROUP_FILTER_KEYS = Object.keys(GroupFilterShape.fields);
367
+ var GroupFilterSchema = rejectUnknownKeys(
368
+ GroupFilterShape,
369
+ () => GROUP_FILTER_KEYS
370
+ );
371
+
227
372
  // src/job/job.schema.ts
228
- import { array as array8, boolean as boolean9, lazy, mixed as mixed3, number as number5, object as object16, string as string13 } from "yup";
373
+ import { array as array9, boolean as boolean11, lazy, mixed as mixed3, number as number6, object as object18, string as string14 } from "yup";
229
374
 
230
375
  // src/location/location.schema.ts
231
- import { array as array2, number as number3, object as object4, string as string3 } from "yup";
232
- var LocationSchema = object4({
233
- placeId: string3().optional().nullable(),
234
- country: string3().required(),
235
- countryCode: string3().required(),
236
- state: string3().optional().nullable(),
237
- stateCode: string3().optional().nullable(),
238
- city: string3().optional().nullable(),
239
- address: string3().optional().nullable(),
240
- geo: object4({
241
- type: string3().oneOf(["Point"]).default("Point"),
242
- coordinates: array2().of(number3().required()).length(2).required()
376
+ import { array as array3, number as number4, object as object6, string as string4 } from "yup";
377
+ var LocationSchema = object6({
378
+ placeId: string4().optional().nullable(),
379
+ country: string4().required(),
380
+ countryCode: string4().required(),
381
+ state: string4().optional().nullable(),
382
+ stateCode: string4().optional().nullable(),
383
+ city: string4().optional().nullable(),
384
+ address: string4().optional().nullable(),
385
+ geo: object6({
386
+ type: string4().oneOf(["Point"]).default("Point"),
387
+ coordinates: array3().of(number4().required()).length(2).required()
243
388
  // [longitude, latitude]
244
389
  }).required()
245
390
  });
246
391
 
247
392
  // src/page/page.schema.ts
248
- import { array as array5, boolean as boolean7, date, number as number4, object as object10, string as string7 } from "yup";
393
+ import { array as array6, boolean as boolean9, date, number as number5, object as object12, string as string8 } from "yup";
249
394
 
250
395
  // src/page/page.constant.ts
251
396
  var PageType = /* @__PURE__ */ ((PageType2) => {
@@ -283,44 +428,44 @@ var ContactTypes = /* @__PURE__ */ ((ContactTypes2) => {
283
428
  var SupportedContactTypes = Object.values(ContactTypes);
284
429
 
285
430
  // src/page/page-location.schema.ts
286
- import { boolean as boolean4, object as object5, string as string4 } from "yup";
287
- var PageLocationSchema = object5().shape({
288
- isHeadquarters: boolean4().oneOf([true, false]).default(false).label("Is Headquarters"),
289
- coverImage: string4().optional().label("Cover Image"),
290
- name: string4().required().label("Office Name"),
431
+ import { boolean as boolean6, object as object7, string as string5 } from "yup";
432
+ var PageLocationSchema = object7().shape({
433
+ isHeadquarters: boolean6().oneOf([true, false]).default(false).label("Is Headquarters"),
434
+ coverImage: string5().optional().label("Cover Image"),
435
+ name: string5().required().label("Office Name"),
291
436
  /* TODO: Add Location Schema to enable geo spacial search*/
292
437
  // location: LocationSchema,
293
- contact: object5().shape({
294
- email: string4().email().optional().label("Email"),
295
- phone: string4().optional().label("Phone"),
296
- address: string4().required().label("Address")
438
+ contact: object7().shape({
439
+ email: string5().email().optional().label("Email"),
440
+ phone: string5().optional().label("Phone"),
441
+ address: string5().required().label("Address")
297
442
  })
298
443
  }).label("PageLocationSchema");
299
444
 
300
445
  // src/page/institute/institute.schema.ts
301
- import { object as object6 } from "yup";
302
- var InstituteSchema = object6().shape({}).label("InstituteSchema");
446
+ import { object as object8 } from "yup";
447
+ var InstituteSchema = object8().shape({}).label("InstituteSchema");
303
448
 
304
449
  // src/page/company/company.schema.ts
305
- import { array as array4, boolean as boolean5, object as object8 } from "yup";
450
+ import { array as array5, boolean as boolean7, object as object10 } from "yup";
306
451
 
307
452
  // src/page/company/perks-and-benefits.schema.ts
308
- import { array as array3 } from "yup";
309
- import { object as object7, string as string5 } from "yup";
310
- var PerksAndBenefitSchema = object7().shape({
311
- name: string5().trim().max(50).required().label("Name"),
312
- description: string5().max(1e3).required().label("Description"),
313
- category: array3().of(string5()).optional().label("Category")
453
+ import { array as array4 } from "yup";
454
+ import { object as object9, string as string6 } from "yup";
455
+ var PerksAndBenefitSchema = object9().shape({
456
+ name: string6().trim().max(50).required().label("Name"),
457
+ description: string6().max(1e3).required().label("Description"),
458
+ category: array4().of(string6()).optional().label("Category")
314
459
  });
315
460
 
316
461
  // src/page/company/company.schema.ts
317
- var CompanySchema = object8().shape({
318
- equalOpportunityEmployer: boolean5().optional().label("Equal Opportunity Employer"),
319
- perksAndBenefits: array4().of(PerksAndBenefitSchema).optional().label("Perks and Benefits")
462
+ var CompanySchema = object10().shape({
463
+ equalOpportunityEmployer: boolean7().optional().label("Equal Opportunity Employer"),
464
+ perksAndBenefits: array5().of(PerksAndBenefitSchema).optional().label("Perks and Benefits")
320
465
  }).noUnknown().strict().label("Company Schema");
321
466
 
322
467
  // src/social-account/social-account.schema.ts
323
- import { boolean as boolean6, object as object9, string as string6 } from "yup";
468
+ import { boolean as boolean8, object as object11, string as string7 } from "yup";
324
469
 
325
470
  // src/social-account/social-account.constant.ts
326
471
  var SocialAccount = /* @__PURE__ */ ((SocialAccount2) => {
@@ -358,43 +503,43 @@ var SocialAccount = /* @__PURE__ */ ((SocialAccount2) => {
358
503
  var SupportedSocialAccounts = Object.values(SocialAccount);
359
504
 
360
505
  // src/social-account/social-account.schema.ts
361
- var SocialAccountSchema = object9().shape({
506
+ var SocialAccountSchema = object11().shape({
362
507
  // TODO: Remove isNew - it's unused. Backend (user-social-accounts.service.ts) just strips it before saving.
363
- isNew: boolean6().default(false).optional().label("Is New"),
364
- type: string6().oneOf(SupportedSocialAccounts).required().label("Account Type"),
365
- url: string6().required().label("URL")
508
+ isNew: boolean8().default(false).optional().label("Is New"),
509
+ type: string7().oneOf(SupportedSocialAccounts).required().label("Account Type"),
510
+ url: string7().required().label("URL")
366
511
  }).noUnknown().strict().label("Social Account");
367
512
 
368
513
  // src/page/page.schema.ts
369
- var PageSchema = object10().shape({
370
- id: string7().required().label("ID"),
371
- shortId: string7().required().label("Short ID"),
372
- name: string7().required().label("Name"),
373
- headline: string7().max(100).optional().label("Headline"),
374
- about: string7().required().min(100).max(1e4).label("About"),
375
- website: string7().required().label("Website"),
376
- slug: string7().required().label("Slug"),
377
- employeeCount: string7().optional().label("Employee Count"),
378
- yearFounded: string7().optional().label("Year Founded"),
379
- type: string7().oneOf(SupportedPageTypes).required().label("Page Type"),
380
- locations: array5().of(PageLocationSchema).optional().label("Locations"),
514
+ var PageSchema = object12().shape({
515
+ id: string8().required().label("ID"),
516
+ shortId: string8().required().label("Short ID"),
517
+ name: string8().required().label("Name"),
518
+ headline: string8().max(100).optional().label("Headline"),
519
+ about: string8().required().min(100).max(1e4).label("About"),
520
+ website: string8().required().label("Website"),
521
+ slug: string8().required().label("Slug"),
522
+ employeeCount: string8().optional().label("Employee Count"),
523
+ yearFounded: string8().optional().label("Year Founded"),
524
+ type: string8().oneOf(SupportedPageTypes).required().label("Page Type"),
525
+ locations: array6().of(PageLocationSchema).optional().label("Locations"),
381
526
  /*
382
527
  * @deprecated
383
528
  * 'locations' prop already has contact information, so this can be removed
384
529
  */
385
- contacts: array5().of(
386
- object10().shape({
387
- type: string7().oneOf(SupportedContactTypes).required().label("Type"),
388
- value: string7().required().label("Value"),
389
- label: string7().optional().label("Label"),
390
- isPrimary: boolean7().optional().default(false).label("Is Primary"),
391
- isVerified: boolean7().optional().default(false).label("Is Verified")
530
+ contacts: array6().of(
531
+ object12().shape({
532
+ type: string8().oneOf(SupportedContactTypes).required().label("Type"),
533
+ value: string8().required().label("Value"),
534
+ label: string8().optional().label("Label"),
535
+ isPrimary: boolean9().optional().default(false).label("Is Primary"),
536
+ isVerified: boolean9().optional().default(false).label("Is Verified")
392
537
  })
393
538
  ).optional().label("Contacts"),
394
539
  verified: date().optional().label("Verified"),
395
- logo: object10().shape({
396
- dark: string7().optional().nullable().label("Dark Logo"),
397
- light: string7().required().label("Light Logo")
540
+ logo: object12().shape({
541
+ dark: string8().optional().nullable().label("Dark Logo"),
542
+ light: string8().required().label("Light Logo")
398
543
  }).optional().nullable().default(null).label("Logo"),
399
544
  // Taxonomy (see job-taxonomy.constant.ts): category -> industry -> subCategory,
400
545
  // stored as slugs and translated for display. As on JobSchema, these are NOT
@@ -402,21 +547,21 @@ var PageSchema = object10().shape({
402
547
  // never a reason to reject the whole page. `categories` is the top level (kept
403
548
  // required for backward compatibility); `industries` and `subCategories` are the
404
549
  // deeper levels the tree picker fills in when a leaf auto-selects its parents.
405
- categories: array5().of(string7().trim().required()).required().min(1).label("Categories"),
406
- industries: array5().of(string7().trim().required()).optional().default([]).label("Industries"),
407
- subCategories: array5().of(string7().trim().required()).optional().default([]).label("Sub Categories"),
408
- socialAccounts: array5().of(SocialAccountSchema).optional().label("Social Accounts"),
409
- isOwner: boolean7().optional().label("Is Owner"),
410
- isFeatured: boolean7().optional().default(false).label("Is Featured"),
411
- featuredMarkets: array5().of(string7().required()).optional().default([]).label("Featured Markets"),
412
- status: string7().oneOf(SupportedPageStatuses).required().label("Status"),
413
- followersCount: number4().min(0).optional().label("Followers Count")
550
+ categories: array6().of(string8().trim().required()).required().min(1).label("Categories"),
551
+ industries: array6().of(string8().trim().required()).optional().default([]).label("Industries"),
552
+ subCategories: array6().of(string8().trim().required()).optional().default([]).label("Sub Categories"),
553
+ socialAccounts: array6().of(SocialAccountSchema).optional().label("Social Accounts"),
554
+ isOwner: boolean9().optional().label("Is Owner"),
555
+ isFeatured: boolean9().optional().default(false).label("Is Featured"),
556
+ featuredMarkets: array6().of(string8().required()).optional().default([]).label("Featured Markets"),
557
+ status: string8().oneOf(SupportedPageStatuses).required().label("Status"),
558
+ followersCount: number5().min(0).optional().label("Followers Count")
414
559
  }).concat(CompanySchema).concat(InstituteSchema).concat(DbDefaultSchema).noUnknown(true).strict(true).required().label("Page Schema");
415
560
 
416
561
  // src/skill/skill.schema.ts
417
- import { array as array6, object as object11, string as string8 } from "yup";
418
- var SkillSchema = object11({
419
- name: string8().trim().required().label("Skill Name"),
562
+ import { array as array7, object as object13, string as string9 } from "yup";
563
+ var SkillSchema = object13({
564
+ name: string9().trim().required().label("Skill Name"),
420
565
  /**
421
566
  * The taxonomy concept this skill resolved to, assigned by thejob-taxonomy-service
422
567
  * via `POST /normalize/batch`. Stable slug, e.g. "power-bi".
@@ -434,14 +579,14 @@ var SkillSchema = object11({
434
579
  *
435
580
  * Consumers must therefore read `conceptName ?? name`, never `conceptName` alone.
436
581
  */
437
- conceptId: string8().optional().nullable().label("Concept ID"),
582
+ conceptId: string9().optional().nullable().label("Concept ID"),
438
583
  /** Canonical display name for `conceptId`, e.g. "Power BI". See `conceptId`. */
439
- conceptName: string8().optional().nullable().label("Concept Name"),
440
- logo: object11({
441
- light: string8().required().label("Light Logo"),
442
- dark: string8().optional().nullable().label("Dark Logo")
584
+ conceptName: string9().optional().nullable().label("Concept Name"),
585
+ logo: object13({
586
+ light: string9().required().label("Light Logo"),
587
+ dark: string9().optional().nullable().label("Dark Logo")
443
588
  }).optional().nullable().default(null).label("Logo"),
444
- tags: array6().of(string8().max(50)).max(20).optional().label("Tags")
589
+ tags: array7().of(string9().max(50)).max(20).optional().label("Tags")
445
590
  }).concat(DbDefaultSchema).noUnknown().strict().label("Skill");
446
591
 
447
592
  // src/question/question.constant.ts
@@ -460,24 +605,24 @@ var AnswerChoiceType = /* @__PURE__ */ ((AnswerChoiceType2) => {
460
605
  var SupportedAnswerChoiceTypes = Object.values(AnswerChoiceType);
461
606
 
462
607
  // src/question/question.schema.ts
463
- import { boolean as boolean8, object as object12, string as string9 } from "yup";
464
- var QuestionSchema = object12().shape({
465
- label: string9().max(50).optional().label("Label"),
466
- default: boolean8().optional().default(false).label("Default"),
467
- title: string9().required().max(200).label("Title"),
468
- type: string9().oneOf(SupportedQuestionTypes).required().label("Type"),
469
- isOptional: boolean8().optional().label("Mark if the question is optional"),
470
- readonly: boolean8().optional().label("Readonly"),
471
- isNew: boolean8().optional().label("Is New")
608
+ import { boolean as boolean10, object as object14, string as string10 } from "yup";
609
+ var QuestionSchema = object14().shape({
610
+ label: string10().max(50).optional().label("Label"),
611
+ default: boolean10().optional().default(false).label("Default"),
612
+ title: string10().required().max(200).label("Title"),
613
+ type: string10().oneOf(SupportedQuestionTypes).required().label("Type"),
614
+ isOptional: boolean10().optional().label("Mark if the question is optional"),
615
+ readonly: boolean10().optional().label("Readonly"),
616
+ isNew: boolean10().optional().label("Is New")
472
617
  });
473
618
 
474
619
  // src/question/input-question.schema.ts
475
- import { object as object13, string as string10 } from "yup";
620
+ import { object as object15, string as string11 } from "yup";
476
621
  var InputQuestionSchema = QuestionSchema.concat(
477
- object13().shape({
478
- type: string10().oneOf(["input" /* Input */]).required().label("Question type"),
479
- preferredAnswer: string10().max(100).optional().label("Preferred Answer"),
480
- answers: string10().max(500).when("isOptional", {
622
+ object15().shape({
623
+ type: string11().oneOf(["input" /* Input */]).required().label("Question type"),
624
+ preferredAnswer: string11().max(100).optional().label("Preferred Answer"),
625
+ answers: string11().max(500).when("isOptional", {
481
626
  is: (isOptional) => Boolean(isOptional),
482
627
  then: (schema) => schema.optional(),
483
628
  otherwise: (schema) => schema.required()
@@ -486,10 +631,10 @@ var InputQuestionSchema = QuestionSchema.concat(
486
631
  );
487
632
 
488
633
  // src/question/choice-question.schema.ts
489
- import { array as array7, mixed as mixed2, object as object14, string as string11 } from "yup";
634
+ import { array as array8, mixed as mixed2, object as object16, string as string12 } from "yup";
490
635
  var ChoicePreferredAnswerSchema = (label = "Preferred answer") => mixed2().when("answerChoiceType", {
491
636
  is: (value) => value === "multiple" /* Multiple */,
492
- then: () => array7(string11()).test(
637
+ then: () => array8(string12()).test(
493
638
  "is-valid-option",
494
639
  `${label} must be from the provided options`,
495
640
  function(answer) {
@@ -500,7 +645,7 @@ var ChoicePreferredAnswerSchema = (label = "Preferred answer") => mixed2().when(
500
645
  return true;
501
646
  }
502
647
  ).optional().label(label),
503
- otherwise: () => string11().test(
648
+ otherwise: () => string12().test(
504
649
  "is-valid-option",
505
650
  `${label} must be from the provided options`,
506
651
  function(answer) {
@@ -511,18 +656,18 @@ var ChoicePreferredAnswerSchema = (label = "Preferred answer") => mixed2().when(
511
656
  });
512
657
  var getChoiceAnswerSchema = (answerChoiceType, isOptional) => {
513
658
  if (answerChoiceType === "multiple" /* Multiple */) {
514
- const multiAnswerSchema = array7(string11().max(500));
659
+ const multiAnswerSchema = array8(string12().max(500));
515
660
  return isOptional ? multiAnswerSchema.optional().default([]) : multiAnswerSchema.min(1, "Select at least one option.").required("Select at least one option.").default([]);
516
661
  }
517
- const singleAnswerSchema = string11().max(500);
662
+ const singleAnswerSchema = string12().max(500);
518
663
  return isOptional ? singleAnswerSchema.optional() : singleAnswerSchema.required("Select an option.");
519
664
  };
520
665
  var ChoiceQuestionSchema = QuestionSchema.concat(
521
- object14().shape({
522
- type: string11().oneOf(["choice" /* Choice */]).required().label("Question type"),
523
- optionsFrom: string11().oneOf(["skill" /* Skill */, "education_level" /* EducationLevel */, "experience_level" /* ExperienceLevel */]).optional().label("Option field"),
524
- options: array7(string11().required()).min(1).required().label("Options"),
525
- answerChoiceType: string11().oneOf(SupportedAnswerChoiceTypes).required().default("single" /* Single */).label("Type"),
666
+ object16().shape({
667
+ type: string12().oneOf(["choice" /* Choice */]).required().label("Question type"),
668
+ optionsFrom: string12().oneOf(["skill" /* Skill */, "education_level" /* EducationLevel */, "experience_level" /* ExperienceLevel */]).optional().label("Option field"),
669
+ options: array8(string12().required()).min(1).required().label("Options"),
670
+ answerChoiceType: string12().oneOf(SupportedAnswerChoiceTypes).required().default("single" /* Single */).label("Type"),
526
671
  preferredAnswer: ChoicePreferredAnswerSchema("Preferred answer"),
527
672
  answers: mixed2().when(
528
673
  ["answerChoiceType", "isOptional"],
@@ -532,12 +677,12 @@ var ChoiceQuestionSchema = QuestionSchema.concat(
532
677
  );
533
678
 
534
679
  // src/question/read-and-acknowledge.schema.ts
535
- import { object as object15, string as string12 } from "yup";
536
- var AcknowledgeAnswerSchema = string12().oneOf(["yes" /* Yes */, "no" /* No */]).required("This question must be acknowledged.");
680
+ import { object as object17, string as string13 } from "yup";
681
+ var AcknowledgeAnswerSchema = string13().oneOf(["yes" /* Yes */, "no" /* No */]).required("This question must be acknowledged.");
537
682
  var ReadAndAcknowledgeQuestionSchema = QuestionSchema.concat(
538
- object15().shape({
539
- type: string12().oneOf(["readAndAcknowledge" /* ReadAndAcknowledge */]).required().label("Question type"),
540
- description: string12().max(1e4).required().label("Content"),
683
+ object17().shape({
684
+ type: string13().oneOf(["readAndAcknowledge" /* ReadAndAcknowledge */]).required().label("Question type"),
685
+ description: string13().max(1e4).required().label("Content"),
541
686
  preferredAnswer: AcknowledgeAnswerSchema.label("Preferred Answer"),
542
687
  answers: AcknowledgeAnswerSchema.label("Answer")
543
688
  })
@@ -618,20 +763,20 @@ var SupportedApplicationReceivePreferences = Object.values(
618
763
  );
619
764
 
620
765
  // src/job/job.schema.ts
621
- var JobSchema = object16({
766
+ var JobSchema = object18({
622
767
  /** Unique id usable by external systems to reference the job. */
623
- uniqueId: string13().optional().label("Unique ID"),
768
+ uniqueId: string14().optional().label("Unique ID"),
624
769
  /** Job title shown in listings and search. */
625
- headline: string13().required().min(5).max(150).label("Headline"),
770
+ headline: string14().required().min(5).max(150).label("Headline"),
626
771
  /** User-friendly, unique URL identifier. */
627
- slug: string13().required().max(250).label("Slug"),
772
+ slug: string14().required().max(250).label("Slug"),
628
773
  /** How applications are received (inbox vs external website). */
629
774
  applicationReceivePreference: mixed3().oneOf([
630
775
  "inbox" /* Inbox */,
631
776
  "external_website" /* ExternalWebsite */
632
777
  ]).required().label("Preference"),
633
778
  /** Required when applicationReceivePreference is ExternalWebsite. */
634
- externalApplyLink: string13().when("applicationReceivePreference", {
779
+ externalApplyLink: string14().when("applicationReceivePreference", {
635
780
  is: (value) => value === "external_website" /* ExternalWebsite */,
636
781
  then: (schema) => schema.url().required().label("Apply for Job link"),
637
782
  otherwise: (schema) => schema.optional()
@@ -639,35 +784,35 @@ var JobSchema = object16({
639
784
  /** Company the job belongs to (subset of PageSchema). */
640
785
  company: PageSchema.pick(["name", "slug", "type", "logo"]).deepPartial().concat(PageSchema.pick(["name", "type"])).required().label("Company"),
641
786
  /** @deprecated kept for backwards compatibility. */
642
- designation: string13().optional().nullable().label("Designation"),
643
- employmentType: string13().required().label("Employment type"),
644
- workMode: string13().required().label("Work mode"),
787
+ designation: string14().optional().nullable().label("Designation"),
788
+ employmentType: string14().required().label("Employment type"),
789
+ workMode: string14().required().label("Work mode"),
645
790
  /**
646
791
  * `conceptId`/`conceptName` carry the taxonomy-service normalization. They are
647
792
  * picked here (not just `name`/`logo`) because SkillSchema is `.noUnknown()`:
648
793
  * an unpicked field that is actually PRESENT fails validation, so omitting them
649
794
  * would reject every normalized job.
650
795
  */
651
- skills: array8().of(
796
+ skills: array9().of(
652
797
  SkillSchema.pick(["name", "logo", "conceptId", "conceptName"]).required().label("Skill")
653
798
  ).nullable().optional().label("Skills"),
654
- bookmarks: array8().of(UserIdAndCreatedAtSchema).optional().label("Bookmarks"),
655
- embedding: object16({
656
- vector: array8(number5().required()).optional().label("Embedding vector"),
657
- model: string13().optional().label("Embedding model")
799
+ bookmarks: array9().of(UserIdAndCreatedAtSchema).optional().label("Bookmarks"),
800
+ embedding: object18({
801
+ vector: array9(number6().required()).optional().label("Embedding vector"),
802
+ model: string14().optional().label("Embedding model")
658
803
  }).nullable().optional().label("Embedding"),
659
- applicants: array8().of(UserIdAndCreatedAtSchema).optional().label("Applicants"),
660
- applicantCount: number5().optional().label("Applicant count"),
661
- externalApplicationLinkClickCount: number5().optional().label("External application link click count"),
662
- description: string13().required().min(100).max(5e4).label("Description"),
663
- descriptionMarkdown: string13().required().label("Description Markdown"),
664
- descriptionHTML: string13().optional().label("Description HTML"),
804
+ applicants: array9().of(UserIdAndCreatedAtSchema).optional().label("Applicants"),
805
+ applicantCount: number6().optional().label("Applicant count"),
806
+ externalApplicationLinkClickCount: number6().optional().label("External application link click count"),
807
+ description: string14().required().min(100).max(5e4).label("Description"),
808
+ descriptionMarkdown: string14().required().label("Description Markdown"),
809
+ descriptionHTML: string14().optional().label("Description HTML"),
665
810
  /**
666
811
  * Job locations. Required even for remote roles (compliance/legal). Defaults
667
812
  * to an empty array; min(1) is intentionally not enforced yet.
668
813
  */
669
- locations: array8().of(LocationSchema).default([]).label("Location"),
670
- educationLevel: array8(string13().oneOf(SupportedEducationLevels)).required().min(1).label("Educational level"),
814
+ locations: array9().of(LocationSchema).default([]).label("Location"),
815
+ educationLevel: array9(string14().oneOf(SupportedEducationLevels)).required().min(1).label("Educational level"),
671
816
  /** Posting validity window (start/expiry). */
672
817
  validity: DurationSchema({
673
818
  format: SYSTEM_DATE_FORMAT,
@@ -680,21 +825,21 @@ var JobSchema = object16({
680
825
  * has no `validity.endDate` and must be verified by crawling its page. Guards
681
826
  * re-check cadence so the same job is not re-crawled on every sweep.
682
827
  */
683
- lastExpiryCheckAt: number5().optional().nullable().label("Last expiry check at"),
828
+ lastExpiryCheckAt: number6().optional().nullable().label("Last expiry check at"),
684
829
  /**
685
830
  * When this job transitioned to `status: closed` (epoch ms). Server-owned
686
831
  * operational field: stamped by every jobs-service close path (expiry
687
832
  * auto-close and the dated lapse sweep). Distinct from `updatedAt` so a later
688
833
  * edit to a closed job does not move it.
689
834
  */
690
- closedAt: number5().optional().nullable().label("Closed at"),
691
- experienceLevel: string13().oneOf(SupportedExperienceLevels).nullable().optional().label("Experience level"),
692
- contactEmail: string13().email().nullable().optional().label("Contact email"),
693
- workingHoursPerWeek: number5().nullable().optional().min(1).max(24 * 7).label("Working hours per week"),
694
- tags: array8().max(50).optional().label("Tags"),
835
+ closedAt: number6().optional().nullable().label("Closed at"),
836
+ experienceLevel: string14().oneOf(SupportedExperienceLevels).nullable().optional().label("Experience level"),
837
+ contactEmail: string14().email().nullable().optional().label("Contact email"),
838
+ workingHoursPerWeek: number6().nullable().optional().min(1).max(24 * 7).label("Working hours per week"),
839
+ tags: array9().max(50).optional().label("Tags"),
695
840
  /** Direct/Quick-apply questionnaire. Each item is validated against the
696
841
  * concrete question schema for its `type`, with the stored `answers` omitted. */
697
- questionnaire: array8().of(
842
+ questionnaire: array9().of(
698
843
  lazy((question) => {
699
844
  const schema = getSchemaByQuestion(question.type);
700
845
  if (question.type === "input" /* Input */) {
@@ -711,12 +856,12 @@ var JobSchema = object16({
711
856
  return schema;
712
857
  })
713
858
  ).label("Questionnaire"),
714
- status: string13().oneOf(SupportedJobStatuses).default("draft" /* Draft */).required().label("Status"),
715
- seoTags: object16({
716
- description: string13().optional().label("Description"),
717
- keywords: array8().of(string13()).optional().label("Keywords")
859
+ status: string14().oneOf(SupportedJobStatuses).default("draft" /* Draft */).required().label("Status"),
860
+ seoTags: object18({
861
+ description: string14().optional().label("Description"),
862
+ keywords: array9().of(string14()).optional().label("Keywords")
718
863
  }).nullable().optional().label("SEO Tags"),
719
- jdSummary: string13().optional().label("JD Summary"),
864
+ jdSummary: string14().optional().label("JD Summary"),
720
865
  /**
721
866
  * Structured, model-extracted sections of the posting, re-organized into clean
722
867
  * bullet points. This is our OWN unique presentation of the job (rendered as the
@@ -725,28 +870,28 @@ var JobSchema = object16({
725
870
  * optional: absent on jobs not yet (re-)enriched, and any section is `[]`/omitted
726
871
  * when the posting genuinely has none.
727
872
  */
728
- jobHighlights: object16({
729
- responsibilities: array8().of(string13()).optional().label("Responsibilities"),
730
- requirements: array8().of(string13()).optional().label("Requirements"),
731
- benefits: array8().of(string13()).optional().label("Benefits"),
732
- qualifications: array8().of(string13()).optional().label("Qualifications")
873
+ jobHighlights: object18({
874
+ responsibilities: array9().of(string14()).optional().label("Responsibilities"),
875
+ requirements: array9().of(string14()).optional().label("Requirements"),
876
+ benefits: array9().of(string14()).optional().label("Benefits"),
877
+ qualifications: array9().of(string14()).optional().label("Qualifications")
733
878
  }).nullable().optional().label("Job Highlights"),
734
- canCreateAlert: boolean9().optional().label("Can create alert"),
735
- canDirectApply: boolean9().optional().label("Can direct apply"),
736
- hasApplied: boolean9().optional().label("Has applied"),
737
- isOwner: boolean9().optional().label("Is owner"),
738
- hasBookmarked: boolean9().optional().label("Has bookmarked"),
739
- hasReported: boolean9().optional().label("Has reported"),
740
- ratePerHour: number5().optional().min(0).label("Rate per hour"),
741
- isPromoted: boolean9().optional().label("Is promoted"),
742
- salaryRange: object16({
743
- currency: string13().required().label("Currency").default("USD"),
744
- min: number5().nullable().optional().min(0).label("Minimum Salary"),
745
- max: number5().nullable().optional().min(0).label("Maximum Salary"),
746
- compensationType: string13().oneOf(SupportedCompensationTypes).nullable().optional().label("Compensation Type"),
747
- isNegotiable: boolean9().nullable().optional().default(true).label("Is Negotiable")
879
+ canCreateAlert: boolean11().optional().label("Can create alert"),
880
+ canDirectApply: boolean11().optional().label("Can direct apply"),
881
+ hasApplied: boolean11().optional().label("Has applied"),
882
+ isOwner: boolean11().optional().label("Is owner"),
883
+ hasBookmarked: boolean11().optional().label("Has bookmarked"),
884
+ hasReported: boolean11().optional().label("Has reported"),
885
+ ratePerHour: number6().optional().min(0).label("Rate per hour"),
886
+ isPromoted: boolean11().optional().label("Is promoted"),
887
+ salaryRange: object18({
888
+ currency: string14().required().label("Currency").default("USD"),
889
+ min: number6().nullable().optional().min(0).label("Minimum Salary"),
890
+ max: number6().nullable().optional().min(0).label("Maximum Salary"),
891
+ compensationType: string14().oneOf(SupportedCompensationTypes).nullable().optional().label("Compensation Type"),
892
+ isNegotiable: boolean11().nullable().optional().default(true).label("Is Negotiable")
748
893
  }).nullable().optional().label("Salary Range"),
749
- reports: array8().of(UserIdAndCreatedAtSchema).optional().label("Reports"),
894
+ reports: array9().of(UserIdAndCreatedAtSchema).optional().label("Reports"),
750
895
  // Taxonomy fields are constants (see job-taxonomy.constant.ts), stored as slugs and
751
896
  // translated for display. They are deliberately NOT `oneOf`-constrained here: a value
752
897
  // outside the taxonomy must be DROPPED, never a reason to reject the posting. Yup
@@ -759,15 +904,135 @@ var JobSchema = object16({
759
904
  // time (values get renamed, retired, added), so stored jobs will always outlive the
760
905
  // enum — being lenient here is what lets the taxonomy evolve without a migration
761
906
  // gate on every write.
762
- category: string13().nullable().optional().label("Category"),
763
- industry: string13().nullable().optional().label("Industry"),
764
- subCategories: array8().of(string13().trim().required()).nullable().optional().label("Sub Categories"),
765
- termsAccepted: boolean9().oneOf([true], "Accept terms before proceeding").required("Accept terms before proceeding").label("Terms accepted"),
766
- isFeatured: boolean9().optional().default(false).label("Is featured"),
907
+ category: string14().nullable().optional().label("Category"),
908
+ industry: string14().nullable().optional().label("Industry"),
909
+ subCategories: array9().of(string14().trim().required()).nullable().optional().label("Sub Categories"),
910
+ termsAccepted: boolean11().oneOf([true], "Accept terms before proceeding").required("Accept terms before proceeding").label("Terms accepted"),
911
+ isFeatured: boolean11().optional().default(false).label("Is featured"),
767
912
  /** Market country codes to feature this job in; empty = global. */
768
- featuredMarkets: array8().of(string13().required()).optional().default([]).label("Featured markets")
913
+ featuredMarkets: array9().of(string14().required()).optional().default([]).label("Featured markets")
769
914
  }).concat(DbDefaultSchema).noUnknown().label("Job");
770
915
 
916
+ // src/job/job-filter.schema.ts
917
+ import { boolean as boolean12, number as number7, object as object19, string as string15 } from "yup";
918
+
919
+ // src/user/user.constant.ts
920
+ var UserRole = /* @__PURE__ */ ((UserRole2) => {
921
+ UserRole2["Admin"] = "admin";
922
+ UserRole2["Moderator"] = "moderator";
923
+ UserRole2["Manager"] = "manager";
924
+ UserRole2["Recruiter"] = "recruiter";
925
+ UserRole2["JobSeeker"] = "job_seeker";
926
+ UserRole2["Coordinator"] = "coordinator";
927
+ UserRole2["System"] = "system";
928
+ UserRole2["Normal"] = "normal";
929
+ return UserRole2;
930
+ })(UserRole || {});
931
+ var SupportedUserRoles = Object.values(UserRole);
932
+ var UserStatus = /* @__PURE__ */ ((UserStatus2) => {
933
+ UserStatus2["Active"] = "active";
934
+ UserStatus2["Blocked"] = "blocked";
935
+ UserStatus2["Pending"] = "pending";
936
+ UserStatus2["Deleted"] = "deleted";
937
+ UserStatus2["MarkedForDeletion"] = "marked_for_deletion";
938
+ return UserStatus2;
939
+ })(UserStatus || {});
940
+ var SupportedUserStatuses = Object.values(UserStatus);
941
+ var DefaultUserRoles = ["normal" /* Normal */];
942
+ var UserProfileVisibility = /* @__PURE__ */ ((UserProfileVisibility2) => {
943
+ UserProfileVisibility2["Public"] = "public";
944
+ UserProfileVisibility2["Private"] = "private";
945
+ UserProfileVisibility2["Hidden"] = "hidden";
946
+ return UserProfileVisibility2;
947
+ })(UserProfileVisibility || {});
948
+ var SupportedUserProfileVisibilities = Object.values(
949
+ UserProfileVisibility
950
+ );
951
+ var SignupContextSource = /* @__PURE__ */ ((SignupContextSource2) => {
952
+ SignupContextSource2["CfHeader"] = "cf-header";
953
+ SignupContextSource2["Location"] = "location";
954
+ SignupContextSource2["Default"] = "default";
955
+ return SignupContextSource2;
956
+ })(SignupContextSource || {});
957
+ var SupportedSignupContextSources = Object.values(SignupContextSource);
958
+ var UserDetailType = /* @__PURE__ */ ((UserDetailType2) => {
959
+ UserDetailType2["Overview"] = "overview";
960
+ UserDetailType2["AdditionalInfo"] = "additionalInfo";
961
+ UserDetailType2["General"] = "general";
962
+ UserDetailType2["SocialAccounts"] = "socialAccounts";
963
+ UserDetailType2["WorkExperiences"] = "workExperiences";
964
+ UserDetailType2["Educations"] = "educations";
965
+ UserDetailType2["Skills"] = "skills";
966
+ UserDetailType2["Languages"] = "languages";
967
+ UserDetailType2["Certifications"] = "certifications";
968
+ UserDetailType2["Interests"] = "interests";
969
+ UserDetailType2["Projects"] = "projects";
970
+ return UserDetailType2;
971
+ })(UserDetailType || {});
972
+ var ProficiencyLevel = /* @__PURE__ */ ((ProficiencyLevel2) => {
973
+ ProficiencyLevel2["BasicAwareness"] = "basic_awareness";
974
+ ProficiencyLevel2["Beginner"] = "beginner";
975
+ ProficiencyLevel2["Intermediate"] = "intermediate";
976
+ ProficiencyLevel2["Advanced"] = "advanced";
977
+ ProficiencyLevel2["Expert"] = "expert";
978
+ return ProficiencyLevel2;
979
+ })(ProficiencyLevel || {});
980
+ var SupportedProficiencyLevels = Object.values(ProficiencyLevel);
981
+ var JobSearchUrgency = /* @__PURE__ */ ((JobSearchUrgency2) => {
982
+ JobSearchUrgency2["Asap"] = "asap";
983
+ JobSearchUrgency2["WithinThreeMonths"] = "within_3_months";
984
+ JobSearchUrgency2["WithinSixMonths"] = "within_6_months";
985
+ JobSearchUrgency2["PassivelyBrowsing"] = "passively_browsing";
986
+ return JobSearchUrgency2;
987
+ })(JobSearchUrgency || {});
988
+ var SupportedJobSearchUrgencies = Object.values(JobSearchUrgency);
989
+ var ReferralSource = /* @__PURE__ */ ((ReferralSource2) => {
990
+ ReferralSource2["Recruiter"] = "recruiter";
991
+ ReferralSource2["LinkedIn"] = "linkedin";
992
+ ReferralSource2["TikTokInstagram"] = "tiktok_instagram";
993
+ ReferralSource2["Google"] = "google";
994
+ ReferralSource2["Reddit"] = "reddit";
995
+ ReferralSource2["ChatGptGemini"] = "chatgpt_gemini";
996
+ ReferralSource2["Friend"] = "friend";
997
+ ReferralSource2["Other"] = "other";
998
+ return ReferralSource2;
999
+ })(ReferralSource || {});
1000
+ var SupportedReferralSources = Object.values(ReferralSource);
1001
+ var JobAlertFrequency = /* @__PURE__ */ ((JobAlertFrequency2) => {
1002
+ JobAlertFrequency2["Off"] = "off";
1003
+ JobAlertFrequency2["Daily"] = "daily";
1004
+ JobAlertFrequency2["Weekly"] = "weekly";
1005
+ return JobAlertFrequency2;
1006
+ })(JobAlertFrequency || {});
1007
+ var SupportedJobAlertFrequencies = Object.values(JobAlertFrequency);
1008
+ var NotificationType = /* @__PURE__ */ ((NotificationType2) => {
1009
+ NotificationType2["GroupActivity"] = "groupActivity";
1010
+ NotificationType2["JobActivity"] = "jobActivity";
1011
+ NotificationType2["ApplicationUpdates"] = "applicationUpdates";
1012
+ NotificationType2["ProductUpdates"] = "productUpdates";
1013
+ return NotificationType2;
1014
+ })(NotificationType || {});
1015
+ var SupportedNotificationTypes = Object.values(NotificationType);
1016
+ var SupportedSalaryCurrencies = ["USD", "EUR", "GBP", "SEK", "INR"];
1017
+ var MIN_SALARY_LOWER_BOUND = 0;
1018
+ var MIN_SALARY_UPPER_BOUND = 1e6;
1019
+ var PREFILL_MIRRORED_KEYS = [
1020
+ "headline",
1021
+ "aboutMe",
1022
+ "mobile",
1023
+ "location",
1024
+ "experienceLevel",
1025
+ "socialAccounts",
1026
+ "workExperiences",
1027
+ "educations"
1028
+ ];
1029
+ var PRIVATE_PROFILE_FIELDS = [
1030
+ "email",
1031
+ "emailVerified",
1032
+ "mobile",
1033
+ "additionalInfo"
1034
+ ];
1035
+
771
1036
  // src/job/job-taxonomy.constant.ts
772
1037
  var JobCategory = /* @__PURE__ */ ((JobCategory2) => {
773
1038
  JobCategory2["TechnologyDataAndDigital"] = "technology_data_and_digital";
@@ -1835,6 +2100,185 @@ var TAXONOMY_LABELS = {
1835
2100
  "fleet_management": "Fleet Management"
1836
2101
  };
1837
2102
 
2103
+ // src/job/job-filter.schema.ts
2104
+ var GEO_GROUP = "Geo";
2105
+ var JobFilterShape = object19({
2106
+ // ── Classification ──────────────────────────────────────────────────────────
2107
+ // Closed vocabularies, sent verbatim and matched exactly: a partial match
2108
+ // would make "remote" also match a hypothetical "remote_first".
2109
+ employmentType: enumFacet(SupportedEmploymentTypes, "Employment Type"),
2110
+ workMode: enumFacet(SupportedWorkModes, "Work Mode"),
2111
+ experienceLevel: enumFacet(SupportedExperienceLevels, "Experience Level"),
2112
+ educationLevel: enumFacet(SupportedEducationLevels, "Education Level"),
2113
+ status: enumFacet(SupportedJobStatuses, "Status"),
2114
+ // ── Taxonomy ────────────────────────────────────────────────────────────────
2115
+ // NOT `enumFacet`, deliberately. Taxonomy is stored as the ENGLISH LABEL
2116
+ // ("Technology, Data & Digital"), not the enum constant, so a caller sends the
2117
+ // label and constraining these to `SupportedJobCategories` would reject every
2118
+ // value that actually matches.
2119
+ //
2120
+ // `meta.options` publishes the vocabulary anyway, for forms only. The schema
2121
+ // still accepts any string — it must, per the note above — but an admin
2122
+ // picking one of eight categories should not be typing it: the stored label
2123
+ // is "Technology, Data & Digital", and a hand-typed "Technology" matches
2124
+ // nothing while looking correctly configured.
2125
+ category: tokens("Category").meta({ options: SupportedJobCategories }),
2126
+ industry: tokens("Industry").meta({ options: SupportedJobIndustries }),
2127
+ subCategories: tokens("Sub-category").meta({
2128
+ options: SupportedJobSubCategories
2129
+ }),
2130
+ tags: tokens("Tags"),
2131
+ // ── Identity ────────────────────────────────────────────────────────────────
2132
+ /**
2133
+ * Specific jobs, by `shortId`. The one facet that names jobs outright rather
2134
+ * than narrowing by an attribute of them.
2135
+ *
2136
+ * It exists for a HAND-PICKED set: a group whose jobs an admin chose
2137
+ * individually stores exactly this list as `group.jobIds`, so the field and
2138
+ * the facet share a name and the stored data needs no translation.
2139
+ *
2140
+ * Case is LOAD-BEARING and must not be normalised: a shortId is mixed case
2141
+ * (`tI8wnSIgRG5O`), so the uppercase treatment country and state codes get
2142
+ * would turn every lookup into a match on nothing.
2143
+ */
2144
+ jobIds: tokens("Jobs"),
2145
+ // ── Skills and company ──────────────────────────────────────────────────────
2146
+ //
2147
+ // `widget` is a RENDERING hint for whoever builds a form from this schema: it
2148
+ // names the picker that produces values this facet can actually match, and
2149
+ // nothing validates differently because of it. A form generated without one
2150
+ // falls back to a free text box, which is what every admin filter was doing —
2151
+ // so "Angular" was typed where the stored skill is "angular" and the filter
2152
+ // narrowed to nothing while looking correctly configured.
2153
+ skills: tokens("Skills").meta({ widget: "skill" }),
2154
+ /** Company page SLUGS: a display name breaks the day a company renames itself. */
2155
+ companies: tokens("Companies").meta({ widget: "company" }),
2156
+ /**
2157
+ * Company display names, for callers that only have one. Prefer `companies`.
2158
+ *
2159
+ * `hidden`: not a second control in a generated form. An admin picking a
2160
+ * company gets the page, and `companies` (its slug) is the stable way to name
2161
+ * it — a display name stops matching the day a company renames itself. This
2162
+ * stays in the vocabulary for API callers who genuinely only hold a name.
2163
+ */
2164
+ companyNames: tokens("Company Names").meta({ widget: "hidden" }),
2165
+ // ── Location ────────────────────────────────────────────────────────────────
2166
+ //
2167
+ // Four facets, THREE controls. They are independent OR-lists: jobs-service
2168
+ // matches each path separately, so `countryCodes` alone asks "anywhere in
2169
+ // India" and `cities` alone asks "in Pune, wherever that is". Collapsing them
2170
+ // into one object facet would lose exactly that, and lose the cross-entry
2171
+ // reading ("has an India office OR a London office") with it.
2172
+ //
2173
+ // What they must NOT be is free text. A country here is `IN`, not "India",
2174
+ // and a state code is a BARE `KA`, not `IN-KA` — type either long form and
2175
+ // the filter matches nothing while looking correctly configured.
2176
+ //
2177
+ // So each names the GEO reference autocomplete (common-service `/geo/*`, a
2178
+ // closed vocabulary) and the field of the picked row it stores. `geoLevel`
2179
+ // says which endpoint to search and `geoField` which key lands in the facet.
2180
+ //
2181
+ // THREE controls, not four: a picked state carries its name and its code, so
2182
+ // the old separate "State Codes" box asked twice for one fact. See `states`.
2183
+ //
2184
+ // Geoapify's `/locations` is NOT the vocabulary here. It geocodes free-text
2185
+ // addresses — the right tool for "where is this job", the wrong one for a
2186
+ // filter, since it is a paid call per keystroke and its results are open.
2187
+ /** ISO 3166-1 alpha-2, uppercase. */
2188
+ countryCodes: codeTokens("Countries").meta({
2189
+ group: GEO_GROUP,
2190
+ widget: "geo",
2191
+ geoLevel: "country",
2192
+ geoField: "countryCode"
2193
+ }),
2194
+ /** Stored BARE (`KA`, not `IN-KA`) — see @todo STATE-CODE-ISO-PREFIX. */
2195
+ stateCodes: codeTokens("States").meta({
2196
+ group: GEO_GROUP,
2197
+ widget: "geo",
2198
+ geoLevel: "state",
2199
+ geoField: "stateCode"
2200
+ }),
2201
+ /**
2202
+ * State NAMES, kept as a facet but WITHOUT a control of its own.
2203
+ *
2204
+ * There is no second box to fill: a picked state carries its name and its
2205
+ * code together, so asking an admin for both was asking twice for one fact.
2206
+ * The picker writes `stateCodes`, which is where the data is going.
2207
+ *
2208
+ * The facet stays because jobs still hold state names — 15,483 carry a name
2209
+ * with NO code — and a saved campaign may already constrain on it. Removing
2210
+ * it would silently drop that constraint from those campaigns. It becomes
2211
+ * unreachable from the UI and is settled by the data migration.
2212
+ *
2213
+ * @todo STATE-CODE-ISO-PREFIX — retire once every job carries a stateCode.
2214
+ */
2215
+ states: tokens("States").meta({ group: GEO_GROUP, widget: "hidden" }),
2216
+ /**
2217
+ * City NAMES, and the one facet here that does not match on a code.
2218
+ *
2219
+ * A name is a display label, not a matching key: it is not unique (99 Indian
2220
+ * city names repeat in the reference data) and not stable (Bengaluru was
2221
+ * Bangalore). Every other facet here matches a code for exactly that reason.
2222
+ *
2223
+ * Cities cannot yet. There is no city code to match ON either side: the
2224
+ * reference dataset carries none, and no job stores one — `LocationSchema`
2225
+ * has country/state codes and a city NAME. A `cityCodes` facet would match
2226
+ * zero of 82,875 jobs on day one.
2227
+ *
2228
+ * So this stays on the name, and the picker does the work a code would: the
2229
+ * admin selects a real city from the reference data, so the stored string is
2230
+ * guaranteed to be a spelling that exists rather than a typo that silently
2231
+ * matches nothing.
2232
+ *
2233
+ * @todo CITY-CODE-UNLOCODE — the fix is UN/LOCODE (or IATA) as the city key,
2234
+ * and it is a data programme, not a facet change: ingest and reconcile the
2235
+ * codes against the 152,970 reference rows, add `cityCode` to
2236
+ * `LocationSchema`, backfill the ~81,000 jobs that carry only a name, and fix
2237
+ * the write paths that emit it. Only then can this match on a code.
2238
+ */
2239
+ cities: tokens("Cities").meta({
2240
+ group: GEO_GROUP,
2241
+ widget: "geo",
2242
+ geoLevel: "city",
2243
+ geoField: "name"
2244
+ }),
2245
+ // ── Flags ───────────────────────────────────────────────────────────────────
2246
+ // No `.default(false)`: an unchecked box must mean "no constraint", not "only
2247
+ // the non-featured ones", so absent simply omits the clause.
2248
+ isFeatured: boolean12().optional().label("Featured Only"),
2249
+ isPromoted: boolean12().optional().label("Promoted Only"),
2250
+ featuredMarkets: tokens("Featured Markets"),
2251
+ // ── Ranges ──────────────────────────────────────────────────────────────────
2252
+ /**
2253
+ * Matched as an OVERLAP against the job's advertised band, not as a bound on
2254
+ * one end: `{min}` keeps jobs whose `salaryRange.max` reaches it.
2255
+ */
2256
+ // NOT `currencyBound`: that guards the USER filter, where several currencies
2257
+ // can be selected and a bound is meaningless across them. Here the currency is
2258
+ // a single sibling field, so the bound is plain and the currency simply
2259
+ // narrows which jobs are compared.
2260
+ salary: object19({
2261
+ min: number7().integer().min(0).optional().default(void 0).label("Salary \u2265"),
2262
+ max: number7().integer().min(0).optional().default(void 0).label("Salary \u2264"),
2263
+ currency: string15().oneOf(SupportedSalaryCurrencies).optional().label("Currency")
2264
+ }).optional().default(void 0).label("Salary"),
2265
+ /** Posted within this many days. Relative, so a saved filter does not rot. */
2266
+ postedWithinDays: days("Posted Within (days)"),
2267
+ /**
2268
+ * Absolute posting window, for callers that want bounds rather than the
2269
+ * relative `postedWithinDays`. EPOCH MS, matching how the field is stored — a
2270
+ * coerced date string would not compare.
2271
+ */
2272
+ createdAt: object19({
2273
+ min: epoch("Posted After"),
2274
+ max: epoch("Posted Before")
2275
+ }).optional().default(void 0).label("Posted Between")
2276
+ }).label("Job Filter");
2277
+ var JobFilterSchema = rejectUnknownKeys(
2278
+ JobFilterShape,
2279
+ () => Object.keys(JobFilterShape.fields)
2280
+ );
2281
+
1838
2282
  // src/job/job-taxonomy.util.ts
1839
2283
  var INDUSTRY_OF_SUB = new Map(
1840
2284
  JOB_TAXONOMY.flatMap(
@@ -1879,7 +2323,7 @@ function deriveIndustry(category, subCategories) {
1879
2323
  }
1880
2324
 
1881
2325
  // src/post/post.schema.ts
1882
- import { array as array9, boolean as boolean10, number as number6, object as object17, string as string14 } from "yup";
2326
+ import { array as array11, boolean as boolean13, number as number8, object as object20, string as string16 } from "yup";
1883
2327
 
1884
2328
  // src/post/post.constant.ts
1885
2329
  var PostStatus = /* @__PURE__ */ ((PostStatus2) => {
@@ -1891,177 +2335,540 @@ var PostStatus = /* @__PURE__ */ ((PostStatus2) => {
1891
2335
  var SupportedPostStatuses = Object.values(PostStatus);
1892
2336
 
1893
2337
  // src/post/post.schema.ts
1894
- var PostSchema = object17({
2338
+ var PostSchema = object20({
1895
2339
  /** Headline shown in listings and at the top of the post. */
1896
- title: string14().required().min(5).max(200).label("Title"),
2340
+ title: string16().required().min(5).max(200).label("Title"),
1897
2341
  /** User-friendly, unique URL identifier. */
1898
- slug: string14().required().max(250).label("Slug"),
2342
+ slug: string16().required().max(250).label("Slug"),
1899
2343
  /** Short summary used in listings and meta description fallback. */
1900
- excerpt: string14().max(500).optional().label("Excerpt"),
2344
+ excerpt: string16().max(500).optional().label("Excerpt"),
1901
2345
  /** Rendered HTML source of truth for the post body (sanitized on write/render).
1902
2346
  * Supports rich formatting markdown can't express (image size/alignment, etc). */
1903
- bodyHTML: string14().required().min(1).label("Body (HTML)"),
2347
+ bodyHTML: string16().required().min(1).label("Body (HTML)"),
1904
2348
  /** Optional Markdown mirror of the body (portable text / search fallback).
1905
2349
  * Kept for back-compat and plain-text derivation; bodyHTML is authoritative. */
1906
- bodyMarkdown: string14().optional().label("Body (Markdown)"),
2350
+ bodyMarkdown: string16().optional().label("Body (Markdown)"),
1907
2351
  /** Cover/hero image URL. Not `.url()`-validated: the value is either an
1908
2352
  * uploaded file URL served by this platform (which may be a bare host like
1909
2353
  * `http://localhost:3082/...` in dev, that yup's `.url()` wrongly rejects) or
1910
2354
  * an admin-pasted link. Kept as a plain string; empty ⇒ null. */
1911
- coverImage: string14().nullable().optional().label("Cover image"),
2355
+ coverImage: string16().nullable().optional().label("Cover image"),
1912
2356
  /** Author (user id). */
1913
- authorId: string14().required().label("Author ID"),
2357
+ authorId: string16().required().label("Author ID"),
1914
2358
  /** Free-form tags for filtering and related-posts. */
1915
- tags: array9().of(string14().trim().required()).default([]).label("Tags"),
2359
+ tags: array11().of(string16().trim().required()).default([]).label("Tags"),
1916
2360
  /** Estimated reading time in minutes (server-derived from body length). */
1917
- readingTimeMinutes: number6().min(0).optional().label("Reading time (minutes)"),
2361
+ readingTimeMinutes: number8().min(0).optional().label("Reading time (minutes)"),
1918
2362
  /** SEO overrides. */
1919
- seoTags: object17({
1920
- description: string14().optional().label("Description"),
1921
- keywords: array9().of(string14().required()).optional().label("Keywords")
2363
+ seoTags: object20({
2364
+ description: string16().optional().label("Description"),
2365
+ keywords: array11().of(string16().required()).optional().label("Keywords")
1922
2366
  }).nullable().optional().label("SEO Tags"),
1923
2367
  /** When the post was first published (epoch ms). Server-owned. */
1924
- publishedAt: number6().nullable().optional().label("Published at"),
2368
+ publishedAt: number8().nullable().optional().label("Published at"),
1925
2369
  /** Whether the post is featured in the blog listing. */
1926
- isFeatured: boolean10().default(false).optional().label("Is featured"),
2370
+ isFeatured: boolean13().default(false).optional().label("Is featured"),
1927
2371
  /** Publication state. */
1928
- status: string14().oneOf(SupportedPostStatuses).default("draft" /* Draft */).required().label("Status")
2372
+ status: string16().oneOf(SupportedPostStatuses).default("draft" /* Draft */).required().label("Status")
1929
2373
  }).concat(DbDefaultSchema).noUnknown().label("Post");
1930
2374
 
1931
- // src/marketing/marketing.schema.ts
1932
- import { array as array10, boolean as boolean11, lazy as lazy2, mixed as mixed4, number as number7, object as object18, string as string15 } from "yup";
2375
+ // src/geo/geo.constant.ts
2376
+ var GeoKind = /* @__PURE__ */ ((GeoKind2) => {
2377
+ GeoKind2["Country"] = "country";
2378
+ GeoKind2["State"] = "state";
2379
+ GeoKind2["City"] = "city";
2380
+ return GeoKind2;
2381
+ })(GeoKind || {});
2382
+ var SupportedGeoKinds = Object.values(GeoKind);
2383
+ var GeoSyncStatus = /* @__PURE__ */ ((GeoSyncStatus3) => {
2384
+ GeoSyncStatus3["Pending"] = "pending";
2385
+ GeoSyncStatus3["Running"] = "running";
2386
+ GeoSyncStatus3["Succeeded"] = "succeeded";
2387
+ GeoSyncStatus3["Failed"] = "failed";
2388
+ GeoSyncStatus3["Skipped"] = "skipped";
2389
+ return GeoSyncStatus3;
2390
+ })(GeoSyncStatus || {});
2391
+ var SupportedGeoSyncStatuses = Object.values(GeoSyncStatus);
2392
+ var TerminalGeoSyncStatuses = [
2393
+ "succeeded" /* Succeeded */,
2394
+ "failed" /* Failed */,
2395
+ "skipped" /* Skipped */
2396
+ ];
2397
+ var GeoSyncPhase = /* @__PURE__ */ ((GeoSyncPhase3) => {
2398
+ GeoSyncPhase3["Checking"] = "checking";
2399
+ GeoSyncPhase3["Countries"] = "countries";
2400
+ GeoSyncPhase3["States"] = "states";
2401
+ GeoSyncPhase3["Cities"] = "cities";
2402
+ GeoSyncPhase3["Indexing"] = "indexing";
2403
+ GeoSyncPhase3["Done"] = "done";
2404
+ return GeoSyncPhase3;
2405
+ })(GeoSyncPhase || {});
2406
+ var SupportedGeoSyncPhases = Object.values(GeoSyncPhase);
2407
+ var GeoSyncTrigger = /* @__PURE__ */ ((GeoSyncTrigger3) => {
2408
+ GeoSyncTrigger3["Manual"] = "manual";
2409
+ GeoSyncTrigger3["Boot"] = "boot";
2410
+ return GeoSyncTrigger3;
2411
+ })(GeoSyncTrigger || {});
2412
+ var SupportedGeoSyncTriggers = Object.values(GeoSyncTrigger);
2413
+ var GEO_COLLECTIONS = {
2414
+ countries: "geo_countries",
2415
+ states: "geo_states",
2416
+ cities: "geo_cities",
2417
+ syncRuns: "geo_sync_runs"
2418
+ };
2419
+ var GEO_SEARCH_MAX = 64;
1933
2420
 
1934
- // src/user/user.constant.ts
1935
- var UserRole = /* @__PURE__ */ ((UserRole2) => {
1936
- UserRole2["Admin"] = "admin";
1937
- UserRole2["Moderator"] = "moderator";
1938
- UserRole2["Manager"] = "manager";
1939
- UserRole2["Recruiter"] = "recruiter";
1940
- UserRole2["JobSeeker"] = "job_seeker";
1941
- UserRole2["Coordinator"] = "coordinator";
1942
- UserRole2["System"] = "system";
1943
- UserRole2["Normal"] = "normal";
1944
- return UserRole2;
1945
- })(UserRole || {});
1946
- var SupportedUserRoles = Object.values(UserRole);
1947
- var UserStatus = /* @__PURE__ */ ((UserStatus2) => {
1948
- UserStatus2["Active"] = "active";
1949
- UserStatus2["Blocked"] = "blocked";
1950
- UserStatus2["Pending"] = "pending";
1951
- UserStatus2["Deleted"] = "deleted";
1952
- UserStatus2["MarkedForDeletion"] = "marked_for_deletion";
1953
- return UserStatus2;
1954
- })(UserStatus || {});
1955
- var SupportedUserStatuses = Object.values(UserStatus);
1956
- var DefaultUserRoles = ["normal" /* Normal */];
1957
- var UserProfileVisibility = /* @__PURE__ */ ((UserProfileVisibility2) => {
1958
- UserProfileVisibility2["Public"] = "public";
1959
- UserProfileVisibility2["Private"] = "private";
1960
- UserProfileVisibility2["Hidden"] = "hidden";
1961
- return UserProfileVisibility2;
1962
- })(UserProfileVisibility || {});
1963
- var SupportedUserProfileVisibilities = Object.values(
1964
- UserProfileVisibility
2421
+ // src/geo/country.schema.ts
2422
+ import { array as array12, number as number9, object as object21, string as string17 } from "yup";
2423
+ var CountrySchema = object21({
2424
+ /** The upstream dr5hn id. Stable, and the key every upsert matches on. */
2425
+ id: number9().integer().required().label("ID"),
2426
+ name: string17().trim().required().label("Name"),
2427
+ /**
2428
+ * ISO 3166-1 alpha-2, UPPERCASE (`IN`, `SE`).
2429
+ *
2430
+ * Uppercased on ingest, because every filter facet in this package
2431
+ * uppercase-normalizes codes (`codeTokens`) and stored job/user documents are
2432
+ * already clean uppercase alpha-2. A lowercase value here would compare
2433
+ * unequal to all of them and silently match nothing.
2434
+ */
2435
+ countryCode: string17().trim().uppercase().length(2).required().label("Country Code"),
2436
+ iso3: string17().trim().uppercase().length(3).optional().nullable().label("ISO3"),
2437
+ numericCode: string17().trim().optional().nullable().label("Numeric Code"),
2438
+ phonecode: string17().trim().optional().nullable().label("Phone Code"),
2439
+ capital: string17().trim().optional().nullable().label("Capital"),
2440
+ currency: string17().trim().optional().nullable().label("Currency"),
2441
+ currencyName: string17().trim().optional().nullable().label("Currency Name"),
2442
+ currencySymbol: string17().trim().optional().nullable().label("Currency Symbol"),
2443
+ tld: string17().trim().optional().nullable().label("TLD"),
2444
+ /** The name in its own script, e.g. "भारत". Stored, but NOT searchable; see `searchByLocale`. */
2445
+ native: string17().trim().optional().nullable().label("Native Name"),
2446
+ population: number9().optional().nullable().label("Population"),
2447
+ gdp: number9().optional().nullable().label("GDP"),
2448
+ region: string17().trim().optional().nullable().label("Region"),
2449
+ regionId: number9().integer().optional().nullable().label("Region ID"),
2450
+ subregion: string17().trim().optional().nullable().label("Subregion"),
2451
+ subregionId: number9().integer().optional().nullable().label("Subregion ID"),
2452
+ nationality: string17().trim().optional().nullable().label("Nationality"),
2453
+ areaSqKm: number9().optional().nullable().label("Area (sq km)"),
2454
+ postalCodeFormat: string17().trim().optional().nullable().label("Postal Code Format"),
2455
+ postalCodeRegex: string17().optional().nullable().label("Postal Code Regex"),
2456
+ timezones: array12().of(
2457
+ object21({
2458
+ zoneName: string17().optional().nullable().label("Zone Name"),
2459
+ gmtOffset: number9().optional().nullable().label("GMT Offset"),
2460
+ gmtOffsetName: string17().optional().nullable().label("GMT Offset Name"),
2461
+ abbreviation: string17().optional().nullable().label("Abbreviation"),
2462
+ tzName: string17().optional().nullable().label("TZ Name")
2463
+ })
2464
+ ).optional().nullable().label("Timezones"),
2465
+ /**
2466
+ * Localized names keyed by locale (`{ sv: "Indien", fr: "Inde", … }`), ~19 of
2467
+ * them. Deliberately UNSHAPED: the key set is upstream's to change, and
2468
+ * declaring it here would turn a new locale into a validation failure. The
2469
+ * VALUES are split per-locale into `searchByLocale`.
2470
+ */
2471
+ translations: object21().optional().nullable().label("Translations"),
2472
+ /**
2473
+ * Coordinates as NUMBERS.
2474
+ *
2475
+ * Upstream ships these as strings (`"33.00000000"`). They are coerced on
2476
+ * ingest, because a range query against a string compares lexicographically:
2477
+ * `"9" > "10"` is true, so a bounding-box filter would be quietly wrong rather
2478
+ * than failing loudly.
2479
+ */
2480
+ latitude: number9().optional().nullable().label("Latitude"),
2481
+ longitude: number9().optional().nullable().label("Longitude"),
2482
+ emoji: string17().optional().nullable().label("Emoji"),
2483
+ emojiU: string17().optional().nullable().label("Emoji Unicode"),
2484
+ /**
2485
+ * The name field searched for the DEFAULT (English) locale: the canonical
2486
+ * `name`, and nothing else.
2487
+ *
2488
+ * Search is scoped to ONE locale, so the searchable names are split across
2489
+ * this field and `searchByLocale` rather than pooled into one array. A pooled
2490
+ * field would make every locale's search implicitly a search of all 19
2491
+ * languages: a French UI searching "Inde" would also match German "Indien".
2492
+ */
2493
+ searchDefault: array12().of(string17().required()).optional().default([]).label("Search Default"),
2494
+ /**
2495
+ * Per-locale name fields, keyed by lowercased BCP-47 tag (`de`, `pt-br`).
2496
+ * Each carries ONLY that locale's translation, which is what makes
2497
+ * `?locale=de` German and not "German plus everything that looks similar".
2498
+ *
2499
+ * Unshaped for the same reason as `translations`: the key set is upstream's.
2500
+ *
2501
+ * `native` is deliberately NOT indexed here. It cannot be attributed to a
2502
+ * language with this data — 84% of city native names equal SEVERAL locales'
2503
+ * translations (Latin spellings coinciding across de/it/nl/hr/pl) while
2504
+ * distinctive non-Latin names like `भारत` match none — so filing `कर्नाटक`
2505
+ * under German would be worse than not indexing it at all.
2506
+ *
2507
+ * The 19 upstream locales are br, ko, pt-BR, pt, nl, hr, fa, de, es, fr, ja,
2508
+ * it, zh-CN, tr, ru, uk, pl, hi, ar. **`sv` is absent**, so a Swedish search
2509
+ * matches nothing. That is the accepted cost of strict scoping.
2510
+ */
2511
+ searchByLocale: object21().optional().nullable().label("Search By Locale"),
2512
+ /** Epoch ms of the run that last wrote this row. Drives the stale-row prune. */
2513
+ syncedAt: number9().optional().nullable().label("Synced At")
2514
+ }).label("Country");
2515
+
2516
+ // src/geo/state.schema.ts
2517
+ import { array as array13, number as number10, object as object22, string as string18 } from "yup";
2518
+ var StateSchema = object22({
2519
+ /** The upstream dr5hn id. Stable, and the key every upsert matches on. */
2520
+ id: number10().integer().required().label("ID"),
2521
+ name: string18().trim().required().label("Name"),
2522
+ countryId: number10().integer().optional().nullable().label("Country ID"),
2523
+ /** ISO 3166-1 alpha-2 of the parent, UPPERCASE. See `CountrySchema.countryCode`. */
2524
+ countryCode: string18().trim().uppercase().length(2).required().label("Country Code"),
2525
+ countryName: string18().trim().optional().nullable().label("Country Name"),
2526
+ /**
2527
+ * The BARE subdivision code, e.g. `KA` (upstream `iso2`).
2528
+ *
2529
+ * `@todo STATE-CODE-ISO-PREFIX` — this is **not globally unique**: `BDS`, `CA`,
2530
+ * `NL` and `SP` all recur across several countries, so it is only meaningful
2531
+ * PAIRED with `countryCode`. That is why the cities route is nested under both
2532
+ * codes rather than under a state code alone, which would silently serve
2533
+ * another country's cities. `stateCodeIso` below is the migration target;
2534
+ * both are stored so the migration has its destination present before any
2535
+ * consumer moves.
2536
+ */
2537
+ stateCode: string18().trim().uppercase().optional().nullable().label("State Code"),
2538
+ /** Fully-qualified ISO 3166-2, e.g. `IN-KA` (upstream `iso3166_2`). */
2539
+ stateCodeIso: string18().trim().uppercase().optional().nullable().label("ISO 3166-2 Code"),
2540
+ fipsCode: string18().trim().optional().nullable().label("FIPS Code"),
2541
+ /** Upstream's own word for the subdivision: "province", "state", "district", … */
2542
+ type: string18().trim().optional().nullable().label("Type"),
2543
+ level: number10().integer().optional().nullable().label("Level"),
2544
+ parentId: number10().integer().optional().nullable().label("Parent ID"),
2545
+ /** The name in its own script. Stored, but NOT searchable; see `searchByLocale`. */
2546
+ native: string18().trim().optional().nullable().label("Native Name"),
2547
+ /** Numbers, not the strings upstream ships. See `CountrySchema.latitude`. */
2548
+ latitude: number10().optional().nullable().label("Latitude"),
2549
+ longitude: number10().optional().nullable().label("Longitude"),
2550
+ timezone: string18().trim().optional().nullable().label("Timezone"),
2551
+ /** Localized names keyed by locale. Unshaped; see `CountrySchema.translations`. */
2552
+ translations: object22().optional().nullable().label("Translations"),
2553
+ /**
2554
+ * The name field searched for the DEFAULT (English) locale: the canonical
2555
+ * `name`, and nothing else.
2556
+ *
2557
+ * Search is scoped to ONE locale, so the searchable names are split across
2558
+ * this field and `searchByLocale` rather than pooled into one array. A pooled
2559
+ * field would make every locale's search implicitly a search of all 19
2560
+ * languages: a French UI searching "Inde" would also match German "Indien".
2561
+ */
2562
+ searchDefault: array13().of(string18().required()).optional().default([]).label("Search Default"),
2563
+ /**
2564
+ * Per-locale name fields, keyed by lowercased BCP-47 tag (`de`, `pt-br`).
2565
+ * Each carries ONLY that locale's translation, which is what makes
2566
+ * `?locale=de` German and not "German plus everything that looks similar".
2567
+ *
2568
+ * Unshaped for the same reason as `translations`: the key set is upstream's.
2569
+ *
2570
+ * `native` is deliberately NOT indexed here. It cannot be attributed to a
2571
+ * language with this data — 84% of city native names equal SEVERAL locales'
2572
+ * translations (Latin spellings coinciding across de/it/nl/hr/pl) while
2573
+ * distinctive non-Latin names like `भारत` match none — so filing `कर्नाटक`
2574
+ * under German would be worse than not indexing it at all.
2575
+ *
2576
+ * The 19 upstream locales are br, ko, pt-BR, pt, nl, hr, fa, de, es, fr, ja,
2577
+ * it, zh-CN, tr, ru, uk, pl, hi, ar. **`sv` is absent**, so a Swedish search
2578
+ * matches nothing. That is the accepted cost of strict scoping.
2579
+ */
2580
+ searchByLocale: object22().optional().nullable().label("Search By Locale"),
2581
+ /** Epoch ms of the run that last wrote this row. Drives the stale-row prune. */
2582
+ syncedAt: number10().optional().nullable().label("Synced At")
2583
+ }).label("State");
2584
+
2585
+ // src/geo/city.schema.ts
2586
+ import { array as array14, number as number11, object as object23, string as string19 } from "yup";
2587
+ var CitySchema = object23({
2588
+ /** The upstream dr5hn id. Stable, and the key every upsert matches on. */
2589
+ id: number11().integer().required().label("ID"),
2590
+ name: string19().trim().required().label("Name"),
2591
+ stateId: number11().integer().optional().nullable().label("State ID"),
2592
+ /** The BARE subdivision code. Only meaningful paired with `countryCode`; see `StateSchema`. */
2593
+ stateCode: string19().trim().uppercase().optional().nullable().label("State Code"),
2594
+ /**
2595
+ * Fully-qualified ISO 3166-2, e.g. `IN-KA`.
2596
+ *
2597
+ * DERIVED on ingest as `${countryCode}-${stateCode}`, because the upstream city
2598
+ * record carries only the bare `state_code` — unlike the state record, which
2599
+ * ships `iso3166_2` directly. Stored anyway so the `@todo STATE-CODE-ISO-PREFIX`
2600
+ * migration can re-point the cities route without a 153k-row re-ingest.
2601
+ * Null when either half is missing.
2602
+ */
2603
+ stateCodeIso: string19().trim().uppercase().optional().nullable().label("ISO 3166-2 Code"),
2604
+ stateName: string19().trim().optional().nullable().label("State Name"),
2605
+ countryId: number11().integer().optional().nullable().label("Country ID"),
2606
+ /** ISO 3166-1 alpha-2 of the parent, UPPERCASE. See `CountrySchema.countryCode`. */
2607
+ countryCode: string19().trim().uppercase().length(2).required().label("Country Code"),
2608
+ countryName: string19().trim().optional().nullable().label("Country Name"),
2609
+ /** Numbers, not the strings upstream ships. See `CountrySchema.latitude`. */
2610
+ latitude: number11().optional().nullable().label("Latitude"),
2611
+ longitude: number11().optional().nullable().label("Longitude"),
2612
+ /** The name in its own script. Stored, but NOT searchable; see `searchByLocale`. */
2613
+ native: string19().trim().optional().nullable().label("Native Name"),
2614
+ type: string19().trim().optional().nullable().label("Type"),
2615
+ level: number11().integer().optional().nullable().label("Level"),
2616
+ parentId: number11().integer().optional().nullable().label("Parent ID"),
2617
+ /**
2618
+ * Null on most rows. Worth keeping (it is a good relevance signal when
2619
+ * present) but never worth SORTING by alone, since a null-heavy sort buries
2620
+ * every city upstream has no figure for.
2621
+ */
2622
+ population: number11().optional().nullable().label("Population"),
2623
+ timezone: string19().trim().optional().nullable().label("Timezone"),
2624
+ /** Localized names keyed by locale. Unshaped; see `CountrySchema.translations`. */
2625
+ translations: object23().optional().nullable().label("Translations"),
2626
+ /**
2627
+ * The name field searched for the DEFAULT (English) locale: the canonical
2628
+ * `name`, and nothing else.
2629
+ *
2630
+ * Search is scoped to ONE locale, so the searchable names are split across
2631
+ * this field and `searchByLocale` rather than pooled into one array. A pooled
2632
+ * field would make every locale's search implicitly a search of all 19
2633
+ * languages: a French UI searching "Inde" would also match German "Indien".
2634
+ */
2635
+ searchDefault: array14().of(string19().required()).optional().default([]).label("Search Default"),
2636
+ /**
2637
+ * Per-locale name fields, keyed by lowercased BCP-47 tag (`de`, `pt-br`).
2638
+ * Each carries ONLY that locale's translation, which is what makes
2639
+ * `?locale=de` German and not "German plus everything that looks similar".
2640
+ *
2641
+ * Unshaped for the same reason as `translations`: the key set is upstream's.
2642
+ *
2643
+ * `native` is deliberately NOT indexed here. It cannot be attributed to a
2644
+ * language with this data — 84% of city native names equal SEVERAL locales'
2645
+ * translations (Latin spellings coinciding across de/it/nl/hr/pl) while
2646
+ * distinctive non-Latin names like `भारत` match none — so filing `कर्नाटक`
2647
+ * under German would be worse than not indexing it at all.
2648
+ *
2649
+ * The 19 upstream locales are br, ko, pt-BR, pt, nl, hr, fa, de, es, fr, ja,
2650
+ * it, zh-CN, tr, ru, uk, pl, hi, ar. **`sv` is absent**, so a Swedish search
2651
+ * matches nothing. That is the accepted cost of strict scoping.
2652
+ */
2653
+ searchByLocale: object23().optional().nullable().label("Search By Locale"),
2654
+ /** Epoch ms of the run that last wrote this row. Drives the stale-row prune. */
2655
+ syncedAt: number11().optional().nullable().label("Synced At")
2656
+ }).label("City");
2657
+
2658
+ // src/geo/country-filter.schema.ts
2659
+ import { object as object24 } from "yup";
2660
+ var CountryFilterShape = object24({
2661
+ /** ISO 3166-1 alpha-2 codes, e.g. `IN`. Uppercased, matching how they are stored. */
2662
+ countryCodes: codeTokens("Country Codes"),
2663
+ /** ISO 3166-1 alpha-3 codes, e.g. `IND`. */
2664
+ iso3s: codeTokens("ISO3 Codes"),
2665
+ /**
2666
+ * `tokens`, NOT `codeTokens`: a region is a display string ("Asia", "Europe"),
2667
+ * and uppercasing it would match nothing.
2668
+ */
2669
+ regions: tokens("Regions"),
2670
+ subregions: tokens("Subregions"),
2671
+ /** Display names, matched exactly. Free-text matching is `?search=`. */
2672
+ names: tokens("Names")
2673
+ }).label("Country Filter");
2674
+ var CountryFilterSchema = rejectUnknownKeys(
2675
+ CountryFilterShape,
2676
+ () => Object.keys(CountryFilterShape.fields)
2677
+ );
2678
+
2679
+ // src/geo/state-filter.schema.ts
2680
+ import { object as object25 } from "yup";
2681
+ var StateFilterShape = object25({
2682
+ /** ISO 3166-1 alpha-2 of the parent country, e.g. `IN`. */
2683
+ countryCodes: codeTokens("Country Codes"),
2684
+ /**
2685
+ * BARE subdivision codes, e.g. `KA`.
2686
+ *
2687
+ * `@todo STATE-CODE-ISO-PREFIX` — not globally unique, so this facet ALONE is
2688
+ * an ambiguous question: `stateCodes[0]=BDS` matches Badakhshan in Afghanistan
2689
+ * and every other `BDS` elsewhere. Pair it with `countryCodes` to mean one
2690
+ * place. See `StateSchema.stateCode`.
2691
+ */
2692
+ stateCodes: codeTokens("State Codes"),
2693
+ /** Display names, matched exactly. Free-text matching is `?search=`. */
2694
+ names: tokens("Names"),
2695
+ /**
2696
+ * Upstream's own subdivision word: "province", "state", "district", …
2697
+ *
2698
+ * `tokens`, NOT `codeTokens`: these are lowercase display strings and
2699
+ * uppercasing them would match nothing. An OPEN vocabulary rather than an
2700
+ * `enumFacet`, because upstream ships 30-odd values and adding one should not
2701
+ * require a schema release here.
2702
+ */
2703
+ types: tokens("Types")
2704
+ }).label("State Filter");
2705
+ var StateFilterSchema = rejectUnknownKeys(
2706
+ StateFilterShape,
2707
+ () => Object.keys(StateFilterShape.fields)
2708
+ );
2709
+
2710
+ // src/geo/city-filter.schema.ts
2711
+ import { object as object26 } from "yup";
2712
+ var CityFilterShape = object26({
2713
+ /** ISO 3166-1 alpha-2 of the parent country, e.g. `IN`. */
2714
+ countryCodes: codeTokens("Country Codes"),
2715
+ /**
2716
+ * BARE subdivision codes, e.g. `KA`. Ambiguous without `countryCodes`; see
2717
+ * `StateFilterSchema.stateCodes` and `@todo STATE-CODE-ISO-PREFIX`.
2718
+ */
2719
+ stateCodes: codeTokens("State Codes"),
2720
+ /** Display names, matched exactly. Free-text matching is `?search=`. */
2721
+ names: tokens("Names")
2722
+ }).label("City Filter");
2723
+ var CityFilterSchema = rejectUnknownKeys(
2724
+ CityFilterShape,
2725
+ () => Object.keys(CityFilterShape.fields)
2726
+ );
2727
+
2728
+ // src/geo/geo-sync.schema.ts
2729
+ import { boolean as boolean14, mixed as mixed4, number as number12, object as object27, string as string20 } from "yup";
2730
+ var GeoSyncCountsSchema = object27({
2731
+ read: number12().integer().min(0).default(0).label("Read"),
2732
+ upserted: number12().integer().min(0).default(0).label("Upserted"),
2733
+ modified: number12().integer().min(0).default(0).label("Modified"),
2734
+ matched: number12().integer().min(0).default(0).label("Matched"),
2735
+ /** Rows removed because upstream dropped them. See `prunedSkipped`. */
2736
+ pruned: number12().integer().min(0).default(0).label("Pruned"),
2737
+ /**
2738
+ * True when the stale-row prune was REFUSED because it would have removed more
2739
+ * than its safety threshold of the collection. A partial fetch that looked
2740
+ * like a success must not be able to empty the table, so the run reports the
2741
+ * refusal rather than acting on it.
2742
+ */
2743
+ prunedSkipped: boolean14().default(false).label("Prune Skipped")
2744
+ }).label("Sync Counts");
2745
+ var GeoSyncRunSchema = object27({
2746
+ jobId: string20().required().label("Job ID"),
2747
+ status: mixed4().oneOf(SupportedGeoSyncStatuses).required().label("Status"),
2748
+ trigger: mixed4().oneOf(SupportedGeoSyncTriggers).optional().label("Trigger"),
2749
+ /** Whether the caller asked to run even though the release tag was unchanged. */
2750
+ force: boolean14().default(false).label("Force"),
2751
+ /**
2752
+ * The upstream release tag this run fetched, e.g. `v3.2-export.7`.
2753
+ *
2754
+ * Null only when the tag could not be read AND `force` was set — a run that
2755
+ * cannot tell what version it is fetching is otherwise a failure, not a
2756
+ * refresh, so it is not allowed to proceed unattended.
2757
+ */
2758
+ releaseTag: string20().optional().nullable().label("Release Tag"),
2759
+ /** The tag of the last successful run, i.e. what this one is replacing. */
2760
+ previousReleaseTag: string20().optional().nullable().label("Previous Release Tag"),
2761
+ startedAt: number12().required().label("Started At"),
2762
+ finishedAt: number12().optional().nullable().label("Finished At"),
2763
+ durationMs: number12().optional().nullable().label("Duration (ms)"),
2764
+ phase: mixed4().oneOf(SupportedGeoSyncPhases).optional().nullable().label("Phase"),
2765
+ /**
2766
+ * Live progress for the dashboard bar.
2767
+ *
2768
+ * `total` is an ESTIMATE — the previous successful run's count for the same
2769
+ * collection, or a hardcoded fallback on the very first run, since the true
2770
+ * total is not known until the stream ends. The UI should render it as one.
2771
+ */
2772
+ progress: object27({
2773
+ collection: string20().optional().nullable().label("Collection"),
2774
+ processed: number12().integer().min(0).default(0).label("Processed"),
2775
+ total: number12().integer().min(0).optional().nullable().label("Total")
2776
+ }).optional().nullable().label("Progress"),
2777
+ counts: object27({
2778
+ countries: GeoSyncCountsSchema.optional().nullable().label("Countries"),
2779
+ states: GeoSyncCountsSchema.optional().nullable().label("States"),
2780
+ cities: GeoSyncCountsSchema.optional().nullable().label("Cities")
2781
+ }).optional().nullable().label("Counts"),
2782
+ /** Why a `skipped` run was skipped, in words the dashboard can show verbatim. */
2783
+ reason: string20().optional().nullable().label("Reason"),
2784
+ /** The failure message on a `failed` run. Surfaced verbatim to the admin. */
2785
+ error: string20().optional().nullable().label("Error")
2786
+ }).label("Geo Sync Run");
2787
+
2788
+ // src/language/language-filter.schema.ts
2789
+ import { object as object28 } from "yup";
2790
+ var LanguageFilterShape = object28({
2791
+ /** Language names, matched exactly. Free-text matching is `?search=`. */
2792
+ names: tokens("Names")
2793
+ }).label("Language Filter");
2794
+ var LanguageFilterSchema = rejectUnknownKeys(
2795
+ LanguageFilterShape,
2796
+ () => Object.keys(LanguageFilterShape.fields)
2797
+ );
2798
+
2799
+ // src/page/page-filter.schema.ts
2800
+ import { boolean as boolean15, object as object29 } from "yup";
2801
+ var PageFilterShape = object29({
2802
+ /**
2803
+ * `type`, NOT `pageType`.
2804
+ *
2805
+ * The enum constant is named `PageType`, which makes `pageType` the obvious
2806
+ * guess, and it is wrong: the stored key is `type` (verified — `pageType`
2807
+ * exists on 0 of 1,449 documents, while `type` splits 1,198 company / 251
2808
+ * institute). The name of a constant is not the name of a field.
2809
+ */
2810
+ type: enumFacet(SupportedPageTypes, "Page Type"),
2811
+ /**
2812
+ * Publication state. Worth knowing when filtering: 69 documents carry NO
2813
+ * `status` at all and some carry `null`, so `status: ['published']` is a real
2814
+ * narrowing (1,379 of 1,449) and absent means "no constraint", not "published".
2815
+ */
2816
+ status: enumFacet(SupportedPageStatuses, "Status"),
2817
+ // ── Flags ───────────────────────────────────────────────────────────────────
2818
+ // No `.default(false)`: absent means no constraint. Note `isFeatured` is
2819
+ // present on only 55 documents, so combining it with `status: ['published']`
2820
+ // leaves 11 pages — a legitimate filter that returns almost nothing.
2821
+ isFeatured: boolean15().optional().label("Featured Only"),
2822
+ /** Lowercase (`in`, `se`, `ww`), like groups. See `GroupFilterSchema`. */
2823
+ featuredMarkets: tokens("Featured Markets"),
2824
+ /** Page SLUGS. A company is matched by slug: a display name breaks on rename. */
2825
+ slugs: tokens("Slugs")
2826
+ }).label("Page Filter");
2827
+ var PAGE_FILTER_KEYS = Object.keys(PageFilterShape.fields);
2828
+ var PageFilterSchema = rejectUnknownKeys(
2829
+ PageFilterShape,
2830
+ () => PAGE_FILTER_KEYS
1965
2831
  );
1966
- var SignupContextSource = /* @__PURE__ */ ((SignupContextSource2) => {
1967
- SignupContextSource2["CfHeader"] = "cf-header";
1968
- SignupContextSource2["Location"] = "location";
1969
- SignupContextSource2["Default"] = "default";
1970
- return SignupContextSource2;
1971
- })(SignupContextSource || {});
1972
- var SupportedSignupContextSources = Object.values(SignupContextSource);
1973
- var UserDetailType = /* @__PURE__ */ ((UserDetailType2) => {
1974
- UserDetailType2["Overview"] = "overview";
1975
- UserDetailType2["AdditionalInfo"] = "additionalInfo";
1976
- UserDetailType2["General"] = "general";
1977
- UserDetailType2["SocialAccounts"] = "socialAccounts";
1978
- UserDetailType2["WorkExperiences"] = "workExperiences";
1979
- UserDetailType2["Educations"] = "educations";
1980
- UserDetailType2["Skills"] = "skills";
1981
- UserDetailType2["Languages"] = "languages";
1982
- UserDetailType2["Certifications"] = "certifications";
1983
- UserDetailType2["Interests"] = "interests";
1984
- UserDetailType2["Projects"] = "projects";
1985
- return UserDetailType2;
1986
- })(UserDetailType || {});
1987
- var ProficiencyLevel = /* @__PURE__ */ ((ProficiencyLevel2) => {
1988
- ProficiencyLevel2["BasicAwareness"] = "basic_awareness";
1989
- ProficiencyLevel2["Beginner"] = "beginner";
1990
- ProficiencyLevel2["Intermediate"] = "intermediate";
1991
- ProficiencyLevel2["Advanced"] = "advanced";
1992
- ProficiencyLevel2["Expert"] = "expert";
1993
- return ProficiencyLevel2;
1994
- })(ProficiencyLevel || {});
1995
- var SupportedProficiencyLevels = Object.values(ProficiencyLevel);
1996
- var JobSearchUrgency = /* @__PURE__ */ ((JobSearchUrgency2) => {
1997
- JobSearchUrgency2["Asap"] = "asap";
1998
- JobSearchUrgency2["WithinThreeMonths"] = "within_3_months";
1999
- JobSearchUrgency2["WithinSixMonths"] = "within_6_months";
2000
- JobSearchUrgency2["PassivelyBrowsing"] = "passively_browsing";
2001
- return JobSearchUrgency2;
2002
- })(JobSearchUrgency || {});
2003
- var SupportedJobSearchUrgencies = Object.values(JobSearchUrgency);
2004
- var ReferralSource = /* @__PURE__ */ ((ReferralSource2) => {
2005
- ReferralSource2["Recruiter"] = "recruiter";
2006
- ReferralSource2["LinkedIn"] = "linkedin";
2007
- ReferralSource2["TikTokInstagram"] = "tiktok_instagram";
2008
- ReferralSource2["Google"] = "google";
2009
- ReferralSource2["Reddit"] = "reddit";
2010
- ReferralSource2["ChatGptGemini"] = "chatgpt_gemini";
2011
- ReferralSource2["Friend"] = "friend";
2012
- ReferralSource2["Other"] = "other";
2013
- return ReferralSource2;
2014
- })(ReferralSource || {});
2015
- var SupportedReferralSources = Object.values(ReferralSource);
2016
- var JobAlertFrequency = /* @__PURE__ */ ((JobAlertFrequency2) => {
2017
- JobAlertFrequency2["Off"] = "off";
2018
- JobAlertFrequency2["Daily"] = "daily";
2019
- JobAlertFrequency2["Weekly"] = "weekly";
2020
- return JobAlertFrequency2;
2021
- })(JobAlertFrequency || {});
2022
- var SupportedJobAlertFrequencies = Object.values(JobAlertFrequency);
2023
- var NotificationChannel = /* @__PURE__ */ ((NotificationChannel2) => {
2024
- NotificationChannel2["GroupActivity"] = "groupActivity";
2025
- NotificationChannel2["JobActivity"] = "jobActivity";
2026
- NotificationChannel2["ApplicationUpdates"] = "applicationUpdates";
2027
- NotificationChannel2["ProductUpdates"] = "productUpdates";
2028
- return NotificationChannel2;
2029
- })(NotificationChannel || {});
2030
- var SupportedNotificationChannels = Object.values(NotificationChannel);
2031
- var SupportedSalaryCurrencies = ["USD", "EUR", "GBP", "SEK", "INR"];
2032
- var MIN_SALARY_LOWER_BOUND = 0;
2033
- var MIN_SALARY_UPPER_BOUND = 1e6;
2034
- var PREFILL_MIRRORED_KEYS = [
2035
- "headline",
2036
- "aboutMe",
2037
- "mobile",
2038
- "location",
2039
- "experienceLevel",
2040
- "socialAccounts",
2041
- "workExperiences",
2042
- "educations"
2043
- ];
2044
- var PRIVATE_PROFILE_FIELDS = [
2045
- "email",
2046
- "emailVerified",
2047
- "mobile",
2048
- "additionalInfo"
2049
- ];
2832
+
2833
+ // src/marketing/marketing.schema.ts
2834
+ import { array as array16, boolean as boolean17, lazy as lazy2, mixed as mixed5, number as number14, object as object31, string as string22 } from "yup";
2050
2835
 
2051
2836
  // src/marketing/marketing.constant.ts
2052
2837
  var CampaignStatus = /* @__PURE__ */ ((CampaignStatus2) => {
2053
2838
  CampaignStatus2["Draft"] = "draft";
2054
- CampaignStatus2["Sending"] = "sending";
2055
- CampaignStatus2["Sent"] = "sent";
2839
+ CampaignStatus2["Running"] = "running";
2840
+ CampaignStatus2["Paused"] = "paused";
2841
+ CampaignStatus2["Stopped"] = "stopped";
2842
+ CampaignStatus2["Completed"] = "completed";
2056
2843
  CampaignStatus2["Failed"] = "failed";
2057
2844
  return CampaignStatus2;
2058
2845
  })(CampaignStatus || {});
2059
2846
  var SupportedCampaignStatuses = Object.values(CampaignStatus);
2847
+ var TerminalCampaignStatuses = [
2848
+ "stopped" /* Stopped */,
2849
+ "completed" /* Completed */,
2850
+ "failed" /* Failed */
2851
+ ];
2852
+ var CampaignStatusTransitions = {
2853
+ ["draft" /* Draft */]: ["running" /* Running */],
2854
+ ["running" /* Running */]: [
2855
+ "paused" /* Paused */,
2856
+ "stopped" /* Stopped */,
2857
+ "completed" /* Completed */,
2858
+ "failed" /* Failed */
2859
+ ],
2860
+ ["paused" /* Paused */]: ["running" /* Running */, "stopped" /* Stopped */],
2861
+ ["stopped" /* Stopped */]: [],
2862
+ ["completed" /* Completed */]: [],
2863
+ ["failed" /* Failed */]: []
2864
+ };
2865
+ function canTransitionCampaign(from, to) {
2866
+ return CampaignStatusTransitions[from]?.includes(to) ?? false;
2867
+ }
2060
2868
  var CampaignTriggerKind = /* @__PURE__ */ ((CampaignTriggerKind2) => {
2061
2869
  CampaignTriggerKind2["Manual"] = "manual";
2062
2870
  CampaignTriggerKind2["Once"] = "once";
2063
2871
  CampaignTriggerKind2["Schedule"] = "schedule";
2064
- CampaignTriggerKind2["Lifecycle"] = "lifecycle";
2065
2872
  return CampaignTriggerKind2;
2066
2873
  })(CampaignTriggerKind || {});
2067
2874
  var SupportedCampaignTriggerKinds = Object.values(CampaignTriggerKind);
@@ -2079,21 +2886,16 @@ var CampaignEndKind = /* @__PURE__ */ ((CampaignEndKind2) => {
2079
2886
  return CampaignEndKind2;
2080
2887
  })(CampaignEndKind || {});
2081
2888
  var SupportedCampaignEndKinds = Object.values(CampaignEndKind);
2082
- var CampaignAnchorField = /* @__PURE__ */ ((CampaignAnchorField2) => {
2083
- CampaignAnchorField2["CreatedAt"] = "createdAt";
2084
- CampaignAnchorField2["OnboardingCompletedAt"] = "onboardingCompletedAt";
2085
- CampaignAnchorField2["MarketingConsentUpdatedAt"] = "marketingConsent.updatedAt";
2086
- return CampaignAnchorField2;
2087
- })(CampaignAnchorField || {});
2088
- var SupportedCampaignAnchorFields = Object.values(CampaignAnchorField);
2089
- var BindingKind = /* @__PURE__ */ ((BindingKind2) => {
2090
- BindingKind2["Static"] = "static";
2091
- BindingKind2["Recipient"] = "recipient";
2092
- BindingKind2["Url"] = "url";
2093
- BindingKind2["Source"] = "source";
2094
- return BindingKind2;
2095
- })(BindingKind || {});
2096
- var SupportedBindingKinds = Object.values(BindingKind);
2889
+ var TemplateVariableBindingKind = /* @__PURE__ */ ((TemplateVariableBindingKind2) => {
2890
+ TemplateVariableBindingKind2["Static"] = "static";
2891
+ TemplateVariableBindingKind2["Recipient"] = "recipient";
2892
+ TemplateVariableBindingKind2["Url"] = "url";
2893
+ TemplateVariableBindingKind2["Source"] = "source";
2894
+ return TemplateVariableBindingKind2;
2895
+ })(TemplateVariableBindingKind || {});
2896
+ var SupportedTemplateVariableBindingKinds = Object.values(
2897
+ TemplateVariableBindingKind
2898
+ );
2097
2899
  var OccurrenceStatus = /* @__PURE__ */ ((OccurrenceStatus2) => {
2098
2900
  OccurrenceStatus2["Pending"] = "pending";
2099
2901
  OccurrenceStatus2["Running"] = "running";
@@ -2105,24 +2907,50 @@ var OccurrenceStatus = /* @__PURE__ */ ((OccurrenceStatus2) => {
2105
2907
  var SupportedOccurrenceStatuses = Object.values(OccurrenceStatus);
2106
2908
  var CAMPAIGN_MAX_RECIPIENTS_CEILING = 5e3;
2107
2909
 
2108
- // src/marketing/marketing.schema.ts
2109
- var tokens = (label) => array10().of(string15().trim().required()).optional().default([]).label(label);
2110
- var enumFacet = (values, label) => array10().of(string15().oneOf(values).required()).optional().default([]).label(label);
2111
- var days = (label) => number7().integer().min(0).max(3650).optional().default(void 0).label(label);
2112
- var months = (label) => number7().integer().min(0).max(1200).optional().default(void 0).label(label);
2113
- var percent = (label) => number7().integer().min(0).max(100).optional().default(void 0).label(label);
2114
- var epoch = (label) => number7().integer().min(0).optional().default(void 0).label(label);
2115
- var salaryBound = (label) => number7().integer().min(0).optional().default(void 0).label(label).when("salaryCurrencies", {
2116
- is: (v) => !Array.isArray(v) || v.length !== 1,
2117
- then: (s) => s.test(
2118
- "requires-single-currency",
2119
- "Pick exactly one salary currency to filter by salary",
2120
- (value) => value === void 0 || value === null
2121
- )
2122
- });
2123
- var ListFilterBranchSchema = object18({
2910
+ // src/user/user-filter.schema.ts
2911
+ import { array as array15, boolean as boolean16, number as number13, object as object30, string as string21 } from "yup";
2912
+ var UserFilterBranchShape = object30({
2913
+ /**
2914
+ * Exact email addresses. An IDENTITY lookup, not a search.
2915
+ *
2916
+ * The admin `search` box already matches email, but as a case-insensitive
2917
+ * unanchored regex over email + first + last, which no index can seek: it
2918
+ * scans every key. Measured against 103,680 users, finding one person by
2919
+ * their full address examined all 103,680 documents and took 552ms, where the
2920
+ * same lookup as an exact match on the indexed `email` field is 0ms and
2921
+ * examines 1.
2922
+ *
2923
+ * So this is for when the caller ALREADY KNOWS the address (previewing what
2924
+ * one named recipient will receive, reconciling a bounce) and `search` is for
2925
+ * when a human is typing a fragment.
2926
+ *
2927
+ * Values are LOWER-CASED before matching, not matched case-insensitively: all
2928
+ * 103,680 stored addresses are already lower case (verified), so normalising
2929
+ * the input keeps the match an indexed equality instead of a regex that would
2930
+ * put the scan straight back.
2931
+ */
2932
+ emails: array15().of(string21().required()).optional().default([]).label("Email Addresses"),
2933
+ /**
2934
+ * Better Auth account ids. An IDENTITY lookup like `emails`, and for the same
2935
+ * reason: the caller already knows the exact value.
2936
+ *
2937
+ * BACKEND-ONLY, despite living beside the human-facing facets. The consumer
2938
+ * is hono-auth, which holds an account id and needs the profile's `shortId`
2939
+ * to sign into the JWT it is minting. It cannot use `GET /me` (it is the one
2940
+ * issuing the token) and cannot use `GET /users/:shortId` (the shortId is
2941
+ * precisely what it lacks), so the list route with a facet is the lookup.
2942
+ *
2943
+ * Matched as an indexed equality on `authAccountId`, which is already indexed
2944
+ * as the mirror's link key. No case normalisation, unlike `emails`: these are
2945
+ * opaque generated ids, stored exactly as Better Auth issued them, so folding
2946
+ * case could only ever turn a hit into a miss.
2947
+ *
2948
+ * The admin filter panel builds its controls explicitly rather than from this
2949
+ * shape, so declaring it here does not put an account-id box in the UI.
2950
+ */
2951
+ authAccountIds: array15().of(string21().required()).optional().default([]).label("Auth Account IDs"),
2124
2952
  // ── Enum facets (closed vocabularies, chip pickers) ─────────────────────────
2125
- countries: array10().of(string15().required()).optional().default([]).label("Countries"),
2953
+ countries: array15().of(string21().required()).optional().default([]).label("Countries"),
2126
2954
  roles: enumFacet(SupportedUserRoles, "Roles"),
2127
2955
  // The user's own current seniority (UserSchema.experienceLevel).
2128
2956
  experienceLevels: enumFacet(SupportedExperienceLevels, "Experience Levels"),
@@ -2138,40 +2966,112 @@ var ListFilterBranchSchema = object18({
2138
2966
  SupportedUserProfileVisibilities,
2139
2967
  "Profile Visibility"
2140
2968
  ),
2141
- // Proficiency held on any skill entry (skills[].proficiencyLevel).
2142
- skillProficiencyLevels: enumFacet(
2143
- SupportedProficiencyLevels,
2144
- "Skill Proficiency"
2145
- ),
2146
- // Proficiency held on any language entry (languages[].proficiencyLevel).
2147
- languageProficiencyLevels: enumFacet(
2148
- SupportedProficiencyLevels,
2149
- "Language Proficiency"
2150
- ),
2151
2969
  // Mode of any education entry (educations[].studyType).
2970
+ //
2971
+ // Left as a STANDALONE enum, unlike skill/language proficiency above: it is a
2972
+ // property of a person's education in general ("studied full time"), and the
2973
+ // measured divergence against pairing it with `courses` was 1 user out of
2974
+ // 3,465. Pairing is for qualifiers that are MEANINGLESS alone; this one is
2975
+ // not, so it stays cheap to use.
2152
2976
  studyTypes: enumFacet(SupportedStudyTypes, "Study Type"),
2153
2977
  // Platforms the user linked a social account for (socialAccounts[].type).
2154
2978
  socialAccountTypes: enumFacet(SupportedSocialAccounts, "Social Accounts"),
2155
2979
  // Currency of the user's salary floor (minSalaryCurrency).
2156
2980
  salaryCurrencies: enumFacet(SupportedSalaryCurrencies, "Salary Currency"),
2157
2981
  // ── Free-text token facets (nested profile arrays) ──────────────────────────
2158
- skills: tokens("Skills"),
2159
- languages: tokens("Languages"),
2982
+ // Skill and language each carry their OWN proficiency, because a level is
2983
+ // only meaningful attached to the thing it qualifies. See `qualified`:
2984
+ // as two independent facets, "python + expert" returned 301 users where only
2985
+ // 103 were actually expert at Python.
2986
+ //
2987
+ // `[{ name: 'python', proficiencyLevel: 'expert' }]` — the level is optional,
2988
+ // so naming a skill without one still filters on the skill alone.
2989
+ skills: qualified(SupportedProficiencyLevels, "proficiencyLevel", "Skills"),
2990
+ languages: qualified(SupportedProficiencyLevels, "proficiencyLevel", "Languages"),
2160
2991
  interests: tokens("Interests"),
2161
- companies: tokens("Companies"),
2992
+ // LOOSE forms, kept deliberately. Each of these is a real question on its own
2993
+ // ("worked at Google", "was an engineer somewhere"), and asking for two of
2994
+ // them usually does mean "has each of these somewhere in their history".
2995
+ // They AND across the whole array, so different entries may satisfy each.
2996
+ //
2997
+ // `companyName` / `instituteName`, NOT the `company` widget the job filter
2998
+ // uses. Both are PICKED from real pages — a profile stores the whole
2999
+ // `PageSchema.pick(["name","slug","type","logo"])` object, not free text — but
3000
+ // the facet matches `workExperiences.company.name`, so the value to store is
3001
+ // the page's NAME. Storing its slug would be a picker that matches nothing.
3002
+ companies: tokens("Companies").meta({ widget: "companyName" }),
2162
3003
  designations: tokens("Job Titles / Designations"),
2163
- institutes: tokens("Institutes"),
3004
+ institutes: tokens("Institutes").meta({ widget: "instituteName" }),
2164
3005
  courses: tokens("Courses"),
2165
3006
  fieldsOfStudy: tokens("Fields of Study"),
2166
- certifications: tokens("Certifications"),
2167
- certificationAuthorities: tokens("Certification Authorities"),
3007
+ // PRECISE forms: all parts must describe ONE entry.
3008
+ //
3009
+ // The narrower question the loose facets above cannot express. "Engineer AT
3010
+ // Google" as `companies=Google & designations=engineer` also matches someone
3011
+ // who was an engineer at Acme and a designer at Google, because Mongo
3012
+ // satisfies each path from a different array entry. Measured on production:
3013
+ // VIT + B.Tech returned 71 users where 64 had one education entry that was
3014
+ // both, and Infosys + Test Engineer returned 7 where 6 actually matched.
3015
+ //
3016
+ // Additive rather than a replacement: both readings are legitimate, so the
3017
+ // vocabulary carries both and the caller says which it means. 1,573 users
3018
+ // have two or more work entries and 1,761 have two or more educations, so the
3019
+ // distinction is real and grows as profiles fill in.
3020
+ //
3021
+ // The fields mirror what the profile form actually collects for one entry
3022
+ // (`WorkExperienceSchema` / `EducationSchema`), minus the parts that cannot
3023
+ // narrow an audience: `description` is prose, and a per-entry `location`
3024
+ // duplicates the top-level geography facets against a much emptier path.
3025
+ //
3026
+ // `isCurrent` reads `duration.isActive`, which is what the profile form calls
3027
+ // "I currently work here". It is the field that makes this facet worth having
3028
+ // per entry: "engineer at Google" and "engineer at Google NOW" are different
3029
+ // audiences, and only the second is reachable without it. Live counts:
3030
+ // 2,190 users have a current job against 4,527 with a past one, 1,456 have a
3031
+ // remote entry, 4,088 are still studying.
3032
+ workHistory: conjunction(
3033
+ [
3034
+ "company",
3035
+ "designation",
3036
+ { name: "isCurrent", type: "boolean" },
3037
+ { name: "isRemote", type: "boolean" }
3038
+ ],
3039
+ "Work History"
3040
+ ),
3041
+ educationHistory: conjunction(
3042
+ [
3043
+ "institute",
3044
+ "course",
3045
+ "fieldOfStudy",
3046
+ { name: "studyType", type: "string", oneOf: SupportedStudyTypes },
3047
+ { name: "isCurrent", type: "boolean" },
3048
+ { name: "isDistanceLearning", type: "boolean" }
3049
+ ],
3050
+ "Education History"
3051
+ ),
3052
+ // The issuing body belongs to the certificate, not to the person: "AWS SA"
3053
+ // plus "Microsoft" as two facets matches someone holding an AWS cert AND any
3054
+ // unrelated Microsoft one. 38% of users with a certification hold two or more,
3055
+ // so the entries are there to diverge.
3056
+ //
3057
+ // `authority` is free text rather than a closed vocabulary, hence `"open"`.
3058
+ certifications: qualified("open", "authority", "Certifications"),
2168
3059
  projects: tokens("Projects"),
2169
3060
  jobRoles: tokens("Target Roles"),
2170
3061
  jobLocations: tokens("Preferred Job Locations"),
2171
3062
  // Companies a recruiter is hiring for (recruiterProfile.hiringFor[].page.name).
2172
- hiringForCompanies: tokens("Hiring For (Companies)"),
3063
+ // A picked page (`recruiterProfile.hiringFor.page`), matched on `.name`.
3064
+ //
3065
+ // `pageName`, which searches BOTH page types: `RecruiterPageLinkSchema` picks
3066
+ // `PageSchema` without constraining `type`, so a recruiter may be hiring for
3067
+ // an institute as legitimately as for a company. Production carries exactly
3068
+ // that on the coordinator side — a `coordinatesAt` entry holding a company —
3069
+ // so scoping either of these to one type would hide real data.
3070
+ hiringForCompanies: tokens("Hiring For").meta({ widget: "pageName" }),
2173
3071
  // Institutes a coordinator works at (coordinatorProfile.coordinatesAt[].page.name).
2174
- coordinatesAtInstitutes: tokens("Coordinates At (Institutes)"),
3072
+ // Same: a picked page (`coordinatorProfile.coordinatesAt.page`), on `.name`,
3073
+ // and likewise unconstrained in `type`.
3074
+ coordinatesAtInstitutes: tokens("Coordinates At").meta({ widget: "pageName" }),
2175
3075
  // Case-insensitive keyword match against headline + aboutMe.
2176
3076
  keywords: tokens("Profile Keywords"),
2177
3077
  // ── Range / boolean facets ──────────────────────────────────────────────────
@@ -2179,14 +3079,16 @@ var ListFilterBranchSchema = object18({
2179
3079
  // integer and 50000 SEK is not 50000 USD, so an un-currencied range matches
2180
3080
  // across currencies and quietly means nothing. Requiring exactly one currency
2181
3081
  // alongside a bound is what makes the comparison sound.
2182
- minSalaryFloor: salaryBound("Min Salary \u2265"),
2183
- minSalaryCeil: salaryBound("Min Salary \u2264"),
2184
- openToWorkOnly: boolean11().optional().default(false).label("Open to Work Only"),
2185
- remoteExperienceOnly: boolean11().optional().default(false).label("Has Remote Experience"),
2186
- hasResumeOnly: boolean11().optional().default(false).label("Has Resume"),
2187
- onboardingCompletedOnly: boolean11().optional().default(false).label("Onboarding Completed"),
2188
- emailVerifiedOnly: boolean11().optional().default(false).label("Email Verified Only"),
2189
- mobileVerifiedOnly: boolean11().optional().default(false).label("Mobile Verified Only"),
3082
+ // The sibling currency facet is named explicitly: this bound is inert without
3083
+ // it, so the dependency is stated rather than assumed.
3084
+ minSalaryFloor: currencyBound("Min Salary \u2265", "salaryCurrencies"),
3085
+ minSalaryCeil: currencyBound("Min Salary \u2264", "salaryCurrencies"),
3086
+ openToWorkOnly: boolean16().optional().default(false).label("Open to Work Only"),
3087
+ remoteExperienceOnly: boolean16().optional().default(false).label("Has Remote Experience"),
3088
+ hasResumeOnly: boolean16().optional().default(false).label("Has Resume"),
3089
+ onboardingCompletedOnly: boolean16().optional().default(false).label("Onboarding Completed"),
3090
+ emailVerifiedOnly: boolean16().optional().default(false).label("Email Verified Only"),
3091
+ mobileVerifiedOnly: boolean16().optional().default(false).label("Mobile Verified Only"),
2190
3092
  // ── Lifecycle windows ───────────────────────────────────────────────────────
2191
3093
  // Relative, because a saved list is resolved fresh on every send: a stored
2192
3094
  // absolute epoch would freeze at authoring time and the audience would decay
@@ -2214,10 +3116,10 @@ var ListFilterBranchSchema = object18({
2214
3116
  // Radius search around a point, against location.geo. All three required
2215
3117
  // together; validated as a group rather than field-by-field so a half-filled
2216
3118
  // radius cannot silently match everyone.
2217
- withinRadius: object18({
2218
- lat: number7().min(-90).max(90).required().label("Latitude"),
2219
- lng: number7().min(-180).max(180).required().label("Longitude"),
2220
- km: number7().moreThan(0).max(2e4).required().label("Radius (km)")
3119
+ withinRadius: object30({
3120
+ lat: number13().min(-90).max(90).required().label("Latitude"),
3121
+ lng: number13().min(-180).max(180).required().label("Longitude"),
3122
+ km: number13().moreThan(0).max(2e4).required().label("Radius (km)")
2221
3123
  }).optional().default(void 0).label("Within Radius"),
2222
3124
  // ── Signup context (immutable market / locale) ──────────────────────────────
2223
3125
  signupCountries: tokens("Signup Countries"),
@@ -2228,14 +3130,27 @@ var ListFilterBranchSchema = object18({
2228
3130
  // match — filtering on this necessarily excludes them.
2229
3131
  educationLevels: enumFacet(SupportedEducationLevels, "Education Level"),
2230
3132
  // ── Consent metadata ────────────────────────────────────────────────────────
2231
- // `marketingConsent.subscribed` is NOT a facet: it is a hard opt-in gate
2232
- // applied unconditionally by user-service and is not relaxable from here.
3133
+ // `marketingSubscribed` IS a facet, and deliberately so. It used to be a hard
3134
+ // opt-in gate applied unconditionally by user-service; that gate was removed
3135
+ // when user-service became a pure data source, so consent is now an ordinary
3136
+ // filter that marketing-service defaults to `true` in `resolveAudience`.
3137
+ //
3138
+ // NO `.default()` HERE, unlike every other boolean facet. The send spreads its
3139
+ // default first and lets a stored value win, so this must distinguish "the
3140
+ // list said nothing" (absent -> default applies) from "the list said false"
3141
+ // (override -> mails users who never opted in). A `.default(false)` would
3142
+ // materialise the second case on every list that never chose it.
3143
+ //
3144
+ // @todo CONSENT-FILTER-ENFORCEMENT — nothing enforces consent anywhere now.
3145
+ marketingSubscribed: boolean16().optional().label("Marketing Subscribed"),
2233
3146
  jobAlertFrequencies: enumFacet(
2234
3147
  SupportedJobAlertFrequencies,
2235
3148
  "Job Alert Frequency"
2236
3149
  ),
2237
3150
  consentSources: tokens("Consent Source"),
2238
3151
  consentUpdatedWithinDays: days("Consent Updated Within (days)"),
3152
+ /** The absolute bound `consentUpdatedWithinDays` resolves into (epoch ms). */
3153
+ consentUpdatedAfter: epoch("Consent Updated After"),
2239
3154
  // ── Notification preferences ────────────────────────────────────────────────
2240
3155
  // Read as opt-IN (`=== true`), matching marketingConsent and NOT the `!== false`
2241
3156
  // reading used for transactional notification routing. A campaign is bulk mail,
@@ -2243,19 +3158,31 @@ var ListFilterBranchSchema = object18({
2243
3158
  // `productUpdates` defaults to false on the user document, so targeting it
2244
3159
  // selects only people who deliberately turned it on.
2245
3160
  notificationChannelsEnabled: enumFacet(
2246
- SupportedNotificationChannels,
3161
+ SupportedNotificationTypes,
2247
3162
  "Notification Channels Enabled"
2248
3163
  ),
2249
3164
  // ── Tenure / recency ────────────────────────────────────────────────────────
2250
3165
  // Against workExperiences[].duration and educations[].duration ("YYYY-MM").
2251
- currentlyEmployed: boolean11().optional().default(false).label("Currently Employed"),
3166
+ currentlyEmployed: boolean16().optional().default(false).label("Currently Employed"),
2252
3167
  graduatedWithinMonths: months("Graduated Within (months)"),
2253
3168
  skillUsedWithinMonths: months("Skill Used Within (months)"),
3169
+ // The ABSOLUTE bounds the three relative windows above resolve into
3170
+ // (`graduatedWithinMonths` -> `graduatedAfter`, and so on). Both forms are
3171
+ // accepted: a caller may send either, and user-service derives the bound when
3172
+ // only the window is given.
3173
+ //
3174
+ // A UI should offer the WINDOW, not these. A saved list is re-resolved on
3175
+ // every send, so a stored absolute date rots — "graduated after 2024-03" keeps
3176
+ // meaning that forever, while "within 18 months" keeps meaning what the admin
3177
+ // intended. They are declared because the vocabulary must be complete, not
3178
+ // because a list should store one.
3179
+ graduatedAfter: string21().trim().optional().label("Graduated After"),
3180
+ skillUsedAfter: string21().trim().optional().label("Skill Used After"),
2254
3181
  // ── Profile richness ────────────────────────────────────────────────────────
2255
- hasImage: boolean11().optional().default(false).label("Has Photo"),
2256
- hasHeadline: boolean11().optional().default(false).label("Has Headline"),
2257
- hasAboutMe: boolean11().optional().default(false).label("Has About Me"),
2258
- hasMobile: boolean11().optional().default(false).label("Has Mobile Number"),
3182
+ hasImage: boolean16().optional().default(false).label("Has Photo"),
3183
+ hasHeadline: boolean16().optional().default(false).label("Has Headline"),
3184
+ hasAboutMe: boolean16().optional().default(false).label("Has About Me"),
3185
+ hasMobile: boolean16().optional().default(false).label("Has Mobile Number"),
2259
3186
  // Segment by WHICH profile section is missing (profileCompleteness.missing[]),
2260
3187
  // rather than only by the overall percentage.
2261
3188
  missingSections: tokens("Missing Profile Sections"),
@@ -2267,50 +3194,39 @@ var ListFilterBranchSchema = object18({
2267
3194
  // data lives in other services' collections. Tri-state: `false` is a real
2268
3195
  // filter ("has NO job alert"), which is the audience an alert nudge wants, so
2269
3196
  // these are `undefined` when unset rather than defaulting to false.
2270
- hasJobAlert: boolean11().optional().default(void 0).label("Has a Job Alert"),
2271
- hasApplied: boolean11().optional().default(void 0).label("Has Applied"),
2272
- appliedWithinDays: days("Applied Within (days)"),
2273
- // ── Free-text search (name / email) ─────────────────────────────────────────
2274
- search: string15().trim().optional().label("Search")
3197
+ hasJobAlert: boolean16().optional().default(void 0).label("Has a Job Alert"),
3198
+ hasApplied: boolean16().optional().default(void 0).label("Has Applied"),
3199
+ appliedWithinDays: days("Applied Within (days)")
3200
+ // `search` is NOT here. It is free text over name/email, not a facet: it names
3201
+ // no field on a user document, user-service treats it as a RESERVED PARAM
3202
+ // beside the filter rather than a filter key, and it is meaningless inside an
3203
+ // `or` branch (a union of "matches this text" with a facet clause). It travels
3204
+ // as a sibling of the filter — see `PreviewFilterSchema` in marketing-service —
3205
+ // so a saved audience cannot carry free text it would silently re-run later.
2275
3206
  }).label("Audience Filter Branch");
2276
- var rejectUnknownKeys = (schema, allowed) => schema.test(
2277
- "no-unknown-filter-keys",
2278
- ({ value, label }) => {
2279
- const known = new Set(allowed());
2280
- const bad = Object.keys(value ?? {}).filter(
2281
- (k) => !known.has(k)
2282
- );
2283
- return `${label} has unsupported keys: ${bad.join(", ")}`;
2284
- },
2285
- (value) => {
2286
- if (value == null) return true;
2287
- const known = new Set(allowed());
2288
- return Object.keys(value).every(
2289
- (k) => known.has(k)
2290
- );
2291
- }
2292
- );
2293
- var BRANCH_KEYS = Object.keys(ListFilterBranchSchema.fields);
2294
- var ListFilterBranch = rejectUnknownKeys(
2295
- ListFilterBranchSchema,
2296
- () => BRANCH_KEYS
2297
- );
2298
- var ListFilterSchema = rejectUnknownKeys(
2299
- ListFilterBranchSchema.shape({
2300
- or: array10().of(ListFilterBranch).optional().default([]).label("Or Any Of")
2301
- }).label("Audience Filter"),
3207
+ var BRANCH_KEYS = Object.keys(UserFilterBranchShape.fields);
3208
+ var UserFilterBranch = rejectUnknownKeys(UserFilterBranchShape, () => BRANCH_KEYS);
3209
+ var UserFilterSchema = rejectUnknownKeys(
3210
+ UserFilterBranchShape.shape({
3211
+ or: array15().of(UserFilterBranch).optional().default([]).label("Or Any Of")
3212
+ }).label("User Filter"),
2302
3213
  () => [...BRANCH_KEYS, "or"]
2303
3214
  );
2304
- var MailingListSchema = object18({
2305
- name: string15().required().min(1).max(120).label("Name"),
2306
- description: string15().optional().max(500).label("Description"),
2307
- filter: ListFilterSchema.default(() => ListFilterSchema.getDefault())
3215
+
3216
+ // src/marketing/marketing.schema.ts
3217
+ var MailingListFilterSchema = UserFilterSchema;
3218
+ var MailingListSchema = object31({
3219
+ name: string22().required().min(1).max(120).label("Name"),
3220
+ description: string22().optional().max(500).label("Description"),
3221
+ filter: MailingListFilterSchema.default(
3222
+ () => MailingListFilterSchema.getDefault()
3223
+ )
2308
3224
  }).label("Mailing List");
2309
- var CampaignTriggerSchema = object18({
2310
- kind: string15().oneOf(SupportedCampaignTriggerKinds).required().default("manual" /* Manual */).label("Trigger"),
3225
+ var CampaignTriggerSchema = object31({
3226
+ kind: string22().oneOf(SupportedCampaignTriggerKinds).required().default("manual" /* Manual */).label("Trigger"),
2311
3227
  // IANA zone, e.g. "Asia/Kolkata". Local wall-clock times are interpreted in it,
2312
3228
  // so occurrences land at the intended hour across DST rather than drifting.
2313
- timezone: string15().optional().default("UTC").label("Timezone").when("kind", {
3229
+ timezone: string22().optional().default("UTC").label("Timezone").when("kind", {
2314
3230
  is: (k) => k !== "manual" /* Manual */,
2315
3231
  then: (s) => s.required()
2316
3232
  }),
@@ -2324,26 +3240,26 @@ var CampaignTriggerSchema = object18({
2324
3240
  is: "schedule" /* Schedule */,
2325
3241
  then: (s) => s.required()
2326
3242
  }),
2327
- interval: object18({
2328
- every: number7().integer().min(1).max(365).required().label("Every"),
2329
- unit: string15().oneOf(SupportedCampaignIntervalUnits).required().label("Unit")
3243
+ interval: object31({
3244
+ every: number14().integer().min(1).max(365).required().label("Every"),
3245
+ unit: string22().oneOf(SupportedCampaignIntervalUnits).required().label("Unit")
2330
3246
  }).optional().default(void 0).label("Repeats").when("kind", {
2331
3247
  is: "schedule" /* Schedule */,
2332
3248
  then: (s) => s.required()
2333
3249
  }),
2334
3250
  /** Local wall-clock send time, "HH:mm" in `timezone`. */
2335
- timeOfDay: string15().matches(/^([01]\d|2[0-3]):[0-5]\d$/, "Use HH:mm").optional().default("09:00").label("Time of Day"),
3251
+ timeOfDay: string22().matches(/^([01]\d|2[0-3]):[0-5]\d$/, "Use HH:mm").optional().default("09:00").label("Time of Day"),
2336
3252
  /** For weekly intervals: ISO weekdays, 1 = Monday .. 7 = Sunday. */
2337
- byWeekday: array10().of(number7().integer().min(1).max(7).required()).optional().default([]).label("On Days"),
3253
+ byWeekday: array16().of(number14().integer().min(1).max(7).required()).optional().default([]).label("On Days"),
2338
3254
  /**
2339
3255
  * For monthly intervals: 1-31, or -1 meaning "last day of the month". A month
2340
3256
  * shorter than the chosen day is clamped to its last day, so 31 in February
2341
3257
  * fires on the 28th/29th rather than being skipped.
2342
3258
  */
2343
- byMonthday: number7().integer().min(-1).max(31).notOneOf([0]).optional().default(void 0).label("Day of Month"),
2344
- end: object18({
2345
- kind: string15().oneOf(SupportedCampaignEndKinds).required().label("Ends"),
2346
- occurrences: number7().integer().min(1).max(1e4).optional().default(void 0).label("After N Sends").when("kind", {
3259
+ byMonthday: number14().integer().min(-1).max(31).notOneOf([0]).optional().default(void 0).label("Day of Month"),
3260
+ end: object31({
3261
+ kind: string22().oneOf(SupportedCampaignEndKinds).required().label("Ends"),
3262
+ occurrences: number14().integer().min(1).max(1e4).optional().default(void 0).label("After N Sends").when("kind", {
2347
3263
  is: "afterOccurrences" /* AfterOccurrences */,
2348
3264
  then: (s) => s.required()
2349
3265
  }),
@@ -2351,52 +3267,80 @@ var CampaignTriggerSchema = object18({
2351
3267
  is: "onDate" /* OnDate */,
2352
3268
  then: (s) => s.required()
2353
3269
  })
2354
- }).optional().default(() => ({ kind: "never" /* Never */ })).label("End Condition"),
2355
- // kind: lifecycle
2356
- anchorField: string15().oneOf(SupportedCampaignAnchorFields).optional().default(void 0).label("Anchor Date").when("kind", {
2357
- is: "lifecycle" /* Lifecycle */,
2358
- then: (s) => s.required()
2359
- }),
2360
- /** Days after the anchor at which this user becomes eligible. */
2361
- offsetDays: number7().integer().min(0).max(3650).optional().default(void 0).label("Days After Anchor").when("kind", {
2362
- is: "lifecycle" /* Lifecycle */,
2363
- then: (s) => s.required()
2364
- }),
3270
+ }).optional().default(() => ({ kind: "never" /* Never */ })).label("End Condition")
3271
+ }).label("Trigger");
3272
+ var CampaignDataSourceSchema = object31({
3273
+ id: string22().required().label("Data Source"),
2365
3274
  /**
2366
- * How long a user stays eligible after crossing the offset. Needed because the
2367
- * scheduler samples periodically and a zero-width window would miss anyone not
2368
- * observed on the exact day. A user inside the window on several ticks is still
2369
- * mailed once: the permanent idempotency key, not this value, guarantees that.
3275
+ * Derive this source's filter per recipient from their profile, instead of
3276
+ * using the admin-entered `params` for everyone.
3277
+ *
3278
+ * A second AXIS on a datasource, not a datasource of its own: a
3279
+ * personalisation filter is an ordinary filter for that source, the only
3280
+ * difference being who wrote it. Jobs previously shipped the two modes as
3281
+ * two registry entries (`recent-jobs` / `matched-jobs`), which let them drift
3282
+ * apart down to their output names.
3283
+ *
3284
+ * Optional, and absent reads as false, so every existing campaign stays
3285
+ * un-personalised. Whether a source can honour it at all is declared by the
3286
+ * source itself (`supportsPersonalisation`), which this package cannot see.
2370
3287
  */
2371
- windowDays: number7().integer().min(1).max(90).optional().default(2).label("Eligibility Window (days)")
2372
- }).label("Trigger");
2373
- var BindingSchema = object18({
2374
- kind: string15().oneOf(SupportedBindingKinds).required().label("Source"),
2375
- value: string15().required().min(1).max(2e3).label("Value")
2376
- }).label("Binding");
2377
- var CampaignSchema = object18({
2378
- name: string15().required().min(1).max(200).label("Campaign Name"),
3288
+ personalised: boolean17().optional().label("Personalised"),
3289
+ /**
3290
+ * A free-form bag whose keys are defined by the chosen source's OWN
3291
+ * `paramsSchema`, which this package cannot see. Same `lazy` treatment as
3292
+ * `bindings` above, and for the same reason: a bare `object()` has no known
3293
+ * keys, so under `stripUnknown: true` every param is "unknown" and the bag
3294
+ * arrives as `{}` — a campaign configured to mail remote Python roles in
3295
+ * Bangalore silently saves as an unfiltered platform-wide digest.
3296
+ *
3297
+ * Values stay unvalidated here on purpose; the marketing service validates
3298
+ * them against the real source schema in `checkCampaignConfig`, which is the
3299
+ * only place that knows what a given source accepts.
3300
+ */
3301
+ params: lazy2((value) => {
3302
+ const keys = value && typeof value === "object" ? Object.keys(value) : [];
3303
+ return object31(
3304
+ Object.fromEntries(keys.map((k) => [k, mixed5().optional()]))
3305
+ ).optional().default(() => ({})).label("Parameters");
3306
+ })
3307
+ }).label("Data Source");
3308
+ var TemplateVariableBindingSchema = object31({
3309
+ kind: string22().oneOf(SupportedTemplateVariableBindingKinds).required().label("Source"),
3310
+ value: string22().required().min(1).max(2e3).label("Value")
3311
+ }).label("Template Variable Binding");
3312
+ var CampaignSchema = object31({
3313
+ name: string22().required().min(1).max(200).label("Campaign Name"),
2379
3314
  // The template renders its own subject; an override is only needed for the
2380
3315
  // inline-HTML path, hence not required when a template is chosen.
2381
- subject: string15().optional().max(255).label("Subject").when("templateKey", {
3316
+ subject: string22().optional().max(255).label("Subject").when("templateKey", {
2382
3317
  is: (v) => !v,
2383
3318
  then: (s) => s.required().min(1)
2384
3319
  }),
2385
3320
  /**
2386
3321
  * Inline HTML body; may contain {{unsubscribe_url}}. Optional now that a
2387
3322
  * campaign can instead name a registry template — exactly one of the two is
2388
- * required. Scheduled and lifecycle campaigns must use a template: the
2389
- * registry path is the one with a compliance-checked footer and a render test,
3323
+ * required. Scheduled campaigns must use a template: the registry path is the
3324
+ * one with a compliance-checked footer and a render test,
2390
3325
  * and hand-pasted HTML that repeats forever is a mistake that repeats forever.
2391
3326
  */
2392
- html: string15().optional().label("HTML Body").when("templateKey", {
3327
+ html: string22().optional().label("HTML Body").when("templateKey", {
2393
3328
  is: (v) => !v,
2394
3329
  then: (s) => s.required().min(1)
2395
3330
  }),
2396
- text: string15().optional().label("Plain-text Body"),
2397
- listId: string15().required().label("Mailing List"),
3331
+ text: string22().optional().label("Plain-text Body"),
3332
+ listId: string22().required().label("Mailing List"),
3333
+ /**
3334
+ * Where the campaign is in its life. See `CampaignStatus` for the transitions.
3335
+ *
3336
+ * Declared here so the enum is ENFORCED on write. It was previously written
3337
+ * straight onto the document by the service with no schema behind it, and a
3338
+ * value the enum never contained (`scheduled`) reached production rows and
3339
+ * rendered in the admin list as though it were real.
3340
+ */
3341
+ status: string22().oneOf(SupportedCampaignStatuses).optional().default("draft" /* Draft */).label("Status"),
2398
3342
  /** Key from email-service's template registry. Mutually exclusive with `html`. */
2399
- templateKey: string15().optional().label("Email Template"),
3343
+ templateKey: string22().optional().label("Email Template"),
2400
3344
  /**
2401
3345
  * One entry per variable the chosen template declares. Validated at save time
2402
3346
  * against the template's own `variables`, so a missing required binding is a
@@ -2413,56 +3357,50 @@ var CampaignSchema = object18({
2413
3357
  */
2414
3358
  bindings: lazy2((value) => {
2415
3359
  const keys = value && typeof value === "object" ? Object.keys(value) : [];
2416
- return object18(
2417
- Object.fromEntries(keys.map((k) => [k, BindingSchema.required()]))
3360
+ return object31(
3361
+ Object.fromEntries(keys.map((k) => [k, TemplateVariableBindingSchema.required()]))
2418
3362
  ).optional().default(() => ({})).label("Template Data");
2419
3363
  }),
2420
- /** Registered data source supplying per-recipient content, plus its params. */
2421
- dataSource: object18({
2422
- id: string15().required().label("Data Source"),
2423
- /**
2424
- * A free-form bag whose keys are defined by the chosen source's OWN
2425
- * `paramsSchema`, which this package cannot see. Same `lazy` treatment as
2426
- * `bindings` above, and for the same reason: a bare `object()` has no known
2427
- * keys, so under `stripUnknown: true` every param is "unknown" and the bag
2428
- * arrives as `{}` — a campaign configured to mail remote Python roles in
2429
- * Bangalore silently saves as an unfiltered platform-wide digest.
2430
- *
2431
- * Values stay unvalidated here on purpose; the marketing service validates
2432
- * them against the real source schema in `checkCampaignConfig`, which is the
2433
- * only place that knows what a given source accepts.
2434
- */
2435
- params: lazy2((value) => {
2436
- const keys = value && typeof value === "object" ? Object.keys(value) : [];
2437
- return object18(
2438
- Object.fromEntries(keys.map((k) => [k, mixed4().optional()]))
2439
- ).optional().default(() => ({})).label("Parameters");
2440
- })
2441
- }).optional().default(void 0).label("Data Source"),
3364
+ /**
3365
+ * The data sources this campaign draws content from.
3366
+ *
3367
+ * SEVERAL, because one mail may combine them. Each source's outputs are
3368
+ * NAMESPACED by its id in the rendered payload (`jobs.jobCount`,
3369
+ * `groups.groupCount`), so two sources providing the same name cannot
3370
+ * overwrite each other and a template says which one it means.
3371
+ */
3372
+ dataSources: array16().of(CampaignDataSourceSchema).optional().default(void 0).label("Data Sources"),
2442
3373
  trigger: CampaignTriggerSchema.default(
2443
3374
  () => CampaignTriggerSchema.getDefault()
2444
3375
  ),
2445
3376
  // ── Safety ──────────────────────────────────────────────────────────────────
2446
- // Both default to the safe position: a new campaign neither runs nor sends
2447
- // until someone deliberately says so, and says so twice.
2448
- enabled: boolean11().optional().default(false).label("Enabled"),
2449
- dryRun: boolean11().optional().default(true).label("Dry Run"),
2450
- maxRecipients: number7().integer().min(1).max(CAMPAIGN_MAX_RECIPIENTS_CEILING).optional().default(void 0).label("Max Recipients"),
2451
- utm: object18({
2452
- campaign: string15().optional().max(120).label("utm_campaign"),
2453
- content: string15().optional().max(120).label("utm_content")
3377
+ /**
3378
+ * Still TWO deliberate acts before mail leaves, as before: a campaign must be
3379
+ * activated (`status: running`) AND taken out of dry run.
3380
+ *
3381
+ * The first half used to be a separate `enabled` boolean living alongside the
3382
+ * status, which meant two fields encoding one idea and able to disagree — an
3383
+ * `enabled: false` campaign whose status said it was sending was
3384
+ * representable, and the admin list showed exactly that. Activation is a
3385
+ * state transition, so it belongs in the state.
3386
+ */
3387
+ dryRun: boolean17().optional().default(true).label("Dry Run"),
3388
+ maxRecipients: number14().integer().min(1).max(CAMPAIGN_MAX_RECIPIENTS_CEILING).optional().default(void 0).label("Max Recipients"),
3389
+ utm: object31({
3390
+ campaign: string22().optional().max(120).label("utm_campaign"),
3391
+ content: string22().optional().max(120).label("utm_content")
2454
3392
  }).optional().default(void 0).label("Link Tagging")
2455
3393
  }).label("Campaign");
2456
3394
 
2457
3395
  // src/pagination/pagination.schema.ts
2458
- import { number as number8, object as object19, string as string16 } from "yup";
2459
- var PaginationSchema = object19().shape({
2460
- page: number8().required().min(1).label("Page").default(1),
2461
- limit: number8().required().min(1).max(100).label("Limit").default(10),
2462
- sortBy: string16().notOneOf([""]).optional().label("SortBy"),
2463
- nextPage: string16().optional().label("Next Page Token"),
2464
- prevPage: string16().optional().label("Previous Page Token"),
2465
- sortDirection: string16().oneOf(["asc", "desc"]).optional().label("Sort Direction")
3396
+ import { number as number15, object as object32, string as string23 } from "yup";
3397
+ var PaginationSchema = object32().shape({
3398
+ page: number15().required().min(1).label("Page").default(1),
3399
+ limit: number15().required().min(1).max(100).label("Limit").default(10),
3400
+ sortBy: string23().notOneOf([""]).optional().label("SortBy"),
3401
+ nextPage: string23().optional().label("Next Page Token"),
3402
+ prevPage: string23().optional().label("Previous Page Token"),
3403
+ sortDirection: string23().oneOf(["asc", "desc"]).optional().label("Sort Direction")
2466
3404
  });
2467
3405
 
2468
3406
  // src/pagination/pagination.constant.ts
@@ -2478,7 +3416,7 @@ var DefaultPaginationOptions = {
2478
3416
  };
2479
3417
 
2480
3418
  // src/saved-search/saved-search.schema.ts
2481
- import { number as number9, object as object20, string as string17 } from "yup";
3419
+ import { number as number16, object as object33, string as string24 } from "yup";
2482
3420
 
2483
3421
  // src/saved-search/saved-search.constant.ts
2484
3422
  var SavedSearchStatus = /* @__PURE__ */ ((SavedSearchStatus2) => {
@@ -2489,21 +3427,21 @@ var SavedSearchStatus = /* @__PURE__ */ ((SavedSearchStatus2) => {
2489
3427
  var SupportedSavedSearchStatuses = Object.values(SavedSearchStatus);
2490
3428
 
2491
3429
  // src/saved-search/saved-search.schema.ts
2492
- var SavedSearchSchema = object20({
3430
+ var SavedSearchSchema = object33({
2493
3431
  /** Public identifier. Server-assigned. */
2494
- shortId: string17().trim().label("Short Id"),
3432
+ shortId: string24().trim().label("Short Id"),
2495
3433
  /**
2496
3434
  * Owner's Better Auth account id. Server-assigned from the verified token,
2497
3435
  * never accepted from a request body.
2498
3436
  */
2499
- ownerUserId: string17().trim().label("Owner"),
3437
+ ownerUserId: string24().trim().label("Owner"),
2500
3438
  /** What the user calls this search, e.g. "Remote React roles". */
2501
- label: string17().trim().max(120).required().label("Label"),
3439
+ label: string24().trim().max(120).required().label("Label"),
2502
3440
  /**
2503
3441
  * The free-text query. Optional because a search can be filters-only ("all
2504
3442
  * remote jobs in Sweden"), which is a legitimate alert to want.
2505
3443
  */
2506
- search: string17().trim().max(500).optional().label("Search Text"),
3444
+ search: string24().trim().max(500).optional().label("Search Text"),
2507
3445
  /**
2508
3446
  * Field filters, in the same `{ field: values }` shape the search endpoint
2509
3447
  * parses from `filter[field]=a,b`. Held as a free-form object rather than a
@@ -2511,7 +3449,7 @@ var SavedSearchSchema = object20({
2511
3449
  * change independently; validating them here would put this package in the
2512
3450
  * business of tracking that vocabulary.
2513
3451
  */
2514
- filter: object20().optional().default({}).label("Filters"),
3452
+ filter: object33().optional().default({}).label("Filters"),
2515
3453
  /**
2516
3454
  * How often to mail matches. `off` keeps the search saved but silent, which is
2517
3455
  * the difference between "I don't want mail" and "delete my search".
@@ -2520,26 +3458,26 @@ var SavedSearchSchema = object20({
2520
3458
  * than defining a second one: two vocabularies for the same concept is how
2521
3459
  * they drift.
2522
3460
  */
2523
- frequency: string17().oneOf(SupportedJobAlertFrequencies).default("off" /* Off */).label("Alert Frequency"),
2524
- status: string17().oneOf(SupportedSavedSearchStatuses).default("active" /* Active */).label("Status"),
3461
+ frequency: string24().oneOf(SupportedJobAlertFrequencies).default("off" /* Off */).label("Alert Frequency"),
3462
+ status: string24().oneOf(SupportedSavedSearchStatuses).default("active" /* Active */).label("Status"),
2525
3463
  /**
2526
3464
  * Epoch ms of the last alert sent for this search. The matching sweep reads it
2527
3465
  * to decide whether the cadence is due, and writes it only AFTER a successful
2528
3466
  * publish, so a failed run retries next tick rather than silently skipping a
2529
3467
  * period.
2530
3468
  */
2531
- lastNotifiedAt: number9().optional().label("Last Notified At"),
3469
+ lastNotifiedAt: number16().optional().label("Last Notified At"),
2532
3470
  /**
2533
3471
  * Epoch ms high-water mark: only jobs created after this are "new" for the
2534
3472
  * next alert. Kept separate from `lastNotifiedAt` because a run that finds no
2535
3473
  * matches should still advance what counts as new, without implying mail was
2536
3474
  * sent.
2537
3475
  */
2538
- lastMatchedAt: number9().optional().label("Last Matched At")
3476
+ lastMatchedAt: number16().optional().label("Last Matched At")
2539
3477
  }).noUnknown().strict();
2540
3478
 
2541
3479
  // src/report/report.schema.ts
2542
- import { mixed as mixed5, object as object21, string as string18 } from "yup";
3480
+ import { mixed as mixed6, object as object34, string as string25 } from "yup";
2543
3481
 
2544
3482
  // src/report/report.constant.ts
2545
3483
  var ResourceType = /* @__PURE__ */ ((ResourceType3) => {
@@ -2563,15 +3501,15 @@ var ReportReason = /* @__PURE__ */ ((ReportReason3) => {
2563
3501
  var SupportedReportReasons = Object.values(ReportReason);
2564
3502
 
2565
3503
  // src/report/report.schema.ts
2566
- var ReportSchema = object21({
2567
- type: mixed5().oneOf(SupportedResourceTypes).required().label("Type"),
2568
- resourceId: string18().required().label("Resource ID"),
2569
- reason: mixed5().oneOf(SupportedReportReasons).required("Please choose a reason").label("Reason"),
2570
- comment: string18().max(1e3).optional().label("Comment")
3504
+ var ReportSchema = object34({
3505
+ type: mixed6().oneOf(SupportedResourceTypes).required().label("Type"),
3506
+ resourceId: string25().required().label("Resource ID"),
3507
+ reason: mixed6().oneOf(SupportedReportReasons).required("Please choose a reason").label("Reason"),
3508
+ comment: string25().max(1e3).optional().label("Comment")
2571
3509
  }).label("Report Schema");
2572
3510
 
2573
- // src/feedback/feedback.schema.ts
2574
- import { mixed as mixed6, object as object22, string as string19 } from "yup";
3511
+ // src/feedback/feedback-filter.schema.ts
3512
+ import { object as object35 } from "yup";
2575
3513
 
2576
3514
  // src/feedback/feedback.constant.ts
2577
3515
  var FeedbackType = /* @__PURE__ */ ((FeedbackType3) => {
@@ -2593,64 +3531,317 @@ var FeedbackStatus = /* @__PURE__ */ ((FeedbackStatus3) => {
2593
3531
  var SupportedFeedbackStatuses = Object.values(FeedbackStatus);
2594
3532
  var FEEDBACK_MESSAGE_MAX = 4e3;
2595
3533
 
3534
+ // src/feedback/feedback-filter.schema.ts
3535
+ var FeedbackFilterShape = object35({
3536
+ type: enumFacet(SupportedFeedbackTypes, "Type"),
3537
+ status: enumFacet(SupportedFeedbackStatuses, "Status")
3538
+ }).label("Feedback Filter");
3539
+ var FeedbackFilterSchema = rejectUnknownKeys(
3540
+ FeedbackFilterShape,
3541
+ () => Object.keys(FeedbackFilterShape.fields)
3542
+ );
3543
+
2596
3544
  // src/feedback/feedback.schema.ts
2597
- var FeedbackSchema = object22({
2598
- type: mixed6().oneOf(SupportedFeedbackTypes).required().label("Type"),
2599
- message: string19().trim().required("Please enter a message").max(FEEDBACK_MESSAGE_MAX).label("Message"),
2600
- email: string19().email().optional().label("Email"),
2601
- pageUrl: string19().optional().label("Page URL"),
2602
- status: mixed6().oneOf(SupportedFeedbackStatuses).optional().label("Status")
3545
+ import { mixed as mixed7, object as object36, string as string26 } from "yup";
3546
+ var FeedbackSchema = object36({
3547
+ type: mixed7().oneOf(SupportedFeedbackTypes).required().label("Type"),
3548
+ message: string26().trim().required("Please enter a message").max(FEEDBACK_MESSAGE_MAX).label("Message"),
3549
+ email: string26().email().optional().label("Email"),
3550
+ pageUrl: string26().optional().label("Page URL"),
3551
+ status: mixed7().oneOf(SupportedFeedbackStatuses).optional().label("Status")
2603
3552
  }).label("Feedback Schema");
2604
3553
 
3554
+ // src/skill/skill-filter.schema.ts
3555
+ import { object as object37 } from "yup";
3556
+ var SkillFilterShape = object37({
3557
+ /** Display names, matched exactly. Free-text matching is `?search=`. */
3558
+ names: tokens("Names"),
3559
+ /**
3560
+ * Taxonomy concept ids, e.g. "power-bi".
3561
+ *
3562
+ * `tokens`, NOT `codeTokens`: a conceptId is a lowercase slug, and
3563
+ * uppercasing it would match nothing.
3564
+ */
3565
+ conceptIds: tokens("Concept IDs"),
3566
+ tags: tokens("Tags")
3567
+ }).label("Skill Filter");
3568
+ var SkillFilterSchema = rejectUnknownKeys(
3569
+ SkillFilterShape,
3570
+ () => Object.keys(SkillFilterShape.fields)
3571
+ );
3572
+
3573
+ // src/billing/billing.schema.ts
3574
+ import { array as array17, boolean as boolean18, number as number17, object as object38, string as string27 } from "yup";
3575
+
3576
+ // src/billing/billing.constant.ts
3577
+ var PlanKind = /* @__PURE__ */ ((PlanKind2) => {
3578
+ PlanKind2["Subscription"] = "subscription";
3579
+ PlanKind2["OneOff"] = "one-off";
3580
+ return PlanKind2;
3581
+ })(PlanKind || {});
3582
+ var SupportedPlanKinds = Object.values(PlanKind);
3583
+ var PlanStatus = /* @__PURE__ */ ((PlanStatus2) => {
3584
+ PlanStatus2["Draft"] = "draft";
3585
+ PlanStatus2["Active"] = "active";
3586
+ PlanStatus2["Archived"] = "archived";
3587
+ return PlanStatus2;
3588
+ })(PlanStatus || {});
3589
+ var SupportedPlanStatuses = Object.values(PlanStatus);
3590
+ var GrantType = /* @__PURE__ */ ((GrantType2) => {
3591
+ GrantType2["Boolean"] = "boolean";
3592
+ GrantType2["Quota"] = "quota";
3593
+ GrantType2["Credit"] = "credit";
3594
+ return GrantType2;
3595
+ })(GrantType || {});
3596
+ var SupportedGrantTypes = Object.values(GrantType);
3597
+ var QuotaPeriod = /* @__PURE__ */ ((QuotaPeriod2) => {
3598
+ QuotaPeriod2["Month"] = "month";
3599
+ QuotaPeriod2["Year"] = "year";
3600
+ QuotaPeriod2["Lifetime"] = "lifetime";
3601
+ return QuotaPeriod2;
3602
+ })(QuotaPeriod || {});
3603
+ var SupportedQuotaPeriods = Object.values(QuotaPeriod);
3604
+ var BillingPeriod = /* @__PURE__ */ ((BillingPeriod2) => {
3605
+ BillingPeriod2["Month"] = "month";
3606
+ BillingPeriod2["Year"] = "year";
3607
+ return BillingPeriod2;
3608
+ })(BillingPeriod || {});
3609
+ var SupportedBillingPeriods = Object.values(BillingPeriod);
3610
+ var SubscriptionStatus = /* @__PURE__ */ ((SubscriptionStatus2) => {
3611
+ SubscriptionStatus2["Active"] = "active";
3612
+ SubscriptionStatus2["Trialing"] = "trialing";
3613
+ SubscriptionStatus2["PastDue"] = "past_due";
3614
+ SubscriptionStatus2["Paused"] = "paused";
3615
+ SubscriptionStatus2["Canceled"] = "canceled";
3616
+ return SubscriptionStatus2;
3617
+ })(SubscriptionStatus || {});
3618
+ var SupportedSubscriptionStatuses = Object.values(SubscriptionStatus);
3619
+ var EntitlingSubscriptionStatuses = [
3620
+ "active" /* Active */,
3621
+ "trialing" /* Trialing */,
3622
+ "past_due" /* PastDue */
3623
+ ];
3624
+ var TransactionStatus = /* @__PURE__ */ ((TransactionStatus2) => {
3625
+ TransactionStatus2["Draft"] = "draft";
3626
+ TransactionStatus2["Ready"] = "ready";
3627
+ TransactionStatus2["Billed"] = "billed";
3628
+ TransactionStatus2["Paid"] = "paid";
3629
+ TransactionStatus2["Completed"] = "completed";
3630
+ TransactionStatus2["Canceled"] = "canceled";
3631
+ TransactionStatus2["PastDue"] = "past_due";
3632
+ return TransactionStatus2;
3633
+ })(TransactionStatus || {});
3634
+ var SupportedTransactionStatuses = Object.values(TransactionStatus);
3635
+ var PaddleEnvironment = /* @__PURE__ */ ((PaddleEnvironment2) => {
3636
+ PaddleEnvironment2["Sandbox"] = "sandbox";
3637
+ PaddleEnvironment2["Production"] = "production";
3638
+ return PaddleEnvironment2;
3639
+ })(PaddleEnvironment || {});
3640
+ var SupportedPaddleEnvironments = Object.values(PaddleEnvironment);
3641
+ var FeatureKey = /* @__PURE__ */ ((FeatureKey2) => {
3642
+ FeatureKey2["JobPost"] = "job.post";
3643
+ FeatureKey2["JobAlerts"] = "job.alerts";
3644
+ FeatureKey2["PageCreate"] = "page.create";
3645
+ FeatureKey2["GroupCreate"] = "group.create";
3646
+ FeatureKey2["CandidateFinder"] = "candidate.finder";
3647
+ FeatureKey2["SearchAdvanced"] = "search.advanced";
3648
+ FeatureKey2["GetDiscovered"] = "get-discovered";
3649
+ return FeatureKey2;
3650
+ })(FeatureKey || {});
3651
+ var SupportedFeatureKeys = Object.values(FeatureKey);
3652
+ var EntitlementDenialReason = /* @__PURE__ */ ((EntitlementDenialReason2) => {
3653
+ EntitlementDenialReason2["NotEntitled"] = "not-entitled";
3654
+ EntitlementDenialReason2["QuotaExhausted"] = "quota-exhausted";
3655
+ EntitlementDenialReason2["NoCredits"] = "no-credits";
3656
+ return EntitlementDenialReason2;
3657
+ })(EntitlementDenialReason || {});
3658
+ var SupportedEntitlementDenialReasons = Object.values(
3659
+ EntitlementDenialReason
3660
+ );
3661
+ var GRANT_MAX_LIMIT = 1e5;
3662
+ var CONSUME_MAX_UNITS = 1e3;
3663
+
3664
+ // src/billing/billing.schema.ts
3665
+ var MoneySchema = object38({
3666
+ amountMinor: number17().integer().min(0).required().label("Amount (minor units)"),
3667
+ /**
3668
+ * ISO-4217, as returned by Paddle. Deliberately NOT constrained to
3669
+ * `SupportedSalaryCurrencies`: that list is the job-salary vocabulary, and
3670
+ * Paddle bills in currencies it does not contain. Constraining it here would
3671
+ * reject real payments.
3672
+ */
3673
+ currencyCode: string27().trim().uppercase().required().label("Currency")
3674
+ }).label("Money");
3675
+ var PlanGrantSchema = object38({
3676
+ /**
3677
+ * The gated capability, e.g. `job.post`. A free string on purpose: see the
3678
+ * note on `FeatureKey` in `billing.constant.ts`. Adding a feature to the
3679
+ * paywall must not require releasing this package.
3680
+ */
3681
+ feature: string27().trim().required().min(1).max(64).label("Feature"),
3682
+ type: string27().oneOf(SupportedGrantTypes).required().default("boolean" /* Boolean */).label("Grant Type"),
3683
+ /** Uses per period. Required for `quota`, meaningless otherwise. */
3684
+ limit: number17().integer().min(1).max(GRANT_MAX_LIMIT).optional().label("Limit").when("type", {
3685
+ is: "quota" /* Quota */,
3686
+ then: (s) => s.required("A quota grant needs a limit")
3687
+ }),
3688
+ period: string27().oneOf(SupportedQuotaPeriods).optional().default("month" /* Month */).label("Period").when("type", {
3689
+ is: "quota" /* Quota */,
3690
+ then: (s) => s.required("A quota grant needs a period")
3691
+ }),
3692
+ /** Non-expiring uses granted per purchase. Required for `credit`. */
3693
+ amount: number17().integer().min(1).max(GRANT_MAX_LIMIT).optional().label("Amount").when("type", {
3694
+ is: "credit" /* Credit */,
3695
+ then: (s) => s.required("A credit grant needs an amount")
3696
+ })
3697
+ }).label("Plan Grant");
3698
+ var PaddlePriceIdsSchema = object38({
3699
+ sandbox: array17().of(string27().required()).optional().default([]).label("Sandbox Price IDs"),
3700
+ live: array17().of(string27().required()).optional().default([]).label("Live Price IDs")
3701
+ }).label("Paddle Price IDs");
3702
+ var PlanSchema = object38({
3703
+ /** Stable slug, e.g. `employer-pro`. The identifier everything else references. */
3704
+ key: string27().trim().lowercase().required().min(2).max(64).label("Key"),
3705
+ name: string27().required().min(1).max(120).label("Name"),
3706
+ description: string27().optional().max(500).label("Description"),
3707
+ kind: string27().oneOf(SupportedPlanKinds).required().default("subscription" /* Subscription */).label("Kind"),
3708
+ status: string27().oneOf(SupportedPlanStatuses).required().default("draft" /* Draft */).label("Status"),
3709
+ paddlePriceIds: PaddlePriceIdsSchema.default(
3710
+ () => PaddlePriceIdsSchema.getDefault()
3711
+ ),
3712
+ grants: array17().of(PlanGrantSchema.required()).required().min(1, "A plan must grant at least one feature").label("Grants"),
3713
+ /** Display price for the pricing page. Billing is always Paddle's own figure. */
3714
+ price: MoneySchema.optional().default(void 0).label("Price"),
3715
+ /**
3716
+ * How often this plan bills, for the pricing page's monthly/yearly toggle and
3717
+ * for rendering `price` with the right suffix.
3718
+ *
3719
+ * Display only: Paddle owns the real billing interval, and this never drives a
3720
+ * charge. It is optional because one-off plans have no interval, and because
3721
+ * plans seeded before this field existed must keep validating. Treat absent on
3722
+ * a subscription as "monthly" at the use site rather than defaulting it here,
3723
+ * so an unset value stays visible as unset.
3724
+ */
3725
+ billingPeriod: string27().oneOf(SupportedBillingPeriods).optional().label("Billing Period"),
3726
+ /** Ordering on the pricing page, ascending. */
3727
+ sortOrder: number17().integer().optional().default(0).label("Sort Order"),
3728
+ createdAt: number17().optional().label("Created At"),
3729
+ updatedAt: number17().optional().label("Updated At")
3730
+ }).label("Plan");
3731
+ var BillingSubscriptionSchema = object38({
3732
+ userId: string27().required().label("User ID"),
3733
+ paddleSubscriptionId: string27().required().label("Paddle Subscription ID"),
3734
+ paddleCustomerId: string27().required().label("Paddle Customer ID"),
3735
+ planKey: string27().required().label("Plan Key"),
3736
+ priceIds: array17().of(string27().required()).required().label("Price IDs"),
3737
+ status: string27().oneOf(SupportedSubscriptionStatuses).required().label("Status"),
3738
+ /** Start of the period the quota is anchored to. */
3739
+ currentPeriodStart: number17().optional().label("Current Period Start"),
3740
+ currentPeriodEnd: number17().optional().label("Current Period End"),
3741
+ /** Set when a cancellation is scheduled; access continues until the period ends. */
3742
+ canceledAt: number17().optional().label("Canceled At"),
3743
+ /** `occurred_at` of the newest Paddle event applied. Guards out-of-order writes. */
3744
+ lastEventAt: number17().required().default(0).label("Last Event At"),
3745
+ createdAt: number17().optional().label("Created At"),
3746
+ updatedAt: number17().optional().label("Updated At")
3747
+ }).label("Billing Subscription");
3748
+ var BillingTransactionSchema = object38({
3749
+ userId: string27().required().label("User ID"),
3750
+ paddleTransactionId: string27().required().label("Paddle Transaction ID"),
3751
+ paddleCustomerId: string27().optional().label("Paddle Customer ID"),
3752
+ paddleSubscriptionId: string27().optional().label("Paddle Subscription ID"),
3753
+ planKey: string27().optional().label("Plan Key"),
3754
+ priceIds: array17().of(string27().required()).required().label("Price IDs"),
3755
+ status: string27().oneOf(SupportedTransactionStatuses).required().label("Status"),
3756
+ total: MoneySchema.optional().default(void 0).label("Total"),
3757
+ billedAt: number17().optional().label("Billed At"),
3758
+ lastEventAt: number17().required().default(0).label("Last Event At"),
3759
+ createdAt: number17().optional().label("Created At"),
3760
+ updatedAt: number17().optional().label("Updated At")
3761
+ }).label("Billing Transaction");
3762
+ var FeatureEntitlementSchema = object38({
3763
+ feature: string27().required().label("Feature"),
3764
+ type: string27().oneOf(SupportedGrantTypes).required().label("Grant Type"),
3765
+ /** Whether the feature may be used AT ALL right now. */
3766
+ allowed: boolean18().required().default(false).label("Allowed"),
3767
+ /** Quota ceiling for the period. Absent for boolean grants. */
3768
+ limit: number17().integer().optional().label("Limit"),
3769
+ used: number17().integer().optional().label("Used"),
3770
+ /** Quota remaining plus any credit balance. Absent for boolean grants. */
3771
+ remaining: number17().integer().optional().label("Remaining"),
3772
+ /** Non-expiring balance from one-off purchases. */
3773
+ credits: number17().integer().optional().label("Credits"),
3774
+ /** When the quota next refreshes. */
3775
+ resetsAt: number17().optional().label("Resets At")
3776
+ }).label("Feature Entitlement");
3777
+ var EntitlementDenialSchema = object38({
3778
+ feature: string27().required().label("Feature"),
3779
+ reason: string27().oneOf(SupportedEntitlementDenialReasons).required().label("Reason"),
3780
+ limit: number17().integer().optional().label("Limit"),
3781
+ used: number17().integer().optional().label("Used"),
3782
+ resetsAt: number17().optional().label("Resets At"),
3783
+ upgradeUrl: string27().optional().label("Upgrade URL")
3784
+ }).label("Entitlement Denial");
3785
+ var EntitlementConsumeSchema = object38({
3786
+ userId: string27().required().label("User ID"),
3787
+ feature: string27().trim().required().min(1).max(64).label("Feature"),
3788
+ units: number17().integer().min(1).max(CONSUME_MAX_UNITS).required().default(1).label("Units"),
3789
+ /**
3790
+ * Caller-supplied de-duplication key. Makes a spend exactly-once across the
3791
+ * retries that at-least-once delivery and ordinary network failures produce.
3792
+ */
3793
+ idempotencyKey: string27().trim().required().min(8).max(128).label("Idempotency Key")
3794
+ }).label("Entitlement Consume");
3795
+
2605
3796
  // src/user/user.schema.ts
2606
- import { array as array14, boolean as boolean15, number as number12, object as object34, string as string32 } from "yup";
3797
+ import { array as array21, boolean as boolean22, number as number20, object as object51, string as string40 } from "yup";
2607
3798
 
2608
3799
  // src/user/work-experience.schema.ts
2609
- import { boolean as boolean12, object as object23, string as string20 } from "yup";
2610
- var WorkExperienceSchema = object23().shape({
3800
+ import { boolean as boolean19, object as object39, string as string28 } from "yup";
3801
+ var WorkExperienceSchema = object39().shape({
2611
3802
  company: PageSchema.pick(["name", "slug", "type", "logo"]).deepPartial().concat(PageSchema.pick(["name", "type"])).required().label("Company"),
2612
- designation: string20().trim().required().label("Designation"),
3803
+ designation: string28().trim().required().label("Designation"),
2613
3804
  duration: DurationSchema({
2614
3805
  format: "YYYY-MM",
2615
3806
  startLabel: "Start Date",
2616
3807
  endLabel: "End Date"
2617
3808
  }).required().label("Duration"),
2618
- description: string20().trim().max(3e3).optional().label("Description"),
3809
+ description: string28().trim().max(3e3).optional().label("Description"),
2619
3810
  location: LocationSchema.required().label("Location"),
2620
- isRemote: boolean12().oneOf([true, false]).default(false).label("Is Remote")
3811
+ isRemote: boolean19().oneOf([true, false]).default(false).label("Is Remote")
2621
3812
  }).noUnknown().strict().label("Work Experience");
2622
3813
 
2623
3814
  // src/user/education.schema.ts
2624
- import { boolean as boolean13, object as object24, string as string21 } from "yup";
2625
- var EducationSchema = object24({
3815
+ import { boolean as boolean20, object as object40, string as string29 } from "yup";
3816
+ var EducationSchema = object40({
2626
3817
  institute: PageSchema.pick(["name", "slug", "type", "logo"]).deepPartial().concat(PageSchema.pick(["name", "type"])).required().label("Institute"),
2627
- course: string21().trim().required().label("Course"),
2628
- fieldOfStudy: string21().trim().required().label("Field of Study"),
3818
+ course: string29().trim().required().label("Course"),
3819
+ fieldOfStudy: string29().trim().required().label("Field of Study"),
2629
3820
  duration: DurationSchema({
2630
3821
  format: "YYYY-MM",
2631
3822
  startLabel: "Start Date",
2632
3823
  endLabel: "End Date"
2633
3824
  }).required().label("Duration"),
2634
- description: string21().trim().max(3e3).optional().label("Description"),
2635
- isDistanceLearning: boolean13().default(false).label("Is Distance Learning"),
3825
+ description: string29().trim().max(3e3).optional().label("Description"),
3826
+ isDistanceLearning: boolean20().default(false).label("Is Distance Learning"),
2636
3827
  location: LocationSchema.required().label("Location"),
2637
- studyType: string21().oneOf(SupportedStudyTypes).trim().max(50).required().label("Study Type")
3828
+ studyType: string29().oneOf(SupportedStudyTypes).trim().max(50).required().label("Study Type")
2638
3829
  }).noUnknown().strict().label("Education");
2639
3830
 
2640
3831
  // src/user/user-skill.schema.ts
2641
- import { object as object25, string as string22 } from "yup";
2642
- var UserSkillSchema = object25({
2643
- name: string22().required().label("Name"),
2644
- proficiencyLevel: string22().oneOf(SupportedProficiencyLevels).required().label("Proficiency Level"),
3832
+ import { object as object41, string as string30 } from "yup";
3833
+ var UserSkillSchema = object41({
3834
+ name: string30().required().label("Name"),
3835
+ proficiencyLevel: string30().oneOf(SupportedProficiencyLevels).required().label("Proficiency Level"),
2645
3836
  lastUsed: dateString().format("YYYY-MM").nullable().optional().label("Last Used")
2646
3837
  }).noUnknown().strict().label("Skill");
2647
3838
 
2648
3839
  // src/user/project.schema.ts
2649
- import { object as object26, string as string23 } from "yup";
2650
- var UserProjectSchema = object26({
2651
- name: string23().trim().max(50).required().label("Name"),
2652
- url: string23().optional().label("URL"),
2653
- description: string23().trim().min(100).max(3e3).required().label("Description"),
3840
+ import { object as object42, string as string31 } from "yup";
3841
+ var UserProjectSchema = object42({
3842
+ name: string31().trim().max(50).required().label("Name"),
3843
+ url: string31().optional().label("URL"),
3844
+ description: string31().trim().min(100).max(3e3).required().label("Description"),
2654
3845
  duration: DurationSchema({
2655
3846
  format: "YYYY-MM",
2656
3847
  startLabel: "Start Date",
@@ -2659,54 +3850,68 @@ var UserProjectSchema = object26({
2659
3850
  }).noUnknown().strict().label("Project");
2660
3851
 
2661
3852
  // src/user/user-certification.schema.ts
2662
- import { object as object27, string as string24 } from "yup";
2663
- var UserCertificationSchema = object27({
2664
- name: string24().trim().max(100).required().label("Name"),
3853
+ import { object as object43, string as string32 } from "yup";
3854
+ var UserCertificationSchema = object43({
3855
+ name: string32().trim().max(100).required().label("Name"),
2665
3856
  // TODO: Add validation for authority
2666
- authority: string24().trim().max(100).required().label("Authority"),
2667
- licenseNumber: string24().trim().max(50).optional().label("License Number"),
3857
+ authority: string32().trim().max(100).required().label("Authority"),
3858
+ licenseNumber: string32().trim().max(50).optional().label("License Number"),
2668
3859
  duration: DurationSchema({
2669
3860
  format: "YYYY-MM",
2670
3861
  startLabel: "Start Date",
2671
3862
  endLabel: "End Date"
2672
3863
  }).optional().nullable().label("Duration"),
2673
- url: string24().optional().label("URL")
3864
+ url: string32().optional().label("URL")
2674
3865
  }).noUnknown().strict().label("Certification");
2675
3866
 
2676
3867
  // src/user/user-interest.schema.ts
2677
- import { string as string25 } from "yup";
2678
- var UserInterestSchema = string25().trim().required().label("Interest");
3868
+ import { object as object44, string as string33 } from "yup";
3869
+ var UserInterestSchema = object44({
3870
+ /** The taxonomy entry's id. */
3871
+ id: string33().trim().required().label("Interest Id"),
3872
+ /** Display name, e.g. "Cloud Computing". */
3873
+ name: string33().trim().required().label("Interest"),
3874
+ /** Material-symbols icon name, e.g. "cloud". */
3875
+ logo: string33().trim().required().label("Interest Logo")
3876
+ }).required().label("Interest");
2679
3877
 
2680
3878
  // src/user/user-language.schema.ts
2681
- import { object as object28, string as string26 } from "yup";
2682
- var UserLanguageSchema = object28({
2683
- name: string26().trim().required().label("Name"),
2684
- proficiencyLevel: string26().oneOf(SupportedProficiencyLevels).trim().max(50).required().label("Proficiency Level")
3879
+ import { object as object45, string as string34 } from "yup";
3880
+ var UserLanguageSchema = object45({
3881
+ name: string34().trim().required().label("Name"),
3882
+ proficiencyLevel: string34().oneOf(SupportedProficiencyLevels).trim().max(50).required().label("Proficiency Level")
2685
3883
  }).noUnknown().strict().label("Language");
2686
3884
 
2687
3885
  // src/user/user-additional-info.schema.ts
2688
- import { object as object29, string as string27 } from "yup";
2689
- var UserAdditionalInfoSchema = object29({
2690
- title: string27().trim().max(60).required().label("Title"),
2691
- description: string27().trim().max(1e3).required().label("Description")
3886
+ import { object as object46, string as string35 } from "yup";
3887
+ var UserAdditionalInfoSchema = object46({
3888
+ title: string35().trim().max(60).required().label("Title"),
3889
+ description: string35().trim().max(1e3).required().label("Description")
2692
3890
  }).noUnknown().strict().label("User Additional Info");
2693
3891
 
2694
3892
  // src/user/general-detail.schema.ts
2695
- import { number as number10, object as object30, string as string28 } from "yup";
2696
- var UserGeneralDetailSchema = object30({
2697
- id: string28().optional().label("ID"),
2698
- name: object30().shape({
2699
- first: string28().trim().max(50).required().label("First Name"),
2700
- last: string28().trim().max(50).optional().label("Last Name")
3893
+ import { number as number18, object as object47, string as string36 } from "yup";
3894
+ var UserGeneralDetailSchema = object47({
3895
+ id: string36().optional().label("ID"),
3896
+ name: object47().shape({
3897
+ first: string36().trim().max(50).required().label("First Name"),
3898
+ last: string36().trim().max(50).optional().label("Last Name")
2701
3899
  }).required().label("Name"),
2702
- headline: string28().trim().max(200).optional().label("Headline"),
2703
- image: string28().optional().label("Image"),
2704
- aboutMe: string28().trim().max(3e3).optional().label("About Me"),
2705
- email: string28().required().label("Email"),
2706
- mobile: string28().nullable().optional().label("Mobile"),
2707
- emailVerified: string28().nullable().optional().label("Email Verified"),
2708
- mobileVerified: string28().nullable().optional().label("Mobile Verified"),
2709
- experienceLevel: string28().oneOf(SupportedExperienceLevels).required().label("Experience level"),
3900
+ headline: string36().trim().max(200).optional().label("Headline"),
3901
+ image: string36().optional().label("Image"),
3902
+ // The DISPLAYED avatar (`image`) is a flattened export with any status frame
3903
+ // ("#OpenToWork", …) already burned into the pixels. `imageOriginal` keeps the
3904
+ // un-framed source the user uploaded, so the image builder can reopen it and
3905
+ // apply a DIFFERENT frame instead of stacking a second one on top of the first.
3906
+ // Optional and absent on every pre-existing user: treat a missing value as
3907
+ // "only the flattened image is available".
3908
+ imageOriginal: string36().optional().label("Original Image"),
3909
+ aboutMe: string36().trim().max(3e3).optional().label("About Me"),
3910
+ email: string36().required().label("Email"),
3911
+ mobile: string36().nullable().optional().label("Mobile"),
3912
+ emailVerified: string36().nullable().optional().label("Email Verified"),
3913
+ mobileVerified: string36().nullable().optional().label("Mobile Verified"),
3914
+ experienceLevel: string36().oneOf(SupportedExperienceLevels).required().label("Experience level"),
2710
3915
  /**
2711
3916
  * The user's HIGHEST qualification, e.g. `bachelor`.
2712
3917
  *
@@ -2725,7 +3930,7 @@ var UserGeneralDetailSchema = object30({
2725
3930
  * UNCONSTRAINED on education rather than filtering the user out — the same
2726
3931
  * opt-out reasoning `notificationPrefs` uses.
2727
3932
  */
2728
- educationLevel: string28().oneOf(SupportedEducationLevels).optional().label("Education level"),
3933
+ educationLevel: string36().oneOf(SupportedEducationLevels).optional().label("Education level"),
2729
3934
  location: LocationSchema.required().label("Location"),
2730
3935
  /**
2731
3936
  * Where the account was created, captured once and never rewritten.
@@ -2741,38 +3946,38 @@ var UserGeneralDetailSchema = object30({
2741
3946
  * - `location` derived from the user's self-entered profile location
2742
3947
  * - `default` assumed (no signal available); presumption, not evidence
2743
3948
  */
2744
- signupContext: object30().shape({
3949
+ signupContext: object47().shape({
2745
3950
  // ISO 3166-1 alpha-2, uppercase — the shape `cf-ipcountry` returns, so
2746
3951
  // measured and inferred values are directly comparable.
2747
- country: string28().trim().uppercase().length(2).required().label("Signup Country"),
2748
- locale: string28().trim().required().label("Signup Locale"),
2749
- source: string28().oneOf(SupportedSignupContextSources).required().label("Signup Context Source"),
2750
- at: number10().required().label("Signup Context At")
3952
+ country: string36().trim().uppercase().length(2).required().label("Signup Country"),
3953
+ locale: string36().trim().required().label("Signup Locale"),
3954
+ source: string36().oneOf(SupportedSignupContextSources).required().label("Signup Context Source"),
3955
+ at: number18().required().label("Signup Context At")
2751
3956
  }).optional().default(void 0).label("Signup Context"),
2752
- profileVisibility: string28().oneOf(SupportedUserProfileVisibilities).default("public" /* Public */).label("Profile Visibility")
3957
+ profileVisibility: string36().oneOf(SupportedUserProfileVisibilities).default("public" /* Public */).label("Profile Visibility")
2753
3958
  }).noUnknown().strict().label("General Detail");
2754
3959
 
2755
3960
  // src/user/user-job-preferences.schema.ts
2756
- import { array as array11, boolean as boolean14, date as date2, number as number11, object as object31, string as string29 } from "yup";
2757
- var UserJobPreferencesSchema = object31({
3961
+ import { array as array18, boolean as boolean21, date as date2, number as number19, object as object48, string as string37 } from "yup";
3962
+ var UserJobPreferencesSchema = object48({
2758
3963
  /**
2759
3964
  * Whether the job seeker is openly signalling availability. Surfaces the
2760
3965
  * "Open to work" badge on the public profile and boosts visibility in
2761
3966
  * recruiter search. Defaults to `false` - users must opt in.
2762
3967
  */
2763
- openToWork: boolean14().default(false).label("Open to Work"),
3968
+ openToWork: boolean21().default(false).label("Open to Work"),
2764
3969
  /**
2765
3970
  * How urgently the user is looking for their next job. Drives match scoring
2766
3971
  * and can downgrade the visibility of expired or low-relevance postings for
2767
3972
  * users in `passively_browsing` mode.
2768
3973
  */
2769
- jobSearchUrgency: string29().oneOf(SupportedJobSearchUrgencies).required().label("Job Search Urgency"),
3974
+ jobSearchUrgency: string37().oneOf(SupportedJobSearchUrgencies).required().label("Job Search Urgency"),
2770
3975
  /**
2771
3976
  * Locations the user wants to work in. Stores the full `LocationSchema`
2772
3977
  * shape (country, city, state, geo, etc.) as returned by the locations
2773
3978
  * autocomplete API.
2774
3979
  */
2775
- jobLocations: array11().of(LocationSchema.required().label("Job Location")).min(1, "Pick at least one preferred location.").required().label("Job Locations"),
3980
+ jobLocations: array18().of(LocationSchema.required().label("Job Location")).min(1, "Pick at least one preferred location.").required().label("Job Locations"),
2776
3981
  /**
2777
3982
  * Seniority levels the user wants jobs to be matched against. Multi-select
2778
3983
  * (up to 2) for borderline candidates.
@@ -2783,7 +3988,7 @@ var UserJobPreferencesSchema = object31({
2783
3988
  * Both reuse the same `ExperienceLevel` taxonomy from `common.constant.ts`
2784
3989
  * so search/match logic doesn't have to translate between two vocabularies.
2785
3990
  */
2786
- targetExperienceLevels: array11().of(string29().oneOf(SupportedExperienceLevels).required()).min(1, "Pick at least one experience level.").max(2, "You can select at most 2 experience levels.").required().label("Target Experience Levels"),
3991
+ targetExperienceLevels: array18().of(string37().oneOf(SupportedExperienceLevels).required()).min(1, "Pick at least one experience level.").max(2, "You can select at most 2 experience levels.").required().label("Target Experience Levels"),
2787
3992
  /**
2788
3993
  * Specializations the user is searching for - free-form titles.
2789
3994
  *
@@ -2792,19 +3997,19 @@ var UserJobPreferencesSchema = object31({
2792
3997
  * surfaces a curated "Popular Roles" picker for discovery, but the user
2793
3998
  * can ultimately add any title; matching/normalization happens downstream.
2794
3999
  */
2795
- jobRoles: array11().of(string29().trim().max(120).required()).min(1, "Pick at least one role.").required().label("Job Roles"),
4000
+ jobRoles: array18().of(string37().trim().max(120).required()).min(1, "Pick at least one role.").required().label("Job Roles"),
2796
4001
  /**
2797
4002
  * Minimum acceptable annual base salary, in `minSalaryCurrency`.
2798
4003
  * Stored as an integer in the currency's major unit (e.g. dollars, not cents).
2799
4004
  * Defaults to 0 ("any salary") rather than being optional - every job-seeker
2800
4005
  * has a floor, even if it's zero.
2801
4006
  */
2802
- minSalary: number11().integer().min(MIN_SALARY_LOWER_BOUND).max(MIN_SALARY_UPPER_BOUND).default(0).required().label("Minimum Salary"),
2803
- minSalaryCurrency: string29().oneOf(SupportedSalaryCurrencies).default("USD").required().label("Minimum Salary Currency"),
4007
+ minSalary: number19().integer().min(MIN_SALARY_LOWER_BOUND).max(MIN_SALARY_UPPER_BOUND).default(0).required().label("Minimum Salary"),
4008
+ minSalaryCurrency: string37().oneOf(SupportedSalaryCurrencies).default("USD").required().label("Minimum Salary Currency"),
2804
4009
  /**
2805
4010
  * Marketing-attribution capture from the final onboarding step.
2806
4011
  */
2807
- referralSource: string29().oneOf(SupportedReferralSources).required().label("Referral Source"),
4012
+ referralSource: string37().oneOf(SupportedReferralSources).required().label("Referral Source"),
2808
4013
  /**
2809
4014
  * Resumes the user has uploaded. Embedded directly on the user document
2810
4015
  * (capped at 5) - they're cheap to denormalize, every wizard step that
@@ -2818,18 +4023,18 @@ var UserJobPreferencesSchema = object31({
2818
4023
  * AI resume-builder flow - onboarding uploads land here, AI-built resumes
2819
4024
  * live in their own collection.
2820
4025
  */
2821
- resumes: array11().of(
2822
- object31({
4026
+ resumes: array18().of(
4027
+ object48({
2823
4028
  // Stable id assigned on upload - used to address a specific entry
2824
4029
  // for delete / set-primary operations without exposing the GCS URL
2825
4030
  // as a route key.
2826
- id: string29().required().label("ID"),
2827
- url: string29().required().label("Resume URL"),
2828
- filename: string29().trim().max(255).optional().label("Filename"),
2829
- sizeBytes: number11().integer().min(0).optional().label("Size (bytes)"),
2830
- mimeType: string29().trim().max(120).optional().label("MIME Type"),
4031
+ id: string37().required().label("ID"),
4032
+ url: string37().required().label("Resume URL"),
4033
+ filename: string37().trim().max(255).optional().label("Filename"),
4034
+ sizeBytes: number19().integer().min(0).optional().label("Size (bytes)"),
4035
+ mimeType: string37().trim().max(120).optional().label("MIME Type"),
2831
4036
  uploadedAt: date2().required().label("Uploaded At"),
2832
- isPrimary: boolean14().default(false).label("Is Primary")
4037
+ isPrimary: boolean21().default(false).label("Is Primary")
2833
4038
  }).noUnknown().strict().label("Resume")
2834
4039
  ).max(5, "You can keep at most 5 resumes on file.").default([]).label("Resumes"),
2835
4040
  /**
@@ -2856,92 +4061,92 @@ var UserJobPreferencesSchema = object31({
2856
4061
  * APIs. If `user.<field>` is empty there, the correct behavior is empty,
2857
4062
  * because the user has not confirmed it yet.
2858
4063
  */
2859
- pendingPrefill: object31({
2860
- headline: string29().trim().optional().label("Pending Headline"),
2861
- aboutMe: string29().trim().optional().label("Pending About Me"),
2862
- mobile: string29().trim().optional().label("Pending Mobile"),
4064
+ pendingPrefill: object48({
4065
+ headline: string37().trim().optional().label("Pending Headline"),
4066
+ aboutMe: string37().trim().optional().label("Pending About Me"),
4067
+ mobile: string37().trim().optional().label("Pending Mobile"),
2863
4068
  location: LocationSchema.optional().default(void 0).label("Pending Location"),
2864
- experienceLevel: string29().oneOf(SupportedExperienceLevels).optional().label("Pending Experience Level"),
2865
- socialAccounts: array11().of(SocialAccountSchema.required()).optional().label("Pending Social Accounts"),
2866
- workExperiences: array11().of(WorkExperienceSchema.required()).optional().label("Pending Work Experiences"),
2867
- educations: array11().of(EducationSchema.required()).optional().label("Pending Educations")
4069
+ experienceLevel: string37().oneOf(SupportedExperienceLevels).optional().label("Pending Experience Level"),
4070
+ socialAccounts: array18().of(SocialAccountSchema.required()).optional().label("Pending Social Accounts"),
4071
+ workExperiences: array18().of(WorkExperienceSchema.required()).optional().label("Pending Work Experiences"),
4072
+ educations: array18().of(EducationSchema.required()).optional().label("Pending Educations")
2868
4073
  }).nullable().optional().default(void 0).label("Pending Prefill")
2869
4074
  }).noUnknown().strict().label("User Job Preferences Schema");
2870
4075
 
2871
4076
  // src/user/user-recruiter-profile.schema.ts
2872
- import { array as array12, object as object32, string as string30 } from "yup";
2873
- var RecruiterPageLinkSchema = object32({
4077
+ import { array as array19, object as object49, string as string38 } from "yup";
4078
+ var RecruiterPageLinkSchema = object49({
2874
4079
  page: PageSchema.pick(["name", "slug", "type", "logo"]).deepPartial().concat(PageSchema.pick(["name", "type"])).required().label("Company Page"),
2875
- jobTitle: string30().trim().max(100).optional().label("Job Title")
4080
+ jobTitle: string38().trim().max(100).optional().label("Job Title")
2876
4081
  }).noUnknown().strict().label("Recruiter Page Link");
2877
- var UserRecruiterProfileSchema = object32({
2878
- recruiterProfile: object32({
2879
- hiringFor: array12().of(RecruiterPageLinkSchema).default([]).label("Hiring For")
4082
+ var UserRecruiterProfileSchema = object49({
4083
+ recruiterProfile: object49({
4084
+ hiringFor: array19().of(RecruiterPageLinkSchema).default([]).label("Hiring For")
2880
4085
  }).optional().default(void 0).label("Recruiter Profile")
2881
4086
  }).noUnknown().strict().label("User Recruiter Profile");
2882
4087
 
2883
4088
  // src/user/user-coordinator-profile.schema.ts
2884
- import { array as array13, object as object33, string as string31 } from "yup";
2885
- var CoordinatorPageLinkSchema = object33({
4089
+ import { array as array20, object as object50, string as string39 } from "yup";
4090
+ var CoordinatorPageLinkSchema = object50({
2886
4091
  page: PageSchema.pick(["name", "slug", "type", "logo"]).deepPartial().concat(PageSchema.pick(["name", "type"])).required().label("Institute Page"),
2887
- jobTitle: string31().trim().max(100).optional().label("Job Title")
4092
+ jobTitle: string39().trim().max(100).optional().label("Job Title")
2888
4093
  }).noUnknown().strict().label("Coordinator Page Link");
2889
- var UserCoordinatorProfileSchema = object33({
2890
- coordinatorProfile: object33({
2891
- coordinatesAt: array13().of(CoordinatorPageLinkSchema).default([]).label("Coordinates At")
4094
+ var UserCoordinatorProfileSchema = object50({
4095
+ coordinatorProfile: object50({
4096
+ coordinatesAt: array20().of(CoordinatorPageLinkSchema).default([]).label("Coordinates At")
2892
4097
  }).optional().default(void 0).label("Coordinator Profile")
2893
4098
  }).noUnknown().strict().label("User Coordinator Profile");
2894
4099
 
2895
4100
  // src/user/user.schema.ts
2896
- var UserSchema = object34({
4101
+ var UserSchema = object51({
2897
4102
  /**
2898
4103
  * Related auth account id from auth Server (e.g. Better auth https://auth.thejob.dev)
2899
4104
  */
2900
- authAccountId: string32().required().label("Auth Account ID"),
4105
+ authAccountId: string40().required().label("Auth Account ID"),
2901
4106
  /**
2902
4107
  * Social media information about the user (e.g. LinkedIn, GitHub, etc.)
2903
4108
  */
2904
- socialAccounts: array14().of(SocialAccountSchema).default([]).label("Social Accounts"),
4109
+ socialAccounts: array21().of(SocialAccountSchema).default([]).label("Social Accounts"),
2905
4110
  /**
2906
4111
  * Work experience information about the user
2907
4112
  */
2908
- workExperiences: array14().of(WorkExperienceSchema).required().default([]).label("Work Experiences"),
4113
+ workExperiences: array21().of(WorkExperienceSchema).required().default([]).label("Work Experiences"),
2909
4114
  /**
2910
4115
  * Education information about the user
2911
4116
  */
2912
- educations: array14().of(EducationSchema).required().default([]).label("Educations"),
4117
+ educations: array21().of(EducationSchema).required().default([]).label("Educations"),
2913
4118
  /**
2914
4119
  * Skills information about the user
2915
4120
  */
2916
- skills: array14().of(UserSkillSchema).required().default([]).label("Skills"),
4121
+ skills: array21().of(UserSkillSchema).required().default([]).label("Skills"),
2917
4122
  /**
2918
4123
  * Projects information about the user
2919
4124
  */
2920
- projects: array14().of(UserProjectSchema).default([]).label("Projects"),
4125
+ projects: array21().of(UserProjectSchema).default([]).label("Projects"),
2921
4126
  /**
2922
4127
  * Certifications information about the user
2923
4128
  */
2924
- certifications: array14().of(UserCertificationSchema).default([]).label("Certifications"),
4129
+ certifications: array21().of(UserCertificationSchema).default([]).label("Certifications"),
2925
4130
  /**
2926
4131
  * Interests information about the user.
2927
4132
  */
2928
- interests: array14().of(UserInterestSchema).optional().default([]).label("Interests"),
4133
+ interests: array21().of(UserInterestSchema).optional().default([]).label("Interests"),
2929
4134
  /**
2930
4135
  * Languages information about the user
2931
4136
  */
2932
- languages: array14().of(UserLanguageSchema).required().default([]).label("Languages"),
4137
+ languages: array21().of(UserLanguageSchema).required().default([]).label("Languages"),
2933
4138
  /**
2934
4139
  * Additional information about the user
2935
4140
  */
2936
- additionalInfo: array14().of(UserAdditionalInfoSchema).default([]).label("Additional Information"),
4141
+ additionalInfo: array21().of(UserAdditionalInfoSchema).default([]).label("Additional Information"),
2937
4142
  /**
2938
4143
  * Status of the user account
2939
4144
  */
2940
- status: string32().oneOf(SupportedUserStatuses).required().label("Status"),
4145
+ status: string40().oneOf(SupportedUserStatuses).required().label("Status"),
2941
4146
  /**
2942
4147
  * Roles assigned to the user
2943
4148
  */
2944
- roles: array14().of(string32().oneOf(SupportedUserRoles)).required().default(DefaultUserRoles).label("Roles"),
4149
+ roles: array21().of(string40().oneOf(SupportedUserRoles)).required().default(DefaultUserRoles).label("Roles"),
2945
4150
  /**
2946
4151
  * Explicit consent to receive marketing email (newsletters, job alerts by
2947
4152
  * broadcast, product updates). Marketing sends are HARD-GATED on
@@ -2953,18 +4158,18 @@ var UserSchema = object34({
2953
4158
  * unsubscribe sets `subscribed=false` so consent and the email-service
2954
4159
  * suppression list stay consistent. Defaults to NOT subscribed (opt-in).
2955
4160
  */
2956
- marketingConsent: object34({
2957
- subscribed: boolean15().default(false).label("Subscribed to marketing email"),
4161
+ marketingConsent: object51({
4162
+ subscribed: boolean22().default(false).label("Subscribed to marketing email"),
2958
4163
  /** Epoch ms of the last consent change (opt-in or opt-out). */
2959
- updatedAt: number12().optional().label("Consent updated at"),
4164
+ updatedAt: number20().optional().label("Consent updated at"),
2960
4165
  /** Where the consent change originated, e.g. `onboarding`, `unsubscribe`, `settings`. */
2961
- source: string32().optional().label("Consent source"),
4166
+ source: string40().optional().label("Consent source"),
2962
4167
  /**
2963
4168
  * How often the user receives job-alert emails. `off` disables job alerts
2964
4169
  * only; other marketing stays governed by `subscribed`. Unset means the
2965
4170
  * user has not chosen a cadence yet.
2966
4171
  */
2967
- jobAlertFrequency: string32().oneOf(SupportedJobAlertFrequencies).optional().label("Job alert frequency")
4172
+ jobAlertFrequency: string40().oneOf(SupportedJobAlertFrequencies).optional().label("Job alert frequency")
2968
4173
  }).default({ subscribed: false }).label("Marketing Consent"),
2969
4174
  /**
2970
4175
  * Per-channel toggles for NOTIFICATION email — mail about activity on the
@@ -2979,31 +4184,132 @@ var UserSchema = object34({
2979
4184
  *
2980
4185
  * Transactional mail (magic-link, verification) ignores this object entirely.
2981
4186
  */
2982
- notificationPrefs: object34({
2983
- groupActivity: boolean15().default(true).label("Group activity email"),
2984
- jobActivity: boolean15().default(true).label("Job activity email"),
2985
- applicationUpdates: boolean15().default(true).label("Application update email"),
2986
- productUpdates: boolean15().default(false).label("Product update email")
4187
+ notificationPrefs: object51({
4188
+ groupActivity: boolean22().default(true).label("Group activity email"),
4189
+ jobActivity: boolean22().default(true).label("Job activity email"),
4190
+ applicationUpdates: boolean22().default(true).label("Application update email"),
4191
+ productUpdates: boolean22().default(false).label("Product update email")
2987
4192
  }).default({
2988
4193
  groupActivity: true,
2989
4194
  jobActivity: true,
2990
4195
  applicationUpdates: true,
2991
4196
  productUpdates: false
2992
4197
  }).label("Notification Preferences"),
4198
+ /**
4199
+ * Consent to receive WEB PUSH notifications.
4200
+ *
4201
+ * Push is a TRANSPORT, not a category: it carries the same notifications
4202
+ * `notificationPrefs` already governs, delivered to a device instead of an
4203
+ * inbox. That is why no push value is added to `NotificationType` — the user
4204
+ * chooses whether push is used at all, and WHICH kinds are worth pushing
4205
+ * stays a product decision.
4206
+ *
4207
+ * Opt-IN, and it must not be folded into `notificationPrefs`: those are
4208
+ * opt-OUT and read `!== false`, so an absent value means ENABLED. Applied to
4209
+ * push, every user who predates this field would read as having granted
4210
+ * permission they were never asked for. This mirrors `marketingConsent`
4211
+ * instead, which is the opt-in shape in this file, down to the provenance
4212
+ * fields.
4213
+ *
4214
+ * Consent alone never delivers anything: a push send also needs a live
4215
+ * subscription (see `PushSubscriptionSchema`). This flag is the user's
4216
+ * intent, the subscription is the browser grant, and both are required.
4217
+ */
4218
+ pushConsent: object51({
4219
+ subscribed: boolean22().default(false).label("Subscribed to push notifications"),
4220
+ /** Epoch ms of the last consent change (opt-in or opt-out). */
4221
+ updatedAt: number20().optional().label("Push consent updated at"),
4222
+ /** Where the change originated, e.g. `onboarding`, `settings`, `prompt`. */
4223
+ source: string40().optional().label("Push consent source")
4224
+ }).default({ subscribed: false }).label("Push Consent"),
2993
4225
  /**
2994
4226
  * Vector embedding of the user's profile for semantic/hybrid search.
2995
4227
  * Generated from a structured summary of skills, experience, education, etc.
2996
4228
  * Only generated for public profiles with sufficient completeness (≥60%).
2997
4229
  */
2998
- embedding: object34({
2999
- vector: array14(number12().required()).optional().label("Embedding vector"),
3000
- model: string32().optional().label("Embedding model")
3001
- }).nullable().optional().label("Embedding")
4230
+ embedding: object51({
4231
+ vector: array21(number20().required()).optional().label("Embedding vector"),
4232
+ model: string40().optional().label("Embedding model")
4233
+ }).nullable().optional().label("Embedding"),
4234
+ /**
4235
+ * Epoch ms the account was created, assigned by the storing service.
4236
+ *
4237
+ * Declared here, matching `GroupSchema`, because it is not merely stored: all
4238
+ * 103,608 user documents carry it, `UserFilterSchema` filters on it
4239
+ * (`createdAtWithinDays` / `createdAtOlderThanDays`), and the admin grid sorts
4240
+ * and displays it. It was absent, so a consumer deriving its row type from
4241
+ * this schema could not name the column it renders, and this object is
4242
+ * `.noUnknown().strict()` — an undeclared key is stripped on validation.
4243
+ *
4244
+ * `updatedAt` is deliberately NOT added alongside it: nothing derives from it
4245
+ * yet, and an optional field is only safe to add when something needs it.
4246
+ */
4247
+ createdAt: number20().optional().label("Created At")
3002
4248
  }).concat(UserGeneralDetailSchema).concat(UserJobPreferencesSchema).concat(UserRecruiterProfileSchema).concat(UserCoordinatorProfileSchema).noUnknown().strict().label("User Schema");
3003
4249
 
4250
+ // src/user/push-subscription.schema.ts
4251
+ import { number as number21, object as object52, string as string41 } from "yup";
4252
+ var PushSubscriptionSchema = object52({
4253
+ /**
4254
+ * The user this subscription belongs to, by the `shortId` every service
4255
+ * keys a user on, matching `UserSchema.shortId`.
4256
+ *
4257
+ * NOT the Better Auth account id. That id is meaningful only inside the auth
4258
+ * service, and owning these rows by one would force the service that sends
4259
+ * push to translate an id it has no other use for. The signed-in browser
4260
+ * gets its own shortId straight off the JWT claim.
4261
+ *
4262
+ * Named `userShortId` rather than `shortId` because a stored row has a
4263
+ * `shortId` of its OWN (see `PushSubscriptionRecord`); one field cannot be
4264
+ * both the row's id and its owner's.
4265
+ */
4266
+ userShortId: string41().required().label("User Short ID"),
4267
+ /**
4268
+ * The push service URL the browser issued. Vendor-specific (FCM, Mozilla,
4269
+ * Apple) and effectively unbounded in length, so it carries no length cap.
4270
+ *
4271
+ * This is the IDENTITY of a subscription: it is what a re-subscribing
4272
+ * browser sends back unchanged, and what a 404/410 response identifies as
4273
+ * dead. Upserts and deletes both key on it.
4274
+ */
4275
+ endpoint: string41().required().label("Push Endpoint"),
4276
+ /**
4277
+ * Encryption material from the browser. Web Push payloads are encrypted to
4278
+ * these keys, so a row missing either one cannot be delivered to at all.
4279
+ */
4280
+ keys: object52({
4281
+ /** Client public key, base64url. */
4282
+ p256dh: string41().required().label("P256DH Key"),
4283
+ /** Client auth secret, base64url. */
4284
+ auth: string41().required().label("Auth Secret")
4285
+ }).required().label("Push Keys"),
4286
+ /**
4287
+ * Epoch ms the push service says the subscription expires, when it says
4288
+ * anything at all. Usually absent — most browsers return null — so this is
4289
+ * informational and never the basis for pruning. A 404/410 on send is the
4290
+ * only reliable death signal.
4291
+ */
4292
+ expirationTime: number21().nullable().optional().label("Expiration Time"),
4293
+ /**
4294
+ * The subscribing browser's user agent, kept so a user can recognise their
4295
+ * own devices in a "where you receive notifications" list. Display only:
4296
+ * never parsed, and never used to decide delivery.
4297
+ */
4298
+ userAgent: string41().optional().label("User Agent"),
4299
+ /** Epoch ms the subscription was first registered. */
4300
+ createdAt: number21().optional().label("Created At"),
4301
+ /**
4302
+ * Epoch ms this subscription was last confirmed live, refreshed when the
4303
+ * browser re-registers it. A subscription that stops being refreshed is a
4304
+ * device that stopped visiting, which is useful for pruning rows that never
4305
+ * fail loudly enough to be caught by a 410.
4306
+ */
4307
+ lastSeenAt: number21().optional().label("Last Seen At")
4308
+ }).noUnknown().strict().label("Push Subscription Schema");
4309
+
3004
4310
  // src/user/user-completeness.schema.ts
3005
- import { object as object35 } from "yup";
3006
- var UserCompletenessSchema = object35({
4311
+ import { object as object53 } from "yup";
4312
+ var UserCompletenessSchema = object53({
3007
4313
  additionalInfo: CompletenessScoreSchema(0).label("Additional Info"),
3008
4314
  // Optional
3009
4315
  certifications: CompletenessScoreSchema(0).label("Certifications"),
@@ -3027,22 +4333,29 @@ var StudentCompletenessSchema = UserCompletenessSchema.omit([
3027
4333
  export {
3028
4334
  AnswerChoiceType,
3029
4335
  ApplicationReceivePreference,
3030
- BindingKind,
3031
- BindingSchema,
4336
+ BillingPeriod,
4337
+ BillingSubscriptionSchema,
4338
+ BillingTransactionSchema,
3032
4339
  CAMPAIGN_MAX_RECIPIENTS_CEILING,
3033
- CampaignAnchorField,
4340
+ CONSUME_MAX_UNITS,
4341
+ CampaignDataSourceSchema,
3034
4342
  CampaignEndKind,
3035
4343
  CampaignIntervalUnit,
3036
4344
  CampaignSchema,
3037
4345
  CampaignStatus,
4346
+ CampaignStatusTransitions,
3038
4347
  CampaignTriggerKind,
3039
4348
  CampaignTriggerSchema,
3040
4349
  ChoiceQuestionSchema,
4350
+ CityFilterSchema,
4351
+ CitySchema,
3041
4352
  Common,
3042
4353
  CompensationType,
3043
4354
  CompletenessScoreSchema,
3044
4355
  ContactTypes,
3045
4356
  CoordinatorPageLinkSchema,
4357
+ CountryFilterSchema,
4358
+ CountrySchema,
3046
4359
  DISPLAY_DATE_FORMAT,
3047
4360
  DISPLAY_DATE_FORMAT_SHORT,
3048
4361
  DateStringSchema,
@@ -3056,12 +4369,29 @@ export {
3056
4369
  EducationSchema,
3057
4370
  EmployeeCount,
3058
4371
  EmploymentType,
4372
+ EntitlementConsumeSchema,
4373
+ EntitlementDenialReason,
4374
+ EntitlementDenialSchema,
4375
+ EntitlingSubscriptionStatuses,
3059
4376
  ExperienceLevel,
3060
4377
  FEEDBACK_MESSAGE_MAX,
4378
+ FeatureEntitlementSchema,
4379
+ FeatureKey,
4380
+ FeedbackFilterSchema,
3061
4381
  FeedbackSchema,
3062
4382
  FeedbackStatus,
3063
4383
  FeedbackType,
4384
+ GEO_COLLECTIONS,
4385
+ GEO_SEARCH_MAX,
4386
+ GRANT_MAX_LIMIT,
3064
4387
  GeneraDetailFields,
4388
+ GeoKind,
4389
+ GeoSyncPhase,
4390
+ GeoSyncRunSchema,
4391
+ GeoSyncStatus,
4392
+ GeoSyncTrigger,
4393
+ GrantType,
4394
+ GroupFilterSchema,
3065
4395
  GroupManagedBy,
3066
4396
  GroupMembershipSchema,
3067
4397
  GroupMembershipStatus,
@@ -3073,31 +4403,41 @@ export {
3073
4403
  JOB_TAXONOMY,
3074
4404
  JobAlertFrequency,
3075
4405
  JobCategory,
4406
+ JobFilterSchema,
3076
4407
  JobIndustry,
3077
4408
  JobSchema,
3078
4409
  JobSearchUrgency,
3079
4410
  JobStatus,
3080
4411
  JobSubCategory,
3081
- ListFilterBranch,
3082
- ListFilterBranchSchema,
3083
- ListFilterSchema,
4412
+ LanguageFilterSchema,
3084
4413
  LocationSchema,
3085
4414
  MIN_SALARY_LOWER_BOUND,
3086
4415
  MIN_SALARY_UPPER_BOUND,
4416
+ MailingListFilterSchema,
3087
4417
  MailingListSchema,
3088
- NotificationChannel,
4418
+ MoneySchema,
4419
+ NotificationType,
3089
4420
  OccurrenceStatus,
3090
4421
  PREFILL_MIRRORED_KEYS,
3091
4422
  PRIVATE_PROFILE_FIELDS,
4423
+ PaddleEnvironment,
4424
+ PaddlePriceIdsSchema,
4425
+ PageFilterSchema,
3092
4426
  PageSchema,
3093
4427
  PageStatus,
3094
4428
  PageType,
3095
4429
  PaginationSchema,
4430
+ PlanGrantSchema,
4431
+ PlanKind,
4432
+ PlanSchema,
4433
+ PlanStatus,
3096
4434
  PostSchema,
3097
4435
  PostStatus,
3098
4436
  ProficiencyLevel,
4437
+ PushSubscriptionSchema,
3099
4438
  QuestionSchema,
3100
4439
  QuestionType,
4440
+ QuotaPeriod,
3101
4441
  ReadAndAcknowledgeQuestionSchema,
3102
4442
  RecruiterPageLinkSchema,
3103
4443
  ReferralSource,
@@ -3109,15 +4449,18 @@ export {
3109
4449
  SavedSearchSchema,
3110
4450
  SavedSearchStatus,
3111
4451
  SignupContextSource,
4452
+ SkillFilterSchema,
3112
4453
  SkillSchema,
3113
4454
  SocialAccount,
3114
4455
  SocialAccountSchema,
4456
+ StateFilterSchema,
4457
+ StateSchema,
3115
4458
  StudentCompletenessSchema,
3116
4459
  StudyType,
4460
+ SubscriptionStatus,
3117
4461
  SupportedAnswerChoiceTypes,
3118
4462
  SupportedApplicationReceivePreferences,
3119
- SupportedBindingKinds,
3120
- SupportedCampaignAnchorFields,
4463
+ SupportedBillingPeriods,
3121
4464
  SupportedCampaignEndKinds,
3122
4465
  SupportedCampaignIntervalUnits,
3123
4466
  SupportedCampaignStatuses,
@@ -3127,22 +4470,37 @@ export {
3127
4470
  SupportedEducationLevels,
3128
4471
  SupportedEmployeeCounts,
3129
4472
  SupportedEmploymentTypes,
4473
+ SupportedEntitlementDenialReasons,
3130
4474
  SupportedExperienceLevels,
4475
+ SupportedFeatureKeys,
3131
4476
  SupportedFeedbackStatuses,
3132
4477
  SupportedFeedbackTypes,
4478
+ SupportedGeoKinds,
4479
+ SupportedGeoSyncPhases,
4480
+ SupportedGeoSyncStatuses,
4481
+ SupportedGeoSyncTriggers,
4482
+ SupportedGrantTypes,
4483
+ SupportedGroupManagedBy,
4484
+ SupportedGroupMembershipStatuses,
4485
+ SupportedGroupStatuses,
4486
+ SupportedGroupVisibilities,
3133
4487
  SupportedJobAlertFrequencies,
3134
4488
  SupportedJobCategories,
3135
4489
  SupportedJobIndustries,
3136
4490
  SupportedJobSearchUrgencies,
3137
4491
  SupportedJobStatuses,
3138
4492
  SupportedJobSubCategories,
3139
- SupportedNotificationChannels,
4493
+ SupportedNotificationTypes,
3140
4494
  SupportedOccurrenceStatuses,
4495
+ SupportedPaddleEnvironments,
3141
4496
  SupportedPageStatuses,
3142
4497
  SupportedPageTypes,
4498
+ SupportedPlanKinds,
4499
+ SupportedPlanStatuses,
3143
4500
  SupportedPostStatuses,
3144
4501
  SupportedProficiencyLevels,
3145
4502
  SupportedQuestionTypes,
4503
+ SupportedQuotaPeriods,
3146
4504
  SupportedReferralSources,
3147
4505
  SupportedReportReasons,
3148
4506
  SupportedResourceTypes,
@@ -3151,17 +4509,26 @@ export {
3151
4509
  SupportedSignupContextSources,
3152
4510
  SupportedSocialAccounts,
3153
4511
  SupportedStudyTypes,
4512
+ SupportedSubscriptionStatuses,
4513
+ SupportedTemplateVariableBindingKinds,
4514
+ SupportedTransactionStatuses,
3154
4515
  SupportedUserProfileVisibilities,
3155
4516
  SupportedUserRoles,
3156
4517
  SupportedUserStatuses,
3157
4518
  SupportedWorkModes,
3158
4519
  TAXONOMY_LABELS,
4520
+ TemplateVariableBindingKind,
4521
+ TemplateVariableBindingSchema,
4522
+ TerminalCampaignStatuses,
4523
+ TerminalGeoSyncStatuses,
3159
4524
  TermsAcceptedSchema,
4525
+ TransactionStatus,
3160
4526
  UserAdditionalInfoSchema,
3161
4527
  UserCertificationSchema,
3162
4528
  UserCompletenessSchema,
3163
4529
  UserCoordinatorProfileSchema,
3164
4530
  UserDetailType,
4531
+ UserFilterSchema,
3165
4532
  UserGeneralDetailSchema,
3166
4533
  UserIdAndCreatedAtSchema,
3167
4534
  UserInterestSchema,
@@ -3176,12 +4543,24 @@ export {
3176
4543
  UserStatus,
3177
4544
  WorkExperienceSchema,
3178
4545
  WorkMode,
4546
+ canTransitionCampaign,
3179
4547
  categoryOfIndustry,
3180
4548
  categoryOfSubCategory,
4549
+ codeTokens,
4550
+ conjunction,
4551
+ currencyBound,
3181
4552
  dateString,
4553
+ days,
3182
4554
  deriveIndustry,
4555
+ enumFacet,
4556
+ epoch,
3183
4557
  getSchemaByQuestion,
3184
4558
  industriesOfCategory,
3185
4559
  industryOfSubCategory,
3186
- subCategoriesOfIndustry
4560
+ months,
4561
+ percent,
4562
+ qualified,
4563
+ rejectUnknownKeys,
4564
+ subCategoriesOfIndustry,
4565
+ tokens
3187
4566
  };