@headless-lms/core 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/events.d.ts +9 -9
- package/dist/assets/index.d.ts +3 -2
- package/dist/assets/index.js +2 -1
- package/dist/assets/jobs.d.ts +15 -0
- package/dist/assets/jobs.js +30 -0
- package/dist/assets/jobs.js.map +1 -0
- package/dist/assets/ports.d.ts +4 -3
- package/dist/assets/service.d.ts +0 -1
- package/dist/assets/service.js +0 -4
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/actions.js +2 -63
- package/dist/automations/actions.js.map +1 -1
- package/dist/automations/catalog.d.ts +1 -2
- package/dist/automations/catalog.js +69 -62
- package/dist/automations/catalog.js.map +1 -1
- package/dist/automations/community-derivations.js +4 -4
- package/dist/automations/community-derivations.js.map +1 -1
- package/dist/automations/community-mail.d.ts +3 -17
- package/dist/automations/community-mail.js +1 -21
- package/dist/automations/community-mail.js.map +1 -1
- package/dist/automations/events.d.ts +0 -10
- package/dist/automations/index.d.ts +11 -4
- package/dist/automations/index.js +8 -1
- package/dist/automations/notifications/debounce.d.ts +25 -0
- package/dist/automations/notifications/debounce.js +93 -0
- package/dist/automations/notifications/debounce.js.map +1 -0
- package/dist/automations/notifications/defaults.d.ts +8 -0
- package/dist/automations/notifications/defaults.js +316 -0
- package/dist/automations/notifications/defaults.js.map +1 -0
- package/dist/automations/notifications/delivery.d.ts +28 -0
- package/dist/automations/notifications/delivery.js +88 -0
- package/dist/automations/notifications/delivery.js.map +1 -0
- package/dist/automations/notifications/processor.d.ts +64 -0
- package/dist/automations/notifications/processor.js +270 -0
- package/dist/automations/notifications/processor.js.map +1 -0
- package/dist/automations/notifications/rules/content.d.ts +6 -0
- package/dist/automations/notifications/rules/content.js +39 -0
- package/dist/automations/notifications/rules/content.js.map +1 -0
- package/dist/automations/notifications/rules/discussion.d.ts +7 -0
- package/dist/automations/notifications/rules/discussion.js +678 -0
- package/dist/automations/notifications/rules/discussion.js.map +1 -0
- package/dist/automations/notifications/rules/entitlements.d.ts +6 -0
- package/dist/automations/notifications/rules/entitlements.js +249 -0
- package/dist/automations/notifications/rules/entitlements.js.map +1 -0
- package/dist/automations/notifications/rules/index.d.ts +6 -0
- package/dist/automations/notifications/rules/index.js +15 -0
- package/dist/automations/notifications/rules/index.js.map +1 -0
- package/dist/automations/notifications/rules/organizations.d.ts +6 -0
- package/dist/automations/notifications/rules/organizations.js +28 -0
- package/dist/automations/notifications/rules/organizations.js.map +1 -0
- package/dist/automations/notifications/rules/recipients.d.ts +32 -0
- package/dist/automations/notifications/rules/recipients.js +99 -0
- package/dist/automations/notifications/rules/recipients.js.map +1 -0
- package/dist/automations/notifications/service.d.ts +27 -0
- package/dist/automations/notifications/service.js +45 -0
- package/dist/automations/notifications/service.js.map +1 -0
- package/dist/automations/notifications/types.d.ts +7 -0
- package/dist/automations/notifications/types.js +473 -0
- package/dist/automations/notifications/types.js.map +1 -0
- package/dist/automations/ports.d.ts +119 -5
- package/dist/automations/service.d.ts +10 -13
- package/dist/automations/service.js +21 -83
- package/dist/automations/service.js.map +1 -1
- package/dist/content/events.d.ts +130 -80
- package/dist/content/events.js +11 -0
- package/dist/content/events.js.map +1 -1
- package/dist/content/index.d.ts +6 -4
- package/dist/content/index.js +3 -1
- package/dist/content/model.d.ts +2 -2
- package/dist/content/notification-categories.d.ts +36 -0
- package/dist/content/notification-categories.js +64 -0
- package/dist/content/notification-categories.js.map +1 -0
- package/dist/content/ports.d.ts +25 -6
- package/dist/content/service.d.ts +4 -4
- package/dist/content/service.js +24 -8
- package/dist/content/service.js.map +1 -1
- package/dist/content/titles.d.ts +22 -0
- package/dist/content/titles.js +69 -0
- package/dist/content/titles.js.map +1 -0
- package/dist/content/types.d.ts +2 -2
- package/dist/discussion/chats.d.ts +9 -4
- package/dist/discussion/chats.js +80 -47
- package/dist/discussion/chats.js.map +1 -1
- package/dist/discussion/digests.d.ts +24 -0
- package/dist/discussion/digests.js +104 -0
- package/dist/discussion/digests.js.map +1 -0
- package/dist/discussion/events.d.ts +194 -28
- package/dist/discussion/events.js +74 -17
- package/dist/discussion/events.js.map +1 -1
- package/dist/discussion/index.d.ts +7 -2
- package/dist/discussion/index.js +6 -1
- package/dist/discussion/jobs.d.ts +16 -0
- package/dist/discussion/jobs.js +40 -0
- package/dist/discussion/jobs.js.map +1 -0
- package/dist/discussion/mentions.d.ts +11 -0
- package/dist/discussion/mentions.js +35 -0
- package/dist/discussion/mentions.js.map +1 -0
- package/dist/discussion/ports.d.ts +107 -24
- package/dist/discussion/ports.js.map +1 -1
- package/dist/discussion/posts.d.ts +18 -8
- package/dist/discussion/posts.js +207 -155
- package/dist/discussion/posts.js.map +1 -1
- package/dist/discussion/service.d.ts +17 -8
- package/dist/discussion/service.js +105 -41
- package/dist/discussion/service.js.map +1 -1
- package/dist/discussion/subscriptions.d.ts +37 -0
- package/dist/discussion/subscriptions.js +99 -0
- package/dist/discussion/subscriptions.js.map +1 -0
- package/dist/discussion/titles.d.ts +26 -0
- package/dist/discussion/titles.js +84 -0
- package/dist/discussion/titles.js.map +1 -0
- package/dist/entitlements/events.d.ts +20 -20
- package/dist/entitlements/index.d.ts +2 -1
- package/dist/entitlements/index.js +2 -1
- package/dist/entitlements/jobs.d.ts +12 -0
- package/dist/entitlements/jobs.js +18 -0
- package/dist/entitlements/jobs.js.map +1 -0
- package/dist/entitlements/ports.d.ts +7 -3
- package/dist/entitlements/service.d.ts +5 -2
- package/dist/entitlements/service.js +23 -3
- package/dist/entitlements/service.js.map +1 -1
- package/dist/identity/devices.d.ts +30 -0
- package/dist/identity/devices.js +107 -0
- package/dist/identity/devices.js.map +1 -0
- package/dist/identity/events.d.ts +2 -2
- package/dist/identity/index.d.ts +5 -4
- package/dist/identity/index.js +3 -2
- package/dist/identity/model.d.ts +2 -2
- package/dist/identity/notification-settings.d.ts +28 -0
- package/dist/identity/notification-settings.js +113 -0
- package/dist/identity/notification-settings.js.map +1 -0
- package/dist/identity/ports.d.ts +78 -54
- package/dist/integrations/events.d.ts +3 -3
- package/dist/organizations/events.d.ts +32 -32
- package/dist/organizations/index.d.ts +2 -1
- package/dist/organizations/index.js +2 -1
- package/dist/organizations/notification-categories.d.ts +32 -0
- package/dist/organizations/notification-categories.js +51 -0
- package/dist/organizations/notification-categories.js.map +1 -0
- package/dist/organizations/ports.d.ts +3 -0
- package/dist/organizations/service.d.ts +2 -0
- package/dist/organizations/service.js +7 -0
- package/dist/organizations/service.js.map +1 -1
- package/dist/progress/events.d.ts +14 -14
- package/dist/reporting/member/index.d.ts +2 -2
- package/dist/reporting/member/model.d.ts +56 -23
- package/dist/reporting/member/ports.d.ts +9 -6
- package/dist/reporting/member/service.d.ts +33 -14
- package/dist/reporting/member/service.js +138 -60
- package/dist/reporting/member/service.js.map +1 -1
- package/dist/shared/errors.d.ts +13 -1
- package/dist/shared/errors.js +18 -1
- package/dist/shared/errors.js.map +1 -1
- package/dist/shared/id.d.ts +3 -0
- package/dist/shared/id.js +4 -1
- package/dist/shared/id.js.map +1 -1
- package/dist/shared/mailer.d.ts +11 -1
- package/dist/shared/mailer.js +32 -5
- package/dist/shared/mailer.js.map +1 -1
- package/dist/shared/notification-categories.d.ts +16 -0
- package/dist/shared/notification-categories.js +94 -0
- package/dist/shared/notification-categories.js.map +1 -0
- package/dist/shared/notification-settings.d.ts +14 -0
- package/dist/shared/notification-settings.js +61 -0
- package/dist/shared/notification-settings.js.map +1 -0
- package/dist/shared/ports.d.ts +48 -2
- package/dist/shared/settings.d.ts +2 -1
- package/dist/shared/settings.js +1 -0
- package/dist/shared/settings.js.map +1 -1
- package/dist/types/automations.d.ts +8 -2
- package/dist/types/content.d.ts +2 -2
- package/dist/types/discussion.d.ts +2 -2
- package/dist/types/email-templates.d.ts +97 -3
- package/dist/types/identity.d.ts +2 -2
- package/dist/types/index.d.ts +7 -6
- package/dist/types/notifications.d.ts +157 -0
- package/dist/types/notifications.js +1 -0
- package/dist/types/schemas/automations.d.ts +1 -10
- package/dist/types/schemas/automations.js +1 -3
- package/dist/types/schemas/automations.js.map +1 -1
- package/dist/types/schemas/content.d.ts +62 -1
- package/dist/types/schemas/content.js +25 -3
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +85 -12
- package/dist/types/schemas/discussion.js +47 -13
- package/dist/types/schemas/discussion.js.map +1 -1
- package/dist/types/schemas/identity.d.ts +1 -85
- package/dist/types/schemas/identity.js +1 -42
- package/dist/types/schemas/identity.js.map +1 -1
- package/dist/types/schemas/index.d.ts +6 -5
- package/dist/types/schemas/index.js +6 -5
- package/dist/types/schemas/notifications.d.ts +95 -0
- package/dist/types/schemas/notifications.js +56 -0
- package/dist/types/schemas/notifications.js.map +1 -0
- package/dist/types/schemas/progress.d.ts +3 -3
- package/package.json +1 -1
- package/dist/automations/community-notifications.d.ts +0 -16
- package/dist/automations/community-notifications.js +0 -84
- package/dist/automations/community-notifications.js.map +0 -1
- package/dist/automations/seeds.d.ts +0 -17
- package/dist/automations/seeds.js +0 -235
- package/dist/automations/seeds.js.map +0 -1
- package/dist/identity/notifications.d.ts +0 -27
- package/dist/identity/notifications.js +0 -101
- package/dist/identity/notifications.js.map +0 -1
|
@@ -5,4 +5,5 @@ import { AccessFacts, CommunityAccessReader, EndingGrant, EndingGrantKey, Entitl
|
|
|
5
5
|
import { EntitlementsServiceImpl } from "./service.js";
|
|
6
6
|
import { EntitlementCreated, EntitlementDeleted, EntitlementEndingSoon, EntitlementEvent, EntitlementExpired, EntitlementUpdated, SpaceMemberCreated, SpaceMemberDeleted, SpaceMemberUpdated, entitlementEvents } from "./events.js";
|
|
7
7
|
import { seesSpace } from "./rules.js";
|
|
8
|
-
|
|
8
|
+
import { GRANTS_EXPIRE_JOB, entitlementsJobs, entitlementsRecurringJobs } from "./jobs.js";
|
|
9
|
+
export { type AccessFacts, type CommunityAccessReader, type EndingGrant, type EndingGrantKey, type Entitlement, type EntitlementCreated, type EntitlementDeleted, type EntitlementEndingSoon, type EntitlementEvent, type EntitlementExpired, type EntitlementNotice, type EntitlementStatus, type EntitlementUpdated, type EntitlementsQuery, type EntitlementsRepository, type EntitlementsService, EntitlementsServiceImpl, GRANTS_EXPIRE_JOB, type GrantEntitlementInput, type GrantKey, type GrantTarget, type InsertSpaceMemberInput, type OpenableSpace, type Page, type SpaceAccessRule, type SpaceMember, type SpaceMemberCreated, type SpaceMemberDeleted, type SpaceMemberSource, type SpaceMemberStatus, type SpaceMemberUpdated, type SpaceMembersQuery, entitlementEvents, entitlementsJobs, entitlementsRecurringJobs, seesSpace };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { entitlementEvents } from "./events.js";
|
|
2
2
|
import { seesSpace } from "./rules.js";
|
|
3
3
|
import { EntitlementsServiceImpl } from "./service.js";
|
|
4
|
-
|
|
4
|
+
import { GRANTS_EXPIRE_JOB, entitlementsJobs, entitlementsRecurringJobs } from "./jobs.js";
|
|
5
|
+
export { EntitlementsServiceImpl, GRANTS_EXPIRE_JOB, entitlementEvents, entitlementsJobs, entitlementsRecurringJobs, seesSpace };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { RecurringJob, ScheduledJob } from "../shared/ports.js";
|
|
2
|
+
import { EntitlementsService } from "./ports.js";
|
|
3
|
+
//#region src/entitlements/jobs.d.ts
|
|
4
|
+
declare const GRANTS_EXPIRE_JOB = "entitlements.grants.expire";
|
|
5
|
+
interface EntitlementsJobDeps {
|
|
6
|
+
grants: Pick<EntitlementsService, 'expireDueGrants'>;
|
|
7
|
+
}
|
|
8
|
+
declare function entitlementsJobs(deps: EntitlementsJobDeps): ScheduledJob[];
|
|
9
|
+
declare const entitlementsRecurringJobs: RecurringJob[];
|
|
10
|
+
//#endregion
|
|
11
|
+
export { EntitlementsJobDeps, GRANTS_EXPIRE_JOB, entitlementsJobs, entitlementsRecurringJobs };
|
|
12
|
+
//# sourceMappingURL=jobs.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/entitlements/jobs.ts
|
|
2
|
+
const GRANTS_EXPIRE_JOB = "entitlements.grants.expire";
|
|
3
|
+
function entitlementsJobs(deps) {
|
|
4
|
+
return [{
|
|
5
|
+
job: GRANTS_EXPIRE_JOB,
|
|
6
|
+
async run(_payload, fire) {
|
|
7
|
+
await deps.grants.expireDueGrants(fire.startedAt);
|
|
8
|
+
}
|
|
9
|
+
}];
|
|
10
|
+
}
|
|
11
|
+
const entitlementsRecurringJobs = [{
|
|
12
|
+
job: GRANTS_EXPIRE_JOB,
|
|
13
|
+
cron: "0 * * * *"
|
|
14
|
+
}];
|
|
15
|
+
//#endregion
|
|
16
|
+
export { GRANTS_EXPIRE_JOB, entitlementsJobs, entitlementsRecurringJobs };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=jobs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jobs.js","names":[],"sources":["../../src/entitlements/jobs.ts"],"sourcesContent":["import type { RecurringJob, ScheduledJob } from '../shared/ports.js';\nimport type { EntitlementsService } from './ports.js';\n\nexport const GRANTS_EXPIRE_JOB = 'entitlements.grants.expire';\n\nexport interface EntitlementsJobDeps {\n grants: Pick<EntitlementsService, 'expireDueGrants'>;\n}\n\nexport function entitlementsJobs(deps: EntitlementsJobDeps): ScheduledJob[] {\n return [\n {\n job: GRANTS_EXPIRE_JOB,\n async run(_payload, fire) {\n await deps.grants.expireDueGrants(fire.startedAt);\n },\n },\n ];\n}\n\nexport const entitlementsRecurringJobs: RecurringJob[] = [\n { job: GRANTS_EXPIRE_JOB, cron: '0 * * * *' },\n];\n"],"mappings":";AAGA,MAAa,oBAAoB;AAMjC,SAAgB,iBAAiB,MAA2C;CAC1E,OAAO,CACL;EACE,KAAK;EACL,MAAM,IAAI,UAAU,MAAM;GACxB,MAAM,KAAK,OAAO,gBAAgB,KAAK,SAAS;EAClD;CACF,CACF;AACF;AAEA,MAAa,4BAA4C,CACvD;CAAE,KAAK;CAAmB,MAAM;AAAY,CAC9C"}
|
|
@@ -95,11 +95,13 @@ interface EntitlementsService extends CommunityAccessReader {
|
|
|
95
95
|
page: number;
|
|
96
96
|
pageSize: number;
|
|
97
97
|
}): Promise<Page<SpaceMember>>;
|
|
98
|
-
|
|
99
|
-
hasDueNotices(orgId: string, now: Date): Promise<boolean>;
|
|
98
|
+
expireDueGrants(now: Date): Promise<void>;
|
|
100
99
|
communitiesOpenedBy(orgId: string, grant: GrantTarget): Promise<string[]>;
|
|
101
100
|
communityModeratorIds(orgId: string, communityId: string): Promise<string[]>;
|
|
102
101
|
communityIdsOpenedFor(orgId: string, orgUserId: string): Promise<string[]>;
|
|
102
|
+
openCommunityIds(orgId: string, actor: Actor): Promise<string[]>;
|
|
103
|
+
communityMemberIds(orgId: string, communityId: string): Promise<string[]>;
|
|
104
|
+
contentHolderIds(orgId: string, contentId: string): Promise<string[]>;
|
|
103
105
|
countActiveSpaceMembers(orgId: string, spaceId: string): Promise<number>;
|
|
104
106
|
}
|
|
105
107
|
interface EntitlementsRepository {
|
|
@@ -127,11 +129,13 @@ interface EntitlementsRepository {
|
|
|
127
129
|
listGrantsExpiredBy(orgId: string, now: Date, limit: number, afterId?: string): Promise<Entitlement[]>;
|
|
128
130
|
/** Grants ending in the window with no notice, in (grant id, community id) order, after `afterKey`. */
|
|
129
131
|
listGrantsEndingBetween(orgId: string, after: Date, until: Date, limit: number, afterKey?: EndingGrantKey): Promise<EndingGrant[]>;
|
|
130
|
-
|
|
132
|
+
orgIdsWithDueNotices(now: Date, until: Date): Promise<string[]>;
|
|
131
133
|
claimNotice(orgId: string, notice: EntitlementNotice, now: Date): Promise<boolean>;
|
|
132
134
|
communityModeratorIds(orgId: string, communityId: string): Promise<string[]>;
|
|
133
135
|
communityMemberIds(orgId: string, communityId: string): Promise<string[]>;
|
|
134
136
|
communityIdsOpenedFor(orgId: string, orgUserId: string): Promise<string[]>;
|
|
137
|
+
openCommunityIds(orgId: string, orgUserId: string): Promise<string[]>;
|
|
138
|
+
contentHolderIds(orgId: string, contentId: string): Promise<string[]>;
|
|
135
139
|
}
|
|
136
140
|
/** Tx-scoped port bundle for this context's mutating use cases — every member
|
|
137
141
|
* is bound to the UnitOfWork's transaction, so the write and the event append
|
|
@@ -41,6 +41,9 @@ declare class EntitlementsServiceImpl implements EntitlementsService {
|
|
|
41
41
|
communitiesOpenedBy(orgId: string, grant: GrantTarget): Promise<string[]>;
|
|
42
42
|
communityModeratorIds(orgId: string, communityId: string): Promise<string[]>;
|
|
43
43
|
communityIdsOpenedFor(orgId: string, orgUserId: string): Promise<string[]>;
|
|
44
|
+
openCommunityIds(orgId: string, actor: Actor): Promise<string[]>;
|
|
45
|
+
communityMemberIds(orgId: string, communityId: string): Promise<string[]>;
|
|
46
|
+
contentHolderIds(orgId: string, contentId: string): Promise<string[]>;
|
|
44
47
|
countActiveSpaceMembers(orgId: string, spaceId: string): Promise<number>;
|
|
45
48
|
requestJoin(orgId: string, actor: Actor, communityId: string, spaceId: string): Promise<SpaceMember>;
|
|
46
49
|
invite(orgId: string, actor: Actor, communityId: string, spaceId: string, orgUserId: string): Promise<SpaceMember>;
|
|
@@ -55,8 +58,8 @@ declare class EntitlementsServiceImpl implements EntitlementsService {
|
|
|
55
58
|
page: number;
|
|
56
59
|
pageSize: number;
|
|
57
60
|
}): Promise<Page<SpaceMember>>;
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
expireDueGrants(now: Date): Promise<void>;
|
|
62
|
+
private expireDueIn;
|
|
60
63
|
private requireSpaceIn;
|
|
61
64
|
private requireVisibleSpaceIn;
|
|
62
65
|
private assertHasMembers;
|
|
@@ -160,6 +160,15 @@ var EntitlementsServiceImpl = class {
|
|
|
160
160
|
communityIdsOpenedFor(orgId, orgUserId) {
|
|
161
161
|
return this.repo.communityIdsOpenedFor(orgId, orgUserId);
|
|
162
162
|
}
|
|
163
|
+
openCommunityIds(orgId, actor) {
|
|
164
|
+
return this.repo.openCommunityIds(orgId, actor.id);
|
|
165
|
+
}
|
|
166
|
+
communityMemberIds(orgId, communityId) {
|
|
167
|
+
return this.repo.communityMemberIds(orgId, communityId);
|
|
168
|
+
}
|
|
169
|
+
contentHolderIds(orgId, contentId) {
|
|
170
|
+
return this.repo.contentHolderIds(orgId, contentId);
|
|
171
|
+
}
|
|
163
172
|
countActiveSpaceMembers(orgId, spaceId) {
|
|
164
173
|
return this.repo.countActiveSpaceMembers(orgId, spaceId);
|
|
165
174
|
}
|
|
@@ -299,10 +308,21 @@ var EntitlementsServiceImpl = class {
|
|
|
299
308
|
...canModerate(actor.role) ? {} : { statuses: ["active"] }
|
|
300
309
|
});
|
|
301
310
|
}
|
|
302
|
-
|
|
303
|
-
|
|
311
|
+
async expireDueGrants(now) {
|
|
312
|
+
const orgIds = await this.repo.orgIdsWithDueNotices(now, new Date(now.getTime() + ENDING_SOON_MS));
|
|
313
|
+
let failed = 0;
|
|
314
|
+
for (const orgId of orgIds) try {
|
|
315
|
+
await this.expireDueIn(orgId, now);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
failed += 1;
|
|
318
|
+
this.logger.error("due grants not expired", {
|
|
319
|
+
orgId,
|
|
320
|
+
err
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
if (failed > 0) throw new Error(`due grants of ${failed} orgs could not be expired`);
|
|
304
324
|
}
|
|
305
|
-
async
|
|
325
|
+
async expireDueIn(orgId, now) {
|
|
306
326
|
const until = new Date(now.getTime() + ENDING_SOON_MS);
|
|
307
327
|
let appended = 0;
|
|
308
328
|
let afterId;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":[],"sources":["../../src/entitlements/service.ts"],"sourcesContent":["// entitlements context — service implementation (inbound port).\n//\n// Mutations run inside the context's UnitOfWork: the domain write and the\n// outbox append commit in ONE transaction (transactional outbox). This\n// service never publishes — the outbox relay republishes committed events to\n// the EventBus at-least-once.\nimport type { CommunityReader, Space, SpaceRowLock } from '../content/index.js';\nimport type {\n ContentType,\n Entitlement,\n EntitlementsQuery,\n GrantEntitlementInput,\n Page,\n SpaceMember,\n} from './model.js';\nimport type {\n AccessFacts,\n EntitlementsRepository,\n EntitlementsService,\n EntitlementsUnitOfWork,\n EndingGrantKey,\n GrantTarget,\n InsertSpaceMemberInput,\n OpenableSpace,\n SpaceAccessRule,\n} from './ports.js';\nimport type { Actor } from '../shared/actor.js';\nimport type { Logger, NewDomainEvent, OutboxAppender } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { ConflictError, ForbiddenError, NotFoundError } from '../shared/errors.js';\nimport { canModerate, isStaffRole } from '../organizations/index.js';\nimport { entitlementEvents } from './events.js';\nimport { laterExpiry, opensSpace, seesSpace } from './rules.js';\n\nconst ENDING_SOON_MS = 7 * 24 * 60 * 60 * 1000;\nconst NOTICE_BATCH_SIZE = 100;\n\nexport type EntitlementsServiceParams = {\n /** Read-only access (list) — runs outside any transaction. */\n repo: EntitlementsRepository;\n content: CommunityReader;\n /** Atomic write scope: tx-bound repo + outbox appender. */\n uow: EntitlementsUnitOfWork;\n logger?: Logger;\n};\n\nexport class EntitlementsServiceImpl implements EntitlementsService {\n private readonly repo: EntitlementsRepository;\n private readonly content: CommunityReader;\n private readonly uow: EntitlementsUnitOfWork;\n private readonly logger: Logger;\n\n constructor(params: EntitlementsServiceParams) {\n this.repo = params.repo;\n this.content = params.content;\n this.uow = params.uow;\n this.logger = params.logger ?? noopLogger;\n }\n\n list(orgId: string, query: EntitlementsQuery): Promise<Page<Entitlement>> {\n return this.repo.list(orgId, query);\n }\n\n async grant(orgId: string, input: GrantEntitlementInput): Promise<Entitlement> {\n const entitlement = await this.uow.run(async ({ entitlements, outbox }) => {\n const created = await entitlements.insert(orgId, input);\n await outbox.append([\n entitlementEvents.entitlementCreated.make({\n orgId,\n data: created,\n }),\n ]);\n return created;\n });\n this.logger.info('entitlement granted', {\n orgId,\n entitlementId: entitlement.id,\n orgUserId: entitlement.orgUserId,\n bundleId: entitlement.bundleId,\n contentId: entitlement.contentId,\n });\n return entitlement;\n }\n\n async ensureGrant(orgId: string, input: GrantEntitlementInput): Promise<Entitlement> {\n const { entitlement, change } = await this.uow.run(async ({ entitlements, outbox }) => {\n const created = await entitlements.insertIfAbsent(orgId, input);\n if (created) {\n await outbox.append([entitlementEvents.entitlementCreated.make({ orgId, data: created })]);\n return { entitlement: created, change: 'created' as const };\n }\n const existing = await entitlements.lockGrant(orgId, input);\n if (!existing) {\n throw new ConflictError('The grant changed while it was being given');\n }\n if (existing.status === 'revoked') {\n return { entitlement: existing, change: null };\n }\n const expiresAt = laterExpiry(existing.expiresAt, input.expiresAt ?? null);\n if (expiresAt?.getTime() === existing.expiresAt?.getTime()) {\n return { entitlement: existing, change: null };\n }\n const updated = await entitlements.setExpiresAt(orgId, existing.id, expiresAt);\n if (!updated) {\n throw new NotFoundError('Entitlement', existing.id);\n }\n await outbox.append([entitlementEvents.entitlementUpdated.make({ orgId, data: updated })]);\n return { entitlement: updated, change: 'updated' as const };\n });\n if (change) {\n this.logger.info(change === 'created' ? 'entitlement granted' : 'entitlement extended', {\n orgId,\n entitlementId: entitlement.id,\n orgUserId: entitlement.orgUserId,\n bundleId: entitlement.bundleId,\n contentId: entitlement.contentId,\n });\n }\n return entitlement;\n }\n\n async setStatus(\n orgId: string,\n id: string,\n status: 'active' | 'revoked',\n ): Promise<Entitlement | null> {\n const entitlement = await this.uow.run(async ({ entitlements, outbox }) => {\n const updated = await entitlements.setStatus(orgId, id, status);\n if (!updated) {\n return null;\n }\n await outbox.append([\n status === 'revoked'\n ? entitlementEvents.entitlementDeleted.make({\n orgId,\n data: updated,\n })\n : entitlementEvents.entitlementUpdated.make({\n orgId,\n data: updated,\n }),\n ]);\n return updated;\n });\n if (entitlement) {\n this.logger.info('entitlement status changed', { orgId, entitlementId: id, status });\n }\n return entitlement;\n }\n\n hasAccess(orgId: string, orgUserId: string, contentId: string): Promise<boolean> {\n return this.repo.hasAccess(orgId, orgUserId, contentId);\n }\n\n listAccessibleContent(orgId: string, orgUserId: string, type?: ContentType): Promise<string[]> {\n return this.repo.listAccessibleContent(orgId, orgUserId, type);\n }\n\n async isCommunityMember(orgId: string, actor: Actor, communityId: string): Promise<boolean> {\n const facts = await this.accessOf(orgId, actor.id, communityId, null);\n return facts?.member === true;\n }\n\n async canOpenSpace(orgId: string, actor: Actor, spaceId: string): Promise<boolean> {\n const space = await this.content.getSpace(orgId, spaceId);\n if (!space) {\n return false;\n }\n return this.canOpen(orgId, actor, space);\n }\n\n async canOpen(orgId: string, actor: Actor, space: OpenableSpace): Promise<boolean> {\n const facts = await this.accessOf(orgId, actor.id, space.communityId, space.id);\n if (!facts) {\n return false;\n }\n return opensSpace(facts.role, facts.member, space.accessRule, facts.activeInSpace);\n }\n\n async spaceStandings(\n orgId: string,\n actor: Actor,\n communityId: string,\n ): Promise<Record<string, SpaceMember['status']>> {\n const rows = await this.repo.listSpaceMembersFor(orgId, actor.id, communityId);\n return Object.fromEntries(rows.map((row) => [row.spaceId, row.status]));\n }\n\n async openSpacesFor(orgId: string, actor: Actor, communityId: string): Promise<Space[]> {\n const facts = await this.accessOf(orgId, actor.id, communityId, null);\n if (!facts?.member) {\n return [];\n }\n const [spaces, activeIds] = await Promise.all([\n this.content.listSpaces(orgId, communityId),\n this.repo.activeSpaceIds(orgId, actor.id, communityId),\n ]);\n const active = new Set(activeIds);\n return spaces.filter((space) =>\n opensSpace(facts.role, facts.member, space.accessRule, active.has(space.id)),\n );\n }\n\n async openSpaceIds(orgId: string, actor: Actor, communityId: string): Promise<string[]> {\n const spaces = await this.openSpacesFor(orgId, actor, communityId);\n return spaces.map((space) => space.id);\n }\n\n async communityMembersAmong(\n orgId: string,\n communityId: string,\n orgUserIds: string[],\n ): Promise<string[]> {\n if (orgUserIds.length === 0) {\n return [];\n }\n const facts = await this.repo.accessAmong(orgId, communityId, null, orgUserIds);\n return facts.filter((fact) => fact.member).map((fact) => fact.orgUserId);\n }\n\n async spaceOpenersAmong(orgId: string, spaceId: string, orgUserIds: string[]): Promise<string[]> {\n if (orgUserIds.length === 0) {\n return [];\n }\n const space = await this.content.getSpace(orgId, spaceId);\n if (!space) {\n return [];\n }\n const facts = await this.repo.accessAmong(orgId, space.communityId, space.id, orgUserIds);\n return facts\n .filter((fact) => opensSpace(fact.role, fact.member, space.accessRule, fact.activeInSpace))\n .map((fact) => fact.orgUserId);\n }\n\n async spaceOpeners(orgId: string, spaceId: string): Promise<string[]> {\n const space = await this.content.getSpace(orgId, spaceId);\n if (!space) {\n return [];\n }\n const members = await this.repo.communityMemberIds(orgId, space.communityId);\n return this.spaceOpenersAmong(orgId, spaceId, members);\n }\n\n communitiesOpenedBy(orgId: string, grant: GrantTarget): Promise<string[]> {\n return this.repo.communitiesOpenedBy(orgId, grant);\n }\n\n communityModeratorIds(orgId: string, communityId: string): Promise<string[]> {\n return this.repo.communityModeratorIds(orgId, communityId);\n }\n\n communityIdsOpenedFor(orgId: string, orgUserId: string): Promise<string[]> {\n return this.repo.communityIdsOpenedFor(orgId, orgUserId);\n }\n\n countActiveSpaceMembers(orgId: string, spaceId: string): Promise<number> {\n return this.repo.countActiveSpaceMembers(orgId, spaceId);\n }\n\n async requestJoin(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n ): Promise<SpaceMember> {\n const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);\n this.assertRule(space.accessRule, 'by_request');\n if (canModerate(actor.role)) {\n throw new ForbiddenError('Staff do not request a place in a space');\n }\n await this.assertGrant(orgId, actor, actor.id, space.communityId);\n const created = await this.createMember(orgId, actor, space, actor.id, 'requested', 'request');\n this.logger.info('space member created', {\n orgId,\n spaceId,\n orgUserId: actor.id,\n status: created.status,\n actorId: actor.id,\n });\n return created;\n }\n\n async invite(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n orgUserId: string,\n ): Promise<SpaceMember> {\n const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);\n this.assertRule(space.accessRule, 'by_invite');\n await this.assertCanManage(orgId, actor, space.communityId);\n await this.assertHolds(orgId, actor, orgUserId, space.communityId);\n const created = await this.createMember(orgId, actor, space, orgUserId, 'invited', 'invite');\n this.logger.info('space member created', {\n orgId,\n spaceId,\n orgUserId,\n status: created.status,\n actorId: actor.id,\n });\n return created;\n }\n\n async addSpaceMember(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n orgUserId: string,\n ): Promise<SpaceMember> {\n const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);\n this.assertHasMembers(space.accessRule);\n await this.assertCanManage(orgId, actor, space.communityId);\n await this.assertHolds(orgId, actor, orgUserId, space.communityId);\n const created = await this.uow.run(async (scope) => {\n const locked = await this.lockSpace(scope.spaces, orgId, spaceId);\n this.assertHasMembers(locked.accessRule);\n return this.insertMember(\n scope.entitlements,\n scope.outbox,\n orgId,\n actor,\n {\n communityId: space.communityId,\n spaceId,\n orgUserId,\n status: 'active',\n source: 'manual',\n },\n locked,\n );\n });\n this.logger.info('space member created', {\n orgId,\n spaceId,\n orgUserId,\n status: created.status,\n actorId: actor.id,\n });\n return created;\n }\n\n async accept(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n ): Promise<SpaceMember> {\n this.assertHolder(actor);\n const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);\n this.assertRule(space.accessRule, 'by_invite');\n await this.assertGrant(orgId, actor, actor.id, space.communityId);\n return this.activate(orgId, actor, spaceId, actor.id, 'by_invite', 'invited', () =>\n ownRowMissing('invited'),\n );\n }\n\n async approve(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n orgUserId: string,\n ): Promise<SpaceMember> {\n const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);\n this.assertRule(space.accessRule, 'by_request');\n await this.assertCanManage(orgId, actor, space.communityId);\n await this.assertGrant(orgId, actor, orgUserId, space.communityId);\n return this.activate(\n orgId,\n actor,\n spaceId,\n orgUserId,\n 'by_request',\n 'requested',\n () => new NotFoundError('Space member', orgUserId),\n );\n }\n\n async decline(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n orgUserId: string,\n ): Promise<SpaceMember> {\n const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);\n await this.assertCanManage(orgId, actor, space.communityId);\n return this.deleteMember(\n orgId,\n actor,\n spaceId,\n orgUserId,\n 'requested',\n () => new NotFoundError('Space member', orgUserId),\n );\n }\n\n async refuse(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n ): Promise<SpaceMember> {\n const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);\n this.assertHolder(actor);\n await this.assertGrant(orgId, actor, actor.id, space.communityId);\n return this.deleteMember(orgId, actor, spaceId, actor.id, 'invited', () =>\n ownRowMissing('invited'),\n );\n }\n\n async leave(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n ): Promise<SpaceMember> {\n const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);\n this.assertHolder(actor);\n await this.assertGrant(orgId, actor, actor.id, space.communityId);\n return this.deleteMember(orgId, actor, spaceId, actor.id, 'active', () =>\n ownRowMissing('active'),\n );\n }\n\n async remove(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n orgUserId: string,\n ): Promise<SpaceMember> {\n const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);\n this.assertHasMembers(space.accessRule);\n await this.assertCanManage(orgId, actor, space.communityId);\n const { updated, previousStatus } = await this.uow.run(\n async ({ entitlements, spaces, outbox }) => {\n const locked = await this.lockSpace(spaces, orgId, spaceId);\n this.assertHasMembers(locked.accessRule);\n const row = await entitlements.lockSpaceMember(orgId, spaceId, orgUserId);\n if (!row) {\n this.logger.warn('space member remove rejected — not found', {\n orgId,\n spaceId,\n orgUserId,\n actorId: actor.id,\n });\n throw new NotFoundError('Space member', orgUserId);\n }\n if (row.status === 'revoked') {\n throw new ConflictError('This person is already removed from the space');\n }\n const next = await entitlements.updateSpaceMemberStatus(orgId, row.id, 'revoked');\n if (!next) {\n throw new NotFoundError('Space member', row.id);\n }\n await outbox.append([\n entitlementEvents.spaceMemberUpdated.make({\n orgId,\n data: next,\n metadata: { actorOrgUserId: actor.id, previousStatus: row.status },\n }),\n ]);\n return { updated: next, previousStatus: row.status };\n },\n );\n this.logger.info('space member removed', {\n orgId,\n spaceId,\n orgUserId,\n previousStatus,\n actorId: actor.id,\n });\n return updated;\n }\n\n async listSpaceMembers(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n query: { page: number; pageSize: number },\n ): Promise<Page<SpaceMember>> {\n const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);\n if (!(await this.canOpen(orgId, actor, space))) {\n throw new NotFoundError('Space', spaceId);\n }\n return this.repo.listSpaceMembers(orgId, spaceId, {\n ...query,\n ...(canModerate(actor.role) ? {} : { statuses: ['active'] }),\n });\n }\n\n hasDueNotices(orgId: string, now: Date): Promise<boolean> {\n return this.repo.hasDueNotices(orgId, now, new Date(now.getTime() + ENDING_SOON_MS));\n }\n\n async expireDue(orgId: string, now: Date): Promise<void> {\n const until = new Date(now.getTime() + ENDING_SOON_MS);\n let appended = 0;\n let afterId: string | undefined;\n for (;;) {\n const batch = await this.uow.run(async ({ entitlements, outbox }) => {\n const events: NewDomainEvent[] = [];\n const grants = await entitlements.listGrantsExpiredBy(\n orgId,\n now,\n NOTICE_BATCH_SIZE,\n afterId,\n );\n for (const grant of grants) {\n if (!grant.expiresAt) {\n continue;\n }\n const claimed = await entitlements.claimNotice(\n orgId,\n { kind: 'expired', entitlementId: grant.id, expiresAt: grant.expiresAt },\n now,\n );\n if (claimed) {\n events.push(entitlementEvents.entitlementExpired.make({ orgId, data: grant }));\n }\n }\n if (events.length > 0) {\n await outbox.append(events);\n }\n return { events: events.length, last: grants.at(-1)?.id, size: grants.length };\n });\n appended += batch.events;\n if (batch.size < NOTICE_BATCH_SIZE || batch.last === undefined) {\n break;\n }\n afterId = batch.last;\n }\n let afterKey: EndingGrantKey | undefined;\n for (;;) {\n const batch = await this.uow.run(async ({ entitlements, outbox }) => {\n const events: NewDomainEvent[] = [];\n const ending = await entitlements.listGrantsEndingBetween(\n orgId,\n now,\n until,\n NOTICE_BATCH_SIZE,\n afterKey,\n );\n for (const { grant, communityId } of ending) {\n if (!grant.expiresAt) {\n continue;\n }\n const claimed = await entitlements.claimNotice(\n orgId,\n {\n kind: 'ending_soon',\n entitlementId: grant.id,\n communityId,\n expiresAt: grant.expiresAt,\n },\n now,\n );\n if (claimed) {\n events.push(\n entitlementEvents.entitlementEndingSoon.make({\n orgId,\n data: { ...grant, communityId },\n }),\n );\n }\n }\n if (events.length > 0) {\n await outbox.append(events);\n }\n const last = ending.at(-1);\n return {\n events: events.length,\n last: last && { entitlementId: last.grant.id, communityId: last.communityId },\n size: ending.length,\n };\n });\n appended += batch.events;\n if (batch.size < NOTICE_BATCH_SIZE || batch.last === undefined) {\n break;\n }\n afterKey = batch.last;\n }\n if (appended > 0) {\n this.logger.info('due grants expired', { orgId, events: appended });\n }\n }\n\n private async requireSpaceIn(orgId: string, communityId: string, spaceId: string) {\n const space = await this.content.getSpace(orgId, spaceId);\n if (!space || space.communityId !== communityId) {\n this.logger.warn('space members rejected — space not found', {\n orgId,\n communityId,\n spaceId,\n });\n throw new NotFoundError('Space', spaceId);\n }\n return space;\n }\n\n private async requireVisibleSpaceIn(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n ) {\n const space = await this.requireSpaceIn(orgId, communityId, spaceId);\n if (seesSpace(actor.role, space.accessRule, undefined)) {\n return space;\n }\n const row = await this.repo.findSpaceMember(orgId, spaceId, actor.id);\n if (!seesSpace(actor.role, space.accessRule, row?.status)) {\n throw new NotFoundError('Space', spaceId);\n }\n return space;\n }\n\n private assertHasMembers(accessRule: SpaceAccessRule): void {\n if (accessRule === 'all_members') {\n throw new ConflictError('An all-members space has no space members');\n }\n }\n\n private assertRule(accessRule: SpaceAccessRule, expected: 'by_request' | 'by_invite'): void {\n this.assertHasMembers(accessRule);\n if (accessRule !== expected) {\n throw new ConflictError(\n accessRule === 'by_invite' ? 'This space is by invitation' : 'This space is by request',\n );\n }\n }\n\n private assertHolder(actor: Actor): void {\n if (isStaffRole(actor.role)) {\n throw new ForbiddenError('Staff do not hold a place in a space');\n }\n }\n\n private async assertGrant(\n orgId: string,\n actor: Actor,\n orgUserId: string,\n communityId: string,\n ): Promise<void> {\n const member = await this.repo.hasCommunityAccess(orgId, orgUserId, communityId);\n if (!member) {\n throw new ForbiddenError(\n orgUserId === actor.id\n ? 'You are not a member of this community'\n : 'That person is not a member of this community',\n );\n }\n }\n\n private async assertHolds(\n orgId: string,\n actor: Actor,\n orgUserId: string,\n communityId: string,\n ): Promise<void> {\n const facts = await this.accessOf(orgId, orgUserId, communityId, null);\n if (facts && isStaffRole(facts.role)) {\n throw new ForbiddenError('Staff do not hold a place in a space');\n }\n await this.assertGrant(orgId, actor, orgUserId, communityId);\n }\n\n private async assertCanManage(orgId: string, actor: Actor, communityId: string): Promise<void> {\n const facts = await this.accessOf(orgId, actor.id, communityId, null);\n if (facts?.member && canModerate(facts.role)) {\n return;\n }\n throw new ForbiddenError('You cannot manage members of this space');\n }\n\n private async accessOf(\n orgId: string,\n orgUserId: string,\n communityId: string,\n spaceId: string | null,\n ): Promise<AccessFacts | null> {\n const [facts] = await this.repo.accessAmong(orgId, communityId, spaceId, [orgUserId]);\n return facts ?? null;\n }\n\n private async createMember(\n orgId: string,\n actor: Actor,\n space: { communityId: string; id: string },\n orgUserId: string,\n status: InsertSpaceMemberInput['status'],\n source: InsertSpaceMemberInput['source'],\n ): Promise<SpaceMember> {\n return this.uow.run(async ({ entitlements, outbox }) =>\n this.insertMember(entitlements, outbox, orgId, actor, {\n communityId: space.communityId,\n spaceId: space.id,\n orgUserId,\n status,\n source,\n }),\n );\n }\n\n private async insertMember(\n entitlements: EntitlementsRepository,\n outbox: OutboxAppender,\n orgId: string,\n actor: Actor,\n input: InsertSpaceMemberInput,\n room?: Space,\n ): Promise<SpaceMember> {\n const existing = await entitlements.lockSpaceMember(orgId, input.spaceId, input.orgUserId);\n if (existing && existing.status !== 'revoked') {\n throw new ConflictError(\n input.orgUserId === actor.id\n ? 'You already have a place in this space'\n : 'That person already has a place in this space',\n );\n }\n if (room) {\n await this.assertRoom(entitlements, orgId, room);\n }\n if (existing) {\n await entitlements.deleteSpaceMember(orgId, existing.id);\n }\n const created = await entitlements.insertSpaceMember(orgId, input);\n await outbox.append([\n entitlementEvents.spaceMemberCreated.make({\n orgId,\n data: created,\n metadata: { actorOrgUserId: actor.id },\n }),\n ]);\n return created;\n }\n\n private async activate(\n orgId: string,\n actor: Actor,\n spaceId: string,\n orgUserId: string,\n rule: 'by_request' | 'by_invite',\n from: SpaceMember['status'],\n missing: () => Error,\n ): Promise<SpaceMember> {\n const updated = await this.uow.run(async ({ entitlements, spaces, outbox }) => {\n const locked = await this.lockSpace(spaces, orgId, spaceId);\n this.assertRule(locked.accessRule, rule);\n const row = await entitlements.lockSpaceMember(orgId, spaceId, orgUserId);\n if (row?.status === 'active') {\n throw alreadyPlaced(actor, orgUserId);\n }\n if (!row || row.status !== from) {\n this.logger.warn('space member activate rejected — not found', {\n orgId,\n spaceId,\n orgUserId,\n actorId: actor.id,\n });\n throw missing();\n }\n await this.assertRoom(entitlements, orgId, locked);\n const next = await entitlements.updateSpaceMemberStatus(orgId, row.id, 'active');\n if (!next) {\n throw missing();\n }\n await outbox.append([\n entitlementEvents.spaceMemberUpdated.make({\n orgId,\n data: next,\n metadata: { actorOrgUserId: actor.id, previousStatus: from },\n }),\n ]);\n return next;\n });\n this.logger.info('space member activated', { orgId, spaceId, orgUserId, actorId: actor.id });\n return updated;\n }\n\n private async deleteMember(\n orgId: string,\n actor: Actor,\n spaceId: string,\n orgUserId: string,\n from: SpaceMember['status'],\n missing: () => Error,\n ): Promise<SpaceMember> {\n const row = await this.uow.run(async ({ entitlements, outbox }) => {\n const current = await entitlements.lockSpaceMember(orgId, spaceId, orgUserId);\n if (from !== 'active' && current?.status === 'active') {\n throw alreadyPlaced(actor, orgUserId);\n }\n if (!current || current.status !== from) {\n this.logger.warn('space member delete rejected — not found', {\n orgId,\n spaceId,\n orgUserId,\n actorId: actor.id,\n });\n throw missing();\n }\n await entitlements.deleteSpaceMember(orgId, current.id);\n await outbox.append([\n entitlementEvents.spaceMemberDeleted.make({\n orgId,\n data: current,\n metadata: { actorOrgUserId: actor.id, previousStatus: from },\n }),\n ]);\n return current;\n });\n this.logger.info('space member deleted', {\n orgId,\n spaceId,\n orgUserId,\n previousStatus: from,\n actorId: actor.id,\n });\n return row;\n }\n\n private async lockSpace(spaces: SpaceRowLock, orgId: string, spaceId: string) {\n const locked = await spaces.lockSpace(orgId, spaceId);\n if (!locked) {\n throw new NotFoundError('Space', spaceId);\n }\n return locked;\n }\n\n private async assertRoom(\n entitlements: EntitlementsRepository,\n orgId: string,\n space: Space,\n ): Promise<void> {\n if (space.capacity === null) {\n return;\n }\n const count = await entitlements.countActiveSpaceMembers(orgId, space.id);\n if (count >= space.capacity) {\n throw new ConflictError('This space is full');\n }\n }\n}\n\nfunction alreadyPlaced(actor: Actor, orgUserId: string): ConflictError {\n return new ConflictError(\n orgUserId === actor.id\n ? 'You already have a place in this space'\n : 'That person already has a place in this space',\n );\n}\n\nfunction ownRowMissing(status: 'invited' | 'active'): ConflictError {\n return new ConflictError(\n status === 'invited'\n ? 'You do not have an invitation to this space'\n : 'You are not a member of this space',\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.js","names":[],"sources":["../../src/entitlements/service.ts"],"sourcesContent":["// entitlements context — service implementation (inbound port).\n//\n// Mutations run inside the context's UnitOfWork: the domain write and the\n// outbox append commit in ONE transaction (transactional outbox). This\n// service never publishes — the outbox relay republishes committed events to\n// the EventBus at-least-once.\nimport type { CommunityReader, Space, SpaceRowLock } from '../content/index.js';\nimport type {\n ContentType,\n Entitlement,\n EntitlementsQuery,\n GrantEntitlementInput,\n Page,\n SpaceMember,\n} from './model.js';\nimport type {\n AccessFacts,\n EntitlementsRepository,\n EntitlementsService,\n EntitlementsUnitOfWork,\n EndingGrantKey,\n GrantTarget,\n InsertSpaceMemberInput,\n OpenableSpace,\n SpaceAccessRule,\n} from './ports.js';\nimport type { Actor } from '../shared/actor.js';\nimport type { Logger, NewDomainEvent, OutboxAppender } from '../shared/ports.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { ConflictError, ForbiddenError, NotFoundError } from '../shared/errors.js';\nimport { canModerate, isStaffRole } from '../organizations/index.js';\nimport { entitlementEvents } from './events.js';\nimport { laterExpiry, opensSpace, seesSpace } from './rules.js';\n\nconst ENDING_SOON_MS = 7 * 24 * 60 * 60 * 1000;\nconst NOTICE_BATCH_SIZE = 100;\n\nexport type EntitlementsServiceParams = {\n /** Read-only access (list) — runs outside any transaction. */\n repo: EntitlementsRepository;\n content: CommunityReader;\n /** Atomic write scope: tx-bound repo + outbox appender. */\n uow: EntitlementsUnitOfWork;\n logger?: Logger;\n};\n\nexport class EntitlementsServiceImpl implements EntitlementsService {\n private readonly repo: EntitlementsRepository;\n private readonly content: CommunityReader;\n private readonly uow: EntitlementsUnitOfWork;\n private readonly logger: Logger;\n\n constructor(params: EntitlementsServiceParams) {\n this.repo = params.repo;\n this.content = params.content;\n this.uow = params.uow;\n this.logger = params.logger ?? noopLogger;\n }\n\n list(orgId: string, query: EntitlementsQuery): Promise<Page<Entitlement>> {\n return this.repo.list(orgId, query);\n }\n\n async grant(orgId: string, input: GrantEntitlementInput): Promise<Entitlement> {\n const entitlement = await this.uow.run(async ({ entitlements, outbox }) => {\n const created = await entitlements.insert(orgId, input);\n await outbox.append([\n entitlementEvents.entitlementCreated.make({\n orgId,\n data: created,\n }),\n ]);\n return created;\n });\n this.logger.info('entitlement granted', {\n orgId,\n entitlementId: entitlement.id,\n orgUserId: entitlement.orgUserId,\n bundleId: entitlement.bundleId,\n contentId: entitlement.contentId,\n });\n return entitlement;\n }\n\n async ensureGrant(orgId: string, input: GrantEntitlementInput): Promise<Entitlement> {\n const { entitlement, change } = await this.uow.run(async ({ entitlements, outbox }) => {\n const created = await entitlements.insertIfAbsent(orgId, input);\n if (created) {\n await outbox.append([entitlementEvents.entitlementCreated.make({ orgId, data: created })]);\n return { entitlement: created, change: 'created' as const };\n }\n const existing = await entitlements.lockGrant(orgId, input);\n if (!existing) {\n throw new ConflictError('The grant changed while it was being given');\n }\n if (existing.status === 'revoked') {\n return { entitlement: existing, change: null };\n }\n const expiresAt = laterExpiry(existing.expiresAt, input.expiresAt ?? null);\n if (expiresAt?.getTime() === existing.expiresAt?.getTime()) {\n return { entitlement: existing, change: null };\n }\n const updated = await entitlements.setExpiresAt(orgId, existing.id, expiresAt);\n if (!updated) {\n throw new NotFoundError('Entitlement', existing.id);\n }\n await outbox.append([entitlementEvents.entitlementUpdated.make({ orgId, data: updated })]);\n return { entitlement: updated, change: 'updated' as const };\n });\n if (change) {\n this.logger.info(change === 'created' ? 'entitlement granted' : 'entitlement extended', {\n orgId,\n entitlementId: entitlement.id,\n orgUserId: entitlement.orgUserId,\n bundleId: entitlement.bundleId,\n contentId: entitlement.contentId,\n });\n }\n return entitlement;\n }\n\n async setStatus(\n orgId: string,\n id: string,\n status: 'active' | 'revoked',\n ): Promise<Entitlement | null> {\n const entitlement = await this.uow.run(async ({ entitlements, outbox }) => {\n const updated = await entitlements.setStatus(orgId, id, status);\n if (!updated) {\n return null;\n }\n await outbox.append([\n status === 'revoked'\n ? entitlementEvents.entitlementDeleted.make({\n orgId,\n data: updated,\n })\n : entitlementEvents.entitlementUpdated.make({\n orgId,\n data: updated,\n }),\n ]);\n return updated;\n });\n if (entitlement) {\n this.logger.info('entitlement status changed', { orgId, entitlementId: id, status });\n }\n return entitlement;\n }\n\n hasAccess(orgId: string, orgUserId: string, contentId: string): Promise<boolean> {\n return this.repo.hasAccess(orgId, orgUserId, contentId);\n }\n\n listAccessibleContent(orgId: string, orgUserId: string, type?: ContentType): Promise<string[]> {\n return this.repo.listAccessibleContent(orgId, orgUserId, type);\n }\n\n async isCommunityMember(orgId: string, actor: Actor, communityId: string): Promise<boolean> {\n const facts = await this.accessOf(orgId, actor.id, communityId, null);\n return facts?.member === true;\n }\n\n async canOpenSpace(orgId: string, actor: Actor, spaceId: string): Promise<boolean> {\n const space = await this.content.getSpace(orgId, spaceId);\n if (!space) {\n return false;\n }\n return this.canOpen(orgId, actor, space);\n }\n\n async canOpen(orgId: string, actor: Actor, space: OpenableSpace): Promise<boolean> {\n const facts = await this.accessOf(orgId, actor.id, space.communityId, space.id);\n if (!facts) {\n return false;\n }\n return opensSpace(facts.role, facts.member, space.accessRule, facts.activeInSpace);\n }\n\n async spaceStandings(\n orgId: string,\n actor: Actor,\n communityId: string,\n ): Promise<Record<string, SpaceMember['status']>> {\n const rows = await this.repo.listSpaceMembersFor(orgId, actor.id, communityId);\n return Object.fromEntries(rows.map((row) => [row.spaceId, row.status]));\n }\n\n async openSpacesFor(orgId: string, actor: Actor, communityId: string): Promise<Space[]> {\n const facts = await this.accessOf(orgId, actor.id, communityId, null);\n if (!facts?.member) {\n return [];\n }\n const [spaces, activeIds] = await Promise.all([\n this.content.listSpaces(orgId, communityId),\n this.repo.activeSpaceIds(orgId, actor.id, communityId),\n ]);\n const active = new Set(activeIds);\n return spaces.filter((space) =>\n opensSpace(facts.role, facts.member, space.accessRule, active.has(space.id)),\n );\n }\n\n async openSpaceIds(orgId: string, actor: Actor, communityId: string): Promise<string[]> {\n const spaces = await this.openSpacesFor(orgId, actor, communityId);\n return spaces.map((space) => space.id);\n }\n\n async communityMembersAmong(\n orgId: string,\n communityId: string,\n orgUserIds: string[],\n ): Promise<string[]> {\n if (orgUserIds.length === 0) {\n return [];\n }\n const facts = await this.repo.accessAmong(orgId, communityId, null, orgUserIds);\n return facts.filter((fact) => fact.member).map((fact) => fact.orgUserId);\n }\n\n async spaceOpenersAmong(orgId: string, spaceId: string, orgUserIds: string[]): Promise<string[]> {\n if (orgUserIds.length === 0) {\n return [];\n }\n const space = await this.content.getSpace(orgId, spaceId);\n if (!space) {\n return [];\n }\n const facts = await this.repo.accessAmong(orgId, space.communityId, space.id, orgUserIds);\n return facts\n .filter((fact) => opensSpace(fact.role, fact.member, space.accessRule, fact.activeInSpace))\n .map((fact) => fact.orgUserId);\n }\n\n async spaceOpeners(orgId: string, spaceId: string): Promise<string[]> {\n const space = await this.content.getSpace(orgId, spaceId);\n if (!space) {\n return [];\n }\n const members = await this.repo.communityMemberIds(orgId, space.communityId);\n return this.spaceOpenersAmong(orgId, spaceId, members);\n }\n\n communitiesOpenedBy(orgId: string, grant: GrantTarget): Promise<string[]> {\n return this.repo.communitiesOpenedBy(orgId, grant);\n }\n\n communityModeratorIds(orgId: string, communityId: string): Promise<string[]> {\n return this.repo.communityModeratorIds(orgId, communityId);\n }\n\n communityIdsOpenedFor(orgId: string, orgUserId: string): Promise<string[]> {\n return this.repo.communityIdsOpenedFor(orgId, orgUserId);\n }\n\n openCommunityIds(orgId: string, actor: Actor): Promise<string[]> {\n return this.repo.openCommunityIds(orgId, actor.id);\n }\n\n communityMemberIds(orgId: string, communityId: string): Promise<string[]> {\n return this.repo.communityMemberIds(orgId, communityId);\n }\n\n contentHolderIds(orgId: string, contentId: string): Promise<string[]> {\n return this.repo.contentHolderIds(orgId, contentId);\n }\n\n countActiveSpaceMembers(orgId: string, spaceId: string): Promise<number> {\n return this.repo.countActiveSpaceMembers(orgId, spaceId);\n }\n\n async requestJoin(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n ): Promise<SpaceMember> {\n const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);\n this.assertRule(space.accessRule, 'by_request');\n if (canModerate(actor.role)) {\n throw new ForbiddenError('Staff do not request a place in a space');\n }\n await this.assertGrant(orgId, actor, actor.id, space.communityId);\n const created = await this.createMember(orgId, actor, space, actor.id, 'requested', 'request');\n this.logger.info('space member created', {\n orgId,\n spaceId,\n orgUserId: actor.id,\n status: created.status,\n actorId: actor.id,\n });\n return created;\n }\n\n async invite(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n orgUserId: string,\n ): Promise<SpaceMember> {\n const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);\n this.assertRule(space.accessRule, 'by_invite');\n await this.assertCanManage(orgId, actor, space.communityId);\n await this.assertHolds(orgId, actor, orgUserId, space.communityId);\n const created = await this.createMember(orgId, actor, space, orgUserId, 'invited', 'invite');\n this.logger.info('space member created', {\n orgId,\n spaceId,\n orgUserId,\n status: created.status,\n actorId: actor.id,\n });\n return created;\n }\n\n async addSpaceMember(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n orgUserId: string,\n ): Promise<SpaceMember> {\n const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);\n this.assertHasMembers(space.accessRule);\n await this.assertCanManage(orgId, actor, space.communityId);\n await this.assertHolds(orgId, actor, orgUserId, space.communityId);\n const created = await this.uow.run(async (scope) => {\n const locked = await this.lockSpace(scope.spaces, orgId, spaceId);\n this.assertHasMembers(locked.accessRule);\n return this.insertMember(\n scope.entitlements,\n scope.outbox,\n orgId,\n actor,\n {\n communityId: space.communityId,\n spaceId,\n orgUserId,\n status: 'active',\n source: 'manual',\n },\n locked,\n );\n });\n this.logger.info('space member created', {\n orgId,\n spaceId,\n orgUserId,\n status: created.status,\n actorId: actor.id,\n });\n return created;\n }\n\n async accept(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n ): Promise<SpaceMember> {\n this.assertHolder(actor);\n const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);\n this.assertRule(space.accessRule, 'by_invite');\n await this.assertGrant(orgId, actor, actor.id, space.communityId);\n return this.activate(orgId, actor, spaceId, actor.id, 'by_invite', 'invited', () =>\n ownRowMissing('invited'),\n );\n }\n\n async approve(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n orgUserId: string,\n ): Promise<SpaceMember> {\n const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);\n this.assertRule(space.accessRule, 'by_request');\n await this.assertCanManage(orgId, actor, space.communityId);\n await this.assertGrant(orgId, actor, orgUserId, space.communityId);\n return this.activate(\n orgId,\n actor,\n spaceId,\n orgUserId,\n 'by_request',\n 'requested',\n () => new NotFoundError('Space member', orgUserId),\n );\n }\n\n async decline(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n orgUserId: string,\n ): Promise<SpaceMember> {\n const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);\n await this.assertCanManage(orgId, actor, space.communityId);\n return this.deleteMember(\n orgId,\n actor,\n spaceId,\n orgUserId,\n 'requested',\n () => new NotFoundError('Space member', orgUserId),\n );\n }\n\n async refuse(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n ): Promise<SpaceMember> {\n const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);\n this.assertHolder(actor);\n await this.assertGrant(orgId, actor, actor.id, space.communityId);\n return this.deleteMember(orgId, actor, spaceId, actor.id, 'invited', () =>\n ownRowMissing('invited'),\n );\n }\n\n async leave(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n ): Promise<SpaceMember> {\n const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);\n this.assertHolder(actor);\n await this.assertGrant(orgId, actor, actor.id, space.communityId);\n return this.deleteMember(orgId, actor, spaceId, actor.id, 'active', () =>\n ownRowMissing('active'),\n );\n }\n\n async remove(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n orgUserId: string,\n ): Promise<SpaceMember> {\n const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);\n this.assertHasMembers(space.accessRule);\n await this.assertCanManage(orgId, actor, space.communityId);\n const { updated, previousStatus } = await this.uow.run(\n async ({ entitlements, spaces, outbox }) => {\n const locked = await this.lockSpace(spaces, orgId, spaceId);\n this.assertHasMembers(locked.accessRule);\n const row = await entitlements.lockSpaceMember(orgId, spaceId, orgUserId);\n if (!row) {\n this.logger.warn('space member remove rejected — not found', {\n orgId,\n spaceId,\n orgUserId,\n actorId: actor.id,\n });\n throw new NotFoundError('Space member', orgUserId);\n }\n if (row.status === 'revoked') {\n throw new ConflictError('This person is already removed from the space');\n }\n const next = await entitlements.updateSpaceMemberStatus(orgId, row.id, 'revoked');\n if (!next) {\n throw new NotFoundError('Space member', row.id);\n }\n await outbox.append([\n entitlementEvents.spaceMemberUpdated.make({\n orgId,\n data: next,\n metadata: { actorOrgUserId: actor.id, previousStatus: row.status },\n }),\n ]);\n return { updated: next, previousStatus: row.status };\n },\n );\n this.logger.info('space member removed', {\n orgId,\n spaceId,\n orgUserId,\n previousStatus,\n actorId: actor.id,\n });\n return updated;\n }\n\n async listSpaceMembers(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n query: { page: number; pageSize: number },\n ): Promise<Page<SpaceMember>> {\n const space = await this.requireVisibleSpaceIn(orgId, actor, communityId, spaceId);\n if (!(await this.canOpen(orgId, actor, space))) {\n throw new NotFoundError('Space', spaceId);\n }\n return this.repo.listSpaceMembers(orgId, spaceId, {\n ...query,\n ...(canModerate(actor.role) ? {} : { statuses: ['active'] }),\n });\n }\n\n async expireDueGrants(now: Date): Promise<void> {\n const orgIds = await this.repo.orgIdsWithDueNotices(\n now,\n new Date(now.getTime() + ENDING_SOON_MS),\n );\n let failed = 0;\n for (const orgId of orgIds) {\n try {\n await this.expireDueIn(orgId, now);\n } catch (err) {\n failed += 1;\n this.logger.error('due grants not expired', { orgId, err });\n }\n }\n if (failed > 0) {\n throw new Error(`due grants of ${failed} orgs could not be expired`);\n }\n }\n\n private async expireDueIn(orgId: string, now: Date): Promise<void> {\n const until = new Date(now.getTime() + ENDING_SOON_MS);\n let appended = 0;\n let afterId: string | undefined;\n for (;;) {\n const batch = await this.uow.run(async ({ entitlements, outbox }) => {\n const events: NewDomainEvent[] = [];\n const grants = await entitlements.listGrantsExpiredBy(\n orgId,\n now,\n NOTICE_BATCH_SIZE,\n afterId,\n );\n for (const grant of grants) {\n if (!grant.expiresAt) {\n continue;\n }\n const claimed = await entitlements.claimNotice(\n orgId,\n { kind: 'expired', entitlementId: grant.id, expiresAt: grant.expiresAt },\n now,\n );\n if (claimed) {\n events.push(entitlementEvents.entitlementExpired.make({ orgId, data: grant }));\n }\n }\n if (events.length > 0) {\n await outbox.append(events);\n }\n return { events: events.length, last: grants.at(-1)?.id, size: grants.length };\n });\n appended += batch.events;\n if (batch.size < NOTICE_BATCH_SIZE || batch.last === undefined) {\n break;\n }\n afterId = batch.last;\n }\n let afterKey: EndingGrantKey | undefined;\n for (;;) {\n const batch = await this.uow.run(async ({ entitlements, outbox }) => {\n const events: NewDomainEvent[] = [];\n const ending = await entitlements.listGrantsEndingBetween(\n orgId,\n now,\n until,\n NOTICE_BATCH_SIZE,\n afterKey,\n );\n for (const { grant, communityId } of ending) {\n if (!grant.expiresAt) {\n continue;\n }\n const claimed = await entitlements.claimNotice(\n orgId,\n {\n kind: 'ending_soon',\n entitlementId: grant.id,\n communityId,\n expiresAt: grant.expiresAt,\n },\n now,\n );\n if (claimed) {\n events.push(\n entitlementEvents.entitlementEndingSoon.make({\n orgId,\n data: { ...grant, communityId },\n }),\n );\n }\n }\n if (events.length > 0) {\n await outbox.append(events);\n }\n const last = ending.at(-1);\n return {\n events: events.length,\n last: last && { entitlementId: last.grant.id, communityId: last.communityId },\n size: ending.length,\n };\n });\n appended += batch.events;\n if (batch.size < NOTICE_BATCH_SIZE || batch.last === undefined) {\n break;\n }\n afterKey = batch.last;\n }\n if (appended > 0) {\n this.logger.info('due grants expired', { orgId, events: appended });\n }\n }\n\n private async requireSpaceIn(orgId: string, communityId: string, spaceId: string) {\n const space = await this.content.getSpace(orgId, spaceId);\n if (!space || space.communityId !== communityId) {\n this.logger.warn('space members rejected — space not found', {\n orgId,\n communityId,\n spaceId,\n });\n throw new NotFoundError('Space', spaceId);\n }\n return space;\n }\n\n private async requireVisibleSpaceIn(\n orgId: string,\n actor: Actor,\n communityId: string,\n spaceId: string,\n ) {\n const space = await this.requireSpaceIn(orgId, communityId, spaceId);\n if (seesSpace(actor.role, space.accessRule, undefined)) {\n return space;\n }\n const row = await this.repo.findSpaceMember(orgId, spaceId, actor.id);\n if (!seesSpace(actor.role, space.accessRule, row?.status)) {\n throw new NotFoundError('Space', spaceId);\n }\n return space;\n }\n\n private assertHasMembers(accessRule: SpaceAccessRule): void {\n if (accessRule === 'all_members') {\n throw new ConflictError('An all-members space has no space members');\n }\n }\n\n private assertRule(accessRule: SpaceAccessRule, expected: 'by_request' | 'by_invite'): void {\n this.assertHasMembers(accessRule);\n if (accessRule !== expected) {\n throw new ConflictError(\n accessRule === 'by_invite' ? 'This space is by invitation' : 'This space is by request',\n );\n }\n }\n\n private assertHolder(actor: Actor): void {\n if (isStaffRole(actor.role)) {\n throw new ForbiddenError('Staff do not hold a place in a space');\n }\n }\n\n private async assertGrant(\n orgId: string,\n actor: Actor,\n orgUserId: string,\n communityId: string,\n ): Promise<void> {\n const member = await this.repo.hasCommunityAccess(orgId, orgUserId, communityId);\n if (!member) {\n throw new ForbiddenError(\n orgUserId === actor.id\n ? 'You are not a member of this community'\n : 'That person is not a member of this community',\n );\n }\n }\n\n private async assertHolds(\n orgId: string,\n actor: Actor,\n orgUserId: string,\n communityId: string,\n ): Promise<void> {\n const facts = await this.accessOf(orgId, orgUserId, communityId, null);\n if (facts && isStaffRole(facts.role)) {\n throw new ForbiddenError('Staff do not hold a place in a space');\n }\n await this.assertGrant(orgId, actor, orgUserId, communityId);\n }\n\n private async assertCanManage(orgId: string, actor: Actor, communityId: string): Promise<void> {\n const facts = await this.accessOf(orgId, actor.id, communityId, null);\n if (facts?.member && canModerate(facts.role)) {\n return;\n }\n throw new ForbiddenError('You cannot manage members of this space');\n }\n\n private async accessOf(\n orgId: string,\n orgUserId: string,\n communityId: string,\n spaceId: string | null,\n ): Promise<AccessFacts | null> {\n const [facts] = await this.repo.accessAmong(orgId, communityId, spaceId, [orgUserId]);\n return facts ?? null;\n }\n\n private async createMember(\n orgId: string,\n actor: Actor,\n space: { communityId: string; id: string },\n orgUserId: string,\n status: InsertSpaceMemberInput['status'],\n source: InsertSpaceMemberInput['source'],\n ): Promise<SpaceMember> {\n return this.uow.run(async ({ entitlements, outbox }) =>\n this.insertMember(entitlements, outbox, orgId, actor, {\n communityId: space.communityId,\n spaceId: space.id,\n orgUserId,\n status,\n source,\n }),\n );\n }\n\n private async insertMember(\n entitlements: EntitlementsRepository,\n outbox: OutboxAppender,\n orgId: string,\n actor: Actor,\n input: InsertSpaceMemberInput,\n room?: Space,\n ): Promise<SpaceMember> {\n const existing = await entitlements.lockSpaceMember(orgId, input.spaceId, input.orgUserId);\n if (existing && existing.status !== 'revoked') {\n throw new ConflictError(\n input.orgUserId === actor.id\n ? 'You already have a place in this space'\n : 'That person already has a place in this space',\n );\n }\n if (room) {\n await this.assertRoom(entitlements, orgId, room);\n }\n if (existing) {\n await entitlements.deleteSpaceMember(orgId, existing.id);\n }\n const created = await entitlements.insertSpaceMember(orgId, input);\n await outbox.append([\n entitlementEvents.spaceMemberCreated.make({\n orgId,\n data: created,\n metadata: { actorOrgUserId: actor.id },\n }),\n ]);\n return created;\n }\n\n private async activate(\n orgId: string,\n actor: Actor,\n spaceId: string,\n orgUserId: string,\n rule: 'by_request' | 'by_invite',\n from: SpaceMember['status'],\n missing: () => Error,\n ): Promise<SpaceMember> {\n const updated = await this.uow.run(async ({ entitlements, spaces, outbox }) => {\n const locked = await this.lockSpace(spaces, orgId, spaceId);\n this.assertRule(locked.accessRule, rule);\n const row = await entitlements.lockSpaceMember(orgId, spaceId, orgUserId);\n if (row?.status === 'active') {\n throw alreadyPlaced(actor, orgUserId);\n }\n if (!row || row.status !== from) {\n this.logger.warn('space member activate rejected — not found', {\n orgId,\n spaceId,\n orgUserId,\n actorId: actor.id,\n });\n throw missing();\n }\n await this.assertRoom(entitlements, orgId, locked);\n const next = await entitlements.updateSpaceMemberStatus(orgId, row.id, 'active');\n if (!next) {\n throw missing();\n }\n await outbox.append([\n entitlementEvents.spaceMemberUpdated.make({\n orgId,\n data: next,\n metadata: { actorOrgUserId: actor.id, previousStatus: from },\n }),\n ]);\n return next;\n });\n this.logger.info('space member activated', { orgId, spaceId, orgUserId, actorId: actor.id });\n return updated;\n }\n\n private async deleteMember(\n orgId: string,\n actor: Actor,\n spaceId: string,\n orgUserId: string,\n from: SpaceMember['status'],\n missing: () => Error,\n ): Promise<SpaceMember> {\n const row = await this.uow.run(async ({ entitlements, outbox }) => {\n const current = await entitlements.lockSpaceMember(orgId, spaceId, orgUserId);\n if (from !== 'active' && current?.status === 'active') {\n throw alreadyPlaced(actor, orgUserId);\n }\n if (!current || current.status !== from) {\n this.logger.warn('space member delete rejected — not found', {\n orgId,\n spaceId,\n orgUserId,\n actorId: actor.id,\n });\n throw missing();\n }\n await entitlements.deleteSpaceMember(orgId, current.id);\n await outbox.append([\n entitlementEvents.spaceMemberDeleted.make({\n orgId,\n data: current,\n metadata: { actorOrgUserId: actor.id, previousStatus: from },\n }),\n ]);\n return current;\n });\n this.logger.info('space member deleted', {\n orgId,\n spaceId,\n orgUserId,\n previousStatus: from,\n actorId: actor.id,\n });\n return row;\n }\n\n private async lockSpace(spaces: SpaceRowLock, orgId: string, spaceId: string) {\n const locked = await spaces.lockSpace(orgId, spaceId);\n if (!locked) {\n throw new NotFoundError('Space', spaceId);\n }\n return locked;\n }\n\n private async assertRoom(\n entitlements: EntitlementsRepository,\n orgId: string,\n space: Space,\n ): Promise<void> {\n if (space.capacity === null) {\n return;\n }\n const count = await entitlements.countActiveSpaceMembers(orgId, space.id);\n if (count >= space.capacity) {\n throw new ConflictError('This space is full');\n }\n }\n}\n\nfunction alreadyPlaced(actor: Actor, orgUserId: string): ConflictError {\n return new ConflictError(\n orgUserId === actor.id\n ? 'You already have a place in this space'\n : 'That person already has a place in this space',\n );\n}\n\nfunction ownRowMissing(status: 'invited' | 'active'): ConflictError {\n return new ConflictError(\n status === 'invited'\n ? 'You do not have an invitation to this space'\n : 'You are not a member of this space',\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Logger } from "../types/ports.js";
|
|
2
|
+
import { DeviceView, PushConfig, PushDevice } from "../types/notifications.js";
|
|
3
|
+
import "../types/index.js";
|
|
4
|
+
import "../shared/ports.js";
|
|
5
|
+
import { DeviceRegistration, DevicesRepository, DevicesService, MemberActor } from "./ports.js";
|
|
6
|
+
//#region src/identity/devices.d.ts
|
|
7
|
+
type DevicesServiceParams = {
|
|
8
|
+
repo: DevicesRepository;
|
|
9
|
+
push: PushConfig;
|
|
10
|
+
logger?: Logger;
|
|
11
|
+
};
|
|
12
|
+
declare class DevicesServiceImpl implements DevicesService {
|
|
13
|
+
private readonly repo;
|
|
14
|
+
private readonly push;
|
|
15
|
+
private readonly logger;
|
|
16
|
+
constructor(params: DevicesServiceParams);
|
|
17
|
+
register(orgId: string, actor: MemberActor, input: DeviceRegistration): Promise<PushDevice>;
|
|
18
|
+
list(orgId: string, actor: MemberActor): Promise<DeviceView[]>;
|
|
19
|
+
remove(orgId: string, actor: MemberActor, deviceId: string): Promise<void>;
|
|
20
|
+
activeDevices(orgId: string, userId: string, since: Date): Promise<PushDevice[]>;
|
|
21
|
+
withActiveDevices(orgId: string, userIds: string[], since: Date): Promise<string[]>;
|
|
22
|
+
removeDead(orgId: string, deviceIds: string[]): Promise<void>;
|
|
23
|
+
pushConfig(): {
|
|
24
|
+
vapidPublicKey: string | null;
|
|
25
|
+
};
|
|
26
|
+
private registrationProblems;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { DevicesServiceImpl, DevicesServiceParams };
|
|
30
|
+
//# sourceMappingURL=devices.d.ts.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { NotFoundError, ValidationError } from "../shared/errors.js";
|
|
2
|
+
import { noopLogger } from "../shared/logger.js";
|
|
3
|
+
//#region src/identity/devices.ts
|
|
4
|
+
function pushDevice(device) {
|
|
5
|
+
return {
|
|
6
|
+
id: device.id,
|
|
7
|
+
provider: device.provider,
|
|
8
|
+
token: device.token,
|
|
9
|
+
p256dh: device.p256dh,
|
|
10
|
+
auth: device.auth,
|
|
11
|
+
appId: device.appId,
|
|
12
|
+
environment: device.environment
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
var DevicesServiceImpl = class {
|
|
16
|
+
repo;
|
|
17
|
+
push;
|
|
18
|
+
logger;
|
|
19
|
+
constructor(params) {
|
|
20
|
+
this.repo = params.repo;
|
|
21
|
+
this.push = params.push;
|
|
22
|
+
this.logger = params.logger ?? noopLogger;
|
|
23
|
+
}
|
|
24
|
+
async register(orgId, actor, input) {
|
|
25
|
+
const problems = this.registrationProblems(input);
|
|
26
|
+
if (problems.length > 0) throw new ValidationError(problems);
|
|
27
|
+
const device = await this.repo.upsert(orgId, {
|
|
28
|
+
userId: actor.userId,
|
|
29
|
+
provider: input.provider,
|
|
30
|
+
token: input.token,
|
|
31
|
+
p256dh: input.keys?.p256dh ?? null,
|
|
32
|
+
auth: input.keys?.auth ?? null,
|
|
33
|
+
appId: input.appId ?? null,
|
|
34
|
+
environment: input.environment ?? null
|
|
35
|
+
}, /* @__PURE__ */ new Date());
|
|
36
|
+
this.logger.info("device registered", {
|
|
37
|
+
orgId,
|
|
38
|
+
userId: actor.userId,
|
|
39
|
+
deviceId: device.id,
|
|
40
|
+
provider: device.provider
|
|
41
|
+
});
|
|
42
|
+
return pushDevice(device);
|
|
43
|
+
}
|
|
44
|
+
list(orgId, actor) {
|
|
45
|
+
return this.repo.listForUser(orgId, actor.userId, null);
|
|
46
|
+
}
|
|
47
|
+
async remove(orgId, actor, deviceId) {
|
|
48
|
+
if (!await this.repo.deleteOwn(orgId, actor.userId, deviceId)) throw new NotFoundError("Device", deviceId);
|
|
49
|
+
this.logger.info("device removed", {
|
|
50
|
+
orgId,
|
|
51
|
+
userId: actor.userId,
|
|
52
|
+
deviceId
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async activeDevices(orgId, userId, since) {
|
|
56
|
+
return (await this.repo.listForUser(orgId, userId, since)).map(pushDevice);
|
|
57
|
+
}
|
|
58
|
+
async withActiveDevices(orgId, userIds, since) {
|
|
59
|
+
if (userIds.length === 0) return [];
|
|
60
|
+
return this.repo.usersWithDevices(orgId, [...new Set(userIds)], since);
|
|
61
|
+
}
|
|
62
|
+
async removeDead(orgId, deviceIds) {
|
|
63
|
+
if (deviceIds.length === 0) return;
|
|
64
|
+
await this.repo.deleteMany(orgId, deviceIds);
|
|
65
|
+
this.logger.info("dead devices removed", {
|
|
66
|
+
orgId,
|
|
67
|
+
count: deviceIds.length
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
pushConfig() {
|
|
71
|
+
return { vapidPublicKey: this.push.webpush?.publicKey ?? null };
|
|
72
|
+
}
|
|
73
|
+
registrationProblems(input) {
|
|
74
|
+
const problems = [];
|
|
75
|
+
if (!this.push[input.provider]) {
|
|
76
|
+
problems.push({
|
|
77
|
+
path: "provider",
|
|
78
|
+
message: `push through ${input.provider} is not configured`
|
|
79
|
+
});
|
|
80
|
+
return problems;
|
|
81
|
+
}
|
|
82
|
+
if (input.provider === "apns") {
|
|
83
|
+
const bundleIds = this.push.apns?.bundleIds ?? [];
|
|
84
|
+
if (!input.appId || !bundleIds.includes(input.appId)) problems.push({
|
|
85
|
+
path: "appId",
|
|
86
|
+
message: "must be one of the configured iOS bundle ids"
|
|
87
|
+
});
|
|
88
|
+
} else if (input.environment !== void 0) problems.push({
|
|
89
|
+
path: "environment",
|
|
90
|
+
message: "applies to apns devices only"
|
|
91
|
+
});
|
|
92
|
+
if (input.provider === "webpush") {
|
|
93
|
+
if (!input.keys) problems.push({
|
|
94
|
+
path: "keys",
|
|
95
|
+
message: "a webpush device needs its p256dh and auth keys"
|
|
96
|
+
});
|
|
97
|
+
} else if (input.keys !== void 0) problems.push({
|
|
98
|
+
path: "keys",
|
|
99
|
+
message: "apply to webpush devices only"
|
|
100
|
+
});
|
|
101
|
+
return problems;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
//#endregion
|
|
105
|
+
export { DevicesServiceImpl };
|
|
106
|
+
|
|
107
|
+
//# sourceMappingURL=devices.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devices.js","names":[],"sources":["../../src/identity/devices.ts"],"sourcesContent":["import type { DeviceView, PushConfig, PushDevice } from '../types/index.js';\nimport type { Logger } from '../shared/ports.js';\nimport { NotFoundError, ValidationError, type ValidationProblem } from '../shared/errors.js';\nimport { noopLogger } from '../shared/logger.js';\nimport type {\n DeviceRegistration,\n DevicesRepository,\n DevicesService,\n MemberActor,\n} from './ports.js';\n\nfunction pushDevice(device: DeviceView): PushDevice {\n return {\n id: device.id,\n provider: device.provider,\n token: device.token,\n p256dh: device.p256dh,\n auth: device.auth,\n appId: device.appId,\n environment: device.environment,\n };\n}\n\nexport type DevicesServiceParams = {\n repo: DevicesRepository;\n push: PushConfig;\n logger?: Logger;\n};\n\nexport class DevicesServiceImpl implements DevicesService {\n private readonly repo: DevicesRepository;\n private readonly push: PushConfig;\n private readonly logger: Logger;\n\n constructor(params: DevicesServiceParams) {\n this.repo = params.repo;\n this.push = params.push;\n this.logger = params.logger ?? noopLogger;\n }\n\n async register(\n orgId: string,\n actor: MemberActor,\n input: DeviceRegistration,\n ): Promise<PushDevice> {\n const problems = this.registrationProblems(input);\n if (problems.length > 0) {\n throw new ValidationError(problems);\n }\n const device = await this.repo.upsert(\n orgId,\n {\n userId: actor.userId,\n provider: input.provider,\n token: input.token,\n p256dh: input.keys?.p256dh ?? null,\n auth: input.keys?.auth ?? null,\n appId: input.appId ?? null,\n environment: input.environment ?? null,\n },\n new Date(),\n );\n this.logger.info('device registered', {\n orgId,\n userId: actor.userId,\n deviceId: device.id,\n provider: device.provider,\n });\n return pushDevice(device);\n }\n\n list(orgId: string, actor: MemberActor): Promise<DeviceView[]> {\n return this.repo.listForUser(orgId, actor.userId, null);\n }\n\n async remove(orgId: string, actor: MemberActor, deviceId: string): Promise<void> {\n if (!(await this.repo.deleteOwn(orgId, actor.userId, deviceId))) {\n throw new NotFoundError('Device', deviceId);\n }\n this.logger.info('device removed', { orgId, userId: actor.userId, deviceId });\n }\n\n async activeDevices(orgId: string, userId: string, since: Date): Promise<PushDevice[]> {\n const devices = await this.repo.listForUser(orgId, userId, since);\n return devices.map(pushDevice);\n }\n\n async withActiveDevices(orgId: string, userIds: string[], since: Date): Promise<string[]> {\n if (userIds.length === 0) {\n return [];\n }\n return this.repo.usersWithDevices(orgId, [...new Set(userIds)], since);\n }\n\n async removeDead(orgId: string, deviceIds: string[]): Promise<void> {\n if (deviceIds.length === 0) {\n return;\n }\n await this.repo.deleteMany(orgId, deviceIds);\n this.logger.info('dead devices removed', { orgId, count: deviceIds.length });\n }\n\n pushConfig(): { vapidPublicKey: string | null } {\n return { vapidPublicKey: this.push.webpush?.publicKey ?? null };\n }\n\n private registrationProblems(input: DeviceRegistration): ValidationProblem[] {\n const problems: ValidationProblem[] = [];\n if (!this.push[input.provider]) {\n problems.push({\n path: 'provider',\n message: `push through ${input.provider} is not configured`,\n });\n return problems;\n }\n if (input.provider === 'apns') {\n const bundleIds = this.push.apns?.bundleIds ?? [];\n if (!input.appId || !bundleIds.includes(input.appId)) {\n problems.push({\n path: 'appId',\n message: 'must be one of the configured iOS bundle ids',\n });\n }\n } else if (input.environment !== undefined) {\n problems.push({\n path: 'environment',\n message: 'applies to apns devices only',\n });\n }\n if (input.provider === 'webpush') {\n if (!input.keys) {\n problems.push({\n path: 'keys',\n message: 'a webpush device needs its p256dh and auth keys',\n });\n }\n } else if (input.keys !== undefined) {\n problems.push({ path: 'keys', message: 'apply to webpush devices only' });\n }\n return problems;\n }\n}\n"],"mappings":"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"}
|
|
@@ -22,9 +22,9 @@ declare const identityEvents: {
|
|
|
22
22
|
id: import("zod").ZodString;
|
|
23
23
|
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
24
24
|
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
25
|
-
email: import("zod").ZodEmail;
|
|
26
25
|
firstName: import("zod").ZodNullable<import("zod").ZodString>;
|
|
27
26
|
lastName: import("zod").ZodNullable<import("zod").ZodString>;
|
|
27
|
+
email: import("zod").ZodEmail;
|
|
28
28
|
}, import("zod/v4/core").$strip>>;
|
|
29
29
|
userUpdated: RuntimeEventDefinition<VersionedDomainEvent<{
|
|
30
30
|
id: string;
|
|
@@ -46,9 +46,9 @@ declare const identityEvents: {
|
|
|
46
46
|
id: import("zod").ZodString;
|
|
47
47
|
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
48
48
|
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
49
|
-
email: import("zod").ZodEmail;
|
|
50
49
|
firstName: import("zod").ZodNullable<import("zod").ZodString>;
|
|
51
50
|
lastName: import("zod").ZodNullable<import("zod").ZodString>;
|
|
51
|
+
email: import("zod").ZodEmail;
|
|
52
52
|
}, import("zod/v4/core").$strip>>;
|
|
53
53
|
userDeleted: RuntimeEventDefinition<VersionedDomainEvent<{
|
|
54
54
|
id: string;
|
package/dist/identity/index.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { CreateUserInput,
|
|
1
|
+
import { CreateUserInput, ProvisionUserInput, UpdateUserInput, UserProfile, UserProfileView } from "../types/schemas/identity.js";
|
|
2
2
|
import { User, UserId } from "../types/identity.js";
|
|
3
3
|
import "./model.js";
|
|
4
4
|
import "./types.js";
|
|
5
|
-
import { AuthHeaders,
|
|
5
|
+
import { AuthHeaders, CommunityAccessReader, DeviceRegistration, DevicesRepository, DevicesService, IdentityRepository, IdentityService, MemberActor, NewDevice, NotificationCategoryReader, NotificationSettingsRepository, NotificationSettingsService, ProfilePatch, SessionAdmin, SettingChange, SettingKey, UpdateProfileInput, UserEditor, UserProvisioner, UserResolver } from "./ports.js";
|
|
6
6
|
import { IdentityServiceImpl } from "./service.js";
|
|
7
|
-
import {
|
|
7
|
+
import { NotificationSettingsServiceImpl } from "./notification-settings.js";
|
|
8
|
+
import { DevicesServiceImpl } from "./devices.js";
|
|
8
9
|
import { IdentityEvent, UserCreated, UserDeleted, UserUpdated, identityEvents } from "./events.js";
|
|
9
10
|
import { ACCESS_TOKEN_TTL_SECONDS, ORG_CLAIM } from "./access.js";
|
|
10
|
-
export { ACCESS_TOKEN_TTL_SECONDS, type AuthHeaders, type CreateUserInput, type
|
|
11
|
+
export { ACCESS_TOKEN_TTL_SECONDS, type AuthHeaders, type CommunityAccessReader, type CreateUserInput, type DeviceRegistration, type DevicesRepository, type DevicesService, DevicesServiceImpl, type IdentityEvent, type IdentityRepository, type IdentityService, IdentityServiceImpl, type MemberActor, type NewDevice, type NotificationCategoryReader, type NotificationSettingsRepository, type NotificationSettingsService, NotificationSettingsServiceImpl, ORG_CLAIM, type ProfilePatch, type ProvisionUserInput, type SessionAdmin, type SettingChange, type SettingKey, type UpdateProfileInput, type UpdateUserInput, type User, type UserCreated, type UserDeleted, type UserEditor, type UserId, type UserProfile, type UserProfileView, type UserProvisioner, type UserResolver, type UserUpdated, identityEvents };
|
package/dist/identity/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { identityEvents } from "./events.js";
|
|
2
2
|
import { IdentityServiceImpl } from "./service.js";
|
|
3
|
-
import {
|
|
3
|
+
import { NotificationSettingsServiceImpl } from "./notification-settings.js";
|
|
4
|
+
import { DevicesServiceImpl } from "./devices.js";
|
|
4
5
|
import { ACCESS_TOKEN_TTL_SECONDS, ORG_CLAIM } from "./access.js";
|
|
5
|
-
export { ACCESS_TOKEN_TTL_SECONDS, IdentityServiceImpl,
|
|
6
|
+
export { ACCESS_TOKEN_TTL_SECONDS, DevicesServiceImpl, IdentityServiceImpl, NotificationSettingsServiceImpl, ORG_CLAIM, identityEvents };
|
package/dist/identity/model.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { UserProfile, UserProfileView } from "../types/schemas/identity.js";
|
|
2
2
|
import { User } from "../types/identity.js";
|
|
3
3
|
import "../types/index.js";
|
|
4
|
-
export type {
|
|
4
|
+
export type { User, UserProfile, UserProfileView };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Logger } from "../types/ports.js";
|
|
2
|
+
import { SettingsScope, StoredSetting } from "../types/notifications.js";
|
|
3
|
+
import "../types/index.js";
|
|
4
|
+
import { ChannelRegistry } from "../shared/ports.js";
|
|
5
|
+
import { CommunityAccessReader, MemberActor, NotificationCategoryReader, NotificationSettingsRepository, NotificationSettingsService, SettingChange } from "./ports.js";
|
|
6
|
+
//#region src/identity/notification-settings.d.ts
|
|
7
|
+
type NotificationSettingsServiceParams = {
|
|
8
|
+
repo: NotificationSettingsRepository;
|
|
9
|
+
categories: NotificationCategoryReader;
|
|
10
|
+
access: CommunityAccessReader;
|
|
11
|
+
channels: Pick<ChannelRegistry, 'list'>;
|
|
12
|
+
logger?: Logger;
|
|
13
|
+
};
|
|
14
|
+
declare class NotificationSettingsServiceImpl implements NotificationSettingsService {
|
|
15
|
+
private readonly repo;
|
|
16
|
+
private readonly categories;
|
|
17
|
+
private readonly access;
|
|
18
|
+
private readonly channels;
|
|
19
|
+
private readonly logger;
|
|
20
|
+
constructor(params: NotificationSettingsServiceParams);
|
|
21
|
+
get(orgId: string, userId: string, scope: SettingsScope): Promise<StoredSetting[]>;
|
|
22
|
+
getMany(orgId: string, userIds: string[], scope: SettingsScope, categories: string[]): Promise<Record<string, StoredSetting[]>>;
|
|
23
|
+
set(orgId: string, actor: MemberActor, scope: SettingsScope, changes: SettingChange[]): Promise<void>;
|
|
24
|
+
switchedOn(orgId: string, communityId: string, category: string, spaceId: string | null): Promise<string[]>;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { NotificationSettingsServiceImpl, NotificationSettingsServiceParams };
|
|
28
|
+
//# sourceMappingURL=notification-settings.d.ts.map
|