@openora/core 0.4.1-canary.66 → 0.4.1-canary.68
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/.tsbuildinfo +1 -1
- package/dist/audit/__tests__/audit.service.int.test.js +202 -2
- package/dist/audit/__tests__/audit.service.int.test.js.map +1 -1
- package/dist/audit/plugin.d.ts +2 -0
- package/dist/audit/plugin.d.ts.map +1 -1
- package/dist/audit/plugin.js +76 -34
- package/dist/audit/plugin.js.map +1 -1
- package/dist/casino/gaming/__tests__/gaming.service.int.test.js +4 -3
- package/dist/casino/gaming/__tests__/gaming.service.int.test.js.map +1 -1
- package/dist/casino/gaming/plugin.d.ts +1 -1
- package/dist/casino/gaming/plugin.d.ts.map +1 -1
- package/dist/casino/gaming/plugin.js +3 -3
- package/dist/casino/gaming/plugin.js.map +1 -1
- package/dist/casino/gaming/service/gaming.service.d.ts +3 -2
- package/dist/casino/gaming/service/gaming.service.d.ts.map +1 -1
- package/dist/casino/gaming/service/gaming.service.js +9 -2
- package/dist/casino/gaming/service/gaming.service.js.map +1 -1
- package/dist/compliance/__tests__/compliance.service.int.test.js +4 -3
- package/dist/compliance/__tests__/compliance.service.int.test.js.map +1 -1
- package/dist/compliance/__tests__/kyc-admin.router.int.test.js +2 -1
- package/dist/compliance/__tests__/kyc-admin.router.int.test.js.map +1 -1
- package/dist/compliance/__tests__/kyc.service.int.test.js +2 -1
- package/dist/compliance/__tests__/kyc.service.int.test.js.map +1 -1
- package/dist/compliance/__tests__/rg.router.int.test.js +4 -2
- package/dist/compliance/__tests__/rg.router.int.test.js.map +1 -1
- package/dist/compliance/__tests__/rg.service.int.test.js +4 -2
- package/dist/compliance/__tests__/rg.service.int.test.js.map +1 -1
- package/dist/compliance/plugin.d.ts.map +1 -1
- package/dist/compliance/plugin.js +4 -2
- package/dist/compliance/plugin.js.map +1 -1
- package/dist/compliance/service/compliance.service.d.ts +3 -2
- package/dist/compliance/service/compliance.service.d.ts.map +1 -1
- package/dist/compliance/service/compliance.service.js +5 -1
- package/dist/compliance/service/compliance.service.js.map +1 -1
- package/dist/compliance/service/kyc.service.d.ts +3 -1
- package/dist/compliance/service/kyc.service.d.ts.map +1 -1
- package/dist/compliance/service/kyc.service.js +6 -2
- package/dist/compliance/service/kyc.service.js.map +1 -1
- package/dist/compliance/service/rg.service.d.ts +3 -1
- package/dist/compliance/service/rg.service.d.ts.map +1 -1
- package/dist/compliance/service/rg.service.js +7 -0
- package/dist/compliance/service/rg.service.js.map +1 -1
- package/dist/contracts/adapters/identity-reader.d.ts +2 -0
- package/dist/contracts/adapters/identity-reader.d.ts.map +1 -1
- package/dist/contracts/adapters/identity-reader.js.map +1 -1
- package/dist/contracts/schemas/events.d.ts +63 -0
- package/dist/contracts/schemas/events.d.ts.map +1 -1
- package/dist/contracts/schemas/events.js +140 -24
- package/dist/contracts/schemas/events.js.map +1 -1
- package/dist/engagement/chat/__tests__/chat.service.int.test.js +8 -3
- package/dist/engagement/chat/__tests__/chat.service.int.test.js.map +1 -1
- package/dist/engagement/chat/plugin.d.ts.map +1 -1
- package/dist/engagement/chat/plugin.js +2 -2
- package/dist/engagement/chat/plugin.js.map +1 -1
- package/dist/engagement/chat/service/chat.service.d.ts +3 -2
- package/dist/engagement/chat/service/chat.service.d.ts.map +1 -1
- package/dist/engagement/chat/service/chat.service.js +18 -1
- package/dist/engagement/chat/service/chat.service.js.map +1 -1
- package/dist/engagement/contracts.d.ts +1 -0
- package/dist/engagement/contracts.d.ts.map +1 -1
- package/dist/engagement/contracts.js +1 -0
- package/dist/engagement/contracts.js.map +1 -1
- package/dist/engagement/index.d.ts +1 -0
- package/dist/engagement/index.d.ts.map +1 -1
- package/dist/engagement/index.js +1 -0
- package/dist/engagement/index.js.map +1 -1
- package/dist/engagement/notifications/__tests__/notifications.service.int.test.js +34 -0
- package/dist/engagement/notifications/__tests__/notifications.service.int.test.js.map +1 -1
- package/dist/engagement/notifications/contract/index.d.ts +9 -1
- package/dist/engagement/notifications/contract/index.d.ts.map +1 -1
- package/dist/engagement/notifications/contract/index.js +2 -0
- package/dist/engagement/notifications/contract/index.js.map +1 -1
- package/dist/engagement/notifications/plugin.d.ts.map +1 -1
- package/dist/engagement/notifications/plugin.js +24 -0
- package/dist/engagement/notifications/plugin.js.map +1 -1
- package/dist/engagement/notifications/router/index.d.ts +2 -0
- package/dist/engagement/notifications/router/index.d.ts.map +1 -1
- package/dist/engagement/server.d.ts +1 -0
- package/dist/engagement/server.d.ts.map +1 -1
- package/dist/engagement/server.js +1 -0
- package/dist/engagement/server.js.map +1 -1
- package/dist/engagement/social/__tests__/social.router.int.test.d.ts +2 -0
- package/dist/engagement/social/__tests__/social.router.int.test.d.ts.map +1 -0
- package/dist/engagement/social/__tests__/social.router.int.test.js +97 -0
- package/dist/engagement/social/__tests__/social.router.int.test.js.map +1 -0
- package/dist/engagement/social/__tests__/social.service.int.test.d.ts +2 -0
- package/dist/engagement/social/__tests__/social.service.int.test.d.ts.map +1 -0
- package/dist/engagement/social/__tests__/social.service.int.test.js +233 -0
- package/dist/engagement/social/__tests__/social.service.int.test.js.map +1 -0
- package/dist/engagement/social/contract/index.d.ts +73 -0
- package/dist/engagement/social/contract/index.d.ts.map +1 -0
- package/dist/engagement/social/contract/index.js +54 -0
- package/dist/engagement/social/contract/index.js.map +1 -0
- package/dist/engagement/social/drizzle/migrations/0000_fine_jamie_braddock.sql +24 -0
- package/dist/engagement/social/drizzle/migrations/0001_lame_eddie_brock.sql +10 -0
- package/dist/engagement/social/drizzle/migrations/0002_foamy_saracen.sql +1 -0
- package/dist/engagement/social/drizzle/migrations/meta/0000_snapshot.json +232 -0
- package/dist/engagement/social/drizzle/migrations/meta/0001_snapshot.json +129 -0
- package/dist/engagement/social/drizzle/migrations/meta/0002_snapshot.json +134 -0
- package/dist/engagement/social/drizzle/migrations/meta/_journal.json +27 -0
- package/dist/engagement/social/drizzle.config.d.ts +4 -0
- package/dist/engagement/social/drizzle.config.d.ts.map +1 -0
- package/dist/engagement/social/drizzle.config.js +21 -0
- package/dist/engagement/social/drizzle.config.js.map +1 -0
- package/dist/engagement/social/index.d.ts +3 -0
- package/dist/engagement/social/index.d.ts.map +1 -0
- package/dist/engagement/social/index.js +6 -0
- package/dist/engagement/social/index.js.map +1 -0
- package/dist/engagement/social/migrate.d.ts +5 -0
- package/dist/engagement/social/migrate.d.ts.map +1 -0
- package/dist/engagement/social/migrate.js +17 -0
- package/dist/engagement/social/migrate.js.map +1 -0
- package/dist/engagement/social/plugin.d.ts +111 -0
- package/dist/engagement/social/plugin.d.ts.map +1 -0
- package/dist/engagement/social/plugin.js +16 -0
- package/dist/engagement/social/plugin.js.map +1 -0
- package/dist/engagement/social/router/index.d.ts +31 -0
- package/dist/engagement/social/router/index.d.ts.map +1 -0
- package/dist/engagement/social/router/index.js +31 -0
- package/dist/engagement/social/router/index.js.map +1 -0
- package/dist/engagement/social/schema/index.d.ts +128 -0
- package/dist/engagement/social/schema/index.d.ts.map +1 -0
- package/dist/engagement/social/schema/index.js +36 -0
- package/dist/engagement/social/schema/index.js.map +1 -0
- package/dist/engagement/social/service/social.service.d.ts +39 -0
- package/dist/engagement/social/service/social.service.d.ts.map +1 -0
- package/dist/engagement/social/service/social.service.js +251 -0
- package/dist/engagement/social/service/social.service.js.map +1 -0
- package/dist/iam/__tests__/iam.service.int.test.js +42 -38
- package/dist/iam/__tests__/iam.service.int.test.js.map +1 -1
- package/dist/iam/plugin.d.ts.map +1 -1
- package/dist/iam/plugin.js +2 -2
- package/dist/iam/plugin.js.map +1 -1
- package/dist/iam/service/iam.service.d.ts +3 -2
- package/dist/iam/service/iam.service.d.ts.map +1 -1
- package/dist/iam/service/iam.service.js +24 -10
- package/dist/iam/service/iam.service.js.map +1 -1
- package/dist/pam/identity/__tests__/identity.rate-limit.int.test.js +6 -2
- package/dist/pam/identity/__tests__/identity.rate-limit.int.test.js.map +1 -1
- package/dist/pam/identity/__tests__/identity.service.int.test.js +3 -2
- package/dist/pam/identity/__tests__/identity.service.int.test.js.map +1 -1
- package/dist/pam/identity/__tests__/login-enforcement.service.int.test.js +8 -3
- package/dist/pam/identity/__tests__/login-enforcement.service.int.test.js.map +1 -1
- package/dist/pam/identity/adapters/identity-reader.service.d.ts +1 -0
- package/dist/pam/identity/adapters/identity-reader.service.d.ts.map +1 -1
- package/dist/pam/identity/adapters/identity-reader.service.js +11 -0
- package/dist/pam/identity/adapters/identity-reader.service.js.map +1 -1
- package/dist/pam/identity/plugin.d.ts.map +1 -1
- package/dist/pam/identity/plugin.js +16 -3
- package/dist/pam/identity/plugin.js.map +1 -1
- package/dist/pam/identity/service/identity.service.d.ts +4 -2
- package/dist/pam/identity/service/identity.service.d.ts.map +1 -1
- package/dist/pam/identity/service/identity.service.js +81 -31
- package/dist/pam/identity/service/identity.service.js.map +1 -1
- package/dist/pam/identity/service/phone-login.service.d.ts.map +1 -1
- package/dist/pam/identity/service/phone-login.service.js +16 -6
- package/dist/pam/identity/service/phone-login.service.js.map +1 -1
- package/dist/pam/identity/service/session.service.d.ts +4 -2
- package/dist/pam/identity/service/session.service.d.ts.map +1 -1
- package/dist/pam/identity/service/session.service.js +5 -1
- package/dist/pam/identity/service/session.service.js.map +1 -1
- package/dist/pam/player-management/__tests__/kyc-status-writer.int.test.js +2 -1
- package/dist/pam/player-management/__tests__/kyc-status-writer.int.test.js.map +1 -1
- package/dist/pam/player-management/__tests__/player.service.int.test.js +1 -0
- package/dist/pam/player-management/__tests__/player.service.int.test.js.map +1 -1
- package/dist/pam/player-management/service/kyc-status-writer.d.ts.map +1 -1
- package/dist/pam/player-management/service/kyc-status-writer.js +2 -1
- package/dist/pam/player-management/service/kyc-status-writer.js.map +1 -1
- package/dist/pam/player-management/service/player.service.d.ts.map +1 -1
- package/dist/pam/player-management/service/player.service.js +1 -0
- package/dist/pam/player-management/service/player.service.js.map +1 -1
- package/dist/pam/tag/__tests__/tag-evaluation.service.int.test.js +57 -13
- package/dist/pam/tag/__tests__/tag-evaluation.service.int.test.js.map +1 -1
- package/dist/pam/tag/service/tag.service.d.ts.map +1 -1
- package/dist/pam/tag/service/tag.service.js +25 -0
- package/dist/pam/tag/service/tag.service.js.map +1 -1
- package/dist/server/auth/admin-guard.d.ts +3 -2
- package/dist/server/auth/admin-guard.d.ts.map +1 -1
- package/dist/server/auth/admin-guard.js +28 -9
- package/dist/server/auth/admin-guard.js.map +1 -1
- package/dist/server/kernel/__tests__/event-bus.int.test.js +9 -5
- package/dist/server/kernel/__tests__/event-bus.int.test.js.map +1 -1
- package/dist/server/runtime/create-app.d.ts.map +1 -1
- package/dist/server/runtime/create-app.js +4 -2
- package/dist/server/runtime/create-app.js.map +1 -1
- package/dist/testing/mock.d.ts +2 -1
- package/dist/testing/mock.d.ts.map +1 -1
- package/dist/testing/mock.js +8 -0
- package/dist/testing/mock.js.map +1 -1
- package/dist/wallet/__tests__/wallet-auto-rule.router.int.test.js +2 -1
- package/dist/wallet/__tests__/wallet-auto-rule.router.int.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet-auto-withdrawal-config.router.int.test.js +4 -2
- package/dist/wallet/__tests__/wallet-auto-withdrawal-config.router.int.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet-auto-withdrawal.int.test.js +5 -2
- package/dist/wallet/__tests__/wallet-auto-withdrawal.int.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet-kyc-gate.int.test.js +4 -2
- package/dist/wallet/__tests__/wallet-kyc-gate.int.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet-webhook.router.int.test.js +4 -2
- package/dist/wallet/__tests__/wallet-webhook.router.int.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet.rate-limit.int.test.js +4 -2
- package/dist/wallet/__tests__/wallet.rate-limit.int.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet.router.int.test.js +2 -1
- package/dist/wallet/__tests__/wallet.router.int.test.js.map +1 -1
- package/dist/wallet/__tests__/wallet.service.int.test.js +4 -2
- package/dist/wallet/__tests__/wallet.service.int.test.js.map +1 -1
- package/dist/wallet/plugin.d.ts.map +1 -1
- package/dist/wallet/plugin.js +2 -1
- package/dist/wallet/plugin.js.map +1 -1
- package/dist/wallet/service/wallet.service.d.ts +4 -2
- package/dist/wallet/service/wallet.service.d.ts.map +1 -1
- package/dist/wallet/service/wallet.service.js +8 -1
- package/dist/wallet/service/wallet.service.js.map +1 -1
- package/package.json +21 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { and, eq, inArray, isNull, or } from 'drizzle-orm';
|
|
2
|
+
import { createDomainError, makeConflictError, makeNotFoundError, serializeRow, } from '@openora/core/server';
|
|
3
|
+
import { chatUserBlock } from '@openora/core/engagement/schema/chat';
|
|
4
|
+
import { player } from '@openora/core/pam/schema/profile';
|
|
5
|
+
import { friendship } from '../schema/index.js';
|
|
6
|
+
// The caller targeted themselves (SendFriendRequestInputSchema.targetUserId === callerId).
|
|
7
|
+
export const SelfFriendRequestError = createDomainError('SelfFriendRequestError', () => 'You cannot send a friend request to yourself');
|
|
8
|
+
// A genuinely nonexistent target only - never thrown for a target that exists but
|
|
9
|
+
// can't be requested (moderated or blocking), so an API consumer can trust that a
|
|
10
|
+
// NOT_FOUND means "no such player" and not have to guess.
|
|
11
|
+
export const FriendRequestTargetNotFoundError = makeNotFoundError('FriendRequestTarget');
|
|
12
|
+
// The target exists but can't receive requests right now - covers BOTH "suspended
|
|
13
|
+
// or closed" and "target has blocked the caller" behind the SAME code/message so
|
|
14
|
+
// callers still cannot distinguish a blocking account from a moderated one (the
|
|
15
|
+
// block itself stays undisclosed); this only stops lying that the account is
|
|
16
|
+
// missing entirely.
|
|
17
|
+
export const FriendRequestUnavailableError = makeConflictError('FRIEND_REQUEST_UNAVAILABLE', 'This player cannot receive friend requests right now');
|
|
18
|
+
// The caller's own chat block is disclosed because it is the caller's state.
|
|
19
|
+
export const BlockedBySelfError = makeConflictError('BLOCKED_BY_SELF', 'You have blocked this player - unblock them first');
|
|
20
|
+
export const AlreadyFriendsError = makeConflictError('ALREADY_FRIENDS', 'You are already friends with this player');
|
|
21
|
+
export const RequestAlreadyPendingError = makeConflictError('REQUEST_ALREADY_PENDING', 'A friend request to this player is already pending');
|
|
22
|
+
export const FriendRequestRefusedError = makeConflictError('FRIEND_REQUEST_REFUSED', 'This friend request was refused');
|
|
23
|
+
// Postgres unique-violation. friendship has exactly one unique index
|
|
24
|
+
// (friendship_pair_key), so any 23505 on an insert into it is unambiguous - no
|
|
25
|
+
// constraint-name check needed. drizzle-orm wraps the driver error in a
|
|
26
|
+
// DrizzleQueryError, so the pg error (and its `.code`) is on `.cause`, not the
|
|
27
|
+
// thrown error itself - checked against a real Postgres unique-index violation
|
|
28
|
+
// in social.service.int.test.ts (a mocked query builder can't catch this).
|
|
29
|
+
function pgErrorCode(e) {
|
|
30
|
+
if (typeof e !== 'object' || e === null) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if ('code' in e && typeof e.code === 'string') {
|
|
34
|
+
return e.code;
|
|
35
|
+
}
|
|
36
|
+
if ('cause' in e) {
|
|
37
|
+
return pgErrorCode(e.cause);
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
function isUniqueConstraintViolation(e) {
|
|
42
|
+
return pgErrorCode(e) === '23505';
|
|
43
|
+
}
|
|
44
|
+
function toFriendshipDto(row) {
|
|
45
|
+
return serializeRow(row, {
|
|
46
|
+
dateFields: ['createdAt', 'acceptedAt', 'refusedAt'],
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// Finds the single row for a pair regardless of who is requester/addressee - the
|
|
50
|
+
// canonical-pair unique index guarantees at most one match.
|
|
51
|
+
function pairCondition(userA, userB) {
|
|
52
|
+
return or(and(eq(friendship.requesterId, userA), eq(friendship.addresseeId, userB)), and(eq(friendship.requesterId, userB), eq(friendship.addresseeId, userA)));
|
|
53
|
+
}
|
|
54
|
+
export class SocialService {
|
|
55
|
+
drizzle;
|
|
56
|
+
events;
|
|
57
|
+
constructor(drizzle, events) {
|
|
58
|
+
this.drizzle = drizzle;
|
|
59
|
+
this.events = events;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Sends a friend request from `callerId` to `targetUserId`. The insert is
|
|
63
|
+
* attempted directly against the canonical-pair unique index (no pre-SELECT,
|
|
64
|
+
* avoiding a TOCTOU race); a unique-violation means a row for this pair already
|
|
65
|
+
* exists, which is resolved by re-reading it - including the mutual/simultaneous
|
|
66
|
+
* case where the existing row was already addressed TO the caller, which
|
|
67
|
+
* auto-accepts instead of conflicting. Events are emitted strictly after the
|
|
68
|
+
* mutating statement/transaction has committed.
|
|
69
|
+
*/
|
|
70
|
+
async sendFriendRequest(callerId, targetUserId) {
|
|
71
|
+
if (targetUserId === callerId) {
|
|
72
|
+
throw new SelfFriendRequestError();
|
|
73
|
+
}
|
|
74
|
+
// Batched: caller + target resolved in one query rather than two round trips.
|
|
75
|
+
const players = await this.drizzle.db
|
|
76
|
+
.select({ userId: player.userId, displayName: player.displayName, status: player.status })
|
|
77
|
+
.from(player)
|
|
78
|
+
.where(inArray(player.userId, [callerId, targetUserId]));
|
|
79
|
+
const targetPlayer = players.find((p) => p.userId === targetUserId);
|
|
80
|
+
const callerPlayer = players.find((p) => p.userId === callerId);
|
|
81
|
+
if (!targetPlayer) {
|
|
82
|
+
throw new FriendRequestTargetNotFoundError(targetUserId);
|
|
83
|
+
}
|
|
84
|
+
if (targetPlayer.status === 'suspended' || targetPlayer.status === 'closed') {
|
|
85
|
+
throw new FriendRequestUnavailableError();
|
|
86
|
+
}
|
|
87
|
+
if (!callerPlayer) {
|
|
88
|
+
// Invariant: an authenticated caller always has a player row. Not a domain
|
|
89
|
+
// error (nothing the caller can act on) - fail loudly instead of masking it.
|
|
90
|
+
throw new Error(`Player profile missing for authenticated caller: ${callerId}`);
|
|
91
|
+
}
|
|
92
|
+
// Both directions checked in one query: at most the two rows for this pair exist.
|
|
93
|
+
const blocks = await this.drizzle.db
|
|
94
|
+
.select({ blockerId: chatUserBlock.blockerId, blockedId: chatUserBlock.blockedId })
|
|
95
|
+
.from(chatUserBlock)
|
|
96
|
+
.where(and(isNull(chatUserBlock.removedAt), pairBlockCondition(callerId, targetUserId)));
|
|
97
|
+
if (blocks.some((b) => b.blockerId === targetUserId && b.blockedId === callerId)) {
|
|
98
|
+
// Target has blocked the caller - never disclosed, same error as "suspended".
|
|
99
|
+
throw new FriendRequestUnavailableError();
|
|
100
|
+
}
|
|
101
|
+
if (blocks.some((b) => b.blockerId === callerId && b.blockedId === targetUserId)) {
|
|
102
|
+
throw new BlockedBySelfError();
|
|
103
|
+
}
|
|
104
|
+
let inserted;
|
|
105
|
+
try {
|
|
106
|
+
const rows = await this.drizzle.db
|
|
107
|
+
.insert(friendship)
|
|
108
|
+
.values({ requesterId: callerId, addresseeId: targetUserId })
|
|
109
|
+
.returning();
|
|
110
|
+
inserted = rows[0];
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (!isUniqueConstraintViolation(error)) {
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (inserted) {
|
|
118
|
+
this.events.emit('social.friend_request.sent', {
|
|
119
|
+
friendshipId: inserted.id,
|
|
120
|
+
requesterId: inserted.requesterId,
|
|
121
|
+
addresseeId: inserted.addresseeId,
|
|
122
|
+
requesterDisplayName: callerPlayer.displayName,
|
|
123
|
+
});
|
|
124
|
+
return toFriendshipDto(inserted);
|
|
125
|
+
}
|
|
126
|
+
const accepted = await this.resolveExistingPair(callerId, targetUserId);
|
|
127
|
+
this.events.emit('social.friend_request.accepted', {
|
|
128
|
+
friendshipId: accepted.id,
|
|
129
|
+
requesterId: accepted.requesterId,
|
|
130
|
+
addresseeId: accepted.addresseeId,
|
|
131
|
+
accepterId: callerId,
|
|
132
|
+
accepterDisplayName: callerPlayer.displayName,
|
|
133
|
+
});
|
|
134
|
+
return toFriendshipDto(accepted);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Re-reads the row a unique-violation on `friendship_pair_key` proved already
|
|
138
|
+
* exists and resolves it: already accepted -> conflict, same-direction duplicate
|
|
139
|
+
* -> conflict, opposite-direction (mutual/simultaneous request) -> auto-accept.
|
|
140
|
+
* `FOR UPDATE` serializes this against a concurrent resolution of the same pair.
|
|
141
|
+
*/
|
|
142
|
+
async resolveExistingPair(callerId, targetUserId) {
|
|
143
|
+
return this.drizzle.db.transaction(async (tx) => {
|
|
144
|
+
const rows = await tx
|
|
145
|
+
.select()
|
|
146
|
+
.from(friendship)
|
|
147
|
+
.where(pairCondition(callerId, targetUserId))
|
|
148
|
+
.for('update')
|
|
149
|
+
.limit(1);
|
|
150
|
+
const existing = rows[0];
|
|
151
|
+
if (!existing) {
|
|
152
|
+
// The row that caused the violation is gone (eg concurrently removed) -
|
|
153
|
+
// safe to surface as "no longer pending", the caller can retry.
|
|
154
|
+
throw new RequestAlreadyPendingError();
|
|
155
|
+
}
|
|
156
|
+
if (existing.refusedAt !== null) {
|
|
157
|
+
throw new FriendRequestRefusedError();
|
|
158
|
+
}
|
|
159
|
+
if (existing.acceptedAt !== null) {
|
|
160
|
+
throw new AlreadyFriendsError();
|
|
161
|
+
}
|
|
162
|
+
if (existing.requesterId === callerId) {
|
|
163
|
+
throw new RequestAlreadyPendingError();
|
|
164
|
+
}
|
|
165
|
+
// Mutual/simultaneous case: existing.requesterId === targetUserId - the
|
|
166
|
+
// pending row was already addressed TO the caller.
|
|
167
|
+
const updatedRows = await tx
|
|
168
|
+
.update(friendship)
|
|
169
|
+
.set({ acceptedAt: new Date(), refusedAt: null })
|
|
170
|
+
.where(eq(friendship.id, existing.id))
|
|
171
|
+
.returning();
|
|
172
|
+
const updated = updatedRows[0];
|
|
173
|
+
if (!updated) {
|
|
174
|
+
throw new RequestAlreadyPendingError();
|
|
175
|
+
}
|
|
176
|
+
return updated;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Batch relationship lookup for a set of target users, from the caller's point
|
|
181
|
+
* of view. Reads are batched (one query per table, `IN (...)` over the target
|
|
182
|
+
* ids) rather than N+1 per target.
|
|
183
|
+
*/
|
|
184
|
+
async getRelationships(callerId, targetUserIds) {
|
|
185
|
+
const uniqueTargetIds = [...new Set(targetUserIds)];
|
|
186
|
+
const players = await this.drizzle.db
|
|
187
|
+
.select({ userId: player.userId, status: player.status })
|
|
188
|
+
.from(player)
|
|
189
|
+
.where(inArray(player.userId, uniqueTargetIds));
|
|
190
|
+
const playerByUserId = new Map(players.map((p) => [p.userId, p]));
|
|
191
|
+
const friendships = await this.drizzle.db
|
|
192
|
+
.select()
|
|
193
|
+
.from(friendship)
|
|
194
|
+
.where(or(and(eq(friendship.requesterId, callerId), inArray(friendship.addresseeId, uniqueTargetIds)), and(inArray(friendship.requesterId, uniqueTargetIds), eq(friendship.addresseeId, callerId))));
|
|
195
|
+
const friendshipByTargetId = new Map();
|
|
196
|
+
for (const row of friendships) {
|
|
197
|
+
const targetId = row.requesterId === callerId ? row.addresseeId : row.requesterId;
|
|
198
|
+
friendshipByTargetId.set(targetId, row);
|
|
199
|
+
}
|
|
200
|
+
const blocks = await this.drizzle.db
|
|
201
|
+
.select({ blockerId: chatUserBlock.blockerId, blockedId: chatUserBlock.blockedId })
|
|
202
|
+
.from(chatUserBlock)
|
|
203
|
+
.where(and(isNull(chatUserBlock.removedAt), or(and(eq(chatUserBlock.blockerId, callerId), inArray(chatUserBlock.blockedId, uniqueTargetIds)), and(inArray(chatUserBlock.blockerId, uniqueTargetIds), eq(chatUserBlock.blockedId, callerId)))));
|
|
204
|
+
const blockedByMe = new Set(blocks.filter((b) => b.blockerId === callerId).map((b) => b.blockedId));
|
|
205
|
+
const blockedByTarget = new Set(blocks.filter((b) => b.blockedId === callerId).map((b) => b.blockerId));
|
|
206
|
+
return targetUserIds.map((userId) => {
|
|
207
|
+
const targetPlayer = playerByUserId.get(userId);
|
|
208
|
+
if (!targetPlayer ||
|
|
209
|
+
targetPlayer.status === 'suspended' ||
|
|
210
|
+
targetPlayer.status === 'closed' ||
|
|
211
|
+
blockedByTarget.has(userId)) {
|
|
212
|
+
return { userId, status: 'unavailable', friendshipId: null, canSendRequest: false };
|
|
213
|
+
}
|
|
214
|
+
if (blockedByMe.has(userId)) {
|
|
215
|
+
return { userId, status: 'blocked_by_me', friendshipId: null, canSendRequest: false };
|
|
216
|
+
}
|
|
217
|
+
const existing = friendshipByTargetId.get(userId);
|
|
218
|
+
if (existing?.refusedAt !== null && existing?.refusedAt !== undefined) {
|
|
219
|
+
return { userId, status: 'refused', friendshipId: existing.id, canSendRequest: false };
|
|
220
|
+
}
|
|
221
|
+
if (existing?.acceptedAt !== null && existing?.acceptedAt !== undefined) {
|
|
222
|
+
return { userId, status: 'friends', friendshipId: existing.id, canSendRequest: false };
|
|
223
|
+
}
|
|
224
|
+
if (existing?.acceptedAt === null &&
|
|
225
|
+
existing?.refusedAt === null &&
|
|
226
|
+
existing?.requesterId === callerId) {
|
|
227
|
+
return {
|
|
228
|
+
userId,
|
|
229
|
+
status: 'pending_outgoing',
|
|
230
|
+
friendshipId: existing.id,
|
|
231
|
+
canSendRequest: false,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
if (existing?.acceptedAt === null &&
|
|
235
|
+
existing?.refusedAt === null &&
|
|
236
|
+
existing?.requesterId === userId) {
|
|
237
|
+
return {
|
|
238
|
+
userId,
|
|
239
|
+
status: 'pending_incoming',
|
|
240
|
+
friendshipId: existing.id,
|
|
241
|
+
canSendRequest: false,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return { userId, status: 'none', friendshipId: null, canSendRequest: true };
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function pairBlockCondition(callerId, targetUserId) {
|
|
249
|
+
return or(and(eq(chatUserBlock.blockerId, targetUserId), eq(chatUserBlock.blockedId, callerId)), and(eq(chatUserBlock.blockerId, callerId), eq(chatUserBlock.blockedId, targetUserId)));
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=social.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"social.service.js","sourceRoot":"","sources":["../../../../src/engagement/social/service/social.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { describe, it, expect, vi, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
3
3
|
import { sql, eq } from 'drizzle-orm';
|
|
4
|
-
import { mock, NO_CLIENT_META, makeEventBus } from '../../testing/mock.js';
|
|
4
|
+
import { makeIdentityReader, mock, NO_CLIENT_META, makeEventBus } from '../../testing/mock.js';
|
|
5
5
|
import { statement, findOneOrThrow, RedisCache, RedisRateLimiter, } from '@openora/core/server';
|
|
6
6
|
import { createTestDb, createTestRedis } from '@openora/core/testing';
|
|
7
7
|
import { migrate as migrateIam } from '@openora/core/iam/migrate';
|
|
@@ -12,6 +12,10 @@ import { IamService, DbAdminPermissionResolver, RoleNotFoundError, InvalidGrantE
|
|
|
12
12
|
let db;
|
|
13
13
|
let redis;
|
|
14
14
|
const makeEmail = () => mock({ send: vi.fn().mockResolvedValue(undefined) });
|
|
15
|
+
const identityReader = makeIdentityReader();
|
|
16
|
+
function makeIamService(drizzle, events, email, sessionCommands, rateLimiter) {
|
|
17
|
+
return new IamService(drizzle, events, email, identityReader, sessionCommands, rateLimiter);
|
|
18
|
+
}
|
|
15
19
|
// Bootstrap super-admin: no DB assignment row + user.role === 'admin' passes the static fallback.
|
|
16
20
|
const ADMIN_CALLER = { userId: randomUUID(), role: 'admin', ...NO_CLIENT_META };
|
|
17
21
|
const SUPPORT_CALLER = { userId: randomUUID(), role: 'support', ...NO_CLIENT_META };
|
|
@@ -55,7 +59,7 @@ beforeEach(async () => {
|
|
|
55
59
|
});
|
|
56
60
|
describe('IamService.listCatalog', () => {
|
|
57
61
|
it('omits the admin module from the assignable catalog', () => {
|
|
58
|
-
const svc =
|
|
62
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), makeEmail());
|
|
59
63
|
const catalog = svc.listCatalog();
|
|
60
64
|
expect(catalog.modules.some((m) => m.resource === 'admin')).toBe(false);
|
|
61
65
|
expect(catalog.modules.some((m) => m.resource === 'player')).toBe(true);
|
|
@@ -143,7 +147,7 @@ describe('IamService.setRolePermissions', () => {
|
|
|
143
147
|
it('rejects a non-super-admin caller and audits the denial', async () => {
|
|
144
148
|
const role = await seedRole();
|
|
145
149
|
const events = makeEventBus();
|
|
146
|
-
const svc =
|
|
150
|
+
const svc = makeIamService(db.drizzle, events, makeEmail());
|
|
147
151
|
await expect(svc.setRolePermissions({
|
|
148
152
|
roleId: role.id,
|
|
149
153
|
grants: [{ resource: 'player', level: 'read' }],
|
|
@@ -157,7 +161,7 @@ describe('IamService.setRolePermissions', () => {
|
|
|
157
161
|
}));
|
|
158
162
|
});
|
|
159
163
|
it('throws RoleNotFoundError when the role does not exist', async () => {
|
|
160
|
-
const svc =
|
|
164
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), makeEmail());
|
|
161
165
|
await expect(svc.setRolePermissions({
|
|
162
166
|
roleId: randomUUID(),
|
|
163
167
|
grants: [{ resource: 'player', level: 'read' }],
|
|
@@ -166,7 +170,7 @@ describe('IamService.setRolePermissions', () => {
|
|
|
166
170
|
});
|
|
167
171
|
it('throws InvalidGrantError for an unknown module', async () => {
|
|
168
172
|
const role = await seedRole();
|
|
169
|
-
const svc =
|
|
173
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), makeEmail());
|
|
170
174
|
await expect(svc.setRolePermissions({
|
|
171
175
|
roleId: role.id,
|
|
172
176
|
grants: [{ resource: 'nonexistent', level: 'read' }],
|
|
@@ -175,7 +179,7 @@ describe('IamService.setRolePermissions', () => {
|
|
|
175
179
|
});
|
|
176
180
|
it('rejects a grant targeting the admin module (super-admin-only, A6)', async () => {
|
|
177
181
|
const role = await seedRole();
|
|
178
|
-
const svc =
|
|
182
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), makeEmail());
|
|
179
183
|
await expect(svc.setRolePermissions({
|
|
180
184
|
roleId: role.id,
|
|
181
185
|
grants: [{ resource: 'admin', level: 'read_write' }],
|
|
@@ -184,7 +188,7 @@ describe('IamService.setRolePermissions', () => {
|
|
|
184
188
|
});
|
|
185
189
|
it('blocks editing a super-admin role (always full)', async () => {
|
|
186
190
|
const role = await seedRole({ isSuperAdmin: true, isSystem: true });
|
|
187
|
-
const svc =
|
|
191
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), makeEmail());
|
|
188
192
|
await expect(svc.setRolePermissions({
|
|
189
193
|
roleId: role.id,
|
|
190
194
|
grants: [{ resource: 'player', level: 'read' }],
|
|
@@ -193,7 +197,7 @@ describe('IamService.setRolePermissions', () => {
|
|
|
193
197
|
});
|
|
194
198
|
it('a super caller may grant any level and the rows are persisted', async () => {
|
|
195
199
|
const role = await seedRole();
|
|
196
|
-
const svc =
|
|
200
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), makeEmail());
|
|
197
201
|
const result = await svc.setRolePermissions({
|
|
198
202
|
roleId: role.id,
|
|
199
203
|
grants: [{ resource: 'withdrawal', level: 'read_write' }],
|
|
@@ -210,7 +214,7 @@ describe('IamService.setRolePermissions', () => {
|
|
|
210
214
|
it('emits iam.role.permissions.changed with actorId', async () => {
|
|
211
215
|
const role = await seedRole();
|
|
212
216
|
const events = makeEventBus();
|
|
213
|
-
const svc =
|
|
217
|
+
const svc = makeIamService(db.drizzle, events, makeEmail());
|
|
214
218
|
await svc.setRolePermissions({
|
|
215
219
|
roleId: role.id,
|
|
216
220
|
grants: [{ resource: 'player', level: 'read' }],
|
|
@@ -222,13 +226,13 @@ describe('IamService.setRolePermissions', () => {
|
|
|
222
226
|
describe('IamService.updateRole', () => {
|
|
223
227
|
it('blocks renaming the super-admin role', async () => {
|
|
224
228
|
const role = await seedRole({ isSuperAdmin: true, isSystem: true });
|
|
225
|
-
const svc =
|
|
229
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), makeEmail());
|
|
226
230
|
await expect(svc.updateRole({ roleId: role.id, name: 'Renamed', caller: ADMIN_CALLER })).rejects.toBeInstanceOf(ProtectedRoleError);
|
|
227
231
|
});
|
|
228
232
|
it('allows renaming a predefined (isSystem, non-super) role', async () => {
|
|
229
233
|
const role = await seedRole({ isSystem: true, isSuperAdmin: false });
|
|
230
234
|
const events = makeEventBus();
|
|
231
|
-
const svc =
|
|
235
|
+
const svc = makeIamService(db.drizzle, events, makeEmail());
|
|
232
236
|
const result = await svc.updateRole({ roleId: role.id, name: 'Renamed', caller: ADMIN_CALLER });
|
|
233
237
|
expect(result.name).toBe('Renamed');
|
|
234
238
|
expect(events.emit).toHaveBeenCalledWith('iam.role.updated', expect.objectContaining({ roleId: role.id, actorId: ADMIN_CALLER.userId }));
|
|
@@ -239,18 +243,18 @@ describe('IamService.updateRole', () => {
|
|
|
239
243
|
describe('IamService.deleteRole', () => {
|
|
240
244
|
it('blocks deleting a system role', async () => {
|
|
241
245
|
const role = await seedRole({ isSystem: true });
|
|
242
|
-
const svc =
|
|
246
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), makeEmail());
|
|
243
247
|
await expect(svc.deleteRole({ roleId: role.id, caller: ADMIN_CALLER })).rejects.toBeInstanceOf(ProtectedRoleError);
|
|
244
248
|
});
|
|
245
249
|
it('blocks deleting a super-admin role', async () => {
|
|
246
250
|
const role = await seedRole({ isSuperAdmin: true });
|
|
247
|
-
const svc =
|
|
251
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), makeEmail());
|
|
248
252
|
await expect(svc.deleteRole({ roleId: role.id, caller: ADMIN_CALLER })).rejects.toBeInstanceOf(ProtectedRoleError);
|
|
249
253
|
});
|
|
250
254
|
it('deletes a custom role with no assignments and emits only iam.role.deleted', async () => {
|
|
251
255
|
const role = await seedRole();
|
|
252
256
|
const events = makeEventBus();
|
|
253
|
-
const svc =
|
|
257
|
+
const svc = makeIamService(db.drizzle, events, makeEmail());
|
|
254
258
|
const result = await svc.deleteRole({ roleId: role.id, caller: ADMIN_CALLER });
|
|
255
259
|
expect(result).toEqual({ success: true });
|
|
256
260
|
expect(events.emit).toHaveBeenCalledWith('iam.role.deleted', expect.objectContaining({ roleId: role.id, actorId: ADMIN_CALLER.userId }));
|
|
@@ -264,7 +268,7 @@ describe('IamService.deleteRole', () => {
|
|
|
264
268
|
await seedAssignment(userA, role.id);
|
|
265
269
|
await seedAssignment(userB, role.id);
|
|
266
270
|
const events = makeEventBus();
|
|
267
|
-
const svc =
|
|
271
|
+
const svc = makeIamService(db.drizzle, events, makeEmail());
|
|
268
272
|
const result = await svc.deleteRole({ roleId: role.id, caller: ADMIN_CALLER });
|
|
269
273
|
expect(result).toEqual({ success: true });
|
|
270
274
|
expect(events.emit).toHaveBeenCalledWith('iam.role.revoked', {
|
|
@@ -292,7 +296,7 @@ describe('IamService.assignRole', () => {
|
|
|
292
296
|
const target = await seedUser('admin');
|
|
293
297
|
await seedAssignment(target.id, role.id);
|
|
294
298
|
const events = makeEventBus();
|
|
295
|
-
const svc =
|
|
299
|
+
const svc = makeIamService(db.drizzle, events, makeEmail());
|
|
296
300
|
const result = await svc.assignRole({
|
|
297
301
|
userId: target.id,
|
|
298
302
|
roleId: role.id,
|
|
@@ -307,18 +311,18 @@ describe('IamService.assignRole', () => {
|
|
|
307
311
|
it('rejects assigning a role to a non-admin (player) account', async () => {
|
|
308
312
|
const role = await seedRole();
|
|
309
313
|
const player = await seedUser('player');
|
|
310
|
-
const svc =
|
|
314
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), makeEmail());
|
|
311
315
|
await expect(svc.assignRole({ userId: player.id, roleId: role.id, caller: ADMIN_CALLER })).rejects.toThrow(NotAnAdminUserError);
|
|
312
316
|
});
|
|
313
317
|
it('rejects assigning a role to an unknown user', async () => {
|
|
314
318
|
const role = await seedRole();
|
|
315
|
-
const svc =
|
|
319
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), makeEmail());
|
|
316
320
|
await expect(svc.assignRole({ userId: randomUUID(), roleId: role.id, caller: ADMIN_CALLER })).rejects.toThrow(AdminUserNotFoundError);
|
|
317
321
|
});
|
|
318
322
|
it('the unique index keeps two concurrent assigns from double-inserting', async () => {
|
|
319
323
|
const role = await seedRole();
|
|
320
324
|
const target = await seedUser('admin');
|
|
321
|
-
const svc =
|
|
325
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), makeEmail());
|
|
322
326
|
await Promise.allSettled([
|
|
323
327
|
svc.assignRole({ userId: target.id, roleId: role.id, caller: ADMIN_CALLER }),
|
|
324
328
|
svc.assignRole({ userId: target.id, roleId: role.id, caller: ADMIN_CALLER }),
|
|
@@ -335,7 +339,7 @@ describe('IamService.unassignRole', () => {
|
|
|
335
339
|
const superRole = await seedRole({ isSuperAdmin: true, isSystem: true });
|
|
336
340
|
const target = randomUUID();
|
|
337
341
|
await seedAssignment(target, superRole.id);
|
|
338
|
-
const svc =
|
|
342
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), makeEmail());
|
|
339
343
|
await expect(svc.unassignRole({ userId: target, roleId: superRole.id, caller: ADMIN_CALLER })).rejects.toBeInstanceOf(LastSuperAdminError);
|
|
340
344
|
// The holder survives the rejected unassign.
|
|
341
345
|
expect(await db.drizzle.db.select().from(adminRoleAssignment)).toHaveLength(1);
|
|
@@ -343,7 +347,7 @@ describe('IamService.unassignRole', () => {
|
|
|
343
347
|
it('returns success but emits no revoked event when nothing was deleted', async () => {
|
|
344
348
|
const role = await seedRole();
|
|
345
349
|
const events = makeEventBus();
|
|
346
|
-
const svc =
|
|
350
|
+
const svc = makeIamService(db.drizzle, events, makeEmail());
|
|
347
351
|
const result = await svc.unassignRole({
|
|
348
352
|
userId: randomUUID(),
|
|
349
353
|
roleId: role.id,
|
|
@@ -357,7 +361,7 @@ describe('IamService.unassignRole', () => {
|
|
|
357
361
|
const target = randomUUID();
|
|
358
362
|
await seedAssignment(target, role.id);
|
|
359
363
|
const events = makeEventBus();
|
|
360
|
-
const svc =
|
|
364
|
+
const svc = makeIamService(db.drizzle, events, makeEmail());
|
|
361
365
|
await svc.unassignRole({ userId: target, roleId: role.id, caller: ADMIN_CALLER });
|
|
362
366
|
expect(events.emit).toHaveBeenCalledWith('iam.role.revoked', expect.objectContaining({ roleId: role.id, userId: target, actorId: ADMIN_CALLER.userId }));
|
|
363
367
|
expect(await db.drizzle.db.select().from(adminRoleAssignment)).toHaveLength(0);
|
|
@@ -368,7 +372,7 @@ describe('IamService.unassignRole', () => {
|
|
|
368
372
|
const userB = randomUUID();
|
|
369
373
|
await seedAssignment(userA, superRole.id);
|
|
370
374
|
await seedAssignment(userB, superRole.id);
|
|
371
|
-
const svc =
|
|
375
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), makeEmail());
|
|
372
376
|
const results = await Promise.allSettled([
|
|
373
377
|
svc.unassignRole({ userId: userA, roleId: superRole.id, caller: ADMIN_CALLER }),
|
|
374
378
|
svc.unassignRole({ userId: userB, roleId: superRole.id, caller: ADMIN_CALLER }),
|
|
@@ -387,7 +391,7 @@ describe('IamService.previewEffectivePermissions', () => {
|
|
|
387
391
|
await seedPermission(roleA.id, 'player', 'read');
|
|
388
392
|
await seedPermission(roleA.id, 'withdrawal', 'read');
|
|
389
393
|
await seedPermission(roleB.id, 'player', 'read_write');
|
|
390
|
-
const svc =
|
|
394
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), makeEmail());
|
|
391
395
|
const result = await svc.previewEffectivePermissions({ roleIds: [roleA.id, roleB.id] });
|
|
392
396
|
expect(result.permissions).toContainEqual({ resource: 'player', level: 'read_write' });
|
|
393
397
|
expect(result.permissions).toContainEqual({ resource: 'withdrawal', level: 'read' });
|
|
@@ -395,14 +399,14 @@ describe('IamService.previewEffectivePermissions', () => {
|
|
|
395
399
|
});
|
|
396
400
|
it('super-admin role in the set yields all modules read_write', async () => {
|
|
397
401
|
const superRole = await seedRole({ isSuperAdmin: true });
|
|
398
|
-
const svc =
|
|
402
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), makeEmail());
|
|
399
403
|
const result = await svc.previewEffectivePermissions({ roleIds: [superRole.id] });
|
|
400
404
|
expect(result.permissions).toHaveLength(Object.keys(statement).length);
|
|
401
405
|
expect(result.permissions.every((p) => p.level === 'read_write')).toBe(true);
|
|
402
406
|
});
|
|
403
407
|
it('falls back to static role permissions when the user has no dynamic role assignments', async () => {
|
|
404
408
|
const u = await seedUser('support');
|
|
405
|
-
const svc =
|
|
409
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), makeEmail());
|
|
406
410
|
const result = await svc.previewEffectivePermissions({ userId: u.id });
|
|
407
411
|
expect(result.permissions).not.toHaveLength(0);
|
|
408
412
|
expect(result.permissions).toContainEqual({ resource: 'player', level: 'read' });
|
|
@@ -424,7 +428,7 @@ describe('IamService.acceptInvitation', () => {
|
|
|
424
428
|
it('accepts once and emits exactly one event; a replay conflicts and emits nothing more', async () => {
|
|
425
429
|
const token = await seedInvitation();
|
|
426
430
|
const events = makeEventBus();
|
|
427
|
-
const svc =
|
|
431
|
+
const svc = makeIamService(db.drizzle, events, makeEmail());
|
|
428
432
|
const first = await svc.acceptInvitation(token);
|
|
429
433
|
expect(first).toEqual({ success: true, email: 'who@admin.com' });
|
|
430
434
|
await expect(svc.acceptInvitation(token)).rejects.toBeInstanceOf(InvitationConflictError);
|
|
@@ -433,7 +437,7 @@ describe('IamService.acceptInvitation', () => {
|
|
|
433
437
|
it('two concurrent accepts: exactly one succeeds, one event (atomic conditional UPDATE)', async () => {
|
|
434
438
|
const token = await seedInvitation();
|
|
435
439
|
const events = makeEventBus();
|
|
436
|
-
const svc =
|
|
440
|
+
const svc = makeIamService(db.drizzle, events, makeEmail());
|
|
437
441
|
const results = await Promise.allSettled([
|
|
438
442
|
svc.acceptInvitation(token),
|
|
439
443
|
svc.acceptInvitation(token),
|
|
@@ -450,7 +454,7 @@ describe('IamService.inviteAdmin', () => {
|
|
|
450
454
|
it('creates a pending invitation and calls SEND_EMAIL when caller is super-admin', async () => {
|
|
451
455
|
const role = await seedRole();
|
|
452
456
|
const email = makeEmail();
|
|
453
|
-
const svc =
|
|
457
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), email);
|
|
454
458
|
const result = await svc.inviteAdmin({
|
|
455
459
|
email: 'new@admin.com',
|
|
456
460
|
roleId: role.id,
|
|
@@ -467,7 +471,7 @@ describe('IamService.inviteAdmin', () => {
|
|
|
467
471
|
it('rejects a non-super-admin caller', async () => {
|
|
468
472
|
const role = await seedRole();
|
|
469
473
|
const email = makeEmail();
|
|
470
|
-
const svc =
|
|
474
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), email);
|
|
471
475
|
await expect(svc.inviteAdmin({ email: 'new@admin.com', roleId: role.id, caller: SUPPORT_CALLER })).rejects.toBeInstanceOf(NotSuperAdminError);
|
|
472
476
|
expect(email.send).not.toHaveBeenCalled();
|
|
473
477
|
});
|
|
@@ -475,7 +479,7 @@ describe('IamService.inviteAdmin', () => {
|
|
|
475
479
|
describe('IamService paginated lists', () => {
|
|
476
480
|
it('listRoles returns { items, total, page, limit }', async () => {
|
|
477
481
|
const role = await seedRole();
|
|
478
|
-
const svc =
|
|
482
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), makeEmail());
|
|
479
483
|
const result = await svc.listRoles({ page: 1, limit: 10 });
|
|
480
484
|
expect(result.total).toBe(1);
|
|
481
485
|
expect(result.page).toBe(1);
|
|
@@ -492,7 +496,7 @@ describe('IamService paginated lists', () => {
|
|
|
492
496
|
status: 'pending',
|
|
493
497
|
expiresAt: new Date(Date.now() + 60_000),
|
|
494
498
|
});
|
|
495
|
-
const svc =
|
|
499
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), makeEmail());
|
|
496
500
|
const result = await svc.listInvitations({ page: 1, limit: 20 });
|
|
497
501
|
expect(result.total).toBe(1);
|
|
498
502
|
expect(result.items).toHaveLength(1);
|
|
@@ -501,7 +505,7 @@ describe('IamService paginated lists', () => {
|
|
|
501
505
|
it('listAssignments returns the paginated wrapper with joined role fields', async () => {
|
|
502
506
|
const role = await seedRole({ name: 'Ops' });
|
|
503
507
|
await seedAssignment(randomUUID(), role.id);
|
|
504
|
-
const svc =
|
|
508
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), makeEmail());
|
|
505
509
|
const result = await svc.listAssignments({ page: 1, limit: 20 });
|
|
506
510
|
expect(result.total).toBe(1);
|
|
507
511
|
expect(result.items).toHaveLength(1);
|
|
@@ -511,7 +515,7 @@ describe('IamService paginated lists', () => {
|
|
|
511
515
|
describe('IamService.reportAccessDenied', () => {
|
|
512
516
|
it('records and audits a genuine denial (caller lacks the resource/level)', async () => {
|
|
513
517
|
const events = makeEventBus();
|
|
514
|
-
const svc =
|
|
518
|
+
const svc = makeIamService(db.drizzle, events, makeEmail());
|
|
515
519
|
const result = await svc.reportAccessDenied({
|
|
516
520
|
resource: 'withdrawal',
|
|
517
521
|
level: 'read',
|
|
@@ -527,7 +531,7 @@ describe('IamService.reportAccessDenied', () => {
|
|
|
527
531
|
});
|
|
528
532
|
it('does not record or emit when the caller genuinely holds the permission', async () => {
|
|
529
533
|
const events = makeEventBus();
|
|
530
|
-
const svc =
|
|
534
|
+
const svc = makeIamService(db.drizzle, events, makeEmail());
|
|
531
535
|
const result = await svc.reportAccessDenied({
|
|
532
536
|
resource: 'player',
|
|
533
537
|
level: 'read_write',
|
|
@@ -539,7 +543,7 @@ describe('IamService.reportAccessDenied', () => {
|
|
|
539
543
|
it('with a rate limiter wired, a repeat report within the window is throttled and not recorded again', async () => {
|
|
540
544
|
const events = makeEventBus();
|
|
541
545
|
const limiter = new RedisRateLimiter(redis.client);
|
|
542
|
-
const svc =
|
|
546
|
+
const svc = makeIamService(db.drizzle, events, makeEmail(), undefined, limiter);
|
|
543
547
|
const first = await svc.reportAccessDenied({
|
|
544
548
|
resource: 'withdrawal',
|
|
545
549
|
level: 'read',
|
|
@@ -558,7 +562,7 @@ describe('IamService.reportAccessDenied', () => {
|
|
|
558
562
|
});
|
|
559
563
|
describe('IamService.forceLogout', () => {
|
|
560
564
|
it('rejects a non-super-admin caller', async () => {
|
|
561
|
-
const svc =
|
|
565
|
+
const svc = makeIamService(db.drizzle, makeEventBus(), makeEmail());
|
|
562
566
|
await expect(svc.forceLogout({ userId: randomUUID(), caller: SUPPORT_CALLER })).rejects.toBeInstanceOf(NotSuperAdminError);
|
|
563
567
|
});
|
|
564
568
|
it('allows a super-admin caller to delete sessions for a user via SessionCommands', async () => {
|
|
@@ -566,7 +570,7 @@ describe('IamService.forceLogout', () => {
|
|
|
566
570
|
const sessionCommands = mock({
|
|
567
571
|
revokeAll: vi.fn().mockResolvedValue({ success: true }),
|
|
568
572
|
});
|
|
569
|
-
const svc =
|
|
573
|
+
const svc = makeIamService(db.drizzle, events, makeEmail(), sessionCommands);
|
|
570
574
|
const targetUserId = randomUUID();
|
|
571
575
|
const result = await svc.forceLogout({ userId: targetUserId, caller: ADMIN_CALLER });
|
|
572
576
|
expect(result).toEqual({ success: true });
|