ad2app-lib 1.44.0 → 1.44.1

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.
@@ -203,25 +203,40 @@ export interface I_SponsoredBy {
203
203
  /** Present once the sponsorship is ending: the date their plan runs to. */
204
204
  until?: string;
205
205
  }
206
+ /**
207
+ * Every DTO below carries class-validator decorators and a constructor that
208
+ * survives being called with nothing.
209
+ *
210
+ * Both are load-bearing and were missing until 2026-09-22. The backend mounts
211
+ * a global `ValidationPipe`, which validates the decorated metadata of the
212
+ * class a handler's `@Body()` is typed with — an undecorated class, or a body
213
+ * typed as a plain interface, is simply passed through unchecked. And the pipe
214
+ * instantiates the class with NO arguments, so a constructor that reads
215
+ * `data.name` would throw a TypeError on every request rather than validate
216
+ * anything. The rest of this library's DTOs take `data?: Partial<T>` for that
217
+ * reason; these now do too.
218
+ */
206
219
  export declare class CreateAgencyOrganizationDto {
207
220
  name: string;
221
+ /** Rendered as a link on the admin screen, so the protocol is checked here. */
208
222
  website?: string;
209
223
  registryId: string;
224
+ /** ISO-3166 alpha-2: which register a reviewer should look in. */
210
225
  registryCountry: string;
211
226
  contactEmail: string;
212
- constructor(data: CreateAgencyOrganizationDto);
227
+ constructor(data?: Partial<CreateAgencyOrganizationDto>);
213
228
  }
214
229
  export declare class InviteAgencyMemberDto {
215
230
  email: string;
216
- constructor(data: InviteAgencyMemberDto);
231
+ constructor(data?: Partial<InviteAgencyMemberDto>);
217
232
  }
218
233
  export declare class RedeemAgencyInviteDto {
219
234
  token: string;
220
- constructor(data: RedeemAgencyInviteDto);
235
+ constructor(data?: Partial<RedeemAgencyInviteDto>);
221
236
  }
222
237
  export declare class AcceptAgencyMemberInviteDto {
223
238
  token: string;
224
- constructor(data: AcceptAgencyMemberInviteDto);
239
+ constructor(data?: Partial<AcceptAgencyMemberInviteDto>);
225
240
  }
226
241
  /**
227
242
  * Accepting consent. Either arm identifies the grant being answered; `scopes`
@@ -230,31 +245,34 @@ export declare class AcceptAgencyMemberInviteDto {
230
245
  export declare class AcceptGrantDto {
231
246
  token?: string;
232
247
  grantId?: string;
248
+ /** A subset of the three reads. Never empty: sharing nothing is declining. */
233
249
  scopes?: I_AgencyGrantScope[];
234
- constructor(data: AcceptGrantDto);
250
+ constructor(data?: Partial<AcceptGrantDto>);
235
251
  }
236
252
  /** The creator narrowing or widening a live grant — the only other write path. */
237
253
  export declare class UpdateGrantScopesDto {
238
254
  scopes: I_AgencyGrantScope[];
239
- constructor(data: UpdateGrantScopesDto);
255
+ constructor(data?: Partial<UpdateGrantScopesDto>);
240
256
  }
241
257
  export declare class SponsorCreatorDto {
242
258
  tier: SponsoredTier;
243
- constructor(data: SponsorCreatorDto);
259
+ constructor(data?: Partial<SponsorCreatorDto>);
244
260
  }
245
261
  export declare class AgencyDiscoverQueryDto {
246
262
  /** An EXACT platform username or e-mail. Never recorded in analytics (FR-015). */
247
263
  q: string;
248
- constructor(data: AgencyDiscoverQueryDto);
264
+ constructor(data?: Partial<AgencyDiscoverQueryDto>);
249
265
  }
250
266
  export declare class AgencyAccessRequestDto {
251
267
  userId: string;
252
268
  scopes: I_AgencyGrantScope[];
253
- constructor(data: AgencyAccessRequestDto);
269
+ constructor(data?: Partial<AgencyAccessRequestDto>);
254
270
  }
255
271
  export declare class AdminSetOrganizationStatusDto {
256
272
  status: Extract<AgencyOrganizationStatus, 'approved' | 'rejected'>;
257
- /** Required on reject — a rejected applicant is shown this (FR-002). */
273
+ /** Required on reject — a rejected applicant is shown this (FR-002). The
274
+ * "required when rejecting" half is a rule about the PAIR, so it stays in
275
+ * the service where both fields are in hand. */
258
276
  reason?: string;
259
- constructor(data: AdminSetOrganizationStatusDto);
277
+ constructor(data?: Partial<AdminSetOrganizationStatusDto>);
260
278
  }
@@ -1,20 +1,16 @@
1
1
  "use strict";
2
- /**
3
- * Agency domain — organizations, memberships and creator access grants (spec 175).
4
- *
5
- * The shape behind "an agency sees its creators' numbers, because the creators
6
- * said yes". Two doors, one product: a creator account is unchanged by any of
7
- * this, and gains at most one grant.
8
- *
9
- * The one idea worth carrying into every consumer: a GRANT IS READ-ONLY AND
10
- * CREATOR-OWNED. The agency asks for scopes; only a creator's own act ever
11
- * writes `grantedScopes` (FR-008, SC-002). Nothing here can express a write on
12
- * a creator's account, and that is deliberate — the absence is the feature.
13
- *
14
- * See specs/175-agency-roster/contracts/lib-types.md.
15
- */
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
16
11
  Object.defineProperty(exports, "__esModule", { value: true });
17
12
  exports.AdminSetOrganizationStatusDto = exports.AgencyAccessRequestDto = exports.AgencyDiscoverQueryDto = exports.SponsorCreatorDto = exports.UpdateGrantScopesDto = exports.AcceptGrantDto = exports.AcceptAgencyMemberInviteDto = exports.RedeemAgencyInviteDto = exports.InviteAgencyMemberDto = exports.CreateAgencyOrganizationDto = exports.AGENCY_ROSTER_NEEDS_ATTENTION_STATES = exports.AGENCY_ROSTER_ROW_STATES = exports.CREATOR_ACCESS_GRANT_STATES = exports.AGENCY_ORGANIZATION_STATUSES = exports.isAgencyGrantScope = exports.AGENCY_GRANT_SCOPES = exports.AGENCY_ROUTE_PATHS = void 0;
13
+ const class_validator_1 = require("class-validator");
18
14
  // ── Cross-repo route constants ───────────────────────────────────────────────
19
15
  /**
20
16
  * Paths that BOTH repos have to agree on, with one home here.
@@ -125,76 +121,179 @@ exports.AGENCY_ROSTER_NEEDS_ATTENTION_STATES = [
125
121
  'stale',
126
122
  ];
127
123
  // ── DTOs ─────────────────────────────────────────────────────────────────────
124
+ /**
125
+ * Every DTO below carries class-validator decorators and a constructor that
126
+ * survives being called with nothing.
127
+ *
128
+ * Both are load-bearing and were missing until 2026-09-22. The backend mounts
129
+ * a global `ValidationPipe`, which validates the decorated metadata of the
130
+ * class a handler's `@Body()` is typed with — an undecorated class, or a body
131
+ * typed as a plain interface, is simply passed through unchecked. And the pipe
132
+ * instantiates the class with NO arguments, so a constructor that reads
133
+ * `data.name` would throw a TypeError on every request rather than validate
134
+ * anything. The rest of this library's DTOs take `data?: Partial<T>` for that
135
+ * reason; these now do too.
136
+ */
128
137
  class CreateAgencyOrganizationDto {
129
138
  constructor(data) {
130
- this.name = data.name;
131
- this.website = data.website;
132
- this.registryId = data.registryId;
133
- this.registryCountry = data.registryCountry;
134
- this.contactEmail = data.contactEmail;
139
+ this.name = data?.name ?? '';
140
+ this.website = data?.website;
141
+ this.registryId = data?.registryId ?? '';
142
+ this.registryCountry = data?.registryCountry ?? '';
143
+ this.contactEmail = data?.contactEmail ?? '';
135
144
  }
136
145
  }
137
146
  exports.CreateAgencyOrganizationDto = CreateAgencyOrganizationDto;
147
+ __decorate([
148
+ (0, class_validator_1.IsString)(),
149
+ (0, class_validator_1.IsNotEmpty)(),
150
+ __metadata("design:type", String)
151
+ ], CreateAgencyOrganizationDto.prototype, "name", void 0);
152
+ __decorate([
153
+ (0, class_validator_1.IsOptional)(),
154
+ (0, class_validator_1.IsUrl)({ protocols: ['http', 'https'], require_protocol: true }),
155
+ __metadata("design:type", String)
156
+ ], CreateAgencyOrganizationDto.prototype, "website", void 0);
157
+ __decorate([
158
+ (0, class_validator_1.IsString)(),
159
+ (0, class_validator_1.IsNotEmpty)(),
160
+ __metadata("design:type", String)
161
+ ], CreateAgencyOrganizationDto.prototype, "registryId", void 0);
162
+ __decorate([
163
+ (0, class_validator_1.IsString)(),
164
+ (0, class_validator_1.Length)(2, 2),
165
+ __metadata("design:type", String)
166
+ ], CreateAgencyOrganizationDto.prototype, "registryCountry", void 0);
167
+ __decorate([
168
+ (0, class_validator_1.IsEmail)(),
169
+ __metadata("design:type", String)
170
+ ], CreateAgencyOrganizationDto.prototype, "contactEmail", void 0);
138
171
  class InviteAgencyMemberDto {
139
172
  constructor(data) {
140
- this.email = data.email;
173
+ this.email = data?.email ?? '';
141
174
  }
142
175
  }
143
176
  exports.InviteAgencyMemberDto = InviteAgencyMemberDto;
177
+ __decorate([
178
+ (0, class_validator_1.IsEmail)(),
179
+ __metadata("design:type", String)
180
+ ], InviteAgencyMemberDto.prototype, "email", void 0);
144
181
  class RedeemAgencyInviteDto {
145
182
  constructor(data) {
146
- this.token = data.token;
183
+ this.token = data?.token ?? '';
147
184
  }
148
185
  }
149
186
  exports.RedeemAgencyInviteDto = RedeemAgencyInviteDto;
187
+ __decorate([
188
+ (0, class_validator_1.IsString)(),
189
+ (0, class_validator_1.IsNotEmpty)(),
190
+ __metadata("design:type", String)
191
+ ], RedeemAgencyInviteDto.prototype, "token", void 0);
150
192
  class AcceptAgencyMemberInviteDto {
151
193
  constructor(data) {
152
- this.token = data.token;
194
+ this.token = data?.token ?? '';
153
195
  }
154
196
  }
155
197
  exports.AcceptAgencyMemberInviteDto = AcceptAgencyMemberInviteDto;
198
+ __decorate([
199
+ (0, class_validator_1.IsString)(),
200
+ (0, class_validator_1.IsNotEmpty)(),
201
+ __metadata("design:type", String)
202
+ ], AcceptAgencyMemberInviteDto.prototype, "token", void 0);
156
203
  /**
157
204
  * Accepting consent. Either arm identifies the grant being answered; `scopes`
158
205
  * is the creator's answer and defaults to the requested set when absent.
159
206
  */
160
207
  class AcceptGrantDto {
161
208
  constructor(data) {
162
- this.token = data.token;
163
- this.grantId = data.grantId;
164
- this.scopes = data.scopes;
209
+ this.token = data?.token;
210
+ this.grantId = data?.grantId;
211
+ this.scopes = data?.scopes;
165
212
  }
166
213
  }
167
214
  exports.AcceptGrantDto = AcceptGrantDto;
215
+ __decorate([
216
+ (0, class_validator_1.IsOptional)(),
217
+ (0, class_validator_1.IsString)(),
218
+ (0, class_validator_1.IsNotEmpty)(),
219
+ __metadata("design:type", String)
220
+ ], AcceptGrantDto.prototype, "token", void 0);
221
+ __decorate([
222
+ (0, class_validator_1.IsOptional)(),
223
+ (0, class_validator_1.IsUUID)(),
224
+ __metadata("design:type", String)
225
+ ], AcceptGrantDto.prototype, "grantId", void 0);
226
+ __decorate([
227
+ (0, class_validator_1.IsOptional)(),
228
+ (0, class_validator_1.IsArray)(),
229
+ (0, class_validator_1.ArrayNotEmpty)(),
230
+ (0, class_validator_1.IsIn)(exports.AGENCY_GRANT_SCOPES, { each: true }),
231
+ __metadata("design:type", Array)
232
+ ], AcceptGrantDto.prototype, "scopes", void 0);
168
233
  /** The creator narrowing or widening a live grant — the only other write path. */
169
234
  class UpdateGrantScopesDto {
170
235
  constructor(data) {
171
- this.scopes = data.scopes;
236
+ this.scopes = data?.scopes ?? [];
172
237
  }
173
238
  }
174
239
  exports.UpdateGrantScopesDto = UpdateGrantScopesDto;
240
+ __decorate([
241
+ (0, class_validator_1.IsArray)(),
242
+ (0, class_validator_1.ArrayNotEmpty)(),
243
+ (0, class_validator_1.IsIn)(exports.AGENCY_GRANT_SCOPES, { each: true }),
244
+ __metadata("design:type", Array)
245
+ ], UpdateGrantScopesDto.prototype, "scopes", void 0);
175
246
  class SponsorCreatorDto {
176
247
  constructor(data) {
177
- this.tier = data.tier;
248
+ this.tier = data?.tier ?? 'starter';
178
249
  }
179
250
  }
180
251
  exports.SponsorCreatorDto = SponsorCreatorDto;
252
+ __decorate([
253
+ (0, class_validator_1.IsIn)(['starter', 'pro']),
254
+ __metadata("design:type", String)
255
+ ], SponsorCreatorDto.prototype, "tier", void 0);
181
256
  class AgencyDiscoverQueryDto {
182
257
  constructor(data) {
183
- this.q = data.q;
258
+ this.q = data?.q ?? '';
184
259
  }
185
260
  }
186
261
  exports.AgencyDiscoverQueryDto = AgencyDiscoverQueryDto;
262
+ __decorate([
263
+ (0, class_validator_1.IsString)(),
264
+ (0, class_validator_1.IsNotEmpty)(),
265
+ __metadata("design:type", String)
266
+ ], AgencyDiscoverQueryDto.prototype, "q", void 0);
187
267
  class AgencyAccessRequestDto {
188
268
  constructor(data) {
189
- this.userId = data.userId;
190
- this.scopes = data.scopes;
269
+ this.userId = data?.userId ?? '';
270
+ this.scopes = data?.scopes ?? [];
191
271
  }
192
272
  }
193
273
  exports.AgencyAccessRequestDto = AgencyAccessRequestDto;
274
+ __decorate([
275
+ (0, class_validator_1.IsUUID)(),
276
+ __metadata("design:type", String)
277
+ ], AgencyAccessRequestDto.prototype, "userId", void 0);
278
+ __decorate([
279
+ (0, class_validator_1.IsArray)(),
280
+ (0, class_validator_1.ArrayNotEmpty)(),
281
+ (0, class_validator_1.IsIn)(exports.AGENCY_GRANT_SCOPES, { each: true }),
282
+ __metadata("design:type", Array)
283
+ ], AgencyAccessRequestDto.prototype, "scopes", void 0);
194
284
  class AdminSetOrganizationStatusDto {
195
285
  constructor(data) {
196
- this.status = data.status;
197
- this.reason = data.reason;
286
+ this.status = data?.status ?? 'approved';
287
+ this.reason = data?.reason;
198
288
  }
199
289
  }
200
290
  exports.AdminSetOrganizationStatusDto = AdminSetOrganizationStatusDto;
291
+ __decorate([
292
+ (0, class_validator_1.IsIn)(['approved', 'rejected']),
293
+ __metadata("design:type", Object)
294
+ ], AdminSetOrganizationStatusDto.prototype, "status", void 0);
295
+ __decorate([
296
+ (0, class_validator_1.IsOptional)(),
297
+ (0, class_validator_1.IsString)(),
298
+ __metadata("design:type", String)
299
+ ], AdminSetOrganizationStatusDto.prototype, "reason", void 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ad2app-lib",
3
- "version": "1.44.0",
3
+ "version": "1.44.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "commonjs",
@@ -51,7 +51,7 @@
51
51
  "prepare": "npm run build"
52
52
  },
53
53
  "keywords": [],
54
- "author": "Maciej Górski@ad2.app",
54
+ "author": "Maciej G\u00f3rski@ad2.app",
55
55
  "license": "ISC",
56
56
  "description": "Package to share types and utils across the ad2app projects",
57
57
  "dependencies": {
@@ -10,6 +10,14 @@ import {
10
10
  isAgencyGrantScope,
11
11
  CreateAgencyOrganizationDto,
12
12
  AgencyAccessRequestDto,
13
+ InviteAgencyMemberDto,
14
+ RedeemAgencyInviteDto,
15
+ AcceptAgencyMemberInviteDto,
16
+ AcceptGrantDto,
17
+ UpdateGrantScopesDto,
18
+ SponsorCreatorDto,
19
+ AgencyDiscoverQueryDto,
20
+ AdminSetOrganizationStatusDto,
13
21
  } from "./agency";
14
22
 
15
23
  test("the grant scope set is exactly the three reads the spec allows", () => {
@@ -78,3 +86,53 @@ test("DTOs construct from a plain object (the house `new I_DTO()` shape)", () =>
78
86
  const request = new AgencyAccessRequestDto({ userId: "u-1", scopes: ["analytics"] });
79
87
  assert.deepEqual(request.scopes, ["analytics"]);
80
88
  });
89
+
90
+ test("every agency DTO survives being constructed with NOTHING", () => {
91
+ // What a validation pipe actually does. `plainToInstance` calls the
92
+ // constructor with no arguments before copying the body onto it, so a
93
+ // constructor that read `data.name` threw a TypeError on every request
94
+ // instead of validating anything (found 2026-09-22).
95
+ const DTOS = [
96
+ CreateAgencyOrganizationDto,
97
+ InviteAgencyMemberDto,
98
+ RedeemAgencyInviteDto,
99
+ AcceptAgencyMemberInviteDto,
100
+ AcceptGrantDto,
101
+ UpdateGrantScopesDto,
102
+ SponsorCreatorDto,
103
+ AgencyDiscoverQueryDto,
104
+ AgencyAccessRequestDto,
105
+ AdminSetOrganizationStatusDto,
106
+ ];
107
+
108
+ for (const Dto of DTOS) {
109
+ assert.doesNotThrow(() => new (Dto as new () => unknown)(), `${Dto.name} threw on an empty construct`);
110
+ }
111
+ });
112
+
113
+ test("every agency DTO carries validation metadata, not just a shape", () => {
114
+ // An undecorated class typed onto a handler's @Body() is passed through
115
+ // unchecked: the pipe validates DECORATORS, and there were none until
116
+ // 2026-09-22. Read from class-validator's own registry rather than by
117
+ // booting a pipe.
118
+ const { getMetadataStorage } = require("class-validator");
119
+ const storage = getMetadataStorage();
120
+
121
+ const DECORATED = [
122
+ CreateAgencyOrganizationDto,
123
+ InviteAgencyMemberDto,
124
+ RedeemAgencyInviteDto,
125
+ AcceptAgencyMemberInviteDto,
126
+ AcceptGrantDto,
127
+ UpdateGrantScopesDto,
128
+ SponsorCreatorDto,
129
+ AgencyDiscoverQueryDto,
130
+ AgencyAccessRequestDto,
131
+ AdminSetOrganizationStatusDto,
132
+ ];
133
+
134
+ for (const Dto of DECORATED) {
135
+ const rules = storage.getTargetValidationMetadatas(Dto, Dto.name, false, false);
136
+ assert.ok(rules.length > 0, `${Dto.name} has no validation rules`);
137
+ }
138
+ });
@@ -1,3 +1,16 @@
1
+ import {
2
+ ArrayNotEmpty,
3
+ IsArray,
4
+ IsEmail,
5
+ IsIn,
6
+ IsNotEmpty,
7
+ IsOptional,
8
+ IsString,
9
+ IsUrl,
10
+ IsUUID,
11
+ Length,
12
+ } from 'class-validator';
13
+
1
14
  /**
2
15
  * Agency domain — organizations, memberships and creator access grants (spec 175).
3
16
  *
@@ -278,43 +291,77 @@ export interface I_SponsoredBy {
278
291
 
279
292
  // ── DTOs ─────────────────────────────────────────────────────────────────────
280
293
 
294
+ /**
295
+ * Every DTO below carries class-validator decorators and a constructor that
296
+ * survives being called with nothing.
297
+ *
298
+ * Both are load-bearing and were missing until 2026-09-22. The backend mounts
299
+ * a global `ValidationPipe`, which validates the decorated metadata of the
300
+ * class a handler's `@Body()` is typed with — an undecorated class, or a body
301
+ * typed as a plain interface, is simply passed through unchecked. And the pipe
302
+ * instantiates the class with NO arguments, so a constructor that reads
303
+ * `data.name` would throw a TypeError on every request rather than validate
304
+ * anything. The rest of this library's DTOs take `data?: Partial<T>` for that
305
+ * reason; these now do too.
306
+ */
307
+
281
308
  export class CreateAgencyOrganizationDto {
309
+ @IsString()
310
+ @IsNotEmpty()
282
311
  name: string;
312
+
313
+ /** Rendered as a link on the admin screen, so the protocol is checked here. */
314
+ @IsOptional()
315
+ @IsUrl({ protocols: ['http', 'https'], require_protocol: true })
283
316
  website?: string;
317
+
318
+ @IsString()
319
+ @IsNotEmpty()
284
320
  registryId: string;
321
+
322
+ /** ISO-3166 alpha-2: which register a reviewer should look in. */
323
+ @IsString()
324
+ @Length(2, 2)
285
325
  registryCountry: string;
326
+
327
+ @IsEmail()
286
328
  contactEmail: string;
287
329
 
288
- constructor(data: CreateAgencyOrganizationDto) {
289
- this.name = data.name;
290
- this.website = data.website;
291
- this.registryId = data.registryId;
292
- this.registryCountry = data.registryCountry;
293
- this.contactEmail = data.contactEmail;
330
+ constructor(data?: Partial<CreateAgencyOrganizationDto>) {
331
+ this.name = data?.name ?? '';
332
+ this.website = data?.website;
333
+ this.registryId = data?.registryId ?? '';
334
+ this.registryCountry = data?.registryCountry ?? '';
335
+ this.contactEmail = data?.contactEmail ?? '';
294
336
  }
295
337
  }
296
338
 
297
339
  export class InviteAgencyMemberDto {
340
+ @IsEmail()
298
341
  email: string;
299
342
 
300
- constructor(data: InviteAgencyMemberDto) {
301
- this.email = data.email;
343
+ constructor(data?: Partial<InviteAgencyMemberDto>) {
344
+ this.email = data?.email ?? '';
302
345
  }
303
346
  }
304
347
 
305
348
  export class RedeemAgencyInviteDto {
349
+ @IsString()
350
+ @IsNotEmpty()
306
351
  token: string;
307
352
 
308
- constructor(data: RedeemAgencyInviteDto) {
309
- this.token = data.token;
353
+ constructor(data?: Partial<RedeemAgencyInviteDto>) {
354
+ this.token = data?.token ?? '';
310
355
  }
311
356
  }
312
357
 
313
358
  export class AcceptAgencyMemberInviteDto {
359
+ @IsString()
360
+ @IsNotEmpty()
314
361
  token: string;
315
362
 
316
- constructor(data: AcceptAgencyMemberInviteDto) {
317
- this.token = data.token;
363
+ constructor(data?: Partial<AcceptAgencyMemberInviteDto>) {
364
+ this.token = data?.token ?? '';
318
365
  }
319
366
  }
320
367
 
@@ -323,60 +370,89 @@ export class AcceptAgencyMemberInviteDto {
323
370
  * is the creator's answer and defaults to the requested set when absent.
324
371
  */
325
372
  export class AcceptGrantDto {
373
+ @IsOptional()
374
+ @IsString()
375
+ @IsNotEmpty()
326
376
  token?: string;
377
+
378
+ @IsOptional()
379
+ @IsUUID()
327
380
  grantId?: string;
381
+
382
+ /** A subset of the three reads. Never empty: sharing nothing is declining. */
383
+ @IsOptional()
384
+ @IsArray()
385
+ @ArrayNotEmpty()
386
+ @IsIn(AGENCY_GRANT_SCOPES as unknown as string[], { each: true })
328
387
  scopes?: I_AgencyGrantScope[];
329
388
 
330
- constructor(data: AcceptGrantDto) {
331
- this.token = data.token;
332
- this.grantId = data.grantId;
333
- this.scopes = data.scopes;
389
+ constructor(data?: Partial<AcceptGrantDto>) {
390
+ this.token = data?.token;
391
+ this.grantId = data?.grantId;
392
+ this.scopes = data?.scopes;
334
393
  }
335
394
  }
336
395
 
337
396
  /** The creator narrowing or widening a live grant — the only other write path. */
338
397
  export class UpdateGrantScopesDto {
398
+ @IsArray()
399
+ @ArrayNotEmpty()
400
+ @IsIn(AGENCY_GRANT_SCOPES as unknown as string[], { each: true })
339
401
  scopes: I_AgencyGrantScope[];
340
402
 
341
- constructor(data: UpdateGrantScopesDto) {
342
- this.scopes = data.scopes;
403
+ constructor(data?: Partial<UpdateGrantScopesDto>) {
404
+ this.scopes = data?.scopes ?? [];
343
405
  }
344
406
  }
345
407
 
346
408
  export class SponsorCreatorDto {
409
+ @IsIn(['starter', 'pro'])
347
410
  tier: SponsoredTier;
348
411
 
349
- constructor(data: SponsorCreatorDto) {
350
- this.tier = data.tier;
412
+ constructor(data?: Partial<SponsorCreatorDto>) {
413
+ this.tier = data?.tier ?? 'starter';
351
414
  }
352
415
  }
353
416
 
354
417
  export class AgencyDiscoverQueryDto {
355
418
  /** An EXACT platform username or e-mail. Never recorded in analytics (FR-015). */
419
+ @IsString()
420
+ @IsNotEmpty()
356
421
  q: string;
357
422
 
358
- constructor(data: AgencyDiscoverQueryDto) {
359
- this.q = data.q;
423
+ constructor(data?: Partial<AgencyDiscoverQueryDto>) {
424
+ this.q = data?.q ?? '';
360
425
  }
361
426
  }
362
427
 
363
428
  export class AgencyAccessRequestDto {
429
+ @IsUUID()
364
430
  userId: string;
431
+
432
+ @IsArray()
433
+ @ArrayNotEmpty()
434
+ @IsIn(AGENCY_GRANT_SCOPES as unknown as string[], { each: true })
365
435
  scopes: I_AgencyGrantScope[];
366
436
 
367
- constructor(data: AgencyAccessRequestDto) {
368
- this.userId = data.userId;
369
- this.scopes = data.scopes;
437
+ constructor(data?: Partial<AgencyAccessRequestDto>) {
438
+ this.userId = data?.userId ?? '';
439
+ this.scopes = data?.scopes ?? [];
370
440
  }
371
441
  }
372
442
 
373
443
  export class AdminSetOrganizationStatusDto {
444
+ @IsIn(['approved', 'rejected'])
374
445
  status: Extract<AgencyOrganizationStatus, 'approved' | 'rejected'>;
375
- /** Required on reject — a rejected applicant is shown this (FR-002). */
446
+
447
+ /** Required on reject — a rejected applicant is shown this (FR-002). The
448
+ * "required when rejecting" half is a rule about the PAIR, so it stays in
449
+ * the service where both fields are in hand. */
450
+ @IsOptional()
451
+ @IsString()
376
452
  reason?: string;
377
453
 
378
- constructor(data: AdminSetOrganizationStatusDto) {
379
- this.status = data.status;
380
- this.reason = data.reason;
454
+ constructor(data?: Partial<AdminSetOrganizationStatusDto>) {
455
+ this.status = data?.status ?? 'approved';
456
+ this.reason = data?.reason;
381
457
  }
382
458
  }