ad2app-lib 1.43.0 → 1.44.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.
@@ -0,0 +1,382 @@
1
+ /**
2
+ * Agency domain — organizations, memberships and creator access grants (spec 175).
3
+ *
4
+ * The shape behind "an agency sees its creators' numbers, because the creators
5
+ * said yes". Two doors, one product: a creator account is unchanged by any of
6
+ * this, and gains at most one grant.
7
+ *
8
+ * The one idea worth carrying into every consumer: a GRANT IS READ-ONLY AND
9
+ * CREATOR-OWNED. The agency asks for scopes; only a creator's own act ever
10
+ * writes `grantedScopes` (FR-008, SC-002). Nothing here can express a write on
11
+ * a creator's account, and that is deliberate — the absence is the feature.
12
+ *
13
+ * See specs/175-agency-roster/contracts/lib-types.md.
14
+ */
15
+
16
+ import type { PublishPlatform } from '../publish-limits/types';
17
+
18
+ // ── Cross-repo route constants ───────────────────────────────────────────────
19
+
20
+ /**
21
+ * Paths that BOTH repos have to agree on, with one home here.
22
+ *
23
+ * The backend mails this path; the frontend registers it. They deploy
24
+ * separately, so nothing else notices them drifting — and the first version of
25
+ * that link pointed at a route no repo served, which is how an entire user
26
+ * story shipped with its headline capability unreachable.
27
+ *
28
+ * A test in each repo asserts its own side against these constants. The first
29
+ * attempt at that guard read across the filesystem into a sibling checkout,
30
+ * which worked on one laptop and threw ENOENT in CI; a shared constant is the
31
+ * version that actually runs.
32
+ */
33
+ export const AGENCY_ROUTE_PATHS = {
34
+ /** A colleague accepting a team invitation (FR-005). */
35
+ joinTeam: '/agency/join-team',
36
+ /** A creator consenting to an agency's access (FR-007). */
37
+ join: '/agency/join',
38
+ /** A creator answering an access request (FR-016). */
39
+ requests: '/agency/requests',
40
+ } as const;
41
+
42
+ // ── Scopes ───────────────────────────────────────────────────────────────────
43
+
44
+ /**
45
+ * The complete set of reads a grant may carry. A CEILING, not a starting point.
46
+ *
47
+ * All three are reads of what the creator already sees on their own screens.
48
+ * Private messages, comment replies and moderation, publishing, connection
49
+ * changes and settings are absent by construction (FR-010) — there is no scope
50
+ * string that could name them, so a widened grant is a spec change rather than
51
+ * a config change.
52
+ */
53
+ export const AGENCY_GRANT_SCOPES = [
54
+ /** the creator's own five analytics reads, full history, no date filtering */
55
+ 'analytics',
56
+ /** comments under the creator's posts, READ only — the one inbox read a grant reaches */
57
+ 'comments',
58
+ /** the creator's published posts with their own numbers — never drafts or the queue */
59
+ 'posts',
60
+ ] as const;
61
+
62
+ export type I_AgencyGrantScope = (typeof AGENCY_GRANT_SCOPES)[number];
63
+
64
+ /**
65
+ * Narrowing guard — an unrecognised scope must fail closed.
66
+ *
67
+ * Scope strings arrive from request bodies (the agency's ask, the creator's
68
+ * answer), so this is a trust boundary, not a convenience.
69
+ */
70
+ export const isAgencyGrantScope = (value: string): value is I_AgencyGrantScope =>
71
+ (AGENCY_GRANT_SCOPES as readonly string[]).includes(value);
72
+
73
+ // ── Organization ─────────────────────────────────────────────────────────────
74
+
75
+ /** `'brand'` arrives with spec 148; one column, no enum table. */
76
+ export type AgencyOrganizationType = 'agency';
77
+
78
+ /**
79
+ * `closed` was added by the 2026-09-21 requirements review (FR-004b).
80
+ *
81
+ * It exists so closure has an end state that is NOT a deleted row: grants are
82
+ * `on delete cascade` from the organization, so hard-deleting one would destroy
83
+ * the consent records FR-009 says are never deleted — and which are also the
84
+ * invoice basis.
85
+ */
86
+ export const AGENCY_ORGANIZATION_STATUSES = [
87
+ 'pending',
88
+ 'approved',
89
+ 'rejected',
90
+ 'closed',
91
+ ] as const;
92
+
93
+ export type AgencyOrganizationStatus = (typeof AGENCY_ORGANIZATION_STATUSES)[number];
94
+
95
+ /** Mirrored from Stripe. `comped` is a 100% coupon and is treated as active. */
96
+ export type AgencySubscriptionStatus =
97
+ | 'none'
98
+ | 'active'
99
+ | 'trialing'
100
+ | 'past_due'
101
+ | 'unpaid'
102
+ | 'canceled'
103
+ | 'comped';
104
+
105
+ /** Two roles, no more (FR-004). Only the owner reaches billing, members and the link. */
106
+ export type AgencyMemberRole = 'owner' | 'member';
107
+
108
+ export type SponsoredTier = 'starter' | 'pro';
109
+
110
+ export interface I_AgencyOrganization {
111
+ id: string;
112
+ type: AgencyOrganizationType;
113
+ name: string;
114
+ website?: string;
115
+ /** NIP/KRS or a foreign equivalent — free text, verified by a human, never by code. */
116
+ registryId: string;
117
+ /** ISO-3166 alpha-2 of the register the identifier belongs to. */
118
+ registryCountry: string;
119
+ contactEmail: string;
120
+ status: AgencyOrganizationStatus;
121
+ /** The reason an admin gave; shown to a rejected applicant (FR-002). */
122
+ statusReason?: string;
123
+ subscriptionStatus: AgencySubscriptionStatus;
124
+ /** The date sponsorships run to when a grant is revoked or the plan lapses. */
125
+ currentPeriodEnd?: string;
126
+ createdAt: string;
127
+ }
128
+
129
+ export interface I_AgencyMember {
130
+ id: string;
131
+ organizationId: string;
132
+ /** Null while an e-mail invitation is outstanding — nobody has accepted yet. */
133
+ userId?: string;
134
+ invitedEmail: string;
135
+ role: AgencyMemberRole;
136
+ invitedAt: string;
137
+ /** "Who is in" is exactly `acceptedAt != null`. */
138
+ acceptedAt?: string;
139
+ revokedAt?: string;
140
+ }
141
+
142
+ // ── Grant ────────────────────────────────────────────────────────────────────
143
+
144
+ /**
145
+ * The five states the data model pins.
146
+ *
147
+ * `declined` and `expired` belong to the US5 request path (FR-016) and were
148
+ * missing from the spec's own Key Entities until the 2026-09-21 review. There
149
+ * is no transition OUT of `revoked`, `declined` or `expired`: a new consent is
150
+ * a new row, so the record of what was agreed is never overwritten.
151
+ */
152
+ export const CREATOR_ACCESS_GRANT_STATES = [
153
+ 'requested',
154
+ 'active',
155
+ 'declined',
156
+ 'expired',
157
+ 'revoked',
158
+ ] as const;
159
+
160
+ export type CreatorAccessGrantState = (typeof CREATOR_ACCESS_GRANT_STATES)[number];
161
+
162
+ export type CreatorAccessGrantOrigin = 'invite_link' | 'agency_request';
163
+
164
+ /** Who ended it. Shown in the agency's past-creators history (FR-013b). */
165
+ export type CreatorAccessGrantRevokedBy =
166
+ | 'creator'
167
+ | 'agency'
168
+ | 'organization'
169
+ | 'account-deleted'
170
+ | 'purge'
171
+ | 'system';
172
+
173
+ export interface I_CreatorAccessGrant {
174
+ id: string;
175
+ /**
176
+ * Null once the creator's user row is hard-purged (audit D-D): the grant
177
+ * survives as the consent record, with `creatorIdentity` standing in for the
178
+ * person who is no longer there.
179
+ */
180
+ creatorUserId?: string;
181
+ organizationId: string;
182
+ organizationName: string;
183
+ /** What the agency asked for. */
184
+ requestedScopes: I_AgencyGrantScope[];
185
+ /** What the creator actually granted — a subset, possibly smaller, never larger. */
186
+ grantedScopes: I_AgencyGrantScope[];
187
+ /** Frozen at grant time; what the history shows after a purge. */
188
+ creatorIdentity: { handle: string; displayName?: string };
189
+ state: CreatorAccessGrantState;
190
+ origin: CreatorAccessGrantOrigin;
191
+ requestedAt?: string;
192
+ expiresAt?: string;
193
+ grantedAt?: string;
194
+ /** Which terms the creator accepted. A `draft-` prefix means pre-addendum (FR-020). */
195
+ termsVersion?: string;
196
+ revokedAt?: string;
197
+ revokedBy?: CreatorAccessGrantRevokedBy;
198
+ /**
199
+ * The agency covering this creator's plan. `until` is set when the
200
+ * sponsorship is ending — it runs to the close of the period already paid
201
+ * for, so a revoke never silently drops the creator's plan (FR-014).
202
+ */
203
+ sponsorship?: { tier: SponsoredTier; since: string; until?: string };
204
+ /** Which platforms the grant reaches right now — derived at read time, never stored. */
205
+ platformsReached: PublishPlatform[];
206
+ }
207
+
208
+ // ── Roster ───────────────────────────────────────────────────────────────────
209
+
210
+ /**
211
+ * Roster row states, DECLARED IN PRECEDENCE ORDER (data-model §Derived).
212
+ *
213
+ * A row holds exactly one state: the first of these that applies, most-blocking
214
+ * first. `revoked` is deliberately absent — a revoked creator leaves the roster
215
+ * entirely for the settings history (FR-011).
216
+ */
217
+ export const AGENCY_ROSTER_ROW_STATES = [
218
+ /** the agency asked, the creator has not answered */
219
+ 'requested',
220
+ /** the creator's own plan has lapsed */
221
+ 'lapsed',
222
+ /** the creator has no live platform connection */
223
+ 'disconnected',
224
+ /** no successful daily sync in 24 hours (FR-011, Jan 2026-09-21) */
225
+ 'stale',
226
+ 'active',
227
+ ] as const;
228
+
229
+ export type AgencyRosterRowState = (typeof AGENCY_ROSTER_ROW_STATES)[number];
230
+
231
+ /**
232
+ * What "needs attention" means — ONE home for the KPI tile, the roster segment
233
+ * and the rail badge, so the three can never drift apart (FR-011).
234
+ *
235
+ * `requested` is excluded on purpose: it waits on the CREATOR, and FR-004a says
236
+ * the badge means something waits on the agency.
237
+ */
238
+ export const AGENCY_ROSTER_NEEDS_ATTENTION_STATES = [
239
+ 'lapsed',
240
+ 'disconnected',
241
+ 'stale',
242
+ ] as const satisfies readonly AgencyRosterRowState[];
243
+
244
+ export type AgencyRosterNeedsAttentionState =
245
+ (typeof AGENCY_ROSTER_NEEDS_ATTENTION_STATES)[number];
246
+
247
+ export interface I_AgencyRosterRow {
248
+ grantId: string;
249
+ creator: { id: string; handle: string };
250
+ platforms: PublishPlatform[];
251
+ state: AgencyRosterRowState;
252
+ grantedScopes: I_AgencyGrantScope[];
253
+ lastSyncAt?: string;
254
+ }
255
+
256
+ export interface I_AgencyRoster {
257
+ totals: {
258
+ creators: number;
259
+ active: number;
260
+ /** Summed only over creators holding `analytics`; disconnected platforms excluded. */
261
+ followers: number;
262
+ followersDelta7d: number;
263
+ /** Calendar week, Mon-Sun, organization timezone, creators holding `comments`. */
264
+ commentsThisWeek: number;
265
+ /** Count of rows in AGENCY_ROSTER_NEEDS_ATTENTION_STATES. */
266
+ needsAttention: number;
267
+ };
268
+ items: I_AgencyRosterRow[];
269
+ }
270
+
271
+ /** What a sponsored creator sees on their OWN subscription screen (FR-014). */
272
+ export interface I_SponsoredBy {
273
+ organizationName: string;
274
+ tier: SponsoredTier;
275
+ /** Present once the sponsorship is ending: the date their plan runs to. */
276
+ until?: string;
277
+ }
278
+
279
+ // ── DTOs ─────────────────────────────────────────────────────────────────────
280
+
281
+ export class CreateAgencyOrganizationDto {
282
+ name: string;
283
+ website?: string;
284
+ registryId: string;
285
+ registryCountry: string;
286
+ contactEmail: string;
287
+
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;
294
+ }
295
+ }
296
+
297
+ export class InviteAgencyMemberDto {
298
+ email: string;
299
+
300
+ constructor(data: InviteAgencyMemberDto) {
301
+ this.email = data.email;
302
+ }
303
+ }
304
+
305
+ export class RedeemAgencyInviteDto {
306
+ token: string;
307
+
308
+ constructor(data: RedeemAgencyInviteDto) {
309
+ this.token = data.token;
310
+ }
311
+ }
312
+
313
+ export class AcceptAgencyMemberInviteDto {
314
+ token: string;
315
+
316
+ constructor(data: AcceptAgencyMemberInviteDto) {
317
+ this.token = data.token;
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Accepting consent. Either arm identifies the grant being answered; `scopes`
323
+ * is the creator's answer and defaults to the requested set when absent.
324
+ */
325
+ export class AcceptGrantDto {
326
+ token?: string;
327
+ grantId?: string;
328
+ scopes?: I_AgencyGrantScope[];
329
+
330
+ constructor(data: AcceptGrantDto) {
331
+ this.token = data.token;
332
+ this.grantId = data.grantId;
333
+ this.scopes = data.scopes;
334
+ }
335
+ }
336
+
337
+ /** The creator narrowing or widening a live grant — the only other write path. */
338
+ export class UpdateGrantScopesDto {
339
+ scopes: I_AgencyGrantScope[];
340
+
341
+ constructor(data: UpdateGrantScopesDto) {
342
+ this.scopes = data.scopes;
343
+ }
344
+ }
345
+
346
+ export class SponsorCreatorDto {
347
+ tier: SponsoredTier;
348
+
349
+ constructor(data: SponsorCreatorDto) {
350
+ this.tier = data.tier;
351
+ }
352
+ }
353
+
354
+ export class AgencyDiscoverQueryDto {
355
+ /** An EXACT platform username or e-mail. Never recorded in analytics (FR-015). */
356
+ q: string;
357
+
358
+ constructor(data: AgencyDiscoverQueryDto) {
359
+ this.q = data.q;
360
+ }
361
+ }
362
+
363
+ export class AgencyAccessRequestDto {
364
+ userId: string;
365
+ scopes: I_AgencyGrantScope[];
366
+
367
+ constructor(data: AgencyAccessRequestDto) {
368
+ this.userId = data.userId;
369
+ this.scopes = data.scopes;
370
+ }
371
+ }
372
+
373
+ export class AdminSetOrganizationStatusDto {
374
+ status: Extract<AgencyOrganizationStatus, 'approved' | 'rejected'>;
375
+ /** Required on reject — a rejected applicant is shown this (FR-002). */
376
+ reason?: string;
377
+
378
+ constructor(data: AdminSetOrganizationStatusDto) {
379
+ this.status = data.status;
380
+ this.reason = data.reason;
381
+ }
382
+ }
@@ -41,5 +41,8 @@ export * from "./scheduling";
41
41
  // ── Agent domain (delegated authority + direct media upload, spec 163) ───────
42
42
  export * from "./agent";
43
43
 
44
+ // ── Agency domain (organizations, memberships, creator access grants, spec 175)
45
+ export * from "./agency";
46
+
44
47
  // ── Access control ────────────────────────────────────────────────────────────
45
48
  export * from "./I_AccessDenial";