@updraft-solutions/mcrit-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +240 -0
  2. package/dist/chunk-2DRKSYW4.cjs +99 -0
  3. package/dist/chunk-2M32EOYI.js +145 -0
  4. package/dist/chunk-AN55SAGU.js +1189 -0
  5. package/dist/chunk-D5DUVW34.js +176 -0
  6. package/dist/chunk-HQSF3WGF.cjs +14 -0
  7. package/dist/chunk-JGIAOBZI.js +99 -0
  8. package/dist/chunk-JRVRIKNM.cjs +168 -0
  9. package/dist/chunk-LVTDNDWE.cjs +124 -0
  10. package/dist/chunk-LXNCAKJZ.js +0 -0
  11. package/dist/chunk-MF7G76QS.cjs +176 -0
  12. package/dist/chunk-MI4YBENQ.cjs +145 -0
  13. package/dist/chunk-MOOGM3DW.cjs +1 -0
  14. package/dist/chunk-NQV2D3FJ.js +14 -0
  15. package/dist/chunk-TJXWL42D.js +124 -0
  16. package/dist/chunk-VYT5KQWF.js +168 -0
  17. package/dist/chunk-XKZQ5W7E.cjs +1189 -0
  18. package/dist/courses/index.cjs +7 -0
  19. package/dist/courses/index.d.cts +58 -0
  20. package/dist/courses/index.d.ts +58 -0
  21. package/dist/courses/index.js +7 -0
  22. package/dist/crew/index.cjs +10 -0
  23. package/dist/crew/index.d.cts +102 -0
  24. package/dist/crew/index.d.ts +102 -0
  25. package/dist/crew/index.js +10 -0
  26. package/dist/crew/vue.cjs +34 -0
  27. package/dist/crew/vue.d.cts +31 -0
  28. package/dist/crew/vue.d.ts +31 -0
  29. package/dist/crew/vue.js +34 -0
  30. package/dist/format/index.cjs +44 -0
  31. package/dist/format/index.d.cts +69 -0
  32. package/dist/format/index.d.ts +69 -0
  33. package/dist/format/index.js +44 -0
  34. package/dist/index.cjs +261 -0
  35. package/dist/index.d.cts +11 -0
  36. package/dist/index.d.ts +11 -0
  37. package/dist/index.js +261 -0
  38. package/dist/media/index.cjs +30 -0
  39. package/dist/media/index.d.cts +89 -0
  40. package/dist/media/index.d.ts +89 -0
  41. package/dist/media/index.js +30 -0
  42. package/dist/models-CY3MmvCg.d.cts +33801 -0
  43. package/dist/models-CY3MmvCg.d.ts +33801 -0
  44. package/dist/money/index.cjs +18 -0
  45. package/dist/money/index.d.cts +66 -0
  46. package/dist/money/index.d.ts +66 -0
  47. package/dist/money/index.js +18 -0
  48. package/dist/pagination/index.cjs +6 -0
  49. package/dist/pagination/index.d.cts +16 -0
  50. package/dist/pagination/index.d.ts +16 -0
  51. package/dist/pagination/index.js +6 -0
  52. package/dist/schemas/index.cjs +158 -0
  53. package/dist/schemas/index.d.cts +1019 -0
  54. package/dist/schemas/index.d.ts +1019 -0
  55. package/dist/schemas/index.js +158 -0
  56. package/dist/types/index.cjs +1 -0
  57. package/dist/types/index.d.cts +143 -0
  58. package/dist/types/index.d.ts +143 -0
  59. package/dist/types/index.js +1 -0
  60. package/package.json +171 -0
  61. package/src/courses/index.ts +202 -0
  62. package/src/crew/index.ts +255 -0
  63. package/src/crew/vue.ts +70 -0
  64. package/src/format/index.ts +265 -0
  65. package/src/index.ts +76 -0
  66. package/src/media/index.ts +224 -0
  67. package/src/money/index.ts +168 -0
  68. package/src/pagination/index.ts +27 -0
  69. package/src/schemas/common.ts +73 -0
  70. package/src/schemas/faq.ts +94 -0
  71. package/src/schemas/fee.ts +37 -0
  72. package/src/schemas/index.ts +6 -0
  73. package/src/schemas/models.ts +996 -0
  74. package/src/schemas/report.ts +306 -0
  75. package/src/schemas/safety-report.ts +163 -0
  76. package/src/types/api.ts +37 -0
  77. package/src/types/compliance.ts +44 -0
  78. package/src/types/index.ts +3 -0
  79. package/src/types/models.ts +124 -0
package/README.md ADDED
@@ -0,0 +1,240 @@
1
+ # `@updraft-solutions/mcrit-sdk`
2
+
3
+ Shared TypeScript types, Zod schemas, and pure helpers for the MCRIT
4
+ aviation fleet management platform — consumed by the `mcrit-app` SPA and
5
+ the public `inflight-homepage` site so both speak the same API contract
6
+ and apply the same business logic.
7
+
8
+ The SDK is **runtime-agnostic and side-effect free**: no Vue, no Nuxt, no
9
+ Pinia, no HTTP client. Just types and pure functions that operate on the
10
+ resource shapes returned by the Laravel API.
11
+
12
+ ---
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pnpm add @updraft-solutions/mcrit-sdk
18
+ ```
19
+
20
+ Peer dependency:
21
+
22
+ ```bash
23
+ pnpm add moment-timezone
24
+ ```
25
+
26
+ `moment-timezone` is only required if you use the `format` module. If you
27
+ want German or another non-English label output, also import the matching
28
+ moment locale **once** at your app entry:
29
+
30
+ ```ts
31
+ // e.g. a Nuxt plugin or app/main.ts
32
+ import "moment/locale/de";
33
+ ```
34
+
35
+ ---
36
+
37
+ ## Subpath exports
38
+
39
+ Every helper is tree-shakeable. Import from the narrow subpath you need,
40
+ or from the root if you want everything in one place.
41
+
42
+ | Subpath | What's inside |
43
+ |---|---|
44
+ | `@updraft-solutions/mcrit-sdk` | Re-exports everything below |
45
+ | `@updraft-solutions/mcrit-sdk/schemas` | Zod schemas for API resources |
46
+ | `@updraft-solutions/mcrit-sdk/types` | Plain TypeScript types (inferred or hand-written) |
47
+ | `@updraft-solutions/mcrit-sdk/money` | Dinero-based fee and price helpers |
48
+ | `@updraft-solutions/mcrit-sdk/courses` | Course variant and total-cost logic |
49
+ | `@updraft-solutions/mcrit-sdk/format` | Date, time, percentage, byte, membership formatters |
50
+ | `@updraft-solutions/mcrit-sdk/crew` | Crew-role constants, predicates, membership helpers |
51
+ | `@updraft-solutions/mcrit-sdk/media` | MIME-type classification + Spatie MediaLibrary mappers |
52
+ | `@updraft-solutions/mcrit-sdk/pagination` | Pagination meta math |
53
+
54
+ ---
55
+
56
+ ## Quick examples
57
+
58
+ ### Schemas & types
59
+
60
+ ```ts
61
+ import { feeSchema, courseSchema } from "@updraft-solutions/mcrit-sdk/schemas";
62
+ import type { Course, Fee } from "@updraft-solutions/mcrit-sdk/types";
63
+
64
+ const fee = feeSchema.parse(apiResponse); // runtime validation
65
+ const course: Course = await loadCourse(); // type only
66
+ ```
67
+
68
+ ### Money
69
+
70
+ ```ts
71
+ import { parseFee, displayFee, fromCents, formatToEuroString } from "@updraft-solutions/mcrit-sdk/money";
72
+
73
+ displayFee(fee, "gross"); // → "€ 1.234,56"
74
+ parseFee(fee)?.currentPrice("net"); // → Dinero instance
75
+ formatToEuroString(123456); // → "€ 1.234,56"
76
+ fromCents(50000).toFormat(); // → "500,00 €"
77
+ ```
78
+
79
+ ### Courses
80
+
81
+ ```ts
82
+ import { parseCourse } from "@updraft-solutions/mcrit-sdk/courses";
83
+
84
+ const variants = parseCourse(course)?.variants("gross") ?? [];
85
+ // [{ variant, fees: ParsedCourseFee[], totalCost: Dinero }, ...]
86
+
87
+ for (const v of variants) {
88
+ console.log(v.variant, v.totalCost.toFormat());
89
+ }
90
+ ```
91
+
92
+ `parseCourse` handles fees with pivot data nested under `pivot` OR
93
+ flattened directly on the fee object — both API shapes are accepted.
94
+
95
+ ### Format
96
+
97
+ ```ts
98
+ import {
99
+ formatDate,
100
+ formatDateTime,
101
+ bookingDuration,
102
+ bytesToHuman,
103
+ timeUntil,
104
+ } from "@updraft-solutions/mcrit-sdk/format";
105
+
106
+ formatDate("2026-05-15"); // "15.05.2026"
107
+ formatDateTime(booking.etdZ, "Europe/Berlin"); // "15.05.2026 09:00"
108
+ bookingDuration(booking); // "01:30"
109
+ bytesToHuman(1_572_864); // "1.5 MB"
110
+ timeUntil("2026-05-10T12:00:00Z", "de"); // "vor 5 Tagen" (needs moment de locale loaded)
111
+ ```
112
+
113
+ ### Crew
114
+
115
+ ```ts
116
+ import {
117
+ CREW_ROLES,
118
+ ROLE_ATTRIBUTES,
119
+ PIC_PRIORITY,
120
+ isInstructorClassRole,
121
+ hasMembershipRole,
122
+ defaultCrewRoleForMembership,
123
+ } from "@updraft-solutions/mcrit-sdk/crew";
124
+
125
+ PIC_PRIORITY; // ['examiner', 'instructor', 'pilot', 'student']
126
+ isInstructorClassRole("authorizing_instructor"); // true
127
+ defaultCrewRoleForMembership(membership); // "student" | "instructor" | "pilot"
128
+ ```
129
+
130
+ Mirrors the backend `App\Enums\CrewMemberPosition`. See
131
+ `m-crit-api/docs/features/crew-composition.md` for the full data model.
132
+
133
+ ### Media
134
+
135
+ ```ts
136
+ import {
137
+ mapMediaConversions,
138
+ getMediaType,
139
+ getMediaIcon,
140
+ getMediaDisplayName,
141
+ } from "@updraft-solutions/mcrit-sdk/media";
142
+
143
+ const gallery = mapMediaConversions(model.media);
144
+ // [{ href, thumbnail, mediaType, icon, previewable, ... }]
145
+
146
+ getMediaType("application/pdf"); // "document"
147
+ getMediaIcon("video/mp4"); // "mdi-video"
148
+
149
+ getMediaDisplayName("image/png"); // "Bild" (German default)
150
+ getMediaDisplayName("image/png", { image: "Picture" }); // "Picture" (i18n override)
151
+ ```
152
+
153
+ ### Pagination
154
+
155
+ ```ts
156
+ import { paginationMeta } from "@updraft-solutions/mcrit-sdk/pagination";
157
+
158
+ paginationMeta({ page: 2, itemsPerPage: 25 }, 137);
159
+ // { start: 26, end: 50, total: 137 }
160
+ ```
161
+
162
+ ---
163
+
164
+ ## API shapes vs `*Like` shapes
165
+
166
+ The SDK distinguishes two kinds of types:
167
+
168
+ - **Strict shapes** (`Fee`, `Price`, `Course`, `CourseFee`, ...) are
169
+ `z.infer<>` of the corresponding Zod schemas — what the API actually
170
+ returns when fully loaded.
171
+ - **Permissive `*Like` shapes** (`FeeLike`, `PriceLike`, `CourseLike`,
172
+ `CourseFeeLike`) are accepted by the helpers (`parseFee`,
173
+ `parseCourse`, `displayFee`, ...) so callers can pass partial or
174
+ legacy payload shapes without casting.
175
+
176
+ Use the strict types in your own code; rely on `*Like` only as input to
177
+ SDK helpers.
178
+
179
+ ---
180
+
181
+ ## Local development
182
+
183
+ This repo is a normal pnpm package. From the SDK root:
184
+
185
+ ```bash
186
+ pnpm install
187
+ pnpm typecheck # tsc --noEmit
188
+ pnpm build # tsup → dist/{esm,cjs,d.ts}
189
+ pnpm dev # tsup --watch
190
+ ```
191
+
192
+ ### Inside the MCRIT monorepo
193
+
194
+ The MCRIT working folder (`mcrit-app`, `inflight-homepage-nuxt-3`, and
195
+ this repo as sibling git checkouts) uses a pnpm workspace at the parent
196
+ folder. With `workspace:*` (or any matching version range) in the
197
+ consuming app's `package.json`, pnpm symlinks this directory into the
198
+ consumer's `node_modules` — edits show up immediately, no publish loop.
199
+
200
+ If consumers use the `source` export condition (Vite via
201
+ `resolve.conditions: ['source']`), they read `src/*.ts` directly and skip
202
+ the `tsup` build entirely during dev.
203
+
204
+ ### Adding a new helper
205
+
206
+ 1. Pick or create the appropriate subpath under `src/` (e.g. `src/money/`).
207
+ 2. Keep the implementation pure — no framework imports, no I/O.
208
+ 3. Add the file to the `entry` list in `tsup.config.ts`.
209
+ 4. Add the subpath under `exports` in `package.json` (mirror the existing
210
+ pattern: `source` + `import.types` + `import.default` + `require.types`
211
+ + `require.default`).
212
+ 5. Re-export from `src/index.ts` if it should also be reachable from the
213
+ root.
214
+ 6. `pnpm typecheck && pnpm build` — both must be green.
215
+
216
+ ---
217
+
218
+ ## Releases
219
+
220
+ Publishes are automated. Push a tag matching `v*` to trigger
221
+ `.github/workflows/publish.yml`:
222
+
223
+ ```bash
224
+ npm version patch # bumps package.json and creates v0.1.1 commit + tag
225
+ git push && git push --tags
226
+ ```
227
+
228
+ The workflow verifies the tag matches `package.json`, runs
229
+ `pnpm typecheck && pnpm build`, then publishes to npm with provenance.
230
+
231
+ The `NPM_TOKEN` GitHub repository secret must be configured (an
232
+ "Automation" or "Granular" token from npmjs.com with publish rights to
233
+ `@updraft-solutions/mcrit-sdk`).
234
+
235
+ ---
236
+
237
+ ## License
238
+
239
+ UNLICENSED — internal Updraft Solutions package. Not for redistribution
240
+ outside MCRIT projects.
@@ -0,0 +1,99 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/money/index.ts
2
+ var _dinerojs = require('dinero.js'); var _dinerojs2 = _interopRequireDefault(_dinerojs);
3
+ var _lodashes = require('lodash-es');
4
+ _dinerojs2.default.globalLocale = "de-DE";
5
+ function money(amount) {
6
+ try {
7
+ let parsed = amount;
8
+ if (parsed && typeof parsed === "object") {
9
+ const obj = parsed;
10
+ if (obj.value && typeof obj.value === "object" && typeof obj.value.amount !== "undefined") {
11
+ parsed = obj.value.amount;
12
+ } else if (typeof obj.amount !== "undefined") {
13
+ parsed = obj.amount;
14
+ }
15
+ }
16
+ const int = parseInt(String(_nullishCoalesce(parsed, () => ( "0"))), 10);
17
+ if (Number.isNaN(int)) {
18
+ throw new Error("Invalid amount");
19
+ }
20
+ return _dinerojs2.default.call(void 0, { amount: int, currency: "EUR" });
21
+ } catch (e) {
22
+ console.error("[mcrit-sdk/money] failed to parse amount:", amount, e);
23
+ return _dinerojs2.default.call(void 0, { amount: 0, currency: "EUR" });
24
+ }
25
+ }
26
+ function moneyFormatted(dinero) {
27
+ return dinero ? dinero.toFormat() : "";
28
+ }
29
+ function parsePrice(price) {
30
+ if (!price) {
31
+ return null;
32
+ }
33
+ return {
34
+ ...price,
35
+ net_total: money(_nullishCoalesce(_optionalChain([price, 'access', _ => _.net_total, 'optionalAccess', _2 => _2.amount]), () => ( 0))),
36
+ gross_total: money(_nullishCoalesce(_optionalChain([price, 'access', _3 => _3.gross_total, 'optionalAccess', _4 => _4.amount]), () => ( 0)))
37
+ };
38
+ }
39
+ function priceFor(parsed, netGross) {
40
+ if (netGross === "net") {
41
+ return parsed.net_total;
42
+ }
43
+ if (netGross === "gross") {
44
+ return parsed.gross_total;
45
+ }
46
+ return parsed;
47
+ }
48
+ function parseFee(fee) {
49
+ if (!fee) {
50
+ return null;
51
+ }
52
+ return {
53
+ currentPrice(netGross = null) {
54
+ const parsed = parsePrice(_nullishCoalesce(fee.price, () => ( null)));
55
+ return parsed ? priceFor(parsed, netGross) : null;
56
+ },
57
+ upcomingPrice(netGross = null) {
58
+ const now = Date.now();
59
+ const upcoming = _lodashes.head.call(void 0,
60
+ _lodashes.sortBy.call(void 0,
61
+ _lodashes.filter.call(void 0, _nullishCoalesce(fee.prices, () => ( [])), (p) => {
62
+ const t = p.valid_from ? Date.parse(p.valid_from) : NaN;
63
+ return Number.isFinite(t) && t > now;
64
+ }),
65
+ (p) => Date.parse(_nullishCoalesce(p.valid_from, () => ( "")))
66
+ )
67
+ );
68
+ if (!upcoming) {
69
+ return null;
70
+ }
71
+ const parsed = parsePrice(upcoming);
72
+ return parsed ? priceFor(parsed, netGross) : null;
73
+ }
74
+ };
75
+ }
76
+ function displayFee(fee, netGross = "gross", fallback = "Keine Geb\xFChr hinterlegt") {
77
+ const current = _optionalChain([parseFee, 'call', _5 => _5(fee), 'optionalAccess', _6 => _6.currentPrice, 'call', _7 => _7(netGross)]);
78
+ if (current && "toFormat" in current) {
79
+ return current.toFormat();
80
+ }
81
+ return fallback;
82
+ }
83
+ function fromCents(amount) {
84
+ return _dinerojs2.default.call(void 0, { amount, currency: "EUR" });
85
+ }
86
+ function formatToEuroString(amount) {
87
+ const cents = parseInt(String(_nullishCoalesce(amount, () => ( "0"))), 10);
88
+ return `\u20AC ${fromCents(Number.isNaN(cents) ? 0 : cents).setLocale("de-DE").toFormat("0,0.00")}`;
89
+ }
90
+
91
+
92
+
93
+
94
+
95
+
96
+
97
+
98
+
99
+ exports.money = money; exports.moneyFormatted = moneyFormatted; exports.parsePrice = parsePrice; exports.parseFee = parseFee; exports.displayFee = displayFee; exports.fromCents = fromCents; exports.formatToEuroString = formatToEuroString;
@@ -0,0 +1,145 @@
1
+ // src/crew/index.ts
2
+ function hasMembershipRole(membership, roleName) {
3
+ return membership?.roles?.some((r) => r.name === roleName) ?? false;
4
+ }
5
+ function defaultCrewRoleForMembership(membership) {
6
+ if (hasMembershipRole(membership, "student")) {
7
+ return "student";
8
+ }
9
+ if (hasMembershipRole(membership, "instructor")) {
10
+ return "instructor";
11
+ }
12
+ return "pilot";
13
+ }
14
+ function createCrewValidator(config) {
15
+ function attributesOf(role) {
16
+ return config.positions[role];
17
+ }
18
+ function rolesFromCrew(crew) {
19
+ return crew.map((c) => c.role).filter((r) => typeof r === "string");
20
+ }
21
+ function membershipsWithSystemRole(crew, systemRole) {
22
+ return crew.filter(
23
+ (c) => c.membership?.roles?.some(
24
+ (r) => r.name === systemRole && (r.company_id == null || r.company_id === null)
25
+ )
26
+ );
27
+ }
28
+ function evaluateConstraint(constraint, crew) {
29
+ switch (constraint.kind) {
30
+ case "single_pic": {
31
+ if (crew.length === 0) {
32
+ return null;
33
+ }
34
+ const roles = rolesFromCrew(crew);
35
+ for (const eligibleRole of config.pic_priority) {
36
+ const matches = roles.filter((r) => r === eligibleRole).length;
37
+ if (matches === 1) {
38
+ return null;
39
+ }
40
+ if (matches > 1) {
41
+ return {
42
+ kind: constraint.kind,
43
+ message_key: "crew.multiple_same_pic_role",
44
+ details: { role: eligibleRole }
45
+ };
46
+ }
47
+ }
48
+ return {
49
+ kind: constraint.kind,
50
+ message_key: "crew.no_eligible_pic"
51
+ };
52
+ }
53
+ case "min_members": {
54
+ if (crew.length >= constraint.min) {
55
+ return null;
56
+ }
57
+ return {
58
+ kind: constraint.kind,
59
+ message_key: "crew.min_members",
60
+ details: { min: constraint.min, actual: crew.length }
61
+ };
62
+ }
63
+ case "when_crew_contains_role_requires_one_of": {
64
+ const roles = rolesFromCrew(crew);
65
+ if (!roles.includes(constraint.when_role)) {
66
+ return null;
67
+ }
68
+ const satisfied = constraint.required_roles.some((r) => roles.includes(r));
69
+ if (satisfied) {
70
+ return null;
71
+ }
72
+ return {
73
+ kind: constraint.kind,
74
+ message_key: constraint.message_key,
75
+ details: {
76
+ when_role: constraint.when_role,
77
+ required_roles: constraint.required_roles
78
+ }
79
+ };
80
+ }
81
+ case "when_any_membership_has_system_role_requires_one_of": {
82
+ const flagged = membershipsWithSystemRole(crew, constraint.system_role);
83
+ if (flagged.length === 0) {
84
+ return null;
85
+ }
86
+ const roles = rolesFromCrew(crew);
87
+ const satisfied = constraint.required_roles.some((r) => roles.includes(r));
88
+ if (satisfied) {
89
+ return null;
90
+ }
91
+ return {
92
+ kind: constraint.kind,
93
+ message_key: constraint.message_key,
94
+ details: {
95
+ system_role: constraint.system_role,
96
+ required_roles: constraint.required_roles,
97
+ membership_ids: flagged.map((c) => c.membership_id).filter((id) => id != null)
98
+ }
99
+ };
100
+ }
101
+ }
102
+ }
103
+ return {
104
+ isOnBoardRole: (role) => attributesOf(role)?.isOnBoard ?? false,
105
+ isInstructorClassRole: (role) => attributesOf(role)?.isInstructorClass ?? false,
106
+ isPicEligibleRole: (role) => attributesOf(role)?.picPriority != null,
107
+ resolvePic(crew) {
108
+ if (crew.length === 0) {
109
+ return { type: "none" };
110
+ }
111
+ const roles = rolesFromCrew(crew);
112
+ for (const eligibleRole of config.pic_priority) {
113
+ const indexes = [];
114
+ roles.forEach((r, i) => {
115
+ if (r === eligibleRole) {
116
+ indexes.push(i);
117
+ }
118
+ });
119
+ if (indexes.length === 1) {
120
+ return { type: "ok", index: indexes[0], role: eligibleRole };
121
+ }
122
+ if (indexes.length > 1) {
123
+ return { type: "tie", role: eligibleRole };
124
+ }
125
+ }
126
+ return { type: "none" };
127
+ },
128
+ evaluate(crew) {
129
+ const violations = [];
130
+ for (const constraint of config.constraints) {
131
+ const violation = evaluateConstraint(constraint, crew);
132
+ if (violation !== null) {
133
+ violations.push(violation);
134
+ }
135
+ }
136
+ return { ok: violations.length === 0, violations };
137
+ }
138
+ };
139
+ }
140
+
141
+ export {
142
+ hasMembershipRole,
143
+ defaultCrewRoleForMembership,
144
+ createCrewValidator
145
+ };