ad2app-lib 1.43.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.
- package/dist/analytics/index.d.ts +55 -0
- package/dist/analytics/index.js +22 -0
- package/dist/types/I_AccessDenial.d.ts +55 -0
- package/dist/types/I_AccessDenial.js +56 -0
- package/dist/types/agency.d.ts +278 -0
- package/dist/types/agency.js +299 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +2 -0
- package/package.json +2 -2
- package/src/analytics/index.test.ts +49 -0
- package/src/analytics/index.ts +68 -0
- package/src/types/I_AccessDenial.ts +58 -0
- package/src/types/access-denial.test.ts +31 -7
- package/src/types/agency.test.ts +138 -0
- package/src/types/agency.ts +458 -0
- package/src/types/index.ts +3 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"use strict";
|
|
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
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
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");
|
|
14
|
+
// ── Cross-repo route constants ───────────────────────────────────────────────
|
|
15
|
+
/**
|
|
16
|
+
* Paths that BOTH repos have to agree on, with one home here.
|
|
17
|
+
*
|
|
18
|
+
* The backend mails this path; the frontend registers it. They deploy
|
|
19
|
+
* separately, so nothing else notices them drifting — and the first version of
|
|
20
|
+
* that link pointed at a route no repo served, which is how an entire user
|
|
21
|
+
* story shipped with its headline capability unreachable.
|
|
22
|
+
*
|
|
23
|
+
* A test in each repo asserts its own side against these constants. The first
|
|
24
|
+
* attempt at that guard read across the filesystem into a sibling checkout,
|
|
25
|
+
* which worked on one laptop and threw ENOENT in CI; a shared constant is the
|
|
26
|
+
* version that actually runs.
|
|
27
|
+
*/
|
|
28
|
+
exports.AGENCY_ROUTE_PATHS = {
|
|
29
|
+
/** A colleague accepting a team invitation (FR-005). */
|
|
30
|
+
joinTeam: '/agency/join-team',
|
|
31
|
+
/** A creator consenting to an agency's access (FR-007). */
|
|
32
|
+
join: '/agency/join',
|
|
33
|
+
/** A creator answering an access request (FR-016). */
|
|
34
|
+
requests: '/agency/requests',
|
|
35
|
+
};
|
|
36
|
+
// ── Scopes ───────────────────────────────────────────────────────────────────
|
|
37
|
+
/**
|
|
38
|
+
* The complete set of reads a grant may carry. A CEILING, not a starting point.
|
|
39
|
+
*
|
|
40
|
+
* All three are reads of what the creator already sees on their own screens.
|
|
41
|
+
* Private messages, comment replies and moderation, publishing, connection
|
|
42
|
+
* changes and settings are absent by construction (FR-010) — there is no scope
|
|
43
|
+
* string that could name them, so a widened grant is a spec change rather than
|
|
44
|
+
* a config change.
|
|
45
|
+
*/
|
|
46
|
+
exports.AGENCY_GRANT_SCOPES = [
|
|
47
|
+
/** the creator's own five analytics reads, full history, no date filtering */
|
|
48
|
+
'analytics',
|
|
49
|
+
/** comments under the creator's posts, READ only — the one inbox read a grant reaches */
|
|
50
|
+
'comments',
|
|
51
|
+
/** the creator's published posts with their own numbers — never drafts or the queue */
|
|
52
|
+
'posts',
|
|
53
|
+
];
|
|
54
|
+
/**
|
|
55
|
+
* Narrowing guard — an unrecognised scope must fail closed.
|
|
56
|
+
*
|
|
57
|
+
* Scope strings arrive from request bodies (the agency's ask, the creator's
|
|
58
|
+
* answer), so this is a trust boundary, not a convenience.
|
|
59
|
+
*/
|
|
60
|
+
const isAgencyGrantScope = (value) => exports.AGENCY_GRANT_SCOPES.includes(value);
|
|
61
|
+
exports.isAgencyGrantScope = isAgencyGrantScope;
|
|
62
|
+
/**
|
|
63
|
+
* `closed` was added by the 2026-09-21 requirements review (FR-004b).
|
|
64
|
+
*
|
|
65
|
+
* It exists so closure has an end state that is NOT a deleted row: grants are
|
|
66
|
+
* `on delete cascade` from the organization, so hard-deleting one would destroy
|
|
67
|
+
* the consent records FR-009 says are never deleted — and which are also the
|
|
68
|
+
* invoice basis.
|
|
69
|
+
*/
|
|
70
|
+
exports.AGENCY_ORGANIZATION_STATUSES = [
|
|
71
|
+
'pending',
|
|
72
|
+
'approved',
|
|
73
|
+
'rejected',
|
|
74
|
+
'closed',
|
|
75
|
+
];
|
|
76
|
+
// ── Grant ────────────────────────────────────────────────────────────────────
|
|
77
|
+
/**
|
|
78
|
+
* The five states the data model pins.
|
|
79
|
+
*
|
|
80
|
+
* `declined` and `expired` belong to the US5 request path (FR-016) and were
|
|
81
|
+
* missing from the spec's own Key Entities until the 2026-09-21 review. There
|
|
82
|
+
* is no transition OUT of `revoked`, `declined` or `expired`: a new consent is
|
|
83
|
+
* a new row, so the record of what was agreed is never overwritten.
|
|
84
|
+
*/
|
|
85
|
+
exports.CREATOR_ACCESS_GRANT_STATES = [
|
|
86
|
+
'requested',
|
|
87
|
+
'active',
|
|
88
|
+
'declined',
|
|
89
|
+
'expired',
|
|
90
|
+
'revoked',
|
|
91
|
+
];
|
|
92
|
+
// ── Roster ───────────────────────────────────────────────────────────────────
|
|
93
|
+
/**
|
|
94
|
+
* Roster row states, DECLARED IN PRECEDENCE ORDER (data-model §Derived).
|
|
95
|
+
*
|
|
96
|
+
* A row holds exactly one state: the first of these that applies, most-blocking
|
|
97
|
+
* first. `revoked` is deliberately absent — a revoked creator leaves the roster
|
|
98
|
+
* entirely for the settings history (FR-011).
|
|
99
|
+
*/
|
|
100
|
+
exports.AGENCY_ROSTER_ROW_STATES = [
|
|
101
|
+
/** the agency asked, the creator has not answered */
|
|
102
|
+
'requested',
|
|
103
|
+
/** the creator's own plan has lapsed */
|
|
104
|
+
'lapsed',
|
|
105
|
+
/** the creator has no live platform connection */
|
|
106
|
+
'disconnected',
|
|
107
|
+
/** no successful daily sync in 24 hours (FR-011, Jan 2026-09-21) */
|
|
108
|
+
'stale',
|
|
109
|
+
'active',
|
|
110
|
+
];
|
|
111
|
+
/**
|
|
112
|
+
* What "needs attention" means — ONE home for the KPI tile, the roster segment
|
|
113
|
+
* and the rail badge, so the three can never drift apart (FR-011).
|
|
114
|
+
*
|
|
115
|
+
* `requested` is excluded on purpose: it waits on the CREATOR, and FR-004a says
|
|
116
|
+
* the badge means something waits on the agency.
|
|
117
|
+
*/
|
|
118
|
+
exports.AGENCY_ROSTER_NEEDS_ATTENTION_STATES = [
|
|
119
|
+
'lapsed',
|
|
120
|
+
'disconnected',
|
|
121
|
+
'stale',
|
|
122
|
+
];
|
|
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
|
+
*/
|
|
137
|
+
class CreateAgencyOrganizationDto {
|
|
138
|
+
constructor(data) {
|
|
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 ?? '';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
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);
|
|
171
|
+
class InviteAgencyMemberDto {
|
|
172
|
+
constructor(data) {
|
|
173
|
+
this.email = data?.email ?? '';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
exports.InviteAgencyMemberDto = InviteAgencyMemberDto;
|
|
177
|
+
__decorate([
|
|
178
|
+
(0, class_validator_1.IsEmail)(),
|
|
179
|
+
__metadata("design:type", String)
|
|
180
|
+
], InviteAgencyMemberDto.prototype, "email", void 0);
|
|
181
|
+
class RedeemAgencyInviteDto {
|
|
182
|
+
constructor(data) {
|
|
183
|
+
this.token = data?.token ?? '';
|
|
184
|
+
}
|
|
185
|
+
}
|
|
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);
|
|
192
|
+
class AcceptAgencyMemberInviteDto {
|
|
193
|
+
constructor(data) {
|
|
194
|
+
this.token = data?.token ?? '';
|
|
195
|
+
}
|
|
196
|
+
}
|
|
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);
|
|
203
|
+
/**
|
|
204
|
+
* Accepting consent. Either arm identifies the grant being answered; `scopes`
|
|
205
|
+
* is the creator's answer and defaults to the requested set when absent.
|
|
206
|
+
*/
|
|
207
|
+
class AcceptGrantDto {
|
|
208
|
+
constructor(data) {
|
|
209
|
+
this.token = data?.token;
|
|
210
|
+
this.grantId = data?.grantId;
|
|
211
|
+
this.scopes = data?.scopes;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
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);
|
|
233
|
+
/** The creator narrowing or widening a live grant — the only other write path. */
|
|
234
|
+
class UpdateGrantScopesDto {
|
|
235
|
+
constructor(data) {
|
|
236
|
+
this.scopes = data?.scopes ?? [];
|
|
237
|
+
}
|
|
238
|
+
}
|
|
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);
|
|
246
|
+
class SponsorCreatorDto {
|
|
247
|
+
constructor(data) {
|
|
248
|
+
this.tier = data?.tier ?? 'starter';
|
|
249
|
+
}
|
|
250
|
+
}
|
|
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);
|
|
256
|
+
class AgencyDiscoverQueryDto {
|
|
257
|
+
constructor(data) {
|
|
258
|
+
this.q = data?.q ?? '';
|
|
259
|
+
}
|
|
260
|
+
}
|
|
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);
|
|
267
|
+
class AgencyAccessRequestDto {
|
|
268
|
+
constructor(data) {
|
|
269
|
+
this.userId = data?.userId ?? '';
|
|
270
|
+
this.scopes = data?.scopes ?? [];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
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);
|
|
284
|
+
class AdminSetOrganizationStatusDto {
|
|
285
|
+
constructor(data) {
|
|
286
|
+
this.status = data?.status ?? 'approved';
|
|
287
|
+
this.reason = data?.reason;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
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/dist/types/index.d.ts
CHANGED
package/dist/types/index.js
CHANGED
|
@@ -55,5 +55,7 @@ __exportStar(require("./I_SM_Platform"), exports);
|
|
|
55
55
|
__exportStar(require("./scheduling"), exports);
|
|
56
56
|
// ── Agent domain (delegated authority + direct media upload, spec 163) ───────
|
|
57
57
|
__exportStar(require("./agent"), exports);
|
|
58
|
+
// ── Agency domain (organizations, memberships, creator access grants, spec 175)
|
|
59
|
+
__exportStar(require("./agency"), exports);
|
|
58
60
|
// ── Access control ────────────────────────────────────────────────────────────
|
|
59
61
|
__exportStar(require("./I_AccessDenial"), exports);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ad2app-lib",
|
|
3
|
-
"version": "1.
|
|
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
|
|
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": {
|
|
@@ -159,8 +159,57 @@ const EVENT_PROPERTY_WITNESS: { [E in keyof EventProperties]: EventProperties[E]
|
|
|
159
159
|
// the browser that declined is a fact nobody can read.
|
|
160
160
|
[EVENTS.ANALYTICS_CONSENT_RECORDED]: { state: "declined" },
|
|
161
161
|
[EVENTS.FIRST_AUTHENTICATED]: {},
|
|
162
|
+
// 175 — the agency set. Every witness carries organization_id + actor_role,
|
|
163
|
+
// because those are the two mandatory fields of AgencyEventProperties.
|
|
164
|
+
[EVENTS.AGENCY_SIGNUP_SUBMITTED]: { organization_id: "org-1", actor_role: "owner" },
|
|
165
|
+
[EVENTS.AGENCY_APPROVED]: { organization_id: "org-1", actor_role: "admin" },
|
|
166
|
+
[EVENTS.AGENCY_REJECTED]: { organization_id: "org-1", actor_role: "admin" },
|
|
167
|
+
[EVENTS.AGENCY_MEMBER_INVITED]: { organization_id: "org-1", actor_role: "owner" },
|
|
168
|
+
[EVENTS.AGENCY_INVITE_LINK_ROTATED]: { organization_id: "org-1", actor_role: "owner" },
|
|
169
|
+
[EVENTS.AGENCY_INVITE_OPENED]: { organization_id: "org-1", actor_role: "creator", origin: "invite_link" },
|
|
170
|
+
[EVENTS.AGENCY_CONSENT_ACCEPTED]: {
|
|
171
|
+
organization_id: "org-1",
|
|
172
|
+
grant_id: "g-1",
|
|
173
|
+
actor_role: "creator",
|
|
174
|
+
terms_version: "draft-2026-09-21",
|
|
175
|
+
},
|
|
176
|
+
[EVENTS.AGENCY_CONSENT_DECLINED]: { organization_id: "org-1", grant_id: "g-1", actor_role: "creator" },
|
|
177
|
+
[EVENTS.AGENCY_GRANT_REVOKED]: { organization_id: "org-1", grant_id: "g-1", actor_role: "creator" },
|
|
178
|
+
[EVENTS.AGENCY_ROSTER_VIEWED]: { organization_id: "org-1", actor_role: "member" },
|
|
179
|
+
[EVENTS.AGENCY_CREATOR_VIEWED]: {
|
|
180
|
+
organization_id: "org-1",
|
|
181
|
+
grant_id: "g-1",
|
|
182
|
+
actor_role: "member",
|
|
183
|
+
creator_user_id: "u-1",
|
|
184
|
+
},
|
|
185
|
+
[EVENTS.AGENCY_SPONSORSHIP_STARTED]: {
|
|
186
|
+
organization_id: "org-1",
|
|
187
|
+
grant_id: "g-1",
|
|
188
|
+
actor_role: "owner",
|
|
189
|
+
tier: "pro",
|
|
190
|
+
},
|
|
191
|
+
[EVENTS.AGENCY_SPONSORSHIP_ENDED]: { organization_id: "org-1", grant_id: "g-1", actor_role: "system" },
|
|
192
|
+
[EVENTS.AGENCY_DISCOVER_SEARCHED]: { organization_id: "org-1", actor_role: "member", matched: false },
|
|
193
|
+
[EVENTS.AGENCY_ACCESS_REQUESTED]: {
|
|
194
|
+
organization_id: "org-1",
|
|
195
|
+
grant_id: "g-1",
|
|
196
|
+
actor_role: "member",
|
|
197
|
+
origin: "agency_request",
|
|
198
|
+
},
|
|
162
199
|
};
|
|
163
200
|
|
|
201
|
+
test("no agency event may carry the discover query — only whether it matched (FR-015)", () => {
|
|
202
|
+
const agencyEvents = Object.values(EVENTS).filter((name) => name.startsWith("agency_"));
|
|
203
|
+
assert.equal(agencyEvents.length, 15, "the 175 set is 15 events");
|
|
204
|
+
for (const name of agencyEvents) {
|
|
205
|
+
const props = EVENT_PROPERTY_WITNESS[name as keyof typeof EVENT_PROPERTY_WITNESS];
|
|
206
|
+
for (const key of Object.keys(props)) {
|
|
207
|
+
assert.notEqual(key, "q", `${name} must never carry the search term`);
|
|
208
|
+
assert.notEqual(key, "query", `${name} must never carry the search term`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
164
213
|
test("EVENTS values are 1:1 with EventProperties keys (no missing or typo'd event)", () => {
|
|
165
214
|
const eventValues = Object.values(EVENTS).sort();
|
|
166
215
|
const propertyKeys = Object.keys(EVENT_PROPERTY_WITNESS).sort();
|
package/src/analytics/index.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { AccessDenialCode } from "../types/I_AccessDenial";
|
|
13
|
+
import type { SponsoredTier } from "../types/agency";
|
|
13
14
|
|
|
14
15
|
/** Canonical PostHog event names. */
|
|
15
16
|
export const EVENTS = {
|
|
@@ -167,6 +168,29 @@ export const EVENTS = {
|
|
|
167
168
|
NATIVE_SESSION_LAUNCHED: 'native_session_launched',
|
|
168
169
|
NATIVE_SESSION_RENEWED: 'native_session_renewed',
|
|
169
170
|
NATIVE_SESSION_ENDED: 'native_session_ended',
|
|
171
|
+
|
|
172
|
+
// Agency domain (175). Every one of these is captured under the acting
|
|
173
|
+
// person's own distinct id: agency-side events under the MEMBER's, with
|
|
174
|
+
// creator_user_id as a property, never under the creator's (audit F17) --
|
|
175
|
+
// otherwise an agency's activity would rewrite the creator's own timeline.
|
|
176
|
+
AGENCY_SIGNUP_SUBMITTED: 'agency_signup_submitted',
|
|
177
|
+
AGENCY_APPROVED: 'agency_approved', // server-owned (backend)
|
|
178
|
+
AGENCY_REJECTED: 'agency_rejected', // server-owned (backend)
|
|
179
|
+
AGENCY_MEMBER_INVITED: 'agency_member_invited',
|
|
180
|
+
AGENCY_INVITE_LINK_ROTATED: 'agency_invite_link_rotated',
|
|
181
|
+
AGENCY_INVITE_OPENED: 'agency_invite_opened',
|
|
182
|
+
AGENCY_CONSENT_ACCEPTED: 'agency_consent_accepted',
|
|
183
|
+
AGENCY_CONSENT_DECLINED: 'agency_consent_declined',
|
|
184
|
+
AGENCY_GRANT_REVOKED: 'agency_grant_revoked',
|
|
185
|
+
AGENCY_ROSTER_VIEWED: 'agency_roster_viewed',
|
|
186
|
+
AGENCY_CREATOR_VIEWED: 'agency_creator_viewed',
|
|
187
|
+
AGENCY_SPONSORSHIP_STARTED: 'agency_sponsorship_started',
|
|
188
|
+
AGENCY_SPONSORSHIP_ENDED: 'agency_sponsorship_ended',
|
|
189
|
+
// Carries `matched` and NEVER the query (FR-015, audit F17): the search term
|
|
190
|
+
// is the one field that would turn product analytics into a record of which
|
|
191
|
+
// creators an agency went looking for.
|
|
192
|
+
AGENCY_DISCOVER_SEARCHED: 'agency_discover_searched',
|
|
193
|
+
AGENCY_ACCESS_REQUESTED: 'agency_access_requested',
|
|
170
194
|
} as const;
|
|
171
195
|
|
|
172
196
|
export type EventName = (typeof EVENTS)[keyof typeof EVENTS];
|
|
@@ -380,6 +404,29 @@ export type PublishFailureReason =
|
|
|
380
404
|
| 'unknown'; // no error detail or unclassifiable
|
|
381
405
|
|
|
382
406
|
/** Property shape per event. Keeps emitters honest across repos. */
|
|
407
|
+
/**
|
|
408
|
+
* The shared shape every agency event carries (spec 175, contract lib-types.md).
|
|
409
|
+
*
|
|
410
|
+
* `organization_id` is mandatory because every agency event is an act BY an
|
|
411
|
+
* organization — an agency event without one cannot be attributed, and the
|
|
412
|
+
* whole point of the set is reading adoption per agency after launch (FR-018).
|
|
413
|
+
*/
|
|
414
|
+
export interface AgencyEventProperties {
|
|
415
|
+
organization_id: string;
|
|
416
|
+
/** Present once a specific consent is the subject of the act. */
|
|
417
|
+
grant_id?: string;
|
|
418
|
+
actor_role: 'owner' | 'member' | 'creator' | 'admin' | 'system';
|
|
419
|
+
origin?: 'invite_link' | 'agency_request';
|
|
420
|
+
terms_version?: string;
|
|
421
|
+
tier?: SponsoredTier;
|
|
422
|
+
/**
|
|
423
|
+
* The creator the act concerns, as a PROPERTY. Never the distinct id an
|
|
424
|
+
* agency-side event is captured under (audit F17): an agency browsing its
|
|
425
|
+
* roster must not write events into its creators' own timelines.
|
|
426
|
+
*/
|
|
427
|
+
creator_user_id?: string;
|
|
428
|
+
}
|
|
429
|
+
|
|
383
430
|
export interface EventProperties {
|
|
384
431
|
[EVENTS.LANDING_CTA_CLICKED]: {
|
|
385
432
|
location:
|
|
@@ -607,6 +654,27 @@ export interface EventProperties {
|
|
|
607
654
|
[EVENTS.NATIVE_SESSION_LAUNCHED]: { outcome: 'restored' | 'none' | 'degraded' };
|
|
608
655
|
[EVENTS.NATIVE_SESSION_RENEWED]: Record<string, never>;
|
|
609
656
|
[EVENTS.NATIVE_SESSION_ENDED]: { cause: 'rejected' | 'sign_out' };
|
|
657
|
+
|
|
658
|
+
// Agency domain (175, contract lib-types.md). One shared shape: who acted
|
|
659
|
+
// (`actor_role`), on whose behalf (`organization_id`) and against which
|
|
660
|
+
// consent (`grant_id`). `creator_user_id` is a PROPERTY of an agency-side
|
|
661
|
+
// event, never the distinct id it is captured under (audit F17).
|
|
662
|
+
[EVENTS.AGENCY_SIGNUP_SUBMITTED]: AgencyEventProperties;
|
|
663
|
+
[EVENTS.AGENCY_APPROVED]: AgencyEventProperties;
|
|
664
|
+
[EVENTS.AGENCY_REJECTED]: AgencyEventProperties;
|
|
665
|
+
[EVENTS.AGENCY_MEMBER_INVITED]: AgencyEventProperties;
|
|
666
|
+
[EVENTS.AGENCY_INVITE_LINK_ROTATED]: AgencyEventProperties;
|
|
667
|
+
[EVENTS.AGENCY_INVITE_OPENED]: AgencyEventProperties;
|
|
668
|
+
[EVENTS.AGENCY_CONSENT_ACCEPTED]: AgencyEventProperties;
|
|
669
|
+
[EVENTS.AGENCY_CONSENT_DECLINED]: AgencyEventProperties;
|
|
670
|
+
[EVENTS.AGENCY_GRANT_REVOKED]: AgencyEventProperties;
|
|
671
|
+
[EVENTS.AGENCY_ROSTER_VIEWED]: AgencyEventProperties;
|
|
672
|
+
[EVENTS.AGENCY_CREATOR_VIEWED]: AgencyEventProperties;
|
|
673
|
+
[EVENTS.AGENCY_SPONSORSHIP_STARTED]: AgencyEventProperties;
|
|
674
|
+
[EVENTS.AGENCY_SPONSORSHIP_ENDED]: AgencyEventProperties;
|
|
675
|
+
// `matched` only. The query itself is never sent (FR-015).
|
|
676
|
+
[EVENTS.AGENCY_DISCOVER_SEARCHED]: AgencyEventProperties & { matched: boolean };
|
|
677
|
+
[EVENTS.AGENCY_ACCESS_REQUESTED]: AgencyEventProperties;
|
|
610
678
|
}
|
|
611
679
|
|
|
612
680
|
/** Canonical person property keys (set via identify / $set). */
|
|
@@ -24,6 +24,64 @@ export const ACCESS_DENIAL_CODES = {
|
|
|
24
24
|
* for this one and do NOT redirect.
|
|
25
25
|
*/
|
|
26
26
|
ENTITLEMENT_PRECONDITION: "entitlement-precondition",
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The caller is not an admin.
|
|
30
|
+
*
|
|
31
|
+
* `RoleGuard` answered a 401 with Polish prose until 2026-09-21, which the
|
|
32
|
+
* frontend's session handling reads as an expired session — so a non-admin
|
|
33
|
+
* who opened an admin screen could be signed out rather than refused. A 403
|
|
34
|
+
* with a code says "you may not", which is the true statement.
|
|
35
|
+
*/
|
|
36
|
+
ADMIN_REQUIRED: "admin-required",
|
|
37
|
+
|
|
38
|
+
// ── Agency domain (spec 175) ──────────────────────────────────────────────
|
|
39
|
+
/** The caller holds no accepted membership in any organization. */
|
|
40
|
+
AGENCY_NOT_MEMBER: "agency-not-member",
|
|
41
|
+
/** Their organization is still awaiting a human decision (FR-002). */
|
|
42
|
+
AGENCY_PENDING: "agency-pending",
|
|
43
|
+
/** Their organization was rejected; the reason travels in the body. */
|
|
44
|
+
AGENCY_REJECTED: "agency-rejected",
|
|
45
|
+
/** No live subscription: the roster is dark until the plan is paid (FR-013a). */
|
|
46
|
+
AGENCY_UNPAID: "agency-unpaid",
|
|
47
|
+
/**
|
|
48
|
+
* ONE code for EVERY "cannot see this creator" case — no grant, another
|
|
49
|
+
* organization's creator, a non-existent id, a non-discoverable account.
|
|
50
|
+
*
|
|
51
|
+
* The single code IS the privacy control (FR-011, SC-005, audit F6). Distinct
|
|
52
|
+
* codes would let an agency learn that an account exists by the shape of its
|
|
53
|
+
* refusal, which is the same existence oracle the discover query is
|
|
54
|
+
* structurally built to avoid.
|
|
55
|
+
*/
|
|
56
|
+
AGENCY_NO_ACCESS: "agency-no-access",
|
|
57
|
+
/**
|
|
58
|
+
* The grant exists but does not carry the scope this read needs.
|
|
59
|
+
*
|
|
60
|
+
* Returned ONLY for a creator the caller already holds on the roster — for
|
|
61
|
+
* anyone else it would prove a grant exists, so they get AGENCY_NO_ACCESS
|
|
62
|
+
* instead (SC-012).
|
|
63
|
+
*/
|
|
64
|
+
AGENCY_SCOPE_NOT_GRANTED: "agency-scope-not-granted",
|
|
65
|
+
/** The consent screen is off in this environment (FR-020, the legal gate). */
|
|
66
|
+
AGENCY_CONSENT_DISABLED: "agency-consent-disabled",
|
|
67
|
+
/** The roster invite link expired, was rotated, or its organization is not approved. */
|
|
68
|
+
AGENCY_LINK_INVALID: "agency-link-invalid",
|
|
69
|
+
/** An admin may not decide on an organization they belong to (FR-017). */
|
|
70
|
+
AGENCY_ADMIN_SELF_APPROVAL: "agency-admin-self-approval",
|
|
71
|
+
/**
|
|
72
|
+
* A sole owner tried to leave or delete their account. Refused until they
|
|
73
|
+
* hand ownership over or close the organization explicitly (FR-004): closing
|
|
74
|
+
* carries a fan-out that cancels a subscription and ends sponsorships, and
|
|
75
|
+
* that must be a decision rather than a side effect.
|
|
76
|
+
*/
|
|
77
|
+
AGENCY_SOLE_OWNER_BLOCKED: "agency-sole-owner-blocked",
|
|
78
|
+
/** A member invitation token that is expired, already used, or revoked (FR-005). */
|
|
79
|
+
AGENCY_MEMBER_INVITE_INVALID: "agency-member-invite-invalid",
|
|
80
|
+
/**
|
|
81
|
+
* The invited or accepting person already belongs to an organization (one
|
|
82
|
+
* per person, FR-004). Names the case and NOTHING else about that account.
|
|
83
|
+
*/
|
|
84
|
+
AGENCY_ALREADY_IN_ORGANIZATION: "agency-already-in-organization",
|
|
27
85
|
} as const;
|
|
28
86
|
|
|
29
87
|
export type AccessDenialCode =
|
|
@@ -60,11 +60,35 @@ test('precondition is optional, so an older backend still sends a valid body', (
|
|
|
60
60
|
assert.equal(withIt.precondition, 'no-connection');
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
test('the denial codes
|
|
64
|
-
// FR-003
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
test('the three original denial codes keep their exact values', () => {
|
|
64
|
+
// Spec 170 FR-003 locked these because that feature added a FIELD and must
|
|
65
|
+
// not renegotiate `code`. The lock is on the VALUES, not on the set ever
|
|
66
|
+
// growing: these three strings are what shipped clients branch on, so a
|
|
67
|
+
// rename is a breaking change however harmless it looks in a diff.
|
|
68
|
+
assert.equal(ACCESS_DENIAL_CODES.BETA_REQUIRED, 'beta-required');
|
|
69
|
+
assert.equal(ACCESS_DENIAL_CODES.SUBSCRIPTION_REQUIRED, 'subscription-required');
|
|
70
|
+
assert.equal(ACCESS_DENIAL_CODES.ENTITLEMENT_PRECONDITION, 'entitlement-precondition');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('every code is a unique kebab-case string', () => {
|
|
74
|
+
const values = Object.values(ACCESS_DENIAL_CODES);
|
|
75
|
+
|
|
76
|
+
assert.equal(new Set(values).size, values.length, 'duplicate denial code');
|
|
77
|
+
for (const value of values) assert.match(value, /^[a-z]+(-[a-z]+)*$/, `not kebab-case: ${value}`);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('every AGENCY code is namespaced, so a new domain cannot shadow an old wall', () => {
|
|
81
|
+
// Spec 175 added twelve. A prefix per domain is what keeps "cannot see this
|
|
82
|
+
// creator" from ever colliding with an entitlement wall a client already
|
|
83
|
+
// handles correctly — the two want completely different behaviour.
|
|
84
|
+
// `admin-required` is deliberately NOT agency-prefixed: RoleGuard is generic
|
|
85
|
+
// and guards the legacy marketplace controllers too.
|
|
86
|
+
const agencyCodes = Object.entries(ACCESS_DENIAL_CODES).filter(([key]) =>
|
|
87
|
+
key.startsWith('AGENCY_'),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
assert.equal(agencyCodes.length, 12);
|
|
91
|
+
for (const [, value] of agencyCodes) {
|
|
92
|
+
assert.match(value, /^agency-/, `un-namespaced agency code: ${value}`);
|
|
93
|
+
}
|
|
70
94
|
});
|